fix color

This commit is contained in:
2026-01-19 19:38:38 +08:00
parent 22003ea65e
commit b98ba91fc1
20 changed files with 436 additions and 32 deletions

Binary file not shown.

View File

@@ -26,10 +26,10 @@ INPUT_FILE = '01_distance.xlsx'
OUTPUT_FILE = '02_pairing.xlsx' OUTPUT_FILE = '02_pairing.xlsx'
# 约束参数 # 约束参数
L_MAX = 50 # 距离阈值 (英里) L_MAX = 30 # 距离阈值 (英里)
MU_SUM_MAX = 450 # 需求和上限 MU_SUM_MAX = 350 # 需求和上限
CV_MAX = 0.5 # 变异系数上限 CV_MAX = 0.3 # 变异系数上限
Q = 400 # 卡车容量 Q = 350 # 卡车容量
# 价值函数权重 # 价值函数权重
ALPHA = 1.0 # 容量利用率权重 ALPHA = 1.0 # 容量利用率权重

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -16,17 +16,66 @@ Task 3 - Step 7: 敏感性分析 (Refined)
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from scipy import stats from scipy import stats
import matplotlib.pyplot as plt
import warnings import warnings
import os import os
import tempfile
from pathlib import Path from pathlib import Path
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# 设置绘图风格 # Matplotlib cache dirs避免 home 目录不可写导致的警告)
plt.style.use('seaborn-v0_8-whitegrid') _mpl_config_dir = Path(tempfile.gettempdir()) / "mm_task3_mplconfig"
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans'] # 适配中文 _xdg_cache_dir = Path(tempfile.gettempdir()) / "mm_task3_xdg_cache"
plt.rcParams['axes.unicode_minus'] = False _mpl_config_dir.mkdir(parents=True, exist_ok=True)
_xdg_cache_dir.mkdir(parents=True, exist_ok=True)
os.environ.setdefault("MPLCONFIGDIR", str(_mpl_config_dir))
os.environ.setdefault("XDG_CACHE_HOME", str(_xdg_cache_dir))
import matplotlib.pyplot as plt
from cycler import cycler
# 设置绘图风格(低饱和度 Morandi 色系)
MORANDI = {
"bg": "#F5F2ED",
"grid": "#D7D1C7",
"text": "#3F3F3F",
"muted_blue": "#8FA3A7",
"muted_blue_dark": "#6E858A",
"sage": "#AEB8A6",
"terracotta": "#B07A6A",
"warm_gray": "#8C857A",
}
plt.rcParams.update(
{
"figure.facecolor": MORANDI["bg"],
"axes.facecolor": MORANDI["bg"],
"savefig.facecolor": MORANDI["bg"],
"axes.edgecolor": MORANDI["grid"],
"axes.labelcolor": MORANDI["text"],
"xtick.color": MORANDI["text"],
"ytick.color": MORANDI["text"],
"text.color": MORANDI["text"],
"grid.color": MORANDI["grid"],
"grid.alpha": 0.55,
"axes.grid": True,
"axes.prop_cycle": cycler(
"color",
[
MORANDI["muted_blue"],
MORANDI["sage"],
MORANDI["terracotta"],
MORANDI["warm_gray"],
],
),
"legend.frameon": True,
"legend.framealpha": 0.9,
"legend.facecolor": MORANDI["bg"],
"legend.edgecolor": MORANDI["grid"],
"font.sans-serif": ["Arial Unicode MS", "SimHei", "DejaVu Sans"],
"axes.unicode_minus": False,
}
)
# ============================================ # ============================================
# 基础参数和函数 (保持不变) # 基础参数和函数 (保持不变)
@@ -333,16 +382,38 @@ for i, param in enumerate(params_list):
data = df_results[df_results['param'] == param].sort_values('value') data = df_results[df_results['param'] == param].sort_values('value')
# 绘制 E1/E2 - 左轴 # 绘制 E1/E2 - 左轴
line1, = ax.plot(data['value'], data['E1'], 'b-', linewidth=1.6, label='Expected Service (E1)') line1, = ax.plot(
line2, = ax.plot(data['value'], data['E2'], 'g-', linewidth=1.6, alpha=0.9, label='Quality-weighted (E2)') data['value'],
data['E1'],
linestyle='-',
color=MORANDI["muted_blue_dark"],
linewidth=1.8,
label='Expected Service (E1)',
)
line2, = ax.plot(
data['value'],
data['E2'],
linestyle='-',
color=MORANDI["sage"],
linewidth=1.8,
alpha=0.95,
label='Quality-weighted (E2)',
)
ax.set_xlabel(param_labels[param][0]) ax.set_xlabel(param_labels[param][0])
ax.set_ylabel('Service (E1/E2)') ax.set_ylabel('Service (E1/E2)')
# 绘制 R1 (风险) - 右轴 # 绘制 R1 (风险) - 右轴
ax2 = ax.twinx() ax2 = ax.twinx()
line3, = ax2.plot(data['value'], data['R1'], 'r--', linewidth=1.6, label='Shortfall Risk (R1)') line3, = ax2.plot(
ax2.set_ylabel('Risk Probability (R1)', color='r') data['value'],
ax2.tick_params(axis='y', labelcolor='r') data['R1'],
linestyle='--',
color=MORANDI["terracotta"],
linewidth=1.8,
label='Shortfall Risk (R1)',
)
ax2.set_ylabel('Risk Probability (R1)', color=MORANDI["terracotta"])
ax2.tick_params(axis='y', labelcolor=MORANDI["terracotta"])
# 添加基准线 # 添加基准线
if param == 'merge_ratio': if param == 'merge_ratio':
@@ -354,7 +425,7 @@ for i, param in enumerate(params_list):
elif param == 'cv_max': elif param == 'cv_max':
base_val = BASE_CV_MAX base_val = BASE_CV_MAX
ax.axvline(x=base_val, color='gray', linestyle=':', alpha=0.5) ax.axvline(x=base_val, color=MORANDI["warm_gray"], linestyle=':', alpha=0.65)
# 图例 # 图例
lines = [line1, line2, line3] lines = [line1, line2, line3]
@@ -362,7 +433,7 @@ for i, param in enumerate(params_list):
ax.legend(lines, labels, loc='best', frameon=True) ax.legend(lines, labels, loc='best', frameon=True)
ax.set_title(f'Effect of {param_labels[param][0]}') ax.set_title(f'Effect of {param_labels[param][0]}')
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.55)
plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.savefig(OUTPUT_FIG, dpi=300) plt.savefig(OUTPUT_FIG, dpi=300)

