2026-01-19 19:38:38 +08:00
<!doctype html>
2026-01-20 01:55:46 +08:00
< html lang = "en" >
2026-01-19 19:38:38 +08:00
< head >
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
< title > Task 3 Fig.1 (CartoDB + Leaflet)< / title >
< link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
crossorigin=""
/>
< style >
html,
body,
#map {
height: 100%;
margin: 0;
}
2026-01-20 01:55:46 +08:00
body {
background: #ffffff;
}
#map {
background: #ffffff;
}
2026-01-19 19:38:38 +08:00
.legend {
2026-01-20 01:55:46 +08:00
background: rgba(255, 255, 255, 0.96);
2026-01-19 19:38:38 +08:00
padding: 10px 12px;
border-radius: 8px;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15);
font: 12px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
2026-01-20 01:55:46 +08:00
border: 1px solid rgba(0, 0, 0, 0.15);
2026-01-19 19:38:38 +08:00
}
.legend .title {
font-weight: 700;
margin-bottom: 6px;
}
.legend .row {
display: flex;
justify-content: space-between;
gap: 12px;
white-space: nowrap;
}
.legend .swatch {
width: 12px;
height: 12px;
border-radius: 10px;
display: inline-block;
margin-right: 6px;
border: 1px solid rgba(255, 255, 255, 0.95);
vertical-align: -1px;
}
.legend .muted {
color: rgba(0, 0, 0, 0.65);
margin-top: 6px;
}
.legend .line {
height: 2px;
background: rgba(188, 108, 37, 0.75);
border-radius: 2px;
margin-top: 6px;
}
< / style >
< / head >
< body >
< div id = "map" > < / div >
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin = "" > < / script >
< script src = "./fig1_points.js" > < / script >
< script >
const points = window.FIG1_POINTS || [];
const links = window.FIG1_LINKS || [];
if (!points.length) {
2026-01-20 01:55:46 +08:00
alert(
"Missing FIG1_POINTS: run `python task3/08_visualize.py` to generate `task3/fig1_points.js`, and ensure it is in the same directory as this HTML file."
);
2026-01-19 19:38:38 +08:00
}
const map = L.map("map", { preferCanvas: true });
// CartoDB basemap (Positron)
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
subdomains: "abcd",
maxZoom: 20,
attribution:
'© < a href = "https://www.openstreetmap.org/copyright" > OSM< / a > contributors © < a href = "https://carto.com/attributions" > CARTO< / a > ',
}).addTo(map);
const byId = new Map(points.map((p) => [p.site_id, p]));
const muValues = points.map((p) => p.mu);
const muMin = Math.min(...muValues);
const muMax = Math.max(...muValues);
const COLORS = {
// warmer + higher contrast
paired: "#e76f51",
unpaired: "#6c757d",
link: "#bc6c25",
};
function clamp01(x) {
return Math.max(0, Math.min(1, x));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function radiusForMu(mu) {
const t = clamp01((mu - muMin) / (muMax - muMin || 1));
return lerp(5, 20, t);
}
function colorForPaired(isPaired) {
return isPaired ? COLORS.paired : COLORS.unpaired;
}
const siteLayer = L.featureGroup();
for (const p of points) {
const marker = L.circleMarker([p.lat, p.lng], {
radius: radiusForMu(p.mu),
color: "rgba(255,255,255,0.96)",
weight: 1.35,
fillColor: colorForPaired(p.is_paired),
fillOpacity: 0.92,
});
2026-01-20 01:55:46 +08:00
marker.bindPopup(
2026-01-19 19:38:38 +08:00
`< div style = "font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;" > \n` +
`< div style = "font-weight:700;margin-bottom:6px;" > ${p.site_id}. ${p.site_name}< / div > ` +
2026-01-20 01:55:46 +08:00
`< div > < b > Mean (μ)< / b > : ${p.mu.toFixed(1)}< / div > ` +
`< div > < b > Std (σ )< / b > : ${p.sigma.toFixed(1)}< / div > ` +
`< div > < b > Annual visits (k)< / b > : ${p.k}< / div > ` +
`< div > < b > Paired< / b > : ${p.is_paired ? "Yes" : "No"}< / div > ` +
2026-01-19 19:38:38 +08:00
`< / div > `
);
siteLayer.addLayer(marker);
}
siteLayer.addTo(map);
const linkLayer = L.featureGroup();
for (const e of links) {
const a = byId.get(e.site_i_id);
const b = byId.get(e.site_j_id);
if (!a || !b) continue;
const line = L.polyline(
[
[a.lat, a.lng],
[b.lat, b.lng],
],
{
color: COLORS.link,
weight: 2.6,
opacity: 0.5,
}
);
line.bindPopup(
`< div style = "font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;" > \n` +
`< div style = "font-weight:700;margin-bottom:6px;" > Pair< / div > ` +
`< div > ${e.site_i_id}. ${e.site_i_name}< / div > ` +
`< div > ${e.site_j_id}. ${e.site_j_name}< / div > ` +
2026-01-20 01:55:46 +08:00
`< div > < b > Distance< / b > : ${e.distance.toFixed(2)} mi< / div > ` +
2026-01-19 19:38:38 +08:00
`< / div > `
);
linkLayer.addLayer(line);
}
linkLayer.addTo(map);
const all = L.featureGroup([siteLayer, linkLayer]);
map.fitBounds(all.getBounds().pad(0.12));
const legend = L.control({ position: "bottomleft" });
legend.onAdd = function () {
const pairedCount = points.filter((p) => p.is_paired).length;
2026-01-20 01:55:46 +08:00
const linkCount = links.length;
2026-01-19 19:38:38 +08:00
const div = L.DomUtil.create("div", "legend");
div.innerHTML =
2026-01-20 01:55:46 +08:00
`< div class = "title" > Task 3 Fig.1 Pairing Map (Interactive)< / div > ` +
`< div class = "row" > < span > < span class = "swatch" style = "background:${COLORS.paired}" > < / span > Paired sites< / span > < span > ${pairedCount}< / span > < / div > ` +
`< div class = "row" > < span > < span class = "swatch" style = "background:${COLORS.unpaired}" > < / span > Unpaired sites< / span > < span > ${points.length - pairedCount}< / span > < / div > ` +
`< div class = "muted" > Marker size: mean demand μ (linear scale)< / div > ` +
2026-01-19 19:38:38 +08:00
`< div class = "line" > < / div > ` +
2026-01-20 01:55:46 +08:00
`< div class = "muted" > Links: ${linkCount} selected pairs (click for distance)< / div > `;
2026-01-19 19:38:38 +08:00
return div;
};
legend.addTo(map);
< / script >
< / body >
< / html >