Add Gantt-style timeline to conference list page
Shows the next 90 days as a horizontal bar chart above the conference list. A greedy interval-colouring algorithm assigns each conference to a lane so overlapping conferences appear in separate rows. Lane colour cycles through a fixed palette so simultaneous events are easy to tell apart. Month boundary markers and a red today-line provide orientation. All position maths happens in build_conference_timeline() in Python; the Jinja template is pure rendering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
feaefba03c
commit
20f1e31119
2 changed files with 167 additions and 6 deletions
70
web_view.py
70
web_view.py
|
|
@ -440,6 +440,73 @@ def _conference_description(conf: StrDict) -> str:
|
|||
return "\n".join(lines) if lines else "Conference"
|
||||
|
||||
|
||||
def build_conference_timeline(
|
||||
current: list[StrDict], future: list[StrDict], today: date, days: int = 90
|
||||
) -> dict | None:
|
||||
"""Build data for a Gantt-style timeline of upcoming conferences."""
|
||||
timeline_start = today
|
||||
timeline_end = today + timedelta(days=days)
|
||||
|
||||
visible = [
|
||||
c
|
||||
for c in (current + future)
|
||||
if c["start_date"] <= timeline_end and c["end_date"] >= today
|
||||
]
|
||||
if not visible:
|
||||
return None
|
||||
|
||||
visible.sort(key=lambda c: c["start_date"])
|
||||
|
||||
# Greedy interval-coloring: assign each conference a lane (row)
|
||||
lane_ends: list[date] = []
|
||||
conf_data = []
|
||||
for conf in visible:
|
||||
lane = next(
|
||||
(i for i, end in enumerate(lane_ends) if end < conf["start_date"]),
|
||||
len(lane_ends),
|
||||
)
|
||||
if lane == len(lane_ends):
|
||||
lane_ends.append(conf["end_date"])
|
||||
else:
|
||||
lane_ends[lane] = conf["end_date"]
|
||||
|
||||
start_off = max((conf["start_date"] - timeline_start).days, 0)
|
||||
end_off = min((conf["end_date"] - timeline_start).days + 1, days)
|
||||
left_pct = round(start_off / days * 100, 2)
|
||||
width_pct = max(round((end_off - start_off) / days * 100, 2), 0.5)
|
||||
|
||||
conf_data.append(
|
||||
{
|
||||
"name": conf["name"],
|
||||
"url": conf.get("url"),
|
||||
"lane": lane,
|
||||
"left_pct": left_pct,
|
||||
"width_pct": width_pct,
|
||||
"label": (
|
||||
f"{conf['name']}"
|
||||
f" ({conf['start_date'].strftime('%-d %b')}–"
|
||||
f"{conf['end_date'].strftime('%-d %b')})"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Month markers for x-axis labels
|
||||
months = []
|
||||
d = today.replace(day=1)
|
||||
while d <= timeline_end:
|
||||
off = max((d - timeline_start).days, 0)
|
||||
months.append({"label": d.strftime("%b %Y"), "left_pct": round(off / days * 100, 2)})
|
||||
# advance to next month
|
||||
d = (d.replace(day=28) + timedelta(days=4)).replace(day=1)
|
||||
|
||||
return {
|
||||
"confs": conf_data,
|
||||
"lane_count": len(lane_ends),
|
||||
"months": months,
|
||||
"days": days,
|
||||
}
|
||||
|
||||
|
||||
def build_conference_ical(items: list[StrDict]) -> bytes:
|
||||
"""Build iCalendar feed for all conferences."""
|
||||
lines = [
|
||||
|
|
@ -488,10 +555,13 @@ def conference_list() -> str:
|
|||
]
|
||||
future = [conf for conf in items if conf["start_date"] > today]
|
||||
|
||||
timeline = build_conference_timeline(current, future, today)
|
||||
|
||||
return flask.render_template(
|
||||
"conference_list.html",
|
||||
current=current,
|
||||
future=future,
|
||||
timeline=timeline,
|
||||
today=today,
|
||||
get_country=agenda.get_country,
|
||||
fx_rate=agenda.fx.get_rates(app.config),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue