-
Notifications
You must be signed in to change notification settings - Fork 0
feat: interactive Plotly scatter charts for HTML dashboard #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,18 +172,25 @@ def render_html_dashboard( | |
| if memory_json and memory_json.exists(): | ||
| memory = json.loads(memory_json.read_text()) | ||
|
|
||
| svgs: dict[str, str] = {} | ||
| # Load SVGs from scatter_dir | ||
| heatmap_svg: str | None = None | ||
| scatter_svgs: dict[str, str] = {} | ||
| if scatter_dir: | ||
| for name in ("scatter_tiers", "scatter_features", "heatmap"): | ||
| svg_path = scatter_dir / f"{name}.svg" | ||
| for name, prefix in [ | ||
| ("heatmap.svg", "heatmap-"), | ||
| ("scatter_tiers.svg", "sctier-"), | ||
| ("scatter_features.svg", "scfeat-"), | ||
| ]: | ||
| svg_path = scatter_dir / name | ||
| if svg_path.exists(): | ||
| svgs[name] = _namespace_svg_ids(svg_path.read_text(), f"{name}-") | ||
| scatter_svgs[name] = _namespace_svg_ids(svg_path.read_text(), prefix) | ||
| heatmap_svg = scatter_svgs.get("heatmap.svg") | ||
|
|
||
| body_parts = [ | ||
| _section_nav(has_memory=memory is not None), | ||
| _section_overview(fidelity, perf), | ||
| _section_matrix(fidelity), | ||
| _section_scatter(svgs), | ||
| _section_scatter(fidelity, perf, heatmap_svg, scatter_svgs), | ||
| _section_comparison(fidelity, perf), | ||
| _section_features(fidelity), | ||
| _section_performance(perf), | ||
|
|
@@ -227,11 +234,11 @@ def render_html_dashboard( | |
| background:var(--bg);color:var(--text);font-size:14px;line-height:1.5} | ||
| a{color:var(--accent);text-decoration:none} | ||
| a:hover{text-decoration:underline;color:#80bfff} | ||
| .container{max-width:1440px;margin:0 auto;padding:1.5rem 1.5rem} | ||
| .container{max-width:2800px;margin:0 auto;padding:1.5rem 3rem} | ||
|
|
||
| /* ── Nav ── */ | ||
| nav{position:sticky;top:0;z-index:100;background:linear-gradient(135deg,#0a0a0a,#191919); | ||
| padding:.6rem 1.5rem;display:flex;align-items:center;gap:1.5rem; | ||
| padding:.6rem 3rem;display:flex;align-items:center;gap:1.5rem; | ||
| box-shadow:0 2px 12px rgba(0,0,0,.4);border-bottom:1px solid #2d2d2d} | ||
| nav .brand{font-weight:700;font-size:1.1rem;color:#ededed;letter-spacing:-.02em} | ||
| nav .links{display:flex;gap:1rem;flex-wrap:wrap} | ||
|
|
@@ -381,6 +388,28 @@ def render_html_dashboard( | |
| .wolfxl-hero .wolf-text p{margin:.2rem 0 0;font-size:.82rem;color:#fbbf24;line-height:1.4} | ||
| .wolfxl-hero .wolf-text a{color:#fb923c;font-weight:600} | ||
|
|
||
| /* ── Interactive chart containers ── */ | ||
| .chart-maximize-wrap{position:relative;margin-bottom:1rem} | ||
| .chart-maximize-btn{position:absolute;top:6px;right:6px;z-index:10; | ||
| background:rgba(25,25,25,.85);border:1px solid #2d2d2d;border-radius:6px; | ||
| color:#a0a0a0;font-size:1rem;padding:.25rem .5rem;cursor:pointer; | ||
| transition:all .15s;line-height:1} | ||
| .chart-maximize-btn:hover{background:#282828;color:#ededed;border-color:#51a8ff} | ||
| .plotly-chart{width:100%;overflow-x:auto} | ||
| .plotly-chart .js-plotly-plot{width:100%!important} | ||
|
|
||
| /* ── Chart maximize modal ── */ | ||
| .chart-modal-overlay{display:none;position:fixed;inset:0;z-index:9999; | ||
| background:rgba(0,0,0,.88);align-items:center;justify-content:center;padding:2vh 2vw} | ||
| .chart-modal-overlay.active{display:flex} | ||
| .chart-modal-content{width:96vw;height:92vh;background:#0a0a0a;border-radius:12px; | ||
| border:1px solid #2d2d2d;position:relative;overflow:hidden} | ||
| .chart-modal-close{position:absolute;top:10px;right:14px;z-index:10; | ||
| background:rgba(25,25,25,.9);border:1px solid #2d2d2d;border-radius:6px; | ||
| color:#a0a0a0;font-size:1.2rem;padding:.3rem .7rem;cursor:pointer;line-height:1} | ||
| .chart-modal-close:hover{background:#282828;color:#ededed;border-color:#ff6066} | ||
| .chart-modal-body{width:100%;height:100%;overflow:auto} | ||
|
|
||
| /* ── Misc ── */ | ||
| .btn{display:inline-block;padding:.3rem .8rem;border:1px solid #2d2d2d;border-radius:6px; | ||
| font-size:.78rem;cursor:pointer;background:#191919;color:#a0a0a0} | ||
|
|
@@ -403,6 +432,45 @@ def render_html_dashboard( | |
| .container{padding:1rem} | ||
| .filter-box input{width:100%} | ||
| } | ||
| @media(min-width:2000px){ | ||
| body{font-size:16px} | ||
| .container{padding:2rem 4rem} | ||
| nav{padding:.7rem 4rem;gap:2rem} | ||
| nav .brand{font-size:1.3rem} | ||
| nav .links a{font-size:.95rem} | ||
| h1{font-size:2rem} | ||
| h2{font-size:1.6rem} | ||
| h3{font-size:1.25rem} | ||
| .stat-card .val{font-size:2.8rem} | ||
| .stat-card .lbl{font-size:.95rem} | ||
| .stat-card{padding:1.6rem} | ||
| table{font-size:.92rem} | ||
| th,td{padding:.55rem .75rem} | ||
| .matrix th,.matrix td{padding:.45rem .65rem;min-width:64px;font-size:.88rem} | ||
| .matrix .feat{min-width:160px} | ||
| details summary{font-size:1rem;padding:.85rem 1.2rem} | ||
| .meta-bar{font-size:.92rem} | ||
| .wolfxl-hero{padding:1.5rem 2rem} | ||
| .wolfxl-hero .wolf-text h3{font-size:1.2rem} | ||
| .wolfxl-hero .wolf-text p{font-size:.95rem} | ||
| .filter-box input{width:360px;font-size:.95rem;padding:.5rem .9rem} | ||
| } | ||
| @media(min-width:3000px){ | ||
| body{font-size:18px} | ||
| .container{padding:2.5rem 5rem} | ||
| nav{padding:.8rem 5rem} | ||
| nav .brand{font-size:1.5rem} | ||
| h1{font-size:2.4rem} | ||
| h2{font-size:1.9rem} | ||
| h3{font-size:1.45rem} | ||
| .stat-card .val{font-size:3.4rem} | ||
| .stat-card .lbl{font-size:1.05rem} | ||
| table{font-size:1rem} | ||
| th,td{padding:.6rem .85rem} | ||
| .matrix th,.matrix td{padding:.5rem .75rem;min-width:76px;font-size:.95rem} | ||
| .matrix .feat{min-width:180px} | ||
| details summary{font-size:1.1rem} | ||
| } | ||
| """ | ||
|
|
||
| # ==================================================================== | ||
|
|
@@ -469,6 +537,55 @@ def render_html_dashboard( | |
| btn.textContent=open?'Expand All':'Collapse All'; | ||
| }); | ||
| }); | ||
| /* Chart maximize modal — all content is same-origin Plotly output, no untrusted data */ | ||
| (function(){ | ||
| var overlay=document.createElement('div'); | ||
| overlay.className='chart-modal-overlay'; | ||
| var content=document.createElement('div'); | ||
| content.className='chart-modal-content'; | ||
| var closeBtn=document.createElement('button'); | ||
| closeBtn.className='chart-modal-close'; | ||
| closeBtn.title='Close'; | ||
| closeBtn.textContent='\u00d7'; | ||
| var modalBody=document.createElement('div'); | ||
| modalBody.className='chart-modal-body'; | ||
| content.appendChild(closeBtn); | ||
| content.appendChild(modalBody); | ||
| overlay.appendChild(content); | ||
| document.body.appendChild(overlay); | ||
| var closeModal=function(){ | ||
| overlay.classList.remove('active'); | ||
| while(modalBody.firstChild)modalBody.removeChild(modalBody.firstChild); | ||
| }; | ||
| closeBtn.addEventListener('click',closeModal); | ||
| overlay.addEventListener('click',function(e){if(e.target===overlay)closeModal();}); | ||
| document.addEventListener('keydown',function(e){if(e.key==='Escape')closeModal();}); | ||
| document.querySelectorAll('.chart-maximize-btn').forEach(function(btn){ | ||
| btn.addEventListener('click',function(){ | ||
| var wrap=btn.closest('.chart-maximize-wrap'); | ||
| var chart=wrap?wrap.querySelector('.plotly-chart'):null; | ||
| if(!chart)return; | ||
| while(modalBody.firstChild)modalBody.removeChild(modalBody.firstChild); | ||
| /* Rebuild Plotly chart in modal with full interactivity */ | ||
| var srcPlot=chart.querySelector('.js-plotly-plot'); | ||
| if(srcPlot&&srcPlot.data&&srcPlot.layout&&window.Plotly){ | ||
| var plotDiv=document.createElement('div'); | ||
| plotDiv.style.width='100%'; | ||
| plotDiv.style.height='100%'; | ||
| modalBody.appendChild(plotDiv); | ||
| overlay.classList.add('active'); | ||
| var newLayout=Object.assign({},srcPlot.layout,{autosize:true, | ||
| height:content.clientHeight-20,width:content.clientWidth-20}); | ||
| window.Plotly.newPlot(plotDiv,srcPlot.data,newLayout, | ||
| {displayModeBar:true,displaylogo:false,responsive:true}); | ||
| }else{ | ||
| /* Fallback: clone static content */ | ||
| modalBody.appendChild(chart.cloneNode(true)); | ||
| overlay.classList.add('active'); | ||
| } | ||
| }); | ||
| }); | ||
| })(); | ||
| /* Smooth scroll */ | ||
| document.querySelectorAll('nav a[href^="#"]').forEach(a=>{ | ||
| a.addEventListener('click',e=>{ | ||
|
|
@@ -693,17 +810,57 @@ def _green(lib: str) -> int: | |
| return "\n".join(rows) | ||
|
|
||
|
|
||
| def _section_scatter(svgs: dict[str, str]) -> str: | ||
| if not svgs: | ||
| def _section_scatter( | ||
| fidelity: dict[str, Any], | ||
| perf: dict[str, Any] | None, | ||
| heatmap_svg: str | None, | ||
| scatter_svgs: dict[str, str] | None = None, | ||
| ) -> str: | ||
| if not perf and not heatmap_svg and not scatter_svgs: | ||
| return "" | ||
|
|
||
| parts = ['<section id="scatter" class="container"><h2>Fidelity vs. Throughput</h2>'] | ||
| for key, label in [ | ||
| ("scatter_tiers", "By Feature Group"), | ||
| ("scatter_features", "Per Feature"), | ||
| ("heatmap", "Heatmap"), | ||
| ]: | ||
| if key in svgs: | ||
| parts.append(f'<h3>{label}</h3><div class="svg-wrap">{svgs[key]}</div>') | ||
|
|
||
| # Interactive Plotly scatter charts (preferred when perf data exists) | ||
| if perf: | ||
| from excelbench.results.scatter_interactive import ( | ||
| render_interactive_scatter_features_from_data, | ||
| render_interactive_scatter_tiers_from_data, | ||
| ) | ||
|
|
||
| tiers_html = render_interactive_scatter_tiers_from_data(fidelity, perf) | ||
| features_html = render_interactive_scatter_features_from_data(fidelity, perf) | ||
|
|
||
| parts.append( | ||
| '<h3>By Feature Group</h3>' | ||
| f'<div class="chart-maximize-wrap">' | ||
| f'<button class="chart-maximize-btn" title="Maximize"' | ||
| f' aria-label="Maximize chart">\u26f6</button>' | ||
| f'<div class="plotly-chart">{tiers_html}</div></div>' | ||
| ) | ||
| parts.append( | ||
| '<h3>Per Feature</h3>' | ||
| f'<div class="chart-maximize-wrap">' | ||
| f'<button class="chart-maximize-btn" title="Maximize"' | ||
| f' aria-label="Maximize chart">\u26f6</button>' | ||
| f'<div class="plotly-chart">{features_html}</div></div>' | ||
| ) | ||
|
Comment on lines
+834
to
+847
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The HTML structure for wrapping the interactive charts is duplicated for both 'By Feature Group' and 'Per Feature'. To improve maintainability and reduce code duplication, consider extracting this repeated structure into a helper function. For example: def _create_chart_html(title: str, chart_content: str) -> str:
return (
f'<h3>{title}</h3>'
f'<div class="chart-maximize-wrap">'
f'<button class="chart-maximize-btn" title="Maximize"'
f' aria-label="Maximize chart">\u26f6</button>'
f'<div class="plotly-chart">{chart_content}</div></div>'
)
# ... inside _section_scatter ...
parts.append(_create_chart_html('By Feature Group', tiers_html))
parts.append(_create_chart_html('Per Feature', features_html)) |
||
| else: | ||
| # Fallback: embed pre-rendered static SVGs when perf data is unavailable | ||
| if scatter_svgs: | ||
| tiers_svg = scatter_svgs.get("scatter_tiers.svg") | ||
| feats_svg = scatter_svgs.get("scatter_features.svg") | ||
| if tiers_svg: | ||
| parts.append(f'<h3>By Feature Group</h3>' | ||
| f'<div class="svg-wrap">{tiers_svg}</div>') | ||
| if feats_svg: | ||
| parts.append(f'<h3>Per Feature</h3>' | ||
| f'<div class="svg-wrap">{feats_svg}</div>') | ||
|
|
||
| # Static heatmap SVG (always shown if available) | ||
| if heatmap_svg: | ||
| parts.append(f'<h3>Heatmap</h3><div class="svg-wrap">{heatmap_svg}</div>') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The application reads the content of |
||
|
|
||
| parts.append("</section>") | ||
| return "\n".join(parts) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
!importantin CSS can lead to maintenance issues as it makes styles difficult to override. It's often a sign that the CSS specificity needs to be adjusted.While it might be necessary here to override inline styles from Plotly, it's worth exploring alternatives first. Could you try increasing the selector's specificity? If
!importantis truly unavoidable, please add a comment explaining why it's needed.