@pond-ts/charts 0.56.2 → 0.58.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 +1218 -1
- package/dist/AreaChart.d.ts +12 -1
- package/dist/AreaChart.js +131 -13
- package/dist/BarChart.d.ts +84 -9
- package/dist/BarChart.js +295 -40
- 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 +79 -48
- package/dist/ChartContainer.js +482 -60
- package/dist/ChartRow.d.ts +9 -2
- package/dist/ChartRow.js +86 -12
- 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 +28 -1
- package/dist/YAxis.js +24 -2
- package/dist/annotations.d.ts +74 -0
- package/dist/annotations.js +97 -7
- package/dist/area.d.ts +34 -1
- package/dist/area.js +88 -1
- package/dist/bars.d.ts +178 -5
- package/dist/bars.js +504 -46
- 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 +871 -36
- package/dist/cursors.d.ts +161 -0
- package/dist/cursors.js +503 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +157 -0
- package/dist/heat.d.ts +163 -0
- package/dist/heat.js +659 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +25 -2
- 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/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 +517 -11
- package/dist/theme.js +220 -39
- 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/package.json +3 -3
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
|
@@ -106,6 +106,33 @@ export interface YAxisProps {
|
|
|
106
106
|
boundaryLabels?: boolean;
|
|
107
107
|
/** Gutter width in CSS pixels (default 50). */
|
|
108
108
|
width?: number;
|
|
109
|
+
/**
|
|
110
|
+
* **Keep the scale, draw no gutter.** The axis still registers its domain
|
|
111
|
+
* (`min`/`max`/`scale`/`pad`) and layers still bind to it by `id`, but it
|
|
112
|
+
* renders nothing and reserves **no width** — the plot gets the space.
|
|
113
|
+
*
|
|
114
|
+
* A `<YAxis>` does two jobs: it *holds the scale* and it *renders a gutter*.
|
|
115
|
+
* Without this there was no way to ask for the first without the second, so a
|
|
116
|
+
* chart with a **fixed** domain whose scale is already explained by its
|
|
117
|
+
* chrome (threshold band lines, a legend, a panel header) had two reachable
|
|
118
|
+
* options and needed a third:
|
|
119
|
+
*
|
|
120
|
+
* | | auto domain | explicit domain |
|
|
121
|
+
* |---|---|---|
|
|
122
|
+
* | **gutter** | `<YAxis />` | `<YAxis min max />` |
|
|
123
|
+
* | **no gutter** | omit the axis | ← this prop |
|
|
124
|
+
*
|
|
125
|
+
* Omitting the axis is not the same thing: the row then supplies an implicit
|
|
126
|
+
* auto-domain axis, and the fixed domain is exactly what must not be given
|
|
127
|
+
* up. `width={0}` is not it either — the labels still draw, now over the
|
|
128
|
+
* plot.
|
|
129
|
+
*
|
|
130
|
+
* **Gridlines are unaffected.** They belong to the plot, not the gutter, and
|
|
131
|
+
* `<ChartContainer grid>` already controls them — so a hidden axis can still rule
|
|
132
|
+
* its own gridlines, which is usually what a "the shape matters, the numbers
|
|
133
|
+
* don't" chart wants. Turn them off there if you want neither.
|
|
134
|
+
*/
|
|
135
|
+
hide?: boolean;
|
|
109
136
|
/**
|
|
110
137
|
* This axis instance's colour — tick labels and the axis title take it,
|
|
111
138
|
* overriding the theme's `axis.label` / `axis.title.color`. The multi-axis
|
|
@@ -128,5 +155,5 @@ export interface YAxisProps {
|
|
|
128
155
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
129
156
|
* (default: the first axis).
|
|
130
157
|
*/
|
|
131
|
-
export declare function YAxis({ id, side, label, scale, min, max, format, ticks, tickCount, pad, boundaryLabels, width, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
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;
|
|
132
159
|
//# 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, labelPlacement = 'rotated', color, index = 0, }) {
|
|
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, }) {
|
|
21
21
|
const container = useContext(ContainerContext);
|
|
22
22
|
if (container === null) {
|
|
23
23
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -29,7 +29,11 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
29
29
|
const spec = useMemo(() => ({
|
|
30
30
|
id,
|
|
31
31
|
side,
|
|
32
|
-
|
|
32
|
+
// A hidden axis reserves no gutter — the row's slot placement reads this
|
|
33
|
+
// width, so zeroing it here is what gives the space back to the plot.
|
|
34
|
+
// Everything else about the spec is unchanged: the domain still resolves
|
|
35
|
+
// and layers still bind to it, which is the whole point of the prop.
|
|
36
|
+
width: hide ? 0 : width,
|
|
33
37
|
scale,
|
|
34
38
|
min,
|
|
35
39
|
max,
|
|
@@ -43,6 +47,7 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
43
47
|
id,
|
|
44
48
|
side,
|
|
45
49
|
width,
|
|
50
|
+
hide,
|
|
46
51
|
scale,
|
|
47
52
|
min,
|
|
48
53
|
max,
|
|
@@ -65,6 +70,23 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
65
70
|
useEffect(() => {
|
|
66
71
|
registerAxis(slot, spec);
|
|
67
72
|
}, [registerAxis, slot, spec]);
|
|
73
|
+
// `hide`: everything above still runs — the axis is registered, so its scale
|
|
74
|
+
// exists and layers bind to it — and everything below (the gutter chrome)
|
|
75
|
+
// does not. Placed after the last hook so the early return can't change hook
|
|
76
|
+
// order when `hide` is toggled at runtime.
|
|
77
|
+
//
|
|
78
|
+
// It renders an **empty box at the reserved slot width**, not nothing. The
|
|
79
|
+
// container reserves each axis *column* at the widest across rows
|
|
80
|
+
// (`maxSlotWidths`), so a hidden axis sharing a column with a visible one in
|
|
81
|
+
// another row is still allotted that column's width — and drawing nothing
|
|
82
|
+
// there slides this row's plot left, out of line with its siblings and with
|
|
83
|
+
// the shared x-axis. When this axis is alone in its column the reservation is
|
|
84
|
+
// its own `width: 0`, the box is zero-wide, and the plot reclaims the space,
|
|
85
|
+
// which is the point of the prop. Both cases fall out of the same expression.
|
|
86
|
+
if (hide) {
|
|
87
|
+
const hiddenSlot = row.axisSlots.get(slot) ?? 0;
|
|
88
|
+
return hiddenSlot > 0 ? (_jsx("div", { "aria-hidden": "true", style: { flex: `0 0 ${hiddenSlot}px`, height: `${row.height}px` } })) : null;
|
|
89
|
+
}
|
|
68
90
|
const { theme } = container;
|
|
69
91
|
const yScale = row.yScales.get(id);
|
|
70
92
|
// The auto-tick count — the row resolves it (explicit `tickCount` else
|
package/dist/annotations.d.ts
CHANGED
|
@@ -161,6 +161,80 @@ export interface BaselineProps {
|
|
|
161
161
|
/** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
|
|
162
162
|
* Its label anchors at the left, at the line's height. */
|
|
163
163
|
export declare function Baseline({ value, axis, label, labelSide, labelPosition, id, selected, selectable, hovered, editing, onChange, indicator, role, }: BaselineProps): import("react/jsx-runtime").JSX.Element | null;
|
|
164
|
+
export interface ZoneProps {
|
|
165
|
+
/** Lower bound in the linked y-axis's units. */
|
|
166
|
+
from: number;
|
|
167
|
+
/** Upper bound in the linked y-axis's units. `from`/`to` may arrive either way
|
|
168
|
+
* round (they're ordered here), and either may be **infinite** for an
|
|
169
|
+
* open-ended band (`to={Infinity}` — the AQI "Hazardous" tail, a
|
|
170
|
+
* `ZoneTime.openEnded` zone): the rect clamps to the plot. */
|
|
171
|
+
to: number;
|
|
172
|
+
/** Which `<YAxis>` (by id) to measure against; omit for the row's default axis. */
|
|
173
|
+
axis?: string;
|
|
174
|
+
/** Chip label, anchored at the band's vertical centre. **Omit for no label** —
|
|
175
|
+
* unlike `<Region>` a zone does *not* auto-label its bounds, because they're
|
|
176
|
+
* already legible on the y axis it spans (a region's x span isn't). The label
|
|
177
|
+
* worth showing is a **name** — `"Good"`, `"Z4 threshold"` — which only the
|
|
178
|
+
* caller has. */
|
|
179
|
+
label?: string;
|
|
180
|
+
/** Which side of the chart the label chip sits. **Default `left`.** */
|
|
181
|
+
labelSide?: 'left' | 'right';
|
|
182
|
+
/** Stable consumer id — a click reports it via `onSelectAnnotation`. Only
|
|
183
|
+
* meaningful with `selectable`. */
|
|
184
|
+
id?: string;
|
|
185
|
+
/** Controlled selection — brightens to the front (level 1). Ignored unless
|
|
186
|
+
* `selectable`. */
|
|
187
|
+
selected?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether the band responds to hover + selection. **Default `false`** — the
|
|
190
|
+
* opposite of the rest of the family, and deliberately so: a zone spans the
|
|
191
|
+
* **full plot width**, and a zone *set* tiles the whole y range, so the pointer
|
|
192
|
+
* is always inside one. Interactive by default, they'd light up on every
|
|
193
|
+
* mousemove and their hit rects would swallow the plot's own clicks. A zone is
|
|
194
|
+
* background context first (level 3, pointer-transparent); opt in per band when
|
|
195
|
+
* a band is genuinely a thing to point at.
|
|
196
|
+
*/
|
|
197
|
+
selectable?: boolean;
|
|
198
|
+
/** Theme **role** — colours this band from `theme.annotation.roles[role]` (its
|
|
199
|
+
* `color`, optionally `fillOpacity`), keeping the shared depth ramp. This is
|
|
200
|
+
* how a zone set gets its **semantic palette** (`good` green, `moderate`
|
|
201
|
+
* yellow, …): the scale lives in the theme, not at the call site. Omitted /
|
|
202
|
+
* unknown ⇒ the base annotation colour. */
|
|
203
|
+
role?: string;
|
|
204
|
+
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the
|
|
205
|
+
* band remotely. Ignored unless `selectable`. */
|
|
206
|
+
hovered?: boolean;
|
|
207
|
+
/** Draw the horizontal **boundary lines** at `from`/`to`. **Default `false`** —
|
|
208
|
+
* again the opposite of `<Region>`, because zone sets are usually
|
|
209
|
+
* **contiguous**: every interior boundary is shared by two bands, so edges-on
|
|
210
|
+
* draws each one twice at double opacity. `true` outlines an isolated band (a
|
|
211
|
+
* target range). */
|
|
212
|
+
edges?: boolean;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A shaded band between two **y values**, spanning the full plot width — the
|
|
216
|
+
* y-axis counterpart of `<Region>`. The mark for a **classification of the value
|
|
217
|
+
* axis**: US EPA AQI categories, heart-rate / power zones, a control chart's
|
|
218
|
+
* spec limits, an SLO band.
|
|
219
|
+
*
|
|
220
|
+
* Being a `<Layers>` child it lives in its row and is scaled by that row's y
|
|
221
|
+
* axis — pass `axis` to pick one on a dual-axis row. Like every annotation it
|
|
222
|
+
* paints in the SVG overlay **above** the data canvas, so keep the fill light
|
|
223
|
+
* (the register's `fillOpacity`, ~0.1–0.2) and the trace reads cleanly through
|
|
224
|
+
* it. A zone set is a wash of colour behind the story, not a layer competing
|
|
225
|
+
* with it.
|
|
226
|
+
*
|
|
227
|
+
* Zones are **background context by default** (`selectable={false}`,
|
|
228
|
+
* `edges={false}`, no label) because that is what a tiled zone set is; see
|
|
229
|
+
* {@link ZoneProps.selectable} for why the family's usual defaults invert here.
|
|
230
|
+
* Colour comes from the theme's {@link ZoneProps.role | role} map, so a palette
|
|
231
|
+
* is a theme, not six call-site colours.
|
|
232
|
+
*
|
|
233
|
+
* Unlike the other marks a zone has **no `onChange`** — dragging zone edges
|
|
234
|
+
* (a zone editor) is a real feature but has no consumer yet; the band is
|
|
235
|
+
* declarative until one arrives.
|
|
236
|
+
*/
|
|
237
|
+
export declare function Zone({ from, to, axis, label, labelSide, id, selected, selectable, hovered, role, edges, }: ZoneProps): import("react/jsx-runtime").JSX.Element | null;
|
|
164
238
|
export interface RegionProps {
|
|
165
239
|
/** Start x in axis units (time or value). */
|
|
166
240
|
from: number;
|
package/dist/annotations.js
CHANGED
|
@@ -6,7 +6,8 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* User-authored **annotations** — marks you place *on* a chart, in a register
|
|
8
8
|
* deliberately distinct from the data: `<Region>` (a shaded x-span), `<Baseline>`
|
|
9
|
-
* (a horizontal value line),
|
|
9
|
+
* (a horizontal value line), `<Marker>` (a vertical x line), and `<Zone>` (a
|
|
10
|
+
* shaded y-span — `<Region>`'s counterpart on the value axis). All four render
|
|
10
11
|
* in the theme's turquoise {@link ChartTheme.annotation} register so a placed mark
|
|
11
12
|
* never reads as data ("the data stays foam; the marks you place are turquoise").
|
|
12
13
|
*
|
|
@@ -101,7 +102,7 @@ const overlayStyle = {
|
|
|
101
102
|
* Read the container + row frames an annotation needs (throw if misplaced), and
|
|
102
103
|
* resolve the mark's annotation style for its optional `role`. A `role` recolors
|
|
103
104
|
* *this* mark from the theme's `annotation.roles[role]` map (`color`, and
|
|
104
|
-
* optionally `fillOpacity`) while keeping the shared depth ramp — so a smile can
|
|
105
|
+
* optionally `fillOpacity` / `dash`) while keeping the shared depth ramp — so a smile can
|
|
105
106
|
* place a green ATM baseline, a neutral vertical, and a distinct marker at once
|
|
106
107
|
* without splitting the whole register. An unknown / unset role falls back to
|
|
107
108
|
* the base `annotation` register (`roles[role] ?? annotation`).
|
|
@@ -117,17 +118,23 @@ function useAnnotationFrame(name, role) {
|
|
|
117
118
|
}
|
|
118
119
|
const base = container.theme.annotation ?? DEFAULT_ANNOTATION;
|
|
119
120
|
const roleStyle = role !== undefined ? base.roles?.[role] : undefined;
|
|
120
|
-
// The role overrides only colour (+ optional fill); depth stays the
|
|
121
|
-
// ramp, so selection / hover / edit levels read identically per role.
|
|
121
|
+
// The role overrides only colour (+ optional fill / dash); depth stays the
|
|
122
|
+
// shared ramp, so selection / hover / edit levels read identically per role.
|
|
122
123
|
const ann = roleStyle
|
|
123
124
|
? {
|
|
124
125
|
...base,
|
|
125
126
|
color: roleStyle.color,
|
|
126
127
|
fillOpacity: roleStyle.fillOpacity ?? base.fillOpacity,
|
|
128
|
+
dash: roleStyle.dash ?? base.dash,
|
|
127
129
|
}
|
|
128
130
|
: base;
|
|
129
131
|
return { container, row, ann };
|
|
130
132
|
}
|
|
133
|
+
/** The register's dash as an SVG `stroke-dasharray`, or `undefined` for solid
|
|
134
|
+
* (an empty pattern means solid, matching `LineStyle.dash`). */
|
|
135
|
+
function dashArray(dash) {
|
|
136
|
+
return dash === undefined || dash.length === 0 ? undefined : dash.join(' ');
|
|
137
|
+
}
|
|
131
138
|
/** Register this annotation with the container (so it can draw the mark's guide on
|
|
132
139
|
* other rows, order regions, and serve snap targets), keyed by the caller's stable
|
|
133
140
|
* per-instance slot key; unregister on unmount. `xs` should be memoised by the
|
|
@@ -504,7 +511,7 @@ export function Marker({ at, label, id, selected = false, selectable = true, hov
|
|
|
504
511
|
// The staff (vertical line) hangs from the top of its flag — so a flag stacked
|
|
505
512
|
// into a lower lane doesn't leave line poking above it. No label ⇒ full height.
|
|
506
513
|
const staffTop = text ? FLAG_TOP + lane * LANE_H : 0;
|
|
507
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("line", { x1: x, y1: staffTop, x2: x, y2: h, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: x, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })), selectable && (_jsx(DragArea, { x: x - HIT_PAD, y: 0, w: 2 * HIT_PAD, h: h, cursor: editing ? 'ew-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragActive: (a) => container.setDragging(a ? selfKey : null), onDrag: (px) => onChange?.(snapToGuides(container, selfKey, px) ??
|
|
514
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("line", { x1: x, y1: staffTop, x2: x, y2: h, stroke: ann.color, strokeWidth: 1, opacity: opacity, strokeDasharray: dashArray(ann.dash), shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: x, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })), selectable && (_jsx(DragArea, { x: x - HIT_PAD, y: 0, w: 2 * HIT_PAD, h: h, cursor: editing ? 'ew-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragActive: (a) => container.setDragging(a ? selfKey : null), onDrag: (px) => onChange?.(snapToGuides(container, selfKey, px) ??
|
|
508
515
|
+container.xScale.invert(px)) }))] }), chipLabel && x >= 0 && x <= container.plotWidth && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
509
516
|
top: `${FLAG_TOP + lane * LANE_H}px`,
|
|
510
517
|
...flagChipX(x, container.plotWidth),
|
|
@@ -552,7 +559,7 @@ export function Baseline({ value, axis, label, labelSide = 'left', labelPosition
|
|
|
552
559
|
const text = label === false ? '' : (label ?? (fmt ? fmt(value) : String(value)));
|
|
553
560
|
// Handle pill near the right end (clears the left-anchored label).
|
|
554
561
|
const handleX = w - 14;
|
|
555
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragActive: (a) => container.setDragging(a ? selfKey : null), onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), text && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
562
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, strokeDasharray: dashArray(ann.dash), shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragActive: (a) => container.setDragging(a ? selfKey : null), onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), text && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
556
563
|
top: `${y}px`,
|
|
557
564
|
[labelSide === 'right' ? 'right' : 'left']: '2px',
|
|
558
565
|
// `center` rides on the line; `above` sits its bottom edge on the line.
|
|
@@ -570,6 +577,89 @@ export function Baseline({ value, axis, label, labelSide = 'left', labelPosition
|
|
|
570
577
|
}, children: fmt ? fmt(value) : String(value) }));
|
|
571
578
|
})()] }));
|
|
572
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* A shaded band between two **y values**, spanning the full plot width — the
|
|
582
|
+
* y-axis counterpart of `<Region>`. The mark for a **classification of the value
|
|
583
|
+
* axis**: US EPA AQI categories, heart-rate / power zones, a control chart's
|
|
584
|
+
* spec limits, an SLO band.
|
|
585
|
+
*
|
|
586
|
+
* Being a `<Layers>` child it lives in its row and is scaled by that row's y
|
|
587
|
+
* axis — pass `axis` to pick one on a dual-axis row. Like every annotation it
|
|
588
|
+
* paints in the SVG overlay **above** the data canvas, so keep the fill light
|
|
589
|
+
* (the register's `fillOpacity`, ~0.1–0.2) and the trace reads cleanly through
|
|
590
|
+
* it. A zone set is a wash of colour behind the story, not a layer competing
|
|
591
|
+
* with it.
|
|
592
|
+
*
|
|
593
|
+
* Zones are **background context by default** (`selectable={false}`,
|
|
594
|
+
* `edges={false}`, no label) because that is what a tiled zone set is; see
|
|
595
|
+
* {@link ZoneProps.selectable} for why the family's usual defaults invert here.
|
|
596
|
+
* Colour comes from the theme's {@link ZoneProps.role | role} map, so a palette
|
|
597
|
+
* is a theme, not six call-site colours.
|
|
598
|
+
*
|
|
599
|
+
* Unlike the other marks a zone has **no `onChange`** — dragging zone edges
|
|
600
|
+
* (a zone editor) is a real feature but has no consumer yet; the band is
|
|
601
|
+
* declarative until one arrives.
|
|
602
|
+
*/
|
|
603
|
+
export function Zone({ from, to, axis, label, labelSide = 'left', id, selected = false, selectable = false, hovered, role, edges = false, }) {
|
|
604
|
+
const { container, row, ann } = useAnnotationFrame('Zone', role);
|
|
605
|
+
const selfKey = useSlotKey();
|
|
606
|
+
const { hovering, reportHover } = useAnnotationHover(container, id, hovered);
|
|
607
|
+
// A horizontal band casts no vertical guide (like a baseline) — register with
|
|
608
|
+
// no xs, so it's tracked for ordering but offers no snap target.
|
|
609
|
+
const xs = useMemo(() => [], []);
|
|
610
|
+
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'zone', xs, selected, selectable, false, // zones aren't editable — no single-annotation edit state
|
|
611
|
+
label ?? '', false);
|
|
612
|
+
// No select while a create tool is armed — the chart is in draw mode then.
|
|
613
|
+
const select = id !== undefined && container.creating === null
|
|
614
|
+
? () => container.onSelectAnnotation?.(id)
|
|
615
|
+
: undefined;
|
|
616
|
+
const axisId = axis ?? row.defaultAxisId;
|
|
617
|
+
const yScale = row.yScales.get(axisId);
|
|
618
|
+
// The axis may not have resolved yet (a layer mounts before its <YAxis>); skip
|
|
619
|
+
// until its scale exists rather than guessing a domain.
|
|
620
|
+
if (yScale === undefined)
|
|
621
|
+
return null;
|
|
622
|
+
const h = row.height;
|
|
623
|
+
const w = container.plotWidth;
|
|
624
|
+
// A NaN bound has no position to draw at — cull rather than let it fall
|
|
625
|
+
// through the finite check below and silently become the domain's low end.
|
|
626
|
+
if (Number.isNaN(from) || Number.isNaN(to))
|
|
627
|
+
return null;
|
|
628
|
+
// Open-ended bounds resolve against the axis **domain** before scaling, not
|
|
629
|
+
// after: d3's interpolator is `a·(1−t) + b·t`, so an infinite `t` yields
|
|
630
|
+
// `0 · Infinity` = NaN rather than an off-plot pixel. Substituting the domain
|
|
631
|
+
// end is also exactly the intent — an open band reaches the plot edge.
|
|
632
|
+
const [d0, d1] = yScale.domain();
|
|
633
|
+
const dLo = Math.min(d0 ?? 0, d1 ?? 0);
|
|
634
|
+
const dHi = Math.max(d0 ?? 0, d1 ?? 0);
|
|
635
|
+
const bound = (v) => (Number.isFinite(v) ? v : v > 0 ? dHi : dLo);
|
|
636
|
+
// Order + clamp to the plot: a band may run past the axis domain (the AQI
|
|
637
|
+
// 151–200 band on an axis topping out at 120). Clamping keeps the rect inside
|
|
638
|
+
// its row; a band entirely outside culls.
|
|
639
|
+
const lo = Math.min(from, to);
|
|
640
|
+
const hi = Math.max(from, to);
|
|
641
|
+
const loY = yScale(bound(lo));
|
|
642
|
+
const hiY = yScale(bound(hi));
|
|
643
|
+
const top = Math.max(Math.min(loY, hiY), 0);
|
|
644
|
+
const bottom = Math.min(Math.max(loY, hiY), h);
|
|
645
|
+
if (bottom <= top)
|
|
646
|
+
return null;
|
|
647
|
+
const bandH = bottom - top;
|
|
648
|
+
// Edit is never on (no onChange) — the levels reduce to selected / hover /
|
|
649
|
+
// resting, and to a flat level 3 when the band is inert background.
|
|
650
|
+
const edgeOpacity = rampAt(ann.depth, lineLevel(selectable, false, hovering, selected));
|
|
651
|
+
const fillOpacity = ann.fillOpacity *
|
|
652
|
+
rampAt(FILL_MULT, bodyLevel(selectable, false, hovering, selected));
|
|
653
|
+
/** A boundary line — drawn only where the band has a *real*, in-plot edge. An
|
|
654
|
+
* open-ended bound has no boundary at all, and a bound the clamp cut off would
|
|
655
|
+
* otherwise draw its line on the plot border, reading as chrome. */
|
|
656
|
+
const boundary = (v, aty) => edges && Number.isFinite(v) && aty >= 0 && aty <= h ? (_jsx("line", { x1: 0, y1: aty, x2: w, y2: aty, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, strokeDasharray: dashArray(ann.dash), shapeRendering: "crispEdges" })) : null;
|
|
657
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: h, style: overlayStyle, children: [_jsx("rect", { x: 0, y: top, width: w, height: bandH, fill: ann.color, opacity: fillOpacity }), boundary(lo, loY), boundary(hi, hiY), selectable && (_jsx(DragArea, { x: 0, y: top, w: w, h: bandH, cursor: "inherit", editable: false, onHover: reportHover, onSelect: select, onDrag: () => { } }))] }), label !== undefined && label !== '' && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
658
|
+
top: `${top + bandH / 2}px`,
|
|
659
|
+
[labelSide === 'right' ? 'right' : 'left']: '2px',
|
|
660
|
+
transform: 'translateY(-50%)',
|
|
661
|
+
}, children: label }))] }));
|
|
662
|
+
}
|
|
573
663
|
/** A shaded span over an x range — a lap, a zone, a selected interval. Its label
|
|
574
664
|
* flies as a flag off the left edge. */
|
|
575
665
|
export function Region({ from, to, label, id, selected = false, selectable = true, hovered, editing = false, onChange, edges = true, role, }) {
|
|
@@ -616,7 +706,7 @@ export function Region({ from, to, label, id, selected = false, selectable = tru
|
|
|
616
706
|
// even after the dragged edge crosses it — {@link orderRegion} then re-opens the
|
|
617
707
|
// region the other way instead of dead-ending at zero width.
|
|
618
708
|
const edgeRef = useRef(null);
|
|
619
|
-
const edge = (atX) => (_jsx("line", { x1: atX, y1: 0, x2: atX, y2: h, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, shapeRendering: "crispEdges" }));
|
|
709
|
+
const edge = (atX) => (_jsx("line", { x1: atX, y1: 0, x2: atX, y2: h, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, strokeDasharray: dashArray(ann.dash), shapeRendering: "crispEdges" }));
|
|
620
710
|
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("rect", { x: left, y: 0, width: spanW, height: h, fill: ann.color, opacity: fillOpacity }), edges && edge(xa), edges && edge(xb), showHandles && (_jsxs(_Fragment, { children: [_jsx(Pill, { cx: xa, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color }), _jsx(Pill, { cx: xb, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })] })), selectable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: left, y: 0, w: spanW, h: h, cursor: editing ? 'grab' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragActive: (a) => container.setDragging(a ? selfKey : null), onDragStart: (px) => {
|
|
621
711
|
dragRef.current = { from, to, startPx: px };
|
|
622
712
|
}, onDrag: (px) => {
|
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
|