From 84b7ce877d6ee2bd1d1c336fb47c26b56ac966bf Mon Sep 17 00:00:00 2001 From: Andrew Theurer Date: Thu, 23 Apr 2026 14:46:27 -0400 Subject: [PATCH 1/5] Replace bar chart labels with iteration chips and horizontal scroll - Remove X-axis text labels, hierarchical group-by headers, and deep dive selection boxes/cards from compare view - Add iteration chips below bars aligned with bar positions using same layout structure as previous hier-row (60px label spacer) - Click chip to toggle deep dive selection (color-coded with theme) - Click bar to pin values legend (existing behavior preserved) - Horizontal scroll wrapper for chart area with min-width per iteration - Gap spacers match chart data gaps at flex:1 - Chips exclude group-by dimensions (shown only in toolbar) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web-ui/src/components/CompareView.jsx | 194 +++++------------- queries/cdmq/web-ui/src/index.css | 61 ++++++ 2 files changed, 113 insertions(+), 142 deletions(-) diff --git a/queries/cdmq/web-ui/src/components/CompareView.jsx b/queries/cdmq/web-ui/src/components/CompareView.jsx index 5a705984..513ba93c 100644 --- a/queries/cdmq/web-ui/src/components/CompareView.jsx +++ b/queries/cdmq/web-ui/src/components/CompareView.jsx @@ -1397,42 +1397,6 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set return (

{chart.metricName}

- {/* Deep dive selected iterations — display only, selection happens on bar chart */} - {deepDiveIterations && ci === 0 && deepDiveIterations.size > 0 && ( -
-
- Deep Dive Iterations ({deepDiveIterations.size}): - {deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS && ( - Max {MAX_DEEP_DIVE_ITERS} reached - )} -
-
- {Array.from(deepDiveIterations).map(function (itId, ii) { - var it = iterations.find(function (iter) { return iter.iterationId === itId; }); - if (!it) return null; - var themeColor = ITER_THEME_BASES[ii % ITER_THEME_BASES.length]; - var iterItems = buildIterItems(it, iterations, hiddenFields); - return ( -
- {iterItems.length > 0 ? iterItems.map(function (item, pi) { - var label = item.type === 'benchmark' ? item.val : item.names.join(',') + '=' + item.val; - return ( - - {item.type === 'tag' && {item.names.join(',')}} - {item.type === 'tag' ? '=' + item.val : label} - - ); - }) : {it.iterationId.substring(0, 8)}} - -
- ); - })} -
-
- )} {chart.commonItems.length > 0 && (
@@ -1653,7 +1617,8 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{chart.metricName}
-
+
+
{/* Toolbar: hidden dims, add, auto, clear — above headers */}
{hiddenFields.map(function (dim) { @@ -1682,115 +1647,12 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set )}
- {/* Hierarchical group-by headers with inline controls */} - {hasGroupBy(groupByList) && (function () { - var nonGaps = chart.data.filter(function (d) { return !d.isGap; }); - var iterMap = {}; - iterations.forEach(function (it) { iterMap[it.iterationId] = it; }); - var levels = []; - groupByList.forEach(function (dim, dimIdx) { - var spans = []; - var currentVal = null; - var currentCount = 0; - nonGaps.forEach(function (d) { - var origIter = iterMap[d.iterationId]; - var val = origIter ? getDimValue(origIter, dim) : ''; - if (val !== currentVal) { - if (currentVal !== null) spans.push({ value: formatDimValue(dim, currentVal), count: currentCount }); - currentVal = val; - currentCount = 0; - } - currentCount++; - }); - if (currentVal !== null) spans.push({ value: formatDimValue(dim, currentVal), count: currentCount }); - // Skip dimensions with only one span (single value across all iterations) - if (spans.length <= 1) return; - levels.push({ label: formatDimLabel(dim), dim: dim, dimIdx: dimIdx, spans: spans }); - }); - return levels.map(function (level, li) { - return ( -
-
{level.label}
-
- {level.spans.map(function (span, si2) { - return ( -
- {span.value} -
- ); - })} -
-
- {level.dimIdx > 0 && ( - - )} - {level.dimIdx < groupByList.length - 1 && ( - - )} - -
-
- ); - }); - })()} - {/* Deep dive iteration selection row — aligned with bars */} - {deepDiveIterations && (function () { - var nonGaps = chart.data.filter(function (d) { return !d.isGap; }); - var atLimit = deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; - return ( -
-
-
- {chart.data.map(function (d, di) { - if (d.isGap) return
; - var isSelected = deepDiveIterations.has(d.iterationId); - var selArr = Array.from(deepDiveIterations); - var themeIdx = isSelected ? selArr.indexOf(d.iterationId) : -1; - var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; - return ( -
- ); - })} -
-
← select up to 6 for deep dive
-
- ); - })()} - + + {/* Iteration chips below bars — uses same layout as hier-row */} +
+
+
+ {chart.data.map(function (d, di) { + if (d.isGap) return
; + var isPinned = resolvedPinnedEntry && resolvedPinnedEntry.entry && resolvedPinnedEntry.entry.iterationId === d.iterationId; + var ddSelected = deepDiveIterations && deepDiveIterations.has(d.iterationId); + var atLimit = deepDiveIterations && deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; + var themeIdx = ddSelected ? Array.from(deepDiveIterations).indexOf(d.iterationId) : -1; + var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; + // Build label from varying params not covered by group-by + var origIter = iterations.find(function (it) { return it.iterationId === d.iterationId; }); + var iterItems = origIter ? buildIterItems(origIter, iterations, hiddenFields) : []; + // Exclude group-by dimensions from the chip label + var groupBySet = new Set(groupByList); + var filteredItems = iterItems.filter(function (item) { + if (item.type === 'benchmark') return !groupBySet.has('benchmark'); + return !item.names.some(function (n) { return groupBySet.has((item.type === 'tag' ? 'tag:' : 'param:') + n); }); + }); + return ( +
+ {filteredItems.length > 0 ? filteredItems.map(function (item, pi) { + var label = item.type === 'benchmark' ? item.val : item.names.join(',') + '=' + item.val; + return ( + + {item.type === 'tag' && {item.names.join(',')}} + {item.type === 'tag' ? '=' + item.val : label} + + ); + }) : {d.name || (d.iterationId || '').substring(0, 8)}} +
+ ); + })} +
+
+
{hasOverlays ? (
diff --git a/queries/cdmq/web-ui/src/index.css b/queries/cdmq/web-ui/src/index.css index 3a39e66a..1215b8d1 100644 --- a/queries/cdmq/web-ui/src/index.css +++ b/queries/cdmq/web-ui/src/index.css @@ -1066,6 +1066,67 @@ a.run-id:hover { flex: 1; } +/* Scrollable chart area */ +.compare-chart-scroll { + overflow-x: auto; + scrollbar-width: thin; +} + +/* Iteration chips below bar chart */ +.compare-iter-chips-row { + display: flex; + flex: 1; + gap: 0; + align-items: flex-start; + min-width: 0; +} + +.compare-iter-chip-gap { + flex: 1; +} + +.compare-iter-chip-col { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 4px 2px; + border: 2px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + transition: border-color 0.15s, background-color 0.15s; + min-width: 0; + box-sizing: border-box; +} + +.compare-iter-chip-col:hover { + border-color: var(--accent); +} + +.compare-iter-chip-dd-on { + border-width: 2px; + font-weight: 600; +} + +.compare-iter-chip-param.param, +.compare-iter-chip-param.tag, +.compare-iter-chip-param.benchmark-badge { + white-space: normal; + overflow-wrap: break-word; + word-break: break-word; + font-size: 10px; + text-align: center; + max-width: 100%; +} + +.compare-iter-chip-id { + font-size: 10px; + font-family: 'SF Mono', ui-monospace, Consolas, monospace; + color: var(--text-muted); + text-align: center; +} + .compare-subtitle { display: flex; flex-wrap: wrap; From a16a93b9cbe2c3a5a6e55be8ef0f34598d6fe68d Mon Sep 17 00:00:00 2001 From: Andrew Theurer Date: Thu, 23 Apr 2026 22:53:03 -0400 Subject: [PATCH 2/5] Add CSS for spanning group chips and grid layout (WIP) CSS additions for the compare view chip layout rearchitecture: - Spanning group-by dimension label styling - Spanning group-by value chip styling with text wrapping - CSS Grid wrapper for chips below bar chart - Chip alignment work in progress (grid approach needed) Co-Authored-By: Claude Opus 4.6 (1M context) --- queries/cdmq/web-ui/src/index.css | 54 ++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/queries/cdmq/web-ui/src/index.css b/queries/cdmq/web-ui/src/index.css index 1215b8d1..d43c9831 100644 --- a/queries/cdmq/web-ui/src/index.css +++ b/queries/cdmq/web-ui/src/index.css @@ -1072,12 +1072,22 @@ a.run-id:hover { scrollbar-width: thin; } +/* CSS Grid for chips below bar chart */ +.compare-chips-grid-wrap { + overflow-x: hidden; +} + +.compare-chips-grid { + display: grid; + gap: 2px; +} + /* Iteration chips below bar chart */ .compare-iter-chips-row { display: flex; flex: 1; gap: 0; - align-items: flex-start; + align-items: stretch; min-width: 0; } @@ -1127,6 +1137,48 @@ a.run-id:hover { text-align: center; } +/* Spanning group-by dimension label */ +.compare-span-label { + width: 60px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 10px; + font-weight: 600; + padding: 2px 4px; + margin-right: 2px; + border-radius: var(--radius-sm); + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; +} + +/* Spanning group-by value chips */ +.compare-span-chip { + text-align: center; + padding: 3px 4px; + margin: 1px; + border-radius: var(--radius-sm); + font-size: 10px; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + display: flex; + align-items: center; + justify-content: center; + min-height: 16px; +} + +.compare-span-chip.param, +.compare-span-chip.tag, +.compare-span-chip.benchmark-badge { + white-space: normal; + margin: 0 !important; + display: flex; +} + .compare-subtitle { display: flex; flex-wrap: wrap; From 7e1979d84199bd376c223e6123f92451420c2726 Mon Sep 17 00:00:00 2001 From: Andrew Theurer Date: Fri, 24 Apr 2026 11:01:05 -0400 Subject: [PATCH 3/5] CSS Grid chip layout with per-dimension rows, reordering, and run ID support Replace flex-based iteration chips with a CSS Grid where each varying dimension gets its own row. Group-by dimensions show spanning chips for shared values; per-iteration cells support deep-dive selection. Dimension label chips include up/down/remove controls for reordering the group-by hierarchy. All varying dimensions (including run ID) now auto-populate groupByList on entry. Gap insertion between groups removed since spanning chips convey grouping visually. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web-ui/src/components/CompareView.jsx | 179 ++++++++++++------ queries/cdmq/web-ui/src/index.css | 83 +++++++- 2 files changed, 202 insertions(+), 60 deletions(-) diff --git a/queries/cdmq/web-ui/src/components/CompareView.jsx b/queries/cdmq/web-ui/src/components/CompareView.jsx index 513ba93c..a58a88cc 100644 --- a/queries/cdmq/web-ui/src/components/CompareView.jsx +++ b/queries/cdmq/web-ui/src/components/CompareView.jsx @@ -88,11 +88,13 @@ function computeCommonVarying(iters, hiddenSet) { if (iters.length === 0) return { common: [], varyingKeys: new Set() }; var hidden = hiddenSet || new Set(); + var runIds = new Set(); var benchmarks = new Set(); var paramValues = {}; var tagValues = {}; iters.forEach(function (it) { + if (it.runId && !hidden.has('run')) runIds.add(it.runId); if (it.benchmark && !hidden.has('benchmark')) benchmarks.add(it.benchmark); (it.params || []).forEach(function (p) { if (hidden.has('param:' + p.arg)) return; @@ -109,6 +111,11 @@ function computeCommonVarying(iters, hiddenSet) { var common = []; var varyingKeys = new Set(); + // Run + if (runIds.size > 1) { + varyingKeys.add('run'); + } + // Benchmark if (benchmarks.size === 1) { common.push({ key: 'benchmark', val: Array.from(benchmarks)[0], type: 'benchmark' }); @@ -594,11 +601,9 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set }); // Sort by distinct count ascending (fewest values = best grouping level) dimCounts.sort(function (a, b) { return a.count - b.count; }); - // Use all but the last one as group-by (last one stays as bar label) - if (dimCounts.length > 1) { - setGroupByList(dimCounts.slice(0, dimCounts.length - 1).map(function (d) { return d.value; })); - } else if (dimCounts.length === 1) { - setGroupByList([dimCounts[0].value]); + // All varying dimensions participate in group-by + if (dimCounts.length > 0) { + setGroupByList(dimCounts.map(function (d) { return d.value; })); } }, [iterations, dimOptions]); @@ -1028,12 +1033,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set var it = sorted[i]; var gv = getCompoundGroupValue(it, groupByList); - // Insert gap between groups - if (hasGroupBy(groupByList) && gv !== prevGroup) { - if (prevGroup !== null) { - chartData.push({ name: '', value: null, isGap: true }); - } - } + // No gap insertion — spanning chips show grouping visually prevGroup = gv; var mv = metricValues[it.iterationId]; @@ -1787,52 +1787,119 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set })} - {/* Iteration chips below bars — uses same layout as hier-row */} -
-
-
- {chart.data.map(function (d, di) { - if (d.isGap) return
; - var isPinned = resolvedPinnedEntry && resolvedPinnedEntry.entry && resolvedPinnedEntry.entry.iterationId === d.iterationId; - var ddSelected = deepDiveIterations && deepDiveIterations.has(d.iterationId); - var atLimit = deepDiveIterations && deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; - var themeIdx = ddSelected ? Array.from(deepDiveIterations).indexOf(d.iterationId) : -1; - var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; - // Build label from varying params not covered by group-by - var origIter = iterations.find(function (it) { return it.iterationId === d.iterationId; }); - var iterItems = origIter ? buildIterItems(origIter, iterations, hiddenFields) : []; - // Exclude group-by dimensions from the chip label - var groupBySet = new Set(groupByList); - var filteredItems = iterItems.filter(function (item) { - if (item.type === 'benchmark') return !groupBySet.has('benchmark'); - return !item.names.some(function (n) { return groupBySet.has((item.type === 'tag' ? 'tag:' : 'param:') + n); }); + {/* Chips grid below bars — one row per varying dimension */} +
+ {(function () { + var orderedDims = []; + groupByList.forEach(function (dim) { + if (chart.varyingKeys.has(dim)) orderedDims.push(dim); }); - return ( -
- {filteredItems.length > 0 ? filteredItems.map(function (item, pi) { - var label = item.type === 'benchmark' ? item.val : item.names.join(',') + '=' + item.val; - return ( - - {item.type === 'tag' && {item.names.join(',')}} - {item.type === 'tag' ? '=' + item.val : label} - - ); - }) : {d.name || (d.iterationId || '').substring(0, 8)}} -
- ); - })} -
+ chart.varyingKeys.forEach(function (dim) { + if (orderedDims.indexOf(dim) < 0) orderedDims.push(dim); + }); + + return orderedDims.map(function (dim, dimIdx) { + var row = dimIdx + 1; + var groupByIdx = groupByList.indexOf(dim); + var isGroupBy = groupByIdx >= 0; + var runs = []; + for (var di = 0; di < chart.data.length; di++) { + var d = chart.data[di]; + if (d.isGap) continue; + var origIter = iterations.find(function (it) { return it.iterationId === d.iterationId; }); + var val = origIter ? getDimValue(origIter, dim) : ''; + if (runs.length === 0 || val !== runs[runs.length - 1].val) { + runs.push({ val: val, firstDi: di, lastDi: di, iterIds: [d.iterationId] }); + } else { + runs[runs.length - 1].lastDi = di; + runs[runs.length - 1].iterIds.push(d.iterationId); + } + } + return ( + +
+ {formatDimLabel(dim)} + {dimIdx > 0 && ( + + )} + {dimIdx < orderedDims.length - 1 && ( + + )} + +
+ {runs.map(function (run, ri) { + var colStart = run.firstDi + 2; + var span = run.lastDi - run.firstDi + 1; + var formatted = formatDimValue(dim, run.val); + var isPerIter = run.iterIds.length === 1; + var iterId = isPerIter ? run.iterIds[0] : null; + var ddSelected = isPerIter && deepDiveIterations && deepDiveIterations.has(iterId); + var atLimit = deepDiveIterations && deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; + var themeIdx = ddSelected ? Array.from(deepDiveIterations).indexOf(iterId) : -1; + var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; + + var chipStyle = { gridRow: row, gridColumn: colStart + ' / span ' + span }; + if (ddSelected && themeColor) { + chipStyle.borderColor = themeColor; + chipStyle.backgroundColor = themeColor + '15'; + } + + return ( +
+ {formatted} +
+ ); + })} +
+ ); + }); + })()}
diff --git a/queries/cdmq/web-ui/src/index.css b/queries/cdmq/web-ui/src/index.css index d43c9831..c6131f8c 100644 --- a/queries/cdmq/web-ui/src/index.css +++ b/queries/cdmq/web-ui/src/index.css @@ -1073,15 +1073,76 @@ a.run-id:hover { } /* CSS Grid for chips below bar chart */ -.compare-chips-grid-wrap { - overflow-x: hidden; -} - .compare-chips-grid { display: grid; gap: 2px; } +.compare-chips-grid-label { + display: flex; + align-items: center; + gap: 2px; + padding: 2px 4px 2px 6px; + font-size: 10px; + font-weight: 600; + color: var(--text-secondary); + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + min-height: 20px; +} + +.compare-dim-groupby { + background: rgba(91, 141, 239, 0.08); + border-color: rgba(91, 141, 239, 0.3); +} + +.compare-dim-name { + flex: 1; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + text-align: right; + font-family: 'SF Mono', ui-monospace, Consolas, monospace; +} + +.compare-dim-btn { + flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + padding: 0; + font-size: 8px; + color: var(--text-muted); + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 2px; +} + +.compare-dim-btn:hover { + color: var(--accent); + background: rgba(91, 141, 239, 0.1); +} + +.compare-dim-btn:disabled { + opacity: 0.2; + cursor: default; +} + +.compare-dim-btn:disabled:hover { + background: none; + color: var(--text-muted); +} + +.compare-dim-btn-x:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} + /* Iteration chips below bar chart */ .compare-iter-chips-row { display: flex; @@ -1179,6 +1240,20 @@ a.run-id:hover { display: flex; } +.compare-span-chip-clickable { + cursor: pointer; + transition: border-color 0.15s, background-color 0.15s; +} + +.compare-span-chip-clickable:hover { + border-color: var(--accent); +} + +.compare-span-chip-dd { + border-width: 2px; + font-weight: 600; +} + .compare-subtitle { display: flex; flex-wrap: wrap; From dc092a582f24ee05873c990e369475080e0b0e0f Mon Sep 17 00:00:00 2001 From: Andrew Theurer Date: Fri, 24 Apr 2026 14:01:28 -0400 Subject: [PATCH 4/5] Deep dive selection row, vertical flip, fixed chart widths, UI refinements Add dedicated deep-dive selection row at top of chip grid with lettered indicators (A-F) and theme colors. Flip vertical layout so supplemental metrics render above the primary chart. Remove max-width cap on app container. Move hidden dimension chips to sidebar for restoration. Fix chart width instability by using fixed pixel widths for grid columns and locking chart area width across all metric panels. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web-ui/src/components/CompareView.jsx | 331 ++++++++++++++---- queries/cdmq/web-ui/src/index.css | 76 +++- 2 files changed, 338 insertions(+), 69 deletions(-) diff --git a/queries/cdmq/web-ui/src/components/CompareView.jsx b/queries/cdmq/web-ui/src/components/CompareView.jsx index a58a88cc..c55939ea 100644 --- a/queries/cdmq/web-ui/src/components/CompareView.jsx +++ b/queries/cdmq/web-ui/src/components/CompareView.jsx @@ -1413,6 +1413,213 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set )} + {/* Panel-mode supplemental metrics: rendered above the primary chart */} + {supplementalMetrics.map(function (sm, si) { + if (sm.display !== 'panel') return null; + var color = SUPP_COLORS[si % SUPP_COLORS.length]; + var dataKey = 'supp_' + si; + var vals = []; + chart.data.forEach(function (d) { + if (d.isGap) return; + if (d[dataKey] != null) vals.push(d[dataKey]); + Object.keys(d).forEach(function (k) { + if (k.startsWith(dataKey + '_') && !k.endsWith('_stddevPct') && !k.endsWith('_error') && !k.endsWith('_samples') && d[k] != null) { + vals.push(d[k]); + } + }); + }); + var min = vals.length > 0 ? Math.min.apply(null, vals) : 0; + var max = vals.length > 0 ? Math.max.apply(null, vals) : 1; + var pad = (max - min) * 0.1 || 0.1; + return ( + + {renderMetricControls(sm, si)} +
+
+
{sm.source}::{sm.type}
+
+ + + + + + + {entry.name} +
+ ); + }} + /> + {hasOverlays ? ( + + ) : ( + + )} + {pinnedEntry && pinnedEntry.entry && pinnedEntry.entry.name && ( + + )} + {(function () { + if (sm.breakouts.length > 0) { + var labelSet = new Set(); + chart.data.forEach(function (d) { + if (d.isGap) return; + Object.keys(d).forEach(function (k) { + var prefix = dataKey + '_'; + if (k.startsWith(prefix) && !k.endsWith('_stddevPct') && !k.endsWith('_error') && !k.endsWith('_samples')) { + labelSet.add(k); + } + }); + }); + var labels = Array.from(labelSet).sort(naturalCompare); + var ct = sm.chartType || 'bar'; + if (labels.length > 0) { + return labels.map(function (lk, li) { + var labelName = lk.substring((dataKey + '_').length); + var itemColor = SUPP_COLORS[(si + li) % SUPP_COLORS.length]; + if (ct === 'line') { + return ( + + ); + } + return ( + + w3 - 4 || Math.abs(h3) < 14) return null; + return ( + {text3} + ); + } + var val2 = props.value; + var w2 = props.width; + var h2 = props.height; + if (val2 == null || w2 == null || h2 == null) return null; + var text2 = formatBarLabel(val2); + if (text2.length * 8 > w2 - 4 || h2 < 16) return null; + return ( + {text2} + ); + }} /> + {chart.data.map(function (entry, idx) { + var isPinnedBk = pinnedEntry && pinnedEntry.entry && pinnedEntry.entry.iterationId === entry.iterationId; + var bkOpacity = pinnedEntry ? (isPinnedBk ? 0.9 : 0.2) : 0.7; + return ; + })} + + ); + }); + } + } + return ( + + + w4 - 4 || h4 < 16) return null; + return ( + {text4} + ); + }} /> + {chart.data.map(function (entry, idx) { + var isPinnedCell = pinnedEntry && pinnedEntry.entry && pinnedEntry.entry.iterationId === entry.iterationId; + var cellOpacity = pinnedEntry ? (isPinnedCell ? 0.9 : 0.2) : 0.7; + return ; + })} + + ); + })()} + + +
+ {supplementalMetrics.length > 0 &&
 
} +
+ {resolvedPinnedEntry && resolvedPinnedEntry.entry && !resolvedPinnedEntry.entry.isGap ? (function () { + var e = resolvedPinnedEntry.entry; + if (sm.breakouts.length > 0) { + var prefix = dataKey + '_'; + var flatItems = []; + Object.keys(e).filter(function (k) { + return k.startsWith(prefix) && !k.endsWith('_stddevPct') && !k.endsWith('_error') && !k.endsWith('_samples'); + }).sort(naturalCompare).forEach(function (k, ki) { + var labelName = k.substring(prefix.length); + flatItems.push({ label: labelName, value: e[k] != null ? formatValue(e[k]) : '-', color: SUPP_COLORS[(si + ki) % SUPP_COLORS.length] }); + }); + var groupItems = flatItems.map(function (item) { + return { segments: parseBreakoutSegments(item.label), value: item.value, color: item.color }; + }); + return renderGroupedBreakouts(groupItems, 0, sm.breakouts); + } else { + var v = e[dataKey]; + return ( +
+
{sm.source}::{sm.type}
+
{v != null ? formatValue(v) : '-'}
+
+ ); + } + })() :
Click a bar
} +
+
+
+ + ); + })} + + {/* Overlay-mode metric controls */} + {supplementalMetrics.map(function (sm, si) { + if (sm.display === 'panel') return null; + return renderMetricControls(sm, si); + })} + {false && supplementalMetrics.map(function (sm, si) { if (sm.display !== 'panel') return null; var color = SUPP_COLORS[si % SUPP_COLORS.length]; @@ -1436,7 +1643,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -1618,34 +1825,10 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{chart.metricName}
-
+
{/* Toolbar: hidden dims, add, auto, clear — above headers */}
- {hiddenFields.map(function (dim) { - var opt = allDimOptions.find(function (o) { return o.value === dim; }); - return ( - {opt ? opt.label : dim} - ); - })} - {dimOptions.filter(function (o) { return o.value !== 'none' && !groupByList.includes(o.value) && !hiddenFields.includes(o.value); }).length > 0 && ( - - )} - {groupByList.length > 0 && ( - - )}
@@ -1788,10 +1971,45 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set {/* Chips grid below bars — one row per varying dimension */} + {(function () { + var rightOffset = hasOverlays ? 110 : 31; + var iterColWidth = Math.floor((Math.max(600, nonGapData.length * 120 + 120) - 120 - rightOffset) / nonGapData.length); + return (
+ {/* Row 1: deep-dive selection */} +
+ {'Deep Dive →'} +
+ {chart.data.map(function (d, di) { + if (d.isGap) return null; + var col = di + 2; + var ddSelected = deepDiveIterations && deepDiveIterations.has(d.iterationId); + var atLimit = deepDiveIterations && deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; + var ddArr = deepDiveIterations ? Array.from(deepDiveIterations) : []; + var themeIdx = ddSelected ? ddArr.indexOf(d.iterationId) : -1; + var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; + var letter = themeIdx >= 0 ? String.fromCharCode(65 + themeIdx) : ''; + return ( +
+ {letter} +
+ ); + })} {(function () { var orderedDims = []; groupByList.forEach(function (dim) { @@ -1802,7 +2020,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set }); return orderedDims.map(function (dim, dimIdx) { - var row = dimIdx + 1; + var row = dimIdx + 2; var groupByIdx = groupByList.indexOf(dim); var isGroupBy = groupByIdx >= 0; var runs = []; @@ -1865,32 +2083,11 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set var colStart = run.firstDi + 2; var span = run.lastDi - run.firstDi + 1; var formatted = formatDimValue(dim, run.val); - var isPerIter = run.iterIds.length === 1; - var iterId = isPerIter ? run.iterIds[0] : null; - var ddSelected = isPerIter && deepDiveIterations && deepDiveIterations.has(iterId); - var atLimit = deepDiveIterations && deepDiveIterations.size >= MAX_DEEP_DIVE_ITERS; - var themeIdx = ddSelected ? Array.from(deepDiveIterations).indexOf(iterId) : -1; - var themeColor = themeIdx >= 0 ? ITER_THEME_BASES[themeIdx % ITER_THEME_BASES.length] : null; - - var chipStyle = { gridRow: row, gridColumn: colStart + ' / span ' + span }; - if (ddSelected && themeColor) { - chipStyle.borderColor = themeColor; - chipStyle.backgroundColor = themeColor + '15'; - } - return (
{formatted}
@@ -1901,6 +2098,8 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set }); })()}
+ ); + })()}
{hasOverlays ? ( @@ -1911,6 +2110,20 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
 
) : null}
+ {hiddenFields.length > 0 && ( +
+
Hidden
+ {hiddenFields.map(function (dim) { + var opt = allDimOptions.find(function (o) { return o.value === dim; }); + return ( + {opt ? opt.label : formatDimLabel(dim)} + ); + })} +
+ )} {resolvedPinnedEntry && resolvedPinnedEntry.entry && !resolvedPinnedEntry.entry.isGap && resolvedPinnedEntry.entry.value != null ? (function () { var e = resolvedPinnedEntry.entry; var items = []; @@ -1989,8 +2202,8 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set ); })()} - {/* Panel-mode supplemental metrics: rendered below the primary chart */} - {supplementalMetrics.map(function (sm, si) { + {/* Old panel-mode metrics removed — now rendered above the primary chart */} + {false && supplementalMetrics.map(function (sm, si) { if (sm.display !== 'panel') return null; var color = SUPP_COLORS[si % SUPP_COLORS.length]; var dataKey = 'supp_' + si; @@ -2012,7 +2225,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -2192,12 +2405,6 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set ); })} - {/* Overlay-mode metric controls: rendered below the primary chart */} - {supplementalMetrics.map(function (sm, si) { - if (sm.display === 'panel') return null; - return renderMetricControls(sm, si); - })} -
); })} diff --git a/queries/cdmq/web-ui/src/index.css b/queries/cdmq/web-ui/src/index.css index c6131f8c..cba039e5 100644 --- a/queries/cdmq/web-ui/src/index.css +++ b/queries/cdmq/web-ui/src/index.css @@ -60,7 +60,6 @@ body { } .app { - max-width: 1400px; margin: 0 auto; padding: 16px 24px 120px; } @@ -1240,18 +1239,44 @@ a.run-id:hover { display: flex; } -.compare-span-chip-clickable { +/* Deep-dive selection row */ +.compare-dd-label { + border-color: transparent; + background: transparent; +} + +.compare-dd-cell { + display: flex; + align-items: center; + justify-content: center; + border: 2px dashed var(--border); + border-radius: var(--radius-sm); cursor: pointer; - transition: border-color 0.15s, background-color 0.15s; + min-height: 22px; + font-size: 13px; + font-weight: 700; + font-family: 'SF Mono', ui-monospace, Consolas, monospace; + color: rgba(255, 255, 255, 0.9); + transition: background-color 0.15s, border-color 0.15s; } -.compare-span-chip-clickable:hover { +.compare-dd-cell:hover { border-color: var(--accent); + border-style: solid; } -.compare-span-chip-dd { - border-width: 2px; - font-weight: 600; +.compare-dd-selected { + border-style: solid; +} + +.compare-dd-disabled { + opacity: 0.3; + cursor: default; +} + +.compare-dd-disabled:hover { + border-color: var(--border); + border-style: dashed; } .compare-subtitle { @@ -1980,6 +2005,43 @@ a.run-id:hover { align-self: flex-start; } +.compare-sidebar-hidden { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; + padding-bottom: 6px; + border-bottom: 1px solid var(--border); + margin-bottom: 4px; +} + +.compare-sidebar-hidden-label { + font-size: 9px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.compare-sidebar-hidden-chip { + display: inline-block; + padding: 2px 6px; + font-size: 10px; + font-family: 'SF Mono', ui-monospace, Consolas, monospace; + background: var(--surface); + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + transition: border-color 0.15s, color 0.15s; +} + +.compare-sidebar-hidden-chip:hover { + border-color: var(--accent); + color: var(--text); + border-style: solid; +} + .compare-sidebar-empty { color: var(--text-muted); font-size: 11px; From 90cc353e1b0de05deb5a9c9784579a10e56d8c3f Mon Sep 17 00:00:00 2001 From: Andrew Theurer Date: Fri, 24 Apr 2026 14:14:26 -0400 Subject: [PATCH 5/5] Fix chart and grid width alignment Lock all chart areas with flex: none to prevent flex from overriding the fixed width. Use minmax(0, 1fr) grid columns instead of pixel widths to eliminate Math.floor rounding error. Set explicit YAxis width={60} on primary chart for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web-ui/src/components/CompareView.jsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/queries/cdmq/web-ui/src/components/CompareView.jsx b/queries/cdmq/web-ui/src/components/CompareView.jsx index c55939ea..54eba53e 100644 --- a/queries/cdmq/web-ui/src/components/CompareView.jsx +++ b/queries/cdmq/web-ui/src/components/CompareView.jsx @@ -1437,7 +1437,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -1643,7 +1643,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -1839,7 +1839,8 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set /> @@ -1971,13 +1972,9 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set {/* Chips grid below bars — one row per varying dimension */} - {(function () { - var rightOffset = hasOverlays ? 110 : 31; - var iterColWidth = Math.floor((Math.max(600, nonGapData.length * 120 + 120) - 120 - rightOffset) / nonGapData.length); - return (
{/* Row 1: deep-dive selection */}
@@ -2098,8 +2095,6 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set }); })()}
- ); - })()}
{hasOverlays ? ( @@ -2225,7 +2220,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+