Skip to content

Commit 5c63048

Browse files
committed
update-sites/stats: add two-site comparisons
1 parent 1292232 commit 5c63048

File tree

1 file changed

+164
-25
lines changed

1 file changed

+164
-25
lines changed

_pages/update-sites/stats.md

Lines changed: 164 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ section: Extend:Update Sites
2323
display: flex;
2424
gap: 2px;
2525
flex-wrap: wrap;
26+
width: 100%;
27+
}
28+
#controls .grid div.widgets select {
29+
flex: 1;
2630
}
2731
#controls label, #controls select {
2832
padding-right: 0.4em;
@@ -47,6 +51,16 @@ section: Extend:Update Sites
4751
<label class="heading">Update Site:</label>
4852
<select id="site" onchange="updateChart()"></select>
4953

54+
<label class="heading">Compare To:</label>
55+
<div class="widgets">
56+
<select id="op" onchange="updateChart()">
57+
<option value="+">+</option>
58+
<option value="/">/</option>
59+
<option value="%">%</option>
60+
</select>
61+
<select id="site2" onchange="updateChart()"></select>
62+
</div>
63+
5064
<label class="heading">Time Window:</label>
5165
<div class="widgets">
5266
<label><input type="radio" id="time-daily" name="timeWindow" value="daily" checked onchange="updateChart()"> Daily</label>
@@ -82,11 +96,13 @@ section: Extend:Update Sites
8296

8397
function getSelectedValues() {
8498
const site = document.getElementById('site').value;
99+
const op = document.getElementById('op').value;
100+
const site2 = document.getElementById('site2').value;
85101
const timeWindow = document.querySelector('input[name="timeWindow"]:checked').value;
86102
const countType = document.querySelector('input[name="countType"]:checked').value;
87103
const rollingAverage = document.getElementById('rolling-average').checked;
88104

89-
return { site, timeWindow, countType, rollingAverage };
105+
return { site, op, site2, timeWindow, countType, rollingAverage };
90106
}
91107

92108
function updateRollingAverageState() {
@@ -184,6 +200,63 @@ section: Extend:Update Sites
184200
return filled;
185201
}
186202

203+
function combineForStackedChart(data1, data2) {
204+
if (!data1 || !data2) return data1 || data2 || [];
205+
206+
// Create maps for efficient lookup
207+
const map1 = new Map(data1.map(([date, value]) => [date.getTime(), value]));
208+
const map2 = new Map(data2.map(([date, value]) => [date.getTime(), value]));
209+
210+
// Get all unique dates from both datasets
211+
const allDates = new Set([...map1.keys(), ...map2.keys()]);
212+
const result = [];
213+
214+
for (const dateKey of Array.from(allDates).sort()) {
215+
const date = new Date(dateKey);
216+
const val1 = map1.get(dateKey) || 0;
217+
const val2 = map2.get(dateKey) || 0;
218+
219+
// Format: [date, site1_value, site2_value]
220+
result.push([date, val1, val2]);
221+
}
222+
223+
return result;
224+
}
225+
226+
function combineDataSets(data1, data2, operation) {
227+
if (!data1 || !data2) return data1 || data2 || [];
228+
229+
// Create maps for efficient lookup
230+
const map1 = new Map(data1.map(([date, value]) => [date.getTime(), value]));
231+
const map2 = new Map(data2.map(([date, value]) => [date.getTime(), value]));
232+
233+
// Get all unique dates from both datasets
234+
const allDates = new Set([...map1.keys(), ...map2.keys()]);
235+
const result = [];
236+
237+
for (const dateKey of Array.from(allDates).sort()) {
238+
const date = new Date(dateKey);
239+
const val1 = map1.get(dateKey) || 0;
240+
const val2 = map2.get(dateKey) || 0;
241+
242+
let combinedValue;
243+
switch (operation) {
244+
case '/':
245+
combinedValue = val2 === 0 ? 0 : val1 / val2;
246+
break;
247+
case '%':
248+
combinedValue = (val1 + val2) === 0 ? 0 : (val1 / (val1 + val2)) * 100;
249+
break;
250+
default:
251+
combinedValue = val1;
252+
}
253+
254+
result.push([date, combinedValue]);
255+
}
256+
257+
return result;
258+
}
259+
187260
async function fetchStatsData(site, timeWindow, countType) {
188261
const cacheKey = getCacheKey(site, timeWindow, countType);
189262

@@ -232,7 +305,7 @@ section: Extend:Update Sites
232305
}
233306

