@sentropic/design-system-svelte 0.34.37 → 0.34.39
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/DecompositionTreeChart.svelte +399 -0
- package/dist/DecompositionTreeChart.svelte.d.ts +48 -0
- package/dist/DecompositionTreeChart.svelte.d.ts.map +1 -0
- package/dist/Density2DChart.svelte +438 -0
- package/dist/Density2DChart.svelte.d.ts +39 -0
- package/dist/Density2DChart.svelte.d.ts.map +1 -0
- package/dist/EventFeedPanel.svelte +222 -0
- package/dist/EventFeedPanel.svelte.d.ts +45 -0
- package/dist/EventFeedPanel.svelte.d.ts.map +1 -0
- package/dist/EventFeedPanel.test.d.ts +2 -0
- package/dist/EventFeedPanel.test.d.ts.map +1 -0
- package/dist/EventFeedPanel.test.js +66 -0
- package/dist/VectorFieldChart.svelte +347 -0
- package/dist/VectorFieldChart.svelte.d.ts +43 -0
- package/dist/VectorFieldChart.svelte.d.ts.map +1 -0
- package/dist/VectorFieldChart.test.d.ts +2 -0
- package/dist/VectorFieldChart.test.d.ts.map +1 -0
- package/dist/VectorFieldChart.test.js +59 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Density2DChart - densité 2D NON-géographique sur axes numériques (heatmap
|
|
4
|
+
* binned, façon Tableau/Dataiku density). On agrège des points (x,y) en cellules
|
|
5
|
+
* d'une grille régulière `bins×bins` sur l'étendue [minX,maxX]×[minY,maxY] ; la
|
|
6
|
+
* couleur d'une cellule encode la DENSITÉ (somme des poids) normalisée sur
|
|
7
|
+
* l'échelle catégorielle continue category1..8 (reprise de HeatmapChart /
|
|
8
|
+
* AnomalySwimLane). Axes X/Y gradués (niceTicks) + légende rampe Low→High.
|
|
9
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner).
|
|
10
|
+
*
|
|
11
|
+
* Props obligatoires :
|
|
12
|
+
* data Density2DPoint[] - tableau { x, y, weight? }
|
|
13
|
+
*
|
|
14
|
+
* Props optionnelles :
|
|
15
|
+
* bins number (nb de bins par axe, défaut 12)
|
|
16
|
+
* label string
|
|
17
|
+
* width number (défaut 640)
|
|
18
|
+
* height number (défaut 320)
|
|
19
|
+
* size number (alias de width)
|
|
20
|
+
* class string
|
|
21
|
+
*/
|
|
22
|
+
export type Density2DTone =
|
|
23
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
24
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
25
|
+
|
|
26
|
+
export type Density2DPoint = {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
weight?: number;
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script lang="ts">
|
|
34
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
35
|
+
|
|
36
|
+
type Density2DChartProps = {
|
|
37
|
+
data: Density2DPoint[];
|
|
38
|
+
bins?: number;
|
|
39
|
+
label?: string;
|
|
40
|
+
width?: number;
|
|
41
|
+
height?: number;
|
|
42
|
+
size?: number;
|
|
43
|
+
class?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
data = [],
|
|
48
|
+
bins = 12,
|
|
49
|
+
label,
|
|
50
|
+
width,
|
|
51
|
+
height = 320,
|
|
52
|
+
size,
|
|
53
|
+
class: className
|
|
54
|
+
}: Density2DChartProps = $props();
|
|
55
|
+
|
|
56
|
+
const resolvedWidth = $derived(width ?? size ?? 640);
|
|
57
|
+
|
|
58
|
+
const MARGIN = { top: 16, right: 18, bottom: 36, left: 48 };
|
|
59
|
+
const TONES = [
|
|
60
|
+
"category1","category2","category3","category4","category5","category6","category7","category8"
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
// Échelle continue : densité normalisée 0..max → category1..8 (reprise de
|
|
64
|
+
// HeatmapChart). max ≤ 0 ou densité non finie → category1 (intensité plancher).
|
|
65
|
+
function toneForDensity(density: number, densityMax: number): Density2DTone {
|
|
66
|
+
if (!Number.isFinite(density) || densityMax <= 0) return "category1";
|
|
67
|
+
const ratio = Math.max(0, Math.min(1, density / densityMax));
|
|
68
|
+
const index = Math.max(0, Math.min(TONES.length - 1, Math.floor(ratio * TONES.length)));
|
|
69
|
+
return TONES[index];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
73
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
|
|
74
|
+
return [Number.isFinite(max) ? max : 0];
|
|
75
|
+
}
|
|
76
|
+
const range = max - min;
|
|
77
|
+
const rough = range / Math.max(target - 1, 1);
|
|
78
|
+
const pow = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
79
|
+
const norm = rough / pow;
|
|
80
|
+
let step: number;
|
|
81
|
+
if (norm < 1.5) step = pow;
|
|
82
|
+
else if (norm < 3) step = 2 * pow;
|
|
83
|
+
else if (norm < 7) step = 5 * pow;
|
|
84
|
+
else step = 10 * pow;
|
|
85
|
+
const start = Math.floor(min / step) * step;
|
|
86
|
+
const end = Math.ceil(max / step) * step;
|
|
87
|
+
const ticks: number[] = [];
|
|
88
|
+
for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toFixed(10)));
|
|
89
|
+
return ticks;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
|
|
93
|
+
if (d1 === d0) return r0;
|
|
94
|
+
return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatTick(v: number): string {
|
|
98
|
+
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
|
|
99
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let hoveredKey: string | null = $state(null);
|
|
103
|
+
|
|
104
|
+
const plotWidth = $derived(Math.max(resolvedWidth - MARGIN.left - MARGIN.right, 1));
|
|
105
|
+
const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
|
|
106
|
+
|
|
107
|
+
// Nombre de bins effectif : entier ≥ 1, plafonné pour rester lisible.
|
|
108
|
+
const binCount = $derived(
|
|
109
|
+
Math.max(1, Math.min(40, Math.floor(Number.isFinite(bins) ? bins : 12)))
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Normalise : ne garde que les points aux coordonnées finies.
|
|
113
|
+
const validData = $derived(
|
|
114
|
+
data.filter((d) => d && Number.isFinite(d.x) && Number.isFinite(d.y))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Étendue [minX,maxX]×[minY,maxY] + ticks « nice » pour les axes.
|
|
118
|
+
const scales = $derived.by(() => {
|
|
119
|
+
const xs = validData.map((d) => d.x);
|
|
120
|
+
const ys = validData.map((d) => d.y);
|
|
121
|
+
const xTicks = niceTicks(xs.length ? Math.min(...xs) : 0, xs.length ? Math.max(...xs) : 1);
|
|
122
|
+
const yTicks = niceTicks(ys.length ? Math.min(...ys) : 0, ys.length ? Math.max(...ys) : 1);
|
|
123
|
+
return {
|
|
124
|
+
xTicks,
|
|
125
|
+
yTicks,
|
|
126
|
+
xMin: xTicks[0],
|
|
127
|
+
xMax: xTicks[xTicks.length - 1],
|
|
128
|
+
yMin: yTicks[0],
|
|
129
|
+
yMax: yTicks[yTicks.length - 1]
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
type Bin = {
|
|
134
|
+
key: string;
|
|
135
|
+
ix: number;
|
|
136
|
+
iy: number;
|
|
137
|
+
density: number;
|
|
138
|
+
x: number;
|
|
139
|
+
y: number;
|
|
140
|
+
width: number;
|
|
141
|
+
height: number;
|
|
142
|
+
cx: number;
|
|
143
|
+
cy: number;
|
|
144
|
+
x0: number;
|
|
145
|
+
x1: number;
|
|
146
|
+
y0: number;
|
|
147
|
+
y1: number;
|
|
148
|
+
tone: Density2DTone;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Binning régulier : grille binCount×binCount sur l'étendue ; chaque point
|
|
152
|
+
// tombe dans une cellule, sa densité = somme des poids (défaut 1).
|
|
153
|
+
const layout = $derived.by(() => {
|
|
154
|
+
const { xMin, xMax, yMin, yMax } = scales;
|
|
155
|
+
if (validData.length === 0 || xMax === xMin || yMax === yMin) {
|
|
156
|
+
return { bins: [] as Bin[], densityMax: 0 };
|
|
157
|
+
}
|
|
158
|
+
const counts = new Float64Array(binCount * binCount);
|
|
159
|
+
const idx = (ix: number, iy: number) => iy * binCount + ix;
|
|
160
|
+
for (const d of validData) {
|
|
161
|
+
const fx = (d.x - xMin) / (xMax - xMin);
|
|
162
|
+
const fy = (d.y - yMin) / (yMax - yMin);
|
|
163
|
+
const ix = Math.max(0, Math.min(binCount - 1, Math.floor(fx * binCount)));
|
|
164
|
+
const iy = Math.max(0, Math.min(binCount - 1, Math.floor(fy * binCount)));
|
|
165
|
+
const w = typeof d.weight === "number" && Number.isFinite(d.weight) ? d.weight : 1;
|
|
166
|
+
counts[idx(ix, iy)] += w;
|
|
167
|
+
}
|
|
168
|
+
let densityMax = 0;
|
|
169
|
+
for (let i = 0; i < counts.length; i++) densityMax = Math.max(densityMax, counts[i]);
|
|
170
|
+
|
|
171
|
+
const cellW = plotWidth / binCount;
|
|
172
|
+
const cellH = plotHeight / binCount;
|
|
173
|
+
const out: Bin[] = [];
|
|
174
|
+
for (let iy = 0; iy < binCount; iy++) {
|
|
175
|
+
for (let ix = 0; ix < binCount; ix++) {
|
|
176
|
+
const density = counts[idx(ix, iy)];
|
|
177
|
+
if (density <= 0) continue;
|
|
178
|
+
// iy=0 en bas (y croissant vers le haut) → inverse l'axe écran.
|
|
179
|
+
const x = MARGIN.left + ix * cellW;
|
|
180
|
+
const y = MARGIN.top + (binCount - 1 - iy) * cellH;
|
|
181
|
+
const x0 = xMin + (ix / binCount) * (xMax - xMin);
|
|
182
|
+
const x1 = xMin + ((ix + 1) / binCount) * (xMax - xMin);
|
|
183
|
+
const y0 = yMin + (iy / binCount) * (yMax - yMin);
|
|
184
|
+
const y1 = yMin + ((iy + 1) / binCount) * (yMax - yMin);
|
|
185
|
+
out.push({
|
|
186
|
+
key: `${ix}-${iy}`,
|
|
187
|
+
ix,
|
|
188
|
+
iy,
|
|
189
|
+
density,
|
|
190
|
+
x,
|
|
191
|
+
y,
|
|
192
|
+
width: Math.max(cellW - 1, 1),
|
|
193
|
+
height: Math.max(cellH - 1, 1),
|
|
194
|
+
cx: x + cellW / 2,
|
|
195
|
+
cy: y + cellH / 2,
|
|
196
|
+
x0,
|
|
197
|
+
x1,
|
|
198
|
+
y0,
|
|
199
|
+
y1,
|
|
200
|
+
tone: toneForDensity(density, densityMax)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { bins: out, densityMax };
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const binCells = $derived(layout.bins);
|
|
208
|
+
|
|
209
|
+
const xAxisTicks = $derived.by(() => {
|
|
210
|
+
const { xTicks, xMin, xMax } = scales;
|
|
211
|
+
return xTicks.map((t) => ({
|
|
212
|
+
value: t,
|
|
213
|
+
x: MARGIN.left + scaleLinear(t, xMin, xMax, 0, plotWidth)
|
|
214
|
+
}));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const yAxisTicks = $derived.by(() => {
|
|
218
|
+
const { yTicks, yMin, yMax } = scales;
|
|
219
|
+
return yTicks.map((t) => ({
|
|
220
|
+
value: t,
|
|
221
|
+
y: MARGIN.top + scaleLinear(t, yMin, yMax, plotHeight, 0)
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const dataValueItems = $derived(
|
|
226
|
+
binCells.map(
|
|
227
|
+
(b) => `[${formatTick(b.x0)}–${formatTick(b.x1)}] × [${formatTick(b.y0)}–${formatTick(b.y1)}]: ${b.density}`
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const legendItems = $derived(TONES.map((tone) => ({ tone })));
|
|
232
|
+
const hasLegend = $derived(binCells.length > 0);
|
|
233
|
+
|
|
234
|
+
function handlePointerMove(event: PointerEvent) {
|
|
235
|
+
const target = event.target;
|
|
236
|
+
if (!(target instanceof Element)) {
|
|
237
|
+
hoveredKey = null;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const key = target.getAttribute("data-chart-key");
|
|
241
|
+
hoveredKey = key ?? null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const hoveredCell = $derived.by(() => {
|
|
245
|
+
if (hoveredKey === null) return null;
|
|
246
|
+
return binCells.find((b) => b.key === hoveredKey) ?? null;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const classes = () => ["st-density2DChart", className].filter(Boolean).join(" ");
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<div class={classes()}>
|
|
253
|
+
<div
|
|
254
|
+
class="st-density2DChart__visual"
|
|
255
|
+
role="img"
|
|
256
|
+
aria-label={label}
|
|
257
|
+
onpointermove={handlePointerMove}
|
|
258
|
+
onpointerleave={() => (hoveredKey = null)}
|
|
259
|
+
>
|
|
260
|
+
<svg
|
|
261
|
+
viewBox="0 0 {resolvedWidth} {height}"
|
|
262
|
+
preserveAspectRatio="xMidYMid meet"
|
|
263
|
+
width="100%"
|
|
264
|
+
height="100%"
|
|
265
|
+
focusable="false"
|
|
266
|
+
aria-hidden="true"
|
|
267
|
+
>
|
|
268
|
+
<!-- tick labels (axe Y) -->
|
|
269
|
+
{#each yAxisTicks as tick (tick.value)}
|
|
270
|
+
<text class="st-density2DChart__tickLabel" x={MARGIN.left - 8} y={tick.y} text-anchor="end" dominant-baseline="middle">
|
|
271
|
+
{formatTick(tick.value)}
|
|
272
|
+
</text>
|
|
273
|
+
{/each}
|
|
274
|
+
|
|
275
|
+
<!-- tick labels (axe X) -->
|
|
276
|
+
{#each xAxisTicks as tick (tick.value)}
|
|
277
|
+
<text class="st-density2DChart__tickLabel" x={tick.x} y={height - MARGIN.bottom + 16} text-anchor="middle">
|
|
278
|
+
{formatTick(tick.value)}
|
|
279
|
+
</text>
|
|
280
|
+
{/each}
|
|
281
|
+
|
|
282
|
+
<!-- axes -->
|
|
283
|
+
<line class="st-density2DChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
|
|
284
|
+
<line class="st-density2DChart__axis" x1={MARGIN.left} x2={resolvedWidth - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
|
|
285
|
+
|
|
286
|
+
<!-- cellules de densité (grille bins×bins, couleur ∝ densité) -->
|
|
287
|
+
{#each binCells as cell (cell.key)}
|
|
288
|
+
<rect
|
|
289
|
+
class="st-density2DChart__cell st-density2DChart__cell--{cell.tone}"
|
|
290
|
+
class:st-density2DChart__cell--dim={hoveredKey !== null && hoveredKey !== cell.key}
|
|
291
|
+
x={cell.x}
|
|
292
|
+
y={cell.y}
|
|
293
|
+
width={cell.width}
|
|
294
|
+
height={cell.height}
|
|
295
|
+
rx="1"
|
|
296
|
+
data-chart-key={cell.key}
|
|
297
|
+
/>
|
|
298
|
+
{/each}
|
|
299
|
+
</svg>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{#if hasLegend}
|
|
303
|
+
<div class="st-density2DChart__legend" aria-hidden="true">
|
|
304
|
+
<span class="st-density2DChart__legendText">Low</span>
|
|
305
|
+
<span class="st-density2DChart__legendRamp">
|
|
306
|
+
{#each legendItems as item (item.tone)}
|
|
307
|
+
<span class="st-density2DChart__legendSwatch st-density2DChart__legendSwatch--{item.tone}"></span>
|
|
308
|
+
{/each}
|
|
309
|
+
</span>
|
|
310
|
+
<span class="st-density2DChart__legendText">High</span>
|
|
311
|
+
</div>
|
|
312
|
+
{/if}
|
|
313
|
+
|
|
314
|
+
<ChartDataList label={label ?? "density 2d"} items={dataValueItems} />
|
|
315
|
+
|
|
316
|
+
{#if hoveredCell}
|
|
317
|
+
{@const cell = hoveredCell}
|
|
318
|
+
<div
|
|
319
|
+
class="st-density2DChart__tooltip"
|
|
320
|
+
role="presentation"
|
|
321
|
+
style="left: {(cell.cx / resolvedWidth) * 100}%; top: {(cell.cy / height) * 100}%"
|
|
322
|
+
>
|
|
323
|
+
<span class="st-density2DChart__tooltipLabel">[{formatTick(cell.x0)}–{formatTick(cell.x1)}] × [{formatTick(cell.y0)}–{formatTick(cell.y1)}]</span>
|
|
324
|
+
<span class="st-density2DChart__tooltipValue">{cell.density}</span>
|
|
325
|
+
</div>
|
|
326
|
+
{/if}
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<style>
|
|
330
|
+
.st-density2DChart {
|
|
331
|
+
color: var(--st-semantic-text-secondary);
|
|
332
|
+
display: block;
|
|
333
|
+
font-family: inherit;
|
|
334
|
+
position: relative;
|
|
335
|
+
width: 100%;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.st-density2DChart svg {
|
|
339
|
+
display: block;
|
|
340
|
+
overflow: visible;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.st-density2DChart__visual {
|
|
344
|
+
display: block;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.st-density2DChart__axis {
|
|
348
|
+
stroke: var(--st-semantic-border-subtle);
|
|
349
|
+
stroke-width: 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.st-density2DChart__tickLabel {
|
|
353
|
+
fill: var(--st-semantic-text-secondary);
|
|
354
|
+
font-size: 0.6875rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.st-density2DChart__cell {
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
stroke: var(--st-semantic-surface-default, Canvas);
|
|
360
|
+
stroke-width: 0.5;
|
|
361
|
+
transition: opacity 120ms ease;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.st-density2DChart__cell--dim {
|
|
365
|
+
opacity: 0.4;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.st-density2DChart__cell--category1,
|
|
369
|
+
.st-density2DChart__legendSwatch--category1 { fill: var(--st-semantic-data-category1); background: var(--st-semantic-data-category1); }
|
|
370
|
+
.st-density2DChart__cell--category2,
|
|
371
|
+
.st-density2DChart__legendSwatch--category2 { fill: var(--st-semantic-data-category2); background: var(--st-semantic-data-category2); }
|
|
372
|
+
.st-density2DChart__cell--category3,
|
|
373
|
+
.st-density2DChart__legendSwatch--category3 { fill: var(--st-semantic-data-category3); background: var(--st-semantic-data-category3); }
|
|
374
|
+
.st-density2DChart__cell--category4,
|
|
375
|
+
.st-density2DChart__legendSwatch--category4 { fill: var(--st-semantic-data-category4); background: var(--st-semantic-data-category4); }
|
|
376
|
+
.st-density2DChart__cell--category5,
|
|
377
|
+
.st-density2DChart__legendSwatch--category5 { fill: var(--st-semantic-data-category5); background: var(--st-semantic-data-category5); }
|
|
378
|
+
.st-density2DChart__cell--category6,
|
|
379
|
+
.st-density2DChart__legendSwatch--category6 { fill: var(--st-semantic-data-category6); background: var(--st-semantic-data-category6); }
|
|
380
|
+
.st-density2DChart__cell--category7,
|
|
381
|
+
.st-density2DChart__legendSwatch--category7 { fill: var(--st-semantic-data-category7); background: var(--st-semantic-data-category7); }
|
|
382
|
+
.st-density2DChart__cell--category8,
|
|
383
|
+
.st-density2DChart__legendSwatch--category8 { fill: var(--st-semantic-data-category8); background: var(--st-semantic-data-category8); }
|
|
384
|
+
|
|
385
|
+
.st-density2DChart__legend {
|
|
386
|
+
align-items: center;
|
|
387
|
+
display: flex;
|
|
388
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
389
|
+
margin-top: var(--st-spacing-2, 0.5rem);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.st-density2DChart__legendRamp {
|
|
393
|
+
display: inline-grid;
|
|
394
|
+
grid-template-columns: repeat(8, minmax(0.75rem, 1fr));
|
|
395
|
+
min-width: 8rem;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.st-density2DChart__legendSwatch {
|
|
399
|
+
display: block;
|
|
400
|
+
height: 0.5rem;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.st-density2DChart__legendText {
|
|
404
|
+
color: var(--st-semantic-text-secondary);
|
|
405
|
+
font-size: 0.75rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.st-density2DChart__tooltip {
|
|
409
|
+
background: var(--st-semantic-surface-inverse);
|
|
410
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
411
|
+
color: var(--st-semantic-text-inverse);
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
font-size: 0.75rem;
|
|
415
|
+
gap: 0.125rem;
|
|
416
|
+
line-height: 1.2;
|
|
417
|
+
padding: 0.375rem 0.5rem;
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
position: absolute;
|
|
420
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
421
|
+
white-space: nowrap;
|
|
422
|
+
z-index: 1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.st-density2DChart__tooltipLabel {
|
|
426
|
+
font-weight: 600;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.st-density2DChart__tooltipValue {
|
|
430
|
+
opacity: 0.85;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
434
|
+
.st-density2DChart__cell {
|
|
435
|
+
transition: none;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Density2DChart - densité 2D NON-géographique sur axes numériques (heatmap
|
|
3
|
+
* binned, façon Tableau/Dataiku density). On agrège des points (x,y) en cellules
|
|
4
|
+
* d'une grille régulière `bins×bins` sur l'étendue [minX,maxX]×[minY,maxY] ; la
|
|
5
|
+
* couleur d'une cellule encode la DENSITÉ (somme des poids) normalisée sur
|
|
6
|
+
* l'échelle catégorielle continue category1..8 (reprise de HeatmapChart /
|
|
7
|
+
* AnomalySwimLane). Axes X/Y gradués (niceTicks) + légende rampe Low→High.
|
|
8
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner).
|
|
9
|
+
*
|
|
10
|
+
* Props obligatoires :
|
|
11
|
+
* data Density2DPoint[] - tableau { x, y, weight? }
|
|
12
|
+
*
|
|
13
|
+
* Props optionnelles :
|
|
14
|
+
* bins number (nb de bins par axe, défaut 12)
|
|
15
|
+
* label string
|
|
16
|
+
* width number (défaut 640)
|
|
17
|
+
* height number (défaut 320)
|
|
18
|
+
* size number (alias de width)
|
|
19
|
+
* class string
|
|
20
|
+
*/
|
|
21
|
+
export type Density2DTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
22
|
+
export type Density2DPoint = {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
weight?: number;
|
|
26
|
+
};
|
|
27
|
+
type Density2DChartProps = {
|
|
28
|
+
data: Density2DPoint[];
|
|
29
|
+
bins?: number;
|
|
30
|
+
label?: string;
|
|
31
|
+
width?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
size?: number;
|
|
34
|
+
class?: string;
|
|
35
|
+
};
|
|
36
|
+
declare const Density2DChart: import("svelte").Component<Density2DChartProps, {}, "">;
|
|
37
|
+
type Density2DChart = ReturnType<typeof Density2DChart>;
|
|
38
|
+
export default Density2DChart;
|
|
39
|
+
//# sourceMappingURL=Density2DChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Density2DChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Density2DChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,aAAa,GACrB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAMF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,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;AA6QJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* EventFeedPanel — flux d'événements datés, scrollable (façon New Relic /
|
|
4
|
+
* observabilité). PANNEAU (liste), pas un graphe SVG : une liste verticale
|
|
5
|
+
* scrollable d'événements horodatés, chacun teinté/iconé par sa SÉVÉRITÉ
|
|
6
|
+
* (tons sémantiques --st-semantic-feedback-*). Distinct de TimelineChart
|
|
7
|
+
* (axe temporel SVG) : ici la temporalité est un simple tri décroissant, le
|
|
8
|
+
* rendu est du DOM (rôle `feed` / `list`) défilable.
|
|
9
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
10
|
+
*
|
|
11
|
+
* Modèle : items triés par `at` DÉCROISSANT (le plus récent en tête), badge
|
|
12
|
+
* de sévérité (pastille + libellé), horodatage formaté (heure locale) et
|
|
13
|
+
* message. Défilement vertical borné par `maxHeight`/`height`. a11y :
|
|
14
|
+
* `role="feed"` sur la liste, `role="article"` par item.
|
|
15
|
+
*
|
|
16
|
+
* Props obligatoires :
|
|
17
|
+
* data EventFeedPanelEvent[] - tableau d'événements
|
|
18
|
+
*
|
|
19
|
+
* Props optionnelles :
|
|
20
|
+
* label string - libellé accessible du flux
|
|
21
|
+
* maxHeight number - hauteur max en px (déclenche le scroll)
|
|
22
|
+
* height number - alias de maxHeight (hauteur fixe)
|
|
23
|
+
* class string
|
|
24
|
+
*/
|
|
25
|
+
export type EventFeedPanelSeverity =
|
|
26
|
+
| "info"
|
|
27
|
+
| "success"
|
|
28
|
+
| "warning"
|
|
29
|
+
| "error"
|
|
30
|
+
| (string & {});
|
|
31
|
+
|
|
32
|
+
export type EventFeedPanelEvent = {
|
|
33
|
+
/** Horodatage en millisecondes epoch (ou tout nombre croissant). */
|
|
34
|
+
at: number;
|
|
35
|
+
/** Catégorie libre de l'événement (« deploy », « alert »…). */
|
|
36
|
+
type: string;
|
|
37
|
+
/** Sévérité : pilote la couleur/pastille (sémantique feedback). */
|
|
38
|
+
severity: EventFeedPanelSeverity;
|
|
39
|
+
/** Message principal affiché. */
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<script lang="ts">
|
|
45
|
+
type EventFeedPanelProps = {
|
|
46
|
+
data: EventFeedPanelEvent[];
|
|
47
|
+
label?: string;
|
|
48
|
+
maxHeight?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
class?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
data = [],
|
|
55
|
+
label,
|
|
56
|
+
maxHeight,
|
|
57
|
+
height,
|
|
58
|
+
class: className
|
|
59
|
+
}: EventFeedPanelProps = $props();
|
|
60
|
+
|
|
61
|
+
// Sévérités connues (tons sémantiques) ; toute autre valeur retombe sur
|
|
62
|
+
// « neutral » (bordure forte) pour rester tokenisée sans casser le rendu.
|
|
63
|
+
const KNOWN_SEVERITIES = ["info", "success", "warning", "error"] as const;
|
|
64
|
+
|
|
65
|
+
function severityTone(severity: string): string {
|
|
66
|
+
return (KNOWN_SEVERITIES as readonly string[]).includes(severity) ? severity : "neutral";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Horodatage lisible : heure locale (HH:MM) ; tombe sur la valeur brute si
|
|
70
|
+
// le nombre n'est pas un epoch plausible.
|
|
71
|
+
function formatTime(at: number): string {
|
|
72
|
+
if (!Number.isFinite(at)) return "";
|
|
73
|
+
const date = new Date(at);
|
|
74
|
+
if (Number.isNaN(date.getTime())) return String(at);
|
|
75
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Items valides triés par `at` DÉCROISSANT (le plus récent en tête).
|
|
79
|
+
const items = $derived(
|
|
80
|
+
data
|
|
81
|
+
.filter((d) => d && Number.isFinite(d.at) && typeof d.message === "string")
|
|
82
|
+
.map((d, index) => ({
|
|
83
|
+
index,
|
|
84
|
+
datum: d,
|
|
85
|
+
tone: severityTone(String(d.severity)),
|
|
86
|
+
time: formatTime(d.at)
|
|
87
|
+
}))
|
|
88
|
+
.sort((a, b) => b.datum.at - a.datum.at)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const resolvedMaxHeight = $derived(maxHeight ?? height);
|
|
92
|
+
|
|
93
|
+
const scrollStyle = $derived(
|
|
94
|
+
typeof resolvedMaxHeight === "number" && Number.isFinite(resolvedMaxHeight)
|
|
95
|
+
? `max-height: ${resolvedMaxHeight}px;`
|
|
96
|
+
: undefined
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const classes = () => ["st-eventFeedPanel", className].filter(Boolean).join(" ");
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<div class={classes()}>
|
|
103
|
+
{#if label}
|
|
104
|
+
<p class="st-eventFeedPanel__label" id="st-eventFeedPanel-label">{label}</p>
|
|
105
|
+
{/if}
|
|
106
|
+
<ul
|
|
107
|
+
class="st-eventFeedPanel__list"
|
|
108
|
+
role="feed"
|
|
109
|
+
aria-label={label}
|
|
110
|
+
aria-busy="false"
|
|
111
|
+
style={scrollStyle}
|
|
112
|
+
>
|
|
113
|
+
{#each items as item (item.index)}
|
|
114
|
+
<li
|
|
115
|
+
class="st-eventFeedPanel__item st-eventFeedPanel__item--{item.tone}"
|
|
116
|
+
role="article"
|
|
117
|
+
aria-label={`${item.datum.type} — ${item.datum.message}`}
|
|
118
|
+
>
|
|
119
|
+
<span class="st-eventFeedPanel__badge st-eventFeedPanel__badge--{item.tone}" aria-hidden="true"></span>
|
|
120
|
+
<div class="st-eventFeedPanel__body">
|
|
121
|
+
<div class="st-eventFeedPanel__meta">
|
|
122
|
+
<span class="st-eventFeedPanel__type">{item.datum.type}</span>
|
|
123
|
+
<time class="st-eventFeedPanel__time">{item.time}</time>
|
|
124
|
+
</div>
|
|
125
|
+
<p class="st-eventFeedPanel__message">{item.datum.message}</p>
|
|
126
|
+
</div>
|
|
127
|
+
</li>
|
|
128
|
+
{/each}
|
|
129
|
+
</ul>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<style>
|
|
133
|
+
.st-eventFeedPanel {
|
|
134
|
+
color: var(--st-semantic-text-secondary);
|
|
135
|
+
display: block;
|
|
136
|
+
font-family: inherit;
|
|
137
|
+
width: 100%;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.st-eventFeedPanel__label {
|
|
141
|
+
color: var(--st-semantic-text-primary);
|
|
142
|
+
font-size: 0.75rem;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
margin: 0 0 var(--st-spacing-2, 0.5rem) 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.st-eventFeedPanel__list {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: var(--st-spacing-1, 0.25rem);
|
|
151
|
+
list-style: none;
|
|
152
|
+
margin: 0;
|
|
153
|
+
overflow-y: auto;
|
|
154
|
+
padding: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.st-eventFeedPanel__item {
|
|
158
|
+
align-items: flex-start;
|
|
159
|
+
background: var(--st-semantic-surface-subtle);
|
|
160
|
+
border-left: 3px solid var(--st-semantic-border-strong);
|
|
161
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
162
|
+
display: flex;
|
|
163
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
164
|
+
padding: var(--st-spacing-2, 0.5rem) var(--st-spacing-3, 0.75rem);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.st-eventFeedPanel__item--info { border-left-color: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
|
|
168
|
+
.st-eventFeedPanel__item--success { border-left-color: var(--st-semantic-feedback-success); }
|
|
169
|
+
.st-eventFeedPanel__item--warning { border-left-color: var(--st-semantic-feedback-warning); }
|
|
170
|
+
.st-eventFeedPanel__item--error { border-left-color: var(--st-semantic-feedback-error); }
|
|
171
|
+
.st-eventFeedPanel__item--neutral { border-left-color: var(--st-semantic-border-strong); }
|
|
172
|
+
|
|
173
|
+
.st-eventFeedPanel__badge {
|
|
174
|
+
border-radius: var(--st-radius-full, 9999px);
|
|
175
|
+
flex: none;
|
|
176
|
+
height: 0.5rem;
|
|
177
|
+
margin-top: 0.3125rem;
|
|
178
|
+
width: 0.5rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.st-eventFeedPanel__badge--info { background: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
|
|
182
|
+
.st-eventFeedPanel__badge--success { background: var(--st-semantic-feedback-success); }
|
|
183
|
+
.st-eventFeedPanel__badge--warning { background: var(--st-semantic-feedback-warning); }
|
|
184
|
+
.st-eventFeedPanel__badge--error { background: var(--st-semantic-feedback-error); }
|
|
185
|
+
.st-eventFeedPanel__badge--neutral { background: var(--st-semantic-border-strong); }
|
|
186
|
+
|
|
187
|
+
.st-eventFeedPanel__body {
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 0.125rem;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.st-eventFeedPanel__meta {
|
|
195
|
+
align-items: baseline;
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
198
|
+
justify-content: space-between;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.st-eventFeedPanel__type {
|
|
202
|
+
color: var(--st-semantic-text-primary);
|
|
203
|
+
font-size: 0.75rem;
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
letter-spacing: 0.02em;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.st-eventFeedPanel__time {
|
|
210
|
+
color: var(--st-semantic-text-secondary);
|
|
211
|
+
flex: none;
|
|
212
|
+
font-size: 0.6875rem;
|
|
213
|
+
font-variant-numeric: tabular-nums;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.st-eventFeedPanel__message {
|
|
217
|
+
color: var(--st-semantic-text-secondary);
|
|
218
|
+
font-size: 0.8125rem;
|
|
219
|
+
line-height: 1.35;
|
|
220
|
+
margin: 0;
|
|
221
|
+
}
|
|
222
|
+
</style>
|