108 lines
5.4 KiB
Plaintext
108 lines
5.4 KiB
Plaintext
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[] = []
|
|
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 numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
|
|
shared function maximumValue() {
|
|
return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
|
|
}
|
|
shared function isHidden(index) { return hiddenIndexes.includes(index) }
|
|
shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
|
|
shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
|
|
shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
|
|
shared function barY(item) { return 84 - barHeight(item) }
|
|
|
|
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 = hiddenIndexes.slice()
|
|
if (next.includes(index)) {
|
|
next = next.filter(function (value) { return value !== index })
|
|
} else {
|
|
next.push(index)
|
|
}
|
|
hiddenIndexes = next
|
|
output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
|
|
}
|
|
}
|
|
|
|
view {
|
|
<figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wrn-chart {class}'>
|
|
{#if title || description}
|
|
<figcaption class="wrn-chart__header">
|
|
{#if title}<strong>{title}</strong>{/if}
|
|
{#if description}<span>{description}</span>{/if}
|
|
</figcaption>
|
|
{/if}
|
|
<svg class="wrn-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
|
|
<line x1="8" y1="84" x2="96" y2="84" class="wrn-chart__axis" />
|
|
{#each items as item, index}
|
|
<g data-hidden='{isHidden(index) ? "true" : "false"}'>
|
|
<rect
|
|
x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
|
|
rx="1.5" class="wrn-chart__bar" role="button" tabindex="0"
|
|
aria-label='{item[labelKey] + ": " + numericValue(item)}'
|
|
@click='selectPoint(item, index, event)'
|
|
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
|
|
/>
|
|
{#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wrn-chart__value">{numericValue(item)}</text>{/if}
|
|
<text x='{barX(index) + barWidth() / 2}' y="94" class="wrn-chart__label">{item[labelKey]}</text>
|
|
</g>
|
|
{/each}
|
|
</svg>
|
|
{#if showLegend}
|
|
<div class="wrn-chart__legend" aria-label="Chart legend">
|
|
{#each items as item, index}
|
|
<button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
|
|
<span aria-hidden="true"></span>{item[labelKey]}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
<slot />
|
|
</figure>
|
|
}
|
|
|
|
style {
|
|
.wrn-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; 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__header { display: grid; gap: 0.2rem; }
|
|
.wrn-chart__header > span { color: var(--wrn-color-muted); font-size: 0.8rem; }
|
|
.wrn-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
|
|
.wrn-chart__axis { stroke: var(--wrn-color-border); stroke-width: 0.5; }
|
|
.wrn-chart__bar { fill: var(--wrn-color-primary); cursor: pointer; transition: opacity var(--wrn-motion-fast), transform var(--wrn-motion-fast); transform-box: fill-box; transform-origin: bottom; }
|
|
.wrn-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
|
|
.wrn-chart__bar:focus-visible { outline: 1.5px solid var(--wrn-color-focus); outline-offset: 1px; }
|
|
.wrn-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
|
|
.wrn-chart__value, .wrn-chart__label { fill: var(--wrn-color-text); font-size: 3.5px; text-anchor: middle; }
|
|
.wrn-chart__label { fill: var(--wrn-color-muted); }
|
|
.wrn-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
.wrn-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wrn-color-border); border-radius: 999px; color: var(--wrn-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
|
|
.wrn-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wrn-color-primary); }
|
|
.wrn-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
|
|
}
|
|
}
|