@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.
- package/dist/ContourChart.svelte +398 -0
- package/dist/ContourChart.svelte.d.ts +44 -0
- package/dist/ContourChart.svelte.d.ts.map +1 -0
- package/dist/ContourChart.test.d.ts +2 -0
- package/dist/ContourChart.test.d.ts.map +1 -0
- package/dist/ContourChart.test.js +59 -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/WindBarbChart.svelte +367 -0
- package/dist/WindBarbChart.svelte.d.ts +45 -0
- package/dist/WindBarbChart.svelte.d.ts.map +1 -0
- package/dist/WindBarbChart.test.d.ts +2 -0
- package/dist/WindBarbChart.test.d.ts.map +1 -0
- package/dist/WindBarbChart.test.js +65 -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,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
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* WindBarbChart — barbes de vent (symboles météo : vitesse + direction) le
|
|
4
|
+
* long d'un axe temporel (façon Highcharts « windbarb »). Une barbe par point :
|
|
5
|
+
* la hampe est orientée selon `direction` (en degrés) et les barbules encodent
|
|
6
|
+
* la `speed` (en nœuds) par paliers météo standard (demi-barbule = 5 kt,
|
|
7
|
+
* barbule pleine = 10 kt, fanion = 50 kt). La couleur encode la vitesse sur
|
|
8
|
+
* l'échelle catégorielle continue category1..8. a11y : `role="img"` +
|
|
9
|
+
* `data-chart-key` + liste accessible des points.
|
|
10
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
11
|
+
*
|
|
12
|
+
* Convention de direction (météo) : `direction` est la direction d'où vient le
|
|
13
|
+
* vent, en degrés (0° = Nord). La hampe pointe vers la source et les barbules
|
|
14
|
+
* sont placées du côté droit de la hampe (hémisphère nord).
|
|
15
|
+
*
|
|
16
|
+
* Props obligatoires :
|
|
17
|
+
* data WindBarbChartDatum[] - {at, speed, direction}
|
|
18
|
+
*
|
|
19
|
+
* Props optionnelles :
|
|
20
|
+
* label string
|
|
21
|
+
* width number (défaut 640)
|
|
22
|
+
* height number (défaut 160)
|
|
23
|
+
* size number (longueur de la hampe en px ; défaut 32)
|
|
24
|
+
* class string
|
|
25
|
+
*/
|
|
26
|
+
export type WindBarbChartTone =
|
|
27
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
28
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
29
|
+
|
|
30
|
+
export type WindBarbChartDatum = {
|
|
31
|
+
/** Position sur l'axe temporel (timestamp ou index). */
|
|
32
|
+
at: number;
|
|
33
|
+
/** Vitesse du vent en nœuds (≥ 0) : pilote les barbules et la couleur. */
|
|
34
|
+
speed: number;
|
|
35
|
+
/** Direction (d'où vient le vent) en DEGRÉS (0° = Nord). */
|
|
36
|
+
direction: number;
|
|
37
|
+
};
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
42
|
+
|
|
43
|
+
type WindBarbChartProps = {
|
|
44
|
+
data: WindBarbChartDatum[];
|
|
45
|
+
label?: string;
|
|
46
|
+
width?: number;
|
|
47
|
+
height?: number;
|
|
48
|
+
size?: number;
|
|
49
|
+
class?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let {
|
|
53
|
+
data = [],
|
|
54
|
+
label,
|
|
55
|
+
width = 640,
|
|
56
|
+
height = 160,
|
|
57
|
+
size = 32,
|
|
58
|
+
class: className
|
|
59
|
+
}: WindBarbChartProps = $props();
|
|
60
|
+
|
|
61
|
+
const MARGIN = { top: 16, right: 18, bottom: 36, left: 24 };
|
|
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
|
+
// Décompose une vitesse (kt) en barbules météo : fanions (50), pleines (10),
|
|
99
|
+
// demi (5). On arrondit au plus proche multiple de 5 (convention standard).
|
|
100
|
+
function barbCounts(speed: number): { flags: number; full: number; half: number } {
|
|
101
|
+
let rounded = Math.round(speed / 5) * 5;
|
|
102
|
+
if (rounded < 0) rounded = 0;
|
|
103
|
+
const flags = Math.floor(rounded / 50);
|
|
104
|
+
rounded -= flags * 50;
|
|
105
|
+
const full = Math.floor(rounded / 10);
|
|
106
|
+
rounded -= full * 10;
|
|
107
|
+
const half = Math.floor(rounded / 5);
|
|
108
|
+
return { flags, full, half };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let hoveredKey: string | null = $state(null);
|
|
112
|
+
|
|
113
|
+
// Points valides : position finie, vitesse finie ≥ 0, direction finie.
|
|
114
|
+
const validData = $derived(
|
|
115
|
+
data.filter(
|
|
116
|
+
(d) =>
|
|
117
|
+
d &&
|
|
118
|
+
Number.isFinite(d.at) &&
|
|
119
|
+
Number.isFinite(d.speed) &&
|
|
120
|
+
d.speed >= 0 &&
|
|
121
|
+
Number.isFinite(d.direction)
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const scales = $derived.by(() => {
|
|
126
|
+
const ats = validData.map((d) => d.at);
|
|
127
|
+
const xTicks = niceTicks(Math.min(...ats), Math.max(...ats));
|
|
128
|
+
const plotW = Math.max(width - MARGIN.left - MARGIN.right, 1);
|
|
129
|
+
return {
|
|
130
|
+
xTicks,
|
|
131
|
+
xMin: xTicks[0], xMax: xTicks[xTicks.length - 1],
|
|
132
|
+
plotW
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const maxSpeed = $derived(
|
|
137
|
+
validData.reduce((max, d) => (d.speed > max ? d.speed : max), 0)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Ligne de base des barbes : centrée verticalement dans la zone de tracé.
|
|
141
|
+
const baseY = $derived(MARGIN.top + (height - MARGIN.top - MARGIN.bottom) / 2);
|
|
142
|
+
|
|
143
|
+
// Une barbe par point : hampe orientée + barbules le long de la hampe.
|
|
144
|
+
const barbs = $derived.by(() => {
|
|
145
|
+
const { xMin, xMax, plotW } = scales;
|
|
146
|
+
const max = maxSpeed > 0 ? maxSpeed : 1;
|
|
147
|
+
return validData.map((d, i) => {
|
|
148
|
+
const cx = MARGIN.left + scaleLinear(d.at, xMin, xMax, 0, plotW);
|
|
149
|
+
const cy = baseY;
|
|
150
|
+
// Direction météo : 0° = Nord (vers le haut). On oriente la hampe vers la
|
|
151
|
+
// source du vent. Angle écran (Y descend) : haut = -90°.
|
|
152
|
+
const rad = ((d.direction - 90) * Math.PI) / 180;
|
|
153
|
+
const ux = Math.cos(rad);
|
|
154
|
+
const uy = Math.sin(rad);
|
|
155
|
+
// Pointe de la hampe (extrémité libre, côté source du vent).
|
|
156
|
+
const tipX = cx + ux * size;
|
|
157
|
+
const tipY = cy + uy * size;
|
|
158
|
+
// Vecteur perpendiculaire (côté barbules, à droite de la hampe).
|
|
159
|
+
const px = -uy;
|
|
160
|
+
const py = ux;
|
|
161
|
+
const { flags, full, half } = barbCounts(d.speed);
|
|
162
|
+
const barbLen = size * 0.42;
|
|
163
|
+
const halfLen = barbLen * 0.55;
|
|
164
|
+
const spacing = size * 0.16;
|
|
165
|
+
const ticks: { x1: number; y1: number; x2: number; y2: number; kind: "full" | "half" | "flag1" | "flag2" }[] = [];
|
|
166
|
+
// On place les symboles depuis la pointe vers la base.
|
|
167
|
+
let along = size - spacing;
|
|
168
|
+
for (let f = 0; f < flags; f++) {
|
|
169
|
+
const aX = cx + ux * along;
|
|
170
|
+
const aY = cy + uy * along;
|
|
171
|
+
const bAlong = along - spacing;
|
|
172
|
+
const bX = cx + ux * bAlong;
|
|
173
|
+
const bY = cy + uy * bAlong;
|
|
174
|
+
const tipFX = aX + px * barbLen;
|
|
175
|
+
const tipFY = aY + py * barbLen;
|
|
176
|
+
ticks.push({ x1: aX, y1: aY, x2: tipFX, y2: tipFY, kind: "flag1" });
|
|
177
|
+
ticks.push({ x1: bX, y1: bY, x2: tipFX, y2: tipFY, kind: "flag2" });
|
|
178
|
+
along = bAlong - spacing;
|
|
179
|
+
}
|
|
180
|
+
for (let f = 0; f < full; f++) {
|
|
181
|
+
const aX = cx + ux * along;
|
|
182
|
+
const aY = cy + uy * along;
|
|
183
|
+
ticks.push({ x1: aX, y1: aY, x2: aX + px * barbLen, y2: aY + py * barbLen, kind: "full" });
|
|
184
|
+
along -= spacing;
|
|
185
|
+
}
|
|
186
|
+
for (let h = 0; h < half; h++) {
|
|
187
|
+
const aX = cx + ux * along;
|
|
188
|
+
const aY = cy + uy * along;
|
|
189
|
+
ticks.push({ x1: aX, y1: aY, x2: aX + px * halfLen, y2: aY + py * halfLen, kind: "half" });
|
|
190
|
+
along -= spacing;
|
|
191
|
+
}
|
|
192
|
+
// Ton catégoriel par bin de vitesse (max → category8).
|
|
193
|
+
const bin = Math.min(Math.floor((d.speed / max) * TONES.length), TONES.length - 1);
|
|
194
|
+
return {
|
|
195
|
+
key: `${i}`,
|
|
196
|
+
datum: d,
|
|
197
|
+
cx, cy, tipX, tipY, ticks,
|
|
198
|
+
tone: TONES[Math.max(0, bin)]
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const dataValueItems = $derived(
|
|
204
|
+
validData.map((d) => `${fmt(d.at)} · ${fmt(d.speed)} kt @ ${fmt(d.direction)}°`)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
function handlePointerMove(event: PointerEvent) {
|
|
208
|
+
const target = event.target;
|
|
209
|
+
if (!(target instanceof Element)) {
|
|
210
|
+
hoveredKey = null;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
hoveredKey = target.getAttribute("data-chart-key");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hoveredBarb = $derived.by(() => {
|
|
217
|
+
if (hoveredKey === null) return null;
|
|
218
|
+
return barbs.find((b) => b.key === hoveredKey) ?? null;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const classes = () => ["st-windBarbChart", className].filter(Boolean).join(" ");
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<div class={classes()}>
|
|
225
|
+
<div
|
|
226
|
+
class="st-windBarbChart__visual"
|
|
227
|
+
role="img"
|
|
228
|
+
aria-label={label}
|
|
229
|
+
onpointermove={handlePointerMove}
|
|
230
|
+
onpointerleave={() => (hoveredKey = null)}
|
|
231
|
+
>
|
|
232
|
+
<svg
|
|
233
|
+
viewBox="0 0 {width} {height}"
|
|
234
|
+
preserveAspectRatio="xMidYMid meet"
|
|
235
|
+
width="100%"
|
|
236
|
+
height="100%"
|
|
237
|
+
focusable="false"
|
|
238
|
+
aria-hidden="true"
|
|
239
|
+
>
|
|
240
|
+
<!-- axe temporel X : ticks -->
|
|
241
|
+
{#each scales.xTicks as t (t)}
|
|
242
|
+
{@const x = MARGIN.left + scaleLinear(t, scales.xMin, scales.xMax, 0, scales.plotW)}
|
|
243
|
+
<text class="st-windBarbChart__tick" x={x} y={height - MARGIN.bottom + 16} text-anchor="middle">{fmt(t)}</text>
|
|
244
|
+
{/each}
|
|
245
|
+
|
|
246
|
+
<!-- axe de base -->
|
|
247
|
+
<line class="st-windBarbChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
|
|
248
|
+
|
|
249
|
+
<!-- une barbe par point : hampe + barbules -->
|
|
250
|
+
{#each barbs as b (b.key)}
|
|
251
|
+
<g
|
|
252
|
+
class="st-windBarbChart__barb st-windBarbChart__barb--{b.tone}"
|
|
253
|
+
class:st-windBarbChart__barb--dim={hoveredKey !== null && hoveredKey !== b.key}
|
|
254
|
+
>
|
|
255
|
+
<line class="st-windBarbChart__shaft" x1={b.cx} y1={b.cy} x2={b.tipX} y2={b.tipY} data-chart-key={b.key} />
|
|
256
|
+
{#each b.ticks as tk, ti (ti)}
|
|
257
|
+
<line class="st-windBarbChart__feather st-windBarbChart__feather--{tk.kind}" x1={tk.x1} y1={tk.y1} x2={tk.x2} y2={tk.y2} />
|
|
258
|
+
{/each}
|
|
259
|
+
</g>
|
|
260
|
+
{/each}
|
|
261
|
+
</svg>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<ChartDataList label={label ?? "wind barb"} items={dataValueItems} />
|
|
265
|
+
|
|
266
|
+
{#if hoveredBarb}
|
|
267
|
+
{@const b = hoveredBarb}
|
|
268
|
+
<div
|
|
269
|
+
class="st-windBarbChart__tooltip"
|
|
270
|
+
role="presentation"
|
|
271
|
+
style="left: {(b.cx / width) * 100}%; top: {(b.cy / height) * 100}%"
|
|
272
|
+
>
|
|
273
|
+
<span class="st-windBarbChart__tooltipLabel">{fmt(b.datum.at)}</span>
|
|
274
|
+
<span class="st-windBarbChart__tooltipValue">{fmt(b.datum.speed)} kt @ {fmt(b.datum.direction)}°</span>
|
|
275
|
+
</div>
|
|
276
|
+
{/if}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<style>
|
|
280
|
+
.st-windBarbChart {
|
|
281
|
+
color: var(--st-semantic-text-secondary);
|
|
282
|
+
display: block;
|
|
283
|
+
font-family: inherit;
|
|
284
|
+
position: relative;
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.st-windBarbChart svg {
|
|
289
|
+
display: block;
|
|
290
|
+
overflow: visible;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.st-windBarbChart__visual {
|
|
294
|
+
display: block;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.st-windBarbChart__axis {
|
|
298
|
+
stroke: var(--st-semantic-border-subtle);
|
|
299
|
+
stroke-width: 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.st-windBarbChart__tick {
|
|
303
|
+
fill: var(--st-semantic-text-secondary);
|
|
304
|
+
font-size: 0.6875rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.st-windBarbChart__barb {
|
|
308
|
+
transition: opacity 120ms ease;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.st-windBarbChart__barb--dim {
|
|
312
|
+
opacity: 0.35;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.st-windBarbChart__shaft {
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
stroke: currentColor;
|
|
318
|
+
stroke-linecap: round;
|
|
319
|
+
stroke-width: 2;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.st-windBarbChart__feather {
|
|
323
|
+
stroke: currentColor;
|
|
324
|
+
stroke-linecap: round;
|
|
325
|
+
stroke-width: 2;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.st-windBarbChart__barb--category1 { color: var(--st-semantic-data-category1); }
|
|
329
|
+
.st-windBarbChart__barb--category2 { color: var(--st-semantic-data-category2); }
|
|
330
|
+
.st-windBarbChart__barb--category3 { color: var(--st-semantic-data-category3); }
|
|
331
|
+
.st-windBarbChart__barb--category4 { color: var(--st-semantic-data-category4); }
|
|
332
|
+
.st-windBarbChart__barb--category5 { color: var(--st-semantic-data-category5); }
|
|
333
|
+
.st-windBarbChart__barb--category6 { color: var(--st-semantic-data-category6); }
|
|
334
|
+
.st-windBarbChart__barb--category7 { color: var(--st-semantic-data-category7); }
|
|
335
|
+
.st-windBarbChart__barb--category8 { color: var(--st-semantic-data-category8); }
|
|
336
|
+
|
|
337
|
+
.st-windBarbChart__tooltip {
|
|
338
|
+
background: var(--st-semantic-surface-inverse);
|
|
339
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
340
|
+
color: var(--st-semantic-text-inverse);
|
|
341
|
+
display: inline-flex;
|
|
342
|
+
flex-direction: column;
|
|
343
|
+
font-size: 0.75rem;
|
|
344
|
+
gap: 0.125rem;
|
|
345
|
+
line-height: 1.2;
|
|
346
|
+
padding: 0.375rem 0.5rem;
|
|
347
|
+
pointer-events: none;
|
|
348
|
+
position: absolute;
|
|
349
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
z-index: 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.st-windBarbChart__tooltipLabel {
|
|
355
|
+
font-weight: 600;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.st-windBarbChart__tooltipValue {
|
|
359
|
+
opacity: 0.85;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@media (prefers-reduced-motion: reduce) {
|
|
363
|
+
.st-windBarbChart__barb {
|
|
364
|
+
transition: none;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WindBarbChart — barbes de vent (symboles météo : vitesse + direction) le
|
|
3
|
+
* long d'un axe temporel (façon Highcharts « windbarb »). Une barbe par point :
|
|
4
|
+
* la hampe est orientée selon `direction` (en degrés) et les barbules encodent
|
|
5
|
+
* la `speed` (en nœuds) par paliers météo standard (demi-barbule = 5 kt,
|
|
6
|
+
* barbule pleine = 10 kt, fanion = 50 kt). La couleur encode la vitesse sur
|
|
7
|
+
* l'échelle catégorielle continue category1..8. a11y : `role="img"` +
|
|
8
|
+
* `data-chart-key` + liste accessible des points.
|
|
9
|
+
* API canonique (référence Svelte, React/Vue/Angular doivent s'aligner).
|
|
10
|
+
*
|
|
11
|
+
* Convention de direction (météo) : `direction` est la direction d'où vient le
|
|
12
|
+
* vent, en degrés (0° = Nord). La hampe pointe vers la source et les barbules
|
|
13
|
+
* sont placées du côté droit de la hampe (hémisphère nord).
|
|
14
|
+
*
|
|
15
|
+
* Props obligatoires :
|
|
16
|
+
* data WindBarbChartDatum[] - {at, speed, direction}
|
|
17
|
+
*
|
|
18
|
+
* Props optionnelles :
|
|
19
|
+
* label string
|
|
20
|
+
* width number (défaut 640)
|
|
21
|
+
* height number (défaut 160)
|
|
22
|
+
* size number (longueur de la hampe en px ; défaut 32)
|
|
23
|
+
* class string
|
|
24
|
+
*/
|
|
25
|
+
export type WindBarbChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
26
|
+
export type WindBarbChartDatum = {
|
|
27
|
+
/** Position sur l'axe temporel (timestamp ou index). */
|
|
28
|
+
at: number;
|
|
29
|
+
/** Vitesse du vent en nœuds (≥ 0) : pilote les barbules et la couleur. */
|
|
30
|
+
speed: number;
|
|
31
|
+
/** Direction (d'où vient le vent) en DEGRÉS (0° = Nord). */
|
|
32
|
+
direction: number;
|
|
33
|
+
};
|
|
34
|
+
type WindBarbChartProps = {
|
|
35
|
+
data: WindBarbChartDatum[];
|
|
36
|
+
label?: string;
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
size?: number;
|
|
40
|
+
class?: string;
|
|
41
|
+
};
|
|
42
|
+
declare const WindBarbChart: import("svelte").Component<WindBarbChartProps, {}, "">;
|
|
43
|
+
type WindBarbChart = ReturnType<typeof WindBarbChart>;
|
|
44
|
+
export default WindBarbChart;
|
|
45
|
+
//# sourceMappingURL=WindBarbChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WindBarbChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/WindBarbChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAC3B,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;AA0NJ,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WindBarbChart.test.d.ts","sourceRoot":"","sources":["../src/lib/WindBarbChart.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { render } from "@testing-library/svelte";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import WindBarbChart from "./WindBarbChart.svelte";
|
|
4
|
+
const series = [
|
|
5
|
+
{ at: 0, speed: 5, direction: 0 },
|
|
6
|
+
{ at: 1, speed: 15, direction: 90 },
|
|
7
|
+
{ at: 2, speed: 30, direction: 180 },
|
|
8
|
+
{ at: 3, speed: 65, direction: 270 }
|
|
9
|
+
];
|
|
10
|
+
const barbs = (container) => Array.from(container.querySelectorAll(".st-windBarbChart__barb"));
|
|
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("WindBarbChart", () => {
|
|
14
|
+
it("renders an img role and one barb per datum", () => {
|
|
15
|
+
const { container } = render(WindBarbChart, { props: { data: series, label: "Vent" } });
|
|
16
|
+
expect(container.querySelector('[role="img"]')).toBeTruthy();
|
|
17
|
+
expect(barbs(container).length).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
it("draws a shaft per barb", () => {
|
|
20
|
+
const { container } = render(WindBarbChart, { props: { data: series, label: "W" } });
|
|
21
|
+
expect(container.querySelectorAll(".st-windBarbChart__shaft").length).toBe(4);
|
|
22
|
+
});
|
|
23
|
+
it("draws feather symbols encoding the speed (half / full / flag)", () => {
|
|
24
|
+
// 65 kt = 1 fanion (50) + 1 barbule pleine (10) + 1 demi (5) = 2 + 1 + 1 = 4 traits.
|
|
25
|
+
const { container } = render(WindBarbChart, {
|
|
26
|
+
props: { data: [{ at: 0, speed: 65, direction: 0 }], label: "W" }
|
|
27
|
+
});
|
|
28
|
+
expect(container.querySelectorAll(".st-windBarbChart__feather").length).toBe(4);
|
|
29
|
+
});
|
|
30
|
+
it("colours barbs by speed bin (largest speed → category8)", () => {
|
|
31
|
+
const { container } = render(WindBarbChart, { props: { data: series, label: "W" } });
|
|
32
|
+
const last = barbs(container).at(-1);
|
|
33
|
+
expect(last.classList.contains("st-windBarbChart__barb--category8")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it("renders a graduated time axis with nice ticks", () => {
|
|
36
|
+
const { container } = render(WindBarbChart, { props: { data: series, label: "W" } });
|
|
37
|
+
expect(container.querySelectorAll(".st-windBarbChart__axis").length).toBe(1);
|
|
38
|
+
expect(container.querySelectorAll(".st-windBarbChart__tick").length).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
it("lists every datum in the accessible data list", () => {
|
|
41
|
+
const { container } = render(WindBarbChart, {
|
|
42
|
+
props: { data: [{ at: 2, speed: 10, direction: 45 }], label: "W" }
|
|
43
|
+
});
|
|
44
|
+
expect(listItems(container)[0]).toBe("2 · 10 kt @ 45°");
|
|
45
|
+
});
|
|
46
|
+
it("drops non-finite or negative-speed points before rendering", () => {
|
|
47
|
+
const { container } = render(WindBarbChart, {
|
|
48
|
+
props: {
|
|
49
|
+
data: [
|
|
50
|
+
{ at: Number.NaN, speed: 5, direction: 0 },
|
|
51
|
+
{ at: 0, speed: -1, direction: 0 },
|
|
52
|
+
{ at: 1, speed: 10, direction: 0 }
|
|
53
|
+
],
|
|
54
|
+
label: "W"
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
expect(barbs(container).length).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
it("merges a custom class onto the root", () => {
|
|
60
|
+
const { container } = render(WindBarbChart, { props: { data: series, class: "mine" } });
|
|
61
|
+
const root = container.querySelector(".st-windBarbChart");
|
|
62
|
+
expect(structuralClass(root)).toBe("st-windBarbChart");
|
|
63
|
+
expect(root.classList.contains("mine")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -99,6 +99,10 @@ 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";
|
|
104
|
+
export { default as ContourChart } from "./ContourChart.svelte";
|
|
105
|
+
export { default as WindBarbChart } from "./WindBarbChart.svelte";
|
|
102
106
|
export { default as WaffleChart } from "./WaffleChart.svelte";
|
|
103
107
|
export { default as RibbonChart } from "./RibbonChart.svelte";
|
|
104
108
|
export { default as AnomalySwimLaneChart } from "./AnomalySwimLaneChart.svelte";
|
|
@@ -231,6 +235,10 @@ export type { GaugeChartTone, GaugeChartThreshold, GaugeChartFormat } from "./Ga
|
|
|
231
235
|
export type { SolidGaugeTone, SolidGaugeThreshold, SolidGaugeFormat } from "./SolidGaugeChart.svelte";
|
|
232
236
|
export type { StateTimelineTone, StateTimelineSegment, StateTimelineSeries } from "./StateTimelineChart.svelte";
|
|
233
237
|
export type { StatusHistoryTone, StatusHistoryBucket, StatusHistorySeries } from "./StatusHistoryChart.svelte";
|
|
238
|
+
export type { EventFeedPanelSeverity, EventFeedPanelEvent } from "./EventFeedPanel.svelte";
|
|
239
|
+
export type { VectorFieldChartTone, VectorFieldChartDatum } from "./VectorFieldChart.svelte";
|
|
240
|
+
export type { ContourChartTone, ContourChartDatum } from "./ContourChart.svelte";
|
|
241
|
+
export type { WindBarbChartTone, WindBarbChartDatum } from "./WindBarbChart.svelte";
|
|
234
242
|
export type { WaffleTone, WaffleChartDatum } from "./WaffleChart.svelte";
|
|
235
243
|
export type { RibbonChartTone, RibbonChartDatum } from "./RibbonChart.svelte";
|
|
236
244
|
export type { AnomalySwimLaneTone, AnomalySwimLaneBucket, AnomalySwimLaneSeries } from "./AnomalySwimLaneChart.svelte";
|