@pond-ts/charts 0.57.0 → 0.59.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/API.md +576 -0
- package/CHANGELOG.md +1213 -1
- package/dist/AreaChart.d.ts +12 -1
- package/dist/AreaChart.js +131 -13
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +263 -39
- package/dist/BarList.d.ts +85 -5
- package/dist/BarList.js +25 -4
- package/dist/BoxList.d.ts +70 -3
- package/dist/BoxList.js +21 -7
- package/dist/BoxPlot.d.ts +2 -1
- package/dist/BoxPlot.js +101 -9
- package/dist/Candlestick.d.ts +13 -1
- package/dist/Candlestick.js +89 -3
- package/dist/ChartContainer.d.ts +36 -48
- package/dist/ChartContainer.js +465 -59
- package/dist/ChartRow.d.ts +9 -2
- package/dist/ChartRow.js +176 -14
- package/dist/HeatMap.d.ts +176 -0
- package/dist/HeatMap.js +344 -0
- package/dist/Layers.d.ts +5 -1
- package/dist/Layers.js +1014 -253
- package/dist/Legend.js +8 -4
- package/dist/LineChart.d.ts +18 -1
- package/dist/LineChart.js +165 -4
- package/dist/ListTable.d.ts +30 -3
- package/dist/ListTable.js +381 -23
- package/dist/ScatterChart.d.ts +3 -2
- package/dist/ScatterChart.js +68 -4
- package/dist/XAxis.js +40 -22
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +3 -1
- package/dist/area.d.ts +34 -1
- package/dist/area.js +88 -1
- package/dist/bars.d.ts +67 -6
- package/dist/bars.js +250 -35
- package/dist/box.d.ts +2 -2
- package/dist/box.js +158 -40
- package/dist/brush.d.ts +142 -0
- package/dist/brush.js +179 -0
- package/dist/child-index.d.ts +27 -0
- package/dist/child-index.js +57 -0
- package/dist/context.d.ts +870 -39
- package/dist/cursors.d.ts +161 -0
- package/dist/cursors.js +503 -0
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +157 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +16 -1
- package/dist/heat.d.ts +163 -0
- package/dist/heat.js +659 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +27 -0
- package/dist/line.d.ts +137 -0
- package/dist/line.js +328 -0
- package/dist/ohlc.d.ts +16 -1
- package/dist/ohlc.js +93 -4
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/scatter.d.ts +17 -9
- package/dist/scatter.js +221 -33
- package/dist/select.d.ts +13 -5
- package/dist/select.js +14 -6
- package/dist/selection-fixtures.d.ts +174 -0
- package/dist/selection-fixtures.js +569 -0
- package/dist/selection-stories.d.ts +73 -0
- package/dist/selection-stories.js +301 -0
- package/dist/selectors.d.ts +316 -0
- package/dist/selectors.js +391 -0
- package/dist/span.d.ts +122 -0
- package/dist/span.js +203 -0
- package/dist/sweep.d.ts +154 -0
- package/dist/sweep.js +282 -0
- package/dist/theme.d.ts +510 -5
- package/dist/theme.js +217 -41
- package/dist/tracker.d.ts +6 -0
- package/dist/tracker.js +6 -0
- package/dist/tradingAxis.fixture.d.ts +78 -0
- package/dist/tradingAxis.fixture.js +215 -0
- package/dist/useChartLegend.js +18 -3
- package/dist/yticks.d.ts +3 -0
- package/dist/yticks.js +104 -0
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ export type { XAxisProps } from './XAxis.js';
|
|
|
33
33
|
export type { AxisTransform } from './derivedTicks.js';
|
|
34
34
|
export { TimeAxis } from './TimeAxis.js';
|
|
35
35
|
export { CategoryAxis } from './CategoryAxis.js';
|
|
36
|
+
export { HeatMap } from './HeatMap.js';
|
|
37
|
+
export type { HeatMapProps } from './HeatMap.js';
|
|
38
|
+
export { bandedColor, heatValueExtent } from './heat.js';
|
|
39
|
+
export type { HeatStyle } from './heat.js';
|
|
36
40
|
export type { AxisFormat, CursorFormat } from './format.js';
|
|
37
41
|
export { LineChart } from './LineChart.js';
|
|
38
42
|
export type { LineChartProps } from './LineChart.js';
|
|
@@ -69,19 +73,24 @@ export type { RegionProps, BaselineProps, MarkerProps, ZoneProps, } from './anno
|
|
|
69
73
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
70
74
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
71
75
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
72
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow, } from './data.js';
|
|
73
|
-
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
76
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, categoryStacks, transposeRow, } from './data.js';
|
|
77
|
+
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, CategoryStackDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
74
78
|
export type { Orientation } from './bars.js';
|
|
75
79
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
76
80
|
export type { Curve } from './curve.js';
|
|
77
81
|
export type { GapMode } from './gaps.js';
|
|
78
82
|
export type { DecimateOption } from './decimate.js';
|
|
79
83
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
80
|
-
export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, BoxStyle, CandleStyle, BarStyle, } from './theme.js';
|
|
84
|
+
export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, ScatterStates, BoxStyle, BoxStates, BoxLadder, HeatStates, CandleStyle, BarStyle, } from './theme.js';
|
|
81
85
|
export { cssVarTheme } from './css-theme.js';
|
|
82
86
|
export type { ChartThemeOverrides, VarReader } from './css-theme.js';
|
|
83
87
|
export { useChartTheme } from './useChartTheme.js';
|
|
84
88
|
export type { UseChartThemeOptions } from './useChartTheme.js';
|
|
85
|
-
export
|
|
89
|
+
export { LineCursor, PointCursor, InlineCursor, FlagCursor, CrosshairCursor, RangeCursor, } from './cursors.js';
|
|
90
|
+
export type { LineCursorProps, PointCursorProps, InlineCursorProps, FlagCursorProps, CrosshairCursorProps, RangeCursorProps, } from './cursors.js';
|
|
91
|
+
export { Selector, MultiSelector } from './selectors.js';
|
|
92
|
+
export type { SelectorProps, MultiSelectorProps } from './selectors.js';
|
|
93
|
+
export type { CursorMode, TrackerInfo, TrackerSample, SelectInfo, SelectModifiers, RangeSpan, SpanSelection, SelectionEntry, } from './context.js';
|
|
94
|
+
export { selectionContains, isSpanSelection, sameMark } from './span.js';
|
|
86
95
|
export type { DrawStatsFrame, LayerDrawInfo } from './context.js';
|
|
87
96
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,11 @@ export { YAxis } from './YAxis.js';
|
|
|
26
26
|
export { XAxis } from './XAxis.js';
|
|
27
27
|
export { TimeAxis } from './TimeAxis.js';
|
|
28
28
|
export { CategoryAxis } from './CategoryAxis.js';
|
|
29
|
+
// Heat map — a grid of cells, bins on x and the series' columns as rows,
|
|
30
|
+
// colour encoding the value ([PND-HEATMAP]). A single column is a stripe,
|
|
31
|
+
// drawn by the same path.
|
|
32
|
+
export { HeatMap } from './HeatMap.js';
|
|
33
|
+
export { bandedColor, heatValueExtent } from './heat.js';
|
|
29
34
|
export { LineChart } from './LineChart.js';
|
|
30
35
|
export { BandChart } from './BandChart.js';
|
|
31
36
|
export { AreaChart } from './AreaChart.js';
|
|
@@ -63,6 +68,11 @@ export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeri
|
|
|
63
68
|
stacksFromGroups, stacksFromColumns, stacksFromBins,
|
|
64
69
|
// Categorical row-read: one bar per `{ label, value }` on the category axis.
|
|
65
70
|
categoryStack,
|
|
71
|
+
// …and its stacked sibling: one bar per `{ label, values }`, segments named by
|
|
72
|
+
// `columns` ([PND-CATSTACK]). Public for the same reason every reader above is
|
|
73
|
+
// — a caller assembling a `StackedBarSeries` by hand needs it — and because
|
|
74
|
+
// API.md already documented it as public while `index.ts` did not export it.
|
|
75
|
+
categoryStacks,
|
|
66
76
|
// The transpose reader — one row of a wide series read across into categories.
|
|
67
77
|
transposeRow, } from './data.js';
|
|
68
78
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
@@ -71,4 +81,21 @@ export { defaultTheme, estelaTheme } from './theme.js';
|
|
|
71
81
|
// toggle so a canvas chart follows dark/light (`useChartTheme`).
|
|
72
82
|
export { cssVarTheme } from './css-theme.js';
|
|
73
83
|
export { useChartTheme } from './useChartTheme.js';
|
|
84
|
+
// Cursor presets — the mounted-component successors of the `cursor` string
|
|
85
|
+
// modes (interaction RFC §4/A4.1): mount one as a child of <ChartContainer>
|
|
86
|
+
// (the default for every row) or inside a <ChartRow> (the per-row override).
|
|
87
|
+
// The underlying CursorSpec / ResolvedCursorFrame contract stays unpublished
|
|
88
|
+
// until the presets have proven it (RFC Q3).
|
|
89
|
+
export { LineCursor, PointCursor, InlineCursor, FlagCursor, CrosshairCursor, RangeCursor, } from './cursors.js';
|
|
90
|
+
// Selection as a mounted component (interaction RFC §7 / A10): `<Selector>`
|
|
91
|
+
// wraps what it applies to and owns both the gesture and the state it
|
|
92
|
+
// produces — `selected` / `hovered` / `onSelect` / `onHover` are its props,
|
|
93
|
+
// not the container's. Mounting is what enables a plot click at all.
|
|
94
|
+
// `<MultiSelector>` (RFC §8) is its sweep superset: a click still selects one
|
|
95
|
+
// mark, a drag sweeps many and releases `(hits, modifiers, spans)` (A5.2).
|
|
96
|
+
export { Selector, MultiSelector } from './selectors.js';
|
|
97
|
+
// Span-selection membership (interaction RFC A5.2) — the same predicate the
|
|
98
|
+
// layers run per mark, exported so a consumer editing a mixed `selected` array
|
|
99
|
+
// never re-implements the interval test, plus the entry discriminant.
|
|
100
|
+
export { selectionContains, isSpanSelection, sameMark } from './span.js';
|
|
74
101
|
//# sourceMappingURL=index.js.map
|
package/dist/line.d.ts
CHANGED
|
@@ -82,4 +82,141 @@ export declare function drawLine(ctx: CanvasRenderingContext2D, cs: ChartSeries,
|
|
|
82
82
|
* negligible next to the row sweep.
|
|
83
83
|
*/
|
|
84
84
|
export declare function sessionRuns(x: Float64Array, length: number, boundaries: readonly number[]): Array<[number, number]>;
|
|
85
|
+
/** How near the pointer must come to a trace's path to count as a hit, in px.
|
|
86
|
+
* Generous on purpose: a 1.5px stroke is not a target anyone can hit, and a
|
|
87
|
+
* trace has no fat mark to aim at the way a bar does. */
|
|
88
|
+
export declare const TRACE_HIT_PX = 6;
|
|
89
|
+
/**
|
|
90
|
+
* **Is the pointer on this trace?** Distance from `(px, py)` to the drawn
|
|
91
|
+
* polyline, in pixels, or `null` past the tolerance.
|
|
92
|
+
*
|
|
93
|
+
* Returns the **nearest sample's index** on a hit, purely as click provenance —
|
|
94
|
+
* the selection a trace commits is series-scoped ([PND-TRACESEL]), because a
|
|
95
|
+
* sample is not a mark. The index is what the readout can name, not what the
|
|
96
|
+
* selection is keyed on.
|
|
97
|
+
*
|
|
98
|
+
* The x cut is a binary search over `xScale(cs.x[i])` rather than an inverse:
|
|
99
|
+
* a layer's `hitTest` is handed the forward scale only, and a trading-time or
|
|
100
|
+
* band scale has no honest inverse anyway. Monotonic either way, so bisection
|
|
101
|
+
* holds. Then only the two segments either side of that index are measured —
|
|
102
|
+
* the pointer cannot be nearest to any other, so this is `O(log N)` and not a
|
|
103
|
+
* scan, which matters on a decimated million-point trace.
|
|
104
|
+
*
|
|
105
|
+
* Gaps are skipped: a segment with a non-finite end is not drawn, so it cannot
|
|
106
|
+
* be hit. That is the same rule `drawLine` paints by, and it is why clicking a
|
|
107
|
+
* dropout selects nothing rather than the bridge across it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function traceHitIndex(cs: ChartSeries, px: number, py: number, xScale: Scale, yScale: Scale, tolerance?: number): number | null;
|
|
110
|
+
/**
|
|
111
|
+
* The **interaction state** a trace draws in, resolved from the selection
|
|
112
|
+
* ([PND-TRACESEL]).
|
|
113
|
+
*
|
|
114
|
+
* - `'rest'` — nothing selected anywhere, or this trace is what's selected and
|
|
115
|
+
* nothing needs emphasis beyond its own.
|
|
116
|
+
* - `'selected'` — this series is the selected one: thicken it.
|
|
117
|
+
* - `'hover'` — transient echo of `selected`.
|
|
118
|
+
* - `'dimmed'` — something *else* is selected: recede, hue intact.
|
|
119
|
+
*/
|
|
120
|
+
export type TraceState = 'rest' | 'selected' | 'hover' | 'dimmed';
|
|
121
|
+
/**
|
|
122
|
+
* The style a trace strokes with in a given state — weight and alpha only, so a
|
|
123
|
+
* muted or emphasised line still reads as *which* series it is
|
|
124
|
+
* (`LineStyle.selectedWidth`'s doc has the argument).
|
|
125
|
+
*
|
|
126
|
+
* Returned as a `[style, alpha]` pair rather than folded into the style,
|
|
127
|
+
* because alpha belongs to the canvas' `globalAlpha` and not to a stroke
|
|
128
|
+
* colour: baking it into the colour would lose the hue on a themed line that
|
|
129
|
+
* already carries an alpha of its own.
|
|
130
|
+
*/
|
|
131
|
+
export declare function traceStateStyle(style: LineStyle, state: TraceState): readonly [LineStyle, number];
|
|
132
|
+
/**
|
|
133
|
+
* Run `draw` twice to paint a trace **partitioned by a swept window**: the
|
|
134
|
+
* whole trace in `outside`, then the same trace again in `inside`, **clipped to
|
|
135
|
+
* the window's pixel band**.
|
|
136
|
+
*
|
|
137
|
+
* Clipping rather than slicing the series, and that is the load-bearing choice.
|
|
138
|
+
* Slicing would have to re-cull, re-decimate and re-split each piece — three
|
|
139
|
+
* cache misses and three chances for the seam to disagree with itself, and the
|
|
140
|
+
* decimated polyline's bucket edges would not line up with the window's, so
|
|
141
|
+
* the boundary would visibly jitter as the drag moved. Clipping strokes the
|
|
142
|
+
* *identical* geometry twice and lets the rasteriser cut it, so the seam is
|
|
143
|
+
* exact by construction and both passes hit the same decimation cache entry.
|
|
144
|
+
*
|
|
145
|
+
* The window arrives in **pixels** because that is what a clip rect wants and
|
|
146
|
+
* the caller has already mapped it through the scale it drew with.
|
|
147
|
+
*/
|
|
148
|
+
export declare function drawPartitioned(ctx: CanvasRenderingContext2D, windowPx: readonly [number, number], height: number, outside: () => LayerDrawStats, inside: () => LayerDrawStats,
|
|
149
|
+
/**
|
|
150
|
+
* Clip the emphasised pass to the window. **`true` for a fill**, whose
|
|
151
|
+
* boundary is a vertical wall by construction; **`false` for a stroke that
|
|
152
|
+
* has sliced its own path** (see {@link sliceTrace}), because a clip would
|
|
153
|
+
* shear the ribbon flat and defeat the round cap the slice exists to allow.
|
|
154
|
+
*/
|
|
155
|
+
clipInside?: boolean,
|
|
156
|
+
/**
|
|
157
|
+
* Half the emphasised stroke's width, in px — how far its **round cap**
|
|
158
|
+
* overhangs the window boundary. The outside pass's clip is inset by this,
|
|
159
|
+
* so the cap lands on bare ground instead of on top of the muted trace.
|
|
160
|
+
*
|
|
161
|
+
* Without it the boundary column carries two antialiased edges stacked (the
|
|
162
|
+
* muted stroke's clipped end, plus the cap) and the seam reads muddy at
|
|
163
|
+
* zoom. Measured, not guessed: the draw issues exactly two strokes per
|
|
164
|
+
* trace, so the murk was overlap, never a double render.
|
|
165
|
+
*/
|
|
166
|
+
capOverhangPx?: number,
|
|
167
|
+
/** The plot's CSS width — see {@link plotExtentOf}. Defaults to the device
|
|
168
|
+
* width only so a bare test stub still clips something sane. */
|
|
169
|
+
plotWidthCss?: number): LayerDrawStats;
|
|
170
|
+
/**
|
|
171
|
+
* A trace sliced to the key window `[lo, hi]`, with its **endpoints
|
|
172
|
+
* interpolated** onto the path — or `null` when nothing of it falls inside.
|
|
173
|
+
*
|
|
174
|
+
* This exists so the emphasised pass of a partitioned draw can be a real path
|
|
175
|
+
* whose own ends are the window's ends, which is what lets a **round cap**
|
|
176
|
+
* show. A clipped stroke cannot have one: the clip shears the ribbon on a
|
|
177
|
+
* vertical line wherever the rect cuts it, so the cap is drawn off in the
|
|
178
|
+
* hidden part of the path and the visible end is always a hard vertical edge,
|
|
179
|
+
* whatever `lineCap` says.
|
|
180
|
+
*
|
|
181
|
+
* The endpoints are interpolated rather than snapped to the nearest sample
|
|
182
|
+
* because at low density snapping would visibly overshoot or undershoot the
|
|
183
|
+
* window the reader just swept — the emphasis would not line up with the band
|
|
184
|
+
* that produced it.
|
|
185
|
+
*
|
|
186
|
+
* A boundary landing on a **gap** contributes no interpolated point: there is
|
|
187
|
+
* no drawn segment there to sit on, and inventing one would bridge a hole the
|
|
188
|
+
* trace deliberately shows.
|
|
189
|
+
*/
|
|
190
|
+
export declare function sliceTrace(cs: ChartSeries, lo: number, hi: number): ChartSeries | null;
|
|
191
|
+
/**
|
|
192
|
+
* **EXPERIMENT ([PND-ANNSNAP]).** Vertical rules at a swept window's edges, in
|
|
193
|
+
* the annotation register — a preview of what "promote this sweep to an
|
|
194
|
+
* annotation" would look like, drawn *underneath* the trace.
|
|
195
|
+
*
|
|
196
|
+
* Two caveats, and the second decides whether this survives:
|
|
197
|
+
*
|
|
198
|
+
* - **Opaque on purpose.** Every spanned layer in the row draws its own edges
|
|
199
|
+
* at the same x, so a translucent stroke would composite once per trace and
|
|
200
|
+
* darken with the number of series. Opaque makes the overdraw idempotent.
|
|
201
|
+
* - **A real annotation could not sit here.** Annotations render in the SVG
|
|
202
|
+
* overlay *above* the canvas, so a promoted span's rules would land on top of
|
|
203
|
+
* the traces, not under them. This is canvas-side precisely because "under"
|
|
204
|
+
* was asked for — if the look is kept, the honest options are to accept rules
|
|
205
|
+
* above the ink, or to give the annotation register a canvas-underlay pass.
|
|
206
|
+
*/
|
|
207
|
+
/**
|
|
208
|
+
* The plot's extent in **CSS pixels**, from the scales' own ranges.
|
|
209
|
+
*
|
|
210
|
+
* Not `ctx.canvas.width`/`height`: those are **device** pixels, while the
|
|
211
|
+
* context is pre-transformed by the device ratio, so mixing them overshoots at
|
|
212
|
+
* dpr>1 and — the case that actually breaks — *undershoots* at dpr<1 (a
|
|
213
|
+
* zoomed-out browser), collapsing a clip that should span the plot. The scales
|
|
214
|
+
* are the honest source, and this needs no dpr arithmetic at all. Falls back to
|
|
215
|
+
* the canvas dims only when a scale exposes no range (a bare test stub).
|
|
216
|
+
*/
|
|
217
|
+
export declare function plotExtentOf(ctx: CanvasRenderingContext2D, xScale: Scale, yScale: Scale): {
|
|
218
|
+
readonly width: number;
|
|
219
|
+
readonly height: number;
|
|
220
|
+
};
|
|
221
|
+
export declare function strokeSpanEdges(ctx: CanvasRenderingContext2D, windowPx: readonly [number, number], height: number, color: string, width?: number): void;
|
|
85
222
|
//# sourceMappingURL=line.d.ts.map
|
package/dist/line.js
CHANGED
|
@@ -260,4 +260,332 @@ export function sessionRuns(x, length, boundaries) {
|
|
|
260
260
|
runs.push([start, length]);
|
|
261
261
|
return runs;
|
|
262
262
|
}
|
|
263
|
+
/** How near the pointer must come to a trace's path to count as a hit, in px.
|
|
264
|
+
* Generous on purpose: a 1.5px stroke is not a target anyone can hit, and a
|
|
265
|
+
* trace has no fat mark to aim at the way a bar does. */
|
|
266
|
+
export const TRACE_HIT_PX = 6;
|
|
267
|
+
/**
|
|
268
|
+
* **Is the pointer on this trace?** Distance from `(px, py)` to the drawn
|
|
269
|
+
* polyline, in pixels, or `null` past the tolerance.
|
|
270
|
+
*
|
|
271
|
+
* Returns the **nearest sample's index** on a hit, purely as click provenance —
|
|
272
|
+
* the selection a trace commits is series-scoped ([PND-TRACESEL]), because a
|
|
273
|
+
* sample is not a mark. The index is what the readout can name, not what the
|
|
274
|
+
* selection is keyed on.
|
|
275
|
+
*
|
|
276
|
+
* The x cut is a binary search over `xScale(cs.x[i])` rather than an inverse:
|
|
277
|
+
* a layer's `hitTest` is handed the forward scale only, and a trading-time or
|
|
278
|
+
* band scale has no honest inverse anyway. Monotonic either way, so bisection
|
|
279
|
+
* holds. Then only the two segments either side of that index are measured —
|
|
280
|
+
* the pointer cannot be nearest to any other, so this is `O(log N)` and not a
|
|
281
|
+
* scan, which matters on a decimated million-point trace.
|
|
282
|
+
*
|
|
283
|
+
* Gaps are skipped: a segment with a non-finite end is not drawn, so it cannot
|
|
284
|
+
* be hit. That is the same rule `drawLine` paints by, and it is why clicking a
|
|
285
|
+
* dropout selects nothing rather than the bridge across it.
|
|
286
|
+
*/
|
|
287
|
+
export function traceHitIndex(cs, px, py, xScale, yScale, tolerance = TRACE_HIT_PX) {
|
|
288
|
+
const n = cs.length;
|
|
289
|
+
if (n === 0)
|
|
290
|
+
return null;
|
|
291
|
+
// Bisect to the first sample at or past the pointer's x.
|
|
292
|
+
let lo = 0;
|
|
293
|
+
let hi = n;
|
|
294
|
+
while (lo < hi) {
|
|
295
|
+
const mid = (lo + hi) >> 1;
|
|
296
|
+
if (xScale(cs.x[mid]) < px)
|
|
297
|
+
lo = mid + 1;
|
|
298
|
+
else
|
|
299
|
+
hi = mid;
|
|
300
|
+
}
|
|
301
|
+
let best = Infinity;
|
|
302
|
+
let bestIdx = null;
|
|
303
|
+
// The pointer's nearest point on the path lies on one of the two segments
|
|
304
|
+
// touching `lo`, so `lo - 1 … lo + 1` bounds everything worth measuring.
|
|
305
|
+
// The segments touching `lo` are `[lo-1, lo]` and `[lo, lo+1]`, so the
|
|
306
|
+
// indices to measure are `lo-1` and `lo` — inclusive of `lo`. The previous
|
|
307
|
+
// exclusive bound `< min(n-1, lo+1)` collapsed to an empty range when the
|
|
308
|
+
// pointer sat past the last sample (`lo === n`), so a within-tolerance click
|
|
309
|
+
// off the right end missed while the left-end mirror worked (reviewer find).
|
|
310
|
+
for (let i = Math.max(0, lo - 1); i <= Math.min(n - 2, lo); i += 1) {
|
|
311
|
+
const ay = cs.y[i];
|
|
312
|
+
const by = cs.y[i + 1];
|
|
313
|
+
if (!Number.isFinite(ay) || !Number.isFinite(by))
|
|
314
|
+
continue;
|
|
315
|
+
const ax = xScale(cs.x[i]);
|
|
316
|
+
const bx = xScale(cs.x[i + 1]);
|
|
317
|
+
const d = pointSegmentDistance(px, py, ax, yScale(ay), bx, yScale(by));
|
|
318
|
+
if (d < best) {
|
|
319
|
+
best = d;
|
|
320
|
+
// Attribute to whichever END the pointer is nearer, so the provenance
|
|
321
|
+
// index is the sample a reader would say they clicked.
|
|
322
|
+
const da = Math.hypot(px - ax, py - yScale(ay));
|
|
323
|
+
const db = Math.hypot(px - bx, py - yScale(by));
|
|
324
|
+
bestIdx = da <= db ? i : i + 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// A single-sample trace draws no segment, so measure the point itself —
|
|
328
|
+
// otherwise a one-point series would be unhittable.
|
|
329
|
+
if (bestIdx === null && n === 1 && Number.isFinite(cs.y[0])) {
|
|
330
|
+
const d = Math.hypot(px - xScale(cs.x[0]), py - yScale(cs.y[0]));
|
|
331
|
+
if (d <= tolerance)
|
|
332
|
+
return 0;
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return best <= tolerance ? bestIdx : null;
|
|
336
|
+
}
|
|
337
|
+
/** Euclidean distance from a point to a line segment, in the same units. */
|
|
338
|
+
function pointSegmentDistance(px, py, ax, ay, bx, by) {
|
|
339
|
+
const dx = bx - ax;
|
|
340
|
+
const dy = by - ay;
|
|
341
|
+
const lenSq = dx * dx + dy * dy;
|
|
342
|
+
// A degenerate segment (both ends on one pixel) is a point.
|
|
343
|
+
if (lenSq === 0)
|
|
344
|
+
return Math.hypot(px - ax, py - ay);
|
|
345
|
+
// Project onto the segment, clamped to its ends.
|
|
346
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
|
|
347
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The style a trace strokes with in a given state — weight and alpha only, so a
|
|
351
|
+
* muted or emphasised line still reads as *which* series it is
|
|
352
|
+
* (`LineStyle.selectedWidth`'s doc has the argument).
|
|
353
|
+
*
|
|
354
|
+
* Returned as a `[style, alpha]` pair rather than folded into the style,
|
|
355
|
+
* because alpha belongs to the canvas' `globalAlpha` and not to a stroke
|
|
356
|
+
* colour: baking it into the colour would lose the hue on a themed line that
|
|
357
|
+
* already carries an alpha of its own.
|
|
358
|
+
*/
|
|
359
|
+
export function traceStateStyle(style, state) {
|
|
360
|
+
const selected = style.selectedWidth ?? style.width * 2;
|
|
361
|
+
switch (state) {
|
|
362
|
+
case 'selected':
|
|
363
|
+
return [{ ...style, width: selected }, 1];
|
|
364
|
+
case 'hover':
|
|
365
|
+
return [{ ...style, width: style.hoverWidth ?? selected }, 1];
|
|
366
|
+
case 'dimmed':
|
|
367
|
+
return [style, style.dimmedOpacity ?? 0.32];
|
|
368
|
+
case 'rest':
|
|
369
|
+
return [style, 1];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Run `draw` twice to paint a trace **partitioned by a swept window**: the
|
|
374
|
+
* whole trace in `outside`, then the same trace again in `inside`, **clipped to
|
|
375
|
+
* the window's pixel band**.
|
|
376
|
+
*
|
|
377
|
+
* Clipping rather than slicing the series, and that is the load-bearing choice.
|
|
378
|
+
* Slicing would have to re-cull, re-decimate and re-split each piece — three
|
|
379
|
+
* cache misses and three chances for the seam to disagree with itself, and the
|
|
380
|
+
* decimated polyline's bucket edges would not line up with the window's, so
|
|
381
|
+
* the boundary would visibly jitter as the drag moved. Clipping strokes the
|
|
382
|
+
* *identical* geometry twice and lets the rasteriser cut it, so the seam is
|
|
383
|
+
* exact by construction and both passes hit the same decimation cache entry.
|
|
384
|
+
*
|
|
385
|
+
* The window arrives in **pixels** because that is what a clip rect wants and
|
|
386
|
+
* the caller has already mapped it through the scale it drew with.
|
|
387
|
+
*/
|
|
388
|
+
export function drawPartitioned(ctx, windowPx, height, outside, inside,
|
|
389
|
+
/**
|
|
390
|
+
* Clip the emphasised pass to the window. **`true` for a fill**, whose
|
|
391
|
+
* boundary is a vertical wall by construction; **`false` for a stroke that
|
|
392
|
+
* has sliced its own path** (see {@link sliceTrace}), because a clip would
|
|
393
|
+
* shear the ribbon flat and defeat the round cap the slice exists to allow.
|
|
394
|
+
*/
|
|
395
|
+
clipInside = true,
|
|
396
|
+
/**
|
|
397
|
+
* Half the emphasised stroke's width, in px — how far its **round cap**
|
|
398
|
+
* overhangs the window boundary. The outside pass's clip is inset by this,
|
|
399
|
+
* so the cap lands on bare ground instead of on top of the muted trace.
|
|
400
|
+
*
|
|
401
|
+
* Without it the boundary column carries two antialiased edges stacked (the
|
|
402
|
+
* muted stroke's clipped end, plus the cap) and the seam reads muddy at
|
|
403
|
+
* zoom. Measured, not guessed: the draw issues exactly two strokes per
|
|
404
|
+
* trace, so the murk was overlap, never a double render.
|
|
405
|
+
*/
|
|
406
|
+
capOverhangPx = 0,
|
|
407
|
+
/** The plot's CSS width — see {@link plotExtentOf}. Defaults to the device
|
|
408
|
+
* width only so a bare test stub still clips something sane. */
|
|
409
|
+
plotWidthCss) {
|
|
410
|
+
const [x0, x1] = windowPx;
|
|
411
|
+
// A collapsed window has no inside to paint, so one plain pass is the whole
|
|
412
|
+
// picture — and `clip()` on a zero-width rect would suppress the second pass
|
|
413
|
+
// anyway.
|
|
414
|
+
if (x1 <= x0)
|
|
415
|
+
return outside();
|
|
416
|
+
// **Both passes are clipped, and the outside one to the window's COMPLEMENT.**
|
|
417
|
+
// Painting the muted trace full-width and the emphasised one over it works
|
|
418
|
+
// for a line, whose stroke is opaque and covers itself. It is wrong for an
|
|
419
|
+
// **area**: a semi-transparent fill composites with whatever is under it, so
|
|
420
|
+
// the window would carry muted-plus-emphasised stacked and read darker than
|
|
421
|
+
// either — the emphasis would depend on what it was drawn over. Clipping the
|
|
422
|
+
// outside pass away from the window means each pixel is painted exactly once,
|
|
423
|
+
// whatever its alpha.
|
|
424
|
+
ctx.save();
|
|
425
|
+
ctx.beginPath();
|
|
426
|
+
// The complement as two rects in one path: everything left of the window,
|
|
427
|
+
// everything right of it. A negative-width rect is legal but not portable
|
|
428
|
+
// across every canvas impl, so clamp rather than rely on it.
|
|
429
|
+
const inset = Math.max(0, capOverhangPx);
|
|
430
|
+
const leftEdge = x0 - inset;
|
|
431
|
+
const rightEdge = x1 + inset;
|
|
432
|
+
if (leftEdge > 0)
|
|
433
|
+
ctx.rect(0, 0, leftEdge, height);
|
|
434
|
+
const right = plotWidthCss ?? ctx.canvas.width;
|
|
435
|
+
ctx.rect(rightEdge, 0, Math.max(0, right - rightEdge), height);
|
|
436
|
+
ctx.clip();
|
|
437
|
+
const stats = outside();
|
|
438
|
+
ctx.restore();
|
|
439
|
+
if (!clipInside) {
|
|
440
|
+
// The caller's path already ends where the window does, so it needs no
|
|
441
|
+
// clip — and its round caps may overhang the boundary by half a stroke,
|
|
442
|
+
// which is exactly the look: a rounded end sitting on the muted trace.
|
|
443
|
+
inside();
|
|
444
|
+
return stats;
|
|
445
|
+
}
|
|
446
|
+
ctx.save();
|
|
447
|
+
ctx.beginPath();
|
|
448
|
+
ctx.rect(x0, 0, x1 - x0, height);
|
|
449
|
+
ctx.clip();
|
|
450
|
+
inside();
|
|
451
|
+
ctx.restore();
|
|
452
|
+
// The stats describe the geometry walked, and the clipped pass walks the
|
|
453
|
+
// same series — reporting it twice would double every count in the draw
|
|
454
|
+
// budget for what is one trace.
|
|
455
|
+
return stats;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* A trace sliced to the key window `[lo, hi]`, with its **endpoints
|
|
459
|
+
* interpolated** onto the path — or `null` when nothing of it falls inside.
|
|
460
|
+
*
|
|
461
|
+
* This exists so the emphasised pass of a partitioned draw can be a real path
|
|
462
|
+
* whose own ends are the window's ends, which is what lets a **round cap**
|
|
463
|
+
* show. A clipped stroke cannot have one: the clip shears the ribbon on a
|
|
464
|
+
* vertical line wherever the rect cuts it, so the cap is drawn off in the
|
|
465
|
+
* hidden part of the path and the visible end is always a hard vertical edge,
|
|
466
|
+
* whatever `lineCap` says.
|
|
467
|
+
*
|
|
468
|
+
* The endpoints are interpolated rather than snapped to the nearest sample
|
|
469
|
+
* because at low density snapping would visibly overshoot or undershoot the
|
|
470
|
+
* window the reader just swept — the emphasis would not line up with the band
|
|
471
|
+
* that produced it.
|
|
472
|
+
*
|
|
473
|
+
* A boundary landing on a **gap** contributes no interpolated point: there is
|
|
474
|
+
* no drawn segment there to sit on, and inventing one would bridge a hole the
|
|
475
|
+
* trace deliberately shows.
|
|
476
|
+
*/
|
|
477
|
+
export function sliceTrace(cs, lo, hi) {
|
|
478
|
+
const n = cs.length;
|
|
479
|
+
if (n === 0 || hi <= lo)
|
|
480
|
+
return null;
|
|
481
|
+
// First index at or past `lo`, first past `hi` — the interior run.
|
|
482
|
+
let a = 0;
|
|
483
|
+
let b = n;
|
|
484
|
+
while (a < b) {
|
|
485
|
+
const mid = (a + b) >> 1;
|
|
486
|
+
if (cs.x[mid] < lo)
|
|
487
|
+
a = mid + 1;
|
|
488
|
+
else
|
|
489
|
+
b = mid;
|
|
490
|
+
}
|
|
491
|
+
let e = a;
|
|
492
|
+
while (e < n && cs.x[e] <= hi)
|
|
493
|
+
e += 1;
|
|
494
|
+
const xs = [];
|
|
495
|
+
const ys = [];
|
|
496
|
+
/** Value on the segment `[i-1, i]` at key `k`, or null across a gap/edge. */
|
|
497
|
+
const at = (i, k) => {
|
|
498
|
+
if (i <= 0 || i >= n)
|
|
499
|
+
return null;
|
|
500
|
+
const y0 = cs.y[i - 1];
|
|
501
|
+
const y1 = cs.y[i];
|
|
502
|
+
if (!Number.isFinite(y0) || !Number.isFinite(y1))
|
|
503
|
+
return null;
|
|
504
|
+
const x0 = cs.x[i - 1];
|
|
505
|
+
const x1 = cs.x[i];
|
|
506
|
+
if (x1 === x0)
|
|
507
|
+
return y1;
|
|
508
|
+
const t = (k - x0) / (x1 - x0);
|
|
509
|
+
return y0 + (y1 - y0) * t;
|
|
510
|
+
};
|
|
511
|
+
const head = at(a, lo);
|
|
512
|
+
if (head !== null) {
|
|
513
|
+
xs.push(lo);
|
|
514
|
+
ys.push(head);
|
|
515
|
+
}
|
|
516
|
+
for (let i = a; i < e; i += 1) {
|
|
517
|
+
xs.push(cs.x[i]);
|
|
518
|
+
ys.push(cs.y[i]);
|
|
519
|
+
}
|
|
520
|
+
const tail = at(e, hi);
|
|
521
|
+
if (tail !== null) {
|
|
522
|
+
xs.push(hi);
|
|
523
|
+
ys.push(tail);
|
|
524
|
+
}
|
|
525
|
+
if (xs.length === 0)
|
|
526
|
+
return null;
|
|
527
|
+
return {
|
|
528
|
+
x: Float64Array.from(xs),
|
|
529
|
+
y: Float64Array.from(ys),
|
|
530
|
+
length: xs.length,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* **EXPERIMENT ([PND-ANNSNAP]).** Vertical rules at a swept window's edges, in
|
|
535
|
+
* the annotation register — a preview of what "promote this sweep to an
|
|
536
|
+
* annotation" would look like, drawn *underneath* the trace.
|
|
537
|
+
*
|
|
538
|
+
* Two caveats, and the second decides whether this survives:
|
|
539
|
+
*
|
|
540
|
+
* - **Opaque on purpose.** Every spanned layer in the row draws its own edges
|
|
541
|
+
* at the same x, so a translucent stroke would composite once per trace and
|
|
542
|
+
* darken with the number of series. Opaque makes the overdraw idempotent.
|
|
543
|
+
* - **A real annotation could not sit here.** Annotations render in the SVG
|
|
544
|
+
* overlay *above* the canvas, so a promoted span's rules would land on top of
|
|
545
|
+
* the traces, not under them. This is canvas-side precisely because "under"
|
|
546
|
+
* was asked for — if the look is kept, the honest options are to accept rules
|
|
547
|
+
* above the ink, or to give the annotation register a canvas-underlay pass.
|
|
548
|
+
*/
|
|
549
|
+
/**
|
|
550
|
+
* The plot's extent in **CSS pixels**, from the scales' own ranges.
|
|
551
|
+
*
|
|
552
|
+
* Not `ctx.canvas.width`/`height`: those are **device** pixels, while the
|
|
553
|
+
* context is pre-transformed by the device ratio, so mixing them overshoots at
|
|
554
|
+
* dpr>1 and — the case that actually breaks — *undershoots* at dpr<1 (a
|
|
555
|
+
* zoomed-out browser), collapsing a clip that should span the plot. The scales
|
|
556
|
+
* are the honest source, and this needs no dpr arithmetic at all. Falls back to
|
|
557
|
+
* the canvas dims only when a scale exposes no range (a bare test stub).
|
|
558
|
+
*/
|
|
559
|
+
export function plotExtentOf(ctx, xScale, yScale) {
|
|
560
|
+
const span = (s, fallback) => {
|
|
561
|
+
const r = s.range?.();
|
|
562
|
+
if (r === undefined || r.length < 2)
|
|
563
|
+
return fallback;
|
|
564
|
+
return Math.abs(+r[r.length - 1] - +r[0]);
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
width: span(xScale, ctx.canvas.width),
|
|
568
|
+
height: span(yScale, ctx.canvas.height),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
export function strokeSpanEdges(ctx, windowPx, height, color, width = 1) {
|
|
572
|
+
const prior = ctx.strokeStyle;
|
|
573
|
+
const priorWidth = ctx.lineWidth;
|
|
574
|
+
const priorAlpha = ctx.globalAlpha;
|
|
575
|
+
ctx.globalAlpha = 1;
|
|
576
|
+
ctx.strokeStyle = color;
|
|
577
|
+
ctx.lineWidth = width;
|
|
578
|
+
for (const x of windowPx) {
|
|
579
|
+
// Half-pixel offset so a 1px rule lands on one device column instead of
|
|
580
|
+
// straddling two and rendering as a 2px smear.
|
|
581
|
+
const px = Math.round(x) + 0.5;
|
|
582
|
+
ctx.beginPath();
|
|
583
|
+
ctx.moveTo(px, 0);
|
|
584
|
+
ctx.lineTo(px, height);
|
|
585
|
+
ctx.stroke();
|
|
586
|
+
}
|
|
587
|
+
ctx.strokeStyle = prior;
|
|
588
|
+
ctx.lineWidth = priorWidth;
|
|
589
|
+
ctx.globalAlpha = priorAlpha;
|
|
590
|
+
}
|
|
263
591
|
//# sourceMappingURL=line.js.map
|
package/dist/ohlc.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Scale } from './line.js';
|
|
|
3
3
|
import type { CandleStyle } from './theme.js';
|
|
4
4
|
import type { LayerDrawStats } from './context.js';
|
|
5
5
|
import { type DecimateOption } from './decimate.js';
|
|
6
|
+
import type { SpanSelection } from './context.js';
|
|
6
7
|
/**
|
|
7
8
|
* How an OHLC mark renders (pjm17971's fork 2 — bundled as one component, like
|
|
8
9
|
* {@link BoxShape}, not split into a separate `<OHLCBar>`):
|
|
@@ -79,5 +80,19 @@ export declare function resolveCandleStyle(style: CandleStyle, open: number, clo
|
|
|
79
80
|
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
80
81
|
* beyond the `barSpanPx` tuple.
|
|
81
82
|
*/
|
|
82
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The candle whose **slot** contains `(px, py)` — the same rect-containment
|
|
85
|
+
* `boxAt` does, over the candle's full `[x0, x1] × [high, low]` extent.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately the slot and not the ink. A candle's body can be a doji a
|
|
88
|
+
* pixel tall and its wick is a hairline; requiring the pointer to land on
|
|
89
|
+
* drawn pixels would make most candles unclickable. (That the box layer makes
|
|
90
|
+
* the same choice *without* saying so is [PND-BOXHIT].)
|
|
91
|
+
*/
|
|
92
|
+
export declare function ohlcAt(ohlc: OhlcSeries, px: number, py: number, xScale: Scale, yScale: Scale, gapPx: number, minWidthPx: number): [index: number, begin: number, close: number] | null;
|
|
93
|
+
export declare function drawCandles(ctx: CanvasRenderingContext2D, ohlc: OhlcSeries, xScale: Scale, yScale: Scale, style: CandleStyle, variant?: CandleVariant, colorBy?: ColorBy, gapPx?: number, minWidthPx?: number, decimate?: DecimateOption,
|
|
94
|
+
/** Candle keys (each candle's `x`) currently selected / hovered, and the
|
|
95
|
+
* selection's span entries — the same three channels the bar and box draws
|
|
96
|
+
* take. Empty ⇒ a display-only candle, byte-identical to before. */
|
|
97
|
+
selectedKeys?: readonly number[], hoveredKeys?: readonly number[], spans?: readonly SpanSelection[]): LayerDrawStats;
|
|
83
98
|
//# sourceMappingURL=ohlc.d.ts.map
|