add pictures to statcounter
This commit is contained in:
+166
-31
@@ -145,7 +145,7 @@ HTML = r"""
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.ranges { display: flex; gap: 8px; margin-bottom: 18px; }
|
||||
.ranges { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
|
||||
|
||||
.range-btn {
|
||||
background: var(--card);
|
||||
@@ -302,6 +302,7 @@ HTML = r"""
|
||||
<button class="range-btn active" data-range="all">All</button>
|
||||
<button class="range-btn" data-range="7d">Last 7 days</button>
|
||||
<button class="range-btn" data-range="24h">Last 24 h</button>
|
||||
<button class="range-btn" data-range="6h">Last 6 h</button>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
@@ -315,6 +316,11 @@ HTML = r"""
|
||||
<div class="value" id="stat-per-day">–</div>
|
||||
<div class="hint">Average over full history</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Growth 6 h</div>
|
||||
<div class="value" id="stat-delta-6h">–</div>
|
||||
<div class="hint" id="stat-delta-6h-hint">vs. measurement ~6 h ago</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Growth 24 h</div>
|
||||
<div class="value" id="stat-delta-24h">–</div>
|
||||
@@ -400,6 +406,55 @@ HTML = r"""
|
||||
<div id="heatmap"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Images – all graphs</div>
|
||||
<div class="chart-sub">Only exact measurements (counted every 2 h) are plotted – the estimates in between are excluded.</div>
|
||||
<div class="chart-sub" id="images-empty" style="display:none">
|
||||
No image counts yet – they appear after the next collector run with the image migration in place.
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Images over time</div>
|
||||
<div class="chart-sub">Image URLs found while crawling · exact count every 2 h · only exact measurements are plotted (no estimates) · dashed line = trend & 24 h forecast</div>
|
||||
<div class="chart-box"><canvas id="imagesMainChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Change per 5 minutes</div>
|
||||
<div class="chart-sub">Normalized to a 5-minute interval · spans between two exact counts (2 h) · grey = raw · colored = smoothed (moving average over 5 measurements)</div>
|
||||
<div class="chart-box small"><canvas id="imagesDeltaChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Cumulative net growth</div>
|
||||
<div class="chart-sub">Total images gained/lost since the start of the selected range · linear time axis</div>
|
||||
<div class="chart-box small"><canvas id="imagesCumChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Images per hour (last 48 h)</div>
|
||||
<div class="chart-sub">Sum of changes per hour bucket · green = growth, red = loss</div>
|
||||
<div class="chart-box small"><canvas id="imagesHourlyChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Image growth by hour of day</div>
|
||||
<div class="chart-sub">Average gain per 5 min, grouped by hour of day (whole history)</div>
|
||||
<div class="chart-box small"><canvas id="imagesHodChart"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Records – biggest image changes</div>
|
||||
<div class="chart-sub">Largest gains and losses between two consecutive measurements (selected range)</div>
|
||||
<div id="records-images"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Activity heatmap – image gain per hour of day</div>
|
||||
<div class="chart-container heatmap">
|
||||
<div class="chart-sub">Rows = days · columns = hours (0–23) · green = growth, red = loss, grey = no data</div>
|
||||
<div id="heatmap-images"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer" id="footer"></div>
|
||||
</div>
|
||||
|
||||
@@ -559,10 +614,26 @@ function parsePoints(values) {
|
||||
.sort(function (a, b) { return a.x - b.x; });
|
||||
}
|
||||
|
||||
function filterRange(points) {
|
||||
// Nur exakte Bildzählungen (alle 2 h). Die dazwischen geschätzten Werte
|
||||
// werden in keinem Diagramm dargestellt, damit alle Graphen ausschließlich
|
||||
// echte Messpunkte zeigen.
|
||||
function toImagePoints(points) {
|
||||
return (points || [])
|
||||
.filter(function (p) { return p.images !== null && p.images !== undefined; })
|
||||
.filter(function (p) { return p.imagesExact; })
|
||||
.map(function (p) { return { x: p.x, y: p.images }; });
|
||||
}
|
||||
|
||||
const RANGE_SPANS = {
|
||||
'6h': 6 * 3600000,
|
||||
'24h': 24 * 3600000,
|
||||
'7d': 7 * 24 * 3600000
|
||||
};
|
||||
|
||||
function filterRange(points, anchorX) {
|
||||
if (range === 'all' || !points.length) return points;
|
||||
const last = points[points.length - 1].x;
|
||||
const span = range === '24h' ? 24 * 3600000 : 7 * 24 * 3600000;
|
||||
const last = anchorX === undefined ? points[points.length - 1].x : anchorX;
|
||||
const span = RANGE_SPANS[range] || RANGE_SPANS['24h'];
|
||||
return points.filter(function (p) { return p.x >= last - span; });
|
||||
}
|
||||
|
||||
@@ -675,6 +746,12 @@ function baseScales(yFmt) {
|
||||
|
||||
function tooltipTitle(items) { return fmtDateFull(items[0].parsed.x); }
|
||||
|
||||
// Pages keep the existing camelCase ids (mainChart); other metrics get a prefix
|
||||
// (imagesMainChart).
|
||||
function chartId(prefix, suffix) {
|
||||
return prefix ? prefix + suffix : suffix.charAt(0).toLowerCase() + suffix.slice(1);
|
||||
}
|
||||
|
||||
function gradientFill(colorTop, colorBottom) {
|
||||
return function (context) {
|
||||
const chart = context.chart;
|
||||
@@ -689,8 +766,14 @@ function gradientFill(colorTop, colorBottom) {
|
||||
|
||||
/* ---------- charts ---------- */
|
||||
|
||||
function renderCharts(points, deltas, smoothedDeltas) {
|
||||
destroyCharts();
|
||||
function renderCharts(cfg) {
|
||||
const prefix = cfg.prefix;
|
||||
const points = cfg.points;
|
||||
const deltas = cfg.deltas;
|
||||
const smoothedDeltas = cfg.smoothedDeltas;
|
||||
const unit = cfg.unit;
|
||||
const accent = cfg.accent;
|
||||
const accentBar = cfg.accentBar;
|
||||
const grid = 'rgba(128,128,128,0.15)';
|
||||
|
||||
/* 1. main chart + trend */
|
||||
@@ -710,21 +793,21 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
];
|
||||
}
|
||||
|
||||
charts.main = new Chart(document.getElementById('mainChart'), {
|
||||
charts[prefix + 'main'] = new Chart(document.getElementById(chartId(prefix, 'MainChart')), {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Pages',
|
||||
label: unit,
|
||||
data: points,
|
||||
parsing: false,
|
||||
borderColor: ACCENT,
|
||||
borderColor: accent,
|
||||
borderWidth: 2,
|
||||
pointRadius: 2.5,
|
||||
pointHoverRadius: 5,
|
||||
tension: 0.25,
|
||||
fill: true,
|
||||
backgroundColor: gradientFill('rgba(79,140,255,0.28)', 'rgba(79,140,255,0.02)'),
|
||||
backgroundColor: gradientFill(cfg.gradTop, cfg.gradBottom),
|
||||
spanGaps: false
|
||||
},
|
||||
{
|
||||
@@ -753,8 +836,8 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
title: tooltipTitle,
|
||||
label: function (item) {
|
||||
return item.datasetIndex === 0
|
||||
? (fmt(item.parsed.y) + ' Pages')
|
||||
: ('Trend: ' + fmt(item.parsed.y) + ' Pages');
|
||||
? (fmt(item.parsed.y) + ' ' + unit)
|
||||
: ('Trend: ' + fmt(item.parsed.y) + ' ' + unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,7 +846,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
});
|
||||
|
||||
/* 2. delta chart */
|
||||
charts.delta = new Chart(document.getElementById('deltaChart'), {
|
||||
charts[prefix + 'delta'] = new Chart(document.getElementById(chartId(prefix, 'DeltaChart')), {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
@@ -781,7 +864,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
label: 'Change per 5 min',
|
||||
data: smoothedDeltas,
|
||||
parsing: false,
|
||||
borderColor: ACCENT,
|
||||
borderColor: accent,
|
||||
borderWidth: 2.5,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
@@ -824,7 +907,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
label: function (item) {
|
||||
const raw = item.raw;
|
||||
return [
|
||||
'Smoothed: ' + fmtSigned(raw.y) + ' Pages per 5 min',
|
||||
'Smoothed: ' + fmtSigned(raw.y) + ' ' + unit + ' per 5 min',
|
||||
'Raw: ' + fmtSigned(raw.raw) + ' per 5 min (change ' + fmtSigned(raw.diff) + ' in ' + fmtDur(raw.dt) + ')'
|
||||
];
|
||||
}
|
||||
@@ -839,7 +922,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
return { x: p.x, y: p.y - points[0].y };
|
||||
});
|
||||
|
||||
charts.cum = new Chart(document.getElementById('cumChart'), {
|
||||
charts[prefix + 'cum'] = new Chart(document.getElementById(chartId(prefix, 'CumChart')), {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
@@ -874,7 +957,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: tooltipTitle,
|
||||
label: function (item) { return fmtSigned(item.parsed.y) + ' Pages since range start'; }
|
||||
label: function (item) { return fmtSigned(item.parsed.y) + ' ' + unit + ' since range start'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,7 +974,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
const hourlyLabels = hours.map(function (h) { return fmtDate(h); });
|
||||
const hourlyValues = hours.map(function (h) { return Math.round(buckets[h]); });
|
||||
|
||||
charts.hourly = new Chart(document.getElementById('hourlyChart'), {
|
||||
charts[prefix + 'hourly'] = new Chart(document.getElementById(chartId(prefix, 'HourlyChart')), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: hourlyLabels,
|
||||
@@ -917,7 +1000,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { usePointStyle: true, boxWidth: 8, color: mutedColor() } },
|
||||
tooltip: { callbacks: { label: function (item) { return fmtSigned(item.parsed.y) + ' Pages in this hour'; } } }
|
||||
tooltip: { callbacks: { label: function (item) { return fmtSigned(item.parsed.y) + ' ' + unit + ' in this hour'; } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -934,14 +1017,14 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
return hodCount[i] ? Math.round(s / hodCount[i] * 10) / 10 : 0;
|
||||
});
|
||||
|
||||
charts.hod = new Chart(document.getElementById('hodChart'), {
|
||||
charts[prefix + 'hod'] = new Chart(document.getElementById(chartId(prefix, 'HodChart')), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: hodValues.map(function (_, i) { return (i < 10 ? '0' : '') + i + ':00'; }),
|
||||
datasets: [{
|
||||
label: 'Avg gain per 5 min',
|
||||
data: hodValues,
|
||||
backgroundColor: hodValues.map(function (v) { return v >= 0 ? 'rgba(79,140,255,0.65)' : 'rgba(255,107,107,0.65)'; }),
|
||||
backgroundColor: hodValues.map(function (v) { return v >= 0 ? accentBar : 'rgba(255,107,107,0.65)'; }),
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
@@ -959,7 +1042,7 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { usePointStyle: true, boxWidth: 8, color: mutedColor() } },
|
||||
tooltip: { callbacks: { label: function (item) { return fmtSigned(item.parsed.y) + ' Pages per 5 min (avg of ' + hodCount[item.dataIndex] + ' measurements)'; } } }
|
||||
tooltip: { callbacks: { label: function (item) { return fmtSigned(item.parsed.y) + ' ' + unit + ' per 5 min (avg of ' + hodCount[item.dataIndex] + ' measurements)'; } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -967,8 +1050,8 @@ function renderCharts(points, deltas, smoothedDeltas) {
|
||||
|
||||
/* ---------- records table ---------- */
|
||||
|
||||
function renderRecords(deltas) {
|
||||
const el = document.getElementById('records');
|
||||
function renderRecords(deltas, id, unit) {
|
||||
const el = document.getElementById(id);
|
||||
if (deltas.length < 2) {
|
||||
el.innerHTML = '<div class="chart-sub">Not enough data yet.</div>';
|
||||
return;
|
||||
@@ -985,7 +1068,7 @@ function renderRecords(deltas) {
|
||||
|
||||
el.innerHTML =
|
||||
'<table class="records">' +
|
||||
'<tr><th>Time</th><th style="text-align:right">Change</th><th style="text-align:right">Span</th></tr>' +
|
||||
'<tr><th>Time</th><th style="text-align:right">Change (' + unit + ')</th><th style="text-align:right">Span</th></tr>' +
|
||||
rows(gains, 'pos') +
|
||||
rows(losses, 'neg') +
|
||||
'</table>';
|
||||
@@ -993,8 +1076,8 @@ function renderRecords(deltas) {
|
||||
|
||||
/* ---------- heatmap ---------- */
|
||||
|
||||
function renderHeatmap(points) {
|
||||
const el = document.getElementById('heatmap');
|
||||
function renderHeatmap(points, id, unit) {
|
||||
const el = document.getElementById(id);
|
||||
if (!points.length) { el.innerHTML = ''; return; }
|
||||
|
||||
function localDayKey(d) {
|
||||
@@ -1044,7 +1127,7 @@ function renderHeatmap(points) {
|
||||
const op = maxAbs ? Math.min(0.85, 0.15 + 0.7 * Math.abs(dv) / maxAbs) : 0.3;
|
||||
const color = dv >= 0 ? '46,204,113' : '255,107,107';
|
||||
style = ' style="background:rgba(' + color + ',' + op.toFixed(2) + ')"';
|
||||
title = fmtDay(new Date(day + 'T12:00:00').getTime()) + ' ' + h + ':00 – ' + fmtSigned(dv) + ' Pages (' + fmt(val) + ' total)';
|
||||
title = fmtDay(new Date(day + 'T12:00:00').getTime()) + ' ' + h + ':00 – ' + fmtSigned(dv) + ' ' + unit + ' (' + fmt(val) + ' total)';
|
||||
}
|
||||
html += '<td><div class="cell"' + style + ' title="' + title + '"></div></td>';
|
||||
}
|
||||
@@ -1123,6 +1206,12 @@ function renderAll() {
|
||||
setSigned('stat-per-day', perHour * 24);
|
||||
}
|
||||
|
||||
const span6h = closestBefore(all, last.x - 6 * 3600000, Math.max(3 * intervalMs, 3600000));
|
||||
setDeltaCard('stat-delta-6h', 'stat-delta-6h-hint',
|
||||
span6h ? last.y - span6h.y : null,
|
||||
span6h ? last.x - span6h.x : 0,
|
||||
span6h ? (last.y - span6h.y) * (3600000 / Math.max(1, last.x - span6h.x)) * (5 / 60) : 0);
|
||||
|
||||
const span24h = closestBefore(all, last.x - 24 * 3600000, Math.max(6 * intervalMs, 3 * 3600000));
|
||||
setDeltaCard('stat-delta-24h', 'stat-delta-24h-hint',
|
||||
span24h ? last.y - span24h.y : null,
|
||||
@@ -1217,9 +1306,55 @@ function renderAll() {
|
||||
document.getElementById('footer').textContent = 'Forecast available once there are at least 2 measurements.';
|
||||
}
|
||||
|
||||
renderCharts(fpoints, deltas, smoothedDeltas);
|
||||
renderRecords(deltas);
|
||||
renderHeatmap(all);
|
||||
/* images: same graphs, but only exact measurements (every 2 h) */
|
||||
const imageAll = toImagePoints(all);
|
||||
const imageFpoints = filterRange(imageAll, last.x);
|
||||
const imageDeltas = computeDeltas(imageFpoints).deltas;
|
||||
const imageSmoothY = movingAverage(imageDeltas.map(function (d) { return d.y; }), DELTA_SMOOTH_WINDOW);
|
||||
const imageSmoothedDeltas = imageDeltas.map(function (d, i) {
|
||||
return { x: d.x, y: imageSmoothY[i], raw: d.y, dt: d.dt, diff: d.diff };
|
||||
});
|
||||
const imagesEmpty = document.getElementById('images-empty');
|
||||
const hasImageValues = all.some(function (p) { return p.images !== null; });
|
||||
if (!imageAll.length) {
|
||||
imagesEmpty.textContent = hasImageValues
|
||||
? 'Only estimated image counts so far – the graphs show exact counts only (every 2 h).'
|
||||
: 'No image counts yet – they appear after the next collector run with the image migration in place.';
|
||||
imagesEmpty.style.display = '';
|
||||
} else if (!imageFpoints.length) {
|
||||
imagesEmpty.textContent = 'No exact image counts in the selected range (counted every 2 h).';
|
||||
imagesEmpty.style.display = '';
|
||||
} else {
|
||||
imagesEmpty.style.display = 'none';
|
||||
}
|
||||
|
||||
destroyCharts();
|
||||
renderCharts({
|
||||
prefix: '',
|
||||
points: fpoints,
|
||||
deltas: deltas,
|
||||
smoothedDeltas: smoothedDeltas,
|
||||
unit: 'Pages',
|
||||
accent: ACCENT,
|
||||
accentBar: 'rgba(79,140,255,0.65)',
|
||||
gradTop: 'rgba(79,140,255,0.28)',
|
||||
gradBottom: 'rgba(79,140,255,0.02)'
|
||||
});
|
||||
renderCharts({
|
||||
prefix: 'images',
|
||||
points: imageFpoints,
|
||||
deltas: imageDeltas,
|
||||
smoothedDeltas: imageSmoothedDeltas,
|
||||
unit: 'Images',
|
||||
accent: ACCENT2,
|
||||
accentBar: 'rgba(180,122,255,0.65)',
|
||||
gradTop: 'rgba(180,122,255,0.28)',
|
||||
gradBottom: 'rgba(180,122,255,0.02)'
|
||||
});
|
||||
renderRecords(deltas, 'records', 'Pages');
|
||||
renderRecords(imageDeltas, 'records-images', 'Images');
|
||||
renderHeatmap(all, 'heatmap', 'Pages');
|
||||
renderHeatmap(imageAll, 'heatmap-images', 'Images');
|
||||
}
|
||||
|
||||
async function update() {
|
||||
|
||||
Reference in New Issue
Block a user