layerchart 2.0.0-next.62 → 2.0.0-next.63

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.
Files changed (53) hide show
  1. package/dist/canvas.d.ts +4 -0
  2. package/dist/canvas.js +4 -0
  3. package/dist/components/Arc/Arc.shared.svelte.d.ts +2 -0
  4. package/dist/components/ArcLabel/ArcLabel.shared.svelte.d.ts +1 -0
  5. package/dist/components/Circle/Circle.shared.svelte.js +24 -5
  6. package/dist/components/Circle/Circle.svelte.test.js +70 -0
  7. package/dist/components/Dodge/Dodge.shared.svelte.d.ts +132 -0
  8. package/dist/components/Dodge/Dodge.shared.svelte.js +240 -0
  9. package/dist/components/Dodge/Dodge.svelte +88 -0
  10. package/dist/components/Dodge/Dodge.svelte.d.ts +27 -0
  11. package/dist/components/Dodge/Dodge.test.d.ts +1 -0
  12. package/dist/components/Dodge/Dodge.test.js +128 -0
  13. package/dist/components/Image/Image.html.svelte +0 -8
  14. package/dist/components/Image/Image.svg.svelte +1 -9
  15. package/dist/components/Pattern/Pattern.canvas.svelte +4 -1
  16. package/dist/components/Pattern/Pattern.shared.svelte.d.ts +31 -2
  17. package/dist/components/Pattern/Pattern.shared.svelte.js +20 -1
  18. package/dist/components/Pattern/Pattern.svg.svelte +17 -1
  19. package/dist/components/Raster/Raster.base.svelte +2 -8
  20. package/dist/components/Rect/Rect.canvas.svelte +2 -4
  21. package/dist/components/Rect/Rect.canvas.svelte.d.ts +1 -1
  22. package/dist/components/Rect/Rect.html.svelte +3 -9
  23. package/dist/components/Rect/Rect.html.svelte.d.ts +1 -1
  24. package/dist/components/Rect/Rect.shared.svelte.d.ts +5 -2
  25. package/dist/components/Rect/Rect.shared.svelte.js +26 -13
  26. package/dist/components/Rect/Rect.svelte.test.js +45 -0
  27. package/dist/components/Rect/Rect.svg.svelte +36 -21
  28. package/dist/components/Rect/Rect.svg.svelte.d.ts +1 -1
  29. package/dist/components/Spline/Spline.base.svelte +3 -2
  30. package/dist/components/Text/Text.canvas.svelte +9 -0
  31. package/dist/components/Text/Text.html.svelte +6 -0
  32. package/dist/components/Text/Text.shared.svelte.d.ts +25 -2
  33. package/dist/components/Text/Text.shared.svelte.js +36 -5
  34. package/dist/components/Text/Text.svelte.test.js +40 -0
  35. package/dist/components/Text/Text.svg.svelte +7 -1
  36. package/dist/components/Waffle/Waffle.shared.svelte.d.ts +182 -0
  37. package/dist/components/Waffle/Waffle.shared.svelte.js +300 -0
  38. package/dist/components/Waffle/Waffle.svelte +148 -0
  39. package/dist/components/Waffle/Waffle.svelte.d.ts +5 -0
  40. package/dist/components/index.d.ts +4 -0
  41. package/dist/components/index.js +4 -0
  42. package/dist/html.d.ts +4 -0
  43. package/dist/html.js +4 -0
  44. package/dist/states/chart.svelte.js +8 -4
  45. package/dist/states/chart.svelte.test.js +53 -0
  46. package/dist/svg.d.ts +4 -0
  47. package/dist/svg.js +4 -0
  48. package/dist/utils/canvas.js +54 -13
  49. package/dist/utils/canvas.svelte.test.js +44 -0
  50. package/dist/utils/download.d.ts +5 -3
  51. package/dist/utils/download.js +36 -16
  52. package/dist/utils/stack.js +10 -2
  53. package/package.json +1 -1
package/dist/canvas.d.ts CHANGED
@@ -166,6 +166,10 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
166
166
  export * from './components/graph/Sankey.svelte';
167
167
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
168
168
  export * from './components/force/ForceSimulation.svelte';
169
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
170
+ export * from './components/Dodge/Dodge.svelte';
171
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
172
+ export * from './components/Waffle/Waffle.svelte';
169
173
  export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte';
