Data

Scorecard

A score out of a maximum as a filled ring, with a letter grade, a verdict and the parts it is made of.

#data#dashboard#chart#progress#no-js

Demo

Preview

Performance

AExcellent

Largest paint 1.1 s

SEO

DFair

3 pages lack a description

Quiz

RetryNot yet

Accessibility

CFair

5 issues to fix

  • Contrast18 / 25
  • Labels and names20 / 25
  • Keyboard25 / 25
  • Structure8 / 25

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/scorecard.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/scorecard.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/scorecard.qwe
view ScorecardDemo() =    <div.demo-stack>        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px">            <Scorecard id="scorecard-demo-perf" label="Performance" score={92} hint="Largest paint 1.1 s"/>            <Scorecard id="scorecard-demo-seo" label="SEO" score={64} hint="3 pages lack a description"/>            <Scorecard id="scorecard-demo-quiz" label="Quiz" score={4} limit={10} grade="Retry" verdict="Not yet" tone="accent"/>        </div>        <Scorecard id="scorecard-demo-a11y" label="Accessibility" score={71} size="lg" hint="5 issues to fix" items={[            { label: "Contrast", score: 18, limit: 25 },            { label: "Labels and names", score: 20, limit: 25 },            { label: "Keyboard", score: 25, limit: 25 },            { label: "Structure", score: 8, limit: 25 },        ]}/>    </div>

Source

