From 5afed0951b2edeb1fcef6470268ce123d4ebea61 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Tue, 21 Apr 2026 09:51:52 -0700 Subject: [PATCH] fix(#860): cap channel timeline chart to top 8 by volume (#864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & Why The "Messages / Hour by Channel" chart on `/#/analytics` Channels tab rendered all channels in both the SVG and legend, causing legend overflow when 20+ channels are present. ## Fix - Sort channels by total message volume (descending) - Render only the top 8 in the chart and legend - Show "+N more" in the legend when channels are truncated - `maxCount` for Y-axis scaling is computed from visible channels only, so the chart uses its full vertical range Single-file change: `public/analytics.js` — only `renderChannelTimeline()` modified. No shared helpers touched. Fixes #860 Co-authored-by: you --- public/analytics.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/public/analytics.js b/public/analytics.js index 27eb823d..57f01ee6 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -840,29 +840,44 @@ } } + var CHANNEL_TIMELINE_MAX_SERIES = 8; + function renderChannelTimeline(data) { if (!data.length) return '
No data
'; var hours = []; var hourSet = {}; var channelList = []; var channelSet = {}; var lookup = {}; - var maxCount = 1; + var channelVolume = {}; for (var i = 0; i < data.length; i++) { var d = data[i]; if (!hourSet[d.hour]) { hourSet[d.hour] = 1; hours.push(d.hour); } if (!channelSet[d.channel]) { channelSet[d.channel] = 1; channelList.push(d.channel); } lookup[d.hour + '|' + d.channel] = d.count; - if (d.count > maxCount) maxCount = d.count; + channelVolume[d.channel] = (channelVolume[d.channel] || 0) + d.count; } hours.sort(); + // Sort channels by total volume descending, cap to top N + channelList.sort(function(a, b) { return channelVolume[b] - channelVolume[a]; }); + var hiddenCount = Math.max(0, channelList.length - CHANNEL_TIMELINE_MAX_SERIES); + var visibleChannels = channelList.slice(0, CHANNEL_TIMELINE_MAX_SERIES); + + var maxCount = 1; + for (var vi = 0; vi < visibleChannels.length; vi++) { + for (var hi2 = 0; hi2 < hours.length; hi2++) { + var c = lookup[hours[hi2] + '|' + visibleChannels[vi]] || 0; + if (c > maxCount) maxCount = c; + } + } + var colors = ['#ef4444','#22c55e','#3b82f6','#f59e0b','#8b5cf6','#ec4899','#14b8a6','#64748b']; var w = 600, h = 180, pad = 35; var xScale = (w - pad * 2) / Math.max(hours.length - 1, 1); var yScale = (h - pad * 2) / maxCount; var svg = 'Channel message activity over time'; - for (var ci = 0; ci < channelList.length; ci++) { + for (var ci = 0; ci < visibleChannels.length; ci++) { var pts = []; for (var hi = 0; hi < hours.length; hi++) { - var count = lookup[hours[hi] + '|' + channelList[ci]] || 0; + var count = lookup[hours[hi] + '|' + visibleChannels[ci]] || 0; var x = pad + hi * xScale; var y = h - pad - count * yScale; pts.push(x + ',' + y); @@ -876,8 +891,11 @@ } svg += ''; var legendParts = []; - for (var lci = 0; lci < channelList.length; lci++) { - legendParts.push('' + esc(channelList[lci]) + ''); + for (var lci = 0; lci < visibleChannels.length; lci++) { + legendParts.push('' + esc(visibleChannels[lci]) + ''); + } + if (hiddenCount > 0) { + legendParts.push('+' + hiddenCount + ' more'); } svg += '
' + legendParts.join('') + '
'; return svg;