Skip to content

Commit 0a5c5a2

Browse files
mbreiserclaude
andcommitted
feat: rich HTML tooltips on filmstrip blocks and lane SVG elements (#53)
- Filmstrip block tooltips now use HTML with colored hint line (light red) - Lane view SVG elements (controller bars, plugin circles, wait bars) show custom tooltips on hover with: - Command name (color-coded: green/blue/gray) - Absolute start/end times (e.g. "3s → 10s") - Full parameters for plugin commands - Mode and frame rate/gain for trialParams - Replaced native SVG <title> elements with data-tooltip + JS positioning - Plugin circles enlarged from r=4 to r=5 for easier hover targeting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bc57057 commit 0a5c5a2

1 file changed

Lines changed: 67 additions & 27 deletions

File tree

experiment_designer.html

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@
837837
max-width: 100%;
838838
}
839839

840-
/* Custom tooltip for filmstrip blocks (native title is unreliable with draggable) */
840+
/* Custom tooltip for filmstrip blocks and lane elements */
841841
.block-tooltip {
842842
display: none;
843843
position: fixed;
@@ -848,11 +848,11 @@
848848
padding: 0.5rem 0.7rem;
849849
font-size: 0.72rem;
850850
line-height: 1.45;
851-
white-space: pre;
851+
white-space: nowrap;
852852
z-index: 1000;
853853
pointer-events: none;
854854
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
855-
max-width: 320px;
855+
max-width: 360px;
856856
}
857857

858858
.timeline-block.dragging {
@@ -1150,7 +1150,7 @@ <h1>Experiment Designer</h1>
11501150

11511151
<!-- Footer -->
11521152
<div class="app-footer">
1153-
<p><a href="https://github.com/reiserlab/webDisplayTools" target="_blank">Reiser Lab</a> | Experiment Designer v0.8.2 | 2026-04-09 23:16 ET</p>
1153+
<p><a href="https://github.com/reiserlab/webDisplayTools" target="_blank">Reiser Lab</a> | Experiment Designer v0.8.3 | 2026-04-09 23:57 ET</p>
11541154
</div>
11551155

11561156
<!-- Hidden file input for import -->
@@ -1741,12 +1741,12 @@ <h1>Experiment Designer</h1>
17411741
return blocks;
17421742
}
17431743

1744-
/** Build a descriptive tooltip string for a filmstrip block (#53). */
1744+
/** Build an HTML tooltip for a filmstrip block (#53). */
17451745
function buildBlockTooltip(block) {
1746-
var lines = [block.label];
1747-
lines.push('Duration: ' + formatDuration(block.duration));
1746+
var h = '<div style="font-weight:600;">' + escapeHtml(block.label) + '</div>';
1747+
h += '<div>Duration: ' + formatDuration(block.duration) + '</div>';
17481748
if (block.pattern) {
1749-
lines.push('Pattern: ' + block.pattern.split('/').pop());
1749+
h += '<div>Pattern: ' + escapeHtml(block.pattern.split('/').pop()) + '</div>';
17501750
}
17511751
if (block.commands && block.commands.length > 0) {
17521752
var cmdSummary = [];
@@ -1774,17 +1774,14 @@ <h1>Experiment Designer</h1>
17741774
cmdSummary.push(cmd.command_name);
17751775
}
17761776
}
1777-
lines.push('Commands (' + block.commands.length + '):');
1777+
h += '<div style="margin-top:2px;">Commands (' + block.commands.length + '):</div>';
17781778
for (var ci = 0; ci < cmdSummary.length; ci++) {
1779-
lines.push(' ' + cmdSummary[ci]);
1779+
h += '<div style="padding-left:0.6em;">' + escapeHtml(cmdSummary[ci]) + '</div>';
17801780
}
17811781
}
1782-
if (block.type === 'condition') {
1783-
lines.push('Click to edit, drag to reorder');
1784-
} else {
1785-
lines.push('Click to edit');
1786-
}
1787-
return lines.join('\n');
1782+
var hint = block.type === 'condition' ? 'Click to edit, drag to reorder' : 'Click to edit';
1783+
h += '<div style="color:#ff6b6b;margin-top:3px;font-style:italic;">' + hint + '</div>';
1784+
return h;
17881785
}
17891786

