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,740 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// histogram geom
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Counts samples into bins and draws one rect per bin. Composes with the
|
|
5
|
+
// existing position machinery (`identity` / `stack` / `dodge` / `fill`) so
|
|
6
|
+
// multi-group overlay, stacked, dodged, and 100% stacked histograms all fall
|
|
7
|
+
// out of the same code path. `mirror: true` renders the bars below the
|
|
8
|
+
// baseline so two layers compose into a back-to-back distribution.
|
|
9
|
+
|
|
10
|
+
import type { Color, Layer } from "insomni";
|
|
11
|
+
import { valueLabelMark } from "../../annotations.ts";
|
|
12
|
+
import {
|
|
13
|
+
binBreaks,
|
|
14
|
+
binWithBreaks,
|
|
15
|
+
groupBy,
|
|
16
|
+
histogramMeasureValue,
|
|
17
|
+
type BinClosed,
|
|
18
|
+
type BinResult,
|
|
19
|
+
type BinRule,
|
|
20
|
+
type HistogramMeasure,
|
|
21
|
+
} from "../../stats/index.ts";
|
|
22
|
+
|
|
23
|
+
export type { HistogramMeasure };
|
|
24
|
+
import type { ContinuousScale } from "../../scales.ts";
|
|
25
|
+
import { resolveAes, type Aes, type ResolvedAes } from "../aes.ts";
|
|
26
|
+
import { alphaize, seriesColor } from "../color-utils.ts";
|
|
27
|
+
import { barSwatch } from "../../legend.ts";
|
|
28
|
+
import type {
|
|
29
|
+
CompileContext,
|
|
30
|
+
CompiledHitTest,
|
|
31
|
+
Geom,
|
|
32
|
+
GeomHoverDecorator,
|
|
33
|
+
HoveredHit,
|
|
34
|
+
ResolvedChannelMap,
|
|
35
|
+
ScaleHints,
|
|
36
|
+
} from "./types.ts";
|
|
37
|
+
import { defaultMarkFill, wrapMark } from "./_mark.ts";
|
|
38
|
+
import { emphasisContext } from "./emphasis.ts";
|
|
39
|
+
import { DEFAULT_GROUP_PADDING, synthAes } from "./_categorical.ts";
|
|
40
|
+
|
|
41
|
+
export type HistogramPosition = "identity" | "stack" | "dodge" | "fill";
|
|
42
|
+
|
|
43
|
+
export interface HistogramChannels<T> {
|
|
44
|
+
/** Numeric variable to bin. Provide one of `x` (vertical bars) or `y`. */
|
|
45
|
+
x?: Aes<T, number>;
|
|
46
|
+
/** Numeric variable to bin. Provide one of `x` (vertical bars) or `y`. */
|
|
47
|
+
y?: Aes<T, number>;
|
|
48
|
+
/** Optional categorical group key — splits the sample into per-group bins. */
|
|
49
|
+
color?: Aes<T, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface HistogramOptions {
|
|
53
|
+
/** Explicit bin count. Loses to `binwidth` and `breaks`. */
|
|
54
|
+
bins?: number;
|
|
55
|
+
/** Explicit bin width in data units. Beats `bins` and `rule`. */
|
|
56
|
+
binwidth?: number;
|
|
57
|
+
/** Fully explicit edge array. Beats every other bin selector. */
|
|
58
|
+
breaks?: readonly number[];
|
|
59
|
+
/**
|
|
60
|
+
* Auto-bin rule when none of `bins`, `binwidth`, `breaks` is set.
|
|
61
|
+
*
|
|
62
|
+
* - `"sturges"` (default) — R / ggplot default; works for ~normal data.
|
|
63
|
+
* - `"rice"` — slightly more bins.
|
|
64
|
+
* - `"scott"` — uses σ; good for normal-ish data.
|
|
65
|
+
* - `"fd"` — Freedman–Diaconis; robust to outliers.
|
|
66
|
+
*/
|
|
67
|
+
rule?: BinRule;
|
|
68
|
+
/** Clip / extend the value-axis range used when computing edges. */
|
|
69
|
+
domain?: readonly [number, number];
|
|
70
|
+
/** Round outer edges + step to nice numbers. Default `true`. */
|
|
71
|
+
nice?: boolean;
|
|
72
|
+
/** Edge convention. Default `"left"` (`[x0, x1)` plus closed last bin). */
|
|
73
|
+
closed?: BinClosed;
|
|
74
|
+
/**
|
|
75
|
+
* Y measure. Default `"count"`.
|
|
76
|
+
*
|
|
77
|
+
* - `"count"` / `"frequency"` — raw bin count.
|
|
78
|
+
* - `"density"` — `count / (n · width)`. Integrates to 1 across bins.
|
|
79
|
+
* - `"proportion"` — `count / n`. Sums to 1 across bins (per group).
|
|
80
|
+
*/
|
|
81
|
+
y?: HistogramMeasure;
|
|
82
|
+
/**
|
|
83
|
+
* Multi-group layout. Default `"stack"` when `color` channel is present,
|
|
84
|
+
* else `"identity"`.
|
|
85
|
+
*
|
|
86
|
+
* - `"identity"` — bars share a baseline of 0; useful with reduced
|
|
87
|
+
* `fillAlpha` for an overlay effect.
|
|
88
|
+
* - `"stack"` — per-bin counts stacked.
|
|
89
|
+
* - `"dodge"` — sub-divide bin width across groups, side by side.
|
|
90
|
+
* - `"fill"` — stack normalized to `[0, 1]` per bin.
|
|
91
|
+
*/
|
|
92
|
+
position?: HistogramPosition;
|
|
93
|
+
/** Render bars below the baseline (negate the count axis). Default `false`. */
|
|
94
|
+
mirror?: boolean;
|
|
95
|
+
/** Override theme `fillAlpha`. Lower this for `position: "identity"` overlays. */
|
|
96
|
+
fillAlpha?: number;
|
|
97
|
+
fill?: Color;
|
|
98
|
+
stroke?: Color;
|
|
99
|
+
strokeWidth?: number;
|
|
100
|
+
cornerRadius?: number;
|
|
101
|
+
/** Pixel gap between adjacent bars. Default `0` (continuous wall). */
|
|
102
|
+
gap?: number;
|
|
103
|
+
/** Inner-band padding for `position: "dodge"` (fraction of bin width). Default `0.05`. */
|
|
104
|
+
groupPadding?: number;
|
|
105
|
+
/** Optional per-bar label (uses bin midpoint + value). */
|
|
106
|
+
showCounts?: (value: number, bin: BinResult, key?: string) => string;
|
|
107
|
+
labelColor?: Color;
|
|
108
|
+
labelFontSize?: number;
|
|
109
|
+
/** Used by the auto-legend. */
|
|
110
|
+
label?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface PerGroup {
|
|
114
|
+
key: string;
|
|
115
|
+
bins: BinResult[];
|
|
116
|
+
/** Sample size (after filtering non-finite + domain). */
|
|
117
|
+
n: number;
|
|
118
|
+
fill: Color;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ResolvedLayout {
|
|
122
|
+
orientation: "x" | "y";
|
|
123
|
+
breaks: number[];
|
|
124
|
+
groups: PerGroup[];
|
|
125
|
+
position: HistogramPosition;
|
|
126
|
+
measure: HistogramMeasure;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function histogram<T>(
|
|
130
|
+
channels: HistogramChannels<T>,
|
|
131
|
+
options: HistogramOptions = {},
|
|
132
|
+
): Geom<T> {
|
|
133
|
+
if (channels.x === undefined && channels.y === undefined) {
|
|
134
|
+
throw new Error("histogram(): one of `x` or `y` must be provided");
|
|
135
|
+
}
|
|
136
|
+
if (channels.x !== undefined && channels.y !== undefined) {
|
|
137
|
+
throw new Error("histogram(): provide either `x` or `y`, not both");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const orientation: "x" | "y" = channels.x !== undefined ? "x" : "y";
|
|
141
|
+
const valueAesRaw = (orientation === "x" ? channels.x : channels.y) as Aes<T, number>;
|
|
142
|
+
|
|
143
|
+
// Synthesise the count-axis channel as a constant-0 accessor. The pipeline
|
|
144
|
+
// builds a numeric scale on the back of this; `prepareDomain` then overrides
|
|
145
|
+
// the domain with the actual count extent.
|
|
146
|
+
const synthesisedCount: Aes<T, number> = 0 as unknown as Aes<T, number>;
|
|
147
|
+
const declaredChannels = {
|
|
148
|
+
x: orientation === "x" ? channels.x : synthesisedCount,
|
|
149
|
+
y: orientation === "y" ? channels.y : synthesisedCount,
|
|
150
|
+
color: channels.color,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const measure: HistogramMeasure = options.y ?? "count";
|
|
154
|
+
const positionDefault: HistogramPosition = channels.color !== undefined ? "stack" : "identity";
|
|
155
|
+
const position: HistogramPosition = options.position ?? positionDefault;
|
|
156
|
+
const mirror = options.mirror ?? false;
|
|
157
|
+
|
|
158
|
+
function buildLayout(data: readonly T[]): ResolvedLayout | null {
|
|
159
|
+
const valueAes = resolveAes<T, number>(valueAesRaw);
|
|
160
|
+
const colorAes: ResolvedAes<T, unknown> | undefined = channels.color
|
|
161
|
+
? resolveAes<T, unknown>(channels.color)
|
|
162
|
+
: undefined;
|
|
163
|
+
|
|
164
|
+
// Null policy (see aes.ts): rows whose categorical color channel is
|
|
165
|
+
// null/undefined are dropped — they do not form a `"null"` bin group.
|
|
166
|
+
// Numeric values are kept; downstream `binBreaks` already tolerates NaN.
|
|
167
|
+
const allValues: number[] = [];
|
|
168
|
+
const allKeys: string[] | undefined = colorAes ? [] : undefined;
|
|
169
|
+
for (let i = 0; i < data.length; i++) {
|
|
170
|
+
const datum = data[i]!;
|
|
171
|
+
if (colorAes) {
|
|
172
|
+
const k = colorAes.fn(datum, i);
|
|
173
|
+
if (k === null || k === undefined) continue;
|
|
174
|
+
// oxlint-disable-next-line no-base-to-string -- color key is a scalar (string/number); String() is safe here
|
|
175
|
+
allKeys!.push(String(k));
|
|
176
|
+
}
|
|
177
|
+
allValues.push(valueAes.fn(datum, i));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const breaks = binBreaks(allValues, {
|
|
181
|
+
bins: options.bins,
|
|
182
|
+
binwidth: options.binwidth,
|
|
183
|
+
breaks: options.breaks,
|
|
184
|
+
rule: options.rule,
|
|
185
|
+
domain: options.domain,
|
|
186
|
+
nice: options.nice,
|
|
187
|
+
});
|
|
188
|
+
if (breaks.length < 2) return null;
|
|
189
|
+
|
|
190
|
+
// Group order: first-occurrence in input. Single-group fast-path uses one
|
|
191
|
+
// bucket keyed by the empty string.
|
|
192
|
+
const groupValues = allKeys
|
|
193
|
+
? groupBy(allValues, (_v, i) => allKeys[i]!)
|
|
194
|
+
: new Map<string, number[]>([["", allValues]]);
|
|
195
|
+
const groupOrder = [...groupValues.keys()];
|
|
196
|
+
|
|
197
|
+
const closed = options.closed ?? "left";
|
|
198
|
+
const groups: PerGroup[] = [];
|
|
199
|
+
for (const key of groupOrder) {
|
|
200
|
+
const values = groupValues.get(key)!;
|
|
201
|
+
const bins = binWithBreaks(values, breaks, closed);
|
|
202
|
+
let n = 0;
|
|
203
|
+
for (const b of bins) n += b.count;
|
|
204
|
+
groups.push({ key, bins, n, fill: { r: 0, g: 0, b: 0, a: 1 } });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
orientation,
|
|
209
|
+
breaks,
|
|
210
|
+
groups,
|
|
211
|
+
position,
|
|
212
|
+
measure,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const measureValue = (b: BinResult, n: number): number => histogramMeasureValue(b, n, measure);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Per-bin position offsets keyed by group index. Returns parallel arrays
|
|
220
|
+
* `[base, top]` per group per bin so the renderer doesn't recompute them.
|
|
221
|
+
*/
|
|
222
|
+
function computeStacks(layout: ResolvedLayout): { base: number; top: number }[][] {
|
|
223
|
+
const k = layout.breaks.length - 1;
|
|
224
|
+
const groupCount = layout.groups.length;
|
|
225
|
+
const out: { base: number; top: number }[][] = layout.groups.map(() =>
|
|
226
|
+
Array.from({ length: k }),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (layout.position === "identity") {
|
|
230
|
+
for (let g = 0; g < groupCount; g++) {
|
|
231
|
+
const group = layout.groups[g]!;
|
|
232
|
+
for (let i = 0; i < k; i++) {
|
|
233
|
+
const v = measureValue(group.bins[i]!, group.n);
|
|
234
|
+
out[g]![i] = { base: 0, top: v };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (layout.position === "stack" || layout.position === "fill") {
|
|
241
|
+
for (let i = 0; i < k; i++) {
|
|
242
|
+
let acc = 0;
|
|
243
|
+
let total = 0;
|
|
244
|
+
if (layout.position === "fill") {
|
|
245
|
+
for (let g = 0; g < groupCount; g++) {
|
|
246
|
+
total += measureValue(layout.groups[g]!.bins[i]!, layout.groups[g]!.n);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (let g = 0; g < groupCount; g++) {
|
|
250
|
+
const v = measureValue(layout.groups[g]!.bins[i]!, layout.groups[g]!.n);
|
|
251
|
+
let base = acc;
|
|
252
|
+
let top = acc + v;
|
|
253
|
+
if (layout.position === "fill") {
|
|
254
|
+
if (total > 0) {
|
|
255
|
+
base = acc / total;
|
|
256
|
+
top = (acc + v) / total;
|
|
257
|
+
} else {
|
|
258
|
+
base = 0;
|
|
259
|
+
top = 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
out[g]![i] = { base, top };
|
|
263
|
+
acc += v;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// dodge: each group draws its own bar within the bin slot — base 0, top v.
|
|
270
|
+
for (let g = 0; g < groupCount; g++) {
|
|
271
|
+
const group = layout.groups[g]!;
|
|
272
|
+
for (let i = 0; i < k; i++) {
|
|
273
|
+
const v = measureValue(group.bins[i]!, group.n);
|
|
274
|
+
out[g]![i] = { base: 0, top: v };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function maxStackTop(stacks: { base: number; top: number }[][]): number {
|
|
281
|
+
let max = 0;
|
|
282
|
+
for (const group of stacks) {
|
|
283
|
+
for (const seg of group) {
|
|
284
|
+
if (seg.top > max) max = seg.top;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return max;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
kind: "histogram",
|
|
292
|
+
channels: declaredChannels,
|
|
293
|
+
label: options.label,
|
|
294
|
+
scaleHints: {
|
|
295
|
+
[orientation === "x" ? "y" : "x"]: { includeZero: true },
|
|
296
|
+
} as ScaleHints,
|
|
297
|
+
legendSwatch: (color) => barSwatch({ fill: color, size: 12 }),
|
|
298
|
+
prepareDomain(data) {
|
|
299
|
+
const layout = buildLayout(data);
|
|
300
|
+
if (!layout) return undefined;
|
|
301
|
+
const stacks = computeStacks(layout);
|
|
302
|
+
const max = maxStackTop(stacks);
|
|
303
|
+
const valueExtend: [number, number] = [
|
|
304
|
+
layout.breaks[0]!,
|
|
305
|
+
layout.breaks[layout.breaks.length - 1]!,
|
|
306
|
+
];
|
|
307
|
+
// The count axis is synthesised (constant 0 per row) so numericExtent
|
|
308
|
+
// yields a useless `[-1, 1]`. Pin it explicitly via `domain` (which
|
|
309
|
+
// takes precedence over `extend`/`includeZero` in `applyHint`). The
|
|
310
|
+
// value axis only needs to extend to bin edges, so we use `extend` —
|
|
311
|
+
// unions cleanly with sibling layers via `mergeHint`.
|
|
312
|
+
const countLo = mirror ? -max : 0;
|
|
313
|
+
const countHi = mirror ? 0 : max;
|
|
314
|
+
const countDomain: [number, number] = countLo === countHi ? [0, 1] : [countLo, countHi];
|
|
315
|
+
const valueHint = { extend: valueExtend };
|
|
316
|
+
const countHint = { domain: countDomain };
|
|
317
|
+
if (layout.orientation === "x") {
|
|
318
|
+
return { x: valueHint, y: countHint };
|
|
319
|
+
}
|
|
320
|
+
return { x: countHint, y: valueHint };
|
|
321
|
+
},
|
|
322
|
+
compile(ctx: CompileContext<T>) {
|
|
323
|
+
const { data, scales, plot, theme, atlas } = ctx;
|
|
324
|
+
const layout = buildLayout(data);
|
|
325
|
+
if (!layout) return [];
|
|
326
|
+
|
|
327
|
+
const stacks = computeStacks(layout);
|
|
328
|
+
|
|
329
|
+
// Resolve fills.
|
|
330
|
+
const fillAlpha = options.fillAlpha ?? theme.marks.fillAlpha;
|
|
331
|
+
const baseFill: Color = options.fill ?? defaultMarkFill(theme);
|
|
332
|
+
if (channels.color) {
|
|
333
|
+
const keys = layout.groups.map((g) => g.key);
|
|
334
|
+
const fillFor = seriesColor(scales.color, theme.palettes.categorical, keys);
|
|
335
|
+
for (const g of layout.groups) g.fill = alphaize(fillFor(g.key), fillAlpha);
|
|
336
|
+
} else {
|
|
337
|
+
const fill = alphaize(baseFill, fillAlpha);
|
|
338
|
+
for (const g of layout.groups) g.fill = fill;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const stroke = options.stroke;
|
|
342
|
+
const strokeWidth = options.strokeWidth;
|
|
343
|
+
const cornerRadius = options.cornerRadius ?? theme.marks.barCornerRadius;
|
|
344
|
+
const gap = options.gap ?? 0;
|
|
345
|
+
const groupPadding = options.groupPadding ?? DEFAULT_GROUP_PADDING;
|
|
346
|
+
|
|
347
|
+
const valueAxis = (
|
|
348
|
+
layout.orientation === "x" ? scales.x.axisScale : scales.y.axisScale
|
|
349
|
+
) as ContinuousScale;
|
|
350
|
+
const countAxis = (
|
|
351
|
+
layout.orientation === "x" ? scales.y.axisScale : scales.x.axisScale
|
|
352
|
+
) as ContinuousScale;
|
|
353
|
+
|
|
354
|
+
const ox = plot.topLeft.x;
|
|
355
|
+
const oy = plot.topLeft.y;
|
|
356
|
+
const groupCount = layout.groups.length;
|
|
357
|
+
|
|
358
|
+
// Hover emphasis — the dim-others treatment rides the core's GPU emphasis
|
|
359
|
+
// uniform (P5-T3): tag each cell with a stable per-cell key (the flat cell
|
|
360
|
+
// counter that compileHitTest reports as `dataIndex`); the mount fades
|
|
361
|
+
// non-focused cells with no marks recompile. No compile-time color dim /
|
|
362
|
+
// halo. `dataIndex` in compileHitTest is the flat counter `n` over the same
|
|
363
|
+
// i × g iteration order (skipping empty cells), so we replicate it here.
|
|
364
|
+
const emph = emphasisContext(ctx, "histogram");
|
|
365
|
+
|
|
366
|
+
const builders: { length: number; addTo: (l: Layer) => Layer }[] = [];
|
|
367
|
+
// Note: first builder uses a custom addTo that paints rects directly,
|
|
368
|
+
// so it can't reuse `wrapMark`. Label builder below does.
|
|
369
|
+
|
|
370
|
+
builders.push({
|
|
371
|
+
length: layout.breaks.length - 1,
|
|
372
|
+
addTo(layer: Layer) {
|
|
373
|
+
let cellN = 0; // mirrors compileHitTest's `n` counter
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < layout.breaks.length - 1; i++) {
|
|
376
|
+
const x0Px = valueAxis(layout.breaks[i]!);
|
|
377
|
+
const x1Px = valueAxis(layout.breaks[i + 1]!);
|
|
378
|
+
const binLo = Math.min(x0Px, x1Px);
|
|
379
|
+
const binHi = Math.max(x0Px, x1Px);
|
|
380
|
+
const binSpan = binHi - binLo;
|
|
381
|
+
for (let g = 0; g < groupCount; g++) {
|
|
382
|
+
const group = layout.groups[g]!;
|
|
383
|
+
const seg = stacks[g]![i]!;
|
|
384
|
+
if (seg.top === seg.base) continue;
|
|
385
|
+
const baseV = mirror ? -seg.base : seg.base;
|
|
386
|
+
const topV = mirror ? -seg.top : seg.top;
|
|
387
|
+
const basePx = countAxis(baseV);
|
|
388
|
+
const topPx = countAxis(topV);
|
|
389
|
+
const lo = Math.min(basePx, topPx);
|
|
390
|
+
const hi = Math.max(basePx, topPx);
|
|
391
|
+
|
|
392
|
+
let cellLo = binLo;
|
|
393
|
+
let cellSpan = binSpan;
|
|
394
|
+
if (layout.position === "dodge" && groupCount > 1) {
|
|
395
|
+
const padPx = binSpan * groupPadding;
|
|
396
|
+
const inner = (binSpan - padPx) / groupCount;
|
|
397
|
+
cellLo = binLo + padPx / 2 + g * inner;
|
|
398
|
+
cellSpan = inner;
|
|
399
|
+
}
|
|
400
|
+
const halfGap = Math.min(gap / 2, Math.max(0, cellSpan / 2 - 0.5));
|
|
401
|
+
const x = cellLo + halfGap;
|
|
402
|
+
const w = Math.max(0, cellSpan - halfGap * 2);
|
|
403
|
+
|
|
404
|
+
// Per-cell emphasis key — keyed by the flat cell counter `cellN`,
|
|
405
|
+
// which is exactly the `dataIndex` compileHitTest reports.
|
|
406
|
+
const emphasisKey = emph?.keyFor(cellN);
|
|
407
|
+
|
|
408
|
+
if (layout.orientation === "x") {
|
|
409
|
+
layer.pushRect({
|
|
410
|
+
x: ox + x,
|
|
411
|
+
y: oy + lo,
|
|
412
|
+
width: w,
|
|
413
|
+
height: Math.max(0, hi - lo),
|
|
414
|
+
fill: group.fill,
|
|
415
|
+
stroke,
|
|
416
|
+
strokeWidth,
|
|
417
|
+
cornerRadius,
|
|
418
|
+
emphasisKey,
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
layer.pushRect({
|
|
422
|
+
x: ox + lo,
|
|
423
|
+
y: oy + x,
|
|
424
|
+
width: Math.max(0, hi - lo),
|
|
425
|
+
height: w,
|
|
426
|
+
fill: group.fill,
|
|
427
|
+
stroke,
|
|
428
|
+
strokeWidth,
|
|
429
|
+
cornerRadius,
|
|
430
|
+
emphasisKey,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
cellN++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return layer;
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Optional value labels above each bar.
|
|
442
|
+
if (atlas && options.showCounts) {
|
|
443
|
+
const fmt = options.showCounts;
|
|
444
|
+
const labelColor: Color = options.labelColor ?? theme.text.color;
|
|
445
|
+
const labelFontSize = options.labelFontSize ?? theme.marks.labelFontSize;
|
|
446
|
+
|
|
447
|
+
type LabelEntry = {
|
|
448
|
+
binIndex: number;
|
|
449
|
+
groupIndex: number;
|
|
450
|
+
bin: BinResult;
|
|
451
|
+
group: PerGroup;
|
|
452
|
+
seg: { base: number; top: number };
|
|
453
|
+
};
|
|
454
|
+
const entries: LabelEntry[] = [];
|
|
455
|
+
for (let i = 0; i < layout.breaks.length - 1; i++) {
|
|
456
|
+
for (let g = 0; g < groupCount; g++) {
|
|
457
|
+
const group = layout.groups[g]!;
|
|
458
|
+
const seg = stacks[g]![i]!;
|
|
459
|
+
if (seg.top === seg.base) continue;
|
|
460
|
+
entries.push({
|
|
461
|
+
binIndex: i,
|
|
462
|
+
groupIndex: g,
|
|
463
|
+
bin: group.bins[i]!,
|
|
464
|
+
group,
|
|
465
|
+
seg,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const orientationX = layout.orientation === "x";
|
|
471
|
+
const labelMark = valueLabelMark(entries, {
|
|
472
|
+
x: (e) => {
|
|
473
|
+
const x0Px = valueAxis(layout.breaks[e.binIndex]!);
|
|
474
|
+
const x1Px = valueAxis(layout.breaks[e.binIndex + 1]!);
|
|
475
|
+
const lo = Math.min(x0Px, x1Px);
|
|
476
|
+
const hi = Math.max(x0Px, x1Px);
|
|
477
|
+
const span = hi - lo;
|
|
478
|
+
if (orientationX) {
|
|
479
|
+
if (layout.position === "dodge" && groupCount > 1) {
|
|
480
|
+
const inner = (span * (1 - groupPadding)) / groupCount;
|
|
481
|
+
return lo + span * (groupPadding / 2) + e.groupIndex * inner + inner / 2;
|
|
482
|
+
}
|
|
483
|
+
return (lo + hi) / 2;
|
|
484
|
+
}
|
|
485
|
+
// horizontal: x is the count axis
|
|
486
|
+
const topV = mirror ? -e.seg.top : e.seg.top;
|
|
487
|
+
return countAxis(topV);
|
|
488
|
+
},
|
|
489
|
+
y: (e) => {
|
|
490
|
+
if (orientationX) {
|
|
491
|
+
const topV = mirror ? -e.seg.top : e.seg.top;
|
|
492
|
+
return countAxis(topV);
|
|
493
|
+
}
|
|
494
|
+
const x0Px = valueAxis(layout.breaks[e.binIndex]!);
|
|
495
|
+
const x1Px = valueAxis(layout.breaks[e.binIndex + 1]!);
|
|
496
|
+
const lo = Math.min(x0Px, x1Px);
|
|
497
|
+
const hi = Math.max(x0Px, x1Px);
|
|
498
|
+
const span = hi - lo;
|
|
499
|
+
if (layout.position === "dodge" && groupCount > 1) {
|
|
500
|
+
const inner = (span * (1 - groupPadding)) / groupCount;
|
|
501
|
+
return lo + span * (groupPadding / 2) + e.groupIndex * inner + inner / 2;
|
|
502
|
+
}
|
|
503
|
+
return (lo + hi) / 2;
|
|
504
|
+
},
|
|
505
|
+
text: (e) =>
|
|
506
|
+
fmt(
|
|
507
|
+
measureValue(e.bin, e.group.n),
|
|
508
|
+
e.bin,
|
|
509
|
+
e.group.key === "" ? undefined : e.group.key,
|
|
510
|
+
),
|
|
511
|
+
color: labelColor,
|
|
512
|
+
fontSize: labelFontSize,
|
|
513
|
+
align: orientationX ? "center" : "left",
|
|
514
|
+
offset: orientationX
|
|
515
|
+
? { x: 0, y: mirror ? labelFontSize + 2 : -6 }
|
|
516
|
+
: { x: mirror ? -6 : 6, y: 0 },
|
|
517
|
+
});
|
|
518
|
+
builders.push(wrapMark(labelMark, plot.topLeft, entries.length));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return builders;
|
|
522
|
+
},
|
|
523
|
+
emphasisResolution(ctx) {
|
|
524
|
+
return emphasisContext(ctx, "histogram")?.resolver() ?? null;
|
|
525
|
+
},
|
|
526
|
+
hoverDecoration(ctx: CompileContext<T>): GeomHoverDecorator | null {
|
|
527
|
+
// Focus halo on the hovered bin cell — recovered from the old snap path's
|
|
528
|
+
// compile-time outline (deleted in e6d5643). Now an OVERLAY decorator
|
|
529
|
+
// replayed into the live overlay layer (NO marks recompile). Overlay shapes
|
|
530
|
+
// leave emphasisKey 0 ⇒ EXEMPT ⇒ the halo stays full-strength while the
|
|
531
|
+
// other cells dim via the GPU uniform. The hit's `dataIndex` is the flat
|
|
532
|
+
// cell counter `cellN`, so we replicate the same i×g iteration order
|
|
533
|
+
// (skipping empty cells) to find the hovered cell's rect + group fill.
|
|
534
|
+
const { data, plot } = ctx;
|
|
535
|
+
const hoverCfg = ctx.theme.interactions.hover;
|
|
536
|
+
const layout = buildLayout(data);
|
|
537
|
+
if (!layout) return null;
|
|
538
|
+
|
|
539
|
+
const fillAlpha = options.fillAlpha ?? ctx.theme.marks.fillAlpha;
|
|
540
|
+
const baseFill: Color = options.fill ?? defaultMarkFill(ctx.theme);
|
|
541
|
+
if (ctx.scales.color && layout.groups.length > 0 && options.fill === undefined) {
|
|
542
|
+
const keys = layout.groups.map((g) => g.key);
|
|
543
|
+
const fillFor = seriesColor(ctx.scales.color, ctx.theme.palettes.categorical, keys);
|
|
544
|
+
for (const g of layout.groups) g.fill = alphaize(fillFor(g.key), fillAlpha);
|
|
545
|
+
} else {
|
|
546
|
+
const fill = alphaize(baseFill, fillAlpha);
|
|
547
|
+
for (const g of layout.groups) g.fill = fill;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const stacks = computeStacks(layout);
|
|
551
|
+
const cornerRadius = options.cornerRadius ?? ctx.theme.marks.barCornerRadius;
|
|
552
|
+
const gap = options.gap ?? 0;
|
|
553
|
+
const groupPadding = options.groupPadding ?? DEFAULT_GROUP_PADDING;
|
|
554
|
+
const valueAxis = (
|
|
555
|
+
layout.orientation === "x" ? ctx.scales.x.axisScale : ctx.scales.y.axisScale
|
|
556
|
+
) as ContinuousScale;
|
|
557
|
+
const countAxis = (
|
|
558
|
+
layout.orientation === "x" ? ctx.scales.y.axisScale : ctx.scales.x.axisScale
|
|
559
|
+
) as ContinuousScale;
|
|
560
|
+
const ox = plot.topLeft.x;
|
|
561
|
+
const oy = plot.topLeft.y;
|
|
562
|
+
const groupCount = layout.groups.length;
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
geomKind: "histogram",
|
|
566
|
+
data,
|
|
567
|
+
decorate(hit: HoveredHit, layer: Layer): void {
|
|
568
|
+
if (!hoverCfg.enabled || hit.data !== data) return;
|
|
569
|
+
let cellN = 0;
|
|
570
|
+
for (let i = 0; i < layout.breaks.length - 1; i++) {
|
|
571
|
+
const x0Px = valueAxis(layout.breaks[i]!);
|
|
572
|
+
const x1Px = valueAxis(layout.breaks[i + 1]!);
|
|
573
|
+
const binLo = Math.min(x0Px, x1Px);
|
|
574
|
+
const binHi = Math.max(x0Px, x1Px);
|
|
575
|
+
const binSpan = binHi - binLo;
|
|
576
|
+
for (let g = 0; g < groupCount; g++) {
|
|
577
|
+
const group = layout.groups[g]!;
|
|
578
|
+
const seg = stacks[g]![i]!;
|
|
579
|
+
if (seg.top === seg.base) continue;
|
|
580
|
+
if (cellN === hit.dataIndex) {
|
|
581
|
+
const baseV = mirror ? -seg.base : seg.base;
|
|
582
|
+
const topV = mirror ? -seg.top : seg.top;
|
|
583
|
+
const basePx = countAxis(baseV);
|
|
584
|
+
const topPx = countAxis(topV);
|
|
585
|
+
const lo = Math.min(basePx, topPx);
|
|
586
|
+
const hi = Math.max(basePx, topPx);
|
|
587
|
+
let cellLo = binLo;
|
|
588
|
+
let cellSpan = binSpan;
|
|
589
|
+
if (layout.position === "dodge" && groupCount > 1) {
|
|
590
|
+
const padPx = binSpan * groupPadding;
|
|
591
|
+
const inner = (binSpan - padPx) / groupCount;
|
|
592
|
+
cellLo = binLo + padPx / 2 + g * inner;
|
|
593
|
+
cellSpan = inner;
|
|
594
|
+
}
|
|
595
|
+
const halfGap = Math.min(gap / 2, Math.max(0, cellSpan / 2 - 0.5));
|
|
596
|
+
const x = cellLo + halfGap;
|
|
597
|
+
const w = Math.max(0, cellSpan - halfGap * 2);
|
|
598
|
+
const ringColor: Color = hoverCfg.haloColor ?? { ...group.fill, a: 1 };
|
|
599
|
+
if (layout.orientation === "x") {
|
|
600
|
+
layer.pushRect({
|
|
601
|
+
x: ox + x,
|
|
602
|
+
y: oy + lo,
|
|
603
|
+
width: w,
|
|
604
|
+
height: Math.max(0, hi - lo),
|
|
605
|
+
cornerRadius,
|
|
606
|
+
stroke: ringColor,
|
|
607
|
+
strokeWidth: hoverCfg.haloStrokeWidth,
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
layer.pushRect({
|
|
611
|
+
x: ox + lo,
|
|
612
|
+
y: oy + x,
|
|
613
|
+
width: Math.max(0, hi - lo),
|
|
614
|
+
height: w,
|
|
615
|
+
cornerRadius,
|
|
616
|
+
stroke: ringColor,
|
|
617
|
+
strokeWidth: hoverCfg.haloStrokeWidth,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
cellN++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
|
|
629
|
+
const { data, scales, plot } = ctx;
|
|
630
|
+
const layout = buildLayout(data);
|
|
631
|
+
if (!layout) return null;
|
|
632
|
+
|
|
633
|
+
const stacks = computeStacks(layout);
|
|
634
|
+
const valueAxis = (
|
|
635
|
+
layout.orientation === "x" ? scales.x.axisScale : scales.y.axisScale
|
|
636
|
+
) as ContinuousScale;
|
|
637
|
+
const countAxis = (
|
|
638
|
+
layout.orientation === "x" ? scales.y.axisScale : scales.x.axisScale
|
|
639
|
+
) as ContinuousScale;
|
|
640
|
+
const groupPadding = options.groupPadding ?? DEFAULT_GROUP_PADDING;
|
|
641
|
+
const groupCount = layout.groups.length;
|
|
642
|
+
const k = layout.breaks.length - 1;
|
|
643
|
+
|
|
644
|
+
const ox = plot.topLeft.x;
|
|
645
|
+
const oy = plot.topLeft.y;
|
|
646
|
+
const cap = groupCount * k;
|
|
647
|
+
const positions = new Float32Array(cap * 2);
|
|
648
|
+
const rects = new Float32Array(cap * 4);
|
|
649
|
+
const dataIndex = new Int32Array(cap);
|
|
650
|
+
const seriesKey: (string | undefined)[] = Array.from({ length: cap });
|
|
651
|
+
const binMidpoints: number[] = Array.from({ length: cap });
|
|
652
|
+
const binValues: number[] = Array.from({ length: cap });
|
|
653
|
+
const groupKeys: (string | undefined)[] = Array.from({ length: cap });
|
|
654
|
+
let n = 0;
|
|
655
|
+
let minBinSpan = Infinity;
|
|
656
|
+
for (let i = 0; i < k; i++) {
|
|
657
|
+
const x0Px = valueAxis(layout.breaks[i]!);
|
|
658
|
+
const x1Px = valueAxis(layout.breaks[i + 1]!);
|
|
659
|
+
const binLo = Math.min(x0Px, x1Px);
|
|
660
|
+
const binHi = Math.max(x0Px, x1Px);
|
|
661
|
+
const binSpan = binHi - binLo;
|
|
662
|
+
for (let g = 0; g < groupCount; g++) {
|
|
663
|
+
const seg = stacks[g]![i]!;
|
|
664
|
+
if (seg.top === seg.base) continue;
|
|
665
|
+
const baseV = mirror ? -seg.base : seg.base;
|
|
666
|
+
const topV = mirror ? -seg.top : seg.top;
|
|
667
|
+
const basePx = countAxis(baseV);
|
|
668
|
+
const topPx = countAxis(topV);
|
|
669
|
+
const segLo = Math.min(basePx, topPx);
|
|
670
|
+
const segHi = Math.max(basePx, topPx);
|
|
671
|
+
|
|
672
|
+
let cellLo = binLo;
|
|
673
|
+
let cellSpan = binSpan;
|
|
674
|
+
if (layout.position === "dodge" && groupCount > 1) {
|
|
675
|
+
const padPx = binSpan * groupPadding;
|
|
676
|
+
const inner = (binSpan - padPx) / groupCount;
|
|
677
|
+
cellLo = binLo + padPx / 2 + g * inner;
|
|
678
|
+
cellSpan = inner;
|
|
679
|
+
}
|
|
680
|
+
if (cellSpan < minBinSpan) minBinSpan = cellSpan;
|
|
681
|
+
const cx = cellLo + cellSpan / 2;
|
|
682
|
+
const cy = (segLo + segHi) / 2;
|
|
683
|
+
|
|
684
|
+
if (layout.orientation === "x") {
|
|
685
|
+
positions[n * 2] = ox + cx;
|
|
686
|
+
positions[n * 2 + 1] = oy + cy;
|
|
687
|
+
// Whole-bar footprint for region hit testing.
|
|
688
|
+
rects[n * 4] = ox + cellLo;
|
|
689
|
+
rects[n * 4 + 1] = oy + segLo;
|
|
690
|
+
rects[n * 4 + 2] = cellSpan;
|
|
691
|
+
rects[n * 4 + 3] = segHi - segLo;
|
|
692
|
+
} else {
|
|
693
|
+
positions[n * 2] = ox + cy;
|
|
694
|
+
positions[n * 2 + 1] = oy + cx;
|
|
695
|
+
rects[n * 4] = ox + segLo;
|
|
696
|
+
rects[n * 4 + 1] = oy + cellLo;
|
|
697
|
+
rects[n * 4 + 2] = segHi - segLo;
|
|
698
|
+
rects[n * 4 + 3] = cellSpan;
|
|
699
|
+
}
|
|
700
|
+
const group = layout.groups[g]!;
|
|
701
|
+
const groupKey = group.key === "" ? undefined : group.key;
|
|
702
|
+
seriesKey[n] = groupKey ?? `bin${i}`;
|
|
703
|
+
groupKeys[n] = groupKey;
|
|
704
|
+
binMidpoints[n] = (layout.breaks[i]! + layout.breaks[i + 1]!) / 2;
|
|
705
|
+
binValues[n] = measureValue(group.bins[i]!, group.n);
|
|
706
|
+
dataIndex[n] = n;
|
|
707
|
+
n++;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (n === 0) return null;
|
|
711
|
+
|
|
712
|
+
const hasGroups = channels.color !== undefined;
|
|
713
|
+
const channelsMap: ResolvedChannelMap<T> = {
|
|
714
|
+
x: synthAes(layout.orientation === "x" ? "bin" : measure, (_d, idx) =>
|
|
715
|
+
layout.orientation === "x" ? binMidpoints[idx] : binValues[idx],
|
|
716
|
+
),
|
|
717
|
+
y: synthAes(layout.orientation === "x" ? measure : "bin", (_d, idx) =>
|
|
718
|
+
layout.orientation === "x" ? binValues[idx] : binMidpoints[idx],
|
|
719
|
+
),
|
|
720
|
+
color: hasGroups ? synthAes("group", (_d, idx) => groupKeys[idx] ?? "") : undefined,
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Sized to half the smallest dodged-cell span so the cursor inside a
|
|
724
|
+
// bar's footprint picks it; off-axis still uses Euclidean default.
|
|
725
|
+
const pickRadius = Math.max(Number.isFinite(minBinSpan) ? minBinSpan / 2 : 0, 6);
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
geomKind: "histogram",
|
|
729
|
+
label: options.label,
|
|
730
|
+
positions: positions.subarray(0, n * 2),
|
|
731
|
+
rects: rects.subarray(0, n * 4),
|
|
732
|
+
dataIndex: dataIndex.subarray(0, n),
|
|
733
|
+
seriesKey: seriesKey.slice(0, n),
|
|
734
|
+
pickRadius,
|
|
735
|
+
channels: channelsMap,
|
|
736
|
+
data,
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|