/** * Chart display modes — how a metrics card should paint its series. * * Catalog agents emit chartType: line | area | stacked. * The desktop can also render bar / multibar / pie where they make sense. */ /** @typedef {'line'|'area'|'stacked'|'bar'|'multibar'|'pie'} ChartMode */ /** @type {ChartMode[]} */ export const CHART_MODES = ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'] /** Short labels for the type toggle button */ export const CHART_MODE_LABEL = { line: 'line', area: 'area', stacked: 'stack', bar: 'bar', multibar: 'bars', pie: 'pie', } /** * @param {unknown} raw * @returns {ChartMode} */ export function normalizeChartMode(raw) { const m = String(raw || 'line') .trim() .toLowerCase() .replace(/[_-]/g, '') if (m === 'multibar' || m === 'bargroup') return 'multibar' if (m === 'stack') return 'stacked' if (CHART_MODES.includes(/** @type {ChartMode} */ (m))) { return /** @type {ChartMode} */ (m) } return 'line' } /** * Default mode from catalog metadata. * @param {object} [meta] * @returns {ChartMode} */ export function defaultModeFromMeta(meta = {}) { return normalizeChartMode(meta.chartType || meta.chart_type || 'line') } /** * @param {object} [meta] */ function dimCount(meta = {}) { const dims = meta.dimensions if (Array.isArray(dims)) return dims.length if (dims && typeof dims === 'object') return Object.keys(dims).length return 0 } /** * Composition charts (parts of a whole) — pie / stacked are natural. * @param {object} [meta] */ export function isCompositionChart(meta = {}) { const mode = defaultModeFromMeta(meta) if (mode === 'stacked') return true const units = String(meta.units || '') if (/%|percentage|percent/i.test(units)) return true const id = String(meta.id || meta.name || '') // Common composition contexts if (/\.(cpu|ram|swap|slab|kernel|states|usage)$/i.test(id)) return true if (/^(system\.cpu|system\.ram|mem\.(swap|kernel|slab))/i.test(id)) return true return false } /** * Modes that make sense for this chart (catalog default first). * @param {object} [meta] * @returns {ChartMode[]} */ export function allowedModesFor(meta = {}) { const preferred = defaultModeFromMeta(meta) const n = dimCount(meta) const composition = isCompositionChart(meta) /** @type {Set} */ const set = new Set(['line', 'area']) if (n !== 1) set.add('stacked') set.add('bar') if (n === 0 || (n >= 2 && n <= 16)) set.add('multibar') if (composition || preferred === 'stacked') set.add('pie') // Always allow the catalog default set.add(preferred) const order = /** @type {ChartMode[]} */ ([ preferred, ...CHART_MODES.filter((m) => m !== preferred && set.has(m)), ]) return order } /** * Cycle to the next allowed mode. * @param {ChartMode|string} current * @param {object} [meta] * @returns {ChartMode} */ export function nextChartMode(current, meta = {}) { const allowed = allowedModesFor(meta) const cur = normalizeChartMode(current) const idx = allowed.indexOf(cur) return allowed[(idx < 0 ? 0 : idx + 1) % allowed.length] } /** * Human hint for tooltips / empty states. * @param {ChartMode|string} mode */ export function chartModeHint(mode) { switch (normalizeChartMode(mode)) { case 'area': return 'Filled time series — good for volume and rates' case 'stacked': return 'Stacked over time — parts of a whole' case 'bar': return 'Columns over time — compare magnitude per sample' case 'multibar': return 'Grouped bars — compare dimensions side by side' case 'pie': return 'Snapshot share — latest values as a whole' default: return 'Lines over time — independent series' } }