@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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventFeedPanel — flux d'événements datés, scrollable (façon New Relic /
|
|
3
|
+
* observabilité). PANNEAU (liste), pas un graphe SVG : une liste verticale
|
|
4
|
+
* scrollable d'événements horodatés, chacun teinté/iconé par sa SÉVÉRITÉ
|
|
5
|
+
* (tons sémantiques --st-semantic-feedback-*). Distinct de TimelineChart
|
|
6
|
+
* (axe temporel SVG) : ici la temporalité est un simple tri décroissant, le
|
|
7
|
+
* rendu est du DOM (rôle `feed` / `list`) défilable.
|
|
8
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
9
|
+
*
|
|
10
|
+
* Modèle : items triés par `at` DÉCROISSANT (le plus récent en tête), badge
|
|
11
|
+
* de sévérité (pastille + libellé), horodatage formaté (heure locale) et
|
|
12
|
+
* message. Défilement vertical borné par `maxHeight`/`height`. a11y :
|
|
13
|
+
* `role="feed"` sur la liste, `role="article"` par item.
|
|
14
|
+
*
|
|
15
|
+
* Props obligatoires :
|
|
16
|
+
* data EventFeedPanelEvent[] - tableau d'événements
|
|
17
|
+
*
|
|
18
|
+
* Props optionnelles :
|
|
19
|
+
* label string - libellé accessible du flux
|
|
20
|
+
* maxHeight number - hauteur max en px (déclenche le scroll)
|
|
21
|
+
* height number - alias de maxHeight (hauteur fixe)
|
|
22
|
+
* class string
|
|
23
|
+
*/
|
|
24
|
+
export type EventFeedPanelSeverity = "info" | "success" | "warning" | "error" | (string & {});
|
|
25
|
+
export type EventFeedPanelEvent = {
|
|
26
|
+
/** Horodatage en millisecondes epoch (ou tout nombre croissant). */
|
|
27
|
+
at: number;
|
|
28
|
+
/** Catégorie libre de l'événement (« deploy », « alert »…). */
|
|
29
|
+
type: string;
|
|
30
|
+
/** Sévérité : pilote la couleur/pastille (sémantique feedback). */
|
|
31
|
+
severity: EventFeedPanelSeverity;
|
|
32
|
+
/** Message principal affiché. */
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
type EventFeedPanelProps = {
|
|
36
|
+
data: EventFeedPanelEvent[];
|
|
37
|
+
label?: string;
|
|
38
|
+
maxHeight?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
class?: string;
|
|
41
|
+
};
|
|
42
|
+
declare const EventFeedPanel: import("svelte").Component<EventFeedPanelProps, {}, "">;
|
|
43
|
+
type EventFeedPanel = ReturnType<typeof EventFeedPanel>;
|
|
44
|
+
export default EventFeedPanel;
|
|
45
|
+
//# sourceMappingURL=EventFeedPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventFeedPanel.svelte.d.ts","sourceRoot":"","sources":["../src/lib/EventFeedPanel.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,sBAAsB,GAC9B,MAAM,GACN,SAAS,GACT,SAAS,GACT,OAAO,GACP,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAAG;IAChC,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,EAAE,sBAAsB,CAAC;IACjC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+EJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventFeedPanel.test.d.ts","sourceRoot":"","sources":["../src/lib/EventFeedPanel.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { render } from "@testing-library/svelte";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import EventFeedPanel from "./EventFeedPanel.svelte";
|
|
4
|
+
const events = [
|
|
5
|
+
{ at: 1_700_000_000_000, type: "deploy", severity: "info", message: "Deployed v1.2.0" },
|
|
6
|
+
{ at: 1_700_000_300_000, type: "alert", severity: "error", message: "5xx spike" },
|
|
7
|
+
{ at: 1_700_000_100_000, type: "scale", severity: "success", message: "Scaled up" },
|
|
8
|
+
{ at: 1_700_000_200_000, type: "warn", severity: "warning", message: "High latency" }
|
|
9
|
+
];
|
|
10
|
+
const items = (container) => Array.from(container.querySelectorAll(".st-eventFeedPanel__item"));
|
|
11
|
+
const structuralClass = (el) => el.className.split(/\s+/)[0];
|
|
12
|
+
describe("EventFeedPanel", () => {
|
|
13
|
+
it("renders a feed with one article per event", () => {
|
|
14
|
+
const { container } = render(EventFeedPanel, { props: { data: events, label: "Activité" } });
|
|
15
|
+
expect(container.querySelector('[role="feed"]')).toBeTruthy();
|
|
16
|
+
expect(items(container).length).toBe(4);
|
|
17
|
+
expect(container.querySelectorAll('[role="article"]').length).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
it("sorts events by timestamp descending (newest first)", () => {
|
|
20
|
+
const { container } = render(EventFeedPanel, { props: { data: events, label: "A" } });
|
|
21
|
+
const messages = items(container).map((li) => li.querySelector(".st-eventFeedPanel__message")?.textContent);
|
|
22
|
+
expect(messages).toEqual(["5xx spike", "High latency", "Scaled up", "Deployed v1.2.0"]);
|
|
23
|
+
});
|
|
24
|
+
it("applies a severity tone class per item", () => {
|
|
25
|
+
const { container } = render(EventFeedPanel, { props: { data: events, label: "A" } });
|
|
26
|
+
expect(container.querySelector(".st-eventFeedPanel__item--info")).toBeTruthy();
|
|
27
|
+
expect(container.querySelector(".st-eventFeedPanel__item--success")).toBeTruthy();
|
|
28
|
+
expect(container.querySelector(".st-eventFeedPanel__item--warning")).toBeTruthy();
|
|
29
|
+
expect(container.querySelector(".st-eventFeedPanel__item--error")).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
it("falls back to the neutral tone for unknown severities", () => {
|
|
32
|
+
const { container } = render(EventFeedPanel, {
|
|
33
|
+
props: { data: [{ at: 1, type: "x", severity: "debug", message: "noise" }], label: "A" }
|
|
34
|
+
});
|
|
35
|
+
expect(container.querySelector(".st-eventFeedPanel__item--neutral")).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
it("drops non-finite timestamps before rendering", () => {
|
|
38
|
+
const { container } = render(EventFeedPanel, {
|
|
39
|
+
props: {
|
|
40
|
+
data: [
|
|
41
|
+
{ at: Number.NaN, type: "x", severity: "info", message: "bad" },
|
|
42
|
+
{ at: 2, type: "y", severity: "info", message: "good" }
|
|
43
|
+
],
|
|
44
|
+
label: "A"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
expect(items(container).length).toBe(1);
|
|
48
|
+
expect(container.querySelector(".st-eventFeedPanel__message")?.textContent).toBe("good");
|
|
49
|
+
});
|
|
50
|
+
it("constrains the scroll height when maxHeight is set", () => {
|
|
51
|
+
const { container } = render(EventFeedPanel, { props: { data: events, maxHeight: 180 } });
|
|
52
|
+
const list = container.querySelector(".st-eventFeedPanel__list");
|
|
53
|
+
expect(list.getAttribute("style")).toContain("max-height: 180px");
|
|
54
|
+
});
|
|
55
|
+
it("accepts height as an alias of maxHeight", () => {
|
|
56
|
+
const { container } = render(EventFeedPanel, { props: { data: events, height: 120 } });
|
|
57
|
+
const list = container.querySelector(".st-eventFeedPanel__list");
|
|
58
|
+
expect(list.getAttribute("style")).toContain("max-height: 120px");
|
|
59
|
+
});
|
|
60
|
+
it("merges a custom class onto the root", () => {
|
|
61
|
+
const { container } = render(EventFeedPanel, { props: { data: events, class: "mine" } });
|
|
62
|
+
const root = container.querySelector(".st-eventFeedPanel");
|
|
63
|
+
expect(structuralClass(root)).toBe("st-eventFeedPanel");
|
|
64
|
+
expect(root.classList.contains("mine")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* VectorFieldChart — champ de vecteurs (façon Highcharts « vector »). Grille
|
|
4
|
+
* de flèches dont la LONGUEUR est proportionnelle à la magnitude (`length`) et
|
|
5
|
+
* l'ORIENTATION suit la direction (`direction`, en degrés). Axes X/Y gradués
|
|
6
|
+
* (mêmes « niceTicks » que les autres charts). La couleur encode la magnitude
|
|
7
|
+
* (échelle category1..8). a11y : `role="img"` + liste accessible des points.
|
|
8
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
9
|
+
*
|
|
10
|
+
* Convention de direction : 0° pointe vers la droite (axe +X), l'angle
|
|
11
|
+
* augmente dans le sens trigonométrique (90° = vers le haut). Les longueurs
|
|
12
|
+
* sont normalisées sur la plus grande magnitude pour rester dans la cellule.
|
|
13
|
+
*
|
|
14
|
+
* Props obligatoires :
|
|
15
|
+
* data VectorFieldChartDatum[] - {x, y, length, direction}
|
|
16
|
+
*
|
|
17
|
+
* Props optionnelles :
|
|
18
|
+
* label string
|
|
19
|
+
* width number (défaut 640)
|
|
20
|
+
* height number (défaut 320)
|
|
21
|
+
* size number (longueur max d'une flèche en px ; défaut 26)
|
|
22
|
+
* class string
|
|
23
|
+
*/
|
|
24
|
+
export type VectorFieldChartTone =
|
|
25
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
26
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
27
|
+
|
|
28
|
+
export type VectorFieldChartDatum = {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
/** Magnitude (≥ 0) : pilote la longueur normalisée et la couleur. */
|
|
32
|
+
length: number;
|
|
33
|
+
/** Direction en DEGRÉS (0° = +X, sens trigonométrique). */
|
|
34
|
+
direction: number;
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
40
|
+
|
|
41
|
+
type VectorFieldChartProps = {
|
|
42
|
+
data: VectorFieldChartDatum[];
|
|
43
|
+
label?: string;
|
|
44
|
+
width?: number;
|
|
45
|
+
height?: number;
|
|
46
|
+
size?: number;
|
|
47
|
+
class?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let {
|
|
51
|
+
data = [],
|
|
52
|
+
label,
|
|
53
|
+
width = 640,
|
|
54
|
+
height = 320,
|
|
55
|
+
size = 26,
|
|
56
|
+
class: className
|
|
57
|
+
}: VectorFieldChartProps = $props();
|
|
58
|
+
|
|
59
|
+
const MARGIN = { top: 16, right: 18, bottom: 36, left: 48 };
|
|
60
|
+
|
|
61
|
+
const TONES = [
|
|
62
|
+
"category1","category2","category3","category4",
|
|
63
|
+
"category5","category6","category7","category8"
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
67
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
|
|
68
|
+
return [Number.isFinite(max) ? max : 0];
|
|
69
|
+
}
|
|
70
|
+
const range = max - min;
|
|
71
|
+
const rough = range / Math.max(target - 1, 1);
|
|
72
|
+
const pow = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
73
|
+
const norm = rough / pow;
|
|
74
|
+
let step: number;
|
|
75
|
+
if (norm < 1.5) step = pow;
|
|
76
|
+
else if (norm < 3) step = 2 * pow;
|
|
77
|
+
else if (norm < 7) step = 5 * pow;
|
|
78
|
+
else step = 10 * pow;
|
|
79
|
+
const start = Math.floor(min / step) * step;
|
|
80
|
+
const end = Math.ceil(max / step) * step;
|
|
81
|
+
const ticks: number[] = [];
|
|
82
|
+
for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toFixed(10)));
|
|
83
|
+
return ticks;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
|
|
87
|
+
if (d1 === d0) return r0;
|
|
88
|
+
return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fmt(v: number): string {
|
|
92
|
+
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
|
|
93
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let hoveredKey: string | null = $state(null);
|
|
97
|
+
|
|
98
|
+
// Points valides : coordonnées finies, magnitude finie ≥ 0.
|
|
99
|
+
const validData = $derived(
|
|
100
|
+
data.filter(
|
|
101
|
+
(d) =>
|
|
102
|
+
d &&
|
|
103
|
+
Number.isFinite(d.x) &&
|
|
104
|
+
Number.isFinite(d.y) &&
|
|
105
|
+
Number.isFinite(d.length) &&
|
|
106
|
+
d.length >= 0 &&
|
|
107
|
+
Number.isFinite(d.direction)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const scales = $derived.by(() => {
|
|
112
|
+
const xs = validData.map((d) => d.x);
|
|
113
|
+
const ys = validData.map((d) => d.y);
|
|
114
|
+
const xTicks = niceTicks(Math.min(...xs), Math.max(...xs));
|
|
115
|
+
const yTicks = niceTicks(Math.min(...ys), Math.max(...ys));
|
|
116
|
+
const plotW = Math.max(width - MARGIN.left - MARGIN.right, 1);
|
|
117
|
+
const plotH = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
|
|
118
|
+
return {
|
|
119
|
+
xTicks, yTicks,
|
|
120
|
+
xMin: xTicks[0], xMax: xTicks[xTicks.length - 1],
|
|
121
|
+
yMin: yTicks[0], yMax: yTicks[yTicks.length - 1],
|
|
122
|
+
plotW, plotH
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const maxLength = $derived(
|
|
127
|
+
validData.reduce((max, d) => (d.length > max ? d.length : max), 0)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Une flèche par point : segment (base → pointe) + 2 traits de pointe.
|
|
131
|
+
const arrows = $derived.by(() => {
|
|
132
|
+
const { xMin, xMax, yMin, yMax, plotW, plotH } = scales;
|
|
133
|
+
const max = maxLength > 0 ? maxLength : 1;
|
|
134
|
+
return validData.map((d, i) => {
|
|
135
|
+
const cx = MARGIN.left + scaleLinear(d.x, xMin, xMax, 0, plotW);
|
|
136
|
+
const cy = MARGIN.top + scaleLinear(d.y, yMin, yMax, plotH, 0);
|
|
137
|
+
// Longueur normalisée (la plus grande magnitude = `size`).
|
|
138
|
+
const len = (d.length / max) * size;
|
|
139
|
+
// Direction trigonométrique : +X à droite, +Y vers le haut (donc -sin en
|
|
140
|
+
// espace écran où l'axe Y descend).
|
|
141
|
+
const rad = (d.direction * Math.PI) / 180;
|
|
142
|
+
const dx = Math.cos(rad) * len;
|
|
143
|
+
const dy = -Math.sin(rad) * len;
|
|
144
|
+
// Flèche centrée sur le point : moitié de chaque côté.
|
|
145
|
+
const x1 = cx - dx / 2;
|
|
146
|
+
const y1 = cy - dy / 2;
|
|
147
|
+
const x2 = cx + dx / 2;
|
|
148
|
+
const y2 = cy + dy / 2;
|
|
149
|
+
// Pointe : deux petits traits en arrière de la tête.
|
|
150
|
+
const head = Math.min(Math.max(len * 0.28, 3), 8);
|
|
151
|
+
const headAngle = (28 * Math.PI) / 180;
|
|
152
|
+
const baseAngle = Math.atan2(y2 - y1, x2 - x1);
|
|
153
|
+
const hx1 = x2 - head * Math.cos(baseAngle - headAngle);
|
|
154
|
+
const hy1 = y2 - head * Math.sin(baseAngle - headAngle);
|
|
155
|
+
const hx2 = x2 - head * Math.cos(baseAngle + headAngle);
|
|
156
|
+
const hy2 = y2 - head * Math.sin(baseAngle + headAngle);
|
|
157
|
+
// Ton catégoriel par bin de magnitude (max → category8).
|
|
158
|
+
const bin = Math.min(Math.floor((d.length / max) * TONES.length), TONES.length - 1);
|
|
159
|
+
return {
|
|
160
|
+
key: `${i}`,
|
|
161
|
+
datum: d,
|
|
162
|
+
cx, cy, x1, y1, x2, y2, hx1, hy1, hx2, hy2,
|
|
163
|
+
tone: TONES[Math.max(0, bin)]
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const dataValueItems = $derived(
|
|
169
|
+
validData.map(
|
|
170
|
+
(d) => `x ${d.x}, y ${d.y} · |v| ${fmt(d.length)} @ ${fmt(d.direction)}°`
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
function handlePointerMove(event: PointerEvent) {
|
|
175
|
+
const target = event.target;
|
|
176
|
+
if (!(target instanceof Element)) {
|
|
177
|
+
hoveredKey = null;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
hoveredKey = target.getAttribute("data-chart-key");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hoveredArrow = $derived.by(() => {
|
|
184
|
+
if (hoveredKey === null) return null;
|
|
185
|
+
return arrows.find((a) => a.key === hoveredKey) ?? null;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const classes = () => ["st-vectorFieldChart", className].filter(Boolean).join(" ");
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<div class={classes()}>
|
|
192
|
+
<div
|
|
193
|
+
class="st-vectorFieldChart__visual"
|
|
194
|
+
role="img"
|
|
195
|
+
aria-label={label}
|
|
196
|
+
onpointermove={handlePointerMove}
|
|
197
|
+
onpointerleave={() => (hoveredKey = null)}
|
|
198
|
+
>
|
|
199
|
+
<svg
|
|
200
|
+
viewBox="0 0 {width} {height}"
|
|
201
|
+
preserveAspectRatio="xMidYMid meet"
|
|
202
|
+
width="100%"
|
|
203
|
+
height="100%"
|
|
204
|
+
focusable="false"
|
|
205
|
+
aria-hidden="true"
|
|
206
|
+
>
|
|
207
|
+
<!-- gridlines + ticks Y -->
|
|
208
|
+
{#each scales.yTicks as t (t)}
|
|
209
|
+
{@const y = MARGIN.top + scaleLinear(t, scales.yMin, scales.yMax, scales.plotH, 0)}
|
|
210
|
+
<line class="st-vectorFieldChart__grid" x1={MARGIN.left} x2={width - MARGIN.right} y1={y} y2={y} />
|
|
211
|
+
<text class="st-vectorFieldChart__tick" x={MARGIN.left - 6} y={y} text-anchor="end" dominant-baseline="middle">{fmt(t)}</text>
|
|
212
|
+
{/each}
|
|
213
|
+
<!-- ticks X -->
|
|
214
|
+
{#each scales.xTicks as t (t)}
|
|
215
|
+
{@const x = MARGIN.left + scaleLinear(t, scales.xMin, scales.xMax, 0, scales.plotW)}
|
|
216
|
+
<text class="st-vectorFieldChart__tick" x={x} y={height - MARGIN.bottom + 16} text-anchor="middle">{fmt(t)}</text>
|
|
217
|
+
{/each}
|
|
218
|
+
|
|
219
|
+
<!-- axes -->
|
|
220
|
+
<line class="st-vectorFieldChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
|
|
221
|
+
<line class="st-vectorFieldChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
|
|
222
|
+
|
|
223
|
+
<!-- une flèche par point : segment + pointe -->
|
|
224
|
+
{#each arrows as a (a.key)}
|
|
225
|
+
<g
|
|
226
|
+
class="st-vectorFieldChart__arrow st-vectorFieldChart__arrow--{a.tone}"
|
|
227
|
+
class:st-vectorFieldChart__arrow--dim={hoveredKey !== null && hoveredKey !== a.key}
|
|
228
|
+
>
|
|
229
|
+
<line class="st-vectorFieldChart__shaft" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} data-chart-key={a.key} />
|
|
230
|
+
<line class="st-vectorFieldChart__head" x1={a.x2} y1={a.y2} x2={a.hx1} y2={a.hy1} />
|
|
231
|
+
<line class="st-vectorFieldChart__head" x1={a.x2} y1={a.y2} x2={a.hx2} y2={a.hy2} />
|
|
232
|
+
</g>
|
|
233
|
+
{/each}
|
|
234
|
+
</svg>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<ChartDataList label={label ?? "vector field"} items={dataValueItems} />
|
|
238
|
+
|
|
239
|
+
{#if hoveredArrow}
|
|
240
|
+
{@const a = hoveredArrow}
|
|
241
|
+
<div
|
|
242
|
+
class="st-vectorFieldChart__tooltip"
|
|
243
|
+
role="presentation"
|
|
244
|
+
style="left: {(a.cx / width) * 100}%; top: {(a.cy / height) * 100}%"
|
|
245
|
+
>
|
|
246
|
+
<span class="st-vectorFieldChart__tooltipLabel">x {a.datum.x} · y {a.datum.y}</span>
|
|
247
|
+
<span class="st-vectorFieldChart__tooltipValue">|v| {fmt(a.datum.length)} @ {fmt(a.datum.direction)}°</span>
|
|
248
|
+
</div>
|
|
249
|
+
{/if}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<style>
|
|
253
|
+
.st-vectorFieldChart {
|
|
254
|
+
color: var(--st-semantic-text-secondary);
|
|
255
|
+
display: block;
|
|
256
|
+
font-family: inherit;
|
|
257
|
+
position: relative;
|
|
258
|
+
width: 100%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.st-vectorFieldChart svg {
|
|
262
|
+
display: block;
|
|
263
|
+
overflow: visible;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.st-vectorFieldChart__visual {
|
|
267
|
+
display: block;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.st-vectorFieldChart__grid {
|
|
271
|
+
opacity: 0.7;
|
|
272
|
+
stroke: var(--st-semantic-border-subtle);
|
|
273
|
+
stroke-dasharray: 2 3;
|
|
274
|
+
stroke-width: 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.st-vectorFieldChart__axis {
|
|
278
|
+
stroke: var(--st-semantic-border-subtle);
|
|
279
|
+
stroke-width: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.st-vectorFieldChart__tick {
|
|
283
|
+
fill: var(--st-semantic-text-secondary);
|
|
284
|
+
font-size: 0.6875rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.st-vectorFieldChart__arrow {
|
|
288
|
+
transition: opacity 120ms ease;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.st-vectorFieldChart__arrow--dim {
|
|
292
|
+
opacity: 0.35;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.st-vectorFieldChart__shaft {
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
stroke: currentColor;
|
|
298
|
+
stroke-linecap: round;
|
|
299
|
+
stroke-width: 2;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.st-vectorFieldChart__head {
|
|
303
|
+
stroke: currentColor;
|
|
304
|
+
stroke-linecap: round;
|
|
305
|
+
stroke-width: 2;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.st-vectorFieldChart__arrow--category1 { color: var(--st-semantic-data-category1); }
|
|
309
|
+
.st-vectorFieldChart__arrow--category2 { color: var(--st-semantic-data-category2); }
|
|
310
|
+
.st-vectorFieldChart__arrow--category3 { color: var(--st-semantic-data-category3); }
|
|
311
|
+
.st-vectorFieldChart__arrow--category4 { color: var(--st-semantic-data-category4); }
|
|
312
|
+
.st-vectorFieldChart__arrow--category5 { color: var(--st-semantic-data-category5); }
|
|
313
|
+
.st-vectorFieldChart__arrow--category6 { color: var(--st-semantic-data-category6); }
|
|
314
|
+
.st-vectorFieldChart__arrow--category7 { color: var(--st-semantic-data-category7); }
|
|
315
|
+
.st-vectorFieldChart__arrow--category8 { color: var(--st-semantic-data-category8); }
|
|
316
|
+
|
|
317
|
+
.st-vectorFieldChart__tooltip {
|
|
318
|
+
background: var(--st-semantic-surface-inverse);
|
|
319
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
320
|
+
color: var(--st-semantic-text-inverse);
|
|
321
|
+
display: inline-flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
font-size: 0.75rem;
|
|
324
|
+
gap: 0.125rem;
|
|
325
|
+
line-height: 1.2;
|
|
326
|
+
padding: 0.375rem 0.5rem;
|
|
327
|
+
pointer-events: none;
|
|
328
|
+
position: absolute;
|
|
329
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
330
|
+
white-space: nowrap;
|
|
331
|
+
z-index: 1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.st-vectorFieldChart__tooltipLabel {
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.st-vectorFieldChart__tooltipValue {
|
|
339
|
+
opacity: 0.85;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@media (prefers-reduced-motion: reduce) {
|
|
343
|
+
.st-vectorFieldChart__arrow {
|
|
344
|
+
transition: none;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VectorFieldChart — champ de vecteurs (façon Highcharts « vector »). Grille
|
|
3
|
+
* de flèches dont la LONGUEUR est proportionnelle à la magnitude (`length`) et
|
|
4
|
+
* l'ORIENTATION suit la direction (`direction`, en degrés). Axes X/Y gradués
|
|
5
|
+
* (mêmes « niceTicks » que les autres charts). La couleur encode la magnitude
|
|
6
|
+
* (échelle category1..8). a11y : `role="img"` + liste accessible des points.
|
|
7
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
8
|
+
*
|
|
9
|
+
* Convention de direction : 0° pointe vers la droite (axe +X), l'angle
|
|
10
|
+
* augmente dans le sens trigonométrique (90° = vers le haut). Les longueurs
|
|
11
|
+
* sont normalisées sur la plus grande magnitude pour rester dans la cellule.
|
|
12
|
+
*
|
|
13
|
+
* Props obligatoires :
|
|
14
|
+
* data VectorFieldChartDatum[] - {x, y, length, direction}
|
|
15
|
+
*
|
|
16
|
+
* Props optionnelles :
|
|
17
|
+
* label string
|
|
18
|
+
* width number (défaut 640)
|
|
19
|
+
* height number (défaut 320)
|
|
20
|
+
* size number (longueur max d'une flèche en px ; défaut 26)
|
|
21
|
+
* class string
|
|
22
|
+
*/
|
|
23
|
+
export type VectorFieldChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
24
|
+
export type VectorFieldChartDatum = {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
/** Magnitude (≥ 0) : pilote la longueur normalisée et la couleur. */
|
|
28
|
+
length: number;
|
|
29
|
+
/** Direction en DEGRÉS (0° = +X, sens trigonométrique). */
|
|
30
|
+
direction: number;
|
|
31
|
+
};
|
|
32
|
+
type VectorFieldChartProps = {
|
|
33
|
+
data: VectorFieldChartDatum[];
|
|
34
|
+
label?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
size?: number;
|
|
38
|
+
class?: string;
|
|
39
|
+
};
|
|
40
|
+
declare const VectorFieldChart: import("svelte").Component<VectorFieldChartProps, {}, "">;
|
|
41
|
+
type VectorFieldChart = ReturnType<typeof VectorFieldChart>;
|
|
42
|
+
export default VectorFieldChart;
|
|
43
|
+
//# sourceMappingURL=VectorFieldChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VectorFieldChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/VectorFieldChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,oBAAoB,GAC5B,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,qBAAqB,EAAE,CAAC;IAC9B,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;AAiMJ,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VectorFieldChart.test.d.ts","sourceRoot":"","sources":["../src/lib/VectorFieldChart.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { render } from "@testing-library/svelte";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import VectorFieldChart from "./VectorFieldChart.svelte";
|
|
4
|
+
const field = [
|
|
5
|
+
{ x: 0, y: 0, length: 1, direction: 0 },
|
|
6
|
+
{ x: 1, y: 0, length: 2, direction: 90 },
|
|
7
|
+
{ x: 0, y: 1, length: 3, direction: 180 },
|
|
8
|
+
{ x: 1, y: 1, length: 4, direction: 270 }
|
|
9
|
+
];
|
|
10
|
+
const arrows = (container) => Array.from(container.querySelectorAll(".st-vectorFieldChart__arrow"));
|
|
11
|
+
const listItems = (container) => Array.from(container.querySelectorAll(".st-chartDataList li")).map((n) => n.textContent?.trim());
|
|
12
|
+
const structuralClass = (el) => el.className.split(/\s+/)[0];
|
|
13
|
+
describe("VectorFieldChart", () => {
|
|
14
|
+
it("renders an img role and one arrow per datum", () => {
|
|
15
|
+
const { container } = render(VectorFieldChart, { props: { data: field, label: "Vent" } });
|
|
16
|
+
expect(container.querySelector('[role="img"]')).toBeTruthy();
|
|
17
|
+
expect(arrows(container).length).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
it("draws a shaft and two head segments per arrow", () => {
|
|
20
|
+
const { container } = render(VectorFieldChart, { props: { data: field, label: "V" } });
|
|
21
|
+
expect(container.querySelectorAll(".st-vectorFieldChart__shaft").length).toBe(4);
|
|
22
|
+
expect(container.querySelectorAll(".st-vectorFieldChart__head").length).toBe(8);
|
|
23
|
+
});
|
|
24
|
+
it("colours arrows by magnitude bin (largest magnitude → category8)", () => {
|
|
25
|
+
const { container } = render(VectorFieldChart, { props: { data: field, label: "V" } });
|
|
26
|
+
const last = arrows(container).at(-1);
|
|
27
|
+
expect(last.classList.contains("st-vectorFieldChart__arrow--category8")).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it("renders graduated X/Y axes with nice ticks", () => {
|
|
30
|
+
const { container } = render(VectorFieldChart, { props: { data: field, label: "V" } });
|
|
31
|
+
expect(container.querySelectorAll(".st-vectorFieldChart__axis").length).toBe(2);
|
|
32
|
+
expect(container.querySelectorAll(".st-vectorFieldChart__tick").length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
it("lists every datum in the accessible data list", () => {
|
|
35
|
+
const { container } = render(VectorFieldChart, {
|
|
36
|
+
props: { data: [{ x: 2, y: 3, length: 5, direction: 45 }], label: "V" }
|
|
37
|
+
});
|
|
38
|
+
expect(listItems(container)[0]).toBe("x 2, y 3 · |v| 5 @ 45°");
|
|
39
|
+
});
|
|
40
|
+
it("drops non-finite or negative-magnitude points before rendering", () => {
|
|
41
|
+
const { container } = render(VectorFieldChart, {
|
|
42
|
+
props: {
|
|
43
|
+
data: [
|
|
44
|
+
{ x: Number.NaN, y: 0, length: 1, direction: 0 },
|
|
45
|
+
{ x: 0, y: 0, length: -1, direction: 0 },
|
|
46
|
+
{ x: 1, y: 1, length: 2, direction: 0 }
|
|
47
|
+
],
|
|
48
|
+
label: "V"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
expect(arrows(container).length).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
it("merges a custom class onto the root", () => {
|
|
54
|
+
const { container } = render(VectorFieldChart, { props: { data: field, class: "mine" } });
|
|
55
|
+
const root = container.querySelector(".st-vectorFieldChart");
|
|
56
|
+
expect(structuralClass(root)).toBe("st-vectorFieldChart");
|
|
57
|
+
expect(root.classList.contains("mine")).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -99,11 +99,15 @@ export { default as GaugeChart } from "./GaugeChart.svelte";
|
|
|
99
99
|
export { default as SolidGaugeChart } from "./SolidGaugeChart.svelte";
|
|
100
100
|
export { default as StateTimelineChart } from "./StateTimelineChart.svelte";
|
|
101
101
|
export { default as StatusHistoryChart } from "./StatusHistoryChart.svelte";
|
|
102
|
+
export { default as EventFeedPanel } from "./EventFeedPanel.svelte";
|
|
103
|
+
export { default as VectorFieldChart } from "./VectorFieldChart.svelte";
|
|
102
104
|
export { default as WaffleChart } from "./WaffleChart.svelte";
|
|
103
105
|
export { default as RibbonChart } from "./RibbonChart.svelte";
|
|
104
106
|
export { default as AnomalySwimLaneChart } from "./AnomalySwimLaneChart.svelte";
|
|
105
107
|
export { default as FlamegraphChart } from "./FlamegraphChart.svelte";
|
|
106
108
|
export { default as TraceWaterfallChart } from "./TraceWaterfallChart.svelte";
|
|
109
|
+
export { default as DecompositionTreeChart } from "./DecompositionTreeChart.svelte";
|
|
110
|
+
export { default as Density2DChart } from "./Density2DChart.svelte";
|
|
107
111
|
export { default as GeoMap } from "./GeoMap.svelte";
|
|
108
112
|
export { default as Header } from "./Header.svelte";
|
|
109
113
|
export { default as HeatmapChart } from "./HeatmapChart.svelte";
|
|
@@ -229,11 +233,15 @@ export type { GaugeChartTone, GaugeChartThreshold, GaugeChartFormat } from "./Ga
|
|
|
229
233
|
export type { SolidGaugeTone, SolidGaugeThreshold, SolidGaugeFormat } from "./SolidGaugeChart.svelte";
|
|
230
234
|
export type { StateTimelineTone, StateTimelineSegment, StateTimelineSeries } from "./StateTimelineChart.svelte";
|
|
231
235
|
export type { StatusHistoryTone, StatusHistoryBucket, StatusHistorySeries } from "./StatusHistoryChart.svelte";
|
|
236
|
+
export type { EventFeedPanelSeverity, EventFeedPanelEvent } from "./EventFeedPanel.svelte";
|
|
237
|
+
export type { VectorFieldChartTone, VectorFieldChartDatum } from "./VectorFieldChart.svelte";
|
|
232
238
|
export type { WaffleTone, WaffleChartDatum } from "./WaffleChart.svelte";
|
|
233
239
|
export type { RibbonChartTone, RibbonChartDatum } from "./RibbonChart.svelte";
|
|
234
240
|
export type { AnomalySwimLaneTone, AnomalySwimLaneBucket, AnomalySwimLaneSeries } from "./AnomalySwimLaneChart.svelte";
|
|
235
241
|
export type { FlamegraphNode } from "./FlamegraphChart.svelte";
|
|
236
242
|
export type { TraceSpan } from "./TraceWaterfallChart.svelte";
|
|
243
|
+
export type { DecompositionTreeNode, DecompositionTreeLevel, DecompositionTreeData } from "./DecompositionTreeChart.svelte";
|
|
244
|
+
export type { Density2DTone, Density2DPoint } from "./Density2DChart.svelte";
|
|
237
245
|
export type { FunnelChartTone, FunnelChartDatum } from "./FunnelChart.svelte";
|
|
238
246
|
export type { ViolinChartDatum, ViolinChartTone } from "./ViolinChart.svelte";
|
|
239
247
|
export type { WaterfallType, WaterfallChartDatum } from "./WaterfallChart.svelte";
|