@sentropic/design-system-svelte 0.31.0 → 0.33.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/GeoMap.svelte +754 -0
- package/dist/GeoMap.svelte.d.ts +118 -0
- package/dist/GeoMap.svelte.d.ts.map +1 -0
- package/dist/LineChart.svelte +81 -7
- package/dist/LineChart.svelte.d.ts +8 -0
- package/dist/LineChart.svelte.d.ts.map +1 -1
- package/dist/ScatterPlot.svelte +74 -6
- package/dist/ScatterPlot.svelte.d.ts +17 -0
- package/dist/ScatterPlot.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type GeoMapTone =
|
|
3
|
+
| "category1" | "category2" | "category3" | "category4"
|
|
4
|
+
| "category5" | "category6" | "category7" | "category8";
|
|
5
|
+
|
|
6
|
+
/** Coordonnée géographique — même forme que `GeoCoordinate` (dataviz-core). */
|
|
7
|
+
export type GeoMapCoordinate = {
|
|
8
|
+
latitude: number;
|
|
9
|
+
longitude: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Emprise géographique — même forme que `GeoBounds` (dataviz-core). */
|
|
13
|
+
export type GeoMapBounds = {
|
|
14
|
+
south: number;
|
|
15
|
+
west: number;
|
|
16
|
+
north: number;
|
|
17
|
+
east: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type GeoMapProjection = "equirectangular" | "mercator";
|
|
21
|
+
|
|
22
|
+
export type GeoMapGeometryType =
|
|
23
|
+
| "Point"
|
|
24
|
+
| "MultiPoint"
|
|
25
|
+
| "LineString"
|
|
26
|
+
| "MultiLineString"
|
|
27
|
+
| "Polygon"
|
|
28
|
+
| "MultiPolygon";
|
|
29
|
+
|
|
30
|
+
/** Géométrie GeoJSON — même forme que `GeoJsonGeometry` (dataviz-core). */
|
|
31
|
+
export type GeoMapGeometry = {
|
|
32
|
+
type: GeoMapGeometryType;
|
|
33
|
+
coordinates: unknown[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Entité géographique — sous-ensemble structurel de `GeoJsonFeature` (dataviz-core). */
|
|
37
|
+
export type GeoMapFeature = {
|
|
38
|
+
id: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
value?: number;
|
|
41
|
+
geometry: GeoMapGeometry;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Point géographique — sur-ensemble structurel de `GeoPoint` (dataviz-core). */
|
|
45
|
+
export type GeoMapPoint = GeoMapCoordinate & {
|
|
46
|
+
id?: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
value?: number;
|
|
49
|
+
tone?: GeoMapTone;
|
|
50
|
+
/** Rayon explicite en px (prioritaire sur l'échelle par `value`), borné à 32. */
|
|
51
|
+
r?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Flux géographique — sous-ensemble structurel de `GeoFlowLink` (dataviz-core). */
|
|
55
|
+
export type GeoMapFlow = {
|
|
56
|
+
id?: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
source: GeoMapCoordinate;
|
|
59
|
+
target: GeoMapCoordinate;
|
|
60
|
+
value?: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Entités GeoJSON (polygones, lignes, points) ; ton par couche ou cycle de palette par entité. */
|
|
64
|
+
export type GeoMapGeojsonLayer = {
|
|
65
|
+
type: "geojson";
|
|
66
|
+
features: GeoMapFeature[];
|
|
67
|
+
tone?: GeoMapTone;
|
|
68
|
+
label?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Choroplèthe : entités + valeur par id (`regions` dataviz : `key` → `value`) → intensité color-mix. */
|
|
72
|
+
export type GeoMapChoroplethLayer = {
|
|
73
|
+
type: "choropleth";
|
|
74
|
+
features: GeoMapFeature[];
|
|
75
|
+
values: Record<string, number>;
|
|
76
|
+
/** Ton de base de la rampe d'intensité (défaut `category1`). */
|
|
77
|
+
tone?: GeoMapTone;
|
|
78
|
+
label?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Points/épingles : rayon ∝ `value` (bornes `minRadius`/`maxRadius`, défauts 5/14 comme dataviz). */
|
|
82
|
+
export type GeoMapPointsLayer = {
|
|
83
|
+
type: "points";
|
|
84
|
+
points: GeoMapPoint[];
|
|
85
|
+
tone?: GeoMapTone;
|
|
86
|
+
minRadius?: number;
|
|
87
|
+
maxRadius?: number;
|
|
88
|
+
label?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Densité : cercles translucides superposés, rayon/intensité ∝ `value` (poids). */
|
|
92
|
+
export type GeoMapDensityLayer = {
|
|
93
|
+
type: "density";
|
|
94
|
+
points: GeoMapPoint[];
|
|
95
|
+
/** Ton de la nappe (défaut `category3` — parité visuelle dataviz). */
|
|
96
|
+
tone?: GeoMapTone;
|
|
97
|
+
maxRadius?: number;
|
|
98
|
+
label?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Flux : arcs quadratiques source → target, épaisseur ∝ `value` (défaut ton `category1`). */
|
|
102
|
+
export type GeoMapFlowLayer = {
|
|
103
|
+
type: "flow";
|
|
104
|
+
flows: GeoMapFlow[];
|
|
105
|
+
tone?: GeoMapTone;
|
|
106
|
+
label?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Hexbin : binning hexagonal des points (même binning que dataviz-core), intensité ∝ valeur agrégée. */
|
|
110
|
+
export type GeoMapHexbinLayer = {
|
|
111
|
+
type: "hexbin";
|
|
112
|
+
points: GeoMapPoint[];
|
|
113
|
+
/** Taille de cellule en degrés (défaut 1). */
|
|
114
|
+
cellSize?: number;
|
|
115
|
+
tone?: GeoMapTone;
|
|
116
|
+
label?: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Clusters : regroupement glouton des points (même algo que dataviz-core), centroïdes marqueurs distinctifs. */
|
|
120
|
+
export type GeoMapClusterLayer = {
|
|
121
|
+
type: "cluster";
|
|
122
|
+
points: GeoMapPoint[];
|
|
123
|
+
/** Rayon de regroupement en degrés (défaut 1). */
|
|
124
|
+
radius?: number;
|
|
125
|
+
tone?: GeoMapTone;
|
|
126
|
+
label?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type GeoMapLayer =
|
|
130
|
+
| GeoMapGeojsonLayer
|
|
131
|
+
| GeoMapChoroplethLayer
|
|
132
|
+
| GeoMapPointsLayer
|
|
133
|
+
| GeoMapDensityLayer
|
|
134
|
+
| GeoMapFlowLayer
|
|
135
|
+
| GeoMapHexbinLayer
|
|
136
|
+
| GeoMapClusterLayer;
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<script lang="ts">
|
|
140
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
141
|
+
|
|
142
|
+
type GeoMapProps = {
|
|
143
|
+
layers: GeoMapLayer[];
|
|
144
|
+
width?: number;
|
|
145
|
+
height?: number;
|
|
146
|
+
projection?: GeoMapProjection;
|
|
147
|
+
/** Emprise explicite ; sinon auto-ajustement sur les données de toutes les couches + marge. */
|
|
148
|
+
bounds?: GeoMapBounds;
|
|
149
|
+
label: string;
|
|
150
|
+
class?: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let {
|
|
154
|
+
layers,
|
|
155
|
+
width = 520,
|
|
156
|
+
height = 320,
|
|
157
|
+
projection = "equirectangular",
|
|
158
|
+
bounds,
|
|
159
|
+
label,
|
|
160
|
+
class: className
|
|
161
|
+
}: GeoMapProps = $props();
|
|
162
|
+
|
|
163
|
+
const PADDING = 24;
|
|
164
|
+
const MAX_POINT_RADIUS = 32;
|
|
165
|
+
const WORLD: GeoMapBounds = { south: -90, west: -180, north: 90, east: 180 };
|
|
166
|
+
const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
|
|
167
|
+
const GEOMETRY_TYPES = new Set(["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon"]);
|
|
168
|
+
|
|
169
|
+
function isFiniteCoordinate<T extends GeoMapCoordinate>(c: T | undefined): c is T {
|
|
170
|
+
return !!c && Number.isFinite(c.latitude) && Number.isFinite(c.longitude);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function scaleNumber(value: number, min: number, max: number, start: number, end: number): number {
|
|
174
|
+
return max === min ? (start + end) / 2 : start + ((value - min) / (max - min)) * (end - start);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Pourcentage color-mix valide (0–100, arrondi). */
|
|
178
|
+
function mixPercent(value: number): number {
|
|
179
|
+
return Math.round(Math.max(0, Math.min(100, value)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function coordinatePair(value: unknown): GeoMapCoordinate | undefined {
|
|
183
|
+
if (!Array.isArray(value) || value.length < 2 || Array.isArray(value[0])) return undefined;
|
|
184
|
+
const longitude = Number(value[0]);
|
|
185
|
+
const latitude = Number(value[1]);
|
|
186
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return undefined;
|
|
187
|
+
return { latitude, longitude };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectGeometryCoordinates(value: unknown, out: GeoMapCoordinate[]): void {
|
|
191
|
+
if (!Array.isArray(value)) return;
|
|
192
|
+
const pair = coordinatePair(value);
|
|
193
|
+
if (pair) {
|
|
194
|
+
out.push(pair);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const item of value) collectGeometryCoordinates(item, out);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isGeometry(geometry: GeoMapGeometry | undefined): geometry is GeoMapGeometry {
|
|
201
|
+
return !!geometry && GEOMETRY_TYPES.has(geometry.type) && Array.isArray(geometry.coordinates);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function layerCoordinates(layer: GeoMapLayer): GeoMapCoordinate[] {
|
|
205
|
+
switch (layer.type) {
|
|
206
|
+
case "geojson":
|
|
207
|
+
case "choropleth": {
|
|
208
|
+
const out: GeoMapCoordinate[] = [];
|
|
209
|
+
for (const feature of layer.features ?? []) {
|
|
210
|
+
if (isGeometry(feature.geometry)) collectGeometryCoordinates(feature.geometry.coordinates, out);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
case "points":
|
|
215
|
+
case "density":
|
|
216
|
+
case "hexbin":
|
|
217
|
+
case "cluster":
|
|
218
|
+
return (layer.points ?? []).filter(isFiniteCoordinate);
|
|
219
|
+
case "flow": {
|
|
220
|
+
const out: GeoMapCoordinate[] = [];
|
|
221
|
+
for (const flow of layer.flows ?? []) {
|
|
222
|
+
if (isFiniteCoordinate(flow.source)) out.push(flow.source);
|
|
223
|
+
if (isFiniteCoordinate(flow.target)) out.push(flow.target);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validBounds(candidate: GeoMapBounds | undefined): GeoMapBounds | null {
|
|
231
|
+
return candidate &&
|
|
232
|
+
Number.isFinite(candidate.south) && Number.isFinite(candidate.west) &&
|
|
233
|
+
Number.isFinite(candidate.north) && Number.isFinite(candidate.east) &&
|
|
234
|
+
candidate.north > candidate.south && candidate.east > candidate.west
|
|
235
|
+
? candidate
|
|
236
|
+
: null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function fitBounds(all: GeoMapLayer[]): GeoMapBounds {
|
|
240
|
+
const coords = all.flatMap(layerCoordinates);
|
|
241
|
+
if (coords.length === 0) return WORLD;
|
|
242
|
+
let south = Infinity, west = Infinity, north = -Infinity, east = -Infinity;
|
|
243
|
+
for (const c of coords) {
|
|
244
|
+
if (c.latitude < south) south = c.latitude;
|
|
245
|
+
if (c.latitude > north) north = c.latitude;
|
|
246
|
+
if (c.longitude < west) west = c.longitude;
|
|
247
|
+
if (c.longitude > east) east = c.longitude;
|
|
248
|
+
}
|
|
249
|
+
// Marge : 5 % de l'étendue, 1° minimum (point isolé), bornée au monde.
|
|
250
|
+
const latPad = Math.max((north - south) * 0.05, 1);
|
|
251
|
+
const lonPad = Math.max((east - west) * 0.05, 1);
|
|
252
|
+
return {
|
|
253
|
+
south: Math.max(south - latPad, -90),
|
|
254
|
+
west: Math.max(west - lonPad, -180),
|
|
255
|
+
north: Math.min(north + latPad, 90),
|
|
256
|
+
east: Math.min(east + lonPad, 180)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mercatorY(latitude: number): number {
|
|
261
|
+
const clamped = Math.max(-85, Math.min(85, latitude));
|
|
262
|
+
return Math.log(Math.tan(Math.PI / 4 + (clamped * Math.PI) / 360));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const mapBounds = $derived(validBounds(bounds) ?? fitBounds(layers ?? []));
|
|
266
|
+
|
|
267
|
+
const project = $derived.by(() => {
|
|
268
|
+
const b = mapBounds;
|
|
269
|
+
const innerW = Math.max(width - PADDING * 2, 1);
|
|
270
|
+
const innerH = Math.max(height - PADDING * 2, 1);
|
|
271
|
+
const projY = (latitude: number) => (projection === "mercator" ? mercatorY(latitude) : latitude);
|
|
272
|
+
const top = projY(b.north);
|
|
273
|
+
const bottom = projY(b.south);
|
|
274
|
+
const lonSpan = b.east - b.west || 1;
|
|
275
|
+
const ySpan = top - bottom || 1;
|
|
276
|
+
return (c: GeoMapCoordinate) => ({
|
|
277
|
+
x: PADDING + ((c.longitude - b.west) / lonSpan) * innerW,
|
|
278
|
+
y: PADDING + ((top - projY(c.latitude)) / ySpan) * innerH
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
function linePath(coordinates: unknown[]): string {
|
|
283
|
+
return coordinates
|
|
284
|
+
.map((item, index) => {
|
|
285
|
+
const pair = coordinatePair(item);
|
|
286
|
+
if (!pair) return "";
|
|
287
|
+
const p = project(pair);
|
|
288
|
+
return `${index === 0 ? "M" : "L"} ${p.x} ${p.y}`;
|
|
289
|
+
})
|
|
290
|
+
.filter(Boolean)
|
|
291
|
+
.join(" ");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ringsPath(coordinates: unknown[]): string {
|
|
295
|
+
return coordinates
|
|
296
|
+
.map((ring) => {
|
|
297
|
+
const path = Array.isArray(ring) ? linePath(ring) : "";
|
|
298
|
+
return path === "" ? "" : `${path} Z`;
|
|
299
|
+
})
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
.join(" ");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function geometryPath(geometry: GeoMapGeometry): string {
|
|
305
|
+
switch (geometry.type) {
|
|
306
|
+
case "Point": {
|
|
307
|
+
const pair = coordinatePair(geometry.coordinates);
|
|
308
|
+
if (!pair) return "";
|
|
309
|
+
const p = project(pair);
|
|
310
|
+
return `M ${p.x - 5} ${p.y} a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0`;
|
|
311
|
+
}
|
|
312
|
+
case "MultiPoint":
|
|
313
|
+
return geometry.coordinates
|
|
314
|
+
.map((c) => geometryPath({ type: "Point", coordinates: c as unknown[] }))
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.join(" ");
|
|
317
|
+
case "LineString":
|
|
318
|
+
return linePath(geometry.coordinates);
|
|
319
|
+
case "MultiLineString":
|
|
320
|
+
return geometry.coordinates
|
|
321
|
+
.map((line) => (Array.isArray(line) ? linePath(line) : ""))
|
|
322
|
+
.filter(Boolean)
|
|
323
|
+
.join(" ");
|
|
324
|
+
case "Polygon":
|
|
325
|
+
return ringsPath(geometry.coordinates);
|
|
326
|
+
case "MultiPolygon":
|
|
327
|
+
return geometry.coordinates
|
|
328
|
+
.map((polygon) => (Array.isArray(polygon) ? ringsPath(polygon) : ""))
|
|
329
|
+
.filter(Boolean)
|
|
330
|
+
.join(" ");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function pointRadius(point: GeoMapPoint, min: number, max: number, rMin: number, rMax: number): number {
|
|
335
|
+
if (typeof point.r === "number" && Number.isFinite(point.r) && point.r >= 0) {
|
|
336
|
+
return Math.min(point.r, MAX_POINT_RADIUS);
|
|
337
|
+
}
|
|
338
|
+
return scaleNumber(point.value ?? 1, min, max, rMin, rMax);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function hexagonPoints(cx: number, cy: number, radius: number): string {
|
|
342
|
+
return Array.from({ length: 6 }, (_, index) => {
|
|
343
|
+
const angle = (Math.PI / 3) * index - Math.PI / 6;
|
|
344
|
+
return `${cx + Math.cos(angle) * radius},${cy + Math.sin(angle) * radius}`;
|
|
345
|
+
}).join(" ");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function flowPath(source: GeoMapCoordinate, target: GeoMapCoordinate): string {
|
|
349
|
+
const a = project(source);
|
|
350
|
+
const b = project(target);
|
|
351
|
+
// Arc quadratique : point de contrôle au milieu, décalé perpendiculairement.
|
|
352
|
+
const mx = (a.x + b.x) / 2;
|
|
353
|
+
const my = (a.y + b.y) / 2;
|
|
354
|
+
const dx = b.x - a.x;
|
|
355
|
+
const dy = b.y - a.y;
|
|
356
|
+
const cx = mx - dy * 0.18;
|
|
357
|
+
const cy = my + dx * 0.18;
|
|
358
|
+
return `M ${a.x} ${a.y} Q ${cx} ${cy} ${b.x} ${b.y}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
type HexbinBin = { id: string; q: number; r: number; center: GeoMapCoordinate; count: number; value: number };
|
|
362
|
+
|
|
363
|
+
function binPoints(points: GeoMapPoint[], cellSize: number): HexbinBin[] {
|
|
364
|
+
const hexHeight = cellSize * (Math.sqrt(3) / 2);
|
|
365
|
+
const bins = new Map<string, HexbinBin>();
|
|
366
|
+
for (const point of points) {
|
|
367
|
+
const q = Math.trunc(point.longitude / cellSize);
|
|
368
|
+
const r = Math.trunc(point.latitude / hexHeight);
|
|
369
|
+
const id = `${q}:${r}`;
|
|
370
|
+
const bin = bins.get(id);
|
|
371
|
+
const value = Number.isFinite(point.value) ? (point.value as number) : 1;
|
|
372
|
+
if (bin) {
|
|
373
|
+
bin.count += 1;
|
|
374
|
+
bin.value += value;
|
|
375
|
+
} else {
|
|
376
|
+
bins.set(id, { id, q, r, center: { latitude: r * hexHeight, longitude: q * cellSize }, count: 1, value });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return [...bins.values()];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
type Cluster = GeoMapCoordinate & { id: string; count: number; value: number };
|
|
383
|
+
|
|
384
|
+
function clusterPoints(points: GeoMapPoint[], radius: number): Cluster[] {
|
|
385
|
+
const clusters: Cluster[] = [];
|
|
386
|
+
for (const point of points) {
|
|
387
|
+
const cluster = clusters.find((item) => {
|
|
388
|
+
const dLat = item.latitude - point.latitude;
|
|
389
|
+
const dLon = item.longitude - point.longitude;
|
|
390
|
+
return Math.sqrt(dLat * dLat + dLon * dLon) <= radius;
|
|
391
|
+
});
|
|
392
|
+
const value = Number.isFinite(point.value) ? (point.value as number) : 1;
|
|
393
|
+
if (!cluster) {
|
|
394
|
+
clusters.push({
|
|
395
|
+
id: `cluster:${clusters.length}`,
|
|
396
|
+
latitude: point.latitude,
|
|
397
|
+
longitude: point.longitude,
|
|
398
|
+
count: 1,
|
|
399
|
+
value
|
|
400
|
+
});
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
cluster.latitude = (cluster.latitude * cluster.count + point.latitude) / (cluster.count + 1);
|
|
404
|
+
cluster.longitude = (cluster.longitude * cluster.count + point.longitude) / (cluster.count + 1);
|
|
405
|
+
cluster.count += 1;
|
|
406
|
+
cluster.value += value;
|
|
407
|
+
}
|
|
408
|
+
return clusters;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function positiveOr(value: number | undefined, fallback: number): number {
|
|
412
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
type FeatureMark = { key: string; d: string; tone: GeoMapTone; line: boolean; text: string };
|
|
416
|
+
type RegionMark = { key: string; d: string; mix: number | null; text: string };
|
|
417
|
+
type CircleMark = { key: string; cx: number; cy: number; r: number; tone: GeoMapTone; mix?: number; text: string };
|
|
418
|
+
type FlowMark = { key: string; d: string; strokeWidth: number; text: string };
|
|
419
|
+
type HexMark = { key: string; points: string; mix: number; text: string; tone: GeoMapTone };
|
|
420
|
+
|
|
421
|
+
type LayerMarks =
|
|
422
|
+
| { type: "geojson"; key: string; summary: string; features: FeatureMark[] }
|
|
423
|
+
| { type: "choropleth"; key: string; summary: string; tone: GeoMapTone; regions: RegionMark[] }
|
|
424
|
+
| { type: "points"; key: string; summary: string; circles: CircleMark[] }
|
|
425
|
+
| { type: "density"; key: string; summary: string; tone: GeoMapTone; circles: CircleMark[] }
|
|
426
|
+
| { type: "flow"; key: string; summary: string; tone: GeoMapTone; flows: FlowMark[] }
|
|
427
|
+
| { type: "hexbin"; key: string; summary: string; bins: HexMark[] }
|
|
428
|
+
| { type: "cluster"; key: string; summary: string; clusters: CircleMark[] };
|
|
429
|
+
|
|
430
|
+
const layerMarks = $derived.by((): LayerMarks[] => {
|
|
431
|
+
return (layers ?? []).map((layer, layerIndex): LayerMarks => {
|
|
432
|
+
const key = `layer-${layerIndex}`;
|
|
433
|
+
switch (layer.type) {
|
|
434
|
+
case "geojson": {
|
|
435
|
+
const features: FeatureMark[] = [];
|
|
436
|
+
(layer.features ?? []).forEach((feature, index) => {
|
|
437
|
+
if (!isGeometry(feature.geometry)) return;
|
|
438
|
+
const d = geometryPath(feature.geometry);
|
|
439
|
+
if (d === "") return;
|
|
440
|
+
features.push({
|
|
441
|
+
key: `${feature.id}-${index}`,
|
|
442
|
+
d,
|
|
443
|
+
tone: layer.tone ?? TONES[index % TONES.length],
|
|
444
|
+
line: feature.geometry.type === "LineString" || feature.geometry.type === "MultiLineString",
|
|
445
|
+
text: `${feature.label ?? feature.id}${feature.value === undefined ? "" : `: ${feature.value}`}`
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
return { type: "geojson", key, summary: `${layer.label ?? "GeoJSON"}: ${features.length} entités`, features };
|
|
449
|
+
}
|
|
450
|
+
case "choropleth": {
|
|
451
|
+
const values = layer.values ?? {};
|
|
452
|
+
const finiteValues = (layer.features ?? [])
|
|
453
|
+
.map((feature) => values[feature.id])
|
|
454
|
+
.filter((v): v is number => Number.isFinite(v));
|
|
455
|
+
const max = Math.max(1, ...finiteValues);
|
|
456
|
+
const regions: RegionMark[] = [];
|
|
457
|
+
(layer.features ?? []).forEach((feature, index) => {
|
|
458
|
+
if (!isGeometry(feature.geometry)) return;
|
|
459
|
+
const d = geometryPath(feature.geometry);
|
|
460
|
+
if (d === "") return;
|
|
461
|
+
const value = values[feature.id];
|
|
462
|
+
const valid = Number.isFinite(value);
|
|
463
|
+
regions.push({
|
|
464
|
+
key: `${feature.id}-${index}`,
|
|
465
|
+
d,
|
|
466
|
+
mix: valid ? mixPercent(scaleNumber(value, 0, max, 22, 90)) : null,
|
|
467
|
+
text: valid ? `${feature.label ?? feature.id}: ${value}` : `${feature.label ?? feature.id}`
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
type: "choropleth",
|
|
472
|
+
key,
|
|
473
|
+
summary: `${layer.label ?? "Choroplèthe"}: ${regions.length} régions`,
|
|
474
|
+
tone: layer.tone ?? "category1",
|
|
475
|
+
regions
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
case "points": {
|
|
479
|
+
const valid = (layer.points ?? []).filter(isFiniteCoordinate);
|
|
480
|
+
const values = valid.map((p) => (Number.isFinite(p.value) ? (p.value as number) : 1));
|
|
481
|
+
const min = Math.min(0, ...values);
|
|
482
|
+
const max = Math.max(1, ...values);
|
|
483
|
+
const rMin = positiveOr(layer.minRadius, 5);
|
|
484
|
+
const rMax = positiveOr(layer.maxRadius, 14);
|
|
485
|
+
const circles = valid.map((point, index): CircleMark => {
|
|
486
|
+
const p = project(point);
|
|
487
|
+
return {
|
|
488
|
+
key: `${point.id ?? index}`,
|
|
489
|
+
cx: p.x,
|
|
490
|
+
cy: p.y,
|
|
491
|
+
r: pointRadius(point, min, max, rMin, rMax),
|
|
492
|
+
tone: point.tone ?? layer.tone ?? TONES[index % TONES.length],
|
|
493
|
+
text: `${point.label ?? point.id ?? `(${point.latitude}, ${point.longitude})`}${point.value === undefined ? "" : `: ${point.value}`}`
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
return { type: "points", key, summary: `${layer.label ?? "Points"}: ${circles.length}`, circles };
|
|
497
|
+
}
|
|
498
|
+
case "density": {
|
|
499
|
+
const valid = (layer.points ?? []).filter(isFiniteCoordinate);
|
|
500
|
+
const weights = valid.map((p) => (Number.isFinite(p.value) ? (p.value as number) : 1));
|
|
501
|
+
const max = Math.max(1, ...weights);
|
|
502
|
+
const rMax = positiveOr(layer.maxRadius, 17);
|
|
503
|
+
const circles = valid.map((point, index): CircleMark => {
|
|
504
|
+
const p = project(point);
|
|
505
|
+
const weight = Number.isFinite(point.value) ? (point.value as number) : 1;
|
|
506
|
+
return {
|
|
507
|
+
key: `${point.id ?? index}`,
|
|
508
|
+
cx: p.x,
|
|
509
|
+
cy: p.y,
|
|
510
|
+
r: scaleNumber(weight, 0, max, 8, rMax),
|
|
511
|
+
tone: layer.tone ?? "category3",
|
|
512
|
+
mix: mixPercent(scaleNumber(weight, 0, max, 25, 60)),
|
|
513
|
+
text: `(${point.latitude}, ${point.longitude}): ${weight}`
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
type: "density",
|
|
518
|
+
key,
|
|
519
|
+
summary: `${layer.label ?? "Densité"}: ${circles.length} points`,
|
|
520
|
+
tone: layer.tone ?? "category3",
|
|
521
|
+
circles
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case "flow": {
|
|
525
|
+
const valid = (layer.flows ?? []).filter((f) => isFiniteCoordinate(f.source) && isFiniteCoordinate(f.target));
|
|
526
|
+
const values = valid.map((f) => (Number.isFinite(f.value) ? (f.value as number) : 1));
|
|
527
|
+
const max = Math.max(1, ...values);
|
|
528
|
+
const flows = valid.map((flow, index): FlowMark => {
|
|
529
|
+
const value = Number.isFinite(flow.value) ? (flow.value as number) : 1;
|
|
530
|
+
return {
|
|
531
|
+
key: `${flow.id ?? index}`,
|
|
532
|
+
d: flowPath(flow.source, flow.target),
|
|
533
|
+
strokeWidth: scaleNumber(value, 0, max, 2, 9),
|
|
534
|
+
text: `${flow.label ?? `(${flow.source.latitude}, ${flow.source.longitude}) → (${flow.target.latitude}, ${flow.target.longitude})`}: ${value}`
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
return {
|
|
538
|
+
type: "flow",
|
|
539
|
+
key,
|
|
540
|
+
summary: `${layer.label ?? "Flux"}: ${flows.length}`,
|
|
541
|
+
tone: layer.tone ?? "category1",
|
|
542
|
+
flows
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
case "hexbin": {
|
|
546
|
+
const valid = (layer.points ?? []).filter(isFiniteCoordinate);
|
|
547
|
+
const cellSize = positiveOr(layer.cellSize, 1);
|
|
548
|
+
const bins = binPoints(valid, cellSize);
|
|
549
|
+
const max = Math.max(1, ...bins.map((bin) => bin.value));
|
|
550
|
+
const marks = bins.map((bin, index): HexMark => {
|
|
551
|
+
const p = project(bin.center);
|
|
552
|
+
return {
|
|
553
|
+
key: bin.id,
|
|
554
|
+
points: hexagonPoints(p.x, p.y, scaleNumber(bin.value, 0, max, 10, 22)),
|
|
555
|
+
mix: mixPercent(scaleNumber(bin.value, 0, max, 25, 85)),
|
|
556
|
+
tone: layer.tone ?? TONES[index % TONES.length],
|
|
557
|
+
text: `${bin.id}: ${bin.value}`
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
return { type: "hexbin", key, summary: `${layer.label ?? "Hexbin"}: ${marks.length} alvéoles`, bins: marks };
|
|
561
|
+
}
|
|
562
|
+
case "cluster": {
|
|
563
|
+
const valid = (layer.points ?? []).filter(isFiniteCoordinate);
|
|
564
|
+
const radius = positiveOr(layer.radius, 1);
|
|
565
|
+
const clusters = clusterPoints(valid, radius);
|
|
566
|
+
const max = Math.max(1, ...clusters.map((cluster) => cluster.count));
|
|
567
|
+
const marks = clusters.map((cluster, index): CircleMark => {
|
|
568
|
+
const p = project(cluster);
|
|
569
|
+
return {
|
|
570
|
+
key: cluster.id,
|
|
571
|
+
cx: p.x,
|
|
572
|
+
cy: p.y,
|
|
573
|
+
r: scaleNumber(cluster.count, 0, max, 8, 24),
|
|
574
|
+
tone: layer.tone ?? TONES[index % TONES.length],
|
|
575
|
+
text: `${cluster.id}: ${cluster.count}`
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
return { type: "cluster", key, summary: `${layer.label ?? "Clusters"}: ${marks.length}`, clusters: marks };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const dataValueItems = $derived(
|
|
585
|
+
layerMarks.flatMap((marks) => {
|
|
586
|
+
switch (marks.type) {
|
|
587
|
+
case "geojson":
|
|
588
|
+
return [marks.summary, ...marks.features.map((f) => f.text)];
|
|
589
|
+
case "choropleth":
|
|
590
|
+
return [marks.summary, ...marks.regions.map((r) => r.text)];
|
|
591
|
+
case "points":
|
|
592
|
+
case "density":
|
|
593
|
+
return [marks.summary, ...marks.circles.map((c) => c.text)];
|
|
594
|
+
case "flow":
|
|
595
|
+
return [marks.summary, ...marks.flows.map((f) => f.text)];
|
|
596
|
+
case "hexbin":
|
|
597
|
+
return [marks.summary, ...marks.bins.map((b) => b.text)];
|
|
598
|
+
case "cluster":
|
|
599
|
+
return [marks.summary, ...marks.clusters.map((c) => c.text)];
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const classes = () => ["st-geoMap", className].filter(Boolean).join(" ");
|
|
605
|
+
</script>
|
|
606
|
+
|
|
607
|
+
<div class={classes()}>
|
|
608
|
+
<div class="st-geoMap__visual" role="img" aria-label={label}>
|
|
609
|
+
<svg viewBox="0 0 {width} {height}" preserveAspectRatio="xMidYMid meet" width="100%" height="100%" focusable="false" aria-hidden="true">
|
|
610
|
+
<rect class="st-geoMap__frame" x="0.5" y="0.5" width={width - 1} height={height - 1} rx="4" />
|
|
611
|
+
{#each layerMarks as marks (marks.key)}
|
|
612
|
+
<g class="st-geoMap__layer st-geoMap__layer--{marks.type}">
|
|
613
|
+
{#if marks.type === "geojson"}
|
|
614
|
+
{#each marks.features as feature (feature.key)}
|
|
615
|
+
<path
|
|
616
|
+
class="st-geoMap__feature st-geoMap__feature--{feature.tone}{feature.line ? ' st-geoMap__feature--line' : ''}"
|
|
617
|
+
d={feature.d}
|
|
618
|
+
fill-rule="evenodd"
|
|
619
|
+
/>
|
|
620
|
+
{/each}
|
|
621
|
+
{:else if marks.type === "choropleth"}
|
|
622
|
+
{#each marks.regions as region (region.key)}
|
|
623
|
+
{#if region.mix === null}
|
|
624
|
+
<path class="st-geoMap__region st-geoMap__region--empty" d={region.d} fill-rule="evenodd" />
|
|
625
|
+
{:else}
|
|
626
|
+
<path
|
|
627
|
+
class="st-geoMap__region st-geoMap__region--{marks.tone}"
|
|
628
|
+
d={region.d}
|
|
629
|
+
fill-rule="evenodd"
|
|
630
|
+
style="--st-geoMap-mix: {region.mix}%"
|
|
631
|
+
/>
|
|
632
|
+
{/if}
|
|
633
|
+
{/each}
|
|
634
|
+
{:else if marks.type === "points"}
|
|
635
|
+
{#each marks.circles as circle (circle.key)}
|
|
636
|
+
<circle class="st-geoMap__point st-geoMap__point--{circle.tone}" cx={circle.cx} cy={circle.cy} r={circle.r} />
|
|
637
|
+
{/each}
|
|
638
|
+
{:else if marks.type === "density"}
|
|
639
|
+
{#each marks.circles as circle (circle.key)}
|
|
640
|
+
<circle
|
|
641
|
+
class="st-geoMap__density st-geoMap__density--{circle.tone}"
|
|
642
|
+
cx={circle.cx}
|
|
643
|
+
cy={circle.cy}
|
|
644
|
+
r={circle.r}
|
|
645
|
+
style="--st-geoMap-mix: {circle.mix}%"
|
|
646
|
+
/>
|
|
647
|
+
{/each}
|
|
648
|
+
{:else if marks.type === "flow"}
|
|
649
|
+
{#each marks.flows as flow (flow.key)}
|
|
650
|
+
<path class="st-geoMap__flow st-geoMap__flow--{marks.tone}" d={flow.d} stroke-width={flow.strokeWidth} />
|
|
651
|
+
{/each}
|
|
652
|
+
{:else if marks.type === "hexbin"}
|
|
653
|
+
{#each marks.bins as bin (bin.key)}
|
|
654
|
+
<polygon
|
|
655
|
+
class="st-geoMap__hexbin st-geoMap__hexbin--{bin.tone}"
|
|
656
|
+
points={bin.points}
|
|
657
|
+
style="--st-geoMap-mix: {bin.mix}%"
|
|
658
|
+
/>
|
|
659
|
+
{/each}
|
|
660
|
+
{:else if marks.type === "cluster"}
|
|
661
|
+
{#each marks.clusters as cluster (cluster.key)}
|
|
662
|
+
<g class="st-geoMap__cluster st-geoMap__cluster--{cluster.tone}">
|
|
663
|
+
<circle class="st-geoMap__clusterDot" cx={cluster.cx} cy={cluster.cy} r={cluster.r} />
|
|
664
|
+
<circle class="st-geoMap__clusterRing" cx={cluster.cx} cy={cluster.cy} r={cluster.r + 3} />
|
|
665
|
+
</g>
|
|
666
|
+
{/each}
|
|
667
|
+
{/if}
|
|
668
|
+
</g>
|
|
669
|
+
{/each}
|
|
670
|
+
</svg>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<ChartDataList {label} items={dataValueItems} />
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<style>
|
|
677
|
+
.st-geoMap { color: var(--st-semantic-text-secondary); display: block; font-family: inherit; position: relative; width: 100%; }
|
|
678
|
+
.st-geoMap svg, .st-geoMap__visual { display: block; }
|
|
679
|
+
.st-geoMap__frame { fill: var(--st-semantic-surface-default, Canvas); stroke: var(--st-semantic-border-subtle); stroke-width: 1; }
|
|
680
|
+
/* GeoJSON : remplissage translucide (color-mix) + contour au ton. */
|
|
681
|
+
.st-geoMap__feature { stroke-width: 1.5; transition: fill-opacity 120ms ease; }
|
|
682
|
+
.st-geoMap__feature--category1 { fill: color-mix(in srgb, var(--st-semantic-data-category1) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category1); }
|
|
683
|
+
.st-geoMap__feature--category2 { fill: color-mix(in srgb, var(--st-semantic-data-category2) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category2); }
|
|
684
|
+
.st-geoMap__feature--category3 { fill: color-mix(in srgb, var(--st-semantic-data-category3) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category3); }
|
|
685
|
+
.st-geoMap__feature--category4 { fill: color-mix(in srgb, var(--st-semantic-data-category4) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category4); }
|
|
686
|
+
.st-geoMap__feature--category5 { fill: color-mix(in srgb, var(--st-semantic-data-category5) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category5); }
|
|
687
|
+
.st-geoMap__feature--category6 { fill: color-mix(in srgb, var(--st-semantic-data-category6) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category6); }
|
|
688
|
+
.st-geoMap__feature--category7 { fill: color-mix(in srgb, var(--st-semantic-data-category7) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category7); }
|
|
689
|
+
.st-geoMap__feature--category8 { fill: color-mix(in srgb, var(--st-semantic-data-category8) var(--st-geoMap-mix, 34%), transparent); stroke: var(--st-semantic-data-category8); }
|
|
690
|
+
.st-geoMap__feature--line { fill: none; }
|
|
691
|
+
/* Choroplèthe : intensité ∝ valeur via color-mix sur le ton de la couche. */
|
|
692
|
+
.st-geoMap__region { stroke: var(--st-semantic-border-subtle); stroke-width: 1; }
|
|
693
|
+
.st-geoMap__region--empty { fill: var(--st-semantic-surface-default, Canvas); }
|
|
694
|
+
.st-geoMap__region--category1 { fill: color-mix(in srgb, var(--st-semantic-data-category1) var(--st-geoMap-mix, 60%), transparent); }
|
|
695
|
+
.st-geoMap__region--category2 { fill: color-mix(in srgb, var(--st-semantic-data-category2) var(--st-geoMap-mix, 60%), transparent); }
|
|
696
|
+
.st-geoMap__region--category3 { fill: color-mix(in srgb, var(--st-semantic-data-category3) var(--st-geoMap-mix, 60%), transparent); }
|
|
697
|
+
.st-geoMap__region--category4 { fill: color-mix(in srgb, var(--st-semantic-data-category4) var(--st-geoMap-mix, 60%), transparent); }
|
|
698
|
+
.st-geoMap__region--category5 { fill: color-mix(in srgb, var(--st-semantic-data-category5) var(--st-geoMap-mix, 60%), transparent); }
|
|
699
|
+
.st-geoMap__region--category6 { fill: color-mix(in srgb, var(--st-semantic-data-category6) var(--st-geoMap-mix, 60%), transparent); }
|
|
700
|
+
.st-geoMap__region--category7 { fill: color-mix(in srgb, var(--st-semantic-data-category7) var(--st-geoMap-mix, 60%), transparent); }
|
|
701
|
+
.st-geoMap__region--category8 { fill: color-mix(in srgb, var(--st-semantic-data-category8) var(--st-geoMap-mix, 60%), transparent); }
|
|
702
|
+
/* Points/épingles : ton plein, légère translucidité (parité dataviz). */
|
|
703
|
+
.st-geoMap__point { fill-opacity: 0.82; }
|
|
704
|
+
.st-geoMap__point--category1 { fill: var(--st-semantic-data-category1); }
|
|
705
|
+
.st-geoMap__point--category2 { fill: var(--st-semantic-data-category2); }
|
|
706
|
+
.st-geoMap__point--category3 { fill: var(--st-semantic-data-category3); }
|
|
707
|
+
.st-geoMap__point--category4 { fill: var(--st-semantic-data-category4); }
|
|
708
|
+
.st-geoMap__point--category5 { fill: var(--st-semantic-data-category5); }
|
|
709
|
+
.st-geoMap__point--category6 { fill: var(--st-semantic-data-category6); }
|
|
710
|
+
.st-geoMap__point--category7 { fill: var(--st-semantic-data-category7); }
|
|
711
|
+
.st-geoMap__point--category8 { fill: var(--st-semantic-data-category8); }
|
|
712
|
+
/* Densité : cercles translucides superposés (color-mix ∝ poids). */
|
|
713
|
+
.st-geoMap__density--category1 { fill: color-mix(in srgb, var(--st-semantic-data-category1) var(--st-geoMap-mix, 45%), transparent); }
|
|
714
|
+
.st-geoMap__density--category2 { fill: color-mix(in srgb, var(--st-semantic-data-category2) var(--st-geoMap-mix, 45%), transparent); }
|
|
715
|
+
.st-geoMap__density--category3 { fill: color-mix(in srgb, var(--st-semantic-data-category3) var(--st-geoMap-mix, 45%), transparent); }
|
|
716
|
+
.st-geoMap__density--category4 { fill: color-mix(in srgb, var(--st-semantic-data-category4) var(--st-geoMap-mix, 45%), transparent); }
|
|
717
|
+
.st-geoMap__density--category5 { fill: color-mix(in srgb, var(--st-semantic-data-category5) var(--st-geoMap-mix, 45%), transparent); }
|
|
718
|
+
.st-geoMap__density--category6 { fill: color-mix(in srgb, var(--st-semantic-data-category6) var(--st-geoMap-mix, 45%), transparent); }
|
|
719
|
+
.st-geoMap__density--category7 { fill: color-mix(in srgb, var(--st-semantic-data-category7) var(--st-geoMap-mix, 45%), transparent); }
|
|
720
|
+
.st-geoMap__density--category8 { fill: color-mix(in srgb, var(--st-semantic-data-category8) var(--st-geoMap-mix, 45%), transparent); }
|
|
721
|
+
/* Flux : arcs quadratiques, épaisseur portée par l'attribut stroke-width. */
|
|
722
|
+
.st-geoMap__flow { fill: none; stroke-linecap: round; stroke-opacity: 0.62; }
|
|
723
|
+
.st-geoMap__flow--category1 { stroke: var(--st-semantic-data-category1); }
|
|
724
|
+
.st-geoMap__flow--category2 { stroke: var(--st-semantic-data-category2); }
|
|
725
|
+
.st-geoMap__flow--category3 { stroke: var(--st-semantic-data-category3); }
|
|
726
|
+
.st-geoMap__flow--category4 { stroke: var(--st-semantic-data-category4); }
|
|
727
|
+
.st-geoMap__flow--category5 { stroke: var(--st-semantic-data-category5); }
|
|
728
|
+
.st-geoMap__flow--category6 { stroke: var(--st-semantic-data-category6); }
|
|
729
|
+
.st-geoMap__flow--category7 { stroke: var(--st-semantic-data-category7); }
|
|
730
|
+
.st-geoMap__flow--category8 { stroke: var(--st-semantic-data-category8); }
|
|
731
|
+
/* Hexbin : intensité ∝ valeur agrégée via color-mix. */
|
|
732
|
+
.st-geoMap__hexbin--category1 { fill: color-mix(in srgb, var(--st-semantic-data-category1) var(--st-geoMap-mix, 72%), transparent); }
|
|
733
|
+
.st-geoMap__hexbin--category2 { fill: color-mix(in srgb, var(--st-semantic-data-category2) var(--st-geoMap-mix, 72%), transparent); }
|
|
734
|
+
.st-geoMap__hexbin--category3 { fill: color-mix(in srgb, var(--st-semantic-data-category3) var(--st-geoMap-mix, 72%), transparent); }
|
|
735
|
+
.st-geoMap__hexbin--category4 { fill: color-mix(in srgb, var(--st-semantic-data-category4) var(--st-geoMap-mix, 72%), transparent); }
|
|
736
|
+
.st-geoMap__hexbin--category5 { fill: color-mix(in srgb, var(--st-semantic-data-category5) var(--st-geoMap-mix, 72%), transparent); }
|
|
737
|
+
.st-geoMap__hexbin--category6 { fill: color-mix(in srgb, var(--st-semantic-data-category6) var(--st-geoMap-mix, 72%), transparent); }
|
|
738
|
+
.st-geoMap__hexbin--category7 { fill: color-mix(in srgb, var(--st-semantic-data-category7) var(--st-geoMap-mix, 72%), transparent); }
|
|
739
|
+
.st-geoMap__hexbin--category8 { fill: color-mix(in srgb, var(--st-semantic-data-category8) var(--st-geoMap-mix, 72%), transparent); }
|
|
740
|
+
/* Clusters : centroïde distinctif (disque + anneau), toné via currentColor. */
|
|
741
|
+
.st-geoMap__cluster--category1 { color: var(--st-semantic-data-category1); }
|
|
742
|
+
.st-geoMap__cluster--category2 { color: var(--st-semantic-data-category2); }
|
|
743
|
+
.st-geoMap__cluster--category3 { color: var(--st-semantic-data-category3); }
|
|
744
|
+
.st-geoMap__cluster--category4 { color: var(--st-semantic-data-category4); }
|
|
745
|
+
.st-geoMap__cluster--category5 { color: var(--st-semantic-data-category5); }
|
|
746
|
+
.st-geoMap__cluster--category6 { color: var(--st-semantic-data-category6); }
|
|
747
|
+
.st-geoMap__cluster--category7 { color: var(--st-semantic-data-category7); }
|
|
748
|
+
.st-geoMap__cluster--category8 { color: var(--st-semantic-data-category8); }
|
|
749
|
+
.st-geoMap__clusterDot { fill: currentColor; fill-opacity: 0.78; }
|
|
750
|
+
.st-geoMap__clusterRing { fill: none; stroke: currentColor; stroke-width: 1.5; }
|
|
751
|
+
@media (prefers-reduced-motion: reduce) {
|
|
752
|
+
.st-geoMap__feature { transition: none; }
|
|
753
|
+
}
|
|
754
|
+
</style>
|