npm package: @spider-analyzer/timeline
Description: React graphical component to display metric over time with a time selection feature.
Author: Thibaut Raballand
Latest version: 4.0.3

# TimeLine

React graphical component to display metric over time with a time selection feature.
- Drag & pan to shift time
- Scroll to zoom time resolution
- Drag to move, resize or redraw time selection
- Fine tuning for intuitive use
- Customizable styling and tools 

Live example: https://timeline.oss.spider-analyzer.io

![alt text][example]

![alt text][styled]

## Content
- [Features](#features)
  * [Displaying metrics](#displaying-metrics)
  * [Quality line](#quality-line)
  * [Selecting time](#selecting-time)
  * [Zooming](#zooming)
  * [Dragging the domain](#dragging-the-domain)
  * [Buttons](#buttons)
  * [Limits when refreshing](#limits-when-refreshing)
- [Design considerations](#design-considerations)
- [Integration](#integration)
- [Props](#props)
  * [className](#classname)
  * [classes](#classes)
  * [rcToolTipPrefixCls](#rctooltipprefixcls)
  * [margin](#margin)
  * [xAxis](#xaxis)
  * [yAxis](#yaxis)
  * [timeSpan](#timespan)
  * [histo](#histo)
  * [showHistoToolTip](#showhistotooltip)
  * [HistoToolTip](#HistoToolTip)
  * [quality](#quality)
  * [qualityScale](#qualityscale)
  * [zoomOutFactor](#zoomoutfactor)
  * [domains](#domains)
  * [maxDomain](#maxdomain)
  * [metricsDefinition](#metricsdefinition)
  * [biggestVisibleDomain](#biggestvisibledomain)
  * [biggestTimeSpan](#biggesttimespan)
  * [smallestResolution](#smallestresolution)
  * [labels](#labels)
  * [showLegend](#showlegend)
  * [tools](#tools)
  * [fetchWhileSliding](#fetchwhilesliding)
  * [selectBarOnClick](#selectbaronclick)
- [Actions](#actions)
  * [onLoadDefaultDomain()](#onloaddefaultdomain)
  * [onLoadHisto(intervalMs: number, start: Moment, end: Moment)](#onloadhistointervalms-number-start-moment-end-moment)
  * [onCustomRange(start: Moment, stop: Moment)](#oncustomrangestart-moment-stop-moment)
  * [onShowMessage(msg: string)](#onshowmessagemsg-string)
  * [onUpdateDomains(domains: arrayOf({min: Moment, max: Moment}))](#onupdatedomainsdomains-arrayofmin-moment-max-moment)
  * [onResetTime()](#onresettime)
  * [onFormatTimeToolTips(time: Moment) :string](#onformattimetooltipstime-moment-string)
  * [onFormatTimeLegend(time: Date) :string](#onformattimelegendtime-date-string)
  * [onFormatMetricLegend(value: number) :string](#onformatmetriclegendvalue-number-string)
- [Public functions](#public-functions)
  * [zoomIn()](#zoomin)
  * [zoomOut()](#zoomout)
  * [shiftTimeLine(number)](#shifttimelinenumber)
- [Testing / Dev](#testing--dev)
- [Dependencies](#dependencies)
- [ToDo](#todo)
- [Trello dashboard](#trello-dashboard)

## Features

### Displaying metrics
- TimeLine displays the evolution of metric(s) through time
- The time displayed has a min and max time, called hereunder a `domain`
- TimeLine can display from 1 to n metrics at once
  - Metrics to be displayed are defined by `metricsDefinition` prop.
  - When several metrics are displayed, their values are stacked up
  - Metrics are stacked in the order of `metricsDefinition.legend[]`
  - Colors of the histogram bars are defined in `metricsDefinition.colors[]`
  - Order of metrics must be the same in `metricsDefinition.legend[]`, `metricsDefinition.colors[]` and `histo.items[].metrics[]`
- Names of the metrics are defined in `metricsDefinition.legend[]`, and displayed left to the chart
- The maximum value in the domain is displayed at the top of the y axis
- Current time is displayed by a vertical arrow on the x axis

### Quality line
- Timeline may display a 'quality' line below the chart showing the quality of the metrics collection
- This line may have a different x-axis granularity as the metrics
- This line takes quality in input by time slot, with the quality being a number, for instance, between 0 and 100%.
- A color scale renders the quality in different color depending on the value
- A custom tip can be displayed for each distinct slot

### Selecting time
A cursor allows to select time in the viewed domain.
- The cursor has handles on its side to resize it by drag and drop
- The cursor is moved backward or forward if the resizing is too small
- The cursor can be moved by drag an drop
  - If moved outside the domain, the domain is adjusted (shifted)
- The cursor displays tooltips to show start and stop of time selection
- The cursor can be re-drawn by click and dragging over the chart
  - When clicking outside the cursor
  - When clicking below the cursor

### Zooming
- TimeLine can be zoomed in (higher time resolution):
  - **Scroll down** with the mouse over the graph.
    - The TimeLine is zoomed on the x position of the mouse, by a factor 4.
  - **Double click** on the time selection cursor.
    - The TimeLine is zoomed over the selected area.
  - **Click** on the zoom-in icon at the top right corner of the time selection cursor.
    - The TimeLine is zoomed over the selected area.
- TimeLine can be zoomed out:
  - **Scroll down** with the mouse over the graph.
  - **Click** on the zoom-in icon at the right corner of the x axis.
    - At start, zooming out is done using `zoomOutFactor`
    - Once zoomed in, zooming out reverts the last zoom level

Limits:
- Zoom-in is possible until 15 pixels = `smallestResolution`
- Zoom-out is possible until visible domain on the timeline = `biggestVisibleDomain`
  
### Dragging the domain
Time domain can be dragged forward or backward by pressing ctrl and dragging the mouse on the chart.
  - If a `maxDomain` limit is defined, the timeline cannot display dates outside this domain
  - The zoom levels above this one are adjusted if the min/max gets beyond their limits. For better U/X.

### Buttons
- On each side of the x axis, the double arrow icons allow sliding the domain forward or backward
- On the right of the chart, two icons allow to:
  - Reset the chart: getting back to initial zoom level.
  - Goto now: Move the domain and time selection to select current time.

### Limits when refreshing
- Zooming is not possible while a data refresh is in progress. To avoid too many calls to the API at once.
- When resizing the TimeLine, it will ask for data refresh only each 30 pixels resize. Thus avoiding many calls to the API.

## Design considerations
TimeLine is design for integration in time series or operational data reporting and display.
It is perfectly suited for integration aside a grid of records to define the time range of records to display. 

When integrating the TimeLine in your project, you have to define:
- The metrics to display, their colors and legend: `metricsDefinition`
- The default domain to display on load: `domains[0]` loaded by `onLoadDefaultDomain`
- The absolute minimum and maximum time to display (optional): `maxDomain`
- The limit in duration of a visible domain (optional): `biggestVisibleDomain`
- The smallest resolution unit of the time displayed: `smallestResolution`
- What to do when resetting time: `onResetTime`.
  - Default expected behavior would be to execute `onLoadDefaultDomain` and reset the domains.
- How to display time:
  - On x axis: `onFormatTimeLegend`
  - In tooltips: `onFormatTimeToolTips`
- How to display metric value on y axis: `onFormatMetricLegend`
- If selected time should be rounded.

Default resolution is millisecond, the time may rounded outside TimeLine component to second, minute or so.
- To do this, adjust `onCustomRange`, `onLoadDefaultDomain`, `onLoadHisto` functions, as shown in demo.
- `smallestResolution` and `onFormatTimeToolTips` should be adjusted in consequence.

You may also redefine labels displayed: `labels`

## Integration
Install using npm or your favorite tool.

`npm install --save @spider-analyzer/timeline`

Include in your react application.

**Warning:** 
Current version requires some props to be Moment.js objects. So you would need Moment in your own application.
You can reduce webpack bundle when using webpack, as timeline lib is not using any locale of Moment.js.
See: https://github.com/jmblog/how-to-optimize-momentjs-with-webpack.


```js
<TimeLine
    className={'timeLine'}
    timeSpan = {this.state.timeSpan}                    //start and stop of selection
    histo = {{
        items: this.state.items,                        // table of histo columns: [{time: moment, metrics: [metric1 value, metric2 value...], total: sum of metrics}, ...]
        intervalMs: this.state.intervalMs               //interval duration of column in ms
    }}
    showHistoToolTip
    quality = {{
        items: this.state.quality,                      //table of quality indicators [{time: moment, quality: float [0,1], tip: string}]
        intervalMin: this.state.intervalMin             //interval duration of column in Min
    }}
    qualityScale = {
        scaleLinear()
            .domain([0,1])
            .range(['red','green'])
            .clamp(true)
    }                                                   //color scale (optional)
    zoomOutFactor = {1.75}                              //zoom out factor if never zoomed in
    domains = {this.state.domains}                      //array of zoom levels
    maxDomain = {this.state.maxDomain}                  //max zoom level allowed
    metricsDefinition = {metricsDefinition}
    biggestVisibleDomain = {moment.duration('P1M')}     //maximum visible duration, cannot zoom out further
    biggestTimeSpan = {moment.duration('P1D')}          //maximum duration that can be selected
    smallestResolution = {moment.duration('PT1M')}      //max zoom level: 15pixels = duration
    labels={{
        backwardButtonTip: 'Slide into the past'
    }}
    tools={{
        gotoNow: false
    }}
    showLegend
    fetchWhileSliding
    selectBarOnClick
    
    onLoadDefaultDomain = {this.onLoadDefaultDomain}    //called on mount to get the default domain
    onLoadHisto = {this.onLoadHisto}                    //called to load items. give the needed interval, computed from props.width, props.domains[0]
    onCustomRange = {this.onCustomRange}                //called when user has drawn or resized the cursor
    onShowMessage = {console.log}                       //called to display an error message
    onUpdateDomains = {this.onUpdateDomains}            //called to save domains
    onResetTime = {this.onResetTime}                    //called when user want to reset timeline
    onFormatTimeToolTips = {this.onFormatTimeToolTips}  //called to display time in tooltips
    onFormatTimeLegend = {multiFormat}                  //called to format x axis legend
    onFormatMetricLegend = {formatNumber}               //called to format y axis metric legend
/>
```

TimeLine component is a controlled component.

## Props
### className
Main class used in the `<div/>` encapsulating the svg graphic.
The width and height of the chart should be defined there. In % or strict dimensions.
When resizing the window, the chart will adapt. 

### classes
Allows to override any classes used by the component.

To be used by a CSS-in-JS solution, are by listing classes from your own CSS.
To find the classes to update... best is to play with developer tools ;)

### rcToolTipPrefixCls
Allows changing the class prefix for rc-tooltip

### margin
Allows to adjust the margin around the graphic area.

The metrics legend, and time legend are rendered inside the margin.

```js
margin: PropTypes.shape({
    left: PropTypes.number,
    right: PropTypes.number,
    top: PropTypes.number,
    bottom: PropTypes.number,
})
```

### xAxis
Allows customising the xAxis (time):
- arrowPath: SVG path of the arrow
- spaceBetweenTicks: How many pixels between two points in the x grid / legend
- barsBetweenTicks: How many histogram bar do you want between ticks
- showGrid: Displays vertical lines for each tick (default: no)
- height: Height of the axis (useful if you change the text style ;))  

spaceBetweenTicks and spaceBetweenTicks are used to compute the resolution 'interval' of the onLoadHisto function. 

```js
xAxis: PropTypes.shape({
    arrowPath: PropTypes.string,
    spaceBetweenTicks: PropTypes.number.isRequired,
    barsBetweenTicks: PropTypes.number.isRequired,
    showGrid: PropTypes.bool,
    height: PropTypes.number,
})
```

### yAxis
Allows customising the yAxis (metrics):
- arrowPath: SVG path of the arrow
- spaceBetweenTicks: How many pixels between two points in the x grid / legend
- showGrid: Displays horizontal lines for each tick (default: no)

```js
yAxis: PropTypes.shape({
    path: PropTypes.string,
    spaceBetweenTicks: PropTypes.number.isRequired,
    showGrid: PropTypes.bool,
})
```

### timeSpan
Defines the start and stop of the selected time window.
```js
timeSpan: PropTypes.shape({
    start: PropTypes.instanceOf(moment).isRequired,
    stop: PropTypes.instanceOf(moment).isRequired
}).isRequired
```
Ex:
```js
timeSpan = {
    start: moment().subtract(1, 'HOUR'),
    stop: moment().add(1, 'HOUR'),
}
```

### histo
Provides the data to display.

/!\ items and intervalMs have to be provided in sync.

When loading new items when processing [onLoadHisto](#onloadhistointervalms-number-start-moment-end-moment), both intervalMs and items must be
given back together. That's why they are in same prop.
If intervalMs is not consistent with items own duration 'interval', then you'll have an ugly
glitch when zooming out and resizing. And wrong information display when zooming in.

So, please do not keep intervalMs in an intermediate state with a closer update loop
than the loading request itself.

```js
histo: PropTypes.shape({
    items: PropTypes.arrayOf(PropTypes.shape({
        time: PropTypes.instanceOf(moment).isRequired, //time of histogram bar
        metrics: PropTypes.arrayOf(PropTypes.number).isRequired, //array of values
        total: PropTypes.number.isRequired, //total of values of the array
    })),
    intervalMs: PropTypes.number //interval of each bar
}).isRequired
```

### showHistoToolTip
When true, timeline will display a tooltip when hover the histogram stacked bars.

The tooltip lists the time slot and the different metrics values for the bar.
It may be customized by providing a custom `HistoToolTip` component.

```js
showHistoToolTip: PropTypes.bool
```

### HistoToolTip
React component to replace and customize the histogram tooltips content.

Provided props:
```js
HistoTooltip.propTypes = {
    classes: PropTypes.object,     // classes you gave in input of TimeLine
    item: PropTypes.shape({        // histogram bar
        start: PropTypes.instanceOf(moment), // start of the bar
        end: PropTypes.instanceOf(moment),   // end of the bar
        x1: PropTypes.number,      // start position in x axis
        x2: PropTypes.number,      // start position in y axis
        metrics: PropTypes.arrayOf(PropTypes.number), // as provided in histo prop
        total: PropTypes.number,   // total of the metrics bar
    }),
    metricsDefinition: PropTypes.shape().isRequired,  // prop you gave
    onFormatTimeToolTips: PropTypes.func.isRequired,  // prop you gave
    onFormatMetricLegend: PropTypes.func.isRequired,  // prop you gave
};
```

### quality
Provides the data to display on the quality line.
```js
quality: PropTypes.shape({
    items: PropTypes.arrayOf(PropTypes.shape({
        time: PropTypes.instanceOf(moment).isRequired, //time of quality slot
        quality: PropTypes.number.isRequired, //quality of the slot
        tip: PropTypes.node, //text to display in tooltip - optional
    })),
    intervalMin: PropTypes.number //duration of each slot (in minutes)
})
```

### qualityScale
Allows to override the color scale for the quality line.
Expects a function converting a quality number into a CSS color.
```js
qualityScale: PropTypes.func
```

### zoomOutFactor
Allows to override the default zoom factor (1.25) for zooming out
when never zoomed in before. 

Should be > 1 ;)

```js
zoomOutFactor: PropTypes.number
```

### domains
Stores/defines the actual zooms levels of the timeline.
```js
domains: PropTypes.arrayOf(PropTypes.shape({
    min: PropTypes.instanceOf(moment).isRequired,
    max: PropTypes.instanceOf(moment).isRequired
})).isRequired
```
- min and max are the visual bounds of the TimeLine
- When zooming in/out, `onUpdateDomains` is called with an update of the domains.
- On mount, the timeline calls `onLoadDefaultDomain` that should be used to define the initial domain.

Ex:
```js
domains = [{
    min: moment().subtract(1, 'WEEK').startOf('DAY'),
    max: moment().endOf('DAY')
}]
```

### maxDomain
May/should specify a maximum domain that will set min and max bounds when shifting the TimeLine.
```js
maxDomain: PropTypes.shape({
    min: PropTypes.instanceOf(moment).isRequired,
    max: PropTypes.instanceOf(moment).isRequired
})
```
Ex:
```js
maxDomain = {
    min: moment().subtract(2, 'MONTHS').startOf('DAY'),
    max: moment().add(1, 'WEEK').endOf('DAY')
}
```

### metricsDefinition
Defines the metrics that will be displayed on the chart: count, legend, formatting
```js
metricsDefinition: PropTypes.shape({
    count: PropTypes.number.isRequired,
    legends: PropTypes.arrayOf(PropTypes.string).isRequired,
    colors: PropTypes.arrayOf(PropTypes.shape({
        fill: PropTypes.string.isRequired,
        stroke: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
    })).isRequired
}).isRequired
```
Ex:
```js
metricsDefinition = {
    count: 3, //Count of metric in the graphic
    legends: ['Info', 'Warn', 'Fail'], //Name of the metrics, in order. Will be displayed left of the chart
    colors: [{ //Colors of the metrics, in order: fill of bar, stroke of bar, text in legend
        fill: '#9be18c',
        stroke: '#5db352',
        text: '#5db352'
    },
    {
        fill: '#f6bc62',
        stroke: '#e69825',
        text: '#e69825'
    },{
        fill: '#ff5d5a',
        stroke: '#f6251e',
        text: '#f6251e'
    }]
}
```

### biggestVisibleDomain
Defines the maximum visible duration of a domain, if any.
For instance, allows set a maxDomain of 1 year, but limit visible histogram to a window of 1 month.
Limits the overloading of the aggregation API.
```js
biggestVisibleDomain: PropTypes.object //expects a Duration created by moment.duration() object
```
Ex:
```js
biggestVisibleDomain = moment.duration('P1M')
``` 

### biggestTimeSpan
Defines the maximum duration that can be selected, if any.
```js
biggestTimeSpan: PropTypes.object //expects a Duration created by moment.duration() object
```
Ex:
```js
biggestTimeSpan = moment.duration('P1D')
``` 


### smallestResolution
Defines the smallest zoom resolution to display (for 15 pixels).
```js
smallestResolution: PropTypes.object.isRequired //expects a Duration created by moment.duration() object
```
Ex:
```js
smallestResolution = moment.duration('PT1M')
``` 

### labels
Overrides labels to display for ToolTips and onShowMessage calls.
Provided for translation.
```js
labels: PropTypes.shape({
    forwardButtonTip: PropTypes.string,
    backwardButtonTip: PropTypes.string,
    resetButtonTip: PropTypes.string,
    gotoNowButtonTip: PropTypes.string,
    doubleClickMaxZoomMsg: PropTypes.string,
    zoomInWithoutChangingSelectionMsg: PropTypes.string,
    zoomSelectionResolutionExtended: PropTypes.string,
    maxSelectionMsg: PropTypes.string,
    scrollMaxZoomMsg: PropTypes.string,
    minZoomMsg: PropTypes.string,
    maxDomainMsg: PropTypes.string,
    minDomainMsg: PropTypes.string,
    gotoCursor: PropTypes.string,
    zoomInLabel: PropTypes.string,
    zoomOutLabel: PropTypes.string,
})
```

Default:
```js
const defaultLabels = {
    forwardButtonTip: 'Slide forward',
    backwardButtonTip: 'Slide backward',
    resetButtonTip: 'Reset time span',
    gotoNowButtonTip: 'Goto Now',
    doubleClickMaxZoomMsg: 'Cannot zoom anymore!',
    zoomInWithoutChangingSelectionMsg: 'Please change time selection before clicking on zoom ;)',
    zoomSelectionResolutionExtended: 'You reached maximum zoom level',
    maxSelectionMsg: 'You reached maximum selection',
    scrollMaxZoomMsg: 'Cannot zoom anymore!',
    minZoomMsg: 'You reached minimum zoom level',
    maxDomainMsg: 'You reached maximum visible time',
    minDomainMsg: 'You reached minimum visible time',
    gotoCursor: 'Goto Cursor',
    zoomInLabel: 'Zoom in',
    zoomOutLabel: 'Zoom out',
};
```

### showLegend
Allow deactivating legend display, left to vertical axis

```js
showLegend: PropTypes.bool
```

### tools
Allow deactivating tools.
All are present by default.

```js
tools: PropTypes.shape({
    slideForward: PropTypes.bool,
    slideBackward: PropTypes.bool,
    resetTimeline: PropTypes.bool,
    gotoNow: PropTypes.bool,
    cursor: PropTypes.bool,
    zoomIn: PropTypes.bool,
    zoomOut: PropTypes.bool,
})
```

### fetchWhileSliding
Defines if timeline should try to refresh data when sliding domain.
May overload the aggregation API.
```js
fetchWhileSliding: PropTypes.bool
```

### selectBarOnClick
Defines if clicking on a bar of the histogram automatically switch the time selection to this bar.
```js
selectBarOnClick: PropTypes.bool
```

## Actions

### onLoadDefaultDomain()
Called on mount to get the default domain.
- Expected to initialize the `domains` prop.
- Usually with a single default domain `[{min: moment, max: moment}]`.

### onLoadHisto(intervalMs: number, start: Moment, end: Moment)
Called to load items. Give the needed interval, computed from props.width and props.domains[0].
- Expected to update the `histo` prop.

**Called on:**
- mount if domain is set
- domain change
- width change
- sliding if `fetchWhileSliding` prop is set

**Parameters:**
- intervalsMs: Number - Milliseconds to use in the aggregation query
- start: Moment - Current domain min
- end: Moment - Current domain max

### onCustomRange(start: Moment, stop: Moment)
Called when user has drawn or resized the cursor.
- Expected to update the `timeSpan` prop.

### onShowMessage(msg: string)
Called to display an error message.

### onUpdateDomains(domains: arrayOf({min: Moment, max: Moment}))
Called to save domains.
- Expected to save the `domains` prop.

### onResetTime()
Called when user want to reset timeline.
- Expected to update the `domains` prop. Usually to a new array with only default domain.
- `domains[0]` is expected to be changed (new object) to trigger a new `onLoadHisto` call. 

### onFormatTimeToolTips(time: Moment) :string
Called to display time in tooltips.
- Must return a formatted date as string.

Ex:
```js
onFormatTimeToolTips = (time) => {
    return moment(time).second(0).millisecond(0).format(TIME_FORMAT_TOOLTIP);
};
```

### onFormatTimeLegend(time: Date) :string
Called to format the x axis legend. Depending of zoom resolution, the input will be a date rounded to:
- millisecond
- second
- minute
- hour
- day
- month
- year


- Must return a formatted date as string.
- Result should be different for each rounded date.

Example:
```js
import {timeFormat, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3';

const formatMillisecond = timeFormat('.%L'), // .456
    formatSecond = timeFormat(':%S'),        // :43
    formatMinute = timeFormat('%H:%M'),      // 13:12
    formatHour = timeFormat('%H:00'),        // 13:00
    formatDay = timeFormat('%b %d'),         // Nov 02
    formatMonth = timeFormat('%b %d'),       // Nov 01
    formatYear = timeFormat('%Y %b %d')      // 2017 Nov 01
;

const onFormatTimeLegend = (date) => { 
    return (timeSecond(date) < date ? formatMillisecond
        : timeMinute(date) < date ? formatSecond
            : timeHour(date) < date ? formatMinute
                : timeDay(date) < date ? formatHour
                    : timeMonth(date) < date ? formatDay
                        : timeYear(date) < date ? formatMonth
                            : formatYear)(date);
};
``` 

### onFormatMetricLegend(value: number) :string
Called to format metric amount value to display on the top of y axis.

Example:
```js
import {formatLocale } from 'd3';

const locale = formatLocale({
    decimal: '.',
    thousands: ' ',
    grouping: [3],
});

const onFormatMetricLegend = (number) => {
    return locale.format(`,d`)(number);
};
```

## Public functions
Three functions are exposed on the mounted component (through React Ref):
Other functions are supposed to be private.

### zoomIn()
- Zooms the time scale over the selected time frame.
- A message will be sent if zooming is not possible any more (resolution...).

This can be used to externalize ZoomIn button behavior in your app and own U/X.

### zoomOut()
- Zooms out the time scale to previous zoom level or from `zoomOutFactor`.
- A message will be sent if zooming out is not possible any more because of `biggestVisibleDomain`.

This can be used to externalize ZoomOut button behavior in your app and own U/X.

### shiftTimeLine(number)
- Moves gradually the timeline backward (>0) or forward(<0)

This can be used to externalize sliding buttons behavior in your app and own U/X.


## Testing / Dev
You may run the demo in hot reloading mode:
```bash
#clone the repo
git clone https://gitlab.com/TincaTibo/timeline.git
cd timeline/test

#make a docker image with a demo app
make image (requires docker and npm)
```

Then, run the demo in prod mode
```
# run the demo in prod mode
make demo
```

Or in dev mode
```
# run the demo in dev mode (volume mount the dev files for hot reloading)
make start
```
To access it, go to http://localhost:5000 in your browser

## Dependencies
- React 16
- D3.js
- moment.js
- lodash
- rc-tooltip

## ToDo
- Make input agnostic to moment.js

## Trello dashboard
[Timeline React Component](https://trello.com/b/DxjkiuFB/timeline-react-component)



[example]: https://gitlab.com/TincaTibo/timeline/raw/master/img/TimeLine.png "Example"
[styled]: https://gitlab.com/TincaTibo/timeline/-/raw/master/img/Styled-Timeline.png "Styled example"