From 6d9f579b60138a1e7854fd50fb50c301f147e36f Mon Sep 17 00:00:00 2001
From: meriadec <meriadec.pillet@gmail.com>
Date: Sun, 11 Mar 2018 22:00:00 +0100
Subject: [PATCH] Rewrite Chart component

---
 package.json                                  |   1 +
 src/components/base/NewChart/Tooltip.js       |  58 ++++++
 .../base/NewChart/handleMouseEvents.js        | 105 ++++++++++
 src/components/base/NewChart/helpers.js       |  42 ++++
 src/components/base/NewChart/index.js         | 183 ++++++++++++++++
 src/components/base/NewChart/refreshDraw.js   | 106 ++++++++++
 src/components/base/NewChart/refreshNodes.js  | 129 ++++++++++++
 src/components/base/NewChart/stories.js       |  70 +++++++
 src/components/base/NewChart/types.js         |  28 +++
 src/components/base/Tooltip/index.js          |  41 +++-
 src/index.ejs                                 |   2 -
 src/styles/reset.js                           |   2 +-
 yarn.lock                                     | 195 +++++++++++++++---
 13 files changed, 926 insertions(+), 36 deletions(-)
 create mode 100644 src/components/base/NewChart/Tooltip.js
 create mode 100644 src/components/base/NewChart/handleMouseEvents.js
 create mode 100644 src/components/base/NewChart/helpers.js
 create mode 100644 src/components/base/NewChart/index.js
 create mode 100644 src/components/base/NewChart/refreshDraw.js
 create mode 100644 src/components/base/NewChart/refreshNodes.js
 create mode 100644 src/components/base/NewChart/stories.js
 create mode 100644 src/components/base/NewChart/types.js

diff --git a/package.json b/package.json
index db27f11b..7d0272e8 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
     "bs58check": "^2.1.1",
     "color": "^3.0.0",
     "cross-env": "^5.1.3",
+    "d3": "^4.13.0",
     "debug": "^3.1.0",
     "downshift": "^1.30.0",
     "electron-store": "^1.3.0",
