Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"xlwt>=1.3.0",
# Visualization
"matplotlib>=3.7.0",
"plotly>=6.0",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -117,6 +118,8 @@ module = [
"matplotlib",
"matplotlib.*",
"numpy",
"plotly",
"plotly.*",
"numpy.*",
"pyumya",
"pyumya.*",
Expand Down
189 changes: 173 additions & 16 deletions src/excelbench/results/html_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of !important in 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 !important is truly unavoidable, please add a comment explaining why it's needed.


/* ── 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}
Expand All @@ -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}
}
"""

# ====================================================================
Expand Down Expand Up @@ -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=>{
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The application reads the content of heatmap.svg from the results directory and embeds it directly into the HTML dashboard without sanitization. If an attacker can control the content of this file (e.g., by providing a malicious results directory), they can achieve Cross-Site Scripting (XSS) when the dashboard is opened. It is recommended to sanitize the SVG content using a library like bleach or a dedicated SVG sanitizer before embedding it.


parts.append("</section>")
return "\n".join(parts)

Expand Down
2 changes: 1 addition & 1 deletion src/excelbench/results/scatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
(0, 50, "#1a0a0a", "0"), # dark red tint
(50, 80, "#1a1400", "1"), # dark amber tint
(80, 100, "#0c1f12", "2"), # dark green tint (score 2)
(100, 110, "#133d1f", "3"), # brighter green tint (score 3 — visually distinct)
(100, 110, "#0e2916", "3"), # subtle green tint (score 3 — distinct but gridline-friendly)
]


Expand Down
Loading