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;
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 : 320 px ;
855+ max-width : 360 px ;
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). */
17451745function 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
17901787function 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 ) + ' → ' + 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 ) + ' → ' + 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
20562096function handleTrackClick ( e ) {
0 commit comments