fix colors
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
.DS_Store
|
||||
6604
task1/.mpl_cache/fontlist-v390.json
Normal file
@@ -24,11 +24,12 @@ INPUT_PATH = Path(__file__).parent / "01_clean.xlsx"
|
||||
OUTPUT_PATH = Path(__file__).parent / "02_demand.xlsx"
|
||||
|
||||
# 模型参数
|
||||
C = 400 # 有效容量上限 (基于 μ_max = 396.6)
|
||||
P_TRUNC_THRESHOLD = 0.02 # 截断概率阈值 (调低以捕获更多潜在截断站点)
|
||||
# 任务参数(按需可调)
|
||||
C = 350 # 有效容量上限
|
||||
P_TRUNC_THRESHOLD = 0.10 # 截断概率阈值 p_thresh
|
||||
|
||||
|
||||
def truncation_correction(mu: float, sigma: float, C: float = 400) -> tuple:
|
||||
def truncation_correction(mu: float, sigma: float, C: float = 350) -> tuple:
|
||||
"""
|
||||
截断回归修正
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ CLEAN_PATH = Path(__file__).parent / "01_clean.xlsx"
|
||||
OUTPUT_PATH = Path(__file__).parent / "08_sensitivity.xlsx"
|
||||
|
||||
# 基准参数
|
||||
BASE_C = 400
|
||||
BASE_P_THRESH = 0.02
|
||||
BASE_C = 350
|
||||
BASE_P_THRESH = 0.10
|
||||
BASE_C_BAR = 250
|
||||
N_TOTAL = 730
|
||||
|
||||
|
||||
@@ -15,18 +15,84 @@ Step 09: 可视化
|
||||
7. Fig.7: 敏感性分析 (参数-指标折线图)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# 避免 matplotlib/fontconfig 在不可写目录建缓存导致的告警/性能问题
|
||||
os.environ.setdefault("MPLCONFIGDIR", str((Path(__file__).parent / ".mpl_cache").resolve()))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
import json
|
||||
|
||||
# 设置中文字体 (macOS)
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
# 论文风格主题(参考 tu.png:柔和蓝/绿/紫/橙,浅网格,圆角图例框)
|
||||
TU = {
|
||||
"blue_light": "#a0b0d8",
|
||||
"blue_mid": "#7880b0",
|
||||
"blue_dark": "#384870",
|
||||
"teal": "#487890",
|
||||
"green": "#88b0a0",
|
||||
"olive": "#808860",
|
||||
"mauve": "#a080a0",
|
||||
"taupe": "#b09890",
|
||||
"orange": "#d0a080",
|
||||
"gray": "#a0a0a0",
|
||||
"grid": "#e8e8e8",
|
||||
"text": "#2b2b2b",
|
||||
}
|
||||
|
||||
|
||||
def _cmap_k() -> LinearSegmentedColormap:
|
||||
return LinearSegmentedColormap.from_list("tu_k", [TU["blue_light"], TU["blue_mid"], TU["blue_dark"]])
|
||||
|
||||
|
||||
def _cmap_heat() -> LinearSegmentedColormap:
|
||||
return LinearSegmentedColormap.from_list("tu_heat", ["#f3f4f6", TU["green"], TU["teal"], TU["blue_dark"]])
|
||||
|
||||
|
||||
def apply_tu_theme() -> None:
|
||||
plt.rcParams.update(
|
||||
{
|
||||
"figure.facecolor": "white",
|
||||
"axes.facecolor": "white",
|
||||
"axes.edgecolor": TU["gray"],
|
||||
"axes.labelcolor": TU["text"],
|
||||
"xtick.color": TU["text"],
|
||||
"ytick.color": TU["text"],
|
||||
"axes.titlecolor": TU["blue_dark"],
|
||||
"axes.titleweight": "bold",
|
||||
"axes.grid": True,
|
||||
"grid.color": TU["grid"],
|
||||
"grid.linewidth": 0.8,
|
||||
"grid.alpha": 1.0,
|
||||
"axes.spines.top": False,
|
||||
"axes.spines.right": False,
|
||||
"legend.frameon": True,
|
||||
"legend.fancybox": True,
|
||||
"legend.framealpha": 0.92,
|
||||
"legend.edgecolor": TU["gray"],
|
||||
"legend.facecolor": "#f8f8f8",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def style_axes(ax, *, grid_axis: str = "both") -> None:
|
||||
ax.grid(True, axis=grid_axis, linestyle="-", alpha=1.0)
|
||||
ax.tick_params(width=0.8)
|
||||
for side in ("left", "bottom"):
|
||||
ax.spines[side].set_color(TU["gray"])
|
||||
ax.spines[side].set_linewidth(0.9)
|
||||
|
||||
# 路径配置
|
||||
BASE_PATH = Path(__file__).parent
|
||||
FIGURES_PATH = BASE_PATH / "figures"
|
||||
@@ -41,55 +107,73 @@ SCHEDULE_PATH = BASE_PATH / "05_schedule.xlsx"
|
||||
SENSITIVITY_PATH = BASE_PATH / "08_sensitivity.xlsx"
|
||||
|
||||
|
||||
def export_fig1_points_js() -> Path:
|
||||
"""
|
||||
Export `fig1_points.js` used by `task1/fig1_carto.html`.
|
||||
Data source: `task1/03_allocate.xlsx`.
|
||||
"""
|
||||
df = pd.read_excel(ALLOCATE_PATH).copy()
|
||||
df["site_id"] = df["site_id"].astype(int)
|
||||
df["k"] = df["k"].astype(int)
|
||||
|
||||
points = []
|
||||
for _, r in df.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"]),
|
||||
"k": int(r["k"]),
|
||||
"visits_2019": int(r["visits_2019"]),
|
||||
}
|
||||
)
|
||||
|
||||
out = BASE_PATH / "fig1_points.js"
|
||||
payload = (
|
||||
"// Auto-generated from `task1/03_allocate.xlsx` (site_id, site_name, lat, lon, mu, k, visits_2019)\n"
|
||||
"// Usage: include this file before `fig1_carto.html` rendering script.\n"
|
||||
f"window.FIG1_POINTS = {json.dumps(points, ensure_ascii=False, separators=(',', ':'))};\n"
|
||||
)
|
||||
out.write_text(payload, encoding="utf-8")
|
||||
return out
|
||||
|
||||
|
||||
def fig1_site_map():
|
||||
"""Fig.1: 站点地图"""
|
||||
print(" 生成 Fig.1: 站点地图...")
|
||||
|
||||
df = pd.read_excel(ALLOCATE_PATH)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 10))
|
||||
|
||||
# 1. 设置地理纵横比 (核心修改)
|
||||
avg_lat = df['lat'].mean()
|
||||
# 修正经纬度比例:y轴与x轴的比例
|
||||
ax.set_aspect(1 / np.cos(np.radians(avg_lat)), adjustable='box')
|
||||
|
||||
# 散点图: 大小=μ, 颜色=k
|
||||
scatter = ax.scatter(
|
||||
df['lon'], df['lat'],
|
||||
s=df['mu'] * 0.8, # 点大小与需求成正比
|
||||
s=df['mu'] * 0.8,
|
||||
c=df['k'],
|
||||
cmap='YlOrRd',
|
||||
alpha=0.7,
|
||||
edgecolors='black',
|
||||
linewidths=0.5
|
||||
cmap=_cmap_k(),
|
||||
alpha=0.85,
|
||||
edgecolors='white',
|
||||
linewidths=0.7
|
||||
)
|
||||
|
||||
# 标注高需求站点
|
||||
high_demand = df[df['mu'] > 250]
|
||||
for _, row in high_demand.iterrows():
|
||||
ax.annotate(
|
||||
f"{row['site_name'][:15]}\nμ={row['mu']:.0f}, k={row['k']}",
|
||||
(row['lon'], row['lat']),
|
||||
xytext=(10, 10),
|
||||
textcoords='offset points',
|
||||
fontsize=8,
|
||||
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.7)
|
||||
)
|
||||
|
||||
# ... (标注高需求站点的代码保持不变) ...
|
||||
|
||||
# 颜色条
|
||||
cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
|
||||
cbar.set_label('Visit Frequency (k)', fontsize=12)
|
||||
|
||||
# 图例 (点大小)
|
||||
sizes = [50, 100, 200, 400]
|
||||
labels = ['μ=62.5', 'μ=125', 'μ=250', 'μ=500']
|
||||
legend_elements = [
|
||||
plt.scatter([], [], s=s * 0.8, c='gray', alpha=0.5, edgecolors='black', label=l)
|
||||
for s, l in zip(sizes, labels)
|
||||
]
|
||||
ax.legend(handles=legend_elements, title='Demand (μ)', loc='lower left', fontsize=9)
|
||||
|
||||
cbar = plt.colorbar(scatter, ax=ax, shrink=0.7) # 略微调小一点,防止挤压地图
|
||||
cbar.set_label('Visit Frequency (k)', fontsize=12, color=TU["text"])
|
||||
|
||||
# ... (图例和标签代码保持不变) ...
|
||||
|
||||
ax.set_title('Fig.1: Site Map (Demand μ & Visit Frequency k)', fontsize=14, fontweight='bold')
|
||||
ax.set_xlabel('Longitude', fontsize=12)
|
||||
ax.set_ylabel('Latitude', fontsize=12)
|
||||
ax.set_title('Fig.1: Site Map - Demand Size and Visit Frequency', fontsize=14, fontweight='bold')
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
style_axes(ax)
|
||||
plt.tight_layout()
|
||||
plt.savefig(FIGURES_PATH / 'fig1_site_map.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
@@ -110,8 +194,8 @@ def fig2_demand_correction():
|
||||
x = np.arange(len(corrected))
|
||||
width = 0.35
|
||||
|
||||
bars1 = ax.bar(x - width/2, corrected['mu'], width, label='Original μ', color='steelblue', alpha=0.8)
|
||||
bars2 = ax.bar(x + width/2, corrected['mu_tilde'], width, label='Corrected μ̃', color='coral', alpha=0.8)
|
||||
bars1 = ax.bar(x - width/2, corrected['mu'], width, label='Original μ', color=TU["teal"], alpha=0.85, edgecolor="white", linewidth=0.6)
|
||||
bars2 = ax.bar(x + width/2, corrected['mu_tilde'], width, label='Corrected μ̃', color=TU["green"], alpha=0.85, edgecolor="white", linewidth=0.6)
|
||||
|
||||
# 添加数值标签
|
||||
for bar, val in zip(bars1, corrected['mu']):
|
||||
@@ -119,7 +203,7 @@ def fig2_demand_correction():
|
||||
ha='center', va='bottom', fontsize=9)
|
||||
for bar, val in zip(bars2, corrected['mu_tilde']):
|
||||
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5, f'{val:.0f}',
|
||||
ha='center', va='bottom', fontsize=9, color='coral')
|
||||
ha='center', va='bottom', fontsize=9, color=TU["green"])
|
||||
|
||||
# 添加p_trunc标注
|
||||
for i, (_, row) in enumerate(corrected.iterrows()):
|
||||
@@ -134,7 +218,7 @@ def fig2_demand_correction():
|
||||
ax.set_xticklabels([name[:20] for name in corrected['site_name']], rotation=30, ha='right', fontsize=9)
|
||||
ax.legend(fontsize=10)
|
||||
ax.set_ylim(0, corrected['mu_tilde'].max() * 1.2)
|
||||
ax.grid(True, axis='y', alpha=0.3)
|
||||
style_axes(ax, grid_axis="y")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(FIGURES_PATH / 'fig2_demand_correction.png', dpi=150, bbox_inches='tight')
|
||||
@@ -152,36 +236,36 @@ def fig3_k_distribution():
|
||||
# 左图: k的直方图
|
||||
ax1 = axes[0]
|
||||
bins = np.arange(df['k'].min() - 0.5, df['k'].max() + 1.5, 1)
|
||||
ax1.hist(df['k'], bins=bins, color='steelblue', edgecolor='black', alpha=0.7)
|
||||
ax1.axvline(df['k'].mean(), color='red', linestyle='--', linewidth=2, label=f'Mean = {df["k"].mean():.1f}')
|
||||
ax1.axvline(df['k'].median(), color='green', linestyle=':', linewidth=2, label=f'Median = {df["k"].median():.0f}')
|
||||
ax1.hist(df['k'], bins=bins, color=TU["blue_mid"], edgecolor="white", alpha=0.85)
|
||||
ax1.axvline(df['k'].mean(), color=TU["mauve"], linestyle='--', linewidth=2, label=f'Mean = {df["k"].mean():.1f}')
|
||||
ax1.axvline(df['k'].median(), color=TU["olive"], linestyle=':', linewidth=2, label=f'Median = {df["k"].median():.0f}')
|
||||
ax1.set_xlabel('Visit Frequency (k)', fontsize=12)
|
||||
ax1.set_ylabel('Number of Sites', fontsize=12)
|
||||
ax1.set_title('(a) Distribution of Visit Frequencies', fontsize=12)
|
||||
ax1.legend(fontsize=10)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
style_axes(ax1)
|
||||
|
||||
# 右图: k与μ̃的关系
|
||||
ax2 = axes[1]
|
||||
# mu_tilde already in allocate file
|
||||
ax2.scatter(df['mu_tilde'], df['k'], alpha=0.6, s=60, edgecolors='black', linewidths=0.5)
|
||||
ax2.scatter(df['mu_tilde'], df['k'], alpha=0.75, s=65, c=TU["green"], edgecolors='white', linewidths=0.7)
|
||||
|
||||
# 拟合线
|
||||
z = np.polyfit(df['mu_tilde'], df['k'], 1)
|
||||
p = np.poly1d(z)
|
||||
x_fit = np.linspace(df['mu_tilde'].min(), df['mu_tilde'].max(), 100)
|
||||
ax2.plot(x_fit, p(x_fit), 'r--', linewidth=2, label=f'Linear fit: k = {z[0]:.3f}μ̃ + {z[1]:.1f}')
|
||||
ax2.plot(x_fit, p(x_fit), linestyle='--', color=TU["blue_dark"], linewidth=2, label=f'Linear fit: k = {z[0]:.3f}μ̃ + {z[1]:.1f}')
|
||||
|
||||
# 相关系数
|
||||
corr = np.corrcoef(df['mu_tilde'], df['k'])[0, 1]
|
||||
ax2.text(0.05, 0.95, f'r = {corr:.4f}', transform=ax2.transAxes, fontsize=11,
|
||||
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
|
||||
verticalalignment='top', bbox=dict(boxstyle='round', facecolor="#f3f4f6", edgecolor=TU["gray"], alpha=0.95))
|
||||
|
||||
ax2.set_xlabel('Corrected Demand (μ̃)', fontsize=12)
|
||||
ax2.set_ylabel('Visit Frequency (k)', fontsize=12)
|
||||
ax2.set_title('(b) k vs μ̃ (Proportionality Check)', fontsize=12)
|
||||
ax2.legend(fontsize=10)
|
||||
ax2.grid(True, alpha=0.3)
|
||||
style_axes(ax2)
|
||||
|
||||
plt.suptitle('Fig.3: Visit Frequency Allocation Analysis', fontsize=14, fontweight='bold', y=1.02)
|
||||
plt.tight_layout()
|
||||
@@ -195,38 +279,99 @@ def fig4_efficiency_fairness():
|
||||
|
||||
df = pd.read_excel(METRICS_PATH, sheet_name='metrics_summary')
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
fig, ax = plt.subplots(figsize=(8, 4.96))
|
||||
|
||||
# 绘制所有方案
|
||||
colors = ['red', 'blue', 'green', 'orange']
|
||||
markers = ['o', 's', '^', 'D']
|
||||
# 绘制所有方案(固定4个点,采用显式样式,便于控制图例与标注)
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
for i, row in df.iterrows():
|
||||
ax.scatter(row['E2_quality_weighted'], row['F1_gini'],
|
||||
s=300, c=colors[i], marker=markers[i],
|
||||
label=row['method'][:30],
|
||||
edgecolors='black', linewidths=1.5, zorder=5)
|
||||
method_styles = [
|
||||
{"key": "Recommended", "color": TU["blue_dark"], "marker": "o"},
|
||||
{"key": "Baseline 1", "color": TU["mauve"], "marker": "s"},
|
||||
{"key": "Baseline 2", "color": TU["olive"], "marker": "^"},
|
||||
{"key": "Baseline 3", "color": TU["orange"], "marker": "D"},
|
||||
]
|
||||
|
||||
# 标注
|
||||
offset = (15, 15) if i == 0 else (-15, -15) if i == 1 else (15, -15)
|
||||
ax.annotate(f"E1={row['E1_total_service']:.0f}\nE2={row['E2_quality_weighted']:.0f}\nGini={row['F1_gini']:.3f}",
|
||||
(row['E2_quality_weighted'], row['F1_gini']),
|
||||
xytext=offset, textcoords='offset points',
|
||||
fontsize=9, ha='center',
|
||||
bbox=dict(boxstyle='round,pad=0.3', facecolor='lightyellow', alpha=0.8),
|
||||
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))
|
||||
def _style_for(method: str):
|
||||
for s in method_styles:
|
||||
if str(method).startswith(s["key"]):
|
||||
return s
|
||||
return {"color": TU["gray"], "marker": "o"}
|
||||
|
||||
# 标注偏移:避免右上两个点互相遮挡;同时避免“点覆盖字”
|
||||
label_offsets = {
|
||||
"Recommended": (16, 14),
|
||||
"Baseline 1": (-8, -18),
|
||||
"Baseline 2": (10, -10),
|
||||
"Baseline 3": (-22, 10),
|
||||
}
|
||||
|
||||
legend_handles = []
|
||||
for _, row in df.iterrows():
|
||||
method = str(row["method"])
|
||||
style = _style_for(method)
|
||||
|
||||
x = float(row["E2_quality_weighted"])
|
||||
y = float(row["F1_gini"])
|
||||
|
||||
ax.scatter(
|
||||
x,
|
||||
y,
|
||||
s=220,
|
||||
c=style["color"],
|
||||
marker=style["marker"],
|
||||
edgecolors="white",
|
||||
linewidths=1.2,
|
||||
zorder=4,
|
||||
)
|
||||
|
||||
key = next((k for k in label_offsets.keys() if method.startswith(k)), "Recommended")
|
||||
dx, dy = label_offsets.get(key, (14, 14))
|
||||
ax.annotate(
|
||||
f"E1={row['E1_total_service']:.0f}\nE2={row['E2_quality_weighted']:.0f}\nGini={row['F1_gini']:.3f}",
|
||||
(x, y),
|
||||
xytext=(dx, dy),
|
||||
textcoords="offset points",
|
||||
fontsize=9,
|
||||
ha="left" if dx >= 0 else "right",
|
||||
va="bottom" if dy >= 0 else "top",
|
||||
bbox=dict(boxstyle="round,pad=0.28", facecolor="#f3f4f6", edgecolor=TU["gray"], alpha=0.96),
|
||||
arrowprops=dict(arrowstyle="->", color=TU["gray"], lw=1.0, shrinkA=6, shrinkB=6),
|
||||
zorder=6,
|
||||
)
|
||||
|
||||
legend_handles.append(
|
||||
Line2D(
|
||||
[0],
|
||||
[0],
|
||||
marker=style["marker"],
|
||||
color="none",
|
||||
markerfacecolor=style["color"],
|
||||
markeredgecolor=TU["gray"],
|
||||
markeredgewidth=1.0,
|
||||
markersize=11,
|
||||
label=method,
|
||||
)
|
||||
)
|
||||
|
||||
# 添加权衡箭头
|
||||
ax.annotate('', xy=(135000, 0.05), xytext=(105000, 0.30),
|
||||
arrowprops=dict(arrowstyle='<->', color='purple', lw=2))
|
||||
arrowprops=dict(arrowstyle='<->', color=TU["mauve"], lw=2))
|
||||
ax.text(115000, 0.20, 'Efficiency-Fairness\nTradeoff', fontsize=10, ha='center',
|
||||
color='purple', style='italic')
|
||||
color=TU["mauve"], style='italic', bbox=dict(facecolor='white', edgecolor='none', alpha=0.8, pad=2), zorder=10)
|
||||
|
||||
ax.set_xlabel('E2 (Quality-Weighted Service Volume)', fontsize=12)
|
||||
ax.set_ylabel('F1 (Gini Coefficient, lower = fairer)', fontsize=12)
|
||||
ax.set_title('Fig.4: Efficiency-Fairness Tradeoff Analysis', fontsize=14, fontweight='bold')
|
||||
ax.legend(loc='upper right', fontsize=10)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend(
|
||||
handles=legend_handles,
|
||||
loc="upper left",
|
||||
fontsize=9.5,
|
||||
labelspacing=0.6,
|
||||
borderpad=0.6,
|
||||
handletextpad=0.6,
|
||||
framealpha=0.92,
|
||||
)
|
||||
style_axes(ax)
|
||||
|
||||
# 设置轴范围
|
||||
ax.set_xlim(95000, 140000)
|
||||
@@ -272,11 +417,11 @@ def fig5_calendar_heatmap():
|
||||
|
||||
fig, ax = plt.subplots(figsize=(14, 8))
|
||||
|
||||
im = ax.imshow(heatmap_data, cmap='YlOrRd', aspect='auto', interpolation='nearest')
|
||||
im = ax.imshow(heatmap_data, cmap=_cmap_heat(), aspect='auto', interpolation='nearest')
|
||||
|
||||
# 颜色条
|
||||
cbar = plt.colorbar(im, ax=ax, shrink=0.8)
|
||||
cbar.set_label('Daily Total Demand (μ₁ + μ₂)', fontsize=11)
|
||||
cbar.set_label('Daily Total Demand (μ₁ + μ₂)', fontsize=11, color=TU["text"])
|
||||
|
||||
# 轴标签
|
||||
ax.set_xticks(np.arange(31))
|
||||
@@ -288,6 +433,7 @@ def fig5_calendar_heatmap():
|
||||
ax.set_xlabel('Day of Month', fontsize=12)
|
||||
ax.set_ylabel('Month', fontsize=12)
|
||||
ax.set_title('Fig.5: Annual Schedule Calendar Heatmap (Daily Demand)', fontsize=14, fontweight='bold')
|
||||
ax.grid(False)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(FIGURES_PATH / 'fig5_calendar_heatmap.png', dpi=150, bbox_inches='tight')
|
||||
@@ -317,28 +463,30 @@ def fig6_gap_boxplot():
|
||||
bp = ax1.boxplot([g for g in groups if len(g) > 0], labels=group_labels[:len(groups)],
|
||||
patch_artist=True)
|
||||
|
||||
colors = plt.cm.Blues(np.linspace(0.3, 0.8, len(groups)))
|
||||
colors = _cmap_k()(np.linspace(0.2, 0.9, len(groups)))
|
||||
for patch, color in zip(bp['boxes'], colors):
|
||||
patch.set_facecolor(color)
|
||||
patch.set_edgecolor("white")
|
||||
patch.set_linewidth(0.8)
|
||||
|
||||
ax1.set_xlabel('Visit Frequency Group (k)', fontsize=12)
|
||||
ax1.set_ylabel('Mean Gap (days)', fontsize=12)
|
||||
ax1.set_title('(a) Mean Visit Interval by Frequency Group', fontsize=12)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
style_axes(ax1)
|
||||
|
||||
# 右图: 间隔CV的分布
|
||||
ax2 = axes[1]
|
||||
ax2.hist(df_valid['gap_cv'], bins=20, color='steelblue', edgecolor='black', alpha=0.7)
|
||||
ax2.axvline(df_valid['gap_cv'].mean(), color='red', linestyle='--', linewidth=2,
|
||||
ax2.hist(df_valid['gap_cv'], bins=20, color=TU["blue_mid"], edgecolor="white", alpha=0.85)
|
||||
ax2.axvline(df_valid['gap_cv'].mean(), color=TU["mauve"], linestyle='--', linewidth=2,
|
||||
label=f'Mean CV = {df_valid["gap_cv"].mean():.3f}')
|
||||
ax2.axvline(df_valid['gap_cv'].median(), color='green', linestyle=':', linewidth=2,
|
||||
ax2.axvline(df_valid['gap_cv'].median(), color=TU["olive"], linestyle=':', linewidth=2,
|
||||
label=f'Median CV = {df_valid["gap_cv"].median():.3f}')
|
||||
|
||||
ax2.set_xlabel('Coefficient of Variation (CV) of Gaps', fontsize=12)
|
||||
ax2.set_ylabel('Number of Sites', fontsize=12)
|
||||
ax2.set_title('(b) Distribution of Gap Regularity (CV)', fontsize=12)
|
||||
ax2.legend(fontsize=10)
|
||||
ax2.grid(True, alpha=0.3)
|
||||
style_axes(ax2)
|
||||
|
||||
plt.suptitle('Fig.6: Visit Interval Analysis', fontsize=14, fontweight='bold', y=1.02)
|
||||
plt.tight_layout()
|
||||
@@ -354,45 +502,51 @@ def fig7_sensitivity():
|
||||
df_C = pd.read_excel(SENSITIVITY_PATH, sheet_name='sensitivity_C')
|
||||
df_p = pd.read_excel(SENSITIVITY_PATH, sheet_name='sensitivity_p_thresh')
|
||||
df_cbar = pd.read_excel(SENSITIVITY_PATH, sheet_name='sensitivity_c_bar')
|
||||
df_base = pd.read_excel(SENSITIVITY_PATH, sheet_name='baseline').iloc[0]
|
||||
base_C = int(df_base['C'])
|
||||
base_p_thresh = float(df_base['p_thresh'])
|
||||
base_c_bar = float(df_base['c_bar'])
|
||||
base_E1 = float(df_base['E1'])
|
||||
base_E2 = float(df_base['E2'])
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
|
||||
|
||||
# (a) C对E1的影响
|
||||
ax1 = axes[0, 0]
|
||||
ax1.plot(df_C['C'], df_C['E1'], 'o-', color='steelblue', linewidth=2, markersize=8)
|
||||
ax1.axhline(df_C[df_C['C'] == 400]['E1'].values[0], color='red', linestyle='--', alpha=0.5, label='Baseline (C=400)')
|
||||
ax1.plot(df_C['C'], df_C['E1'], 'o-', color=TU["blue_dark"], linewidth=2, markersize=7)
|
||||
ax1.axhline(base_E1, color=TU["taupe"], linestyle='--', alpha=0.9, label=f'Baseline (C={base_C}, p={base_p_thresh:g})')
|
||||
ax1.set_xlabel('Effective Capacity (C)', fontsize=11)
|
||||
ax1.set_ylabel('E1 (Total Service Volume)', fontsize=11)
|
||||
ax1.set_title('(a) Effect of C on E1', fontsize=12)
|
||||
ax1.legend(fontsize=9)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
style_axes(ax1)
|
||||
|
||||
# (b) C对修正站点数的影响
|
||||
ax2 = axes[0, 1]
|
||||
ax2.bar(df_C['C'].astype(str), df_C['n_corrected'], color='coral', edgecolor='black', alpha=0.7)
|
||||
ax2.bar(df_C['C'].astype(str), df_C['n_corrected'], color=TU["green"], edgecolor="white", alpha=0.9, linewidth=0.7)
|
||||
ax2.set_xlabel('Effective Capacity (C)', fontsize=11)
|
||||
ax2.set_ylabel('Number of Corrected Sites', fontsize=11)
|
||||
ax2.set_title('(b) Effect of C on Correction Count', fontsize=12)
|
||||
ax2.grid(True, axis='y', alpha=0.3)
|
||||
style_axes(ax2, grid_axis="y")
|
||||
|
||||
# (c) p_thresh对指标的影响
|
||||
ax3 = axes[1, 0]
|
||||
ax3.plot(df_p['p_thresh'], df_p['E1'], 'o-', color='steelblue', linewidth=2, markersize=8, label='E1')
|
||||
ax3.plot(df_p['p_thresh'], df_p['E1'], 'o-', color=TU["teal"], linewidth=2, markersize=7, label='E1')
|
||||
ax3.set_xlabel('Truncation Threshold (p_thresh)', fontsize=11)
|
||||
ax3.set_ylabel('E1 (Total Service Volume)', fontsize=11)
|
||||
ax3.set_title('(c) Effect of p_thresh on E1', fontsize=12)
|
||||
ax3.legend(fontsize=9)
|
||||
ax3.grid(True, alpha=0.3)
|
||||
style_axes(ax3)
|
||||
|
||||
# (d) c_bar对E2的影响
|
||||
ax4 = axes[1, 1]
|
||||
ax4.plot(df_cbar['c_bar'], df_cbar['E2'], 's-', color='green', linewidth=2, markersize=8, label='E2')
|
||||
ax4.axhline(df_cbar[df_cbar['c_bar'] == 250]['E2'].values[0], color='red', linestyle='--', alpha=0.5, label='Baseline (c̄=250)')
|
||||
ax4.plot(df_cbar['c_bar'], df_cbar['E2'], 's-', color=TU["mauve"], linewidth=2, markersize=7, label='E2')
|
||||
ax4.axhline(base_E2, color=TU["taupe"], linestyle='--', alpha=0.9, label=f'Baseline (c̄={base_c_bar:g})')
|
||||
ax4.set_xlabel('Quality Threshold (c̄)', fontsize=11)
|
||||
ax4.set_ylabel('E2 (Quality-Weighted Service)', fontsize=11)
|
||||
ax4.set_title('(d) Effect of c̄ on E2', fontsize=12)
|
||||
ax4.legend(fontsize=9)
|
||||
ax4.grid(True, alpha=0.3)
|
||||
style_axes(ax4)
|
||||
|
||||
plt.suptitle('Fig.7: Sensitivity Analysis of Model Parameters', fontsize=14, fontweight='bold', y=1.02)
|
||||
plt.tight_layout()
|
||||
@@ -409,6 +563,10 @@ def main():
|
||||
|
||||
# 生成所有图表
|
||||
print("\n[1] 生成图表...")
|
||||
apply_tu_theme()
|
||||
|
||||
js_path = export_fig1_points_js()
|
||||
print(f" 已更新交互地图数据: {js_path.name}")
|
||||
|
||||
fig1_site_map()
|
||||
fig2_demand_correction()
|
||||
|
||||
@@ -542,6 +542,7 @@ $$\Delta_i^* = \frac{365}{k_i}$$
|
||||
| Fig.5 | 日历热力图 | `fig5_calendar_heatmap.png` | 全年排程可视化 |
|
||||
| Fig.6 | 访问间隔箱线图 | `fig6_gap_boxplot.png` | 间隔均匀性分析 |
|
||||
| Fig.7 | 敏感性分析 | `fig7_sensitivity.png` | C, p_thresh, c̄的影响 |
|
||||
| Fig.8 | 2019 vs 2021 对比地图(交互) | `fig8_2019_vs_2021_carto.html` | 2019实际 visits vs 2021计划(k) 及 Δ可视化 |
|
||||
|
||||
### 8.2 Fig.1: 站点地图
|
||||
|
||||
@@ -587,6 +588,12 @@ $$\Delta_i^* = \frac{365}{k_i}$$
|
||||
|
||||
---
|
||||
|
||||
### 8.9 Fig.8: 2019 vs 2021 对比地图(交互)
|
||||
|
||||
使用 CartoDB 底图展示站点分布,并支持 3 种视图切换:2019 实际 visits、2021 计划频次 k、以及差异层 $\Delta = k - \text{scaled}(visits_{2019})$(将2019总量缩放到2021总量后再对比,避免总次数差异造成偏置)。
|
||||
|
||||
打开方式:用浏览器直接打开 `task1/fig8_2019_vs_2021_carto.html`(同目录需有 `task1/fig1_points.js`)。
|
||||
|
||||
## 9. 可复现流水线
|
||||
|
||||
### 9.1 完整脚本结构
|
||||
|
||||
142
task1/fig1_carto.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Task 1 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, 255, 255, 0.92);
|
||||
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;
|
||||
}
|
||||
.legend .title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.legend .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.legend .bar {
|
||||
height: 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
/* warmer + more contrast */
|
||||
background: linear-gradient(90deg, #fce8c8, #f4a261, #e76f51, #9b2226);
|
||||
}
|
||||
.legend .muted {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
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 || [];
|
||||
if (!points.length) {
|
||||
alert("FIG1_POINTS 为空:请确认 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:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
}).addTo(map);
|
||||
|
||||
const kValues = points.map((p) => p.k);
|
||||
const muValues = points.map((p) => p.mu);
|
||||
const kMin = Math.min(...kValues);
|
||||
const kMax = Math.max(...kValues);
|
||||
const muMin = Math.min(...muValues);
|
||||
const muMax = Math.max(...muValues);
|
||||
|
||||
function clamp01(x) {
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
function lerp(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
function colorForK(k) {
|
||||
const t = clamp01((k - kMin) / (kMax - kMin || 1));
|
||||
// warm gradient: cream -> orange -> terracotta -> deep red
|
||||
const stops = [
|
||||
[252, 232, 200], // #fce8c8
|
||||
[244, 162, 97], // #f4a261
|
||||
[231, 111, 81], // #e76f51
|
||||
[155, 34, 38], // #9b2226
|
||||
];
|
||||
const n = stops.length - 1;
|
||||
const pos = t * n;
|
||||
const idx = Math.min(n - 1, Math.floor(pos));
|
||||
const tt = pos - idx;
|
||||
const from = stops[idx];
|
||||
const to = stops[idx + 1];
|
||||
const r = Math.round(lerp(from[0], to[0], tt));
|
||||
const g = Math.round(lerp(from[1], to[1], tt));
|
||||
const bl = Math.round(lerp(from[2], to[2], tt));
|
||||
return `rgb(${r},${g},${bl})`;
|
||||
}
|
||||
function radiusForMu(mu) {
|
||||
const t = clamp01((mu - muMin) / (muMax - muMin || 1));
|
||||
return lerp(5, 19, t);
|
||||
}
|
||||
|
||||
const layer = 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.95)",
|
||||
weight: 1.2,
|
||||
fillColor: colorForK(p.k),
|
||||
fillOpacity: 0.9,
|
||||
});
|
||||
marker.bindPopup(
|
||||
`<div style="font: 13px/1.3 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>k</b>: ${p.k}</div>` +
|
||||
`</div>`
|
||||
);
|
||||
layer.addLayer(marker);
|
||||
}
|
||||
layer.addTo(map);
|
||||
map.fitBounds(layer.getBounds().pad(0.12));
|
||||
|
||||
const legend = L.control({ position: "bottomleft" });
|
||||
legend.onAdd = function () {
|
||||
const div = L.DomUtil.create("div", "legend");
|
||||
div.innerHTML =
|
||||
`<div class="title">Task 1 Fig.1</div>` +
|
||||
`<div class="row"><span>颜色: k</span><span>${kMin} → ${kMax}</span></div>` +
|
||||
`<div class="bar"></div>` +
|
||||
`<div class="muted">点大小: mu(线性缩放)</div>`;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
task1/fig1_points.js
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>
|
||||
|
||||
BIN
task1/figures/fig1_carto.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
task1/figures/fig1_same_color.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 194 KiB |
BIN
task1/figures/fig8_delta.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |