Files
mcm-mfp/task1/fig8_delta.html
2026-01-19 19:43:57 +08:00

202 lines
7.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fig.8: 2019 vs 2021 (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;
}
.panel {
background: rgba(255, 255, 255, 0.92);
padding: 10px 12px;
border-radius: 10px;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15);
font: 12px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
.panel .title {
font-weight: 800;
margin-bottom: 6px;
}
.panel .row {
display: flex;
gap: 10px;
align-items: center;
margin-top: 6px;
}
.panel label {
cursor: pointer;
user-select: none;
}
.legend-bar {
height: 10px;
border-radius: 6px;
background: linear-gradient(90deg, #2563eb, #f3f4f6, #dc2626);
margin-top: 6px;
}
.muted {
color: rgba(0, 0, 0, 0.65);
}
</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 || [];
if (!points.length) {
alert("FIG1_POINTS 为空:请确认 fig1_points.js 与本文件在同一目录。");
}
const map = L.map("map", { preferCanvas: true });
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
subdomains: "abcd",
maxZoom: 20,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
}).addTo(map);
const total2019 = points.reduce((s, p) => s + (p.visits_2019 ?? 0), 0);
const total2021 = points.reduce((s, p) => s + (p.k ?? 0), 0);
const scale2019 = total2019 > 0 ? total2021 / total2019 : 1;
function clamp01(x) {
return Math.max(0, Math.min(1, x));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function radiusLinear(val, vmin, vmax, rmin, rmax) {
const t = clamp01((val - vmin) / (vmax - vmin || 1));
return lerp(rmin, rmax, t);
}
function divergingColor(t) {
// t in [-1,1], blue -> light -> red
const x = clamp01((t + 1) / 2);
const c1 = [37, 99, 235]; // blue
const c2 = [243, 244, 246]; // light gray
const c3 = [220, 38, 38]; // red
const from = x < 0.5 ? c1 : c2;
const to = x < 0.5 ? c2 : c3;
const tt = x < 0.5 ? x / 0.5 : (x - 0.5) / 0.5;
const r = Math.round(lerp(from[0], to[0], tt));
const g = Math.round(lerp(from[1], to[1], tt));
const b = Math.round(lerp(from[2], to[2], tt));
return `rgb(${r},${g},${b})`;
}
const v2019 = points.map((p) => p.visits_2019);
const v2021 = points.map((p) => p.k);
const vDelta = points.map((p) => (p.k ?? 0) - (p.visits_2019 ?? 0) * scale2019);
const v2019Min = Math.min(...v2019);
const v2019Max = Math.max(...v2019);
const v2021Min = Math.min(...v2021);
const v2021Max = Math.max(...v2021);
const deltaAbsMax = Math.max(...vDelta.map((d) => Math.abs(d))) || 1;
const layer = L.featureGroup();
const markers = [];
for (const p of points) {
const marker = L.circleMarker([p.lat, p.lng], {
radius: 6,
color: "white",
weight: 1,
fillColor: "#3b82f6",
fillOpacity: 0.85,
});
marker.bindPopup(() => {
const scaled = (p.visits_2019 ?? 0) * scale2019;
const delta = (p.k ?? 0) - scaled;
const sign = delta >= 0 ? "+" : "";
return (
`<div style="font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">` +
`<div style="font-weight:800;margin-bottom:6px;">${p.site_id}. ${p.site_name}</div>` +
`<div><b>2019 visits</b>: ${p.visits_2019}</div>` +
`<div><b>2021 plan (k)</b>: ${p.k}</div>` +
`<div class="muted"><b>2019 scaled → 2021 total</b>: ${scaled.toFixed(2)}</div>` +
`<div><b>Δ</b> = k - scaled2019: ${sign}${delta.toFixed(2)}</div>` +
`</div>`
);
});
layer.addLayer(marker);
markers.push({ p, marker });
}
layer.addTo(map);
map.fitBounds(layer.getBounds().pad(0.12));
function applyMode(mode) {
for (const { p, marker } of markers) {
if (mode === "2019") {
const r = radiusLinear(p.visits_2019, v2019Min, v2019Max, 4, 18);
marker.setStyle({ radius: r, fillColor: "#3b82f6" });
} else if (mode === "2021") {
const r = radiusLinear(p.k, v2021Min, v2021Max, 4, 18);
marker.setStyle({ radius: r, fillColor: "#10b981" });
} else {
const scaled = (p.visits_2019 ?? 0) * scale2019;
const delta = (p.k ?? 0) - scaled;
const t = Math.max(-1, Math.min(1, delta / deltaAbsMax));
const r = radiusLinear(Math.abs(delta), 0, deltaAbsMax, 4, 18);
marker.setStyle({ radius: r, fillColor: divergingColor(t) });
}
}
const label = document.getElementById("modeLabel");
const meta = document.getElementById("meta");
if (label && meta) {
if (mode === "2019") {
label.textContent = "2019 visits (Point size)";
meta.textContent = `Total: ${total2019}`;
} else if (mode === "2021") {
label.textContent = "2021 Plan k (Point size)";
meta.textContent = `总计: ${total2021}`;
} else {
label.textContent = "Δ = 2021(k) - 2019_scaled(Point size=|Δ|Color=Δ)";
meta.textContent = `Fixed: ${scale2019.toFixed(4)}`;
}
}
}
const control = L.control({ position: "topright" });
control.onAdd = function () {
const div = L.DomUtil.create("div", "panel");
div.innerHTML =
`<div class="title">Fig.8: 2019 vs 2021</div>` +
`<div id="modeLabel" style="font-weight:700"></div>` +
`<div class="row">` +
`<label><input type="radio" name="mode" value="2019" checked /> 2019</label>` +
`<label><input type="radio" name="mode" value="2021" /> 2021</label>` +
`<label><input type="radio" name="mode" value="delta" /> Δ</label>` +
`</div>` +
`<div class="legend-bar"></div>` +
`<div id="meta" class="muted" style="margin-top:6px;"></div>` +
`<div class="muted" style="margin-top:6px;">Click to view details</div>`;
L.DomEvent.disableClickPropagation(div);
return div;
};
control.addTo(map);
document.addEventListener("change", (e) => {
const t = e.target;
if (t && t.name === "mode") applyMode(t.value);
});
applyMode("2019");
</script>
</body>
</html>