ui/scorecard.qwe
// Scorecard: a score out of a maximum, as a ring filled that far with the score in// it, a letter grade and a verdict, and optionally the parts it is made of: a// site audit, a credit score, a quiz result, a health check.////   <Scorecard label="Performance" score={92}/>//   <Scorecard label="Accessibility" score={71} hint="5 issues to fix" items={[//       { label: "Contrast", score: 18, limit: 25 },//       { label: "Labels", score: 20, limit: 25 },//   ]}/>//   <Scorecard label="Quiz" score={7} limit={10} grade="Pass" tone="success"/>//// score: the number scored. limit: the most there is (100). label: what is// scored, above the ring and in what screen readers say. grade: the letter or word// shown (by default from the share of limit: A from 90%, B from 80%, C from 70%,// D from 60%, else F). verdict: a word under it (by default "Excellent", "Good",// "Fair" or "Poor"). tone: the ring's colour, "success", "warning" or "danger"// (by default: from 80% success, from 50% warning, else danger), or "accent".// hint: a line under the verdict. items: parts as { label, score, limit? (the// same limit by default) }, each a thin bar beside the ring. size: "md"// (default, 96px ring) or "lg" (128px). card: false drops the border.// The ring is a role="meter" with its value, so screen readers read the score.import ui/theme.qwe use ui_clslet scorecard_css = css`.ui-scorecard { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 24px; min-width: 0; padding: 18px 20px;  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); --s: 96px; }.ui-scorecard-bare { padding: 0; border: 0; background: none; }.ui-scorecard-success { --t: var(--ui-success); }.ui-scorecard-warning { --t: var(--ui-warning); }.ui-scorecard-danger { --t: var(--ui-danger); }.ui-scorecard-lg { --s: 128px; }.ui-scorecard-main { display: flex; align-items: center; gap: 16px; }.ui-scorecard-ring { position: relative; flex: none; display: grid; place-items: center; width: var(--s); height: var(--s); }.ui-scorecard-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }.ui-scorecard-ring circle { fill: none; stroke-width: 9; }.ui-scorecard-track { stroke: var(--ui-subtle); }.ui-scorecard-arc { stroke: var(--t); stroke-linecap: round; transition: stroke-dasharray .4s ease; }.ui-scorecard-num { display: grid; justify-items: center; line-height: 1; }.ui-scorecard-num b { font-size: calc(var(--s) * .3); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }.ui-scorecard-num span { margin-top: 3px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-scorecard-text { display: grid; gap: 3px; }.ui-scorecard-label { margin: 0; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-scorecard-grade { display: flex; align-items: center; gap: 8px; margin: 0; }.ui-scorecard-letter { display: inline-grid; place-items: center; min-width: 32px; height: 32px; padding: 0 8px; border-radius: var(--ui-radius-sm);  background: color-mix(in srgb, var(--t) 15%, transparent); color: var(--t); font-size: 18px; font-weight: 700; }.ui-scorecard-verdict { font-size: 16px; font-weight: 600; }.ui-scorecard-hint { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-scorecard-items { flex: 1 1 200px; display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-scorecard-item { display: grid; grid-template-columns: 1fr auto; gap: 3px 10px; font-size: 13px; }.ui-scorecard-item span:last-child { color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-scorecard-bar { grid-column: 1 / -1; height: 6px; overflow: hidden; border-radius: 999px; background: var(--ui-subtle); }.ui-scorecard-bar i { display: block; height: 100%; border-radius: inherit; background: var(--t); }@media (prefers-reduced-motion: reduce) { .ui-scorecard-arc { transition: none; } }`// A number as text, whole ones without ".0".fn scorecard_num(n: num) -> str = n == floor(n) ? str(floor(n)) : str(round(n * 10) / 10)fn scorecard_grade(pct: num) -> str = pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 70 ? "C" : pct >= 60 ? "D" : "F"fn scorecard_verdict(pct: num) -> str = pct >= 90 ? "Excellent" : pct >= 75 ? "Good" : pct >= 50 ? "Fair" : "Poor"view Scorecard(score: num, limit: num = 100, label: str? = null, grade: str? = null, verdict: str? = null, tone: str? = null,               hint: str? = null, items: list = [], size: str = "md", card: bool = true, id: str? = null, class: str? = null) {    style global scorecard_css    let part = limit <= 0 ? 0 : (score < 0 ? 0 : (score > limit ? limit : score))    let pct = limit <= 0 ? 0 : round(part * 1000 / limit) / 10    let colour = tone ?? (pct >= 80 ? "success" : pct >= 50 ? "warning" : "danger")    let letter = grade ?? scorecard_grade(pct)    let word = verdict ?? scorecard_verdict(pct)    // The ring: a circle of radius 42 in a 100 box; its length is 2πr, about 263.9.    let dash = round(pct * 263.9 / 100 * 10) / 10    <div class={ui_cls(["ui-scorecard", `ui-scorecard-${colour}`, size == "lg" && "ui-scorecard-lg", !card && "ui-scorecard-bare", class])} id={id}>        <div.ui-scorecard-main>            <div.ui-scorecard-ring role="meter" aria-valuemin="0" aria-valuemax={scorecard_num(limit)} aria-valuenow={scorecard_num(part)}                 aria-valuetext={`${scorecard_num(score)} out of ${scorecard_num(limit)}, grade ${letter}`} aria-label={label ?? "Score"}>                <svg viewBox="0 0 100 100" aria-hidden="true">                    <circle.ui-scorecard-track cx="50" cy="50" r="42"/>                    <circle.ui-scorecard-arc cx="50" cy="50" r="42" stroke-dasharray={`${dash} 264`}/>                </svg>                <span.ui-scorecard-num aria-hidden="true"><b>{scorecard_num(score)}</b><span>of {scorecard_num(limit)}</span></span>            </div>            <div.ui-scorecard-text>                {label != null && <p.ui-scorecard-label>{label}</p>}                <p.ui-scorecard-grade><span.ui-scorecard-letter>{letter}</span><span.ui-scorecard-verdict>{word}</span></p>                {hint != null && <p.ui-scorecard-hint>{hint}</p>}            </div>        </div>        {len(items) > 0 && <ul.ui-scorecard-items aria-label={label != null ? `${label}, by part` : "By part"}>            {for it in items => {                let top = it.limit ?? limit                let w = top <= 0 ? 0 : round(min([max([it.score, 0]), top]) * 1000 / top) / 10                <li.ui-scorecard-item>                    <span>{it.label}</span>                    <span>{scorecard_num(it.score)} / {scorecard_num(top)}</span>                    <span.ui-scorecard-bar aria-hidden="true"><i style={`width: ${w}%`}></i></span>                </li>            }}        </ul>}    </div>}
Thread
85Useful? Like it, or add to its thread below.

Thread 0 posts

On Scorecard: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.