Compare commits
2 Commits
6538cbebaf
...
d11e980a6a
| Author | SHA1 | Date | |
|---|---|---|---|
| d11e980a6a | |||
| c3af38e0f9 |
@@ -20,14 +20,17 @@ _CACHE_TTL = 300 # 5 minutes
|
|||||||
|
|
||||||
|
|
||||||
def _extract_month(time_str: str | None) -> str:
|
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:
|
if not time_str:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
if len(time_str) >= 7 and time_str[4] == '-':
|
s = time_str.strip()
|
||||||
return time_str[:7] # Already YYYY-MM-DD
|
if len(s) >= 7 and s[4] == '-' and s[5:7].isdigit():
|
||||||
if len(time_str) >= 6 and time_str[:4].isdigit():
|
return s[:7] # ISO 8601: YYYY-MM-DD...
|
||||||
return time_str[:4] + '-' + time_str[4:6] # YYYYMMDD → YYYY-MM
|
if len(s) >= 6 and s[:6].isdigit():
|
||||||
return time_str[:7]
|
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):
|
def _cached(key: str, fn, ttl: float = _CACHE_TTL):
|
||||||
"""Return cached result or compute and cache it."""
|
"""Return cached result or compute and cache it."""
|
||||||
|
|||||||
@@ -546,6 +546,8 @@ def _compute_timeline_animation_data(db: Database) -> dict:
|
|||||||
r = rating_map[name]
|
r = rating_map[name]
|
||||||
d = draft_map.get(name)
|
d = draft_map.get(name)
|
||||||
month = _extract_month(d.time if d else None)
|
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"
|
cat = r.categories[0] if r.categories else "Other"
|
||||||
month_set.add(month)
|
month_set.add(month)
|
||||||
category_monthly[month][cat] += 1
|
category_monthly[month][cat] += 1
|
||||||
@@ -559,6 +561,12 @@ def _compute_timeline_animation_data(db: Database) -> dict:
|
|||||||
"month": month,
|
"month": month,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Deliver points in chronological order so the front-end's cumulative
|
||||||
|
# filter (p.month <= frame) is append-only. Otherwise new points get
|
||||||
|
# inserted mid-array and Plotly's index-based frame transition animates
|
||||||
|
# existing markers flying to other drafts' coordinates ("jumping points").
|
||||||
|
points.sort(key=lambda p: (p["month"], p["name"]))
|
||||||
|
|
||||||
months = sorted(month_set)
|
months = sorted(month_set)
|
||||||
# Convert defaultdict to plain dict for JSON
|
# Convert defaultdict to plain dict for JSON
|
||||||
cat_monthly_plain = {m: dict(cats) for m, cats in category_monthly.items()}
|
cat_monthly_plain = {m: dict(cats) for m, cats in category_monthly.items()}
|
||||||
|
|||||||
@@ -141,9 +141,12 @@ if (points.length > 0 && months.length > 0) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initial plot (first month) ---
|
// --- Initial plot: show the FULL landscape (last frame) so the page
|
||||||
const firstTraces = buildTraces(months[0]);
|
// doesn't open on the near-empty earliest month. Play replays from
|
||||||
const firstCount = points.filter(p => p.month <= months[0]).length;
|
// the start to animate the build-up. ---
|
||||||
|
const initMonth = months[months.length - 1];
|
||||||
|
const initTraces = buildTraces(initMonth);
|
||||||
|
const initCount = points.length;
|
||||||
|
|
||||||
// Slider steps
|
// Slider steps
|
||||||
const sliderSteps = months.map(month => ({
|
const sliderSteps = months.map(month => ({
|
||||||
@@ -165,7 +168,7 @@ if (points.length > 0 && months.length > 0) {
|
|||||||
{
|
{
|
||||||
label: '▶ Play',
|
label: '▶ Play',
|
||||||
method: 'animate',
|
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',
|
label: '◼ Pause',
|
||||||
@@ -175,7 +178,7 @@ if (points.length > 0 && months.length > 0) {
|
|||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
sliders: [{
|
sliders: [{
|
||||||
active: 0,
|
active: months.length - 1,
|
||||||
steps: sliderSteps,
|
steps: sliderSteps,
|
||||||
x: 0.05, len: 0.9,
|
x: 0.05, len: 0.9,
|
||||||
xanchor: 'left',
|
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);
|
Plotly.addFrames('tsneAnim', frames);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update badge on animation frame
|
// Update badge on animation frame
|
||||||
const badge = document.querySelector('#monthBadge span');
|
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) {
|
document.getElementById('tsneAnim').on('plotly_animatingframe', function(ev) {
|
||||||
const month = ev.name;
|
const month = ev.name;
|
||||||
|
|||||||
Reference in New Issue
Block a user