@twoux/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE.md +61 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/NOTICE.md +64 -0
  4. package/README.md +211 -2
  5. package/SHADCN-DIFF.md +228 -0
  6. package/agent/AGENTS.md +358 -0
  7. package/agent/docs-md/accessibility.md +49 -0
  8. package/agent/docs-md/action-catalogue.md +81 -0
  9. package/agent/docs-md/actions.md +74 -0
  10. package/agent/docs-md/agent-activity-log.md +70 -0
  11. package/agent/docs-md/agent-permissions.md +82 -0
  12. package/agent/docs-md/agent-run.md +90 -0
  13. package/agent/docs-md/agents-on-behalf.md +74 -0
  14. package/agent/docs-md/ai-column.md +85 -0
  15. package/agent/docs-md/ai-origin-label.md +70 -0
  16. package/agent/docs-md/announcing-features.md +73 -0
  17. package/agent/docs-md/answer-actions.md +81 -0
  18. package/agent/docs-md/app-shell.md +114 -0
  19. package/agent/docs-md/artifact-card.md +89 -0
  20. package/agent/docs-md/ask-on-screen.md +72 -0
  21. package/agent/docs-md/assistant-page.md +100 -0
  22. package/agent/docs-md/assistant-panel.md +142 -0
  23. package/agent/docs-md/avatar.md +82 -0
  24. package/agent/docs-md/banner.md +106 -0
  25. package/agent/docs-md/board.md +163 -0
  26. package/agent/docs-md/breadcrumb.md +89 -0
  27. package/agent/docs-md/breakdown-table.md +97 -0
  28. package/agent/docs-md/bulk-action-bar.md +112 -0
  29. package/agent/docs-md/button.md +293 -0
  30. package/agent/docs-md/card.md +115 -0
  31. package/agent/docs-md/change-list.md +99 -0
  32. package/agent/docs-md/change-request.md +166 -0
  33. package/agent/docs-md/chart-area.md +87 -0
  34. package/agent/docs-md/chart-bar.md +100 -0
  35. package/agent/docs-md/chart-box-plot.md +88 -0
  36. package/agent/docs-md/chart-bullet.md +89 -0
  37. package/agent/docs-md/chart-candlestick.md +112 -0
  38. package/agent/docs-md/chart-card-line.md +120 -0
  39. package/agent/docs-md/chart-donut.md +90 -0
  40. package/agent/docs-md/chart-drill-down.md +72 -0
  41. package/agent/docs-md/chart-funnel.md +71 -0
  42. package/agent/docs-md/chart-heatmap.md +76 -0
  43. package/agent/docs-md/chart-histogram.md +81 -0
  44. package/agent/docs-md/chart-map.md +165 -0
  45. package/agent/docs-md/chart-mixed.md +88 -0
  46. package/agent/docs-md/chart-price.md +92 -0
  47. package/agent/docs-md/chart-radial-bars.md +109 -0
  48. package/agent/docs-md/chart-radial-lines.md +111 -0
  49. package/agent/docs-md/chart-sankey.md +104 -0
  50. package/agent/docs-md/chart-scatter.md +87 -0
  51. package/agent/docs-md/chart-share-bar.md +89 -0
  52. package/agent/docs-md/chart-treemap.md +98 -0
  53. package/agent/docs-md/chart-waterfall.md +93 -0
  54. package/agent/docs-md/checkbox.md +131 -0
  55. package/agent/docs-md/choosing-selection-control.md +71 -0
  56. package/agent/docs-md/citations-sources.md +79 -0
  57. package/agent/docs-md/citing-sources.md +63 -0
  58. package/agent/docs-md/clarify-before-acting.md +69 -0
  59. package/agent/docs-md/clarifying-question.md +92 -0
  60. package/agent/docs-md/code-block.md +174 -0
  61. package/agent/docs-md/colours.md +87 -0
  62. package/agent/docs-md/columns-menu.md +140 -0
  63. package/agent/docs-md/combobox.md +161 -0
  64. package/agent/docs-md/command-palette.md +126 -0
  65. package/agent/docs-md/confidence-note.md +87 -0
  66. package/agent/docs-md/configurable-dashboard.md +76 -0
  67. package/agent/docs-md/confirmation-dialog.md +93 -0
  68. package/agent/docs-md/connected-tools.md +80 -0
  69. package/agent/docs-md/connector-picker.md +127 -0
  70. package/agent/docs-md/consistency.md +53 -0
  71. package/agent/docs-md/content-density.md +61 -0
  72. package/agent/docs-md/conversation-history.md +78 -0
  73. package/agent/docs-md/create-record.md +80 -0
  74. package/agent/docs-md/csv-import.md +88 -0
  75. package/agent/docs-md/dashboard-items.md +78 -0
  76. package/agent/docs-md/data-export.md +61 -0
  77. package/agent/docs-md/data-grid.md +137 -0
  78. package/agent/docs-md/data-vis-colours.md +76 -0
  79. package/agent/docs-md/data-visualisation.md +76 -0
  80. package/agent/docs-md/date-picker.md +178 -0
  81. package/agent/docs-md/delete-record.md +75 -0
  82. package/agent/docs-md/dense-table.md +495 -0
  83. package/agent/docs-md/design-tokens.md +53 -0
  84. package/agent/docs-md/disabled-read-only.md +75 -0
  85. package/agent/docs-md/drag-and-drop.md +74 -0
  86. package/agent/docs-md/dropdown-menu.md +137 -0
  87. package/agent/docs-md/edit-record.md +73 -0
  88. package/agent/docs-md/empty-loading-error.md +78 -0
  89. package/agent/docs-md/empty-state.md +128 -0
  90. package/agent/docs-md/error-messages.md +75 -0
  91. package/agent/docs-md/error-state.md +123 -0
  92. package/agent/docs-md/export-menu.md +86 -0
  93. package/agent/docs-md/feedback-prompt.md +88 -0
  94. package/agent/docs-md/field-mapping.md +78 -0
  95. package/agent/docs-md/file-drop.md +110 -0
  96. package/agent/docs-md/filter-bar.md +138 -0
  97. package/agent/docs-md/filter-panel.md +103 -0
  98. package/agent/docs-md/filtering-search.md +79 -0
  99. package/agent/docs-md/follow-ups-starters.md +79 -0
  100. package/agent/docs-md/form-validation.md +95 -0
  101. package/agent/docs-md/generated-columns.md +65 -0
  102. package/agent/docs-md/generated-drafts-files.md +71 -0
  103. package/agent/docs-md/grouped-list.md +89 -0
  104. package/agent/docs-md/guide-cue.md +170 -0
  105. package/agent/docs-md/help-panel.md +103 -0
  106. package/agent/docs-md/help-system.md +74 -0
  107. package/agent/docs-md/iconography.md +49 -0
  108. package/agent/docs-md/inbox.md +108 -0
  109. package/agent/docs-md/inline-editing-validation.md +80 -0
  110. package/agent/docs-md/inline-input-in-answer.md +70 -0
  111. package/agent/docs-md/items-palette.md +116 -0
  112. package/agent/docs-md/keyboard-model.md +47 -0
  113. package/agent/docs-md/keyboard-navigation.md +68 -0
  114. package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
  115. package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
  116. package/agent/docs-md/labelling-ai-output.md +66 -0
  117. package/agent/docs-md/layout.md +47 -0
  118. package/agent/docs-md/list-table.md +254 -0
  119. package/agent/docs-md/loading-skeleton.md +91 -0
  120. package/agent/docs-md/managing-agents.md +71 -0
  121. package/agent/docs-md/message-composer.md +105 -0
  122. package/agent/docs-md/migrating.md +49 -0
  123. package/agent/docs-md/motion.md +93 -0
  124. package/agent/docs-md/navigation.md +76 -0
  125. package/agent/docs-md/notification-channels.md +65 -0
  126. package/agent/docs-md/notifications-decision.md +100 -0
  127. package/agent/docs-md/onboarding-step.md +114 -0
  128. package/agent/docs-md/order-book.md +108 -0
  129. package/agent/docs-md/page-header.md +96 -0
  130. package/agent/docs-md/pagination.md +88 -0
  131. package/agent/docs-md/pane-layout.md +105 -0
  132. package/agent/docs-md/period-comparison.md +66 -0
  133. package/agent/docs-md/plan-card.md +120 -0
  134. package/agent/docs-md/popover-rules.md +88 -0
  135. package/agent/docs-md/prompt-editor.md +90 -0
  136. package/agent/docs-md/propose-apply.md +65 -0
  137. package/agent/docs-md/provider-logo.md +97 -0
  138. package/agent/docs-md/radio.md +83 -0
  139. package/agent/docs-md/ranked-list.md +102 -0
  140. package/agent/docs-md/reasoning-progress.md +72 -0
  141. package/agent/docs-md/record-detail.md +71 -0
  142. package/agent/docs-md/regenerating-answers.md +70 -0
  143. package/agent/docs-md/run-status-bar.md +94 -0
  144. package/agent/docs-md/saved-tasks-schedules.md +63 -0
  145. package/agent/docs-md/saved-views.md +149 -0
  146. package/agent/docs-md/saving-views.md +68 -0
  147. package/agent/docs-md/scheduler.md +258 -0
  148. package/agent/docs-md/secondary-panels.md +72 -0
  149. package/agent/docs-md/segmented-control.md +88 -0
  150. package/agent/docs-md/selection-bulk-actions.md +70 -0
  151. package/agent/docs-md/separator.md +75 -0
  152. package/agent/docs-md/setup-checklist.md +92 -0
  153. package/agent/docs-md/shape-elevation.md +55 -0
  154. package/agent/docs-md/sheet-grid.md +211 -0
  155. package/agent/docs-md/side-panel.md +134 -0
  156. package/agent/docs-md/sign-in.md +114 -0
  157. package/agent/docs-md/spacing.md +55 -0
  158. package/agent/docs-md/split-button.md +100 -0
  159. package/agent/docs-md/stat-compare.md +82 -0
  160. package/agent/docs-md/static-dashboard.md +75 -0
  161. package/agent/docs-md/status-badge.md +140 -0
  162. package/agent/docs-md/step-card.md +75 -0
  163. package/agent/docs-md/stepper.md +92 -0
  164. package/agent/docs-md/suggested-changes.md +91 -0
  165. package/agent/docs-md/switch.md +78 -0
  166. package/agent/docs-md/table-settings.md +81 -0
  167. package/agent/docs-md/tabs.md +85 -0
  168. package/agent/docs-md/tag-input.md +108 -0
  169. package/agent/docs-md/tag.md +79 -0
  170. package/agent/docs-md/task-builder.md +87 -0
  171. package/agent/docs-md/team-instructions.md +72 -0
  172. package/agent/docs-md/templates-gallery.md +85 -0
  173. package/agent/docs-md/text-input.md +160 -0
  174. package/agent/docs-md/thinking-steps.md +89 -0
  175. package/agent/docs-md/timeline.md +189 -0
  176. package/agent/docs-md/timestamp.md +84 -0
  177. package/agent/docs-md/timestamps.md +70 -0
  178. package/agent/docs-md/toast.md +117 -0
  179. package/agent/docs-md/tooltip.md +90 -0
  180. package/agent/docs-md/typography.md +62 -0
  181. package/agent/docs-md/undo-destructive.md +66 -0
  182. package/agent/docs-md/unsaved-changes.md +71 -0
  183. package/agent/docs-md/usage-meter.md +80 -0
  184. package/agent/docs-md/user-feedback.md +71 -0
  185. package/agent/docs-md/voice-and-tone.md +60 -0
  186. package/agent/docs-md/why-choice.md +93 -0
  187. package/agent/docs-md/workspace-onboarding.md +74 -0
  188. package/agent/docs-md/writing.md +48 -0
  189. package/agent/llms-full.txt +18369 -0
  190. package/agent/llms.txt +198 -0
  191. package/agent/skills/twoux-audit/SKILL.md +49 -0
  192. package/agent/skills/twoux-build/SKILL.md +39 -0
  193. package/agent/skills/twoux-design/SKILL.md +54 -0
  194. package/agent/skills/twoux-write/SKILL.md +32 -0
  195. package/bin/check.mjs +123 -0
  196. package/bin/docs.mjs +24 -0
  197. package/bin/guard.mjs +27 -0
  198. package/bin/init.mjs +113 -0
  199. package/bin/knowledge.mjs +87 -0
  200. package/bin/mcp.mjs +116 -0
  201. package/bin/theme.mjs +168 -0
  202. package/bin/twoux.mjs +25 -0
  203. package/package.json +80 -3
  204. package/registry/jobs.json +1028 -0
  205. package/registry/palette.json +770 -0
  206. package/registry/rules.json +913 -0
  207. package/src/components/agent-review.tsx +364 -0
  208. package/src/components/ai-basics.tsx +212 -0
  209. package/src/components/ai-work.tsx +304 -0
  210. package/src/components/ai.tsx +227 -0
  211. package/src/components/alert-dialog.tsx +203 -0
  212. package/src/components/alert.tsx +86 -0
  213. package/src/components/avatar.tsx +114 -0
  214. package/src/components/badge.tsx +59 -0
  215. package/src/components/board.tsx +397 -0
  216. package/src/components/breadcrumb.tsx +114 -0
  217. package/src/components/button-group.tsx +90 -0
  218. package/src/components/button.tsx +109 -0
  219. package/src/components/calendar.tsx +247 -0
  220. package/src/components/card.tsx +102 -0
  221. package/src/components/chart-map.tsx +336 -0
  222. package/src/components/charts-more.tsx +1331 -0
  223. package/src/components/charts-radial.tsx +328 -0
  224. package/src/components/charts.tsx +390 -0
  225. package/src/components/checkbox.tsx +40 -0
  226. package/src/components/code.tsx +139 -0
  227. package/src/components/collapsible.tsx +37 -0
  228. package/src/components/combobox.tsx +339 -0
  229. package/src/components/command-extras.tsx +11 -0
  230. package/src/components/command.tsx +196 -0
  231. package/src/components/data-grid.tsx +213 -0
  232. package/src/components/data-table.tsx +334 -0
  233. package/src/components/data-views.tsx +740 -0
  234. package/src/components/date-picker.tsx +686 -0
  235. package/src/components/dense-table.tsx +207 -0
  236. package/src/components/dialog.tsx +162 -0
  237. package/src/components/dropdown-menu.tsx +264 -0
  238. package/src/components/empty.tsx +114 -0
  239. package/src/components/feedback-prompt.tsx +80 -0
  240. package/src/components/field.tsx +254 -0
  241. package/src/components/fields.tsx +112 -0
  242. package/src/components/flow.tsx +840 -0
  243. package/src/components/guide-cue.tsx +273 -0
  244. package/src/components/inbox.tsx +113 -0
  245. package/src/components/input-group.tsx +176 -0
  246. package/src/components/input.tsx +26 -0
  247. package/src/components/kbd.tsx +34 -0
  248. package/src/components/label.tsx +27 -0
  249. package/src/components/labels.tsx +176 -0
  250. package/src/components/motion.tsx +136 -0
  251. package/src/components/onboarding.tsx +454 -0
  252. package/src/components/order-book.tsx +185 -0
  253. package/src/components/pane-layout.tsx +417 -0
  254. package/src/components/popover.tsx +96 -0
  255. package/src/components/provider-logos.data.ts +49 -0
  256. package/src/components/radio-group.tsx +52 -0
  257. package/src/components/scheduler.tsx +997 -0
  258. package/src/components/segmented-control.tsx +28 -0
  259. package/src/components/select.tsx +195 -0
  260. package/src/components/separator.tsx +31 -0
  261. package/src/components/sheet.tsx +148 -0
  262. package/src/components/shell.tsx +426 -0
  263. package/src/components/shortcut-sheet.tsx +83 -0
  264. package/src/components/skeleton.tsx +18 -0
  265. package/src/components/sonner.tsx +107 -0
  266. package/src/components/spinner.tsx +13 -0
  267. package/src/components/split-button.tsx +58 -0
  268. package/src/components/stepper.tsx +78 -0
  269. package/src/components/switch.tsx +43 -0
  270. package/src/components/table.tsx +126 -0
  271. package/src/components/tabs.tsx +118 -0
  272. package/src/components/tag-input.tsx +253 -0
  273. package/src/components/textarea.tsx +21 -0
  274. package/src/components/theme.tsx +80 -0
  275. package/src/components/timeline.tsx +408 -0
  276. package/src/components/toggle-group.tsx +87 -0
  277. package/src/components/toggle.tsx +54 -0
  278. package/src/components/tooltip.tsx +65 -0
  279. package/src/components/workspace-actions.tsx +294 -0
  280. package/src/components/workspace-import.tsx +465 -0
  281. package/src/components/workspace.tsx +1884 -0
  282. package/src/index.ts +80 -0
  283. package/src/lib/active-indicator.tsx +53 -0
  284. package/src/lib/chart-kit.tsx +72 -0
  285. package/src/lib/cn.ts +6 -0
  286. package/src/lib/shortcuts.tsx +61 -0
  287. package/src/lib/workspace.tsx +10 -0
  288. package/src/shadcn/alert-dialog.tsx +196 -0
  289. package/src/shadcn/alert.tsx +65 -0
  290. package/src/shadcn/avatar.tsx +108 -0
  291. package/src/shadcn/badge.tsx +47 -0
  292. package/src/shadcn/breadcrumb.tsx +108 -0
  293. package/src/shadcn/button-group.tsx +83 -0
  294. package/src/shadcn/button.tsx +63 -0
  295. package/src/shadcn/calendar.tsx +220 -0
  296. package/src/shadcn/card.tsx +91 -0
  297. package/src/shadcn/checkbox.tsx +31 -0
  298. package/src/shadcn/collapsible.tsx +33 -0
  299. package/src/shadcn/command.tsx +184 -0
  300. package/src/shadcn/dialog.tsx +158 -0
  301. package/src/shadcn/dropdown-menu.tsx +256 -0
  302. package/src/shadcn/empty.tsx +103 -0
  303. package/src/shadcn/field.tsx +248 -0
  304. package/src/shadcn/input-group.tsx +170 -0
  305. package/src/shadcn/input.tsx +20 -0
  306. package/src/shadcn/kbd.tsx +28 -0
  307. package/src/shadcn/label.tsx +23 -0
  308. package/src/shadcn/popover.tsx +88 -0
  309. package/src/shadcn/radio-group.tsx +44 -0
  310. package/src/shadcn/select.tsx +189 -0
  311. package/src/shadcn/separator.tsx +27 -0
  312. package/src/shadcn/sheet.tsx +142 -0
  313. package/src/shadcn/skeleton.tsx +13 -0
  314. package/src/shadcn/sonner.tsx +40 -0
  315. package/src/shadcn/switch.tsx +34 -0
  316. package/src/shadcn/table.tsx +115 -0
  317. package/src/shadcn/tabs.tsx +90 -0
  318. package/src/shadcn/textarea.tsx +17 -0
  319. package/src/shadcn/toggle-group.tsx +83 -0
  320. package/src/shadcn/toggle.tsx +46 -0
  321. package/src/shadcn/tooltip.tsx +56 -0
  322. package/src/styles/palette.css +127 -0
  323. package/src/styles/theme.css +35 -0
  324. package/src/styles/twoux.css +236 -0
  325. package/theme.json +14 -0
