fix colors
This commit is contained in:
201
task1/fig8_delta.html
Normal file
201
task1/fig8_delta.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!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:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors © <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>
|
||||
|
||||
Reference in New Issue
Block a user