@sentropic/design-system-svelte 0.34.38 → 0.34.40

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.
@@ -0,0 +1,398 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * ContourChart — lignes/zones de contour sur une grille 2D régulière (façon
4
+ * Highcharts « contour » / carte topographique). Chaque cellule de la grille
5
+ * est peinte d'une bande de couleur fonction de sa `value` normalisée, sur
6
+ * l'échelle catégorielle continue category1..8 (reprise de AnomalySwimLane /
7
+ * Density2D). Axes X/Y gradués (mêmes « niceTicks » que les autres charts) et
8
+ * légende des paliers. a11y : `role="img"` + liste accessible des points.
9
+ * API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
10
+ *
11
+ * La grille est supposée régulière : les valeurs distinctes de `x` et `y`
12
+ * définissent les colonnes et lignes ; chaque cellule est un rectangle peint
13
+ * du ton correspondant à sa `value`, découpée en `levels` paliers.
14
+ *
15
+ * Props obligatoires :
16
+ * data ContourChartDatum[] - {x, y, value}
17
+ *
18
+ * Props optionnelles :
19
+ * levels number (nombre de paliers de couleur ; défaut 6)
20
+ * label string
21
+ * width number (défaut 640)
22
+ * height number (défaut 320)
23
+ * size number (non utilisé pour le rendu ; réservé parité d'API)
24
+ * class string
25
+ */
26
+ export type ContourChartTone =
27
+ | "category1" | "category2" | "category3" | "category4"
28
+ | "category5" | "category6" | "category7" | "category8";
29
+
30
+ export type ContourChartDatum = {
31
+ x: number;
32
+ y: number;
33
+ /** Valeur scalaire de la cellule : pilote la bande de couleur. */
34
+ value: number;
35
+ };
36
+ </script>
37
+
38
+ <script lang="ts">
39
+ import ChartDataList from "./ChartDataList.svelte";
40
+
41
+ type ContourChartProps = {
42
+ data: ContourChartDatum[];
43
+ levels?: number;
44
+ label?: string;
45
+ width?: number;
46
+ height?: number;
47
+ size?: number;
48
+ class?: string;
49
+ };
50
+
51
+ let {
52
+ data = [],
53
+ levels = 6,
54
+ label,
55
+ width = 640,
56
+ height = 320,
57
+ size,
58
+ class: className
59
+ }: ContourChartProps = $props();
60
+
61
+ const MARGIN = { top: 16, right: 18, bottom: 36, left: 48 };
62
+
63
+ const TONES = [
64
+ "category1","category2","category3","category4",
65
+ "category5","category6","category7","category8"
66
+ ] as const;
67
+
68
+ function niceTicks(min: number, max: number, target = 5): number[] {
69
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
70
+ return [Number.isFinite(max) ? max : 0];
71
+ }
72
+ const range = max - min;
73
+ const rough = range / Math.max(target - 1, 1);
74
+ const pow = Math.pow(10, Math.floor(Math.log10(rough)));
75
+ const norm = rough / pow;
76
+ let step: number;
77
+ if (norm < 1.5) step = pow;
78
+ else if (norm < 3) step = 2 * pow;
79
+ else if (norm < 7) step = 5 * pow;
80
+ else step = 10 * pow;
81
+ const start = Math.floor(min / step) * step;
82
+ const end = Math.ceil(max / step) * step;
83
+ const ticks: number[] = [];
84
+ for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toFixed(10)));
85
+ return ticks;
86
+ }
87
+
88
+ function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
89
+ if (d1 === d0) return r0;
90
+ return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
91
+ }
92
+
93
+ function fmt(v: number): string {
94
+ if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
95
+ return Number.isInteger(v) ? String(v) : v.toFixed(1);
96
+ }
97
+
98
+ let hoveredKey: string | null = $state(null);
99
+
100
+ // Points valides : coordonnées finies, valeur finie.
101
+ const validData = $derived(
102
+ data.filter(
103
+ (d) =>
104
+ d &&
105
+ Number.isFinite(d.x) &&
106
+ Number.isFinite(d.y) &&
107
+ Number.isFinite(d.value)
108
+ )
109
+ );
110
+
111
+ // Nombre de paliers effectif : entier ≥ 1, plafonné à 8 (les tons disponibles).
112
+ const levelCount = $derived(
113
+ Math.max(1, Math.min(TONES.length, Math.floor(Number.isFinite(levels) ? levels : 6)))
114
+ );
115
+
116
+ const valueRange = $derived.by(() => {
117
+ const vals = validData.map((d) => d.value);
118
+ const min = vals.length ? Math.min(...vals) : 0;
119
+ const max = vals.length ? Math.max(...vals) : 0;
120
+ return { min, max };
121
+ });
122
+
123
+ // Palier (0..levelCount-1) puis ton catégoriel : valeur normalisée 0..1 → bande.
124
+ function bandOf(value: number): { band: number; tone: ContourChartTone } {
125
+ const { min, max } = valueRange;
126
+ const ratio = max > min ? (value - min) / (max - min) : 0;
127
+ const band = Math.max(0, Math.min(levelCount - 1, Math.floor(ratio * levelCount)));
128
+ const toneIndex = Math.max(0, Math.min(TONES.length - 1, Math.floor((band / Math.max(levelCount - 1, 1)) * (TONES.length - 1))));
129
+ return { band, tone: TONES[toneIndex] };
130
+ }
131
+
132
+ const scales = $derived.by(() => {
133
+ const xs = validData.map((d) => d.x);
134
+ const ys = validData.map((d) => d.y);
135
+ const xValues = Array.from(new Set(xs)).sort((a, b) => a - b);
136
+ const yValues = Array.from(new Set(ys)).sort((a, b) => a - b);
137
+ const xTicks = niceTicks(Math.min(...xs), Math.max(...xs));
138
+ const yTicks = niceTicks(Math.min(...ys), Math.max(...ys));
139
+ const plotW = Math.max(width - MARGIN.left - MARGIN.right, 1);
140
+ const plotH = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
141
+ return {
142
+ xValues, yValues,
143
+ xTicks, yTicks,
144
+ xMin: xTicks[0], xMax: xTicks[xTicks.length - 1],
145
+ yMin: yTicks[0], yMax: yTicks[yTicks.length - 1],
146
+ plotW, plotH
147
+ };
148
+ });
149
+
150
+ // Largeur/hauteur d'une cellule en espace data (grille régulière), avec repli.
151
+ const cellSpan = $derived.by(() => {
152
+ const { xValues, yValues } = scales;
153
+ const dx = xValues.length > 1 ? xValues[1] - xValues[0] : 1;
154
+ const dy = yValues.length > 1 ? yValues[1] - yValues[0] : 1;
155
+ return { dx, dy };
156
+ });
157
+
158
+ // Une bande rectangulaire par cellule de grille, peinte selon sa value.
159
+ const cells = $derived.by(() => {
160
+ const { xMin, xMax, yMin, yMax, plotW, plotH } = scales;
161
+ const { dx, dy } = cellSpan;
162
+ return validData.map((d, i) => {
163
+ const left = MARGIN.left + scaleLinear(d.x - dx / 2, xMin, xMax, 0, plotW);
164
+ const right = MARGIN.left + scaleLinear(d.x + dx / 2, xMin, xMax, 0, plotW);
165
+ const top = MARGIN.top + scaleLinear(d.y + dy / 2, yMin, yMax, plotH, 0);
166
+ const bottom = MARGIN.top + scaleLinear(d.y - dy / 2, yMin, yMax, plotH, 0);
167
+ const { tone } = bandOf(d.value);
168
+ return {
169
+ key: `${i}`,
170
+ datum: d,
171
+ x: Math.min(left, right),
172
+ y: Math.min(top, bottom),
173
+ width: Math.abs(right - left),
174
+ height: Math.abs(bottom - top),
175
+ cx: (left + right) / 2,
176
+ cy: (top + bottom) / 2,
177
+ tone
178
+ };
179
+ });
180
+ });
181
+
182
+ const dataValueItems = $derived(
183
+ validData.map((d) => `x ${d.x}, y ${d.y} · ${fmt(d.value)}`)
184
+ );
185
+
186
+ const legendItems = $derived(
187
+ Array.from({ length: levelCount }, (_, band) => {
188
+ const toneIndex = Math.max(0, Math.min(TONES.length - 1, Math.floor((band / Math.max(levelCount - 1, 1)) * (TONES.length - 1))));
189
+ return { band, tone: TONES[toneIndex] };
190
+ })
191
+ );
192
+ const hasLegend = $derived(validData.length > 0);
193
+
194
+ function handlePointerMove(event: PointerEvent) {
195
+ const target = event.target;
196
+ if (!(target instanceof Element)) {
197
+ hoveredKey = null;
198
+ return;
199
+ }
200
+ hoveredKey = target.getAttribute("data-chart-key");
201
+ }
202
+
203
+ const hoveredCell = $derived.by(() => {
204
+ if (hoveredKey === null) return null;
205
+ return cells.find((c) => c.key === hoveredKey) ?? null;
206
+ });
207
+
208
+ const classes = () => ["st-contourChart", className].filter(Boolean).join(" ");
209
+ </script>
210
+
211
+ <div class={classes()}>
212
+ <div
213
+ class="st-contourChart__visual"
214
+ role="img"
215
+ aria-label={label}
216
+ onpointermove={handlePointerMove}
217
+ onpointerleave={() => (hoveredKey = null)}
218
+ >
219
+ <svg
220
+ viewBox="0 0 {width} {height}"
221
+ preserveAspectRatio="xMidYMid meet"
222
+ width="100%"
223
+ height="100%"
224
+ focusable="false"
225
+ aria-hidden="true"
226
+ >
227
+ <!-- bandes de contour : une cellule peinte par point de grille -->
228
+ {#each cells as cell (cell.key)}
229
+ <rect
230
+ class="st-contourChart__cell st-contourChart__cell--{cell.tone}"
231
+ class:st-contourChart__cell--dim={hoveredKey !== null && hoveredKey !== cell.key}
232
+ x={cell.x}
233
+ y={cell.y}
234
+ width={cell.width}
235
+ height={cell.height}
236
+ data-chart-key={cell.key}
237
+ />
238
+ {/each}
239
+
240
+ <!-- gridlines + ticks Y -->
241
+ {#each scales.yTicks as t (t)}
242
+ {@const y = MARGIN.top + scaleLinear(t, scales.yMin, scales.yMax, scales.plotH, 0)}
243
+ <line class="st-contourChart__grid" x1={MARGIN.left} x2={width - MARGIN.right} y1={y} y2={y} />
244
+ <text class="st-contourChart__tick" x={MARGIN.left - 6} y={y} text-anchor="end" dominant-baseline="middle">{fmt(t)}</text>
245
+ {/each}
246
+ <!-- ticks X -->
247
+ {#each scales.xTicks as t (t)}
248
+ {@const x = MARGIN.left + scaleLinear(t, scales.xMin, scales.xMax, 0, scales.plotW)}
249
+ <text class="st-contourChart__tick" x={x} y={height - MARGIN.bottom + 16} text-anchor="middle">{fmt(t)}</text>
250
+ {/each}
251
+
252
+ <!-- axes -->
253
+ <line class="st-contourChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
254
+ <line class="st-contourChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
255
+ </svg>
256
+ </div>
257
+
258
+ {#if hasLegend}
259
+ <div class="st-contourChart__legend" aria-hidden="true">
260
+ <span class="st-contourChart__legendText">Low</span>
261
+ <span class="st-contourChart__legendRamp">
262
+ {#each legendItems as item (item.band)}
263
+ <span class="st-contourChart__legendSwatch st-contourChart__legendSwatch--{item.tone}"></span>
264
+ {/each}
265
+ </span>
266
+ <span class="st-contourChart__legendText">High</span>
267
+ </div>
268
+ {/if}
269
+
270
+ <ChartDataList label={label ?? "contour"} items={dataValueItems} />
271
+
272
+ {#if hoveredCell}
273
+ {@const cell = hoveredCell}
274
+ <div
275
+ class="st-contourChart__tooltip"
276
+ role="presentation"
277
+ style="left: {(cell.cx / width) * 100}%; top: {(cell.cy / height) * 100}%"
278
+ >
279
+ <span class="st-contourChart__tooltipLabel">x {cell.datum.x} · y {cell.datum.y}</span>
280
+ <span class="st-contourChart__tooltipValue">{fmt(cell.datum.value)}</span>
281
+ </div>
282
+ {/if}
283
+ </div>
284
+
285
+ <style>
286
+ .st-contourChart {
287
+ color: var(--st-semantic-text-secondary);
288
+ display: block;
289
+ font-family: inherit;
290
+ position: relative;
291
+ width: 100%;
292
+ }
293
+
294
+ .st-contourChart svg {
295
+ display: block;
296
+ overflow: visible;
297
+ }
298
+
299
+ .st-contourChart__visual {
300
+ display: block;
301
+ }
302
+
303
+ .st-contourChart__grid {
304
+ opacity: 0.5;
305
+ stroke: var(--st-semantic-border-subtle);
306
+ stroke-dasharray: 2 3;
307
+ stroke-width: 1;
308
+ }
309
+
310
+ .st-contourChart__axis {
311
+ stroke: var(--st-semantic-border-subtle);
312
+ stroke-width: 1;
313
+ }
314
+
315
+ .st-contourChart__tick {
316
+ fill: var(--st-semantic-text-secondary);
317
+ font-size: 0.6875rem;
318
+ }
319
+
320
+ .st-contourChart__cell {
321
+ cursor: pointer;
322
+ stroke: var(--st-semantic-surface-default, Canvas);
323
+ stroke-width: 0.5;
324
+ transition: opacity 120ms ease;
325
+ }
326
+
327
+ .st-contourChart__cell--dim {
328
+ opacity: 0.35;
329
+ }
330
+
331
+ .st-contourChart__cell--category1 { fill: var(--st-semantic-data-category1); }
332
+ .st-contourChart__cell--category2 { fill: var(--st-semantic-data-category2); }
333
+ .st-contourChart__cell--category3 { fill: var(--st-semantic-data-category3); }
334
+ .st-contourChart__cell--category4 { fill: var(--st-semantic-data-category4); }
335
+ .st-contourChart__cell--category5 { fill: var(--st-semantic-data-category5); }
336
+ .st-contourChart__cell--category6 { fill: var(--st-semantic-data-category6); }
337
+ .st-contourChart__cell--category7 { fill: var(--st-semantic-data-category7); }
338
+ .st-contourChart__cell--category8 { fill: var(--st-semantic-data-category8); }
339
+
340
+ .st-contourChart__legend {
341
+ align-items: center;
342
+ color: var(--st-semantic-text-secondary);
343
+ display: flex;
344
+ font-size: 0.6875rem;
345
+ gap: 0.375rem;
346
+ margin-top: 0.5rem;
347
+ }
348
+
349
+ .st-contourChart__legendRamp {
350
+ display: inline-flex;
351
+ }
352
+
353
+ .st-contourChart__legendSwatch {
354
+ display: inline-block;
355
+ height: 0.75rem;
356
+ width: 1.25rem;
357
+ }
358
+
359
+ .st-contourChart__legendSwatch--category1 { background: var(--st-semantic-data-category1); }
360
+ .st-contourChart__legendSwatch--category2 { background: var(--st-semantic-data-category2); }
361
+ .st-contourChart__legendSwatch--category3 { background: var(--st-semantic-data-category3); }
362
+ .st-contourChart__legendSwatch--category4 { background: var(--st-semantic-data-category4); }
363
+ .st-contourChart__legendSwatch--category5 { background: var(--st-semantic-data-category5); }
364
+ .st-contourChart__legendSwatch--category6 { background: var(--st-semantic-data-category6); }
365
+ .st-contourChart__legendSwatch--category7 { background: var(--st-semantic-data-category7); }
366
+ .st-contourChart__legendSwatch--category8 { background: var(--st-semantic-data-category8); }
367
+
368
+ .st-contourChart__tooltip {
369
+ background: var(--st-semantic-surface-inverse);
370
+ border-radius: var(--st-radius-sm, 0.25rem);
371
+ color: var(--st-semantic-text-inverse);
372
+ display: inline-flex;
373
+ flex-direction: column;
374
+ font-size: 0.75rem;
375
+ gap: 0.125rem;
376
+ line-height: 1.2;
377
+ padding: 0.375rem 0.5rem;
378
+ pointer-events: none;
379
+ position: absolute;
380
+ transform: translate(-50%, calc(-100% - 8px));
381
+ white-space: nowrap;
382
+ z-index: 1;
383
+ }
384
+
385
+ .st-contourChart__tooltipLabel {
386
+ font-weight: 600;
387
+ }
388
+
389
+ .st-contourChart__tooltipValue {
390
+ opacity: 0.85;
391
+ }
392
+
393
+ @media (prefers-reduced-motion: reduce) {
394
+ .st-contourChart__cell {
395
+ transition: none;
396
+ }
397
+ }
398
+ </style>
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ContourChart — lignes/zones de contour sur une grille 2D régulière (façon
3
+ * Highcharts « contour » / carte topographique). Chaque cellule de la grille
4
+ * est peinte d'une bande de couleur fonction de sa `value` normalisée, sur
5
+ * l'échelle catégorielle continue category1..8 (reprise de AnomalySwimLane /
6
+ * Density2D). Axes X/Y gradués (mêmes « niceTicks » que les autres charts) et
7
+ * légende des paliers. a11y : `role="img"` + liste accessible des points.
8
+ * API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
9
+ *
10
+ * La grille est supposée régulière : les valeurs distinctes de `x` et `y`
11
+ * définissent les colonnes et lignes ; chaque cellule est un rectangle peint
12
+ * du ton correspondant à sa `value`, découpée en `levels` paliers.
13
+ *
14
+ * Props obligatoires :
15
+ * data ContourChartDatum[] - {x, y, value}
16
+ *
17
+ * Props optionnelles :
18
+ * levels number (nombre de paliers de couleur ; défaut 6)
19
+ * label string
20
+ * width number (défaut 640)
21
+ * height number (défaut 320)
22
+ * size number (non utilisé pour le rendu ; réservé parité d'API)
23
+ * class string
24
+ */
25
+ export type ContourChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
26
+ export type ContourChartDatum = {
27
+ x: number;
28
+ y: number;
29
+ /** Valeur scalaire de la cellule : pilote la bande de couleur. */
30
+ value: number;
31
+ };
32
+ type ContourChartProps = {
33
+ data: ContourChartDatum[];
34
+ levels?: number;
35
+ label?: string;
36
+ width?: number;
37
+ height?: number;
38
+ size?: number;
39
+ class?: string;
40
+ };
41
+ declare const ContourChart: import("svelte").Component<ContourChartProps, {}, "">;
42
+ type ContourChart = ReturnType<typeof ContourChart>;
43
+ export default ContourChart;
44
+ //# sourceMappingURL=ContourChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContourChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ContourChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,gBAAgB,GACxB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4NJ,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ContourChart.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContourChart.test.d.ts","sourceRoot":"","sources":["../src/lib/ContourChart.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ import { render } from "@testing-library/svelte";
2
+ import { describe, expect, it } from "vitest";
3
+ import ContourChart from "./ContourChart.svelte";
4
+ // Grille 2×2 régulière, valeurs croissantes.
5
+ const grid = [
6
+ { x: 0, y: 0, value: 1 },
7
+ { x: 1, y: 0, value: 2 },
8
+ { x: 0, y: 1, value: 3 },
9
+ { x: 1, y: 1, value: 4 }
10
+ ];
11
+ const cells = (container) => Array.from(container.querySelectorAll(".st-contourChart__cell"));
12
+ const listItems = (container) => Array.from(container.querySelectorAll(".st-chartDataList li")).map((n) => n.textContent?.trim());
13
+ const structuralClass = (el) => el.className.split(/\s+/)[0];
14
+ describe("ContourChart", () => {
15
+ it("renders an img role and one cell per datum", () => {
16
+ const { container } = render(ContourChart, { props: { data: grid, label: "Relief" } });
17
+ expect(container.querySelector('[role="img"]')).toBeTruthy();
18
+ expect(cells(container).length).toBe(4);
19
+ });
20
+ it("colours cells by value band (largest value → category8)", () => {
21
+ const { container } = render(ContourChart, { props: { data: grid, label: "C" } });
22
+ const last = cells(container).at(-1);
23
+ expect(last.classList.contains("st-contourChart__cell--category8")).toBe(true);
24
+ });
25
+ it("renders graduated X/Y axes with nice ticks", () => {
26
+ const { container } = render(ContourChart, { props: { data: grid, label: "C" } });
27
+ expect(container.querySelectorAll(".st-contourChart__axis").length).toBe(2);
28
+ expect(container.querySelectorAll(".st-contourChart__tick").length).toBeGreaterThan(0);
29
+ });
30
+ it("renders a level legend ramp", () => {
31
+ const { container } = render(ContourChart, { props: { data: grid, levels: 4, label: "C" } });
32
+ expect(container.querySelectorAll(".st-contourChart__legendSwatch").length).toBe(4);
33
+ });
34
+ it("lists every datum in the accessible data list", () => {
35
+ const { container } = render(ContourChart, {
36
+ props: { data: [{ x: 2, y: 3, value: 5 }], label: "C" }
37
+ });
38
+ expect(listItems(container)[0]).toBe("x 2, y 3 · 5");
39
+ });
40
+ it("drops non-finite points before rendering", () => {
41
+ const { container } = render(ContourChart, {
42
+ props: {
43
+ data: [
44
+ { x: Number.NaN, y: 0, value: 1 },
45
+ { x: 0, y: 0, value: Number.NaN },
46
+ { x: 1, y: 1, value: 2 }
47
+ ],
48
+ label: "C"
49
+ }
50
+ });
51
+ expect(cells(container).length).toBe(1);
52
+ });
53
+ it("merges a custom class onto the root", () => {
54
+ const { container } = render(ContourChart, { props: { data: grid, class: "mine" } });
55
+ const root = container.querySelector(".st-contourChart");
56
+ expect(structuralClass(root)).toBe("st-contourChart");
57
+ expect(root.classList.contains("mine")).toBe(true);
58
+ });
59
+ });