Add bidirectional hover highlight between Gantt chart and table

Each Gantt bar and table row gets a data-conf-key attribute
(ISO start date + conference name). A small JS lookup map connects
them so hovering either element highlights both simultaneously:
- Gantt bar: filter brightness + inset white box-shadow
- Table row: yellow tint via background-color

Hovering a Gantt bar also scrolls the matching table row into view
(scrollIntoView nearest) so future conferences are reachable without
manual scrolling. The key field is pre-computed in
build_conference_timeline() to keep the template simple.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-03-14 11:02:55 +00:00
parent ef517c98ff
commit 10716f9874
2 changed files with 42 additions and 2 deletions

View file

@ -57,6 +57,16 @@
text-decoration: none;
}
/* Bidirectional hover highlight */
.conf-tl-bar.conf-hl {
filter: brightness(1.25);
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8);
z-index: 15;
}
tr.conf-hl > td {
background-color: rgba(255, 193, 7, 0.25) !important;
}
/* Conference table */
.conf-month-row td {
background: #e9ecef !important;
@ -99,7 +109,8 @@
{% 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 }}">
title="{{ conf.label }}"
data-conf-key="{{ conf.key }}">
{% if conf.url %}<a href="{{ conf.url }}">{{ conf.name }}</a>
{% else %}<span>{{ conf.name }}</span>{% endif %}
</div>
@ -143,7 +154,7 @@
<td colspan="6">{{ month_label }}</td>
</tr>
{% endif %}
<tr{% if item.going %} class="conf-going"{% endif %}>
<tr{% if item.going %} class="conf-going"{% endif %} data-conf-key="{{ item.start_date.isoformat() }}|{{ item.name }}">
<td class="text-nowrap text-muted small">
{%- if item.start_date == item.end_date -%}
{{ item.start_date.strftime("%-d %b %Y") }}
@ -213,4 +224,32 @@
{{ conf_table("Past", past|reverse|list, "went") }}
</div>
<script>
(function () {
// Build a map from conf-key → all matching elements (bars + rows)
const map = new Map();
document.querySelectorAll('[data-conf-key]').forEach(el => {
const k = el.dataset.confKey;
if (!map.has(k)) map.set(k, []);
map.get(k).push(el);
});
function setHighlight(key, on) {
(map.get(key) || []).forEach(el => el.classList.toggle('conf-hl', on));
// When hovering a Gantt bar, scroll the matching table row into view
if (on) {
const row = (map.get(key) || []).find(el => el.tagName === 'TR');
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
map.forEach((els, key) => {
els.forEach(el => {
el.addEventListener('mouseenter', () => setHighlight(key, true));
el.addEventListener('mouseleave', () => setHighlight(key, false));
});
});
})();
</script>
{% endblock %}

View file

@ -482,6 +482,7 @@ def build_conference_timeline(
"lane": lane,
"left_pct": left_pct,
"width_pct": width_pct,
"key": f"{conf['start_date'].isoformat()}|{conf['name']}",
"label": (
f"{conf['name']}"
f" ({conf['start_date'].strftime('%-d %b')}"