svelteplot 0.14.2-pr-555.10 → 0.14.2-pr-552.2
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/dist/constants.js +1 -1
- package/dist/helpers/hexLattice.d.ts +20 -0
- package/dist/helpers/hexLattice.js +73 -0
- package/dist/marks/Hexbin.svelte +374 -0
- package/dist/marks/Hexbin.svelte.d.ts +71 -0
- package/dist/marks/Hexgrid.svelte +72 -0
- package/dist/marks/Hexgrid.svelte.d.ts +23 -0
- package/dist/marks/index.d.ts +2 -0
- package/dist/marks/index.js +2 -0
- package/dist/transforms/hexbin.d.ts +37 -0
- package/dist/transforms/hexbin.js +81 -0
- package/dist/transforms/index.d.ts +1 -0
- package/dist/transforms/index.js +1 -0
- package/dist/types/mark.d.ts +1 -1
- package/dist/types/plot.d.ts +5 -1
- package/package.json +1 -1
package/dist/constants.js
CHANGED
|
@@ -120,7 +120,7 @@ export const CHANNEL_SCALE = {
|
|
|
120
120
|
strokeOpacity: 'opacity'
|
|
121
121
|
};
|
|
122
122
|
export const CSS_VAR = /^var\(--([a-z-0-9,\s]+)\)$/;
|
|
123
|
-
export const CSS_COLOR = /^
|
|
123
|
+
export const CSS_COLOR = /^color\(/;
|
|
124
124
|
export const CSS_COLOR_MIX = /^color-mix\(/; // just check for prefix
|
|
125
125
|
export const CSS_COLOR_CONTRAST = /^color-contrast\(/; // just check for prefix
|
|
126
126
|
export const CSS_RGBA = /^rgba\(/; // just check for prefix
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const HEX_DEFAULT_BIN_WIDTH = 20;
|
|
2
|
+
export type HexLattice = {
|
|
3
|
+
rx: number;
|
|
4
|
+
ry: number;
|
|
5
|
+
dx: number;
|
|
6
|
+
dy: number;
|
|
7
|
+
originX: number;
|
|
8
|
+
originY: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function hexLattice(binWidth: number, originX?: number, originY?: number): HexLattice;
|
|
11
|
+
export declare function hexLatticeXY(dx: number, dy: number, originX?: number, originY?: number): HexLattice;
|
|
12
|
+
export declare function hexCenter(i: number, j: number, lattice: HexLattice): [number, number];
|
|
13
|
+
export declare function pointToHex(px: number, py: number, lattice: HexLattice): {
|
|
14
|
+
i: number;
|
|
15
|
+
j: number;
|
|
16
|
+
cx: number;
|
|
17
|
+
cy: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function hexagonSubpath(cx: number, cy: number, rx: number, ry: number): string;
|
|
20
|
+
export declare function hexCellsInRect(lattice: HexLattice, x0: number, y0: number, x1: number, y1: number): Generator<[number, number]>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Shared hex lattice math used by the `hexbin` transform and the `Hexgrid`
|
|
2
|
+
// mark so binning and the grid overlay always tile the same lattice. Pointy-
|
|
3
|
+
// topped hexagons; odd rows are offset horizontally by rx. The lattice is
|
|
4
|
+
// unit-agnostic — callers pass pitches in pixels (Hexgrid pixel mode) or in
|
|
5
|
+
// data units (data-space mode).
|
|
6
|
+
const SQRT3 = Math.sqrt(3);
|
|
7
|
+
export const HEX_DEFAULT_BIN_WIDTH = 20;
|
|
8
|
+
// Build a regular pointy-topped hex lattice from a single pitch. dy is the
|
|
9
|
+
// canonical dx*√3/2 row pitch for tiling regular hexagons — used by Hexgrid
|
|
10
|
+
// pixel mode and any caller working in a single coordinate system.
|
|
11
|
+
export function hexLattice(binWidth, originX = 0, originY = 0) {
|
|
12
|
+
const rx = binWidth / 2;
|
|
13
|
+
const ry = (rx * 2) / SQRT3;
|
|
14
|
+
return { rx, ry, dx: binWidth, dy: ry * 1.5, originX, originY };
|
|
15
|
+
}
|
|
16
|
+
// Build a lattice with independent column and row pitches. Use this when the
|
|
17
|
+
// x and y axes have different units (data-space hexbin), so each axis's pitch
|
|
18
|
+
// can be scaled by its own data extent and still tile cleanly.
|
|
19
|
+
export function hexLatticeXY(dx, dy, originX = 0, originY = 0) {
|
|
20
|
+
// ry is the vertical half-bounding-box; row pitch dy = 1.5 * ry, so ry = 2/3 dy.
|
|
21
|
+
return { rx: dx / 2, ry: (dy * 2) / 3, dx, dy, originX, originY };
|
|
22
|
+
}
|
|
23
|
+
export function hexCenter(i, j, lattice) {
|
|
24
|
+
const cx = lattice.originX + (i + (j & 1) / 2) * lattice.dx;
|
|
25
|
+
const cy = lattice.originY + j * lattice.dy;
|
|
26
|
+
return [cx, cy];
|
|
27
|
+
}
|
|
28
|
+
// Snaps a pixel point to the nearest hex cell. Must check the rounded row and
|
|
29
|
+
// both neighbors because the row-offset lattice lets an adjacent row be closer
|
|
30
|
+
// than the rounded one.
|
|
31
|
+
export function pointToHex(px, py, lattice) {
|
|
32
|
+
const { dx, dy, originX, originY } = lattice;
|
|
33
|
+
const rx = dx / 2;
|
|
34
|
+
const pj = Math.round((py - originY) / dy) || 0;
|
|
35
|
+
let best = { i: 0, j: 0, cx: 0, cy: 0, d2: Infinity };
|
|
36
|
+
for (const dj of [0, 1, -1]) {
|
|
37
|
+
const cj = pj + dj;
|
|
38
|
+
const ci = Math.round((px - originX - (cj & 1) * rx) / dx) || 0;
|
|
39
|
+
const cx = originX + (ci + (cj & 1) / 2) * dx;
|
|
40
|
+
const cy = originY + cj * dy;
|
|
41
|
+
const d2 = (px - cx) ** 2 + (py - cy) ** 2;
|
|
42
|
+
if (d2 < best.d2)
|
|
43
|
+
best = { i: ci, j: cj, cx, cy, d2 };
|
|
44
|
+
}
|
|
45
|
+
return { i: best.i, j: best.j, cx: best.cx, cy: best.cy };
|
|
46
|
+
}
|
|
47
|
+
// SVG subpath for a single pointy-topped hex, rounded to compact the output.
|
|
48
|
+
export function hexagonSubpath(cx, cy, rx, ry) {
|
|
49
|
+
const r3 = (n) => Math.round(n * 1000) / 1000;
|
|
50
|
+
return (`M${r3(cx)},${r3(cy - ry)}` +
|
|
51
|
+
`l${r3(rx)},${r3(ry / 2)}` +
|
|
52
|
+
`v${r3(ry)}` +
|
|
53
|
+
`l${r3(-rx)},${r3(ry / 2)}` +
|
|
54
|
+
`l${r3(-rx)},${r3(-ry / 2)}` +
|
|
55
|
+
`v${r3(-ry)}Z`);
|
|
56
|
+
}
|
|
57
|
+
// Iterate hex cell centers covering the given pixel rect. Pads by one cell so
|
|
58
|
+
// a clipPath can trim the edges cleanly without gaps.
|
|
59
|
+
export function* hexCellsInRect(lattice, x0, y0, x1, y1) {
|
|
60
|
+
const { dx, dy, originX, originY, rx, ry } = lattice;
|
|
61
|
+
const jMin = Math.floor((y0 - originY - ry) / dy);
|
|
62
|
+
const jMax = Math.ceil((y1 - originY + ry) / dy);
|
|
63
|
+
for (let j = jMin; j <= jMax; j++) {
|
|
64
|
+
const xOffset = (j & 1) * (dx / 2);
|
|
65
|
+
const iMin = Math.floor((x0 - originX - xOffset - rx) / dx);
|
|
66
|
+
const iMax = Math.ceil((x1 - originX - xOffset + rx) / dx);
|
|
67
|
+
for (let i = iMin; i <= iMax; i++) {
|
|
68
|
+
const cx = originX + (i + (j & 1) / 2) * dx;
|
|
69
|
+
const cy = originY + j * dy;
|
|
70
|
+
yield [cx, cy];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders a hexagonal binning of 2D scatter data: groups raw points into a
|
|
3
|
+
pixel-space hex lattice, runs a reducer per bin, and draws each non-empty
|
|
4
|
+
bin as a regular hexagonal cell. Cells are regular by construction because
|
|
5
|
+
the lattice is computed in pixel space (after scales exist) rather than in
|
|
6
|
+
data space, so they tile correctly under any axis aspect ratio.
|
|
7
|
+
|
|
8
|
+
Pairs naturally with the data-less `<Hexgrid />` mark for an empty-cell
|
|
9
|
+
backdrop — both default to `binWidth=20` (px), so a default `<Hexbin>` and
|
|
10
|
+
a default `<Hexgrid>` align by convention without user coordination.
|
|
11
|
+
|
|
12
|
+
The `fill` prop accepts a reducer name (`'count'`, `'mean'`, etc.) to map
|
|
13
|
+
aggregated values through the plot's color scale, or a CSS color for a
|
|
14
|
+
constant fill. Same shape for `stroke`.
|
|
15
|
+
-->
|
|
16
|
+
<script lang="ts" generics="Datum extends DataRecord">
|
|
17
|
+
interface HexbinMarkProps {
|
|
18
|
+
/** Input data — array of records with x/y positions. */
|
|
19
|
+
data?: Datum[] | null;
|
|
20
|
+
/** x position channel (data space). */
|
|
21
|
+
x?: ChannelAccessor<Datum>;
|
|
22
|
+
/** y position channel (data space). */
|
|
23
|
+
y?: ChannelAccessor<Datum>;
|
|
24
|
+
/** horizontal facet channel — bins are computed independently per facet. */
|
|
25
|
+
fx?: ChannelAccessor<Datum>;
|
|
26
|
+
/** vertical facet channel — bins are computed independently per facet. */
|
|
27
|
+
fy?: ChannelAccessor<Datum>;
|
|
28
|
+
/**
|
|
29
|
+
* Hex cell pitch in pixels (distance between adjacent cell centers
|
|
30
|
+
* along x). Default 20, matching `<Hexgrid />`.
|
|
31
|
+
*/
|
|
32
|
+
binWidth?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Fill: a reducer name (`'count'`, `'mean'`, …) to map aggregated
|
|
35
|
+
* values through the color scale, OR a CSS color for a constant fill.
|
|
36
|
+
* Default `'count'` (maps bin counts through the color scale).
|
|
37
|
+
*/
|
|
38
|
+
fill?: ReducerName | string;
|
|
39
|
+
/** Stroke: same shape as `fill`. Default `'none'`. */
|
|
40
|
+
stroke?: ReducerName | string;
|
|
41
|
+
strokeWidth?: number;
|
|
42
|
+
fillOpacity?: number;
|
|
43
|
+
strokeOpacity?: number;
|
|
44
|
+
opacity?: number;
|
|
45
|
+
clipPath?: string;
|
|
46
|
+
class?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
import Mark from '../Mark.svelte';
|
|
50
|
+
import { usePlot } from '../hooks/usePlot.svelte.js';
|
|
51
|
+
import { getPlotDefaults } from '../hooks/plotDefaults.js';
|
|
52
|
+
import { reduceOutputs, type ReducerName } from '../helpers/reduce.js';
|
|
53
|
+
import { resolveProp } from '../helpers/resolve.js';
|
|
54
|
+
import { isColorOrNull } from '../helpers/typeChecks.js';
|
|
55
|
+
import {
|
|
56
|
+
hexLattice,
|
|
57
|
+
pointToHex,
|
|
58
|
+
hexagonSubpath,
|
|
59
|
+
HEX_DEFAULT_BIN_WIDTH
|
|
60
|
+
} from '../helpers/hexLattice.js';
|
|
61
|
+
import type {
|
|
62
|
+
ChannelAccessor,
|
|
63
|
+
DataRecord,
|
|
64
|
+
MarkType,
|
|
65
|
+
ScaledDataRecord
|
|
66
|
+
} from '../types/index.js';
|
|
67
|
+
|
|
68
|
+
// Per-record symbols for the synthetic data passed to <Mark>. Density
|
|
69
|
+
// uses the same pattern (Density.svelte:108-115) — the scale system reads
|
|
70
|
+
// channel values via Symbol-keyed accessors, so user data fields can't
|
|
71
|
+
// collide with our internal channels. Hexbin uses x/y point channels
|
|
72
|
+
// (not Density's x1/x2/y1/y2 range channels) because a bin IS a point;
|
|
73
|
+
// CHANNEL_SCALE maps both to the same x/y scale (see constants.ts:110).
|
|
74
|
+
const GEOM = Symbol('hexbin_geom');
|
|
75
|
+
const X_VAL = Symbol('hexbin_x');
|
|
76
|
+
const Y_VAL = Symbol('hexbin_y');
|
|
77
|
+
const FILL_VAL = Symbol('hexbin_fill');
|
|
78
|
+
const STROKE_VAL = Symbol('hexbin_stroke');
|
|
79
|
+
const FX_VAL = Symbol('hexbin_fx');
|
|
80
|
+
const FY_VAL = Symbol('hexbin_fy');
|
|
81
|
+
|
|
82
|
+
const DEFAULTS = {
|
|
83
|
+
...getPlotDefaults().hexbin
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let markProps: HexbinMarkProps = $props();
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
data,
|
|
90
|
+
x: xAcc,
|
|
91
|
+
y: yAcc,
|
|
92
|
+
fx: fxAcc,
|
|
93
|
+
fy: fyAcc,
|
|
94
|
+
binWidth = HEX_DEFAULT_BIN_WIDTH,
|
|
95
|
+
fill: rawFill = 'count',
|
|
96
|
+
stroke: rawStroke = 'none',
|
|
97
|
+
strokeWidth,
|
|
98
|
+
fillOpacity,
|
|
99
|
+
strokeOpacity,
|
|
100
|
+
opacity,
|
|
101
|
+
clipPath,
|
|
102
|
+
class: className = 'hexbin',
|
|
103
|
+
...options
|
|
104
|
+
}: HexbinMarkProps = $derived({ ...DEFAULTS, ...markProps });
|
|
105
|
+
|
|
106
|
+
const plot = usePlot();
|
|
107
|
+
|
|
108
|
+
// A fill/stroke value is a reducer when it's a function or a string that
|
|
109
|
+
// looks like a reducer name rather than a CSS color. Mirrors Density's
|
|
110
|
+
// isDensityAccessor logic (Density.svelte:159-175).
|
|
111
|
+
function isReducer(v: unknown): boolean {
|
|
112
|
+
if (typeof v === 'function') return true;
|
|
113
|
+
if (typeof v !== 'string') return false;
|
|
114
|
+
const lower = v.toLowerCase();
|
|
115
|
+
if (lower === 'none' || lower === 'inherit' || lower === 'currentcolor') return false;
|
|
116
|
+
return !isColorOrNull(v);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fillIsReducer = $derived(isReducer(rawFill));
|
|
120
|
+
const strokeIsReducer = $derived(isReducer(rawStroke));
|
|
121
|
+
|
|
122
|
+
// Raw data extent in data units, used to bootstrap x/y scale domains
|
|
123
|
+
// before bins are computed (the chicken-and-egg: bins need pixel-space
|
|
124
|
+
// scales, scales need a domain). Same approach as Density.svelte:385-426.
|
|
125
|
+
const extent = $derived.by(() => {
|
|
126
|
+
if (!data?.length || xAcc == null || yAcc == null) return null;
|
|
127
|
+
let xMin = Infinity,
|
|
128
|
+
xMax = -Infinity,
|
|
129
|
+
yMin = Infinity,
|
|
130
|
+
yMax = -Infinity;
|
|
131
|
+
for (const d of data as any[]) {
|
|
132
|
+
const xv = resolveProp(xAcc as any, d);
|
|
133
|
+
const yv = resolveProp(yAcc as any, d);
|
|
134
|
+
if (typeof xv === 'number' && Number.isFinite(xv)) {
|
|
135
|
+
if (xv < xMin) xMin = xv;
|
|
136
|
+
if (xv > xMax) xMax = xv;
|
|
137
|
+
}
|
|
138
|
+
if (typeof yv === 'number' && Number.isFinite(yv)) {
|
|
139
|
+
if (yv < yMin) yMin = yv;
|
|
140
|
+
if (yv > yMax) yMax = yv;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!isFinite(xMin) || !isFinite(yMin)) return null;
|
|
144
|
+
return { x1: xMin, x2: xMax, y1: yMin, y2: yMax };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Pixel-space binning. Builds a regular hex lattice from binWidth (px),
|
|
148
|
+
// projects each raw point through the live x/y scales to pixel space,
|
|
149
|
+
// and snaps to the nearest hex center via pointToHex. Returns null on
|
|
150
|
+
// the bootstrap pass (before scales are computable).
|
|
151
|
+
//
|
|
152
|
+
// Faceting: when fx/fy accessors are provided, raw records are partitioned
|
|
153
|
+
// by (fxVal, fyVal) before binning so each facet panel gets independent
|
|
154
|
+
// bin counts. The lattice itself stays single-source — Mark.svelte applies
|
|
155
|
+
// the per-facet group transform at render time, and pointToHex outputs in
|
|
156
|
+
// plot-global pixel space (same as scales.x/y.fn). Mirrors Density's
|
|
157
|
+
// group-by-facet pattern (Density.svelte:272-286).
|
|
158
|
+
type Bin = {
|
|
159
|
+
i: number;
|
|
160
|
+
j: number;
|
|
161
|
+
cx: number;
|
|
162
|
+
cy: number;
|
|
163
|
+
items: any[];
|
|
164
|
+
fxVal: any;
|
|
165
|
+
fyVal: any;
|
|
166
|
+
};
|
|
167
|
+
const binResult = $derived.by(() => {
|
|
168
|
+
if (!data?.length || xAcc == null || yAcc == null) return null;
|
|
169
|
+
const sx = plot.scales.x?.fn as ((v: any) => number) | undefined;
|
|
170
|
+
const sy = plot.scales.y?.fn as ((v: any) => number) | undefined;
|
|
171
|
+
if (!sx || !sy) return null;
|
|
172
|
+
|
|
173
|
+
const ml = plot.options.marginLeft ?? 0;
|
|
174
|
+
const mt = plot.options.marginTop ?? 0;
|
|
175
|
+
// Origin matches Hexgrid (Hexgrid.svelte:53) and the existing hexbin
|
|
176
|
+
// transform (transforms/hexbin.ts:85) so a default Hexbin and a default
|
|
177
|
+
// Hexgrid tile the same lattice without coordination. Cell (0,0)
|
|
178
|
+
// straddles the top-left corner — symmetric with the X half-pitch
|
|
179
|
+
// offset, which puts the cell half-inside the left edge.
|
|
180
|
+
const lattice = hexLattice(binWidth, ml + binWidth / 2, mt);
|
|
181
|
+
|
|
182
|
+
// Two-level map: (fxVal, fyVal) → (i, j) → bin. Single outer entry
|
|
183
|
+
// when not faceted.
|
|
184
|
+
const groups = new Map<string, Map<string, Bin>>();
|
|
185
|
+
const facetKeys: { key: string; fxVal: any; fyVal: any }[] = [];
|
|
186
|
+
for (const d of data as any[]) {
|
|
187
|
+
const xv = resolveProp(xAcc as any, d);
|
|
188
|
+
const yv = resolveProp(yAcc as any, d);
|
|
189
|
+
const px = sx(xv);
|
|
190
|
+
const py = sy(yv);
|
|
191
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
192
|
+
const fxVal = fxAcc != null ? resolveProp(fxAcc as any, d) : undefined;
|
|
193
|
+
const fyVal = fyAcc != null ? resolveProp(fyAcc as any, d) : undefined;
|
|
194
|
+
const groupKey = `${String(fxVal)}\0${String(fyVal)}`;
|
|
195
|
+
let group = groups.get(groupKey);
|
|
196
|
+
if (!group) {
|
|
197
|
+
group = new Map();
|
|
198
|
+
groups.set(groupKey, group);
|
|
199
|
+
facetKeys.push({ key: groupKey, fxVal, fyVal });
|
|
200
|
+
}
|
|
201
|
+
const { i, j, cx, cy } = pointToHex(px, py, lattice);
|
|
202
|
+
const binKey = `${i},${j}`;
|
|
203
|
+
let bin = group.get(binKey);
|
|
204
|
+
if (!bin) {
|
|
205
|
+
bin = { i, j, cx, cy, items: [], fxVal, fyVal };
|
|
206
|
+
group.set(binKey, bin);
|
|
207
|
+
}
|
|
208
|
+
bin.items.push(d);
|
|
209
|
+
}
|
|
210
|
+
const bins: Bin[] = [];
|
|
211
|
+
for (const g of groups.values()) for (const bin of g.values()) bins.push(bin);
|
|
212
|
+
return { lattice, bins, facetKeys };
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Synthetic records for <Mark>. Bootstrap pass emits corner records so
|
|
216
|
+
// x/y scales compute a domain before bins exist; result pass emits the
|
|
217
|
+
// persistent corner records plus one per bin carrying:
|
|
218
|
+
// X_VAL / Y_VAL raw data extent → x/y scale domain (one value per record;
|
|
219
|
+
// two records together span the extent)
|
|
220
|
+
// FILL_VAL reducer output → color scale domain (when fill is a reducer)
|
|
221
|
+
// STROKE_VAL reducer output → color scale domain (when stroke is a reducer)
|
|
222
|
+
// FX_VAL / FY_VAL facet values → Mark facet filtering (when faceted)
|
|
223
|
+
// GEOM pixel-space hex geometry → rendered by children snippet
|
|
224
|
+
//
|
|
225
|
+
// When faceted, we emit one corner-pair per unique (fxVal, fyVal) so no
|
|
226
|
+
// record carries an undefined facet value (which would create a spurious
|
|
227
|
+
// null facet panel). Mirrors Density.svelte:441-497.
|
|
228
|
+
const markData = $derived.by((): DataRecord[] => {
|
|
229
|
+
const ext = extent;
|
|
230
|
+
const br = binResult;
|
|
231
|
+
const isFaceted = fxAcc != null || fyAcc != null;
|
|
232
|
+
if (!ext) return [];
|
|
233
|
+
|
|
234
|
+
// Build the list of (fxVal, fyVal) pairs the corner records should be
|
|
235
|
+
// emitted for. binResult.facetKeys is the authoritative list once
|
|
236
|
+
// binning has happened; on the bootstrap pass we walk data ourselves
|
|
237
|
+
// since binResult is null.
|
|
238
|
+
type FK = { fxVal: any; fyVal: any };
|
|
239
|
+
let facetKeys: FK[];
|
|
240
|
+
if (br) {
|
|
241
|
+
facetKeys = br.facetKeys.map(({ fxVal, fyVal }) => ({ fxVal, fyVal }));
|
|
242
|
+
if (facetKeys.length === 0) facetKeys = [{ fxVal: undefined, fyVal: undefined }];
|
|
243
|
+
} else if (isFaceted && data?.length) {
|
|
244
|
+
const seen = new Set<string>();
|
|
245
|
+
facetKeys = [];
|
|
246
|
+
for (const d of data as any[]) {
|
|
247
|
+
const fxVal = fxAcc != null ? resolveProp(fxAcc as any, d) : undefined;
|
|
248
|
+
const fyVal = fyAcc != null ? resolveProp(fyAcc as any, d) : undefined;
|
|
249
|
+
const key = `${String(fxVal)}\0${String(fyVal)}`;
|
|
250
|
+
if (!seen.has(key)) {
|
|
251
|
+
seen.add(key);
|
|
252
|
+
facetKeys.push({ fxVal, fyVal });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
facetKeys = [{ fxVal: undefined, fyVal: undefined }];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const records: any[] = [];
|
|
260
|
+
// Persistent corner records (one pair per facet) keep x/y scale domains
|
|
261
|
+
// anchored at [x1,x2]/[y1,y2] across re-derivations and ensure each
|
|
262
|
+
// facet panel registers a domain even when no data lands in some bin.
|
|
263
|
+
for (const { fxVal, fyVal } of facetKeys) {
|
|
264
|
+
const c1: any = { [X_VAL]: ext.x1, [Y_VAL]: ext.y1 };
|
|
265
|
+
const c2: any = { [X_VAL]: ext.x2, [Y_VAL]: ext.y2 };
|
|
266
|
+
if (isFaceted) {
|
|
267
|
+
if (fxAcc != null) {
|
|
268
|
+
c1[FX_VAL] = fxVal;
|
|
269
|
+
c2[FX_VAL] = fxVal;
|
|
270
|
+
}
|
|
271
|
+
if (fyAcc != null) {
|
|
272
|
+
c1[FY_VAL] = fyVal;
|
|
273
|
+
c2[FY_VAL] = fyVal;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
records.push(c1, c2);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!br) return records;
|
|
280
|
+
|
|
281
|
+
const reducerOpts: any = {};
|
|
282
|
+
const outputs: string[] = [];
|
|
283
|
+
if (fillIsReducer) {
|
|
284
|
+
reducerOpts.fill = rawFill;
|
|
285
|
+
outputs.push('fill');
|
|
286
|
+
}
|
|
287
|
+
if (strokeIsReducer) {
|
|
288
|
+
reducerOpts.stroke = rawStroke;
|
|
289
|
+
outputs.push('stroke');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const bin of br.bins) {
|
|
293
|
+
const item: any = {
|
|
294
|
+
// Channel values irrelevant for rendering (GEOM drives that),
|
|
295
|
+
// but must be inside the extent so they don't expand the domain.
|
|
296
|
+
[X_VAL]: ext.x1,
|
|
297
|
+
[Y_VAL]: ext.y1,
|
|
298
|
+
[GEOM]: {
|
|
299
|
+
cx: bin.cx,
|
|
300
|
+
cy: bin.cy,
|
|
301
|
+
rx: br.lattice.rx,
|
|
302
|
+
ry: br.lattice.ry
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
if (isFaceted) {
|
|
306
|
+
if (fxAcc != null) item[FX_VAL] = bin.fxVal;
|
|
307
|
+
if (fyAcc != null) item[FY_VAL] = bin.fyVal;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// reduceOutputs writes item.__fill = countValue etc. (note the
|
|
311
|
+
// `__` prefix — see reduce.ts:113); copy onto Symbol keys so the
|
|
312
|
+
// channel accessors below resolve them and user-data field names
|
|
313
|
+
// can't collide.
|
|
314
|
+
if (outputs.length > 0) {
|
|
315
|
+
reduceOutputs(
|
|
316
|
+
item,
|
|
317
|
+
bin.items,
|
|
318
|
+
reducerOpts,
|
|
319
|
+
outputs as any,
|
|
320
|
+
{ x: xAcc, y: yAcc } as any,
|
|
321
|
+
{} as any
|
|
322
|
+
);
|
|
323
|
+
if (fillIsReducer) item[FILL_VAL] = item['__fill'];
|
|
324
|
+
if (strokeIsReducer) item[STROKE_VAL] = item['__stroke'];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
records.push(item);
|
|
328
|
+
}
|
|
329
|
+
return records;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const markChannels = $derived([
|
|
333
|
+
'x',
|
|
334
|
+
'y',
|
|
335
|
+
...(fillIsReducer ? ['fill'] : []),
|
|
336
|
+
...(strokeIsReducer ? ['stroke'] : []),
|
|
337
|
+
...(fxAcc != null ? ['fx'] : []),
|
|
338
|
+
...(fyAcc != null ? ['fy'] : [])
|
|
339
|
+
] as const);
|
|
340
|
+
|
|
341
|
+
const markChannelProps = $derived({
|
|
342
|
+
x: X_VAL as any,
|
|
343
|
+
y: Y_VAL as any,
|
|
344
|
+
...(fillIsReducer ? { fill: FILL_VAL as any } : {}),
|
|
345
|
+
...(strokeIsReducer ? { stroke: STROKE_VAL as any } : {}),
|
|
346
|
+
...(fxAcc != null ? { fx: FX_VAL as any } : {}),
|
|
347
|
+
...(fyAcc != null ? { fy: FY_VAL as any } : {})
|
|
348
|
+
});
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
<Mark
|
|
352
|
+
type={'hexbin' as MarkType}
|
|
353
|
+
data={markData}
|
|
354
|
+
channels={markChannels as any}
|
|
355
|
+
{...options}
|
|
356
|
+
{...markChannelProps}>
|
|
357
|
+
{#snippet children({ scaledData }: { scaledData: ScaledDataRecord[] })}
|
|
358
|
+
<g class={className} clip-path={clipPath}>
|
|
359
|
+
{#each scaledData as d, i (i)}
|
|
360
|
+
{@const geom = (d.datum as any)?.[GEOM]}
|
|
361
|
+
{#if geom}
|
|
362
|
+
<path
|
|
363
|
+
d={hexagonSubpath(geom.cx, geom.cy, geom.rx, geom.ry)}
|
|
364
|
+
fill={fillIsReducer ? ((d as any).fill ?? 'currentColor') : rawFill}
|
|
365
|
+
stroke={strokeIsReducer ? ((d as any).stroke ?? 'none') : rawStroke}
|
|
366
|
+
stroke-width={strokeWidth}
|
|
367
|
+
fill-opacity={fillOpacity}
|
|
368
|
+
stroke-opacity={strokeOpacity}
|
|
369
|
+
{opacity} />
|
|
370
|
+
{/if}
|
|
371
|
+
{/each}
|
|
372
|
+
</g>
|
|
373
|
+
{/snippet}
|
|
374
|
+
</Mark>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ReducerName } from '../helpers/reduce.js';
|
|
2
|
+
import type { ChannelAccessor, DataRecord } from '../types/index.js';
|
|
3
|
+
declare function $$render<Datum extends DataRecord>(): {
|
|
4
|
+
props: {
|
|
5
|
+
/** Input data — array of records with x/y positions. */
|
|
6
|
+
data?: Datum[] | null;
|
|
7
|
+
/** x position channel (data space). */
|
|
8
|
+
x?: ChannelAccessor<Datum>;
|
|
9
|
+
/** y position channel (data space). */
|
|
10
|
+
y?: ChannelAccessor<Datum>;
|
|
11
|
+
/** horizontal facet channel — bins are computed independently per facet. */
|
|
12
|
+
fx?: ChannelAccessor<Datum>;
|
|
13
|
+
/** vertical facet channel — bins are computed independently per facet. */
|
|
14
|
+
fy?: ChannelAccessor<Datum>;
|
|
15
|
+
/**
|
|
16
|
+
* Hex cell pitch in pixels (distance between adjacent cell centers
|
|
17
|
+
* along x). Default 20, matching `<Hexgrid />`.
|
|
18
|
+
*/
|
|
19
|
+
binWidth?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Fill: a reducer name (`'count'`, `'mean'`, …) to map aggregated
|
|
22
|
+
* values through the color scale, OR a CSS color for a constant fill.
|
|
23
|
+
* Default `'count'` (maps bin counts through the color scale).
|
|
24
|
+
*/
|
|
25
|
+
fill?: ReducerName | string;
|
|
26
|
+
/** Stroke: same shape as `fill`. Default `'none'`. */
|
|
27
|
+
stroke?: ReducerName | string;
|
|
28
|
+
strokeWidth?: number;
|
|
29
|
+
fillOpacity?: number;
|
|
30
|
+
strokeOpacity?: number;
|
|
31
|
+
opacity?: number;
|
|
32
|
+
clipPath?: string;
|
|
33
|
+
class?: string;
|
|
34
|
+
};
|
|
35
|
+
exports: {};
|
|
36
|
+
bindings: "";
|
|
37
|
+
slots: {};
|
|
38
|
+
events: {};
|
|
39
|
+
};
|
|
40
|
+
declare class __sveltets_Render<Datum extends DataRecord> {
|
|
41
|
+
props(): ReturnType<typeof $$render<Datum>>['props'];
|
|
42
|
+
events(): ReturnType<typeof $$render<Datum>>['events'];
|
|
43
|
+
slots(): ReturnType<typeof $$render<Datum>>['slots'];
|
|
44
|
+
bindings(): "";
|
|
45
|
+
exports(): {};
|
|
46
|
+
}
|
|
47
|
+
interface $$IsomorphicComponent {
|
|
48
|
+
new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
|
|
49
|
+
$$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
|
|
50
|
+
} & ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
51
|
+
<Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
52
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Renders a hexagonal binning of 2D scatter data: groups raw points into a
|
|
56
|
+
* pixel-space hex lattice, runs a reducer per bin, and draws each non-empty
|
|
57
|
+
* bin as a regular hexagonal cell. Cells are regular by construction because
|
|
58
|
+
* the lattice is computed in pixel space (after scales exist) rather than in
|
|
59
|
+
* data space, so they tile correctly under any axis aspect ratio.
|
|
60
|
+
*
|
|
61
|
+
* Pairs naturally with the data-less `<Hexgrid />` mark for an empty-cell
|
|
62
|
+
* backdrop — both default to `binWidth=20` (px), so a default `<Hexbin>` and
|
|
63
|
+
* a default `<Hexgrid>` align by convention without user coordination.
|
|
64
|
+
*
|
|
65
|
+
* The `fill` prop accepts a reducer name (`'count'`, `'mean'`, etc.) to map
|
|
66
|
+
* aggregated values through the plot's color scale, or a CSS color for a
|
|
67
|
+
* constant fill. Same shape for `stroke`.
|
|
68
|
+
*/
|
|
69
|
+
declare const Hexbin: $$IsomorphicComponent;
|
|
70
|
+
type Hexbin<Datum extends DataRecord> = InstanceType<typeof Hexbin<Datum>>;
|
|
71
|
+
export default Hexbin;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders a hexagonal grid decoration, typically used alongside hexbin-transformed data.
|
|
3
|
+
A data-less mark similar to Frame.
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
interface HexgridMarkProps {
|
|
7
|
+
/** the hexagon bin width in pixels */
|
|
8
|
+
binWidth?: number;
|
|
9
|
+
/** the stroke color of the grid lines */
|
|
10
|
+
stroke?: string;
|
|
11
|
+
/** the stroke opacity of the grid lines */
|
|
12
|
+
strokeOpacity?: number;
|
|
13
|
+
/** the stroke width of the grid lines */
|
|
14
|
+
strokeWidth?: number;
|
|
15
|
+
/** the fill color of the hexagons */
|
|
16
|
+
fill?: string;
|
|
17
|
+
/** the fill opacity of the hexagons */
|
|
18
|
+
fillOpacity?: number;
|
|
19
|
+
/** CSS class name */
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import Mark from '../Mark.svelte';
|
|
24
|
+
import type { MarkType } from '../types/index.js';
|
|
25
|
+
import { usePlot } from '../hooks/usePlot.svelte.js';
|
|
26
|
+
import {
|
|
27
|
+
hexLattice,
|
|
28
|
+
hexCellsInRect,
|
|
29
|
+
hexagonSubpath,
|
|
30
|
+
HEX_DEFAULT_BIN_WIDTH
|
|
31
|
+
} from '../helpers/hexLattice.js';
|
|
32
|
+
|
|
33
|
+
let markProps: HexgridMarkProps = $props();
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
binWidth = HEX_DEFAULT_BIN_WIDTH,
|
|
37
|
+
stroke = 'currentColor',
|
|
38
|
+
strokeOpacity = 0.1,
|
|
39
|
+
strokeWidth = 1,
|
|
40
|
+
fill = 'none',
|
|
41
|
+
fillOpacity,
|
|
42
|
+
class: className = 'hexgrid'
|
|
43
|
+
}: HexgridMarkProps = $derived({ ...markProps });
|
|
44
|
+
|
|
45
|
+
const plot = usePlot();
|
|
46
|
+
|
|
47
|
+
const pathData = $derived.by(() => {
|
|
48
|
+
const ml = plot.options.marginLeft;
|
|
49
|
+
const mt = plot.options.marginTop;
|
|
50
|
+
const w = plot.facetWidth;
|
|
51
|
+
const h = plot.facetHeight;
|
|
52
|
+
|
|
53
|
+
const lattice = hexLattice(binWidth, ml + binWidth / 2, mt);
|
|
54
|
+
let path = '';
|
|
55
|
+
for (const [cx, cy] of hexCellsInRect(lattice, ml, mt, ml + w, mt + h)) {
|
|
56
|
+
path += hexagonSubpath(cx, cy, lattice.rx, lattice.ry);
|
|
57
|
+
}
|
|
58
|
+
return path;
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<Mark type={'hexgrid' as MarkType}>
|
|
63
|
+
<g class={className}>
|
|
64
|
+
<path
|
|
65
|
+
d={pathData}
|
|
66
|
+
{fill}
|
|
67
|
+
fill-opacity={fillOpacity}
|
|
68
|
+
{stroke}
|
|
69
|
+
stroke-opacity={strokeOpacity}
|
|
70
|
+
stroke-width={strokeWidth} />
|
|
71
|
+
</g>
|
|
72
|
+
</Mark>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface HexgridMarkProps {
|
|
2
|
+
/** the hexagon bin width in pixels */
|
|
3
|
+
binWidth?: number;
|
|
4
|
+
/** the stroke color of the grid lines */
|
|
5
|
+
stroke?: string;
|
|
6
|
+
/** the stroke opacity of the grid lines */
|
|
7
|
+
strokeOpacity?: number;
|
|
8
|
+
/** the stroke width of the grid lines */
|
|
9
|
+
strokeWidth?: number;
|
|
10
|
+
/** the fill color of the hexagons */
|
|
11
|
+
fill?: string;
|
|
12
|
+
/** the fill opacity of the hexagons */
|
|
13
|
+
fillOpacity?: number;
|
|
14
|
+
/** CSS class name */
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders a hexagonal grid decoration, typically used alongside hexbin-transformed data.
|
|
19
|
+
* A data-less mark similar to Frame.
|
|
20
|
+
*/
|
|
21
|
+
declare const Hexgrid: import("svelte").Component<HexgridMarkProps, {}, "">;
|
|
22
|
+
type Hexgrid = ReturnType<typeof Hexgrid>;
|
|
23
|
+
export default Hexgrid;
|
package/dist/marks/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export { default as Geo } from './Geo.svelte';
|
|
|
31
31
|
export { default as Graticule } from './Graticule.svelte';
|
|
32
32
|
export { default as GridX } from './GridX.svelte';
|
|
33
33
|
export { default as GridY } from './GridY.svelte';
|
|
34
|
+
export { default as Hexbin } from './Hexbin.svelte';
|
|
35
|
+
export { default as Hexgrid } from './Hexgrid.svelte';
|
|
34
36
|
export { default as Hull } from './Hull.svelte';
|
|
35
37
|
export { default as Image } from './Image.svelte';
|
|
36
38
|
export { default as Line } from './Line.svelte';
|
package/dist/marks/index.js
CHANGED
|
@@ -31,6 +31,8 @@ export { default as Geo } from './Geo.svelte';
|
|
|
31
31
|
export { default as Graticule } from './Graticule.svelte';
|
|
32
32
|
export { default as GridX } from './GridX.svelte';
|
|
33
33
|
export { default as GridY } from './GridY.svelte';
|
|
34
|
+
export { default as Hexbin } from './Hexbin.svelte';
|
|
35
|
+
export { default as Hexgrid } from './Hexgrid.svelte';
|
|
34
36
|
export { default as Hull } from './Hull.svelte';
|
|
35
37
|
export { default as Image } from './Image.svelte';
|
|
36
38
|
export { default as Line } from './Line.svelte';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReducerName } from '../helpers/reduce.js';
|
|
2
|
+
import type { DataRecord, RawValue, TransformArg } from '../types/index.js';
|
|
3
|
+
type ReducerOption = ReducerName | ((group: DataRecord[]) => RawValue);
|
|
4
|
+
type HexbinOutputChannels = Partial<{
|
|
5
|
+
fill: ReducerOption;
|
|
6
|
+
stroke: ReducerOption;
|
|
7
|
+
r: ReducerOption;
|
|
8
|
+
opacity: ReducerOption;
|
|
9
|
+
fillOpacity: ReducerOption;
|
|
10
|
+
strokeOpacity: ReducerOption;
|
|
11
|
+
}>;
|
|
12
|
+
export type HexbinOptions = HexbinOutputChannels & {
|
|
13
|
+
/**
|
|
14
|
+
* Approximate number of hex bins along the x-axis.
|
|
15
|
+
* The actual bin width is computed from the data extent.
|
|
16
|
+
* Default: 20.
|
|
17
|
+
*/
|
|
18
|
+
bins?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Explicit bin width in data units. Overrides `bins` if set.
|
|
21
|
+
*/
|
|
22
|
+
binWidth?: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Bins data points into hexagonal cells and applies reducers to produce
|
|
26
|
+
* aggregated output channels (e.g. fill="count", r="count").
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <Dot {...hexbin(
|
|
31
|
+
* { data: penguins, x: "culmen_length_mm", y: "culmen_depth_mm" },
|
|
32
|
+
* { fill: "count", r: "count", bins: 15 }
|
|
33
|
+
* )} symbol="hexagon" />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function hexbin({ data, ...channels }: TransformArg<DataRecord>, options?: HexbinOptions): TransformArg<DataRecord>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveChannel } from '../helpers/resolve.js';
|
|
2
|
+
import { extent } from 'd3-array';
|
|
3
|
+
import { reduceOutputs } from '../helpers/reduce.js';
|
|
4
|
+
import { groupFacetsAndZ } from '../helpers/group.js';
|
|
5
|
+
import { hexLattice, pointToHex } from '../helpers/hexLattice.js';
|
|
6
|
+
const CHANNELS = {
|
|
7
|
+
x: Symbol('hexbin_x'),
|
|
8
|
+
y: Symbol('hexbin_y')
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Bins data points into hexagonal cells and applies reducers to produce
|
|
12
|
+
* aggregated output channels (e.g. fill="count", r="count").
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <Dot {...hexbin(
|
|
17
|
+
* { data: penguins, x: "culmen_length_mm", y: "culmen_depth_mm" },
|
|
18
|
+
* { fill: "count", r: "count", bins: 15 }
|
|
19
|
+
* )} symbol="hexagon" />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function hexbin({ data, ...channels }, options = {}) {
|
|
23
|
+
const { bins = 20, binWidth: explicitBinWidth, ...reducerOptions } = options;
|
|
24
|
+
if (channels.x == null || channels.y == null) {
|
|
25
|
+
throw new Error('hexbin requires both x and y channels');
|
|
26
|
+
}
|
|
27
|
+
// Resolve x, y values from data
|
|
28
|
+
const xValues = data.map((d) => resolveChannel('x', d, channels));
|
|
29
|
+
const yValues = data.map((d) => resolveChannel('y', d, channels));
|
|
30
|
+
const [xMin, xMax] = extent(xValues);
|
|
31
|
+
const [yMin, yMax] = extent(yValues);
|
|
32
|
+
if (xMin == null || yMin == null) {
|
|
33
|
+
return { data: [], ...channels, x: CHANNELS.x, y: CHANNELS.y };
|
|
34
|
+
}
|
|
35
|
+
// Cell pitch in data units. Pointy-topped hex with regular geometry — dy
|
|
36
|
+
// derived from dx, so dy is in the same data units as dx. Cells are visually
|
|
37
|
+
// regular only when the data units happen to match across axes; for
|
|
38
|
+
// pixel-correct cells under arbitrary data extents, use the <Hexbin> mark
|
|
39
|
+
// instead, which builds its lattice in pixel space after scales exist.
|
|
40
|
+
const dx = explicitBinWidth ?? (xMax - xMin) / Math.max(1, bins);
|
|
41
|
+
const lattice = hexLattice(dx, xMin + dx / 2, yMin);
|
|
42
|
+
const binMap = new Map();
|
|
43
|
+
for (let i = 0; i < data.length; i++) {
|
|
44
|
+
const px = xValues[i];
|
|
45
|
+
const py = yValues[i];
|
|
46
|
+
if (px == null || py == null || isNaN(px) || isNaN(py))
|
|
47
|
+
continue;
|
|
48
|
+
const { i: pi, j: pj, cx, cy } = pointToHex(px, py, lattice);
|
|
49
|
+
const key = `${pi},${pj}`;
|
|
50
|
+
let bin = binMap.get(key);
|
|
51
|
+
if (!bin) {
|
|
52
|
+
bin = { index: [], cx, cy };
|
|
53
|
+
binMap.set(key, bin);
|
|
54
|
+
}
|
|
55
|
+
bin.index.push(i);
|
|
56
|
+
}
|
|
57
|
+
// Build output data from bins
|
|
58
|
+
const xChannel = typeof channels.x === 'string' ? channels.x : '__hexbin_x';
|
|
59
|
+
const yChannel = typeof channels.y === 'string' ? channels.y : '__hexbin_y';
|
|
60
|
+
let newChannels = {
|
|
61
|
+
...channels,
|
|
62
|
+
x: xChannel,
|
|
63
|
+
y: yChannel
|
|
64
|
+
};
|
|
65
|
+
const outputs = ['fill', 'stroke', 'r', 'opacity', 'fillOpacity', 'strokeOpacity'];
|
|
66
|
+
const newData = [];
|
|
67
|
+
for (const [, bin] of binMap) {
|
|
68
|
+
const items = bin.index.map((i) => data[i]);
|
|
69
|
+
const newGroupChannels = groupFacetsAndZ(items, channels, (groupItems, groupProps) => {
|
|
70
|
+
const item = {
|
|
71
|
+
[xChannel]: bin.cx,
|
|
72
|
+
[yChannel]: bin.cy,
|
|
73
|
+
...groupProps
|
|
74
|
+
};
|
|
75
|
+
reduceOutputs(item, groupItems, reducerOptions, outputs, channels, newChannels);
|
|
76
|
+
newData.push(item);
|
|
77
|
+
});
|
|
78
|
+
newChannels = { ...newChannels, ...newGroupChannels };
|
|
79
|
+
}
|
|
80
|
+
return { data: newData, ...newChannels };
|
|
81
|
+
}
|
|
@@ -6,6 +6,7 @@ export { filter } from './filter.js';
|
|
|
6
6
|
export { map, mapX, mapY } from './map.js';
|
|
7
7
|
export { normalizeX, normalizeY, normalizeParallelX, normalizeParallelY } from './normalize.js';
|
|
8
8
|
export { group, groupX, groupY, groupZ } from './group.js';
|
|
9
|
+
export { hexbin } from './hexbin.js';
|
|
9
10
|
export { intervalX, intervalY } from './interval.js';
|
|
10
11
|
export { jitter, jitterX, jitterY } from './jitter.js';
|
|
11
12
|
export { recordizeX, recordizeY } from './recordize.js';
|
package/dist/transforms/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { filter } from './filter.js';
|
|
|
6
6
|
export { map, mapX, mapY } from './map.js';
|
|
7
7
|
export { normalizeX, normalizeY, normalizeParallelX, normalizeParallelY } from './normalize.js';
|
|
8
8
|
export { group, groupX, groupY, groupZ } from './group.js';
|
|
9
|
+
export { hexbin } from './hexbin.js';
|
|
9
10
|
export { intervalX, intervalY } from './interval.js';
|
|
10
11
|
export { jitter, jitterX, jitterY } from './jitter.js';
|
|
11
12
|
export { recordizeX, recordizeY } from './recordize.js';
|
package/dist/types/mark.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type Mark<T> = {
|
|
|
6
6
|
data: DataRecord<T>[];
|
|
7
7
|
options: T;
|
|
8
8
|
};
|
|
9
|
-
export type MarkType = 'area' | 'arrow' | 'axisX' | 'axisY' | 'barX' | 'barY' | 'cell' | 'contour' | 'custom' | 'delaunayLink' | 'delaunayMesh' | 'density' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'hull' | 'image' | 'link' | 'line' | 'raster' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'trail' | 'voronoi' | 'voronoiMesh' | 'waffleX' | 'waffleY';
|
|
9
|
+
export type MarkType = 'area' | 'arrow' | 'axisX' | 'axisY' | 'barX' | 'barY' | 'cell' | 'contour' | 'custom' | 'delaunayLink' | 'delaunayMesh' | 'density' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'hexbin' | 'hexgrid' | 'hull' | 'image' | 'link' | 'line' | 'raster' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'trail' | 'voronoi' | 'voronoiMesh' | 'waffleX' | 'waffleY';
|
|
10
10
|
export type MarkStyleProps = 'strokeDasharray' | 'strokeLinejoin' | 'strokeLinecap' | 'opacity' | 'cursor' | 'pointerEvents' | 'blend' | 'fill' | 'fillOpacity' | 'fontFamily' | 'fontWeight' | 'fontVariant' | 'fontSize' | 'fontStyle' | 'letterSpacing' | 'wordSpacing' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'x' | 'y' | 'clipPath' | 'mask' | 'filter' | 'angle' | 'radius' | 'symbol' | 'textAnchor' | 'textTransform' | 'textDecoration' | 'width';
|
|
11
11
|
import type { ChannelAccessor, ConstantAccessor, DataRecord, RawValue } from './index.js';
|
|
12
12
|
import type * as CSS from 'csstype';
|
package/dist/types/plot.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ChannelAccessor, ChannelName, ColorScaleOptions, DataRecord, Legen
|
|
|
5
5
|
import type { Snippet } from 'svelte';
|
|
6
6
|
import type { GenericMarkOptions, Mark } from './index.js';
|
|
7
7
|
import type { Clip } from '../helpers/projection.js';
|
|
8
|
-
import type { Area, AreaX, AreaY, Arrow, AxisX, AxisY, BarX, BarY, BoxX, BoxY, Brush, BrushX, BrushY, Cell, Contour, DelaunayLink, DelaunayMesh, Density, DifferenceY, Dot, Frame, Geo, Graticule, GridX, GridY, Hull, Image, Line, Link, Pointer, Raster, Rect, RectX, RectY, RuleX, RuleY, Sphere, Spike, Text, TickX, TickY, Trail, Vector, Voronoi, VoronoiMesh } from '../marks/index.js';
|
|
8
|
+
import type { Area, AreaX, AreaY, Arrow, AxisX, AxisY, BarX, BarY, BoxX, BoxY, Brush, BrushX, BrushY, Cell, Contour, DelaunayLink, DelaunayMesh, Density, DifferenceY, Dot, Frame, Geo, Graticule, GridX, GridY, Hexbin, Hull, Image, Line, Link, Pointer, Raster, Rect, RectX, RectY, RuleX, RuleY, Sphere, Spike, Text, TickX, TickY, Trail, Vector, Voronoi, VoronoiMesh } from '../marks/index.js';
|
|
9
9
|
import type WaffleX from '../marks/WaffleX.svelte';
|
|
10
10
|
import type WaffleY from '../marks/WaffleY.svelte';
|
|
11
11
|
import type BaseAxisX from '../marks/helpers/BaseAxisX.svelte';
|
|
@@ -260,6 +260,10 @@ export type PlotDefaults = {
|
|
|
260
260
|
gridY: Partial<Omit<ComponentProps<typeof GridY>, IgnoreDefaults> & {
|
|
261
261
|
implicit: boolean;
|
|
262
262
|
}> | true;
|
|
263
|
+
/**
|
|
264
|
+
* default props for hexbin marks
|
|
265
|
+
*/
|
|
266
|
+
hexbin: Partial<Omit<ComponentProps<typeof Hexbin>, IgnoreDefaults>>;
|
|
263
267
|
/**
|
|
264
268
|
* default props for hull marks
|
|
265
269
|
*/
|