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:
Edward Betts 2026-03-14 10:47:46 +00:00
parent feaefba03c
commit 20f1e31119
2 changed files with 167 additions and 6 deletions

View file

@ -9,21 +9,109 @@
<style>
.grid-container {
display: grid;
grid-template-columns: repeat({{ column_count }}, auto); /* 7 columns for each piece of information */
grid-template-columns: repeat({{ column_count }}, auto);
gap: 10px;
justify-content: start;
}
.grid-item {
/* Additional styling for grid items can go here */
.heading {
grid-column: 1 / {{ column_count + 1 }};
}
.heading {
grid-column: 1 / {{ column_count + 1 }}; /* Spans from the 1st line to the 7th line */
/* Timeline */
.conf-timeline {
position: relative;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
overflow: hidden;
user-select: none;
}
.conf-tl-month {
position: absolute;
top: 0;
height: 100%;
border-left: 1px solid #dee2e6;
pointer-events: none;
}
.conf-tl-month-label {
position: absolute;
top: 3px;
left: 3px;
font-size: 0.68em;
color: #6c757d;
white-space: nowrap;
}
.conf-tl-today {
position: absolute;
top: 22px;
height: calc(100% - 22px);
border-left: 2px solid #dc3545;
pointer-events: none;
z-index: 10;
}
.conf-tl-bar {
position: absolute;
height: 26px;
border-radius: 3px;
overflow: hidden;
z-index: 5;
}
.conf-tl-bar a, .conf-tl-bar span {
display: block;
padding: 4px 6px;
font-size: 0.72em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
color: white;
text-decoration: none;
}
</style>
{% endblock %}
{% set tl_colors = ["#0d6efd","#198754","#dc3545","#fd7e14","#6f42c1","#20c997","#0dcaf0","#d63384"] %}
{% macro render_timeline(timeline) %}
{% if timeline %}
{% set bar_h = 26 %}
{% set row_h = 32 %}
{% set header_h = 22 %}
{% set total_h = timeline.lane_count * row_h + header_h %}
<div class="mb-4">
<h3>Next 90 days</h3>
<div class="conf-timeline" style="height: {{ total_h }}px;">
{# Month markers #}
{% for m in timeline.months %}
<div class="conf-tl-month" style="left: {{ m.left_pct }}%;">
<span class="conf-tl-month-label">{{ m.label }}</span>
</div>
{% endfor %}
{# Today marker (always at left: 0) #}
<div class="conf-tl-today" style="left: 0;"></div>
{# Conference bars #}
{% for conf in timeline.confs %}
{% set color = tl_colors[conf.lane % tl_colors | length] %}
{% set top_px = conf.lane * row_h + header_h %}
<div class="conf-tl-bar"
style="left: {{ conf.left_pct }}%; width: {{ conf.width_pct }}%; top: {{ top_px }}px; background: {{ color }};"
title="{{ conf.label }}">
{% if conf.url %}
<a href="{{ conf.url }}">{{ conf.name }}</a>
{% else %}
<span>{{ conf.name }}</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro section(heading, item_list, badge) %}
{% if item_list %}
<div class="heading">
@ -49,6 +137,9 @@
<div class="container-fluid mt-2">
<h1>Conferences</h1>
{{ render_timeline(timeline) }}
<div class="grid-container">
{{ section("Current", current, "attending") }}
{{ section("Future", future, "going") }}