svelteplot 0.14.2-pr-551.3 → 0.14.2-pr-552.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/dist/marks/Hexgrid.svelte +92 -0
- package/dist/marks/Hexgrid.svelte.d.ts +23 -0
- package/dist/marks/index.d.ts +1 -0
- package/dist/marks/index.js +1 -0
- package/dist/transforms/hexbin.d.ts +37 -0
- package/dist/transforms/hexbin.js +109 -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/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
|
|
27
|
+
let markProps: HexgridMarkProps = $props();
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
binWidth = 20,
|
|
31
|
+
stroke = 'currentColor',
|
|
32
|
+
strokeOpacity = 0.1,
|
|
33
|
+
strokeWidth = 1,
|
|
34
|
+
fill = 'none',
|
|
35
|
+
fillOpacity,
|
|
36
|
+
class: className = 'hexgrid'
|
|
37
|
+
}: HexgridMarkProps = $derived({ ...markProps });
|
|
38
|
+
|
|
39
|
+
const plot = usePlot();
|
|
40
|
+
|
|
41
|
+
const sqrt3 = Math.sqrt(3);
|
|
42
|
+
|
|
43
|
+
function r3(x: number) {
|
|
44
|
+
return Math.round(x * 1000) / 1000;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pathData = $derived.by(() => {
|
|
48
|
+
// Hex offset constants matching hexbin transform
|
|
49
|
+
const ox = 0.5;
|
|
50
|
+
const oy = 0;
|
|
51
|
+
const rx = binWidth / 2;
|
|
52
|
+
const ry = (rx * 2) / sqrt3;
|
|
53
|
+
const dx = binWidth;
|
|
54
|
+
const dy = ry * 1.5;
|
|
55
|
+
|
|
56
|
+
const w = plot.facetWidth;
|
|
57
|
+
const h = plot.facetHeight;
|
|
58
|
+
const ml = plot.options.marginLeft;
|
|
59
|
+
const mt = plot.options.marginTop;
|
|
60
|
+
|
|
61
|
+
const cols = Math.ceil(w / dx) + 1;
|
|
62
|
+
const rows = Math.ceil(h / dy) + 1;
|
|
63
|
+
|
|
64
|
+
let path = '';
|
|
65
|
+
for (let j = -1; j <= rows; j++) {
|
|
66
|
+
for (let i = -1; i <= cols; i++) {
|
|
67
|
+
const cx = r3((i + (j & 1) / 2 + ox) * dx + ml);
|
|
68
|
+
const cy = r3((j + oy) * dy + mt);
|
|
69
|
+
// Pointy-topped hexagon path
|
|
70
|
+
path += `M${cx},${r3(cy - ry)}`;
|
|
71
|
+
path += `l${r3(rx)},${r3(ry / 2)}`;
|
|
72
|
+
path += `v${r3(ry)}`;
|
|
73
|
+
path += `l${r3(-rx)},${r3(ry / 2)}`;
|
|
74
|
+
path += `l${r3(-rx)},${r3(-ry / 2)}`;
|
|
75
|
+
path += `v${r3(-ry)}Z`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return path;
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<Mark type={'hexgrid' as MarkType}>
|
|
83
|
+
<g class={className}>
|
|
84
|
+
<path
|
|
85
|
+
d={pathData}
|
|
86
|
+
{fill}
|
|
87
|
+
fill-opacity={fillOpacity}
|
|
88
|
+
{stroke}
|
|
89
|
+
stroke-opacity={strokeOpacity}
|
|
90
|
+
stroke-width={strokeWidth} />
|
|
91
|
+
</g>
|
|
92
|
+
</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,7 @@ 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 Hexgrid } from './Hexgrid.svelte';
|
|
34
35
|
export { default as Hull } from './Hull.svelte';
|
|
35
36
|
export { default as Image } from './Image.svelte';
|
|
36
37
|
export { default as Line } from './Line.svelte';
|
package/dist/marks/index.js
CHANGED
|
@@ -31,6 +31,7 @@ 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 Hexgrid } from './Hexgrid.svelte';
|
|
34
35
|
export { default as Hull } from './Hull.svelte';
|
|
35
36
|
export { default as Image } from './Image.svelte';
|
|
36
37
|
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,109 @@
|
|
|
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
|
+
const sqrt3 = Math.sqrt(3);
|
|
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
|
+
// Compute hex cell width in data units
|
|
36
|
+
const dx = explicitBinWidth ?? (xMax - xMin) / Math.max(1, bins);
|
|
37
|
+
// Vertical spacing between hex centers (pointy-topped hexagons)
|
|
38
|
+
const dy = (dx * 1.5) / sqrt3;
|
|
39
|
+
// Hex offset to avoid edge alignment
|
|
40
|
+
const ox = dx * 0.5;
|
|
41
|
+
const oy = 0;
|
|
42
|
+
// Bin data into hex cells
|
|
43
|
+
const binMap = new Map();
|
|
44
|
+
for (let i = 0; i < data.length; i++) {
|
|
45
|
+
const px = xValues[i];
|
|
46
|
+
const py = yValues[i];
|
|
47
|
+
if (px == null || py == null || isNaN(px) || isNaN(py))
|
|
48
|
+
continue;
|
|
49
|
+
// Convert to hex grid coordinates
|
|
50
|
+
let pj = Math.round((py - yMin - oy) / dy);
|
|
51
|
+
let pi = Math.round((px - xMin - ox - (pj & 1) * (dx / 2)) / dx);
|
|
52
|
+
// Snap to nearest hex center and check if an adjacent cell is closer
|
|
53
|
+
const cx0 = (pi + (pj & 1) / 2) * dx + ox + xMin;
|
|
54
|
+
const cy0 = pj * dy + oy + yMin;
|
|
55
|
+
// Check the two candidate rows
|
|
56
|
+
const pj1 = pj + 1;
|
|
57
|
+
const pi1 = Math.round((px - xMin - ox - (pj1 & 1) * (dx / 2)) / dx);
|
|
58
|
+
const cx1 = (pi1 + (pj1 & 1) / 2) * dx + ox + xMin;
|
|
59
|
+
const cy1 = pj1 * dy + oy + yMin;
|
|
60
|
+
const pj2 = pj - 1;
|
|
61
|
+
const pi2 = Math.round((px - xMin - ox - (pj2 & 1) * (dx / 2)) / dx);
|
|
62
|
+
const cx2 = (pi2 + (pj2 & 1) / 2) * dx + ox + xMin;
|
|
63
|
+
const cy2 = pj2 * dy + oy + yMin;
|
|
64
|
+
const d0 = (px - cx0) ** 2 + (py - cy0) ** 2;
|
|
65
|
+
const d1 = (px - cx1) ** 2 + (py - cy1) ** 2;
|
|
66
|
+
const d2 = (px - cx2) ** 2 + (py - cy2) ** 2;
|
|
67
|
+
if (d1 < d0 && d1 < d2) {
|
|
68
|
+
pj = pj1;
|
|
69
|
+
pi = pi1;
|
|
70
|
+
}
|
|
71
|
+
else if (d2 < d0 && d2 < d1) {
|
|
72
|
+
pj = pj2;
|
|
73
|
+
pi = pi2;
|
|
74
|
+
}
|
|
75
|
+
const key = `${pi},${pj}`;
|
|
76
|
+
let bin = binMap.get(key);
|
|
77
|
+
if (!bin) {
|
|
78
|
+
const cx = (pi + (pj & 1) / 2) * dx + ox + xMin;
|
|
79
|
+
const cy = pj * dy + oy + yMin;
|
|
80
|
+
bin = { index: [], cx, cy };
|
|
81
|
+
binMap.set(key, bin);
|
|
82
|
+
}
|
|
83
|
+
bin.index.push(i);
|
|
84
|
+
}
|
|
85
|
+
// Build output data from bins
|
|
86
|
+
const xChannel = typeof channels.x === 'string' ? channels.x : '__hexbin_x';
|
|
87
|
+
const yChannel = typeof channels.y === 'string' ? channels.y : '__hexbin_y';
|
|
88
|
+
let newChannels = {
|
|
89
|
+
...channels,
|
|
90
|
+
x: xChannel,
|
|
91
|
+
y: yChannel
|
|
92
|
+
};
|
|
93
|
+
const outputs = ['fill', 'stroke', 'r', 'opacity', 'fillOpacity', 'strokeOpacity'];
|
|
94
|
+
const newData = [];
|
|
95
|
+
for (const [, bin] of binMap) {
|
|
96
|
+
const items = bin.index.map((i) => data[i]);
|
|
97
|
+
const newGroupChannels = groupFacetsAndZ(items, channels, (groupItems, groupProps) => {
|
|
98
|
+
const item = {
|
|
99
|
+
[xChannel]: bin.cx,
|
|
100
|
+
[yChannel]: bin.cy,
|
|
101
|
+
...groupProps
|
|
102
|
+
};
|
|
103
|
+
reduceOutputs(item, groupItems, reducerOptions, outputs, channels, newChannels);
|
|
104
|
+
newData.push(item);
|
|
105
|
+
});
|
|
106
|
+
newChannels = { ...newChannels, ...newGroupChannels };
|
|
107
|
+
}
|
|
108
|
+
return { data: newData, ...newChannels };
|
|
109
|
+
}
|
|
@@ -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' | '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';
|