Binary file not shown.

View File

@@ -19,6 +19,7 @@ Task 3 - Step 8: 可视化Fig.1/2/4/5
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import json
from pathlib import Path from pathlib import Path
import os import os
@@ -38,11 +39,62 @@ import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib import colors as mcolors
from cycler import cycler
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
MORANDI = {
"bg": "#F5F2ED",
"grid": "#D7D1C7",
"text": "#3F3F3F",
"muted_blue": "#8FA3A7",
"muted_blue_dark": "#6E858A",
"sage": "#AEB8A6",
"clay": "#C6AA9B",
"rose": "#D2B8B3",
"warm_gray": "#8C857A",
"terracotta": "#B07A6A",
}
def _apply_morandi_style() -> None:
plt.rcParams.update(
{
"figure.facecolor": MORANDI["bg"],
"axes.facecolor": MORANDI["bg"],
"savefig.facecolor": MORANDI["bg"],
"axes.edgecolor": MORANDI["grid"],
"axes.labelcolor": MORANDI["text"],
"xtick.color": MORANDI["text"],
"ytick.color": MORANDI["text"],
"text.color": MORANDI["text"],
"grid.color": MORANDI["grid"],
"grid.alpha": 0.55,
"axes.grid": True,
"axes.titleweight": "semibold",
"axes.titlesize": 11,
"axes.labelsize": 10,
"legend.frameon": True,
"legend.framealpha": 0.9,
"legend.facecolor": MORANDI["bg"],
"legend.edgecolor": MORANDI["grid"],
"axes.prop_cycle": cycler(
"color",
[
MORANDI["muted_blue"],
MORANDI["sage"],
MORANDI["clay"],
MORANDI["rose"],
MORANDI["warm_gray"],
],
),
}
)
@dataclass(frozen=True) @dataclass(frozen=True)
class Paths: class Paths:
distance_xlsx: Path distance_xlsx: Path
@@ -76,6 +128,15 @@ def _pair_key(a: int, b: int) -> tuple[int, int]:
def fig1_pairing_map(paths: Paths) -> Path: def fig1_pairing_map(paths: Paths) -> Path:
_apply_morandi_style()
# Warmer + higher contrast for Fig.1 specifically (clearer on paper/screens)
fig1_colors = {
"paired": "#e76f51",
"unpaired": "#6c757d",
"link": "#bc6c25",
}
sites = pd.read_excel(paths.distance_xlsx, sheet_name="sites") sites = pd.read_excel(paths.distance_xlsx, sheet_name="sites")
pairs = pd.read_excel(paths.pairing_xlsx, sheet_name="selected_pairs") pairs = pd.read_excel(paths.pairing_xlsx, sheet_name="selected_pairs")
@@ -97,9 +158,9 @@ def fig1_pairing_map(paths: Paths) -> Path:
ax.plot( ax.plot(
[si["lon"], sj["lon"]], [si["lon"], sj["lon"]],
[si["lat"], sj["lat"]], [si["lat"], sj["lat"]],
color="#2c7fb8", color=fig1_colors["link"],
alpha=0.35, alpha=0.5,
linewidth=1.0, linewidth=1.2,
zorder=1, zorder=1,
) )
@@ -110,8 +171,8 @@ def fig1_pairing_map(paths: Paths) -> Path:
paired["lon"], paired["lon"],
paired["lat"], paired["lat"],
s=18, s=18,
color="#d95f0e", color=fig1_colors["paired"],
alpha=0.85, alpha=0.9,
label=f"Paired sites (n={len(paired)})", label=f"Paired sites (n={len(paired)})",
zorder=2, zorder=2,
) )
@@ -120,7 +181,7 @@ def fig1_pairing_map(paths: Paths) -> Path:
unpaired["lon"], unpaired["lon"],
unpaired["lat"], unpaired["lat"],
s=22, s=22,
color="#636363", color=fig1_colors["unpaired"],
alpha=0.9, alpha=0.9,
marker="x", marker="x",
label=f"Unpaired sites (n={len(unpaired)})", label=f"Unpaired sites (n={len(unpaired)})",
@@ -130,7 +191,7 @@ def fig1_pairing_map(paths: Paths) -> Path:
ax.set_title("Fig.1 Pairing map (sites + selected 34 links)") ax.set_title("Fig.1 Pairing map (sites + selected 34 links)")
ax.set_xlabel("Longitude") ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude") ax.set_ylabel("Latitude")
ax.grid(True, alpha=0.2) ax.grid(True, alpha=0.55)
ax.legend(loc="best", frameon=True) ax.legend(loc="best", frameon=True)
out = paths.figures_dir / "fig1_pairing_map.png" out = paths.figures_dir / "fig1_pairing_map.png"
@@ -140,7 +201,58 @@ def fig1_pairing_map(paths: Paths) -> Path:
return out return out
def export_fig1_points_js(paths: Paths) -> Path:
sites = pd.read_excel(paths.distance_xlsx, sheet_name="sites").copy()
pairs = pd.read_excel(paths.pairing_xlsx, sheet_name="selected_pairs").copy()
sites["site_id"] = sites["site_id"].astype(int)
sites["k"] = sites["k"].astype(int)
paired_ids = set(pairs["site_i_id"].astype(int)).union(
set(pairs["site_j_id"].astype(int))
)
points = []
for _, r in sites.iterrows():
points.append(
{
"site_id": int(r["site_id"]),
"site_name": str(r["site_name"]),
"lat": float(r["lat"]),
"lng": float(r["lon"]),
"mu": float(r["mu"]),
"sigma": float(r["sigma"]),
"k": int(r["k"]),
"is_paired": int(r["site_id"]) in paired_ids,
}
)
links = []
for _, r in pairs.iterrows():
links.append(
{
"site_i_id": int(r["site_i_id"]),
"site_j_id": int(r["site_j_id"]),
"site_i_name": str(r["site_i_name"]),
"site_j_name": str(r["site_j_name"]),
"distance": float(r["distance"]),
}
)
out = BASE_DIR / "fig1_points.js"
payload = (
"// Auto-generated from `task3/01_distance.xlsx` (sites) + `task3/02_pairing.xlsx` (selected_pairs)\n"
"// Usage: open `task3/fig1_carto.html` in a browser (same directory must contain this file).\n"
f"window.FIG1_POINTS = {json.dumps(points, ensure_ascii=False, separators=(',', ':'))};\n"
f"window.FIG1_LINKS = {json.dumps(links, ensure_ascii=False, separators=(',', ':'))};\n"
)
out.write_text(payload, encoding="utf-8")
return out
def fig2_allocation_scatter(paths: Paths) -> Path: def fig2_allocation_scatter(paths: Paths) -> Path:
_apply_morandi_style()
df = pd.read_excel(paths.allocation_xlsx, sheet_name="allocation") df = pd.read_excel(paths.allocation_xlsx, sheet_name="allocation")
q_ratio = df["q_ratio"].to_numpy(dtype=float) q_ratio = df["q_ratio"].to_numpy(dtype=float)
@@ -157,15 +269,22 @@ def fig2_allocation_scatter(paths: Paths) -> Path:
] ]
for ax, (title, x) in zip(axes, panels): for ax, (title, x) in zip(axes, panels):
ax.scatter(x, q_ratio, s=22, alpha=0.75, color="#3182bd", edgecolor="none") ax.scatter(
x,
q_ratio,
s=22,
alpha=0.8,
color=MORANDI["muted_blue"],
edgecolor="none",
)
if np.isfinite(x).all() and np.isfinite(q_ratio).all() and len(x) >= 2: if np.isfinite(x).all() and np.isfinite(q_ratio).all() and len(x) >= 2:
coef = np.polyfit(x, q_ratio, 1) coef = np.polyfit(x, q_ratio, 1)
xx = np.linspace(float(np.min(x)), float(np.max(x)), 100) xx = np.linspace(float(np.min(x)), float(np.max(x)), 100)
yy = coef[0] * xx + coef[1] yy = coef[0] * xx + coef[1]
ax.plot(xx, yy, color="#de2d26", linewidth=2.0, alpha=0.9) ax.plot(xx, yy, color=MORANDI["terracotta"], linewidth=2.0, alpha=0.95)
ax.set_title(title) ax.set_title(title)
ax.set_xlabel("x") ax.set_xlabel("x")
ax.grid(True, alpha=0.2) ax.grid(True, alpha=0.55)
axes[0].set_ylabel("$q^*/Q$") axes[0].set_ylabel("$q^*/Q$")
fig.suptitle("Fig.2 Allocation strategy scatter (34 pairs)", y=1.02) fig.suptitle("Fig.2 Allocation strategy scatter (34 pairs)", y=1.02)
@@ -178,6 +297,8 @@ def fig2_allocation_scatter(paths: Paths) -> Path:
def fig4_calendar_heatmap(paths: Paths) -> Path: def fig4_calendar_heatmap(paths: Paths) -> Path:
_apply_morandi_style()
sites = pd.read_excel(paths.distance_xlsx, sheet_name="sites")[["site_id", "mu"]] sites = pd.read_excel(paths.distance_xlsx, sheet_name="sites")[["site_id", "mu"]]
sites["site_id"] = sites["site_id"].astype(int) sites["site_id"] = sites["site_id"].astype(int)
mu_by_id = dict(zip(sites["site_id"], sites["mu"])) mu_by_id = dict(zip(sites["site_id"], sites["mu"]))
@@ -212,7 +333,17 @@ def fig4_calendar_heatmap(paths: Paths) -> Path:
expected[day - 1, slot - 1] = pair_E.get(_pair_key(i_id, j_id), np.nan) expected[day - 1, slot - 1] = pair_E.get(_pair_key(i_id, j_id), np.nan)
fig, ax = plt.subplots(figsize=(12, 2.6), dpi=200) fig, ax = plt.subplots(figsize=(12, 2.6), dpi=200)
im = ax.imshow(expected.T, aspect="auto", cmap="viridis") cmap = mcolors.LinearSegmentedColormap.from_list(
"morandi",
[
MORANDI["bg"],
"#E7E1D7",
MORANDI["sage"],
MORANDI["muted_blue"],
"#5F7478",
],
)
im = ax.imshow(expected.T, aspect="auto", cmap=cmap)
ax.set_yticks([0, 1], labels=["Slot 1", "Slot 2"]) ax.set_yticks([0, 1], labels=["Slot 1", "Slot 2"])
ax.set_xlabel("Day of year") ax.set_xlabel("Day of year")
ax.set_title("Fig.4 Calendar heatmap (expected service per slot)") ax.set_title("Fig.4 Calendar heatmap (expected service per slot)")
@@ -233,6 +364,8 @@ def fig4_calendar_heatmap(paths: Paths) -> Path:
def fig5_risk_distribution(paths: Paths) -> Path: def fig5_risk_distribution(paths: Paths) -> Path:
_apply_morandi_style()
risk = pd.read_excel(paths.evaluate_xlsx, sheet_name="pair_risk").copy() risk = pd.read_excel(paths.evaluate_xlsx, sheet_name="pair_risk").copy()
p = risk["shortfall_prob_either"].to_numpy(dtype=float) p = risk["shortfall_prob_either"].to_numpy(dtype=float)
p = p[np.isfinite(p)] p = p[np.isfinite(p)]
@@ -240,21 +373,36 @@ def fig5_risk_distribution(paths: Paths) -> Path:
fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), dpi=200) fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), dpi=200)
ax = axes[0] ax = axes[0]
ax.hist(p, bins=10, color="#3182bd", alpha=0.85, edgecolor="white") ax.hist(p, bins=10, color=MORANDI["sage"], alpha=0.9, edgecolor=MORANDI["bg"])
ax.axvline(float(np.mean(p)), color="#de2d26", linewidth=2.0, label=f"mean={np.mean(p):.3f}") ax.axvline(
float(np.mean(p)),
color=MORANDI["terracotta"],
linewidth=2.2,
label=f"mean={np.mean(p):.3f}",
)
ax.set_title("Histogram") ax.set_title("Histogram")
ax.set_xlabel("Shortfall probability (either site)") ax.set_xlabel("Shortfall probability (either site)")
ax.set_ylabel("Count") ax.set_ylabel("Count")
ax.grid(True, alpha=0.2) ax.grid(True, alpha=0.55)
ax.legend(loc="best", frameon=True) ax.legend(loc="best", frameon=True)
ax = axes[1] ax = axes[1]
p_sorted = np.sort(p)[::-1] p_sorted = np.sort(p)[::-1]
ax.plot(np.arange(1, len(p_sorted) + 1), p_sorted, marker="o", markersize=3.5, linewidth=1.5) ax.plot(
np.arange(1, len(p_sorted) + 1),
p_sorted,
marker="o",
markersize=3.2,
linewidth=1.6,
color=MORANDI["muted_blue_dark"],
markerfacecolor=MORANDI["muted_blue"],
markeredgecolor=MORANDI["bg"],
markeredgewidth=0.6,
)
ax.set_title("Sorted by risk (descending)") ax.set_title("Sorted by risk (descending)")
ax.set_xlabel("Pair rank") ax.set_xlabel("Pair rank")
ax.set_ylabel("Shortfall probability") ax.set_ylabel("Shortfall probability")
ax.grid(True, alpha=0.2) ax.grid(True, alpha=0.55)
fig.suptitle("Fig.5 Risk distribution across 34 pairs", y=1.02) fig.suptitle("Fig.5 Risk distribution across 34 pairs", y=1.02)
fig.tight_layout() fig.tight_layout()
@@ -282,6 +430,8 @@ def main() -> None:
out1 = fig1_pairing_map(paths) out1 = fig1_pairing_map(paths)
print(f"Saved: {out1.relative_to(BASE_DIR)}") print(f"Saved: {out1.relative_to(BASE_DIR)}")
out1_js = export_fig1_points_js(paths)
print(f"Saved: {out1_js.relative_to(BASE_DIR)}")
out2 = fig2_allocation_scatter(paths) out2 = fig2_allocation_scatter(paths)
print(f"Saved: {out2.relative_to(BASE_DIR)}") print(f"Saved: {out2.relative_to(BASE_DIR)}")
out4 = fig4_calendar_heatmap(paths) out4 = fig4_calendar_heatmap(paths)

