@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/XAxis.js
CHANGED
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Fragment, useContext } from 'react';
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
4
|
import { derivedTicks } from './derivedTicks.js';
|
|
5
|
-
import { ContainerContext, CursorContext } from './context.js';
|
|
5
|
+
import { ContainerContext, CursorContext, } from './context.js';
|
|
6
|
+
import { xAxisCursorEntries } from './cursors.js';
|
|
6
7
|
import { axisPillStyle } from './chip.js';
|
|
7
8
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
8
9
|
/** Tick strip height (mark + value label) in CSS px. */
|
|
@@ -65,16 +66,19 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
65
66
|
// gridlines and `formatTime` use, so labels and grid stay on the same instants
|
|
66
67
|
// (width-derived on a trading-time axis).
|
|
67
68
|
const { xScale, plotWidth, leftGutter, theme, formatTime, xKind, xTickCount, } = container;
|
|
68
|
-
// The
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
69
|
+
// The cursor's x-axis slot: did the mounted cursor in effect register one
|
|
70
|
+
// (`renderXAxis` — the crosshair's time pill)? While hovering, that is the
|
|
71
|
+
// **hovered row's** effective cursor — so a per-row override reaches this
|
|
72
|
+
// axis, which the old `container.cursor === 'crosshair'` string gate never
|
|
73
|
+
// let it do (a row-level crosshair had no time pill); with no live pointer
|
|
74
|
+
// (a controlled `trackerPosition`), any scope's effective cursor keeps its
|
|
75
|
+
// pill. The slot renders below when the cursor is live in-bounds.
|
|
72
76
|
const cursorX = cursor.cursorX;
|
|
73
|
-
const
|
|
77
|
+
const xSlotEntries = xAxisCursorEntries(container.cursors, cursor.cursorRowKey).filter((e) => e.spec.renderXAxis !== undefined);
|
|
78
|
+
const showCursorTag = xSlotEntries.length > 0 &&
|
|
74
79
|
cursorX !== null &&
|
|
75
80
|
cursorX >= 0 &&
|
|
76
81
|
cursorX <= plotWidth;
|
|
77
|
-
const cursorColor = theme.cursor ?? theme.axis.label;
|
|
78
82
|
const annotationColor = theme.annotation?.color ?? '#0d9488';
|
|
79
83
|
// Derived-unit (`transform`) layout: nice ticks in the derived unit at
|
|
80
84
|
// mixed step sizes, admitted where they keep pixel room (see derivedTicks).
|
|
@@ -417,20 +421,34 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
417
421
|
[onTop ? 'bottom' : 'top']: `${laneY}px`,
|
|
418
422
|
zIndex: 2,
|
|
419
423
|
}, children: t.text })] }, t.id));
|
|
420
|
-
}), showCursorTag &&
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
424
|
+
}), showCursorTag &&
|
|
425
|
+
(() => {
|
|
426
|
+
// The x-axis {@link ResolvedCursorFrame}: no row, no samples — the
|
|
427
|
+
// slot draws a pill from the resolved cursor x + the axis's own
|
|
428
|
+
// readout-formatted time (this axis's precedence chain, `readoutFmt`),
|
|
429
|
+
// placed per the strip's side + tick-label offset.
|
|
430
|
+
const f = {
|
|
431
|
+
cursorX,
|
|
432
|
+
cursorY: null,
|
|
433
|
+
rowKey: null,
|
|
434
|
+
hoveredRowKey: cursor.cursorRowKey,
|
|
435
|
+
samples: [],
|
|
436
|
+
flags: [],
|
|
437
|
+
pointer: null,
|
|
438
|
+
band: null,
|
|
439
|
+
bandY: null, // …nor a transposed one: same reason as `rect`
|
|
440
|
+
bandLine: false,
|
|
441
|
+
bandDragging: false, // no band in the axis strip to edge
|
|
442
|
+
rect: null, // …and no 2-D brush: the strip has no y
|
|
443
|
+
restingCross: false,
|
|
444
|
+
formattedTime: readoutFmt(+xScale.invert(cursorX)),
|
|
445
|
+
plotWidth,
|
|
446
|
+
rowHeight: 0,
|
|
447
|
+
isFirstRow: false,
|
|
448
|
+
theme,
|
|
449
|
+
xAxis: { onTop, pillOffset },
|
|
450
|
+
};
|
|
451
|
+
return xSlotEntries.map((e, i) => (_jsx(Fragment, { children: e.spec.renderXAxis(f) }, `cursor-${i}`)));
|
|
452
|
+
})()] }));
|
|
435
453
|
}
|
|
436
454
|
//# sourceMappingURL=XAxis.js.map
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -46,8 +46,64 @@ export interface YAxisProps {
|
|
|
46
46
|
*
|
|
47
47
|
* A dev-mode warning fires for the cases that are unambiguously a mistake: a
|
|
48
48
|
* refused bound, negative data, or an axis with no positive data at all.
|
|
49
|
+
*
|
|
50
|
+
* `'symlog'` is **linear through zero, logarithmic beyond** — for data that
|
|
51
|
+
* spans orders of magnitude *on both sides of zero*, which `'log'` cannot
|
|
52
|
+
* express at all (it admits no zero and no negatives). The linear window is
|
|
53
|
+
* {@link linearWindow}. Because it admits zero, it resolves its domain on the
|
|
54
|
+
* ordinary **linear** path: no positive-only bound refusal, no rounding out to
|
|
55
|
+
* decades, no gapping of non-positive samples.
|
|
56
|
+
*
|
|
57
|
+
* **The axis owns tick placement, and that is the substance of the feature.**
|
|
58
|
+
* d3's symlog supplies the transform but ticks it *linearly*, which on a ±1M
|
|
59
|
+
* domain with a 20k knee labels nothing below the knee — the exact region the
|
|
60
|
+
* scale was chosen to reveal. pond grids it on zero, the knee (±`linearWindow ×
|
|
61
|
+
* maxAbs`) and mirrored decades beyond, thinned by the same rule the log axis
|
|
62
|
+
* uses. See `yticks.ts`.
|
|
63
|
+
*
|
|
64
|
+
* **The curve is `log1p`, not piecewise — read this before replacing a
|
|
65
|
+
* hand-rolled one.** "Linear through zero, logarithmic beyond" describes how the
|
|
66
|
+
* axis *reads*, not two joined segments: `scaleSymlog` is the single smooth
|
|
67
|
+
* `sign(x) · log1p(|x / knee|)`, so there is no exact boundary at which one law
|
|
68
|
+
* stops and the other starts. A common hand-rolled curve *is* piecewise —
|
|
69
|
+
* exactly linear below the knee, `log10` above — and the two are the same family
|
|
70
|
+
* with materially different shape. Swapping one for the other, a reporting
|
|
71
|
+
* consumer measured small values landing at **roughly half** their former height
|
|
72
|
+
* (a ±9M domain: 283k went from 0.44 to 0.24 of the half-plot above the zero
|
|
73
|
+
* line), while order, the dominance of the tail, and a several-fold lift over a
|
|
74
|
+
* linear axis all held — the chart still says the same thing, but it does not
|
|
75
|
+
* say it identically.
|
|
76
|
+
*
|
|
77
|
+
* **No `linearWindow` recovers a piecewise shape.** The same consumer tried: a
|
|
78
|
+
* smaller window fits the large values while overshooting the small ones about
|
|
79
|
+
* 2×, because the difference is the curve, not the knee. If you need the
|
|
80
|
+
* piecewise curve exactly, you need your own transform — which is the thing this
|
|
81
|
+
* scale exists to let you delete, so weigh that before reaching for it.
|
|
82
|
+
*/
|
|
83
|
+
scale?: 'linear' | 'log' | 'symlog';
|
|
84
|
+
/**
|
|
85
|
+
* `scale="symlog"`'s **linear window**, as a fraction of the domain's largest
|
|
86
|
+
* magnitude. **Default `0.02`** — the knee sits at 2% of `maxAbs`, so a ±1M
|
|
87
|
+
* domain is linear through ±20k and logarithmic beyond. Ignored on any other
|
|
88
|
+
* scale.
|
|
89
|
+
*
|
|
90
|
+
* **Domain-relative, not absolute** (d3's own `constant` is absolute). A chart
|
|
91
|
+
* that re-keys to the largest magnitude on every update would otherwise need
|
|
92
|
+
* the constant recomputed each tick, and would drift silently the moment
|
|
93
|
+
* someone forgot — the fraction survives a domain change with no call-site
|
|
94
|
+
* arithmetic at all.
|
|
95
|
+
*
|
|
96
|
+
* Precisely: a fraction of the **resolved domain before any pan/zoom** — the
|
|
97
|
+
* one the axis's `min`/`max`/`pad`/auto-fit produce. A 2-D gesture is carried
|
|
98
|
+
* as a *pixel* transform and the knee is deliberately **not** recomputed from
|
|
99
|
+
* the zoomed window, so zooming moves the plot without moving the boundary
|
|
100
|
+
* between the two régimes underneath it. (Recomputing would make the same
|
|
101
|
+
* datum linear at one zoom level and logarithmic at the next.)
|
|
102
|
+
*
|
|
103
|
+
* A value outside `(0, 1]` cannot be a knee; the axis draws with the default
|
|
104
|
+
* instead and dev-warns which window is in force.
|
|
49
105
|
*/
|
|
50
|
-
|
|
106
|
+
linearWindow?: number;
|
|
51
107
|
/** Explicit domain bounds; omit to auto-fit the charts linked to this axis. */
|
|
52
108
|
min?: number;
|
|
53
109
|
max?: number;
|
|
@@ -155,5 +211,5 @@ export interface YAxisProps {
|
|
|
155
211
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
156
212
|
* (default: the first axis).
|
|
157
213
|
*/
|
|
158
|
-
export declare function YAxis({ id, side, label, scale, min, max, format, ticks, tickCount, pad, boundaryLabels, width, hide, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element | null;
|
|
214
|
+
export declare function YAxis({ id, side, label, scale, linearWindow, min, max, format, ticks, tickCount, pad, boundaryLabels, width, hide, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element | null;
|
|
159
215
|
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_TICK_COUNT = 5;
|
|
|
17
17
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
18
18
|
* (default: the first axis).
|
|
19
19
|
*/
|
|
20
|
-
export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, hide = false, labelPlacement = 'rotated', color, index = 0, }) {
|
|
20
|
+
export function YAxis({ id, side = 'left', label, scale = 'linear', linearWindow, min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, hide = false, labelPlacement = 'rotated', color, index = 0, }) {
|
|
21
21
|
const container = useContext(ContainerContext);
|
|
22
22
|
if (container === null) {
|
|
23
23
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -35,6 +35,7 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
35
35
|
// and layers still bind to it, which is the whole point of the prop.
|
|
36
36
|
width: hide ? 0 : width,
|
|
37
37
|
scale,
|
|
38
|
+
linearWindow,
|
|
38
39
|
min,
|
|
39
40
|
max,
|
|
40
41
|
pad,
|
|
@@ -49,6 +50,7 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
49
50
|
width,
|
|
50
51
|
hide,
|
|
51
52
|
scale,
|
|
53
|
+
linearWindow,
|
|
52
54
|
min,
|
|
53
55
|
max,
|
|
54
56
|
pad,
|
package/dist/area.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CurveFactory } from 'd3-shape';
|
|
2
2
|
import type { ChartSeries } from './data.js';
|
|
3
|
-
import { type Scale } from './line.js';
|
|
3
|
+
import { type Scale, type TraceState } from './line.js';
|
|
4
4
|
import type { AreaStyle } from './theme.js';
|
|
5
5
|
import type { LayerDrawStats } from './context.js';
|
|
6
6
|
import { type GapMode } from './gaps.js';
|
|
@@ -70,4 +70,37 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
|
|
|
70
70
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
71
71
|
*/
|
|
72
72
|
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption): LayerDrawStats;
|
|
73
|
+
/**
|
|
74
|
+
* **Is the pointer inside this area?** The filled-region counterpart of
|
|
75
|
+
* `traceHitIndex` ([PND-TRACESEL]) — returns the nearest sample's index as
|
|
76
|
+
* click provenance, or `null` when the pointer is outside the fill.
|
|
77
|
+
*
|
|
78
|
+
* An area is not a stroke, so the test is not distance-to-path: the pointer is
|
|
79
|
+
* on the area when it lies **between the trace and the baseline** at that x.
|
|
80
|
+
* That is the honest reading of "you clicked the area", and it makes the whole
|
|
81
|
+
* filled shape the target rather than a 1.5px edge — which is the same reason
|
|
82
|
+
* the list family made the row the target rather than the bar.
|
|
83
|
+
*
|
|
84
|
+
* The x cut bisects `xScale(cs.x[i])` for `traceHitIndex`'s reasons (a layer's
|
|
85
|
+
* `hitTest` gets the forward scale only, and a trading-time scale has no honest
|
|
86
|
+
* inverse), and the trace's y is **interpolated** between the bracketing
|
|
87
|
+
* samples so the boundary follows the drawn edge rather than a step. A tolerance
|
|
88
|
+
* is still added, so the top edge is grabbable from just outside.
|
|
89
|
+
*
|
|
90
|
+
* A gap is a hole, not a bridge: either bracketing sample non-finite ⇒ no hit,
|
|
91
|
+
* matching what `drawArea` actually fills.
|
|
92
|
+
*/
|
|
93
|
+
export declare function areaHitIndex(cs: ChartSeries, baseline: number | undefined, px: number, py: number, xScale: Scale, yScale: Scale, tolerance?: number): number | null;
|
|
94
|
+
/**
|
|
95
|
+
* The style an area fills with in a given interaction state
|
|
96
|
+
* ([PND-TRACESEL]) — the {@link AreaStyle} counterpart of `traceStateStyle`.
|
|
97
|
+
*
|
|
98
|
+
* The channels differ from a line's because what carries the mark differs: an
|
|
99
|
+
* area's mark is its **fill**, so state is the fill's strength plus the edge's
|
|
100
|
+
* weight. A line has only a stroke, so there weight is all there is.
|
|
101
|
+
*
|
|
102
|
+
* Alpha comes back separately, as it does for a line, so a muted area keeps its
|
|
103
|
+
* hue rather than having the fade baked into a colour.
|
|
104
|
+
*/
|
|
105
|
+
export declare function areaStateStyle(style: AreaStyle, state: TraceState): readonly [AreaStyle, number];
|
|
73
106
|
//# sourceMappingURL=area.d.ts.map
|
package/dist/area.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
|
-
import { strokeAffinePolyline } from './line.js';
|
|
2
|
+
import { baselinePxFromScale, strokeAffinePolyline, TRACE_HIT_PX, } from './line.js';
|
|
3
3
|
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
4
4
|
import { cullChartSeries } from './culling.js';
|
|
5
5
|
import { decimateM4Cached } from './decimate.js';
|
|
@@ -345,4 +345,91 @@ function buildGradient(ctx, valueExtent, yScale, baselinePx, style) {
|
|
|
345
345
|
}
|
|
346
346
|
return grad;
|
|
347
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* **Is the pointer inside this area?** The filled-region counterpart of
|
|
350
|
+
* `traceHitIndex` ([PND-TRACESEL]) — returns the nearest sample's index as
|
|
351
|
+
* click provenance, or `null` when the pointer is outside the fill.
|
|
352
|
+
*
|
|
353
|
+
* An area is not a stroke, so the test is not distance-to-path: the pointer is
|
|
354
|
+
* on the area when it lies **between the trace and the baseline** at that x.
|
|
355
|
+
* That is the honest reading of "you clicked the area", and it makes the whole
|
|
356
|
+
* filled shape the target rather than a 1.5px edge — which is the same reason
|
|
357
|
+
* the list family made the row the target rather than the bar.
|
|
358
|
+
*
|
|
359
|
+
* The x cut bisects `xScale(cs.x[i])` for `traceHitIndex`'s reasons (a layer's
|
|
360
|
+
* `hitTest` gets the forward scale only, and a trading-time scale has no honest
|
|
361
|
+
* inverse), and the trace's y is **interpolated** between the bracketing
|
|
362
|
+
* samples so the boundary follows the drawn edge rather than a step. A tolerance
|
|
363
|
+
* is still added, so the top edge is grabbable from just outside.
|
|
364
|
+
*
|
|
365
|
+
* A gap is a hole, not a bridge: either bracketing sample non-finite ⇒ no hit,
|
|
366
|
+
* matching what `drawArea` actually fills.
|
|
367
|
+
*/
|
|
368
|
+
export function areaHitIndex(cs, baseline, px, py, xScale, yScale, tolerance = TRACE_HIT_PX) {
|
|
369
|
+
const n = cs.length;
|
|
370
|
+
if (n === 0)
|
|
371
|
+
return null;
|
|
372
|
+
let lo = 0;
|
|
373
|
+
let hi = n;
|
|
374
|
+
while (lo < hi) {
|
|
375
|
+
const mid = (lo + hi) >> 1;
|
|
376
|
+
if (xScale(cs.x[mid]) < px)
|
|
377
|
+
lo = mid + 1;
|
|
378
|
+
else
|
|
379
|
+
hi = mid;
|
|
380
|
+
}
|
|
381
|
+
// The bracketing pair around the pointer. At either end of the series the
|
|
382
|
+
// pair degenerates to one sample, which is correct — the fill stops there.
|
|
383
|
+
const i = Math.max(0, Math.min(n - 1, lo));
|
|
384
|
+
const j = Math.max(0, Math.min(n - 1, lo === 0 ? 0 : lo - 1));
|
|
385
|
+
const yi = cs.y[i];
|
|
386
|
+
const yj = cs.y[j];
|
|
387
|
+
if (!Number.isFinite(yi) || !Number.isFinite(yj))
|
|
388
|
+
return null;
|
|
389
|
+
const xi = xScale(cs.x[i]);
|
|
390
|
+
const xj = xScale(cs.x[j]);
|
|
391
|
+
// Interpolate the edge at the pointer's x, so the boundary is the drawn
|
|
392
|
+
// slope and not a staircase. Guard the degenerate same-pixel pair.
|
|
393
|
+
const t = xi === xj ? 0 : (px - xj) / (xi - xj);
|
|
394
|
+
const edgePx = yScale(yj + (yi - yj) * Math.max(0, Math.min(1, t)));
|
|
395
|
+
const basePx = baseline === undefined ? baselinePxFromScale(yScale) : yScale(baseline);
|
|
396
|
+
const top = Math.min(edgePx, basePx) - tolerance;
|
|
397
|
+
const bottom = Math.max(edgePx, basePx) + tolerance;
|
|
398
|
+
if (py < top || py > bottom)
|
|
399
|
+
return null;
|
|
400
|
+
// Outside the series' own x span there is no fill to be inside of.
|
|
401
|
+
if (px < xScale(cs.x[0]) - tolerance)
|
|
402
|
+
return null;
|
|
403
|
+
if (px > xScale(cs.x[n - 1]) + tolerance)
|
|
404
|
+
return null;
|
|
405
|
+
return Math.abs(px - xi) <= Math.abs(px - xj) ? i : j;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The style an area fills with in a given interaction state
|
|
409
|
+
* ([PND-TRACESEL]) — the {@link AreaStyle} counterpart of `traceStateStyle`.
|
|
410
|
+
*
|
|
411
|
+
* The channels differ from a line's because what carries the mark differs: an
|
|
412
|
+
* area's mark is its **fill**, so state is the fill's strength plus the edge's
|
|
413
|
+
* weight. A line has only a stroke, so there weight is all there is.
|
|
414
|
+
*
|
|
415
|
+
* Alpha comes back separately, as it does for a line, so a muted area keeps its
|
|
416
|
+
* hue rather than having the fade baked into a colour.
|
|
417
|
+
*/
|
|
418
|
+
export function areaStateStyle(style, state) {
|
|
419
|
+
const emphasised = {
|
|
420
|
+
...style,
|
|
421
|
+
width: style.selectedWidth ?? style.width * 2,
|
|
422
|
+
fillOpacity: style.selectedFillOpacity ?? Math.min(style.fillOpacity * 2, 1),
|
|
423
|
+
};
|
|
424
|
+
switch (state) {
|
|
425
|
+
case 'selected':
|
|
426
|
+
return [emphasised, 1];
|
|
427
|
+
case 'hover':
|
|
428
|
+
return [emphasised, 1];
|
|
429
|
+
case 'dimmed':
|
|
430
|
+
return [style, style.dimmedOpacity ?? 0.32];
|
|
431
|
+
case 'rest':
|
|
432
|
+
return [style, 1];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
348
435
|
//# sourceMappingURL=area.js.map
|
package/dist/bars.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BarSeries, StackedBarSeries } from './data.js';
|
|
2
2
|
import type { Scale } from './line.js';
|
|
3
3
|
import type { BarStyle } from './theme.js';
|
|
4
|
-
import type { LayerDrawStats } from './context.js';
|
|
4
|
+
import type { LayerDrawStats, SpanSelection } from './context.js';
|
|
5
5
|
import { type DecimateOption } from './decimate.js';
|
|
6
6
|
/**
|
|
7
7
|
* Bar growth direction — the histogram orientation. `'vertical'` bars grow **up**
|
|
@@ -55,7 +55,7 @@ export declare function resolveBarBaseline(yScale: Scale): number;
|
|
|
55
55
|
* hit region are deliberately *not* the same geometry: the `gapPx` inset
|
|
56
56
|
* separates columns visually without carving a dead channel out of the target.
|
|
57
57
|
*/
|
|
58
|
-
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
58
|
+
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number, maxWidthPx?: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
59
59
|
/**
|
|
60
60
|
* The value-space span `[lo, hi]` of **threshold band `k`** along a bar running
|
|
61
61
|
* from `base` to `v`, or `null` when the bar doesn't reach that band.
|
|
@@ -146,6 +146,21 @@ export interface BarMark {
|
|
|
146
146
|
readonly key: number;
|
|
147
147
|
readonly mark?: string;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Stroke a selected mark's outline **inside** its ink rect. Canvas strokes
|
|
151
|
+
* centre on the path, so a plain `strokeRect(x0, yTop, …)` paints
|
|
152
|
+
* `lineWidth / 2` *outside* the rect — which, with the default
|
|
153
|
+
* `outlineWidth: 1.5` against the default `gap: 1`, is enough to bridge the
|
|
154
|
+
* whole inter-bar gap from both sides: a swept run of selected bars fused
|
|
155
|
+
* into one unreadable block (you could not count them). Insetting the path by
|
|
156
|
+
* half the line width keeps every stroked pixel within the bar's own ink, so
|
|
157
|
+
* adjacent selected marks stay separated by exactly the gap the resting bars
|
|
158
|
+
* show. A mark too thin to contain its outline (either dimension
|
|
159
|
+
* `<= lineWidth`) skips the stroke — the highlight fill is already the
|
|
160
|
+
* signal, and an outline wider than the bar would only smear into the
|
|
161
|
+
* neighbours this exists to keep distinct.
|
|
162
|
+
*/
|
|
163
|
+
export declare function strokeSelectedOutline(ctx: CanvasRenderingContext2D, x0: number, x1: number, yTop: number, yBottom: number, lineWidth: number): void;
|
|
149
164
|
/**
|
|
150
165
|
* Fill one rectangle per bar in `cs`, each spanning its key's `[begin, end]`
|
|
151
166
|
* (inset by `gapPx`) from the resolved `baseline` to the value.
|
|
@@ -198,7 +213,7 @@ export interface BarMark {
|
|
|
198
213
|
* repaint them one flat colour; per-bar-coloured layers draw every visible bar.
|
|
199
214
|
* Returns {@link LayerDrawStats} for `onDrawStats`.
|
|
200
215
|
*/
|
|
201
|
-
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, seriesId: string | undefined, selection: BarMark
|
|
216
|
+
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, seriesId: string | undefined, selection: readonly BarMark[], hovered: readonly BarMark[], decimate?: DecimateOption, binFills?: readonly (string | undefined)[], banding?: BandLadder, spans?: readonly SpanSelection[]): LayerDrawStats;
|
|
202
217
|
/**
|
|
203
218
|
* The index of the bar whose key span `[begin, end]` contains `time` — the bar
|
|
204
219
|
* **under the cursor** — or `-1` if `time` falls in no bar's span. This is the
|
|
@@ -291,6 +306,9 @@ export interface StackStyle {
|
|
|
291
306
|
readonly fills: readonly string[];
|
|
292
307
|
readonly opacity: number;
|
|
293
308
|
readonly outlineWidth: number;
|
|
309
|
+
/** Cap on a segment's ink span in px, centred in the slot — the stacked
|
|
310
|
+
* counterpart of `BarStyle.maxWidth` ([PND-BARWIDTH]). Omitted ⇒ uncapped. */
|
|
311
|
+
readonly maxWidth?: number;
|
|
294
312
|
/**
|
|
295
313
|
* Optional **per-bin** fill override, aligned index-for-index to the bins
|
|
296
314
|
* (bin `b` uses `binFills[b]`), taking precedence over the per-group
|
|
@@ -335,6 +353,45 @@ export interface StackStyle {
|
|
|
335
353
|
* resting and live, only the floor.
|
|
336
354
|
*/
|
|
337
355
|
readonly emphasisOpacity?: number;
|
|
356
|
+
/**
|
|
357
|
+
* Fill for a segment **not** in a non-empty selection set — the themed
|
|
358
|
+
* de-emphasis ([PND-MULTISEL], RFC A2.3). Unset ⇒ nothing dims, so this is
|
|
359
|
+
* opt-in and back-compatible. Unlike {@link highlight}, this **does** apply
|
|
360
|
+
* over {@link binFills}: dimming is about recession, not identity, so a
|
|
361
|
+
* zone-coloured bar can recede without its colour changing meaning — the
|
|
362
|
+
* dimmed value simply replaces it while it is out of the set.
|
|
363
|
+
*/
|
|
364
|
+
readonly dimmed?: string;
|
|
365
|
+
/**
|
|
366
|
+
* Per-group {@link dimmed}, parallel to {@link fills} — the resolved
|
|
367
|
+
* `theme.bar.default.groupsDimmed` ramp. Set only for a **group-ramped**
|
|
368
|
+
* stack; `undefined` falls back to the flat {@link dimmed}.
|
|
369
|
+
*
|
|
370
|
+
* A stack dimmed to one colour stops being a stack — the segment boundaries
|
|
371
|
+
* vanish and the receded bins read as solid blocks, which is exactly the
|
|
372
|
+
* structure a selection wants to keep visible for comparison.
|
|
373
|
+
*/
|
|
374
|
+
readonly dimmedFills?: readonly string[];
|
|
375
|
+
/**
|
|
376
|
+
* Per-group {@link hover}, parallel to {@link fills} — the resolved
|
|
377
|
+
* `theme.bar.default.groupsHover` ramp. Set only for a **group-ramped**
|
|
378
|
+
* stack; `undefined` falls back to the flat {@link hover}.
|
|
379
|
+
*
|
|
380
|
+
* A single hover colour repaints the pointed-at segment in a hue belonging
|
|
381
|
+
* to another group, so hovering erases the ramp exactly where the reader is
|
|
382
|
+
* looking — and block-scoped hover erases the whole bin.
|
|
383
|
+
*/
|
|
384
|
+
readonly hoverFills?: readonly string[];
|
|
385
|
+
/**
|
|
386
|
+
* Whether {@link fills} carry **group identity** (a resolved group ramp)
|
|
387
|
+
* rather than being one repeated role colour.
|
|
388
|
+
*
|
|
389
|
+
* When they do, a selected segment keeps its own fill and the outline plus
|
|
390
|
+
* the receded neighbours are the cue — the same exclusion {@link binFills}
|
|
391
|
+
* already gets, and for the same reason: replacing a meaning-carrying colour
|
|
392
|
+
* with the flat `highlight` destroys the thing the selection is *about*.
|
|
393
|
+
*/
|
|
394
|
+
readonly groupColored?: boolean;
|
|
338
395
|
}
|
|
339
396
|
/** The narrowed selection / hover identity a stacked segment matches against:
|
|
340
397
|
* the series `id`, the bin's `begin` (its `key`), and the group (its `label`).
|
|
@@ -403,7 +460,7 @@ export declare function stackBase(orientation: Orientation, xScale: Scale, yScal
|
|
|
403
460
|
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
404
461
|
* and the hit rect are identical.
|
|
405
462
|
*/
|
|
406
|
-
export declare function segmentRect(ss: StackedBarSeries, b: number, g: number, orientation: Orientation, xScale: Scale, yScale: Scale, cumBefore: number, gapPx: number, minSpanPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
463
|
+
export declare function segmentRect(ss: StackedBarSeries, b: number, g: number, orientation: Orientation, xScale: Scale, yScale: Scale, cumBefore: number, gapPx: number, minSpanPx: number, maxSpanPx?: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
407
464
|
/**
|
|
408
465
|
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
409
466
|
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
@@ -418,7 +475,7 @@ export declare function segmentRect(ss: StackedBarSeries, b: number, g: number,
|
|
|
418
475
|
*
|
|
419
476
|
* O(N·G) over bins × groups, one fill (+ optional stroke) per drawn segment.
|
|
420
477
|
*/
|
|
421
|
-
export declare function drawStacks(ctx: CanvasRenderingContext2D, ss: StackedBarSeries, orientation: Orientation, xScale: Scale, yScale: Scale, style: StackStyle, gapPx: number, minSpanPx: number, seriesId: string | undefined, selection: StackMark
|
|
478
|
+
export declare function drawStacks(ctx: CanvasRenderingContext2D, ss: StackedBarSeries, orientation: Orientation, xScale: Scale, yScale: Scale, style: StackStyle, gapPx: number, minSpanPx: number, seriesId: string | undefined, selection: readonly StackMark[], hover: readonly StackMark[], banding?: BandLadder, spans?: readonly SpanSelection[]): void;
|
|
422
479
|
/**
|
|
423
480
|
* Hit-test plot-pixel `(px, py)` against `ss`'s stacked segments — the **first**
|
|
424
481
|
* segment whose rect contains the point, or `null`. The geometry is
|
|
@@ -430,5 +487,9 @@ export declare function drawStacks(ctx: CanvasRenderingContext2D, ss: StackedBar
|
|
|
430
487
|
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
431
488
|
* small; click / hover are cheap events).
|
|
432
489
|
*/
|
|
433
|
-
export declare function stackAt(ss: StackedBarSeries, px: number, py: number, orientation: Orientation, xScale: Scale, yScale: Scale, gapPx: number, minSpanPx: number
|
|
490
|
+
export declare function stackAt(ss: StackedBarSeries, px: number, py: number, orientation: Orientation, xScale: Scale, yScale: Scale, gapPx: number, minSpanPx: number,
|
|
491
|
+
/** Must match the draw's cap ([PND-BARWIDTH]) — this function's whole
|
|
492
|
+
* contract is that its rect is the drawn rect, so a cap applied to one and
|
|
493
|
+
* not the other silently drifts the hit target off the ink. */
|
|
494
|
+
maxSpanPx?: number): [bin: number, group: number, begin: number, name: string, value: number] | null;
|
|
434
495
|
//# sourceMappingURL=bars.d.ts.map
|