MarimekkoChart
100% stacked bars as wide as their totals, so every part's area is its value, with its shares on hover.
Demo
| Hardware | Software | Services | Total | |
|---|---|---|---|---|
| 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%) |
| Brand A | Brand B | Brand C | Others | Total | |
|---|---|---|---|---|---|
| Budget | 120k (11%) | 310k (28.4%) | 260k (23.9%) | 400k (36.7%) | 1,090k (50.1%) |
| Mid-range | 210k (33.1%) | 180k (28.3%) | 150k (23.6%) | 95k (15%) | 635k (29.2%) |
| Premium | 340k (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.qwemain.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>}Related
AreaChartFilled areas for one or more series, overlapping or stacked into a total, with a hover crosshair listing every value.6 views · 8 downloadsBarChartBars up or across, one series or several grouped or stacked, on round ticks, with values on hover and a table for screen readers.7 views · 7 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBoxPlotQuartiles, median, whiskers and outliers for each group, worked out from the raw values, with the five numbers on hover.5 views · 7 downloadsBrushChartA large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.6 views · 5 downloadsBubbleChartPoints placed by two numbers and sized by a third, in coloured groups, labelled when big enough, with all three numbers on hover.7 views · 8 downloads
Thread
Thread 0 posts
On MarimekkoChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.