From d11e980a6aa07d2e1a0e7db1d769bf0242068721 Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Sat, 23 May 2026 11:52:13 +0200 Subject: [PATCH] fix(webui): timeline opens on full landscape, drop undated points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the chronological-order fix the page opened on the earliest month (1995-12), showing only a handful of same-colored dots while the legend listed every category — looked broken. - Initialise the plot on the LAST frame (full landscape); Play now replays the build-up from the start (fromcurrent: false), slider starts at the end. - Make _extract_month robust: year-only / junk dates (ISO/ETSI 'time' like '2015/CD Amd 2', bare '2023', '') no longer yield malformed month labels ('2015-/C') or a garbled 'unknown' frame badge. - Drop undated docs from the temporal animation (they remain on /landscape). At the full initial frame every category has points, so the legend matches what is drawn. --- src/webui/data/_shared.py | 15 +++++++++------ src/webui/data/analysis.py | 2 ++ src/webui/templates/timeline.html | 17 ++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/webui/data/_shared.py b/src/webui/data/_shared.py index 9ab8e57..c7fc58f 100644 --- a/src/webui/data/_shared.py +++ b/src/webui/data/_shared.py @@ -20,14 +20,17 @@ _CACHE_TTL = 300 # 5 minutes def _extract_month(time_str: str | None) -> str: - """Normalize a date string to YYYY-MM format.""" + """Normalize a date string to YYYY-MM (or "unknown" if unparseable).""" if not time_str: return "unknown" - if len(time_str) >= 7 and time_str[4] == '-': - return time_str[:7] # Already YYYY-MM-DD - if len(time_str) >= 6 and time_str[:4].isdigit(): - return time_str[:4] + '-' + time_str[4:6] # YYYYMMDD → YYYY-MM - return time_str[:7] + s = time_str.strip() + if len(s) >= 7 and s[4] == '-' and s[5:7].isdigit(): + return s[:7] # ISO 8601: YYYY-MM-DD... + if len(s) >= 6 and s[:6].isdigit(): + return s[:4] + '-' + s[4:6] # Compact: YYYYMMDD → YYYY-MM + if len(s) >= 4 and s[:4].isdigit(): + return s[:4] + '-01' # Year-only / year-prefixed junk (e.g. "2015/CD Amd 2") + return "unknown" def _cached(key: str, fn, ttl: float = _CACHE_TTL): """Return cached result or compute and cache it.""" diff --git a/src/webui/data/analysis.py b/src/webui/data/analysis.py index 71c30cd..0a6af66 100644 --- a/src/webui/data/analysis.py +++ b/src/webui/data/analysis.py @@ -546,6 +546,8 @@ def _compute_timeline_animation_data(db: Database) -> dict: r = rating_map[name] d = draft_map.get(name) month = _extract_month(d.time if d else None) + if month == "unknown": + continue # Undated docs (e.g. ISO/ETSI) can't be placed on a temporal animation cat = r.categories[0] if r.categories else "Other" month_set.add(month) category_monthly[month][cat] += 1 diff --git a/src/webui/templates/timeline.html b/src/webui/templates/timeline.html index 3e8eb3a..2cb20c0 100644 --- a/src/webui/templates/timeline.html +++ b/src/webui/templates/timeline.html @@ -141,9 +141,12 @@ if (points.length > 0 && months.length > 0) { }; }); - // --- Initial plot (first month) --- - const firstTraces = buildTraces(months[0]); - const firstCount = points.filter(p => p.month <= months[0]).length; + // --- Initial plot: show the FULL landscape (last frame) so the page + // doesn't open on the near-empty earliest month. Play replays from + // the start to animate the build-up. --- + const initMonth = months[months.length - 1]; + const initTraces = buildTraces(initMonth); + const initCount = points.length; // Slider steps const sliderSteps = months.map(month => ({ @@ -165,7 +168,7 @@ if (points.length > 0 && months.length > 0) { { label: '▶ Play', method: 'animate', - args: [null, { frame: { duration: 500, redraw: true }, transition: { duration: 300 }, fromcurrent: true }] + args: [null, { frame: { duration: 500, redraw: true }, transition: { duration: 300 }, fromcurrent: false }] }, { label: '◼ Pause', @@ -175,7 +178,7 @@ if (points.length > 0 && months.length > 0) { ] }], sliders: [{ - active: 0, + active: months.length - 1, steps: sliderSteps, x: 0.05, len: 0.9, xanchor: 'left', @@ -194,13 +197,13 @@ if (points.length > 0 && months.length > 0) { }], }; - Plotly.newPlot('tsneAnim', firstTraces, layout, CFG).then(() => { + Plotly.newPlot('tsneAnim', initTraces, layout, CFG).then(() => { Plotly.addFrames('tsneAnim', frames); }); // Update badge on animation frame const badge = document.querySelector('#monthBadge span'); - badge.textContent = `${fmtMonth(months[0])} — ${firstCount} drafts`; + badge.textContent = `${fmtMonth(initMonth)} — ${initCount} drafts`; document.getElementById('tsneAnim').on('plotly_animatingframe', function(ev) { const month = ev.name;