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,545 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// point geom
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { lerpColor, type Color, type Layer } from "insomni";
|
|
6
|
+
import {
|
|
7
|
+
pointMark,
|
|
8
|
+
type MarkBuilder,
|
|
9
|
+
type PointBorderStyle,
|
|
10
|
+
type PointShapeKind,
|
|
11
|
+
} from "../../marks.ts";
|
|
12
|
+
import type { Aes } from "../aes.ts";
|
|
13
|
+
import { resolveAes } from "../aes.ts";
|
|
14
|
+
import { alphaize, withAlpha } from "../color-utils.ts";
|
|
15
|
+
import type {
|
|
16
|
+
CompileContext,
|
|
17
|
+
CompiledHitTest,
|
|
18
|
+
Geom,
|
|
19
|
+
GeomFrame,
|
|
20
|
+
GeomHoverDecorator,
|
|
21
|
+
HoveredHit,
|
|
22
|
+
ResolvedChannelMap,
|
|
23
|
+
} from "./types.ts";
|
|
24
|
+
import {
|
|
25
|
+
defaultMarkFill,
|
|
26
|
+
haloRing,
|
|
27
|
+
inlineMark,
|
|
28
|
+
resolveCoord,
|
|
29
|
+
selectedIndicesFor,
|
|
30
|
+
selectionActive,
|
|
31
|
+
SELECTION_DIM_ALPHA,
|
|
32
|
+
wrapMark,
|
|
33
|
+
} from "./_mark.ts";
|
|
34
|
+
import { DEFAULT_OVERLAY_SCALE } from "../constants.ts";
|
|
35
|
+
|
|
36
|
+
export interface PointChannels<T> {
|
|
37
|
+
x: Aes<T, number | Date>;
|
|
38
|
+
y: Aes<T, number | Date>;
|
|
39
|
+
color?: Aes<T, unknown>;
|
|
40
|
+
size?: Aes<T, number>;
|
|
41
|
+
/**
|
|
42
|
+
* Categorical shape mapping. Values are looked up in the active shape scale
|
|
43
|
+
* — by default a built-in palette (`POINT_SHAPE_PALETTE`). Pass a literal
|
|
44
|
+
* `PointShapeKind` accessor and `.scale("shape", { palette: [...] })` to
|
|
45
|
+
* customize. When the value is already a `PointShapeKind`, it's used as-is.
|
|
46
|
+
*/
|
|
47
|
+
shape?: Aes<T, unknown>;
|
|
48
|
+
alpha?: Aes<T, number>;
|
|
49
|
+
/**
|
|
50
|
+
* Per-datum border treatment. The accessor may return either a resolved
|
|
51
|
+
* {@link PointBorderStyle} (used as-is) or any other categorical value that
|
|
52
|
+
* gets routed through the active `borderStyle` scale (default palette:
|
|
53
|
+
* `solid`, `open`, `dashed`, `dotted`). Configure a custom mapping with
|
|
54
|
+
* `.scale("borderStyle", { palette: [...] })`.
|
|
55
|
+
*/
|
|
56
|
+
borderStyle?: Aes<T, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Optional secondary glyph overlaid on the base shape at the same anchor.
|
|
59
|
+
* Accepts either a resolved {@link PointShapeKind}/`null`, or a categorical
|
|
60
|
+
* value routed through the `overlayGlyph` scale (default palette starts
|
|
61
|
+
* with `null` so the most-common category gets no overlay). Configure with
|
|
62
|
+
* `.scale("overlayGlyph", { palette: [...] })`.
|
|
63
|
+
*/
|
|
64
|
+
overlayGlyph?: Aes<T, unknown>;
|
|
65
|
+
/** Overlay radius as a fraction of base radius. Default `0.6`. */
|
|
66
|
+
overlayScale?: Aes<T, number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PointOptions {
|
|
70
|
+
/** Constant fill if color channel is absent. */
|
|
71
|
+
fill?: Color;
|
|
72
|
+
/** Constant point radius if size channel is absent. */
|
|
73
|
+
radius?: number;
|
|
74
|
+
/** Override theme stroke. */
|
|
75
|
+
stroke?: Color | null;
|
|
76
|
+
/** Override theme stroke width. */
|
|
77
|
+
strokeWidth?: number;
|
|
78
|
+
/** Constant shape if shape channel is absent. */
|
|
79
|
+
shape?: PointShapeKind;
|
|
80
|
+
/** Constant border style if border-style channel is absent. */
|
|
81
|
+
borderStyle?: PointBorderStyle;
|
|
82
|
+
/** Constant overlay glyph if overlay-glyph channel is absent. */
|
|
83
|
+
overlayGlyph?: PointShapeKind | null;
|
|
84
|
+
/** Constant overlay scale if overlay-scale channel is absent. */
|
|
85
|
+
overlayScale?: number;
|
|
86
|
+
/** Display label for legend (defaults to color column name). */
|
|
87
|
+
label?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isBorderStyleLiteral = (v: unknown): v is PointBorderStyle =>
|
|
91
|
+
v === "solid" || v === "dashed" || v === "dotted" || v === "open";
|
|
92
|
+
const isPointShapeLiteral = (v: unknown): v is PointShapeKind => {
|
|
93
|
+
if (typeof v !== "string") return false;
|
|
94
|
+
return (
|
|
95
|
+
v === "circle" ||
|
|
96
|
+
v === "square" ||
|
|
97
|
+
v === "triangle" ||
|
|
98
|
+
v === "diamond" ||
|
|
99
|
+
v === "circle-open" ||
|
|
100
|
+
v === "square-open" ||
|
|
101
|
+
v === "triangle-open" ||
|
|
102
|
+
v === "diamond-open" ||
|
|
103
|
+
v === "cross" ||
|
|
104
|
+
v === "plus" ||
|
|
105
|
+
v === "star"
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the per-datum visual functions shared by `compile` (which wraps
|
|
111
|
+
* `radius` / `fill` / projection in transition lerps) and `hoverDecoration`
|
|
112
|
+
* (which re-emits a single glyph onto the overlay). Centralizing keeps the two
|
|
113
|
+
* paths from drifting on shape / border / overlay / fill resolution. The
|
|
114
|
+
* returned `radius` / `fill` are the *static* (non-animated) values; `compile`
|
|
115
|
+
* layers transition interpolation on top.
|
|
116
|
+
*/
|
|
117
|
+
function pointVisuals<T>(
|
|
118
|
+
channels: PointChannels<T>,
|
|
119
|
+
options: PointOptions,
|
|
120
|
+
ctx: CompileContext<T>,
|
|
121
|
+
) {
|
|
122
|
+
const { scales, theme } = ctx;
|
|
123
|
+
const coord = resolveCoord(ctx);
|
|
124
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
125
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
126
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
127
|
+
const sizeAes = channels.size ? resolveAes<T, number>(channels.size) : undefined;
|
|
128
|
+
const alphaAes = channels.alpha ? resolveAes<T, number>(channels.alpha) : undefined;
|
|
129
|
+
const shapeAes = channels.shape ? resolveAes<T, unknown>(channels.shape) : undefined;
|
|
130
|
+
const borderStyleAes = channels.borderStyle
|
|
131
|
+
? resolveAes<T, unknown>(channels.borderStyle)
|
|
132
|
+
: undefined;
|
|
133
|
+
const overlayGlyphAes = channels.overlayGlyph
|
|
134
|
+
? resolveAes<T, unknown>(channels.overlayGlyph)
|
|
135
|
+
: undefined;
|
|
136
|
+
const overlayScaleAes = channels.overlayScale
|
|
137
|
+
? resolveAes<T, number>(channels.overlayScale)
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
const baseFill: Color = options.fill ?? defaultMarkFill(theme);
|
|
141
|
+
const baseRadius = options.radius ?? theme.marks.pointRadius;
|
|
142
|
+
const baseShape: PointShapeKind = options.shape ?? "circle";
|
|
143
|
+
const stroke =
|
|
144
|
+
options.stroke === null ? undefined : (options.stroke ?? theme.marks.pointStroke ?? undefined);
|
|
145
|
+
const strokeWidth = options.strokeWidth ?? theme.marks.pointStrokeWidth;
|
|
146
|
+
|
|
147
|
+
const xScale = scales.x.fn;
|
|
148
|
+
const yScale = scales.y.fn;
|
|
149
|
+
const colorScale = scales.color?.fn;
|
|
150
|
+
const sizeScale = scales.size?.fn;
|
|
151
|
+
const alphaScale = scales.alpha?.fn;
|
|
152
|
+
const shapeScale = scales.shape?.fn;
|
|
153
|
+
const borderStyleScale = scales.borderStyle?.fn;
|
|
154
|
+
const overlayGlyphScale = scales.overlayGlyph?.fn;
|
|
155
|
+
|
|
156
|
+
const fill = (d: T, i: number): Color => {
|
|
157
|
+
const base: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
|
|
158
|
+
if (alphaAes && alphaScale) return withAlpha(base, alphaScale(alphaAes.fn(d, i)));
|
|
159
|
+
return alphaize(base, theme.marks.fillAlpha);
|
|
160
|
+
};
|
|
161
|
+
const radius = (d: T, i: number): number =>
|
|
162
|
+
sizeAes && sizeScale ? (sizeScale(sizeAes.fn(d, i)) as number) || baseRadius : baseRadius;
|
|
163
|
+
const shape: (d: T, i: number) => PointShapeKind =
|
|
164
|
+
shapeAes && shapeScale
|
|
165
|
+
? (d, i) => shapeScale(shapeAes.fn(d, i)) as PointShapeKind
|
|
166
|
+
: shapeAes
|
|
167
|
+
? (d, i) => shapeAes.fn(d, i) as PointShapeKind
|
|
168
|
+
: () => baseShape;
|
|
169
|
+
const borderStyle: (d: T, i: number) => PointBorderStyle = borderStyleAes
|
|
170
|
+
? (d, i) => {
|
|
171
|
+
const val = borderStyleAes.fn(d, i);
|
|
172
|
+
if (isBorderStyleLiteral(val)) return val;
|
|
173
|
+
return borderStyleScale ? (borderStyleScale(val) as PointBorderStyle) : "solid";
|
|
174
|
+
}
|
|
175
|
+
: () => options.borderStyle ?? "solid";
|
|
176
|
+
const overlayGlyph: (d: T, i: number) => PointShapeKind | null = overlayGlyphAes
|
|
177
|
+
? (d, i) => {
|
|
178
|
+
const val = overlayGlyphAes.fn(d, i);
|
|
179
|
+
if (val === null || val === undefined) return null;
|
|
180
|
+
if (isPointShapeLiteral(val)) return val;
|
|
181
|
+
return overlayGlyphScale ? (overlayGlyphScale(val) as PointShapeKind | null) : null;
|
|
182
|
+
}
|
|
183
|
+
: () => options.overlayGlyph ?? null;
|
|
184
|
+
const overlayScale: (d: T, i: number) => number = overlayScaleAes
|
|
185
|
+
? (d, i) => overlayScaleAes.fn(d, i)
|
|
186
|
+
: () => options.overlayScale ?? DEFAULT_OVERLAY_SCALE;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
coord,
|
|
190
|
+
xAes,
|
|
191
|
+
yAes,
|
|
192
|
+
colorAes,
|
|
193
|
+
sizeAes,
|
|
194
|
+
colorScale,
|
|
195
|
+
sizeScale,
|
|
196
|
+
baseFill,
|
|
197
|
+
baseRadius,
|
|
198
|
+
stroke,
|
|
199
|
+
strokeWidth,
|
|
200
|
+
xScale,
|
|
201
|
+
yScale,
|
|
202
|
+
fill,
|
|
203
|
+
radius,
|
|
204
|
+
shape,
|
|
205
|
+
borderStyle,
|
|
206
|
+
overlayGlyph,
|
|
207
|
+
overlayScale,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function point<T>(channels: PointChannels<T>, options: PointOptions = {}): Geom<T> {
|
|
212
|
+
return {
|
|
213
|
+
kind: "point",
|
|
214
|
+
channels,
|
|
215
|
+
label: options.label,
|
|
216
|
+
compile(ctx: CompileContext<T>) {
|
|
217
|
+
const { data, plot } = ctx;
|
|
218
|
+
const v = pointVisuals(channels, options, ctx);
|
|
219
|
+
const {
|
|
220
|
+
coord,
|
|
221
|
+
xAes,
|
|
222
|
+
yAes,
|
|
223
|
+
colorAes,
|
|
224
|
+
sizeAes,
|
|
225
|
+
colorScale,
|
|
226
|
+
sizeScale,
|
|
227
|
+
baseFill,
|
|
228
|
+
baseRadius,
|
|
229
|
+
stroke,
|
|
230
|
+
strokeWidth,
|
|
231
|
+
xScale,
|
|
232
|
+
yScale,
|
|
233
|
+
fill: baseFillFor,
|
|
234
|
+
shape: shapeFn,
|
|
235
|
+
borderStyle: borderStyleFn,
|
|
236
|
+
overlayGlyph: overlayGlyphFn,
|
|
237
|
+
overlayScale: overlayScaleFn,
|
|
238
|
+
} = v;
|
|
239
|
+
|
|
240
|
+
// Selection dimming — when any row is selected anywhere, non-matching
|
|
241
|
+
// rows on this geom drop to SELECTION_DIM_ALPHA. Rows in `selectedSet`
|
|
242
|
+
// keep their natural alpha and pick up a stroke ring further down.
|
|
243
|
+
const selectedSet = selectedIndicesFor(ctx, "point");
|
|
244
|
+
const dim = selectionActive(ctx);
|
|
245
|
+
const fillFn = dim
|
|
246
|
+
? (d: T, i: number): Color => {
|
|
247
|
+
const c = baseFillFor(d, i);
|
|
248
|
+
return selectedSet?.has(i) ? c : alphaize(c, SELECTION_DIM_ALPHA);
|
|
249
|
+
}
|
|
250
|
+
: baseFillFor;
|
|
251
|
+
|
|
252
|
+
const isHidden = (d: T, i: number) => {
|
|
253
|
+
if (!ctx.hidden || !colorAes) return false;
|
|
254
|
+
const val = colorAes.fn(d, i);
|
|
255
|
+
return ctx.hidden.has(String(val));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const anim = ctx.activeTransition;
|
|
259
|
+
const fromIndexFor = (d: T, i: number) =>
|
|
260
|
+
anim
|
|
261
|
+
? ctx.transitionKey
|
|
262
|
+
? anim.matchIndex(ctx.transitionKey(d, i), i)
|
|
263
|
+
: anim.matchIndex(String(i), i)
|
|
264
|
+
: undefined;
|
|
265
|
+
|
|
266
|
+
// Animated fill: lerp from the stored RGBA toward the current fill.
|
|
267
|
+
const animFill = anim
|
|
268
|
+
? (d: T, i: number): Color => {
|
|
269
|
+
const toFill = fillFn(d, i);
|
|
270
|
+
const fromIndex = fromIndexFor(d, i);
|
|
271
|
+
if (fromIndex === undefined) return { ...toFill, a: toFill.a * anim.t };
|
|
272
|
+
const fromFill: Color = {
|
|
273
|
+
r: anim.from.rgba[fromIndex * 4]!,
|
|
274
|
+
g: anim.from.rgba[fromIndex * 4 + 1]!,
|
|
275
|
+
b: anim.from.rgba[fromIndex * 4 + 2]!,
|
|
276
|
+
a: anim.from.rgba[fromIndex * 4 + 3]!,
|
|
277
|
+
};
|
|
278
|
+
return lerpColor(fromFill, toFill, anim.t);
|
|
279
|
+
}
|
|
280
|
+
: fillFn;
|
|
281
|
+
|
|
282
|
+
// Combined per-datum projection through the active coord. Under
|
|
283
|
+
// `coordCartesian()` `project` is the identity, so this returns the
|
|
284
|
+
// same plot-frame pixel pair we used to emit directly. Under polar
|
|
285
|
+
// (Phase 3) the (x, y) need to be projected as a single point — we
|
|
286
|
+
// compute both axes' lerped values and then project once per axis.
|
|
287
|
+
const projectXY = (d: T, i: number): { x: number; y: number } => {
|
|
288
|
+
if (isHidden(d, i)) return { x: NaN, y: NaN };
|
|
289
|
+
const toX = xScale(xAes.fn(d, i));
|
|
290
|
+
const toY = yScale(yAes.fn(d, i));
|
|
291
|
+
const fromIndex = fromIndexFor(d, i);
|
|
292
|
+
let px = toX;
|
|
293
|
+
let py = toY;
|
|
294
|
+
if (anim && fromIndex !== undefined) {
|
|
295
|
+
if (Number.isFinite(anim.from.x[fromIndex]!)) {
|
|
296
|
+
px = anim.from.x[fromIndex]! + (toX - anim.from.x[fromIndex]!) * anim.t;
|
|
297
|
+
}
|
|
298
|
+
if (Number.isFinite(anim.from.y[fromIndex]!)) {
|
|
299
|
+
py = anim.from.y[fromIndex]! + (toY - anim.from.y[fromIndex]!) * anim.t;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return coord.project({ x: px, y: py });
|
|
303
|
+
};
|
|
304
|
+
// Animated radius: lerp from the stored radius toward the current size.
|
|
305
|
+
const radiusFn: (d: T, i: number) => number =
|
|
306
|
+
sizeAes && sizeScale
|
|
307
|
+
? (d, i) => {
|
|
308
|
+
const toR = v.radius(d, i);
|
|
309
|
+
if (!anim) return toR;
|
|
310
|
+
const fromIndex = fromIndexFor(d, i);
|
|
311
|
+
if (fromIndex === undefined) return toR * anim.t;
|
|
312
|
+
const fromR = anim.from.r?.[fromIndex] ?? toR;
|
|
313
|
+
return fromR + (toR - fromR) * anim.t;
|
|
314
|
+
}
|
|
315
|
+
: () => baseRadius;
|
|
316
|
+
|
|
317
|
+
const mark = pointMark(data, {
|
|
318
|
+
x: (d, i) => projectXY(d, i).x,
|
|
319
|
+
y: (d, i) => projectXY(d, i).y,
|
|
320
|
+
radius: radiusFn,
|
|
321
|
+
fill: animFill,
|
|
322
|
+
stroke,
|
|
323
|
+
strokeWidth,
|
|
324
|
+
shape: shapeFn,
|
|
325
|
+
borderStyle: borderStyleFn,
|
|
326
|
+
overlayGlyph: overlayGlyphFn,
|
|
327
|
+
overlayScale: overlayScaleFn,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const builders: MarkBuilder[] = [wrapMark(mark, plot.topLeft, data.length)];
|
|
331
|
+
|
|
332
|
+
// Hover focus (contrast halo + bring-to-front) is NOT drawn here — under
|
|
333
|
+
// partial redraw the marks layer is baked and never recompiles on hover,
|
|
334
|
+
// so it would never appear. It rides the cheap overlay path instead; see
|
|
335
|
+
// `hoverDecoration` below. Points also intentionally do not dim the rest
|
|
336
|
+
// on hover (sparse marks read fine with just the ring); only selection
|
|
337
|
+
// dims (see `dim` above).
|
|
338
|
+
|
|
339
|
+
// Selection rings — a stroke ring on each selected row.
|
|
340
|
+
if (selectedSet) {
|
|
341
|
+
for (const i of selectedSet) {
|
|
342
|
+
const d = data[i];
|
|
343
|
+
if (d === undefined) continue;
|
|
344
|
+
const xv = xAes.fn(d, i);
|
|
345
|
+
const yv = yAes.fn(d, i);
|
|
346
|
+
if (xv == null || yv == null) continue;
|
|
347
|
+
const rawX = xScale(xv);
|
|
348
|
+
const rawY = yScale(yv);
|
|
349
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
350
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
351
|
+
const r =
|
|
352
|
+
sizeAes && sizeScale ? Number(sizeScale(sizeAes.fn(d, i))) || baseRadius : baseRadius;
|
|
353
|
+
const ringColor: Color =
|
|
354
|
+
colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
|
|
355
|
+
const cx = plot.topLeft.x + projected.x;
|
|
356
|
+
const cy = plot.topLeft.y + projected.y;
|
|
357
|
+
builders.push(inlineMark((layer) => haloRing(layer, cx, cy, r + 4, ringColor, 2)));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return builders;
|
|
362
|
+
},
|
|
363
|
+
hoverDecoration(ctx: CompileContext<T>): GeomHoverDecorator | null {
|
|
364
|
+
const { data, plot } = ctx;
|
|
365
|
+
if (data.length === 0) return null;
|
|
366
|
+
const v = pointVisuals(channels, options, ctx);
|
|
367
|
+
const hoverCfg = ctx.theme.interactions.hover;
|
|
368
|
+
const ringColor: Color = hoverCfg.haloColor ?? ctx.theme.text.color;
|
|
369
|
+
const origin = plot.topLeft;
|
|
370
|
+
return {
|
|
371
|
+
geomKind: "point",
|
|
372
|
+
data,
|
|
373
|
+
decorate(hit: HoveredHit, layer: Layer): void {
|
|
374
|
+
if (!hoverCfg.enabled || hit.data !== data) return;
|
|
375
|
+
const i = hit.dataIndex;
|
|
376
|
+
const d = data[i];
|
|
377
|
+
if (d === undefined) return;
|
|
378
|
+
const xv = v.xAes.fn(d, i);
|
|
379
|
+
const yv = v.yAes.fn(d, i);
|
|
380
|
+
if (xv == null || yv == null) return;
|
|
381
|
+
const rawX = v.xScale(xv);
|
|
382
|
+
const rawY = v.yScale(yv);
|
|
383
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return;
|
|
384
|
+
const p = v.coord.project({ x: rawX, y: rawY });
|
|
385
|
+
const r = v.radius(d, i);
|
|
386
|
+
// Contrast ring sits over the marks; the re-emitted glyph then draws
|
|
387
|
+
// on top so a point tucked under the trend line or behind a neighbor
|
|
388
|
+
// stays fully visible while focused. Un-dimmed fill keeps it vivid.
|
|
389
|
+
haloRing(
|
|
390
|
+
layer,
|
|
391
|
+
origin.x + p.x,
|
|
392
|
+
origin.y + p.y,
|
|
393
|
+
r + 4,
|
|
394
|
+
ringColor,
|
|
395
|
+
hoverCfg.haloStrokeWidth,
|
|
396
|
+
);
|
|
397
|
+
pointMark([d], {
|
|
398
|
+
x: () => p.x,
|
|
399
|
+
y: () => p.y,
|
|
400
|
+
radius: () => r,
|
|
401
|
+
fill: () => v.fill(d, i),
|
|
402
|
+
stroke: v.stroke,
|
|
403
|
+
strokeWidth: v.strokeWidth,
|
|
404
|
+
shape: () => v.shape(d, i),
|
|
405
|
+
borderStyle: () => v.borderStyle(d, i),
|
|
406
|
+
overlayGlyph: () => v.overlayGlyph(d, i),
|
|
407
|
+
overlayScale: () => v.overlayScale(d, i),
|
|
408
|
+
}).addTo(layer, origin);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
|
|
413
|
+
const { data, scales, plot, theme, hidden } = ctx;
|
|
414
|
+
if (data.length === 0) return null;
|
|
415
|
+
const coord = resolveCoord(ctx);
|
|
416
|
+
|
|
417
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
418
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
419
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
420
|
+
const sizeAes = channels.size
|
|
421
|
+
? (resolveAes<T, number>(channels.size) as unknown as ResolvedAesUnknown<T>)
|
|
422
|
+
: undefined;
|
|
423
|
+
const shapeAes = channels.shape ? resolveAes<T, unknown>(channels.shape) : undefined;
|
|
424
|
+
const alphaAes = channels.alpha
|
|
425
|
+
? (resolveAes<T, number>(channels.alpha) as unknown as ResolvedAesUnknown<T>)
|
|
426
|
+
: undefined;
|
|
427
|
+
|
|
428
|
+
const xScale = scales.x.fn;
|
|
429
|
+
const yScale = scales.y.fn;
|
|
430
|
+
const sizeScale = scales.size?.fn;
|
|
431
|
+
const baseRadius = options.radius ?? theme.marks.pointRadius;
|
|
432
|
+
|
|
433
|
+
const positions = new Float32Array(data.length * 2);
|
|
434
|
+
const dataIndex = new Int32Array(data.length);
|
|
435
|
+
const ox = plot.topLeft.x;
|
|
436
|
+
const oy = plot.topLeft.y;
|
|
437
|
+
let n = 0;
|
|
438
|
+
let maxRadius = baseRadius;
|
|
439
|
+
for (let i = 0; i < data.length; i++) {
|
|
440
|
+
const d = data[i]!;
|
|
441
|
+
const xv = xAes.fn(d, i);
|
|
442
|
+
const yv = yAes.fn(d, i);
|
|
443
|
+
if (xv == null || yv == null) continue;
|
|
444
|
+
|
|
445
|
+
if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const rawX = xScale(xv);
|
|
450
|
+
const rawY = yScale(yv);
|
|
451
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
452
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
453
|
+
positions[n * 2] = ox + projected.x;
|
|
454
|
+
positions[n * 2 + 1] = oy + projected.y;
|
|
455
|
+
dataIndex[n] = i;
|
|
456
|
+
n++;
|
|
457
|
+
if (sizeAes && sizeScale) {
|
|
458
|
+
const r = sizeScale(sizeAes.fn(d, i));
|
|
459
|
+
if (typeof r === "number" && r > maxRadius) maxRadius = r;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (n === 0) return null;
|
|
463
|
+
|
|
464
|
+
const channelsMap: ResolvedChannelMap<T> = {
|
|
465
|
+
x: xAes,
|
|
466
|
+
y: yAes,
|
|
467
|
+
color: colorAes,
|
|
468
|
+
size: sizeAes as ResolvedAesUnknown<T> | undefined,
|
|
469
|
+
shape: shapeAes,
|
|
470
|
+
alpha: alphaAes as ResolvedAesUnknown<T> | undefined,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
geomKind: "point",
|
|
475
|
+
label: options.label,
|
|
476
|
+
positions: positions.subarray(0, n * 2),
|
|
477
|
+
dataIndex: dataIndex.subarray(0, n),
|
|
478
|
+
pickRadius: Math.max(8, maxRadius + 4),
|
|
479
|
+
channels: channelsMap,
|
|
480
|
+
data,
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
captureFrame(ctx: CompileContext<T>): GeomFrame | null {
|
|
484
|
+
const { data, scales, theme } = ctx;
|
|
485
|
+
if (data.length === 0) return null;
|
|
486
|
+
|
|
487
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
488
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
489
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
490
|
+
const sizeAes = channels.size ? resolveAes<T, number>(channels.size) : undefined;
|
|
491
|
+
const alphaAes = channels.alpha ? resolveAes<T, number>(channels.alpha) : undefined;
|
|
492
|
+
|
|
493
|
+
const xScale = scales.x.fn;
|
|
494
|
+
const yScale = scales.y.fn;
|
|
495
|
+
const colorScale = scales.color?.fn;
|
|
496
|
+
const sizeScale = scales.size?.fn;
|
|
497
|
+
const alphaScale = scales.alpha?.fn;
|
|
498
|
+
|
|
499
|
+
const baseRadius = options.radius ?? theme.marks.pointRadius;
|
|
500
|
+
const baseFill: Color = options.fill ?? defaultMarkFill(theme);
|
|
501
|
+
|
|
502
|
+
const count = data.length;
|
|
503
|
+
const x = new Float32Array(count);
|
|
504
|
+
const y = new Float32Array(count);
|
|
505
|
+
const rgba = new Float32Array(count * 4);
|
|
506
|
+
const a = new Float32Array(count);
|
|
507
|
+
const r = new Float32Array(count);
|
|
508
|
+
const transitionKey = ctx.transitionKey;
|
|
509
|
+
const ids = transitionKey ? Array.from<string>({ length: count }) : undefined;
|
|
510
|
+
|
|
511
|
+
for (let i = 0; i < count; i++) {
|
|
512
|
+
const d = data[i]!;
|
|
513
|
+
const xv = xAes.fn(d, i);
|
|
514
|
+
const yv = yAes.fn(d, i);
|
|
515
|
+
const px = xScale(xv);
|
|
516
|
+
const py = yScale(yv);
|
|
517
|
+
x[i] = Number.isFinite(px) ? px : NaN;
|
|
518
|
+
y[i] = Number.isFinite(py) ? py : NaN;
|
|
519
|
+
|
|
520
|
+
// Radius
|
|
521
|
+
r[i] =
|
|
522
|
+
sizeAes && sizeScale ? (sizeScale(sizeAes.fn(d, i)) as number) || baseRadius : baseRadius;
|
|
523
|
+
|
|
524
|
+
// Color with alpha
|
|
525
|
+
const c: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
|
|
526
|
+
const alpha =
|
|
527
|
+
alphaAes && alphaScale
|
|
528
|
+
? (alphaScale(alphaAes.fn(d, i)) as number)
|
|
529
|
+
: theme.marks.fillAlpha;
|
|
530
|
+
rgba[i * 4] = c.r;
|
|
531
|
+
rgba[i * 4 + 1] = c.g;
|
|
532
|
+
rgba[i * 4 + 2] = c.b;
|
|
533
|
+
rgba[i * 4 + 3] = c.a * alpha;
|
|
534
|
+
a[i] = alpha;
|
|
535
|
+
if (ids && transitionKey) ids[i] = transitionKey(d, i);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return { count, x, y, rgba, a, r, ids };
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Local alias to keep the cast site readable. SizeAes/AlphaAes resolve as
|
|
544
|
+
// `ResolvedAes<T, number>` but the channel map types them as <T, unknown>.
|
|
545
|
+
type ResolvedAesUnknown<T> = import("../aes.ts").ResolvedAes<T, unknown>;
|