@sentropic/design-system-svelte 0.34.0 → 0.34.21
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/AppChrome.svelte +660 -0
- package/dist/AppChrome.svelte.d.ts +74 -0
- package/dist/AppChrome.svelte.d.ts.map +1 -0
- package/dist/AppChrome.test.d.ts +2 -0
- package/dist/AppChrome.test.d.ts.map +1 -0
- package/dist/AppChrome.test.js +122 -0
- package/dist/AppHeader.svelte +159 -1
- package/dist/AppHeader.svelte.d.ts +18 -1
- package/dist/AppHeader.svelte.d.ts.map +1 -1
- package/dist/ArcDiagramChart.svelte +380 -0
- package/dist/ArcDiagramChart.svelte.d.ts +43 -0
- package/dist/ArcDiagramChart.svelte.d.ts.map +1 -0
- package/dist/AreaRangeChart.svelte +487 -0
- package/dist/AreaRangeChart.svelte.d.ts +38 -0
- package/dist/AreaRangeChart.svelte.d.ts.map +1 -0
- package/dist/AreaSplineRangeChart.svelte +478 -0
- package/dist/AreaSplineRangeChart.svelte.d.ts +37 -0
- package/dist/AreaSplineRangeChart.svelte.d.ts.map +1 -0
- package/dist/BellCurveChart.svelte +487 -0
- package/dist/BellCurveChart.svelte.d.ts +40 -0
- package/dist/BellCurveChart.svelte.d.ts.map +1 -0
- package/dist/Calendar.svelte +11 -0
- package/dist/ChatThread.svelte +32 -1
- package/dist/ChatThread.svelte.d.ts +14 -0
- package/dist/ChatThread.svelte.d.ts.map +1 -1
- package/dist/ColumnPyramidChart.svelte +332 -0
- package/dist/ColumnPyramidChart.svelte.d.ts +35 -0
- package/dist/ColumnPyramidChart.svelte.d.ts.map +1 -0
- package/dist/ColumnRangeChart.svelte +432 -0
- package/dist/ColumnRangeChart.svelte.d.ts +42 -0
- package/dist/ColumnRangeChart.svelte.d.ts.map +1 -0
- package/dist/Combobox.svelte +3 -0
- package/dist/ConfigItemCard.svelte +303 -0
- package/dist/ConfigItemCard.svelte.d.ts +37 -0
- package/dist/ConfigItemCard.svelte.d.ts.map +1 -0
- package/dist/ContentSwitcher.svelte +1 -1
- package/dist/DatePicker.svelte +3 -0
- package/dist/DependencyWheelChart.svelte +413 -0
- package/dist/DependencyWheelChart.svelte.d.ts +42 -0
- package/dist/DependencyWheelChart.svelte.d.ts.map +1 -0
- package/dist/DumbbellChart.svelte +403 -0
- package/dist/DumbbellChart.svelte.d.ts +44 -0
- package/dist/DumbbellChart.svelte.d.ts.map +1 -0
- package/dist/ErrorBarChart.svelte +428 -0
- package/dist/ErrorBarChart.svelte.d.ts +40 -0
- package/dist/ErrorBarChart.svelte.d.ts.map +1 -0
- package/dist/FieldCard.svelte +220 -0
- package/dist/FieldCard.svelte.d.ts +28 -0
- package/dist/FieldCard.svelte.d.ts.map +1 -0
- package/dist/GanttChart.svelte +410 -0
- package/dist/GanttChart.svelte.d.ts +39 -0
- package/dist/GanttChart.svelte.d.ts.map +1 -0
- package/dist/HLCChart.svelte +330 -0
- package/dist/HLCChart.svelte.d.ts +32 -0
- package/dist/HLCChart.svelte.d.ts.map +1 -0
- package/dist/HeikinAshiChart.svelte +365 -0
- package/dist/HeikinAshiChart.svelte.d.ts +37 -0
- package/dist/HeikinAshiChart.svelte.d.ts.map +1 -0
- package/dist/HollowCandlestickChart.svelte +357 -0
- package/dist/HollowCandlestickChart.svelte.d.ts +34 -0
- package/dist/HollowCandlestickChart.svelte.d.ts.map +1 -0
- package/dist/Input.svelte +3 -0
- package/dist/ItemChart.svelte +389 -0
- package/dist/ItemChart.svelte.d.ts +67 -0
- package/dist/ItemChart.svelte.d.ts.map +1 -0
- package/dist/LollipopChart.svelte +1 -1
- package/dist/MultiSelect.svelte +3 -0
- package/dist/NumberInput.svelte +3 -0
- package/dist/OHLCChart.svelte +343 -0
- package/dist/OHLCChart.svelte.d.ts +33 -0
- package/dist/OHLCChart.svelte.d.ts.map +1 -0
- package/dist/OrganizationChart.svelte +284 -0
- package/dist/OrganizationChart.svelte.d.ts +19 -0
- package/dist/OrganizationChart.svelte.d.ts.map +1 -0
- package/dist/PasswordInput.svelte +3 -0
- package/dist/PolygonChart.svelte +189 -0
- package/dist/PolygonChart.svelte.d.ts +17 -0
- package/dist/PolygonChart.svelte.d.ts.map +1 -0
- package/dist/ScoreCard.svelte +172 -0
- package/dist/ScoreCard.svelte.d.ts +27 -0
- package/dist/ScoreCard.svelte.d.ts.map +1 -0
- package/dist/Search.svelte +7 -5
- package/dist/Select.svelte +3 -0
- package/dist/StreamgraphChart.svelte +283 -0
- package/dist/StreamgraphChart.svelte.d.ts +23 -0
- package/dist/StreamgraphChart.svelte.d.ts.map +1 -0
- package/dist/StreamingMessage.svelte +44 -2
- package/dist/StreamingMessage.svelte.d.ts +18 -1
- package/dist/StreamingMessage.svelte.d.ts.map +1 -1
- package/dist/TileMapChart.svelte +314 -0
- package/dist/TileMapChart.svelte.d.ts +45 -0
- package/dist/TileMapChart.svelte.d.ts.map +1 -0
- package/dist/TimePicker.svelte +3 -0
- package/dist/TimelineChart.svelte +362 -0
- package/dist/TimelineChart.svelte.d.ts +22 -0
- package/dist/TimelineChart.svelte.d.ts.map +1 -0
- package/dist/TreegraphChart.svelte +281 -0
- package/dist/TreegraphChart.svelte.d.ts +19 -0
- package/dist/TreegraphChart.svelte.d.ts.map +1 -0
- package/dist/VariablePieChart.svelte +313 -0
- package/dist/VariablePieChart.svelte.d.ts +52 -0
- package/dist/VariablePieChart.svelte.d.ts.map +1 -0
- package/dist/VennChart.svelte +348 -0
- package/dist/VennChart.svelte.d.ts +72 -0
- package/dist/VennChart.svelte.d.ts.map +1 -0
- package/dist/WordCloudChart.svelte +279 -0
- package/dist/WordCloudChart.svelte.d.ts +18 -0
- package/dist/WordCloudChart.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -0
- package/package.json +5 -3
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* VennChart (diagramme de Venn / Euler) - API canonique (référence Svelte,
|
|
4
|
+
* React/Vue doivent s'aligner byte-pour-byte sur la géométrie).
|
|
5
|
+
*
|
|
6
|
+
* Deux ou trois ensembles dessinés en cercles superposés semi-transparents.
|
|
7
|
+
* Chaque zone (region) est décrite par les ensembles qu'elle couvre et une
|
|
8
|
+
* valeur :
|
|
9
|
+
* - { sets: ["A"], value } → membres de A seul ;
|
|
10
|
+
* - { sets: ["A","B"], value } → intersection A∩B ;
|
|
11
|
+
* - { sets: ["A","B","C"], value} → intersection triple.
|
|
12
|
+
*
|
|
13
|
+
* Géométrie DÉTERMINISTE (aucun aléatoire, layout identique entre frameworks) :
|
|
14
|
+
* - Les cercles sont placés à des positions FIXES selon le nombre d'ensembles
|
|
15
|
+
* (2 : côte à côte avec recouvrement ; 3 : triangle équilatéral).
|
|
16
|
+
* - Le rayon de chaque cercle ∝ √(taille totale de l'ensemble), où la taille
|
|
17
|
+
* d'un ensemble est la somme des valeurs de toutes les zones qui le
|
|
18
|
+
* contiennent. Mapping linéaire du √ entre un rayon min et un rayon max.
|
|
19
|
+
*
|
|
20
|
+
* Étiquettes : le nom de chaque ensemble est posé près du cercle ; la valeur de
|
|
21
|
+
* chaque zone d'intersection (sets.length ≥ 2) est posée au centroïde des
|
|
22
|
+
* centres des cercles concernés. Légende des ensembles + tooltip au survol +
|
|
23
|
+
* liste accessible (ChartDataList) hors SVG.
|
|
24
|
+
*
|
|
25
|
+
* Props :
|
|
26
|
+
* data VennChartArea[] - {sets, value}
|
|
27
|
+
* label string - aria-label (role=img)
|
|
28
|
+
* width number (420) - largeur du viewBox
|
|
29
|
+
* height number (360) - hauteur du viewBox
|
|
30
|
+
* class string - classe CSS additionnelle
|
|
31
|
+
*/
|
|
32
|
+
export type VennChartTone =
|
|
33
|
+
| "category1"
|
|
34
|
+
| "category2"
|
|
35
|
+
| "category3"
|
|
36
|
+
| "category4"
|
|
37
|
+
| "category5"
|
|
38
|
+
| "category6"
|
|
39
|
+
| "category7"
|
|
40
|
+
| "category8";
|
|
41
|
+
|
|
42
|
+
export type VennChartArea = {
|
|
43
|
+
sets: string[];
|
|
44
|
+
value: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type VennChartProps = {
|
|
48
|
+
data: VennChartArea[];
|
|
49
|
+
label: string;
|
|
50
|
+
width?: number;
|
|
51
|
+
height?: number;
|
|
52
|
+
class?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const TONES: VennChartTone[] = [
|
|
56
|
+
"category1",
|
|
57
|
+
"category2",
|
|
58
|
+
"category3",
|
|
59
|
+
"category4",
|
|
60
|
+
"category5",
|
|
61
|
+
"category6",
|
|
62
|
+
"category7",
|
|
63
|
+
"category8",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
type VennCircle = {
|
|
67
|
+
name: string;
|
|
68
|
+
tone: VennChartTone;
|
|
69
|
+
cx: number;
|
|
70
|
+
cy: number;
|
|
71
|
+
r: number;
|
|
72
|
+
total: number;
|
|
73
|
+
labelX: number;
|
|
74
|
+
labelY: number;
|
|
75
|
+
anchor: "start" | "middle" | "end";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type VennRegion = {
|
|
79
|
+
sets: string[];
|
|
80
|
+
value: number;
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type VennLayout = {
|
|
86
|
+
circles: VennCircle[];
|
|
87
|
+
regions: VennRegion[];
|
|
88
|
+
items: string[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function safeValue(value: number): number {
|
|
92
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Calcule la géométrie déterministe du diagramme. Identique entre frameworks.
|
|
97
|
+
*/
|
|
98
|
+
export function vennLayout(data: VennChartArea[], width: number, height: number): VennLayout {
|
|
99
|
+
const areas = data
|
|
100
|
+
.map((d) => ({ sets: Array.isArray(d.sets) ? d.sets.filter((s) => typeof s === "string") : [], value: safeValue(d.value) }))
|
|
101
|
+
.filter((d) => d.sets.length > 0 && d.value > 0);
|
|
102
|
+
|
|
103
|
+
// Ensembles dans l'ordre de première apparition (déterministe).
|
|
104
|
+
const order: string[] = [];
|
|
105
|
+
for (const a of areas) {
|
|
106
|
+
for (const s of a.sets) if (!order.includes(s)) order.push(s);
|
|
107
|
+
}
|
|
108
|
+
const names = order.slice(0, 3);
|
|
109
|
+
|
|
110
|
+
if (names.length === 0) {
|
|
111
|
+
return { circles: [], regions: [], items: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Taille totale d'un ensemble = somme des valeurs des zones qui le contiennent.
|
|
115
|
+
const totals = new Map<string, number>();
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
let sum = 0;
|
|
118
|
+
for (const a of areas) if (a.sets.includes(name)) sum += a.value;
|
|
119
|
+
totals.set(name, sum);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cx = width / 2;
|
|
123
|
+
const cy = height / 2;
|
|
124
|
+
const span = Math.min(width, height);
|
|
125
|
+
const rMax = span * 0.3;
|
|
126
|
+
const rMin = span * 0.2;
|
|
127
|
+
const roots = names.map((n) => Math.sqrt(totals.get(n) ?? 0));
|
|
128
|
+
const rootMin = Math.min(...roots);
|
|
129
|
+
const rootMax = Math.max(...roots);
|
|
130
|
+
const rootSpan = rootMax - rootMin;
|
|
131
|
+
const radiusFor = (root: number) => (rootSpan > 0 ? rMin + ((root - rootMin) / rootSpan) * (rMax - rMin) : rMax);
|
|
132
|
+
|
|
133
|
+
// Positions FIXES des centres selon le nombre d'ensembles.
|
|
134
|
+
let centers: Array<{ cx: number; cy: number }>;
|
|
135
|
+
if (names.length === 1) {
|
|
136
|
+
centers = [{ cx, cy }];
|
|
137
|
+
} else if (names.length === 2) {
|
|
138
|
+
const off = span * 0.16;
|
|
139
|
+
centers = [
|
|
140
|
+
{ cx: cx - off, cy },
|
|
141
|
+
{ cx: cx + off, cy },
|
|
142
|
+
];
|
|
143
|
+
} else {
|
|
144
|
+
// Triangle équilatéral (pointe en bas), centré.
|
|
145
|
+
const off = span * 0.17;
|
|
146
|
+
centers = [
|
|
147
|
+
{ cx: cx - off, cy: cy - off * 0.6 },
|
|
148
|
+
{ cx: cx + off, cy: cy - off * 0.6 },
|
|
149
|
+
{ cx, cy: cy + off * 0.85 },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const circles: VennCircle[] = names.map((name, i) => {
|
|
154
|
+
const r = radiusFor(roots[i]);
|
|
155
|
+
const c = centers[i];
|
|
156
|
+
// Étiquette poussée vers l'extérieur depuis le centre du diagramme.
|
|
157
|
+
const dx = c.cx - cx;
|
|
158
|
+
const dy = c.cy - cy;
|
|
159
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
160
|
+
const ux = names.length === 1 ? 0 : dx / len;
|
|
161
|
+
const uy = names.length === 1 ? -1 : dy / len;
|
|
162
|
+
const labelX = c.cx + ux * (r + 6);
|
|
163
|
+
const labelY = c.cy + uy * (r + 6);
|
|
164
|
+
const anchor: "start" | "middle" | "end" = ux > 0.3 ? "start" : ux < -0.3 ? "end" : "middle";
|
|
165
|
+
return {
|
|
166
|
+
name,
|
|
167
|
+
tone: TONES[i % TONES.length],
|
|
168
|
+
cx: c.cx,
|
|
169
|
+
cy: c.cy,
|
|
170
|
+
r,
|
|
171
|
+
total: totals.get(name) ?? 0,
|
|
172
|
+
labelX,
|
|
173
|
+
labelY,
|
|
174
|
+
anchor,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const centerByName = new Map(circles.map((c) => [c.name, c]));
|
|
179
|
+
const regions: VennRegion[] = areas
|
|
180
|
+
.filter((a) => a.sets.length >= 2)
|
|
181
|
+
.map((a) => {
|
|
182
|
+
const pts = a.sets.map((s) => centerByName.get(s)).filter((c): c is VennCircle => c !== undefined);
|
|
183
|
+
const px = pts.length > 0 ? pts.reduce((s, c) => s + c.cx, 0) / pts.length : cx;
|
|
184
|
+
const py = pts.length > 0 ? pts.reduce((s, c) => s + c.cy, 0) / pts.length : cy;
|
|
185
|
+
return { sets: a.sets, value: a.value, x: px, y: py };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const items = areas.map((a) => `${a.sets.join(" ∩ ")}: ${a.value}`);
|
|
189
|
+
|
|
190
|
+
return { circles, regions, items };
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
<script lang="ts">
|
|
195
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
196
|
+
|
|
197
|
+
let { data, label, width = 420, height = 360, class: className }: VennChartProps = $props();
|
|
198
|
+
|
|
199
|
+
let hoveredIndex: number | null = $state(null);
|
|
200
|
+
|
|
201
|
+
const layout = $derived(vennLayout(data, width, height));
|
|
202
|
+
const classes = () => ["st-vennChart", className].filter(Boolean).join(" ");
|
|
203
|
+
|
|
204
|
+
function handleVisualPointerMove(event: PointerEvent) {
|
|
205
|
+
const target = event.target;
|
|
206
|
+
if (!(target instanceof Element)) {
|
|
207
|
+
hoveredIndex = null;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const index = Number(target.getAttribute("data-chart-index"));
|
|
211
|
+
hoveredIndex = Number.isInteger(index) ? index : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const hovered = $derived(hoveredIndex !== null ? layout.circles[hoveredIndex] : undefined);
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<div class={classes()}>
|
|
218
|
+
<div
|
|
219
|
+
class="st-vennChart__visual"
|
|
220
|
+
role="img"
|
|
221
|
+
aria-label={label}
|
|
222
|
+
onpointermove={handleVisualPointerMove}
|
|
223
|
+
onpointerleave={() => (hoveredIndex = null)}
|
|
224
|
+
>
|
|
225
|
+
<svg
|
|
226
|
+
viewBox="0 0 {width} {height}"
|
|
227
|
+
preserveAspectRatio="xMidYMid meet"
|
|
228
|
+
width="100%"
|
|
229
|
+
height="100%"
|
|
230
|
+
focusable="false"
|
|
231
|
+
aria-hidden="true"
|
|
232
|
+
>
|
|
233
|
+
{#each layout.circles as circle, i (circle.name)}
|
|
234
|
+
<circle
|
|
235
|
+
class="st-vennChart__circle st-vennChart__circle--{circle.tone}"
|
|
236
|
+
class:st-vennChart__circle--dim={hoveredIndex !== null && hoveredIndex !== i}
|
|
237
|
+
cx={circle.cx}
|
|
238
|
+
cy={circle.cy}
|
|
239
|
+
r={circle.r}
|
|
240
|
+
data-chart-index={i}
|
|
241
|
+
/>
|
|
242
|
+
{/each}
|
|
243
|
+
|
|
244
|
+
{#each layout.regions as region (region.sets.join("|"))}
|
|
245
|
+
<text class="st-vennChart__value" x={region.x} y={region.y} text-anchor="middle" dominant-baseline="middle">
|
|
246
|
+
{region.value}
|
|
247
|
+
</text>
|
|
248
|
+
{/each}
|
|
249
|
+
|
|
250
|
+
{#each layout.circles as circle (circle.name)}
|
|
251
|
+
<text
|
|
252
|
+
class="st-vennChart__label"
|
|
253
|
+
x={circle.labelX}
|
|
254
|
+
y={circle.labelY}
|
|
255
|
+
text-anchor={circle.anchor}
|
|
256
|
+
dominant-baseline="middle"
|
|
257
|
+
>
|
|
258
|
+
{circle.name}
|
|
259
|
+
</text>
|
|
260
|
+
{/each}
|
|
261
|
+
</svg>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<ChartDataList {label} items={layout.items} />
|
|
265
|
+
|
|
266
|
+
{#if hovered}
|
|
267
|
+
<div
|
|
268
|
+
class="st-vennChart__tooltip"
|
|
269
|
+
role="presentation"
|
|
270
|
+
style="left: {(hovered.cx / width) * 100}%; top: {(hovered.cy / height) * 100}%;"
|
|
271
|
+
>
|
|
272
|
+
<span class="st-vennChart__tooltipLabel">{hovered.name}</span>
|
|
273
|
+
<span class="st-vennChart__tooltipValue">{hovered.total}</span>
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<style>
|
|
279
|
+
.st-vennChart {
|
|
280
|
+
color: var(--st-semantic-text-secondary);
|
|
281
|
+
display: block;
|
|
282
|
+
font-family: inherit;
|
|
283
|
+
max-width: 100%;
|
|
284
|
+
position: relative;
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.st-vennChart svg,
|
|
289
|
+
.st-vennChart__visual {
|
|
290
|
+
display: block;
|
|
291
|
+
overflow: visible;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.st-vennChart__circle {
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
fill-opacity: 0.42;
|
|
297
|
+
stroke: var(--st-semantic-surface-default, Canvas);
|
|
298
|
+
stroke-width: 1.5;
|
|
299
|
+
transition: opacity 120ms ease;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.st-vennChart__circle--dim {
|
|
303
|
+
opacity: 0.45;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.st-vennChart__circle--category1 { fill: var(--st-semantic-data-category1); }
|
|
307
|
+
.st-vennChart__circle--category2 { fill: var(--st-semantic-data-category2); }
|
|
308
|
+
.st-vennChart__circle--category3 { fill: var(--st-semantic-data-category3); }
|
|
309
|
+
.st-vennChart__circle--category4 { fill: var(--st-semantic-data-category4); }
|
|
310
|
+
.st-vennChart__circle--category5 { fill: var(--st-semantic-data-category5); }
|
|
311
|
+
.st-vennChart__circle--category6 { fill: var(--st-semantic-data-category6); }
|
|
312
|
+
.st-vennChart__circle--category7 { fill: var(--st-semantic-data-category7); }
|
|
313
|
+
.st-vennChart__circle--category8 { fill: var(--st-semantic-data-category8); }
|
|
314
|
+
|
|
315
|
+
.st-vennChart__label {
|
|
316
|
+
fill: var(--st-semantic-text-primary);
|
|
317
|
+
font-size: 0.78rem;
|
|
318
|
+
font-weight: 650;
|
|
319
|
+
pointer-events: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.st-vennChart__value {
|
|
323
|
+
fill: var(--st-semantic-text-primary);
|
|
324
|
+
font-size: 0.72rem;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
pointer-events: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.st-vennChart__tooltip {
|
|
330
|
+
background: var(--st-semantic-surface-inverse);
|
|
331
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
332
|
+
color: var(--st-semantic-text-inverse);
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
font-size: 0.75rem;
|
|
336
|
+
gap: 0.125rem;
|
|
337
|
+
line-height: 1.2;
|
|
338
|
+
padding: 0.375rem 0.5rem;
|
|
339
|
+
pointer-events: none;
|
|
340
|
+
position: absolute;
|
|
341
|
+
transform: translate(-50%, -115%);
|
|
342
|
+
white-space: nowrap;
|
|
343
|
+
z-index: 1;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.st-vennChart__tooltipLabel { font-weight: 600; }
|
|
347
|
+
.st-vennChart__tooltipValue { opacity: 0.85; }
|
|
348
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VennChart (diagramme de Venn / Euler) - API canonique (référence Svelte,
|
|
3
|
+
* React/Vue doivent s'aligner byte-pour-byte sur la géométrie).
|
|
4
|
+
*
|
|
5
|
+
* Deux ou trois ensembles dessinés en cercles superposés semi-transparents.
|
|
6
|
+
* Chaque zone (region) est décrite par les ensembles qu'elle couvre et une
|
|
7
|
+
* valeur :
|
|
8
|
+
* - { sets: ["A"], value } → membres de A seul ;
|
|
9
|
+
* - { sets: ["A","B"], value } → intersection A∩B ;
|
|
10
|
+
* - { sets: ["A","B","C"], value} → intersection triple.
|
|
11
|
+
*
|
|
12
|
+
* Géométrie DÉTERMINISTE (aucun aléatoire, layout identique entre frameworks) :
|
|
13
|
+
* - Les cercles sont placés à des positions FIXES selon le nombre d'ensembles
|
|
14
|
+
* (2 : côte à côte avec recouvrement ; 3 : triangle équilatéral).
|
|
15
|
+
* - Le rayon de chaque cercle ∝ √(taille totale de l'ensemble), où la taille
|
|
16
|
+
* d'un ensemble est la somme des valeurs de toutes les zones qui le
|
|
17
|
+
* contiennent. Mapping linéaire du √ entre un rayon min et un rayon max.
|
|
18
|
+
*
|
|
19
|
+
* Étiquettes : le nom de chaque ensemble est posé près du cercle ; la valeur de
|
|
20
|
+
* chaque zone d'intersection (sets.length ≥ 2) est posée au centroïde des
|
|
21
|
+
* centres des cercles concernés. Légende des ensembles + tooltip au survol +
|
|
22
|
+
* liste accessible (ChartDataList) hors SVG.
|
|
23
|
+
*
|
|
24
|
+
* Props :
|
|
25
|
+
* data VennChartArea[] - {sets, value}
|
|
26
|
+
* label string - aria-label (role=img)
|
|
27
|
+
* width number (420) - largeur du viewBox
|
|
28
|
+
* height number (360) - hauteur du viewBox
|
|
29
|
+
* class string - classe CSS additionnelle
|
|
30
|
+
*/
|
|
31
|
+
export type VennChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
32
|
+
export type VennChartArea = {
|
|
33
|
+
sets: string[];
|
|
34
|
+
value: number;
|
|
35
|
+
};
|
|
36
|
+
export type VennChartProps = {
|
|
37
|
+
data: VennChartArea[];
|
|
38
|
+
label: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
class?: string;
|
|
42
|
+
};
|
|
43
|
+
type VennCircle = {
|
|
44
|
+
name: string;
|
|
45
|
+
tone: VennChartTone;
|
|
46
|
+
cx: number;
|
|
47
|
+
cy: number;
|
|
48
|
+
r: number;
|
|
49
|
+
total: number;
|
|
50
|
+
labelX: number;
|
|
51
|
+
labelY: number;
|
|
52
|
+
anchor: "start" | "middle" | "end";
|
|
53
|
+
};
|
|
54
|
+
type VennRegion = {
|
|
55
|
+
sets: string[];
|
|
56
|
+
value: number;
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
};
|
|
60
|
+
export type VennLayout = {
|
|
61
|
+
circles: VennCircle[];
|
|
62
|
+
regions: VennRegion[];
|
|
63
|
+
items: string[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Calcule la géométrie déterministe du diagramme. Identique entre frameworks.
|
|
67
|
+
*/
|
|
68
|
+
export declare function vennLayout(data: VennChartArea[], width: number, height: number): VennLayout;
|
|
69
|
+
declare const VennChart: import("svelte").Component<VennChartProps, {}, "">;
|
|
70
|
+
type VennChart = ReturnType<typeof VennChart>;
|
|
71
|
+
export default VennChart;
|
|
72
|
+
//# sourceMappingURL=VennChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VennChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/VennChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAaF,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,aAAa,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;CACpC,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAMF;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CA6F3F;AAiEH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type WordCloudChartTone =
|
|
3
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
4
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
5
|
+
|
|
6
|
+
export type WordCloudChartWord = {
|
|
7
|
+
text: string;
|
|
8
|
+
weight: number;
|
|
9
|
+
tone?: WordCloudChartTone;
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
15
|
+
|
|
16
|
+
type WordCloudChartProps = {
|
|
17
|
+
/** Mots à placer : la taille de police est proportionnelle au poids. */
|
|
18
|
+
data: WordCloudChartWord[];
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
label: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
data,
|
|
27
|
+
width = 480,
|
|
28
|
+
height = 320,
|
|
29
|
+
label,
|
|
30
|
+
class: className
|
|
31
|
+
}: WordCloudChartProps = $props();
|
|
32
|
+
|
|
33
|
+
const TONES = [
|
|
34
|
+
"category1", "category2", "category3", "category4",
|
|
35
|
+
"category5", "category6", "category7", "category8"
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
// Constantes de layout — IDENTIQUES entre svelte/react/vue (aucun aléatoire).
|
|
39
|
+
const FONT_MIN = 14;
|
|
40
|
+
const FONT_MAX = 52;
|
|
41
|
+
// Largeur estimée d'un glyphe relative à la taille de police.
|
|
42
|
+
const CHAR_W = 0.6;
|
|
43
|
+
// Spirale d'Archimède : r = SPIRAL_A * theta ; pas angulaire constant.
|
|
44
|
+
const SPIRAL_STEP = 0.25;
|
|
45
|
+
const MAX_TURNS = 60;
|
|
46
|
+
// Marge entre boîtes englobantes (en unités SVG).
|
|
47
|
+
const GAP = 4;
|
|
48
|
+
|
|
49
|
+
type Box = { x: number; y: number; w: number; h: number };
|
|
50
|
+
type Placed = {
|
|
51
|
+
word: WordCloudChartWord;
|
|
52
|
+
tone: WordCloudChartTone;
|
|
53
|
+
fontSize: number;
|
|
54
|
+
cx: number;
|
|
55
|
+
cy: number;
|
|
56
|
+
box: Box;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const validWord = (w: WordCloudChartWord): boolean =>
|
|
60
|
+
!!w && typeof w.text === "string" && w.text.length > 0 &&
|
|
61
|
+
Number.isFinite(w.weight) && w.weight > 0;
|
|
62
|
+
|
|
63
|
+
function overlaps(a: Box, b: Box): boolean {
|
|
64
|
+
return !(
|
|
65
|
+
a.x + a.w + GAP <= b.x ||
|
|
66
|
+
b.x + b.w + GAP <= a.x ||
|
|
67
|
+
a.y + a.h + GAP <= b.y ||
|
|
68
|
+
b.y + b.h + GAP <= a.y
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type Layout = { placed: Placed[]; omitted: WordCloudChartWord[] };
|
|
73
|
+
|
|
74
|
+
const layout = $derived.by<Layout>(() => {
|
|
75
|
+
const words = (data ?? []).filter(validWord);
|
|
76
|
+
if (words.length === 0) return { placed: [], omitted: [] };
|
|
77
|
+
|
|
78
|
+
// Trie par poids décroissant (placement déterministe).
|
|
79
|
+
const sorted = words
|
|
80
|
+
.map((w, i) => ({ w, i }))
|
|
81
|
+
.sort((a, b) => (b.w.weight - a.w.weight) || (a.i - b.i))
|
|
82
|
+
.map((e) => e.w);
|
|
83
|
+
|
|
84
|
+
const minW = sorted[sorted.length - 1].weight;
|
|
85
|
+
const maxW = sorted[0].weight;
|
|
86
|
+
const span = maxW - minW;
|
|
87
|
+
|
|
88
|
+
const fontFor = (weight: number): number => {
|
|
89
|
+
if (span <= 0) return (FONT_MIN + FONT_MAX) / 2;
|
|
90
|
+
const t = (weight - minW) / span;
|
|
91
|
+
return FONT_MIN + t * (FONT_MAX - FONT_MIN);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const cx0 = width / 2;
|
|
95
|
+
const cy0 = height / 2;
|
|
96
|
+
const a = Math.min(width, height) / (2 * Math.PI * MAX_TURNS);
|
|
97
|
+
|
|
98
|
+
const placed: Placed[] = [];
|
|
99
|
+
const omitted: WordCloudChartWord[] = [];
|
|
100
|
+
|
|
101
|
+
sorted.forEach((word, idx) => {
|
|
102
|
+
const fontSize = fontFor(word.weight);
|
|
103
|
+
const w = word.text.length * fontSize * CHAR_W;
|
|
104
|
+
const h = fontSize;
|
|
105
|
+
const tone = word.tone ?? TONES[idx % TONES.length];
|
|
106
|
+
|
|
107
|
+
let foundCx = cx0;
|
|
108
|
+
let foundCy = cy0;
|
|
109
|
+
let placedOk = false;
|
|
110
|
+
|
|
111
|
+
// Parcourt la spirale d'Archimède depuis le centre.
|
|
112
|
+
for (let theta = 0; theta <= MAX_TURNS * 2 * Math.PI; theta += SPIRAL_STEP) {
|
|
113
|
+
const r = a * theta;
|
|
114
|
+
const cx = cx0 + r * Math.cos(theta);
|
|
115
|
+
const cy = cy0 + r * Math.sin(theta);
|
|
116
|
+
const box: Box = { x: cx - w / 2, y: cy - h / 2, w, h };
|
|
117
|
+
|
|
118
|
+
// Hors cadre → on continue.
|
|
119
|
+
if (box.x < 0 || box.y < 0 || box.x + box.w > width || box.y + box.h > height) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let collides = false;
|
|
123
|
+
for (const p of placed) {
|
|
124
|
+
if (overlaps(box, p.box)) {
|
|
125
|
+
collides = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!collides) {
|
|
130
|
+
foundCx = cx;
|
|
131
|
+
foundCy = cy;
|
|
132
|
+
placedOk = true;
|
|
133
|
+
placed.push({ word, tone, fontSize, cx, cy, box });
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!placedOk) {
|
|
139
|
+
// Centre (theta=0) déjà testé : si rien ne rentre, on omet le mot.
|
|
140
|
+
omitted.push(word);
|
|
141
|
+
}
|
|
142
|
+
void foundCx;
|
|
143
|
+
void foundCy;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return { placed, omitted };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const placed = $derived(layout.placed);
|
|
150
|
+
const omitted = $derived(layout.omitted);
|
|
151
|
+
|
|
152
|
+
const dataValueItems = $derived([
|
|
153
|
+
...placed.map((p) => `${p.word.text}: ${p.word.weight}`),
|
|
154
|
+
...omitted.map((o) => `${o.text}: ${o.weight} (omis)`)
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
let hoveredIndex: number | null = $state(null);
|
|
158
|
+
|
|
159
|
+
function handleVisualPointerMove(event: PointerEvent) {
|
|
160
|
+
const target = event.target;
|
|
161
|
+
if (!(target instanceof Element)) {
|
|
162
|
+
hoveredIndex = null;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const index = Number(target.getAttribute("data-chart-index"));
|
|
166
|
+
hoveredIndex = Number.isInteger(index) ? index : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const classes = () => ["st-wordCloudChart", className].filter(Boolean).join(" ");
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<div class={classes()}>
|
|
173
|
+
<div
|
|
174
|
+
class="st-wordCloudChart__visual"
|
|
175
|
+
role="img"
|
|
176
|
+
aria-label={label}
|
|
177
|
+
onpointermove={handleVisualPointerMove}
|
|
178
|
+
onpointerleave={() => (hoveredIndex = null)}
|
|
179
|
+
>
|
|
180
|
+
<svg
|
|
181
|
+
viewBox="0 0 {width} {height}"
|
|
182
|
+
preserveAspectRatio="xMidYMid meet"
|
|
183
|
+
width="100%"
|
|
184
|
+
height="100%"
|
|
185
|
+
focusable="false"
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
>
|
|
188
|
+
{#each placed as p, i (p.word.text)}
|
|
189
|
+
<text
|
|
190
|
+
class="st-wordCloudChart__word st-wordCloudChart__word--{p.tone}"
|
|
191
|
+
class:st-wordCloudChart__word--dim={hoveredIndex !== null && hoveredIndex !== i}
|
|
192
|
+
x={p.cx}
|
|
193
|
+
y={p.cy}
|
|
194
|
+
text-anchor="middle"
|
|
195
|
+
dominant-baseline="central"
|
|
196
|
+
font-size={p.fontSize}
|
|
197
|
+
data-chart-index={i}
|
|
198
|
+
>
|
|
199
|
+
{p.word.text}
|
|
200
|
+
</text>
|
|
201
|
+
{/each}
|
|
202
|
+
</svg>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<ChartDataList {label} items={dataValueItems} />
|
|
206
|
+
|
|
207
|
+
{#if hoveredIndex !== null && placed[hoveredIndex]}
|
|
208
|
+
{@const p = placed[hoveredIndex]}
|
|
209
|
+
<div
|
|
210
|
+
class="st-wordCloudChart__tooltip"
|
|
211
|
+
role="presentation"
|
|
212
|
+
style="left: {(p.cx / width) * 100}%; top: {((p.cy - p.fontSize / 2) / height) * 100}%"
|
|
213
|
+
>
|
|
214
|
+
<span class="st-wordCloudChart__tooltipLabel">{p.word.text}</span>
|
|
215
|
+
<span class="st-wordCloudChart__tooltipValue">{p.word.weight}</span>
|
|
216
|
+
</div>
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<style>
|
|
221
|
+
.st-wordCloudChart {
|
|
222
|
+
color: var(--st-semantic-text-secondary);
|
|
223
|
+
display: block;
|
|
224
|
+
font-family: inherit;
|
|
225
|
+
position: relative;
|
|
226
|
+
width: 100%;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.st-wordCloudChart svg,
|
|
230
|
+
.st-wordCloudChart__visual {
|
|
231
|
+
display: block;
|
|
232
|
+
overflow: visible;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.st-wordCloudChart__word {
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
transition: opacity 120ms ease;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.st-wordCloudChart__word--dim {
|
|
242
|
+
opacity: 0.3;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@media (prefers-reduced-motion: reduce) {
|
|
246
|
+
.st-wordCloudChart__word {
|
|
247
|
+
transition: none;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.st-wordCloudChart__word--category1 { fill: var(--st-semantic-data-category1); }
|
|
252
|
+
.st-wordCloudChart__word--category2 { fill: var(--st-semantic-data-category2); }
|
|
253
|
+
.st-wordCloudChart__word--category3 { fill: var(--st-semantic-data-category3); }
|
|
254
|
+
.st-wordCloudChart__word--category4 { fill: var(--st-semantic-data-category4); }
|
|
255
|
+
.st-wordCloudChart__word--category5 { fill: var(--st-semantic-data-category5); }
|
|
256
|
+
.st-wordCloudChart__word--category6 { fill: var(--st-semantic-data-category6); }
|
|
257
|
+
.st-wordCloudChart__word--category7 { fill: var(--st-semantic-data-category7); }
|
|
258
|
+
.st-wordCloudChart__word--category8 { fill: var(--st-semantic-data-category8); }
|
|
259
|
+
|
|
260
|
+
.st-wordCloudChart__tooltip {
|
|
261
|
+
background: var(--st-semantic-surface-inverse);
|
|
262
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
263
|
+
color: var(--st-semantic-text-inverse);
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
flex-direction: column;
|
|
266
|
+
font-size: 0.75rem;
|
|
267
|
+
gap: 0.125rem;
|
|
268
|
+
line-height: 1.2;
|
|
269
|
+
padding: 0.375rem 0.5rem;
|
|
270
|
+
pointer-events: none;
|
|
271
|
+
position: absolute;
|
|
272
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
273
|
+
white-space: nowrap;
|
|
274
|
+
z-index: 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.st-wordCloudChart__tooltipLabel { font-weight: 600; }
|
|
278
|
+
.st-wordCloudChart__tooltipValue { opacity: 0.85; }
|
|
279
|
+
</style>
|