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,619 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Coord — coordinate system abstraction
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// A `Coord` decides how plot-frame pixel positions (the output of x/y scales)
|
|
5
|
+
// map onto layer-pixel space, and how axes are drawn for that mapping.
|
|
6
|
+
//
|
|
7
|
+
// Two coords ship today:
|
|
8
|
+
// - `coordCartesian()` — identity projection, current axis behavior.
|
|
9
|
+
// - `coordPolar()` / `coordRadial()` — (θ, r) → (cx + r·cosθ, cy + r·sinθ)
|
|
10
|
+
// with spokes + concentric-circle axes.
|
|
11
|
+
//
|
|
12
|
+
// The interface is shaped so polar lands without a second refactor:
|
|
13
|
+
// `project` / `segment` are vertex-level hooks; `renderAxes` is the dispatch
|
|
14
|
+
// point for non-Cartesian axis layouts; `handlePan` / `handleZoom` give polar
|
|
15
|
+
// a place to translate pointer deltas into rotation / radial moves rather
|
|
16
|
+
// than direct scale-domain rewrites.
|
|
17
|
+
//
|
|
18
|
+
// Polar is stateful: `bindFrame(plotFrame)` must be called before `project` /
|
|
19
|
+
// `segment` / `renderAxes` so the projection knows where the plot centre is.
|
|
20
|
+
// The pipeline calls this once per panel (faceted) or once per chart
|
|
21
|
+
// (unfaceted). Cartesian's `bindFrame` is a no-op.
|
|
22
|
+
|
|
23
|
+
import { type Color, type GlyphAtlas, type Layer, rgba } from "insomni";
|
|
24
|
+
import { bottomAxis, leftAxis, type AxisOptions } from "../axis.ts";
|
|
25
|
+
import type { ContinuousScale, TimeScale, BandScale } from "../scales.ts";
|
|
26
|
+
import type { PositionScale } from "./scales.ts";
|
|
27
|
+
import type { ScaleBundle } from "./geoms/types.ts";
|
|
28
|
+
|
|
29
|
+
export interface Point {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Inputs to `Coord.renderAxes`. The pipeline owns scale construction and
|
|
36
|
+
* layout; the coord owns axis dispatch (Cartesian → bottom + left; polar →
|
|
37
|
+
* angular ring + radial spokes).
|
|
38
|
+
*/
|
|
39
|
+
export interface CoordAxesArgs {
|
|
40
|
+
axisLayer: Layer;
|
|
41
|
+
scales: ScaleBundle;
|
|
42
|
+
/** Inner plot frame — coords project relative to this. */
|
|
43
|
+
plotFrame: import("insomni").Frame;
|
|
44
|
+
/** True when the chart actually has a column on this channel. Skipped axes are not drawn. */
|
|
45
|
+
hasX: boolean;
|
|
46
|
+
hasY: boolean;
|
|
47
|
+
xAxisOptions: AxisOptions<unknown>;
|
|
48
|
+
yAxisOptions: AxisOptions<unknown>;
|
|
49
|
+
atlas: GlyphAtlas | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inputs to `Coord.handlePan`. The mount / interactions layer passes a pointer
|
|
54
|
+
* delta in plot-frame pixels plus the active plot frame and a viewport handle
|
|
55
|
+
* the coord can use to mutate scale state. Cartesian delegates the whole
|
|
56
|
+
* thing to `viewport.panBy(dx, dy)`; polar decomposes into tangential
|
|
57
|
+
* (rotate `startAngle`) and radial (translate radius domain via the viewport).
|
|
58
|
+
*/
|
|
59
|
+
export interface CoordPanArgs {
|
|
60
|
+
dx: number;
|
|
61
|
+
dy: number;
|
|
62
|
+
/** Plot frame in absolute layer-pixel space (x, y are screen-space origin). */
|
|
63
|
+
plotFrame: import("insomni").Frame;
|
|
64
|
+
/** Viewport handle for scale-domain mutations. */
|
|
65
|
+
viewport: CoordViewportHandle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CoordZoomArgs {
|
|
69
|
+
/**
|
|
70
|
+
* Multiplicative zoom factor — `> 1` zooms in, `< 1` zooms out. May be a
|
|
71
|
+
* scalar (both axes) or a `{ x?, y? }` per-axis form (matches
|
|
72
|
+
* `viewport.zoomAt` and the masking that `bindDataViewport` performs).
|
|
73
|
+
*/
|
|
74
|
+
factor: number | { x?: number; y?: number };
|
|
75
|
+
/** Cursor position in absolute layer-pixel space (matches `viewport.zoomAt`). */
|
|
76
|
+
cx: number;
|
|
77
|
+
cy: number;
|
|
78
|
+
plotFrame: import("insomni").Frame;
|
|
79
|
+
viewport: CoordViewportHandle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Minimal facade over `DataViewport` so the coord can mutate scale state
|
|
84
|
+
* without depending on the full viewport surface. The two methods polar
|
|
85
|
+
* uses (`panBy`, `zoomAt`) match `DataViewport` semantics 1:1; Cartesian's
|
|
86
|
+
* implementation forwards through them so behavior is byte-identical to the
|
|
87
|
+
* pre-`handlePan` path.
|
|
88
|
+
*/
|
|
89
|
+
export interface CoordViewportHandle {
|
|
90
|
+
panBy(dxPx: number, dyPx: number): void;
|
|
91
|
+
zoomAt(anchorSx: number, anchorSy: number, factor: number | { x?: number; y?: number }): void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A coordinate system. Polar / cartesian today; future room for log-polar /
|
|
96
|
+
* geographic projections behind the same interface.
|
|
97
|
+
*/
|
|
98
|
+
export interface Coord {
|
|
99
|
+
readonly kind: "cartesian" | "polar";
|
|
100
|
+
/**
|
|
101
|
+
* Bind the coord to a specific plot frame for the upcoming render.
|
|
102
|
+
* Polar's `project` / `segment` / `renderAxes` need the plot-frame
|
|
103
|
+
* dimensions and centre to compute (cx, cy) and the default outerRadius.
|
|
104
|
+
* Called by the pipeline once per panel (faceted) or once per chart
|
|
105
|
+
* (unfaceted) before any `project` / `renderAxes` call. Cartesian no-ops.
|
|
106
|
+
*/
|
|
107
|
+
bindFrame(plotFrame: import("insomni").Frame): void;
|
|
108
|
+
/**
|
|
109
|
+
* Map a single point from plot-frame pixel space (0..plotFrame.width on x,
|
|
110
|
+
* 0..plotFrame.height on y — i.e. the raw output of `xScale.fn` / `yScale.fn`)
|
|
111
|
+
* to layer-pixel space (still relative to the plot frame's origin —
|
|
112
|
+
* `plot.topLeft` offsets are applied later by each geom).
|
|
113
|
+
*
|
|
114
|
+
* Cartesian: identity. Polar: (θ, r) → (cx + r·cosθ, cy + r·sinθ).
|
|
115
|
+
*/
|
|
116
|
+
project(p: Point): Point;
|
|
117
|
+
/**
|
|
118
|
+
* Tessellate a polyline segment between two points in plot-frame pixel
|
|
119
|
+
* space. Returns the projected pixel points (including both endpoints).
|
|
120
|
+
* Cartesian returns `[project(p1), project(p2)]`; polar returns ~N points
|
|
121
|
+
* along the arc.
|
|
122
|
+
*/
|
|
123
|
+
segment(p1: Point, p2: Point): Point[];
|
|
124
|
+
/**
|
|
125
|
+
* Render axes into `args.axisLayer`. Cartesian draws bottom (x) + left (y);
|
|
126
|
+
* polar draws an angular ring (spokes) + radial concentric circles.
|
|
127
|
+
*/
|
|
128
|
+
renderAxes(args: CoordAxesArgs): void;
|
|
129
|
+
/**
|
|
130
|
+
* Inverse of `project`, for hit-testing / tooltips. Cartesian is the
|
|
131
|
+
* identity within the plot frame. Polar inverts (θ, r) and returns `null`
|
|
132
|
+
* when the projected point is outside `[innerRadius, outerRadius]`.
|
|
133
|
+
*/
|
|
134
|
+
unproject(p: Point): Point | null;
|
|
135
|
+
/**
|
|
136
|
+
* Translate a pointer pan delta into scale-domain mutations.
|
|
137
|
+
* Cartesian forwards to `viewport.panBy(dx, dy)` — byte-identical to the
|
|
138
|
+
* pre-coord path. Polar decomposes the delta relative to the plot centre
|
|
139
|
+
* into tangential (rotate `startAngle`) and radial (translate the radius
|
|
140
|
+
* scale via `viewport.panBy(0, radial_dy)`) components.
|
|
141
|
+
*/
|
|
142
|
+
handlePan(args: CoordPanArgs): void;
|
|
143
|
+
/**
|
|
144
|
+
* Translate a zoom factor + anchor into scale-domain mutations.
|
|
145
|
+
* Cartesian forwards to `viewport.zoomAt(cx, cy, factor)`. Polar scales the
|
|
146
|
+
* radius domain around the cursor's data-space radius; angle scale
|
|
147
|
+
* unchanged.
|
|
148
|
+
*/
|
|
149
|
+
handleZoom(args: CoordZoomArgs): void;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// coordCartesian
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Default coord — identity projection, current axis behavior. Plot's existing
|
|
158
|
+
* pipeline behaves exactly as it did before the `Coord` interface was added
|
|
159
|
+
* when this is in use.
|
|
160
|
+
*/
|
|
161
|
+
export function coordCartesian(): Coord {
|
|
162
|
+
return cartesianSingleton;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cartesianSingleton: Coord = {
|
|
166
|
+
kind: "cartesian",
|
|
167
|
+
bindFrame() {
|
|
168
|
+
// Cartesian is frame-agnostic — geoms apply `plot.topLeft` themselves.
|
|
169
|
+
},
|
|
170
|
+
project(p) {
|
|
171
|
+
return p;
|
|
172
|
+
},
|
|
173
|
+
segment(p1, p2) {
|
|
174
|
+
return [p1, p2];
|
|
175
|
+
},
|
|
176
|
+
renderAxes({ axisLayer, scales, plotFrame, hasX, hasY, xAxisOptions, yAxisOptions }) {
|
|
177
|
+
if (hasX) {
|
|
178
|
+
bottomAxis(
|
|
179
|
+
scales.x.axisScale as never,
|
|
180
|
+
{
|
|
181
|
+
...xAxisOptions,
|
|
182
|
+
gridLength: plotFrame.height,
|
|
183
|
+
axisLineExtent: [0, plotFrame.width],
|
|
184
|
+
} as never,
|
|
185
|
+
).addTo(axisLayer, plotFrame.bottomLeft);
|
|
186
|
+
}
|
|
187
|
+
if (hasY) {
|
|
188
|
+
leftAxis(
|
|
189
|
+
scales.y.axisScale as never,
|
|
190
|
+
{
|
|
191
|
+
...yAxisOptions,
|
|
192
|
+
gridLength: plotFrame.width,
|
|
193
|
+
axisLineExtent: [0, plotFrame.height],
|
|
194
|
+
} as never,
|
|
195
|
+
).addTo(axisLayer, plotFrame.topLeft);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
unproject(p) {
|
|
199
|
+
return p;
|
|
200
|
+
},
|
|
201
|
+
handlePan({ dx, dy, viewport }) {
|
|
202
|
+
// Preserve existing behavior exactly: this is what `bindDataViewport`
|
|
203
|
+
// does today via a direct `viewport.panBy` call.
|
|
204
|
+
viewport.panBy(dx, dy);
|
|
205
|
+
},
|
|
206
|
+
handleZoom({ factor, cx, cy, viewport }) {
|
|
207
|
+
viewport.zoomAt(cx, cy, factor);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// coordPolar
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
const TWO_PI = Math.PI * 2;
|
|
216
|
+
const DEFAULT_QUALITY = Math.PI / 180; // ~1°.
|
|
217
|
+
|
|
218
|
+
export interface CoordPolarOptions {
|
|
219
|
+
/** Angle (radians) where the angular axis starts. Default `-π/2` (top). */
|
|
220
|
+
startAngle?: number;
|
|
221
|
+
/** Angle where the angular axis ends. Default `startAngle + 2π` (full circle). */
|
|
222
|
+
endAngle?: number;
|
|
223
|
+
/**
|
|
224
|
+
* Convenience for non-full-circle layouts: the gap (in radians) between
|
|
225
|
+
* `startAngle` and `endAngle`. `openAngle: 0` ↔ full circle;
|
|
226
|
+
* `openAngle: π/4` ↔ fan with a 45° gap at the start. Ignored when
|
|
227
|
+
* `endAngle` is provided.
|
|
228
|
+
*/
|
|
229
|
+
openAngle?: number;
|
|
230
|
+
/** `1` (default, CCW) or `-1` (CW). */
|
|
231
|
+
direction?: 1 | -1;
|
|
232
|
+
/** Inner radius in pixels. Default `0`. */
|
|
233
|
+
innerRadius?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Outer radius in pixels. Default `min(plotFrame.width, plotFrame.height) / 2`
|
|
236
|
+
* — resolved lazily on `bindFrame`.
|
|
237
|
+
*/
|
|
238
|
+
outerRadius?: number;
|
|
239
|
+
/** Which channel maps to θ. Default `'y'` (gheatmap/circular-tree convention). */
|
|
240
|
+
angleChannel?: "x" | "y";
|
|
241
|
+
/** Angular tessellation step (radians) for `segment` and circular axes. Default ~1°. */
|
|
242
|
+
quality?: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Polar projection. See {@link CoordPolarOptions}.
|
|
247
|
+
*
|
|
248
|
+
* Each call returns a fresh stateful coord so the same `coordPolar({...})`
|
|
249
|
+
* expression can be reused safely. `bindFrame` mutates the centre and the
|
|
250
|
+
* default `outerRadius`.
|
|
251
|
+
*/
|
|
252
|
+
export function coordPolar(opts: CoordPolarOptions = {}): Coord {
|
|
253
|
+
const direction: 1 | -1 = opts.direction ?? 1;
|
|
254
|
+
const angleChannel: "x" | "y" = opts.angleChannel ?? "y";
|
|
255
|
+
const innerRadius = opts.innerRadius ?? 0;
|
|
256
|
+
const explicitOuter = opts.outerRadius;
|
|
257
|
+
const quality = opts.quality ?? DEFAULT_QUALITY;
|
|
258
|
+
|
|
259
|
+
let startAngle = opts.startAngle ?? -Math.PI / 2;
|
|
260
|
+
let endAngle: number;
|
|
261
|
+
if (opts.endAngle !== undefined) {
|
|
262
|
+
endAngle = opts.endAngle;
|
|
263
|
+
} else if (opts.openAngle !== undefined) {
|
|
264
|
+
endAngle = startAngle + (TWO_PI - opts.openAngle);
|
|
265
|
+
} else {
|
|
266
|
+
endAngle = startAngle + TWO_PI;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Bound state. Updated by `bindFrame`.
|
|
270
|
+
let cx = 0;
|
|
271
|
+
let cy = 0;
|
|
272
|
+
let outerR = explicitOuter ?? 0;
|
|
273
|
+
let plotW = 0;
|
|
274
|
+
let plotH = 0;
|
|
275
|
+
|
|
276
|
+
function arcSpan(): number {
|
|
277
|
+
return endAngle - startAngle;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function angleAt(channelPixel: number, channelExtent: number): number {
|
|
281
|
+
const t = channelExtent > 0 ? channelPixel / channelExtent : 0;
|
|
282
|
+
return startAngle + direction * t * arcSpan();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function radiusAt(channelPixel: number, channelExtent: number): number {
|
|
286
|
+
const t = channelExtent > 0 ? channelPixel / channelExtent : 0;
|
|
287
|
+
return innerRadius + t * (outerR - innerRadius);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function project(p: Point): Point {
|
|
291
|
+
const theta = angleChannel === "x" ? angleAt(p.x, plotW) : angleAt(plotH - p.y, plotH);
|
|
292
|
+
const r = angleChannel === "x" ? radiusAt(plotH - p.y, plotH) : radiusAt(p.x, plotW);
|
|
293
|
+
return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function projectThetaR(theta: number, r: number): Point {
|
|
297
|
+
return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function pointToThetaR(p: Point): { theta: number; r: number } {
|
|
301
|
+
const theta = angleChannel === "x" ? angleAt(p.x, plotW) : angleAt(plotH - p.y, plotH);
|
|
302
|
+
const r = angleChannel === "x" ? radiusAt(plotH - p.y, plotH) : radiusAt(p.x, plotW);
|
|
303
|
+
return { theta, r };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function segment(p1: Point, p2: Point): Point[] {
|
|
307
|
+
const a = pointToThetaR(p1);
|
|
308
|
+
const b = pointToThetaR(p2);
|
|
309
|
+
const dTheta = b.theta - a.theta;
|
|
310
|
+
const dR = b.r - a.r;
|
|
311
|
+
// Radial line (same θ): straight spoke — no tessellation needed.
|
|
312
|
+
if (Math.abs(dTheta) < 1e-9) {
|
|
313
|
+
return [projectThetaR(a.theta, a.r), projectThetaR(b.theta, b.r)];
|
|
314
|
+
}
|
|
315
|
+
const absDTheta = Math.abs(dTheta);
|
|
316
|
+
const steps = Math.max(1, Math.ceil(absDTheta / quality));
|
|
317
|
+
const out: Point[] = Array.from({ length: steps + 1 });
|
|
318
|
+
for (let i = 0; i <= steps; i++) {
|
|
319
|
+
const t = i / steps;
|
|
320
|
+
const theta = a.theta + dTheta * t;
|
|
321
|
+
const r = a.r + dR * t; // arc when dR=0, oblique otherwise.
|
|
322
|
+
out[i] = projectThetaR(theta, r);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function unproject(p: Point): Point | null {
|
|
328
|
+
const dx = p.x - cx;
|
|
329
|
+
const dy = p.y - cy;
|
|
330
|
+
const r = Math.hypot(dx, dy);
|
|
331
|
+
if (r < innerRadius - 1e-6 || r > outerR + 1e-6) return null;
|
|
332
|
+
let theta = Math.atan2(dy, dx);
|
|
333
|
+
// Walk theta into the [startAngle, endAngle] half-open range (handling
|
|
334
|
+
// both 2π and partial-arc cases). For full circle this always succeeds.
|
|
335
|
+
const span = arcSpan();
|
|
336
|
+
const dirSpan = direction * span;
|
|
337
|
+
// Normalize offset from startAngle along the configured direction.
|
|
338
|
+
let off = (theta - startAngle) * direction;
|
|
339
|
+
// Bring into [0, 2π).
|
|
340
|
+
off = ((off % TWO_PI) + TWO_PI) % TWO_PI;
|
|
341
|
+
if (off > Math.abs(span) + 1e-6 && Math.abs(span) < TWO_PI - 1e-9) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const tAngle = Math.abs(span) > 0 ? off / Math.abs(span) : 0;
|
|
345
|
+
const tRadial = outerR > innerRadius ? (r - innerRadius) / (outerR - innerRadius) : 0;
|
|
346
|
+
// Re-derive plot-frame pixel coords inverse to `project`.
|
|
347
|
+
const angleVal = tAngle * (angleChannel === "x" ? plotW : plotH);
|
|
348
|
+
const radialVal = tRadial * (angleChannel === "x" ? plotH : plotW);
|
|
349
|
+
if (angleChannel === "x") {
|
|
350
|
+
return { x: angleVal, y: plotH - radialVal };
|
|
351
|
+
}
|
|
352
|
+
return { x: radialVal, y: plotH - angleVal };
|
|
353
|
+
// Note: `dirSpan` reserved for future asymmetric direction handling.
|
|
354
|
+
void dirSpan;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function bindFrame(plotFrame: import("insomni").Frame): void {
|
|
358
|
+
plotW = plotFrame.width;
|
|
359
|
+
plotH = plotFrame.height;
|
|
360
|
+
cx = plotFrame.width / 2;
|
|
361
|
+
cy = plotFrame.height / 2;
|
|
362
|
+
outerR = explicitOuter ?? Math.max(0, Math.min(plotFrame.width, plotFrame.height) / 2);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderAxes(args: CoordAxesArgs): void {
|
|
366
|
+
bindFrame(args.plotFrame);
|
|
367
|
+
const origin = args.plotFrame.topLeft;
|
|
368
|
+
const axisLayer = args.axisLayer;
|
|
369
|
+
|
|
370
|
+
const angleScale = (angleChannel === "x" ? args.scales.x : args.scales.y) as
|
|
371
|
+
| PositionScale
|
|
372
|
+
| undefined;
|
|
373
|
+
const radiusScale = (angleChannel === "x" ? args.scales.y : args.scales.x) as
|
|
374
|
+
| PositionScale
|
|
375
|
+
| undefined;
|
|
376
|
+
const hasAngle = (angleChannel === "x" ? args.hasX : args.hasY) && !!angleScale;
|
|
377
|
+
const hasRadius = (angleChannel === "x" ? args.hasY : args.hasX) && !!radiusScale;
|
|
378
|
+
|
|
379
|
+
const angleOptions = angleChannel === "x" ? args.xAxisOptions : args.yAxisOptions;
|
|
380
|
+
const radiusOptions = angleChannel === "x" ? args.yAxisOptions : args.xAxisOptions;
|
|
381
|
+
|
|
382
|
+
const axisColor = angleOptions.tickColor ?? rgba(0.4, 0.4, 0.4, 1);
|
|
383
|
+
const gridColor =
|
|
384
|
+
(angleOptions.gridLines === false ? undefined : angleOptions.gridColor) ??
|
|
385
|
+
rgba(0.85, 0.85, 0.85, 1);
|
|
386
|
+
const axisWidth = angleOptions.axisLineWidth ?? 1;
|
|
387
|
+
const gridWidth = angleOptions.gridWidth ?? 1;
|
|
388
|
+
|
|
389
|
+
// ---- Radial axis (concentric circles) ----
|
|
390
|
+
if (hasRadius && radiusScale) {
|
|
391
|
+
const ticks = scaleTickValues(radiusScale.axisScale, radiusOptions);
|
|
392
|
+
for (const tick of ticks) {
|
|
393
|
+
const tPx = radiusScale.fn(tick);
|
|
394
|
+
if (!Number.isFinite(tPx)) continue;
|
|
395
|
+
const r = radiusAt(
|
|
396
|
+
angleChannel === "x" ? plotH - tPx : tPx,
|
|
397
|
+
angleChannel === "x" ? plotH : plotW,
|
|
398
|
+
);
|
|
399
|
+
if (r <= 0) continue;
|
|
400
|
+
drawArc(axisLayer, origin, cx, cy, r, startAngle, endAngle, quality, gridColor, gridWidth);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---- Angular axis (spokes from center to outerR) ----
|
|
405
|
+
if (hasAngle && angleScale) {
|
|
406
|
+
const ticks = scaleTickValues(angleScale.axisScale, angleOptions);
|
|
407
|
+
for (const tick of ticks) {
|
|
408
|
+
const tPx = angleScale.fn(tick);
|
|
409
|
+
if (!Number.isFinite(tPx)) continue;
|
|
410
|
+
const theta = angleAt(
|
|
411
|
+
angleChannel === "x" ? tPx : plotH - tPx,
|
|
412
|
+
angleChannel === "x" ? plotW : plotH,
|
|
413
|
+
);
|
|
414
|
+
const r0 = innerRadius;
|
|
415
|
+
const r1 = outerR;
|
|
416
|
+
const p0 = projectThetaR(theta, r0);
|
|
417
|
+
const p1 = projectThetaR(theta, r1);
|
|
418
|
+
pushLineAt(axisLayer, origin, p0, p1, axisColor, axisWidth);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function handlePan(args: CoordPanArgs): void {
|
|
424
|
+
const { dx, dy, plotFrame, viewport } = args;
|
|
425
|
+
// Plot-frame absolute centre.
|
|
426
|
+
const absCx = plotFrame.x + plotFrame.width / 2;
|
|
427
|
+
const absCy = plotFrame.y + plotFrame.height / 2;
|
|
428
|
+
// Use the *midpoint* of the drag (after the delta has been applied to the
|
|
429
|
+
// last pointer event) as the reference for tangential vs radial
|
|
430
|
+
// decomposition. The exact incremental delta is small enough that using
|
|
431
|
+
// the centre-to-current vector is a good approximation; bindDataViewport
|
|
432
|
+
// dispatches sub-frame deltas.
|
|
433
|
+
// Vector from centre to current pointer (approximate as start-of-drag
|
|
434
|
+
// pointer since we don't have it — use centre→delta-direction instead).
|
|
435
|
+
// For incremental decomposition we use the pointer's *frame-relative*
|
|
436
|
+
// position implied by combining frame centre with the delta direction.
|
|
437
|
+
// A simpler and well-defined decomposition: rotate by tangential
|
|
438
|
+
// (perpendicular to the radial vector at the *current* drag location,
|
|
439
|
+
// which we recover by treating (dx, dy) as the radial vector's projection
|
|
440
|
+
// onto an "average pointer" at frame centre + 0.5*outerR in +x).
|
|
441
|
+
//
|
|
442
|
+
// For v1 we go with: radial = projection of (dx, dy) onto the unit vector
|
|
443
|
+
// from centre toward the current cursor; tangential = remainder.
|
|
444
|
+
// Because we don't get the cursor position in this signature, fall back
|
|
445
|
+
// to: dx ↦ tangential rotation, dy ↦ radial pan. This matches the common
|
|
446
|
+
// "drag left/right rotates, drag up/down pans radius" mouse convention.
|
|
447
|
+
// A future enhancement can take the full (pointer, delta) pair via a
|
|
448
|
+
// richer `CoordPanArgs`.
|
|
449
|
+
void absCx;
|
|
450
|
+
void absCy;
|
|
451
|
+
const arc = arcSpan();
|
|
452
|
+
if (arc !== 0 && plotFrame.width > 0) {
|
|
453
|
+
const tangential = dx;
|
|
454
|
+
const rotation = direction * (tangential / plotFrame.width) * arc;
|
|
455
|
+
startAngle += rotation;
|
|
456
|
+
endAngle += rotation;
|
|
457
|
+
}
|
|
458
|
+
if (dy !== 0) {
|
|
459
|
+
// Translate the radius scale's domain. The radius scale corresponds to
|
|
460
|
+
// the *non-angle* viewport channel.
|
|
461
|
+
const radialAxis = angleChannel === "x" ? "y" : "x";
|
|
462
|
+
if (radialAxis === "y") viewport.panBy(0, dy);
|
|
463
|
+
else viewport.panBy(dy, 0);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function handleZoom(args: CoordZoomArgs): void {
|
|
468
|
+
const { factor, cx: ax, cy: ay, viewport } = args;
|
|
469
|
+
// Extract the relevant scalar component: bindDataViewport may issue
|
|
470
|
+
// per-axis factors (`{ x, y }`) after masking; collapse to the radial
|
|
471
|
+
// axis since angle scale stays put under zoom.
|
|
472
|
+
const radialAxis = angleChannel === "x" ? "y" : "x";
|
|
473
|
+
const fRadial: number =
|
|
474
|
+
typeof factor === "number" ? factor : radialAxis === "y" ? (factor.y ?? 1) : (factor.x ?? 1);
|
|
475
|
+
if (fRadial === 1) return;
|
|
476
|
+
if (radialAxis === "y") {
|
|
477
|
+
viewport.zoomAt(ax, ay, { y: fRadial });
|
|
478
|
+
} else {
|
|
479
|
+
viewport.zoomAt(ax, ay, { x: fRadial });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const coord: Coord & {
|
|
484
|
+
/** @internal — exposed for unit tests. Returns the current angular configuration. */
|
|
485
|
+
readonly __polar__: {
|
|
486
|
+
readonly startAngle: () => number;
|
|
487
|
+
readonly endAngle: () => number;
|
|
488
|
+
readonly innerRadius: () => number;
|
|
489
|
+
readonly outerRadius: () => number;
|
|
490
|
+
readonly angleChannel: "x" | "y";
|
|
491
|
+
readonly direction: 1 | -1;
|
|
492
|
+
};
|
|
493
|
+
} = {
|
|
494
|
+
kind: "polar",
|
|
495
|
+
bindFrame,
|
|
496
|
+
project,
|
|
497
|
+
segment,
|
|
498
|
+
renderAxes,
|
|
499
|
+
unproject,
|
|
500
|
+
handlePan,
|
|
501
|
+
handleZoom,
|
|
502
|
+
__polar__: {
|
|
503
|
+
startAngle: () => startAngle,
|
|
504
|
+
endAngle: () => endAngle,
|
|
505
|
+
innerRadius: () => innerRadius,
|
|
506
|
+
outerRadius: () => outerR,
|
|
507
|
+
angleChannel,
|
|
508
|
+
direction,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
return coord;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Convenience polar coord: full circle, root at centre. Equivalent to
|
|
516
|
+
* `coordPolar({ openAngle: 0, innerRadius: 0 })`.
|
|
517
|
+
*/
|
|
518
|
+
export function coordRadial(
|
|
519
|
+
opts: Omit<CoordPolarOptions, "openAngle" | "innerRadius"> = {},
|
|
520
|
+
): Coord {
|
|
521
|
+
return coordPolar({ ...opts, openAngle: 0, innerRadius: 0 });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Pin a coord to one plot frame (faceted panels). The facet loop mutates a
|
|
526
|
+
* SINGLE shared stateful coord via `bindFrame(panel.frame)` per panel; that's
|
|
527
|
+
* fine for the synchronous mark-push path (compiled right after the bind), but
|
|
528
|
+
* a geom's `hoverDecoration` closure captures `ctx.coord` and runs LATER (at
|
|
529
|
+
* hover, from the mount) — by then the shared coord is bound to the LAST
|
|
530
|
+
* panel's frame, so a polar halo would project against the wrong centre. This
|
|
531
|
+
* wrapper re-binds the underlying coord to THIS panel's frame before every
|
|
532
|
+
* frame-dependent delegate, so each panel's closures see a panel-stable
|
|
533
|
+
* projection. Cartesian's `bindFrame` is a no-op so this is zero-cost there.
|
|
534
|
+
*/
|
|
535
|
+
export function frameBoundCoord(coord: Coord, frame: import("insomni").Frame): Coord {
|
|
536
|
+
const rebind = <R>(fn: () => R): R => {
|
|
537
|
+
coord.bindFrame(frame);
|
|
538
|
+
return fn();
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
kind: coord.kind,
|
|
542
|
+
bindFrame: () => coord.bindFrame(frame),
|
|
543
|
+
project: (p) => rebind(() => coord.project(p)),
|
|
544
|
+
segment: (p1, p2) => rebind(() => coord.segment(p1, p2)),
|
|
545
|
+
renderAxes: (a) => rebind(() => coord.renderAxes(a)),
|
|
546
|
+
unproject: (p) => rebind(() => coord.unproject(p)),
|
|
547
|
+
handlePan: (a) => coord.handlePan(a),
|
|
548
|
+
handleZoom: (a) => coord.handleZoom(a),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Helpers
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
function scaleTickValues(
|
|
557
|
+
scale: ContinuousScale | TimeScale | BandScale<string>,
|
|
558
|
+
axisOpts?: { ticks?: unknown },
|
|
559
|
+
): readonly unknown[] {
|
|
560
|
+
if ("bandwidth" in scale) {
|
|
561
|
+
return scale.ticks();
|
|
562
|
+
}
|
|
563
|
+
// Use the user-specified tick count when available; fall back to 8.
|
|
564
|
+
const userTicks = axisOpts?.ticks;
|
|
565
|
+
const count = typeof userTicks === "number" ? userTicks : 8;
|
|
566
|
+
return scale.ticks(count);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function pushLineAt(
|
|
570
|
+
layer: Layer,
|
|
571
|
+
origin: { x: number; y: number },
|
|
572
|
+
p0: Point,
|
|
573
|
+
p1: Point,
|
|
574
|
+
color: Color,
|
|
575
|
+
width: number,
|
|
576
|
+
): void {
|
|
577
|
+
layer.pushLine({
|
|
578
|
+
x1: origin.x + p0.x,
|
|
579
|
+
y1: origin.y + p0.y,
|
|
580
|
+
x2: origin.x + p1.x,
|
|
581
|
+
y2: origin.y + p1.y,
|
|
582
|
+
color,
|
|
583
|
+
width,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function drawArc(
|
|
588
|
+
layer: Layer,
|
|
589
|
+
origin: { x: number; y: number },
|
|
590
|
+
cx: number,
|
|
591
|
+
cy: number,
|
|
592
|
+
r: number,
|
|
593
|
+
startAngle: number,
|
|
594
|
+
endAngle: number,
|
|
595
|
+
quality: number,
|
|
596
|
+
color: Color,
|
|
597
|
+
width: number,
|
|
598
|
+
): void {
|
|
599
|
+
const arc = endAngle - startAngle;
|
|
600
|
+
const steps = Math.max(2, Math.ceil(Math.abs(arc) / quality));
|
|
601
|
+
let prevX = cx + r * Math.cos(startAngle);
|
|
602
|
+
let prevY = cy + r * Math.sin(startAngle);
|
|
603
|
+
for (let i = 1; i <= steps; i++) {
|
|
604
|
+
const t = i / steps;
|
|
605
|
+
const theta = startAngle + arc * t;
|
|
606
|
+
const x = cx + r * Math.cos(theta);
|
|
607
|
+
const y = cy + r * Math.sin(theta);
|
|
608
|
+
layer.pushLine({
|
|
609
|
+
x1: origin.x + prevX,
|
|
610
|
+
y1: origin.y + prevY,
|
|
611
|
+
x2: origin.x + x,
|
|
612
|
+
y2: origin.y + y,
|
|
613
|
+
color,
|
|
614
|
+
width,
|
|
615
|
+
});
|
|
616
|
+
prevX = x;
|
|
617
|
+
prevY = y;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface FromMatrixOptions {
|
|
2
|
+
/** Y labels (rows). Top to bottom. Length must equal `values.length`. */
|
|
3
|
+
rows: readonly string[];
|
|
4
|
+
/** X labels (cols). Left to right. Length must equal `values[0].length`. */
|
|
5
|
+
cols: readonly string[];
|
|
6
|
+
/** Output key for the row label. Default `"row"`. */
|
|
7
|
+
rowKey?: string;
|
|
8
|
+
/** Output key for the col label. Default `"col"`. */
|
|
9
|
+
colKey?: string;
|
|
10
|
+
/** Output key for the cell value. Default `"value"`. */
|
|
11
|
+
valueKey?: string;
|
|
12
|
+
}
|
|
13
|
+
export type LongRow<R extends string, C extends string, V extends string> = {
|
|
14
|
+
[K in R | C | V]: K extends V ? number : string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Convert a 2D `values[row][col]` matrix into long-format rows that the
|
|
18
|
+
* `tile()` geom consumes directly. Cells with `NaN` / `null` / `undefined`
|
|
19
|
+
* are kept (so consumers can opt into NA cell rendering); cells whose
|
|
20
|
+
* coordinates are out of range are skipped.
|
|
21
|
+
*
|
|
22
|
+
* Example:
|
|
23
|
+
* ```ts
|
|
24
|
+
* const long = fromMatrix(temperatures, {
|
|
25
|
+
* rows: ["00:00", "06:00", "12:00", "18:00"],
|
|
26
|
+
* cols: ["Mon", "Tue", "Wed", "Thu", "Fri"],
|
|
27
|
+
* });
|
|
28
|
+
* tile({ x: "col", y: "row", fill: "value" })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function fromMatrix(values: readonly (readonly number[])[], opts: FromMatrixOptions): {
|
|
32
|
+
row: string;
|
|
33
|
+
col: string;
|
|
34
|
+
value: number;
|
|
35
|
+
}[];
|
|
36
|
+
export interface PivotLongerOptions<T> {
|
|
37
|
+
/** Output column name for the original key. Default `"name"`. */
|
|
38
|
+
nameKey?: string;
|
|
39
|
+
/** Output column name for the value. Default `"value"`. */
|
|
40
|
+
valueKey?: string;
|
|
41
|
+
/** Optional id columns kept verbatim alongside the long pair. */
|
|
42
|
+
idColumns?: readonly (keyof T & string)[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pivot a wide row to a sequence of long rows — one per `keys` entry.
|
|
46
|
+
* `idColumns` are copied through to every output row so a per-row identifier
|
|
47
|
+
* (e.g. car name in the mtcars example) is preserved.
|
|
48
|
+
*
|
|
49
|
+
* Example:
|
|
50
|
+
* ```ts
|
|
51
|
+
* const long = pivotLonger(mtcars, ["mpg", "cyl", "disp", "hp"], {
|
|
52
|
+
* idColumns: ["model"],
|
|
53
|
+
* });
|
|
54
|
+
* // → [{ model, name: "mpg", value }, { model, name: "cyl", value }, ...]
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function pivotLonger<T extends Record<string, unknown>>(rows: readonly T[], keys: readonly (keyof T & string)[], options?: PivotLongerOptions<T>): Record<string, unknown>[];
|