diff --git a/queries/cdmq/web-ui/src/components/CompareView.jsx b/queries/cdmq/web-ui/src/components/CompareView.jsx index 5a705984..54eba53e 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]; @@ -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 && (
@@ -1449,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]; @@ -1472,7 +1643,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -1653,148 +1824,23 @@ 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 && ( - - )}
- {/* 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
-
- ); - })()} - + @@ -1925,6 +1971,131 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set })} + {/* Chips grid below bars — one row per varying dimension */} +
+ {/* 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) { + if (chart.varyingKeys.has(dim)) orderedDims.push(dim); + }); + chart.varyingKeys.forEach(function (dim) { + if (orderedDims.indexOf(dim) < 0) orderedDims.push(dim); + }); + + return orderedDims.map(function (dim, dimIdx) { + var row = dimIdx + 2; + 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); + return ( +
+ {formatted} +
+ ); + })} +
+ ); + }); + })()} +
+
{hasOverlays ? (
@@ -1934,6 +2105,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 = []; @@ -2012,8 +2197,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; @@ -2035,7 +2220,7 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
{sm.source}::{sm.type}
-
+
@@ -2215,12 +2400,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 3a39e66a..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; } @@ -1066,6 +1065,220 @@ a.run-id:hover { flex: 1; } +/* Scrollable chart area */ +.compare-chart-scroll { + overflow-x: auto; + scrollbar-width: thin; +} + +/* CSS Grid for chips below bar chart */ +.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; + flex: 1; + gap: 0; + align-items: stretch; + 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; +} + +/* 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; +} + +/* 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; + 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-dd-cell:hover { + border-color: var(--accent); + border-style: solid; +} + +.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 { display: flex; flex-wrap: wrap; @@ -1792,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;