ml-time-graph 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILLS.md ADDED
@@ -0,0 +1,183 @@
1
+ # SKILLS.md — Agent Quick Start for `ml-time-graph`
2
+
3
+ MLTimeGraph is a TypeScript library for visualizing any time-series measurement data (telemetry, physical sensors, system metrics, logs, finance, laboratory or process measurements). Its standout capability is rich contextual annotation — turning raw curves into explanatory records with free-form annotations (arrows, labels, callout rects in data coordinates), time-range highlights, status/phase bands, and threshold zones. It has a single defining architectural property: it generates SVG as a pure string without touching the DOM — the exact same call runs identically in the browser and on the server.
4
+
5
+ For the exhaustive reference, see [USAGE.md](USAGE.md) next to this file.
6
+
7
+ ## Shortest Path to a Visible Chart
8
+
9
+ ```ts
10
+ import { mount } from 'ml-time-graph';
11
+
12
+ mount('#chart', {
13
+ width: 800,
14
+ height: 400,
15
+ series: [
16
+ {
17
+ name: 'Temperature',
18
+ data: [
19
+ { time: 1704067200000, value: 21.5 },
20
+ { time: 1704070800000, value: 22.8 },
21
+ { time: 1704074400000, value: 20.3 },
22
+ ],
23
+ style: { line: { color: '#ef4444' } },
24
+ },
25
+ ],
26
+ });
27
+ ```
28
+
29
+ ## When to Use What
30
+
31
+ - `mount(target, options)`: The declarative browser shortcut when a DOM element or selector exists — measures container width, renders SVG, and injects markup in one call.
32
+ - `new MLTimeGraph(options)`: The core headless engine when working with options objects directly — use for Node.js SSR, headless testing, or custom render pipelines.
33
+ - Builders (`GraphBuilder`, etc.): The fluent, imperative chaining API — use when assembling charts dynamically across conditional branches.
34
+ - Go port (`go-time-graph`): The 1:1 server-side Go implementation (`gitlab.com/mlc0911/mlctimegraph/go-time-graph`) — use when rendering charts in Go backends or microservices without a Node runtime.
35
+
36
+ ## Package Entry Points
37
+
38
+ - `ml-time-graph`: Core chart rendering API — `mount`, `MLTimeGraph`, `SVGRenderer`, `GraphBuilder`, `attachTooltip`, types, and legend helpers (`resolveDash`, `drawMarker`).
39
+ - `ml-time-graph/analyze`: Pure-math statistics companion — `aggregateBySlot`, `downsample` (LTTB), `detectGaps`, `rollingMkt`, `rollingStdDev`, `computeLimitExcursions`, `StatsAggregator`.
40
+ - `ml-time-graph/interaction`: Headless interaction primitives — `Zoom`, `Minimap`, and coordinate-based interaction state management.
41
+ - `ml-time-graph/internals`: Low-level building blocks for custom renderers — `DrawCommand` model, base `Renderer`, layout engine, scales, and axis renderers.
42
+
43
+ ## Common Tasks
44
+
45
+ ### Multiple Series & Multi-Axis
46
+ Map series to vertical axes via `yAxisIndex`:
47
+ ```ts
48
+ mount('#chart', {
49
+ margin: { top: 20, right: 80, bottom: 40, left: 60 },
50
+ series: [
51
+ { name: 'Temp', data: tempData, yAxisIndex: 0, style: { line: { color: '#ef4444' } } },
52
+ { name: 'Humidity', data: humData, yAxisIndex: 1, style: { line: { color: '#3b82f6', style: 'dashed' } } },
53
+ ],
54
+ axes: {
55
+ left: { label: 'Temperature (°C)' },
56
+ right: { label: 'Humidity (%)' },
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### Thresholds & Zone Colouring
62
+ Define thresholds once and reference them by name to colour lines or zones:
63
+ ```ts
64
+ mount('#chart', {
65
+ thresholds: [
66
+ { name: 'warn', value: 25, color: '#f59e0b', line: 'dashed', label: 'Warning' },
67
+ { name: 'crit', value: 30, color: '#ef4444', line: 'solid', label: 'Critical' },
68
+ ],
69
+ series: [{ name: 'Temp', data: tempData, colorByThresholds: ['warn', 'crit'] }],
70
+ });
71
+ ```
72
+
73
+ ### Filled Areas Between Curve and Threshold
74
+ Use `style.fill.regions` with bounds (`from`, `to`, `side`, optional `hatch`):
75
+ ```ts
76
+ style: {
77
+ fill: {
78
+ regions: [
79
+ { from: { threshold: 'warn' }, to: 'series', side: 'above', fill: { color: '#f59e0b33', hatch: 'classic-diagonal' } },
80
+ { from: { threshold: 'crit' }, to: 'series', side: 'above', fill: '#ef444444' },
81
+ ],
82
+ },
83
+ }
84
+ ```
85
+
86
+ ### Annotations & Region Highlights
87
+ Enrich charts with domain context: vertical time-span highlights across the plot, free-form annotations in data coordinates (which automatically scale through zoom/resize), and status bands below:
88
+ ```ts
89
+ mount('#chart', {
90
+ series,
91
+ // Vertical time-range highlight behind the data:
92
+ highlights: [
93
+ { startTime: t1, endTime: t2, label: 'Calibration Phase', color: '#3b82f6', opacity: 0.15 },
94
+ ],
95
+ // Overlays anchored in data coordinates (time/value) tracking zoom & resize:
96
+ annotations: [
97
+ { type: 'arrow', from: { time: t1, value: 20 }, to: { time: t2, value: 35 }, color: '#ef4444' },
98
+ { type: 'label', at: { time: t2, value: 35 }, text: 'Pressure Spike', dy: -10 },
99
+ { type: 'rect', from: { time: t1, value: 15 }, to: { time: t2, value: 25 }, fill: '#10b98122' },
100
+ ],
101
+ // Horizontal status/phase bands below the plot (sharing the time axis):
102
+ annotationBands: [
103
+ {
104
+ name: 'Operating State',
105
+ height: 20,
106
+ items: [
107
+ { startTime: t1, endTime: t2, label: 'Running', fill: '#22c55e44' },
108
+ { startTime: t2, endTime: t3, label: 'Standby', fill: '#eab30844' },
109
+ ],
110
+ },
111
+ ],
112
+ });
113
+ ```
114
+
115
+ ### Data Gaps
116
+ `value: null` marks missing data and breaks the line. Connect with a bridge or render gap spans:
117
+ ```ts
118
+ series: [{
119
+ name: 'Sensor',
120
+ data: [{ time: 1704067200000, value: 20 }, { time: 1704070800000, value: null }, { time: 1704074400000, value: 25 }],
121
+ style: {
122
+ line: { color: '#3b82f6' },
123
+ gap: { display: 'bridge_line', bridge: { color: '#94a3b8', width: 1.5, style: 'dotted' } },
124
+ },
125
+ }]
126
+ ```
127
+
128
+ ### Markers
129
+ Render sample markers on curves or standalone timestamp event lines:
130
+ ```ts
131
+ series: [{
132
+ name: 'Sensor',
133
+ data: sensorData,
134
+ style: { markers: { type: 'circle', size: 3, stroke: '#3b82f6', fill: '#ffffff', threshold: 100 } },
135
+ }],
136
+ markers: [
137
+ { time: 1704070800000, value: 25, label: 'Peak event', color: '#ef4444', lineStyle: 'to-value' },
138
+ ]
139
+ ```
140
+
141
+ ### Axes & Margins
142
+ The layout engine cannot measure rendered text. Always allocate sufficient margins for labels:
143
+ ```ts
144
+ margin: { top: 20, right: 80, bottom: 40, left: 60 },
145
+ axes: {
146
+ x: { label: 'Time', format: (d: Date) => d.toISOString().slice(11, 16) },
147
+ left: { label: 'Temperature (°C)', format: (v: number) => `${v.toFixed(1)} °C` },
148
+ right: { label: 'Humidity (%)', format: (v: number) => `${v.toFixed(0)} %` },
149
+ }
150
+ ```
151
+
152
+ ### Tooltips & Interactive Projections
153
+ Attach DOM tooltips or use pixel/data coordinate projections:
154
+ ```ts
155
+ // Built-in DOM tooltip:
156
+ const chart = mount('#chart', { series, tooltip: { show: true } });
157
+
158
+ // Interactive coordinate projections (crosshairs, zoom, scrubbers):
159
+ chart.renderCommands();
160
+ const timeMs = chart.invertTime(pixelX);
161
+ const value = chart.invertValue(pixelY, yAxisIndex);
162
+ const { x, y } = chart.project(timeMs, value, yAxisIndex);
163
+ ```
164
+
165
+ ### Server-Side Rendering (Node.js)
166
+ Generate pure SVG strings without browser dependencies:
167
+ ```ts
168
+ import { MLTimeGraph, SVGRenderer } from 'ml-time-graph';
169
+ import * as fs from 'node:fs';
170
+
171
+ const chart = new MLTimeGraph({ width: 800, height: 400, series });
172
+ const { content } = new SVGRenderer().render(chart.renderCommands());
173
+ fs.writeFileSync('chart.svg', content);
174
+ ```
175
+
176
+ ## Traps (Read Before Coding)
177
+
178
+ - **Timestamps are numbers, not Date objects:** `time` is always milliseconds since Unix epoch (`number`). Passing a `Date` object fails typechecking or evaluates to `NaN`.
179
+ - **Labels are not measured (fixed margins):** The renderer has no DOM and cannot compute text bounding boxes. Margins default to `{ top: 20, right: 20, bottom: 40, left: 60 }`. If you add a right Y-axis or formatted units (e.g. `120.5 °C`), `right: 20` will clip the labels against the SVG edge. A clipped number is a wrong number, not an incomplete one (`120` clipped to `20`). Always set `margin.right` explicitly (e.g. 70–90 px) when using right axes.
180
+ - **`resolveDash()` vs `getLineStyle()`:** `dashed` and `dotted` predate the scalable dash table and keep fixed SVG patterns (`"4,4"` and `"2,4"`) in the renderer for backwards compatibility. `getLineStyle()` returns the scalable table (`"6, 4"` for width 2). Anyone rendering custom legend swatches must call `resolveDash(variant, width)` so the swatch matches what is actually drawn.
181
+ - **Marker suppression at threshold:** `style.markers.threshold` (and default `theme.pointThreshold = 100`) hides point markers when series sample count exceeds it to prevent visual crowding. Crucially, `chart.legendItems()` also omits the `marker` property when points are suppressed, because a symbol in the legend missing from the curve states something untrue.
182
+ - **Built-in legend is static SVG:** Built-in legends (`inside-right`, etc.) are non-interactive SVG elements. For interactive series toggling or HTML layout below the chart, use `legend: { position: 'separate' }` and render HTML buttons from `chart.legendItems()` using `drawMarker()` and `SVGRenderer` or `resolveDash()`.
183
+ - **Hatch lines adapt to fill colour:** Hatch patterns inherit the opaque colour of the area they belong to (`opaqueColor(fill)`), while the backdrop stays translucent. An earlier default drew fixed blue lines (`#4D88FF`), which falsely signaled an extra state on incident charts where colour is semantic.
package/USAGE.md CHANGED
@@ -174,7 +174,67 @@ legend: {
174
174
  ```
175
175
 
176
176
  `outside-*` reserves margin space (the plot shrinks). `'separate'` draws **no** legend
177
- in the SVG — call `chart.legendItems()` (`{ name, color }[]`) and render your own HTML.
177
+ in the SVG — call `chart.legendItems()` and render your own.
178
+
179
+ ### Drawing your own legend
180
+
181
+ The built-in legend sits left or right inside the SVG and does nothing when clicked.
182
+ Most applications want neither: a legend below the chart, in HTML, where a click hides
183
+ a series. That is what `'separate'` is for.
184
+
185
+ `legendItems()` returns what a swatch needs:
186
+
187
+ ```ts
188
+ interface LegendItem {
189
+ name: string;
190
+ color: string;
191
+ line?: LineVariant; // 'dashed', 'dotted', … — undefined if the series has several lines
192
+ marker?: Exclude<PointStyleType, 'none'>; // only when the chart actually draws markers
193
+ }
194
+ ```
195
+
196
+ Draw the swatch through the same renderer the chart uses, so it cannot drift from the
197
+ curve it names — a 22×10 line with the series' dashes and, if there is one, its marker:
198
+
199
+ ```ts
200
+ import { drawMarker, SVGRenderer, type DrawCommand, type LegendItem } from 'ml-time-graph';
201
+
202
+ function swatch(item: LegendItem): string {
203
+ const commands: DrawCommand[] = [
204
+ { type: 'line', x1: 1, y1: 5, x2: 21, y2: 5, stroke: item.color, strokeWidth: 2, dash: item.line },
205
+ ];
206
+ if (item.marker) {
207
+ drawMarker(commands, undefined, item.marker, 11, 5, 3, item.color, item.color, 1.5);
208
+ }
209
+ return new SVGRenderer({ width: 22, height: 10 }).render(commands).content;
210
+ }
211
+ ```
212
+
213
+ If you build the swatch in HTML or CSS instead of SVG, use **`resolveDash(variant, width)`**
214
+ — it returns `{ strokeDasharray, strokeLinecap? }`, or `null` for a solid line, and it is the
215
+ same function the renderer uses.
216
+
217
+ > Use `resolveDash`, not `getLineStyle`, for anything that has to match a drawn line.
218
+ > `dashed` and `dotted` predate the variant table and keep their historical patterns
219
+ > (`"4,4"` and `"2,4"`, not scaled by stroke width) so that existing charts did not change
220
+ > when the other variants arrived. `getLineStyle` is the table itself and answers `"6, 4"`
221
+ > for `dashed` — correct as a table, wrong as a description of the curve. The difference is
222
+ > nearly invisible and therefore worth knowing: a swatch dashed 6-on-4 beside a curve
223
+ > dashed 4-on-4.
224
+
225
+ **Why the swatch should not be a coloured block.** As long as series differ only by
226
+ colour, a block is enough. The moment they differ by line style or marker — which is
227
+ what you do when a chart is printed, or read by someone who cannot separate red from
228
+ green — a block says nothing: on a black-and-white page every block is the same grey,
229
+ and the legend is useless exactly where it is needed most, as part of a record.
230
+
231
+ For the same reason `marker` is absent when the chart does not draw one. A series with
232
+ `markers.threshold` hides its markers above that many samples; a symbol in the legend
233
+ that is missing from the curve is a false statement, and on paper nobody can check.
234
+
235
+ > `line` is undefined for a series drawn with **several** lines (`style.line` as an
236
+ > array): there is no single answer, and the first one presented as the truth would be
237
+ > a guess.
178
238
 
179
239
  ## Axes
180
240
 
@@ -470,7 +530,7 @@ renders the same JSON config to identical SVG.
470
530
 
471
531
  | Import | Contents |
472
532
  | :--- | :--- |
473
- | `ml-time-graph` | The rendering API — `mount`, `MLTimeGraph`, `SVGRenderer`, `attachTooltip`, and all option/data types. |
533
+ | `ml-time-graph` | The rendering API — `mount`, `MLTimeGraph`, `SVGRenderer`, `attachTooltip`, the legend helpers `getLineStyle` / `drawMarker` / `getHatch`, and all option/data types. |
474
534
  | `ml-time-graph/analyze` | Statistics companion — `aggregateBySlot`, `downsample`, `detectGaps`, `mkt` / `rollingMkt`, `stdDev`, `StatsAggregator`, … (see [Aggregation](#aggregation--helpers)). |
475
535
  | `ml-time-graph/interaction` | Optional interaction primitives — `Zoom`, `Minimap`, `Tooltip` (lower-level than `attachTooltip`). |
476
536
  | `ml-time-graph/internals` | Building blocks for **custom renderers** — the abstract `Renderer`, free render-functions, scales, axis classes, the `DrawCommand` model. |
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { L as LegendOrientation, R as Renderer, M as Marker, a as LegendItem, b as LayoutResult, c as RenderOutput } from './layout-3caueyKS.js';
2
- export { T as Theme, g as getDefaultTheme, r as resetDefaultTheme, s as setDefaultTheme } from './layout-3caueyKS.js';
3
- import { L as LineVariant, A as AnySeries, T as Threshold, H as Highlight, G as Gap, a as GapsConfig, b as Annotation, c as AnnotationBandConfig, D as DrawCommand, d as TimeScale, e as LinearScale, S as SeriesStyle, F as FillSpec, f as TimeSeries, g as HatchVariant, h as ThresholdLabelPosition, P as PointStyleType, i as AnnotationBandItem, j as AggregatedPoint, k as AggregatedSeries, l as DataPoint } from './scale-CWAa8-uO.js';
4
- export { m as AggregationConfig, n as AggregationMode, o as DataPointRef, E as EnumMap, p as FillBound, q as FillDirectionType, r as FillRegion, s as FillSide, t as FillStyle, u as GapConfig, v as GapLabel, w as GapRegion, x as GapStyle, y as LineDashStyle, z as LineStyle, M as MarkerStyle, B as SensorType, C as SeriesOverlay, I as SeriesOverlayKind, J as SeriesType, K as ShadowStyle, N as ThresholdLabelConfig } from './scale-CWAa8-uO.js';
1
+ import { L as LegendOrientation, R as Renderer, M as Marker, a as LegendItem, b as LayoutResult, c as RenderOutput } from './series_renderer-BxGvCHMY.js';
2
+ export { T as Theme, d as drawMarker, g as getDefaultTheme, r as resetDefaultTheme, s as setDefaultTheme } from './series_renderer-BxGvCHMY.js';
3
+ import { L as LineVariant, A as AnySeries, T as Threshold, H as Highlight, G as Gap, a as GapsConfig, b as Annotation, c as AnnotationBandConfig, D as DrawCommand, d as TimeScale, e as LinearScale, S as SeriesStyle, F as FillSpec, f as TimeSeries, g as HatchVariant, h as ThresholdLabelPosition, P as PointStyleType, i as AnnotationBandItem, j as AggregatedPoint, k as AggregatedSeries, l as DataPoint } from './scale-ByGpczxS.js';
4
+ export { m as AggregationConfig, n as AggregationMode, o as DataPointRef, E as EnumMap, p as FillBound, q as FillDirectionType, r as FillRegion, s as FillSide, t as FillStyle, u as GapConfig, v as GapLabel, w as GapRegion, x as GapStyle, y as LineDashStyle, z as LineStyle, M as MarkerStyle, B as SVGLineStyle, C as SensorType, I as SeriesOverlay, J as SeriesOverlayKind, K as SeriesType, N as ShadowStyle, O as ThresholdLabelConfig, Q as getLineStyle, R as resolveDash } from './scale-ByGpczxS.js';
5
5
 
6
6
  /*!
7
7
  * MLTimeGraph — Copyright (c) 2026 Michael Lechner
@@ -844,4 +844,4 @@ declare function parseSeries(obj: unknown): TimeSeries | DataPoint[];
844
844
  /** Parse aggregated series (AggregatedPoint[] — min/max/avg per slot) */
845
845
  declare function parseAggregated(obj: unknown): AggregatedSeries;
846
846
 
847
- export { AggregatedPoint, AggregatedSeries, Annotation, AnnotationBandBuilder, AnnotationBandConfig, AnnotationBandItem, AnnotationBandItemBuilder, AnnotationBuilder, AnySeries, type AxesConfig, type AxisLabelsStyle, type AxisStyle, DataPoint, type FillBetweenThresholdsInput, FillSpec, Gap, GapsConfig, GraphBuilder, type GridLineStyle, type GridStyle, HatchVariant, Highlight, HighlightBuilder, type LegendOptions, type LegendPosition, type LimitStatsPoint, MLTimeGraph, type MLTimeGraphOptions, type Margin, Marker, MarkerBuilder, type MktPoint, PointStyleType, SVGRenderer, SeriesStyle, type StatsAggregatedPoint, type StdDevPoint, Threshold, ThresholdBuilder, ThresholdLabelPosition, type TickConfig, TimeSeries, TimeSeriesBuilder, type TooltipOptions, type TooltipSample, type XAxisConfig, type YAxisConfig, attachTooltip, fillBetweenThresholds, mount, parseAggregated, parseDataPoint, parseSeries };
847
+ export { AggregatedPoint, AggregatedSeries, Annotation, AnnotationBandBuilder, AnnotationBandConfig, AnnotationBandItem, AnnotationBandItemBuilder, AnnotationBuilder, AnySeries, type AxesConfig, type AxisLabelsStyle, type AxisStyle, DataPoint, DrawCommand, type FillBetweenThresholdsInput, FillSpec, Gap, GapsConfig, GraphBuilder, type GridLineStyle, type GridStyle, HatchVariant, Highlight, HighlightBuilder, LegendItem, type LegendOptions, LegendOrientation, type LegendPosition, type LimitStatsPoint, LineVariant, MLTimeGraph, type MLTimeGraphOptions, type Margin, Marker, MarkerBuilder, type MktPoint, PointStyleType, SVGRenderer, SeriesStyle, type StatsAggregatedPoint, type StdDevPoint, Threshold, ThresholdBuilder, ThresholdLabelPosition, type TickConfig, TimeSeries, TimeSeriesBuilder, type TooltipOptions, type TooltipSample, type XAxisConfig, type YAxisConfig, attachTooltip, fillBetweenThresholds, mount, parseAggregated, parseDataPoint, parseSeries };