Conferences
+
+ {{ render_timeline(timeline) }}
+
{{ section("Current", current, "attending") }}
{{ section("Future", future, "going") }}
diff --git a/web_view.py b/web_view.py
index 4bd5203..acdf4d2 100755
--- a/web_view.py
+++ b/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),