From e7228c2be8dbb5887a4553b6cf46af14393f8276 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Thu, 14 May 2026 15:48:30 +0200 Subject: [PATCH] explore: plasma palette; width + opacity sliders for heatmap --- site/src/components/Explore.svelte | 62 +++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/site/src/components/Explore.svelte b/site/src/components/Explore.svelte index 102e323..f87bb5f 100644 --- a/site/src/components/Explore.svelte +++ b/site/src/components/Explore.svelte @@ -28,6 +28,10 @@ let viewMode: 'lines' | 'heatmap' = 'heatmap'; let heatmapMode: 'global' | 'bytype' = 'bytype'; + // Heatmap appearance controls + let heatWidth = 4; + let heatOpacityPct = 10; // per-line opacity %, stacks with overlapping routes + // Tile layers — same as planner const TILES: Record = { grey: { label: 'Grey', attribution: '© CARTO | © OpenStreetMap contributors', tiles: ['https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png','https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png','https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'] }, @@ -39,12 +43,13 @@ const TILE_ORDER = ['grey', 'cyclosm', 'osm', 'topo', 'satellite']; let tileKey = 'grey'; + // 5 points sampled from matplotlib's plasma colormap (t=0.12, 0.32, 0.52, 0.72, 0.88) const TYPE_COLORS: Record = { - cycling: '#e879a0', - running: '#60a5fa', - hiking: '#4ade80', - skiing: '#93c5fd', - other: '#a78bfa', + cycling: '#f89540', // plasma t≈0.75 — orange + running: '#cc4778', // plasma t≈0.50 — crimson + hiking: '#7e03a8', // plasma t≈0.25 — purple + skiing: '#f5d126', // plasma t≈0.87 — amber-yellow + other: '#3b0f70', // plasma t≈0.12 — deep violet }; const HEAT_TYPES = Object.keys(TYPE_COLORS); @@ -137,6 +142,26 @@ function _empty() { return { type: 'FeatureCollection', features: [] }; } + function _heatPaint(width: number, opacityPct: number) { + const o = opacityPct / 100; + return { + 'line-width': ['interpolate', ['linear'], ['zoom'], 5, width*0.5, 10, width, 14, width*1.8, 18, width*2.5], + 'line-opacity': ['interpolate', ['linear'], ['zoom'], 5, o, 10, o*1.2, 14, o*1.5, 18, o*1.8], + }; + } + + function _updateHeatPaint(width: number, opacityPct: number) { + if (!map) return; + const p = _heatPaint(width, opacityPct); + for (const id of ['explore-heat-global', ...HEAT_TYPES.map(t => `explore-heat-${t}`)]) { + if (!map.getLayer(id)) continue; + map.setPaintProperty(id, 'line-width', p['line-width']); + map.setPaintProperty(id, 'line-opacity', p['line-opacity']); + } + } + + $: if (mapReady) _updateHeatPaint(heatWidth, heatOpacityPct); + function _updateMap(filtered: Track[], view: string, heatMode: string) { const linesSrc = map.getSource('explore-lines'); if (!linesSrc) return; @@ -193,19 +218,18 @@ }, }); - const heatWidth = ['interpolate', ['linear'], ['zoom'], 5, 2, 10, 4, 14, 7, 18, 10]; - const heatOpacity = ['interpolate', ['linear'], ['zoom'], 5, 0.10, 10, 0.12, 14, 0.15, 18, 0.18]; + const hp = _heatPaint(heatWidth, heatOpacityPct); // Global heatmap — all lines in warm amber, very low opacity so overlapping routes stack up map.addLayer({ id: 'explore-heat-global', type: 'line', source: 'explore-lines', layout: { visibility: 'none' }, - paint: { 'line-width': heatWidth, 'line-opacity': heatOpacity, 'line-blur': 1, 'line-color': '#f97316' }, + paint: { ...hp, 'line-blur': 1, 'line-color': '#f97316' }, }); // Per-type heatmap layers — same accumulation trick, type-specific colour for (const [type, hex] of Object.entries(TYPE_COLORS)) { map.addLayer({ id: `explore-heat-${type}`, type: 'line', source: 'explore-lines', filter: ['==', ['get', 'type'], type], layout: { visibility: 'none' }, - paint: { 'line-width': heatWidth, 'line-opacity': heatOpacity, 'line-blur': 1, 'line-color': hex }, + paint: { ...hp, 'line-blur': 1, 'line-color': hex }, }); } @@ -300,6 +324,16 @@ +
+ Width + + {heatWidth} +
+
+ Opacity + + {heatOpacityPct}% +
{/if} @@ -420,5 +454,15 @@ .muted { color: var(--text-5); } .small { font-size: 0.72rem; margin: 0; } + .slider-row { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.5rem; + } + .slider-label { font-size: 0.68rem; color: var(--text-5); width: 2.8rem; flex-shrink: 0; } + .slider { flex: 1; accent-color: var(--accent); cursor: pointer; } + .slider-val { font-size: 0.68rem; color: var(--text-4); width: 2rem; text-align: right; flex-shrink: 0; } + .map-wrap { flex: 1; height: 100%; }