@@ -0,0 +1,90 @@
1
+ # Donut chart
2
+
3
+ Two to five parts of one total at a single moment, with the total in the centre and every segment labelled with its count and share. Most of the time a bar chart is better.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - When the question is how one total splits at one moment, and there are two to five parts: open roles by work pattern (On site, Hybrid, Remote (UK)).
10
+ - When the total itself is worth showing, which the centre of the donut holds.
11
+
12
+ ## When not to use
13
+
14
+ - For change over time. Use the Line chart card, or the Stacked area chart for parts over time.
15
+ - For more than five parts. Use a horizontal Bar chart sorted by size.
16
+ - When parts are close in size and the difference matters, such as 31% against 34%. Angles are hard to compare; use a Bar chart.
17
+ - For one value against a goal, such as 18 of 25 roles filled. Use the Stat card with comparison.
18
+ - For ordered stages such as Applied to Offer. Use the Funnel.
19
+
20
+ ## Structure
21
+
22
+ 1. Title and subtitle (the moment or period)
23
+ 2. Ring of segments starting at 12 o'clock, clockwise, with a 2px surface-colour gap between segments
24
+ 3. Centre: total value and a one or two word unit ("412 candidates")
25
+ 4. Segment labels: name, count, share
26
+ 5. Legend, required when any part is zero
27
+ 6. Details popover on hover or select
28
+
29
+ ## Guidelines
30
+
31
+ - Order segments clockwise from 12 o'clock, largest first, unless the parts have their own order (Junior, Mid, Senior), in which case keep that order.
32
+ - Colour segments with categorical colours in fixed order from colour 1. Never give each slice a colour chosen by meaning.
33
+ - Label every segment with its count and share, so the chart reads without hovering.
34
+ - A part with a value of zero has no segment but stays in the legend, so people can see it was counted.
35
+ - Keep a minimum segment size so a small part stays visible and selectable, and show its true share in the label.
36
+ - When people filter parts out, recalculate the centre total and every share.
37
+
38
+ ## Content
39
+
40
+ - Segment labels read count, unit, share: "124 candidates, 30%".
41
+ - Centre text is the total and a short unit: "412" over "candidates". One or two words only.
42
+ - Title is a noun phrase that names the whole: "Open roles by work pattern".
43
+ - If a name must be cut, keep the part that tells it apart: "…Designer, Leeds" rather than "Junior Product…".
44
+
45
+ ## Accessibility
46
+
47
+ - Description: "Donut chart showing open roles by work pattern, today." The Table view lists each part with count and share.
48
+ - Segments are identified by their printed labels and the legend, never by colour alone.
49
+ - Segments reach 3:1 against the background, and the 2px gap keeps neighbours apart.
50
+ - Arrow keys move between segments and show each one's details in the popover.
51
+
52
+ ## Keyboard
53
+
54
+ | Keys | Action |
55
+ |---|---|
56
+ | Tab | Focus the chart, then the legend items |
57
+ | ← → | Move to the previous or next segment |
58
+ | Enter | Pin the details popover |
59
+ | Esc | Close the popover |
60
+
61
+ ## Examples
62
+
63
+ ### sources
64
+
65
+ ```tsx
66
+ import { ChartFrame, DonutChart } from "@twoux/ui";
67
+
68
+ export const meta = { title: "Where applicants come from", description: "Two to five parts of one whole, each labelled with its count and share. For more parts, or to compare sizes closely, use a bar chart.", order: 1 };
69
+
70
+ export default function Example() {
71
+ return (
72
+ <ChartFrame className="w-full max-w-xl" title="Applications by source" description="This month">
73
+ <DonutChart totalLabel="applications" aria-label="Applications by source" segments={[
74
+ { id: "board", label: "Job boards", value: 312 },
75
+ { id: "site", label: "Careers page", value: 164 },
76
+ { id: "ref", label: "Referrals", value: 71 },
77
+ { id: "agency", label: "Agencies", value: 48 },
78
+ ]} />
79
+ </ChartFrame>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Related
85
+
86
+ - Bar chart
87
+ - Stat card with comparison
88
+ - Stacked area chart
89
+ - Data visualisation colours
90
+ - Data visualisation
@@ -0,0 +1,72 @@
1
+ # Chart drill-down
2
+
3
+ Select a point on a chart to see what makes it up, then narrow the chart or the whole page to that point, with every applied filter shown as a pill you can remove.
4
+
5
+ Kind: pattern · Group: Patterns: data and dashboards
6
+
7
+ **Problem:** A hiring lead sees applications drop on 12 Oct and wants to know why. Without a way in from the chart, they rebuild the same filters by hand on another page and lose the thread.
8
+
9
+ **Solution:** Offer three levels and pick one per chart. A breakdown in the details popover answers most questions. If more is needed, a footer in the pinned popover filters either this chart or the whole page, never both on one page. Filters land as pills in the filter bar, write to the URL and come off with one click.
10
+
11
+ **Built from:** Line chart card, Bar chart, Stacked area chart, Donut chart, Filter pills and filter bar, Breakdown table, List table, Popover rules
12
+
13
+ ## How it works
14
+
15
+ 1. Hover or move the crosshair to a point; the details popover shows each series' value. Select it or press Enter to pin the popover.
16
+ 2. Breakdown: under a value, list up to seven parts that make it up, such as applications on 12 Oct by listing. More than seven ends with a link: "View all 14 in table".
17
+ 3. Link either the part's name to its record or its value to a filtered List table, never both.
18
+ 4. Chart drill-down: the popover footer has a title saying where the filter goes ("Filter this chart"), the chosen point as read-only values ("Date 12 Oct"), an optional select ("Team"), and one button.
19
+ 5. Page drill-down: the same footer, titled "Filter this page", applies the values to the page's filter bar so every chart and table updates.
20
+ 6. Every applied value appears as a pill ("Date is 12 Oct") and in the URL, so Clear filters, removing a pill or Back returns to the default view.
21
+
22
+ ## When to use
23
+
24
+ - When people look at charts to investigate: a spike, a drop or an outlier they need to explain.
25
+ - Page drill-down on dashboards where several charts and a table share filters, such as a hiring overview by team and location.
26
+
27
+ ## When not to use
28
+
29
+ - Do not mix chart drill-down and page drill-down on the same page. Pick one; two levels of filters conflict.
30
+ - For a chart with one series and nothing beneath it. The tooltip is enough.
31
+ - On a Board item people rearrange: send them to the full report page instead of filtering inside a tile.
32
+
33
+ ## Guidelines
34
+
35
+ - Always say which filters a drill-down will apply and where, before it runs.
36
+ - Pre-filled values from the selected point are read-only in the popover; only the optional extra choice is editable.
37
+ - Filtering is a reversible view change, so it needs no confirmation and no Undo toast: the pill and Back undo it.
38
+ - A pinned popover follows Popover rules: one open at a time, never cut off, Esc closes it and returns focus to the point.
39
+ - Popover footers hold at most two buttons; more go in a menu.
40
+ - The popover breakdown is never the only place a value lives. The Breakdown table under the chart holds the same split.
41
+
42
+ ## Content
43
+
44
+ - Footer title names the target: "Filter this chart" or "Filter this page". Description lists what will apply: "Date is 12 Oct and Team is Design."
45
+ - Button names the action and target: "Filter chart", "Filter page". Not "Apply" or "Submit".
46
+ - Breakdown rows read as key and value: "Junior Product Designer, Leeds 18".
47
+
48
+ ## Accessibility
49
+
50
+ - Enter on a focused point pins the popover; Tab moves through its links, select and button; Esc closes it and returns focus to the same point.
51
+ - The popover is labelled by the point it describes: "Applications, 12 Oct".
52
+ - After a drill-down applies, announce the new state politely: "Filtered to 12 Oct. 3 charts updated."
53
+ - Read-only values are text, not disabled inputs, so they are read in full and not skipped.
54
+
55
+ ## Do
56
+
57
+ - Do show every drill-down filter as a pill in the filter bar.
58
+ - Do cap a popover breakdown at seven items with a link to the rest.
59
+ - Do keep the drill-down state in the URL.
60
+
61
+ ## Don't
62
+
63
+ - Don't offer chart and page drill-down on the same page.
64
+ - Don't link both the name and the value in a breakdown row.
65
+ - Don't apply filters from a hover; only from a pinned popover and a button.
66
+
67
+ ## Related
68
+
69
+ - Data visualisation
70
+ - Filtering and search
71
+ - Comparison over time
72
+ - Record detail: side panel, overlay or full page
@@ -0,0 +1,71 @@
1
+ # Funnel
2
+
3
+ Horizontal bars for ordered stages (views, applies, interviews, offers) that show each stage's count and its rate from the stage before, darkest to lightest.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - Showing ordered stages where people care how many make it from one step to the next.
10
+ - Comparing amounts at each stage, which simple bars make quick to read.
11
+
12
+ ## When not to use
13
+
14
+ - For unordered categories. Use the TwoUX Bar chart.
15
+ - For change over time. Use the TwoUX Line chart card.
16
+
17
+ ## Structure
18
+
19
+ 1. Title and subtitle (scope, starting count)
20
+ 2. Stage label
21
+ 3. Bar (length by count)
22
+ 4. Count
23
+ 5. Rate from the previous stage
24
+
25
+ ## Guidelines
26
+
27
+ - Encode counts as bar length from a shared left edge; length is judged accurately.
28
+ - Show each stage's rate from the stage before; that is the number people act on.
29
+ - Stages go darkest to lightest in one hue, following order.
30
+ - Write counts beside bars so colour and length are not the only cues.
31
+ - Show the four states: skeleton bars while loading, empty, no match with Clear filters, error with Try again.
32
+ - A stage's count links to the List table filtered to that stage, so people go from "Interviewed 42" to those 42 candidates.
33
+ - Do not use a donut for stages; order is the point and a ring loses it.
34
+
35
+ ## Content
36
+
37
+ - Stage names are the steps people use, "Viewed", "Applied", "Interviewed".
38
+ - The subtitle states the base, "from 6,840 views".
39
+ - Write rates as percentages with the same decimals on every stage.
40
+
41
+ ## Accessibility
42
+
43
+ - 1.1.1 Non-text Content: the accessible name lists each stage with its count.
44
+ - 1.4.11 Non-text Contrast: the lightest stage bar still reaches 3:1, or the printed count carries the value.
45
+ - 1.3.1 Info and Relationships: stages are in reading order in the markup.
46
+ - Stage links are named by stage and count: "View 42 interviewed candidates".
47
+ - Arrow keys move between stages when the funnel is focused.
48
+
49
+ ## Examples
50
+
51
+ ### pipeline
52
+
53
+ ```tsx
54
+ import { ChartFrame, Funnel } from "@twoux/ui";
55
+
56
+ export const meta = { title: "Hiring funnel", description: "Ordered stages as horizontal bars. Each stage shows its count and the share that moved on from the stage before.", order: 1 };
57
+
58
+ export default function Example() {
59
+ return (
60
+ <ChartFrame className="w-full max-w-2xl" title="Pipeline, this quarter" description="All listings">
61
+ <Funnel aria-label="Hiring funnel this quarter" stages={[{ label: "Viewed", value: 8420 }, { label: "Applied", value: 595 }, { label: "Screened", value: 126 }, { label: "Interviewed", value: 48 }, { label: "Offered", value: 9 }]} />
62
+ </ChartFrame>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Related
68
+
69
+ - Bar chart
70
+ - Stat card with comparison
71
+ - Data visualisation colours
@@ -0,0 +1,76 @@
1
+ # Heatmap
2
+
3
+ A grid of two dimensions (such as team by weekday) shaded in one hue from light to dark, with every cell showing its number and a legend under the grid.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - Showing a quantity across two dimensions at once, such as when people apply by team and weekday.
10
+ - Spotting patterns and hot spots matters more than reading exact values; the printed numbers cover exact reading.
11
+
12
+ ## When not to use
13
+
14
+ - When people must compare exact amounts; colour is a poor encoding of magnitude. Use the TwoUX Bar chart or Breakdown table.
15
+ - For one dimension over time. Use the TwoUX Line chart card.
16
+
17
+ ## Structure
18
+
19
+ 1. Title and subtitle
20
+ 2. Row labels (team)
21
+ 3. Column labels (weekday)
22
+ 4. Cells with value text
23
+ 5. Legend: Fewer to More ramp, under the grid
24
+
25
+ ## Guidelines
26
+
27
+ - One hue, light for low and dark for high, never a rainbow.
28
+ - Every cell shows its number, so colour is never the only cue.
29
+ - Avoid red and green together; for diverging values use blue against orange or red.
30
+ - Separate neighbouring cells with a gap in the surface colour.
31
+ - Put the legend under the grid.
32
+ - While loading, show the grid with empty cells and labels; on error replace the grid with the error state and Try again; with no data, an empty state.
33
+ - Missing cells show a dash and "No data" in the tooltip, never the lightest step, which would read as low.
34
+ - Selecting a cell can drill down to the list behind it, such as candidates who applied to Design on Mondays, as a filtered List table.
35
+
36
+ ## Content
37
+
38
+ - Legend ends say "Fewer" and "More", not colour names.
39
+ - Title says what is counted; subtitle gives the period, "Applies by team and weekday, last 30 days".
40
+ - Choose text colour per cell so numbers stay readable on dark steps.
41
+
42
+ ## Accessibility
43
+
44
+ - 1.4.1 Use of Color: values are printed in the cells, which is the sufficient technique of adding text.
45
+ - 1.4.11 Non-text Contrast: heat maps can fall under the essential exception, but because the cells show values TwoUX does not depend on it.
46
+ - 1.1.1 and WAI complex images: give a short alternative and a table of the values.
47
+ - The grid is keyboard reachable: Tab to the grid, arrow keys move cell to cell, and each cell is announced with row, column and value.
48
+ - Accessible description: "Heatmap showing applies by team and weekday, last 30 days."
49
+
50
+ ## Examples
51
+
52
+ ### weekday
53
+
54
+ ```tsx
55
+ import { ChartFrame, Heatmap } from "@twoux/ui";
56
+
57
+ export const meta = { title: "When candidates apply", description: "One hue from light to dark, a legend from fewer to more, and every value in the cell's accessible name. Zero is grey, not the lightest step.", order: 1 };
58
+
59
+ const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
60
+ const TEAMS = ["Design", "Engineering", "Data", "People"];
61
+ const V = [[12, 18, 15, 14, 9, 2, 4], [31, 36, 33, 29, 21, 6, 9], [7, 9, 11, 8, 6, 0, 1], [4, 6, 5, 7, 3, 0, 0]];
62
+
63
+ export default function Example() {
64
+ return (
65
+ <ChartFrame className="w-full max-w-2xl" title="Applications by weekday" description="Last 4 weeks, by team">
66
+ <Heatmap rows={TEAMS} columns={DAYS} values={V} aria-label="Applications by team and weekday" />
67
+ </ChartFrame>
68
+ );
69
+ }
70
+ ```
71
+
72
+ ## Related
73
+
74
+ - Data visualisation colours
75
+ - Breakdown table
76
+ - Bar chart
@@ -0,0 +1,81 @@
1
+ # Histogram
2
+
3
+ How values spread: touching columns, one per equal bin, counted on one axis, with an optional dashed marker such as the median.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - Seeing the shape of a distribution, such as salary offers, where an average hides the tail.
10
+ - Checking whether most values sit in a range before setting a threshold.
11
+
12
+ ## When not to use
13
+
14
+ - To compare distributions of several groups. Use the TwoUX Box plot.
15
+ - For counts by named category. Use the TwoUX Bar chart.
16
+
17
+ ## Structure
18
+
19
+ 1. Title and subtitle
20
+ 2. Y axis: count
21
+ 3. Columns, one per bin, 2px gaps
22
+ 4. X axis: bin edges and title with unit
23
+ 5. Optional dashed marker with label
24
+ 6. Tooltip: range, count, share
25
+
26
+ ## Guidelines
27
+
28
+ - Use equal, round bin widths and say the width in the subtitle or table.
29
+ - Columns touch with a 2px surface gap, because bins are continuous.
30
+ - The y axis starts at zero and counts; there is one axis only.
31
+ - Mark the median or a threshold with a dashed grey line and a text label.
32
+ - Use ChartFrame for the four states: loading keeps the frame, an error says what failed with Try again, an empty period says so, and no match offers Clear filters.
33
+
34
+ ## Content
35
+
36
+ - X axis title names the measure and unit, such as Annual salary offered.
37
+ - Count label is the plural noun of what is counted (offers).
38
+
39
+ ## Accessibility
40
+
41
+ - 1.1.1 and WAI complex images: the chart has a short generated summary as its accessible name and a Table view with every value.
42
+ - 2.1.1 Keyboard: the chart is one tab stop; arrow keys move between marks and show the same tooltip as hover.
43
+ - 1.4.13 Content on Hover or Focus: the tooltip never covers the mark it describes, stays while the pointer is over the chart and closes with Esc.
44
+ - Accessible description: "Histogram showing salary offers in £5k bins, last 12 months."
45
+
46
+ ## Keyboard
47
+
48
+ | Keys | Action |
49
+ |---|---|
50
+ | Tab | Focus the chart |
51
+ | ← → ↑ ↓ | Move to the previous or next mark and show its values |
52
+ | Home End | Jump to the first or last mark |
53
+ | Esc | Hide the tooltip |
54
+
55
+ ## Examples
56
+
57
+ ### salary-offers
58
+
59
+ ```tsx
60
+ import { binValues, ChartFrame, Histogram, histogramTable } from "@twoux/ui";
61
+
62
+ export const meta = { title: "Salary offers", description: "140 offers in £5k bins with the median marked. The shape shows the long tail a single average hides.", order: 1 };
63
+
64
+ const OFFERS = [23000, 27000, 28500, 29500, 30000, 30000, 30000, 30000, 31500, 32500, 33000, 33500, 33500, 34000, 34000, 35500, 36000, 36000, 36500, 37000, 37500, 38000, 38000, 38000, 38000, 38500, 38500, 39000, 39500, 40000, 40000, 40500, 40500, 40500, 40500, 41000, 41000, 41000, 41500, 41500, 42000, 42500, 42500, 43000, 43000, 43000, 43500, 43500, 43500, 44000, 44000, 45000, 45000, 45500, 45500, 46500, 46500, 46500, 47000, 49500, 50000, 50500, 50500, 50500, 50500, 51000, 51000, 51000, 51500, 51500, 52000, 52500, 53000, 53500, 53500, 53500, 54000, 54000, 54500, 55000, 55000, 55000, 55000, 55500, 56000, 56500, 57000, 57500, 57500, 57500, 57500, 57500, 58000, 58000, 58500, 59000, 60000, 62000, 62000, 62000, 62500, 62500, 63000, 63000, 63000, 63000, 63500, 64000, 64500, 65000, 65000, 65500, 66000, 67000, 67500, 68000, 68000, 68000, 70000, 70500, 75500, 75500, 76000, 76500, 76500, 77500, 78500, 79500, 79500, 79500, 82500, 83000, 83500, 83500, 88000, 91000, 92000, 93500, 101500, 104000];
65
+ const BINS = binValues(OFFERS, { binWidth: 5000, start: 20000 });
66
+ const gbp = (n: number) => `£${n / 1000}k`;
67
+
68
+ export default function Example() {
69
+ return (
70
+ <ChartFrame className="w-full" title="Salary offers" description="Offers made, last 12 months" table={histogramTable(BINS, gbp, "Salary")}>
71
+ <Histogram bins={BINS} format={gbp} xTitle="Annual salary offered" countLabel="offers" marker={{ value: 52000, label: "Median £52k" }} aria-label="Salary offers, last 12 months" />
72
+ </ChartFrame>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## Related
78
+
79
+ - Box plot
80
+ - Bar chart
81
+ - Scatter chart
@@ -0,0 +1,165 @@
1
+ # Map chart
2
+
3
+ Regions shaded by a rate in five steps of one hue, or circles sized by an amount at places. For questions where location itself matters; missing regions are hatched and labelled No data.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - When where something happens is the question: clusters, neighbours, regions with nothing, such as trial conversion by country.
10
+ - Placing amounts at locations, such as ARR by customer head office city, with circles sized by value.
11
+
12
+ ## When not to use
13
+
14
+ - To compare amounts or rank countries. Use a Bar chart or a Ranked list: lengths are read more exactly than shades, and small countries don't disappear.
15
+ - For raw counts across regions of very different size or population, as shading. Normalise to a rate or per head, or use markers.
16
+ - For change over time. Use a Line chart card; a map shows one moment.
17
+
18
+ ## Structure
19
+
20
+ 1. Title and subtitle
21
+ 2. Regions from your TopoJSON, thin borders
22
+ 3. Shading in five classed steps, or circles sized by value
23
+ 4. Hatched fill for regions with no data
24
+ 5. Legend: bin ranges and No data
25
+ 6. Details: name, value, share of total, rank
26
+ 7. Pinned details with a drill-down button
27
+ 8. Note listing values with no shape on the map
28
+
29
+ ## Guidelines
30
+
31
+ - Shade by a rate, a share or a per-head figure; put raw counts and amounts on markers, where circle area grows with value.
32
+ - One measure per map: values or markers, never both.
33
+ - Colour is five classed steps of one hue (q1 to q5), darkest for the highest in light, lightest in dark. The legend shows the range of each step.
34
+ - Regions with no value are hatched and labelled No data in the legend. Never draw them as zero or in the lowest step.
35
+ - Join values by feature id (ISO 3166-1 numeric in world-atlas), not by name; names differ between sources.
36
+ - Values with no shape on the map, such as Singapore at 1:110m, are listed under the map and stay in the table.
37
+ - Use Equal Earth for the world so areas are not inflated towards the poles; leave Antarctica out unless it matters.
38
+ - For drill-down, a pinned region offers one button that says where the filter lands (Filter page) and adds a pill to the filter bar.
39
+ - Use ChartFrame for the four states and the Table view (mapTable).
40
+
41
+ ## Content
42
+
43
+ - Title names the measure and period: "Trial conversion by country", subtitle "Paid accounts per 100 trials, July to September".
44
+ - Legend ranges read "Under 5%", "5% to 10%", "20% and over". Missing regions read "No data".
45
+ - Turn off share of total for rates and averages; a share of a sum of rates means nothing.
46
+
47
+ ## Accessibility
48
+
49
+ - 1.1.1 and WAI complex images: the map's accessible name is a generated summary (label, number of regions, top three, how many have no data), and the Table view lists every value.
50
+ - 2.1.1 Keyboard: the map is one tab stop; arrow keys move between regions with data, highest value first, and show the same details as hover.
51
+ - 1.4.13 Content on Hover or Focus: details sit beside the region, stay while the pointer is over the map and close with Esc.
52
+ - 1.4.1 Use of Color: values are in the details, legend ranges and table; No data has a pattern as well as a colour.
53
+ - Accessible description: "Map showing trial conversion by country, July to September."
54
+
55
+ ## Keyboard
56
+
57
+ | Keys | Action |
58
+ |---|---|
59
+ | Tab | Focus the map |
60
+ | ← → ↑ ↓ | Move to the next or previous region with data, highest value first, and show its details |
61
+ | Home End | Jump to the highest or lowest value |
62
+ | Enter | Pin the details; focus moves to the drill-down button when there is one |
63
+ | Esc | Close the details and return focus to the map |
64
+
65
+ ## Examples
66
+
67
+ ### arr-by-city
68
+
69
+ ```tsx
70
+ import { ChartFrame, MapChart, mapTable } from "@twoux/ui";
71
+ import world from "world-atlas/countries-110m.json";
72
+
73
+ export const meta = { title: "ARR by customer location", description: "Raw amounts go on markers, never as shading: circle area grows with ARR, so London and New York stand out without painting all of the UK or the US.", order: 2 };
74
+
75
+ const gbp = (n: number) => `£${(n / 1_000_000).toFixed(1)}m`;
76
+ // Annual recurring revenue by the city of the customer's head office.
77
+ const ARR = [
78
+ { id: "lon", label: "London", lon: -0.13, lat: 51.51, value: 4_820_000 },
79
+ { id: "man", label: "Manchester", lon: -2.24, lat: 53.48, value: 910_000 },
80
+ { id: "dub", label: "Dublin", lon: -6.26, lat: 53.35, value: 1_340_000 },
81
+ { id: "ber", label: "Berlin", lon: 13.4, lat: 52.52, value: 2_150_000 },
82
+ { id: "ams", label: "Amsterdam", lon: 4.9, lat: 52.37, value: 1_620_000 },
83
+ { id: "par", label: "Paris", lon: 2.35, lat: 48.86, value: 1_280_000 },
84
+ { id: "sto", label: "Stockholm", lon: 18.07, lat: 59.33, value: 740_000 },
85
+ { id: "nyc", label: "New York", lon: -74.0, lat: 40.71, value: 3_960_000 },
86
+ { id: "sfo", label: "San Francisco", lon: -122.42, lat: 37.77, value: 2_480_000 },
87
+ { id: "tor", label: "Toronto", lon: -79.38, lat: 43.65, value: 860_000 },
88
+ { id: "syd", label: "Sydney", lon: 151.21, lat: -33.87, value: 1_050_000 },
89
+ { id: "sin", label: "Singapore", lon: 103.82, lat: 1.35, value: 690_000 },
90
+ ];
91
+
92
+ export default function Example() {
93
+ return (
94
+ <ChartFrame className="w-full" title="ARR by customer location" description="Head office city, as of 30 September" table={mapTable(ARR, { format: gbp, first: "City", valueLabel: "ARR" })} height={260}>
95
+ <MapChart topology={world} object="countries" exclude={["010"]} markers={ARR} format={gbp} valueLabel="ARR" aria-label="Map showing ARR by customer head office city, as of 30 September" />
96
+ </ChartFrame>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ### trial-conversion
102
+
103
+ ```tsx
104
+ import * as React from "react";
105
+ import { ChartFrame, FilterBar, FilterPill, MapChart, mapTable } from "@twoux/ui";
106
+ import world from "world-atlas/countries-110m.json";
107
+
108
+ export const meta = { title: "Trial conversion by country", description: "A rate, so countries of different size compare fairly. Arrow keys move from highest to lowest; Enter pins the details and offers Filter page.", order: 1 };
109
+
110
+ // Trials that became paid accounts, per 100 trials, last quarter. ISO 3166-1 numeric ids.
111
+ const RATES = [
112
+ { id: "826", label: "United Kingdom", value: 18.4 },
113
+ { id: "372", label: "Ireland", value: 21.2 },
114
+ { id: "276", label: "Germany", value: 14.9 },
115
+ { id: "250", label: "France", value: 11.3 },
116
+ { id: "528", label: "Netherlands", value: 19.7 },
117
+ { id: "752", label: "Sweden", value: 16.1 },
118
+ { id: "724", label: "Spain", value: 8.2 },
119
+ { id: "616", label: "Poland", value: 9.6 },
120
+ { id: "840", label: "United States", value: 15.8 },
121
+ { id: "124", label: "Canada", value: 13.4 },
122
+ { id: "484", label: "Mexico", value: 5.1 },
123
+ { id: "076", label: "Brazil", value: 6.7 },
124
+ { id: "356", label: "India", value: 4.3 },
125
+ { id: "392", label: "Japan", value: 7.9 },
126
+ { id: "036", label: "Australia", value: 17.2 },
127
+ { id: "710", label: "South Africa", value: 6.0 },
128
+ { id: "702", label: "Singapore", value: 22.5 },
129
+ ];
130
+ const pctFmt = (n: number) => `${n.toFixed(1)}%`;
131
+
132
+ export default function Example() {
133
+ const [country, setCountry] = React.useState<string[]>([]);
134
+ const options = RATES.map((r) => ({ value: r.id, label: r.label }));
135
+ return (
136
+ <div className="grid w-full gap-3">
137
+ <FilterBar>
138
+ <FilterPill field="Country" options={options} selected={country} onChange={setCountry} />
139
+ </FilterBar>
140
+ <ChartFrame className="w-full" title="Trial conversion by country" description="Paid accounts per 100 trials, July to September" table={mapTable(RATES, { format: pctFmt, share: false, first: "Country", valueLabel: "Conversion" })} height={260}>
141
+ <MapChart
142
+ topology={world}
143
+ object="countries"
144
+ exclude={["010"]}
145
+ values={RATES}
146
+ format={pctFmt}
147
+ share={false}
148
+ valueLabel="Conversion"
149
+ filterField="Country"
150
+ onSelect={(r) => setCountry([r.id])}
151
+ aria-label="Map showing trial conversion by country, July to September"
152
+ />
153
+ </ChartFrame>
154
+ </div>
155
+ );
156
+ }
157
+ ```
158
+
159
+ ## Related
160
+
161
+ - Bar chart
162
+ - Ranked list
163
+ - Heatmap
164
+ - Chart drill-down
165
+ - Filter pills and filter bar
@@ -0,0 +1,88 @@
1
+ # Bar and line chart
2
+
3
+ One bar series with one to three lines on the same y-axis and unit, such as weekly applications as bars and their four-week average as a line.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - When a count and a reference for that count share a unit: hires per month as bars, planned hires per month as a line.
10
+ - When volume per period is the main reading and the line adds the trend, such as applications per week with the four-week average.
11
+
12
+ ## When not to use
13
+
14
+ - When the two measures have different units or scales, such as applications (count) and conversion to interview (%). Use two charts side by side; never a second y-axis.
15
+ - When every series is the same kind of measure, such as applications for three listings. Use the Line chart card or grouped bars.
16
+ - For a fixed target that does not change by period. Use a threshold line on the Bar chart instead.
17
+
18
+ ## Structure
19
+
20
+ 1. Title and subtitle
21
+ 2. Plot: one y-axis, zero baseline, bars for the main measure
22
+ 3. Line or lines (2px) for a related measure in the same unit: average, plan or previous period
23
+ 4. Legend: bar swatch and line swatch with names
24
+ 5. Crosshair tooltip with each series at the hovered period
25
+ 6. Optional threshold: dashed line with a text label
26
+
27
+ ## Guidelines
28
+
29
+ - Only one series is drawn as bars. Lines carry up to three related series, so the chart stays within four series in all.
30
+ - Bars start at zero, so the shared axis starts at zero too.
31
+ - Bars use categorical colour 1; lines use the next colours in order. A previous-period line stays the dashed grey from the Period comparison pattern.
32
+ - Draw lines on top of bars and put a marker on each line point so it stays visible where it crosses a bar.
33
+ - Show missing periods as gaps in the line and no bar, not as zero.
34
+ - If the line and the bars stop sharing a unit after a change in the data, split the chart. Do not add a right-hand axis.
35
+
36
+ ## Content
37
+
38
+ - Name each series by role: "Hires", "Plan", "4-week average". The legend says which is the bar and which is the line through its swatch.
39
+ - Axis title states the shared unit once: "Applications". No unit on the y-axis for time.
40
+ - Title is a noun phrase naming the main measure: "Hires against plan".
41
+
42
+ ## Accessibility
43
+
44
+ - Description names both marks: "Bar and line chart showing hires per month against plan, last 12 months."
45
+ - Bars and lines differ by shape, not only by colour, and the tooltip names every series.
46
+ - Bars and lines reach 3:1 against the background; the line marker keeps 3:1 where it sits on a bar.
47
+ - The Table view has one column per series with the same names as the legend.
48
+
49
+ ## Keyboard
50
+
51
+ | Keys | Action |
52
+ |---|---|
53
+ | Tab | Focus the plot, then each legend item |
54
+ | ← → | Move to the previous or next period |
55
+ | Enter | Pin the tooltip |
56
+ | Esc | Close the tooltip |
57
+
58
+ ## Examples
59
+
60
+ ### applications-target
61
+
62
+ ```tsx
63
+ import { CartesianChart, ChartFrame, ChartLegend, type ChartSeries, chartTable, seriesColor } from "@twoux/ui";
64
+ import { WEEKS } from "../chart-card-line/data";
65
+
66
+ export const meta = { title: "Applications against the average", description: "Bars for this period, a line for the 8-week average, and a dashed target. All three share one axis and one unit; different units would need two charts.", order: 1 };
67
+
68
+ const SERIES: ChartSeries[] = [
69
+ { id: "apps", label: "Applications", type: "bar", data: [151, 170, 168, 188, 204, 219, 218, 250] },
70
+ { id: "avg", label: "8-week average", type: "line", data: [160, 163, 165, 171, 178, 186, 192, 196] },
71
+ ];
72
+
73
+ export default function Example() {
74
+ return (
75
+ <ChartFrame className="w-full" title="Applications per week" description="Against the rolling average and the target" table={chartTable("Week", WEEKS, SERIES)} legend={<ChartLegend items={SERIES.map((s, i) => ({ id: s.id, label: s.label, color: seriesColor(i), shape: s.type === "line" ? "line" : "box" }))} />}>
76
+ <CartesianChart kind="mixed" x={WEEKS} series={SERIES} threshold={{ value: 220, label: "Target 220" }} aria-label="Applications per week with average and target" />
77
+ </ChartFrame>
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## Related
83
+
84
+ - Bar chart
85
+ - Line chart card
86
+ - Stat card with comparison
87
+ - Data visualisation colours
88
+ - Data visualisation