Here is a first attempt at creating a clock with Vega-lite. Feel free to come back with some ideas on how to improve this. I could only get it to refresh using vega-embed. The hands are either shown or hidden based on simple transform filters. I can’t seem to use SVG Paths for the hands. Any ideas?
<!doctype html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>
<body>
<div id="vis"></div>
<script type="text/javascript">
</script>
<script id="jsCode" type="text/javascript">
function generateSpec() {
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Vega-lite clock",
"width": 500,
"height": 500,
"data": {
"values": [
{"hour": "12", "min": "1", "minute": "01", "value": 1},
{"hour": "12", "min": "2", "minute": "02", "value": 1},
{"hour": "12", "min": "3", "minute": "03", "value": 1},
{"hour": "12", "min": "4", "minute": "04", "value": 1},
{"hour": "1", "min": "5", "minute": "05", "value": 1},
{"hour": "1", "min": "6", "minute": "06", "value": 1},
{"hour": "1", "min": "7", "minute": "07", "value": 1},
{"hour": "1", "min": "8", "minute": "08", "value": 1},
{"hour": "1", "min": "9", "minute": "09", "value": 1},
{"hour": "2", "min": "10", "minute": "10", "value": 1},
{"hour": "2", "min": "11", "minute": "11", "value": 1},
{"hour": "2", "min": "12", "minute": "12", "value": 1},
{"hour": "2", "min": "13", "minute": "13", "value": 1},
{"hour": "2", "min": "14", "minute": "14", "value": 1},
{"hour": "3", "min": "15", "minute": "15", "value": 1},
{"hour": "3", "min": "16", "minute": "16", "value": 1},
{"hour": "3", "min": "17", "minute": "17", "value": 1},
{"hour": "3", "min": "18", "minute": "18", "value": 1},
{"hour": "3", "min": "19", "minute": "19", "value": 1},
{"hour": "4", "min": "20", "minute": "20", "value": 1},
{"hour": "4", "min": "21", "minute": "21", "value": 1},
{"hour": "4", "min": "22", "minute": "22", "value": 1},
{"hour": "4", "min": "23", "minute": "23", "value": 1},
{"hour": "4", "min": "24", "minute": "24", "value": 1},
{"hour": "5", "min": "25", "minute": "25", "value": 1},
{"hour": "5", "min": "26", "minute": "26", "value": 1},
{"hour": "5", "min": "27", "minute": "27", "value": 1},
{"hour": "5", "min": "28", "minute": "28", "value": 1},
{"hour": "5", "min": "29", "minute": "29", "value": 1},
{"hour": "6", "min": "30", "minute": "30", "value": 1},
{"hour": "6", "min": "31", "minute": "31", "value": 1},
{"hour": "6", "min": "32", "minute": "32", "value": 1},
{"hour": "6", "min": "33", "minute": "33", "value": 1},
{"hour": "6", "min": "34", "minute": "34", "value": 1},
{"hour": "7", "min": "35", "minute": "35", "value": 1},
{"hour": "7", "min": "36", "minute": "36", "value": 1},
{"hour": "7", "min": "37", "minute": "37", "value": 1},
{"hour": "7", "min": "38", "minute": "38", "value": 1},
{"hour": "7", "min": "39", "minute": "39", "value": 1},
{"hour": "8", "min": "40", "minute": "40", "value": 1},
{"hour": "8", "min": "41", "minute": "41", "value": 1},
{"hour": "8", "min": "42", "minute": "42", "value": 1},
{"hour": "8", "min": "43", "minute": "43", "value": 1},
{"hour": "8", "min": "44", "minute": "44", "value": 1},
{"hour": "9", "min": "45", "minute": "45", "value": 1},
{"hour": "9", "min": "46", "minute": "46", "value": 1},
{"hour": "9", "min": "47", "minute": "47", "value": 1},
{"hour": "9", "min": "48", "minute": "48", "value": 1},
{"hour": "9", "min": "49", "minute": "49", "value": 1},
{"hour": "10", "min": "50", "minute": "50", "value": 1},
{"hour": "10", "min": "51", "minute": "51", "value": 1},
{"hour": "10", "min": "52", "minute": "52", "value": 1},
{"hour": "10", "min": "53", "minute": "53", "value": 1},
{"hour": "10", "min": "54", "minute": "54", "value": 1},
{"hour": "11", "min": "55", "minute": "55", "value": 1},
{"hour": "11", "min": "56", "minute": "56", "value": 1},
{"hour": "11", "min": "57", "minute": "57", "value": 1},
{"hour": "11", "min": "58", "minute": "58", "value": 1},
{"hour": "11", "min": "59", "minute": "59", "value": 1},
{"hour": "12", "min": "0", "minute": "60", "value": 1}
]
},
"transform": [
{"joinaggregate": [{"op": "sum", "field": "value", "as": "total"}]},
{
"sort": [{"field": "minute"}],
"window": [{"op": "sum", "field": "value", "as": "Cumulat"}],
"frame": [null, 0]
},
{
"calculate": "(360* ((datum.Cumulat) / datum.total))-90",
"as": "percentage"
}
],
"layer": [
{
"mark": {"type": "arc", "outerRadius": 365, "innerRadius": 275},
"encoding": {"color": {"value": "aliceblue"}}
},
{
"mark": {"type": "arc", "outerRadius": 278, "innerRadius": 275},
"encoding": {"color": {"value": "silver"}}
},
{
"mark": {
"type": "text",
"radius": 287,
"fontSize": 13,
"angle": {"expr": "datum.percentage"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"value": "—"},
"color": {"value": "silver"}
}
},
{
"transform": [
{
"filter": {
"field": "minute",
"oneOf": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
}
}
],
"mark": {
"type": "text",
"radius": 270,
"fontSize": 25,
"angle": {"expr": "datum.percentage"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"value": "⏴"},
"color": {"value": "black"}
}
},
{
"mark": {"type": "text", "radius": 305},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"field": "min", "type": "nominal"},
"color": {"value": "black"}
}
},
{
"transform": [
{
"filter": {
"field": "minute",
"oneOf": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
}
}
],
"mark": {
"type": "text",
"radius": 330,
"fontSize": 15,
"fontWeight": 800
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"field": "hour"},
"color": {"value": "black"}
}
},
{
"transform": [
{
"filter": {
"field": "minute",
"oneOf": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
}
}
],
"mark": {
"type": "text",
"radius": 352,
"fontSize": 12,
"fontWeight": 600,
"text": {"expr": "parseFloat(datum.hour) + 12"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"color": {"value": "silver"}
}
},
{
"transform": [{"filter": "datum.minute === minutes(now())"}],
"mark": {
"type": "text",
"radius": 120,
"fontWeight": 800,
"fontSize": 30,
"angle": {"expr": "datum.percentage"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"value": "————————"},
"color": {"value": "red"}
}
},
{
"transform": [
{
"filter": "datum.hour == hours(now()) || datum.hour == hours(now())-12"
},
{
"filter": "((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 1 || hours(now()) == 13) && datum.minute == '5') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 2 || hours(now()) == 14) && datum.minute == '10') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 3 || hours(now()) == 15) && datum.minute == '15') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 4 || hours(now()) == 16) && datum.minute == '20') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 5 || hours(now()) == 17) && datum.minute == '25') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 6 || hours(now()) == 18) && datum.minute == '30') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 7 || hours(now()) == 19) && datum.minute == '35') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 8 || hours(now()) == 20) && datum.minute == '40') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 9 || hours(now()) == 21) && datum.minute == '45') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 10 || hours(now()) == 22) && datum.minute == '50') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 11 || hours(now()) == 23) && datum.minute == '55') ||((minutes(now()) >= 0 && minutes(now()) <= 12) && (hours(now()) == 12 || hours(now()) == 0 ) && datum.minute == '60') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 1 || hours(now()) == 13) && datum.minute == '6') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 2 || hours(now()) == 14) && datum.minute == '11') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 3 || hours(now()) == 15) && datum.minute == '16') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 4 || hours(now()) == 16) && datum.minute == '21') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 5 || hours(now()) == 17) && datum.minute == '26') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 6 || hours(now()) == 18) && datum.minute == '31') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 7 || hours(now()) == 19) && datum.minute == '36') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 8 || hours(now()) == 20) && datum.minute == '41') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 9 || hours(now()) == 21) && datum.minute == '46') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 10 || hours(now()) == 22) && datum.minute == '51') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 11 || hours(now()) == 23) && datum.minute == '56') ||((minutes(now()) >= 13 && minutes(now()) <= 24) && (hours(now()) == 12 || hours(now()) == 0 ) && datum.minute == '1') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 1 || hours(now()) == 13) && datum.minute == '7') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 2 || hours(now()) == 14) && datum.minute == '12') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 3 || hours(now()) == 15) && datum.minute == '17') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 4 || hours(now()) == 16) && datum.minute == '22') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 5 || hours(now()) == 17) && datum.minute == '27') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 6 || hours(now()) == 18) && datum.minute == '32') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 7 || hours(now()) == 19) && datum.minute == '37') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 8 || hours(now()) == 20) && datum.minute == '42') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 9 || hours(now()) == 21) && datum.minute == '47') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 10 || hours(now()) == 22) && datum.minute == '52') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 11 || hours(now()) == 23) && datum.minute == '57') ||((minutes(now()) >= 25 && minutes(now()) <= 36) && (hours(now()) == 12 || hours(now()) == 0 ) && datum.minute == '2') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 1 || hours(now()) == 13) && datum.minute == '8') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 2 || hours(now()) == 14) && datum.minute == '13') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 3 || hours(now()) == 15) && datum.minute == '18') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 4 || hours(now()) == 16) && datum.minute == '23') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 5 || hours(now()) == 17) && datum.minute == '28') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 6 || hours(now()) == 18) && datum.minute == '33') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 7 || hours(now()) == 19) && datum.minute == '38') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 8 || hours(now()) == 20) && datum.minute == '43') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 9 || hours(now()) == 21) && datum.minute == '48') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 10 || hours(now()) == 22) && datum.minute == '53') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 11 || hours(now()) == 23) && datum.minute == '58') ||((minutes(now()) >= 37 && minutes(now()) <= 48) && (hours(now()) == 12 || hours(now()) == 0 ) && datum.minute == '3') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 1 || hours(now()) == 13) && datum.minute == '9') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 2 || hours(now()) == 14) && datum.minute == '14') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 3 || hours(now()) == 15) && datum.minute == '19') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 4 || hours(now()) == 16) && datum.minute == '24') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 5 || hours(now()) == 17) && datum.minute == '29') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 6 || hours(now()) == 18) && datum.minute == '34') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 7 || hours(now()) == 19) && datum.minute == '39') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 8 || hours(now()) == 20) && datum.minute == '44') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 9 || hours(now()) == 21) && datum.minute == '49') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 10 || hours(now()) == 22) && datum.minute == '54') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 11 || hours(now()) == 23) && datum.minute == '59') ||((minutes(now()) >= 49 && minutes(now()) <= 60) && (hours(now()) == 12 || hours(now()) == 0 ) && datum.minute == '4')"
}
],
"mark": {
"type": "text",
"radius": 80,
"fontWeight": 800,
"fontSize": 60,
"angle": {"expr": "datum.percentage"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"value": "———"},
"color": {"value": "navy"}
}
},
{
"transform": [
{
"filter": "(seconds(now()) == 0 && datum.minute == '60') ||(seconds(now()) == 1 && datum.minute == '1') ||(seconds(now()) == 2 && datum.minute == '2') ||(seconds(now()) == 3 && datum.minute == '3') ||(seconds(now()) == 4 && datum.minute == '4') ||(seconds(now()) == 5 && datum.minute == '5') ||(seconds(now()) == 6 && datum.minute == '6') ||(seconds(now()) == 7 && datum.minute == '7') ||(seconds(now()) == 8 && datum.minute == '8') ||(seconds(now()) == 9 && datum.minute == '9') ||(seconds(now()) == 10 && datum.minute == '10') ||(seconds(now()) == 11 && datum.minute == '11') ||(seconds(now()) == 12 && datum.minute == '12') ||(seconds(now()) == 13 && datum.minute == '13') ||(seconds(now()) == 14 && datum.minute == '14') ||(seconds(now()) == 15 && datum.minute == '15') ||(seconds(now()) == 16 && datum.minute == '16') ||(seconds(now()) == 17 && datum.minute == '17') ||(seconds(now()) == 18 && datum.minute == '18') ||(seconds(now()) == 19 && datum.minute == '19') ||(seconds(now()) == 20 && datum.minute == '20') ||(seconds(now()) == 21 && datum.minute == '21') ||(seconds(now()) == 22 && datum.minute == '22') ||(seconds(now()) == 23 && datum.minute == '23') ||(seconds(now()) == 24 && datum.minute == '24') ||(seconds(now()) == 25 && datum.minute == '25') ||(seconds(now()) == 26 && datum.minute == '26') ||(seconds(now()) == 27 && datum.minute == '27') ||(seconds(now()) == 28 && datum.minute == '28') ||(seconds(now()) == 29 && datum.minute == '29') ||(seconds(now()) == 30 && datum.minute == '30') ||(seconds(now()) == 31 && datum.minute == '31') ||(seconds(now()) == 32 && datum.minute == '32') ||(seconds(now()) == 33 && datum.minute == '33') ||(seconds(now()) == 34 && datum.minute == '34') ||(seconds(now()) == 35 && datum.minute == '35') ||(seconds(now()) == 36 && datum.minute == '36') ||(seconds(now()) == 37 && datum.minute == '37') ||(seconds(now()) == 38 && datum.minute == '38') ||(seconds(now()) == 39 && datum.minute == '39') ||(seconds(now()) == 40 && datum.minute == '40') ||(seconds(now()) == 41 && datum.minute == '41') ||(seconds(now()) == 42 && datum.minute == '42') ||(seconds(now()) == 43 && datum.minute == '43') ||(seconds(now()) == 44 && datum.minute == '44') ||(seconds(now()) == 45 && datum.minute == '45') ||(seconds(now()) == 46 && datum.minute == '46') ||(seconds(now()) == 47 && datum.minute == '47') ||(seconds(now()) == 48 && datum.minute == '48') ||(seconds(now()) == 49 && datum.minute == '49') ||(seconds(now()) == 50 && datum.minute == '50') ||(seconds(now()) == 51 && datum.minute == '51') ||(seconds(now()) == 52 && datum.minute == '52') ||(seconds(now()) == 53 && datum.minute == '53') ||(seconds(now()) == 54 && datum.minute == '54') ||(seconds(now()) == 55 && datum.minute == '55') ||(seconds(now()) == 56 && datum.minute == '56') ||(seconds(now()) == 57 && datum.minute == '57') ||(seconds(now()) == 58 && datum.minute == '58') ||(seconds(now()) == 59 && datum.minute == '59') ||(seconds(now()) == 60 && datum.minute == '60')"
}
],
"mark": {
"type": "text",
"radius": 135,
"fontWeight": 800,
"angle": {"expr": "datum.percentage"}
},
"encoding": {
"theta": {"field": "Cumulat", "type": "quantitative"},
"text": {"value": "———————————————————————"},
"color": {"value": "black"}
}
},
{
"transform": [
{
"filter": {
"field": "minute",
"oneOf": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 0]
}
}
],
"mark": {"type": "circle", "size": 500},
"encoding": {"color": {"value": "silver"}}
},
]
};
return spec;
}
let view;
vegaEmbed('#vis', generateSpec())
.then((result) => {
view = result.view;
})
.catch(console.error);
setInterval(() => {
const newSpec = generateSpec();
vegaEmbed('#vis', newSpec, {actions: false}).then((result) => {
view = result.view;
}).catch(console.error);
}, 1000);
</script>
</body>
</html>
2
Answers
Here is a much more simple approach and also uses svg hands.
Apple iOS style with day date.
Adam
Here is a way to get a timer in Vega-Lite so you don’t need vega-embed. You will get an underline in the editor but you can ignore it.
This updates every second:
Full code of working clock in VL.