mirror of
https://github.com/tanrax/org-social-website
synced 2026-07-25 02:35:00 +02:00
242 lines
9.1 KiB
JavaScript
242 lines
9.1 KiB
JavaScript
/* Network stats section: fetches aggregated statistics from the Org Social
|
|
Relay and renders stat tiles plus a stacked bar chart of monthly activity.
|
|
If the relay is unreachable the section simply stays hidden. */
|
|
(function () {
|
|
"use strict";
|
|
|
|
var RELAY_STATS_URL = "https://relay.org-social.org/stats/";
|
|
/* Months before this are residual test posts that would flatten the scale. */
|
|
var FIRST_YEAR = 2025;
|
|
var FIRST_MONTH = 9;
|
|
|
|
/* Palette validated for color-vision deficiency (worst adjacent pair
|
|
ΔE 16.3, target ≥ 8). Order matters: it is the CVD-safety mechanism. */
|
|
var SERIES = [
|
|
{ key: "posts", label: "Posts", color: "#7f5ab6" },
|
|
{ key: "replies", label: "Replies", color: "#008300" },
|
|
{ key: "boosts", label: "Boosts", color: "#e87ba4" },
|
|
{ key: "reactions", label: "Reactions", color: "#eda100" },
|
|
{ key: "group_messages", label: "Group messages", color: "#2a78d6" },
|
|
{ key: "polls", label: "Polls", color: "#eb6834" }
|
|
];
|
|
|
|
var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
|
|
function formatNumber(value) {
|
|
return value.toLocaleString("en-US");
|
|
}
|
|
|
|
/* Flatten data.years into a list of consecutive months from
|
|
FIRST_YEAR/FIRST_MONTH to the last month present, filling gaps
|
|
with zeros so the time axis stays honest. */
|
|
function buildMonths(years) {
|
|
var last = null;
|
|
Object.keys(years).forEach(function (year) {
|
|
Object.keys(years[year]).forEach(function (month) {
|
|
var y = parseInt(year, 10);
|
|
var m = parseInt(month, 10);
|
|
if (!last || y > last.y || (y === last.y && m > last.m)) {
|
|
last = { y: y, m: m };
|
|
}
|
|
});
|
|
});
|
|
if (!last) {
|
|
return [];
|
|
}
|
|
|
|
var months = [];
|
|
var y = FIRST_YEAR;
|
|
var m = FIRST_MONTH;
|
|
while (y < last.y || (y === last.y && m <= last.m)) {
|
|
var key = (m < 10 ? "0" : "") + m;
|
|
var entry = (years[y] && years[y][key]) || {};
|
|
var month = { label: MONTH_NAMES[m - 1] + " " + y };
|
|
SERIES.forEach(function (series) {
|
|
month[series.key] = entry[series.key] || 0;
|
|
});
|
|
month.total_posts = entry.total_posts || 0;
|
|
month.active_accounts = entry.active_accounts || 0;
|
|
months.push(month);
|
|
m += 1;
|
|
if (m > 12) {
|
|
m = 1;
|
|
y += 1;
|
|
}
|
|
}
|
|
return months;
|
|
}
|
|
|
|
function animateValue(element, target) {
|
|
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (reduceMotion || !window.requestAnimationFrame) {
|
|
element.textContent = formatNumber(target);
|
|
return;
|
|
}
|
|
var duration = 1200;
|
|
var start = null;
|
|
function step(timestamp) {
|
|
if (start === null) {
|
|
start = timestamp;
|
|
}
|
|
var progress = Math.min((timestamp - start) / duration, 1);
|
|
var eased = 1 - Math.pow(1 - progress, 3);
|
|
element.textContent = formatNumber(Math.round(target * eased));
|
|
if (progress < 1) {
|
|
window.requestAnimationFrame(step);
|
|
}
|
|
}
|
|
window.requestAnimationFrame(step);
|
|
}
|
|
|
|
function renderTiles(section, global) {
|
|
var tiles = section.querySelectorAll(".stat-value");
|
|
var targets = [];
|
|
tiles.forEach(function (tile) {
|
|
targets.push({ element: tile, value: global[tile.dataset.stat] || 0 });
|
|
});
|
|
|
|
if (!("IntersectionObserver" in window)) {
|
|
targets.forEach(function (t) {
|
|
t.element.textContent = formatNumber(t.value);
|
|
});
|
|
return;
|
|
}
|
|
var observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
observer.disconnect();
|
|
targets.forEach(function (t) {
|
|
animateValue(t.element, t.value);
|
|
});
|
|
}
|
|
});
|
|
}, { threshold: 0.4 });
|
|
observer.observe(section.querySelector(".stat-tile"));
|
|
}
|
|
|
|
function renderChart(months) {
|
|
var canvas = document.getElementById("activity-chart");
|
|
Chart.defaults.font.family = "'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
|
|
new Chart(canvas, {
|
|
type: "bar",
|
|
data: {
|
|
labels: months.map(function (month) { return month.label; }),
|
|
datasets: SERIES.map(function (series) {
|
|
return {
|
|
label: series.label,
|
|
/* Zero becomes null so empty segments draw nothing
|
|
instead of a stray border hairline. */
|
|
data: months.map(function (month) {
|
|
return month[series.key] || null;
|
|
}),
|
|
backgroundColor: series.color,
|
|
stack: "activity",
|
|
borderColor: "#ffffff",
|
|
borderWidth: 1,
|
|
borderSkipped: false,
|
|
borderRadius: 4,
|
|
maxBarThickness: 24
|
|
};
|
|
})
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: "bottom",
|
|
labels: {
|
|
usePointStyle: true,
|
|
pointStyle: "circle",
|
|
boxHeight: 8,
|
|
color: "#4a4a4a",
|
|
padding: 16
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
footer: function (items) {
|
|
if (!items.length) {
|
|
return "";
|
|
}
|
|
var month = months[items[0].dataIndex];
|
|
return "Total: " + formatNumber(month.total_posts)
|
|
+ "\nActive accounts: " + formatNumber(month.active_accounts);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
stacked: true,
|
|
grid: { display: false },
|
|
border: { color: "#c3c2b7" },
|
|
ticks: { color: "#898781" }
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
grid: { color: "#e1e0d9" },
|
|
border: { display: false },
|
|
ticks: { color: "#898781", precision: 0 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Accessible fallback and contrast relief: the full dataset as a table. */
|
|
function renderTable(months) {
|
|
var table = document.getElementById("activity-table");
|
|
var html = "<thead><tr><th>Month</th>";
|
|
SERIES.forEach(function (series) {
|
|
html += "<th>" + series.label + "</th>";
|
|
});
|
|
html += "<th>Total</th><th>Active accounts</th></tr></thead><tbody>";
|
|
months.forEach(function (month) {
|
|
html += "<tr><td>" + month.label + "</td>";
|
|
SERIES.forEach(function (series) {
|
|
html += "<td>" + formatNumber(month[series.key]) + "</td>";
|
|
});
|
|
html += "<td>" + formatNumber(month.total_posts) + "</td>";
|
|
html += "<td>" + formatNumber(month.active_accounts) + "</td></tr>";
|
|
});
|
|
html += "</tbody>";
|
|
table.innerHTML = html;
|
|
}
|
|
|
|
function init() {
|
|
fetch(RELAY_STATS_URL)
|
|
.then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error("Relay responded with " + response.status);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(function (payload) {
|
|
if (payload.type !== "Success" || !payload.data) {
|
|
return;
|
|
}
|
|
var months = buildMonths(payload.data.years || {});
|
|
if (!months.length) {
|
|
return;
|
|
}
|
|
var section = document.getElementById("network-stats");
|
|
section.hidden = false;
|
|
renderTiles(section, payload.data.global || {});
|
|
renderChart(months);
|
|
renderTable(months);
|
|
})
|
|
.catch(function () {
|
|
/* Relay unreachable: leave the section hidden. */
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|