@pond-ts/charts 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -3
- package/dist/ChartContainer.d.ts +11 -1
- package/dist/ChartContainer.js +20 -2
- package/dist/ChartRow.js +10 -0
- package/dist/Layers.js +148 -25
- package/dist/XAxis.js +99 -2
- package/dist/annotations.d.ts +41 -10
- package/dist/annotations.js +130 -46
- package/dist/chip.d.ts +36 -0
- package/dist/chip.js +85 -1
- package/dist/context.d.ts +50 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/indicators.d.ts +105 -0
- package/dist/indicators.js +114 -0
- package/dist/tracker.d.ts +1 -1
- package/dist/tracker.js +5 -0
- package/package.json +3 -3
package/dist/chip.js
CHANGED
|
@@ -14,7 +14,9 @@ export function flagChipStyle(theme) {
|
|
|
14
14
|
return {
|
|
15
15
|
position: 'absolute',
|
|
16
16
|
background: theme.chip?.background,
|
|
17
|
-
|
|
17
|
+
// Square corners — a flag is a filled panel behind the number, not a pill
|
|
18
|
+
// (the rounded pill is reserved for axis indicators, see `axisPillStyle`).
|
|
19
|
+
borderRadius: '0',
|
|
18
20
|
padding: '0 4px',
|
|
19
21
|
fontFamily: theme.font.family,
|
|
20
22
|
fontSize: `${theme.font.size}px`,
|
|
@@ -24,6 +26,88 @@ export function flagChipStyle(theme) {
|
|
|
24
26
|
lineHeight: 1.5,
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Pick a readable text colour (near-black or white) for text drawn **on top of**
|
|
31
|
+
* `bg`, by its sRGB relative luminance. Handles `#rgb`/`#rrggbb` (the theme
|
|
32
|
+
* palette); any other CSS colour falls back to white. So a saturated blue/red/
|
|
33
|
+
* teal pill gets white text, a pale turquoise pill gets dark text.
|
|
34
|
+
*/
|
|
35
|
+
export function contrastText(bg) {
|
|
36
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(bg.trim());
|
|
37
|
+
const raw = m?.[1];
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return '#ffffff';
|
|
40
|
+
const h = raw.length === 3
|
|
41
|
+
? raw
|
|
42
|
+
.split('')
|
|
43
|
+
.map((c) => c + c)
|
|
44
|
+
.join('')
|
|
45
|
+
: raw;
|
|
46
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
47
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
48
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
49
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
50
|
+
return lum > 0.6 ? '#0b1220' : '#ffffff';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The **axis indicator pill** look — a *solid* filled tag in `color` with
|
|
54
|
+
* auto-contrast text (the ChartIQ / Yahoo price-tag). Distinct from
|
|
55
|
+
* {@link flagChipStyle} (a light in-plot value chip): an on-axis indicator reads
|
|
56
|
+
* as a saturated pill covering the tick, not a floating readout. Note: it does
|
|
57
|
+
* **not** set `lineHeight` — it inherits `normal`, matching a bare tick label, so
|
|
58
|
+
* a pill anchored at the same offset lines up with its tick-label neighbours (a
|
|
59
|
+
* forced lineHeight would shift the text off the tick baseline). Shared by
|
|
60
|
+
* {@link YAxisIndicator}, the crosshair axis pills, and the Baseline/Marker
|
|
61
|
+
* `indicator` pills.
|
|
62
|
+
*/
|
|
63
|
+
export function axisPillStyle(theme, color) {
|
|
64
|
+
return {
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
background: color,
|
|
67
|
+
color: contrastText(color),
|
|
68
|
+
borderRadius: '3px',
|
|
69
|
+
padding: '0 4px',
|
|
70
|
+
fontFamily: theme.font.family,
|
|
71
|
+
fontSize: `${theme.font.size}px`,
|
|
72
|
+
fontVariantNumeric: 'tabular-nums',
|
|
73
|
+
whiteSpace: 'nowrap',
|
|
74
|
+
pointerEvents: 'none',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A small triangle on an axis pill's **plot-facing edge**, pointing into the
|
|
79
|
+
* plot at the value (the callout tab). For a `right`-side pill (extending right
|
|
80
|
+
* across the gutter) it sits on the pill's left edge pointing left; for a `left`
|
|
81
|
+
* pill, the mirror. Render as an absolutely-positioned child of the pill (the
|
|
82
|
+
* pill is itself absolute, so it's the containing block); colour matches the pill.
|
|
83
|
+
*/
|
|
84
|
+
export function pointerStyle(side, color) {
|
|
85
|
+
return {
|
|
86
|
+
position: 'absolute',
|
|
87
|
+
top: '50%',
|
|
88
|
+
transform: 'translateY(-50%)',
|
|
89
|
+
width: 0,
|
|
90
|
+
height: 0,
|
|
91
|
+
borderTop: '4px solid transparent',
|
|
92
|
+
borderBottom: '4px solid transparent',
|
|
93
|
+
...(side === 'right'
|
|
94
|
+
? { left: '-5px', borderRight: `5px solid ${color}` }
|
|
95
|
+
: { right: '-5px', borderLeft: `5px solid ${color}` }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* CSS placing a value pill **on the axis gutter** at `side`: anchor its inner
|
|
100
|
+
* edge at the plot boundary (`plotWidth`) and let it overflow outward across the
|
|
101
|
+
* reserved gutter (the plot div doesn't clip), lifted with `zIndex` above the
|
|
102
|
+
* sibling axis column (rendered later in the row) so it covers the tick behind
|
|
103
|
+
* it. Shared by {@link YAxisIndicator}'s `placement='axis'` and the crosshair
|
|
104
|
+
* cursor's per-series value pills, so both sit identically on the axis.
|
|
105
|
+
*/
|
|
106
|
+
export function axisPillX(side, plotWidth) {
|
|
107
|
+
return side === 'right'
|
|
108
|
+
? { left: `${plotWidth}px`, zIndex: 3 }
|
|
109
|
+
: { right: `${plotWidth}px`, zIndex: 3 };
|
|
110
|
+
}
|
|
27
111
|
/** Gap (px) between a flag chip and its pole — the cursor staff or an annotation's
|
|
28
112
|
* line — so the chip floats just beside the pole rather than sitting on it. */
|
|
29
113
|
const FLAG_GAP = 4;
|
package/dist/context.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ import type { AxisFormat } from './format.js';
|
|
|
11
11
|
* time→pixel `xScale` follow. Y scales stay per-row (row-local data), on the
|
|
12
12
|
* {@link RowFrame}.
|
|
13
13
|
*/
|
|
14
|
+
/** Where a top-flag label sits: its lane (0 = top; overlapping labels stack
|
|
15
|
+
* down) and the chip text to render — the merged label for the representative of
|
|
16
|
+
* a coincident-marker group, `null` for the members folded into it, else the
|
|
17
|
+
* mark's own label. Computed by `computeLabelLanes`. */
|
|
18
|
+
export interface LabelPlacement {
|
|
19
|
+
readonly lane: number;
|
|
20
|
+
readonly label: string | null;
|
|
21
|
+
}
|
|
14
22
|
export interface ContainerFrame {
|
|
15
23
|
readonly timeRange: readonly [number, number];
|
|
16
24
|
readonly width: number;
|
|
@@ -39,6 +47,22 @@ export interface ContainerFrame {
|
|
|
39
47
|
readonly cursorX: number | null;
|
|
40
48
|
/** Set the hovered plot-pixel x; a row's event surface calls this on pointer move. */
|
|
41
49
|
setHoverX(x: number | null): void;
|
|
50
|
+
/**
|
|
51
|
+
* The hovered plot-pixel **y** and the row it's in — for the free-form
|
|
52
|
+
* crosshair's horizontal line + value readout (which are row-specific, unlike
|
|
53
|
+
* the shared vertical `cursorX`). `null` when not hovering a plot. Hover-driven
|
|
54
|
+
* only (no controlled equivalent).
|
|
55
|
+
*/
|
|
56
|
+
readonly cursorY: number | null;
|
|
57
|
+
readonly cursorRowKey: symbol | null;
|
|
58
|
+
/** Set the hovered plot-pixel y + its row; the event surface calls this on move. */
|
|
59
|
+
setHoverY(y: number | null, rowKey: symbol | null): void;
|
|
60
|
+
/**
|
|
61
|
+
* `cursor="crosshair"` **y** snapping. **Default `true`** — the reticle centres
|
|
62
|
+
* on the nearest data point's value. `false` — the y follows the pointer freely
|
|
63
|
+
* (`yScale.invert`). The x always snaps to the data grid either way.
|
|
64
|
+
*/
|
|
65
|
+
readonly crosshairSnap: boolean;
|
|
42
66
|
/**
|
|
43
67
|
* The selected mark, or `null`. Shared across rows (single selection). A layer
|
|
44
68
|
* highlights the mark matching **both** the key (epoch ms) and the series
|
|
@@ -146,10 +170,20 @@ export interface ContainerFrame {
|
|
|
146
170
|
/** Every registered annotation — read by each row to draw the *other* rows'
|
|
147
171
|
* guides, and by a drag to find snap targets. */
|
|
148
172
|
readonly annotations: readonly AnnotationSpec[];
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
173
|
+
/** Per-key top-flag {@link LabelPlacement} — the lane (0 = top; overlapping
|
|
174
|
+
* labels stack down) + the chip text (merged for the representative of a
|
|
175
|
+
* coincident-marker group, `null` for the folded-in members). A key absent
|
|
176
|
+
* from the map sits at lane 0 with its own label. */
|
|
177
|
+
readonly labelLanes: ReadonlyMap<symbol, LabelPlacement>;
|
|
178
|
+
/**
|
|
179
|
+
* The annotation currently being **dragged** (its slot key), or `null`. Set on
|
|
180
|
+
* drag-start, cleared on release. The lane packers (label lanes + x-axis pill
|
|
181
|
+
* lanes) exclude it so the *static* marks don't reshuffle as the dragged one
|
|
182
|
+
* crosses them — only the mark under the pointer moves; it settles on release.
|
|
183
|
+
*/
|
|
184
|
+
readonly draggingKey: symbol | null;
|
|
185
|
+
/** Mark/clear the actively-dragged annotation; a mark's drag calls this. */
|
|
186
|
+
setDragging(key: symbol | null): void;
|
|
153
187
|
/**
|
|
154
188
|
* The armed creation tool, or `null` (idle). Set by the consumer's toolbar;
|
|
155
189
|
* when non-null the plot captures a **create gesture** (draw a new mark) instead
|
|
@@ -233,6 +267,10 @@ export interface AnnotationSpec {
|
|
|
233
267
|
/** The mark's resolved label text — used to pack overlapping top-flag labels
|
|
234
268
|
* (markers + regions) into stacked vertical lanes. */
|
|
235
269
|
readonly label: string;
|
|
270
|
+
/** Whether this mark shows its value as an **axis-edge pill** — a marker on the
|
|
271
|
+
* shared x-axis (drawn by `<XAxis>` at its `at`), a baseline on its y-axis
|
|
272
|
+
* (drawn in place). Regions never set it. */
|
|
273
|
+
readonly indicator: boolean;
|
|
236
274
|
}
|
|
237
275
|
/**
|
|
238
276
|
* A row's per-slot axis widths each side, **slot 0 nearest the plot** (so the
|
|
@@ -368,8 +406,12 @@ export interface TrackerInfo {
|
|
|
368
406
|
* - `inline` — dots + a value chip beside each.
|
|
369
407
|
* - `flag` — dots + value flags (a staffed flag from each point; the staff
|
|
370
408
|
* geometry lands in a later phase — for now flags stack at the top).
|
|
409
|
+
* - `crosshair` — the synced vertical line + a dot on each series, with each
|
|
410
|
+
* series' value pinned to its y-axis edge (an on-axis pill) and the cursor
|
|
411
|
+
* time pinned to the x-axis. The ChartIQ / trading-terminal readout. Values
|
|
412
|
+
* snap to the series (the axis pills read like ticks), not the raw mouse Y.
|
|
371
413
|
*/
|
|
372
|
-
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag';
|
|
414
|
+
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair';
|
|
373
415
|
/** A registered layer plus the axis id it draws against. */
|
|
374
416
|
export interface LayerEntry {
|
|
375
417
|
readonly layer: RowLayer;
|
|
@@ -431,6 +473,9 @@ export interface RowFrame {
|
|
|
431
473
|
* that set `<YAxis ticks>` — so `Layers` draws gridlines at the same positions
|
|
432
474
|
* the axis labels. Absent id ⇒ that axis auto-picks. */
|
|
433
475
|
readonly tickValues: ReadonlyMap<string, readonly number[]>;
|
|
476
|
+
/** The side each axis sits on, keyed by id — so an axis-edge overlay (the
|
|
477
|
+
* crosshair value pills) hugs the correct gutter. */
|
|
478
|
+
readonly axisSides: ReadonlyMap<string, 'left' | 'right'>;
|
|
434
479
|
/** This row's cursor-mode override, or `undefined` to inherit the container's
|
|
435
480
|
* default ({@link ContainerFrame.cursor}). */
|
|
436
481
|
readonly cursor: CursorMode | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type { BarChartProps } from './BarChart.js';
|
|
|
45
45
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
46
46
|
export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
47
47
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
48
|
+
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
49
|
+
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
48
50
|
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
|
|
49
51
|
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, } from './data.js';
|
|
50
52
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,9 @@ export { BarChart } from './BarChart.js';
|
|
|
32
32
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
|
33
33
|
// data): a shaded span, a horizontal value line, a vertical x line.
|
|
34
34
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
35
|
+
// Axis indicators — a value pill pinned to an axis edge (the ChartIQ live tag).
|
|
36
|
+
// `createLiveValue` is the high-frequency, isolated-repaint update path.
|
|
37
|
+
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
35
38
|
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
|
|
36
39
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
37
40
|
// CSS-custom-property → ChartTheme bridge: build a theme from a design system's
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type AxisFormat } from './format.js';
|
|
2
|
+
/**
|
|
3
|
+
* A **live scalar** an axis indicator subscribes to, pushed imperatively from
|
|
4
|
+
* outside React — a WebSocket `onmessage`, a `requestAnimationFrame` loop, a
|
|
5
|
+
* tick handler. Backed by `useSyncExternalStore` on the consuming indicator:
|
|
6
|
+
* calling {@link LiveValue.set} re-renders **only the indicators subscribed to
|
|
7
|
+
* this value** — never the chart tree, never a canvas repaint. This is the path
|
|
8
|
+
* for a value that ticks many times a second (a live last-price tag), set
|
|
9
|
+
* independently of the series' own last point.
|
|
10
|
+
*
|
|
11
|
+
* Create one with {@link createLiveValue} and pass it to
|
|
12
|
+
* `<YAxisIndicator source={…}>`.
|
|
13
|
+
*/
|
|
14
|
+
export interface LiveValue {
|
|
15
|
+
/** Push a new value. Re-renders subscribed indicators only; a no-op if the
|
|
16
|
+
* value is unchanged. Safe to call from outside React at any frequency. */
|
|
17
|
+
set(value: number): void;
|
|
18
|
+
/** @internal Store subscribe, for `useSyncExternalStore`. */
|
|
19
|
+
subscribe(onStoreChange: () => void): () => void;
|
|
20
|
+
/** @internal Current value snapshot, for `useSyncExternalStore`. */
|
|
21
|
+
getSnapshot(): number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a {@link LiveValue} seeded at `initial`. Hold the returned object,
|
|
25
|
+
* call `.set(v)` from your data source, and hand it to
|
|
26
|
+
* `<YAxisIndicator source={…}>` — the pill repositions and relabels on each
|
|
27
|
+
* `set` without re-rendering the chart.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const price = createLiveValue(0);
|
|
31
|
+
* ws.onmessage = (e) => price.set(JSON.parse(e.data).last); // outside React
|
|
32
|
+
* // <YAxisIndicator source={price} color="#4af" format=",.2f" />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function createLiveValue(initial: number): LiveValue;
|
|
36
|
+
export interface YAxisIndicatorProps {
|
|
37
|
+
/**
|
|
38
|
+
* A static value to pin the pill at. Pass this **or** {@link source}. Updating
|
|
39
|
+
* `value` re-renders with its parent — fine for an occasional change; for a
|
|
40
|
+
* high-frequency tick use `source` so only the pill repaints.
|
|
41
|
+
*/
|
|
42
|
+
value?: number;
|
|
43
|
+
/**
|
|
44
|
+
* A {@link LiveValue} to subscribe to — the high-frequency path. `.set(v)`
|
|
45
|
+
* moves and relabels the pill **without re-rendering the chart**. Takes
|
|
46
|
+
* precedence over {@link value} if both are given.
|
|
47
|
+
*/
|
|
48
|
+
source?: LiveValue;
|
|
49
|
+
/** Which `<YAxis>` (by id) to position against; omit for the row's default axis. */
|
|
50
|
+
axis?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Which edge the pill hugs. Default `right` — the conventional side for a live
|
|
53
|
+
* value tag. (Independent of the linked axis's side; set it to match.)
|
|
54
|
+
*/
|
|
55
|
+
side?: 'left' | 'right';
|
|
56
|
+
/**
|
|
57
|
+
* Pill hue — the colour of the series / value it tracks. Defaults to the axis
|
|
58
|
+
* label colour (`theme.axis.label`).
|
|
59
|
+
*/
|
|
60
|
+
color?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Value formatting: a d3 format specifier (e.g. `',.2f'`, `'.1%'`) or a
|
|
63
|
+
* `(value) => string`. Omit to use the linked axis's own formatter, so the pill
|
|
64
|
+
* reads exactly like a tick. Pass a specifier for finer precision than the
|
|
65
|
+
* tick-calibrated default (a live price usually wants `',.2f'`, not the
|
|
66
|
+
* coarser tick rounding). See {@link AxisFormat}.
|
|
67
|
+
*
|
|
68
|
+
* An indicator **always shows the axis value** — there is no label override. A
|
|
69
|
+
* name/annotation belongs on a `<Baseline label>`'s near-line chip, not on the
|
|
70
|
+
* axis pill (an axis pill reads like a tick).
|
|
71
|
+
*/
|
|
72
|
+
format?: AxisFormat;
|
|
73
|
+
/**
|
|
74
|
+
* Draw a thin dashed guide line from the pill across the plot (the ChartIQ
|
|
75
|
+
* "price line"). Default `false`.
|
|
76
|
+
*/
|
|
77
|
+
line?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Add a small triangle on the pill's plot-facing edge, pointing **into** the
|
|
80
|
+
* plot at the value (a callout tab). Default `false`.
|
|
81
|
+
*/
|
|
82
|
+
pointer?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A **value pill pinned to a y-axis edge** — the ChartIQ / Yahoo-Finance live
|
|
86
|
+
* price tag. Positions at `yScale(value)` on the linked axis and renders a chip
|
|
87
|
+
* (the solid {@link axisPillStyle} pill) at the plot's `side` edge, optionally
|
|
88
|
+
* with a dashed guide line across the plot.
|
|
89
|
+
*
|
|
90
|
+
* Render it as a child of `<Layers>` (alongside the chart layers), so it shares
|
|
91
|
+
* the plot's coordinate space:
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* <Layers>
|
|
95
|
+
* <LineChart series={price} axis="usd" />
|
|
96
|
+
* <YAxisIndicator source={liveLast} axis="usd" color="#4af" format=",.2f" line />
|
|
97
|
+
* </Layers>
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* The value is **decoupled from the series' last point** — feed it whatever the
|
|
101
|
+
* live feed reports. For high-frequency updates pass a {@link LiveValue}
|
|
102
|
+
* ({@link source}); `.set()` repaints only the pill.
|
|
103
|
+
*/
|
|
104
|
+
export declare function YAxisIndicator({ value, source, axis, side, color, format, line, pointer, }: YAxisIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
|
|
105
|
+
//# sourceMappingURL=indicators.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useSyncExternalStore } from 'react';
|
|
3
|
+
import { ContainerContext, RowContext } from './context.js';
|
|
4
|
+
import { axisPillStyle, axisPillX, pointerStyle } from './chip.js';
|
|
5
|
+
import { resolveAxisFormat } from './format.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a {@link LiveValue} seeded at `initial`. Hold the returned object,
|
|
8
|
+
* call `.set(v)` from your data source, and hand it to
|
|
9
|
+
* `<YAxisIndicator source={…}>` — the pill repositions and relabels on each
|
|
10
|
+
* `set` without re-rendering the chart.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const price = createLiveValue(0);
|
|
14
|
+
* ws.onmessage = (e) => price.set(JSON.parse(e.data).last); // outside React
|
|
15
|
+
* // <YAxisIndicator source={price} color="#4af" format=",.2f" />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function createLiveValue(initial) {
|
|
19
|
+
let value = initial;
|
|
20
|
+
const listeners = new Set();
|
|
21
|
+
return {
|
|
22
|
+
set(v) {
|
|
23
|
+
// Skip a redundant notify — a repeated identical tick shouldn't wake the
|
|
24
|
+
// subscriber (getSnapshot must be stable between real changes anyway).
|
|
25
|
+
if (v === value)
|
|
26
|
+
return;
|
|
27
|
+
value = v;
|
|
28
|
+
for (const listener of listeners)
|
|
29
|
+
listener();
|
|
30
|
+
},
|
|
31
|
+
subscribe(onStoreChange) {
|
|
32
|
+
listeners.add(onStoreChange);
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(onStoreChange);
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
getSnapshot() {
|
|
38
|
+
return value;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const noopSubscribe = () => () => { };
|
|
43
|
+
/** The full-plot overlay the guide line paints into — above the data canvas,
|
|
44
|
+
* inert to the pointer (matches the annotations' `overlayStyle`). */
|
|
45
|
+
const overlayStyle = {
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: 0,
|
|
48
|
+
left: 0,
|
|
49
|
+
pointerEvents: 'none',
|
|
50
|
+
};
|
|
51
|
+
const TICK_COUNT = 5;
|
|
52
|
+
/**
|
|
53
|
+
* A **value pill pinned to a y-axis edge** — the ChartIQ / Yahoo-Finance live
|
|
54
|
+
* price tag. Positions at `yScale(value)` on the linked axis and renders a chip
|
|
55
|
+
* (the solid {@link axisPillStyle} pill) at the plot's `side` edge, optionally
|
|
56
|
+
* with a dashed guide line across the plot.
|
|
57
|
+
*
|
|
58
|
+
* Render it as a child of `<Layers>` (alongside the chart layers), so it shares
|
|
59
|
+
* the plot's coordinate space:
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Layers>
|
|
63
|
+
* <LineChart series={price} axis="usd" />
|
|
64
|
+
* <YAxisIndicator source={liveLast} axis="usd" color="#4af" format=",.2f" line />
|
|
65
|
+
* </Layers>
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* The value is **decoupled from the series' last point** — feed it whatever the
|
|
69
|
+
* live feed reports. For high-frequency updates pass a {@link LiveValue}
|
|
70
|
+
* ({@link source}); `.set()` repaints only the pill.
|
|
71
|
+
*/
|
|
72
|
+
export function YAxisIndicator({ value, source, axis, side = 'right', color, format, line = false, pointer = false, }) {
|
|
73
|
+
const container = useContext(ContainerContext);
|
|
74
|
+
if (container === null) {
|
|
75
|
+
throw new Error('<YAxisIndicator> must be rendered inside a <ChartContainer>');
|
|
76
|
+
}
|
|
77
|
+
const row = useContext(RowContext);
|
|
78
|
+
if (row === null) {
|
|
79
|
+
throw new Error('<YAxisIndicator> must be rendered inside a <ChartRow>');
|
|
80
|
+
}
|
|
81
|
+
// One unconditional hook that covers both paths: with a `source`, subscribe to
|
|
82
|
+
// its store (a `set` re-renders only this component); without one, a stable
|
|
83
|
+
// no-op subscribe + a snapshot that reads the static `value` prop (which
|
|
84
|
+
// re-renders with the parent). Either way `v` is the current value.
|
|
85
|
+
const v = useSyncExternalStore(source ? source.subscribe : noopSubscribe, source ? source.getSnapshot : () => value ?? NaN);
|
|
86
|
+
const { theme } = container;
|
|
87
|
+
const axisId = axis ?? row.defaultAxisId;
|
|
88
|
+
const yScale = row.yScales.get(axisId);
|
|
89
|
+
// Axis not resolved yet (a layer mounts before its <YAxis>), or no value fed —
|
|
90
|
+
// draw nothing rather than guess.
|
|
91
|
+
if (yScale === undefined || !Number.isFinite(v))
|
|
92
|
+
return null;
|
|
93
|
+
const resolvedColor = color ?? theme.axis.label;
|
|
94
|
+
// A caller `format` resolves against the scale (string specifier or fn);
|
|
95
|
+
// otherwise reuse the axis's own formatter so the pill reads like a tick.
|
|
96
|
+
const fmt = format
|
|
97
|
+
? resolveAxisFormat(yScale, TICK_COUNT, format)
|
|
98
|
+
: row.formats.get(axisId);
|
|
99
|
+
// An indicator always shows the axis value (no label override — a name belongs
|
|
100
|
+
// on a Baseline's near-line chip, not the axis pill).
|
|
101
|
+
const text = fmt ? fmt(v) : String(v);
|
|
102
|
+
const rawY = yScale(v);
|
|
103
|
+
// Clamp the pill's centre so an off-scale value keeps it inside the row rather
|
|
104
|
+
// than half-overflowing the edge (matches the y-tick clamp, F-charts-6).
|
|
105
|
+
const half = theme.font.size / 2 + 1;
|
|
106
|
+
const top = Math.max(half, Math.min(row.height - half, rawY));
|
|
107
|
+
return (_jsxs(_Fragment, { children: [line && (_jsx("svg", { width: container.plotWidth, height: row.height, style: overlayStyle, children: _jsx("line", { x1: 0, y1: rawY, x2: container.plotWidth, y2: rawY, stroke: resolvedColor, strokeWidth: 1, opacity: 0.5, strokeDasharray: "3 3", shapeRendering: "crispEdges" }) })), _jsxs("div", { style: {
|
|
108
|
+
...axisPillStyle(theme, resolvedColor),
|
|
109
|
+
top: `${top}px`,
|
|
110
|
+
...axisPillX(side, container.plotWidth),
|
|
111
|
+
transform: 'translateY(-50%)',
|
|
112
|
+
}, children: [pointer && _jsx("span", { style: pointerStyle(side, resolvedColor) }), text] })] }));
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=indicators.js.map
|
package/dist/tracker.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare const DEFAULT_CURSOR_MODE: CursorMode;
|
|
|
18
18
|
export declare function cursorParts(mode: CursorMode): {
|
|
19
19
|
readonly line: boolean;
|
|
20
20
|
readonly dots: boolean;
|
|
21
|
-
readonly chip: 'none' | 'inline' | 'flag';
|
|
21
|
+
readonly chip: 'none' | 'inline' | 'flag' | 'axis';
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* The crosshair's plot-pixel x from the tracker inputs. A controlled
|
package/dist/tracker.js
CHANGED
|
@@ -24,6 +24,11 @@ export function cursorParts(mode) {
|
|
|
24
24
|
return { line: false, dots: true, chip: 'inline' };
|
|
25
25
|
case 'flag':
|
|
26
26
|
return { line: false, dots: true, chip: 'flag' };
|
|
27
|
+
case 'crosshair':
|
|
28
|
+
// A single reticle (not per-series): `Layers` draws the dashed vertical +
|
|
29
|
+
// full-width horizontal lines, the centre dot, and one value pill itself
|
|
30
|
+
// (so no generic line/dots here); the x-time pill is on `<XAxis>`.
|
|
31
|
+
return { line: false, dots: false, chip: 'axis' };
|
|
27
32
|
case 'none':
|
|
28
33
|
return { line: false, dots: false, chip: 'none' };
|
|
29
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@pond-ts/react": "^0.
|
|
42
|
-
"pond-ts": "^0.
|
|
41
|
+
"@pond-ts/react": "^0.39.0",
|
|
42
|
+
"pond-ts": "^0.39.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|