@pond-ts/charts 0.31.0 → 0.31.1

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/CHANGELOG.md CHANGED
@@ -8,8 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
8
8
  them all. Pre-1.0: minor bumps may include new features and type-level changes;
9
9
  patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.31.0...HEAD
12
- [0.31.0]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.0
11
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...HEAD
12
+ [0.31.1]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.1
13
+ [0.31.0]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...3c4e8bd
13
14
  [0.30.0]: https://github.com/pjm17971/pond-ts/compare/v0.29.0...v0.30.0
14
15
  [0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
15
16
  [0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
@@ -24,6 +25,15 @@ patch bumps are strictly additive.
24
25
  [0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
25
26
  [0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
26
27
 
28
+ ## [0.31.1] — 2026-06-28
29
+
30
+ ### Fixed
31
+
32
+ - **`@pond-ts/charts` and `@pond-ts/fit` now ship their own README** on npm.
33
+ 0.31.0 inadvertently published the `pond-ts` core README on every package
34
+ (each `prepack` copied the repo-root README); charts and fit now carry their
35
+ own. No code or API changes.
36
+
27
37
  ## [0.31.0] — 2026-06-28
28
38
 
29
39
  First published release of **`@pond-ts/charts`** and **`@pond-ts/fit`** (both were
package/README.md CHANGED
@@ -1,228 +1,69 @@
1
- # pond-ts
1
+ # @pond-ts/charts
2
2
 
3
- **Highly optimised, fully typed Timeseries library for TypeScript**
3
+ **React charts for [pond-ts](https://www.npmjs.com/package/pond-ts) time series.**
4
4
 
5
- Schema-driven events, composable batch transforms, push-based streaming
6
- ingest, multi-entity partitioning, and an optional React integration
7
- all strict TypeScript end to end, all immutable.
8
-
9
- **pond-ts** is the TypeScript-first successor to
10
- [pondjs](https://github.com/esnet/pond), rewritten from scratch with a
11
- focus on type safety, composability, and the live-streaming patterns
12
- that pondjs never grew.
5
+ A composable charting layer built directly on pond-ts series: a canvas data
6
+ plane for the heavy line/area/bar drawing, with SVG overlays for axes, cursors,
7
+ and interaction. Line, area, bar, scatter, and box charts; **time and value
8
+ x-axes** inferred from the data; an interactive cursor; and theming.
13
9
 
14
10
  ```sh
15
- npm install pond-ts # core
16
- npm install @pond-ts/react # React hooks (optional)
11
+ npm install @pond-ts/charts pond-ts @pond-ts/react
17
12
  ```
18
13
 
19
- - **Typed schemas** declare once, every transform downstream narrows
20
- off it. `event.get('cpu')` returns `number | undefined` straight from
21
- the schema; no `as` casts.
22
- - **Batch + streaming with the same vocabulary** — `filter`, `map`,
23
- `aggregate`, `rolling`, `diff`, `rate`, `fill`, `cumulative`,
24
- `sample`, `reduce` all exist on both `TimeSeries` and `LiveSeries`.
25
- - **Multi-entity by construction** — `partitionBy('host')` routes per
26
- entity; `rolling` / `aggregate` / `fill` / `sample` over a partitioned
27
- view all become per-entity automatically.
28
- - **Bounded-memory streaming** — retention policies, eviction-aware
29
- views, and sampling decouple downstream window length
30
- from event rate at firehose loads (up to 500k events/sec on a
31
- single node.js instance.)
32
- - **Triggers** — for control of rolling emission cadences. Synchronised
33
- partitioned rolling fires across partitions on every boundary.
34
- - **Typed column extraction** — `series.column('cpu')` returns a
35
- schema-narrowed typed column with single-pass reductions
36
- (`min`/`max`/`sum`/`mean`/`stdev`/`median`/`percentile`/`minMax`),
37
- index downsampling (`bin`), and a zero-copy `toFloat64Array()` for
38
- canvas / WebGL draw loops — no per-event allocation on the hot path.
39
- - **No legacy baggage**
40
-
41
- ## Quick start: batch
14
+ `pond-ts`, `@pond-ts/react`, and `react` (18 or 19) are peer dependencies.
42
15
 
43
- ```ts
44
- import { Sequence, TimeSeries } from 'pond-ts';
16
+ ## Quick start
45
17
 
46
- const schema = [
47
- { name: 'time', kind: 'time' },
48
- { name: 'cpu', kind: 'number' },
49
- { name: 'requests', kind: 'number' },
50
- { name: 'host', kind: 'string' },
51
- ] as const;
18
+ ```tsx
19
+ import { TimeSeries } from 'pond-ts';
20
+ import { ChartContainer, ChartRow, Layers, LineChart } from '@pond-ts/charts';
52
21
 
53
- const cpu = TimeSeries.fromJSON({
22
+ const series = new TimeSeries({
54
23
  name: 'cpu',
55
- schema,
24
+ schema: [
25
+ { name: 'time', kind: 'time' },
26
+ { name: 'cpu', kind: 'number' },
27
+ ] as const,
56
28
  rows: [
57
- ['2025-01-01T00:00:00Z', 0.31, 120, 'host1'],
58
- ['2025-01-01T00:01:00Z', 0.44, 135, 'host2'],
59
- ['2025-01-01T00:02:00Z', 0.52, 141, 'host1'],
60
- ['2025-01-01T00:03:00Z', 0.48, 128, 'host1'],
61
- ['2025-01-01T00:04:00Z', 0.63, 166, 'host3'],
29
+ [1717200000000, 50],
30
+ [1717200060000, 62],
31
+ [1717200120000, 48],
62
32
  ],
63
33
  });
64
34
 
65
- const byMinute = cpu.aggregate(Sequence.every('1m'), {
66
- cpu: 'avg',
67
- requests: 'sum',
68
- host: 'last',
69
- });
70
-
71
- const bands = cpu.baseline('cpu', { window: '2m', sigma: 2 });
72
- // ^ appends rolling avg / sd / upper / lower in one pass.
73
-
74
- const anomalies = cpu.outliers('cpu', { window: '2m', sigma: 2 });
75
- // ^ schema-preserving filter — same columns, just the spikes.
76
- ```
77
-
78
- The full batch surface (`align`, `rolling`, `smooth`, `groupBy`, `join`,
79
- `reduce`, `diff`, `rate`, `fill`, `dedupe`, `materialize`, `sample`,
80
- `partitionBy`, `pivotByGroup`, …) follows the same shape: TimeSeries
81
- in, TimeSeries out, schema preserved.
82
-
83
- ## Quick start: live (streaming)
84
-
85
- ```ts
86
- import { LiveSeries, Sequence } from 'pond-ts';
87
-
88
- // 1. Same schema; this is a live append buffer with retention.
89
- const live = new LiveSeries({
90
- name: 'cpu',
91
- schema,
92
- retention: { maxAge: '10m' }, // keep only the last 10 minutes
93
- });
94
-
95
- // 2. Push as events arrive. Each push is validated against the schema.
96
- live.push([Date.now(), 0.45, 128, 'api-1']);
97
-
98
- // 3. Compose live views — incremental, push-driven, eviction-aware.
99
- const recentAvg = live.rolling('5m', { cpu: 'avg' });
100
- recentAvg.on('event', (e) => render(e.get('cpu')));
101
-
102
- // 4. Snapshot to a TimeSeries for batch analytics at any time.
103
- const snap = live.toTimeSeries();
104
- ```
105
-
106
- The full live surface (`filter`, `map`, `select`, `window`, `aggregate`,
107
- `rolling`, `reduce`, `diff`, `rate`, `pctChange`, `fill`, `cumulative`,
108
- `sample`) is incremental — events flow, views emit, retention bounds
109
- memory.
110
-
111
- ## Quick start: multi-entity
112
-
113
- `partitionBy` routes events into per-key buffers. Every stateful
114
- operator downstream of `partitionBy` runs per-partition automatically:
115
-
116
- ```ts
117
- const perHost = cpu
118
- .partitionBy('host')
119
- .rolling('5m', { cpu: 'avg', cpu_sd: 'stdev' });
120
-
121
- // .collect() fans the per-partition outputs back into a flat TimeSeries
122
- // with the partition key auto-injected as a column.
123
- const flat = perHost.collect();
35
+ export function CpuChart() {
36
+ return (
37
+ <ChartContainer width={640}>
38
+ <ChartRow height={240}>
39
+ <Layers>
40
+ <LineChart series={series} column="cpu" as="cpu" />
41
+ </Layers>
42
+ </ChartRow>
43
+ </ChartContainer>
44
+ );
45
+ }
124
46
  ```
125
47
 
126
- Same shape on the live side`live.partitionBy('host')` returns a
127
- `LivePartitionedSeries` whose `rolling` / `fill` / `diff` / `sample`
128
- methods all maintain per-partition state.
48
+ The container **infers the x-axis from the series** hand it a `ValueSeries`
49
+ (`series.byValue('distance')`) and the same `<LineChart>` plots against distance
50
+ instead of time, with no axis-type prop.
129
51
 
130
- ## Quick start: bounded-memory sampling
52
+ ## What's in the box
131
53
 
132
- At firehose rates, a long rolling baseline blows the heap. `sample({
133
- stride: N })` decouples baseline length from event rate; chain it
134
- between `partitionBy` and `rolling`:
135
-
136
- ```ts
137
- // Per-host 1-in-10 stride feeding a per-host 5m baseline.
138
- live
139
- .partitionBy('host')
140
- .sample({ stride: 10 })
141
- .rolling('5m', { cpu_avg: 'avg', cpu_sd: 'stdev' });
142
- ```
143
-
144
- For visualization, the snapshot side ships reservoir sampling too —
145
- single-pass Algorithm R, sorted by key, fixed point count regardless of
146
- source size:
147
-
148
- ```ts
149
- const points = series.sample({ reservoir: { size: 500 } }).toRows();
150
- // 500 uncorrelated points drawn uniformly from the source.
151
- ```
152
-
153
- ## Performance
154
-
155
- pond-ts is faster on **every** comparable operation, with no regressions —
156
- a **~17x** geometric-mean speedup across the measurable ops, plus a handful
157
- of transforms (`select` / `rename`) that are **effectively instant** (O(1)
158
- column rebinds, below the timer's resolution). The advantage grows with data
159
- size.
160
-
161
- | Category | Speedup (N=16k) | Notes |
162
- | ----------------- | ---------------------------------------------------- | --------------------------------------------- |
163
- | **Rate** | ~120x | Single columnar walk vs Pipeline |
164
- | **Fill** | 77–87x | Single columnar pass vs Pipeline per strategy |
165
- | **Aggregation** | 57–82x | O(N+B) bucketing vs O(N×B) Pipeline |
166
- | **Statistics** | 18–80x | Typed-array reduce vs ImmutableJS iteration |
167
- | **Alignment** | 42x | Forward cursor vs repeated binary search |
168
- | **Construction** | 13x | Columnar intake vs ImmutableJS wrapping |
169
- | **Chained** | 8x | Derived constructors vs per-step Pipeline |
170
- | **Transforms** | `select`/`rename` instant; `collapse` 30x; `map` ~4x | Column reshapes vs Pipeline |
171
- | **Event access** | 6x | Array indexing vs ImmutableJS `get()` |
172
- | **Serialization** | 4x | Lightweight columnar representation |
173
-
174
- See the [full benchmark results](website/docs/reference/benchmarks.mdx)
175
- for detailed numbers. Run locally:
176
-
177
- ```sh
178
- npm run build && node packages/core/bench/vs-pondjs.cjs
179
- ```
54
+ - **Charts** `LineChart`, `AreaChart`, `BarChart`, `ScatterChart`, `BoxPlot`,
55
+ `BandChart`.
56
+ - **Layout** — `ChartContainer` / `ChartRow` / `Layers` composition, with
57
+ `YAxis`, `XAxis`, and `TimeAxis`.
58
+ - **Interaction** — a cursor system (staffed flag, per-row cursor modes, value
59
+ readouts).
60
+ - **Theming** — `defaultTheme` and `estelaTheme`.
180
61
 
181
62
  ## Documentation
182
63
 
183
- The full guide is at **<https://pjm17971.github.io/pond-ts/>**.
184
-
185
- - **[Start here](https://pjm17971.github.io/pond-ts/docs/)**
186
- — five-minute walkthrough with batch, live, and React examples.
187
- - **[Concepts](https://pjm17971.github.io/pond-ts/docs/start-here/concepts)**
188
- — temporal keys, sequences, windowing, partitioning, triggers, late
189
- data.
190
- - **[Transforms reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/transforms/queries)**
191
- — every batch operator (queries, aggregation, alignment, rolling,
192
- smoothing, sampling, cleaning, reshape, anomaly detection).
193
- - **[Live reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/live/live-series)**
194
- — `LiveSeries`, live transforms, triggering.
195
- - **[How-to guides](https://pjm17971.github.io/pond-ts/docs/how-to-guides)**
196
- — building a dashboard, ingesting messy data.
197
- - **[API reference (auto-generated)](https://pjm17971.github.io/pond-ts/generated-api/core/)**
198
- — TypeDoc output, every public class and method.
199
- - **[CHANGELOG](./CHANGELOG.md)** — what shipped in each release.
200
-
201
- ## Examples
202
-
203
- - **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
204
- — a working React dashboard that streams synthetic per-host CPU /
205
- request metrics, computes per-host rolling baselines, flags anomalies
206
- against ±σ bands, and renders everything as live line and bar charts
207
- (~600 lines of TypeScript). Walked through end-to-end in
208
- [Building a dashboard](website/docs/how-to-guides/dashboard-guide.mdx).
209
-
210
- ## Develop
211
-
212
- The repo is an npm-workspaces monorepo with two published packages
213
- (`pond-ts`, `@pond-ts/react`). Node 18+ for runtime; Node 20+ for the
214
- docs site (Docusaurus).
215
-
216
- ```sh
217
- npm install # one-time, hoists deps for both packages
218
- npm run build # build both packages
219
- npm test # runtime + type-level tests on both packages
220
- npm run format # prettier write across the repo
221
- npm run verify # format check + build + test (CI parity)
222
- ```
223
-
224
- `packages/core/` is the `pond-ts` package; `packages/react/` is
225
- `@pond-ts/react`. Docs live in `website/`.
64
+ Guides, the component reference, and live examples live at
65
+ **<https://pjm17971.github.io/pond-ts/>**. Source and issues:
66
+ [github.com/pjm17971/pond-ts](https://github.com/pjm17971/pond-ts).
226
67
 
227
68
  ## License
228
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
4
4
  "private": false,
5
5
  "description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "tsc -p tsconfig.json",
31
- "prepack": "cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
31
+ "prepack": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
32
32
  "test": "npm run test:type && npm run test:runtime",
33
33
  "test:type": "tsc -p tsconfig.types.json",
34
34
  "test:runtime": "vitest run",
@@ -1,110 +0,0 @@
1
- import { type AnnotationSpec } from './context.js';
2
- /**
3
- * Greedy left→right lane packing for the **top-flag** labels (markers + regions): a
4
- * label that would overlap the one to its left drops to the next free lane below,
5
- * so close-in-x labels stack instead of colliding (and a dragged label slides under
6
- * its neighbour). Returns slot-key → lane (0 = top). Baselines, whose labels anchor
7
- * at the left at their own y, don't participate.
8
- */
9
- export declare function computeLabelLanes(annotations: readonly AnnotationSpec[], toPixel: (axisX: number) => number): Map<symbol, number>;
10
- export interface MarkerProps {
11
- /** x position in axis units — epoch ms on a time axis, the value on a value
12
- * axis. (The generalisation of the mockup's "time line": a mark at an x, time
13
- * or value.) */
14
- at: number;
15
- /** Chip label; omit to auto-label with the shared x formatter (the axis's). */
16
- label?: string;
17
- /** Stable consumer id — a click reports it via the container's
18
- * `onSelectAnnotation`, so the consumer can track which mark is selected. */
19
- id?: string;
20
- /** Controlled selection — brightens to the front (level 1). Handles are an
21
- * edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
22
- selected?: boolean;
23
- /** Whether the mark responds to hover + selection (default `true`). When
24
- * `false` it's inert background context — drawn at the back (level 3) always,
25
- * no hover, no select, no edit. */
26
- selectable?: boolean;
27
- /** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
28
- * remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
29
- hovered?: boolean;
30
- /** When `true`, this mark is in **single-annotation edit** (the double-click
31
- * target): handles stay out, it's draggable, and it reads as level 1 — while
32
- * other marks stay static. Independent of the container's global
33
- * `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
34
- * `editingId` and sets `editing={editingId === id}`). */
35
- editing?: boolean;
36
- /** Make the marker **editable** (in edit mode): dragging its line reports the
37
- * new `at` (controlled — wire it back to `at`). The whole line moves. */
38
- onChange?: (at: number) => void;
39
- }
40
- /** A vertical line at an x position (a time, a distance, a lap boundary). */
41
- export declare function Marker({ at, label, id, selected, selectable, hovered, editing, onChange, }: MarkerProps): import("react/jsx-runtime").JSX.Element;
42
- export interface BaselineProps {
43
- /** y value in the linked axis's units. */
44
- value: number;
45
- /** Which `<YAxis>` (by id) to measure against; omit for the row's default axis. */
46
- axis?: string;
47
- /** Chip label; omit to format `value` with that axis's formatter. */
48
- label?: string;
49
- /** Stable consumer id — a click reports it via `onSelectAnnotation`. */
50
- id?: string;
51
- /** Controlled selection — brightens to the front (level 1). Handles are an
52
- * edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
53
- selected?: boolean;
54
- /** Whether the baseline responds to hover + selection (default `true`). When
55
- * `false` it's inert background context — drawn at the back (level 3) always. */
56
- selectable?: boolean;
57
- /** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
58
- * remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
59
- hovered?: boolean;
60
- /** When `true`, this mark is in **single-annotation edit** (the double-click
61
- * target): handles stay out, it's draggable, and it reads as level 1 — while
62
- * other marks stay static. Independent of the container's global
63
- * `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
64
- * `editingId` and sets `editing={editingId === id}`). */
65
- editing?: boolean;
66
- /** Make the baseline **editable** (in edit mode): dragging it vertically reports
67
- * the new `value` (controlled — wire it back to `value`). */
68
- onChange?: (value: number) => void;
69
- }
70
- /** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
71
- * Its label anchors at the left, at the line's height. */
72
- export declare function Baseline({ value, axis, label, id, selected, selectable, hovered, editing, onChange, }: BaselineProps): import("react/jsx-runtime").JSX.Element | null;
73
- export interface RegionProps {
74
- /** Start x in axis units (time or value). */
75
- from: number;
76
- /** End x in axis units. */
77
- to: number;
78
- /** Chip label; omit to auto-label `from–to` with the shared x formatter. */
79
- label?: string;
80
- /** Stable consumer id — a click (or double-click outside edit) reports it via
81
- * `onSelectAnnotation`. */
82
- id?: string;
83
- /** Controlled selection — brightens to the front (level 1; the body too). Edge
84
- * handles are an edit-mode hover affordance, not a selection cue. Ignored if not
85
- * `selectable`. */
86
- selected?: boolean;
87
- /** Whether the region responds to hover + selection (default `true`). When
88
- * `false` it's inert background context — drawn at the back (level 3) always,
89
- * and the double-click hit-test skips it. */
90
- selectable?: boolean;
91
- /** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
92
- * remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
93
- hovered?: boolean;
94
- /** When `true`, this mark is in **single-annotation edit** (the double-click
95
- * target): handles stay out, it's draggable, and it reads as level 1 — while
96
- * other marks stay static. Independent of the container's global
97
- * `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
98
- * `editingId` and sets `editing={editingId === id}`). */
99
- editing?: boolean;
100
- /** Make the region **editable** (in edit mode): drag the body to move it (both
101
- * edges shift), drag an edge to resize. Reports the new `{ from, to }`. */
102
- onChange?: (next: {
103
- from: number;
104
- to: number;
105
- }) => void;
106
- }
107
- /** A shaded span over an x range — a lap, a zone, a selected interval. Its label
108
- * flies as a flag off the left edge. */
109
- export declare function Region({ from, to, label, id, selected, selectable, hovered, editing, onChange, }: RegionProps): import("react/jsx-runtime").JSX.Element;
110
- //# sourceMappingURL=annotations.d.ts.map
@@ -1,459 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
- import { ContainerContext, RowContext, } from './context.js';
4
- import { flagChipStyle, flagChipX } from './chip.js';
5
- import { useSlotKey } from './use-slot-key.js';
6
- /**
7
- * User-authored **annotations** — marks you place *on* a chart, in a register
8
- * deliberately distinct from the data: `<Region>` (a shaded x-span), `<Baseline>`
9
- * (a horizontal value line), and `<Marker>` (a vertical x line). All three render
10
- * in the theme's turquoise {@link ChartTheme.annotation} register so a placed mark
11
- * never reads as data ("the data stays foam; the marks you place are turquoise").
12
- *
13
- * They are children of `<Layers>` (so they share the plot's coordinate space) and
14
- * paint an SVG overlay above the data canvas + below the cursor. Each label is a
15
- * **flag** (the cursor value flag's shape). **Brightness encodes depth** — a mark
16
- * draws at one of three {@link ChartTheme.annotation | depth} levels (forward =
17
- * brightest); `selectable={false}` pins it at the back as inert context.
18
- *
19
- * **Three interaction modes** (all controlled — the consumer holds the ids):
20
- * - **Inspect-select** (any mode): a single click selects a mark
21
- * ({@link ContainerFrame.onSelectAnnotation}); `hovered`/`onHoverAnnotation` sync
22
- * hover both ways (e.g. with a legend); both come **forward** (selected = level 1,
23
- * hover = level 2).
24
- * - **Single-annotation edit** (any mode): a double-click requests edit of just that
25
- * mark ({@link ContainerFrame.onEditAnnotation}); the consumer sets its `editing`
26
- * prop → it gains always-on handles and becomes draggable while the rest stay
27
- * static. An empty plot click exits (fires `onSelectAnnotation(null)`).
28
- * - **Global edit** ({@link ContainerFrame.editAnnotations}): the data cursor steps
29
- * aside and *every* mark with an `onChange` is editable, handles on hover; armed
30
- * {@link ContainerFrame.creating | create tools} draw new marks.
31
- *
32
- * Editing is controlled: a `<Region>` body drags to **move**, its edges **resize**;
33
- * a `<Marker>`/`<Baseline>` drags whole — each reports via `onChange`. An edit hit
34
- * area **claims the gesture** (pointer capture + `stopPropagation`) so a drag never
35
- * starts a pan. Marks register with the container
36
- * ({@link ContainerFrame.annotations}), which draws each mark's **guide** across the
37
- * other rows and lets a drag **snap** to other marks' x-positions.
38
- */
39
- /** Fallback when a theme defines no `annotation` token — a neutral turquoise. */
40
- const DEFAULT_ANNOTATION = {
41
- color: '#14b8a6',
42
- fillOpacity: 0.1,
43
- depth: [1, 0.7, 0.4],
44
- };
45
- /** Handle-pill geometry (px) — long axis vs short axis. */
46
- const HANDLE_LONG = 18;
47
- const HANDLE_SHORT = 6;
48
- /** How wide a line's invisible grab area is, each side (px). */
49
- const HIT_PAD = 5;
50
- /** How wide a region edge's resize grab area is (px) — sits over the body. */
51
- const EDGE_GRAB = 8;
52
- /** Flag-chip top offset (px from the row top) — shared so labels align. */
53
- const FLAG_TOP = 2;
54
- /** Depth level (1 = forward/brightest … 3 = back/dimmest) → an index into the
55
- * theme's `depth` ramp. Brighter reads as more forward, i.e. more attention.
56
- *
57
- * A mark's LINES (marker/baseline line, region edges) and a region's BODY fill
58
- * can sit at different levels: in edit mode the lines come fully forward (level
59
- * 1) while a region body stays one step back (level 2), so the edges read as the
60
- * grabbable thing. A non-`selectable` mark is inert background context — always
61
- * level 3, ignoring hover + selection. */
62
- function lineLevel(selectable, editing, hovering, selected) {
63
- if (!selectable)
64
- return 3;
65
- if (selected)
66
- return 1;
67
- if (editing)
68
- return 1; // edit mode brings the structural lines forward
69
- if (hovering)
70
- return 2;
71
- return 3;
72
- }
73
- /** Region body-fill level — like {@link lineLevel} but one step back in edit mode. */
74
- function bodyLevel(selectable, editing, hovering, selected) {
75
- if (!selectable)
76
- return 3;
77
- if (selected)
78
- return 1;
79
- if (editing || hovering)
80
- return 2;
81
- return 3;
82
- }
83
- /** Region body-fill opacity multiplier by depth level (the fill is subtle so the
84
- * data reads through; it lifts as the region comes forward). */
85
- const FILL_MULT = [1.6, 1.3, 1];
86
- /** Pick the value for a depth level (1–3) from a three-stop ramp. Indexes by a
87
- * literal so the lookup is total (no out-of-range `undefined`). */
88
- function rampAt(ramp, level) {
89
- return level === 1 ? ramp[0] : level === 2 ? ramp[1] : ramp[2];
90
- }
91
- /** The full-plot overlay each annotation paints into — above the data canvas,
92
- * below the cursor. Inert to the pointer by default; an edit-mode hit area opts
93
- * back in to `pointerEvents: auto` for itself only. */
94
- const overlayStyle = {
95
- position: 'absolute',
96
- top: 0,
97
- left: 0,
98
- pointerEvents: 'none',
99
- };
100
- /** Read the container + row frames an annotation needs, or throw if misplaced. */
101
- function useAnnotationFrame(name) {
102
- const container = useContext(ContainerContext);
103
- if (container === null) {
104
- throw new Error(`<${name}> must be rendered inside a <ChartContainer>`);
105
- }
106
- const row = useContext(RowContext);
107
- if (row === null) {
108
- throw new Error(`<${name}> must be rendered inside a <ChartRow>`);
109
- }
110
- const ann = container.theme.annotation ?? DEFAULT_ANNOTATION;
111
- return { container, row, ann };
112
- }
113
- /** Register this annotation with the container (so it can draw the mark's guide on
114
- * other rows, order regions, and serve snap targets), keyed by the caller's stable
115
- * per-instance slot key; unregister on unmount. `xs` should be memoised by the
116
- * caller so the effect only re-runs when the position actually moves. */
117
- function useRegisterAnnotation(container, key, id, rowKey, kind, xs, selected, selectable, editing, label) {
118
- const { registerAnnotation, unregisterAnnotation } = container;
119
- useEffect(() => () => unregisterAnnotation(key), [unregisterAnnotation, key]);
120
- useEffect(() => {
121
- registerAnnotation(key, {
122
- key,
123
- id,
124
- kind,
125
- rowKey,
126
- xs,
127
- selected,
128
- selectable,
129
- editing,
130
- label,
131
- });
132
- }, [
133
- registerAnnotation,
134
- key,
135
- id,
136
- kind,
137
- rowKey,
138
- xs,
139
- selected,
140
- selectable,
141
- editing,
142
- label,
143
- ]);
144
- }
145
- /** Vertical px between stacked label lanes. */
146
- const LANE_H = 22;
147
- /** Rough chip-width model for overlap detection (monospace-ish: chars × width +
148
- * padding) and the min px gap kept between two labels sharing a lane. */
149
- const LABEL_CHAR_W = 7;
150
- const LABEL_PAD = 16;
151
- const LANE_GAP = 6;
152
- /**
153
- * Greedy left→right lane packing for the **top-flag** labels (markers + regions): a
154
- * label that would overlap the one to its left drops to the next free lane below,
155
- * so close-in-x labels stack instead of colliding (and a dragged label slides under
156
- * its neighbour). Returns slot-key → lane (0 = top). Baselines, whose labels anchor
157
- * at the left at their own y, don't participate.
158
- */
159
- export function computeLabelLanes(annotations, toPixel) {
160
- const flags = annotations
161
- .filter((a) => (a.kind === 'marker' || a.kind === 'region') &&
162
- a.label.length > 0 &&
163
- a.xs.length > 0)
164
- .map((a) => {
165
- const ax = a.kind === 'region' ? Math.min(a.xs[0], a.xs[1]) : a.xs[0];
166
- return {
167
- key: a.key,
168
- left: toPixel(ax),
169
- width: a.label.length * LABEL_CHAR_W + LABEL_PAD,
170
- };
171
- })
172
- .sort((p, q) => p.left - q.left);
173
- const laneEnds = []; // right-px of the last label placed in each lane
174
- const lanes = new Map();
175
- for (const f of flags) {
176
- let lane = 0;
177
- while (lane < laneEnds.length && laneEnds[lane] + LANE_GAP > f.left) {
178
- lane += 1;
179
- }
180
- laneEnds[lane] = f.left + f.width;
181
- lanes.set(f.key, lane);
182
- }
183
- return lanes;
184
- }
185
- /** A mark's hover state, synced both ways with the consumer. The effective hover
186
- * is the local pointer hover **OR** the controlled `hovered` prop (so a legend row
187
- * can light the mark remotely); `reportHover` mirrors local pointer enter/leave out
188
- * to {@link ContainerFrame.onHoverAnnotation} by `id` (so the mark can light the
189
- * legend). A mark with no `id` keeps its hover purely local. */
190
- function useAnnotationHover(container, id, hovered) {
191
- const [selfHover, setSelfHover] = useState(false);
192
- const hovering = selfHover || hovered === true;
193
- const reportHover = (h) => {
194
- setSelfHover(h);
195
- if (id !== undefined)
196
- container.onHoverAnnotation?.(h ? id : null);
197
- };
198
- return { hovering, reportHover };
199
- }
200
- /** Pixel radius within which a drag snaps to a guideline (another mark's x). */
201
- const SNAP_PX = 6;
202
- /**
203
- * Snap a dragged plot-pixel `px` to the nearest **guideline** — another
204
- * annotation's x — within {@link SNAP_PX}. Returns that guideline's **axis** value
205
- * to snap to, or `null` if none is near (the caller keeps the raw position).
206
- * Excludes the dragging mark's own `key`, and reads the same registry the guides
207
- * draw from, so a drag visibly clicks onto the lines you can see.
208
- */
209
- function snapToGuides(container, selfKey, px) {
210
- let best = null;
211
- let bestDist = SNAP_PX;
212
- for (const a of container.annotations) {
213
- if (a.key === selfKey)
214
- continue;
215
- for (const tx of a.xs) {
216
- const d = Math.abs(container.xScale(tx) - px);
217
- if (d < bestDist) {
218
- bestDist = d;
219
- best = tx;
220
- }
221
- }
222
- }
223
- return best;
224
- }
225
- /** A label chip — the cursor value flag's shape (shared {@link flagChipStyle}:
226
- * filled, no outline) with text in the annotation register. */
227
- function Chip({ theme, color, style, children, }) {
228
- return (_jsx("div", { style: { ...flagChipStyle(theme), color, ...style }, children: children }));
229
- }
230
- /** A non-interactive handle pill, shown on hover (edit mode) / when selected. */
231
- function Pill({ cx, cy, w, h, color, }) {
232
- return (_jsx("rect", { x: cx - w / 2, y: cy - h / 2, width: w, height: h, rx: 3, fill: color, style: { pointerEvents: 'none' } }));
233
- }
234
- /**
235
- * A transparent hit rect over a selectable mark. It **always** reports hover
236
- * (`onHover`, → level 2 even with edit off), and a **single click that didn't drag
237
- * selects** the mark (`onSelect`) in *any* mode — the inspect-select — while a
238
- * **double-click edits** it (`onEdit` — the consumer flips it into single-annotation
239
- * edit). Both clicks `stopPropagation` so they never reach the plot's data-select /
240
- * deselect. Only when `editable` does it claim the drag gesture (`stopPropagation` +
241
- * guarded pointer capture, so a drag never starts a pan) and report the pointer's
242
- * **plot-pixel** position on press (`onDragStart`) / move (`onDrag`). With editing
243
- * off, press / move bubble so a pan reads straight through.
244
- */
245
- function DragArea({ x, y, w, h, cursor, editable, onHover, onSelect, onEdit, onDragStart, onDrag, }) {
246
- const dragging = useRef(false);
247
- // Tracks whether this press became a drag (moved past a few px) — a click that
248
- // didn't drag selects instead of edits. Tracked in *both* modes so a pan-drag
249
- // started on the mark (edit off) doesn't fire a spurious select on release.
250
- const moved = useRef(false);
251
- const downAt = useRef(null);
252
- const at = (e) => {
253
- const svg = e.currentTarget.ownerSVGElement;
254
- if (svg === null)
255
- return [0, 0];
256
- const r = svg.getBoundingClientRect();
257
- return [e.clientX - r.left, e.clientY - r.top];
258
- };
259
- return (_jsx("rect", { x: x, y: y, width: Math.max(w, 1), height: Math.max(h, 1), fill: "transparent", style: { pointerEvents: 'auto', cursor }, onPointerEnter: () => onHover(true), onPointerLeave: () => {
260
- if (!dragging.current)
261
- onHover(false);
262
- }, onPointerDown: (e) => {
263
- const p = at(e);
264
- moved.current = false;
265
- downAt.current = p; // tracked in both modes for the click/drag guard
266
- if (!editable)
267
- return; // edit off: let it bubble (pan reads through)
268
- e.stopPropagation(); // claim the gesture — don't let the plot start a pan
269
- dragging.current = true;
270
- onDragStart?.(p[0], p[1]);
271
- try {
272
- e.currentTarget.setPointerCapture(e.pointerId);
273
- }
274
- catch {
275
- /* ignore (synthetic / already-released pointer) */
276
- }
277
- }, onPointerMove: (e) => {
278
- const [px, py] = at(e);
279
- const d = downAt.current;
280
- if (d !== null && Math.hypot(px - d[0], py - d[1]) > 3) {
281
- moved.current = true;
282
- }
283
- if (!editable || !dragging.current)
284
- return;
285
- e.stopPropagation();
286
- onDrag(px, py);
287
- }, onPointerUp: (e) => {
288
- if (!dragging.current)
289
- return;
290
- e.stopPropagation();
291
- try {
292
- e.currentTarget.releasePointerCapture(e.pointerId);
293
- }
294
- catch {
295
- /* ignore */
296
- }
297
- dragging.current = false;
298
- onHover(false);
299
- },
300
- // Click (no drag) selects; double-click edits. Stop both so a mark click
301
- // never reaches the plot's data-select / deselect.
302
- onClick: (e) => {
303
- e.stopPropagation();
304
- if (!moved.current)
305
- onSelect?.();
306
- }, onDoubleClick: (e) => {
307
- e.stopPropagation();
308
- onEdit?.();
309
- } }));
310
- }
311
- /** A vertical line at an x position (a time, a distance, a lap boundary). */
312
- export function Marker({ at, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
313
- const { container, row, ann } = useAnnotationFrame('Marker');
314
- const selfKey = useSlotKey();
315
- const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
316
- // Draggable right now: global edit mode OR this mark's single-edit flag, and no
317
- // tool armed, and it has an onChange to report to.
318
- const editable = (container.editAnnotations || editing) &&
319
- container.creating === null &&
320
- onChange !== undefined;
321
- const xs = useMemo(() => [at], [at]);
322
- const text = label ?? container.formatTime(at);
323
- useRegisterAnnotation(container, selfKey, id, row.rowKey, 'marker', xs, selected, selectable, editing, text);
324
- // No select/edit while a create tool is armed — the chart is in draw mode then.
325
- const select = id !== undefined && container.creating === null
326
- ? () => container.onSelectAnnotation?.(id)
327
- : undefined;
328
- const edit = id !== undefined && container.creating === null
329
- ? () => container.onEditAnnotation?.(id)
330
- : undefined;
331
- const x = container.xScale(at);
332
- const h = row.height;
333
- const opacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
334
- const showHandle = editable && (editing || hovering);
335
- const lane = container.labelLanes.get(selfKey) ?? 0;
336
- return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("line", { x1: x, y1: 0, x2: x, y2: h, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: x, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })), selectable && (_jsx(DragArea, { x: x - HIT_PAD, y: 0, w: 2 * HIT_PAD, h: h, cursor: editing ? 'ew-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.(snapToGuides(container, selfKey, px) ??
337
- +container.xScale.invert(px)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
338
- top: `${FLAG_TOP + lane * LANE_H}px`,
339
- ...flagChipX(x, container.plotWidth),
340
- }, children: text })] }));
341
- }
342
- /** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
343
- * Its label anchors at the left, at the line's height. */
344
- export function Baseline({ value, axis, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
345
- const { container, row, ann } = useAnnotationFrame('Baseline');
346
- const selfKey = useSlotKey();
347
- const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
348
- // Draggable right now: global edit mode OR this mark's single-edit flag, and no
349
- // tool armed, and it has an onChange to report to.
350
- const editable = (container.editAnnotations || editing) &&
351
- container.creating === null &&
352
- onChange !== undefined;
353
- // A horizontal line casts no vertical guide — register with no xs (still
354
- // tracked for ordering / future use). No snap target either (the guidelines are
355
- // vertical; a baseline drags vertically).
356
- const xs = useMemo(() => [], []);
357
- useRegisterAnnotation(container, selfKey, id, row.rowKey, 'baseline', xs, selected, selectable, editing, label ?? '');
358
- // No select/edit while a create tool is armed — the chart is in draw mode then.
359
- const select = id !== undefined && container.creating === null
360
- ? () => container.onSelectAnnotation?.(id)
361
- : undefined;
362
- const edit = id !== undefined && container.creating === null
363
- ? () => container.onEditAnnotation?.(id)
364
- : undefined;
365
- const axisId = axis ?? row.defaultAxisId;
366
- const yScale = row.yScales.get(axisId);
367
- // The axis may not have resolved yet (a layer mounts before its <YAxis>); skip
368
- // until its scale exists rather than guessing a domain.
369
- if (yScale === undefined)
370
- return null;
371
- const y = yScale(value);
372
- const w = container.plotWidth;
373
- const opacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
374
- const showHandle = editable && (editing || hovering);
375
- const fmt = row.formats.get(axisId);
376
- const text = label ?? (fmt ? fmt(value) : String(value));
377
- // Handle pill near the right end (clears the left-anchored label).
378
- const handleX = w - 14;
379
- return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: { top: `${y}px`, left: '2px', transform: 'translateY(-50%)' }, children: text })] }));
380
- }
381
- /** A shaded span over an x range — a lap, a zone, a selected interval. Its label
382
- * flies as a flag off the left edge. */
383
- export function Region({ from, to, label, id, selected = false, selectable = true, hovered, editing = false, onChange, }) {
384
- const { container, row, ann } = useAnnotationFrame('Region');
385
- const selfKey = useSlotKey();
386
- const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
387
- // Draggable right now: global edit mode OR this mark's single-edit flag, and no
388
- // tool armed, and it has an onChange to report to.
389
- const editable = (container.editAnnotations || editing) &&
390
- container.creating === null &&
391
- onChange !== undefined;
392
- const xs = useMemo(() => [from, to], [from, to]);
393
- const text = label ?? `${container.formatTime(from)}–${container.formatTime(to)}`;
394
- useRegisterAnnotation(container, selfKey, id, row.rowKey, 'region', xs, selected, selectable, editing, text);
395
- // No select/edit while a create tool is armed — the chart is in draw mode then.
396
- const select = id !== undefined && container.creating === null
397
- ? () => container.onSelectAnnotation?.(id)
398
- : undefined;
399
- const edit = id !== undefined && container.creating === null
400
- ? () => container.onEditAnnotation?.(id)
401
- : undefined;
402
- const xa = container.xScale(from);
403
- const xb = container.xScale(to);
404
- const left = Math.min(xa, xb);
405
- const spanW = Math.abs(xb - xa);
406
- const h = row.height;
407
- // The edges (lines) come fully forward in edit mode; the body fill sits one step
408
- // back (so the edges read as the grabbable thing). Both jump to level 1 selected.
409
- const edgeOpacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
410
- const fillOpacity = ann.fillOpacity *
411
- rampAt(FILL_MULT, bodyLevel(selectable, editable, hovering, selected));
412
- const showHandles = editable && (editing || hovering);
413
- const lane = container.labelLanes.get(selfKey) ?? 0;
414
- // Body move-drag: capture the start position + pointer on press, then move by
415
- // the TOTAL delta from there, so the *raw* position accumulates from a fixed
416
- // origin. Snap is applied only to the output — never fed back into this
417
- // accumulator — so once you drag past SNAP_PX the region releases cleanly
418
- // instead of re-snapping on every small move.
419
- const dragRef = useRef(null);
420
- const edge = (atX) => (_jsx("line", { x1: atX, y1: 0, x2: atX, y2: h, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, shapeRendering: "crispEdges" }));
421
- return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("rect", { x: left, y: 0, width: spanW, height: h, fill: ann.color, opacity: fillOpacity }), edge(xa), edge(xb), showHandles && (_jsxs(_Fragment, { children: [_jsx(Pill, { cx: xa, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color }), _jsx(Pill, { cx: xb, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })] })), selectable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: left, y: 0, w: spanW, h: h, cursor: editing ? 'grab' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragStart: (px) => {
422
- dragRef.current = { from, to, startPx: px };
423
- }, onDrag: (px) => {
424
- const s = dragRef.current;
425
- if (s === null)
426
- return;
427
- // Raw position = start + TOTAL pointer delta (snap-independent),
428
- // so dragging past SNAP_PX escapes a snapped edge.
429
- const delta = +container.xScale.invert(px) -
430
- +container.xScale.invert(s.startPx);
431
- let nf = s.from + delta;
432
- let nt = s.to + delta;
433
- // Snap whichever edge lands near a guideline, keeping the width —
434
- // output only, so the raw drift above can pull free of it.
435
- const sf = snapToGuides(container, selfKey, container.xScale(nf));
436
- const st = snapToGuides(container, selfKey, container.xScale(nt));
437
- if (sf !== null) {
438
- nt += sf - nf;
439
- nf = sf;
440
- }
441
- else if (st !== null) {
442
- nf += st - nt;
443
- nt = st;
444
- }
445
- onChange?.({ from: nf, to: nt });
446
- } }), editable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: xa - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.({
447
- from: snapToGuides(container, selfKey, px) ??
448
- +container.xScale.invert(px),
449
- to,
450
- }) }), _jsx(DragArea, { x: xb - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.({
451
- from,
452
- to: snapToGuides(container, selfKey, px) ??
453
- +container.xScale.invert(px),
454
- }) })] }))] }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
455
- top: `${FLAG_TOP + lane * LANE_H}px`,
456
- ...flagChipX(left, container.plotWidth),
457
- }, children: text })] }));
458
- }
459
- //# sourceMappingURL=annotations.js.map
package/dist/chip.d.ts DELETED
@@ -1,23 +0,0 @@
1
- import type { CSSProperties } from 'react';
2
- import type { ChartTheme } from './theme.js';
3
- /**
4
- * The shared **chip** look — one source of truth for the cursor's value flag and
5
- * the annotation labels, so a placed label and a cursor flag read as the same
6
- * object. A filled panel (the theme's `chip.background`) with crisp tabular text
7
- * and **no outline** — the fill delineates it (a border read as a hard edge on a
8
- * dark ground). The caller layers on `color` (the series / annotation hue) and the
9
- * position (`top` + `left`/`right`).
10
- *
11
- * Note: on a light theme whose `chip.background` doesn't contrast with the plot
12
- * ground, the borderless chip needs another delineator (a subtle shadow, or a
13
- * contrasting chip background) — a token to settle before this ships.
14
- */
15
- export declare function flagChipStyle(theme: ChartTheme): CSSProperties;
16
- /**
17
- * Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
18
- * `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
19
- * in-plot. Shared by the cursor value flag and the annotation labels so a chip
20
- * sits **identically** relative to its pole in both.
21
- */
22
- export declare function flagChipX(x: number, plotWidth: number): CSSProperties;
23
- //# sourceMappingURL=chip.d.ts.map
package/dist/chip.js DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * The shared **chip** look — one source of truth for the cursor's value flag and
3
- * the annotation labels, so a placed label and a cursor flag read as the same
4
- * object. A filled panel (the theme's `chip.background`) with crisp tabular text
5
- * and **no outline** — the fill delineates it (a border read as a hard edge on a
6
- * dark ground). The caller layers on `color` (the series / annotation hue) and the
7
- * position (`top` + `left`/`right`).
8
- *
9
- * Note: on a light theme whose `chip.background` doesn't contrast with the plot
10
- * ground, the borderless chip needs another delineator (a subtle shadow, or a
11
- * contrasting chip background) — a token to settle before this ships.
12
- */
13
- export function flagChipStyle(theme) {
14
- return {
15
- position: 'absolute',
16
- background: theme.chip?.background,
17
- borderRadius: '3px',
18
- padding: '0 4px',
19
- fontFamily: theme.font.family,
20
- fontSize: `${theme.font.size}px`,
21
- fontVariantNumeric: 'tabular-nums',
22
- whiteSpace: 'nowrap',
23
- pointerEvents: 'none',
24
- lineHeight: 1.5,
25
- };
26
- }
27
- /** Gap (px) between a flag chip and its pole — the cursor staff or an annotation's
28
- * line — so the chip floats just beside the pole rather than sitting on it. */
29
- const FLAG_GAP = 4;
30
- /** Past this fraction of the plot, a flag flips to the left of its pole to stay in. */
31
- const FLAG_FLIP = 0.85;
32
- /**
33
- * Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
34
- * `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
35
- * in-plot. Shared by the cursor value flag and the annotation labels so a chip
36
- * sits **identically** relative to its pole in both.
37
- */
38
- export function flagChipX(x, plotWidth) {
39
- return x > plotWidth * FLAG_FLIP
40
- ? { right: `${plotWidth - x + FLAG_GAP}px` }
41
- : { left: `${x + FLAG_GAP}px` };
42
- }
43
- //# sourceMappingURL=chip.js.map