@spider-analyzer/timeline

React graphical component to display metric over time with a time selection feature.


Keywords
react, timeline, horizontal, spider-analyzer, react-component, d3, data analysis, time histogram, time selection
License
MIT
Install
npm install @spider-analyzer/timeline@3.4.0

Documentation

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

Live example: https://spider-analyzer.io/home/components/timeline/

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.
      • The previous zoom level is used.

Limits:

  • Zoom-in is possible until 15 pixels = smallestResolution
  • Zoom-out is limited to initial domain (cannot zoom out at start)

Dragging the domain

Time domain can be dragged forward or backward by pressing ctrl and dragging the mouse on the chart.

  • When on the initial zoom level, the domain is widened
    • If a biggestVisibleDomain duration is defined, once the visible domain reach this maximum visible duration, it is shifted forward or backward rather than widened further.
    • If a maxDomain limit is defined, the timeline cannot display dates outside this domain
  • When zoomed in, the current domain is shifted, moving both min and max time.
    • The zoom levels above this one are adjusted if the min/max gets beyond their limits. For better U/X.

Tools

  • 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
    style={{
        width: '100%',
        height: '100%',
        backgroundColor: 'white',
    }} //merged into main DOM node style
    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
    }}
    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)
    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={{
        backwardButtonTips: {
            slideBackward: 'Slide into the past',
        }
    }}
    tools={{
        gotoNow: false
    }}
    fetchWhileSliding
    
    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

style

CSS style object, merged in top level <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.

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.

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

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

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({
    forwardButtonTips: PropTypes.shape({
        extendForward: PropTypes.string,
        slideForward: PropTypes.string,
    }),
    backwardButtonTips: PropTypes.shape({
        extendBackward: PropTypes.string,
        slideBackward: PropTypes.string,
    }),
    resetButtonTip: PropTypes.string,
    gotoNowButtonTip: PropTypes.string,
    doubleClickMaxZoomMsg: PropTypes.string,
    scrollMaxZoomMsg: PropTypes.string,
    zoomInWithoutChangingSelectionMsg: PropTypes.string,
    zoomSelectionResolutionExtended: PropTypes.string,
    maxSelectionMsg: PropTypes.string,
})

Default:

const defaultLabels = {
    forwardButtonTips: {
        extendForward: 'Extend forward',
        slideForward: 'Slide forward',
    },
    backwardButtonTips: {
        extendBackward: 'Extend backward',
        slideBackward: 'Slide backward',
    },
    resetButtonTip: 'Reset time span',
    gotoNowButtonTip: 'Goto Now',
    doubleClickMaxZoomMsg: 'Cannot zoom anymore!',
    scrollMaxZoomMsg: 'Cannot zoom anymore!',
    zoomInWithoutChangingSelectionMsg: 'Please change time selection before clicking on zoom ;)',
    zoomSelectionResolutionExtended: 'You reached maximum zoom level',
    maxSelectionMsg: 'You reached maximum selection',
    gotoCursor: 'Goto Cursor',
    zoomInLabel: 'Zoom in',
    zoomOutLabel: 'Zoom out',
};

tools

Allow disactivating tools. All are present by default.

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

fetchWhileSliding

Defines if timeline should try to refresh data when sliding domain. May overload the aggregation API.

fetchWhileSliding: 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);
};

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

  • Extract styles

Trello dashboard

Timeline React Component