@sentropic/design-system-svelte 0.23.0 → 0.25.0
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/AppHeader.svelte +197 -0
- package/dist/AppHeader.svelte.d.ts +27 -0
- package/dist/AppHeader.svelte.d.ts.map +1 -0
- package/dist/IdentityMenu.svelte +406 -0
- package/dist/IdentityMenu.svelte.d.ts +42 -0
- package/dist/IdentityMenu.svelte.d.ts.map +1 -0
- package/dist/LanguageToggle.svelte +200 -0
- package/dist/LanguageToggle.svelte.d.ts +26 -0
- package/dist/LanguageToggle.svelte.d.ts.map +1 -0
- package/dist/LollipopChart.svelte +438 -0
- package/dist/LollipopChart.svelte.d.ts +40 -0
- package/dist/LollipopChart.svelte.d.ts.map +1 -0
- package/dist/ParetoChart.svelte +424 -0
- package/dist/ParetoChart.svelte.d.ts +30 -0
- package/dist/ParetoChart.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type LanguageToggleLocale = "fr" | "en";
|
|
3
|
+
|
|
4
|
+
export interface LanguageToggleProps {
|
|
5
|
+
/** Langue courante (contrôlé). */
|
|
6
|
+
locale?: LanguageToggleLocale;
|
|
7
|
+
/** Notifié au changement de langue. */
|
|
8
|
+
onLocaleChange?: (locale: LanguageToggleLocale) => void;
|
|
9
|
+
/** Libellé FR (i18n-agnostique : fourni par le parent). */
|
|
10
|
+
frLabel?: string;
|
|
11
|
+
/** Libellé EN (i18n-agnostique : fourni par le parent). */
|
|
12
|
+
enLabel?: string;
|
|
13
|
+
/** aria-label du sélecteur. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Variante d'affichage :
|
|
17
|
+
* - `select` (défaut) : le <select> de la source (header desktop).
|
|
18
|
+
* - `accordion` : la liste de boutons du tiroir mobile.
|
|
19
|
+
*/
|
|
20
|
+
variant?: "select" | "accordion";
|
|
21
|
+
/** Titre de la section accordéon. */
|
|
22
|
+
accordionLabel?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { ChevronDown } from "@lucide/svelte";
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
locale = "fr",
|
|
32
|
+
onLocaleChange,
|
|
33
|
+
frLabel = "FR",
|
|
34
|
+
enLabel = "EN",
|
|
35
|
+
label = "Langue",
|
|
36
|
+
variant = "select",
|
|
37
|
+
accordionLabel = "Langue",
|
|
38
|
+
class: className,
|
|
39
|
+
}: LanguageToggleProps = $props();
|
|
40
|
+
|
|
41
|
+
let open = $state(false);
|
|
42
|
+
|
|
43
|
+
const classes = () => ["st-languageToggle", className].filter(Boolean).join(" ");
|
|
44
|
+
|
|
45
|
+
function emit(next: LanguageToggleLocale) {
|
|
46
|
+
onLocaleChange?.(next);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onSelectChange(event: Event) {
|
|
50
|
+
const value = (event.currentTarget as HTMLSelectElement).value as LanguageToggleLocale;
|
|
51
|
+
emit(value);
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#if variant === "accordion"}
|
|
56
|
+
<div class={classes()}>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class="st-languageToggle__accordionTrigger"
|
|
60
|
+
aria-expanded={open}
|
|
61
|
+
onclick={() => (open = !open)}
|
|
62
|
+
>
|
|
63
|
+
<span>{accordionLabel}</span>
|
|
64
|
+
<ChevronDown
|
|
65
|
+
class={`st-languageToggle__chevron${open ? " st-languageToggle__chevron--open" : ""}`}
|
|
66
|
+
size={16}
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
</button>
|
|
70
|
+
{#if open}
|
|
71
|
+
<div class="st-languageToggle__accordionPanel">
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class="st-languageToggle__option"
|
|
75
|
+
class:st-languageToggle__option--active={locale === "fr"}
|
|
76
|
+
aria-current={locale === "fr" ? "true" : "false"}
|
|
77
|
+
onclick={() => emit("fr")}
|
|
78
|
+
>
|
|
79
|
+
{frLabel}
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="st-languageToggle__option"
|
|
84
|
+
class:st-languageToggle__option--active={locale === "en"}
|
|
85
|
+
aria-current={locale === "en" ? "true" : "false"}
|
|
86
|
+
onclick={() => emit("en")}
|
|
87
|
+
>
|
|
88
|
+
{enLabel}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
{:else}
|
|
94
|
+
<select
|
|
95
|
+
class={`st-languageToggle__select${className ? ` ${className}` : ""}`}
|
|
96
|
+
value={locale}
|
|
97
|
+
aria-label={label}
|
|
98
|
+
onchange={onSelectChange}
|
|
99
|
+
>
|
|
100
|
+
<option value="fr">{frLabel}</option>
|
|
101
|
+
<option value="en">{enLabel}</option>
|
|
102
|
+
</select>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.st-languageToggle {
|
|
107
|
+
width: 100%;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Variante <select> — calque du <select> source (header desktop). */
|
|
111
|
+
.st-languageToggle__select {
|
|
112
|
+
background: var(--st-semantic-surface-default);
|
|
113
|
+
border: 1px solid var(--st-semantic-border-subtle);
|
|
114
|
+
border-radius: var(--st-radius-md, 0.375rem);
|
|
115
|
+
color: var(--st-semantic-text-primary);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font: inherit;
|
|
118
|
+
font-family: var(--st-font-sans);
|
|
119
|
+
font-size: 0.875rem;
|
|
120
|
+
padding: var(--st-spacing-1, 0.25rem) var(--st-spacing-2, 0.5rem);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.st-languageToggle__select:focus-visible {
|
|
124
|
+
border-color: var(--st-semantic-border-interactive);
|
|
125
|
+
box-shadow: 0 0 0 2px var(--st-semantic-border-interactive);
|
|
126
|
+
outline: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Variante accordéon — calque du tiroir mobile source. */
|
|
130
|
+
.st-languageToggle__accordionTrigger {
|
|
131
|
+
align-items: center;
|
|
132
|
+
background: transparent;
|
|
133
|
+
border: 0;
|
|
134
|
+
border-radius: var(--st-radius-sm, 0.375rem);
|
|
135
|
+
color: var(--st-semantic-text-primary);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
display: flex;
|
|
138
|
+
font: inherit;
|
|
139
|
+
font-family: var(--st-font-sans);
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
justify-content: space-between;
|
|
143
|
+
padding: var(--st-spacing-2, 0.5rem) var(--st-spacing-3, 0.75rem);
|
|
144
|
+
width: 100%;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.st-languageToggle__accordionTrigger:hover {
|
|
148
|
+
background: var(--st-semantic-surface-subtle);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.st-languageToggle__accordionTrigger:focus-visible {
|
|
152
|
+
box-shadow: 0 0 0 2px var(--st-semantic-border-interactive);
|
|
153
|
+
outline: none;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.st-languageToggle :global(.st-languageToggle__chevron) {
|
|
157
|
+
color: var(--st-semantic-text-secondary);
|
|
158
|
+
transition: transform var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.st-languageToggle :global(.st-languageToggle__chevron--open) {
|
|
162
|
+
transform: rotate(180deg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.st-languageToggle__accordionPanel {
|
|
166
|
+
display: grid;
|
|
167
|
+
gap: var(--st-spacing-1, 0.25rem);
|
|
168
|
+
padding: var(--st-spacing-1, 0.25rem) var(--st-spacing-3, 0.75rem) var(--st-spacing-2, 0.5rem);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.st-languageToggle__option {
|
|
172
|
+
background: transparent;
|
|
173
|
+
border: 0;
|
|
174
|
+
border-radius: var(--st-radius-sm, 0.375rem);
|
|
175
|
+
color: var(--st-semantic-text-secondary);
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
font: inherit;
|
|
178
|
+
font-family: var(--st-font-sans);
|
|
179
|
+
font-size: 0.875rem;
|
|
180
|
+
font-weight: 500;
|
|
181
|
+
padding: var(--st-spacing-2, 0.5rem) var(--st-spacing-3, 0.75rem);
|
|
182
|
+
text-align: left;
|
|
183
|
+
width: 100%;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.st-languageToggle__option:hover {
|
|
187
|
+
background: var(--st-semantic-surface-subtle);
|
|
188
|
+
color: var(--st-semantic-text-primary);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.st-languageToggle__option--active {
|
|
192
|
+
background: var(--st-semantic-surface-subtle);
|
|
193
|
+
color: var(--st-semantic-text-primary);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.st-languageToggle__option:focus-visible {
|
|
197
|
+
box-shadow: 0 0 0 2px var(--st-semantic-border-interactive);
|
|
198
|
+
outline: none;
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type LanguageToggleLocale = "fr" | "en";
|
|
2
|
+
export interface LanguageToggleProps {
|
|
3
|
+
/** Langue courante (contrôlé). */
|
|
4
|
+
locale?: LanguageToggleLocale;
|
|
5
|
+
/** Notifié au changement de langue. */
|
|
6
|
+
onLocaleChange?: (locale: LanguageToggleLocale) => void;
|
|
7
|
+
/** Libellé FR (i18n-agnostique : fourni par le parent). */
|
|
8
|
+
frLabel?: string;
|
|
9
|
+
/** Libellé EN (i18n-agnostique : fourni par le parent). */
|
|
10
|
+
enLabel?: string;
|
|
11
|
+
/** aria-label du sélecteur. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Variante d'affichage :
|
|
15
|
+
* - `select` (défaut) : le <select> de la source (header desktop).
|
|
16
|
+
* - `accordion` : la liste de boutons du tiroir mobile.
|
|
17
|
+
*/
|
|
18
|
+
variant?: "select" | "accordion";
|
|
19
|
+
/** Titre de la section accordéon. */
|
|
20
|
+
accordionLabel?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const LanguageToggle: import("svelte").Component<LanguageToggleProps, {}, "">;
|
|
24
|
+
type LanguageToggle = ReturnType<typeof LanguageToggle>;
|
|
25
|
+
export default LanguageToggle;
|
|
26
|
+
//# sourceMappingURL=LanguageToggle.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageToggle.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LanguageToggle.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,uCAAuC;IACvC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACxD,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IACjC,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+DH,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* LollipopChart — tige fine (ligne) + cercle au sommet, par catégorie.
|
|
4
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner).
|
|
5
|
+
*
|
|
6
|
+
* Props obligatoires :
|
|
7
|
+
* data LollipopChartDatum[] - tableau {label, value, tone?}
|
|
8
|
+
* label string - aria-label du graphique
|
|
9
|
+
*
|
|
10
|
+
* Props optionnelles :
|
|
11
|
+
* orientation "vertical"|"horizontal" (défaut "vertical")
|
|
12
|
+
* width number (défaut 480)
|
|
13
|
+
* height number (défaut 240)
|
|
14
|
+
* domain [number, number] - domaine fixe de l'axe des valeurs
|
|
15
|
+
* class string
|
|
16
|
+
*/
|
|
17
|
+
export type LollipopChartTone =
|
|
18
|
+
| "category1"
|
|
19
|
+
| "category2"
|
|
20
|
+
| "category3"
|
|
21
|
+
| "category4"
|
|
22
|
+
| "category5"
|
|
23
|
+
| "category6"
|
|
24
|
+
| "category7"
|
|
25
|
+
| "category8";
|
|
26
|
+
|
|
27
|
+
export type LollipopChartDatum = {
|
|
28
|
+
label: string;
|
|
29
|
+
value: number;
|
|
30
|
+
tone?: LollipopChartTone;
|
|
31
|
+
};
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
36
|
+
import { contrastTextForTone } from "./chartContrast.js";
|
|
37
|
+
|
|
38
|
+
type LollipopChartProps = {
|
|
39
|
+
data: LollipopChartDatum[];
|
|
40
|
+
width?: number;
|
|
41
|
+
height?: number;
|
|
42
|
+
orientation?: "vertical" | "horizontal";
|
|
43
|
+
label: string;
|
|
44
|
+
/**
|
|
45
|
+
* Domaine fixe de l'axe des valeurs `[min, max]`. Quand fourni (et fini),
|
|
46
|
+
* l'échelle l'utilise au lieu du min/max dérivé des données — laissant
|
|
47
|
+
* plusieurs LollipopCharts d'une grille partager une échelle. Absent ou
|
|
48
|
+
* invalide → repli sur la plage auto (inchangé).
|
|
49
|
+
*/
|
|
50
|
+
domain?: [number, number];
|
|
51
|
+
class?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
data = [],
|
|
56
|
+
width = 480,
|
|
57
|
+
height = 240,
|
|
58
|
+
orientation = "vertical",
|
|
59
|
+
label,
|
|
60
|
+
domain,
|
|
61
|
+
class: className
|
|
62
|
+
}: LollipopChartProps = $props();
|
|
63
|
+
|
|
64
|
+
const MARGIN = { top: 12, right: 16, bottom: 32, left: 44 };
|
|
65
|
+
const DOT_RADIUS = 5;
|
|
66
|
+
|
|
67
|
+
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
68
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
|
|
69
|
+
const base = Number.isFinite(max) ? max : 0;
|
|
70
|
+
return [base];
|
|
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 = 1 * 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) {
|
|
85
|
+
ticks.push(Number(v.toFixed(10)));
|
|
86
|
+
}
|
|
87
|
+
return ticks;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
|
|
91
|
+
if (d1 === d0) return r0;
|
|
92
|
+
return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTick(v: number): string {
|
|
96
|
+
if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
|
|
97
|
+
if (Number.isInteger(v)) return String(v);
|
|
98
|
+
return v.toFixed(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let hoveredIndex: number | null = $state(null);
|
|
102
|
+
|
|
103
|
+
// Données valides : value finie.
|
|
104
|
+
const validData = $derived(data.filter((d) => Number.isFinite(d.value)));
|
|
105
|
+
|
|
106
|
+
// Un domaine n'est honoré que si fini et ordonné (min < max).
|
|
107
|
+
const validDomain = $derived.by<[number, number] | null>(() => {
|
|
108
|
+
if (!domain) return null;
|
|
109
|
+
const [d0, d1] = domain;
|
|
110
|
+
if (!Number.isFinite(d0) || !Number.isFinite(d1) || d0 >= d1) return null;
|
|
111
|
+
return [d0, d1];
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const scales = $derived.by(() => {
|
|
115
|
+
const values = validData.map((d) => d.value);
|
|
116
|
+
const minRaw = validDomain ? validDomain[0] : Math.min(0, ...values);
|
|
117
|
+
const maxRaw = validDomain ? validDomain[1] : Math.max(0, ...values);
|
|
118
|
+
const ticks = niceTicks(minRaw, maxRaw, 5);
|
|
119
|
+
const domainMin = ticks[0];
|
|
120
|
+
const domainMax = ticks[ticks.length - 1];
|
|
121
|
+
const plotWidth = Math.max(width - MARGIN.left - MARGIN.right, 1);
|
|
122
|
+
const plotHeight = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
|
|
123
|
+
return { ticks, domainMin, domainMax, plotWidth, plotHeight };
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const lollipops = $derived.by(() => {
|
|
127
|
+
const { domainMin, domainMax, plotWidth, plotHeight } = scales;
|
|
128
|
+
if (validData.length === 0) return [];
|
|
129
|
+
if (orientation === "vertical") {
|
|
130
|
+
const band = plotWidth / validData.length;
|
|
131
|
+
const zeroY = scaleLinear(0, domainMin, domainMax, plotHeight, 0);
|
|
132
|
+
return validData.map((d, i) => {
|
|
133
|
+
const valueY = scaleLinear(d.value, domainMin, domainMax, plotHeight, 0);
|
|
134
|
+
const cx = MARGIN.left + band * (i + 0.5);
|
|
135
|
+
return {
|
|
136
|
+
datum: d,
|
|
137
|
+
tone: d.tone ?? "category1",
|
|
138
|
+
// tige : du zéro jusqu'au point
|
|
139
|
+
stemX1: cx,
|
|
140
|
+
stemY1: MARGIN.top + zeroY,
|
|
141
|
+
stemX2: cx,
|
|
142
|
+
stemY2: MARGIN.top + valueY,
|
|
143
|
+
cx,
|
|
144
|
+
cy: MARGIN.top + valueY,
|
|
145
|
+
labelX: cx,
|
|
146
|
+
labelY: height - MARGIN.bottom + 16
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// horizontal
|
|
151
|
+
const band = plotHeight / validData.length;
|
|
152
|
+
const zeroX = scaleLinear(0, domainMin, domainMax, 0, plotWidth);
|
|
153
|
+
return validData.map((d, i) => {
|
|
154
|
+
const valueX = scaleLinear(d.value, domainMin, domainMax, 0, plotWidth);
|
|
155
|
+
const cy = MARGIN.top + band * (i + 0.5);
|
|
156
|
+
return {
|
|
157
|
+
datum: d,
|
|
158
|
+
tone: d.tone ?? "category1",
|
|
159
|
+
stemX1: MARGIN.left + zeroX,
|
|
160
|
+
stemY1: cy,
|
|
161
|
+
stemX2: MARGIN.left + valueX,
|
|
162
|
+
stemY2: cy,
|
|
163
|
+
cx: MARGIN.left + valueX,
|
|
164
|
+
cy,
|
|
165
|
+
labelX: MARGIN.left - 6,
|
|
166
|
+
labelY: cy
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const dataValueItems = $derived(validData.map((d) => `${d.label}: ${d.value}`));
|
|
172
|
+
|
|
173
|
+
const valueAxisTicks = $derived.by(() => {
|
|
174
|
+
const { ticks, domainMin, domainMax, plotWidth, plotHeight } = scales;
|
|
175
|
+
if (orientation === "vertical") {
|
|
176
|
+
return ticks.map((tick) => ({
|
|
177
|
+
value: tick,
|
|
178
|
+
x1: MARGIN.left,
|
|
179
|
+
x2: MARGIN.left + plotWidth,
|
|
180
|
+
y: MARGIN.top + scaleLinear(tick, domainMin, domainMax, plotHeight, 0),
|
|
181
|
+
x: undefined,
|
|
182
|
+
y1: undefined,
|
|
183
|
+
y2: undefined
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
return ticks.map((tick) => ({
|
|
187
|
+
value: tick,
|
|
188
|
+
x: MARGIN.left + scaleLinear(tick, domainMin, domainMax, 0, plotWidth),
|
|
189
|
+
y1: MARGIN.top,
|
|
190
|
+
y2: MARGIN.top + plotHeight,
|
|
191
|
+
x1: undefined,
|
|
192
|
+
x2: undefined,
|
|
193
|
+
y: undefined
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function handleLeave() {
|
|
198
|
+
hoveredIndex = null;
|
|
199
|
+
}
|
|
200
|
+
function handleVisualPointerMove(event: PointerEvent) {
|
|
201
|
+
const target = event.target;
|
|
202
|
+
if (!(target instanceof Element)) {
|
|
203
|
+
hoveredIndex = null;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const index = Number(target.getAttribute("data-chart-index"));
|
|
207
|
+
hoveredIndex = Number.isInteger(index) ? index : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const classes = () => ["st-lollipopChart", className].filter(Boolean).join(" ");
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<div class={classes()}>
|
|
214
|
+
<div
|
|
215
|
+
class="st-lollipopChart__visual"
|
|
216
|
+
role="img"
|
|
217
|
+
aria-label={label}
|
|
218
|
+
onpointermove={handleVisualPointerMove}
|
|
219
|
+
onpointerleave={handleLeave}
|
|
220
|
+
>
|
|
221
|
+
<svg
|
|
222
|
+
viewBox="0 0 {width} {height}"
|
|
223
|
+
preserveAspectRatio="xMidYMid meet"
|
|
224
|
+
width="100%"
|
|
225
|
+
height="100%"
|
|
226
|
+
focusable="false"
|
|
227
|
+
aria-hidden="true"
|
|
228
|
+
>
|
|
229
|
+
<!-- gridlines + value axis ticks -->
|
|
230
|
+
{#if orientation === "vertical"}
|
|
231
|
+
{#each valueAxisTicks as tick (tick.value)}
|
|
232
|
+
<line class="st-lollipopChart__grid" x1={tick.x1} x2={tick.x2} y1={tick.y} y2={tick.y} />
|
|
233
|
+
<text
|
|
234
|
+
class="st-lollipopChart__tickLabel"
|
|
235
|
+
x={MARGIN.left - 6}
|
|
236
|
+
y={tick.y}
|
|
237
|
+
text-anchor="end"
|
|
238
|
+
dominant-baseline="middle"
|
|
239
|
+
>
|
|
240
|
+
{formatTick(tick.value)}
|
|
241
|
+
</text>
|
|
242
|
+
{/each}
|
|
243
|
+
{:else}
|
|
244
|
+
{#each valueAxisTicks as tick (tick.value)}
|
|
245
|
+
<line class="st-lollipopChart__grid" x1={tick.x} x2={tick.x} y1={tick.y1} y2={tick.y2} />
|
|
246
|
+
<text
|
|
247
|
+
class="st-lollipopChart__tickLabel"
|
|
248
|
+
x={tick.x}
|
|
249
|
+
y={height - MARGIN.bottom + 16}
|
|
250
|
+
text-anchor="middle"
|
|
251
|
+
>
|
|
252
|
+
{formatTick(tick.value)}
|
|
253
|
+
</text>
|
|
254
|
+
{/each}
|
|
255
|
+
{/if}
|
|
256
|
+
|
|
257
|
+
<!-- axes -->
|
|
258
|
+
<line
|
|
259
|
+
class="st-lollipopChart__axis"
|
|
260
|
+
x1={MARGIN.left}
|
|
261
|
+
x2={MARGIN.left}
|
|
262
|
+
y1={MARGIN.top}
|
|
263
|
+
y2={height - MARGIN.bottom}
|
|
264
|
+
/>
|
|
265
|
+
<line
|
|
266
|
+
class="st-lollipopChart__axis"
|
|
267
|
+
x1={MARGIN.left}
|
|
268
|
+
x2={width - MARGIN.right}
|
|
269
|
+
y1={height - MARGIN.bottom}
|
|
270
|
+
y2={height - MARGIN.bottom}
|
|
271
|
+
/>
|
|
272
|
+
|
|
273
|
+
<!-- category labels -->
|
|
274
|
+
{#each lollipops as pop (pop.datum.label)}
|
|
275
|
+
{#if orientation === "vertical"}
|
|
276
|
+
<text
|
|
277
|
+
class="st-lollipopChart__categoryLabel"
|
|
278
|
+
x={pop.labelX}
|
|
279
|
+
y={pop.labelY}
|
|
280
|
+
text-anchor="middle"
|
|
281
|
+
>
|
|
282
|
+
{pop.datum.label}
|
|
283
|
+
</text>
|
|
284
|
+
{:else}
|
|
285
|
+
<text
|
|
286
|
+
class="st-lollipopChart__categoryLabel"
|
|
287
|
+
x={pop.labelX}
|
|
288
|
+
y={pop.labelY}
|
|
289
|
+
text-anchor="end"
|
|
290
|
+
dominant-baseline="middle"
|
|
291
|
+
>
|
|
292
|
+
{pop.datum.label}
|
|
293
|
+
</text>
|
|
294
|
+
{/if}
|
|
295
|
+
{/each}
|
|
296
|
+
|
|
297
|
+
<!-- stems + dots (decorative, inside aria-hidden SVG) -->
|
|
298
|
+
{#each lollipops as pop, i (pop.datum.label)}
|
|
299
|
+
<line
|
|
300
|
+
class="st-lollipopChart__stem"
|
|
301
|
+
x1={pop.stemX1}
|
|
302
|
+
y1={pop.stemY1}
|
|
303
|
+
x2={pop.stemX2}
|
|
304
|
+
y2={pop.stemY2}
|
|
305
|
+
/>
|
|
306
|
+
<circle
|
|
307
|
+
class="st-lollipopChart__dot st-lollipopChart__dot--{pop.tone}"
|
|
308
|
+
cx={pop.cx}
|
|
309
|
+
cy={pop.cy}
|
|
310
|
+
r={DOT_RADIUS}
|
|
311
|
+
data-chart-index={i}
|
|
312
|
+
/>
|
|
313
|
+
<!-- value label near the dot, colour kept readable via contrastTextForTone -->
|
|
314
|
+
<text
|
|
315
|
+
class="st-lollipopChart__valueLabel"
|
|
316
|
+
x={pop.cx}
|
|
317
|
+
y={orientation === "vertical" ? pop.cy - DOT_RADIUS - 4 : pop.cy}
|
|
318
|
+
dx={orientation === "vertical" ? 0 : DOT_RADIUS + 4}
|
|
319
|
+
text-anchor={orientation === "vertical" ? "middle" : "start"}
|
|
320
|
+
dominant-baseline={orientation === "vertical" ? "auto" : "middle"}
|
|
321
|
+
style="fill: {contrastTextForTone(pop.tone)}"
|
|
322
|
+
>
|
|
323
|
+
{formatTick(pop.datum.value)}
|
|
324
|
+
</text>
|
|
325
|
+
{/each}
|
|
326
|
+
</svg>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<ChartDataList {label} items={dataValueItems} />
|
|
330
|
+
|
|
331
|
+
{#if hoveredIndex !== null && lollipops[hoveredIndex]}
|
|
332
|
+
{@const pop = lollipops[hoveredIndex]}
|
|
333
|
+
<div
|
|
334
|
+
class="st-lollipopChart__tooltip"
|
|
335
|
+
role="presentation"
|
|
336
|
+
style="left: {(pop.cx / width) * 100}%; top: {(pop.cy / height) * 100}%"
|
|
337
|
+
>
|
|
338
|
+
<span class="st-lollipopChart__tooltipLabel">{pop.datum.label}</span>
|
|
339
|
+
<span class="st-lollipopChart__tooltipValue">{pop.datum.value}</span>
|
|
340
|
+
</div>
|
|
341
|
+
{/if}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<style>
|
|
345
|
+
.st-lollipopChart {
|
|
346
|
+
color: var(--st-semantic-text-secondary);
|
|
347
|
+
display: block;
|
|
348
|
+
font-family: inherit;
|
|
349
|
+
position: relative;
|
|
350
|
+
width: 100%;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.st-lollipopChart svg {
|
|
354
|
+
display: block;
|
|
355
|
+
overflow: visible;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.st-lollipopChart__visual {
|
|
359
|
+
display: block;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.st-lollipopChart__grid {
|
|
363
|
+
stroke: var(--st-component-lollipopChart-gridStroke, var(--st-semantic-border-subtle));
|
|
364
|
+
stroke-dasharray: 2 3;
|
|
365
|
+
stroke-width: 1;
|
|
366
|
+
opacity: 0.7;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.st-lollipopChart__axis {
|
|
370
|
+
stroke: var(--st-component-lollipopChart-axisStroke, var(--st-semantic-border-subtle));
|
|
371
|
+
stroke-width: 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.st-lollipopChart__tickLabel,
|
|
375
|
+
.st-lollipopChart__categoryLabel {
|
|
376
|
+
fill: var(--st-component-lollipopChart-labelColor, var(--st-semantic-text-secondary));
|
|
377
|
+
font-size: 0.6875rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.st-lollipopChart__valueLabel {
|
|
381
|
+
font-size: 0.625rem;
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.st-lollipopChart__stem {
|
|
386
|
+
stroke: var(--st-semantic-border-interactive, var(--st-semantic-border-subtle));
|
|
387
|
+
stroke-width: 1.5;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.st-lollipopChart__dot {
|
|
391
|
+
cursor: pointer;
|
|
392
|
+
transition: opacity var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.st-lollipopChart__dot:hover {
|
|
396
|
+
opacity: 0.82;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.st-lollipopChart__dot--category1 { fill: var(--st-semantic-data-category1); }
|
|
400
|
+
.st-lollipopChart__dot--category2 { fill: var(--st-semantic-data-category2); }
|
|
401
|
+
.st-lollipopChart__dot--category3 { fill: var(--st-semantic-data-category3); }
|
|
402
|
+
.st-lollipopChart__dot--category4 { fill: var(--st-semantic-data-category4); }
|
|
403
|
+
.st-lollipopChart__dot--category5 { fill: var(--st-semantic-data-category5); }
|
|
404
|
+
.st-lollipopChart__dot--category6 { fill: var(--st-semantic-data-category6); }
|
|
405
|
+
.st-lollipopChart__dot--category7 { fill: var(--st-semantic-data-category7); }
|
|
406
|
+
.st-lollipopChart__dot--category8 { fill: var(--st-semantic-data-category8); }
|
|
407
|
+
|
|
408
|
+
.st-lollipopChart__tooltip {
|
|
409
|
+
background: var(--st-component-lollipopChart-tooltipBackground, var(--st-semantic-surface-inverse));
|
|
410
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
411
|
+
color: var(--st-component-lollipopChart-tooltipText, 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-lollipopChart__tooltipLabel {
|
|
426
|
+
font-weight: 600;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.st-lollipopChart__tooltipValue {
|
|
430
|
+
opacity: 0.85;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
434
|
+
.st-lollipopChart__dot {
|
|
435
|
+
transition: none;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LollipopChart — tige fine (ligne) + cercle au sommet, par catégorie.
|
|
3
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner).
|
|
4
|
+
*
|
|
5
|
+
* Props obligatoires :
|
|
6
|
+
* data LollipopChartDatum[] - tableau {label, value, tone?}
|
|
7
|
+
* label string - aria-label du graphique
|
|
8
|
+
*
|
|
9
|
+
* Props optionnelles :
|
|
10
|
+
* orientation "vertical"|"horizontal" (défaut "vertical")
|
|
11
|
+
* width number (défaut 480)
|
|
12
|
+
* height number (défaut 240)
|
|
13
|
+
* domain [number, number] - domaine fixe de l'axe des valeurs
|
|
14
|
+
* class string
|
|
15
|
+
*/
|
|
16
|
+
export type LollipopChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
17
|
+
export type LollipopChartDatum = {
|
|
18
|
+
label: string;
|
|
19
|
+
value: number;
|
|
20
|
+
tone?: LollipopChartTone;
|
|
21
|
+
};
|
|
22
|
+
type LollipopChartProps = {
|
|
23
|
+
data: LollipopChartDatum[];
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
orientation?: "vertical" | "horizontal";
|
|
27
|
+
label: string;
|
|
28
|
+
/**
|
|
29
|
+
* Domaine fixe de l'axe des valeurs `[min, max]`. Quand fourni (et fini),
|
|
30
|
+
* l'échelle l'utilise au lieu du min/max dérivé des données — laissant
|
|
31
|
+
* plusieurs LollipopCharts d'une grille partager une échelle. Absent ou
|
|
32
|
+
* invalide → repli sur la plage auto (inchangé).
|
|
33
|
+
*/
|
|
34
|
+
domain?: [number, number];
|
|
35
|
+
class?: string;
|
|
36
|
+
};
|
|
37
|
+
declare const LollipopChart: import("svelte").Component<LollipopChartProps, {}, "">;
|
|
38
|
+
type LollipopChart = ReturnType<typeof LollipopChart>;
|
|
39
|
+
export default LollipopChart;
|
|
40
|
+
//# sourceMappingURL=LollipopChart.svelte.d.ts.map
|