Charts

MarimekkoChart

100% stacked bars as wide as their totals, so every part's area is its value, with its shares on hover.

#chart#data#no-js

Demo

Preview
Revenue by region and product
Revenue by region and product
HardwareSoftwareServicesTotal
Europe$42,000 (46.7%)$30,000 (33.3%)$18,000 (20%)$90,000 (33.1%)
Americas$61,000 (45.5%)$52,000 (38.8%)$21,000 (15.7%)$134,000 (49.2%)
Asia$18,000 (43.4%)$9,000 (21.7%)$14,500 (34.9%)$41,500 (15.2%)
Africa$3,500 (51.5%)$1,200 (17.6%)$2,100 (30.9%)$6,800 (2.5%)
Phones sold by brand and price band
Phones sold by brand and price band
Brand ABrand BBrand COthersTotal
Budget120k (11%)310k (28.4%)260k (23.9%)400k (36.7%)1,090k (50.1%)
Mid-range210k (33.1%)180k (28.3%)150k (23.6%)95k (15%)635k (29.2%)
Premium340k (75.6%)90k (20%)20k (4.4%)0k (0%)450k (20.7%)

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

Example

The code of the preview above.

demos/marimekko_chart.qwe
view MarimekkoChartDemo() =    <div.demo-stack>        <MarimekkoChart label="Revenue by region and product" show_label={true} prefix="$"                        labels={["Europe", "Americas", "Asia", "Africa"]}                        series={[{ name: "Hardware", values: [42000, 61000, 18000, 3500] },                                 { name: "Software", values: [30000, 52000, 9000, 1200] },                                 { name: "Services", values: [18000, 21000, 14500, 2100] }]}/>        <MarimekkoChart label="Phones sold by brand and price band" show_label={true} suffix="k" height={200} value_labels={false}                        labels={["Budget", "Mid-range", "Premium"]}                        series={[{ name: "Brand A", values: [120, 210, 340] }, { name: "Brand B", values: [310, 180, 90] },                                 { name: "Brand C", values: [260, 150, 20] }, { name: "Others", values: [400, 95, null] }]}/>    </div>

Source

