feat: raccourcis clavier, URL partageable, sparkline, recherche identité
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Raccourcis clavier : ←/→ (frames), Espace (play/pause), Échap (quitter animation/fermer info), H (basculer heatmap↔flux) - URL partageable : ?period=7&view=flow&city=Paris — état restauré au chargement et mis à jour sans rechargement (history.replaceState) - Sparkline : mini bar-chart SVG dans le StatsPanel montrant l'activité sur la période (données déjà en mémoire, aucune requête) - Recherche identité : champ flottant (⌕) acceptant un nom Ğ1 ou une clé g1…, résout via Subsquid + Cesium+, bascule en vue flux et met la ville en focus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface SparklineProps {
|
||||
timestamps: number[];
|
||||
periodDays: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini bar-chart SVG affichant l'activité journalière sur la période.
|
||||
* Utilise les timestamps déjà en mémoire — aucune requête supplémentaire.
|
||||
*/
|
||||
export function Sparkline({ timestamps, periodDays }: SparklineProps) {
|
||||
const buckets = useMemo(() => {
|
||||
if (timestamps.length === 0) return [];
|
||||
const n = periodDays === 1 ? 24 : Math.min(periodDays, 30);
|
||||
const now = Date.now();
|
||||
const start = now - periodDays * 864e5;
|
||||
const step = (periodDays * 864e5) / n;
|
||||
const counts = new Array(n).fill(0);
|
||||
for (const ts of timestamps) {
|
||||
const i = Math.floor((ts - start) / step);
|
||||
if (i >= 0 && i < n) counts[i]++;
|
||||
}
|
||||
return counts;
|
||||
}, [timestamps, periodDays]);
|
||||
|
||||
if (buckets.length === 0) return null;
|
||||
|
||||
const n = buckets.length;
|
||||
const max = Math.max(...buckets, 1);
|
||||
const W = 100;
|
||||
const H = 32;
|
||||
const barW = W / n;
|
||||
const gap = barW * 0.18;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<svg
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
preserveAspectRatio="none"
|
||||
className="w-full h-8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{buckets.map((count, i) => {
|
||||
const h = Math.max(1, (count / max) * H);
|
||||
return (
|
||||
<rect
|
||||
key={i}
|
||||
x={i * barW + gap / 2}
|
||||
y={H - h}
|
||||
width={barW - gap}
|
||||
height={h}
|
||||
fill="#d4a843"
|
||||
opacity={0.25 + 0.75 * (count / max)}
|
||||
rx={0.5}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<div className="flex justify-between text-[10px] text-[#4b5563]">
|
||||
<span>{periodDays === 1 ? '0h' : 'J-' + periodDays}</span>
|
||||
<span>{periodDays === 1 ? 'maintenant' : 'aujourd\'hui'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user