17901787
function renderTimeline() {
@@ -1881,7 +1878,7 @@ <h1>Experiment Designer</h1>
18811878
tip.className = 'block-tooltip';
18821879
document.body.appendChild(tip);
18831880
}
1884-
tip.textContent = block.dataset.tooltip;
1881+
tip.innerHTML = block.dataset.tooltip;
18851882
tip.style.display = 'block';
18861883
var rect = block.getBoundingClientRect();
18871884
tip.style.left = Math.max(8, rect.left + rect.width / 2 - tip.offsetWidth / 2) + 'px';
@@ -1998,6 +1995,9 @@ <h1>Experiment Designer</h1>
19981995

19991996
var scale = ld.totalDuration > 0 ? blockW / ld.totalDuration : 0;
20001997

1998+
// Absolute time offset for this block (for tooltip display)
1999+
var absOff = bd.timeOffset;
2000+
20012001
// Controller spans
20022002
for (var cs = 0; cs < ld.controllerSpans.length; cs++) {
20032003
var span = ld.controllerSpans[cs];
@@ -2006,12 +2006,20 @@ <h1>Experiment Designer</h1>
20062006
var cy = laneIdx * (LANE_H + LANE_GAP) + LANE_H / 2;
20072007

20082008
if (span.startTime === span.endTime) {
2009-
svg += '<polygon points="' + sx + ',' + (cy - 5) + ' ' + (sx + 5) + ',' + cy + ' ' + sx + ',' + (cy + 5) + ' ' + (sx - 5) + ',' + cy + '" fill="#00e676" opacity="0.7">';
2010-
svg += '<title>' + escapeHtml(span.cmd.command_name || '') + '</title></polygon>';
2009+
var ctipI = '<div style="font-weight:600;color:#00e676;">' + escapeHtml(span.cmd.command_name || '') + '</div>'
2010+
+ '<div>Time: ' + formatDuration(absOff + span.startTime) + '</div>';
2011+
svg += '<polygon points="' + sx + ',' + (cy - 5) + ' ' + (sx + 5) + ',' + cy + ' ' + sx + ',' + (cy + 5) + ' ' + (sx - 5) + ',' + cy + '" fill="#00e676" opacity="0.7" class="lane-tip" data-tooltip="' + escapeAttr(ctipI) + '"/>';
20112012
} else {
20122013
var sw = (span.endTime - span.startTime) * scale;
2013-
svg += '<rect x="' + sx + '" y="' + (laneIdx * (LANE_H + LANE_GAP) + 3) + '" width="' + sw + '" height="' + (LANE_H - 6) + '" fill="#00e676" opacity="0.2" rx="2" stroke="#00e676" stroke-width="0.5">';
2014-
svg += '<title>trialParams: ' + escapeHtml((span.cmd.pattern || '').split('/').pop()) + ' | ' + span.cmd.duration + 's</title></rect>';
2014+
var mStr = span.cmd.mode === 4 ? 'closed-loop' : 'constant-rate';
2015+
var ctipD = '<div style="font-weight:600;color:#00e676;">trialParams</div>';
2016+
if (span.cmd.pattern) ctipD += '<div>Pattern: ' + escapeHtml((span.cmd.pattern || '').split('/').pop()) + '</div>';
2017+
ctipD += '<div>Duration: ' + span.cmd.duration + 's (' + formatDuration(absOff + span.startTime) + ' &rarr; ' + formatDuration(absOff + span.endTime) + ')</div>';
2018+
ctipD += '<div>Mode: ' + mStr;
2019+
if (span.cmd.mode === 4) ctipD += ', Gain: ' + (span.cmd.gain || 0);
2020+
else ctipD += ', FR: ' + (span.cmd.frame_rate || 60);
2021+
ctipD += '</div>';
2022+
svg += '<rect x="' + sx + '" y="' + (laneIdx * (LANE_H + LANE_GAP) + 3) + '" width="' + sw + '" height="' + (LANE_H - 6) + '" fill="#00e676" opacity="0.2" rx="2" stroke="#00e676" stroke-width="0.5" class="lane-tip" data-tooltip="' + escapeAttr(ctipD) + '"/>';
20152023
}
20162024
}
20172025

