@spider-analyzer/timeline
v4.0.3
Published
React graphical component to display metric over time with a time selection feature.
Downloads
58
Maintainers
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://timeline.oss.spider-analyzer.io
Content
- Features
- Design considerations
- Integration
- Props
- Actions
- onLoadDefaultDomain()
- onLoadHisto(intervalMs: number, start: Moment, end: Moment)
- onCustomRange(start: Moment, stop: Moment)
- onShowMessage(msg: string)
- onUpdateDomains(domains: arrayOf({min: Moment, max: Moment}))
- onResetTime()
- onFormatTimeToolTips(time: Moment) :string
- onFormatTimeLegend(time: Date) :string
- onFormatMetricLegend(value: number) :string
- Public functions
- Testing / Dev
- Dependencies
- ToDo
- 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[]
andhisto.items[].metrics[]
- Metrics to be displayed are defined by
- 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.
- Scroll down with the mouse over the graph.
- 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
- At start, zooming out is done using
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 byonLoadDefaultDomain
- 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.
- Default expected behavior would be to execute
- How to display time:
- On x axis:
onFormatTimeLegend
- In tooltips:
onFormatTimeToolTips
- On x axis:
- 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
andonFormatTimeToolTips
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 newonLoadHisto
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