add d3-two-lines-chart
Browse files
app/src/content/embeds/d3-two-lines-chart.html
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Two Line Charts Side-by-Side
|
| 3 |
+
|
| 4 |
+
A configurable side-by-side display of two line charts with zoom/pan, smoothing, and hover tooltips.
|
| 5 |
+
Designed to replace Plotly charts showing comparative metrics (e.g., Good vs Bad examples).
|
| 6 |
+
|
| 7 |
+
Configuration via data-config attribute:
|
| 8 |
+
{
|
| 9 |
+
"charts": [
|
| 10 |
+
{
|
| 11 |
+
"title": "✅ Good Example",
|
| 12 |
+
"language": "French",
|
| 13 |
+
"task": "mlmm_hellaswag_fra_cf",
|
| 14 |
+
"metric": "acc_norm_token"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"title": "❌ Bad Example",
|
| 18 |
+
"language": "Arabic",
|
| 19 |
+
"task": "mlmm_truthfulqa_ara_cf:mc1",
|
| 20 |
+
"metric": "acc_norm_token"
|
| 21 |
+
}
|
| 22 |
+
],
|
| 23 |
+
"statLabel": "Monotonicity", // Label for the stat value (e.g., "Monotonicity", "SNR", etc.)
|
| 24 |
+
"groupSeeds": true, // If false, show each seed separately; if true, group by runname and average
|
| 25 |
+
"smoothingWindow": 3,
|
| 26 |
+
"smoothingCurve": "monotoneX",
|
| 27 |
+
"xAxisLabel": "Tokens (B)",
|
| 28 |
+
"yAxisLabel": "Score",
|
| 29 |
+
"baseUrl": "./finetasks/data"
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
CSV format expected: run_name, tokens, score, ...
|
| 33 |
+
|
| 34 |
+
Example usage in MDX:
|
| 35 |
+
<HtmlEmbed
|
| 36 |
+
src="embeds/d3-two-lines-chart.html"
|
| 37 |
+
config={{
|
| 38 |
+
charts: [
|
| 39 |
+
{ title: "✅ Good", language: "French", task: "mlmm_hellaswag_fra_cf", metric: "acc_norm_token" },
|
| 40 |
+
{ title: "❌ Bad", language: "Arabic", task: "mlmm_truthfulqa_ara_cf:mc1", metric: "acc_norm_token" }
|
| 41 |
+
]
|
| 42 |
+
}}
|
| 43 |
+
/>
|
| 44 |
+
-->
|
| 45 |
+
<div class="d3-two-charts"></div>
|
| 46 |
+
<style>
|
| 47 |
+
.d3-two-charts {
|
| 48 |
+
position: relative;
|
| 49 |
+
width: 100%;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* Flex container like Plotly */
|
| 53 |
+
.d3-two-charts__grid {
|
| 54 |
+
display: flex;
|
| 55 |
+
gap: 20px;
|
| 56 |
+
flex-wrap: wrap;
|
| 57 |
+
width: 100%;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.chart-cell {
|
| 61 |
+
display: flex;
|
| 62 |
+
flex-direction: column;
|
| 63 |
+
position: relative;
|
| 64 |
+
padding: 16px;
|
| 65 |
+
box-shadow: inset 0 0 0 1px var(--border-color);
|
| 66 |
+
border-radius: 8px;
|
| 67 |
+
background: var(--surface-bg, #fff);
|
| 68 |
+
flex: 1;
|
| 69 |
+
min-width: 300px;
|
| 70 |
+
box-sizing: border-box;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.chart-cell__title {
|
| 74 |
+
font-size: 14px;
|
| 75 |
+
font-weight: 700;
|
| 76 |
+
color: var(--text-color);
|
| 77 |
+
margin-bottom: 12px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.chart-cell__stat {
|
| 81 |
+
margin-top: 16px;
|
| 82 |
+
padding: 10px 12px;
|
| 83 |
+
background: var(--page-bg, #f9fafb);
|
| 84 |
+
border-radius: 6px;
|
| 85 |
+
text-align: center;
|
| 86 |
+
font-size: 13px;
|
| 87 |
+
color: var(--text-color);
|
| 88 |
+
border: 1px solid var(--border-color, #e5e7eb);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.chart-cell__stat-label {
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
margin-right: 6px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.chart-cell__stat-value {
|
| 97 |
+
font-weight: 700;
|
| 98 |
+
font-size: 15px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.chart-cell__body {
|
| 102 |
+
position: relative;
|
| 103 |
+
width: 100%;
|
| 104 |
+
overflow: hidden;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.chart-cell__body svg {
|
| 108 |
+
max-width: 100%;
|
| 109 |
+
height: auto;
|
| 110 |
+
display: block;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Legend */
|
| 114 |
+
.chart-cell__legend {
|
| 115 |
+
display: flex;
|
| 116 |
+
flex-wrap: wrap;
|
| 117 |
+
gap: 8px 14px;
|
| 118 |
+
justify-content: center;
|
| 119 |
+
margin-top: 12px;
|
| 120 |
+
font-size: 11px;
|
| 121 |
+
color: var(--text-color);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.chart-cell__legend .item {
|
| 125 |
+
display: inline-flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
gap: 6px;
|
| 128 |
+
white-space: nowrap;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.chart-cell__legend .swatch {
|
| 133 |
+
width: 12px;
|
| 134 |
+
height: 12px;
|
| 135 |
+
border-radius: 2px;
|
| 136 |
+
border: 1px solid var(--border-color);
|
| 137 |
+
display: inline-block;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Reset button */
|
| 141 |
+
.chart-cell .reset-button {
|
| 142 |
+
position: absolute;
|
| 143 |
+
top: 16px;
|
| 144 |
+
right: 16px;
|
| 145 |
+
z-index: 10;
|
| 146 |
+
display: none;
|
| 147 |
+
opacity: 0;
|
| 148 |
+
transition: opacity 0.2s ease;
|
| 149 |
+
font-size: 11px;
|
| 150 |
+
padding: 4px 8px;
|
| 151 |
+
border-radius: 4px;
|
| 152 |
+
background: var(--surface-bg);
|
| 153 |
+
color: var(--text-color);
|
| 154 |
+
border: 1px solid var(--border-color);
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.chart-cell .reset-button:hover {
|
| 159 |
+
background: var(--page-bg);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* Axes */
|
| 163 |
+
.d3-two-charts .axes path.domain {
|
| 164 |
+
stroke: var(--axis-color, #e5e7eb);
|
| 165 |
+
stroke-width: 1;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.d3-two-charts .axes line {
|
| 169 |
+
stroke: var(--axis-color, #e5e7eb);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.d3-two-charts .axes text {
|
| 173 |
+
fill: var(--tick-color, #6b7280);
|
| 174 |
+
font-size: 10px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.d3-two-charts .axis-label {
|
| 178 |
+
fill: var(--text-color);
|
| 179 |
+
font-size: 10px;
|
| 180 |
+
font-weight: 300;
|
| 181 |
+
opacity: 0.7;
|
| 182 |
+
stroke: var(--page-bg, white);
|
| 183 |
+
stroke-width: 3px;
|
| 184 |
+
paint-order: stroke fill;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.d3-two-charts .grid line {
|
| 188 |
+
stroke: var(--grid-color, #f3f4f6);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* Lines */
|
| 192 |
+
.d3-two-charts path.main-line {
|
| 193 |
+
transition: opacity 0.2s ease;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.d3-two-charts path.ghost-line {
|
| 197 |
+
transition: opacity 0.6s ease;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Ghosting on hover */
|
| 201 |
+
.d3-two-charts.hovering path.main-line.ghost {
|
| 202 |
+
opacity: .25;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.d3-two-charts.hovering path.ghost-line.ghost {
|
| 206 |
+
opacity: .05;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.d3-two-charts.hovering .chart-cell__legend .item.ghost {
|
| 210 |
+
opacity: .35;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* Tooltip */
|
| 214 |
+
.d3-two-charts .d3-tooltip {
|
| 215 |
+
z-index: 20;
|
| 216 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.d3-two-charts .d3-tooltip__inner {
|
| 220 |
+
display: flex;
|
| 221 |
+
flex-direction: column;
|
| 222 |
+
gap: 6px;
|
| 223 |
+
min-width: 180px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.d3-two-charts .d3-tooltip__inner>div:first-child {
|
| 227 |
+
font-weight: 800;
|
| 228 |
+
letter-spacing: 0.1px;
|
| 229 |
+
margin-bottom: 0;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.d3-two-charts .d3-tooltip__inner>div:nth-child(2) {
|
| 233 |
+
font-size: 11px;
|
| 234 |
+
color: var(--muted-color, #9ca3af);
|
| 235 |
+
display: block;
|
| 236 |
+
margin-top: -4px;
|
| 237 |
+
margin-bottom: 2px;
|
| 238 |
+
letter-spacing: 0.1px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.d3-two-charts .d3-tooltip__inner>div:nth-child(n+3) {
|
| 242 |
+
padding-top: 6px;
|
| 243 |
+
border-top: 1px solid var(--border-color);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.d3-two-charts .d3-tooltip__color-dot {
|
| 247 |
+
display: inline-block;
|
| 248 |
+
width: 12px;
|
| 249 |
+
height: 12px;
|
| 250 |
+
border-radius: 3px;
|
| 251 |
+
border: 1px solid var(--border-color);
|
| 252 |
+
}
|
| 253 |
+
</style>
|
| 254 |
+
<script>
|
| 255 |
+
(() => {
|
| 256 |
+
const ensureD3 = (cb) => {
|
| 257 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 258 |
+
let s = document.getElementById('d3-cdn-script');
|
| 259 |
+
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 260 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 261 |
+
s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
const bootstrap = () => {
|
| 265 |
+
const scriptEl = document.currentScript;
|
| 266 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 267 |
+
if (!(container && container.classList && container.classList.contains('d3-two-charts'))) {
|
| 268 |
+
const cs = Array.from(document.querySelectorAll('.d3-two-charts')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 269 |
+
container = cs[cs.length - 1] || null;
|
| 270 |
+
}
|
| 271 |
+
if (!container) return;
|
| 272 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 273 |
+
|
| 274 |
+
const d3 = window.d3;
|
| 275 |
+
|
| 276 |
+
// Language mapping
|
| 277 |
+
const languageMap = {
|
| 278 |
+
'Arabic': 'ar', 'Turkish': 'tr', 'Swahili': 'sw', 'Russian': 'ru',
|
| 279 |
+
'Telugu': 'te', 'Thai': 'th', 'Chinese': 'zh', 'French': 'fr', 'Hindi': 'hi'
|
| 280 |
+
};
|
| 281 |
+
|
| 282 |
+
// Run name mapping (same as Plotly version)
|
| 283 |
+
const runNameMap = {
|
| 284 |
+
"orion": "Dataset-A",
|
| 285 |
+
"helios": "Dataset-B",
|
| 286 |
+
"lynx": "Dataset-C",
|
| 287 |
+
"aquila": "Dataset-D",
|
| 288 |
+
"commoncrawl": "CommonCrawl",
|
| 289 |
+
"baseline": "Baseline"
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
function processRunName(runname) {
|
| 293 |
+
if (!runname || typeof runname !== 'string') {
|
| 294 |
+
return String(runname || 'Unknown');
|
| 295 |
+
}
|
| 296 |
+
for (const [key, value] of Object.entries(runNameMap)) {
|
| 297 |
+
if (runname.toLowerCase().includes(key.toLowerCase())) {
|
| 298 |
+
return value;
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
return runname;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Read config from HtmlEmbed props
|
| 305 |
+
function readEmbedConfig() {
|
| 306 |
+
let mountEl = container;
|
| 307 |
+
while (mountEl && !mountEl.getAttribute?.('data-config')) {
|
| 308 |
+
mountEl = mountEl.parentElement;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
let providedConfig = null;
|
| 312 |
+
try {
|
| 313 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 314 |
+
if (cfg && cfg.trim()) {
|
| 315 |
+
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 316 |
+
}
|
| 317 |
+
} catch (e) {
|
| 318 |
+
console.error('Failed to parse data-config', e);
|
| 319 |
+
}
|
| 320 |
+
return providedConfig || {};
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
const embedConfig = readEmbedConfig();
|
| 324 |
+
|
| 325 |
+
// Configuration
|
| 326 |
+
const CONFIG = {
|
| 327 |
+
charts: embedConfig.charts || [],
|
| 328 |
+
smoothing: embedConfig.smoothing !== undefined ? embedConfig.smoothing : false,
|
| 329 |
+
smoothingWindow: embedConfig.smoothingWindow || 3,
|
| 330 |
+
smoothingCurve: embedConfig.smoothingCurve || 'monotoneX',
|
| 331 |
+
chartHeight: 280,
|
| 332 |
+
margin: { top: 20, right: 20, bottom: 40, left: 50 },
|
| 333 |
+
zoomExtent: [1.0, 8],
|
| 334 |
+
xAxisLabel: embedConfig.xAxisLabel || 'Tokens (B)',
|
| 335 |
+
yAxisLabel: embedConfig.yAxisLabel || 'Score',
|
| 336 |
+
baseUrl: embedConfig.baseUrl || './finetasks/data',
|
| 337 |
+
statLabel: embedConfig.statLabel || null, // e.g., "Monotonicity", "SNR", "Randomness", etc.
|
| 338 |
+
statColumn: embedConfig.statColumn || 'avg_spearman', // Column name in stats CSV
|
| 339 |
+
groupSeeds: embedConfig.groupSeeds !== undefined ? embedConfig.groupSeeds : true // Group seeds or show separately
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
// Mapping from stat label to column name (for convenience)
|
| 343 |
+
const statColumnMap = {
|
| 344 |
+
'Monotonicity': 'avg_spearman',
|
| 345 |
+
'SNR': 'avg_snr',
|
| 346 |
+
'Kendall\'s Tau': 'avg_kendall_tau_a',
|
| 347 |
+
'Distance from baseline': 'max_n_std', // Non-Randomness metric
|
| 348 |
+
'Non-Randomness': 'max_n_std',
|
| 349 |
+
'Randomness': 'max_n_std'
|
| 350 |
+
};
|
| 351 |
+
|
| 352 |
+
// Auto-detect column from label if not specified
|
| 353 |
+
if (CONFIG.statLabel && !embedConfig.statColumn && statColumnMap[CONFIG.statLabel]) {
|
| 354 |
+
CONFIG.statColumn = statColumnMap[CONFIG.statLabel];
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
if (!CONFIG.charts.length || CONFIG.charts.length !== 2) {
|
| 358 |
+
container.innerHTML = '<p style="color: var(--danger); font-size: 12px;">Error: Exactly 2 charts must be configured</p>';
|
| 359 |
+
return;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// Create grid
|
| 363 |
+
const grid = document.createElement('div');
|
| 364 |
+
grid.className = 'd3-two-charts__grid';
|
| 365 |
+
container.appendChild(grid);
|
| 366 |
+
|
| 367 |
+
// Create chart cells
|
| 368 |
+
CONFIG.charts.forEach((chartConfig, idx) => {
|
| 369 |
+
const cell = document.createElement('div');
|
| 370 |
+
cell.className = 'chart-cell';
|
| 371 |
+
cell.innerHTML = `
|
| 372 |
+
<div class="chart-cell__title">${chartConfig.title}</div>
|
| 373 |
+
<button class="reset-button">Reset</button>
|
| 374 |
+
<div class="chart-cell__body"></div>
|
| 375 |
+
<div class="chart-cell__legend"></div>
|
| 376 |
+
<div class="chart-cell__stat"></div>
|
| 377 |
+
`;
|
| 378 |
+
grid.appendChild(cell);
|
| 379 |
+
});
|
| 380 |
+
|
| 381 |
+
// Smoothing
|
| 382 |
+
const getCurve = (smooth) => {
|
| 383 |
+
if (!smooth) return d3.curveLinear;
|
| 384 |
+
switch (CONFIG.smoothingCurve) {
|
| 385 |
+
case 'catmullRom': return d3.curveCatmullRom.alpha(0.5);
|
| 386 |
+
case 'monotoneX': return d3.curveMonotoneX;
|
| 387 |
+
case 'basis': return d3.curveBasis;
|
| 388 |
+
default: return d3.curveLinear;
|
| 389 |
+
}
|
| 390 |
+
};
|
| 391 |
+
|
| 392 |
+
function movingAverage(values, windowSize) {
|
| 393 |
+
if (!Array.isArray(values) || values.length === 0 || windowSize <= 1) return values;
|
| 394 |
+
const half = Math.floor(windowSize / 2);
|
| 395 |
+
const out = new Array(values.length);
|
| 396 |
+
for (let i = 0; i < values.length; i++) {
|
| 397 |
+
let sum = 0; let count = 0;
|
| 398 |
+
const start = Math.max(0, i - half);
|
| 399 |
+
const end = Math.min(values.length - 1, i + half);
|
| 400 |
+
for (let j = start; j <= end; j++) { if (!Number.isNaN(values[j].value)) { sum += values[j].value; count++; } }
|
| 401 |
+
const avg = count ? (sum / count) : values[i].value;
|
| 402 |
+
out[i] = { step: values[i].step, value: avg };
|
| 403 |
+
}
|
| 404 |
+
return out;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
function applySmoothing(values, smooth) {
|
| 408 |
+
if (!smooth) return values;
|
| 409 |
+
return movingAverage(values, CONFIG.smoothingWindow);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Smart formatter
|
| 413 |
+
function createSmartFormatter(values) {
|
| 414 |
+
if (!values || values.length === 0) return (v) => v;
|
| 415 |
+
|
| 416 |
+
const min = d3.min(values);
|
| 417 |
+
const max = d3.max(values);
|
| 418 |
+
const range = max - min;
|
| 419 |
+
|
| 420 |
+
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 421 |
+
|
| 422 |
+
if (max >= 1e9) {
|
| 423 |
+
return (v) => {
|
| 424 |
+
const billions = v / 1e9;
|
| 425 |
+
return allIntegers && billions === Math.round(billions)
|
| 426 |
+
? d3.format('d')(Math.round(billions)) + 'B'
|
| 427 |
+
: d3.format('.2f')(billions) + 'B';
|
| 428 |
+
};
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
if (max >= 1e6) {
|
| 432 |
+
return (v) => {
|
| 433 |
+
const millions = v / 1e6;
|
| 434 |
+
return allIntegers && millions === Math.round(millions)
|
| 435 |
+
? d3.format('d')(Math.round(millions)) + 'M'
|
| 436 |
+
: d3.format('.2f')(millions) + 'M';
|
| 437 |
+
};
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
if (max >= 1000 && range >= 100) {
|
| 441 |
+
return (v) => {
|
| 442 |
+
const thousands = v / 1000;
|
| 443 |
+
return allIntegers && thousands === Math.round(thousands)
|
| 444 |
+
? d3.format('d')(Math.round(thousands)) + 'k'
|
| 445 |
+
: d3.format('.1f')(thousands) + 'k';
|
| 446 |
+
};
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
if (allIntegers) {
|
| 450 |
+
return (v) => d3.format('d')(Math.round(v));
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
if (range < 1) {
|
| 454 |
+
return (v) => d3.format('.3f')(v);
|
| 455 |
+
} else if (range < 10) {
|
| 456 |
+
return (v) => d3.format('.2f')(v);
|
| 457 |
+
} else {
|
| 458 |
+
return (v) => d3.format('.1f')(v);
|
| 459 |
+
}
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
// Colors
|
| 463 |
+
const getRunColors = (n) => {
|
| 464 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch (_) { }
|
| 465 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 466 |
+
return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', '#9B59B6', '#16A085', ...(d3.schemeTableau10 || [])].slice(0, n);
|
| 467 |
+
};
|
| 468 |
+
|
| 469 |
+
// Init each chart
|
| 470 |
+
function initChart(cellElement, chartConfig) {
|
| 471 |
+
const bodyEl = cellElement.querySelector('.chart-cell__body');
|
| 472 |
+
const resetBtn = cellElement.querySelector('.reset-button');
|
| 473 |
+
const legendEl = cellElement.querySelector('.chart-cell__legend');
|
| 474 |
+
const statEl = cellElement.querySelector('.chart-cell__stat');
|
| 475 |
+
|
| 476 |
+
let smoothEnabled = CONFIG.smoothing;
|
| 477 |
+
let hasMoved = false;
|
| 478 |
+
let allData = [];
|
| 479 |
+
let runList = [];
|
| 480 |
+
let runColorMap = {};
|
| 481 |
+
let baseline = null;
|
| 482 |
+
let monotonicity = null;
|
| 483 |
+
|
| 484 |
+
// Tooltip
|
| 485 |
+
let tip = cellElement.querySelector('.d3-tooltip');
|
| 486 |
+
let tipInner;
|
| 487 |
+
if (!tip) {
|
| 488 |
+
tip = document.createElement('div');
|
| 489 |
+
tip.className = 'd3-tooltip';
|
| 490 |
+
Object.assign(tip.style, {
|
| 491 |
+
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
|
| 492 |
+
padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
|
| 493 |
+
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', zIndex: '20'
|
| 494 |
+
});
|
| 495 |
+
tipInner = document.createElement('div');
|
| 496 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 497 |
+
tip.appendChild(tipInner);
|
| 498 |
+
cellElement.appendChild(tip);
|
| 499 |
+
} else {
|
| 500 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
// Create SVG
|
| 504 |
+
const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block');
|
| 505 |
+
|
| 506 |
+
// Clip path
|
| 507 |
+
const clipId = 'clip-' + Math.random().toString(36).slice(2);
|
| 508 |
+
const clipPath = svg.append('defs').append('clipPath').attr('id', clipId);
|
| 509 |
+
const clipRect = clipPath.append('rect');
|
| 510 |
+
|
| 511 |
+
// Groups
|
| 512 |
+
const g = svg.append('g');
|
| 513 |
+
const gGrid = g.append('g').attr('class', 'grid');
|
| 514 |
+
const gAxes = g.append('g').attr('class', 'axes');
|
| 515 |
+
const gPlot = g.append('g').attr('class', 'plot').attr('clip-path', `url(#${clipId})`);
|
| 516 |
+
const gHover = g.append('g').attr('class', 'hover-layer');
|
| 517 |
+
const overlay = g.append('rect').attr('class', 'overlay').attr('fill', 'none').attr('pointer-events', 'all').style('cursor', 'grab')
|
| 518 |
+
.on('mousedown', function () {
|
| 519 |
+
d3.select(this).style('cursor', 'grabbing');
|
| 520 |
+
tip.style.opacity = '0';
|
| 521 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 522 |
+
})
|
| 523 |
+
.on('mouseup', function () { d3.select(this).style('cursor', 'grab'); });
|
| 524 |
+
|
| 525 |
+
// Scales
|
| 526 |
+
const xScale = d3.scaleLinear();
|
| 527 |
+
const yScale = d3.scaleLinear();
|
| 528 |
+
|
| 529 |
+
// Hover state
|
| 530 |
+
let hoverLine = null;
|
| 531 |
+
let steps = [];
|
| 532 |
+
let hideTipTimer = null;
|
| 533 |
+
|
| 534 |
+
// Formatters
|
| 535 |
+
let formatStep = (v) => v;
|
| 536 |
+
let formatValue = (v) => v;
|
| 537 |
+
|
| 538 |
+
// Zoom
|
| 539 |
+
const zoom = d3.zoom().scaleExtent(CONFIG.zoomExtent).on('zoom', zoomed);
|
| 540 |
+
overlay.call(zoom);
|
| 541 |
+
|
| 542 |
+
function zoomed(event) {
|
| 543 |
+
const transform = event.transform;
|
| 544 |
+
hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
|
| 545 |
+
updateResetButton();
|
| 546 |
+
|
| 547 |
+
const newXScale = transform.rescaleX(xScale);
|
| 548 |
+
const newYScale = transform.rescaleY(yScale);
|
| 549 |
+
|
| 550 |
+
const innerWidth = xScale.range()[1];
|
| 551 |
+
|
| 552 |
+
// Update grid
|
| 553 |
+
const gridTicks = newYScale.ticks(5);
|
| 554 |
+
gGrid.selectAll('line').data(gridTicks).join('line')
|
| 555 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 556 |
+
.attr('y1', d => newYScale(d)).attr('y2', d => newYScale(d))
|
| 557 |
+
.attr('stroke', 'var(--grid-color)');
|
| 558 |
+
|
| 559 |
+
// Update lines
|
| 560 |
+
const line = d3.line()
|
| 561 |
+
.x(d => newXScale(d.step))
|
| 562 |
+
.y(d => newYScale(d.value))
|
| 563 |
+
.curve(getCurve(smoothEnabled));
|
| 564 |
+
|
| 565 |
+
gPlot.selectAll('path.ghost-line')
|
| 566 |
+
.attr('d', d => {
|
| 567 |
+
const rawLine = d3.line().x(d => newXScale(d.step)).y(d => newYScale(d.value)).curve(d3.curveLinear);
|
| 568 |
+
return rawLine(d.values);
|
| 569 |
+
});
|
| 570 |
+
|
| 571 |
+
gPlot.selectAll('path.main-line')
|
| 572 |
+
.attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
|
| 573 |
+
|
| 574 |
+
// Update axes
|
| 575 |
+
gAxes.select('.x-axis').call(d3.axisBottom(newXScale).ticks(5).tickSizeOuter(0).tickFormat(formatStep));
|
| 576 |
+
gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatValue));
|
| 577 |
+
|
| 578 |
+
// Update baseline position
|
| 579 |
+
if (baseline !== null) {
|
| 580 |
+
gAxes.select('.baseline-line')
|
| 581 |
+
.attr('y1', newYScale(baseline))
|
| 582 |
+
.attr('y2', newYScale(baseline));
|
| 583 |
+
gAxes.select('.baseline-label')
|
| 584 |
+
.attr('y', newYScale(baseline) - 5);
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
function updateResetButton() {
|
| 589 |
+
if (hasMoved) {
|
| 590 |
+
resetBtn.style.display = 'block';
|
| 591 |
+
requestAnimationFrame(() => { resetBtn.style.opacity = '1'; });
|
| 592 |
+
} else {
|
| 593 |
+
resetBtn.style.opacity = '0';
|
| 594 |
+
setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200);
|
| 595 |
+
}
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
function render() {
|
| 599 |
+
const rect = bodyEl.getBoundingClientRect();
|
| 600 |
+
const width = Math.max(1, Math.round(rect.width || 400));
|
| 601 |
+
const height = CONFIG.chartHeight;
|
| 602 |
+
svg.attr('width', width).attr('height', height);
|
| 603 |
+
|
| 604 |
+
const margin = CONFIG.margin;
|
| 605 |
+
const innerWidth = width - margin.left - margin.right;
|
| 606 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 607 |
+
|
| 608 |
+
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 609 |
+
|
| 610 |
+
if (!allData.length) return;
|
| 611 |
+
|
| 612 |
+
// Auto-compute domains
|
| 613 |
+
const stepExtent = d3.extent(allData, d => d.step);
|
| 614 |
+
let valueExtent = d3.extent(allData, d => d.value);
|
| 615 |
+
|
| 616 |
+
// Include baseline in the domain and add margin
|
| 617 |
+
if (baseline !== null) {
|
| 618 |
+
const minValue = Math.min(valueExtent[0], baseline);
|
| 619 |
+
const maxValue = Math.max(valueExtent[1], baseline);
|
| 620 |
+
const range = maxValue - minValue;
|
| 621 |
+
|
| 622 |
+
// Add 10% margin below and 10% above to ensure baseline is visible with gap
|
| 623 |
+
valueExtent = [
|
| 624 |
+
minValue - range * 0.1,
|
| 625 |
+
maxValue + range * 0.1
|
| 626 |
+
];
|
| 627 |
+
} else {
|
| 628 |
+
// Just add 5% margin on both sides
|
| 629 |
+
const range = valueExtent[1] - valueExtent[0];
|
| 630 |
+
valueExtent = [
|
| 631 |
+
valueExtent[0] - range * 0.05,
|
| 632 |
+
valueExtent[1] + range * 0.05
|
| 633 |
+
];
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
xScale.domain(stepExtent).range([0, innerWidth]);
|
| 637 |
+
yScale.domain(valueExtent).range([innerHeight, 0]);
|
| 638 |
+
|
| 639 |
+
// Create smart formatters
|
| 640 |
+
const stepValues = allData.map(d => d.step);
|
| 641 |
+
const metricValues = allData.map(d => d.value);
|
| 642 |
+
formatStep = createSmartFormatter(stepValues);
|
| 643 |
+
formatValue = createSmartFormatter(metricValues);
|
| 644 |
+
|
| 645 |
+
// Update clip
|
| 646 |
+
clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 647 |
+
|
| 648 |
+
// Update overlay
|
| 649 |
+
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 650 |
+
|
| 651 |
+
// Update zoom extent
|
| 652 |
+
zoom.extent([[0, 0], [innerWidth, innerHeight]])
|
| 653 |
+
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 654 |
+
|
| 655 |
+
// Grid
|
| 656 |
+
gGrid.selectAll('line').data(yScale.ticks(5)).join('line')
|
| 657 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 658 |
+
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
|
| 659 |
+
.attr('stroke', 'var(--grid-color)');
|
| 660 |
+
|
| 661 |
+
// Axes
|
| 662 |
+
gAxes.selectAll('*').remove();
|
| 663 |
+
gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`)
|
| 664 |
+
.call(d3.axisBottom(xScale).ticks(5).tickSizeOuter(0).tickFormat(formatStep));
|
| 665 |
+
gAxes.append('g').attr('class', 'y-axis')
|
| 666 |
+
.call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatValue));
|
| 667 |
+
gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)');
|
| 668 |
+
gAxes.selectAll('text').attr('fill', 'var(--tick-color)');
|
| 669 |
+
|
| 670 |
+
// Axis labels
|
| 671 |
+
gAxes.append('text')
|
| 672 |
+
.attr('class', 'axis-label')
|
| 673 |
+
.attr('x', innerWidth / 2)
|
| 674 |
+
.attr('y', innerHeight + 32)
|
| 675 |
+
.attr('text-anchor', 'middle')
|
| 676 |
+
.text(CONFIG.xAxisLabel);
|
| 677 |
+
|
| 678 |
+
gAxes.append('text')
|
| 679 |
+
.attr('class', 'axis-label')
|
| 680 |
+
.attr('transform', 'rotate(-90)')
|
| 681 |
+
.attr('x', -innerHeight / 2)
|
| 682 |
+
.attr('y', -38)
|
| 683 |
+
.attr('text-anchor', 'middle')
|
| 684 |
+
.text(CONFIG.yAxisLabel);
|
| 685 |
+
|
| 686 |
+
// Baseline reference line
|
| 687 |
+
if (baseline !== null) {
|
| 688 |
+
gAxes.append('line')
|
| 689 |
+
.attr('class', 'baseline-line')
|
| 690 |
+
.attr('x1', 0)
|
| 691 |
+
.attr('x2', innerWidth)
|
| 692 |
+
.attr('y1', yScale(baseline))
|
| 693 |
+
.attr('y2', yScale(baseline))
|
| 694 |
+
.attr('stroke', 'var(--text-color, #666)')
|
| 695 |
+
.attr('stroke-width', 1.5)
|
| 696 |
+
.attr('stroke-dasharray', '5,5')
|
| 697 |
+
.attr('opacity', 0.5);
|
| 698 |
+
|
| 699 |
+
gAxes.append('text')
|
| 700 |
+
.attr('class', 'baseline-label')
|
| 701 |
+
.attr('x', innerWidth - 5)
|
| 702 |
+
.attr('y', yScale(baseline) - 5)
|
| 703 |
+
.attr('text-anchor', 'end')
|
| 704 |
+
.attr('font-size', '10px')
|
| 705 |
+
.attr('fill', 'var(--text-color, #666)')
|
| 706 |
+
.attr('opacity', 0.7)
|
| 707 |
+
.text('Baseline');
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
// Group data by run
|
| 711 |
+
const dataByRun = {};
|
| 712 |
+
runList.forEach(run => { dataByRun[run] = []; });
|
| 713 |
+
allData.forEach(d => {
|
| 714 |
+
if (dataByRun[d.run]) dataByRun[d.run].push({ step: d.step, value: d.value });
|
| 715 |
+
});
|
| 716 |
+
runList.forEach(run => { dataByRun[run].sort((a, b) => a.step - b.step); });
|
| 717 |
+
|
| 718 |
+
const series = runList.map(run => ({ run, color: runColorMap[run], values: dataByRun[run] })).filter(s => s.values.length > 0);
|
| 719 |
+
|
| 720 |
+
// Ghost lines
|
| 721 |
+
const ghostLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(d3.curveLinear);
|
| 722 |
+
gPlot.selectAll('path.ghost-line').data(series, d => d.run).join('path')
|
| 723 |
+
.attr('class', 'ghost-line')
|
| 724 |
+
.attr('fill', 'none')
|
| 725 |
+
.attr('stroke', d => d.color)
|
| 726 |
+
.attr('stroke-width', 1.5)
|
| 727 |
+
.attr('opacity', smoothEnabled ? 0.15 : 0)
|
| 728 |
+
.attr('pointer-events', 'none')
|
| 729 |
+
.attr('d', d => ghostLine(d.values));
|
| 730 |
+
|
| 731 |
+
// Main lines
|
| 732 |
+
const mainLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(getCurve(smoothEnabled));
|
| 733 |
+
gPlot.selectAll('path.main-line').data(series, d => d.run).join('path')
|
| 734 |
+
.attr('class', 'main-line')
|
| 735 |
+
.attr('fill', 'none')
|
| 736 |
+
.attr('stroke', d => d.color)
|
| 737 |
+
.attr('stroke-width', 2)
|
| 738 |
+
.attr('opacity', 0.85)
|
| 739 |
+
.attr('d', d => mainLine(applySmoothing(d.values, smoothEnabled)));
|
| 740 |
+
|
| 741 |
+
// Hover
|
| 742 |
+
setupHover(series, innerWidth, innerHeight);
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
function setupHover(series, innerWidth, innerHeight) {
|
| 746 |
+
gHover.selectAll('*').remove();
|
| 747 |
+
|
| 748 |
+
hoverLine = gHover.append('line')
|
| 749 |
+
.style('stroke', 'var(--text-color)')
|
| 750 |
+
.attr('stroke-opacity', 0.25)
|
| 751 |
+
.attr('stroke-width', 1)
|
| 752 |
+
.attr('y1', 0)
|
| 753 |
+
.attr('y2', innerHeight)
|
| 754 |
+
.style('display', 'none')
|
| 755 |
+
.attr('pointer-events', 'none');
|
| 756 |
+
|
| 757 |
+
const stepSet = new Set();
|
| 758 |
+
series.forEach(s => s.values.forEach(v => stepSet.add(v.step)));
|
| 759 |
+
steps = Array.from(stepSet).sort((a, b) => a - b);
|
| 760 |
+
|
| 761 |
+
overlay.on('mousemove', function (ev) {
|
| 762 |
+
if (ev.buttons === 0) onHoverMove(ev, series);
|
| 763 |
+
}).on('mouseleave', onHoverLeave);
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
function onHoverMove(ev, series) {
|
| 767 |
+
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; }
|
| 768 |
+
|
| 769 |
+
const [mx, my] = d3.pointer(ev, overlay.node());
|
| 770 |
+
const targetStep = xScale.invert(mx);
|
| 771 |
+
const nearest = steps.reduce((best, t) => Math.abs(t - targetStep) < Math.abs(best - targetStep) ? t : best, steps[0]);
|
| 772 |
+
|
| 773 |
+
const xpx = xScale(nearest);
|
| 774 |
+
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
|
| 775 |
+
|
| 776 |
+
let html = `<div><strong>${chartConfig.title}</strong></div>`;
|
| 777 |
+
html += `<div>${formatStep(nearest)}</div>`;
|
| 778 |
+
|
| 779 |
+
const entries = series.map(s => {
|
| 780 |
+
const values = s.values;
|
| 781 |
+
let before = null, after = null;
|
| 782 |
+
for (let i = 0; i < values.length; i++) {
|
| 783 |
+
if (values[i].step <= nearest) before = values[i];
|
| 784 |
+
if (values[i].step >= nearest && !after) { after = values[i]; break; }
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
let interpolatedValue = null;
|
| 788 |
+
if (before && after && before.step !== after.step) {
|
| 789 |
+
const t = (nearest - before.step) / (after.step - before.step);
|
| 790 |
+
interpolatedValue = before.value + t * (after.value - before.value);
|
| 791 |
+
} else if (before && before.step === nearest) {
|
| 792 |
+
interpolatedValue = before.value;
|
| 793 |
+
} else if (after && after.step === nearest) {
|
| 794 |
+
interpolatedValue = after.value;
|
| 795 |
+
} else if (before) {
|
| 796 |
+
interpolatedValue = before.value;
|
| 797 |
+
} else if (after) {
|
| 798 |
+
interpolatedValue = after.value;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
return { run: s.run, color: s.color, value: interpolatedValue };
|
| 802 |
+
}).filter(e => e.value != null);
|
| 803 |
+
|
| 804 |
+
entries.sort((a, b) => b.value - a.value);
|
| 805 |
+
|
| 806 |
+
entries.forEach(e => {
|
| 807 |
+
html += `<div style="display:flex;align-items:center;gap:8px;"><span class="d3-tooltip__color-dot" style="background:${e.color}"></span><span>${e.run}</span><span style="margin-left:auto;font-weight:normal;">${e.value.toFixed(4)}</span></div>`;
|
| 808 |
+
});
|
| 809 |
+
|
| 810 |
+
tipInner.innerHTML = html;
|
| 811 |
+
const offsetX = 12, offsetY = 12;
|
| 812 |
+
tip.style.opacity = '1';
|
| 813 |
+
tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
function onHoverLeave() {
|
| 817 |
+
hideTipTimer = setTimeout(() => {
|
| 818 |
+
tip.style.opacity = '0';
|
| 819 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 820 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 821 |
+
}, 100);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
// Reset button
|
| 825 |
+
resetBtn.addEventListener('click', () => {
|
| 826 |
+
overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
| 827 |
+
});
|
| 828 |
+
|
| 829 |
+
// Load data
|
| 830 |
+
async function load() {
|
| 831 |
+
try {
|
| 832 |
+
const langCode = languageMap[chartConfig.language] || chartConfig.language.toLowerCase();
|
| 833 |
+
const task = chartConfig.task;
|
| 834 |
+
const baseUrl = window.location.origin;
|
| 835 |
+
const dataUrl = `${baseUrl}/finetasks/data/${langCode}/${task}_data.csv`;
|
| 836 |
+
const statsUrl = `${baseUrl}/finetasks/data/${langCode}/${task}_stats.csv`;
|
| 837 |
+
|
| 838 |
+
console.log('Loading D3 data from:', dataUrl);
|
| 839 |
+
console.log('Loading D3 stats from:', statsUrl);
|
| 840 |
+
|
| 841 |
+
const [dataResponse, statsResponse] = await Promise.all([
|
| 842 |
+
fetch(dataUrl, { cache: 'no-cache' }).catch(e => {
|
| 843 |
+
console.error('Failed to fetch data:', dataUrl, e);
|
| 844 |
+
throw e;
|
| 845 |
+
}),
|
| 846 |
+
fetch(statsUrl, { cache: 'no-cache' }).catch(e => {
|
| 847 |
+
console.error('Failed to fetch stats:', statsUrl, e);
|
| 848 |
+
throw e;
|
| 849 |
+
})
|
| 850 |
+
]);
|
| 851 |
+
|
| 852 |
+
console.log('Data response status:', dataResponse.status, dataResponse.statusText);
|
| 853 |
+
console.log('Stats response status:', statsResponse.status, statsResponse.statusText);
|
| 854 |
+
|
| 855 |
+
if (!dataResponse.ok) throw new Error(`Failed to load data: ${dataResponse.status} ${dataResponse.statusText}`);
|
| 856 |
+
if (!statsResponse.ok) throw new Error(`Failed to load stats: ${statsResponse.status} ${statsResponse.statusText}`);
|
| 857 |
+
|
| 858 |
+
const csvText = await dataResponse.text();
|
| 859 |
+
const statsText = await statsResponse.text();
|
| 860 |
+
|
| 861 |
+
console.log('CSV text length:', csvText.length);
|
| 862 |
+
console.log('Stats text length:', statsText.length);
|
| 863 |
+
|
| 864 |
+
// Parse CSV
|
| 865 |
+
const rawRows = d3.csvParse(csvText);
|
| 866 |
+
const statsRows = d3.csvParse(statsText);
|
| 867 |
+
|
| 868 |
+
if (!rawRows || rawRows.length === 0) {
|
| 869 |
+
throw new Error('No data found in CSV');
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
console.log('Raw CSV columns:', Object.keys(rawRows[0]));
|
| 873 |
+
console.log('Raw CSV first row:', rawRows[0]);
|
| 874 |
+
console.log('Looking for metric:', chartConfig.metric);
|
| 875 |
+
console.log('Total raw rows:', rawRows.length);
|
| 876 |
+
|
| 877 |
+
// Extract stat value from stats
|
| 878 |
+
if (statsRows && statsRows.length > 0) {
|
| 879 |
+
const statsRow = statsRows.find(row => row.metric === chartConfig.metric);
|
| 880 |
+
if (statsRow && statsRow[CONFIG.statColumn]) {
|
| 881 |
+
monotonicity = parseFloat(statsRow[CONFIG.statColumn]);
|
| 882 |
+
console.log(`${CONFIG.statLabel} value (${CONFIG.statColumn}):`, monotonicity);
|
| 883 |
+
}
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
// Transform to expected format
|
| 887 |
+
// Separate baseline from regular runs
|
| 888 |
+
const dataByRun = {};
|
| 889 |
+
let baselineValue = null;
|
| 890 |
+
let skippedRows = 0;
|
| 891 |
+
|
| 892 |
+
rawRows.forEach((row, idx) => {
|
| 893 |
+
const runname = row.runname || row.run_name || 'unknown';
|
| 894 |
+
const seed = row.seed || '';
|
| 895 |
+
const tokens = parseFloat(row.tokens);
|
| 896 |
+
const metricValue = parseFloat(row[chartConfig.metric]);
|
| 897 |
+
|
| 898 |
+
if (isNaN(tokens) || isNaN(metricValue)) {
|
| 899 |
+
if (idx < 3) {
|
| 900 |
+
console.log(`Skipping row ${idx}: tokens=${row.tokens}, metric=${row[chartConfig.metric]}`);
|
| 901 |
+
}
|
| 902 |
+
skippedRows++;
|
| 903 |
+
return;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
// Check if this is a baseline row
|
| 907 |
+
if (runname.toLowerCase().includes('baseline')) {
|
| 908 |
+
if (baselineValue === null) {
|
| 909 |
+
baselineValue = metricValue;
|
| 910 |
+
console.log('Baseline value found:', baselineValue);
|
| 911 |
+
}
|
| 912 |
+
return; // Don't include baseline in regular data
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
const processedName = processRunName(runname);
|
| 916 |
+
|
| 917 |
+
// Create key: either just runname (grouped) or runname_seed (separate)
|
| 918 |
+
const runKey = CONFIG.groupSeeds ? processedName : `${processedName}_${seed}`;
|
| 919 |
+
|
| 920 |
+
if (!dataByRun[runKey]) {
|
| 921 |
+
dataByRun[runKey] = {};
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
const tokenKey = tokens;
|
| 925 |
+
if (!dataByRun[runKey][tokenKey]) {
|
| 926 |
+
dataByRun[runKey][tokenKey] = [];
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
dataByRun[runKey][tokenKey].push(metricValue);
|
| 930 |
+
});
|
| 931 |
+
|
| 932 |
+
console.log('Skipped rows:', skippedRows);
|
| 933 |
+
console.log('Grouped by runs:', Object.keys(dataByRun));
|
| 934 |
+
|
| 935 |
+
// Compute means and create final data
|
| 936 |
+
allData = [];
|
| 937 |
+
Object.keys(dataByRun).forEach(runName => {
|
| 938 |
+
Object.keys(dataByRun[runName]).forEach(tokenKey => {
|
| 939 |
+
const values = dataByRun[runName][tokenKey];
|
| 940 |
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
| 941 |
+
allData.push({
|
| 942 |
+
run: runName,
|
| 943 |
+
step: parseFloat(tokenKey) / 1e9, // Convert to billions
|
| 944 |
+
value: mean
|
| 945 |
+
});
|
| 946 |
+
});
|
| 947 |
+
});
|
| 948 |
+
|
| 949 |
+
console.log('Processed data points:', allData.length);
|
| 950 |
+
console.log('Unique runs found:', Array.from(new Set(allData.map(d => d.run))));
|
| 951 |
+
console.log('Sample data points:', allData.slice(0, 5));
|
| 952 |
+
|
| 953 |
+
// Validate data
|
| 954 |
+
if (allData.length === 0) {
|
| 955 |
+
throw new Error(`No valid data found for metric: ${chartConfig.metric}`);
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
// Store baseline value
|
| 959 |
+
baseline = baselineValue;
|
| 960 |
+
|
| 961 |
+
// Update stat display with the metric value
|
| 962 |
+
if (statEl && monotonicity !== null && CONFIG.statLabel) {
|
| 963 |
+
statEl.innerHTML = `
|
| 964 |
+
<span class="chart-cell__stat-label">${CONFIG.statLabel}:</span>
|
| 965 |
+
<span class="chart-cell__stat-value">${monotonicity.toFixed(2)}</span>
|
| 966 |
+
`;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
runList = Array.from(new Set(allData.map(d => d.run))).sort();
|
| 970 |
+
|
| 971 |
+
if (runList.length === 0) {
|
| 972 |
+
throw new Error('No runs found in data');
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
const colors = getRunColors(runList.length);
|
| 976 |
+
runList.forEach((run, i) => { runColorMap[run] = colors[i % colors.length]; });
|
| 977 |
+
|
| 978 |
+
// Build legend
|
| 979 |
+
if (legendEl) {
|
| 980 |
+
legendEl.innerHTML = runList.map(run => {
|
| 981 |
+
const color = runColorMap[run];
|
| 982 |
+
return `<span class="item" data-run="${run}"><span class="swatch" style="background:${color}"></span><span>${run}</span></span>`;
|
| 983 |
+
}).join('');
|
| 984 |
+
|
| 985 |
+
// Add hover interactions
|
| 986 |
+
legendEl.querySelectorAll('.item').forEach(el => {
|
| 987 |
+
el.addEventListener('mouseenter', () => {
|
| 988 |
+
const run = el.getAttribute('data-run');
|
| 989 |
+
container.classList.add('hovering');
|
| 990 |
+
cellElement.querySelectorAll('path.main-line').forEach(path => {
|
| 991 |
+
const pathRun = d3.select(path).datum()?.run;
|
| 992 |
+
path.classList.toggle('ghost', pathRun !== run);
|
| 993 |
+
});
|
| 994 |
+
cellElement.querySelectorAll('path.ghost-line').forEach(path => {
|
| 995 |
+
const pathRun = d3.select(path).datum()?.run;
|
| 996 |
+
path.classList.toggle('ghost', pathRun !== run);
|
| 997 |
+
});
|
| 998 |
+
legendEl.querySelectorAll('.item').forEach(it => {
|
| 999 |
+
it.classList.toggle('ghost', it.getAttribute('data-run') !== run);
|
| 1000 |
+
});
|
| 1001 |
+
});
|
| 1002 |
+
|
| 1003 |
+
el.addEventListener('mouseleave', () => {
|
| 1004 |
+
container.classList.remove('hovering');
|
| 1005 |
+
cellElement.querySelectorAll('path.main-line').forEach(path => path.classList.remove('ghost'));
|
| 1006 |
+
cellElement.querySelectorAll('path.ghost-line').forEach(path => path.classList.remove('ghost'));
|
| 1007 |
+
legendEl.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
|
| 1008 |
+
});
|
| 1009 |
+
});
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
render();
|
| 1013 |
+
|
| 1014 |
+
} catch (e) {
|
| 1015 |
+
console.error('Error loading chart:', chartConfig.title, e);
|
| 1016 |
+
const pre = document.createElement('pre');
|
| 1017 |
+
pre.textContent = 'Error loading data: ' + (e && e.message ? e.message : e);
|
| 1018 |
+
pre.style.color = 'var(--danger, #b00020)';
|
| 1019 |
+
pre.style.fontSize = '12px';
|
| 1020 |
+
pre.style.padding = '12px';
|
| 1021 |
+
pre.style.background = 'var(--surface-bg)';
|
| 1022 |
+
pre.style.borderRadius = '6px';
|
| 1023 |
+
pre.style.border = '1px solid var(--danger, #b00020)';
|
| 1024 |
+
bodyEl.appendChild(pre);
|
| 1025 |
+
}
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
// Wrap load in try/catch to prevent one chart from breaking others
|
| 1029 |
+
try {
|
| 1030 |
+
load();
|
| 1031 |
+
} catch (e) {
|
| 1032 |
+
console.error('Failed to initialize chart:', chartConfig.title, e);
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
return { render };
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
// Init all charts
|
| 1039 |
+
const cells = Array.from(grid.querySelectorAll('.chart-cell'));
|
| 1040 |
+
const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.charts[idx]));
|
| 1041 |
+
|
| 1042 |
+
// Responsive
|
| 1043 |
+
let resizeTimer;
|
| 1044 |
+
const handleResize = () => {
|
| 1045 |
+
clearTimeout(resizeTimer);
|
| 1046 |
+
resizeTimer = setTimeout(() => {
|
| 1047 |
+
chartInstances.forEach(chart => chart && chart.render && chart.render());
|
| 1048 |
+
}, 100);
|
| 1049 |
+
};
|
| 1050 |
+
|
| 1051 |
+
const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null;
|
| 1052 |
+
if (ro) {
|
| 1053 |
+
ro.observe(container);
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
window.addEventListener('resize', handleResize);
|
| 1057 |
+
|
| 1058 |
+
setTimeout(() => {
|
| 1059 |
+
chartInstances.forEach(chart => chart && chart.render && chart.render());
|
| 1060 |
+
}, 100);
|
| 1061 |
+
};
|
| 1062 |
+
|
| 1063 |
+
if (document.readyState === 'loading') {
|
| 1064 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 1065 |
+
} else {
|
| 1066 |
+
ensureD3(bootstrap);
|
| 1067 |
+
}
|
| 1068 |
+
})();
|
| 1069 |
+
</script>
|
| 1070 |
+
|