insomni-plot 0.1.0-alpha.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/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// area geom
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { lerpColor, type Color, type Layer, type Vec2 } from "insomni";
|
|
6
|
+
import {
|
|
7
|
+
areaMark,
|
|
8
|
+
pushFilledPolygon,
|
|
9
|
+
stackedAreaMark,
|
|
10
|
+
type MarkBuilder,
|
|
11
|
+
type StackOffset,
|
|
12
|
+
type StackOrder,
|
|
13
|
+
} from "../../marks.ts";
|
|
14
|
+
import { stack } from "../../marks/stack.ts";
|
|
15
|
+
import type { ContinuousScale } from "../../scales.ts";
|
|
16
|
+
import type { Aes } from "../aes.ts";
|
|
17
|
+
import { resolveAes } from "../aes.ts";
|
|
18
|
+
import { alphaize, seriesColor } from "../color-utils.ts";
|
|
19
|
+
import { areaSwatch } from "../../legend.ts";
|
|
20
|
+
import {
|
|
21
|
+
haloRing,
|
|
22
|
+
inlineMark,
|
|
23
|
+
defaultMarkFill,
|
|
24
|
+
resolveCoord,
|
|
25
|
+
resolveFillAlpha,
|
|
26
|
+
SELECTION_DIM_ALPHA,
|
|
27
|
+
selectedIndicesFor,
|
|
28
|
+
selectedSegmentsFor,
|
|
29
|
+
selectionActive,
|
|
30
|
+
wrapMark,
|
|
31
|
+
} from "./_mark.ts";
|
|
32
|
+
import type {
|
|
33
|
+
CompileContext,
|
|
34
|
+
CompiledHitTest,
|
|
35
|
+
Geom,
|
|
36
|
+
GeomFrame,
|
|
37
|
+
ResolvedChannelMap,
|
|
38
|
+
ScaleHints,
|
|
39
|
+
} from "./types.ts";
|
|
40
|
+
|
|
41
|
+
export type AreaPosition = "identity" | "stack" | "fill";
|
|
42
|
+
|
|
43
|
+
export interface AreaChannels<T> {
|
|
44
|
+
x: Aes<T, number | Date>;
|
|
45
|
+
/**
|
|
46
|
+
* Single column → simple area from y=0 to y. Array of column keys → stacked
|
|
47
|
+
* area, with the implicit `__series` color channel keyed by column name.
|
|
48
|
+
*/
|
|
49
|
+
y: Aes<T, number | Date> | readonly (keyof T & string)[];
|
|
50
|
+
color?: Aes<T, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AreaOptions {
|
|
54
|
+
fill?: Color;
|
|
55
|
+
stroke?: Color;
|
|
56
|
+
strokeWidth?: number;
|
|
57
|
+
/** "identity" (default), "stack" (offset zero), or "fill" (offset expand). */
|
|
58
|
+
position?: AreaPosition;
|
|
59
|
+
/** Stack ordering — only meaningful with stacked positions. */
|
|
60
|
+
order?: StackOrder;
|
|
61
|
+
label?: string;
|
|
62
|
+
/**
|
|
63
|
+
* When true, hover hit-tests resolve to the nearest vertex *by x* — the
|
|
64
|
+
* cursor's vertical position doesn't influence which datum (or stacked
|
|
65
|
+
* segment) is picked, so the user can hover anywhere along the area.
|
|
66
|
+
* Default `false` (per-vertex Euclidean pick within `pickRadius`).
|
|
67
|
+
*/
|
|
68
|
+
nearestX?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function area<T>(channels: AreaChannels<T>, options: AreaOptions = {}): Geom<T> {
|
|
72
|
+
const isStacked = Array.isArray(channels.y);
|
|
73
|
+
// Area baselines at 0 on y. For `fill` (expand offset) the rendered range
|
|
74
|
+
// is [0, 1] regardless of input totals, so set an explicit domain.
|
|
75
|
+
const scaleHints: ScaleHints = isStacked
|
|
76
|
+
? options.position === "fill"
|
|
77
|
+
? { y: { domain: [0, 1] } }
|
|
78
|
+
: { y: { includeZero: true } }
|
|
79
|
+
: { y: { includeZero: true } };
|
|
80
|
+
return {
|
|
81
|
+
kind: "area",
|
|
82
|
+
channels: { x: channels.x, y: channels.y, color: channels.color },
|
|
83
|
+
label: options.label,
|
|
84
|
+
scaleHints,
|
|
85
|
+
legendSwatch: (color) => areaSwatch({ fill: color, width: 14, height: 10 }),
|
|
86
|
+
compile(ctx: CompileContext<T>) {
|
|
87
|
+
const { data, scales, plot, theme } = ctx;
|
|
88
|
+
const coord = resolveCoord(ctx);
|
|
89
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
90
|
+
const xFn = scales.x.fn;
|
|
91
|
+
|
|
92
|
+
const fill = options.fill ?? defaultMarkFill(theme);
|
|
93
|
+
const stroke = options.stroke;
|
|
94
|
+
const strokeWidth = options.strokeWidth;
|
|
95
|
+
const rowKey = (d: T, i: number) => (ctx.transitionKey ? ctx.transitionKey(d, i) : String(i));
|
|
96
|
+
|
|
97
|
+
if (!isStacked) {
|
|
98
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
99
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
100
|
+
const isHidden = (d: T, i: number) => {
|
|
101
|
+
if (!ctx.hidden || !colorAes) return false;
|
|
102
|
+
return ctx.hidden.has(String(colorAes.fn(d, i)));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const yScale = scales.y.fn;
|
|
106
|
+
const yBaseline = yScale(0);
|
|
107
|
+
const anim = ctx.activeTransition;
|
|
108
|
+
const fromIndexFor = (d: T, i: number) => anim?.matchIndex(rowKey(d, i), i);
|
|
109
|
+
// Dim simple-area fill when a selection is active anywhere in the
|
|
110
|
+
// chart and no row of this area is selected. Stacked areas do their
|
|
111
|
+
// own per-segment dimming via `dimMulti` below.
|
|
112
|
+
const selectedSetSimple = selectedIndicesFor(ctx, "area");
|
|
113
|
+
const dimSimple = selectionActive(ctx) && selectedSetSimple === null;
|
|
114
|
+
const fillAlphaResolved = resolveFillAlpha(dimSimple, theme);
|
|
115
|
+
const resolvedFill = alphaize(fill, fillAlphaResolved);
|
|
116
|
+
|
|
117
|
+
// Lerp fill color if animating.
|
|
118
|
+
const animFill: Color = anim
|
|
119
|
+
? (() => {
|
|
120
|
+
// Use datum 0's stored color as representative for the whole area fill.
|
|
121
|
+
if (anim.from.count === 0) return resolvedFill;
|
|
122
|
+
const fromFill: Color = {
|
|
123
|
+
r: anim.from.rgba[0]!,
|
|
124
|
+
g: anim.from.rgba[1]!,
|
|
125
|
+
b: anim.from.rgba[2]!,
|
|
126
|
+
a: anim.from.rgba[3]!,
|
|
127
|
+
};
|
|
128
|
+
return lerpColor(fromFill, resolvedFill, anim.t);
|
|
129
|
+
})()
|
|
130
|
+
: resolvedFill;
|
|
131
|
+
|
|
132
|
+
// Compute the per-datum plot-frame x, y0, y1 (with animation lerp),
|
|
133
|
+
// then project each edge vertex through the active coord. Under
|
|
134
|
+
// `coordCartesian()` projection is the identity; polar (Phase 3)
|
|
135
|
+
// will project the top edge and baseline as separate vertex streams.
|
|
136
|
+
const computeFrame = (
|
|
137
|
+
d: T,
|
|
138
|
+
i: number,
|
|
139
|
+
): { x: number; y0: number; y1: number; hidden: boolean } => {
|
|
140
|
+
if (isHidden(d, i)) return { x: NaN, y0: NaN, y1: NaN, hidden: true };
|
|
141
|
+
const toX = xFn(xAes.fn(d, i));
|
|
142
|
+
const toY1 = yScale(yAes.fn(d, i));
|
|
143
|
+
const fromIndex = fromIndexFor(d, i);
|
|
144
|
+
let px = toX;
|
|
145
|
+
let py1 = toY1;
|
|
146
|
+
let py0 = yBaseline;
|
|
147
|
+
if (anim && fromIndex !== undefined) {
|
|
148
|
+
if (Number.isFinite(anim.from.x[fromIndex]!)) {
|
|
149
|
+
px = anim.from.x[fromIndex]! + (toX - anim.from.x[fromIndex]!) * anim.t;
|
|
150
|
+
}
|
|
151
|
+
if (Number.isFinite(anim.from.y[fromIndex]!)) {
|
|
152
|
+
py1 = anim.from.y[fromIndex]! + (toY1 - anim.from.y[fromIndex]!) * anim.t;
|
|
153
|
+
}
|
|
154
|
+
const fromR = anim.from.r?.[fromIndex];
|
|
155
|
+
if (fromR !== undefined && Number.isFinite(fromR)) {
|
|
156
|
+
py0 = fromR + (yBaseline - fromR) * anim.t;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { x: px, y0: py0, y1: py1, hidden: false };
|
|
160
|
+
};
|
|
161
|
+
let builders: MarkBuilder[];
|
|
162
|
+
const isPolar = coord.kind === "polar";
|
|
163
|
+
if (isPolar) {
|
|
164
|
+
// Polar area: the area "baseline" (y0) should map to r=0 (the polar
|
|
165
|
+
// centre) at the data point's angle, rather than the Cartesian y=0
|
|
166
|
+
// baseline (which under angleChannel="y" gives a constant starting
|
|
167
|
+
// angle). We also tessellate the top edge through coord.segment so
|
|
168
|
+
// consecutive sector-tip vertices follow arcs instead of straight
|
|
169
|
+
// chords.
|
|
170
|
+
const xDomain = scales.x.axisScale.domain as [number, number];
|
|
171
|
+
const xBaselinePx = xFn(xDomain[0] ?? 0);
|
|
172
|
+
const tessTop: Vec2[] = [];
|
|
173
|
+
const tessBottom: Vec2[] = [];
|
|
174
|
+
const ox = plot.topLeft.x;
|
|
175
|
+
const oy = plot.topLeft.y;
|
|
176
|
+
for (let i = 0; i < data.length; i++) {
|
|
177
|
+
const d = data[i]!;
|
|
178
|
+
const f = computeFrame(d, i);
|
|
179
|
+
if (f.hidden) continue;
|
|
180
|
+
// Top vertex: full projection at (hours, sector).
|
|
181
|
+
const topP = coord.project({ x: f.x, y: f.y1 });
|
|
182
|
+
// Bottom vertex: at the origin angle (same sector) but r=0.
|
|
183
|
+
const botP = coord.project({ x: xBaselinePx, y: f.y1 });
|
|
184
|
+
// Tessellate the top edge segment from previous → current.
|
|
185
|
+
if (tessTop.length > 0) {
|
|
186
|
+
if (i > 0) {
|
|
187
|
+
const prev = computeFrame(data[i - 1]!, i - 1);
|
|
188
|
+
if (!prev.hidden) {
|
|
189
|
+
const seg = coord.segment({ x: prev.x, y: prev.y1 }, { x: f.x, y: f.y1 });
|
|
190
|
+
// Skip first (already pushed) and last (pushed as topP below).
|
|
191
|
+
for (let k = 1; k < seg.length - 1; k++) {
|
|
192
|
+
tessTop.push({ x: ox + seg[k]!.x, y: oy + seg[k]!.y });
|
|
193
|
+
}
|
|
194
|
+
// Bottom edge: tessellate from previous → current baseline.
|
|
195
|
+
const botSeg = coord.segment(
|
|
196
|
+
{ x: xBaselinePx, y: prev.y1 },
|
|
197
|
+
{ x: xBaselinePx, y: f.y1 },
|
|
198
|
+
);
|
|
199
|
+
for (let k = 1; k < botSeg.length - 1; k++) {
|
|
200
|
+
tessBottom.push({ x: ox + botSeg[k]!.x, y: oy + botSeg[k]!.y });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
tessTop.push({ x: ox + topP.x, y: oy + topP.y });
|
|
206
|
+
tessBottom.push({ x: ox + botP.x, y: oy + botP.y });
|
|
207
|
+
}
|
|
208
|
+
// Build the closed polygon: top edge forward → bottom edge reversed.
|
|
209
|
+
const points: Vec2[] = [...tessTop];
|
|
210
|
+
for (let i = tessBottom.length - 1; i >= 0; i--) {
|
|
211
|
+
points.push(tessBottom[i]!);
|
|
212
|
+
}
|
|
213
|
+
builders = [
|
|
214
|
+
{
|
|
215
|
+
length: data.length,
|
|
216
|
+
addTo(layer: Layer) {
|
|
217
|
+
pushFilledPolygon(layer, points, {
|
|
218
|
+
fill: animFill,
|
|
219
|
+
stroke: stroke && dimSimple ? alphaize(stroke, SELECTION_DIM_ALPHA) : stroke,
|
|
220
|
+
strokeWidth,
|
|
221
|
+
});
|
|
222
|
+
return layer;
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
} else {
|
|
227
|
+
const mark = areaMark(data, {
|
|
228
|
+
x: (d, i) => {
|
|
229
|
+
const f = computeFrame(d, i);
|
|
230
|
+
if (f.hidden) return NaN;
|
|
231
|
+
return coord.project({ x: f.x, y: f.y1 }).x;
|
|
232
|
+
},
|
|
233
|
+
y0: (d, i) => {
|
|
234
|
+
const f = computeFrame(d, i);
|
|
235
|
+
return coord.project({ x: f.x, y: f.y0 }).y;
|
|
236
|
+
},
|
|
237
|
+
y1: (d, i) => {
|
|
238
|
+
const f = computeFrame(d, i);
|
|
239
|
+
if (f.hidden) return NaN;
|
|
240
|
+
return coord.project({ x: f.x, y: f.y1 }).y;
|
|
241
|
+
},
|
|
242
|
+
fill: animFill,
|
|
243
|
+
stroke: stroke && dimSimple ? alphaize(stroke, SELECTION_DIM_ALPHA) : stroke,
|
|
244
|
+
strokeWidth,
|
|
245
|
+
});
|
|
246
|
+
builders = [wrapMark(mark, plot.topLeft, data.length)];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Hover halo on the active top-edge vertex.
|
|
250
|
+
if (ctx.hovered && ctx.hovered.geomKind === "area" && ctx.hovered.data === data) {
|
|
251
|
+
const i = ctx.hovered.dataIndex;
|
|
252
|
+
const d = data[i];
|
|
253
|
+
if (d !== undefined) {
|
|
254
|
+
const xv = xAes.fn(d, i);
|
|
255
|
+
const yv = yAes.fn(d, i);
|
|
256
|
+
if (xv != null && yv != null) {
|
|
257
|
+
const rawX = xFn(xv);
|
|
258
|
+
const rawY = yScale(yv);
|
|
259
|
+
if (Number.isFinite(rawX) && Number.isFinite(rawY)) {
|
|
260
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
261
|
+
const cx = plot.topLeft.x + projected.x;
|
|
262
|
+
const cy = plot.topLeft.y + projected.y;
|
|
263
|
+
builders.push(inlineMark((layer) => haloRing(layer, cx, cy, 6, fill, 2)));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Selection rings — one per selected top-edge vertex.
|
|
270
|
+
const selectedSet = selectedIndicesFor(ctx, "area");
|
|
271
|
+
if (selectedSet) {
|
|
272
|
+
for (const i of selectedSet) {
|
|
273
|
+
const d = data[i];
|
|
274
|
+
if (d === undefined) continue;
|
|
275
|
+
const xv = xAes.fn(d, i);
|
|
276
|
+
const yv = yAes.fn(d, i);
|
|
277
|
+
if (xv == null || yv == null) continue;
|
|
278
|
+
const rawX = xFn(xv);
|
|
279
|
+
const rawY = yScale(yv);
|
|
280
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
281
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
282
|
+
const cx = plot.topLeft.x + projected.x;
|
|
283
|
+
const cy = plot.topLeft.y + projected.y;
|
|
284
|
+
builders.push(inlineMark((layer) => haloRing(layer, cx, cy, 6, fill, 2)));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return builders;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Stacked: y is keys[]; build a stackedAreaMark.
|
|
292
|
+
const keys = channels.y as readonly (keyof T & string)[];
|
|
293
|
+
// activeKeys excludes hidden series; color domain stays full-keys so
|
|
294
|
+
// visible series keep stable colors when siblings are toggled.
|
|
295
|
+
const activeKeys = ctx.hidden ? keys.filter((k) => !ctx.hidden!.has(k)) : keys;
|
|
296
|
+
const yContinuous = scales.y.axisScale as ContinuousScale;
|
|
297
|
+
const offset: StackOffset = options.position === "fill" ? "expand" : "zero";
|
|
298
|
+
const colorFn = seriesColor(scales.color, theme.palettes.categorical, [...keys] as string[]);
|
|
299
|
+
const anim = ctx.activeTransition;
|
|
300
|
+
const stackedSegments = stack(data, [...activeKeys] as string[], {
|
|
301
|
+
offset,
|
|
302
|
+
order: options.order,
|
|
303
|
+
});
|
|
304
|
+
// When a selection is active, dim the base stacked area uniformly and
|
|
305
|
+
// re-overlay the selected segments' top vertex at full color so they
|
|
306
|
+
// pop. Mirrors the multi-series bar treatment.
|
|
307
|
+
const dimMulti = !!ctx.selected && ctx.selected.length > 0;
|
|
308
|
+
const baseFillAlpha = resolveFillAlpha(dimMulti, theme);
|
|
309
|
+
const builders: MarkBuilder[] = [];
|
|
310
|
+
if (anim) {
|
|
311
|
+
const segments = stackedSegments;
|
|
312
|
+
type AnimatedSeries = {
|
|
313
|
+
key: string;
|
|
314
|
+
fill: Color;
|
|
315
|
+
top: { x: number; y: number }[];
|
|
316
|
+
bottom: { x: number; y: number }[];
|
|
317
|
+
};
|
|
318
|
+
const series = new Map<string, AnimatedSeries>();
|
|
319
|
+
const orderKeys: string[] = [];
|
|
320
|
+
for (let i = 0; i < segments.length; i++) {
|
|
321
|
+
const seg = segments[i]!;
|
|
322
|
+
const toX = xFn(xAes.fn(seg.datum, seg.datumIndex));
|
|
323
|
+
const toY0 = yContinuous(seg.base);
|
|
324
|
+
const toY1 = yContinuous(seg.top);
|
|
325
|
+
const fromIndex = anim.matchIndex(`${rowKey(seg.datum, seg.datumIndex)}:${seg.key}`, i);
|
|
326
|
+
const fromX = fromIndex !== undefined ? anim.from.x[fromIndex] : NaN;
|
|
327
|
+
const fromY0 = fromIndex !== undefined ? (anim.from.r?.[fromIndex] ?? NaN) : NaN;
|
|
328
|
+
const fromY1 = fromIndex !== undefined ? anim.from.y[fromIndex] : NaN;
|
|
329
|
+
const px =
|
|
330
|
+
Number.isFinite(fromX) && Number.isFinite(toX) ? fromX + (toX - fromX) * anim.t : toX;
|
|
331
|
+
const py0 =
|
|
332
|
+
Number.isFinite(fromY0) && Number.isFinite(toY0)
|
|
333
|
+
? fromY0 + (toY0 - fromY0) * anim.t
|
|
334
|
+
: toY0;
|
|
335
|
+
const py1 =
|
|
336
|
+
Number.isFinite(fromY1) && Number.isFinite(toY1)
|
|
337
|
+
? fromY1 + (toY1 - fromY1) * anim.t
|
|
338
|
+
: toY1;
|
|
339
|
+
if (!Number.isFinite(px) || !Number.isFinite(py0) || !Number.isFinite(py1)) continue;
|
|
340
|
+
let bucket = series.get(seg.key);
|
|
341
|
+
if (!bucket) {
|
|
342
|
+
const toFill = alphaize(colorFn(seg.key), baseFillAlpha);
|
|
343
|
+
const fill =
|
|
344
|
+
fromIndex !== undefined
|
|
345
|
+
? lerpColor(
|
|
346
|
+
{
|
|
347
|
+
r: anim.from.rgba[fromIndex * 4]!,
|
|
348
|
+
g: anim.from.rgba[fromIndex * 4 + 1]!,
|
|
349
|
+
b: anim.from.rgba[fromIndex * 4 + 2]!,
|
|
350
|
+
a: anim.from.rgba[fromIndex * 4 + 3]!,
|
|
351
|
+
},
|
|
352
|
+
toFill,
|
|
353
|
+
anim.t,
|
|
354
|
+
)
|
|
355
|
+
: { ...toFill, a: toFill.a * anim.t };
|
|
356
|
+
bucket = { key: seg.key, fill, top: [], bottom: [] };
|
|
357
|
+
series.set(seg.key, bucket);
|
|
358
|
+
orderKeys.push(seg.key);
|
|
359
|
+
}
|
|
360
|
+
// Project the top and bottom edge vertices independently through
|
|
361
|
+
// the active coord. Cartesian: identity.
|
|
362
|
+
const projTop = coord.project({ x: px, y: py1 });
|
|
363
|
+
const projBottom = coord.project({ x: px, y: py0 });
|
|
364
|
+
bucket.top.push({ x: plot.topLeft.x + projTop.x, y: plot.topLeft.y + projTop.y });
|
|
365
|
+
bucket.bottom.push({
|
|
366
|
+
x: plot.topLeft.x + projBottom.x,
|
|
367
|
+
y: plot.topLeft.y + projBottom.y,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
builders.push(
|
|
371
|
+
inlineMark((layer) => {
|
|
372
|
+
for (const key of orderKeys) {
|
|
373
|
+
const s = series.get(key)!;
|
|
374
|
+
if (s.top.length < 2) continue;
|
|
375
|
+
const points = [...s.top];
|
|
376
|
+
for (let i = s.bottom.length - 1; i >= 0; i--) points.push(s.bottom[i]!);
|
|
377
|
+
pushFilledPolygon(layer, points, { fill: s.fill, stroke, strokeWidth });
|
|
378
|
+
}
|
|
379
|
+
}, segments.length),
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
// Static (non-animated) stacked path. `stackedAreaMark` owns the
|
|
383
|
+
// top/bottom polygon construction via `x` callback + `valueScale`.
|
|
384
|
+
// Under `coordCartesian()` projection is the identity — projecting
|
|
385
|
+
// only one axis here would produce a half-projected result under
|
|
386
|
+
// polar, so we leave the mark unprojected for Phase 1 and let the
|
|
387
|
+
// Phase 3 polar coord swap in a different mark factory entirely.
|
|
388
|
+
const mark = stackedAreaMark<T, string>(data, [...activeKeys], {
|
|
389
|
+
x: (d, i) => xFn(xAes.fn(d, i)),
|
|
390
|
+
valueScale: yContinuous,
|
|
391
|
+
color: (key) => alphaize(colorFn(key), baseFillAlpha),
|
|
392
|
+
offset,
|
|
393
|
+
order: options.order,
|
|
394
|
+
stroke,
|
|
395
|
+
strokeWidth,
|
|
396
|
+
});
|
|
397
|
+
builders.push(wrapMark(mark, plot.topLeft));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Hover halo + selection rings on stacked-area segment top edge.
|
|
401
|
+
const drawSegmentHalo = (i: number, key: string) => {
|
|
402
|
+
const seg = stackedSegments.find((s) => s.datumIndex === i && s.key === key);
|
|
403
|
+
if (!seg) return;
|
|
404
|
+
const xv = xAes.fn(seg.datum, seg.datumIndex);
|
|
405
|
+
if (xv == null) return;
|
|
406
|
+
const rawX = xFn(xv);
|
|
407
|
+
const rawY = yContinuous(seg.top);
|
|
408
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return;
|
|
409
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
410
|
+
const cx = plot.topLeft.x + projected.x;
|
|
411
|
+
const cy = plot.topLeft.y + projected.y;
|
|
412
|
+
const ringColor = colorFn(key);
|
|
413
|
+
builders.push(inlineMark((layer) => haloRing(layer, cx, cy, 6, ringColor, 2)));
|
|
414
|
+
};
|
|
415
|
+
if (
|
|
416
|
+
ctx.hovered &&
|
|
417
|
+
ctx.hovered.geomKind === "area" &&
|
|
418
|
+
ctx.hovered.data === data &&
|
|
419
|
+
ctx.hovered.seriesKey
|
|
420
|
+
) {
|
|
421
|
+
drawSegmentHalo(ctx.hovered.dataIndex, ctx.hovered.seriesKey);
|
|
422
|
+
}
|
|
423
|
+
const stackedSelected = selectedSegmentsFor(ctx, "area");
|
|
424
|
+
if (stackedSelected) {
|
|
425
|
+
for (const { dataIndex: i, seriesKey: key } of stackedSelected) {
|
|
426
|
+
if (key) drawSegmentHalo(i, key);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return builders;
|
|
430
|
+
},
|
|
431
|
+
compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
|
|
432
|
+
const { data, scales, plot, hidden } = ctx;
|
|
433
|
+
if (data.length === 0) return null;
|
|
434
|
+
const coord = resolveCoord(ctx);
|
|
435
|
+
|
|
436
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
437
|
+
const xScale = scales.x.fn;
|
|
438
|
+
const yScale = scales.y.fn;
|
|
439
|
+
const ox = plot.topLeft.x;
|
|
440
|
+
const oy = plot.topLeft.y;
|
|
441
|
+
const positionsList: number[] = [];
|
|
442
|
+
const dataIndexList: number[] = [];
|
|
443
|
+
const seriesKeyList: (string | undefined)[] = [];
|
|
444
|
+
|
|
445
|
+
if (!isStacked) {
|
|
446
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
447
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
448
|
+
for (let i = 0; i < data.length; i++) {
|
|
449
|
+
const d = data[i]!;
|
|
450
|
+
if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) continue;
|
|
451
|
+
const xv = xAes.fn(d, i);
|
|
452
|
+
const yv = yAes.fn(d, i);
|
|
453
|
+
if (xv == null || yv == null) continue;
|
|
454
|
+
const rawX = xScale(xv);
|
|
455
|
+
const rawY = yScale(yv);
|
|
456
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
457
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
458
|
+
positionsList.push(ox + projected.x, oy + projected.y);
|
|
459
|
+
dataIndexList.push(i);
|
|
460
|
+
seriesKeyList.push(undefined);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
const keys = channels.y as readonly (keyof T & string)[];
|
|
464
|
+
const activeKeys = hidden ? keys.filter((k) => !hidden.has(k)) : keys;
|
|
465
|
+
const segments = stack(data, [...activeKeys] as string[], {
|
|
466
|
+
offset: options.position === "fill" ? "expand" : "zero",
|
|
467
|
+
order: options.order,
|
|
468
|
+
});
|
|
469
|
+
for (const seg of segments) {
|
|
470
|
+
const xv = xAes.fn(seg.datum, seg.datumIndex);
|
|
471
|
+
if (xv == null) continue;
|
|
472
|
+
const rawX = xScale(xv);
|
|
473
|
+
const rawY = yScale(seg.top);
|
|
474
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
475
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
476
|
+
positionsList.push(ox + projected.x, oy + projected.y);
|
|
477
|
+
dataIndexList.push(seg.datumIndex);
|
|
478
|
+
seriesKeyList.push(seg.key);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const n = dataIndexList.length;
|
|
482
|
+
if (n === 0) return null;
|
|
483
|
+
const positions = Float32Array.from(positionsList);
|
|
484
|
+
const dataIndex = Int32Array.from(dataIndexList);
|
|
485
|
+
|
|
486
|
+
const channelsMap: ResolvedChannelMap<T> = {
|
|
487
|
+
x: xAes,
|
|
488
|
+
y: !isStacked ? resolveAes<T, unknown>(channels.y as Aes<T, unknown>) : undefined,
|
|
489
|
+
color: !isStacked && channels.color ? resolveAes<T, unknown>(channels.color) : undefined,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const pickRadius = options.nearestX ? Math.max(plot.width, plot.height) : 12;
|
|
493
|
+
return {
|
|
494
|
+
geomKind: "area",
|
|
495
|
+
label: options.label,
|
|
496
|
+
positions: positions.subarray(0, n * 2),
|
|
497
|
+
dataIndex: dataIndex.subarray(0, n),
|
|
498
|
+
seriesKey: seriesKeyList,
|
|
499
|
+
pickRadius,
|
|
500
|
+
pickAxis: options.nearestX ? "x" : undefined,
|
|
501
|
+
channels: channelsMap,
|
|
502
|
+
data,
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
captureFrame(ctx: CompileContext<T>): GeomFrame | null {
|
|
506
|
+
const { data, scales, theme } = ctx;
|
|
507
|
+
if (data.length === 0) return null;
|
|
508
|
+
|
|
509
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
510
|
+
const xFn = scales.x.fn;
|
|
511
|
+
const yScale = scales.y.fn;
|
|
512
|
+
const count = isStacked
|
|
513
|
+
? (channels.y as readonly (keyof T & string)[]).reduce(
|
|
514
|
+
(sum, key) => sum + (ctx.hidden?.has(key) ? 0 : data.length),
|
|
515
|
+
0,
|
|
516
|
+
)
|
|
517
|
+
: data.length;
|
|
518
|
+
const x = new Float32Array(count);
|
|
519
|
+
const y = new Float32Array(count);
|
|
520
|
+
const rgba = new Float32Array(count * 4);
|
|
521
|
+
const a = new Float32Array(count);
|
|
522
|
+
// r stores the pixel baseline (y0) for lerp.
|
|
523
|
+
const r = new Float32Array(count);
|
|
524
|
+
const ids = ctx.transitionKey ? Array.from<string>({ length: count }) : undefined;
|
|
525
|
+
if (!isStacked) {
|
|
526
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
527
|
+
const yBaseline = yScale(0);
|
|
528
|
+
const areaFill = options.fill ?? defaultMarkFill(theme);
|
|
529
|
+
const resolvedFill = alphaize(areaFill, theme.marks.fillAlpha);
|
|
530
|
+
for (let i = 0; i < count; i++) {
|
|
531
|
+
const d = data[i]!;
|
|
532
|
+
const xv = xAes.fn(d, i);
|
|
533
|
+
const yv = yAes.fn(d, i);
|
|
534
|
+
const px = xFn(xv);
|
|
535
|
+
const py = yScale(yv);
|
|
536
|
+
x[i] = Number.isFinite(px) ? px : NaN;
|
|
537
|
+
y[i] = Number.isFinite(py) ? py : NaN;
|
|
538
|
+
r[i] = yBaseline;
|
|
539
|
+
rgba[i * 4] = resolvedFill.r;
|
|
540
|
+
rgba[i * 4 + 1] = resolvedFill.g;
|
|
541
|
+
rgba[i * 4 + 2] = resolvedFill.b;
|
|
542
|
+
rgba[i * 4 + 3] = resolvedFill.a;
|
|
543
|
+
a[i] = resolvedFill.a;
|
|
544
|
+
if (ids) ids[i] = ctx.transitionKey!(d, i);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
const keys = channels.y as readonly (keyof T & string)[];
|
|
548
|
+
const activeKeys = ctx.hidden ? keys.filter((k) => !ctx.hidden!.has(k)) : keys;
|
|
549
|
+
const colorFn = seriesColor(scales.color, theme.palettes.categorical, [
|
|
550
|
+
...keys,
|
|
551
|
+
] as string[]);
|
|
552
|
+
const segments = stack(data, [...activeKeys] as string[], {
|
|
553
|
+
offset: options.position === "fill" ? "expand" : "zero",
|
|
554
|
+
order: options.order,
|
|
555
|
+
});
|
|
556
|
+
let n = 0;
|
|
557
|
+
for (const seg of segments) {
|
|
558
|
+
const px = xFn(xAes.fn(seg.datum, seg.datumIndex));
|
|
559
|
+
const py = yScale(seg.top);
|
|
560
|
+
const py0 = yScale(seg.base);
|
|
561
|
+
const fill = alphaize(colorFn(seg.key), theme.marks.fillAlpha);
|
|
562
|
+
x[n] = Number.isFinite(px) ? px : NaN;
|
|
563
|
+
y[n] = Number.isFinite(py) ? py : NaN;
|
|
564
|
+
r[n] = Number.isFinite(py0) ? py0 : NaN;
|
|
565
|
+
rgba[n * 4] = fill.r;
|
|
566
|
+
rgba[n * 4 + 1] = fill.g;
|
|
567
|
+
rgba[n * 4 + 2] = fill.b;
|
|
568
|
+
rgba[n * 4 + 3] = fill.a;
|
|
569
|
+
a[n] = fill.a;
|
|
570
|
+
if (ids) ids[n] = `${ctx.transitionKey!(seg.datum, seg.datumIndex)}:${seg.key}`;
|
|
571
|
+
n++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { count, x, y, rgba, a, r, ids };
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Color } from "insomni";
|
|
2
|
+
import { type ColorOrAccent } from "../color-utils.ts";
|
|
3
|
+
import type { Geom } from "./types.ts";
|
|
4
|
+
export interface BandChannels {
|
|
5
|
+
x?: readonly [number | Date, number | Date];
|
|
6
|
+
y?: readonly [number | Date, number | Date];
|
|
7
|
+
}
|
|
8
|
+
export interface BandOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Fill color. Accepts a literal {@link Color} or a theme accent key
|
|
11
|
+
* (`"positive" | "negative" | "warn" | "info"`); accent keys resolve through
|
|
12
|
+
* `theme.accents` and have `theme.marks.bandFillAlpha` applied unless
|
|
13
|
+
* {@link BandOptions.alpha} is set.
|
|
14
|
+
*/
|
|
15
|
+
fill?: ColorOrAccent;
|
|
16
|
+
stroke?: ColorOrAccent;
|
|
17
|
+
strokeWidth?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Override the fill alpha. Defaults to `theme.marks.bandFillAlpha` when
|
|
20
|
+
* `fill` is omitted or is an accent key; defaults to no override (use the
|
|
21
|
+
* color's own alpha) when `fill` is a literal {@link Color}.
|
|
22
|
+
*/
|
|
23
|
+
alpha?: number;
|
|
24
|
+
label?: string;
|
|
25
|
+
labelColor?: Color;
|
|
26
|
+
}
|
|
27
|
+
export declare function band<T>(channels: BandChannels, options?: BandOptions): Geom<T>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createFrame } from "insomni";
|
|
2
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
3
|
+
|
|
4
|
+
import { resolveAes } from "../aes.ts";
|
|
5
|
+
import { buildPositionScale, type ScaleBundle } from "../scales.ts";
|
|
6
|
+
import { themeDefault } from "../theme.ts";
|
|
7
|
+
import { band } from "./band.ts";
|
|
8
|
+
import type { CompileContext } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
interface Row {
|
|
11
|
+
v: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const data: Row[] = [{ v: 25 }, { v: 75 }];
|
|
15
|
+
|
|
16
|
+
function makeCtx(rows: readonly Row[]): CompileContext<Row> {
|
|
17
|
+
const xAes = resolveAes<Row, unknown>("v");
|
|
18
|
+
const yAes = resolveAes<Row, unknown>("v");
|
|
19
|
+
const xScale = buildPositionScale(xAes, rows, [0, 100]);
|
|
20
|
+
const yScale = buildPositionScale(yAes, rows, [200, 0]);
|
|
21
|
+
const scales: ScaleBundle = { x: xScale, y: yScale };
|
|
22
|
+
return {
|
|
23
|
+
data: rows,
|
|
24
|
+
scales,
|
|
25
|
+
plot: createFrame({ x: 50, y: 30, width: 100, height: 200 }),
|
|
26
|
+
theme: themeDefault,
|
|
27
|
+
atlas: undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("band geom — compileHitTest", () => {
|
|
32
|
+
test("vertical band emits one hit at the band's center", () => {
|
|
33
|
+
const geom = band<Row>({ x: [25, 75] }, { label: "warn zone" });
|
|
34
|
+
const hits = geom.compileHitTest!(makeCtx(data))!;
|
|
35
|
+
expect(hits.geomKind).toBe("band");
|
|
36
|
+
expect(hits.dataIndex.length).toBe(1);
|
|
37
|
+
// Band spans scaled x [0, 100]; center x = 50; +plot.x(50) = 100.
|
|
38
|
+
expect(hits.positions[0]).toBeCloseTo(100, 3);
|
|
39
|
+
// y centered in plot frame: 30 + 100 = 130.
|
|
40
|
+
expect(hits.positions[1]).toBeCloseTo(130, 3);
|
|
41
|
+
expect(hits.label).toBe("warn zone");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("horizontal band centers x in the plot frame", () => {
|
|
45
|
+
const geom = band<Row>({ y: [25, 75] });
|
|
46
|
+
const hits = geom.compileHitTest!(makeCtx(data))!;
|
|
47
|
+
// x centered: 50 + 50 = 100.
|
|
48
|
+
expect(hits.positions[0]).toBeCloseTo(100, 3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("pickRadius covers half the band's larger extent", () => {
|
|
52
|
+
const geom = band<Row>({ x: [25, 75] });
|
|
53
|
+
const hits = geom.compileHitTest!(makeCtx(data))!;
|
|
54
|
+
// halfY = plot.height/2 = 100 (off-axis = full height) > halfX = 50.
|
|
55
|
+
expect(hits.pickRadius).toBeCloseTo(100, 3);
|
|
56
|
+
});
|
|
57
|
+
});
|