* The two-stage conversion funnel, drawn as proportional bars rather than a * chart: two bars and a percentage is the whole story, and a two-slice pie or a * two-bar chart would be more chrome than data.
(section, funnel)
| 269 | * two-bar chart would be more chrome than data. |
| 270 | */ |
| 271 | function drawFunnel(section, funnel) { |
| 272 | const host = $(section, 'funnel'); |
| 273 | host.replaceChildren(); |
| 274 | |
| 275 | for (const stage of funnel.stages) { |
| 276 | const row = el('div', 'funnel-stage'); |
| 277 | const head = el('div', 'funnel-label'); |
| 278 | head.append(el('span', null, stage.label), el('span', 'funnel-value', stage.value.toLocaleString('en-US'))); |
| 279 | const track = el('div', 'funnel-track'); |
| 280 | const fill = el('div', 'funnel-fill'); |
| 281 | // Width is the datum, so it is set from JS rather than a style attribute — |
| 282 | // the CSP here allows no inline styles at all. |
| 283 | fill.style.width = `${Math.max(0, Math.min(1, stage.share)) * 100}%`; |
| 284 | track.append(fill); |
| 285 | row.append(head, track); |
| 286 | host.append(row); |
| 287 | } |
| 288 | |
| 289 | const rate = funnel.rate === null ? '—' : `${(funnel.rate * 100).toFixed(1)}%`; |
| 290 | host.append( |
| 291 | el('p', 'funnel-summary', `${rate} converted · ${funnel.dropped.toLocaleString('en-US')} dropped off`), |
| 292 | ); |
| 293 | } |
| 294 | |
| 295 | function drawChart(section, panel, config) { |
| 296 | const canvas = $(section, 'canvas'); |