Data

RatingSummary

The average rating out of 5, how many ratings, and a bar per star count, each a filter button or link if you like.

#chart#commerce#social#data#no-js

Demo

Preview
4.5180 ratings
4.3100 ratings

Showing all reviews.

4.810 ratings
  • 5 stars90%, 9 ratings
  • 4 stars10%, 1 rating
  • 3 stars0%, 0 ratings
  • 2 stars0%, 0 ratings
  • 1 star0%, 0 ratings
00 ratings
  • 5 stars0%, 0 ratings
  • 4 stars0%, 0 ratings
  • 3 stars0%, 0 ratings
  • 2 stars0%, 0 ratings
  • 1 star0%, 0 ratings

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

Example

The code of the preview above.

demos/rating_summary.qwe
component RatingSummaryDemoLive() {    state only = null    <div.demo-stack>        <RatingSummary id="rating-summary-demo-live" counts={{ "5": 61, "4": 22, "3": 9, "2": 3, "1": 5 }} picked={only} on_pick={(s) => { only = s }}/>        <p.demo-note>{only == null ? "Showing all reviews." : `Showing ${only}-star reviews.`}</p>    </div>}view RatingSummaryDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 28px; align-items: start">        <RatingSummary id="rating-summary-demo-plain" label="Kettle" counts={{ "5": 120, "4": 40, "3": 10, "2": 4, "1": 6 }}                       hrefs={{ "5": "#rating_summary", "4": "#rating_summary" }}/>        <RatingSummaryDemoLive/>        <RatingSummary id="rating-summary-demo-compact" compact={true} average={4.8} counts={{ "5": 9, "4": 1 }}/>        <RatingSummary id="rating-summary-demo-none" counts={{}}/>    </div>

Source

ui/rating_summary.qwe
// RatingSummary: how a thing is rated: the average out of 5, how many ratings, and// a bar for each star count, so a glance says whether the fives or the ones win.////   <RatingSummary counts={{ "5": 120, "4": 40, "3": 10, "2": 4, "1": 6 }}/>//   <RatingSummary counts={tally} picked={only} on_pick={(s) => { only = s }}/>//// counts: a map of star to how many gave it ({ "5": 120, ..., "1": 6 }; a star left// out counts 0). average: the average to show, worked out from the counts when// left out (one decimal). label: what it is a rating of, for screen readers.// on_pick(stars): inside one of your components, makes each row a button that// is called with its stars (null again for the one already picked); picked: the// stars whose row shows pressed. Without on_pick, hrefs: a map of star to a link// each row goes to. compact: the average and bars side by side in less room.import ui/theme.qwe use ui_clslet rating_summary_css = css`.ui-rating-summary { display: grid; grid-template-columns: auto 1fr; gap: 8px 20px; align-items: center; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-rating-summary-avg { display: grid; justify-items: center; gap: 2px; min-width: 96px; text-align: center; }.ui-rating-summary-num { font-size: 40px; font-weight: 700; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }.ui-rating-summary-stars { display: inline-flex; gap: 1px; color: var(--ui-warning); }.ui-rating-summary-stars svg { width: 16px; height: 16px; }.ui-rating-summary-off { color: var(--ui-border); }.ui-rating-summary-half { clip-path: inset(0 50% 0 0); }.ui-rating-summary-total { color: var(--ui-muted); font-size: 13px; }.ui-rating-summary-rows { display: grid; gap: 5px; margin: 0; padding: 0; list-style: none; }.ui-rating-summary-row { display: grid; grid-template-columns: 4.2em 1fr 3.2em; align-items: center; gap: 10px; width: 100%; padding: 2px 0; border: 0;  border-radius: var(--ui-radius-sm); background: none; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; }.ui-rating-summary-row:is(button, a) { cursor: pointer; }.ui-rating-summary-row:is(button, a):hover { background: var(--ui-subtle); }.ui-rating-summary-row:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-rating-summary-row[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-rating-summary-row[aria-pressed="true"] .ui-rating-summary-fill { background: var(--ui-accent); }.ui-rating-summary-name { white-space: nowrap; font-size: 13px; }.ui-rating-summary-bar { height: 8px; border-radius: 999px; background: var(--ui-subtle); overflow: hidden; }.ui-rating-summary-fill { display: block; height: 100%; border-radius: inherit; background: var(--ui-warning); }.ui-rating-summary-n { color: var(--ui-muted); font-size: 13px; text-align: right; font-variant-numeric: tabular-nums; }.ui-rating-summary-compact { gap: 4px 14px; }.ui-rating-summary-compact .ui-rating-summary-num { font-size: 28px; }.ui-rating-summary-compact .ui-rating-summary-avg { min-width: 72px; }.ui-rating-summary-compact .ui-rating-summary-rows { gap: 2px; }@container (max-width: 360px) { .ui-rating-summary { grid-template-columns: 1fr; } }`fn rating_summary_count(counts: map, s: int) -> int = int(get(counts, str(s)) ?? 0) ?? 0view RatingSummary(counts: map = {}, average: num? = null, label: str = "Rating", picked: int? = null, on_pick = null,                   hrefs: map = {}, compact: bool = false, id: str? = null, class: str? = null) {    style global rating_summary_css    let total = sum(for s in range(1, 6) => rating_summary_count(counts, s))    let avg = average ?? (total == 0 ? 0 : round(sum(for s in range(1, 6) => s * rating_summary_count(counts, s)) / total, 1))    let top = max(for s in range(1, 6) => rating_summary_count(counts, s)) ?? 0    <div class={ui_cls(["ui-rating-summary", compact && "ui-rating-summary-compact", class])} id={id}>        <div.ui-rating-summary-avg>            <span.ui-rating-summary-num>{avg}</span>            <span.ui-rating-summary-stars role="img" aria-label={`${label}: ${avg} out of 5 stars`}>                {for i in range(0, 5) => {                    let fill = avg >= i + 1 ? "full" : avg >= i + 0.5 ? "half" : "off"                    <svg viewBox="0 0 24 24" class={fill == "off" ? "ui-rating-summary-off" : null} fill="currentColor" aria-hidden="true">                        {fill == "half" && <path.ui-rating-summary-off d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>}                        <path class={fill == "half" ? "ui-rating-summary-half" : null} d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>                    </svg>                }}            </span>            <span.ui-rating-summary-total>{total} {total == 1 ? "rating" : "ratings"}</span>        </div>        <ul.ui-rating-summary-rows aria-label="Ratings by stars">            {for s in [5, 4, 3, 2, 1] => {                let n = rating_summary_count(counts, s)                let pct = total == 0 ? 0 : round(n * 100 / total)                let body = [                    <span.ui-rating-summary-name>{s} {s == 1 ? "star" : "stars"}</span>,                    <span.ui-rating-summary-bar aria-hidden="true"><span.ui-rating-summary-fill style={`width: ${top == 0 ? 0 : round(n * 100 / top)}%`}></span></span>,                    <span.ui-rating-summary-n>{pct}%</span>,                    <span.ui-sr>, {n} {n == 1 ? "rating" : "ratings"}</span>,                ]                <li key={s}>                    {on_pick != null                        ? <button.ui-rating-summary-row type="button" aria-pressed={picked == s ? "true" : "false"} disabled={n == 0}                                  @click={() => { on_pick(picked == s ? null : s) }}>{body}</button>                        : get(hrefs, str(s)) != null                        ? <a.ui-rating-summary-row href={get(hrefs, str(s))} aria-current={picked == s ? "true" : null}>{body}</a>                        : <span.ui-rating-summary-row>{body}</span>}                </li>            }}        </ul>    </div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.