ml-time-graph 1.0.0 → 1.0.5

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/API_DESIGN.md ADDED
@@ -0,0 +1,1211 @@
1
+ # MLTimeGraph — API-Referenz (v1.0.0)
2
+
3
+ *Stand: v1.0.0 — erste npm-Veröffentlichung nach dem Cleanup. Eine API-Form, keine `@deprecated`-Pfade.*
4
+
5
+ Dieses Dokument ist die **API-Referenz für die aktuelle Library-Form**: nested
6
+ `SeriesStyle`-System, strukturiertes `FillSpec`-Modell (inkl. `outer`-Gate für
7
+ klassifizierte Zonen), Statistik-Overlays (`movingMkt` / `movingAverage` /
8
+ `stdDevBand` / `limits`), First-Class-Achsen, `GapsConfig` mit Auto-Detect +
9
+ `bridge_line`-Mode, Theme-Runtime, sowie die Subpath-Module
10
+ `ml-time-graph/analyze` und `ml-time-graph/interaction`.
11
+
12
+ Praktische Beispiele leben in [USAGE.md](USAGE.md) und in der Demo-Galerie
13
+ unter [`demos/`](demos/). Die Migrations-Historie ist als **Appendix** am
14
+ Ende dieses Dokuments — als Begründungs-Kontext für Designentscheidungen.
15
+
16
+ ## Inhalt
17
+
18
+ 1. [Ziel der Library](#1-ziel-der-library)
19
+ 2. [Design-Prinzipien](#2-design-prinzipien)
20
+ 3. [Daten-Vertrag](#3-daten-vertrag)
21
+ 4. [Style-System (Serie)](#4-style-system-serie)
22
+ 5. [Fill-Modell](#5-fill-modell)
23
+ 6. [Achsen + Grid](#6-achsen--grid)
24
+ 7. [Gaps](#7-gaps)
25
+ 8. [Thresholds](#8-thresholds)
26
+ 9. [Annotationen (Marker, Highlights, Overlays, Bands)](#9-annotationen-marker-highlights-overlays-bands)
27
+ 10. [Statistik-Overlays (Moving Avg / MKT / Limits)](#10-statistik-overlays-moving-avg--mkt--limits)
28
+ 11. [Theme + Patterns (Modul-Struktur)](#11-theme--patterns-modul-struktur)
29
+ 12. [MLTimeGraphOptions — Top-Level-Form](#12-mltimegraphoptions--top-level-form)
30
+
31
+ **Appendix:**
32
+
33
+ A. [Migrations-Historie (v0.2 → v0.3 → Cleanup)](#a-migrations-historie-v02--v03--cleanup)
34
+ B. [Polish-Backlog (post-v0.3)](#b-polish-backlog-post-v03)
35
+ C. [Aufgelöste Design-Entscheidungen](#c-aufgelöste-design-entscheidungen)
36
+
37
+ ---
38
+
39
+ ## 1. Ziel der Library
40
+
41
+ > **MLTimeGraph visualisiert Sensor-Messwerte über die Zeit.**
42
+ >
43
+ > - Mehrere Serien pro Chart, optional zwei Y-Achsen (links / rechts).
44
+ > - Beliebige Kombination aus **Linie, Fläche, Punkten, Step** pro Serie —
45
+ > inkl. mehrerer übereinandergelegter Linien (z. B. Step + Catmull-Smooth)
46
+ > für dieselben Daten.
47
+ > - Schwerpunkt: **Datenanalyse mit reichen Annotationen** (Marker, Highlights,
48
+ > Pfeile/Labels/Boxen in Datenkoordinaten, horizontale Annotation-Bänder).
49
+ > - **Statistik-Overlays** auf Roh-Daten: Moving Average, Moving Mean Kinetic
50
+ > Temperature, Standardabweichungs-Band, Limit-Überschreitungen.
51
+ > - **Hatching auf jeder gefüllten Fläche** (Fill-to-Threshold, Zonen-Fill,
52
+ > Min/Max/Avg-Fill, Gap-Fill, Threshold-Halbebene, Annotation-Band-Item) —
53
+ > 11 vorgefertigte Muster siehe [Demo Hatch](demos/hatch.html).
54
+ > - Renderer-agnostisch (`DrawCommand[]` → `Renderer.render()`).
55
+ > - Keine Runtime-Dependencies. MIT mit Attribution.
56
+
57
+ **Begleit-Module** (im selben Repo, eigene Subpath-Exports):
58
+
59
+ - `ml-time-graph/analyze` — Analyse-Helfer (Aggregation, Downsampling,
60
+ Gap-Detection, MKT, StdDev, Limit-Statistik, Geometrie-Splits).
61
+ Kandidat für späteren Ausgliederungs-Schnitt zu `mlcanalyze` (separate
62
+ Library), sobald (a) das API stabil ist und (b) ein zweiter Konsument
63
+ auftaucht. Bis dahin one-repo.
64
+ - `ml-time-graph/interaction` — optionale Interaktions-Helfer (Zoom,
65
+ Minimap, Tooltip). Die Chart selbst bleibt rein darstellend; wer
66
+ interagieren will, importiert diese Helfer dazu oder baut eigene.
67
+
68
+ Was die Library **nicht** ist: kein Universal-Chart, kein Finanzchart (kein
69
+ OHLC).
70
+
71
+ ---
72
+
73
+
74
+ ## 2. Design-Prinzipien
75
+
76
+ Diese Prinzipien sind verbindlich für alle Entscheidungen unten.
77
+
78
+ | # | Prinzip | Begründung |
79
+ |---|---------|------------|
80
+ | **P1** | **Eine Sache, ein Konzept.** Nur eine API für "fülle Fläche", nur eine für "Linie zeichnen", nur eine für "Gap konfigurieren". | Reduziert die zu lernende Oberfläche, verhindert Spec-Drift. |
81
+ | **P2** | **Daten sind Daten.** Das Datenmodell trägt nur, was zum Zeichnen des **Werts an einer Zeit** nötig ist. Statistik, Limits, Domain-Kontext sind separate Layer. | Wiederverwendbarkeit, Testbarkeit, klare Verantwortung. |
82
+ | **P3** | **Verschachtelte Styles statt flacher Properties.** `style.line.color` ist auffindbarer als `color` neben `lineWidth` neben `shadowColor`. | Bessere IDE-Completion, weniger "wo war das nochmal?". |
83
+ | **P4** | **Defaults im Theme, nicht im Aufruf.** Wer keinen Style angibt, kriegt sinnvolle Werte aus dem Theme. Theme ist mutierbar pro Chart. | Konsistenz, schnelle Restyling. |
84
+ | **P5** | **Strikte TypeScript-Typen.** Diskriminierte Unions wo immer möglich (`kind:` als Diskriminator), keine `any`, keine offenen `Record`-Typen für interne API. | Compiler-Sicherheit, weniger Laufzeitfehler. |
85
+ | **P6** | **JSDoc auf jedem öffentlichen Member.** Was, optional Default, Beispiel. | Hover-Doku in jedem IDE. |
86
+ | **P7** | **Max 250 Zeilen pro Datei.** | Lesbarkeit (Projekt-Style-Guide). |
87
+ | **P8** | **Separable Begleit-Module.** Analyse + Interaktion sind eigenständige Unterverzeichnisse mit eigenem Barrel und eigenem Subpath-Export. Beide sind reine Add-ons — die Chart selbst funktioniert ohne sie. | Klarer Render-Kern, optionale Schwere, späterer Ausgliederungs-Schnitt ohne API-Bruch möglich. |
88
+ | **P9** | **Kompositions statt Aufzählungs-Typen.** „Welche Darstellungsart" ist keine Enum-Frage (`seriesType: 'line' \| 'area' \| …`), sondern eine Frage *welche Style-Sub-Objekte gesetzt sind*. Linie + Fläche + Punkte gleichzeitig = einfach alle drei setzen. | Aus Anwendersicht naheliegender, beliebige Kombinationen ohne neue Enum-Werte, Renderer-Pipeline klar (jedes Style-Stück = ein Layer). |
89
+
90
+ ---
91
+
92
+ ## 3. Daten-Vertrag
93
+
94
+ ### 3.1 Roh-Daten
95
+
96
+ ```ts
97
+ /** Ein Mess-Sample. `value: null` markiert eine Lücke — die Linie bricht dort. */
98
+ export interface DataPoint {
99
+ /** Zeitstempel in ms seit Epoch. */
100
+ time: number;
101
+ /** Mess-Wert. `null` = Gap. */
102
+ value: number | null;
103
+ /** Optionaler punktbezogener Annotationstext (z. B. Event-Marker). */
104
+ annotation?: string;
105
+ /**
106
+ * Synthetischer Punkt — keine echte Messung. Wird automatisch von der
107
+ * Library auf interpoliert eingefügten Punkten gesetzt (Threshold-Schnitte,
108
+ * Gap-Ränder), kann aber auch manuell verwendet werden, wenn man eigene
109
+ * berechnete Werte einbringt (z. B. eine Trendlinie auf zwischengeschobenen
110
+ * Stützstellen).
111
+ *
112
+ * Renderer behandeln `synthetic: true` so:
113
+ * - **Marker-Renderer**: kein Punkt-Marker (auch bei `style.markers.type !== 'none'`).
114
+ * - **Tooltip**: nicht hit-testbar — der Tooltip snapt nur auf echte Messungen.
115
+ * - **Werte-Tabelle**: ausgeblendet (kein "Phantom-Eintrag" in der Values-Tab).
116
+ *
117
+ * Default `undefined` = echte Messung.
118
+ */
119
+ synthetic?: boolean;
120
+ }
121
+ ```
122
+
123
+ **Neu** gegenüber heute: das optionale `synthetic`-Flag. Begründung: der
124
+ `SeriesProcessor` setzt heute schon Interpolations-Punkte an Threshold-Schnitten
125
+ und Gap-Rändern, damit Zonen-Fills exakt an der Schwelle stoppen. Diese
126
+ Punkte würden bei aktivierten Markern fälschlich als "Datenpunkt" auftauchen.
127
+ Das Flag erlaubt es, sie sauber zu ignorieren — und gleichzeitig macht es das
128
+ Konzept "berechneter / eingefügter Punkt" für Konsumenten verfügbar.
129
+
130
+ Daten-Modell-Invariante: **`synthetic` betrifft nur das Rendering, nie die
131
+ Skalen-Berechnung** (ein synthetischer Punkt zählt für Min/Max der Y-Achse).
132
+
133
+ ### 3.2 Aggregat (Slim Core + spezialisierte Untertypen)
134
+
135
+ ```ts
136
+ /** Aggregierter Slot — min/max/avg/count über ein Zeitfenster. */
137
+ export interface AggregatedPoint {
138
+ /** Slot-Start in ms. */
139
+ time: number;
140
+ /** Minimum im Slot. `null` = kein Sample im Slot (Gap). */
141
+ min: number | null;
142
+ /** Maximum im Slot. `null` = Gap. */
143
+ max: number | null;
144
+ /** Durchschnitt im Slot. `null` = Gap. */
145
+ avg: number | null;
146
+ /** Anzahl der Roh-Samples im Slot (0 = Gap). */
147
+ count: number;
148
+ }
149
+ ```
150
+
151
+ **Vom heutigen Stand entfernt:** `mkt`, `deltaMkt`, `stdDev`, `minutesAboveHigh`,
152
+ `minutesBelowLow`. Die wandern in **spezialisierte Aggregat-Typen** — Interfaces
153
+ die `AggregatedPoint` strukturell erweitern:
154
+
155
+ ```ts
156
+ /** Rolling Mean Kinetic Temperature je Zeitslot. */
157
+ export interface MktPoint extends AggregatedPoint {
158
+ /** MKT-Wert (USP <1079.2>) für diesen Slot. `null` = Gap. */
159
+ mkt: number | null;
160
+ /** Delta zum vorherigen Slot (optional). */
161
+ deltaMkt?: number | null;
162
+ }
163
+
164
+ /** Aggregat mit Standardabweichung. */
165
+ export interface StdDevPoint extends AggregatedPoint {
166
+ /** Standardabweichung der Samples im Slot. `null` = Gap. */
167
+ stdDev: number | null;
168
+ }
169
+
170
+ /** Aggregat mit Limit-Überschreitungs-Statistik. */
171
+ export interface LimitStatsPoint extends AggregatedPoint {
172
+ /** Anzahl Minuten oberhalb des oberen Limits im Slot. */
173
+ minutesAboveHigh?: number | null;
174
+ /** Anzahl Minuten unterhalb des unteren Limits im Slot. */
175
+ minutesBelowLow?: number | null;
176
+ }
177
+ ```
178
+
179
+ **Warum strukturell statt diskriminiert** (siehe Q1-Entscheidung in Appendix D):
180
+
181
+ - Strukturell heisst: `MktPoint[]` ist ein gültiger `AggregatedPoint[]` —
182
+ jedes Standard-Aggregat-Rendering (Band, Min/Max/Avg) funktioniert automatisch.
183
+ - Spezialisierte Views/Overlays können auf die zusätzlichen Felder zugreifen,
184
+ indem sie auf den Untertyp casten oder per `'mkt' in point`-Guard fragen.
185
+ - Kein extra `kind`-Diskriminator, weniger Boilerplate für Konsumenten.
186
+
187
+ **Zwei klare Wege** für Statistik-Werte:
188
+
189
+ | Use case | Was tun |
190
+ |----------|---------|
191
+ | Ich habe **Roh-Daten** und will im Chart eine Trend-/Stat-Linie obendrauf | `overlays: [{ kind: 'movingAverage', window, … }]` — Library rechnet, siehe §10 |
192
+ | Ich habe **fertig berechnete MKT-Slots** und will sie als primäre Daten zeigen | `series: [{ data: { points: mktSlots /* MktPoint[] */ }, view: 'minmaxavg' }]` — Library nimmt die Werte wie sie sind |
193
+
194
+ Beides funktioniert nebeneinander; die Library zwingt dich nicht in eine Form.
195
+
196
+ ### 3.3 Eingabe-Pfade — wie man Daten reingibt
197
+
198
+ Flach, strukturell:
199
+
200
+ ```ts
201
+ /**
202
+ * Ein Punkt-Array. Strukturell erkannt:
203
+ * - hat `value` → Roh-Datenpunkt(e) → DataPoint[]
204
+ * - hat `min/max/avg` → Aggregat → AggregatedPoint[] (oder spezialisierter Untertyp)
205
+ */
206
+ export type SeriesPoints = DataPoint[] | AggregatedPoint[];
207
+
208
+ export interface Series {
209
+ name: string;
210
+ /** Die Daten — strukturell erkannt. */
211
+ points: SeriesPoints;
212
+ style?: SeriesStyle;
213
+ // …
214
+ }
215
+ ```
216
+
217
+ Konsumiert wird das so:
218
+
219
+ ```ts
220
+ new MLTimeGraph({
221
+ series: [
222
+ {
223
+ name: 'Temperature',
224
+ points: rawTemperaturePoints, // DataPoint[]
225
+ style: { /* … */ },
226
+ },
227
+ {
228
+ name: 'Temperature (hourly avg)',
229
+ points: aggregateBySlot(rawTemperaturePoints, 'hourly'), // AggregatedPoint[]
230
+ view: 'minmaxavg',
231
+ style: { /* … */ },
232
+ },
233
+ {
234
+ name: 'MKT (rolling 7d)',
235
+ points: rollingMkt(rawTemperaturePoints, 7 * 24 * 3_600_000), // MktPoint[]
236
+ style: { line: { color: '#7c3aed' } },
237
+ },
238
+ ],
239
+ });
240
+ ```
241
+
242
+ Die Library erkennt den Punkt-Typ strukturell (`'min' in points[0]` →
243
+ Aggregat, sonst Roh). Spezialisierte Aggregat-Typen wie `MktPoint` werden
244
+ für Standard-Rendering wie `AggregatedPoint` behandelt — Views/Overlays
245
+ können bei Bedarf auf die Extra-Felder zugreifen.
246
+
247
+ **Implikation für Phase 4** (siehe Appendix A): `TimeSeries` und `AggregatedSeries`
248
+ gehen weg, eine einzige `Series`-Form bleibt — bis dahin laufen die alten
249
+ Aliase parallel.
250
+
251
+ ### 3.4 Helfer
252
+
253
+ ```ts
254
+ import { aggregateBySlot, downsample, detectGaps, parseSeries } from 'ml-time-graph';
255
+
256
+ aggregateBySlot(raw, 'hourly'); // DataPoint[] → AggregatedPoint[]
257
+ downsample(raw, 2000); // LTTB
258
+ detectGaps(raw, minGapMs); // automatische Gap-Detektion
259
+ parseSeries(json); // JSON → DataPoint[]
260
+ ```
261
+
262
+ Diese sind unverändert und bleiben das einzige offizielle Helfer-Set.
263
+
264
+ ---
265
+
266
+ ## 4. Style-System (Serie)
267
+
268
+ ### 4.1 Series-Definition
269
+
270
+ ```ts
271
+ export interface Series {
272
+ /** Anzeige-Name (Legende, Tooltip). */
273
+ name: string;
274
+
275
+ /**
276
+ * Die Mess-Daten. Strukturell erkannt:
277
+ * - Roh: `DataPoint[]` (Punkt hat `value`)
278
+ * - Aggregat: `AggregatedPoint[]` oder Untertyp (`MktPoint[]`, `StdDevPoint[]`, …)
279
+ * (Punkt hat `min`/`max`/`avg`)
280
+ */
281
+ points: SeriesPoints;
282
+
283
+ /**
284
+ * Visuelles Styling. Welche Sub-Objekte gesetzt sind, entscheidet was
285
+ * dargestellt wird: `line` → Linie, `fill` → Fläche, `markers` → Punkte.
286
+ * Beliebige Kombination. Alle weglassen = unsichtbar (kein gültiger Zustand).
287
+ * Defaults aus dem Theme.
288
+ */
289
+ style?: SeriesStyle;
290
+
291
+ /** Welche Y-Achse: 0 = links, 1 = rechts. Default 0. */
292
+ axis?: 0 | 1;
293
+
294
+ /** Optionale Enum-Map (numerischer Wert → Label) für kategoriale Sensoren. */
295
+ enumMap?: Record<number, string>;
296
+
297
+ /**
298
+ * Wie eine aggregierte Serie dargestellt wird. Nur bei Aggregat-Punkten.
299
+ * 'band' = gefülltes Rechteck min→max pro Slot, optional + avg-Linie.
300
+ * 'minmaxavg' = drei Linien (min, max, avg) + optionale Fills.
301
+ * Default: 'minmaxavg'.
302
+ */
303
+ view?: 'band' | 'minmaxavg';
304
+
305
+ /** Optionale Statistik-Overlays (siehe §10). */
306
+ overlays?: SeriesOverlay[];
307
+
308
+ /** Stabile id für DOM-Selektoren. Auto-generiert, falls leer. */
309
+ id?: string;
310
+ }
311
+ ```
312
+
313
+ **Kein `seriesType`-Enum.** Stattdessen Komposition durch Style-Sub-Objekte
314
+ (Prinzip P9):
315
+
316
+ | Was du willst | Style |
317
+ |---------------|-------|
318
+ | Nur Linie | `{ line: { color: '#4285f4' } }` |
319
+ | Nur Fläche (Area) | `{ line: false, fill: '#4285f422' }` |
320
+ | Nur Punkte | `{ line: false, markers: { type: 'circle' } }` |
321
+ | Linie + Fläche | `{ line: { color: '#4285f4' }, fill: '#4285f422' }` |
322
+ | Linie + Punkte | `{ line: {...}, markers: { type: 'circle' } }` |
323
+ | Linie + Fläche + Punkte | alle drei Sub-Objekte setzen |
324
+ | Step + Catmull-Smooth gleichzeitig | `line: [{ shape: 'step', color: '#999', width: 1 }, { shape: 'line', smoothing: true, color: '#3b82f6' }]` |
325
+
326
+ `line: LineStyle[]` legt mehrere Linien durch dieselben Punkte — Reihenfolge =
327
+ Z-Reihenfolge. Saubere Lösung für „Schritt-Daten plus geglättete Trendlinie"
328
+ ohne die Daten zu duplizieren.
329
+
330
+ ### 4.2 `SeriesStyle`
331
+
332
+ ```ts
333
+ export interface SeriesStyle {
334
+ /**
335
+ * Linien-Stil. Drei Formen:
336
+ * LineStyle → eine Linie
337
+ * LineStyle[] → mehrere übereinander (z. B. Step + Catmull-Smooth)
338
+ * false → keine Linie zeichnen
339
+ * Default: eine Linie mit Theme-Defaults.
340
+ */
341
+ line?: LineStyle | LineStyle[] | false;
342
+
343
+ /** Flächen-Stil (siehe §5). */
344
+ fill?: FillSpec;
345
+
346
+ /** Punkt-Marker auf den Samples. */
347
+ markers?: MarkerStyle;
348
+
349
+ /** Drop-Shadow für Linie und Punkte. */
350
+ shadow?: ShadowStyle;
351
+
352
+ /** Pro-Serie Override für Gap-Darstellung (siehe §7). */
353
+ gap?: GapStyle;
354
+ }
355
+
356
+ export interface LineStyle {
357
+ /** Stroke-Farbe (Hex / rgba). */
358
+ color?: string;
359
+ /** Strichbreite in px. Default Theme-`strokeWidth`. */
360
+ width?: number;
361
+ /** Strich-Muster. Default 'solid'. */
362
+ style?: LineVariant;
363
+ /** Catmull-Rom-Glättung. Default false. */
364
+ smoothing?: boolean;
365
+ /** Linienform — 'line' (Default) oder 'step' (Treppe). */
366
+ shape?: 'line' | 'step';
367
+ /** Max. Zeitabstand (ms) zwischen Punkten, bevor die Linie bricht. 0 = aus. */
368
+ gapThreshold?: number;
369
+ /** Deckkraft 0..1 (z. B. um eine darunterliegende Step-Linie scheinen zu lassen). */
370
+ opacity?: number;
371
+ }
372
+
373
+ /** Alle Strich-Varianten (Quelle: theme.ts). */
374
+ export type LineVariant =
375
+ | 'solid'
376
+ | 'dotted'
377
+ | 'sparse-dots'
378
+ | 'dashed'
379
+ | 'long-dash'
380
+ | 'dense-dash'
381
+ | 'dash-dot'
382
+ | 'dash-dot-dot'
383
+ | 'loose-dash';
384
+
385
+ export interface MarkerStyle {
386
+ /** Marker-Shape (Default 'none'). */
387
+ type?: PointStyleType;
388
+ /** Größe (Radius oder Halb-Breite) in px. */
389
+ size?: number;
390
+ /** Outline-Farbe. */
391
+ stroke?: string;
392
+ /** Füll-Farbe (für gefüllte Shapes). */
393
+ fill?: string;
394
+ /** Strichbreite des Outlines. */
395
+ strokeWidth?: number;
396
+ /** Marker nur zeigen, wenn die Serie ≤ n Samples hat (Anti-Clutter). */
397
+ threshold?: number;
398
+ }
399
+
400
+ /** Alle Marker-Shapes (Quelle: theme.ts — 13 Varianten). */
401
+ export type PointStyleType =
402
+ | 'none'
403
+ | 'circle' // gefüllter Kreis
404
+ | 'cross' // zwei diagonale Linien
405
+ | 'square' // gefülltes Quadrat
406
+ | 'diamond' // gedrehtes Quadrat
407
+ | 'triangle' // Dreieck (Spitze oben)
408
+ | 'triangle-down' // Dreieck (Spitze unten)
409
+ | 'star' // 5-zackiger Stern
410
+ | 'arrow' // Chevron nach oben
411
+ | 'plus' // Plus-Zeichen
412
+ | 'hexagon' // Sechseck
413
+ | 'hourglass' // Sanduhr-Form
414
+ | 'line-horizontal'; // horizontaler Strich
415
+
416
+ export interface ShadowStyle {
417
+ color?: string;
418
+ blur?: number;
419
+ offsetX?: number;
420
+ offsetY?: number;
421
+ }
422
+ ```
423
+
424
+ **Entfernt** gegenüber heute:
425
+
426
+ - `dashed: boolean` (nur noch `line.style = 'dashed'`).
427
+ - Separate `lineWidth`, `smoothing`, `pointStyle`, `pointSize`, `shadowColor/Blur/OffsetX/OffsetY` auf der Serie — alle in `style.{line,markers,shadow}` umgezogen.
428
+
429
+ ---
430
+
431
+ ## 5. Fill-Modell
432
+
433
+ **Das zentrale Aufräum-Stück.** Eine Fill-Beschreibung, mehrere Geometrie-Ziele.
434
+
435
+ ### 5.1 `FillStyle` — die Form
436
+
437
+ ```ts
438
+ /**
439
+ * Eine Füllung. Drei Formen, klar diskriminiert:
440
+ *
441
+ * string // einfache Farbe ('#ef444433' oder 'rgba(…)')
442
+ * { color: string } // explizite Farbe, sonst gleich
443
+ * { color: string, hatch: HatchVariant, // Farbe + Hatch (mit eigener Linien-Farbe)
444
+ * hatchColor?: string, hatchWidth?: number }
445
+ *
446
+ * Wo gefüllt wird, beschreibt `region` (siehe 6.2) — nicht `FillStyle` selbst.
447
+ */
448
+ export type FillStyle =
449
+ | string
450
+ | { color: string; hatch?: HatchVariant; hatchColor?: string; hatchWidth?: number };
451
+ ```
452
+
453
+ Das bildet exakt deine Anforderung ab: *"farbe, oder farbe+hatching(+farbe)"*.
454
+
455
+ ### 5.2 Wo gefüllt wird — `FillRegion`
456
+
457
+ ```ts
458
+ /**
459
+ * Eine Füllungs-Region. `from` und `to` definieren das Y-Intervall;
460
+ * der X-Bereich ist immer die Serie.
461
+ */
462
+ export interface FillRegion {
463
+ /** Untere Grenze. Default 'chartBottom'. */
464
+ from?: FillBound;
465
+ /** Obere Grenze. Default 'series' (die Linie selbst). */
466
+ to?: FillBound;
467
+ /** Die Füllung. */
468
+ fill: FillStyle;
469
+ /** Nur an dieser Seite des Übergangs zwischen `from`/`to` füllen. */
470
+ side?: 'above' | 'below';
471
+ }
472
+
473
+ export type FillBound =
474
+ | 'series' // die Linie selbst
475
+ | 'chartTop' | 'chartBottom' // Plot-Kanten
476
+ | { threshold: string } // benannter Threshold (siehe §8)
477
+ | { value: number }; // fester Wert
478
+ ```
479
+
480
+ ### 5.3 Wie `SeriesStyle.fill` aussieht
481
+
482
+ ```ts
483
+ export type FillSpec =
484
+ // Häufiger Fall: durchgehende Füllung vom Plot-Boden bis zur Linie
485
+ | FillStyle
486
+ // Erweitert: eine oder mehrere Regionen
487
+ | { regions: FillRegion[] };
488
+ ```
489
+
490
+ ### 5.4 Beispiele — die häufigen Fälle
491
+
492
+ Alle drei Threshold-bezogenen Anwendungsfälle, die im Hatch-Demo vorkommen,
493
+ plus die Brücke zur heutigen API:
494
+
495
+ **A. „Werte oberhalb eines Werts hervorheben"** — der Klassiker.
496
+
497
+ ```ts
498
+ thresholds: [{ name: 'warn', value: 22 }],
499
+ series: [{
500
+ // …
501
+ style: {
502
+ line: { color: '#ef4444' },
503
+ fill: { regions: [{
504
+ from: { threshold: 'warn' },
505
+ to: 'series', // bis hoch zur Linie
506
+ side: 'above', // nur wo die Linie über 'warn' liegt
507
+ fill: { color: '#ef444433', hatch: 'crosshatch' },
508
+ }] },
509
+ },
510
+ }]
511
+ ```
512
+
513
+ **B. „Bereich zwischen zwei Thresholds einfärben"** — z. B. den "OK"-Korridor
514
+ zwischen low und high markieren, oder umgekehrt einen Warnbereich.
515
+
516
+ ```ts
517
+ thresholds: [
518
+ { name: 'low', value: 18 },
519
+ { name: 'high', value: 26 },
520
+ ],
521
+ series: [{
522
+ // …
523
+ style: {
524
+ line: { color: '#3b82f6' },
525
+ fill: { regions: [{
526
+ from: { threshold: 'low' },
527
+ to: { threshold: 'high' },
528
+ fill: { color: '#22c55e22', hatch: 'dots' },
529
+ }] },
530
+ },
531
+ }]
532
+ ```
533
+
534
+ Mit `from` und `to` als Threshold-Referenzen ist der Korridor zwischen zwei
535
+ benannten Schwellen eine Zeile.
536
+
537
+ **C. „Zonen-Fill (mehrere Bereiche, je eigene Farbe + Hatch)"** — der
538
+ Multi-Zone-Fall aus dem Hatch-Demo.
539
+
540
+ ```ts
541
+ thresholds: [
542
+ { name: 'cold', value: 10 },
543
+ { name: 'norm', value: 18 },
544
+ { name: 'hot', value: 24 },
545
+ ],
546
+ series: [{
547
+ // …
548
+ style: {
549
+ line: { color: '#2c0a0a', smoothing: true },
550
+ fill: { regions: [
551
+ { to: { threshold: 'cold' }, fill: { color: '#60a5fa33', hatch: 'classic-diagonal' } },
552
+ { from: { threshold: 'cold' }, to: { threshold: 'norm' }, fill: { color: '#22c55e33', hatch: 'crosshatch' } },
553
+ { from: { threshold: 'norm' }, to: { threshold: 'hot' }, fill: { color: '#fbbf2433', hatch: 'dots' } },
554
+ { from: { threshold: 'hot' }, fill: { color: '#ef444433', hatch: 'waves' } },
555
+ ] },
556
+ },
557
+ }]
558
+ ```
559
+
560
+ Mit dem Helper (§11, Q9 entschieden: Haupt-Barrel):
561
+
562
+ ```ts
563
+ import { fillBetweenThresholds } from 'ml-time-graph';
564
+
565
+ style: {
566
+ fill: fillBetweenThresholds({
567
+ thresholds: ['cold', 'norm', 'hot'],
568
+ colors: ['#60a5fa33', '#22c55e33', '#fbbf2433', '#ef444433'],
569
+ hatches: ['classic-diagonal', 'crosshatch', 'dots', 'waves'],
570
+ }),
571
+ }
572
+ ```
573
+
574
+ Ein Helper kann den Threshold-Zonen-Fall kürzer schreiben:
575
+
576
+ ```ts
577
+ import { fillBetweenThresholds } from 'ml-time-graph';
578
+
579
+ fill: fillBetweenThresholds({
580
+ thresholds: ['warn', 'crit'],
581
+ colors: ['#22c55e22', '#fbbf2433', '#ef444433'],
582
+ hatches: [undefined, 'dots', 'crosshatch'],
583
+ }),
584
+ ```
585
+
586
+ → liefert eine `{ regions: [...] }`-Spec zurück. Die Library selbst kennt nur
587
+ das `regions`-Konzept; `fillBetweenThresholds` ist syntaktischer Zucker.
588
+
589
+ ---
590
+
591
+ ## 6. Achsen + Grid
592
+
593
+ `axisLabels` und `grid` werden zu einem zusammenhängenden `axes`-Objekt.
594
+
595
+ ```ts
596
+ export interface MLTimeGraphOptions {
597
+ axes?: {
598
+ x?: XAxisConfig;
599
+ left?: YAxisConfig;
600
+ right?: YAxisConfig;
601
+ };
602
+ // …
603
+ }
604
+
605
+ export interface YAxisConfig {
606
+ /** Achsen-Beschriftung (rotiert neben der Achse). */
607
+ label?: string;
608
+ /** Override für den Wertebereich. Default: automatisch. */
609
+ domain?: [number, number] | 'auto';
610
+ /** Wert-Formatter für Tick-Labels. */
611
+ format?: (value: number) => string;
612
+ /** Tick-Konfiguration. */
613
+ ticks?: TickConfig;
614
+ /** Grid-Linien-Stil. */
615
+ grid?: GridStyle;
616
+ /** Achsen-Linie + Tick-Stroke-Stil. */
617
+ axis?: AxisStyle;
618
+ /** Beschriftungs-Stil. */
619
+ labels?: { color?: string; fontSize?: number };
620
+ }
621
+
622
+ export interface XAxisConfig {
623
+ label?: string;
624
+ /** Tick-Formatter — bekommt das Date. Default: i18n via `locale`. */
625
+ format?: (d: Date) => string;
626
+ ticks?: TickConfig;
627
+ grid?: GridStyle;
628
+ axis?: AxisStyle;
629
+ labels?: { color?: string; fontSize?: number };
630
+ }
631
+
632
+ export interface TickConfig {
633
+ /** Zielanzahl der Major-Ticks. */
634
+ major?: number;
635
+ /** Wie viele Minor-Ticks zwischen zwei Major-Ticks (Default 0 = aus). */
636
+ minor?: number;
637
+ /** Tick-Länge in px. */
638
+ size?: number;
639
+ }
640
+
641
+ export interface GridStyle {
642
+ /** Major-Grid: durchgehende Linien an den Major-Ticks. */
643
+ major?: { color?: string; width?: number; style?: LineVariant; opacity?: number } | false;
644
+ /** Minor-Grid (Default: keiner). */
645
+ minor?: { color?: string; width?: number; style?: LineVariant; opacity?: number } | false;
646
+ }
647
+
648
+ export interface AxisStyle {
649
+ /** Farbe der Achsen-Linie + Ticks. */
650
+ color?: string;
651
+ /** Strichbreite. */
652
+ width?: number;
653
+ }
654
+ ```
655
+
656
+ Damit fällt das alte `axisLabels` weg, und `grid: GridOptions` wird zu
657
+ `axes.{x|left|right}.grid`.
658
+
659
+ ---
660
+
661
+ ## 7. Gaps
662
+
663
+ Gap-Logik konsolidiert zu **einem** Pfad mit klarer Hierarchie:
664
+
665
+ 1. **Default**: Punkte mit `value: null` brechen die Linie automatisch.
666
+ 2. **Auto-Detect** (`gaps: { autoDetect, minGapMs }`) erzeugt Gap-Regionen aus
667
+ den Zeitabständen.
668
+ 3. **Manuelle Gaps** (`gaps: { regions: [{ startTime, endTime, label, … }] }`)
669
+ werden zusätzlich gezeichnet.
670
+ 4. **Per-Serie-Style** (`series.style.gap`) übersteuert das chart-weite Aussehen
671
+ nur für diese Serie.
672
+
673
+ ```ts
674
+ export interface GapsConfig {
675
+ /** Automatisch erkennen anhand des Zeitabstands. Default true. */
676
+ autoDetect?: boolean;
677
+ /** Min. Zeitabstand für Auto-Detect in ms. Default 60000. */
678
+ minGapMs?: number;
679
+ /** Manuell deklarierte Gap-Regionen. */
680
+ regions?: GapRegion[];
681
+ /** Default-Style für alle Gaps. */
682
+ style?: GapStyle;
683
+ }
684
+
685
+ export interface GapRegion {
686
+ startTime: number;
687
+ endTime: number;
688
+ label?: string;
689
+ /** Override des Default-Styles. */
690
+ style?: GapStyle;
691
+ }
692
+
693
+ export interface GapStyle {
694
+ /** Wie der Gap-Bereich dargestellt wird. */
695
+ display?: 'empty' | 'dashed_border' | 'filled';
696
+ /** Hintergrund-Fill (nur bei display='filled'). */
697
+ fill?: FillStyle;
698
+ /** Opacity. Default 0.15. */
699
+ opacity?: number;
700
+ /** Label-Konfiguration. */
701
+ label?: GapLabel;
702
+ }
703
+
704
+ export interface GapLabel {
705
+ /** Vertikale Position. Default 'middle'. */
706
+ baseline?: 'above' | 'middle' | 'below';
707
+ /** Rotation in Grad. Default 0. */
708
+ rotate?: number;
709
+ /** Schriftfarbe. */
710
+ color?: string;
711
+ /** Schriftgrösse. */
712
+ fontSize?: number;
713
+ }
714
+
715
+ // Auf der Serie: nur Override-Slot
716
+ export interface SeriesStyle {
717
+ // …
718
+ gap?: GapStyle;
719
+ }
720
+ ```
721
+
722
+ Die heutigen 6 `gap*`-Properties auf `TimeSeries` (`gapFill`, `gapFillOpacity`,
723
+ `gapLabel`, `gapLabelBaseline`, `gapLabelRotate`, `gapFontFill`) kollabieren zu
724
+ **einem** `style.gap`-Objekt mit den klar benennbaren Sub-Feldern oben.
725
+
726
+ ---
727
+
728
+ ## 8. Thresholds
729
+
730
+ `Threshold` bleibt im Konzept gleich (benannter Wert, referenzierbar), aber
731
+ der Fill wird ans neue `FillStyle` angeglichen.
732
+
733
+ ```ts
734
+ export interface Threshold {
735
+ /** Eindeutiger Name, mit dem Serien diese Grenze referenzieren. */
736
+ name: string;
737
+ /** Wert. */
738
+ value: number;
739
+ /** Zugehörige Y-Achse. Default 0. */
740
+ axis?: 0 | 1;
741
+ /** Linie-Stil (`false` = keine Linie zeichnen, nur als Referenz). */
742
+ line?: LineStyle | false;
743
+ /** Halb-Ebene füllen. */
744
+ halfPlane?: { side: 'above' | 'below'; fill: FillStyle; opacity?: number };
745
+ /** Label. */
746
+ label?: ThresholdLabelConfig | false;
747
+ /** Drop-Shadow. */
748
+ shadow?: ShadowStyle;
749
+ id?: string;
750
+ }
751
+
752
+ export interface ThresholdLabelConfig {
753
+ text?: string;
754
+ position?: 'left' | 'right' | 'above' | 'below' | 'center';
755
+ rotate?: number;
756
+ baseline?: 'top' | 'middle' | 'bottom';
757
+ color?: string;
758
+ fontSize?: number;
759
+ }
760
+ ```
761
+
762
+ Auf der Serie:
763
+
764
+ ```ts
765
+ export interface Series {
766
+ // …
767
+ /** Linie pro Threshold-Zone einfärben. */
768
+ colorByThresholds?: string[];
769
+ }
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 9. Annotationen (Marker, Highlights, Overlays, Bands)
775
+
776
+ Bleibt im Wesentlichen wie heute — die API ist hier bereits sauber. Kleinere
777
+ Anpassungen für Konsistenz:
778
+
779
+ ### 9.1 `Marker`
780
+
781
+ ```ts
782
+ export interface Marker {
783
+ time: number;
784
+ value?: number;
785
+ label?: string;
786
+ /** Stil-Block (statt flacher color/pointStyle/…-Felder). */
787
+ style?: { color?: string; point?: PointStyleType; size?: number; line?: 'full' | 'to-value' | 'to-top' };
788
+ /** Ziel-Serie für 'to-value' / 'to-top'. */
789
+ seriesIndex?: number;
790
+ }
791
+ ```
792
+
793
+ ### 9.2 `Highlight` — unverändert
794
+
795
+ `Highlight` ist bereits in Ordnung (inkl. `labelPosition: 'above' | 'top' | …`).
796
+
797
+ ### 9.3 Freie Overlays — `Annotation`
798
+
799
+ `Annotation` (`line | arrow | rect | point | label` in Datenkoordinaten) bleibt
800
+ strukturell gleich. `rect` kriegt — wie heute — optional `hatch`.
801
+
802
+ ### 9.4 Annotation-Bands (horizontale Streifen)
803
+
804
+ `AnnotationBandConfig` ist bereits sauber (`name`, `height`, `spacing`, `items`).
805
+ Nur die Item-Properties werden ans neue `FillStyle` angepasst:
806
+
807
+ ```ts
808
+ export interface AnnotationBandItem {
809
+ startTime: number;
810
+ endTime: number;
811
+ fill?: FillStyle; // statt fill + hatch separat
812
+ label?: { text: string; baseline?: 'top' | 'middle' | 'bottom'; color?: string; fontSize?: number };
813
+ stroke?: { color: string; width: number };
814
+ }
815
+ ```
816
+
817
+ ---
818
+
819
+ ## 10. Statistik-Overlays (Moving Avg / MKT / StdDev)
820
+
821
+ Statt Stat-Werte ins Datenmodell einzubacken (`AggregatedPoint.mkt`,
822
+ `AggregatedPoint.stdDev`), werden sie als **Overlays** deklariert und vom
823
+ `SeriesProcessor` zur Render-Zeit aus den Roh-/Aggregat-Daten berechnet.
824
+
825
+ ```ts
826
+ export interface Series {
827
+ // …
828
+ overlays?: SeriesOverlay[];
829
+ }
830
+
831
+ export type SeriesOverlay =
832
+ | { kind: 'movingAverage'; window: number; type?: 'simple' | 'exponential'; style?: SeriesStyle }
833
+ | { kind: 'movingMkt'; window: number; activationEnergy?: number; style?: SeriesStyle }
834
+ | { kind: 'stdDevBand'; multiplier?: number; style?: SeriesStyle }
835
+ | { kind: 'limits'; low?: number; high?: number; style?: SeriesStyle };
836
+ ```
837
+
838
+ Beispiel:
839
+
840
+ ```ts
841
+ const tempSeries: Series = {
842
+ name: 'Temperature',
843
+ data: { kind: 'raw', points: rawTemp },
844
+ style: { line: { color: '#4285f4' } },
845
+ overlays: [
846
+ { kind: 'movingAverage', window: 60_000, style: { line: { color: '#fbbf24', style: 'dashed' } } },
847
+ { kind: 'stdDevBand', multiplier: 2, style: { fill: '#4285f422' } },
848
+ ],
849
+ };
850
+ ```
851
+
852
+ Statt:
853
+
854
+ ```ts
855
+ // heute — Stats müssten vorgängig in AggregatedPoint.stdDev gerechnet sein
856
+ // und werden dann implizit irgendwo gerendert
857
+ ```
858
+
859
+ Vorteil: die Roh-Daten bleiben unberührt, Overlays sind komponierbar,
860
+ sie haben ihren eigenen Style-Block und tauchen automatisch in der Legende auf.
861
+
862
+ **Bestehende + neue Helfer wandern alle in `src/analyze/`** (Subpath-Export
863
+ `ml-time-graph/analyze`, siehe §11). Dort liegen sie als pur funktionale
864
+ Module — `(input, opts) => result` — ohne Render-Abhängigkeit, damit sie
865
+ isoliert testbar sind und später als `mlcanalyze` ausziehen können.
866
+
867
+ | Datei | Inhalt |
868
+ |-------|--------|
869
+ | `src/analyze/aggregator.ts` | `aggregateBySlot`, `downsample` (LTTB), `detectGaps` — heute in `src/data/aggregator.ts` |
870
+ | `src/analyze/processor.ts` | Interpolation, `getRuns`, `splitByBoundaries`, `splitByThreshold` — heute in `src/data/series_processor.ts` |
871
+ | `src/analyze/moving_avg.ts` | Simple + Exponential — heute in `src/statistics/moving_avg.ts` |
872
+ | `src/analyze/stats.ts` | `StatsAggregator.compute()` — heute in `src/statistics/stats.ts` |
873
+ | `src/analyze/mkt.ts` | **NEU.** Mean Kinetic Temperature mit `activationEnergy` (Default 83144, USP <1079.2>) |
874
+ | `src/analyze/std_dev.ts` | **NEU.** Standardabweichungs-Band |
875
+ | `src/analyze/limits.ts` | **NEU.** Aus `{ low, high }` → "Minuten ausserhalb"-Statistik (bisher `AggregatedPoint.minutesAboveHigh/Below`) |
876
+ | `src/analyze/index.ts` | Barrel-Export — die öffentliche `ml-time-graph/analyze`-Oberfläche |
877
+
878
+ `src/statistics/stats_overlay.ts` (der Render-Layer für Stats) **bleibt** unter
879
+ `src/series/` — das ist die Render-Seite, nicht Analyse.
880
+
881
+ Beispiel-Import:
882
+
883
+ ```ts
884
+ import { aggregateBySlot, mkt, stdDev } from 'ml-time-graph/analyze';
885
+ // Daten transformieren BEVOR sie an die Chart gehen, falls man die
886
+ // Berechnung nicht der Chart überlassen will:
887
+ const hourly = aggregateBySlot(raw, 'hourly');
888
+ ```
889
+
890
+ Die Chart kann beides: entweder bereits berechnete Werte zeigen, oder die
891
+ Berechnung intern per `overlays:` selbst anstoßen.
892
+
893
+ ---
894
+
895
+ ## 11. Modul-Struktur (Render-Kern + Begleit-Module)
896
+
897
+ ```
898
+ src/
899
+ ├── MLTimeGraph.ts // Facade
900
+ ├── options.ts // MLTimeGraphOptions, LegendOptions, …
901
+ ├── index.ts // public barrel: ml-time-graph
902
+
903
+ ├── core/ // Skalen, Layout, Geometrie-Basics
904
+ │ ├── layout.ts
905
+ │ ├── scale.ts
906
+ │ └── clip.ts
907
+
908
+ ├── theme/ // ⟵ aufgespalten aus theme.ts (542 LoC)
909
+ │ ├── defaults.ts // theme-Objekt (Farben, Default-Sizes)
910
+ │ └── palette.ts // Farb-Palette
911
+
912
+ ├── patterns/ // SVG-Pattern-Generatoren
913
+ │ ├── hatch.ts // HatchVariant + getHatch()
914
+ │ ├── line.ts // LineVariant + getLineStyle()
915
+ │ └── points.ts // PointStyleType-Geometrie (Marker-Pfade)
916
+
917
+ ├── style/ // Style-Typen (öffentlich)
918
+ │ └── types.ts // LineStyle, FillStyle, FillRegion, MarkerStyle, ShadowStyle, SeriesStyle
919
+
920
+ ├── data/ // reines Daten-Modell + Parsing
921
+ │ ├── types.ts // DataPoint, AggregatedPoint, Series, Threshold, Annotation, …
922
+ │ └── parser.ts // parseSeries, parseAggregated, …
923
+
924
+ ├── axis/ // Zeit-/Wert-Achsen
925
+ │ ├── time_axis.ts
926
+ │ ├── value_axis.ts
927
+ │ └── enum_axis.ts
928
+
929
+ ├── renderer/ // DrawCommand-Renderer
930
+ │ ├── renderer.ts // abstract
931
+ │ ├── svg_renderer.ts // DOM-freier SVG-String
932
+ │ ├── command_builder.ts
933
+ │ ├── grid_renderer.ts
934
+ │ ├── legend_renderer.ts
935
+ │ └── series_renderer.ts // Series → DrawCommand[]
936
+
937
+ ├── series/ // Per-Serie-Geometrie & Render-Layer
938
+ │ ├── line_series.ts
939
+ │ ├── step_series.ts
940
+ │ ├── band_series.ts
941
+ │ ├── minmaxavg_series.ts
942
+ │ ├── zoned_line_series.ts // Multi-Zone-Coloring + Region-Fills
943
+ │ ├── threshold_renderer.ts
944
+ │ ├── gap_renderer.ts
945
+ │ ├── annotation_band.ts
946
+ │ └── stats_overlay.ts // ⟵ aus statistics/ hierher; ist Render-Layer
947
+
948
+ ├── annotation/ // Marker / Highlights / Annotations
949
+ │ ├── marker.ts
950
+ │ ├── highlight.ts
951
+ │ └── annotation_renderer.ts
952
+
953
+ ├── analyze/ // ⟵ Begleit-Modul, Subpath-Export ml-time-graph/analyze
954
+ │ ├── index.ts // Barrel
955
+ │ ├── aggregator.ts // aggregateBySlot, downsample, detectGaps
956
+ │ ├── processor.ts // Interpolation, getRuns, splitByBoundaries
957
+ │ ├── moving_avg.ts
958
+ │ ├── stats.ts // StatsAggregator
959
+ │ ├── mkt.ts // NEU
960
+ │ ├── std_dev.ts // NEU
961
+ │ └── limits.ts // NEU
962
+
963
+ └── interaction/ // ⟵ Begleit-Modul, Subpath-Export ml-time-graph/interaction
964
+ ├── index.ts // Barrel
965
+ ├── zoom.ts
966
+ ├── minimap.ts
967
+ └── tooltip.ts
968
+ ```
969
+
970
+ Jede Datei < 250 Zeilen (Projekt-Style-Guide). `interaction/` ist heute schon
971
+ ein eigenes Verzeichnis — bekommt nur das Barrel + Subpath-Export.
972
+
973
+ ### Subpath-Exports in `package.json`
974
+
975
+ ```json
976
+ {
977
+ "exports": {
978
+ ".": {
979
+ "import": "./dist/index.js",
980
+ "require": "./dist/index.cjs",
981
+ "types": "./dist/index.d.ts"
982
+ },
983
+ "./analyze": {
984
+ "import": "./dist/analyze.js",
985
+ "require": "./dist/analyze.cjs",
986
+ "types": "./dist/analyze.d.ts"
987
+ },
988
+ "./interaction": {
989
+ "import": "./dist/interaction.js",
990
+ "require": "./dist/interaction.cjs",
991
+ "types": "./dist/interaction.d.ts"
992
+ }
993
+ }
994
+ }
995
+ ```
996
+
997
+ Konsumenten importieren je nach Bedarf:
998
+
999
+ ```ts
1000
+ import { MLTimeGraph, SVGRenderer } from 'ml-time-graph'; // Render-Kern
1001
+ import { aggregateBySlot, mkt, stdDev } from 'ml-time-graph/analyze';
1002
+ import { Zoom, Minimap, Tooltip } from 'ml-time-graph/interaction';
1003
+ ```
1004
+
1005
+ Wer nur die Chart braucht, holt sich nicht die Analyse-Helfer in den Bundle.
1006
+
1007
+ ### Theme — global default + per-Chart-Override
1008
+
1009
+ Zwei Ebenen:
1010
+
1011
+ ```ts
1012
+ import { setDefaultTheme, getDefaultTheme, MLTimeGraph } from 'ml-time-graph';
1013
+
1014
+ // 1. Globales Default einmal setzen (z. B. beim App-Start) — optional.
1015
+ setDefaultTheme({
1016
+ stroke: '#0f172a',
1017
+ gridStroke: '#f1f5f9',
1018
+ palette: ['#3b82f6', '#ef4444', '#22c55e', '#f59e0b'],
1019
+ });
1020
+
1021
+ // 2. Per-Chart-Override — wird auf das globale Default gemergt.
1022
+ new MLTimeGraph({
1023
+ theme: { stroke: '#7c3aed' }, // nur stroke wird übersteuert
1024
+ // …
1025
+ });
1026
+ ```
1027
+
1028
+ **Resolutions-Reihenfolge** beim Chart-Konstruktor:
1029
+
1030
+ ```
1031
+ resolvedTheme = { ...BUILTIN_DEFAULTS, ...globalDefault, ...options.theme }
1032
+ ```
1033
+
1034
+ Wenn nirgends gesetzt: nur `BUILTIN_DEFAULTS` (die Werte aus
1035
+ `src/theme/defaults.ts`). Wenn nur global gesetzt: alle Charts kriegen den
1036
+ globalen Look. Per-Chart-Override schlägt beides.
1037
+
1038
+ ```ts
1039
+ // API:
1040
+ export function setDefaultTheme(partial: Partial<Theme>): void;
1041
+ export function getDefaultTheme(): Readonly<Theme>;
1042
+ export function resetDefaultTheme(): void; // zurück auf BUILTIN_DEFAULTS
1043
+ ```
1044
+
1045
+ **Vorsicht** (im Doc dokumentieren!): das globale Default ist ein
1046
+ **Module-Level-Side-Effect**. Konsequenzen:
1047
+
1048
+ - **SSR**: in einer Node.js-Server-Umgebung mit mehreren parallelen Renders
1049
+ würde `setDefaultTheme` aus Request A auch Request B beeinflussen. Empfehlung
1050
+ für SSR: Theme **per-Chart** übergeben, `setDefaultTheme` nur in
1051
+ Browser-/CLI-Kontexten benutzen.
1052
+ - **Tests**: nach Tests, die `setDefaultTheme` setzen, `resetDefaultTheme()`
1053
+ in `afterEach` aufrufen, damit Test-Reihenfolge keine Rolle spielt.
1054
+
1055
+ Diese Caveats stehen so auch in der JSDoc von `setDefaultTheme`, damit
1056
+ TypeScript-Hover sie zeigt.
1057
+
1058
+ ### Späterer Ausgliederungs-Schnitt
1059
+
1060
+ Wenn (a) das Analyse-API stabil ist und (b) ein zweiter Konsument auftaucht,
1061
+ kann `src/analyze/` 1:1 zu einem separaten Package `mlcanalyze` (oder
1062
+ `@mlc/analyze`) auf einem eigenen Repo ausziehen. `ml-time-graph` würde dann
1063
+ `mlcanalyze` als (Peer-)Dependency listen, oder die `overlays:`-Berechnung
1064
+ würde komplett vom Konsumenten kommen. Solange das nicht ansteht — bleibt
1065
+ einrepoig.
1066
+
1067
+ ---
1068
+
1069
+ ## 12. MLTimeGraphOptions — Top-Level-Form
1070
+
1071
+ Das Ziel-Top-Level-API:
1072
+
1073
+ ```ts
1074
+ export interface MLTimeGraphOptions {
1075
+ // — Geometrie
1076
+ width?: number;
1077
+ height?: number;
1078
+ margin?: Margin;
1079
+
1080
+ // — Renderer (DI)
1081
+ renderer?: Renderer;
1082
+
1083
+ // — Daten
1084
+ series: Series[];
1085
+
1086
+ // — Achsen + Grid
1087
+ axes?: { x?: XAxisConfig; left?: YAxisConfig; right?: YAxisConfig };
1088
+
1089
+ // — Hintergrund-Layer (in Z-Reihenfolge: thresholds → highlights → gaps → series → annotations)
1090
+ thresholds?: Threshold[];
1091
+ highlights?: Highlight[];
1092
+ gaps?: GapsConfig;
1093
+
1094
+ // — Vordergrund-Annotationen
1095
+ markers?: Marker[];
1096
+ annotations?: Annotation[];
1097
+ annotationBands?: AnnotationBandConfig[];
1098
+
1099
+ // — Theme + Locale
1100
+ theme?: Partial<typeof theme>;
1101
+ locale?: string;
1102
+
1103
+ // — Legende
1104
+ legend?: LegendOptions;
1105
+ }
1106
+ ```
1107
+
1108
+ `AxisLabels`, `GridOptions` (flach), `GapConfig` (alt) entfallen — ihre Inhalte
1109
+ sind jetzt strukturiert unter `axes` / `gaps`.
1110
+
1111
+ ---
1112
+
1113
+ ## A. Migrations-Historie (v0.2 → v0.3 → Cleanup)
1114
+
1115
+ Die heutige API entstand in vier Phasen über mehrere Branches. Detail-Logs
1116
+ stehen in [`cleanup_progress.md`](docs/cleanup_progress.md) und in den
1117
+ Git-Commit-Messages; hier nur die Eckpunkte als Begründungs-Kontext.
1118
+
1119
+ **Phase 1 — Interner Cleanup (kein API-Bruch).** `theme.ts` (542 LoC) in vier
1120
+ fokussierte Module aufgespalten. Analyse-Helfer aus `src/data/` und
1121
+ `src/statistics/` nach `src/analyze/` umgezogen, Subpath-Export
1122
+ `ml-time-graph/analyze` eingeführt. `src/interaction/` analog mit eigenem
1123
+ Barrel + Subpath. Tote Code-Blöcke entfernt, leere Files gelöscht. Alle 211
1124
+ Tests blieben grün, kein Aufrufer brach.
1125
+
1126
+ **Phase 2 — Style-System additiv eingeführt.** `SeriesStyle` (nested:
1127
+ `line`/`fill`/`markers`/`shadow`/`gap`) als alternative Form zu den ~30 flachen
1128
+ Style-Feldern auf `TimeSeries`. `FillSpec` / `FillRegion` als strukturiertes
1129
+ Fill-Modell (heute auch für legacy `fillToThreshold(s)` der zugrundeliegende
1130
+ Render-Pfad). `axes: AxesConfig` als First-Class-Block. `GapsConfig` als
1131
+ Union-Form für `gaps`. `setDefaultTheme` / `getDefaultTheme` / `resetDefaultTheme`
1132
+ für globale Theme-Defaults. Spezialisierte Aggregat-Typen `MktPoint` /
1133
+ `StdDevPoint` / `LimitStatsPoint`. `synthetic?: boolean` auf `DataPoint`.
1134
+ `SeriesOverlay`-Union (Typ-only, Rendering folgt). `LineVariant` + 13
1135
+ Marker-Shapes vollständig im SVG-Renderer verdrahtet. ~80 % der flachen
1136
+ Style-Felder auf den Series-Interfaces mit `@deprecated`-Migrations-Hinweisen
1137
+ markiert. Alle 211 Phase-1-Tests + 58 neue (= 269) grün.
1138
+
1139
+ **Phase 3 — Statistik-Overlays + Schließen der Wiring-Lücken.** Drei
1140
+ pur-funktionale Compute-Helfer in `src/analyze/`: `mkt`/`rollingMkt` (USP
1141
+ <1079.2>), `stdDev`/`sampleStdDev`/`rollingStdDev`, `computeLimitExcursions`.
1142
+ `SeriesOverlay`-Rendering verdrahtet (`movingAverage`, `movingMkt`, `limits`;
1143
+ `stdDevBand` blieb no-op). Alle weiteren Style-Sub-Objekte (`style.line[]`,
1144
+ `style.markers`, `style.shadow`, `style.gap`, `axes.*.format` / `ticks`,
1145
+ `axes.*.domain`) im Renderer verdrahtet. Legacy `fillToThreshold(s)` per
1146
+ Resolver-Mapping auf den `FillSpecRenderer` umgelenkt — heute ein einziger
1147
+ Fill-Render-Pfad. `GapsConfig.autoDetect` aktiv. 49 neue Tests (= 318).
1148
+
1149
+ **Phase 4 — Stabilisierung 0.3.0.** `USAGE.md` durchgängig auf die neue
1150
+ API umgeschrieben. `VERSION` + `package.json` von `0.2.0` → `0.3.0`. Die
1151
+ legacy flachen Felder blieben noch mit `@deprecated`-Hinweis erhalten.
1152
+
1153
+ **Phase 5 — Hart-Removal-Bruch (durchgeführt 2026-06-01).** Alle
1154
+ `@deprecated`-Pfade entfernt:
1155
+
1156
+ - Flache Style-Felder auf `TimeSeries` (`color`, `lineWidth`, `dashed`,
1157
+ `smoothing`, `shadow*`, `pointStyle/Size/Threshold`, `gapThreshold`,
1158
+ `gapFill*`, `gapLabel*`, `gapFontFill`) gelöscht — alles über
1159
+ `style.{line,fill,markers,shadow,gap}`.
1160
+ - `fillToThreshold` / `fillToThresholds` / `fillByThresholds*` entfernt
1161
+ — Konsumenten nutzen `style.fill.regions[]` oder den
1162
+ `fillBetweenThresholds()`-Helper.
1163
+ - `AggregatedSeries` flache `color` / `smoothing` / `lineWidth` entfernt.
1164
+ - `axisLabels` / `grid`-Top-Level entfernt — alles über
1165
+ `axes.{x,left,right}`.
1166
+ - `AggregatedPoint`-Extras (`mkt`, `deltaMkt`, `stdDev`,
1167
+ `minutesAboveHigh/BelowLow`) auf die spezialisierten Untertypen
1168
+ (`MktPoint`, `StdDevPoint`, `LimitStatsPoint`, `StatsAggregatedPoint`)
1169
+ umgezogen. Interne Konsumenten (`aggregator`, `heatmap`,
1170
+ `model/apotheken`) auf `StatsAggregatedPoint` umgetypt.
1171
+ - 8 Renderer-Klassen → freie Funktionen (`renderMarkers`,
1172
+ `renderHighlights`, `renderAnnotations`, `renderGrid`,
1173
+ `renderThresholds`, `renderLegend`, `renderGaps`, plus die 6
1174
+ Funktionen aus dem ehemaligen `SeriesRenderer`-Static-Container).
1175
+ - `src/style/types.ts` (244 LoC Monolith) in vier konzept-fokussierte
1176
+ Files aufgespalten (`enums.ts`, `series.ts`, `fill.ts`, `gap.ts`).
1177
+ - `#config`-Pattern auf alle restlichen Series-/Axis-Klassen statt
1178
+ Feld-für-Feld-Kopie.
1179
+ - Demo-Migration: 9 Demo-Files auf nested-Style umgestellt.
1180
+
1181
+ Resultat: **0 × `@deprecated`** in `src/`, `demos/src/`, `tests/`. Eine
1182
+ API-Form überall. 340 Tests grün.
1183
+
1184
+ ## B. Polish-Backlog (post-v0.3)
1185
+
1186
+ Liste der Polish-Ideen, die im Backlog (`.mlcai/BACKLOG.md`) leben:
1187
+
1188
+ - `mount()`-One-Liner-Helper — landed (2026-06-01).
1189
+ - Tooltip-Helper mit Multi-Series-Callback (`I-20260601-01`).
1190
+ - `validate`-Option für klarere Fehler bei kaputten Options (`I-20260601-02`).
1191
+ - Public-vs-Internal-Type-Split via Subpath-Exports (`I-20260601-03`).
1192
+ - CSS-Class-basiertes Theming statt SVG-Attribute (`I-20260601-04`).
1193
+ - Journey-Format auf alle Demo-HTMLs ausrollen (`T-20260601-01`).
1194
+
1195
+ ## C. Aufgelöste Design-Entscheidungen
1196
+
1197
+ Während des Cleanups gefällte Entscheidungen, jeweils mit Datum + Kurz-Begründung:
1198
+
1199
+ | # | Frage | Entscheidung |
1200
+ |---|-------|--------------|
1201
+ | Q1 | `Series.data`: diskriminiert oder flach? | **flach** — `points: DataPoint[] \| AggregatedPoint[]`, strukturell erkannt. Spezialisierte Aggregate erweitern `AggregatedPoint` strukturell. |
1202
+ | Q2 | `TimeSeries`/`AggregatedSeries` als Alias erhalten? | **ja**, als `@deprecated`-Type-Aliase bis Phase 5. |
1203
+ | Q3 | Wie wird die Darstellungsart ausgewählt? | **kein `seriesType`-Enum** — Komposition durch Style-Sub-Objekte (`line` da → Linie, `fill` → Fläche, `markers` → Punkte). `line: LineStyle[]` für Multi-Line-Stack. |
1204
+ | Q4 | `fillBetweenThresholds()` als offizieller Helper? | **ja**, im Haupt-Barrel. |
1205
+ | Q5 | Theme per-Chart oder global? | **beides** — `options.theme` per Chart + `setDefaultTheme(partial)` global. Resolution: `BUILTIN_DEFAULTS` ← `globalDefault` ← `options.theme`. SSR-/Test-Caveats in JSDoc. |
1206
+ | Q6 | `interaction/` (Zoom/Minimap/Tooltip): bleiben oder raus? | **bleibt** als Begleit-Modul mit eigenem Barrel + Subpath-Export `ml-time-graph/interaction`. |
1207
+ | Q7 | JSDoc-Tiefe? | **Top-Ebene** ist Pflicht; Property-Doku ergänzt der Konsument bei Bedarf. |
1208
+ | Q8 | Major-Bump direkt nach Phase 4? | **nein**, erst Stabilisierungs-Phase (`0.3.x`/`0.5.x`), dann `1.0`. |
1209
+ | Q9 | Wo lebt `fillBetweenThresholds()`? | **Haupt-Barrel** (`ml-time-graph`). Style-Konstruktion ist Konsumenten-API, kein Analyse-Schritt. |
1210
+ | Q10 | Bei `line: LineStyle[]`: gleiche oder eigene Daten pro Sub-Linie? | **gleiche Daten**. Eigene Daten für Trendlinien gehen über `overlays: [{ kind: 'movingAverage', … }]`. |
1211
+ | Q11 | Wie mit künstlich eingefügten Punkten (Threshold-Schnitte, Gap-Ränder)? | **`DataPoint.synthetic?: boolean`** — wird vom internen `SeriesProcessor` automatisch gesetzt, kann auch manuell verwendet werden. Renderer-Konsequenz: kein Marker, nicht hit-testbar, nicht in Werte-Tabelle. Skalen zählen den Punkt mit. |