View File

@@ -643,6 +643,8 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
``` ```
task3/ task3/
├── fig1_carto.html # 交互地图CartoDB 底图)
├── fig1_points.js # ✅ 自动生成08_visualize.py
├── 08_visualize.py # 可视化脚本 ├── 08_visualize.py # 可视化脚本
└── figures/ └── figures/
├── fig1_pairing_map.png ├── fig1_pairing_map.png
@@ -656,6 +658,8 @@ task3/
![Fig.1 站点配对地图](figures/fig1_pairing_map.png) ![Fig.1 站点配对地图](figures/fig1_pairing_map.png)
交互版:用浏览器打开 `task3/fig1_carto.html`(同目录需有 `task3/fig1_points.js`;需联网加载 CartoDB 底图)。
![Fig.2 分配策略散点图](figures/fig2_allocation_scatter.png) ![Fig.2 分配策略散点图](figures/fig2_allocation_scatter.png)
![Fig.4 日历热力图](figures/fig4_calendar_heatmap.png) ![Fig.4 日历热力图](figures/fig4_calendar_heatmap.png)

175
task3/fig1_carto.html Normal file
View File

@@ -0,0 +1,175 @@
<!doctype html>
<html lang="zh-CN">
<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;
}
.legend {
background: rgba(255, 249, 240, 0.95);
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;
border: 1px solid rgba(188, 108, 37, 0.25);
}
.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) {
alert("FIG1_POINTS 为空:请先运行 task3/08_visualize.py 生成 fig1_points.js或确认该文件与本页面同目录。");
}
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:
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors &copy; <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,
});
marker.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;">${p.site_id}. ${p.site_name}</div>` +
`<div><b>mu</b>: ${p.mu.toFixed(1)}</div>` +
`<div><b>sigma</b>: ${p.sigma.toFixed(1)}</div>` +
`<div><b>k</b>: ${p.k}</div>` +
`<div><b>paired</b>: ${p.is_paired ? "yes" : "no"}</div>` +
`</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>` +
`<div><b>distance</b>: ${e.distance.toFixed(2)} mi</div>` +
`</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;
const div = L.DomUtil.create("div", "legend");
div.innerHTML =
`<div class="title">Task 3 Fig.1 配对地图(交互)</div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.paired}"></span>已配对站点</span><span>${pairedCount}</span></div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.unpaired}"></span>未配对站点</span><span>${points.length - pairedCount}</span></div>` +
`<div class="muted">点大小: mu线性缩放</div>` +
`<div class="line"></div>` +
`<div class="muted">连线: 34条配对点击查看距离</div>`;
return div;
};
legend.addTo(map);
</script>
</body>
</html>

4
task3/fig1_points.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 92 KiB