diff --git a/src/components/base/NewChart/Tooltip.js b/src/components/base/NewChart/Tooltip.js
new file mode 100644
index 00000000..b1be1fa3
--- /dev/null
+++ b/src/components/base/NewChart/Tooltip.js
@@ -0,0 +1,58 @@
+// @flow
+
+import React from 'react'
+
+import { colors as themeColors } from 'styles/theme'
+import { TooltipContainer } from 'components/base/Tooltip'
+
+import type { Item } from './types'
+
+/**
+ * styled-components is not run on those components, because tooltip is
+ * rendered as a string on d3 updates
+ *
+ * so, we use inline style.
+ */
+
+const Arrow = () => (
+  <svg
+    style={{
+      display: 'block',
+      position: 'absolute',
+      left: '50%',
+      bottom: 0,
+      marginBottom: -10,
+      transform: 'translate(-50%, 0)',
+    }}
+    viewBox="0 0 14 6.2"
+    width={16}
+    height={16}
+  >
+    <path fill={themeColors.dark} d="m14 0-5.5 5.6c-0.8 0.8-2 0.8-2.8 0l-5.7-5.6" />
+  </svg>
+)
+
+export default ({ d }: { d: Item }) => (
+  <div style={{ position: 'relative' }}>
+    <div
+      style={{
+        position: 'absolute',
+        bottom: '100%',
+        left: 0,
+        transform: `translate3d(-50%, 0, 0)`,
+        whiteSpace: 'nowrap',
+        marginBottom: 10,
+      }}
+    >
+      <TooltipContainer style={{ textAlign: 'center' }}>
+        <div style={{ fontSize: 14 }}>
+          <b>{Math.round(d.value)}</b>
+        </div>
+        {d.date}
+      </TooltipContainer>
+      <div style={{ background: 'red' }}>
+        <Arrow />
+      </div>
+    </div>
+  </div>
+)
diff --git a/src/components/base/NewChart/handleMouseEvents.js b/src/components/base/NewChart/handleMouseEvents.js
new file mode 100644
index 00000000..81278931
--- /dev/null
+++ b/src/components/base/NewChart/handleMouseEvents.js
@@ -0,0 +1,105 @@
+// @flow
+
+import React from 'react'
+import * as d3 from 'd3'
+import { renderToString } from 'react-dom/server'
+
+import type { Props } from '.'
+import type { CTX } from './types'
+
+import Tooltip from './Tooltip'
+
+export default function handleMouseEvents({
+  ctx,
+  props,
+  shouldTooltipUpdate,
+  onTooltipUpdate,
+}: {
+  ctx: CTX,
+  props: Props,
+  shouldTooltipUpdate: Function,
+  onTooltipUpdate: Function,
+}) {
+  const { MARGINS, HEIGHT, WIDTH, NODES, DATA, x, y } = ctx
+  const { hideAxis } = props
+
+  const bisectDate = d3.bisector(d => d.parsedDate).left
+
+  const node = NODES.wrapper
+    .append('rect')
+    .attr('fill', 'none')
+    .attr('pointer-events', 'all')
+    .attr('class', 'overlay')
+    .attr('width', WIDTH)
+    .attr('height', HEIGHT)
+
+  node
+    .on('mousemove', mouseMove)
+    .on('mouseenter', mouseEnter)
+    .on('mouseout', mouseOut)
+
+  function getStep() {
+    const x0 = x.invert(d3.mouse(this)[0])
+    const i = bisectDate(DATA, x0, 1)
+    const d0 = DATA[i - 1]
+    const d1 = DATA[i]
+    if (!d0 || !d1) {
+      return null
+    }
+    return x0 - d0.parsedDate > d1.parsedDate - x0 ? d1 : d0
+  }
+
+  function mouseEnter() {
+    const d = getStep.call(this)
+    if (!d) {
+      return
+    }
+    NODES.tooltip
+      .style('transition', '100ms cubic-bezier(.61,1,.53,1) opacity')
+      .style('opacity', 1)
+      .style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, ${y(d.value)}px, 0)`)
+    NODES.focus.style('opacity', 1)
+    if (!hideAxis) {
+      NODES.xBar.style('opacity', 1)
+      NODES.yBar.style('opacity', 1)
+    }
+  }
+
+  function mouseOut() {
+    NODES.tooltip.style('opacity', 0).style('transition', '100ms linear opacity')
+    NODES.focus.style('opacity', 0)
+    if (!hideAxis) {
+      NODES.xBar.style('opacity', 0)
+      NODES.yBar.style('opacity', 0)
+    }
+  }
+
+  function mouseMove() {
+    const d = getStep.call(this)
+    if (!d) {
+      return
+    }
+    if (!shouldTooltipUpdate(d)) {
+      return
+    }
+    onTooltipUpdate(d)
+    NODES.focus.attr('transform', `translate(${x(d.parsedDate)},${y(d.value)})`)
+    NODES.tooltip
+      .html(renderToString(<Tooltip d={d.ref} />))
+      .style('transform', `translate3d(${MARGINS.left + x(d.parsedDate)}px, ${y(d.value)}px, 0)`)
+    if (!hideAxis) {
+      NODES.xBar
+        .attr('x1', x(d.parsedDate))
+        .attr('x2', x(d.parsedDate))
+        .attr('y1', HEIGHT)
+        .attr('y2', y(d.value))
+      NODES.yBar
+        .attr('x1', 0)
+        .attr('x2', x(d.parsedDate))
+        .attr('y1', y(d.value))
+        .attr('y2', y(d.value))
+    }
+  }
+
+  return node
+}
diff --git a/src/components/base/NewChart/helpers.js b/src/components/base/NewChart/helpers.js
new file mode 100644
index 00000000..ca98019a
--- /dev/null
+++ b/src/components/base/NewChart/helpers.js
@@ -0,0 +1,42 @@
+import c from 'color'
+
+export function enrichData(data, parseTime) {
+  return data.map((d, i) => ({
+    ...d,
+    ref: d,
+    index: i,
+    parsedDate: parseTime(d.date),
+  }))
+}
+
+export function generateColors(color) {
+  const cColor = c(color)
+  return {
+    line: color,
+    focus: color,
+    gradientStart: cColor.fade(0.7),
+    gradientStop: cColor.fade(1),
+    focusBar: cColor.fade(0.5),
+  }
+}
+
+export function generateMargins(hideAxis) {
+  return {
+    top: hideAxis ? 5 : 10,
+    bottom: hideAxis ? 5 : 40,
+    right: hideAxis ? 5 : 10,
+    left: hideAxis ? 5 : 70,
+  }
+}
+
+export function observeResize(node, cb) {
+  const ro = new ResizeObserver(() => {
+    if (!node) {
+      return
+    }
+    const { width } = node.getBoundingClientRect()
+    cb(width)
+  })
+
+  ro.observe(node)
+}
diff --git a/src/components/base/NewChart/index.js b/src/components/base/NewChart/index.js
new file mode 100644
index 00000000..879a31f0
--- /dev/null
+++ b/src/components/base/NewChart/index.js
@@ -0,0 +1,183 @@
+// @flow
+
+/**
+ *                                   Chart
+ *                                   -----
+ *
+ *                                    XX
+ *                                   XXXX
+ *                          X       XX  X
+ *                         XXXX    XX   X
+ *                        XX  X  XX     X
+ *                       X    XXXX       X     XX    X
+ *                      XX     XX        X   XX XX  XX
+ *                     XX                XX XX   XXXX
+ *                                        XX
+ *                                        XX
+ *  Usage:
+ *
+ *    <Chart
+ *      data={data}
+ *      interactive       // Handle mouse events, display tooltip etc.
+ *      color="#5f8ced"   // Main color for line, gradient, etc.
+ *      height={300}      // Fix height. Width is responsive to container.
+ *    />
+ *
+ *    `data` looks like:
+ *
+ *     [
+ *       { date: '2018-01-01', value: 10 },
+ *       { date: '2018-01-02', value: 25 },
+ *       { date: '2018-01-03', value: 50 },
+ *     ]
+ *
+ */
+
+import React, { PureComponent } from 'react'
+import * as d3 from 'd3'
+import noop from 'lodash/noop'
+
+import refreshNodes from './refreshNodes'
+import refreshDraw from './refreshDraw'
+import handleMouseEvents from './handleMouseEvents'
+import { enrichData, generateColors, generateMargins, observeResize } from './helpers'
+
+import type { Data } from './types'
+
+export type Props = {
+  data: Data, // eslint-disable-line react/no-unused-prop-types
+  color?: string, // eslint-disable-line react/no-unused-prop-types
+  hideAxis?: boolean, // eslint-disable-line react/no-unused-prop-types
+  interactive?: boolean, // eslint-disable-line react/no-unused-prop-types
+  height: number,
+  dateFormat?: string, // eslint-disable-line react/no-unused-prop-types
+}
+
+class Chart extends PureComponent<Props> {
+  static defaultProps = {
+    color: '#000',
+    hideAxis: false,
+    interactive: true,
+    height: 400,
+    dateFormat: '%Y-%m-%d',
+  }
+
+  componentDidMount() {
+    const { width } = this._ruler.getBoundingClientRect()
+    this._width = width
+    this.createChart()
+    observeResize(this._ruler, width => {
+      if (width !== this._width) {
+        this._width = width
+        this.refreshChart(this.props)
+      }
+    })
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    this.refreshChart(prevProps)
+  }
+
+  _ruler: any
+  _node: any
+  _width: number
+  refreshChart: Function
+
+  createChart() {
+    const ctx = {
+      NODES: {},
+      INVALIDATED: {},
+      MARGINS: {},
+      COLORS: {},
+      DATA: [],
+      WIDTH: 0,
+      HEIGHT: 0,
+      x: noop,
+      y: noop,
+    }
+
+    let firstRender = true
+
+    // Keep reference to mouse handler to allow destroy when refresh
+    let mouseHandler = null
+
+    this.refreshChart = prevProps => {
+      const { _node: node, props } = this
+      const { data: raw, color, dateFormat, height, hideAxis, interactive } = props
+      let { COLORS, MARGINS } = ctx
+
+      const DATA = enrichData(raw, d3.timeParse(dateFormat))
+      ctx.DATA = DATA
+
+      // Detect what needs to be updated
+      const INVALIDATED = {
+        color: firstRender || (prevProps && color !== prevProps.color),
+        margin: firstRender || (prevProps && hideAxis !== prevProps.hideAxis),
+      }
+      firstRender = false
+      ctx.INVALIDATED = INVALIDATED
+
+      // Reset color if needed
+      if (INVALIDATED.color) {
+        COLORS = generateColors(color)
+        ctx.COLORS = COLORS
+      }
+
+      // Reset margins if needed
+      if (INVALIDATED.margin) {
+        MARGINS = generateMargins(hideAxis)
+        ctx.MARGINS = MARGINS
+      }
+
+      // Derived draw variables
+      const HEIGHT = Math.max(0, height - MARGINS.top - MARGINS.bottom)
+      const WIDTH = Math.max(0, this._width - MARGINS.left - MARGINS.right)
+      ctx.HEIGHT = HEIGHT
+      ctx.WIDTH = WIDTH
+
+      // Scales and areas
+      const x = d3.scaleTime().range([0, WIDTH])
+      const y = d3.scaleLinear().range([HEIGHT, 0])
+      x.domain(d3.extent(DATA, d => d.parsedDate))
+      y.domain([0, d3.max(DATA, d => d.value)])
+      ctx.x = x
+      ctx.y = y
+
+      // Reference to last tooltip, to prevent un-necessary re-render
+      let lastDisplayedTooltip = null
+
+      // Add/remove nodes depending on props
+      refreshNodes({ ctx, node, props })
+
+      // Redraw
+      refreshDraw({ ctx, props })
+
+      // Mouse handler
+      mouseHandler && mouseHandler.remove() // eslint-disable-line no-unused-expressions
+      if (interactive) {
+        mouseHandler = handleMouseEvents({
+          ctx,
+          props,
+          shouldTooltipUpdate: d => d !== lastDisplayedTooltip,
+          onTooltipUpdate: d => (lastDisplayedTooltip = d),
+        })
+      }
+    }
+
+    this.refreshChart()
+  }
+
+  render() {
+    const { height } = this.props
+    return (
+      <div style={{ position: 'relative', height }} ref={n => (this._ruler = n)}>
+        <div
+          style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
+          ref={n => (this._node = n)}
+        />
+      </div>
+    )
+  }
+}
+
+export default Chart
diff --git a/src/components/base/NewChart/refreshDraw.js b/src/components/base/NewChart/refreshDraw.js
new file mode 100644
index 00000000..c9acc0fb
--- /dev/null
+++ b/src/components/base/NewChart/refreshDraw.js
@@ -0,0 +1,106 @@
+// @flow
+
+import * as d3 from 'd3'
+
+import { colors as themeColors } from 'styles/theme'
+
+import type { Props } from '.'
+import type { CTX } from './types'
+
+export default function refreshDraw({ ctx, props }: { ctx: CTX, props: Props }) {
+  const { NODES, WIDTH, HEIGHT, MARGINS, COLORS, INVALIDATED, DATA, x, y } = ctx
+  const { hideAxis, interactive } = props
+
+  const area = d3
+    .area()
+    .x(d => x(d.parsedDate))
+    .y0(HEIGHT)
+    .y1(d => y(d.value))
+
+  const valueline = d3
+    .line()
+    .x(d => x(d.parsedDate))
+    .y(d => y(d.value))
+
+  // Resize container
+  NODES.svg
+    .attr('width', WIDTH + MARGINS.left + MARGINS.right)
+    .attr('height', HEIGHT + MARGINS.top + MARGINS.bottom)
+
+  // Resize wrapper & axis
+  NODES.wrapper.attr('transform', `translate(${MARGINS.left},${MARGINS.top})`)
+
+  // Resize axis
+  if (!hideAxis) {
+    NODES.axisBot.attr('transform', `translate(0,${HEIGHT + 10})`)
+  }
+
+  if (INVALIDATED.color) {
+    if (interactive) {
+      // Update focus bar colors
+      NODES.xBar.attr('stroke', COLORS.focusBar)
+      NODES.yBar.attr('stroke', COLORS.focusBar)
+      // Update dot color
+      NODES.focus.attr('fill', COLORS.focus)
+    }
+    // Update gradient color
+    NODES.gradientStart.attr('stop-color', COLORS.gradientStart)
+    NODES.gradientStop.attr('stop-color', COLORS.gradientStop)
+
+    // Update line color
+    NODES.line.attr('stroke', COLORS.line)
+  }
+
+  // Hide interactive things
+  if (interactive) {
+    NODES.focus.style('opacity', 0)
+    NODES.tooltip.style('opacity', 0)
+    NODES.xBar.style('opacity', 0)
+    NODES.yBar.style('opacity', 0)
+  }
+
+  // Draw axis
+  if (!hideAxis) {
+    NODES.axisLeft.call(d3.axisLeft(y).ticks(3))
+    NODES.axisBot.call(d3.axisBottom(x).ticks(5))
+    stylizeAxis(NODES.axisLeft)
+    stylizeAxis(NODES.axisBot)
+  }
+
+  // Draw ticks
+  if (!hideAxis) {
+    const yTicks = d3
+      .axisLeft(y)
+      .ticks(3)
+      .tickSize(-WIDTH)
+      .tickFormat('')
+
+    NODES.yTicks.call(yTicks)
+    NODES.yTicks.select('.domain').remove()
+
+    NODES.yTicks
+      .selectAll('.tick line')
+      .attr('stroke', 'rgba(0, 0, 0, 0.1)')
+      .attr('stroke-dasharray', '5, 5')
+
+    NODES.yTicks
+      .selectAll('.tick:first-of-type line')
+      .attr('stroke-width', '2px')
+      .attr('stroke-dasharray', 'none')
+  }
+
+  // Draw line and gradient
+  NODES.fillArea.data([DATA]).attr('d', area)
+  NODES.line.data([DATA]).attr('d', valueline)
+}
+
+function stylizeAxis(axis) {
+  axis.selectAll('.tick line').attr('stroke', 'none')
+  axis.selectAll('path').attr('stroke', 'none')
+  axis
+    .selectAll('text')
+    .attr('stroke', themeColors.smoke)
+    .style('font-size', '12px')
+    .style('font-family', 'Open Sans')
+    .style('font-weight', 300)
+}
diff --git a/src/components/base/NewChart/refreshNodes.js b/src/components/base/NewChart/refreshNodes.js
new file mode 100644
index 00000000..3abd612c
--- /dev/null
+++ b/src/components/base/NewChart/refreshNodes.js
@@ -0,0 +1,129 @@
+// @flow
+
+import * as d3 from 'd3'
+import d from 'debug'
+
+import type { Props } from '.'
+import type { CTX } from './types'
+
+const debug = d('Chart')
+
+export default function refreshNodes({ ctx, node, props }: { ctx: CTX, node: any, props: Props }) {
+  const { NODES, COLORS } = ctx
+  const { hideAxis, interactive } = props
+
+  // Container
+
+  ensure({ NODES, key: 'svg' }, () => d3.select(node).append('svg'))
+  ensure({ NODES, key: 'wrapper' }, () => NODES.svg.append('g'))
+  ensure({ NODES, key: 'defs' }, () => NODES.wrapper.append('defs'))
+
+  // Axis & ticks
+
+  ensure({ onlyIf: !hideAxis, NODES, key: 'axisBot' }, () => NODES.wrapper.append('g'))
+  ensure({ onlyIf: !hideAxis, NODES, key: 'axisLeft' }, () => NODES.wrapper.append('g'))
+  ensure({ onlyIf: !hideAxis, NODES, key: 'yTicks' }, () => NODES.wrapper.append('g'))
+
+  // Focus bars
+
+  ensure({ onlyIf: interactive, NODES, key: 'xBar' }, () =>
+    NODES.wrapper
+      .append('line')
+      .attr('stroke', COLORS.focusBar)
+      .attr('stroke-width', '1px')
+      .attr('stroke-dasharray', '3, 2'),
+  )
+
+  ensure({ onlyIf: interactive, NODES, key: 'yBar' }, () =>
+    NODES.wrapper
+      .append('line')
+      .attr('stroke', COLORS.focusBar)
+      .attr('stroke-width', '1px')
+      .attr('stroke-dasharray', '3, 2'),
+  )
+
+  // Gradient
+
+  ensure({ NODES, key: 'gradient' }, () =>
+    NODES.defs
+      .append('linearGradient')
+      .attr('id', 'gradient')
+      .attr('x1', '0%')
+      .attr('x2', '0%')
+      .attr('y1', '0%')
+      .attr('y2', '100%'),
+  )
+
+  ensure({ NODES, key: 'gradientStart' }, () =>
+    NODES.gradient
+      .append('stop')
+      .attr('stop-color', COLORS.gradientStart)
+      .attr('offset', '0'),
+  )
+
+  ensure({ NODES, key: 'gradientStop' }, () =>
+    NODES.gradient
+      .append('stop')
+      .attr('stop-color', COLORS.gradientStop)
+      .attr('offset', '1'),
+  )
+
+  ensure({ NODES, key: 'fillArea' }, () =>
+    NODES.wrapper.append('path').attr('fill', 'url(#gradient)'),
+  )
+
+  // Line
+
+  ensure({ NODES, key: 'line' }, () =>
+    NODES.wrapper
+      .append('path')
+      .attr('fill', 'none')
+      .attr('stroke', COLORS.line)
+      .attr('stroke-width', '2px'),
+  )
+
+  // Tooltip & focus point
+
+  ensure({ onlyIf: interactive, NODES, key: 'tooltip' }, () =>
+    d3
+      .select(node)
+      .append('div')
+      .style('position', 'absolute')
+      .style('top', '0')
+      .style('left', '0')
+      .style('pointer-events', 'none'),
+  )
+
+  ensure({ onlyIf: interactive, NODES, key: 'focus' }, () =>
+    NODES.wrapper
+      .append('g')
+      .append('circle')
+      .attr('fill', COLORS.focus)
+      .attr('r', 4),
+  )
+
+  ctx.NODES = NODES
+}
+
+function ensure(
+  { onlyIf: condition = true, NODES, key }: { onlyIf?: boolean, NODES: Object, key: string },
+  create,
+) {
+  if (!condition && NODES[key]) {
+    remove(NODES, key)
+  }
+  if (condition && !NODES[key]) {
+    append(NODES, key, create())
+  }
+}
+
+function remove(NODES, key) {
+  debug(`Destroying ${key}`)
+  NODES[key].remove()
+  NODES[key] = null
+}
+
+function append(NODES, key, node) {
+  debug(`Appending ${key}`)
+  NODES[key] = node
+}
diff --git a/src/components/base/NewChart/stories.js b/src/components/base/NewChart/stories.js
new file mode 100644
index 00000000..738a5c6e
--- /dev/null
+++ b/src/components/base/NewChart/stories.js
@@ -0,0 +1,70 @@
+// @flow
+
+import React, { Component, Fragment } from 'react'
+import Chance from 'chance'
+import moment from 'moment'
+import { storiesOf } from '@storybook/react'
+import { boolean, number } from '@storybook/addon-knobs'
+import { color } from '@storybook/addon-knobs/react'
+
+import Chart from 'components/base/NewChart'
+
+const stories = storiesOf('Components/Chart', module)
+
+const data = generateRandomData(365)
+
+type State = {
+  start: number,
+  stop: number,
+}
+
+class Wrapper extends Component<any, State> {
+  state = {
+    start: 0,
+    stop: 365,
+  }
+
+  handleChange = key => e => this.setState({ [key]: Number(e.target.value) })
+
+  render() {
+    const { start, stop } = this.state
+    return (
+      <Fragment>
+        <input type="range" value={start} onChange={this.handleChange('start')} min={0} max={365} />
+        <input
+          type="range"
+          value={stop}
+          style={{ marginLeft: 10 }}
+          onChange={this.handleChange('stop')}
+          min={0}
+          max={365}
+        />
+
+        <Chart
+          interactive={boolean('interactive', true)}
+          hideAxis={boolean('hideAxis', false)}
+          color={color('color', '#5f8ced')}
+          data={data.slice(start, stop)}
+          height={number('height', 300, { min: 100, max: 900 })}
+        />
+      </Fragment>
+    )
+  }
+}
+
+stories.add('default', () => <Wrapper />)
+
+function generateRandomData(n) {
+  const today = moment()
+  const day = moment(today).subtract(n, 'days')
+  const data = []
+  const chance = new Chance()
+  while (!day.isSame(today)) {
+    data.push({
+      date: day.format('YYYY-MM-DD'),
+      value: chance.integer({ min: 0, max: 50e3 }),
+    })
+    day.add(1, 'day')
+  }
+  return data
+}
diff --git a/src/components/base/NewChart/types.js b/src/components/base/NewChart/types.js
new file mode 100644
index 00000000..1c6de152
--- /dev/null
+++ b/src/components/base/NewChart/types.js
@@ -0,0 +1,28 @@
+// @flow
+
+export type Item = {
+  date: string,
+  value: number,
+}
+
+type EnrichedItem = {
+  date: string,
+  value: number,
+  parsedDate: Date,
+  ref: Item,
+}
+
+export type Data = Array<Item>
+export type EnrichedData = Array<EnrichedItem>
+
+export type CTX = {
+  NODES: Object,
+  MARGINS: Object,
+  COLORS: Object,
+  INVALIDATED: Object,
+  HEIGHT: number,
+  WIDTH: number,
+  DATA: EnrichedData,
+  x: Function,
+  y: Function,
+}
diff --git a/src/components/base/Tooltip/index.js b/src/components/base/Tooltip/index.js
index b4895513..eac95f5f 100644
--- a/src/components/base/Tooltip/index.js
+++ b/src/components/base/Tooltip/index.js
@@ -4,7 +4,7 @@ import React, { PureComponent } from 'react'
 import styled from 'styled-components'
 import tippy from 'tippy.js'
 
-import { space } from 'styles/theme'
+import { space, colors } from 'styles/theme'
 
 import Box from 'components/base/Box'
 
@@ -16,15 +16,36 @@ const Template = styled.div`
   display: none;
 `
 
-export const TooltipContainer = styled(Box).attrs({
-  bg: 'dark',
-  borderRadius: 1,
-  color: 'white',
-  ff: 'Open Sans|SemiBold',
-  fontSize: 2,
-  px: 2,
-  py: 1,
-})``
+export const TooltipContainer = ({
+  children,
+  innerRef,
+  style,
+}: {
+  children: any,
+  innerRef?: Function,
+  style?: Object,
+}) => (
+  <div
+    ref={innerRef}
+    style={{
+      background: colors.dark,
+      borderRadius: 4,
+      color: 'white',
+      fontFamily: 'Open Sans',
+      fontWeight: 600,
+      fontSize: 10,
+      padding: '5px 10px 5px 10px',
+      ...style,
+    }}
+  >
+    {children}
+  </div>
+)
+
+TooltipContainer.defaultProps = {
+  innerRef: undefined,
+  style: undefined,
+}
 
 type Props = {
   offset?: Array<number>,
diff --git a/src/index.ejs b/src/index.ejs
index f87f3eb1..db8f5c25 100644
--- a/src/index.ejs
+++ b/src/index.ejs
@@ -53,8 +53,6 @@
       const initApp = (options = {}) => {
         const { force = false } = options
 
-        appEl.style.display = 'flex'
-
         if (force) {
           preloadEl.remove()
         } else {
diff --git a/src/styles/reset.js b/src/styles/reset.js
index ebceaf00..6426099e 100644
--- a/src/styles/reset.js
+++ b/src/styles/reset.js
@@ -21,7 +21,7 @@ body {
 }
 
 #app {
-  display: none;
+  display: flex;
   flex-direction: column;
   min-height: 100vh;
 }
diff --git a/yarn.lock b/yarn.lock
index b48a3009..487d9750 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2655,6 +2655,10 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@2, commander@^2.13.0:
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322"
+
 commander@2.14.x, commander@^2.11.0, commander@^2.12.2, commander@^2.14.1, commander@^2.9.0, commander@~2.14.1:
   version "2.14.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
@@ -2663,10 +2667,6 @@ commander@2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
 
-commander@^2.13.0:
-  version "2.15.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322"
-
 commander@~2.13.0:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@@ -3095,37 +3095,121 @@ cyclist@~0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
 
-d3-array@^1.2.0:
+d3-array@1, d3-array@1.2.1, d3-array@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
 
-d3-collection@1:
+d3-axis@1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
+
+d3-brush@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
+d3-chord@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
+  dependencies:
+    d3-array "1"
+    d3-path "1"
+
+d3-collection@1, d3-collection@1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
 
-d3-color@1:
+d3-color@1, d3-color@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
 
-d3-ease@^1.0.0:
+d3-dispatch@1, d3-dispatch@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
+
+d3-drag@1, d3-drag@1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
+  dependencies:
+    d3-dispatch "1"
+    d3-selection "1"
+
+d3-dsv@1, d3-dsv@1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae"
+  dependencies:
+    commander "2"
+    iconv-lite "0.4"
+    rw "1"
+
+d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
 
-d3-format@1:
+d3-force@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
+  dependencies:
+    d3-collection "1"
+    d3-dispatch "1"
+    d3-quadtree "1"
+    d3-timer "1"
+
+d3-format@1, d3-format@1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
 
-d3-interpolate@1, d3-interpolate@^1.1.1:
+d3-geo@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
+  dependencies:
+    d3-array "1"
+
+d3-hierarchy@1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
+
+d3-interpolate@1, d3-interpolate@1.1.6, d3-interpolate@^1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
   dependencies:
     d3-color "1"
 
-d3-path@1:
+d3-path@1, d3-path@1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
 
-d3-scale@^1.0.0:
+d3-polygon@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
+
+d3-quadtree@1, d3-quadtree@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
+
+d3-queue@3.0.7:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
+
+d3-random@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
+
+d3-request@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/d3-request/-/d3-request-1.0.6.tgz#a1044a9ef4ec28c824171c9379fae6d79474b19f"
+  dependencies:
+    d3-collection "1"
+    d3-dispatch "1"
+    d3-dsv "1"
+    xmlhttprequest "1"
+
+d3-scale@1.0.7, d3-scale@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
   dependencies:
@@ -3137,30 +3221,90 @@ d3-scale@^1.0.0:
     d3-time "1"
     d3-time-format "2"
 
-d3-shape@^1.0.0, d3-shape@^1.2.0:
+d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
+
+d3-shape@1.2.0, d3-shape@^1.0.0, d3-shape@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
   dependencies:
     d3-path "1"
 
-d3-time-format@2:
+d3-time-format@2, d3-time-format@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
   dependencies:
     d3-time "1"
 
-d3-time@1:
+d3-time@1, d3-time@1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
 
-d3-timer@^1.0.0:
+d3-timer@1, d3-timer@1.0.7, d3-timer@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
 
-d3-voronoi@^1.1.2:
+d3-transition@1, d3-transition@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
+  dependencies:
+    d3-color "1"
+    d3-dispatch "1"
+    d3-ease "1"
+    d3-interpolate "1"
+    d3-selection "^1.1.0"
+    d3-timer "1"
+
+d3-voronoi@1.1.2, d3-voronoi@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
 
+d3-zoom@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
+d3@^4.13.0:
+  version "4.13.0"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d"
+  dependencies:
+    d3-array "1.2.1"
+    d3-axis "1.0.8"
+    d3-brush "1.0.4"
+    d3-chord "1.0.4"
+    d3-collection "1.0.4"
+    d3-color "1.0.3"
+    d3-dispatch "1.0.3"
+    d3-drag "1.2.1"
+    d3-dsv "1.0.8"
+    d3-ease "1.0.3"
+    d3-force "1.1.0"
+    d3-format "1.2.2"
+    d3-geo "1.9.1"
+    d3-hierarchy "1.1.5"
+    d3-interpolate "1.1.6"
+    d3-path "1.0.5"
+    d3-polygon "1.0.3"
+    d3-quadtree "1.0.3"
+    d3-queue "3.0.7"
+    d3-random "1.1.0"
+    d3-request "1.0.6"
+    d3-scale "1.0.7"
+    d3-selection "1.3.0"
+    d3-shape "1.2.0"
+    d3-time "1.0.8"
+    d3-time-format "2.1.1"
+    d3-timer "1.0.7"
+    d3-transition "1.1.1"
+    d3-voronoi "1.1.2"
+    d3-zoom "1.7.1"
+
 d@1:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@@ -3572,7 +3716,7 @@ electron-builder-lib@~20.2.0:
     semver "^5.5.0"
     temp-file "^3.1.1"
 
-electron-builder@^20.0.4, electron-builder@^20.4.0:
+electron-builder@^20.4.0:
   version "20.4.0"
   resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.4.0.tgz#d1393719339c17dd7c2dd16d58b4e138ca6646ce"
   dependencies:
@@ -3736,7 +3880,7 @@ electron-webpack@1.13.0:
     webpack-merge "^4.1.1"
     yargs "^11.0.0"
 
-electron@1.8.3, electron@^1.8.2:
+electron@1.8.3:
   version "1.8.3"
   resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.3.tgz#001416ea3a25ce594e317cb5531bc41eadd22f7f"
   dependencies:
@@ -5269,7 +5413,7 @@ i18next@^10.5.0:
   version "10.5.0"
   resolved "https://registry.yarnpkg.com/i18next/-/i18next-10.5.0.tgz#a2a90e67774fa85b8ff9cd0063e697e482440de7"
 
-iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.19, iconv-lite@~0.4.13:
+iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.19, iconv-lite@~0.4.13:
   version "0.4.19"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
 
@@ -6326,9 +6470,6 @@ ledger-test-library@KhalilBellakrid/ledger-test-library-nodejs#7d37482:
   dependencies:
     axios "^0.17.1"
     bindings "^1.3.0"
-    electron "^1.8.2"
-    electron-builder "^20.0.4"
-    electron-rebuild "^1.7.3"
     nan "^2.6.2"
     prebuild-install "^2.2.2"
 
@@ -8949,6 +9090,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+
 rx-lite-aggregates@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
@@ -10686,6 +10831,10 @@ xmldom@0.1.x:
   version "0.1.27"
   resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
 
+xmlhttprequest@1:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
+
 xpipe@*:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"