Watch Example

A watch face by @domoritz, inspired by Braun’s design and @mathiastiberghien’s clock example.

Vega JSON Specification <>

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "A watch face clock visualization showing the current time.",
  "width": 400,
  "height": 400,
  "signals": [
    {"name": "centerX", "init": "width/2"},
    {"name": "centerY", "init": "height/2"},
    {"name": "radiusRef", "init": "min(width,height)*0.95"},
    {"name": "sizeFactor", "init": "radiusRef/400"},
    {"name": "outerRadius", "init": "radiusRef/2"},
    {"name": "innerRadiusMinutes", "init": "radiusRef/2 - (18 * sizeFactor)"},
    {"name": "innerRadiusHours", "init": "radiusRef/2 - (36 * sizeFactor)"},
    {
      "name": "currentDate",
      "init": "now()",
      "on": [{"events": {"type": "timer", "throttle": 10}, "update": "now()"}]
    },
    {
      "name": "currentHour",
      "init": "hours(currentDate)+minutes(currentDate)/60",
      "on": [
        {
          "events": {"signal": "currentDate"},
          "update": "hours(currentDate)+minutes(currentDate)/60"
        }
      ]
    },
    {
      "name": "currentMinute",
      "init": "minutes(currentDate)+seconds(currentDate)/60",
      "on": [
        {
          "events": {"signal": "currentDate"},
          "update": "minutes(currentDate)+seconds(currentDate)/60"
        }
      ]
    },
    {
      "name": "currentSecond",
      "init": "seconds(currentDate)",
      "on": [
        {"events": {"signal": "currentDate"}, "update": "seconds(currentDate)+milliseconds(currentDate)/1000"}
      ]
    }
  ],
  "data": [
    {
      "name": "hours",
      "transform": [
        {"type": "sequence", "start": 0, "stop": 12, "step": 1, "as": "hour"},
        {
          "type": "formula",
          "expr": "centerX - cos(PI/2 + (datum.hour * PI/6)) * outerRadius",
          "as": "x"
        },
        {
          "type": "formula",
          "expr": "centerY - sin(PI/2 + (datum.hour * PI/6)) * outerRadius",
          "as": "y"
        },
        {
          "type": "formula",
          "expr": "centerX - cos(PI/2 + (datum.hour * PI/6)) * innerRadiusHours",
          "as": "x2"
        },
        {
          "type": "formula",
          "expr": "centerY - sin(PI/2 + (datum.hour * PI/6)) * innerRadiusHours",
          "as": "y2"
        },
        {
          "type": "formula",
          "expr": "centerX - cos(PI/2 + (datum.hour * PI/6)) * (innerRadiusHours - 26 * max(sizeFactor, 0.4))",
          "as": "xHour"
        },
        {
          "type": "formula",
          "expr": "centerY - sin(PI/2 + (datum.hour * PI/6)) * (innerRadiusHours - 26 * max(sizeFactor, 0.4))",
          "as": "yHour"
        }
      ]
    },
    {
      "name": "minutes",
      "transform": [
        {"type": "sequence", "start": 0, "stop": 60, "step": 1, "as": "minute"},
        {
          "type": "formula",
          "expr": "centerX - cos(PI/2 + (datum.minute * PI/30)) * outerRadius",
          "as": "x"
        },
        {
          "type": "formula",
          "expr": "centerY - sin(PI/2 + (datum.minute * PI/30)) * outerRadius",
          "as": "y"
        },
        {
          "type": "formula",
          "expr": "centerX - cos(PI/2 + (datum.minute * PI/30)) * innerRadiusMinutes",
          "as": "x2"
        },
        {
          "type": "formula",
          "expr": "centerY - sin(PI/2 + (datum.minute * PI/30)) * innerRadiusMinutes",
          "as": "y2"
        }
      ]
    }
  ],
  "scales": [
    {
      "name": "hourScale",
      "domain": {"data": "hours", "field": "hour"},
      "range": [0, {"signal": "2*PI"}]
    },
    {
      "name": "minutesScale",
      "domain": {"data": "minutes", "field": "minute"},
      "range": [0, {"signal": "2*PI"}]
    }
  ],
  "marks": [
    {
      "type": "arc",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "startAngle": {"value": 0},
          "endAngle": {"signal": "2*PI"},
          "outerRadius": {"signal": "outerRadius"},
          "fill": {"value": "black"},
          "stroke": {"value": "black"}
        }
      }
    },
    {
      "type": "rule",
      "from": {"data": "minutes"},
      "encode": {
        "enter": {
          "x": {"field": "x"},
          "y": {"field": "y"},
          "x2": {"field": "x2"},
          "y2": {"field": "y2"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 0.2)"},
          "stroke": {"value": "white"}
        }
      }
    },
    {
      "type": "rule",
      "from": {"data": "hours"},
      "encode": {
        "enter": {
          "size": {"signal": "pow(2*sizeFactor, 4)"},
          "x": {"field": "x"},
          "y": {"field": "y"},
          "x2": {"field": "x2"},
          "y2": {"field": "y2"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 2)"},
          "stroke": {"value": "white"}
        }
      }
    },
    {
      "type": "text",
      "from": {"data": "hours"},
      "encode": {
        "enter": {
          "x": {"field": "xHour"},
          "y": {"field": "yHour"},
          "align": {"value": "center"},
          "fill": {"value": "white"},
          "baseline": {"value": "middle"},
          "text": {"signal": "datum.hour === 0 ? 12 : datum.hour"},
          "fontSize": {"signal": "28*max(sizeFactor, 0.4)"},
          "fontWeight": {"value": "100"}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "stroke": {"value": "white"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 3)"}
        },
        "update": {
          "x2": {"signal": "centerX - cos(PI/2 + (currentHour * PI/6)) * (innerRadiusHours - (50 * sizeFactor))"},
          "y2": {"signal": "centerY - sin(PI/2 + (currentHour * PI/6)) * (innerRadiusHours - (50 * sizeFactor))"}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "stroke": {"value": "white"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 2)"}
        },
        "update": {
          "x2": {"signal": "centerX - cos(PI/2 + (currentMinute * PI/30)) * (innerRadiusHours + (innerRadiusMinutes-innerRadiusHours)/2)"},
          "y2": {"signal": "centerY - sin(PI/2 + (currentMinute * PI/30)) * (innerRadiusHours + (innerRadiusMinutes-innerRadiusHours)/2)"}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "stroke": {"value": "goldenrod"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 1.5)"}
        },
        "update": {
          "x2": {"signal": "centerX - cos(PI/2 + (currentSecond * PI/30)) * (innerRadiusHours + (innerRadiusMinutes-innerRadiusHours)/2)"},
          "y2": {"signal": "centerY - sin(PI/2 + (currentSecond * PI/30)) * (innerRadiusHours + (innerRadiusMinutes-innerRadiusHours)/2)"}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "stroke": {"value": "goldenrod"},
          "strokeWidth": {"signal": "pow(2*sizeFactor, 3.5)"},
          "strokeCap": {"value": "round"}
        },
        "update": {
          "x2": {"signal": "centerX - cos(PI*3/2 + (currentSecond * PI/30)) * (24 * sizeFactor)"},
          "y2": {"signal": "centerY - sin(PI*3/2 + (currentSecond * PI/30)) * (24 * sizeFactor)"}
        }
      }
    },
    {
      "type": "arc",
      "encode": {
        "enter": {
          "x": {"signal": "centerX"},
          "y": {"signal": "centerY"},
          "startAngle": {"value": 0},
          "endAngle": {"signal": "2*PI"},
          "outerRadius": {"signal": "10*sizeFactor"},
          "fill": {"value": "goldenrod"},
          "stroke": {"value": "goldenrod"},
          "zIndex": {"value": 1}
        }
      }
    }
  ]
}