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
@@ -0,0 +1,300 @@
1
+ import { accessor, chartDataArray } from '../../utils/common.js';
2
+ import { getChartContext } from '../../contexts/chart.js';
3
+ import { createDimensionGetter } from '../../utils/rect.svelte.js';
4
+ /**
5
+ * Shared reactive state for Waffle. Resolves accessors, computes per-datum
6
+ * dimensions and waffle layouts (pattern tile size, polygon path, centroid),
7
+ * and exposes them via `items`.
8
+ */
9
+ export class WaffleState {
10
+ #getProps = () => ({});
11
+ ctx = getChartContext();
12
+ constructor(getProps) {
13
+ this.#getProps = getProps;
14
+ this.ctx.registerComponent({
15
+ name: 'Waffle',
16
+ kind: 'mark',
17
+ markInfo: () => {
18
+ const p = getProps();
19
+ return {
20
+ data: p.data,
21
+ seriesKey: p.seriesKey,
22
+ color: p.fill,
23
+ };
24
+ },
25
+ });
26
+ }
27
+ axis = $derived(this.#getProps().axis ?? this.ctx.valueAxis);
28
+ unit = $derived(Math.max(0, this.#getProps().unit ?? 1));
29
+ gap = $derived(+(this.#getProps().gap ?? 1));
30
+ round = $derived(maybeRound(this.#getProps().round));
31
+ multipleProp = $derived(maybeMultiple(this.#getProps().multiple));
32
+ series = $derived.by(() => {
33
+ const seriesKey = this.#getProps().seriesKey;
34
+ return seriesKey ? this.ctx.series.series.find((s) => s.key === seriesKey) : undefined;
35
+ });
36
+ /** Opacity multiplier — fades to 0.1 when another series is highlighted. */
37
+ seriesOpacity = $derived.by(() => {
38
+ if (this.series?.key == null ||
39
+ this.ctx.series.visibleSeries.length <= 1 ||
40
+ this.ctx.series.isHighlighted(this.series.key, true)) {
41
+ return 1;
42
+ }
43
+ return 0.1;
44
+ });
45
+ seriesAccessor = $derived(this.series
46
+ ? (this.series.value ?? (this.series.data ? undefined : this.series.key))
47
+ : undefined);
48
+ stackAccessors = $derived.by(() => {
49
+ const seriesKey = this.#getProps().seriesKey;
50
+ return seriesKey && this.ctx.series.isStacked
51
+ ? this.ctx.series.getStackAccessors(seriesKey)
52
+ : null;
53
+ });
54
+ data = $derived.by(() => {
55
+ const dataProp = this.#getProps().data;
56
+ if (dataProp)
57
+ return dataProp;
58
+ return this.series?.data ?? chartDataArray(this.ctx.data);
59
+ });
60
+ x = $derived.by(() => {
61
+ const xProp = this.#getProps().x;
62
+ return (xProp ??
63
+ (this.ctx.valueAxis === 'x'
64
+ ? (this.stackAccessors?.value ?? this.seriesAccessor)
65
+ : undefined) ??
66
+ this.ctx.x);
67
+ });
68
+ y = $derived.by(() => {
69
+ const yProp = this.#getProps().y;
70
+ return (yProp ??
71
+ (this.ctx.valueAxis === 'y'
72
+ ? (this.stackAccessors?.value ?? this.seriesAccessor)
73
+ : undefined) ??
74
+ this.ctx.y);
75
+ });
76
+ getDimensions = $derived(createDimensionGetter(this.ctx, () => ({
77
+ x: this.x,
78
+ y: this.y,
79
+ x1: this.#getProps().x1,
80
+ y1: this.#getProps().y1,
81
+ insets: this.#getProps().insets,
82
+ })));
83
+ /** Pixel slope of the value-axis scale (signed). */
84
+ pixelsPerUnit = $derived.by(() => {
85
+ const scale = this.axis === 'y' ? this.ctx.yScale : this.ctx.xScale;
86
+ if (typeof scale !== 'function')
87
+ return 0;
88
+ const a = Number(scale(0));
89
+ const b = Number(scale(1));
90
+ if (!Number.isFinite(a) || !Number.isFinite(b))
91
+ return 0;
92
+ return b - a;
93
+ });
94
+ /** Pixel position of value=0 along the value axis. */
95
+ valueZero = $derived.by(() => {
96
+ const scale = this.axis === 'y' ? this.ctx.yScale : this.ctx.xScale;
97
+ if (typeof scale !== 'function')
98
+ return 0;
99
+ return Number(scale(0)) || 0;
100
+ });
101
+ items = $derived.by(() => {
102
+ const props = this.#getProps();
103
+ const axis = this.axis;
104
+ const unit = this.unit;
105
+ const round = this.round;
106
+ const gap = this.gap;
107
+ const multipleProp = this.multipleProp;
108
+ const data = this.data;
109
+ const pixelsPerUnit = this.pixelsPerUnit;
110
+ const valueZero = this.valueZero;
111
+ if (!data || data.length === 0)
112
+ return [];
113
+ if (!Number.isFinite(pixelsPerUnit) || pixelsPerUnit === 0 || unit <= 0)
114
+ return [];
115
+ const result = [];
116
+ const cellPixels = unit * Math.abs(pixelsPerUnit); // pixels per cell along value axis
117
+ // Determine value range accessor — for stacked series, reads [v1, v2] arrays
118
+ // produced by the chart's stack series; otherwise treats value as [0, v].
119
+ const valueAccessorFn = accessor(axis === 'y'
120
+ ? (this.stackAccessors?.value ?? this.seriesAccessor ?? this.#getProps().y ?? this.ctx.y)
121
+ : (this.stackAccessors?.value ?? this.seriesAccessor ?? this.#getProps().x ?? this.ctx.x));
122
+ for (let i = 0; i < data.length; i++) {
123
+ const d = data[i];
124
+ const dim = this.getDimensions(d);
125
+ if (!dim)
126
+ continue;
127
+ let { x, y, width, height } = dim;
128
+ // Width override
129
+ if (props.width != null && axis === 'y') {
130
+ x = x + (width - props.width) / 2;
131
+ width = props.width;
132
+ }
133
+ if (props.height != null && axis === 'x') {
134
+ y = y + (height - props.height) / 2;
135
+ height = props.height;
136
+ }
137
+ const barSize = axis === 'y' ? width : height;
138
+ const barStart = axis === 'y' ? x : y;
139
+ if (barSize <= 0)
140
+ continue;
141
+ const rawValue = valueAccessorFn(d);
142
+ let v1 = 0;
143
+ let v2;
144
+ if (Array.isArray(rawValue)) {
145
+ v1 = Number(rawValue[0]) || 0;
146
+ v2 = Number(rawValue[1]) || 0;
147
+ }
148
+ else {
149
+ v2 = Number(rawValue) || 0;
150
+ }
151
+ const i1 = round(v1 / unit);
152
+ const i2 = round(v2 / unit);
153
+ if (i1 === i2)
154
+ continue;
155
+ // Default `multiple` from Plot: keep cells approximately square.
156
+ const multiple = multipleProp ?? Math.max(1, Math.floor(Math.sqrt(barSize / cellPixels)));
157
+ // Outer cell tile size (along anchor and dodge axes).
158
+ const cx = Math.min(barSize / multiple, cellPixels * multiple);
159
+ const cy = cellPixels * multiple;
160
+ // Center the cell grid within the bar.
161
+ const tx0 = barStart + (barSize - multiple * cx) / 2;
162
+ // Cells grow away from baseline toward positive values. Sign of
163
+ // `pixelsPerUnit` encodes the screen direction of value growth, so the
164
+ // same transform works for both inverted (typical svg y) and
165
+ // non-inverted scales.
166
+ const valueDir = pixelsPerUnit < 0 ? -1 : 1;
167
+ const polyPoints = wafflePoints(i1, i2, multiple);
168
+ // Pop centroid (last point) before mapping to path string.
169
+ const centroid = polyPoints.pop();
170
+ const transformPoint = axis === 'y'
171
+ ? ([col, row]) => [col * cx, valueDir * row * cy]
172
+ : ([col, row]) => [valueDir * row * cy, col * cx];
173
+ const pts = polyPoints.map(transformPoint);
174
+ const pathData = pts.length === 0 ? '' : 'M' + pts.map((p) => `${p[0]},${p[1]}`).join('L') + 'Z';
175
+ const cPx = transformPoint(centroid);
176
+ const tx = axis === 'y' ? tx0 : valueZero;
177
+ const ty = axis === 'y' ? valueZero : tx0;
178
+ // Resolve fill: explicit `fill` prop > series color > color scale > null
179
+ const fillProp = props.fill;
180
+ let fill = null;
181
+ if (typeof fillProp === 'string')
182
+ fill = fillProp;
183
+ else if (this.series?.color)
184
+ fill = this.series.color;
185
+ else if (this.ctx.config.c)
186
+ fill = String(this.ctx.cGet(d) ?? '') || null;
187
+ result.push({
188
+ data: d,
189
+ index: i,
190
+ pathData,
191
+ tx,
192
+ ty,
193
+ cx,
194
+ cy,
195
+ centroid: { x: tx + cPx[0], y: ty + cPx[1] },
196
+ fill,
197
+ });
198
+ }
199
+ return result;
200
+ });
201
+ /** Resolved gap. */
202
+ resolvedGap = $derived(this.gap);
203
+ }
204
+ function maybeMultiple(multiple) {
205
+ return multiple === undefined ? undefined : Math.max(1, Math.floor(multiple));
206
+ }
207
+ function maybeRound(round) {
208
+ if (round === undefined || round === false)
209
+ return Number;
210
+ if (round === true)
211
+ return Math.round;
212
+ if (typeof round !== 'function') {
213
+ throw new Error(`invalid round: ${round}`);
214
+ }
215
+ return round;
216
+ }
217
+ /**
218
+ * Generate the polygon outline of a waffle covering the cell interval
219
+ * `[i1, i2)` on a grid of `columns` columns. The shape is approximately
220
+ * rectangular but may have one or two corner cuts when the start or end
221
+ * value is not aligned to a row boundary, plus extra cuts for fractional
222
+ * intervals. The last point is the centroid (popped by callers for tooltips
223
+ * and tip placement).
224
+ *
225
+ * Coordinate space is `(column, row)` in cell units — callers transform to
226
+ * pixel space (and may negate the row axis for screen y).
227
+ *
228
+ * @see https://github.com/observablehq/plot/blob/main/src/marks/waffle.js
229
+ */
230
+ export function wafflePoints(i1, i2, columns) {
231
+ if (i2 < i1)
232
+ return wafflePoints(i2, i1, columns);
233
+ if (i1 < 0) {
234
+ return wafflePointsOffset(i1, i2, columns, Math.ceil(-Math.min(i1, i2) / columns));
235
+ }
236
+ const x1f = Math.floor(i1 % columns);
237
+ const x1c = Math.ceil(i1 % columns);
238
+ const x2f = Math.floor(i2 % columns);
239
+ const x2c = Math.ceil(i2 % columns);
240
+ const y1f = Math.floor(i1 / columns);
241
+ const y1c = Math.ceil(i1 / columns);
242
+ const y2f = Math.floor(i2 / columns);
243
+ const y2c = Math.ceil(i2 / columns);
244
+ const points = [];
245
+ if (y2c > y1c)
246
+ points.push([0, y1c]);
247
+ points.push([x1f, y1c], [x1f, y1f + (i1 % 1)], [x1c, y1f + (i1 % 1)]);
248
+ if (!(i1 % columns > columns - 1)) {
249
+ points.push([x1c, y1f]);
250
+ if (y2f > y1f)
251
+ points.push([columns, y1f]);
252
+ }
253
+ if (y2f > y1f)
254
+ points.push([columns, y2f]);
255
+ points.push([x2c, y2f], [x2c, y2f + (i2 % 1)], [x2f, y2f + (i2 % 1)]);
256
+ if (!(i2 % columns < 1)) {
257
+ points.push([x2f, y2c]);
258
+ if (y2c > y1c)
259
+ points.push([0, y2c]);
260
+ }
261
+ points.push(waffleCentroid(i1, i2, columns));
262
+ return points;
263
+ }
264
+ function wafflePointsOffset(i1, i2, columns, k) {
265
+ return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]);
266
+ }
267
+ function waffleCentroid(i1, i2, columns) {
268
+ const r = Math.floor(i2 / columns) - Math.floor(i1 / columns);
269
+ if (r === 0)
270
+ return waffleRowCentroid(i1, i2, columns);
271
+ if (r === 1) {
272
+ if (Math.floor(i2 % columns) > Math.ceil(i1 % columns)) {
273
+ return [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)];
274
+ }
275
+ if (i2 % columns > columns - (i1 % columns)) {
276
+ return waffleRowCentroid(i2 - (i2 % columns), i2, columns);
277
+ }
278
+ return waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns);
279
+ }
280
+ return [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2];
281
+ }
282
+ function waffleRowCentroid(i1, i2, columns) {
283
+ const c = Math.floor(i2) - Math.floor(i1);
284
+ if (c === 0) {
285
+ return [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)];
286
+ }
287
+ if (c === 1) {
288
+ if ((i2 % 1) - (i1 % 1) > 0.5) {
289
+ return [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2];
290
+ }
291
+ if (i2 % 1 > 1 - (i1 % 1)) {
292
+ return [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2];
293
+ }
294
+ return [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2];
295
+ }
296
+ return [
297
+ Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2,
298
+ Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1),
299
+ ];
300
+ }
@@ -0,0 +1,148 @@
1
+ <script lang="ts" module>
2
+ export type { WaffleProps, WafflePropsWithoutHTML } from './Waffle.shared.svelte.js';
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import { cls } from '@layerstack/tailwind';
7
+
8
+ import Group from '../Group/Group.svelte';
9
+ import Path from '../Path/Path.svelte';
10
+ import Pattern from '../Pattern/Pattern.svelte';
11
+
12
+ import { WaffleState, type WaffleProps } from './Waffle.shared.svelte.js';
13
+
14
+ let {
15
+ data: dataProp,
16
+ x: xProp,
17
+ y: yProp,
18
+ x1: x1Prop,
19
+ y1: y1Prop,
20
+ axis,
21
+ unit,
22
+ multiple,
23
+ gap,
24
+ round,
25
+ rx,
26
+ ry,
27
+ seriesKey,
28
+ insets,
29
+ width,
30
+ height,
31
+ key = (_, i) => i,
32
+ fill,
33
+ fillOpacity,
34
+ stroke,
35
+ strokeWidth,
36
+ opacity,
37
+ class: className,
38
+ tooltip,
39
+ onWaffleClick,
40
+ onpointerenter,
41
+ onpointermove,
42
+ onpointerleave,
43
+ onclick,
44
+ symbol,
45
+ ...rest
46
+ }: WaffleProps = $props();
47
+
48
+ const c = new WaffleState(
49
+ () =>
50
+ ({
51
+ data: dataProp,
52
+ x: xProp,
53
+ y: yProp,
54
+ x1: x1Prop,
55
+ y1: y1Prop,
56
+ axis,
57
+ unit,
58
+ multiple,
59
+ gap,
60
+ round,
61
+ rx,
62
+ ry,
63
+ seriesKey,
64
+ insets,
65
+ width,
66
+ height,
67
+ fill,
68
+ }) as WaffleProps
69
+ );
70
+ </script>
71
+
72
+ {#each c.items as item (key(item.data, item.index))}
73
+ {@const onItemEnter = (e: PointerEvent) => {
74
+ onpointerenter?.(e as any);
75
+ if (tooltip) c.ctx.tooltip.show(e, item.data);
76
+ }}
77
+ {@const onItemMove = (e: PointerEvent) => {
78
+ onpointermove?.(e as any);
79
+ if (tooltip) c.ctx.tooltip.show(e, item.data);
80
+ }}
81
+ {@const onItemLeave = (e: PointerEvent) => {
82
+ onpointerleave?.(e as any);
83
+ if (tooltip) c.ctx.tooltip.hide();
84
+ }}
85
+ {@const onItemClick = (e: MouseEvent) => {
86
+ onclick?.(e as any);
87
+ onWaffleClick?.(e, { data: item.data });
88
+ }}
89
+ {@const cellInset = c.gap / 2}
90
+ {@const innerWidth = Math.max(0, item.cx - 2 * cellInset)}
91
+ {@const innerHeight = Math.max(0, item.cy - 2 * cellInset)}
92
+ {@const color = item.fill ?? (typeof fill === 'string' ? fill : undefined) ?? 'currentColor'}
93
+ {#snippet symbolPatternContent()}
94
+ <g transform={`translate(${cellInset},${cellInset})`} {color}>
95
+ {@render symbol?.({
96
+ width: innerWidth,
97
+ height: innerHeight,
98
+ datum: item.data,
99
+ color,
100
+ })}
101
+ </g>
102
+ {/snippet}
103
+ <Group
104
+ x={item.tx}
105
+ y={item.ty}
106
+ class={cls('lc-waffle', className)}
107
+ opacity={(opacity ?? 1) * c.seriesOpacity}
108
+ >
109
+ <Pattern
110
+ width={item.cx}
111
+ height={item.cy}
112
+ rects={[
113
+ {
114
+ inset: cellInset,
115
+ color: item.fill ?? (typeof fill === 'string' ? fill : undefined),
116
+ opacity: fillOpacity,
117
+ rx,
118
+ ry,
119
+ },
120
+ ]}
121
+ patternContent={symbol ? symbolPatternContent : undefined}
122
+ >
123
+ {#snippet children({ pattern })}
124
+ <Path
125
+ pathData={item.pathData}
126
+ fill={pattern}
127
+ stroke={stroke as string | undefined}
128
+ strokeWidth={strokeWidth as number | undefined}
129
+ class="lc-waffle-cell"
130
+ onpointerenter={onItemEnter}
131
+ onpointermove={onItemMove}
132
+ onpointerleave={onItemLeave}
133
+ onclick={onItemClick}
134
+ {...rest}
135
+ />
136
+ {/snippet}
137
+ </Pattern>
138
+ </Group>
139
+ {/each}
140
+
141
+ <style>
142
+ @layer components {
143
+ :global(:where(.lc-waffle-cell)) {
144
+ --fill-color: var(--color-primary, currentColor);
145
+ --stroke-color: none;
146
+ }
147
+ }
148
+ </style>
@@ -0,0 +1,5 @@
1
+ export type { WaffleProps, WafflePropsWithoutHTML } from './Waffle.shared.svelte.js';
2
+ import { type WaffleProps } from './Waffle.shared.svelte.js';
3
+ declare const Waffle: import("svelte").Component<WaffleProps, {}, "">;
4
+ type Waffle = ReturnType<typeof Waffle>;
5
+ export default Waffle;
@@ -49,6 +49,8 @@ export { default as Contour } from './Contour/Contour.svelte';
49
49
  export * from './Contour/Contour.svelte';
50
50
  export { default as Density } from './Density/Density.svelte';
51
51
  export * from './Density/Density.svelte';
52
+ export { default as Dodge } from './Dodge/Dodge.svelte';
53
+ export * from './Dodge/Dodge.svelte';
52
54
  export { default as Ellipse } from './Ellipse/Ellipse.svelte';
53
55
  export * from './Ellipse/Ellipse.svelte';
54
56
  export { default as Frame } from './Frame/Frame.svelte';
@@ -125,5 +127,7 @@ export { default as Violin } from './Violin/Violin.svelte';
125
127
  export * from './Violin/Violin.svelte';
126
128
  export { default as Voronoi } from './Voronoi/Voronoi.svelte';
127
129
  export * from './Voronoi/Voronoi.svelte';
130
+ export { default as Waffle } from './Waffle/Waffle.svelte';
131
+ export * from './Waffle/Waffle.svelte';
128
132
  export { default as WebGL } from './layers/WebGL.svelte';
129
133
  export * from './layers/WebGL.svelte';
@@ -49,6 +49,8 @@ export { default as Contour } from './Contour/Contour.svelte';
49
49
  export * from './Contour/Contour.svelte';
50
50
  export { default as Density } from './Density/Density.svelte';
51
51
  export * from './Density/Density.svelte';
52
+ export { default as Dodge } from './Dodge/Dodge.svelte';
53
+ export * from './Dodge/Dodge.svelte';
52
54
  export { default as Ellipse } from './Ellipse/Ellipse.svelte';
53
55
  export * from './Ellipse/Ellipse.svelte';
54
56
  export { default as Frame } from './Frame/Frame.svelte';
@@ -125,5 +127,7 @@ export { default as Violin } from './Violin/Violin.svelte';
125
127
  export * from './Violin/Violin.svelte';
126
128
  export { default as Voronoi } from './Voronoi/Voronoi.svelte';
127
129
  export * from './Voronoi/Voronoi.svelte';
130
+ export { default as Waffle } from './Waffle/Waffle.svelte';
131
+ export * from './Waffle/Waffle.svelte';
128
132
  export { default as WebGL } from './layers/WebGL.svelte';
129
133
  export * from './layers/WebGL.svelte';
package/dist/html.d.ts CHANGED
@@ -114,3 +114,7 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
114
114
  export * from './components/graph/Sankey.svelte';
115
115
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
116
116
  export * from './components/force/ForceSimulation.svelte';
117
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
118
+ export * from './components/Dodge/Dodge.svelte';
119
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
120
+ export * from './components/Waffle/Waffle.svelte';
package/dist/html.js CHANGED
@@ -91,3 +91,7 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
91
91
  export * from './components/graph/Sankey.svelte';
92
92
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
93
93
  export * from './components/force/ForceSimulation.svelte';
94
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
95
+ export * from './components/Dodge/Dodge.svelte';
96
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
97
+ export * from './components/Waffle/Waffle.svelte';
@@ -656,11 +656,15 @@ export class ChartState {
656
656
  extraMarkValues.push(...info.data.flatMap(accessor(markAccessor)));
657
657
  }
658
658
  }
659
- const allValues = [...seriesValues, ...extraMarkValues];
660
- if (baseline != null) {
661
- return [min([baseline, ...allValues]), max([baseline, ...allValues])];
659
+ const allValues = [...seriesValues, ...extraMarkValues].filter((v) => v != null);
660
+ if (allValues.length > 0) {
661
+ if (baseline != null) {
662
+ return [min([baseline, ...allValues]), max([baseline, ...allValues])];
663
+ }
664
+ return extent(allValues);
662
665
  }
663
- return extent(allValues);
666
+ // Series are metadata-only (e.g. categorical legend with no per-series
667
+ // values on the axis) — fall through to other resolution paths.
664
668
  }
665
669
  }
666
670
  // Interval-based domain: extend to the next interval offset
@@ -893,6 +893,59 @@ describe('ChartState implicit series domain update on visibility toggle', () =>
893
893
  }
894
894
  });
895
895
  });
896
+ describe('ChartState metadata-only series', () => {
897
+ it('should not produce [undefined, undefined] domain when items lack series-key properties', () => {
898
+ const data = [
899
+ { date: '2024-01', category: 'svelte' },
900
+ { date: '2024-02', category: 'sveltekit' },
901
+ { date: '2024-03', category: 'ecosystem' },
902
+ ];
903
+ const { state, cleanup } = createChartState({
904
+ data,
905
+ x: 'date',
906
+ valueAxis: 'y',
907
+ series: [
908
+ { key: 'svelte', color: 'red' },
909
+ { key: 'sveltekit', color: 'orange' },
910
+ { key: 'ecosystem', color: 'blue' },
911
+ ],
912
+ });
913
+ try {
914
+ expect(state._yDomain).toBeUndefined();
915
+ }
916
+ finally {
917
+ cleanup();
918
+ }
919
+ });
920
+ it('should remain stable across visibility toggles instead of throwing', () => {
921
+ const data = [
922
+ { date: '2024-01', category: 'svelte' },
923
+ { date: '2024-02', category: 'sveltekit' },
924
+ ];
925
+ const { state, cleanup } = createChartState({
926
+ data,
927
+ x: 'date',
928
+ valueAxis: 'y',
929
+ series: [
930
+ { key: 'svelte', color: 'red' },
931
+ { key: 'sveltekit', color: 'orange' },
932
+ ],
933
+ motion: { type: 'spring' },
934
+ });
935
+ try {
936
+ expect(state._yDomain).toBeUndefined();
937
+ expect(() => {
938
+ state.seriesState.selectedKeys.toggle('svelte');
939
+ flushSync();
940
+ }).not.toThrow();
941
+ expect(state._yDomain).toBeUndefined();
942
+ expect(state.seriesState.visibleSeries).toHaveLength(1);
943
+ }
944
+ finally {
945
+ cleanup();
946
+ }
947
+ });
948
+ });
896
949
  describe('ChartState degenerate domain', () => {
897
950
  it('should expand degenerate y domain [0, 0] to [0, 1]', () => {
898
951
  const data = [
package/dist/svg.d.ts CHANGED
@@ -168,6 +168,10 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
168
168
  export * from './components/graph/Sankey.svelte';
169
169
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
170
170
  export * from './components/force/ForceSimulation.svelte';
171
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
172
+ export * from './components/Dodge/Dodge.svelte';
173
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
174
+ export * from './components/Waffle/Waffle.svelte';
171
175
  export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte';
172
176
  export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte';
173
177
  export { default as GeoRaster } from './components/geo/GeoRaster/GeoRaster.svelte';
package/dist/svg.js CHANGED
@@ -121,6 +121,10 @@ export { default as Sankey } from './components/graph/Sankey.svelte';
121
121
  export * from './components/graph/Sankey.svelte';
122
122
  export { default as ForceSimulation } from './components/force/ForceSimulation.svelte';
123
123
  export * from './components/force/ForceSimulation.svelte';
124
+ export { default as Dodge } from './components/Dodge/Dodge.svelte';
125
+ export * from './components/Dodge/Dodge.svelte';
126
+ export { default as Waffle } from './components/Waffle/Waffle.svelte';
127
+ export * from './components/Waffle/Waffle.svelte';
124
128
  // Geo helpers (no per-layer rendering)
125
129
  export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte';
126
130
  export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte';