Metric Correlations
This commit is contained in:
+362
-6
@@ -55,6 +55,13 @@ function smoothScrollIntoView(el, block = 'start') {
|
||||
* liveEl?: HTMLElement|null,
|
||||
* liveLabel?: HTMLElement|null,
|
||||
* retentionHint?: HTMLElement|null,
|
||||
* correlateBtn?: HTMLButtonElement|null,
|
||||
* mcBar?: HTMLElement|null,
|
||||
* mcMethod?: HTMLSelectElement|null,
|
||||
* mcHint?: HTMLElement|null,
|
||||
* mcRunBtn?: HTMLButtonElement|null,
|
||||
* mcClearBtn?: HTMLButtonElement|null,
|
||||
* mcResultsPanel?: HTMLElement|null,
|
||||
* }} DashboardEls
|
||||
*/
|
||||
|
||||
@@ -118,6 +125,16 @@ export function createMetricsDashboard(opts) {
|
||||
retentionSeconds: 0,
|
||||
hoverSeriesLen: 0,
|
||||
hoverPaintRaf: 0,
|
||||
/** Metric Correlations mode */
|
||||
mcMode: false,
|
||||
mcMethod: 'volume',
|
||||
/** @type {{ after: number, before: number }|null} absolute unix seconds */
|
||||
highlight: null,
|
||||
/** @type {Array<{ id: string, weight: number, info?: string, context?: string }>|null} */
|
||||
mcResults: null,
|
||||
/** @type {{ startFrac: number, endFrac: number, chartId: string }|null} */
|
||||
mcBrush: null,
|
||||
mcRunning: false,
|
||||
}
|
||||
|
||||
function scheduleHoverPaint() {
|
||||
@@ -532,10 +549,22 @@ export function createMetricsDashboard(opts) {
|
||||
statusEl.textContent = card.source
|
||||
} else statusEl.textContent = ''
|
||||
}
|
||||
const weightChip = article.querySelector('.metric-weight-chip')
|
||||
const mcRow = state.mcResults?.find((r) => r.id === id)
|
||||
if (weightChip) {
|
||||
if (mcRow) {
|
||||
weightChip.textContent = `${(mcRow.weight * 100).toFixed(0)}%`
|
||||
weightChip.classList.remove('hidden')
|
||||
weightChip.title = `Correlation weight ${mcRow.weight.toFixed(3)}`
|
||||
} else {
|
||||
weightChip.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
if (!card || !canvas) return
|
||||
const height = cardHeightFor(id, card)
|
||||
const heightPx = `${height}px`
|
||||
if (canvas.style.height !== heightPx) canvas.style.height = heightPx
|
||||
updateHighlightBand(id)
|
||||
const { dims, hidden } = sortedDims(card, id)
|
||||
const mode = normalizeChartMode(card.mode || defaultModeFromMeta(card.meta))
|
||||
card.mode = mode
|
||||
@@ -822,10 +851,11 @@ export function createMetricsDashboard(opts) {
|
||||
}
|
||||
|
||||
function bindCanvasInteractions(canvas, chartId) {
|
||||
canvas.title = 'Scroll to browse · Ctrl/⌘ or Shift + scroll to zoom · drag horizontally to pan'
|
||||
canvas.title =
|
||||
'Scroll to browse · Ctrl/⌘ or Shift + scroll to zoom · drag horizontally to pan · Correlate mode: drag to highlight'
|
||||
|
||||
canvas.addEventListener('mousemove', (ev) => {
|
||||
if (state.pan?.active) return
|
||||
if (state.pan?.active || state.mcBrush) return
|
||||
const card = state.cards.get(chartId)
|
||||
const seriesLen = seriesLenForCard(chartId)
|
||||
const units = card?.meta?.units || ''
|
||||
@@ -842,7 +872,7 @@ export function createMetricsDashboard(opts) {
|
||||
scheduleHoverPaint()
|
||||
})
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
if (state.pan?.active) return
|
||||
if (state.pan?.active || state.mcBrush) return
|
||||
state.hoverIndex = null
|
||||
state.hoverSeriesLen = 0
|
||||
updateHoverReadout()
|
||||
@@ -867,9 +897,23 @@ export function createMetricsDashboard(opts) {
|
||||
|
||||
canvas.addEventListener('pointerdown', (ev) => {
|
||||
if (ev.button !== 0) return
|
||||
if (state.forcePlay) return
|
||||
const card = state.cards.get(chartId)
|
||||
if (normalizeChartMode(card?.mode) === 'pie') return
|
||||
|
||||
// Correlate mode: horizontal brush sets highlight window
|
||||
if (state.mcMode) {
|
||||
const frac = canvasFracFromClientX(canvas, ev.clientX, card)
|
||||
state.mcBrush = { startFrac: frac, endFrac: frac, chartId }
|
||||
try {
|
||||
canvas.setPointerCapture(ev.pointerId)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
canvas.classList.add('brushing')
|
||||
return
|
||||
}
|
||||
|
||||
if (state.forcePlay) return
|
||||
// Defer pan until a clear horizontal drag — avoids fighting trackpad scroll.
|
||||
state.pan = {
|
||||
active: false,
|
||||
@@ -886,6 +930,13 @@ export function createMetricsDashboard(opts) {
|
||||
}
|
||||
})
|
||||
canvas.addEventListener('pointermove', (ev) => {
|
||||
if (state.mcBrush && state.mcBrush.chartId === chartId) {
|
||||
const card = state.cards.get(chartId)
|
||||
state.mcBrush.endFrac = canvasFracFromClientX(canvas, ev.clientX, card)
|
||||
updateHighlightBand(chartId)
|
||||
syncMcUi()
|
||||
return
|
||||
}
|
||||
if (!state.pan) return
|
||||
const dx = ev.clientX - state.pan.startX
|
||||
const dy = ev.clientY - state.pan.startY
|
||||
@@ -918,6 +969,18 @@ export function createMetricsDashboard(opts) {
|
||||
updateHoverReadout()
|
||||
})
|
||||
const endPan = (ev) => {
|
||||
if (state.mcBrush && state.mcBrush.chartId === chartId) {
|
||||
const brush = state.mcBrush
|
||||
state.mcBrush = null
|
||||
canvas.classList.remove('brushing')
|
||||
try {
|
||||
canvas.releasePointerCapture(ev.pointerId)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
commitBrushHighlight(brush)
|
||||
return
|
||||
}
|
||||
if (!state.pan) return
|
||||
const wasActive = state.pan.active
|
||||
state.pan = null
|
||||
@@ -933,6 +996,89 @@ export function createMetricsDashboard(opts) {
|
||||
canvas.addEventListener('pointercancel', endPan)
|
||||
}
|
||||
|
||||
function canvasFracFromClientX(canvas, clientX, card) {
|
||||
const units = card?.meta?.units || ''
|
||||
const showY = normalizeChartMode(card?.mode) !== 'pie'
|
||||
const padL = padLeftFor(units, showY)
|
||||
const padR = 10
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const plotW = Math.max(1, rect.width - padL - padR)
|
||||
const x = clientX - rect.left - padL
|
||||
return Math.max(0, Math.min(1, x / plotW))
|
||||
}
|
||||
|
||||
/** Map brush fractions (0=oldest … 1=newest in view) to absolute unix seconds. */
|
||||
function commitBrushHighlight(brush) {
|
||||
if (!brush) return
|
||||
const lo = Math.min(brush.startFrac, brush.endFrac)
|
||||
const hi = Math.max(brush.startFrac, brush.endFrac)
|
||||
if (hi - lo < 0.02) {
|
||||
syncMcUi()
|
||||
return
|
||||
}
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
const viewEnd = nowSec - state.endOffset
|
||||
const viewStart = viewEnd - state.afterSeconds
|
||||
const after = Math.round(viewStart + lo * state.afterSeconds)
|
||||
const before = Math.round(viewStart + hi * state.afterSeconds)
|
||||
if (before - after < 15) {
|
||||
if (opts.els.mcHint) {
|
||||
opts.els.mcHint.textContent = 'Highlight must be at least 15 seconds'
|
||||
}
|
||||
syncMcUi()
|
||||
return
|
||||
}
|
||||
state.highlight = { after, before }
|
||||
for (const id of state.cardEls.keys()) updateHighlightBand(id)
|
||||
syncMcUi()
|
||||
}
|
||||
|
||||
function updateHighlightBand(chartId) {
|
||||
const article = state.cardEls.get(chartId)
|
||||
if (!article) return
|
||||
let band = article.querySelector('.metric-highlight-band')
|
||||
if (!band) {
|
||||
band = document.createElement('div')
|
||||
band.className = 'metric-highlight-band'
|
||||
const wrap = article.querySelector('.metric-canvas-wrap') || article
|
||||
wrap.appendChild(band)
|
||||
}
|
||||
const hl = state.highlight
|
||||
const brush = state.mcBrush
|
||||
if (!hl && !brush) {
|
||||
band.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
const viewEnd = nowSec - state.endOffset
|
||||
const viewStart = viewEnd - state.afterSeconds
|
||||
let after
|
||||
let before
|
||||
if (brush && brush.chartId === chartId) {
|
||||
const lo = Math.min(brush.startFrac, brush.endFrac)
|
||||
const hi = Math.max(brush.startFrac, brush.endFrac)
|
||||
after = viewStart + lo * state.afterSeconds
|
||||
before = viewStart + hi * state.afterSeconds
|
||||
} else if (hl) {
|
||||
after = hl.after
|
||||
before = hl.before
|
||||
} else {
|
||||
band.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
const leftFrac = (after - viewStart) / Math.max(1, state.afterSeconds)
|
||||
const rightFrac = (before - viewStart) / Math.max(1, state.afterSeconds)
|
||||
const left = Math.max(0, Math.min(1, leftFrac))
|
||||
const right = Math.max(0, Math.min(1, rightFrac))
|
||||
if (right <= left) {
|
||||
band.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
band.classList.remove('hidden')
|
||||
band.style.left = `${left * 100}%`
|
||||
band.style.width = `${(right - left) * 100}%`
|
||||
}
|
||||
|
||||
function render() {
|
||||
const catalog = opts.getCatalog() || {}
|
||||
let { sections } = groupCatalog(catalog, state.filter)
|
||||
@@ -940,6 +1086,9 @@ export function createMetricsDashboard(opts) {
|
||||
// Board mode: only pinned charts
|
||||
sections = []
|
||||
}
|
||||
if (state.mcResults?.length) {
|
||||
sections = mcResultSections(catalog)
|
||||
}
|
||||
renderToc(sections)
|
||||
renderWall(sections)
|
||||
state.built = true
|
||||
@@ -948,6 +1097,34 @@ export function createMetricsDashboard(opts) {
|
||||
opts.els.boardBtn.classList.toggle('active', state.boardOnly)
|
||||
opts.els.boardBtn.setAttribute('aria-pressed', state.boardOnly ? 'true' : 'false')
|
||||
}
|
||||
if (opts.els.correlateBtn) {
|
||||
opts.els.correlateBtn.classList.toggle('active', state.mcMode)
|
||||
opts.els.correlateBtn.setAttribute('aria-pressed', state.mcMode ? 'true' : 'false')
|
||||
}
|
||||
opts.els.root?.classList.toggle('mc-mode', state.mcMode)
|
||||
opts.els.root?.classList.toggle('mc-results', Boolean(state.mcResults?.length))
|
||||
syncMcUi()
|
||||
for (const id of state.cardEls.keys()) updateHighlightBand(id)
|
||||
}
|
||||
|
||||
function mcResultSections(catalog) {
|
||||
const charts = []
|
||||
for (const row of state.mcResults || []) {
|
||||
const meta = catalog[row.id] || {
|
||||
title: row.info || row.id,
|
||||
context: row.context || '',
|
||||
family: '',
|
||||
}
|
||||
charts.push({ id: row.id, meta })
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'correlations',
|
||||
title: 'Metric Correlations',
|
||||
count: charts.length,
|
||||
groups: [{ family: '_default', charts }],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function setBoardOnly(on) {
|
||||
@@ -1202,7 +1379,11 @@ export function createMetricsDashboard(opts) {
|
||||
<span class="metric-card-status muted"></span>
|
||||
</div>
|
||||
</header>
|
||||
<canvas height="${height}"></canvas>
|
||||
<div class="metric-canvas-wrap">
|
||||
<canvas height="${height}"></canvas>
|
||||
<div class="metric-highlight-band hidden" aria-hidden="true"></div>
|
||||
<span class="metric-weight-chip hidden"></span>
|
||||
</div>
|
||||
<div class="metric-card-legend"></div>
|
||||
<div class="metric-card-stats hidden"></div>
|
||||
<div class="metric-resize" title="Drag to resize · double-click to reset"></div>
|
||||
@@ -1377,6 +1558,12 @@ export function createMetricsDashboard(opts) {
|
||||
}
|
||||
opts.els.forcePlayBtn?.addEventListener('click', () => setForcePlay(!state.forcePlay))
|
||||
opts.els.boardBtn?.addEventListener('click', () => setBoardOnly(!state.boardOnly))
|
||||
opts.els.correlateBtn?.addEventListener('click', () => setMcMode(!state.mcMode))
|
||||
opts.els.mcMethod?.addEventListener('change', () => {
|
||||
state.mcMethod = opts.els.mcMethod?.value || 'volume'
|
||||
})
|
||||
opts.els.mcRunBtn?.addEventListener('click', () => runMetricCorrelations().catch(() => {}))
|
||||
opts.els.mcClearBtn?.addEventListener('click', () => clearMcResults())
|
||||
bindKeyboard()
|
||||
setForcePlay(state.forcePlay)
|
||||
setPlaying(true)
|
||||
@@ -1386,6 +1573,168 @@ export function createMetricsDashboard(opts) {
|
||||
state.endOffset = 0
|
||||
syncPresetUi()
|
||||
syncLiveUi()
|
||||
syncMcUi()
|
||||
}
|
||||
|
||||
function setMcMode(on) {
|
||||
state.mcMode = Boolean(on)
|
||||
if (state.mcMode) {
|
||||
state.boardOnly = false
|
||||
setPlaying(false)
|
||||
} else if (!state.mcResults?.length) {
|
||||
state.highlight = null
|
||||
}
|
||||
opts.els.root?.classList.toggle('mc-mode', state.mcMode)
|
||||
if (opts.els.correlateBtn) {
|
||||
opts.els.correlateBtn.classList.toggle('active', state.mcMode)
|
||||
opts.els.correlateBtn.setAttribute('aria-pressed', state.mcMode ? 'true' : 'false')
|
||||
}
|
||||
syncMcUi()
|
||||
for (const id of state.cardEls.keys()) updateHighlightBand(id)
|
||||
}
|
||||
|
||||
function syncMcUi() {
|
||||
const bar = opts.els.mcBar
|
||||
if (bar) bar.classList.toggle('hidden', !state.mcMode && !state.mcResults?.length)
|
||||
const hl = state.highlight
|
||||
const dur = hl ? hl.before - hl.after : 0
|
||||
const canRun = Boolean(hl && dur >= 15 && !state.mcRunning)
|
||||
if (opts.els.mcRunBtn) opts.els.mcRunBtn.disabled = !canRun || state.mcRunning
|
||||
if (opts.els.mcHint) {
|
||||
if (state.mcRunning) opts.els.mcHint.textContent = 'Scoring metrics…'
|
||||
else if (state.mcResults?.length) {
|
||||
opts.els.mcHint.textContent = `${state.mcResults.length} correlated charts · ${state.mcMethod}`
|
||||
} else if (hl && dur >= 15) {
|
||||
opts.els.mcHint.textContent = `Highlight ${formatDuration(dur)} · baseline ~${formatDuration(dur * 4)} before`
|
||||
} else if (hl) {
|
||||
opts.els.mcHint.textContent = 'Highlight must be at least 15 seconds'
|
||||
} else {
|
||||
opts.els.mcHint.textContent = 'Brush a chart (≥15s), then Find Correlations'
|
||||
}
|
||||
}
|
||||
if (opts.els.mcMethod && opts.els.mcMethod.value !== state.mcMethod) {
|
||||
opts.els.mcMethod.value = state.mcMethod
|
||||
}
|
||||
}
|
||||
|
||||
async function runMetricCorrelations(override = {}) {
|
||||
const hl = override.highlight || state.highlight
|
||||
if (!hl || hl.before - hl.after < 15) {
|
||||
syncMcUi()
|
||||
return
|
||||
}
|
||||
const method = override.method || state.mcMethod || 'volume'
|
||||
state.mcRunning = true
|
||||
syncMcUi()
|
||||
try {
|
||||
const res = await opts.getWeights?.({
|
||||
method,
|
||||
after: hl.after,
|
||||
before: hl.before,
|
||||
// baseline auto-computed server-side when omitted
|
||||
points: 500,
|
||||
limit: 80,
|
||||
timeout: 30_000,
|
||||
})
|
||||
if (res?.error) {
|
||||
if (opts.els.mcHint) opts.els.mcHint.textContent = res.error
|
||||
return
|
||||
}
|
||||
const results = Array.isArray(res?.results) ? res.results : []
|
||||
state.highlight = hl
|
||||
state.mcMethod = method
|
||||
state.mcResults = results.filter((r) => r?.id && Number(r.weight) > 0)
|
||||
state.mcMode = true
|
||||
// Align wall window to highlight + pause
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
const ageEnd = Math.max(0, nowSec - hl.before)
|
||||
const win = Math.max(30, hl.before - hl.after)
|
||||
setWindow(win, ageEnd)
|
||||
setPlaying(false)
|
||||
renderMcResultsPanel(res)
|
||||
render()
|
||||
// Prefetch result cards
|
||||
for (const row of state.mcResults.slice(0, 40)) {
|
||||
fetchChart(row.id).catch(() => {})
|
||||
}
|
||||
} catch (err) {
|
||||
if (opts.els.mcHint) {
|
||||
opts.els.mcHint.textContent = err?.message || 'Correlation failed'
|
||||
}
|
||||
} finally {
|
||||
state.mcRunning = false
|
||||
syncMcUi()
|
||||
}
|
||||
}
|
||||
|
||||
function renderMcResultsPanel(res) {
|
||||
const panel = opts.els.mcResultsPanel
|
||||
if (!panel) return
|
||||
panel.classList.remove('hidden')
|
||||
panel.innerHTML = ''
|
||||
const head = document.createElement('header')
|
||||
head.className = 'related-head'
|
||||
const n = state.mcResults?.length || 0
|
||||
head.innerHTML = `<strong>Correlations</strong><span class="muted">${escapeHtml(state.mcMethod)} · ${n}</span>`
|
||||
const clear = document.createElement('button')
|
||||
clear.type = 'button'
|
||||
clear.className = 'ghost'
|
||||
clear.textContent = 'Clear'
|
||||
clear.addEventListener('click', () => clearMcResults())
|
||||
head.appendChild(clear)
|
||||
panel.appendChild(head)
|
||||
const list = document.createElement('div')
|
||||
list.className = 'related-list'
|
||||
if (!n) {
|
||||
list.innerHTML = '<p class="muted">No strongly changed metrics in this window</p>'
|
||||
} else {
|
||||
for (const row of state.mcResults.slice(0, 40)) {
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.className = 'related-item'
|
||||
const pct = `${(Number(row.weight) * 100).toFixed(0)}%`
|
||||
btn.innerHTML = `<span>${escapeHtml(row.info || row.id)}</span><span class="muted">${pct} · ${escapeHtml(row.id)}</span>`
|
||||
btn.addEventListener('click', () => scrollToChart(row.id))
|
||||
list.appendChild(btn)
|
||||
}
|
||||
}
|
||||
panel.appendChild(list)
|
||||
void res
|
||||
}
|
||||
|
||||
function clearMcResults() {
|
||||
state.mcResults = null
|
||||
state.highlight = null
|
||||
state.mcMode = false
|
||||
state.mcBrush = null
|
||||
opts.els.mcResultsPanel?.classList.add('hidden')
|
||||
if (opts.els.mcResultsPanel) opts.els.mcResultsPanel.innerHTML = ''
|
||||
opts.els.root?.classList.remove('mc-mode', 'mc-results')
|
||||
if (opts.els.correlateBtn) {
|
||||
opts.els.correlateBtn.classList.remove('active')
|
||||
opts.els.correlateBtn.setAttribute('aria-pressed', 'false')
|
||||
}
|
||||
syncMcUi()
|
||||
render()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Correlate around an absolute event time (ms).
|
||||
* @param {number} tsMs
|
||||
* @param {{ method?: string, halfWindowSec?: number }} [optsIn]
|
||||
*/
|
||||
function correlateAround(tsMs, optsIn = {}) {
|
||||
if (!tsMs || !Number.isFinite(tsMs)) return
|
||||
const half = Math.max(30, Number(optsIn.halfWindowSec) || 60)
|
||||
const center = Math.floor(tsMs / 1000)
|
||||
const highlight = { after: center - half, before: center + half }
|
||||
state.highlight = highlight
|
||||
state.mcMethod = optsIn.method || 'anomaly-rate'
|
||||
setMcMode(true)
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
setWindow(Math.max(300, half * 6), Math.max(0, nowSec - highlight.before))
|
||||
setPlaying(false)
|
||||
runMetricCorrelations({ highlight, method: state.mcMethod }).catch(() => {})
|
||||
}
|
||||
|
||||
function bindKeyboard() {
|
||||
@@ -1413,7 +1762,10 @@ export function createMetricsDashboard(opts) {
|
||||
} else if (ev.key === 'b' || ev.key === 'B') {
|
||||
setBoardOnly(!state.boardOnly)
|
||||
} else if (ev.key === 'Escape') {
|
||||
clearRelated()
|
||||
if (state.mcResults || state.mcMode) clearMcResults()
|
||||
else clearRelated()
|
||||
} else if (ev.key === 'c' || ev.key === 'C') {
|
||||
setMcMode(!state.mcMode)
|
||||
} else if (ev.key >= '1' && ev.key <= '5') {
|
||||
const presets = ['1m', '5m', '15m', '1h', '6h']
|
||||
setPreset(presets[Number(ev.key) - 1])
|
||||
@@ -1506,6 +1858,10 @@ export function createMetricsDashboard(opts) {
|
||||
showRelated,
|
||||
clearRelated,
|
||||
setBoardOnly,
|
||||
setMcMode,
|
||||
runMetricCorrelations,
|
||||
clearMcResults,
|
||||
correlateAround,
|
||||
refreshRetention,
|
||||
getState: () => state,
|
||||
}
|
||||
|
||||
@@ -1137,6 +1137,92 @@ button.metrics-tf.thin-history:not(.active) {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.metrics-mc-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
margin: 0 0 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent-primary) 35%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--accent-primary) 8%, var(--bg-secondary));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metrics-mc-bar.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metrics-mc-hint {
|
||||
flex: 1 1 180px;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#metrics-correlate.active,
|
||||
#charts-view.mc-mode #metrics-correlate {
|
||||
background: rgba(52, 211, 153, 0.18);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.metric-canvas-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metric-highlight-band {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(52, 211, 153, 0.16);
|
||||
border-left: 1px solid rgba(52, 211, 153, 0.55);
|
||||
border-right: 1px solid rgba(52, 211, 153, 0.55);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.metric-highlight-band.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metric-weight-chip {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
z-index: 3;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
background: rgba(52, 211, 153, 0.22);
|
||||
color: var(--text-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.metric-weight-chip.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metric-card canvas.brushing {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
#charts-view.mc-mode .metric-card canvas {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.mc-results-panel {
|
||||
border-color: color-mix(in srgb, var(--accent-primary) 40%, var(--border-color));
|
||||
}
|
||||
|
||||
.anomaly-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.related-panel {
|
||||
margin-bottom: var(--space);
|
||||
padding: 12px 14px;
|
||||
@@ -1463,6 +1549,7 @@ button.metrics-tf.thin-history:not(.active) {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.metric-canvas-wrap canvas,
|
||||
.metric-card canvas {
|
||||
width: 100%;
|
||||
height: 148px;
|
||||
|
||||
Reference in New Issue
Block a user