@@ -2025,12 +2033,19 @@ <h1>Experiment Designer</h1>
20252033
for (var ei = 0; ei < events.length; ei++) {
20262034
var evt = events[ei];
20272035
var ex = xOff + evt.time * scale;
2028-
svg += '<circle cx="' + ex + '" cy="' + pcy + '" r="4" fill="#4dabf7" opacity="0.7">';
2029-
var tip = pName + '.' + (evt.cmd.command_name || '');
2036+
var ptip = '<div style="font-weight:600;color:#4dabf7;">' + escapeHtml(pName) + '.' + escapeHtml(evt.cmd.command_name || '') + '</div>';
2037+
ptip += '<div>Time: ' + formatDuration(absOff + evt.time) + '</div>';
20302038
if (evt.cmd.params) {
2031-
tip += ' | ' + Object.keys(evt.cmd.params).map(function(k) { return k + '=' + evt.cmd.params[k]; }).join(', ');
2039+
var pvals = [];
2040+
var epKeys = Object.keys(evt.cmd.params);
2041+
for (var epk = 0; epk < epKeys.length; epk++) {
2042+
if (evt.cmd.params[epKeys[epk]] !== '' && evt.cmd.params[epKeys[epk]] !== null) {
2043+
pvals.push(escapeHtml(epKeys[epk]) + '=' + escapeHtml(String(evt.cmd.params[epKeys[epk]])));
2044+
}
2045+
}
2046+
if (pvals.length) ptip += '<div>Params: ' + pvals.join(', ') + '</div>';
20322047
}
2033-
svg += '<title>' + escapeHtml(tip) + '</title></circle>';
2048+
svg += '<circle cx="' + ex + '" cy="' + pcy + '" r="5" fill="#4dabf7" opacity="0.7" class="lane-tip" data-tooltip="' + escapeAttr(ptip) + '"/>';
20342049
}
20352050
}
20362051

@@ -2040,8 +2055,9 @@ <h1>Experiment Designer</h1>
20402055
var wx = xOff + ws.startTime * scale;
20412056
var ww = (ws.endTime - ws.startTime) * scale;
20422057
if (ww > 1) {
2043-
svg += '<rect x="' + wx + '" y="' + (waitLaneY + 2) + '" width="' + ww + '" height="' + (LANE_H - 6) + '" fill="#8b949e" opacity="0.15" rx="2" stroke="#8b949e" stroke-width="0.5">';
2044-
svg += '<title>wait ' + (ws.endTime - ws.startTime) + 's</title></rect>';
2058+
var wtip = '<div style="font-weight:600;color:#8b949e;">wait</div>';
2059+
wtip += '<div>Duration: ' + (ws.endTime - ws.startTime) + 's (' + formatDuration(absOff + ws.startTime) + ' &rarr; ' + formatDuration(absOff + ws.endTime) + ')</div>';
2060+
svg += '<rect x="' + wx + '" y="' + (waitLaneY + 2) + '" width="' + ww + '" height="' + (LANE_H - 6) + '" fill="#8b949e" opacity="0.15" rx="2" stroke="#8b949e" stroke-width="0.5" class="lane-tip" data-tooltip="' + escapeAttr(wtip) + '"/>';
20452061
}
20462062
}
20472063
}
@@ -2051,6 +2067,30 @@ <h1>Experiment Designer</h1>
20512067
// Render labels into fixed column, SVG into scrollable area (same scroll parent as blocks)
20522068
labelsEl.innerHTML = labelsHtml;
20532069
svgEl.innerHTML = svg;
2070+
2071+
// Custom tooltips on lane SVG elements (reuses #blockTooltip div)
2072+
svgEl.addEventListener('mouseover', function(e) {
2073+
var el = e.target.closest('.lane-tip');
2074+
if (!el || !el.dataset.tooltip) return;
2075+
var tip = document.getElementById('blockTooltip');
2076+
if (!tip) {
2077+
tip = document.createElement('div');
2078+
tip.id = 'blockTooltip';
2079+
tip.className = 'block-tooltip';
2080+
document.body.appendChild(tip);
2081+
}
2082+
tip.innerHTML = el.dataset.tooltip;
2083+
tip.style.display = 'block';
2084+
var rect = el.getBoundingClientRect();
2085+
tip.style.left = Math.max(8, rect.left + rect.width / 2 - tip.offsetWidth / 2) + 'px';
2086+
tip.style.top = (rect.top - tip.offsetHeight - 8) + 'px';
2087+
});
2088+
svgEl.addEventListener('mouseout', function(e) {
2089+
var el = e.target.closest('.lane-tip');
2090+
if (!el) return;
2091+
var tip = document.getElementById('blockTooltip');
2092+
if (tip) tip.style.display = 'none';
2093+
});
20542094
}
20552095

20562096
function handleTrackClick(e) {

0 commit comments

Comments
 (0)