KpiCard
A number with its label, the change since last period coloured by whether that is good, and a small trend as a line or bars.
Demo
Churn
2.1%
down-0.4 ptsvs last month
Signups
1,284
unchanged0vs yesterday
Updated 5 min ago
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/kpi_card.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/kpi_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/kpi_card.qwe
view KpiCardDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px"> <KpiCard id="kpi-card-demo-mrr" label="Monthly recurring revenue" value="$128,400" change={8.2} change_suffix="%" period="vs last month" trend={[91, 96, 102, 99, 110, 118, 128]} href="#mrr"/> <KpiCard id="kpi-card-demo-churn" label="Churn" value="2.1%" change={-0.4} change_suffix=" pts" period="vs last month" good="down" trend={[3.1, 2.9, 3.0, 2.6, 2.5, 2.4, 2.1]} trend_kind="bars"/> <KpiCard id="kpi-card-demo-signups" label="Signups" value="1,284" change={0} period="vs yesterday" tone="info" trend={[1210, 1250, 1190, 1284, 1284]} note="Updated 5 min ago"/> <KpiCard id="kpi-card-demo-errors" label="Errors, 24 h" value="312" change={41} change_suffix="%" good="down" tone="danger" trend={[120, 140, 110, 180, 260, 312]} trend_kind="bars" href="#errors" link_label="See the log"/> </div>Source
ui/kpi_card.qwe
// KpiCard: one number that matters, in a card: its label, how it changed since// the last period, and a small trend drawn underneath, as a line or bars.//// <KpiCard label="Monthly recurring revenue" value="$128,400" change={8.2} change_suffix="%" period="vs last month"// trend={[91, 96, 102, 99, 110, 118, 128]} href="/reports/mrr"/>// <KpiCard label="Churn" value="2.1%" change={-0.4} change_suffix=" pts" good="down" trend={churn} trend_kind="bars" tone="warning"/>//// label: what it counts. value: the number as it should read (format it first).// change: the change since last period, a number: above 0 shows ↑ and green,// below ↓ and red, 0 grey; good="down" when a fall is good (churn, costs), which// swaps the colours. change_suffix: after the change ("%", " pts"). period: what// the change is against ("vs last month"). trend: numbers to draw, oldest first,// as a line (trend_kind "line", default) or bars ("bars"), in the change's colour// (the tone's without one). tone: "accent" (default), "neutral", "success",// "warning", "danger" or "info". href: a link in the footer ("View" or// link_label). note: a line in the footer instead ("Updated 5 min ago").// id: the element's id.import ui/theme.qwe use ui_clslet kpi_card_css = css`.ui-kpicard { display: grid; gap: 6px; align-content: start; min-width: 0; padding: 16px 18px 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg); --t: var(--ui-accent); --c: var(--ui-muted); }.ui-kpicard-neutral { --t: var(--ui-muted); }.ui-kpicard-success { --t: var(--ui-success); }.ui-kpicard-warning { --t: var(--ui-warning); }.ui-kpicard-danger { --t: var(--ui-danger); }.ui-kpicard-info { --t: var(--ui-info); }.ui-kpicard-good { --c: var(--ui-success); }.ui-kpicard-bad { --c: var(--ui-danger); }.ui-kpicard-label { margin: 0; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-kpicard-value { margin: 0; font-size: 30px; font-weight: 650; line-height: 1.15; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }.ui-kpicard-change { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-size: 13px; color: var(--ui-muted); }.ui-kpicard-delta { display: inline-flex; align-items: center; gap: 2px; padding: 1px 7px 1px 5px; border-radius: 999px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--c); background: color-mix(in srgb, var(--c) 13%, transparent); }.ui-kpicard-trend { display: block; width: 100%; height: 44px; margin-top: 6px; overflow: visible; color: var(--t); }.ui-kpicard-good .ui-kpicard-trend, .ui-kpicard-bad .ui-kpicard-trend { color: var(--c); }.ui-kpicard-trend path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }.ui-kpicard-trend .ui-kpicard-fill { fill: currentColor; opacity: .12; stroke: none; }.ui-kpicard-trend rect { fill: currentColor; opacity: .35; }.ui-kpicard-trend rect:last-child { opacity: 1; }.ui-kpicard-foot { display: flex; justify-content: space-between; gap: 8px; margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--ui-border); font-size: 13px; color: var(--ui-muted); }.ui-kpicard-foot a { color: var(--ui-accent); font-weight: 500; text-decoration: none; }.ui-kpicard-foot a:hover { text-decoration: underline; }.ui-kpicard-foot a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }`// A number as SVG writes it, to one decimal, the same on both sides.fn kpi_card_at(n: num) -> str { let r = round(n * 10) / 10 r == floor(r) ? str(floor(r)) : str(r)}// The trend's line in a 100 by 40 box, as SVG path data.fn kpi_card_path(values: list) -> str { let pts = len(values) == 1 ? [values[0], values[0]] : values let lo = min(pts) let span = max(pts) - lo let step = 100 / (len(pts) - 1) "M" + join(for [i, v] in enumerate(pts) => `${kpi_card_at(i * step)},${kpi_card_at(span == 0 ? 20 : 37 - (v - lo) * 34 / span)}`, " L")}view KpiCard(label: str, value, change: num? = null, change_suffix: str = "", period: str? = null, good: str = "up", trend: list? = null, trend_kind: str = "line", tone: str = "accent", href: str? = null, link_label: str = "View", note: str? = null, id: str? = null, class: str? = null) { style global kpi_card_css let mood = change == null ? null : change == 0 ? "flat" : ((change > 0) == (good != "down") ? "good" : "bad") let has_trend = trend != null && len(trend) > 0 let lo = has_trend ? min(trend) : 0 let span = has_trend ? max(trend) - lo : 0 <div class={ui_cls(["ui-kpicard", tone != "accent" && `ui-kpicard-${tone}`, mood != null && `ui-kpicard-${mood}`, class])} id={id}> <p.ui-kpicard-label>{label}</p> <p.ui-kpicard-value>{value}</p> {(change != null || period != null) && <div.ui-kpicard-change> {change != null && <span.ui-kpicard-delta> <span aria-hidden="true">{change > 0 ? "↑" : (change < 0 ? "↓" : "→")}</span> <span.ui-sr>{change > 0 ? "up" : (change < 0 ? "down" : "unchanged")}</span> {change > 0 ? "+" : ""}{change}{change_suffix} </span>} {period != null && <span>{period}</span>} </div>} {has_trend && <svg.ui-kpicard-trend viewBox="0 0 100 40" preserveAspectRatio="none" aria-hidden="true" data-kind={trend_kind}> {trend_kind == "bars" ? (for [i, v] in enumerate(trend) => { let w = 100 / len(trend) let h = span == 0 ? 20 : 3 + (v - lo) * 34 / span <rect x={kpi_card_at(i * w + w * 0.15)} y={kpi_card_at(40 - h)} width={kpi_card_at(w * 0.7)} height={kpi_card_at(h)} rx="1"/> }) : [ <path.ui-kpicard-fill d={`${kpi_card_path(trend)} L100,40 L0,40 Z`}/>, <path d={kpi_card_path(trend)} vector-effect="non-scaling-stroke"/>, ]} </svg>} {(href != null || note != null) && <div.ui-kpicard-foot> <span>{note}</span> {href != null && <a href={href}>{link_label} →</a>} </div>} </div>}Related
ScorecardA score out of a maximum as a filled ring, with a letter grade, a verdict and the parts it is made of.8 views · 5 downloadsMetricTrendA value with an up or down arrow and its change, coloured by whether the move is good; it can work the percentage out.7 views · 6 downloadsProgressListItems each with a label, a bar and its value or percentage, marked in red when one goes over its limit.6 views · 4 downloadsRatingSummaryThe average rating out of 5, how many ratings, and a bar per star count, each a filter button or link if you like.6 views · 5 downloadsRecordCardA record's fields as labelled values in a card, with a title, status, formatted values and actions in a footer.7 views · 4 downloadsStatA KPI tile: a label, a value, how it changed (coloured by whether that is good), and a tiny trend line.6 views · 5 downloads
Thread
Thread 0 posts
On KpiCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.