Create a Digital Clock in Javascript with d3

Updated Feb 5, 2018

d3 is well known for its use in generating data-driven charts. But it’s also a great tool for building user-interface components that aren’t necessarily driven by external data sets. This post will explore constructing a digital clock by taking advantage of nested d3 selections and data. The full code is also available for download.

The finished product is a digital clock depicting the browser time in hours, minutes, and seconds:

We start by importing d3 dependencies and defining constants used in sizing and coloring the components:

Defining constants Copy
import {
  path   as d3_path,
  select as d3_select
from 'd3';
const DIGIT_WIDTH   = 70;
const DIGIT_PADDING = 0.15 * DIGIT_WIDTH;
const BAR_HEIGHT    = 0.2 * (DIGIT_WIDTH - 2*DIGIT_PADDING);
const BAR_SPACE     = 0.1 * BAR_HEIGHT;
const BAR_WIDTH     = DIGIT_WIDTH - 2*DIGIT_PADDING - BAR_HEIGHT;
const DIGIT_HEIGHT  = 2*DIGIT_PADDING + 2*BAR_WIDTH + BAR_HEIGHT + 4*BAR_SPACE;
const DOT_WIDTH     = 2*DIGIT_PADDING + BAR_HEIGHT;
const DOT_SPACE     = (DIGIT_HEIGHT - 2*DIGIT_PADDING - 2*BAR_HEIGHT) / 3;
const COLOR_ON      = '#70fbfd';
const COLOR_OFF     = '#181917';

Here “DIGIT” refers to a single digit in the clock; “BAR” refers to a bar within a digit; and “DOT” refers to the dots between hours and minutes and minutes and seconds. Note that all sizing constants derive from DIGIT_WIDTH, with the help from some magic numbers. Thus to change the clock size simply change the DIGIT_WIDTH value. The color constants correspond to a bar being “on” or “off”. Below is a visual representation of the sizing constants in action:

BAR_WIDTHBAR_HEIGHTDIGIT_WIDTHDIGIT_HEIGHTDIGIT_PADDINGDIGIT_PADDINGBAR_SPACE

Each bar in a digit is an SVGPathElement with the same d attribute value. We use d3’s path module to generate the path attribute value, which is stored as barPath for later use:

Bar path Copy
const barPath = (() => {
  const p = d3_path();
  p.moveTo(0, BAR_HEIGHT / 2);
  p.lineTo(BAR_HEIGHT / 20);
  p.lineTo(BAR_WIDTH - BAR_HEIGHT / 20);
  p.lineTo(BAR_WIDTH, BAR_HEIGHT / 2);
  p.lineTo(BAR_WIDTH - BAR_HEIGHT / 2, BAR_HEIGHT);
  p.lineTo(BAR_HEIGHT / 2, BAR_HEIGHT);
  p.closePath();
  return p.toString();
})();

While each bar shares the same path each has its own x-y positioning, rotation, and on-off state. We define a function barData that generates bar data for a single digit based on the digit’s current value (e.g., 0, 1, 2, 3, … 9). Later we use the function to bind data to digits in the clock.

The function returns an array of objects corresponding to individual bars: [top, top-left, top-right, middle, bottom-left, bottom-right, bottom]. Each object defines the bar’s position within the digit (the x and y properties), the bar’s rotation (the rot property), and its state (the on property):

Bar data Copy
function barData (v) {
  return [
    { // top
      x   : BAR_HEIGHT/2,
      y   : 0,
      rot : 0,
      on  : [02356789].indexOf(v) > -1
    },
    { // top left
      x   : BAR_HEIGHT - BAR_SPACE,
      y   : BAR_HEIGHT/2 + BAR_SPACE,
      rot : 90,
      on  : [045689].indexOf(v) > -1
    },
    { // top right
      x   : BAR_WIDTH + BAR_HEIGHT + BAR_SPACE,
      y   : BAR_HEIGHT/2 + BAR_SPACE,
      rot : 90,
      on  : [01234789].indexOf(v) > -1
    },
    { // middle
      x   : BAR_HEIGHT/2,
      y   : BAR_WIDTH + 2*BAR_SPACE,
      rot : 0,
      on  : [2345689].indexOf(v) > -1
    },
    { // bottom left
      x   : BAR_HEIGHT - BAR_SPACE,
      y   : BAR_WIDTH + BAR_HEIGHT/2 + 3*BAR_SPACE,
      rot : 90,
      on  : [0268].indexOf(v) > -1
    },
    { // bottom right
      x   : BAR_WIDTH + BAR_HEIGHT + BAR_SPACE,
      y   : BAR_WIDTH + BAR_HEIGHT/2 + 3*BAR_SPACE,
      rot : 90,
      on  : [013456789].indexOf(v) > -1
    },
    { // bottom
      x   : BAR_HEIGHT/2,
      y   : 2*BAR_WIDTH + 4*BAR_SPACE,
      rot : 0,
      on  : [0235689].indexOf(v) > -1
    }
  ];
}

All of the object properties are static save for on, which evaluates to true if the bar should be on for the passed digit value v. We now begin rendering the clock itself, starting with the containing svg element and black background:

Static elements Copy
// Create main element.
const svg = d3_select('#digital-clock').append('svg')
  .attr('width'6*DIGIT_WIDTH + 2*DOT_WIDTH)
  .attr('height', DIGIT_HEIGHT)
  .append('g');
// Create black background.
svg.append('rect')
  .attr('width'6*DIGIT_WIDTH + 2*DOT_WIDTH)
  .attr('height', DIGIT_HEIGHT)
  .attr('fill''#000');

Next we render the digits, binding the bar data defined earlier:

Digits Copy
// Create clock.
const clock = svg.append('g')
  .attr('transform''translate(' + DIGIT_PADDING + ',' + DIGIT_PADDING + ')');
// Create digits.
const digits = clock.selectAll('.digit').data([123456])
  .enter()
  .append('g')
    .attr('class''digit')
    .attr('transform', (d, i) => 'translate(' + (i*DIGIT_WIDTH + Math.floor(i/2)*DOT_WIDTH) + ',0)');
// Create bars for each digit.
digits.selectAll('.bar').data(d => barData(d))
  .enter()
  .append('path')
    .attr('class''bar')
    .attr('d', barPath)
    .attr('fill', d => d.on ? COLOR_ON : COLOR_OFF)
    .attr('transform', d => 'translate(' + d.x + ',' + d.y + ') rotate(' + d.rot + ')');

Next we add the dots that appear between the hour and minute and minute and second. We follow a similar approach to creating the bars, first defining the data to position each dot then binding those data to the SVG elements. dotData defines an [x, y] position for each dot:

Dots Copy
// [x, y] positions for dots.
const dotData = [
  [2*DIGIT_WIDTH, DOT_SPACE],
  [2*DIGIT_WIDTH, 2*DOT_SPACE + BAR_HEIGHT],
  [4*DIGIT_WIDTH + DOT_WIDTH, DOT_SPACE],
  [4*DIGIT_WIDTH + DOT_WIDTH, 2*DOT_SPACE + BAR_HEIGHT]
];
// Create dots.
clock.selectAll('.dot').data(dotData)
  .enter()
  .append('rect')
    .attr('class''dot')
    .attr('x', d => d[0])
    .attr('y', d => d[1])
    .attr('width', BAR_HEIGHT)
    .attr('height', BAR_HEIGHT)
    .attr('fill', COLOR_ON);

At this point the code should render the clock, though the time will always be 12:34:56. The final step is to define an update function that uses the current time to update the digit and bar data:

Update function Copy
function update () {
  const date = new Date();
  const h = date.getHours() % 12;
  const m = date.getMinutes();
  const s = date.getSeconds();
  svg.selectAll('.digit').data([
      h === 0 ? 1 : h > 9 ? 1 : -1,
      h === 0 ? 2 : h > 9 ? h - 10 : h,
      Math.floor(m / 10),
      m % 10,
      Math.floor(s / 10),
      s % 10
    ])
    .selectAll('.bar').data(d => barData(d))
    .attr('fill', d => d.on ? COLOR_ON : COLOR_OFF);
}
window.setInterval(update, 20);

That’s it, at long last you’ll be able to know what time it is! The full source file is available here for download. Feel free to comment or ask questions below.

Comments

No comments exist. Be the first!

Leave a comment