170
174
  export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte';
171
175
  export { default as GeoRaster } from './components/geo/GeoRaster/GeoRaster.svelte';
package/dist/canvas.js CHANGED
@@ -119,6 +119,10 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
119
119
  export * from './components/graph/Sankey.svelte';
120
120
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
121
121
  export * from './components/force/ForceSimulation.svelte';
122
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
123
+ export * from './components/Dodge/Dodge.svelte';
124
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
125
+ export * from './components/Waffle/Waffle.svelte';
122
126
  // Geo helpers (no per-layer rendering)
123
127
  export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte';
124
128
  export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte';
@@ -115,6 +115,7 @@ export declare class ArcState {
115
115
  dx?: string | number;
116
116
  dy?: string | number;
117
117
  lineHeight?: string;
118
+ fontSize?: number | string;
118
119
  capHeight?: string;
119
120
  scaleToFit?: boolean;
120
121
  textAnchor?: "start" | "middle" | "end" | "inherit";
@@ -144,6 +145,7 @@ export declare class ArcState {
144
145
  dx?: string | number;
145
146
  dy?: string | number;
146
147
  lineHeight?: string;
148
+ fontSize?: number | string;
147
149
  capHeight?: string;
148
150
  scaleToFit?: boolean;
149
151
  textAnchor?: "start" | "middle" | "end" | "inherit";
@@ -69,6 +69,7 @@ export declare class ArcLabelState {
69
69
  dx?: string | number;
70
70
  dy?: string | number;
71
71
  lineHeight?: string;
72
+ fontSize?: number | string;
72
73
  capHeight?: string;
73
74
  scaleToFit?: boolean;
74
75
  textAnchor?: "start" | "middle" | "end" | "inherit";
@@ -11,18 +11,36 @@ const defaultKey = (_, i) => i;
11
11
  * either the geo projection or the chart's x/y/r scales.
12
12
  */
13
13
  export function resolveCircle(d, props, chartCtx, geo) {
14
+ // When cx/cy/r are omitted, fall back to the chart's accessors
15
+ // (xGet/yGet/rGet) — same pattern as `Points`. Hardcoded defaults
16
+ // (0/0/1) only apply when neither prop nor chart-level config is set.
17
+ const cxDefault = typeof props.cx === 'number'
18
+ ? props.cx
19
+ : props.cx == null && chartCtx.config.x != null
20
+ ? Number(chartCtx.xGet(d)) || 0
21
+ : 0;
22
+ const cyDefault = typeof props.cy === 'number'
23
+ ? props.cy
24
+ : props.cy == null && chartCtx.config.y != null
25
+ ? Number(chartCtx.yGet(d)) || 0
26
+ : 0;
27
+ const rDefault = typeof props.r === 'number'
28
+ ? props.r
29
+ : props.r == null && chartCtx.config.r != null
30
+ ? Number(chartCtx.rGet(d)) || 1
31
+ : 1;
14
32
  if (geo.projection) {
15
33
  const [projX, projY] = resolveGeoDataPair(props.cx, props.cy, d, geo.projection);
16
34
  return {
17
35
  cx: projX,
18
36
  cy: projY,
19
- r: resolveDataProp(props.r, d, chartCtx.rScale, typeof props.r === 'number' ? props.r : 1),
37
+ r: resolveDataProp(props.r, d, chartCtx.rScale, rDefault),
20
38
  };
21
39
  }
22
40
  return {
23
- cx: resolveDataProp(props.cx, d, chartCtx.xScale, 0),
24
- cy: resolveDataProp(props.cy, d, chartCtx.yScale, 0),
25
- r: resolveDataProp(props.r, d, chartCtx.rScale, typeof props.r === 'number' ? props.r : 1),
41
+ cx: resolveDataProp(props.cx, d, chartCtx.xScale, cxDefault),
42
+ cy: resolveDataProp(props.cy, d, chartCtx.yScale, cyDefault),
43
+ r: resolveDataProp(props.r, d, chartCtx.rScale, rDefault),
26
44
  };
27
45
  }
28
46
  /**
@@ -46,7 +64,8 @@ export class CircleState {
46
64
  // Reactive derivations
47
65
  dashArrayResolved = $derived(parseDashArray(this.#getProps().dashArray));
48
66
  dashArrayAttr = $derived(this.dashArrayResolved ? this.dashArrayResolved.join(' ') : undefined);
49
- dataMode = $derived(hasAnyDataProp(this.#getProps().cx, this.#getProps().cy, this.#getProps().r));
67
+ dataMode = $derived(this.#getProps().data != null ||
68
+ hasAnyDataProp(this.#getProps().cx, this.#getProps().cy, this.#getProps().r));
50
69
  #resolvedData = $derived(this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : []);
51
70
  // Per-key motion tracking (only created when motion is configured)
52
71
  #dataMotionMap = null;
@@ -137,5 +137,75 @@ describe('Circle', () => {
137
137
  const circles = page.getByTestId(componentTestId).elements();
138
138
  await expect.poll(() => circles.length).toBe(1);
139
139
  });
140
+ it('should enter data mode when only `data` prop is set, using chart accessors', async () => {
141
+ render(TestHarness, {
142
+ component: Circle,
143
+ chartProps: {
144
+ data,
145
+ x: 'date',
146
+ y: 'value',
147
+ yDomain: [0, 100],
148
+ r: 'value',
149
+ rRange: [2, 10],
150
+ },
151
+ componentProps: {
152
+ data,
153
+ },
154
+ });
155
+ const circles = page.getByTestId(componentTestId).elements();
156
+ await expect.poll(() => circles.length).toBe(3);
157
+ const radii = circles.map((c) => Number(c.getAttribute('r')));
158
+ for (const r of radii) {
159
+ expect(r).toBeGreaterThanOrEqual(2);
160
+ expect(r).toBeLessThanOrEqual(10);
161
+ }
162
+ expect(radii[2]).toBeGreaterThan(radii[0]);
163
+ });
164
+ it('should mix explicit and chart-inherited position props', async () => {
165
+ render(TestHarness, {
166
+ component: Circle,
167
+ chartProps: {
168
+ data,
169
+ x: 'date',
170
+ y: 'value',
171
+ yDomain: [0, 100],
172
+ },
173
+ componentProps: {
174
+ data,
175
+ cx: 'date',
176
+ r: 4,
177
+ },
178
+ });
179
+ const circles = page.getByTestId(componentTestId).elements();
180
+ await expect.poll(() => circles.length).toBe(3);
181
+ const cys = circles.map((c) => Number(c.getAttribute('cy')));
182
+ expect(cys[0]).toBeGreaterThan(cys[1]);
183
+ expect(cys[1]).toBeGreaterThan(cys[2]);
184
+ for (const c of circles) {
185
+ expect(c.getAttribute('r')).toBe('4');
186
+ }
187
+ });
188
+ it('explicit r should win over chart rGet', async () => {
189
+ render(TestHarness, {
190
+ component: Circle,
191
+ chartProps: {
192
+ data,
193
+ x: 'date',
194
+ y: 'value',
195
+ yDomain: [0, 100],
196
+ r: 'value',
197
+ rRange: [2, 10],
198
+ },
199
+ componentProps: {
200
+ data,
201
+ r: 7,
202
+ },
203
+ });
204
+ const circles = page.getByTestId(componentTestId).elements();
205
+ await expect.poll(() => circles.length).toBe(3);
206
+ for (const c of circles) {
207
+ expect(c.getAttribute('r')).toBe('7');
208
+ }
209
+ });
140
210
  });
141
211
  });
@@ -0,0 +1,132 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type DodgeAnchor = 'top' | 'middle' | 'bottom' | 'left' | 'right';
3
+ export type DodgeItem<T> = {
4
+ data: T;
5
+ /** Pixel position along the x-axis. */
6
+ x: number;
7
+ /** Pixel position along the y-axis. */
8
+ y: number;
9
+ /** Resolved circular radius (circular mode) or anchor-axis half-extent (rectangular mode). */
10
+ r: number;
11
+ /** Resolved x-axis half-extent. Equal to `r` in circular mode. */
12
+ rx: number;
13
+ /** Resolved y-axis half-extent. Equal to `r` in circular mode. */
14
+ ry: number;
15
+ /** Original index of the datum in the input `data` array. */
16
+ index: number;
17
+ };
18
+ export type DodgePropsWithoutHTML<T = any> = {
19
+ /** Data to dodge. Falls back to chart context data when omitted. */
20
+ data?: T[];
21
+ /**
22
+ * Axis to dodge along (the axis whose value is computed; the other axis is the anchor).
23
+ * @default 'y'
24
+ */
25
+ axis?: 'x' | 'y';
26
+ /**
27
+ * Anchor edge along the dodge axis.
28
+ * - `axis='y'`: `'top'` (stack down), `'middle'` (stack from center), `'bottom'` (stack up). Default `'bottom'`.
29
+ * - `axis='x'`: `'left'` (stack right), `'middle'`, `'right'`. Default `'left'`.
30
+ */
31
+ anchor?: DodgeAnchor;
32
+ /**
33
+ * Minimum padding between items in pixels.
34
+ * @default 1
35
+ */
36
+ padding?: number;
37
+ /**
38
+ * Circular collision radius (or accessor). Used unless both `rx` and `ry`
39
+ * are provided.
40
+ *
41
+ * Resolution priority:
42
+ * 1. This prop, if set.
43
+ * 2. The chart's `r` accessor (via `rScale`/`rRange`), if configured.
44
+ * 3. Default of `5`.
45
+ */
46
+ r?: number | ((d: T) => number);
47
+ /**
48
+ * X-axis half-extent (or accessor). When set together with `ry`, switches
49
+ * to axis-aligned **rectangular** packing instead of circular collision.
50
+ *
51
+ * For `axis='y'` (vertical dodge), `rx` controls the horizontal collision
52
+ * extent (typically the per-item value, e.g. half the label width). For
53
+ * `axis='x'` (horizontal dodge), `rx` becomes the dodge-axis (column)
54
+ * half-extent and is typically a constant.
55
+ */
56
+ rx?: number | ((d: T) => number);
57
+ /**
58
+ * Y-axis half-extent (or accessor). When set together with `rx`, switches
59
+ * to axis-aligned **rectangular** packing instead of circular collision.
60
+ *
61
+ * For `axis='y'` (vertical dodge), `ry` becomes the dodge-axis (row)
62
+ * half-extent and is typically a constant. For `axis='x'`, `ry` controls
63
+ * the vertical collision extent.
64
+ */
65
+ ry?: number | ((d: T) => number);
66
+ /**
67
+ * Override the anchor-axis pixel accessor.
68
+ * For `axis='y'`, this is x; for `axis='x'`, this is y.
69
+ * Defaults to the chart context's `xGet`/`yGet` (which applies the chart's
70
+ * scale to the chart's `x`/`y` accessor).
71
+ */
72
+ position?: (d: T) => number;
73
+ /**
74
+ * Pixel coordinate (along the dodge axis) of the line items grow away from:
75
+ * the centerline for `anchor='middle'`, the edge for the others.
76
+ *
77
+ * Default depends on `axis` + `anchor`:
78
+ * - `axis='y'`: `0` (top), `ctx.height / 2` (middle), `ctx.height` (bottom)
79
+ * - `axis='x'`: `0` (left), `ctx.width / 2` (middle), `ctx.width` (right)
80
+ *
81
+ * Pass a custom value to dodge within a sub-region — e.g. a band scale's
82
+ * `bandLeft + bandwidth/2` for per-band beeswarms, or a horizontal
83
+ * baseline pixel for split top/bottom timeline labels. Output positions
84
+ * are in chart coordinates (no snippet translation needed).
85
+ */
86
+ baseline?: number;
87
+ /** Snippet receives computed positions in original data order. */
88
+ children?: Snippet<[{
89
+ items: DodgeItem<T>[];
90
+ }]>;
91
+ };
92
+ export type DodgeProps<T = any> = DodgePropsWithoutHTML<T>;
93
+ type DodgeInput<T> = {
94
+ /** Anchor-axis pixel position (always — the algorithm packs the other axis). */
95
+ x: number;
96
+ /** X-axis half-extent. */
97
+ rx: number;
98
+ /** Y-axis half-extent. */
99
+ ry: number;
100
+ data: T;
101
+ index: number;
102
+ };
103
+ type DodgeOpts = {
104
+ axis: 'x' | 'y';
105
+ anchor: DodgeAnchor;
106
+ padding: number;
107
+ /** Pixel coordinate (along the dodge axis) of the line items grow away from. */
108
+ baseline: number;
109
+ /**
110
+ * When `true`, switch from circular to axis-aligned rectangular packing.
111
+ * Inputs' `rx` / `ry` are then treated as independent half-extents per axis
112
+ * (the dodge-axis half-extent should be uniform for sensible row alignment).
113
+ */
114
+ rectangular?: boolean;
115
+ };
116
+ /**
117
+ * Pack items along one axis so they don't overlap, given their positions on
118
+ * the other axis. Modeled after Observable Plot's `dodge` transform — uses an
119
+ * interval tree to find candidate positions in `O(log n + k)` per item.
120
+ *
121
+ * `input.x` is always the anchor-axis position regardless of `axis`. The
122
+ * algorithm packs along the dodge axis and the wrapper swaps `x`/`y` in the
123
+ * result for `axis='x'`.
124
+ *
125
+ * Set `opts.rectangular` to true to switch from circular to axis-aligned
126
+ * rectangular packing — useful for text labels where the bounding box is much
127
+ * wider than tall (so a circular `r` would produce excessive vertical gaps).
128
+ *
129
+ * @see https://observablehq.com/plot/transforms/dodge
130
+ */
131
+ export declare function dodge<T>(input: DodgeInput<T>[], opts: DodgeOpts): DodgeItem<T>[];
132
+ export {};
@@ -0,0 +1,240 @@
1
+ function createIntervalTree() {
2
+ let root = null;
3
+ function insertInto(node, interval) {
4
+ if (!node) {
5
+ return { interval, maxHi: interval[1], left: null, right: null };
6
+ }
7
+ if (interval[0] < node.interval[0]) {
8
+ node.left = insertInto(node.left, interval);
9
+ }
10
+ else {
11
+ node.right = insertInto(node.right, interval);
12
+ }
13
+ if (interval[1] > node.maxHi)
14
+ node.maxHi = interval[1];
15
+ return node;
16
+ }
17
+ function queryNode(node, lo, hi, visit) {
18
+ if (!node || node.maxHi < lo)
19
+ return;
20
+ queryNode(node.left, lo, hi, visit);
21
+ const it = node.interval;
22
+ if (it[0] <= hi && it[1] >= lo)
23
+ visit(it);
24
+ // Once `it[0] > hi` the right subtree (all `lo >= it[0]`) cannot overlap.
25
+ if (it[0] <= hi)
26
+ queryNode(node.right, lo, hi, visit);
27
+ }
28
+ return {
29
+ insert(interval) {
30
+ root = insertInto(root, interval);
31
+ },
32
+ queryInterval(lo, hi, visit) {
33
+ queryNode(root, lo, hi, visit);
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * Direction multiplier for an anchor along its dodge axis.
39
+ *
40
+ * - `'top'` / `'left'`: `+1` — items grow at increasing chart coord
41
+ * - `'bottom'` / `'right'`: `-1` — items grow at decreasing chart coord
42
+ * - `'middle'`: `0` — items spread symmetrically (algorithm
43
+ * treats this specially, not multiplied)
44
+ *
45
+ * Combined with `baseline` (the anchor's pixel coordinate), the algorithm
46
+ * maps a packed local position `p` to chart coords via `baseline + dir * p`
47
+ * (or `baseline ± p` for middle).
48
+ */
49
+ function anchorDirection(axis, anchor) {
50
+ if (anchor === 'middle')
51
+ return 0;
52
+ if (axis === 'y')
53
+ return anchor === 'top' ? 1 : -1; // bottom
54
+ return anchor === 'right' ? -1 : 1; // left
55
+ }
56
+ function compareSymmetric(a, b) {
57
+ return Math.abs(a) - Math.abs(b);
58
+ }
59
+ /**
60
+ * Pack items along one axis so they don't overlap, given their positions on
61
+ * the other axis. Modeled after Observable Plot's `dodge` transform — uses an
62
+ * interval tree to find candidate positions in `O(log n + k)` per item.
63
+ *
64
+ * `input.x` is always the anchor-axis position regardless of `axis`. The
65
+ * algorithm packs along the dodge axis and the wrapper swaps `x`/`y` in the
66
+ * result for `axis='x'`.
67
+ *
68
+ * Set `opts.rectangular` to true to switch from circular to axis-aligned
69
+ * rectangular packing — useful for text labels where the bounding box is much
70
+ * wider than tall (so a circular `r` would produce excessive vertical gaps).
71
+ *
72
+ * @see https://observablehq.com/plot/transforms/dodge
73
+ */
74
+ export function dodge(input, opts) {
75
+ if (opts.rectangular) {
76
+ return dodgeRows(input, opts);
77
+ }
78
+ return dodgeCircular(input, opts);
79
+ }
80
+ /**
81
+ * Map per-item normalized dodge-axis positions (`packed`) back to chart space
82
+ * and package as `DodgeItem`s. Handles the axis swap for `axis='x'` (where
83
+ * `input.x` is the chart-y anchor).
84
+ */
85
+ function buildResult(input, packed, axis, dir, baseline) {
86
+ const factor = dir === 0 ? 1 : dir;
87
+ const result = new Array(input.length);
88
+ for (let i = 0; i < input.length; i++) {
89
+ const item = input[i];
90
+ const dodgePos = packed[i] * factor + baseline;
91
+ // `r` carries the anchor-axis half-extent for back-compat with circular
92
+ // callers (`<Circle r={r}>`). In rectangular mode that's `rx` for axis='y'
93
+ // and `ry` for axis='x' — i.e. whichever axis the layout collides on.
94
+ const rAnchor = axis === 'y' ? item.rx : item.ry;
95
+ result[i] =
96
+ axis === 'y'
97
+ ? {
98
+ data: item.data,
99
+ x: item.x,
100
+ y: dodgePos,
101
+ r: rAnchor,
102
+ rx: item.rx,
103
+ ry: item.ry,
104
+ index: item.index,
105
+ }
106
+ : {
107
+ data: item.data,
108
+ x: dodgePos,
109
+ y: item.x,
110
+ r: rAnchor,
111
+ rx: item.rx,
112
+ ry: item.ry,
113
+ index: item.index,
114
+ };
115
+ }
116
+ return result;
117
+ }
118
+ function dodgeCircular(input, opts) {
119
+ const { axis, anchor, padding, baseline } = opts;
120
+ const dir = anchorDirection(axis, anchor);
121
+ const isMiddle = dir === 0;
122
+ // `intervals[0..k]` is a flat array of [lo0, hi0, lo1, hi1, ...] forbidden
123
+ // zones along the dodge axis for the current item. Slot 0/1 is reserved
124
+ // for the natural anchor zone ([0, 0], a no-op zone keeping y=0 in the
125
+ // candidate set). Tangent positions from each colliding placed item add
126
+ // two more candidate y values each. `candidates` is a parallel buffer for
127
+ // the sortable copy — pre-allocated once to avoid per-iteration allocation.
128
+ const cap = 2 * input.length + 2;
129
+ const intervals = new Float64Array(cap);
130
+ const candidates = new Float64Array(cap);
131
+ const packed = new Float64Array(input.length);
132
+ const tree = createIntervalTree();
133
+ for (let i = 0; i < input.length; i++) {
134
+ const item = input[i];
135
+ // Circular mode: `rx === ry` (Dodge.svelte ensures this), so either one
136
+ // is the circular radius. Using `rx` keeps the field reference uniform.
137
+ const ri = item.rx;
138
+ // y0 shifts the natural anchor by ri+padding so the item sits flush
139
+ // against the baseline. middle anchor (dir=0) needs no shift.
140
+ const y0 = isMiddle ? 0 : ri + padding;
141
+ const l = item.x - ri;
142
+ const h = item.x + ri;
143
+ let k = 2;
144
+ tree.queryInterval(l - padding, h + padding, (interval) => {
145
+ const j = interval[2];
146
+ const yj = packed[j] - y0;
147
+ const dx = item.x - input[j].x;
148
+ const dr = ri + input[j].rx + padding;
149
+ const sq = dr * dr - dx * dx;
150
+ if (sq >= 0) {
151
+ const dy = Math.sqrt(sq);
152
+ intervals[k++] = yj - dy;
153
+ intervals[k++] = yj + dy;
154
+ }
155
+ });
156
+ // Sort the candidate y-values in-place into our reusable buffer. Native
157
+ // typed-array sort is materially faster than Array.sort with a JS
158
+ // comparator. For non-middle anchors we want ascending order (default);
159
+ // for middle we want symmetric (closest to 0 first), which still goes
160
+ // through the typed-array sort and avoids the prior allocation.
161
+ const view = candidates.subarray(0, k);
162
+ view.set(intervals.subarray(0, k));
163
+ if (isMiddle) {
164
+ view.sort(compareSymmetric);
165
+ }
166
+ else {
167
+ view.sort();
168
+ }
169
+ let chosen = y0; // fallback: natural anchor when nothing fits
170
+ outer: for (let c = 0; c < k; c++) {
171
+ const y = view[c];
172
+ // For non-middle anchors, items grow only in the +y direction relative
173
+ // to y0 — negative candidates are below the baseline and inadmissible.
174
+ if (!isMiddle && y < 0)
175
+ continue;
176
+ for (let j = 0; j < k; j += 2) {
177
+ if (intervals[j] + 1e-6 < y && y < intervals[j + 1] - 1e-6) {
178
+ continue outer;
179
+ }
180
+ }
181
+ chosen = y + y0;
182
+ break;
183
+ }
184
+ packed[i] = chosen;
185
+ tree.insert([l, h, i]);
186
+ }
187
+ return buildResult(input, packed, axis, dir, baseline);
188
+ }
189
+ /**
190
+ * Axis-aligned rectangular packing — same interval-tree query as circular
191
+ * dodge, but we only care which rows/columns are already occupied in the
192
+ * overlap range. Items snap to the lowest free row at fixed increments along
193
+ * the dodge axis.
194
+ *
195
+ * Half-extent semantics:
196
+ * - For `axis='y'`: `rx` is the anchor-axis (horizontal) collision extent;
197
+ * `ry` is the dodge-axis (row) half-extent. Row spacing = `2 * ry`.
198
+ * - For `axis='x'`: `ry` is the anchor-axis (vertical) collision extent;
199
+ * `rx` is the dodge-axis (column) half-extent. Column spacing = `2 * rx`.
200
+ *
201
+ * The anchor-axis half-extent typically varies per item (e.g. half a label's
202
+ * width); the dodge-axis half-extent should be uniform for sensible
203
+ * row alignment.
204
+ */
205
+ function dodgeRows(input, opts) {
206
+ const { axis, anchor, padding, baseline } = opts;
207
+ const dir = anchorDirection(axis, anchor);
208
+ const isVertical = axis === 'y';
209
+ const rows = new Int32Array(input.length);
210
+ const packed = new Float64Array(input.length);
211
+ const tree = createIntervalTree();
212
+ for (let i = 0; i < input.length; i++) {
213
+ const item = input[i];
214
+ const rAnchor = isVertical ? item.rx : item.ry;
215
+ const rDodge = isVertical ? item.ry : item.rx;
216
+ const l = item.x - rAnchor;
217
+ const h = item.x + rAnchor;
218
+ const used = new Set();
219
+ tree.queryInterval(l - padding, h + padding, (interval) => {
220
+ used.add(rows[interval[2]]);
221
+ });
222
+ let row = 0;
223
+ while (used.has(row))
224
+ row++;
225
+ rows[i] = row;
226
+ // Row spacing is `2 * rDodge`; centers fall at half-row offsets.
227
+ if (dir === 0) {
228
+ // middle: alternate above/below (even rows above, odd rows below)
229
+ const sign = row % 2 === 0 ? -1 : 1;
230
+ const step = Math.floor(row / 2) + (row === 0 ? 0 : 1);
231
+ packed[i] = sign * step * 2 * rDodge;
232
+ }
233
+ else {
234
+ // start/end: stack outward from the anchor edge
235
+ packed[i] = row * 2 * rDodge + rDodge;
236
+ }
237
+ tree.insert([l, h, i]);
238
+ }
239
+ return buildResult(input, packed, axis, dir, baseline);
240
+ }
@@ -0,0 +1,88 @@
1
+ <script lang="ts" module>
2
+ export type {
3
+ DodgeAnchor,
4
+ DodgeItem,
5
+ DodgeProps,
6
+ DodgePropsWithoutHTML,
7
+ } from './Dodge.shared.svelte.js';
8
+ export { dodge } from './Dodge.shared.svelte.js';
9
+ </script>
10
+
11
+ <script lang="ts" generics="T = any">
12
+ import { getChartContext } from '../../contexts/chart.js';
13
+ import { dodge, type DodgeProps } from './Dodge.shared.svelte.js';
14
+
15
+ let {
16
+ data: dataProp,
17
+ axis = 'y',
18
+ anchor,
19
+ padding = 1,
20
+ r,
21
+ rx,
22
+ ry,
23
+ position,
24
+ baseline: baselineProp,
25
+ children,
26
+ }: DodgeProps<T> = $props();
27
+
28
+ const ctx = getChartContext<T>();
29
+
30
+ ctx.registerComponent({ name: 'Dodge', kind: 'composite-mark' });
31
+
32
+ const resolvedAnchor = $derived(anchor ?? (axis === 'y' ? 'bottom' : 'left'));
33
+
34
+ const data = $derived((dataProp ?? (ctx.data as T[] | undefined) ?? []) as T[]);
35
+
36
+ const positionFn = $derived(
37
+ position ?? ((axis === 'y' ? ctx.xGet : ctx.yGet) as (d: T) => number)
38
+ );
39
+
40
+ // Rectangular mode is opt-in by providing both `rx` and `ry`.
41
+ const rectangular = $derived(rx != null && ry != null);
42
+
43
+ // Resolve `r` (circular fallback) — also serves as the default per-axis
44
+ // half-extent in circular mode (rx === ry === r).
45
+ const rFn = $derived.by(() => {
46
+ if (typeof r === 'function') return r as (d: T) => number;
47
+ if (r != null) return () => r as number;
48
+ if (ctx.config.r) return (d: T) => Number(ctx.rGet(d)) || 0;
49
+ return () => 5;
50
+ });
51
+
52
+ function asFn(v: number | ((d: T) => number) | undefined, fallback: (d: T) => number) {
53
+ if (typeof v === 'function') return v as (d: T) => number;
54
+ if (v != null) return () => v as number;
55
+ return fallback;
56
+ }
57
+
58
+ const rxFn = $derived(asFn(rx, rFn));
59
+ const ryFn = $derived(asFn(ry, rFn));
60
+
61
+ // Default baseline: the chart-coord position of the anchor edge / centerline.
62
+ const baseline = $derived.by(() => {
63
+ if (baselineProp != null) return baselineProp;
64
+ const dim = axis === 'y' ? ctx.height : ctx.width;
65
+ if (resolvedAnchor === 'middle') return dim / 2;
66
+ if (axis === 'y') return resolvedAnchor === 'top' ? 0 : dim; // bottom
67
+ return resolvedAnchor === 'right' ? dim : 0; // left
68
+ });
69
+
70
+ const items = $derived.by(() => {
71
+ const input = data.map((d, index) => ({
72
+ x: Number(positionFn(d)) || 0,
73
+ rx: Number(rxFn(d)) || 0,
74
+ ry: Number(ryFn(d)) || 0,
75
+ data: d,
76
+ index,
77
+ }));
78
+ return dodge(input, {
79
+ axis,
80
+ anchor: resolvedAnchor,
81
+ padding,
82
+ baseline,
83
+ rectangular,
84
+ });
85
+ });
86
+ </script>
87
+
88
+ {@render children?.({ items })}
@@ -0,0 +1,27 @@
1
+ export type { DodgeAnchor, DodgeItem, DodgeProps, DodgePropsWithoutHTML, } from './Dodge.shared.svelte.js';
2
+ export { dodge } from './Dodge.shared.svelte.js';
3
+ import { type DodgeProps } from './Dodge.shared.svelte.js';
4
+ declare function $$render<T = any>(): {
5
+ props: DodgeProps<T>;
6
+ exports: {};
7
+ bindings: "";
8
+ slots: {};
9
+ events: {};
10
+ };
11
+ declare class __sveltets_Render<T = any> {
12
+ props(): ReturnType<typeof $$render<T>>['props'];
13
+ events(): ReturnType<typeof $$render<T>>['events'];
14
+ slots(): ReturnType<typeof $$render<T>>['slots'];
15
+ bindings(): "";
16
+ exports(): {};
17
+ }
18
+ interface $$IsomorphicComponent {
19
+ new <T = any>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
20
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
21
+ } & ReturnType<__sveltets_Render<T>['exports']>;
22
+ <T = any>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
23
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
24
+ }
25
+ declare const Dodge: $$IsomorphicComponent;
26
+ type Dodge<T = any> = InstanceType<typeof Dodge<T>>;
27
+ export default Dodge;
@@ -0,0 +1 @@
1
+ export {};