@spider-analyzer/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

Usage no npm install needed!

<script type="module">
  import spiderAnalyzerTimeline from 'https://cdn.skypack.dev/@spider-analyzer/timeline';
</script>

README

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://spider-analyzer.io/home/components/timeline/

alt text

alt text

Content

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.

<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.

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.

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)
yAxis: PropTypes.shape({
    path: PropTypes.string,
    spaceBetweenTicks: PropTypes.number.isRequired,
    showGrid: PropTypes.bool,
})

timeSpan

Defines the start and stop of the selected time window.

timeSpan: PropTypes.shape({
    start: PropTypes.instanceOf(moment).isRequired,
    stop: PropTypes.instanceOf(moment).isRequired
}).isRequired

Ex:

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, 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.

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.

showHistoToolTip: PropTypes.bool

HistoToolTip

React component to replace and customize the histogram tooltips content.

Provided props:

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.

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.

qualityScale: PropTypes.func

zoomOutFactor

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

Should be > 1 ;)

zoomOutFactor: PropTypes.number

domains

Stores/defines the actual zooms levels of the timeline.

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:

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.

maxDomain: PropTypes.shape({
    min: PropTypes.instanceOf(moment).isRequired,
    max: PropTypes.instanceOf(moment).isRequired
})

Ex:

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

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:

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.

biggestVisibleDomain: PropTypes.object //expects a Duration created by moment.duration() object

Ex:

biggestVisibleDomain = moment.duration('P1M')

biggestTimeSpan

Defines the maximum duration that can be selected, if any.

biggestTimeSpan: PropTypes.object //expects a Duration created by moment.duration() object

Ex:

biggestTimeSpan = moment.duration('P1D')

smallestResolution

Defines the smallest zoom resolution to display (for 15 pixels).

smallestResolution: PropTypes.object.isRequired //expects a Duration created by moment.duration() object

Ex:

smallestResolution = moment.duration('PT1M')

labels

Overrides labels to display for ToolTips and onShowMessage calls. Provided for translation.

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:

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

showLegend: PropTypes.bool

tools

Allow deactivating tools. All are present by default.

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.

fetchWhileSliding: PropTypes.bool

selectBarOnClick

Defines if clicking on a bar of the histogram automatically switch the time selection to this bar.

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:

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:

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:

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:

#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