component Chart { outputs { select(payload: { item: object; index: number; sourceEvent?: Event }) dataPointClick(payload: { item: object; index: number; sourceEvent?: Event }) legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event }) } props { title: string = "Chart" description: string = "" items: unknown[] = [] data: unknown[] = [] type: string = "" valueKey: string = "value" labelKey: string = "label" height: number = 240 showLegend: boolean = true showValues: boolean = true size: string = "default" color: string = "primary" variant: string = "bar" class: string = "" } state hiddenIndexes = [] functions { shared function chartData() { return data.length ? data : items } shared function chartKind() { var kind = type || variant; return kind === "line" || kind === "area" ? kind : "bar" } shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) } shared function isHidden(index) { return hiddenIndexes.includes(index) } // Visible points keep their original index so events report the source item. shared function points() { var source = chartData() var visible = [] for (var index = 0; index < source.length; index++) { if (!hiddenIndexes.includes(index)) { visible.push({ item: source[index], index: index }) } } return visible } // Plot geometry in viewBox units: 600 wide, height tall. shared function plotLeft() { return 44 } shared function plotRight() { return 588 } shared function plotTop() { return 14 } shared function plotBottom() { return Math.max(120, height) - 30 } // Round the scale up to a readable value so the ticks land on clean numbers. shared function niceMax() { var max = 0 var visible = points() for (var index = 0; index < visible.length; index++) { max = Math.max(max, numericValue(visible[index].item)) } if (max <= 0) { return 1 } var magnitude = Math.pow(10, Math.floor(Math.log10(max))) var steps = [1, 2, 2.5, 5, 10] var chosen = 10 * magnitude for (var step = steps.length - 1; step >= 0; step--) { if (steps[step] * magnitude >= max) { chosen = steps[step] * magnitude } } return chosen } shared function ticks() { var top = niceMax() return [0, top * 0.25, top * 0.5, top * 0.75, top] } shared function tickLabel(value) { if (value >= 1000000) { return (value / 1000000).toFixed(value % 1000000 ? 1 : 0) + "M" } if (value >= 1000) { return (value / 1000).toFixed(value % 1000 ? 1 : 0) + "k" } return String(Math.round(value * 100) / 100) } shared function yFor(value) { return plotTop() + (1 - value / niceMax()) * (plotBottom() - plotTop()) } shared function band() { return (plotRight() - plotLeft()) / Math.max(1, points().length) } shared function barWidth() { return Math.min(band() * 0.62, 56) } shared function centerX(position) { return plotLeft() + position * band() + band() / 2 } // Bars: 4px rounded data end, square at the baseline. shared function barPath(point, position) { var width = barWidth() var x = centerX(position) - width / 2 var y = yFor(numericValue(point.item)) var base = plotBottom() var radius = Math.min(4, (base - y) / 2, width / 2) if (base - y < 1) { return "M" + x + "," + (base - 1) + "h" + width + "v1h-" + width + "Z" } return "M" + x + "," + base + "V" + (y + radius) + "Q" + x + "," + y + " " + (x + radius) + "," + y + "H" + (x + width - radius) + "Q" + (x + width) + "," + y + " " + (x + width) + "," + (y + radius) + "V" + base + "Z" } shared function linePath() { var visible = points() var path = "" for (var position = 0; position < visible.length; position++) { path = path + (position ? "L" : "M") + centerX(position) + "," + yFor(numericValue(visible[position].item)) } return path } shared function areaPath() { var visible = points() if (!visible.length) { return "" } return linePath() + "L" + centerX(visible.length - 1) + "," + plotBottom() + "L" + centerX(0) + "," + plotBottom() + "Z" } // Hit targets and tooltips are HTML laid over the SVG, positioned in %. shared function hitStyle(position) { return "left:" + ((plotLeft() + position * band()) / 600 * 100) + "%;width:" + (band() / 600 * 100) + "%;top:" + (plotTop() / Math.max(120, height) * 100) + "%;height:" + ((plotBottom() - plotTop()) / Math.max(120, height) * 100) + "%" } shared function tipStyle(point) { return "top:" + ((yFor(numericValue(point.item)) - plotTop()) / (plotBottom() - plotTop()) * 100) + "%" } client function selectPoint(item, index, sourceEvent) { output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent }) output.select({ item: item, index: index, sourceEvent: sourceEvent }) } client function toggleLegend(item, index, sourceEvent, next) { next = [] for (var position = 0; position < hiddenIndexes.length; position++) { if (hiddenIndexes[position] !== index) { next.push(hiddenIndexes[position]) } } if (next.length === hiddenIndexes.length) { next.push(index) } hiddenIndexes = next output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent }) } } view {
{#if title || description}
{#if title}{title}{/if} {#if description}{description}{/if}
{/if}
{#each ticks() as tick} {/each} {#if chartKind() === "bar"} {#each points() as point, position} {/each} {:else} {#if chartKind() === "area"}{/if} {/if}
{#each points() as point, position} {/each}
{#if !points().length}

No data to show

{/if}
{#if showLegend && chartData().length}
{#each chartData() as item, index} {/each}
{/if}
} style { .wrn-chart { --wrn-chart-color: var(--wrn-color-primary); display: grid; gap: 0.875rem; min-width: 0; margin: 0; padding: 1rem 1.125rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); } .wrn-chart[data-color="secondary"] { --wrn-chart-color: var(--wrn-color-secondary); } .wrn-chart[data-color="success"] { --wrn-chart-color: var(--wrn-color-success); } .wrn-chart[data-color="info"] { --wrn-chart-color: var(--wrn-color-info); } .wrn-chart[data-color="warning"] { --wrn-chart-color: var(--wrn-color-warning); } .wrn-chart[data-color="danger"] { --wrn-chart-color: var(--wrn-color-danger); } .wrn-chart__header { display: grid; gap: 0.2rem; } .wrn-chart__header > strong { font-size: 0.9375rem; font-weight: 620; letter-spacing: -0.01em; } .wrn-chart__header > span { color: var(--wrn-color-text-muted, var(--wrn-color-muted)); font-size: 0.8125rem; } /* The stage reserves the SVG box; ticks, labels and hit targets sit over it in percentages so text never scales with the plot. */ .wrn-chart__stage { position: relative; width: 100%; min-width: 0; } .wrn-chart__plot { position: absolute; inset: 0; display: block; width: 100%; height: 100%; overflow: visible; } .wrn-chart__grid { stroke: var(--wrn-color-border); stroke-width: 1; vector-effect: non-scaling-stroke; stroke-dasharray: 2 4; } .wrn-chart__grid[data-baseline="true"] { stroke: var(--wrn-color-border-strong, var(--wrn-color-border)); stroke-dasharray: none; } .wrn-chart__bar { fill: var(--wrn-chart-color); transition: opacity var(--wrn-motion-fast); } .wrn-chart__line { fill: none; stroke: var(--wrn-chart-color); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; } .wrn-chart__area { fill: color-mix(in srgb, var(--wrn-chart-color) 14%, transparent); stroke: none; } .wrn-chart__ticks > span, .wrn-chart__labels > span { position: absolute; color: var(--wrn-color-text-subtle, var(--wrn-color-muted)); font-family: var(--wrn-font-mono, monospace); font-size: 0.65rem; font-variant-numeric: tabular-nums; line-height: 1; white-space: nowrap; pointer-events: none; } .wrn-chart__ticks > span { left: 0; width: calc(44 / 600 * 100% - 0.4rem); text-align: right; transform: translateY(-50%); } .wrn-chart__labels > span { bottom: 0; max-width: 5.5rem; overflow: hidden; font-family: var(--wrn-font-sans, inherit); font-size: 0.72rem; text-overflow: ellipsis; transform: translate(-50%, -0.35rem); } .wrn-chart__hits { position: absolute; inset: 0; } .wrn-chart__hit { position: absolute; padding: 0; border: 0; border-radius: calc(var(--wrn-radius-sm) - 2px); background: transparent; cursor: pointer; } .wrn-chart__hit:hover, .wrn-chart__hit:focus-visible { background: color-mix(in srgb, var(--wrn-color-text) 4%, transparent); outline: none; } .wrn-chart__hit:focus-visible { box-shadow: inset 0 0 0 1.5px var(--wrn-color-focus, var(--wrn-chart-color)); } .wrn-chart__dot { position: absolute; left: 50%; width: 0.5rem; height: 0.5rem; border-radius: 999px; background: var(--wrn-chart-color); box-shadow: 0 0 0 2px var(--wrn-color-surface); transform: translate(-50%, -50%); transition: transform var(--wrn-motion-fast); } .wrn-chart__hit:is(:hover, :focus-visible) .wrn-chart__dot { transform: translate(-50%, -50%) scale(1.4); } .wrn-chart__value { position: absolute; left: 50%; color: var(--wrn-color-text-muted, var(--wrn-color-text)); font-family: var(--wrn-font-mono, monospace); font-size: 0.68rem; font-weight: 600; font-variant-numeric: tabular-nums; transform: translate(-50%, calc(-100% - 0.3rem)); pointer-events: none; } .wrn-chart__tip { position: absolute; z-index: 3; left: 50%; display: grid; gap: 0.1rem; width: max-content; padding: 0.375rem 0.55rem; border: 1px solid rgb(255 255 255 / 0.08); border-radius: var(--wrn-radius-sm, 0.5rem); color: #f4f5f7; background: #16181d; box-shadow: 0 8px 24px -4px rgb(0 0 0 / 0.35); text-align: left; opacity: 0; transform: translate(-50%, calc(-100% - 0.75rem)); transition: opacity var(--wrn-motion-fast); pointer-events: none; } .wrn-chart__tip small { color: rgb(244 245 247 / 0.7); font-size: 0.68rem; } .wrn-chart__tip strong { font-family: var(--wrn-font-mono, monospace); font-size: 0.8125rem; font-variant-numeric: tabular-nums; } .wrn-chart__hit:is(:hover, :focus-visible) .wrn-chart__tip { opacity: 1; } .wrn-chart__hit:is(:hover, :focus-visible) .wrn-chart__value { opacity: 0; } .wrn-chart__empty { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--wrn-color-text-muted, var(--wrn-color-muted)); font-size: 0.8125rem; } .wrn-chart__legend { display: flex; flex-wrap: wrap; gap: 0.375rem; } .wrn-chart__legend button { display: inline-flex; align-items: center; gap: 0.375rem; min-height: 1.75rem; padding: 0 0.6rem; border: 1px solid var(--wrn-color-border); border-radius: 999px; color: var(--wrn-color-text); background: var(--wrn-color-surface); font: inherit; font-size: 0.75rem; font-weight: 550; cursor: pointer; transition: opacity 120ms ease, background-color 120ms ease; } .wrn-chart__legend button:hover { background: var(--wrn-color-surface-soft); } .wrn-chart__legend button > span { width: 0.5rem; height: 0.5rem; border-radius: 2px; background: var(--wrn-chart-color); } .wrn-chart__legend button[aria-pressed="false"] { color: var(--wrn-color-text-subtle, var(--wrn-color-muted)); text-decoration: line-through; } .wrn-chart__legend button[aria-pressed="false"] > span { background: var(--wrn-color-border-strong, var(--wrn-color-border)); } @media (prefers-reduced-motion: reduce) { .wrn-chart__dot, .wrn-chart__tip, .wrn-chart__bar { transition: none; } } } }