Skip to content

Commit 9ce5fa4

Browse files
committed
refactor: downsample data and some ui improvements
1 parent 2144629 commit 9ce5fa4

52 files changed

Lines changed: 139 additions & 71 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

output/scenario.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ <h1 id="timelineTitle">Run Timeline</h1>
1818
<input type="file" id="fileInput" accept=".json">
1919
<label for="fileInput"><span class="icon">&#128194;</span> <span id="fileLabel">Drop timeseries .json here</span></label>
2020
</div>
21+
<div class="spinner" id="spinner" style="display:none">
22+
<div class="spinner-ring"></div>
23+
<div class="spinner-label">Loading timeseries...</div>
24+
</div>
2125
<div class="cards" id="metaCards" style="margin-bottom:16px"></div>
2226
<div class="chart-card full-chart" style="margin-bottom:30px">
2327
<canvas id="chartTimeline" style="max-height:600px;height:60vh"></canvas>

output/scenario.js

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const stateBgPlugin = {
1010
const xScale = chart.scales.x;
1111
ctx.save();
1212
bands.forEach(band => {
13-
const x1 = xScale.getPixelForValue(band.startIdx);
14-
const x2 = xScale.getPixelForValue(band.endIdx);
13+
const x1 = xScale.getPixelForValue(band.xStart);
14+
const x2 = xScale.getPixelForValue(band.xEnd);
1515
if (x1 === undefined || x2 === undefined) return;
1616
ctx.fillStyle = band.state === 'running'
1717
? 'rgba(0, 201, 167, 0.07)'
@@ -46,23 +46,25 @@ function renderMeta(data) {
4646

4747
function renderTimeline(data) {
4848
destroyTimeline();
49+
hideSpinner();
4950
document.getElementById('dropZone').style.display = 'none';
5051
renderMeta(data);
5152
const n = data.carbon_intensity.length;
5253
if (n === 0) return;
5354

54-
const labels = data.timestamps.map(t => {
55-
const d = new Date(t);
56-
return d.toLocaleString(undefined, {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
55+
const ms = data.timestamps.map(t => new Date(t).getTime());
56+
let offset = 0;
57+
const elapsedH = ms.map((t, i) => {
58+
if (i > 0 && t < ms[i - 1]) offset += 365 * 86400000;
59+
return (t + offset - ms[0]) / 3600000;
5760
});
58-
const idx = data.carbon_intensity.map((_, i) => i);
5961

6062
const stateBands = [];
6163
if (data.state && data.state.length) {
6264
let start = 0;
6365
for (let i = 1; i <= data.state.length; i++) {
6466
if (i === data.state.length || data.state[i] !== data.state[start]) {
65-
stateBands.push({ startIdx: start, endIdx: i - 1, state: data.state[start] });
67+
stateBands.push({ xStart: elapsedH[start], xEnd: elapsedH[i - 1], state: data.state[start] });
6668
start = i;
6769
}
6870
}
@@ -80,17 +82,20 @@ function renderTimeline(data) {
8082
const thetaResume = data.theta_resume;
8183

8284
document.getElementById('timelineTitle').textContent =
83-
'\u03B8\u209A=' + thetaPause + ' \u03B8\u1D63=' + thetaResume + ' \u2022 ' + n + ' steps';
85+
data.region + ' \u03B8\u209A=' + thetaPause + ' \u03B8\u1D63=' + thetaResume;
8486

8587
const canvas = document.getElementById('chartTimeline');
88+
const fmtHHMM = h => { const m = Math.round(h * 60); return String(Math.floor(m / 60)).padStart(2, '0') + ':' + String(m % 60).padStart(2, '0'); };
89+
90+
const xy = (y, i) => ({ x: elapsedH[i], y });
91+
8692
timelineChart = new Chart(canvas, {
8793
type: 'line',
8894
data: {
89-
labels: idx,
9095
datasets: [
9196
{
9297
label: 'Carbon Intensity',
93-
data: data.carbon_intensity,
98+
data: data.carbon_intensity.map(xy),
9499
borderColor: '#2e7dff',
95100
backgroundColor: 'rgba(46,125,255,0.1)',
96101
fill: true,
@@ -101,7 +106,7 @@ function renderTimeline(data) {
101106
},
102107
{
103108
label: '\u03B8_pause',
104-
data: idx.map(() => thetaPause),
109+
data: [{ x: elapsedH[0], y: thetaPause }, { x: elapsedH[elapsedH.length - 1], y: thetaPause }],
105110
borderColor: '#ff5e7a',
106111
borderDash: [4, 4],
107112
pointRadius: 0,
@@ -111,7 +116,7 @@ function renderTimeline(data) {
111116
},
112117
{
113118
label: '\u03B8_resume',
114-
data: idx.map(() => thetaResume),
119+
data: [{ x: elapsedH[0], y: thetaResume }, { x: elapsedH[elapsedH.length - 1], y: thetaResume }],
115120
borderColor: '#00c9a7',
116121
borderDash: [4, 4],
117122
pointRadius: 0,
@@ -121,7 +126,7 @@ function renderTimeline(data) {
121126
},
122127
{
123128
label: 'Cumulative Emissions',
124-
data: data.emissions_kg,
129+
data: data.emissions_kg.map(xy),
125130
borderColor: '#ff9f43',
126131
pointRadius: 0,
127132
borderWidth: 1.5,
@@ -134,15 +139,15 @@ function renderTimeline(data) {
134139
options: {
135140
responsive: true,
136141
maintainAspectRatio: false,
137-
interaction: { mode: 'index', intersect: false },
142+
interaction: { mode: 'nearest', axis: 'x', intersect: false },
138143
plugins: {
139144
legend: {
140145
position: 'top',
141146
labels: { boxWidth: 14, padding: 14, font: { size: 11 } },
142147
},
143148
tooltip: {
144149
callbacks: {
145-
title: ctx => labels[ctx[0].dataIndex] || '',
150+
title: ctx => { const x = ctx[0].parsed.x; if (x == null) return ''; const m = Math.round(x * 60); return String(Math.floor(m / 60)).padStart(2, '0') + ':' + String(m % 60).padStart(2, '0'); },
146151
afterBody: ctx => {
147152
const i = ctx[0].dataIndex;
148153
const s = data.state && data.state[i] ? data.state[i] : '?';
@@ -154,8 +159,11 @@ function renderTimeline(data) {
154159
scales: {
155160
x: {
156161
type: 'linear',
157-
title: { display: true, text: 'Step' },
158-
ticks: { maxTicksLimit: 20 },
162+
title: { display: true, text: 'Elapsed Time' },
163+
ticks: {
164+
maxTicksLimit: 15,
165+
callback: v => fmtHHMM(v),
166+
},
159167
},
160168
y: {
161169
type: 'linear',
@@ -177,6 +185,33 @@ function renderTimeline(data) {
177185
});
178186
}
179187

188+
function showSpinner(label) {
189+
document.getElementById('spinner').style.display = 'flex';
190+
document.querySelector('#spinner .spinner-label').textContent = label || 'Loading...';
191+
}
192+
function hideSpinner() {
193+
document.getElementById('spinner').style.display = 'none';
194+
}
195+
196+
function downsample(arr, maxPoints) {
197+
if (arr.length <= maxPoints) return arr;
198+
const step = arr.length / maxPoints;
199+
return Array.from({length: maxPoints}, (_, i) => arr[Math.floor(i * step)]);
200+
}
201+
202+
function downsampleData(data, maxPoints) {
203+
if (!data.carbon_intensity || data.carbon_intensity.length <= maxPoints) return data;
204+
const indices = Array.from({length: maxPoints}, (_, i) => Math.floor(i * data.carbon_intensity.length / maxPoints));
205+
return {
206+
...data,
207+
carbon_intensity: indices.map(i => data.carbon_intensity[i]),
208+
state: indices.map(i => data.state[i]),
209+
emissions_kg: indices.map(i => data.emissions_kg[i]),
210+
tokens_remaining: indices.map(i => data.tokens_remaining[i]),
211+
timestamps: indices.map(i => data.timestamps[i]),
212+
};
213+
}
214+
180215
function esc(s) {
181216
const d = document.createElement('div');
182217
d.textContent = s;
@@ -189,17 +224,25 @@ function loadData(data) {
189224
document.getElementById('timelineTitle').textContent = 'Invalid or empty timeseries file';
190225
return;
191226
}
227+
const n = data.carbon_intensity.length;
228+
const MAX = 3200;
229+
if (n > MAX) {
230+
data = downsampleData(data, MAX);
231+
}
192232
renderTimeline(data);
193233
}
194234

195235
// ── File drop (local file:// usage, or fallback) ─────────────────
196236
function loadFile(file) {
237+
showSpinner('Parsing ' + file.name + '...');
197238
const reader = new FileReader();
198239
reader.onload = e => {
199240
document.getElementById('fileLabel').textContent = file.name;
241+
hideSpinner();
200242
try {
201243
loadData(JSON.parse(e.target.result));
202244
} catch(err) {
245+
hideSpinner();
203246
alert('Error parsing JSON: ' + err.message);
204247
}
205248
};
@@ -224,12 +267,15 @@ const params = new URLSearchParams(window.location.search);
224267
const id = params.get('id');
225268

226269
if (window.location.protocol.startsWith('http') && id != null) {
270+
showSpinner('Loading timeseries for run ' + id + '...');
227271
fetch('timeseries/' + id + '.json').then(r => {
228272
if (!r.ok) throw new Error('Not found');
229273
return r.json();
230274
}).then(data => {
275+
hideSpinner();
231276
loadData(data);
232277
}).catch(() => {
278+
hideSpinner();
233279
document.getElementById('dropZone').style.display = 'flex';
234280
document.getElementById('timelineTitle').textContent = 'No timeseries data for id=' + id + ' — drop a .json file';
235281
});

output/style.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ tbody td.num { text-align: right; font-variant-numeric: tabular-nums; }
129129
.timeline-meta .meta-label { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--text2); margin-bottom: 2px; }
130130
.timeline-meta .meta-value { font-size: 18px; font-weight: 600; }
131131

132+
.spinner {
133+
display: flex; flex-direction: column; align-items: center; justify-content: center;
134+
padding: 60px 20px; gap: 16px;
135+
}
136+
.spinner-ring {
137+
width: 36px; height: 36px; border: 4px solid var(--border);
138+
border-top-color: var(--accent); border-radius: 50%;
139+
animation: spin .8s linear infinite;
140+
}
141+
.spinner-label { font-size: 14px; color: var(--text2); }
142+
@keyframes spin { to { transform: rotate(360deg); } }
143+
132144
@media (max-width: 800px) {
133145
.charts { grid-template-columns: 1fr; }
134146
body { padding: 12px; }

output/timeseries/0.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/1.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/10.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/11.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/12.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/13.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

output/timeseries/14.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)