block by micahstubbs 8e94f74c7d0a5dec710330574781fda0

stopwatch with d3-component | es2015

Full Screen

an es2015 iteration on the block Stopwatch from @currankelleher


A stopwatch app constructed using d3-component.

The following components are defined and used to construct the app:

Built with blockbuilder.org

forked from curran‘s block: Posts with d3-component

index.html

<!DOCTYPE html>
<head>
  <meta charset='utf-8'>
  <script src='https://unpkg.com/d3@4'></script>
  <script src='https://unpkg.com/d3-component@3'></script>
  <script src='https://unpkg.com/redux@3/dist/redux.min.js'></script>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.23.1/babel.min.js'></script>
  <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  <style>
    body {
      text-align: center;
      margin-top: 75px;
    }
    .time-display {
      color: #3d3d3d;
      font-size: 10em;
      font-family: mono;
      cursor: default;
    }
    button {
      background-color: #7c7c7c;
      border: solid 3px #7c7c7c;
      border-radius: 11px;
      color: white;
      padding: 20px 60px;
      margin: 20px;
      font-size: 58px;
      cursor: pointer;
    }
    button:hover {
      border: solid 3px black;
    }
    button:focus {
      outline: none;
    }
  </style>
</head>
<body>
  <script src='vis.js'></script>
</body>

lebab.sh

# safe
lebab --replace vis.js --transform arrow
lebab --replace vis.js --transform for-of
lebab --replace vis.js --transform for-each
lebab --replace vis.js --transform arg-rest
lebab --replace vis.js --transform arg-spread
lebab --replace vis.js --transform obj-method
lebab --replace vis.js --transform obj-shorthand
lebab --replace vis.js --transform multi-var
# unsafe
lebab --replace vis.js --transform let
lebab --replace vis.js --transform template

vis.js

/* global d3 Redux */

// This function formats the stopwatch time.
const stopwatchFormat = ((() => {
  const twoDigits = d3.format('02.0f');
  return (milliseconds) => {
    const centiseconds = Math.floor(milliseconds / 10);
    const centisecond = centiseconds % 100;
    const seconds = Math.floor(centiseconds / 100);
    const second = seconds % 60;
    const minutes = Math.floor(seconds / 60);
    const minute = minutes % 60;
    const hours = Math.floor(minutes / 60);
    return [
      hours >= 1 ? `${hours}:` : '',
      minutes >= 1 ? (
        `${hours >= 1 ? twoDigits(minute) : minute}:`
      ) : '',
      (minutes >= 1 ? twoDigits(second) : second),
      hours < 1 ? `:${twoDigits(centisecond)}` : '',
    ].join('').replace(/0/g, 'O'); // Don't show the dot in the zeros.
  };
})());

// A component that renders the formatted stopwatch time.
const timeDisplay = ((() => {
  const timerLocal = d3.local();
  return d3.component('div', 'time-display')
    .render(function (selection, d) {
      const timer = timerLocal.get(selection.node());
      if (timer) { timer.stop(); }
      if (d.stopped) {
        selection.text(stopwatchFormat(d.stopTime - d.startTime));
      } else {
        timerLocal.set(selection.node(), d3.timer(() => {
          selection.text(stopwatchFormat(Date.now() - d.startTime));
        }));
      }
    })
    .destroy(function (selection) {
      const timer = timerLocal.get(selection.node());
      if (timer) { timer.stop(); }
    });
})());

// A generic Button component.
const button = d3.component('button')
  .render(function (selection, d) {
    selection
      .text(d.text)
      .on('mousedown', d.onClick);
  });

// The button that either starts or stops (pauses) the stopwatch,
// depending on whether the stopwatch is running or not.
const startStopButton = d3.component('span')
  .render(function (selection, d) {
    selection.call(button, {
      text: d.stopped ? 'Start' : 'Stop',
      onClick: d.stopped ? d.actions.start : d.actions.stop,
    });
  });

// The reset button that stops and resets the stopwatch.
const resetButton = d3.component('span')
  .render(function (selection, d) {
    selection.call(button, {
      text: 'Reset',
      onClick: d.actions.reset,
    });
  });

// The panel that contains the two buttons.
const buttonPanel = d3.component('div')
  .render(function (selection, d) {
    selection
      .call(resetButton, d)
      .call(startStopButton, d);
  });

// The top-level app component.
const app = d3.component('div')
  .render(function (selection, d) {
    selection
      .call(timeDisplay, d)
      .call(buttonPanel, d);
  });

function main() {
  const store = Redux.createStore(reducer);
  const actions = actionsFromDispatch(store.dispatch);
  store.subscribe(render);
  actions.reset();
  function reducer(state, action) {
    state = state || {
      stopped: true,
    };
    let now;
    switch (action.type) {
      case 'START':
        return Object.assign({}, state, {
          stopped: false,
          startTime: Date.now() - (state.stopTime - state.startTime),
        });
      case 'STOP':
        return Object.assign({}, state, {
          stopped: true,
          stopTime: Date.now(),
        });
      case 'RESET':
        now = Date.now();
        return Object.assign({}, state, {
          stopped: true,
          startTime: now,
          stopTime: now,
        });
      default:
        return state;
    }
  }

  function actionsFromDispatch(dispatch) {
    return {
      start() {
        dispatch({ type: 'START' });
      },
      stop() {
        dispatch({ type: 'STOP' });
      },
      reset() {
        dispatch({ type: 'RESET' });
      },
    };
  }

  function render() {
    console.log(store.getState());
    d3.select('body').call(app, store.getState(), {
      actions,
    });
  }
}

// call main() to run the app
main();