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,158 @@
|
|
|
1
|
+
import type { Color, Frame, GlyphAtlas, Layer, Padding } from "insomni";
|
|
2
|
+
import { type AxisOptions } from "../axis.ts";
|
|
3
|
+
import { type AnnotationSpec } from "./annotations.ts";
|
|
4
|
+
import { type FacetSpec } from "./facet.ts";
|
|
5
|
+
import { type PointSwatchSpec } from "../legend.ts";
|
|
6
|
+
import type { AxesSpec, AxisSpec, LegendMergeChannel, LegendSpec, TitleSpec } from "./chart.ts";
|
|
7
|
+
import { type Coord } from "./coord.ts";
|
|
8
|
+
import type { GrammarTransitions } from "./interactions/transitions.ts";
|
|
9
|
+
import type { Geom, ScaleBundle } from "./geoms/index.ts";
|
|
10
|
+
import { type Signal } from "insomni/reactivity";
|
|
11
|
+
import { type AlphaScaleOptions, type BorderStyleScaleOptions, type ColorScaleOptions, type OverlayGlyphScaleOptions, type PositionScaleOptions, type ShapeScaleOptions, type SizeScaleOptions } from "./scales.ts";
|
|
12
|
+
import { type Theme } from "./theme.ts";
|
|
13
|
+
export interface ChartConfig<T> {
|
|
14
|
+
data: readonly T[] | Signal<readonly T[]>;
|
|
15
|
+
width: number | undefined;
|
|
16
|
+
height: number | undefined;
|
|
17
|
+
background: Color | undefined;
|
|
18
|
+
padding: Padding;
|
|
19
|
+
framePadding: {
|
|
20
|
+
top: number;
|
|
21
|
+
right: number;
|
|
22
|
+
bottom: number;
|
|
23
|
+
left: number;
|
|
24
|
+
};
|
|
25
|
+
device: GPUDevice | undefined;
|
|
26
|
+
externalAtlas: GlyphAtlas | undefined;
|
|
27
|
+
theme: Theme;
|
|
28
|
+
layers: readonly Geom<T>[];
|
|
29
|
+
axes: AxesSpec;
|
|
30
|
+
titles: TitleSpec;
|
|
31
|
+
legend: LegendSpec;
|
|
32
|
+
scaleOverrides: {
|
|
33
|
+
x?: PositionScaleOptions;
|
|
34
|
+
y?: PositionScaleOptions;
|
|
35
|
+
color?: ColorScaleOptions<unknown>;
|
|
36
|
+
size?: SizeScaleOptions;
|
|
37
|
+
alpha?: AlphaScaleOptions;
|
|
38
|
+
shape?: ShapeScaleOptions;
|
|
39
|
+
borderStyle?: BorderStyleScaleOptions;
|
|
40
|
+
overlayGlyph?: OverlayGlyphScaleOptions;
|
|
41
|
+
};
|
|
42
|
+
annotations: readonly AnnotationSpec[];
|
|
43
|
+
facet?: FacetSpec<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Coordinate system. Defaults to `coordCartesian()`. Threaded into
|
|
46
|
+
* `CompileContext.coord` and used to dispatch axis rendering.
|
|
47
|
+
*/
|
|
48
|
+
coord: Coord;
|
|
49
|
+
}
|
|
50
|
+
export interface PipelineOutput<T = unknown> {
|
|
51
|
+
scales: ScaleBundle;
|
|
52
|
+
/** Hit-test contributions from any geom that implements `compileHitTest`. */
|
|
53
|
+
hitTests: import("./geoms/types.ts").CompiledHitTest<T>[];
|
|
54
|
+
/**
|
|
55
|
+
* Hover-focus decorators from geoms that implement `hoverDecoration` (point).
|
|
56
|
+
* The mount draws these into the overlay layer on hover — a cheap path that
|
|
57
|
+
* avoids recompiling the baked marks. Empty for geoms whose hover treatment
|
|
58
|
+
* is global (dim-others), which the mount handles via a marks re-bake.
|
|
59
|
+
* Faceted charts collect one decorator per (panel, geom), each closing over
|
|
60
|
+
* its panel's rows (P6-T3).
|
|
61
|
+
*/
|
|
62
|
+
hoverDecorators: import("./geoms/types.ts").GeomHoverDecorator[];
|
|
63
|
+
/**
|
|
64
|
+
* Emphasis-key resolvers from dim-participating geoms (P5-T3). The mount maps
|
|
65
|
+
* an active hover hit to the geom's namespaced focused key and drives the
|
|
66
|
+
* core's animated GPU dim. Empty for charts with no dim geom. Faceted charts
|
|
67
|
+
* collect one resolver per (panel, geom) with a disjoint per-panel key band
|
|
68
|
+
* (P6-T3), so hovering one panel never dims another.
|
|
69
|
+
*/
|
|
70
|
+
emphasisResolvers: import("./geoms/emphasis.ts").EmphasisResolver[];
|
|
71
|
+
/**
|
|
72
|
+
* Inset chart frame after outer padding — the area available to axes, slots,
|
|
73
|
+
* and the plot panel. Used to clip the hud layer so overlay marks (e.g. tip
|
|
74
|
+
* labels) don't bleed into the outer padding region.
|
|
75
|
+
*/
|
|
76
|
+
outerFrame: Frame;
|
|
77
|
+
/**
|
|
78
|
+
* Plot drawing region in absolute element-CSS pixels (top-left of the plot
|
|
79
|
+
* frame, matching `Frame.topLeft`). For faceted charts this is the union
|
|
80
|
+
* of all panel frames — wide enough to enclose every panel — used by the
|
|
81
|
+
* crosshair to clip its guide line to the marks region.
|
|
82
|
+
*/
|
|
83
|
+
plotFrame: Frame;
|
|
84
|
+
/**
|
|
85
|
+
* Position-scale range insets in CSS px (`framePadding` + per-geom
|
|
86
|
+
* `prepareRange` reservations). The position scales' pixel range is
|
|
87
|
+
* `[reserve.left, plotFrame.width - reserve.right]` (X) and
|
|
88
|
+
* `[plotFrame.height - reserve.bottom, reserve.top]` (Y). The mount feeds
|
|
89
|
+
* this into the data viewport via `setFrame(plotFrame, reserve)` so
|
|
90
|
+
* `dataToScreen` matches where marks render.
|
|
91
|
+
*/
|
|
92
|
+
reserve: {
|
|
93
|
+
readonly left: number;
|
|
94
|
+
readonly right: number;
|
|
95
|
+
readonly top: number;
|
|
96
|
+
readonly bottom: number;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Per-panel plot frames for faceted charts (one entry per panel, in render
|
|
100
|
+
* order). Empty / undefined for non-faceted charts. Used by the crosshair
|
|
101
|
+
* to clip its guide line to the active panel rather than the chart-wide
|
|
102
|
+
* union frame. Coordinates match `plotFrame` (absolute element-CSS px).
|
|
103
|
+
*/
|
|
104
|
+
panelFrames?: readonly Frame[];
|
|
105
|
+
/** Categorical legend builder and its placed origin (absolute CSS px). */
|
|
106
|
+
legend?: {
|
|
107
|
+
builder: import("../legend.ts").LegendBuilder;
|
|
108
|
+
origin: {
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export declare function currentData<T>(data: readonly T[] | Signal<readonly T[]>): readonly T[];
|
|
115
|
+
/**
|
|
116
|
+
* Optional tail bundle for {@link runPipeline}. Required core args
|
|
117
|
+
* (`config`, `data`, layers, `atlas`) stay positional; everything that varies
|
|
118
|
+
* per-frame lives here so call sites don't drift when fields are added.
|
|
119
|
+
*/
|
|
120
|
+
export interface RunPipelineOptions<T> {
|
|
121
|
+
hovered?: import("./geoms/types.ts").HoveredHit | null;
|
|
122
|
+
selected?: readonly import("./geoms/types.ts").HoveredHit[];
|
|
123
|
+
hidden?: ReadonlySet<string>;
|
|
124
|
+
legendDimAlpha?: number;
|
|
125
|
+
transitions?: GrammarTransitions;
|
|
126
|
+
transitionKey?: (datum: T, index: number) => string;
|
|
127
|
+
}
|
|
128
|
+
export declare function runPipeline<T>(config: ChartConfig<T>, data: readonly T[], axisLayer: Layer, marksLayer: Layer, hudLayer: Layer, atlas: GlyphAtlas | undefined, options?: RunPipelineOptions<T>): PipelineOutput<T>;
|
|
129
|
+
/**
|
|
130
|
+
* Extract the existing scale's domain into a `PositionScaleOptions` that pins
|
|
131
|
+
* a new scale to the same input range — used to keep faceted panels aligned
|
|
132
|
+
* under `scales: "fixed"`.
|
|
133
|
+
*
|
|
134
|
+
* Exported for unit testing — not part of the public grammar surface.
|
|
135
|
+
*/
|
|
136
|
+
export declare function shareDomain(scale: ScaleBundle["x"]): PositionScaleOptions;
|
|
137
|
+
/** @internal Exported for unit tests only. */
|
|
138
|
+
export declare function makeAxisOptions(spec: AxisSpec | undefined, theme: Theme, atlas: GlyphAtlas | undefined): AxisOptions<unknown>;
|
|
139
|
+
type LegendChannelScales = Omit<ScaleBundle, "x" | "y">;
|
|
140
|
+
/**
|
|
141
|
+
* Build one point-style swatch for a merged legend entry. Resolves each
|
|
142
|
+
* listed channel's scale at `value` and composes the result into a single
|
|
143
|
+
* `PointSwatchSpec`. Channels listed in `mergeSet` but lacking an active
|
|
144
|
+
* scale are silently skipped — the merge declaration is the consumer's
|
|
145
|
+
* assertion that the channels exist *if* they're configured.
|
|
146
|
+
*/
|
|
147
|
+
export declare function composeMergedPointSwatch(value: unknown, color: Color, scales: LegendChannelScales, mergeSet: ReadonlySet<LegendMergeChannel>, theme: Theme): PointSwatchSpec;
|
|
148
|
+
/**
|
|
149
|
+
* Detect which non-color channels share the color scale's categorical domain,
|
|
150
|
+
* so the legend can fold them into one row per entry without the consumer
|
|
151
|
+
* spelling out `merge: [...]`. A channel auto-merges when its scale exists AND
|
|
152
|
+
* its `.domain` is structurally equal to the color domain. `size` is numeric
|
|
153
|
+
* and never auto-merges; opt in via `legendSpec.merge` if you want it.
|
|
154
|
+
*
|
|
155
|
+
* Exported for testing.
|
|
156
|
+
*/
|
|
157
|
+
export declare function autoMergeChannels(colorDomain: readonly unknown[], scales: LegendChannelScales): LegendMergeChannel[];
|
|
158
|
+
export {};
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createLayer } from "insomni";
|
|
3
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
4
|
+
|
|
5
|
+
import { resolveAes } from "./aes.ts";
|
|
6
|
+
import { plot } from "./chart.ts";
|
|
7
|
+
import { coordPolar } from "./coord.ts";
|
|
8
|
+
import { EMPHASIS_GEOM_STRIDE } from "./geoms/emphasis.ts";
|
|
9
|
+
import type { ChannelSpec, CompileContext, Geom, HoveredHit } from "./geoms/types.ts";
|
|
10
|
+
import { runPipeline, shareDomain, type ChartConfig } from "./pipeline.ts";
|
|
11
|
+
import { buildPositionScale } from "./scales.ts";
|
|
12
|
+
|
|
13
|
+
interface Row {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data: Row[] = [
|
|
19
|
+
{ x: 1, y: 2 },
|
|
20
|
+
{ x: 2, y: 4 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function recordingGeom(received: { ctx: CompileContext<Row> | null }): Geom<Row> {
|
|
24
|
+
const channels: ChannelSpec<Row> = { x: "x" as unknown, y: "y" as unknown };
|
|
25
|
+
return {
|
|
26
|
+
kind: "point",
|
|
27
|
+
channels,
|
|
28
|
+
compile(ctx) {
|
|
29
|
+
received.ctx = ctx;
|
|
30
|
+
return [];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ChartWithConfig {
|
|
36
|
+
__config__: ChartConfig<Row>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function configFromChart<T extends ChartWithConfig>(chart: T): ChartConfig<Row> {
|
|
40
|
+
return chart.__config__;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("runPipeline — hovered context", () => {
|
|
44
|
+
it("threads `hovered: null` into compile context by default", () => {
|
|
45
|
+
const received: { ctx: CompileContext<Row> | null } = { ctx: null };
|
|
46
|
+
const chart = plot({ data, width: 400, height: 300 }).layer(recordingGeom(received));
|
|
47
|
+
const config = configFromChart(chart as unknown as ChartWithConfig);
|
|
48
|
+
const a = createLayer({ space: "ui" });
|
|
49
|
+
const m = createLayer({ space: "ui" });
|
|
50
|
+
const h = createLayer({ space: "ui" });
|
|
51
|
+
runPipeline(config, data, a, m, h, undefined);
|
|
52
|
+
expect(received.ctx).not.toBeNull();
|
|
53
|
+
expect(received.ctx!.hovered).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("forwards a HoveredHit value into compile context", () => {
|
|
57
|
+
const received: { ctx: CompileContext<Row> | null } = { ctx: null };
|
|
58
|
+
const chart = plot({ data, width: 400, height: 300 }).layer(recordingGeom(received));
|
|
59
|
+
const config = configFromChart(chart as unknown as ChartWithConfig);
|
|
60
|
+
const a = createLayer({ space: "ui" });
|
|
61
|
+
const m = createLayer({ space: "ui" });
|
|
62
|
+
const h = createLayer({ space: "ui" });
|
|
63
|
+
const hovered: HoveredHit = {
|
|
64
|
+
geomKind: "point",
|
|
65
|
+
dataIndex: 1,
|
|
66
|
+
data,
|
|
67
|
+
x: 100,
|
|
68
|
+
y: 200,
|
|
69
|
+
};
|
|
70
|
+
runPipeline(config, data, a, m, h, undefined, { hovered });
|
|
71
|
+
expect(received.ctx?.hovered).toBe(hovered);
|
|
72
|
+
expect(received.ctx?.hovered?.dataIndex).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("runPipeline — emphasis (P5-T3)", () => {
|
|
77
|
+
it("threads a disjoint emphasisBase per geom (geomEmphasisBase(gi))", () => {
|
|
78
|
+
const r0: { ctx: CompileContext<Row> | null } = { ctx: null };
|
|
79
|
+
const r1: { ctx: CompileContext<Row> | null } = { ctx: null };
|
|
80
|
+
const chart = plot({ data, width: 400, height: 300 })
|
|
81
|
+
.layer(recordingGeom(r0))
|
|
82
|
+
.layer(recordingGeom(r1));
|
|
83
|
+
const config = configFromChart(chart as unknown as ChartWithConfig);
|
|
84
|
+
runPipeline(
|
|
85
|
+
config,
|
|
86
|
+
data,
|
|
87
|
+
createLayer({ space: "ui" }),
|
|
88
|
+
createLayer({ space: "ui" }),
|
|
89
|
+
createLayer({ space: "ui" }),
|
|
90
|
+
undefined,
|
|
91
|
+
);
|
|
92
|
+
// Bands are disjoint and ascend with geom index.
|
|
93
|
+
expect(r0.ctx!.emphasisBase).toBeGreaterThan(0);
|
|
94
|
+
expect(r1.ctx!.emphasisBase).toBeGreaterThan(r0.ctx!.emphasisBase!);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("collects emphasisResolvers from geoms that implement emphasisResolution", () => {
|
|
98
|
+
const resolverGeom: Geom<Row> = {
|
|
99
|
+
kind: "bar",
|
|
100
|
+
channels: { x: "x" as unknown, y: "y" as unknown },
|
|
101
|
+
compile: () => [],
|
|
102
|
+
emphasisResolution: (ctx) => ({
|
|
103
|
+
geomKind: "bar",
|
|
104
|
+
data: ctx.data,
|
|
105
|
+
resolve: () => (ctx.emphasisBase ?? 0) + 1,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
const chart = plot({ data, width: 400, height: 300 }).layer(resolverGeom);
|
|
109
|
+
const config = configFromChart(chart as unknown as ChartWithConfig);
|
|
110
|
+
const out = runPipeline(
|
|
111
|
+
config,
|
|
112
|
+
data,
|
|
113
|
+
createLayer({ space: "ui" }),
|
|
114
|
+
createLayer({ space: "ui" }),
|
|
115
|
+
createLayer({ space: "ui" }),
|
|
116
|
+
undefined,
|
|
117
|
+
);
|
|
118
|
+
expect(out.emphasisResolvers).toHaveLength(1);
|
|
119
|
+
expect(out.emphasisResolvers[0]!.geomKind).toBe("bar");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("emphasisResolvers is empty for a chart with no dim geom", () => {
|
|
123
|
+
const r: { ctx: CompileContext<Row> | null } = { ctx: null };
|
|
124
|
+
const chart = plot({ data, width: 400, height: 300 }).layer(recordingGeom(r));
|
|
125
|
+
const config = configFromChart(chart as unknown as ChartWithConfig);
|
|
126
|
+
const out = runPipeline(
|
|
127
|
+
config,
|
|
128
|
+
data,
|
|
129
|
+
createLayer({ space: "ui" }),
|
|
130
|
+
createLayer({ space: "ui" }),
|
|
131
|
+
createLayer({ space: "ui" }),
|
|
132
|
+
undefined,
|
|
133
|
+
);
|
|
134
|
+
expect(out.emphasisResolvers).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Faceted emphasis parity (P6-T3)
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
interface FacetRow {
|
|
143
|
+
panel: string;
|
|
144
|
+
x: number;
|
|
145
|
+
y: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Two panels (a / b), two rows each, so the facet path runs >1 panel.
|
|
149
|
+
const facetData: FacetRow[] = [
|
|
150
|
+
{ panel: "a", x: 1, y: 2 },
|
|
151
|
+
{ panel: "a", x: 2, y: 4 },
|
|
152
|
+
{ panel: "b", x: 3, y: 6 },
|
|
153
|
+
{ panel: "b", x: 4, y: 8 },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
interface FacetConfigChart {
|
|
157
|
+
__config__: ChartConfig<FacetRow>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function facetConfig<T extends FacetConfigChart>(chart: T): ChartConfig<FacetRow> {
|
|
161
|
+
return chart.__config__;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A dim-participating ("bar") geom that mirrors the real emphasis contract: it
|
|
166
|
+
* tags via `emphasisBase` (recording each compile ctx so tests can read the
|
|
167
|
+
* per-panel band) and exposes a resolver + decorator that close over the ctx's
|
|
168
|
+
* (panel-sliced) data. The resolver returns `base + dataIndex + 1`, matching the
|
|
169
|
+
* `emphasisKeyFor` convention, so panel bands are observable through it.
|
|
170
|
+
*/
|
|
171
|
+
function facetBarGeom(seen: { ctxs: CompileContext<FacetRow>[] }): Geom<FacetRow> {
|
|
172
|
+
return {
|
|
173
|
+
kind: "bar",
|
|
174
|
+
channels: { x: "x" as unknown, y: "y" as unknown },
|
|
175
|
+
compile(ctx) {
|
|
176
|
+
seen.ctxs.push(ctx);
|
|
177
|
+
return [];
|
|
178
|
+
},
|
|
179
|
+
emphasisResolution: (ctx) => ({
|
|
180
|
+
geomKind: "bar",
|
|
181
|
+
data: ctx.data,
|
|
182
|
+
resolve: (hit) =>
|
|
183
|
+
hit.geomKind === "bar" && hit.data === ctx.data
|
|
184
|
+
? (ctx.emphasisBase ?? 0) + hit.dataIndex + 1
|
|
185
|
+
: null,
|
|
186
|
+
}),
|
|
187
|
+
hoverDecoration: (ctx) => ({
|
|
188
|
+
geomKind: "bar",
|
|
189
|
+
data: ctx.data,
|
|
190
|
+
decorate: () => {},
|
|
191
|
+
}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Stub atlas (measureText only) so the faceted strip's `pushText` succeeds in
|
|
196
|
+
// jsdom. Mirrors `makeFakeTextAtlas` in axis.test.ts.
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
function fakeAtlas(): any {
|
|
199
|
+
return {
|
|
200
|
+
measureText(text: string, opts: { fontSize?: number }) {
|
|
201
|
+
const fs = opts.fontSize ?? 12;
|
|
202
|
+
return { width: text.length * fs * 0.6, height: fs };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function runFacetPipeline(
|
|
208
|
+
config: ChartConfig<FacetRow>,
|
|
209
|
+
data: readonly FacetRow[],
|
|
210
|
+
options?: Parameters<typeof runPipeline<FacetRow>>[6],
|
|
211
|
+
) {
|
|
212
|
+
const atlas = fakeAtlas();
|
|
213
|
+
const axisLayer = createLayer({ space: "ui", atlas });
|
|
214
|
+
const marksLayer = createLayer({ space: "ui", atlas });
|
|
215
|
+
const hudLayer = createLayer({ space: "ui", atlas });
|
|
216
|
+
// The facet strip + axes call `pushText` (glyph shaping needs real atlas
|
|
217
|
+
// metrics we don't stub); we exercise emphasis collection, not text layout, so
|
|
218
|
+
// no-op the text path. Mirrors axis.test.ts's recording-layer trick.
|
|
219
|
+
for (const l of [axisLayer, hudLayer]) {
|
|
220
|
+
(l as unknown as { pushText: () => void }).pushText = () => {};
|
|
221
|
+
}
|
|
222
|
+
return runPipeline(config, data, axisLayer, marksLayer, hudLayer, atlas, options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
describe("runPipeline — faceted emphasis (P6-T3)", () => {
|
|
226
|
+
it("threads a disjoint emphasisBase per panel (no cross-panel collision)", () => {
|
|
227
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
228
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
229
|
+
.layer(facetBarGeom(seen))
|
|
230
|
+
.facet({ by: "panel", ncol: 2 });
|
|
231
|
+
runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
232
|
+
|
|
233
|
+
// One compile per panel (single geom × 2 panels).
|
|
234
|
+
expect(seen.ctxs).toHaveLength(2);
|
|
235
|
+
const baseA = seen.ctxs[0]!.emphasisBase;
|
|
236
|
+
const baseB = seen.ctxs[1]!.emphasisBase;
|
|
237
|
+
expect(baseA).toBeGreaterThan(0);
|
|
238
|
+
expect(baseB).toBeGreaterThan(0);
|
|
239
|
+
// effectiveGi = panelIndex * geomCount(1) + gi(0) → panelIndex.
|
|
240
|
+
expect(baseA).toBe(1 * EMPHASIS_GEOM_STRIDE); // geomEmphasisBase(0)
|
|
241
|
+
expect(baseB).toBe(2 * EMPHASIS_GEOM_STRIDE); // geomEmphasisBase(1)
|
|
242
|
+
// The two panels never share a band → a key from panel A can't land in B.
|
|
243
|
+
expect(baseA).not.toBe(baseB);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("collects one emphasisResolver per panel; a panel-B hit resolves into B's band", () => {
|
|
247
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
248
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
249
|
+
.layer(facetBarGeom(seen))
|
|
250
|
+
.facet({ by: "panel", ncol: 2 });
|
|
251
|
+
const out = runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
252
|
+
|
|
253
|
+
expect(out.emphasisResolvers).toHaveLength(2);
|
|
254
|
+
|
|
255
|
+
// Mirror the mount's `focusedKeyFor`: match a hit to its resolver via
|
|
256
|
+
// (geomKind, data identity). Faceted hits carry the panel's sliced rows.
|
|
257
|
+
const panelARows = seen.ctxs[0]!.data;
|
|
258
|
+
const panelBRows = seen.ctxs[1]!.data;
|
|
259
|
+
expect(panelARows).not.toBe(panelBRows);
|
|
260
|
+
|
|
261
|
+
function resolve(hit: HoveredHit): number {
|
|
262
|
+
for (const r of out.emphasisResolvers) {
|
|
263
|
+
if (r.geomKind === hit.geomKind && r.data === hit.data) return r.resolve(hit) ?? 0;
|
|
264
|
+
}
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const hitA: HoveredHit = { geomKind: "bar", dataIndex: 0, data: panelARows, x: 0, y: 0 };
|
|
269
|
+
const hitB: HoveredHit = { geomKind: "bar", dataIndex: 0, data: panelBRows, x: 0, y: 0 };
|
|
270
|
+
const keyA = resolve(hitA);
|
|
271
|
+
const keyB = resolve(hitB);
|
|
272
|
+
|
|
273
|
+
// Same ordinal, different panels → keys differ and land in their own bands.
|
|
274
|
+
expect(keyA).toBe(1 * EMPHASIS_GEOM_STRIDE + 1);
|
|
275
|
+
expect(keyB).toBe(2 * EMPHASIS_GEOM_STRIDE + 1);
|
|
276
|
+
expect(keyA).not.toBe(keyB);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("collects one hoverDecorator per panel (focus-halo path)", () => {
|
|
280
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
281
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
282
|
+
.layer(facetBarGeom(seen))
|
|
283
|
+
.facet({ by: "panel", ncol: 2 });
|
|
284
|
+
const out = runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
285
|
+
|
|
286
|
+
expect(out.hoverDecorators).toHaveLength(2);
|
|
287
|
+
// Each decorator closes over a distinct panel's rows.
|
|
288
|
+
const datas = out.hoverDecorators.map((d) => d.data);
|
|
289
|
+
expect(datas[0]).not.toBe(datas[1]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("hands each panel a distinct, frame-pinned coord (deferred-closure safe)", () => {
|
|
293
|
+
// Under polar facets, a geom's hoverDecoration closure captures `ctx.coord`
|
|
294
|
+
// and runs at hover time — after the facet loop has bound the SHARED coord
|
|
295
|
+
// to the last panel's frame. If every panel's ctx held that one shared
|
|
296
|
+
// instance, panel A's halo would project against panel B's centre. The
|
|
297
|
+
// pipeline now wraps the shared coord per panel (`frameBoundCoord`) so each
|
|
298
|
+
// ctx carries an independent, frame-pinned coord.
|
|
299
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
300
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
301
|
+
.coord(coordPolar({ angleChannel: "x" }))
|
|
302
|
+
.layer(facetBarGeom(seen))
|
|
303
|
+
.facet({ by: "panel", ncol: 2 });
|
|
304
|
+
runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
305
|
+
|
|
306
|
+
expect(seen.ctxs).toHaveLength(2);
|
|
307
|
+
const coordA = seen.ctxs[0]!.coord!;
|
|
308
|
+
const coordB = seen.ctxs[1]!.coord!;
|
|
309
|
+
// Distinct instances — neither is the other, so a later panel's bind cannot
|
|
310
|
+
// poison an earlier panel's captured projection.
|
|
311
|
+
expect(coordA).not.toBe(coordB);
|
|
312
|
+
expect(coordA.kind).toBe("polar");
|
|
313
|
+
|
|
314
|
+
// Panel-stability under interleaving: projecting through A, then B, then A
|
|
315
|
+
// again yields A's SAME result both times (each call re-pins to its own
|
|
316
|
+
// frame). With a single shared coord, the B call would leave the coord bound
|
|
317
|
+
// to B's frame and corrupt A's second projection whenever the frames differ.
|
|
318
|
+
const p = { x: 30, y: 40 };
|
|
319
|
+
const a1 = coordA.project(p);
|
|
320
|
+
coordB.project(p);
|
|
321
|
+
const a2 = coordA.project(p);
|
|
322
|
+
expect(a2).toEqual(a1);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("trips the existing key-band ceiling guard when panels × geoms overflows u32", () => {
|
|
326
|
+
// effectiveGi = panelIndex * geomCount + gi. With 4 geom layers per panel
|
|
327
|
+
// the guard (effectiveGi >= EMPHASIS_MAX_GEOM_INDEX + 1 = 4094) trips at
|
|
328
|
+
// panelIndex 1023, gi 2 (1023*4 + 2 = 4094) — so only ~1024 panels need to
|
|
329
|
+
// compile before the throw, not ~4094 (avoids flaking near vitest's 5s
|
|
330
|
+
// default under full-file scheduling). 1024 panels × 4 geoms reaches a max
|
|
331
|
+
// effectiveGi of 1023*4 + 3 = 4095 ≥ 4094, guaranteeing the overflow.
|
|
332
|
+
const geomCount = 4;
|
|
333
|
+
const panelCount = 1024; // max effectiveGi 1023*4 + 3 = 4095 ≥ 4094 → throws
|
|
334
|
+
const manyPanels: FacetRow[] = Array.from({ length: panelCount }, (_, i) => ({
|
|
335
|
+
panel: `p${i}`,
|
|
336
|
+
x: i,
|
|
337
|
+
y: i,
|
|
338
|
+
}));
|
|
339
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
340
|
+
let chart = plot({ data: manyPanels, width: 4000, height: 3000 });
|
|
341
|
+
for (let g = 0; g < geomCount; g++) chart = chart.layer(facetBarGeom(seen));
|
|
342
|
+
chart = chart.facet({ by: "panel", ncol: panelCount });
|
|
343
|
+
const config = facetConfig(chart as unknown as FacetConfigChart);
|
|
344
|
+
// geomEmphasisBase throws (RangeError) at effectiveGi 4094 — same guard the
|
|
345
|
+
// non-faceted path relies on, now reached by the faceted flattening.
|
|
346
|
+
expect(() => runFacetPipeline(config, manyPanels)).toThrow(/ceiling/);
|
|
347
|
+
}, 30_000); // vitest's 5s default so it never flakes on a busy CI runner. // Belt-and-braces: this builds ~1024 panels; give it generous headroom over
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Faceted per-panel scales (ECS back-out regression — D1)
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
//
|
|
354
|
+
// The ECS integration populated entities against the chart-wide scales while
|
|
355
|
+
// geoms re-resolved against per-panel scales — so every panel rendered at the
|
|
356
|
+
// chart-wide domain (the faceting mis-render P0 bug). With the back-out, geoms
|
|
357
|
+
// read `ctx.scales` directly, and the facet path hands each panel a scale bundle
|
|
358
|
+
// built from its OWN rows under `scales: "free"`. These assert the geom actually
|
|
359
|
+
// receives that per-panel bundle.
|
|
360
|
+
|
|
361
|
+
function domainOf(scale: { axisScale: unknown }): readonly unknown[] {
|
|
362
|
+
return (scale.axisScale as { domain: readonly unknown[] }).domain;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
describe("runPipeline — faceted per-panel scales (D1 regression)", () => {
|
|
366
|
+
it('scales:"free" gives each panel a scale bundle built from its own rows', () => {
|
|
367
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
368
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
369
|
+
.layer(facetBarGeom(seen))
|
|
370
|
+
.facet({ by: "panel", ncol: 2, scales: "free" });
|
|
371
|
+
runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
372
|
+
|
|
373
|
+
expect(seen.ctxs).toHaveLength(2);
|
|
374
|
+
const xA = domainOf(seen.ctxs[0]!.scales.x);
|
|
375
|
+
const xB = domainOf(seen.ctxs[1]!.scales.x);
|
|
376
|
+
const yA = domainOf(seen.ctxs[0]!.scales.y);
|
|
377
|
+
const yB = domainOf(seen.ctxs[1]!.scales.y);
|
|
378
|
+
|
|
379
|
+
// Panel a rows have x ∈ {1,2}, y ∈ {2,4}; panel b rows x ∈ {3,4}, y ∈ {6,8}.
|
|
380
|
+
// Under free scales the per-panel domains must differ — if every panel saw
|
|
381
|
+
// the chart-wide domain (the ECS bug) these would be equal.
|
|
382
|
+
expect(xA).not.toEqual(xB);
|
|
383
|
+
expect(yA).not.toEqual(yB);
|
|
384
|
+
// And each panel's max must track its own data, not the chart-wide max.
|
|
385
|
+
expect(Number(xB[xB.length - 1])).toBeGreaterThan(Number(xA[xA.length - 1]));
|
|
386
|
+
expect(Number(yB[yB.length - 1])).toBeGreaterThan(Number(yA[yA.length - 1]));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('scales:"fixed" pins every panel to the shared chart-wide domain', () => {
|
|
390
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
391
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
392
|
+
.layer(facetBarGeom(seen))
|
|
393
|
+
.facet({ by: "panel", ncol: 2, scales: "fixed" });
|
|
394
|
+
runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
395
|
+
|
|
396
|
+
expect(seen.ctxs).toHaveLength(2);
|
|
397
|
+
// Fixed: panels share the chart-wide domain so axes line up across panels.
|
|
398
|
+
expect(domainOf(seen.ctxs[0]!.scales.x)).toEqual(domainOf(seen.ctxs[1]!.scales.x));
|
|
399
|
+
expect(domainOf(seen.ctxs[0]!.scales.y)).toEqual(domainOf(seen.ctxs[1]!.scales.y));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("each panel's geom sees its own sliced rows as ctx.data", () => {
|
|
403
|
+
const seen: { ctxs: CompileContext<FacetRow>[] } = { ctxs: [] };
|
|
404
|
+
const chart = plot({ data: facetData, width: 400, height: 300 })
|
|
405
|
+
.layer(facetBarGeom(seen))
|
|
406
|
+
.facet({ by: "panel", ncol: 2 });
|
|
407
|
+
runFacetPipeline(facetConfig(chart as unknown as FacetConfigChart), facetData);
|
|
408
|
+
|
|
409
|
+
expect(seen.ctxs).toHaveLength(2);
|
|
410
|
+
// ctx.data is the panel slice, not the full chart data.
|
|
411
|
+
expect(seen.ctxs[0]!.data).not.toBe(facetData);
|
|
412
|
+
expect(seen.ctxs[0]!.data.every((r) => r.panel === "a")).toBe(true);
|
|
413
|
+
expect(seen.ctxs[1]!.data.every((r) => r.panel === "b")).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// shareDomain — faceted `scales: "fixed"` domain pinning
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
describe("shareDomain — scale-type preservation", () => {
|
|
422
|
+
const aes = resolveAes<Row, unknown>("x");
|
|
423
|
+
|
|
424
|
+
it("preserves a log scale type and passes its domain through", () => {
|
|
425
|
+
const data: Row[] = [
|
|
426
|
+
{ x: 1, y: 0 },
|
|
427
|
+
{ x: 1000, y: 0 },
|
|
428
|
+
];
|
|
429
|
+
const scale = buildPositionScale(aes, data, [0, 300], { type: "log", domain: [1, 1000] });
|
|
430
|
+
expect(scale.type).toBe("log");
|
|
431
|
+
|
|
432
|
+
const shared = shareDomain(scale);
|
|
433
|
+
// A faceted log axis must stay log under scales:"fixed" — not silently
|
|
434
|
+
// collapse to linear.
|
|
435
|
+
expect(shared.type).toBe("log");
|
|
436
|
+
expect(shared.domain).toEqual([1, 1000]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("preserves band / time / sqrt and defaults the rest to linear", () => {
|
|
440
|
+
const catAes = resolveAes<{ c: string }, unknown>("c");
|
|
441
|
+
const bandScale = buildPositionScale(catAes, [{ c: "a" }, { c: "b" }], [0, 100], {
|
|
442
|
+
type: "band",
|
|
443
|
+
domain: ["a", "b"],
|
|
444
|
+
});
|
|
445
|
+
expect(shareDomain(bandScale).type).toBe("band");
|
|
446
|
+
|
|
447
|
+
const numData: Row[] = [
|
|
448
|
+
{ x: 0, y: 0 },
|
|
449
|
+
{ x: 100, y: 0 },
|
|
450
|
+
];
|
|
451
|
+
const sqrtScale = buildPositionScale(aes, numData, [0, 100], {
|
|
452
|
+
type: "sqrt",
|
|
453
|
+
domain: [0, 100],
|
|
454
|
+
});
|
|
455
|
+
expect(shareDomain(sqrtScale).type).toBe("sqrt");
|
|
456
|
+
|
|
457
|
+
const linScale = buildPositionScale(aes, numData, [0, 100], {
|
|
458
|
+
type: "linear",
|
|
459
|
+
domain: [0, 100],
|
|
460
|
+
});
|
|
461
|
+
expect(shareDomain(linScale).type).toBe("linear");
|
|
462
|
+
});
|
|
463
|
+
});
|