explore: plasma palette; width + opacity sliders for heatmap

This commit is contained in:
Davide Scaini
2026-05-14 15:48:30 +02:00
parent 298fe3ea39
commit e7228c2be8
+53 -9
View File
@@ -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<string, { tiles: string[]; attribution: string; label: string }> = {
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<string, string> = {
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 @@
<button class="pill small" class:active={heatmapMode === 'global'} onclick={() => heatmapMode = 'global'}>Global</button>
<button class="pill small" class:active={heatmapMode === 'bytype'} onclick={() => heatmapMode = 'bytype'}>By type</button>
</div>
<div class="slider-row">
<span class="slider-label">Width</span>
<input type="range" min="2" max="12" step="1" class="slider" bind:value={heatWidth} />
<span class="slider-val">{heatWidth}</span>
</div>
<div class="slider-row">
<span class="slider-label">Opacity</span>
<input type="range" min="3" max="30" step="1" class="slider" bind:value={heatOpacityPct} />
<span class="slider-val">{heatOpacityPct}%</span>
</div>
{/if}
</section>
@@ -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%; }
</style>