@spider-analyzer/timeline
v5.0.17
Published
React graphical component to display metric over time with a time selection feature.
Maintainers
Readme
TimeLine
React graphical component to display metrics over time with an interactive time-selection cursor, zoom, pan, and an optional quality line.
- Drag & pan to shift time
- Scroll to zoom
- Drag to move, resize or redraw the time selection
- Built-in zoom-out undo (no host-managed stack required)
- Themable through plain CSS classes — no CSS-in-JS peer dep
Live example: https://timeline.oss.spider-analyzer.io

Content
- Features
- Design considerations
- Installation
- Integration example
- Props
- Callbacks
- Imperative API
- Styling
- Migrating from v4
- Testing / Dev
- Dependencies
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. - 1 to n stacked metrics per bar — defined by
metricsDefinition. - Colors of the histogram bars are defined in
metricsDefinition.colors[]. - Metric names are displayed to the left of the chart.
- The maximum value in the visible window is shown at the top of the y-axis.
- Current time is displayed as a vertical arrow on the x-axis.
Quality line
- Optional secondary line below the chart showing a per-slot quality value.
- May have its own x-axis granularity, independent of the histogram.
- Custom tooltip content per slot.
Selecting time
- The cursor has handles to resize it via drag-and-drop.
- If dragged outside the domain, the domain shifts.
- Cursor can be redrawn by clicking and dragging over the chart.
Zooming
- Scroll — zooms on the mouse position (factor 4 on zoom-in).
- Double-click on the cursor — zooms over the selection.
- Zoom-in/out icons on the cursor / x-axis.
- Zoom-out undo — zooming out after zooming in returns to the exact previous view (internal breadcrumb, transparent to the host).
Limits:
- Zoom-in stops at 15 px =
smallestResolution. - Zoom-out stops at
biggestVisibleDomain.
Dragging the domain
Ctrl+ drag shifts the visible window.- Respects
maxDomainif set.
Buttons
- Double arrow icons on either side of the x-axis slide the domain.
- Right-side icons: reset, goto-now.
Data refresh limits
- Zoom is disabled while a fetch is in progress.
- Resize triggers a refresh only every 30 px — avoids flooding the API.
Design considerations
TimeLine is designed for integration into time-series and operational reporting UIs. It pairs naturally with a grid of records, using the cursor to select the time range of interest.
When integrating, you define:
- Metrics to display + their colors and legend:
metricsDefinition. - The initial window: return it from
onLoadDefaultDomain, or pass it directly asdomain. - Optional outer bounds:
maxDomain. - Zoom limits:
biggestVisibleDomain,smallestResolution. - Reset behavior:
onResetTime(typically clearsdomainso the component re-asks for a default). - Formatters for x-axis, tooltips, and metric values.
- Required time zone (IANA name):
timeZone.
Installation
npm install --save @spider-analyzer/timelineImport the stylesheet once in your entry:
import '@spider-analyzer/timeline/dist/timeline.css';
import '@spider-analyzer/timeline/dist/tipDark.css';No moment / MUI / lodash peer deps. luxon is declared as a
dependency but is already on most modern React apps; if not, npm will
pull it in transitively.
Integration example
<TimeLine
className="my-timeline"
timeZone="Europe/Paris" /* required */
timeSpan={this.state.timeSpan} /* { start: Date, stop: Date } */
histo={{
items: this.state.items, /* [{ time: Date, metrics: number[], total: number }] */
intervalMs: this.state.intervalMs, /* number */
}}
showHistoToolTip
quality={{
items: this.state.quality, /* [{ time: Date, quality: 0..1, tip?: ReactNode }] */
intervalMin: this.state.intervalMin,
}}
zoomOutFactor={1.75}
domain={this.state.domain} /* { min: Date, max: Date } | null */
maxDomain={this.state.maxDomain}
metricsDefinition={metricsDefinition}
biggestVisibleDomain={30 * 24 * 60 * 60 * 1000} /* 1 month in ms */
biggestTimeSpan={24 * 60 * 60 * 1000} /* 1 day in ms */
smallestResolution={60 * 1000} /* 1 minute in ms */
labels={{ backwardButtonTip: 'Slide into the past' }}
tools={{ gotoNow: false }}
showLegend
fetchWhileSliding
selectBarOnClick
onLoadDefaultDomain={this.onLoadDefaultDomain} /* returns Domain | Promise<Domain> */
onLoadHisto={this.onLoadHisto} /* ({ intervalMs, start, end }) */
onTimeSpanChange={this.onTimeSpanChange} /* ({ start, stop }) */
onShowMessage={console.log}
onDomainChange={this.onDomainChange} /* (domain) */
onResetTime={this.onResetTime}
onFormatTimeToolTips={this.onFormatTimeToolTips}
onFormatTimeLegend={multiFormat}
onFormatMetricLegend={formatNumber}
/>TimeLine is a controlled component.
Props
| Prop | Type | Required | Notes |
|------------------------|-------------------------------------------------------------------------------------------|----------|--------------------------------------------------------------------------------|
| timeZone | string (IANA) | yes | Threads into the internal tz-aware time engine for formatting. |
| domain | { min: Date, max: Date } \| null | yes | Pass null on first render to defer to onLoadDefaultDomain. |
| timeSpan | { start: Date, stop: Date } | yes | The cursor selection. |
| histo | { items: HistoItem[], intervalMs: number \| null } | yes | items[].time is Date. items and intervalMs MUST be updated atomically. |
| metricsDefinition | { count, legends, colors } | yes | See example below. |
| maxDomain | { min?: Date, max?: Date } | no | Hard outer bounds for pan. |
| biggestVisibleDomain | number (ms) | no | Max visible window; caps zoom-out. |
| biggestTimeSpan | number (ms) | no | Max selectable cursor span. |
| smallestResolution | number (ms) | yes | Floor for zoom-in (15 px == this duration). |
| quality | { items: QualityItem[], intervalMin?: number } | no | Optional quality line below the chart. |
| qualityScale | (q: number) => string | no | Color for a quality value. Defaults to a red→green scale. |
| zoomOutFactor | number | no | Default 1.25. Used only when the internal breadcrumb is empty. |
| showHistoToolTip | boolean | no | |
| HistoToolTip | React component | no | Custom tooltip content — see below. |
| className | string | no | Applied to the outer container. |
| classes | Record<slot, string> | no | Override per-slot class names. See Styling. |
| rcToolTipPrefixCls | string | no | Override rc-tooltip class prefix. |
| margin | { left?, right?, top?, bottom? } numbers | no | |
| xAxis, yAxis | axis config objects (see below) | no | |
| tools | { slideForward?, slideBackward?, resetTimeline?, gotoNow?, cursor?, zoomIn?, zoomOut? } | no | Toggle individual tools. |
| fetchWhileSliding | boolean | no | Refetch while panning. |
| selectBarOnClick | boolean | no | Clicking a histogram bar snaps the cursor to it. |
| labels | Record<string, string> | no | Translations / message overrides. |
| showLegend | boolean | no | Defaults to true. |
xAxis / yAxis:
xAxis?: {
arrowPath?: string;
spaceBetweenTicks: number;
barsBetweenTicks: number;
showGrid?: boolean;
height?: number;
};
yAxis?: {
arrowPath?: string;
spaceBetweenTicks?: number;
showGrid?: boolean;
};metricsDefinition:
{
count: 3,
legends: ['Info', 'Warn', 'Fail'],
colors: [
{ fill: '#9be18c', stroke: '#5db352', text: '#5db352' },
{ fill: '#f6bc62', stroke: '#e69825', text: '#e69825' },
{ fill: '#ff5d5a', stroke: '#f6251e', text: '#f6251e' },
],
}Custom HistoToolTip props:
HistoToolTip.propTypes = {
classes: object,
item: {
start: Date,
end: Date,
x1: number,
x2: number,
metrics: number[],
total: number,
},
metricsDefinition: object,
onFormatTimeToolTips: (time: Date) => ReactNode,
onFormatMetricLegend: (value: number) => string,
};Callbacks
onLoadDefaultDomain(): Domain | Promise<Domain> | void
Called on mount when domain is null. Return the default domain
synchronously or asynchronously; the component seeds its internal stack
from the returned value and emits onDomainChange once resolved.
onLoadHisto({ intervalMs, start, end }): void
Called when the component needs histogram data. Fires on mount (if the
domain is known), on domain changes, on width changes, and on pan if
fetchWhileSliding is set.
onTimeSpanChange({ start, stop }): void
Called when the user resizes, moves, or redraws the cursor.
onDomainChange(domain): void
Called whenever the visible domain changes — zoom in/out, pan, or a
cursor-triggered edge shift. Host updates its domain state.
onResetTime(): void
Called when the user clicks the reset tool. Typical handler: clear the
host domain and let onLoadDefaultDomain re-seed.
onShowMessage(msg: ReactNode): void
Called to display an informational or error message (e.g. "reached max zoom").
onFormatTimeToolTips(time: Date): ReactNode
Formats time for the cursor tooltips. Use your timezone library of
choice — the component passes a raw Date.
// With luxon
onFormatTimeToolTips = (time) =>
DateTime.fromJSDate(time).toFormat('yyyy-LL-dd HH:mm:ss');onFormatTimeLegend(time: Date): string
Formats the x-axis tick. time is rounded to one of: millisecond,
second, minute, hour, day, month, year. Return a different string per
rounded level for readable ticks.
onFormatMetricLegend(value: number): string
Formats a metric amount shown at the top of the y-axis.
Imperative API
Three methods are exposed on the ref:
zoomIn()— zooms to the current cursor selection.zoomOut()— pops the internal breadcrumb; if empty, expands byzoomOutFactor.shiftTimeLine(delta)— animates the domain backward (>0) or forward (<0) bydeltapixels.
const ref = useRef();
<TimeLine ref={ref} ... />
<button onClick={() => ref.current.zoomIn()}>+</button>Styling
The library ships a single stylesheet (dist/timeline.css) whose rules
are keyed on tl-<slot> class names. Every SVG element also receives
any additional class you hand it via the classes prop — so your rules
win via CSS specificity + source order.
/* my-theme.css */
.my-cursorArea { fill: #4347fdff; stroke: #4347fdff; fill-opacity: 0.1; }
.my-cursorLeftHandle,
.my-cursorRightHandle { stroke: #4347fdff; }
.my-zoomIn { fill: #4347fdff; }
.my-verticalAxisText { fill: #949494; }
.my-legend { transform: translateX(-25px); }import './my-theme.css';
<TimeLine
classes={{
cursorArea: 'my-cursorArea',
cursorLeftHandle: 'my-cursorLeftHandle',
cursorRightHandle:'my-cursorRightHandle',
zoomIn: 'my-zoomIn',
verticalAxisText: 'my-verticalAxisText',
legend: 'my-legend',
/* any tl-<slot> in timeline.css is overridable */
}}
...
/>Full list of slots: see src/timeline.css in the repo (or inspect a
rendered timeline — every class starts with tl-). The test/src/App.jsx
demo exercises the full set.
Migrating from v4
See MIGRATION-v5.md for a step-by-step guide. TL;DR:
- moment objects →
Date;moment.Duration→number(ms). domains: Domain[]→domain: Domain | null.onUpdateDomains→onDomainChange;onCustomRange→onTimeSpanChange.onLoadHisto(intervalMs, start, end)→onLoadHisto({intervalMs, start, end}).onLoadDefaultDomain()now RETURNS the default (sync or Promise).- New required
timeZoneprop. @material-ui/stylespeer dep gone; style via theclassesprop pointing at your own CSS class names.
Testing / Dev
git clone https://gitlab.com/TincaTibo/timeline.git
cd timeline
npm install
npm test # Vitest regression suite (12 tests)
npm run build # tsup -> dist/ (ESM + CJS + .d.ts + timeline.css)
npm run typecheck # tsc --noEmitDemo app (Vite, no Docker required):
cd test
make start # dev server on :5000 with HMR, aliased to ../src
make demo # production build + `vite preview`Editing any file under src/ hot-reloads the demo — no package rebuild.
Dependencies
- React ≥ 16.8 (hooks).
- d3-{scale,selection,drag,time}
- luxon (runtime timezone engine — internal; host doesn't use it)
- rc-tooltip
- clsx
- prop-types (dev-time only — types ship via
.d.ts)
Removed in v5: @material-ui/styles, moment-timezone, lodash-es.
