feat: vue dividende universel — overlay membres actifs géolocalisés
ci/woodpecker/push/woodpecker Pipeline was successful

Bouton DU (gauche carte) : affiche en overlay des cercles verts
proportionnels au nombre de membres WoT actifs géolocalisés par ville.
Chargement à la demande, mis en cache 1h.

Pipeline :
  SubsquidAdapter.fetchActiveMemberKeys() → isMember:true (~7000)
  CesiumAdapter.resolveGeoByKeysBatched() → lots de 500 clés
  DataService.fetchMemberCities()         → agrégation + cache 1h
  HeatMap → CircleMarkers Leaflet en overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-28 12:57:19 +01:00
parent 0136ff9ce1
commit 7c9d626b98
5 changed files with 168 additions and 6 deletions
+30 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet.heat';
import type { Transaction } from '../data/mockData';
import type { MemberCity } from '../services/DataService';
// Leaflet default marker fix (Vite asset pipeline)
import iconUrl from 'leaflet/dist/images/marker-icon.png';
@@ -10,6 +11,7 @@ L.Icon.Default.mergeOptions({ iconUrl, shadowUrl: iconShadowUrl });
interface HeatMapProps {
transactions: Transaction[];
memberCities?: MemberCity[];
}
const HEAT_OPTIONS: L.HeatMapOptions = {
@@ -29,10 +31,11 @@ const HEAT_OPTIONS: L.HeatMapOptions = {
},
};
export function HeatMap({ transactions }: HeatMapProps) {
export function HeatMap({ transactions, memberCities = [] }: HeatMapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<L.Map | null>(null);
const heatRef = useRef<L.HeatLayer | null>(null);
const memberLayerRef = useRef<L.LayerGroup | null>(null);
// Two img overlays that cross-fade between each other.
// The canvas opacity is NEVER touched — it stays at leaflet's default.
const prevRef = useRef<HTMLImageElement | null>(null);
@@ -59,9 +62,11 @@ export function HeatMap({ transactions }: HeatMapProps) {
L.control.zoom({ position: 'bottomright' }).addTo(map);
const heat = L.heatLayer([], HEAT_OPTIONS).addTo(map);
const memberLayer = L.layerGroup().addTo(map);
mapRef.current = map;
heatRef.current = heat;
memberLayerRef.current = memberLayer;
// Pendant zoom/pan : cache les overlays → le canvas live est visible directement.
// Après zoom/pan : resynchronise le snapshot sur le canvas redesssiné.
@@ -100,9 +105,33 @@ export function HeatMap({ transactions }: HeatMapProps) {
map.remove();
mapRef.current = null;
heatRef.current = null;
memberLayerRef.current = null;
};
}, []);
// Overlay membres DU : cercles proportionnels au nombre de membres par ville
useEffect(() => {
const layer = memberLayerRef.current;
if (!layer) return;
layer.clearLayers();
if (memberCities.length === 0) return;
const maxCount = Math.max(...memberCities.map((c) => c.count), 1);
for (const city of memberCities) {
const radius = 4 + Math.sqrt(city.count / maxCount) * 18;
L.circleMarker([city.lat, city.lng], {
radius,
color: '#00c853',
fillColor: '#00c853',
fillOpacity: 0.18,
weight: 1.5,
opacity: 0.7,
})
.bindTooltip(`<b>${city.city}</b><br/>${city.count} membre${city.count > 1 ? 's' : ''}`, { sticky: true })
.addTo(layer);
}
}, [memberCities]);
// Crossfade: two img overlays swap roles each frame.
// Canvas is never hidden — we only read its pixel data via toDataURL().
useEffect(() => {