Skip to content

Commit e0a1084

Browse files
dashboard
1 parent de9c443 commit e0a1084

5 files changed

Lines changed: 163 additions & 6 deletions

File tree

cybench/runs/analysis/index_map_lib.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,40 @@
2222
"EL": "GR",
2323
}
2424

25+
# Natural Earth admin-0 polygons sometimes include overseas departments under the
26+
# parent ISO (e.g. French Guiana under FR). CY-Bench countries are metropolitan;
27+
# clip to these WGS84 bounding boxes (min_lon, min_lat, max_lon, max_lat).
28+
_METROPOLITAN_BBOX_WGS84: dict[str, tuple[float, float, float, float]] = {
29+
"FR": (-5.5, 41.0, 10.0, 51.5),
30+
}
31+
_OVERSEAS_MAP_ISO = "XX"
32+
33+
34+
def _explode_metropolitan_map_units(frame: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
35+
"""Explode multipolygons; re-tag overseas parts so they are not benchmark-colored."""
36+
if frame.empty:
37+
return frame
38+
exploded = frame.explode(index_parts=True).reset_index(drop=True)
39+
if not _METROPOLITAN_BBOX_WGS84:
40+
return exploded
41+
42+
iso = exploded["ISO_A2"].astype(str)
43+
centroids = exploded.geometry.representative_point()
44+
for map_iso, (min_lon, min_lat, max_lon, max_lat) in _METROPOLITAN_BBOX_WGS84.items():
45+
parent = iso == map_iso
46+
if not parent.any():
47+
continue
48+
inside = (
49+
(centroids.x >= min_lon)
50+
& (centroids.x <= max_lon)
51+
& (centroids.y >= min_lat)
52+
& (centroids.y <= max_lat)
53+
)
54+
overseas = parent & ~inside
55+
if overseas.any():
56+
exploded.loc[overseas, "ISO_A2"] = _OVERSEAS_MAP_ISO
57+
return exploded
58+
2559

2660
def map_iso_for_cybencH(cc: str) -> str:
2761
key = cc.upper()
@@ -124,6 +158,7 @@ def export_world_geojson(dest: Path, *, simplify: float = 0.08) -> Path:
124158
keep["ISO_A2"] = iso
125159
keep = keep[keep["ISO_A2"].notna() & (keep["ISO_A2"] != "-99") & (keep["ISO_A2"] != "nan")]
126160
keep["geometry"] = keep.geometry.simplify(simplify, preserve_topology=True)
161+
keep = _explode_metropolitan_map_units(keep)
127162
dest.parent.mkdir(parents=True, exist_ok=True)
128163
keep.to_file(dest, driver="GeoJSON")
129164
return dest

cybench/runs/analysis/model_family_radar_lib.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,8 @@ def build_radar_payload(
822822
"benchmark_map_isos": benchmark_map_isos,
823823
"map_coverage_note": (
824824
"Only CY-Bench countries are colored; all other land is neutral gray. "
825-
"ISO country polygons are used as-is (e.g. the United States outline includes Alaska)."
825+
"ISO country polygons are used as-is (e.g. the United States outline includes Alaska). "
826+
"France is metropolitan only (French Guiana is not colored)."
826827
),
827828
"crops": crops,
828829
"views": list(EVALUATION_VIEWS),

cybench/runs/viz/model_family_radar_template.html

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,13 @@
188188
border: 1px solid var(--border);
189189
background: linear-gradient(to right, #d73027, #fee08b, #1a9850);
190190
}
191-
.map-export-toolbar {
191+
.export-toolbar {
192192
display: flex;
193193
flex-wrap: wrap;
194194
gap: 0.45rem;
195195
margin-top: 0.35rem;
196196
}
197-
.map-export-toolbar button {
197+
.export-toolbar button {
198198
font: inherit;
199199
font-size: 0.85rem;
200200
padding: 0.35rem 0.65rem;
@@ -204,11 +204,11 @@
204204
color: var(--text);
205205
cursor: pointer;
206206
}
207-
.map-export-toolbar button:hover:not(:disabled) {
207+
.export-toolbar button:hover:not(:disabled) {
208208
border-color: var(--accent);
209209
color: var(--accent);
210210
}
211-
.map-export-toolbar button:disabled {
211+
.export-toolbar button:disabled {
212212
opacity: 0.45;
213213
cursor: not-allowed;
214214
}
@@ -261,6 +261,10 @@ <h2>Median across countries (family representatives)</h2>
261261
<tbody></tbody>
262262
</table>
263263
</div>
264+
<div class="export-toolbar">
265+
<button type="button" id="table-export-latex">Download table (LaTeX)</button>
266+
<button type="button" id="table-export-latex-copy">Copy LaTeX</button>
267+
</div>
264268
</div>
265269

266270
<div class="card">
@@ -283,10 +287,11 @@ <h2>World map</h2>
283287
<p class="muted" id="map-mode-note">Colors show which model family wins in each country for the selected evaluation aspect.</p>
284288
<p class="muted" id="map-coverage-note"></p>
285289
<div class="world-map-wrap" id="winner-map-wrap"><p class="muted">Loading map…</p></div>
286-
<div class="map-export-toolbar">
290+
<div class="export-toolbar">
287291
<button type="button" id="map-export-svg" disabled>Download map (SVG)</button>
288292
<button type="button" id="map-export-png" disabled>Download map (PNG)</button>
289293
</div>
294+
<p class="muted">For LaTeX figures: use PNG with <code>\includegraphics</code>, or convert SVG to PDF (Inkscape) for vector output — pdfLaTeX does not embed SVG directly.</p>
290295
<div class="winner-legend" id="winner-legend"></div>
291296
<div class="benefit-legend" id="benefit-legend" style="display:none;">
292297
<span class="benefit-gradient" aria-hidden="true"></span>
@@ -322,6 +327,10 @@ <h2>World map</h2>
322327
const mapCoverageNote = document.getElementById("map-coverage-note");
323328
const mapExportSvgBtn = document.getElementById("map-export-svg");
324329
const mapExportPngBtn = document.getElementById("map-export-png");
330+
const tableExportLatexBtn = document.getElementById("table-export-latex");
331+
const tableExportLatexCopyBtn = document.getElementById("table-export-latex-copy");
332+
333+
let currentTableFamilies = [];
325334

326335
const MAP_FILL_OUTSIDE = "#e8eaed";
327336
const MAP_FILL_NO_DATA = "#f5f5f5";
@@ -725,7 +734,100 @@ <h2>World map</h2>
725734
return `${Number(raw).toFixed(3)} [${Number(band.q25).toFixed(3)}, ${Number(band.q75).toFixed(3)}]`;
726735
}
727736

737+
function escapeLatex(text) {
738+
return String(text)
739+
.replace(/\\/g, "\\textbackslash{}")
740+
.replace(/[&%$#_{}]/g, ch => `\\${ch}`)
741+
.replace(/~/g, "\\textasciitilde{}")
742+
.replace(/\^/g, "\\textasciicaret{}");
743+
}
744+
745+
function latexViewHeader(view) {
746+
const metric = view.display || view.metric;
747+
if (metric === "r") return `${view.label} ($r$)`;
748+
if (metric === "NRMSE") return `${view.label} (NRMSE)`;
749+
return `${view.label} (${metric})`;
750+
}
751+
752+
function formatMetricLatex(fam, metric) {
753+
const raw = fam.raw[metric];
754+
if (raw == null) return "---";
755+
const band = (fam.iqr || {})[metric];
756+
const med = Number(raw).toFixed(3);
757+
if (!band || band.q25 == null || band.q75 == null) return med;
758+
return `${med} [${Number(band.q25).toFixed(3)}, ${Number(band.q75).toFixed(3)}]`;
759+
}
760+
761+
function tableExportMeta() {
762+
const hz = horizonSelect.value;
763+
const crop = cropSelect.value;
764+
const hzLabel = hz === "eos" ? "end-of-season" : hz === "mid" ? "mid-season" : hz;
765+
const cropLabel = crop === "all" ? "all crops" : crop;
766+
return { hz, crop, hzLabel, cropLabel };
767+
}
768+
769+
function tableExportFilename() {
770+
const { hz, crop } = tableExportMeta();
771+
return `model_families_table_${hz}_${crop}.tex`;
772+
}
773+
774+
function buildMetricsTableLatex(families) {
775+
if (!families.length) return "";
776+
const { hzLabel, cropLabel } = tableExportMeta();
777+
const headers = ["Family", "Representative", ...DATA.views.map(latexViewHeader)];
778+
const colSpec = `ll${"r".repeat(DATA.views.length)}`;
779+
const headerRow = headers.map(escapeLatex).join(" & ");
780+
const bodyRows = families.map(fam => {
781+
const cells = [
782+
escapeLatex(fam.family),
783+
escapeLatex(fam.display_name),
784+
...DATA.views.map(v => formatMetricLatex(fam, v.metric)),
785+
];
786+
return ` ${cells.join(" & ")} \\\\`;
787+
}).join("\n");
788+
return `% CY-Bench model families table (generated from dashboard)
789+
% Requires: \\usepackage{booktabs}
790+
\\begin{table}[ht]
791+
\\centering
792+
\\caption{Model family performance: median [IQR] across countries. Horizon: ${escapeLatex(hzLabel)}; crops: ${escapeLatex(cropLabel)}.}
793+
\\label{tab:cybench-model-families}
794+
\\begin{tabular}{${colSpec}}
795+
\\toprule
796+
${headerRow} \\\\
797+
\\midrule
798+
${bodyRows}
799+
\\bottomrule
800+
\\end{tabular}
801+
\\end{table}
802+
`;
803+
}
804+
805+
function downloadMetricsTableLatex() {
806+
const latex = buildMetricsTableLatex(currentTableFamilies);
807+
if (!latex) return;
808+
triggerBlobDownload(
809+
new Blob([latex], { type: "text/plain;charset=utf-8" }),
810+
tableExportFilename(),
811+
);
812+
}
813+
814+
async function copyMetricsTableLatex() {
815+
const latex = buildMetricsTableLatex(currentTableFamilies);
816+
if (!latex || !tableExportLatexCopyBtn) return;
817+
const original = tableExportLatexCopyBtn.textContent;
818+
try {
819+
await navigator.clipboard.writeText(latex);
820+
tableExportLatexCopyBtn.textContent = "Copied!";
821+
setTimeout(() => { tableExportLatexCopyBtn.textContent = original; }, 1600);
822+
} catch (_err) {
823+
downloadMetricsTableLatex();
824+
tableExportLatexCopyBtn.textContent = "Downloaded instead";
825+
setTimeout(() => { tableExportLatexCopyBtn.textContent = original; }, 1600);
826+
}
827+
}
828+
728829
function renderTable(families) {
830+
currentTableFamilies = families;
729831
const viewHeaders = DATA.views.map(v => `${v.label} (${v.display || v.metric})`);
730832
const headers = ["Family", "Representative", ...viewHeaders];
731833
metricsTable.querySelector("thead").innerHTML =
@@ -1049,6 +1151,8 @@ <h2>World map</h2>
10491151

10501152
if (mapExportSvgBtn) mapExportSvgBtn.addEventListener("click", downloadMapSvg);
10511153
if (mapExportPngBtn) mapExportPngBtn.addEventListener("click", downloadMapPng);
1154+
if (tableExportLatexBtn) tableExportLatexBtn.addEventListener("click", downloadMetricsTableLatex);
1155+
if (tableExportLatexCopyBtn) tableExportLatexCopyBtn.addEventListener("click", copyMetricsTableLatex);
10521156

10531157
function loadWinnerMapGeometry() {
10541158
if (!DATA.geojson_href) {

tests/runs/test_index_map.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ def test_export_world_geojson_includes_france(tmp_path: Path):
6464
text = dest.read_text(encoding="utf-8")
6565
assert '"ISO_A2": "FR"' in text or '"ISO_A2":"FR"' in text
6666

67+
import geopandas as gpd
68+
69+
world = gpd.read_file(dest)
70+
fr = world[world["ISO_A2"] == "FR"]
71+
assert not fr.empty
72+
for _, row in fr.iterrows():
73+
c = row.geometry.centroid
74+
assert c.y > 30, f"FR polygon should be metropolitan Europe, got lat={c.y}"
75+
assert c.x > -15, f"FR polygon should not be in South America, got lon={c.x}"
76+
# French Guiana should be detached from FR (neutral gray on the map).
77+
overseas = world[world["ISO_A2"] == "XX"]
78+
assert not overseas.empty
79+
assert any(row.geometry.centroid.x < -30 for _, row in overseas.iterrows())
80+
6781

6882
def test_build_index_map_payload(tmp_path: Path):
6983
(tmp_path / "insights.html").write_text("<html></html>", encoding="utf-8")

tests/runs/test_model_family_radar.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,6 @@ def test_build_radar_html_embeds_payload(tmp_path: Path):
377377
assert "data-mode=\"benefit\"" in html
378378
assert 'id="map-export-svg"' in html
379379
assert 'id="map-export-png"' in html
380+
assert 'id="table-export-latex"' in html
381+
assert "buildMetricsTableLatex" in html
382+
assert "booktabs" in html

0 commit comments

Comments
 (0)