layerchart 2.0.0-next.62 → 2.0.0-next.64
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/canvas.d.ts +4 -0
- package/dist/canvas.js +4 -0
- package/dist/components/Arc/Arc.shared.svelte.d.ts +2 -0
- package/dist/components/ArcLabel/ArcLabel.shared.svelte.d.ts +1 -0
- package/dist/components/Circle/Circle.shared.svelte.js +24 -5
- package/dist/components/Circle/Circle.svelte.test.js +70 -0
- package/dist/components/Dodge/Dodge.shared.svelte.d.ts +132 -0
- package/dist/components/Dodge/Dodge.shared.svelte.js +240 -0
- package/dist/components/Dodge/Dodge.svelte +88 -0
- package/dist/components/Dodge/Dodge.svelte.d.ts +27 -0
- package/dist/components/Dodge/Dodge.test.d.ts +1 -0
- package/dist/components/Dodge/Dodge.test.js +128 -0
- package/dist/components/Image/Image.html.svelte +0 -8
- package/dist/components/Image/Image.svg.svelte +1 -9
- package/dist/components/Pattern/Pattern.canvas.svelte +4 -1
- package/dist/components/Pattern/Pattern.shared.svelte.d.ts +31 -2
- package/dist/components/Pattern/Pattern.shared.svelte.js +20 -1
- package/dist/components/Pattern/Pattern.svg.svelte +17 -1
- package/dist/components/Raster/Raster.base.svelte +2 -8
- package/dist/components/Rect/Rect.canvas.svelte +2 -4
- package/dist/components/Rect/Rect.canvas.svelte.d.ts +1 -1
- package/dist/components/Rect/Rect.html.svelte +3 -9
- package/dist/components/Rect/Rect.html.svelte.d.ts +1 -1
- package/dist/components/Rect/Rect.shared.svelte.d.ts +5 -2
- package/dist/components/Rect/Rect.shared.svelte.js +26 -13
- package/dist/components/Rect/Rect.svelte.test.js +45 -0
- package/dist/components/Rect/Rect.svg.svelte +36 -21
- package/dist/components/Rect/Rect.svg.svelte.d.ts +1 -1
- package/dist/components/Spline/Spline.base.svelte +3 -2
- package/dist/components/Text/Text.canvas.svelte +9 -0
- package/dist/components/Text/Text.html.svelte +6 -0
- package/dist/components/Text/Text.shared.svelte.d.ts +25 -2
- package/dist/components/Text/Text.shared.svelte.js +36 -5
- package/dist/components/Text/Text.svelte.test.js +40 -0
- package/dist/components/Text/Text.svg.svelte +7 -1
- package/dist/components/Waffle/Waffle.shared.svelte.d.ts +182 -0
- package/dist/components/Waffle/Waffle.shared.svelte.js +300 -0
- package/dist/components/Waffle/Waffle.svelte +148 -0
- package/dist/components/Waffle/Waffle.svelte.d.ts +5 -0
- package/dist/components/charts/ArcChart/ArcChart.base.svelte +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +4 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.js +4 -0
- package/dist/states/chart.svelte.js +8 -4
- package/dist/states/chart.svelte.test.js +53 -0
- package/dist/svg.d.ts +4 -0
- package/dist/svg.js +4 -0
- package/dist/utils/canvas.js +54 -13
- package/dist/utils/canvas.svelte.test.js +44 -0
- package/dist/utils/download.d.ts +5 -3
- package/dist/utils/download.js +36 -16
- package/dist/utils/stack.js +10 -2
- 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';
|
package/dist/components/index.js
CHANGED
|
@@ -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 (
|
|
661
|
-
|
|
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
|
-
|
|
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';
|