ui/marimekko_chart.qwe
// MarimekkoChart: a 100% stacked bar chart whose bars are as wide as their totals,// so each rectangle's area is its value: market share by segment and region, spend// by team and category. CSS only: hovering a part shows its value and shares, and// screen readers get the numbers as a table.////   <MarimekkoChart label="Revenue by region" prefix="$" labels={["Europe", "Americas", "Asia"]} series={[//       { name: "Hardware", values: [42000, 61000, 18000] }, { name: "Software", values: [30000, 52000, 9000] }]}/>//// labels: the bars, left to right; each is written under its bar with its share of// the whole. series: a list of { name, values }, one value per label (up to eight,// each in a colour of its own, with a legend), stacked from the bottom; a null or// negative value counts as 0. Each bar's parts add up to 100% of its height and its// width is its share of everything. value_labels: each part's share of its bar// written inside it, where there is room (true). prefix and suffix: around every// value shown ("$", " GB"); decimals: digits after the point, always, instead of up// to two as needed. height: the plot's height in pixels (260). label: what the chart// shows; it names it for screen readers and captions their table. show_label also// writes it above.import ui/theme.qwe use ui_clslet marimekko_chart_css = css`.ui-mekko {  --ui-mekko-1: light-dark(#2a78d6, #3987e5); --ui-mekko-2: light-dark(#eb6834, #d95926);  --ui-mekko-3: light-dark(#1baf7a, #199e70); --ui-mekko-4: light-dark(#eda100, #c98500);  --ui-mekko-5: light-dark(#e87ba4, #d55181); --ui-mekko-6: light-dark(#008300, #008300);  --ui-mekko-7: light-dark(#6250d6, #9085e9); --ui-mekko-8: light-dark(#e34948, #e66767);  display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-mekko-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-mekko-s1 { --c: var(--ui-mekko-1); } .ui-mekko-s2 { --c: var(--ui-mekko-2); } .ui-mekko-s3 { --c: var(--ui-mekko-3); }.ui-mekko-s4 { --c: var(--ui-mekko-4); } .ui-mekko-s5 { --c: var(--ui-mekko-5); } .ui-mekko-s6 { --c: var(--ui-mekko-6); }.ui-mekko-s7 { --c: var(--ui-mekko-7); } .ui-mekko-s8 { --c: var(--ui-mekko-8); }.ui-mekko-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-mekko-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-mekko-key { width: 10px; height: 10px; border-radius: 3px; background: var(--c); flex: none; }.ui-mekko-frame { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 8px; padding-top: 8px; }.ui-mekko-y { position: relative; min-width: 24px; }.ui-mekko-y span { position: absolute; right: 0; transform: translateY(50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-mekko-plot { position: relative; }.ui-mekko-col { position: absolute; top: 0; bottom: 0; }.ui-mekko-part { position: absolute; left: 0; right: 0; bottom: var(--a); height: var(--b); border: 1px solid var(--ui-bg);  border-radius: 3px; background: var(--c); color: #fff; container-type: size; }.ui-mekko-part:hover { z-index: 5; filter: brightness(1.08); outline: 2px solid var(--ui-fg); outline-offset: -1px; }.ui-mekko-in { display: grid; place-items: center; height: 100%; overflow: hidden; font-size: 11px; font-weight: 600;  font-variant-numeric: tabular-nums; white-space: nowrap; }@container (max-width: 34px) or (max-height: 18px) { .ui-mekko-in { display: none; } }.ui-mekko-tip { position: absolute; z-index: 20; display: none; min-width: 120px; padding: 7px 10px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  box-shadow: var(--ui-shadow); color: var(--ui-fg); font-size: 12px; pointer-events: none; }.ui-mekko-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-mekko-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-mekko-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-mekko-tip { bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); }.ui-mekko-first .ui-mekko-tip { left: 0; transform: none; }.ui-mekko-last .ui-mekko-tip { left: auto; right: 0; transform: none; }.ui-mekko-part:hover .ui-mekko-tip { display: block; }.ui-mekko-x { position: relative; height: 32px; margin-top: 6px; }.ui-mekko-x span { position: absolute; top: 0; display: grid; overflow: hidden; color: var(--ui-muted); text-align: center; }.ui-mekko-x b { overflow: hidden; color: var(--ui-fg); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }.ui-mekko-x small { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }`// n (a whole number, 0 or more) with at least `width` digits.fn marimekko_chart_pad(n: int, width: int) -> str {    let s = str(n)    len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn marimekko_chart_group(n: int) -> str =    n < 1000 ? str(n) : marimekko_chart_group(floor(n / 1000)) + "," + marimekko_chart_pad(n % 1000, 3)// A value as it reads, with commas: `places` digits after the point, or up to two as// needed when that is null: 48210 is "48,210", 6350.5 is "6,350.5" ("6,350.50" with 2).// Worked out in whole numbers, so the server and the browser write the same text.fn marimekko_chart_num(v: num, places: int? = null) -> str {    let digits = places == null ? 2 : (places < 0 ? 0 : (places > 4 ? 4 : places))    let scale = [1, 10, 100, 1000, 10000][digits]    let units = round(abs(v) * scale)    let frac = units % scale    let tail = digits == 0 || (places == null && frac == 0) ? ""        : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${marimekko_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${marimekko_chart_group(floor(units / scale))}${tail}`}// A coordinate or a percentage for CSS and SVG, to two decimals.fn marimekko_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// A share out of 100, to one decimal: "42%", "12.5%", "0.4%".fn marimekko_chart_share(p: num) -> str {    let tenths = round(p * 10)    `${floor(tenths / 10)}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}%`}view MarimekkoChart(labels: list, series: list, value_labels: bool = true, prefix: str = "", suffix: str = "",                    decimals: int? = null, height: int = 260, label: str = "Marimekko chart", show_label: bool = false,                    class: str? = null) {    style global marimekko_chart_css    let n = len(labels)    let k = len(series)    let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${marimekko_chart_num(abs(v), decimals)}${suffix}`    let at = (j, i) => series[j].values[i] != null && series[j].values[i] > 0 ? series[j].values[i] : 0    let totals = for i in range(n) => sum(for j in range(k) => at(j, i))    let all = sum(totals)    let wide = (i) => all > 0 ? totals[i] * 100 / all : 0    let lefts = for i in range(n) => sum(for q in range(i) => wide(q))    let tall = (j, i) => totals[i] > 0 ? at(j, i) * 100 / totals[i] : 0    <figure class={ui_cls(["ui-mekko", class])} aria-label={label}>        {show_label && <figcaption.ui-mekko-title>{label}</figcaption>}        <ul.ui-mekko-legend aria-hidden="true">            {for [j, s] in enumerate(series) => <li><span class={`ui-mekko-key ui-mekko-s${j % 8 + 1}`}></span>{s.name}</li>}        </ul>        <div.ui-mekko-frame aria-hidden="true">            <div.ui-mekko-y style={`height: ${height}px`}>                {for t in [0, 25, 50, 75, 100] => <span style={`bottom: ${t}%`}>{t}%</span>}            </div>            <div.ui-mekko-plot style={`height: ${height}px`}>                {for i in range(n) => totals[i] > 0 && <div                    class={ui_cls(["ui-mekko-col", i == 0 && n > 1 && "ui-mekko-first", i == n - 1 && n > 1 && "ui-mekko-last"])}                    style={`left: ${marimekko_chart_pct(lefts[i])}%; width: ${marimekko_chart_pct(wide(i))}%`}>                    {for j in range(k) => at(j, i) > 0 && <div class={`ui-mekko-part ui-mekko-s${j % 8 + 1}`}                        style={`--a: ${marimekko_chart_pct(sum(for q in range(j) => tall(q, i)))}%; --b: ${marimekko_chart_pct(tall(j, i))}%`}>                        {value_labels && <span.ui-mekko-in>{marimekko_chart_share(tall(j, i))}</span>}                        <span.ui-mekko-tip>                            <strong>{series[j].name} · {labels[i]}</strong>                            <p>Value<b>{fmt(at(j, i))}</b></p>                            <p>Share of {labels[i]}<b>{marimekko_chart_share(tall(j, i))}</b></p>                            <p>Share of all<b>{marimekko_chart_share(all > 0 ? at(j, i) * 100 / all : 0)}</b></p>                        </span>                    </div>}                </div>}            </div>            <span></span>            <div.ui-mekko-x>                {for i in range(n) => totals[i] > 0 && <span style={`left: ${marimekko_chart_pct(lefts[i])}%; width: ${marimekko_chart_pct(wide(i))}%`}                    title={`${labels[i]}: ${fmt(totals[i])}`}>                    <b>{labels[i]}</b><small>{fmt(totals[i])} · {marimekko_chart_share(wide(i))}</small>                </span>}            </div>        </div>        <table.ui-sr>            <caption>{label}</caption>            <thead><tr><th scope="col"></th>{for s in series => <th scope="col">{s.name}</th>}<th scope="col">Total</th></tr></thead>            <tbody>                {for i in range(n) => <tr>                    <th scope="row">{labels[i]}</th>                    {for j in range(k) => <td>{fmt(at(j, i))} ({marimekko_chart_share(tall(j, i))})</td>}                    <td>{fmt(totals[i])} ({marimekko_chart_share(wide(i))})</td>                </tr>}            </tbody>        </table>    </figure>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.