feat: vue dividende universel — overlay membres actifs géolocalisés
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user