viewof map = {
const width = 640;
const height = 800;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("font-family", "Poppins, sans-serif");
const projection = d3.geoMercator().fitSize([width, height], countywgs84);
const path = d3.geoPath().projection(projection);
svg.append("g")
.selectAll("path")
.data(countywgs84.features)
.join("path")
.attr("d", path)
.attr("fill", "#f5f5f5")
.attr("stroke","#333");
const colorScale = d3.scaleSequential(
d3.interpolateRgbBasis([
"#e8e1fe","#cbbbfd","#af94fc","#926efb","#7549f9","#5a28f6"
])
).domain([0, 60]);
const selectedNros = new Set();
const CIRCLE_BASE = "#29254d";
const CIRCLE_SEL = "#29254d";
const SQUARE_SEL = "#3C308C";
const SQUARE_OPACITY = 0.8;
const HOVER_STROKE = "#FFDC52";
/* ─── travel-time squares ─────────────────────────────────── */
const rects = svg.append("g")
.selectAll("rect")
.data(data_ttm)
.join("rect")
.attr("x", d => projection([+d.x, +d.y])[0] - 5)
.attr("y", d => projection([+d.x, +d.y])[1] - 5)
.attr("width",10).attr("height",10)
.attr("fill", d => colorScale(+d.travel_time_p50))
.attr("fill-opacity",1)
.attr("stroke-width",0)
.on("mouseover", function(event,d){
const clr = selectedNros.has(d.to_id)?HOVER_STROKE:"#29254d";
d3.select(this).raise().attr("stroke",clr).attr("stroke-width",2);
updateCircleStyles(d.to_id);
})
.on("mouseout", function(){
d3.select(this).attr("stroke",null).attr("stroke-width",0);
updateCircleStyles(null);
});
rects.append("title").text(d=>`${d.travel_time_p50} min`);
/* ─── health-station circles ──────────────────────────────── */
const circles = svg.append("g")
.selectAll("circle")
.data(data_hs)
.join("circle")
.attr("cx", d => projection([d.x, d.y])[0])
.attr("cy", d => projection([d.x, d.y])[1])
.attr("r",6)
.attr("fill",CIRCLE_BASE)
.style("cursor","pointer")
.on("click",function(event,d){
const id=d.nro;
selectedNros.has(id)?selectedNros.delete(id):selectedNros.add(id);
updateCircleStyles(); updateRectColors(); d3.select(this).raise();
});
function updateCircleStyles(hoverId=null){
circles.each(function(c){
const sel=selectedNros.has(c.nro);
const hov=hoverId&&c.nro===hoverId;
d3.select(this)
.attr("fill", sel?CIRCLE_SEL:CIRCLE_BASE)
.attr("stroke", hov?HOVER_STROKE:sel?"#E8614F":null)
.attr("stroke-width", hov?3:sel?2:0);
if(hov) d3.select(this).raise();
});
}
function updateRectColors(){
rects.each(function(d){
const sel=selectedNros.has(d.to_id);
d3.select(this)
.attr("fill", sel?SQUARE_SEL:colorScale(+d.travel_time_p50))
.attr("fill-opacity", sel?SQUARE_OPACITY:1);
});
}
/* ─── municipality borders ───────────────────────────────── */
svg.append("g")
.selectAll("path")
.data(municipalitywgs84.features)
.join("path")
.attr("d", path)
.attr("fill","none")
.attr("stroke","#989d9e");
/* ─── 2. GRADIENT LEGEND ────────────────────────────────── */
const legendBarW=300, legendH=12, labelOffset=90, legendSvgW=labelOffset+legendBarW;
const legendSvg = d3.create("svg")
.attr("width",legendSvgW)
.attr("height",40)
.attr("font-family","Poppins, sans-serif"); // ← NEW
legendSvg.append("text")
.attr("x",0).attr("y",legendH-1)
.attr("font-size",13).attr("font-weight","600")
.attr("fill","#333")
.text("Matka-aika");
const gradId="legend-gradient";
const grad=legendSvg.append("defs")
.append("linearGradient")
.attr("id",gradId)
.attr("x1","0%").attr("x2","100%")
.attr("y1","0%").attr("y2","0%");
d3.range(0,1.01,0.1).forEach(t=>{
grad.append("stop")
.attr("offset",`${t*100}%`)
.attr("stop-color",colorScale(t*60));
});
legendSvg.append("rect")
.attr("x",labelOffset).attr("width",legendBarW).attr("height",legendH)
.attr("fill",`url(#${gradId})`).attr("stroke","#989d9e");
legendSvg.append("text")
.attr("x",labelOffset).attr("y",legendH+12)
.attr("font-size",12).attr("fill","#333").text("0 min");
legendSvg.append("text")
.attr("x",labelOffset+legendBarW/2).attr("y",legendH+12)
.attr("font-size",12).attr("fill","#333")
.attr("text-anchor","middle")
.text("30 min");
legendSvg.append("text")
.attr("x",labelOffset+legendBarW).attr("y",legendH+12)
.attr("font-size",12).attr("fill","#333")
.attr("text-anchor","end")
.text("60 min");
/* ─── 3. COMBINE & RETURN ───────────────────────────────── */
const container = html`
<div style="display:flex;flex-direction:column;align-items:center;gap:30px;font-family:Poppins,sans-serif;">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
</div>`;
container.append(svg.node());
container.append(legendSvg.node());
container.value=null;
return container;
}