ElectionResults
One race's results: candidates in party colours with votes and share bars, the share of the vote counted, the margin, and the winner once it is called.
Demo
Ohio · U.S. Senate
✓ Projected winner| Candidate | Votes | Share |
|---|---|---|
| Maria LopezDEM, winner | 2,145,332 | 49.8% |
| John BeckerREPIncumbent | 2,101,876 | 48.8% |
| Ann WuLIB | 61,422 | 1.4% |
87% of expected vote countedUpdated 11:42 PM ET
Maria Lopez won by 43,456 votes (1.0 points)
Riverside · Mayor
Too early to call| Candidate | Votes | Share |
|---|---|---|
| Sam Ortiz | 18,204 | 40.9% |
| Priya NairIND | 17,950 | 40.3% |
| Dan ColeGRN | 6,011 | 13.5% |
| Others | 2,381 | 5.3% |
41% of expected vote counted
Sam Ortiz leads by 254 votes (0.6 points)
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/election_results.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/election_results.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/election_results.qwe
view ElectionResultsDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; align-items: start"> <ElectionResults id="election-results-demo-ohio" race="Ohio · U.S. Senate" href="#ohio-senate" reporting={87.4} called="Maria Lopez" updated="Updated 11:42 PM ET" candidates={[ { name: "John Becker", party: "REP", votes: 2101876, incumbent: true }, { name: "Maria Lopez", party: "DEM", votes: 2145332 }, { name: "Ann Wu", party: "LIB", votes: 61422 }, ]}/> <ElectionResults id="election-results-demo-mayor" race="Riverside · Mayor" reporting={41} show={3} candidates={[ { name: "Sam Ortiz", votes: 18204, color: "#0f8b8d" }, { name: "Priya Nair", party: "IND", votes: 17950 }, { name: "Dan Cole", party: "GRN", votes: 6011 }, { name: "Lee Park", votes: 1404 }, { name: "Ruth Ames", votes: 977 }, ]}/> </div>Source
ui/election_results.qwe
// ElectionResults: one race on election night: each candidate with their party's// colour, votes and share as a bar, the most votes first; how much of the expected// vote is counted; and who won once the race is called ("✓ Winner", "Projected// winner"), or "Too early to call" while it is not, with the leader's margin.//// <ElectionResults race="Ohio · U.S. Senate" reporting={87} called="Maria Lopez" updated="Updated 11:42 PM ET"// candidates={[// { name: "Maria Lopez", party: "DEM", votes: 2145332 },// { name: "John Becker", party: "REP", votes: 2101876, incumbent: true },// { name: "Ann Wu", party: "LIB", votes: 61422 },// ]}/>//// race: the race's name, an <h3>, a link to href when given. candidates: maps of// { name, votes, party?, color?, incumbent? }; party is shown after the name, and// colours the bar: "DEM" (or "D") blue, "REP" (or "R") red, "GRN" green, "LIB"// gold, "IND" and others grey; color: any CSS colour instead. incumbent: marks them// "Incumbent". reporting: the share of the expected vote counted, 0 to 100 (a// number); left out, no meter. called: the name of the winner once the race is// called; called_label: what it says then ("Projected winner"); uncalled_label:// what it says before ("Too early to call"). show: how many candidates to show (all;// the rest are added up as "Others"). updated: when, as it reads. locale: how numbers// are written ("en-US"). id: the element's id. The candidates are a <table>, so// screen readers read name, votes and share in turn.import ui/theme.qwe use ui_clslet election_results_css = css`.ui-elx { display: grid; gap: 12px; min-width: 0; max-width: 520px; padding: 16px 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.4 var(--ui-font); font-variant-numeric: tabular-nums; }.ui-elx-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }.ui-elx .ui-elx-race { margin: 0; font-size: 17px; font-weight: 750; }.ui-elx-race a { color: inherit; text-decoration: none; }.ui-elx-race a:hover { color: var(--ui-accent); }.ui-elx-race a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-elx-call { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; background: var(--ui-subtle); color: var(--ui-muted); white-space: nowrap; }.ui-elx-called .ui-elx-call { background: var(--ui-success-soft); color: var(--ui-success); }.ui-elx-table { width: 100%; border-collapse: collapse; }.ui-elx-table th { padding: 0 0 6px; color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; text-align: right; }.ui-elx-table th:first-child { text-align: left; }.ui-elx-table td { padding: 8px 0 10px; border-top: 1px solid var(--ui-border); vertical-align: top; text-align: right; white-space: nowrap; }.ui-elx-table td:first-child { width: 100%; text-align: left; white-space: normal; }.ui-elx-table td + td { padding-left: 14px; }.ui-elx-name { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; font-weight: 600; }.ui-elx-party { padding: 0 5px; border-radius: 3px; background: color-mix(in srgb, var(--c) 16%, transparent); color: var(--c); font-size: 11px; font-weight: 700; }.ui-elx-inc { color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-elx-win { display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; background: var(--ui-success); color: var(--ui-bg); font-size: 11px; font-weight: 800; }.ui-elx-bar { display: block; height: 8px; margin-top: 6px; border-radius: 4px; background: var(--ui-subtle); overflow: hidden; }.ui-elx-bar span { display: block; height: 100%; border-radius: 4px; background: var(--c); }.ui-elx-lost .ui-elx-bar span { opacity: .45; }.ui-elx-pct { font-size: 16px; font-weight: 700; }.ui-elx-votes { color: var(--ui-muted); }.ui-elx-winner .ui-elx-pct { color: var(--ui-fg); }.ui-elx-c-dem { --c: var(--ui-info); }.ui-elx-c-rep { --c: var(--ui-danger); }.ui-elx-c-grn { --c: var(--ui-success); }.ui-elx-c-lib { --c: var(--ui-warning); }.ui-elx-c-other { --c: var(--ui-muted); }.ui-elx-foot { display: grid; gap: 6px; color: var(--ui-muted); font-size: 12px; }.ui-elx-count { display: flex; justify-content: space-between; gap: 8px; }.ui-elx-count b { color: var(--ui-fg); font-weight: 650; }.ui-elx-meter { height: 4px; border-radius: 2px; background: var(--ui-border); overflow: hidden; }.ui-elx-meter span { display: block; height: 100%; background: var(--ui-fg); }.ui-elx-margin { margin: 0; }.ui-elx-margin b { color: var(--ui-fg); font-weight: 650; }`// The class of a party's colour: "DEM" is dem.fn election_results_party(party: str?) -> str = match upper(party ?? "") { "DEM" | "D" => "dem" "REP" | "R" | "GOP" => "rep" "GRN" | "G" | "GREEN" => "grn" "LIB" | "L" => "lib" _ => "other"}// A share to one decimal, as it reads: "51.2%".fn election_results_share(p: num, locale: str) -> str = `${format_number(round(p, 1), locale, 1)}%`view ElectionResults(race: str, candidates: list, reporting = null, called: str? = null, called_label: str = "Projected winner", uncalled_label: str = "Too early to call", show: int? = null, updated: str? = null, href: str? = null, locale: str = "en-US", id: str? = null, class: str? = null) { style global election_results_css let ranked = sort_by(candidates, (c) => -c.votes) let kept = show != null && len(ranked) > show ? take(ranked, show) + [{ name: "Others", votes: sum(drop(ranked, show) | map((c) => c.votes)), others: true }] : ranked let total = sum(candidates | map((c) => c.votes)) let share = (v) => total > 0 ? v * 100 / total : 0 let lead = len(ranked) > 1 ? ranked[0].votes - ranked[1].votes : null let is_called = called != null let counted = reporting != null ? (reporting < 0 ? 0 : (reporting > 100 ? 100 : reporting)) : null let num = (n) => format_number(n, locale, 0) <section class={ui_cls(["ui-elx", is_called && "ui-elx-called", class])} id={id} aria-label={`Results: ${race}`}> <div.ui-elx-head> <h3.ui-elx-race>{href != null ? <a href={href}>{race}</a> : race}</h3> <span.ui-elx-call>{is_called ? ["✓ ", called_label] : uncalled_label}</span> </div> <table.ui-elx-table> <caption class="ui-sr">{race}: votes and share for each candidate{is_called ? `; ${called} is the ${lower(called_label)}` : ""}</caption> <thead><tr><th scope="col">Candidate</th><th scope="col">Votes</th><th scope="col">Share</th></tr></thead> <tbody> {for c in kept => { let won = is_called && c.name == called let tone = c.color != null ? "" : `ui-elx-c-${c.others == true ? "other" : election_results_party(c.party)}` <tr class={ui_cls([tone, won && "ui-elx-winner", is_called && !won && "ui-elx-lost"])} style={c.color != null ? `--c: ${c.color}` : null}> <td> <span.ui-elx-name> {won && <span.ui-elx-win aria-hidden="true">✓</span>} <span>{c.name}</span> {c.party != null && <span.ui-elx-party>{upper(c.party)}</span>} {c.incumbent == true && <span.ui-elx-inc>Incumbent</span>} {won && <span.ui-sr>, winner</span>} </span> <span.ui-elx-bar aria-hidden="true"><span style={`width: ${round(share(c.votes), 2)}%`}></span></span> </td> <td.ui-elx-votes>{num(c.votes)}</td> <td.ui-elx-pct>{election_results_share(share(c.votes), locale)}</td> </tr> }} </tbody> </table> <div.ui-elx-foot> {counted != null && [ <div.ui-elx-count><span><b>{format_number(round(counted), locale, 0)}%</b> of expected vote counted</span>{updated != null && <span>{updated}</span>}</div>, <div.ui-elx-meter role="progressbar" aria-label="Expected vote counted" aria-valuemin="0" aria-valuemax="100" aria-valuenow={round(counted)}> <span style={`width: ${round(counted, 2)}%`}></span> </div>, ]} {counted == null && updated != null && <div.ui-elx-count><span>{updated}</span></div>} {lead != null && total > 0 && <p.ui-elx-margin> <b>{ranked[0].name}</b> {is_called && ranked[0].name == called ? "won" : "leads"} by {num(lead)} votes ({format_number(round(share(lead), 1), locale, 1)} points) </p>} </div> </section>}Related
SeatChartA parliament as a half circle of seats coloured by party, undeclared seats in grey, the majority marked, and a legend with seats won and lost.8 views · 7 downloadsPortfolioAllocationA donut of a portfolio's holdings beside a table of each one's weight, value and gain, with the totals.6 views · 6 downloadsAdSlotAn advertisement's place kept at its size so the story never jumps: standard sizes by name, another on phones, a clear label, a placeholder, sticky in a rail.6 views · 8 downloadsArticleHeaderThe top of a story: its label and section, the headline, the dek, every author grouped by role, published and updated times, and the reading time.8 views · 8 downloadsBreakingNewsA breaking-news banner: the label in a block of colour, the headline and a line more, how long ago it came in, kept up to date, and a close that can be remembered.9 views · 8 downloadsCorrectionNoticeA correction, clarification, update, editor's note or retraction on a story, dated, with a link to the corrections policy; boxed on top or a line at the end.8 views · 7 downloads
Thread
Thread 0 posts
On ElectionResults: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.