Public
Edited
May 28
Insert cell
Insert cell
finlandMap = FileAttachment("SuomenHyvinvointialuejako_2023_10k.geojson").json()
Insert cell
ph_municipalities = FileAttachment("PH_municipalities.geojson").json()
Insert cell
data_ttm = FileAttachment("ttm_coord_PH_final.csv").csv()
Insert cell
data_hs = FileAttachment("hs_PH.csv").csv()
Insert cell
colorScale = d3.scaleQuantize()
.domain([5, 15]) // assuming unemployment rate ranges from 5% to 15%
.range([
"#e8e1fe", // lightest
"#cbbbfd",
"#af94fc",
"#926efb",
"#7549f9" // darkest
])
Insert cell
filteredMapFinland = {
return{
type: "FeatureCollection",
features: finlandMap.features.filter(feature => {
console.log("Feature NAMEFIN:", feature.properties.NAMEFIN);
return feature.properties.NAMEFIN === "Päijät-Hämeen hyvinvointialue";
})
};
}
Insert cell
//countywgs84 = convertToWGS84(filteredMapFinland)
Insert cell
municipalitywgs84 = FileAttachment("municipality_wgs84.json").json()
Insert cell
countywgs84 = FileAttachment("county_wgs84.json").json()
Insert cell
viewof map = {
/* ─── dimensions ─── */
const width = 640;
const height = 800;

/* ─── 1. MAIN MAP SVG ─────────────────────────────────────── */
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("font-family", "Poppins, sans-serif"); // ← NEW

/* ── projection & base map ─────────────────────────────────── */
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");

/* ─── purple scale (0–60 min) ──────────────────────────────── */
const colorScale = d3.scaleSequential(
d3.interpolateRgbBasis([
"#e8e1fe","#cbbbfd","#af94fc","#926efb","#7549f9","#5a28f6"
])
).domain([0, 60]);

/* ─── state & style constants ──────────────────────────────── */
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;
}

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more