234307
async function updateChart() {
235-
const { site, timeWindow, countType, rollingAverage } = getSelectedValues();
308+
const { site, op, site2, timeWindow, countType, rollingAverage } = getSelectedValues();
236309

237310
if (!site) return;
238311

@@ -243,26 +316,82 @@ section: Extend:Update Sites
243316
document.getElementById('loading').style.display = 'block';
244317

245318
try {
246-
const rawData = await fetchStatsData(site, timeWindow, countType);
319+
// Fetch data for primary site
320+
const rawData1 = await fetchStatsData(site, timeWindow, countType);
321+
let data = fillDateGaps(rawData1, timeWindow);
322+
let chartTitle = site;
323+
let yLabel = `${countType === 'unique' ? 'Unique IP Addresses' : 'Total Update Checks'}`;
324+
325+
// Configuration for chart
326+
let chartConfig = {
327+
rollPeriod: rollingAverage && timeWindow === 'daily' ? 7 : 1,
328+
labels: ['Date', `${countType === 'unique' ? 'Unique IPs' : 'Total Checks'}`],
329+
ylabel: yLabel,
330+
title: `${chartTitle} - ${timeWindow.charAt(0).toUpperCase() + timeWindow.slice(1)} ${countType === 'unique' ? 'Unique' : 'Total'} Statistics`
331+
};
332+
333+
// Set X-axis formatting based on time window
334+
if (timeWindow === 'yearly') {
335+
chartConfig.axes = {
336+
x: {
337+
axisLabelFormatter: function(d) {
338+
return d.getFullYear().toString();
339+
},
340+
ticker: function(a, b, pixels, opts, dygraph, vals) {
341+
// Generate yearly ticks
342+
const startYear = new Date(a).getFullYear();
343+
const endYear = new Date(b).getFullYear();
344+
const ticks = [];
345+
for (let year = startYear; year <= endYear; year++) {
346+
ticks.push({v: new Date(year, 0, 1).getTime(), label: year.toString()});
347+
}
348+
return ticks;
349+
}
350+
}
351+
};
352+
} else if (timeWindow === 'monthly') {
353+
chartConfig.axes = {
354+
x: {
355+
axisLabelFormatter: function(d) {
356+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
357+
}
358+
}
359+
};
360+
}
247361

248-
// Fill date gaps to avoid weird connecting lines
249-
const data = fillDateGaps(rawData, timeWindow);
362+
// If site2 is selected, fetch and combine data
363+
if (site2 && site2 !== site) {
364+
const rawData2 = await fetchStatsData(site2, timeWindow, countType);
365+
const filledData2 = fillDateGaps(rawData2, timeWindow);
366+
367+
if (op === '+') {
368+
// For sum, create stacked chart with both series
369+
data = combineForStackedChart(data, filledData2);
370+
chartTitle = `${site} + ${site2}`;
371+
chartConfig.labels = ['Date', site, site2];
372+
chartConfig.stackedGraph = true;
373+
chartConfig.fillGraph = true;
374+
chartConfig.colors = ['#1f77b4', '#ff7f0e'];
375+
} else {
376+
// For other operations, combine into single series
377+
data = combineDataSets(data, filledData2, op);
378+
switch (op) {
379+
case '/':
380+
chartTitle = `${site} / ${site2}`;
381+
yLabel = `Ratio (${site}/${site2})`;
382+
break;
383+
case '%':
384+
chartTitle = `${site} as % of (${site} + ${site2})`;
385+
yLabel = `Percentage (%)`;
386+
break;
387+
}
388+
}
250389

251-
let rollPeriod = 1;
252-
if (rollingAverage && timeWindow === 'daily') {
253-
rollPeriod = 7;
390+
chartConfig.title = `${chartTitle} - ${timeWindow.charAt(0).toUpperCase() + timeWindow.slice(1)} ${countType === 'unique' ? 'Unique' : 'Total'} Statistics`;
391+
chartConfig.ylabel = yLabel;
254392
}
255393

256-
new Dygraph(
257-
document.getElementById("stats-chart"),
258-
data,
259-
{
260-
rollPeriod: rollPeriod,
261-
labels: ['Date', `${countType === 'unique' ? 'Unique IPs' : 'Total Checks'}`],
262-
ylabel: `${countType === 'unique' ? 'Unique IP Addresses' : 'Total Update Checks'}`,
263-
title: `${site} - ${timeWindow.charAt(0).toUpperCase() + timeWindow.slice(1)} ${countType === 'unique' ? 'Unique' : 'Total'} Statistics`
264-
}
265-
);
394+
new Dygraph(document.getElementById("stats-chart"), data, chartConfig);
266395

267396
} catch (error) {
268397
document.getElementById("stats-chart").innerHTML =
@@ -292,20 +421,25 @@ section: Extend:Update Sites
292421

293422
// Add sites as options to dropdown list
294423
const siteSelect = document.getElementById('site');
424+
const site2Select = document.getElementById('site2');
295425
for (const siteName of window.availableSites) {
296426
const siteOption = new Option();
297-
siteOption.value = siteName;
427+
const site2Option = new Option();
428+
siteOption.value = site2Option.value = siteName;
298429

299430
// Add metadata to option text if available
300431
const metadata = sitesData[siteName];
301432
if (metadata && metadata.total_unique_ips) {
302-
siteOption.innerHTML = `${siteName} (${metadata.total_unique_ips.toLocaleString()})`;
433+
siteOption.innerHTML = site2Option.innerHTML =
434+
`${siteName} (${metadata.total_unique_ips.toLocaleString()})`;
303435
} else {
304-
siteOption.innerHTML = siteName;
436+
siteOption.innerHTML = site2Option.innerHTML = siteName;
305437
}
306438

307-
if (siteName === 'Java-8') siteOption.selected = true;
439+
if (siteName === 'Fiji') siteOption.selected = true;
440+
else if (siteName === 'Java-8') site2Option.selected = true;
308441
siteSelect.appendChild(siteOption);
442+
site2Select.appendChild(site2Option);
309443
}
310444

311445
// Initial chart update
@@ -314,13 +448,18 @@ section: Extend:Update Sites
314448
} catch (error) {
315449
console.error('Failed to initialize page:', error);
316450
// Fallback to hardcoded list if sites.json fails
317-
window.availableSites = ['Java-8', 'Fiji', 'ImageJ', 'Bio-Formats'];
451+
window.availableSites = ['Java-8', 'Fiji'];
318452
const siteSelect = document.getElementById('site');
453+
const site2Select = document.getElementById('site2');
319454
for (const siteName of window.availableSites) {
320455
const siteOption = new Option();
321-
siteOption.value = siteOption.innerHTML = siteName;
322-
if (siteName === 'Java-8') siteOption.selected = true;
456+
const site2Option = new Option();
457+
siteOption.value = site2Option.value =
458+
siteOption.innerHTML = site2Option.innerHTML = siteName;
459+
if (siteName === 'Fiji') siteOption.selected = true;
460+
else if (siteName === 'Java-8') site2Option.selected = true;
323461
siteSelect.appendChild(siteOption);
462+
site2Select.appendChild(site2Option);
324463
}
325464
updateChart();
326465
}

0 commit comments

Comments
 (0)