@@ -23,6 +23,10 @@ section: Extend:Update Sites
23
23
display : flex ;
24
24
gap : 2px ;
25
25
flex-wrap : wrap ;
26
+ width : 100% ;
27
+ }
28
+ #controls .grid div .widgets select {
29
+ flex : 1 ;
26
30
}
27
31
#controls label , #controls select {
28
32
padding-right : 0.4em ;
@@ -47,6 +51,16 @@ section: Extend:Update Sites
47
51
<label class =" heading " >Update Site:</label >
48
52
<select id =" site " onchange =" updateChart ()" ></select >
49
53
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
+
50
64
<label class =" heading " >Time Window:</label >
51
65
<div class =" widgets " >
52
66
<label><input type="radio" id="time-daily" name="timeWindow" value="daily" checked onchange="updateChart()"> Daily</label>
@@ -82,11 +96,13 @@ section: Extend:Update Sites
82
96
83
97
function getSelectedValues () {
84
98
const site = document .getElementById (' site' ).value ;
99
+ const op = document .getElementById (' op' ).value ;
100
+ const site2 = document .getElementById (' site2' ).value ;
85
101
const timeWindow = document .querySelector (' input[name="timeWindow"]:checked' ).value ;
86
102
const countType = document .querySelector (' input[name="countType"]:checked' ).value ;
87
103
const rollingAverage = document .getElementById (' rolling-average' ).checked ;
88
104
89
- return { site, timeWindow, countType, rollingAverage };
105
+ return { site, op, site2, timeWindow, countType, rollingAverage };
90
106
}
91
107
92
108
function updateRollingAverageState () {
@@ -184,6 +200,63 @@ section: Extend:Update Sites
184
200
return filled;
185
201
}
186
202
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
+
187
260
async function fetchStatsData (site , timeWindow , countType ) {
188
261
const cacheKey = getCacheKey (site, timeWindow, countType);
189
262
@@ -232,7 +305,7 @@ section: Extend:Update Sites
232
305
}
233
306
234
307
async function updateChart () {
235
- const { site , timeWindow , countType , rollingAverage } = getSelectedValues ();
308
+ const { site , op , site2 , timeWindow , countType , rollingAverage } = getSelectedValues ();
236
309
237
310
if (! site) return ;
238
311
@@ -243,26 +316,82 @@ section: Extend:Update Sites
243
316
document .getElementById (' loading' ).style .display = ' block' ;
244
317
245
318
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
+ }
247
361
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
+ }
250
389
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;
254
392
}
255
393
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);
266
395
267
396
} catch (error) {
268
397
document .getElementById (" stats-chart" ).innerHTML =
@@ -292,20 +421,25 @@ section: Extend:Update Sites
292
421
293
422
// Add sites as options to dropdown list
294
423
const siteSelect = document .getElementById (' site' );
424
+ const site2Select = document .getElementById (' site2' );
295
425
for (const siteName of window .availableSites ) {
296
426
const siteOption = new Option ();
297
- siteOption .value = siteName;
427
+ const site2Option = new Option ();
428
+ siteOption .value = site2Option .value = siteName;
298
429
299
430
// Add metadata to option text if available
300
431
const metadata = sitesData[siteName];
301
432
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 ()} )` ;
303
435
} else {
304
- siteOption .innerHTML = siteName;
436
+ siteOption .innerHTML = site2Option . innerHTML = siteName;
305
437
}
306
438
307
- if (siteName === ' Java-8' ) siteOption .selected = true ;
439
+ if (siteName === ' Fiji' ) siteOption .selected = true ;
440
+ else if (siteName === ' Java-8' ) site2Option .selected = true ;
308
441
siteSelect .appendChild (siteOption);
442
+ site2Select .appendChild (site2Option);
309
443
}
310
444
311
445
// Initial chart update
@@ -314,13 +448,18 @@ section: Extend:Update Sites
314
448
} catch (error) {
315
449
console .error (' Failed to initialize page:' , error);
316
450
// Fallback to hardcoded list if sites.json fails
317
- window .availableSites = [' Java-8' , ' Fiji' , ' ImageJ ' , ' Bio-Formats ' ];
451
+ window .availableSites = [' Java-8' , ' Fiji' ];
318
452
const siteSelect = document .getElementById (' site' );
453
+ const site2Select = document .getElementById (' site2' );
319
454
for (const siteName of window .availableSites ) {
320
455
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 ;
323
461
siteSelect .appendChild (siteOption);
462
+ site2Select .appendChild (site2Option);
324
463
}
325
464
updateChart ();
326
465
}
0 commit comments