@misoto22/design 0.3.1 → 0.6.0

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 (266) hide show
  1. package/README.md +124 -0
  2. package/bin/misoto22-design.mjs +205 -0
  3. package/dist/agent/Accordion.md +41 -0
  4. package/dist/agent/Alert.md +42 -0
  5. package/dist/agent/AppShell.md +40 -0
  6. package/dist/agent/AreaChart.md +68 -0
  7. package/dist/agent/Article.md +41 -0
  8. package/dist/agent/Avatar.md +32 -0
  9. package/dist/agent/Badge.md +40 -0
  10. package/dist/agent/BarChart.md +65 -0
  11. package/dist/agent/BarList.md +40 -0
  12. package/dist/agent/BigNumber.md +45 -0
  13. package/dist/agent/BoxPlot.md +58 -0
  14. package/dist/agent/Breadcrumb.md +32 -0
  15. package/dist/agent/BulletChart.md +48 -0
  16. package/dist/agent/Button.md +56 -0
  17. package/dist/agent/Calendar.md +42 -0
  18. package/dist/agent/Card.md +49 -0
  19. package/dist/agent/Checkbox.md +35 -0
  20. package/dist/agent/Collapsible.md +46 -0
  21. package/dist/agent/Combobox.md +50 -0
  22. package/dist/agent/Command.md +93 -0
  23. package/dist/agent/ComposedChart.md +57 -0
  24. package/dist/agent/ContextMenu.md +59 -0
  25. package/dist/agent/DatePicker.md +75 -0
  26. package/dist/agent/Diagram.md +46 -0
  27. package/dist/agent/Dialog.md +52 -0
  28. package/dist/agent/DropdownMenu.md +70 -0
  29. package/dist/agent/EmptyState.md +37 -0
  30. package/dist/agent/ErrorState.md +41 -0
  31. package/dist/agent/Facet.md +82 -0
  32. package/dist/agent/Field.md +37 -0
  33. package/dist/agent/FigureBand.md +37 -0
  34. package/dist/agent/FloatingIconButton.md +38 -0
  35. package/dist/agent/FunnelChart.md +52 -0
  36. package/dist/agent/Heatmap.md +53 -0
  37. package/dist/agent/Histogram.md +63 -0
  38. package/dist/agent/Input.md +30 -0
  39. package/dist/agent/Kbd.md +29 -0
  40. package/dist/agent/LineChart.md +60 -0
  41. package/dist/agent/LinkArrow.md +29 -0
  42. package/dist/agent/NativeSelect.md +40 -0
  43. package/dist/agent/NavItem.md +35 -0
  44. package/dist/agent/Pagination.md +43 -0
  45. package/dist/agent/PieChart.md +55 -0
  46. package/dist/agent/Popover.md +57 -0
  47. package/dist/agent/Progress.md +33 -0
  48. package/dist/agent/RadarChart.md +54 -0
  49. package/dist/agent/RadialChart.md +57 -0
  50. package/dist/agent/RadioGroup.md +44 -0
  51. package/dist/agent/SankeyChart.md +61 -0
  52. package/dist/agent/ScatterChart.md +54 -0
  53. package/dist/agent/ScrollArea.md +40 -0
  54. package/dist/agent/SearchableMenu.md +46 -0
  55. package/dist/agent/Select.md +75 -0
  56. package/dist/agent/Separator.md +35 -0
  57. package/dist/agent/Sheet.md +57 -0
  58. package/dist/agent/Skeleton.md +73 -0
  59. package/dist/agent/Slider.md +39 -0
  60. package/dist/agent/Sparkline.md +45 -0
  61. package/dist/agent/Spinner.md +39 -0
  62. package/dist/agent/StatusDot.md +36 -0
  63. package/dist/agent/StatusPill.md +28 -0
  64. package/dist/agent/Steps.md +44 -0
  65. package/dist/agent/Switch.md +29 -0
  66. package/dist/agent/Table.md +85 -0
  67. package/dist/agent/Tabs.md +48 -0
  68. package/dist/agent/Tag.md +35 -0
  69. package/dist/agent/Textarea.md +26 -0
  70. package/dist/agent/Toast.md +26 -0
  71. package/dist/agent/ToggleGroup.md +44 -0
  72. package/dist/agent/Tooltip.md +51 -0
  73. package/dist/agent/TreemapChart.md +52 -0
  74. package/dist/agent/WaterfallChart.md +62 -0
  75. package/dist/agent/catalog.json +1791 -0
  76. package/dist/agent/index.md +131 -0
  77. package/dist/article.css +393 -0
  78. package/dist/charts/AreaChart/AreaChart.d.ts +191 -0
  79. package/dist/charts/AreaChart/AreaChart.js +480 -0
  80. package/dist/charts/AreaChart/AreaChart.js.map +1 -0
  81. package/dist/charts/BarChart/BarChart.d.ts +179 -0
  82. package/dist/charts/BarChart/BarChart.js +591 -0
  83. package/dist/charts/BarChart/BarChart.js.map +1 -0
  84. package/dist/charts/BarList/BarList.d.ts +57 -0
  85. package/dist/charts/BarList/BarList.js +71 -0
  86. package/dist/charts/BarList/BarList.js.map +1 -0
  87. package/dist/charts/BigNumber/BigNumber.d.ts +56 -0
  88. package/dist/charts/BigNumber/BigNumber.js +31 -0
  89. package/dist/charts/BigNumber/BigNumber.js.map +1 -0
  90. package/dist/charts/BoxPlot/BoxPlot.d.ts +177 -0
  91. package/dist/charts/BoxPlot/BoxPlot.js +495 -0
  92. package/dist/charts/BoxPlot/BoxPlot.js.map +1 -0
  93. package/dist/charts/BulletChart/BulletChart.d.ts +100 -0
  94. package/dist/charts/BulletChart/BulletChart.js +162 -0
  95. package/dist/charts/BulletChart/BulletChart.js.map +1 -0
  96. package/dist/charts/ComposedChart/ComposedChart.d.ts +171 -0
  97. package/dist/charts/ComposedChart/ComposedChart.js +667 -0
  98. package/dist/charts/ComposedChart/ComposedChart.js.map +1 -0
  99. package/dist/charts/Facet/Facet.d.ts +240 -0
  100. package/dist/charts/Facet/Facet.js +133 -0
  101. package/dist/charts/Facet/Facet.js.map +1 -0
  102. package/dist/charts/Facet/panels.d.ts +187 -0
  103. package/dist/charts/Facet/panels.js +203 -0
  104. package/dist/charts/Facet/panels.js.map +1 -0
  105. package/dist/charts/FunnelChart/FunnelChart.d.ts +101 -0
  106. package/dist/charts/FunnelChart/FunnelChart.js +147 -0
  107. package/dist/charts/FunnelChart/FunnelChart.js.map +1 -0
  108. package/dist/charts/Heatmap/Heatmap.d.ts +78 -0
  109. package/dist/charts/Heatmap/Heatmap.js +108 -0
  110. package/dist/charts/Heatmap/Heatmap.js.map +1 -0
  111. package/dist/charts/Histogram/Histogram.d.ts +165 -0
  112. package/dist/charts/Histogram/Histogram.js +350 -0
  113. package/dist/charts/Histogram/Histogram.js.map +1 -0
  114. package/dist/charts/LineChart/LineChart.d.ts +167 -0
  115. package/dist/charts/LineChart/LineChart.js +486 -0
  116. package/dist/charts/LineChart/LineChart.js.map +1 -0
  117. package/dist/charts/PieChart/PieChart.d.ts +128 -0
  118. package/dist/charts/PieChart/PieChart.js +309 -0
  119. package/dist/charts/PieChart/PieChart.js.map +1 -0
  120. package/dist/charts/RadarChart/RadarChart.d.ts +123 -0
  121. package/dist/charts/RadarChart/RadarChart.js +299 -0
  122. package/dist/charts/RadarChart/RadarChart.js.map +1 -0
  123. package/dist/charts/RadialChart/RadialChart.d.ts +124 -0
  124. package/dist/charts/RadialChart/RadialChart.js +257 -0
  125. package/dist/charts/RadialChart/RadialChart.js.map +1 -0
  126. package/dist/charts/SankeyChart/SankeyChart.d.ts +137 -0
  127. package/dist/charts/SankeyChart/SankeyChart.js +501 -0
  128. package/dist/charts/SankeyChart/SankeyChart.js.map +1 -0
  129. package/dist/charts/ScatterChart/ScatterChart.d.ts +128 -0
  130. package/dist/charts/ScatterChart/ScatterChart.js +269 -0
  131. package/dist/charts/ScatterChart/ScatterChart.js.map +1 -0
  132. package/dist/charts/Sparkline/Sparkline.d.ts +53 -0
  133. package/dist/charts/Sparkline/Sparkline.js +93 -0
  134. package/dist/charts/Sparkline/Sparkline.js.map +1 -0
  135. package/dist/charts/TreemapChart/TreemapChart.d.ts +93 -0
  136. package/dist/charts/TreemapChart/TreemapChart.js +118 -0
  137. package/dist/charts/TreemapChart/TreemapChart.js.map +1 -0
  138. package/dist/charts/WaterfallChart/WaterfallChart.d.ts +164 -0
  139. package/dist/charts/WaterfallChart/WaterfallChart.js +319 -0
  140. package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -0
  141. package/dist/charts/index.d.ts +41 -0
  142. package/dist/charts/index.js +128 -0
  143. package/dist/charts/index.js.map +1 -0
  144. package/dist/charts/lib/annotations.d.ts +103 -0
  145. package/dist/charts/lib/annotations.js +99 -0
  146. package/dist/charts/lib/annotations.js.map +1 -0
  147. package/dist/charts/lib/axis.d.ts +18 -0
  148. package/dist/charts/lib/axis.js +21 -0
  149. package/dist/charts/lib/axis.js.map +1 -0
  150. package/dist/charts/lib/background.d.ts +33 -0
  151. package/dist/charts/lib/background.js +173 -0
  152. package/dist/charts/lib/background.js.map +1 -0
  153. package/dist/charts/lib/brush.d.ts +94 -0
  154. package/dist/charts/lib/brush.js +535 -0
  155. package/dist/charts/lib/brush.js.map +1 -0
  156. package/dist/charts/lib/chart.d.ts +94 -0
  157. package/dist/charts/lib/chart.js +134 -0
  158. package/dist/charts/lib/chart.js.map +1 -0
  159. package/dist/charts/lib/dot.d.ts +29 -0
  160. package/dist/charts/lib/dot.js +78 -0
  161. package/dist/charts/lib/dot.js.map +1 -0
  162. package/dist/charts/lib/empty.d.ts +31 -0
  163. package/dist/charts/lib/empty.js +13 -0
  164. package/dist/charts/lib/empty.js.map +1 -0
  165. package/dist/charts/lib/export.d.ts +103 -0
  166. package/dist/charts/lib/export.js +204 -0
  167. package/dist/charts/lib/export.js.map +1 -0
  168. package/dist/charts/lib/figure.d.ts +61 -0
  169. package/dist/charts/lib/figure.js +59 -0
  170. package/dist/charts/lib/figure.js.map +1 -0
  171. package/dist/charts/lib/format.d.ts +46 -0
  172. package/dist/charts/lib/format.js +56 -0
  173. package/dist/charts/lib/format.js.map +1 -0
  174. package/dist/charts/lib/legend.d.ts +45 -0
  175. package/dist/charts/lib/legend.js +117 -0
  176. package/dist/charts/lib/legend.js.map +1 -0
  177. package/dist/charts/lib/loading.d.ts +48 -0
  178. package/dist/charts/lib/loading.js +105 -0
  179. package/dist/charts/lib/loading.js.map +1 -0
  180. package/dist/charts/lib/paint.d.ts +154 -0
  181. package/dist/charts/lib/paint.js +551 -0
  182. package/dist/charts/lib/paint.js.map +1 -0
  183. package/dist/charts/lib/sonify-control.d.ts +163 -0
  184. package/dist/charts/lib/sonify-control.js +172 -0
  185. package/dist/charts/lib/sonify-control.js.map +1 -0
  186. package/dist/charts/lib/sonify.d.ts +248 -0
  187. package/dist/charts/lib/sonify.js +262 -0
  188. package/dist/charts/lib/sonify.js.map +1 -0
  189. package/dist/charts/lib/toolbar.d.ts +143 -0
  190. package/dist/charts/lib/toolbar.js +156 -0
  191. package/dist/charts/lib/toolbar.js.map +1 -0
  192. package/dist/charts/lib/tooltip.d.ts +59 -0
  193. package/dist/charts/lib/tooltip.js +135 -0
  194. package/dist/charts/lib/tooltip.js.map +1 -0
  195. package/dist/charts/lib/values.d.ts +56 -0
  196. package/dist/charts/lib/values.js +71 -0
  197. package/dist/charts/lib/values.js.map +1 -0
  198. package/dist/charts/lib/zoom.d.ts +139 -0
  199. package/dist/charts/lib/zoom.js +314 -0
  200. package/dist/charts/lib/zoom.js.map +1 -0
  201. package/dist/components/AppShell/AppShell.d.ts +15 -1
  202. package/dist/components/AppShell/AppShell.js +8 -4
  203. package/dist/components/AppShell/AppShell.js.map +1 -1
  204. package/dist/components/Article/Article.d.ts +51 -0
  205. package/dist/components/Article/Article.js +21 -0
  206. package/dist/components/Article/Article.js.map +1 -0
  207. package/dist/components/Button/Button.js +2 -2
  208. package/dist/components/Button/Button.js.map +1 -1
  209. package/dist/components/Calendar/Calendar.d.ts +1 -1
  210. package/dist/components/Calendar/Calendar.js +259 -24
  211. package/dist/components/Calendar/Calendar.js.map +1 -1
  212. package/dist/components/Checkbox/Checkbox.js +1 -1
  213. package/dist/components/Checkbox/Checkbox.js.map +1 -1
  214. package/dist/components/Collapsible/Collapsible.d.ts +15 -1
  215. package/dist/components/Collapsible/Collapsible.js +18 -0
  216. package/dist/components/Collapsible/Collapsible.js.map +1 -1
  217. package/dist/components/Combobox/Combobox.js +1 -1
  218. package/dist/components/Combobox/Combobox.js.map +1 -1
  219. package/dist/components/Command/Command.js +7 -7
  220. package/dist/components/Command/Command.js.map +1 -1
  221. package/dist/components/ContextMenu/ContextMenu.js +2 -2
  222. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  223. package/dist/components/Diagram/Diagram.d.ts +81 -0
  224. package/dist/components/Diagram/Diagram.js +139 -0
  225. package/dist/components/Diagram/Diagram.js.map +1 -0
  226. package/dist/components/Dialog/Dialog.js +1 -1
  227. package/dist/components/Dialog/Dialog.js.map +1 -1
  228. package/dist/components/DropdownMenu/DropdownMenu.js +2 -2
  229. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  230. package/dist/components/FigureBand/FigureBand.js +41 -30
  231. package/dist/components/FigureBand/FigureBand.js.map +1 -1
  232. package/dist/components/Popover/Popover.js +1 -1
  233. package/dist/components/Popover/Popover.js.map +1 -1
  234. package/dist/components/Select/Select.js +2 -2
  235. package/dist/components/Select/Select.js.map +1 -1
  236. package/dist/components/Sheet/Sheet.js +1 -1
  237. package/dist/components/Sheet/Sheet.js.map +1 -1
  238. package/dist/components/Slider/Slider.js +1 -1
  239. package/dist/components/Slider/Slider.js.map +1 -1
  240. package/dist/components/Steps/Steps.d.ts +65 -0
  241. package/dist/components/Steps/Steps.js +55 -0
  242. package/dist/components/Steps/Steps.js.map +1 -0
  243. package/dist/components/Switch/Switch.js +1 -1
  244. package/dist/components/Switch/Switch.js.map +1 -1
  245. package/dist/components/Tooltip/Tooltip.js +1 -1
  246. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  247. package/dist/index.d.ts +4 -1
  248. package/dist/index.js +3 -0
  249. package/dist/index.js.map +1 -1
  250. package/dist/lib/useSelectionIndicator.js +14 -10
  251. package/dist/lib/useSelectionIndicator.js.map +1 -1
  252. package/dist/semantic.css +19 -0
  253. package/dist/styles.css +1 -1
  254. package/dist/themes.css +103 -7
  255. package/dist/tokens.css +193 -10
  256. package/dist/tokens.d.ts +25 -0
  257. package/dist/tokens.js +154 -8
  258. package/dist/tokens.json +154 -8
  259. package/package.json +45 -20
  260. package/skills/misoto22-design/SKILL.md +171 -0
  261. package/skills/misoto22-design/evals/claims.json +95 -0
  262. package/skills/misoto22-design/rules/a11y.md +115 -0
  263. package/skills/misoto22-design/rules/composition.md +108 -0
  264. package/skills/misoto22-design/rules/forms.md +128 -0
  265. package/skills/misoto22-design/rules/naming.md +155 -0
  266. package/skills/misoto22-design/rules/tokens.md +131 -0
@@ -0,0 +1,163 @@
1
+ import * as react from 'react';
2
+ import { FC } from 'react';
3
+ import { ChartConfig } from './chart.js';
4
+ import { SonifyOptions, SonifyPoint, SonifySeries, SonifyState } from './sonify.js';
5
+ import 'recharts';
6
+
7
+ /**
8
+ * A chart's rows, flattened into what the engine plays.
9
+ *
10
+ * Anything that is not a finite number becomes a gap rather than a zero. A
11
+ * missing month is not a month with no visitors, and sounding it as the bottom
12
+ * of the range would invent a crash that never happened.
13
+ *
14
+ * A series whose `label` is not text falls back to its key: the label is spoken,
15
+ * and an icon cannot be.
16
+ */
17
+ declare function chartSonifySeries(rows: Record<string, unknown>[], config: ChartConfig, xDataKey?: string, keys?: string[]): SonifySeries[];
18
+ interface UseSonifyOptions extends SonifyOptions {
19
+ /**
20
+ * What the run is called. Spoken once, when the last note has finished, so a
21
+ * listener knows the silence is the end rather than a stall.
22
+ */
23
+ name?: string;
24
+ /**
25
+ * Fires as each point sounds, and with `null` between series.
26
+ *
27
+ * The hook already tracks this for you (`activePoint`); this is for a call
28
+ * site that wants to drive something else — a reference line on the plot, a
29
+ * row highlight in a table beside it.
30
+ */
31
+ onPointChange?: (point: SonifyPoint | null) => void;
32
+ }
33
+ interface UseSonifyResult {
34
+ state: SonifyState;
35
+ isPlaying: boolean;
36
+ /** False where the browser has no Web Audio. Resolved after mount, never during render. */
37
+ isSupported: boolean;
38
+ /** The point sounding right now, or `null` between series and at rest. */
39
+ activePoint: SonifyPoint | null;
40
+ /** The latest text for a live region — a series' range, or the end of the run. */
41
+ announcement: string;
42
+ /** Starts, or resumes. Call it from an event handler and nowhere else. */
43
+ play: () => void;
44
+ pause: () => void;
45
+ stop: () => void;
46
+ /** Play when idle or paused, pause when playing — what one button needs. */
47
+ toggle: () => void;
48
+ /** How long the whole run takes, in milliseconds. */
49
+ durationMs: number;
50
+ }
51
+ /**
52
+ * Plays a set of series as pitch over time, and reports what is sounding.
53
+ *
54
+ * The controller is rebuilt whenever the data or the tuning changes and the
55
+ * previous one is destroyed with it, so a brush drag or an unmount cancels
56
+ * every pending timer and releases the audio hardware. No `AudioContext` is
57
+ * constructed until the first `play()`.
58
+ *
59
+ * @example
60
+ * const series = useMemo(() => chartSonifySeries(rows, config, 'month'), [rows, config])
61
+ * const { toggle, isPlaying, announcement } = useSonify(series, { name: 'Visitors per month' })
62
+ *
63
+ * return (
64
+ * <>
65
+ * <button type="button" onClick={toggle}>{isPlaying ? 'Pause' : 'Play'}</button>
66
+ * <span role="status" aria-live="polite" className="sr-only">{announcement}</span>
67
+ * </>
68
+ * )
69
+ */
70
+ declare function useSonify(series: SonifySeries[], options?: UseSonifyOptions): UseSonifyResult;
71
+ /** Where the control sits above the plot. */
72
+ type SonifyAlign = 'start' | 'end';
73
+ interface SonifyProps extends SonifyOptions {
74
+ /**
75
+ * Which series play, in order. Defaults to every series in the chart's
76
+ * config.
77
+ *
78
+ * Worth narrowing on a chart with four series: sequential playback means four
79
+ * runs, and a listener rarely wants all of them.
80
+ */
81
+ keys?: string[];
82
+ /** Where the control sits above the plot. */
83
+ align?: SonifyAlign;
84
+ /** Overrides the control's visible label. */
85
+ label?: string;
86
+ /**
87
+ * Fires as each point sounds, and with `null` between series — for a call
88
+ * site that wants to mark the point on the plot itself.
89
+ */
90
+ onPointChange?: (point: SonifyPoint | null) => void;
91
+ /** Merged onto the control row, last. */
92
+ className?: string;
93
+ }
94
+ /**
95
+ * Plays this chart's values as sound.
96
+ *
97
+ * A slot: it renders nothing, and the chart root reads its props — the same
98
+ * shape as `<Chart.Values>`. Composing it is what adds the control above the
99
+ * plot, so a chart that does not ask for sonification ships none of it.
100
+ *
101
+ * Reach for it on the charts where a run of values over an axis IS the reading:
102
+ * a line, an area, a bar series over time. It is not offered on a pie or a
103
+ * treemap, where there is no order for the notes to be in.
104
+ *
105
+ * @example
106
+ * <LineChart title="Visitors per month" config={config} data={data} xDataKey="month">
107
+ * <LineChart.Sonify />
108
+ * <LineChart.XAxis dataKey="month" />
109
+ * <LineChart.Line dataKey="desktop" />
110
+ * </LineChart>
111
+ */
112
+ declare const Sonify: FC<SonifyProps>;
113
+ /**
114
+ * What a chart root calls to turn its composed `<Chart.Sonify />` slot and its
115
+ * on-screen rows into playable series.
116
+ *
117
+ * The memo is keyed on the VALUE of the slot's `keys`, not on its identity: a
118
+ * slot's props object is rebuilt on every parent render, so an identity
119
+ * dependency would replace the controller — and cancel whatever was playing —
120
+ * every time anything else on the page moved.
121
+ *
122
+ * Rows are the rows on screen, brush included. Sonifying the full data behind a
123
+ * brushed range would say something the picture does not.
124
+ *
125
+ * `rows` and `config` are compared by identity, as everywhere else in the
126
+ * package: a chart handed a freshly built `config` object on every render
127
+ * rebuilds its run each time, which stops playback. Define both outside the
128
+ * render, which is what every example here does.
129
+ */
130
+ declare function useChartSonifySeries(slot: SonifyProps | null, rows: Record<string, unknown>[], config: ChartConfig, xDataKey?: string): SonifySeries[];
131
+ interface ChartSonifyButtonProps extends SonifyProps {
132
+ /** The figure's name. Spoken as part of the control's accessible name. */
133
+ title: string;
134
+ /** The series to play, already flattened — see `chartSonifySeries`. */
135
+ series: SonifySeries[];
136
+ }
137
+ /**
138
+ * The control that starts a chart's sonification.
139
+ *
140
+ * A real `<button>`, because this is the whole point of the feature: an audio
141
+ * reading that a keyboard cannot reach is not an accessibility feature. The
142
+ * visible label is the verb only — "Play", "Pause" — and the rest of the
143
+ * accessible name is visually hidden, so the name still contains the visible
144
+ * text verbatim (WCAG 2.5.3) while a screen reader hears which figure it
145
+ * belongs to.
146
+ *
147
+ * No `aria-pressed`. A toggle button announces as "pressed"/"not pressed",
148
+ * which for a transport control contradicts the label it is sitting on —
149
+ * "Pause, toggle button, pressed" leaves the listener to work out whether the
150
+ * chart is playing. A label that names the next action says it once.
151
+ *
152
+ * The live region carries the RANGE announcement before each series, not a line
153
+ * per point: a fourteen-point run would interrupt itself fourteen times and the
154
+ * listener would hear none of the tones. The per-point readout beside the
155
+ * button is `aria-hidden` for the same reason — it is there for a sighted
156
+ * reader following along.
157
+ *
158
+ * @example
159
+ * <ChartSonifyButton title="Visitors per month" series={series} />
160
+ */
161
+ declare function ChartSonifyButton({ title, series, align, label, className, keys: _keys, onPointChange, formatValue, ...options }: ChartSonifyButtonProps): react.JSX.Element;
162
+
163
+ export { ChartSonifyButton, type ChartSonifyButtonProps, Sonify, type SonifyAlign, type SonifyProps, type UseSonifyOptions, type UseSonifyResult, chartSonifySeries, Sonify as default, useChartSonifySeries, useSonify };
@@ -0,0 +1,172 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Pause, Play, Square } from "lucide-react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Button } from "../../components/Button/Button.js";
7
+ import { defaultTick } from "./format.js";
8
+ import {
9
+ createSonification,
10
+ hasAudioSupport
11
+ } from "./sonify.js";
12
+ function categoryText(value) {
13
+ if (typeof value === "number") return value.toLocaleString();
14
+ if (typeof value === "string" || typeof value === "boolean") return String(value);
15
+ return "";
16
+ }
17
+ function chartSonifySeries(rows, config, xDataKey, keys) {
18
+ const wanted = keys ?? Object.keys(config);
19
+ const categories = xDataKey ? rows.map((row) => categoryText(row[xDataKey])) : void 0;
20
+ return wanted.filter((key) => key in config).map((key) => {
21
+ const label = config[key]?.label;
22
+ return {
23
+ key,
24
+ label: typeof label === "string" || typeof label === "number" ? String(label) : key,
25
+ values: rows.map((row) => {
26
+ const value = row[key];
27
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
28
+ }),
29
+ categories
30
+ };
31
+ });
32
+ }
33
+ function useSonify(series, options = {}) {
34
+ const {
35
+ name,
36
+ onPointChange,
37
+ noteMs,
38
+ gapMs,
39
+ leadInMs,
40
+ rootFrequency,
41
+ semitones,
42
+ wave,
43
+ volume,
44
+ formatValue = defaultTick
45
+ } = options;
46
+ const [state, setState] = useState("idle");
47
+ const [activePoint, setActivePoint] = useState(null);
48
+ const [announcement, setAnnouncement] = useState("");
49
+ const [isSupported, setIsSupported] = useState(true);
50
+ const pointChange = useRef(onPointChange);
51
+ useEffect(() => {
52
+ pointChange.current = onPointChange;
53
+ }, [onPointChange]);
54
+ useEffect(() => {
55
+ setIsSupported(hasAudioSupport());
56
+ }, []);
57
+ const controller = useMemo(
58
+ () => createSonification({
59
+ series,
60
+ options: { noteMs, gapMs, leadInMs, rootFrequency, semitones, wave, volume, formatValue },
61
+ onPoint: (point) => {
62
+ setActivePoint(point);
63
+ pointChange.current?.(point);
64
+ },
65
+ onAnnounce: setAnnouncement,
66
+ onStateChange: setState,
67
+ onEnd: () => setAnnouncement(name ? `End of ${name}.` : "End of chart.")
68
+ }),
69
+ [series, noteMs, gapMs, leadInMs, rootFrequency, semitones, wave, volume, formatValue, name]
70
+ );
71
+ useEffect(() => {
72
+ setState("idle");
73
+ setActivePoint(null);
74
+ return () => controller.destroy();
75
+ }, [controller]);
76
+ const play = useCallback(() => controller.play(), [controller]);
77
+ const pause = useCallback(() => controller.pause(), [controller]);
78
+ const stop = useCallback(() => {
79
+ controller.stop();
80
+ setAnnouncement("");
81
+ }, [controller]);
82
+ const toggle = useCallback(() => {
83
+ if (controller.state === "playing") controller.pause();
84
+ else controller.play();
85
+ }, [controller]);
86
+ return {
87
+ state,
88
+ isPlaying: state === "playing",
89
+ isSupported,
90
+ activePoint,
91
+ announcement,
92
+ play,
93
+ pause,
94
+ stop,
95
+ toggle,
96
+ durationMs: controller.durationMs
97
+ };
98
+ }
99
+ const ALIGN = {
100
+ start: "justify-start",
101
+ end: "justify-end"
102
+ };
103
+ const Sonify = () => null;
104
+ const NO_SERIES = [];
105
+ const KEY_SEPARATOR = "\0";
106
+ function useChartSonifySeries(slot, rows, config, xDataKey) {
107
+ const hasSlot = slot !== null;
108
+ const keys = slot?.keys?.join(KEY_SEPARATOR);
109
+ return useMemo(
110
+ () => hasSlot ? chartSonifySeries(rows, config, xDataKey, keys?.split(KEY_SEPARATOR)) : NO_SERIES,
111
+ [hasSlot, keys, rows, config, xDataKey]
112
+ );
113
+ }
114
+ function ChartSonifyButton({
115
+ title,
116
+ series,
117
+ align = "end",
118
+ label,
119
+ className,
120
+ keys: _keys,
121
+ onPointChange,
122
+ formatValue = defaultTick,
123
+ ...options
124
+ }) {
125
+ const { state, isPlaying, isSupported, activePoint, announcement, stop, toggle } = useSonify(
126
+ series,
127
+ { ...options, formatValue, name: title, onPointChange }
128
+ );
129
+ const hasPoints = series.some((entry) => entry.values.some((value) => value !== null));
130
+ const isDisabled = !isSupported || !hasPoints;
131
+ const Icon = isPlaying ? Pause : Play;
132
+ const reason = !isSupported ? " \u2014 audio is not available in this browser" : !hasPoints ? " \u2014 nothing to play" : "";
133
+ return /* @__PURE__ */ jsxs("div", { className: cn("mb-2 flex items-center gap-2", ALIGN[align], className), children: [
134
+ activePoint && // Visual only. The live region is deliberately not told about every
135
+ // point; see the component note above.
136
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: "mono-meta truncate text-(--ink-3-aa)", children: [
137
+ activePoint.category ?? `#${activePoint.index + 1}`,
138
+ activePoint.value !== null && ` \xB7 ${formatValue(activePoint.value)}`
139
+ ] }),
140
+ state !== "idle" && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "md", onClick: stop, children: [
141
+ /* @__PURE__ */ jsx(Square, { size: 14, strokeWidth: 1.5, "aria-hidden": true }),
142
+ "Stop",
143
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
144
+ " playing ",
145
+ title,
146
+ " as sound"
147
+ ] })
148
+ ] }),
149
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "md", onClick: toggle, disabled: isDisabled, children: [
150
+ /* @__PURE__ */ jsx(Icon, { size: 14, strokeWidth: 1.5, "aria-hidden": true }),
151
+ label ?? (isPlaying ? "Pause" : "Play"),
152
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
153
+ " ",
154
+ isPlaying ? "playing" : "this chart",
155
+ " as sound: ",
156
+ title,
157
+ reason
158
+ ] })
159
+ ] }),
160
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })
161
+ ] });
162
+ }
163
+ var sonify_control_default = Sonify;
164
+ export {
165
+ ChartSonifyButton,
166
+ Sonify,
167
+ chartSonifySeries,
168
+ sonify_control_default as default,
169
+ useChartSonifySeries,
170
+ useSonify
171
+ };
172
+ //# sourceMappingURL=sonify-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/charts/lib/sonify-control.tsx"],"sourcesContent":["'use client'\n\nimport { Pause, Play, Square } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useRef, useState, type FC } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Button } from '../../components/Button/Button'\nimport type { ChartConfig } from './chart'\nimport { defaultTick } from './format'\nimport {\n createSonification,\n hasAudioSupport,\n type SonifyOptions,\n type SonifyPoint,\n type SonifySeries,\n type SonifyState,\n} from './sonify'\n\n/**\n * The React surface over `sonify.ts` — a hook, a control, and the slot a chart\n * composes.\n *\n * Everything here obeys the one rule the engine is built around: **sound only\n * ever starts from an explicit user action.** There is no effect that plays, no\n * `autoPlay` prop, and no dependency change that can begin a run. That is not\n * the same conversation as `prefers-reduced-motion` — sound is not motion, and\n * reading a motion preference as \"no audio either\" would silence the readers\n * this feature exists for.\n */\n\n/** A cell as speakable text. Speech has no use for an object. */\nfunction categoryText(value: unknown): string {\n if (typeof value === 'number') return value.toLocaleString()\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n return ''\n}\n\n/**\n * A chart's rows, flattened into what the engine plays.\n *\n * Anything that is not a finite number becomes a gap rather than a zero. A\n * missing month is not a month with no visitors, and sounding it as the bottom\n * of the range would invent a crash that never happened.\n *\n * A series whose `label` is not text falls back to its key: the label is spoken,\n * and an icon cannot be.\n */\nexport function chartSonifySeries(\n rows: Record<string, unknown>[],\n config: ChartConfig,\n xDataKey?: string,\n keys?: string[],\n): SonifySeries[] {\n const wanted = keys ?? Object.keys(config)\n const categories = xDataKey ? rows.map((row) => categoryText(row[xDataKey])) : undefined\n\n return wanted\n .filter((key) => key in config)\n .map((key) => {\n const label = config[key]?.label\n return {\n key,\n label: typeof label === 'string' || typeof label === 'number' ? String(label) : key,\n values: rows.map((row) => {\n const value = row[key]\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n }),\n categories,\n }\n })\n}\n\nexport interface UseSonifyOptions extends SonifyOptions {\n /**\n * What the run is called. Spoken once, when the last note has finished, so a\n * listener knows the silence is the end rather than a stall.\n */\n name?: string\n /**\n * Fires as each point sounds, and with `null` between series.\n *\n * The hook already tracks this for you (`activePoint`); this is for a call\n * site that wants to drive something else — a reference line on the plot, a\n * row highlight in a table beside it.\n */\n onPointChange?: (point: SonifyPoint | null) => void\n}\n\nexport interface UseSonifyResult {\n state: SonifyState\n isPlaying: boolean\n /** False where the browser has no Web Audio. Resolved after mount, never during render. */\n isSupported: boolean\n /** The point sounding right now, or `null` between series and at rest. */\n activePoint: SonifyPoint | null\n /** The latest text for a live region — a series' range, or the end of the run. */\n announcement: string\n /** Starts, or resumes. Call it from an event handler and nowhere else. */\n play: () => void\n pause: () => void\n stop: () => void\n /** Play when idle or paused, pause when playing — what one button needs. */\n toggle: () => void\n /** How long the whole run takes, in milliseconds. */\n durationMs: number\n}\n\n/**\n * Plays a set of series as pitch over time, and reports what is sounding.\n *\n * The controller is rebuilt whenever the data or the tuning changes and the\n * previous one is destroyed with it, so a brush drag or an unmount cancels\n * every pending timer and releases the audio hardware. No `AudioContext` is\n * constructed until the first `play()`.\n *\n * @example\n * const series = useMemo(() => chartSonifySeries(rows, config, 'month'), [rows, config])\n * const { toggle, isPlaying, announcement } = useSonify(series, { name: 'Visitors per month' })\n *\n * return (\n * <>\n * <button type=\"button\" onClick={toggle}>{isPlaying ? 'Pause' : 'Play'}</button>\n * <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">{announcement}</span>\n * </>\n * )\n */\nexport function useSonify(series: SonifySeries[], options: UseSonifyOptions = {}): UseSonifyResult {\n const {\n name,\n onPointChange,\n noteMs,\n gapMs,\n leadInMs,\n rootFrequency,\n semitones,\n wave,\n volume,\n formatValue = defaultTick,\n } = options\n\n const [state, setState] = useState<SonifyState>('idle')\n const [activePoint, setActivePoint] = useState<SonifyPoint | null>(null)\n const [announcement, setAnnouncement] = useState('')\n // Starts optimistic so the server and the first client render agree; the\n // effect below is the only place the browser is asked.\n const [isSupported, setIsSupported] = useState(true)\n\n // Held in a ref rather than in the memo's dependencies: a call site that\n // passes an inline arrow would otherwise rebuild the controller — and cancel\n // the run — on every render.\n const pointChange = useRef(onPointChange)\n useEffect(() => {\n pointChange.current = onPointChange\n }, [onPointChange])\n\n useEffect(() => {\n setIsSupported(hasAudioSupport())\n }, [])\n\n const controller = useMemo(\n () =>\n createSonification({\n series,\n options: { noteMs, gapMs, leadInMs, rootFrequency, semitones, wave, volume, formatValue },\n onPoint: (point) => {\n setActivePoint(point)\n pointChange.current?.(point)\n },\n onAnnounce: setAnnouncement,\n onStateChange: setState,\n onEnd: () => setAnnouncement(name ? `End of ${name}.` : 'End of chart.'),\n }),\n [series, noteMs, gapMs, leadInMs, rootFrequency, semitones, wave, volume, formatValue, name],\n )\n\n useEffect(() => {\n setState('idle')\n setActivePoint(null)\n // Runs when the data changes as well as on unmount, which is what stops a\n // brushed-away run from playing on against rows that are no longer drawn.\n return () => controller.destroy()\n }, [controller])\n\n const play = useCallback(() => controller.play(), [controller])\n const pause = useCallback(() => controller.pause(), [controller])\n const stop = useCallback(() => {\n controller.stop()\n setAnnouncement('')\n }, [controller])\n\n const toggle = useCallback(() => {\n if (controller.state === 'playing') controller.pause()\n else controller.play()\n }, [controller])\n\n return {\n state,\n isPlaying: state === 'playing',\n isSupported,\n activePoint,\n announcement,\n play,\n pause,\n stop,\n toggle,\n durationMs: controller.durationMs,\n }\n}\n\n/** Where the control sits above the plot. */\nexport type SonifyAlign = 'start' | 'end'\n\nconst ALIGN: Record<SonifyAlign, string> = {\n start: 'justify-start',\n end: 'justify-end',\n}\n\nexport interface SonifyProps extends SonifyOptions {\n /**\n * Which series play, in order. Defaults to every series in the chart's\n * config.\n *\n * Worth narrowing on a chart with four series: sequential playback means four\n * runs, and a listener rarely wants all of them.\n */\n keys?: string[]\n /** Where the control sits above the plot. */\n align?: SonifyAlign\n /** Overrides the control's visible label. */\n label?: string\n /**\n * Fires as each point sounds, and with `null` between series — for a call\n * site that wants to mark the point on the plot itself.\n */\n onPointChange?: (point: SonifyPoint | null) => void\n /** Merged onto the control row, last. */\n className?: string\n}\n\n/**\n * Plays this chart's values as sound.\n *\n * A slot: it renders nothing, and the chart root reads its props — the same\n * shape as `<Chart.Values>`. Composing it is what adds the control above the\n * plot, so a chart that does not ask for sonification ships none of it.\n *\n * Reach for it on the charts where a run of values over an axis IS the reading:\n * a line, an area, a bar series over time. It is not offered on a pie or a\n * treemap, where there is no order for the notes to be in.\n *\n * @example\n * <LineChart title=\"Visitors per month\" config={config} data={data} xDataKey=\"month\">\n * <LineChart.Sonify />\n * <LineChart.XAxis dataKey=\"month\" />\n * <LineChart.Line dataKey=\"desktop\" />\n * </LineChart>\n */\nexport const Sonify: FC<SonifyProps> = () => null\n\n/** Shared so an absent slot always yields the same array, not a new empty one. */\nconst NO_SERIES: SonifySeries[] = []\n\n/** Separator for the memo key below. Never appears in a series key. */\nconst KEY_SEPARATOR = '\\u0000'\n\n/**\n * What a chart root calls to turn its composed `<Chart.Sonify />` slot and its\n * on-screen rows into playable series.\n *\n * The memo is keyed on the VALUE of the slot's `keys`, not on its identity: a\n * slot's props object is rebuilt on every parent render, so an identity\n * dependency would replace the controller — and cancel whatever was playing —\n * every time anything else on the page moved.\n *\n * Rows are the rows on screen, brush included. Sonifying the full data behind a\n * brushed range would say something the picture does not.\n *\n * `rows` and `config` are compared by identity, as everywhere else in the\n * package: a chart handed a freshly built `config` object on every render\n * rebuilds its run each time, which stops playback. Define both outside the\n * render, which is what every example here does.\n */\nexport function useChartSonifySeries(\n slot: SonifyProps | null,\n rows: Record<string, unknown>[],\n config: ChartConfig,\n xDataKey?: string,\n): SonifySeries[] {\n const hasSlot = slot !== null\n const keys = slot?.keys?.join(KEY_SEPARATOR)\n\n return useMemo(\n () =>\n hasSlot\n ? chartSonifySeries(rows, config, xDataKey, keys?.split(KEY_SEPARATOR))\n : NO_SERIES,\n [hasSlot, keys, rows, config, xDataKey],\n )\n}\n\nexport interface ChartSonifyButtonProps extends SonifyProps {\n /** The figure's name. Spoken as part of the control's accessible name. */\n title: string\n /** The series to play, already flattened — see `chartSonifySeries`. */\n series: SonifySeries[]\n}\n\n/**\n * The control that starts a chart's sonification.\n *\n * A real `<button>`, because this is the whole point of the feature: an audio\n * reading that a keyboard cannot reach is not an accessibility feature. The\n * visible label is the verb only — \"Play\", \"Pause\" — and the rest of the\n * accessible name is visually hidden, so the name still contains the visible\n * text verbatim (WCAG 2.5.3) while a screen reader hears which figure it\n * belongs to.\n *\n * No `aria-pressed`. A toggle button announces as \"pressed\"/\"not pressed\",\n * which for a transport control contradicts the label it is sitting on —\n * \"Pause, toggle button, pressed\" leaves the listener to work out whether the\n * chart is playing. A label that names the next action says it once.\n *\n * The live region carries the RANGE announcement before each series, not a line\n * per point: a fourteen-point run would interrupt itself fourteen times and the\n * listener would hear none of the tones. The per-point readout beside the\n * button is `aria-hidden` for the same reason — it is there for a sighted\n * reader following along.\n *\n * @example\n * <ChartSonifyButton title=\"Visitors per month\" series={series} />\n */\nexport function ChartSonifyButton({\n title,\n series,\n align = 'end',\n label,\n className,\n keys: _keys,\n onPointChange,\n formatValue = defaultTick,\n ...options\n}: ChartSonifyButtonProps) {\n const { state, isPlaying, isSupported, activePoint, announcement, stop, toggle } = useSonify(\n series,\n { ...options, formatValue, name: title, onPointChange },\n )\n\n const hasPoints = series.some((entry) => entry.values.some((value) => value !== null))\n const isDisabled = !isSupported || !hasPoints\n const Icon = isPlaying ? Pause : Play\n\n const reason = !isSupported\n ? ' — audio is not available in this browser'\n : !hasPoints\n ? ' — nothing to play'\n : ''\n\n return (\n <div className={cn('mb-2 flex items-center gap-2', ALIGN[align], className)}>\n {activePoint && (\n // Visual only. The live region is deliberately not told about every\n // point; see the component note above.\n <span aria-hidden className=\"mono-meta truncate text-(--ink-3-aa)\">\n {activePoint.category ?? `#${activePoint.index + 1}`}\n {activePoint.value !== null && ` · ${formatValue(activePoint.value)}`}\n </span>\n )}\n\n {state !== 'idle' && (\n <Button variant=\"ghost\" size=\"md\" onClick={stop}>\n <Square size={14} strokeWidth={1.5} aria-hidden />\n Stop\n <span className=\"sr-only\"> playing {title} as sound</span>\n </Button>\n )}\n\n <Button variant=\"secondary\" size=\"md\" onClick={toggle} disabled={isDisabled}>\n <Icon size={14} strokeWidth={1.5} aria-hidden />\n {label ?? (isPlaying ? 'Pause' : 'Play')}\n <span className=\"sr-only\">\n {' '}\n {isPlaying ? 'playing' : 'this chart'} as sound: {title}\n {reason}\n </span>\n </Button>\n\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n </div>\n )\n}\n\nexport default Sonify\n"],"mappings":"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","names":[]}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * A chart's values as pitch over time — the reading a listener gets instead of
3
+ * the picture.
4
+ *
5
+ * The hidden data table (see `figure.tsx`) hands a screen-reader user every
6
+ * number, which is access but not a SHAPE: fourteen figures read one at a time
7
+ * do not tell anyone that the line dips in March and never recovers. A run of
8
+ * tones does, in three seconds, and it is the one thing a chart can offer that
9
+ * a table cannot. Highcharts is the only mainstream library that ships it; this
10
+ * is that idea, without the dependency.
11
+ *
12
+ * Nothing here touches `window` or constructs an `AudioContext` at module load.
13
+ * The context is created on the first `play()`, which is the only moment a
14
+ * browser will allow it — every engine blocks audio until a user gesture, and a
15
+ * context built at import time arrives permanently `suspended`.
16
+ *
17
+ * **This never starts on its own.** `prefers-reduced-motion` is not the setting
18
+ * that governs it — sound is not motion, and gating audio behind a motion
19
+ * preference would both silence readers who want it and imply that readers who
20
+ * do not want motion also do not want sound. The correct rule is simpler and
21
+ * absolute: audio begins from an explicit user action, and from nothing else.
22
+ * There is deliberately no `autoPlay` option to pass.
23
+ */
24
+ /** Whether the run is sounding, held, or not started. */
25
+ type SonifyState = 'idle' | 'playing' | 'paused';
26
+ /**
27
+ * The oscillator behind every note.
28
+ *
29
+ * `triangle` is the default because the two obvious choices are both worse: a
30
+ * `sine` has no harmonics above its fundamental, so on a laptop speaker — which
31
+ * reproduces almost nothing under 200 Hz — the bottom of the range simply
32
+ * vanishes, and a listener hears silence where the data is lowest. A `square`
33
+ * is the opposite problem: all odd harmonics at full strength is fatiguing
34
+ * inside ten notes and genuinely unpleasant across sixty. A triangle carries
35
+ * enough harmonic content to survive a small speaker and stay comfortable.
36
+ */
37
+ type SonifyWave = 'sine' | 'triangle' | 'square' | 'sawtooth';
38
+ /** The value range the pitch scale is stretched across. */
39
+ interface SonifyDomain {
40
+ min: number;
41
+ max: number;
42
+ }
43
+ /** One series, flattened to what sound needs: its values, in order. */
44
+ interface SonifySeries {
45
+ /** The series key, so a caller can tie a point back to its own data. */
46
+ key: string;
47
+ /** What the series is called, spoken before its run of tones. */
48
+ label: string;
49
+ /** One entry per point. `null` is a gap — it holds its slot in silence. */
50
+ values: (number | null)[];
51
+ /** The category axis, if there is one. Used to say where the run starts and ends. */
52
+ categories?: string[];
53
+ }
54
+ /** The point sounding right now, for a caller that wants to highlight it. */
55
+ interface SonifyPoint {
56
+ seriesKey: string;
57
+ seriesLabel: string;
58
+ /** Position within its own series. */
59
+ index: number;
60
+ /** The category this point sits at, when the chart has a category axis. */
61
+ category?: string;
62
+ /** `null` for a gap in the data, which sounds as a rest. */
63
+ value: number | null;
64
+ }
65
+ interface SonifyOptions {
66
+ /** How long each point holds the floor. Below ~90ms the run stops being readable. */
67
+ noteMs?: number;
68
+ /** Silence after a series, before the next one is announced. */
69
+ gapMs?: number;
70
+ /**
71
+ * The pause between an announcement and the first note it describes.
72
+ *
73
+ * Not decoration: a screen reader needs the floor to itself to read the range
74
+ * out, and a tone starting on top of it means the listener gets neither.
75
+ */
76
+ leadInMs?: number;
77
+ /** The pitch the lowest value sounds at, in Hz. */
78
+ rootFrequency?: number;
79
+ /** How far above the root the highest value sits, in semitones. */
80
+ semitones?: number;
81
+ wave?: SonifyWave;
82
+ /** Peak gain per note, 0–1. Deliberately quiet; a chart is not a media player. */
83
+ volume?: number;
84
+ /**
85
+ * How a number is spoken in an announcement. Defaults to the axis's own
86
+ * formatting.
87
+ *
88
+ * Define it outside the render or memoise it. It is part of what the run
89
+ * says, so a new function identity rebuilds the run — and a rebuilt run is a
90
+ * stopped one.
91
+ */
92
+ formatValue?: (value: number) => string;
93
+ }
94
+ /**
95
+ * The defaults, and why each is the number it is.
96
+ *
97
+ * `rootFrequency` 220 Hz (A3) with a `semitones` span of 24 puts the whole
98
+ * range between A3 and A5. Below roughly 200 Hz a laptop or phone speaker
99
+ * reproduces very little, and above about 1 kHz sustained tones start to read
100
+ * as an alarm; two octaves inside that window is the widest span that stays
101
+ * comfortable at both ends. Two octaves is also about where pitch DISCRIMINATION
102
+ * stops improving — stretching to four does not let anyone read the numbers off
103
+ * more precisely, it just makes the top shrill.
104
+ */
105
+ declare const SONIFY_DEFAULTS: {
106
+ readonly noteMs: 220;
107
+ readonly gapMs: 700;
108
+ readonly leadInMs: 1200;
109
+ readonly rootFrequency: 220;
110
+ readonly semitones: 24;
111
+ readonly wave: "triangle";
112
+ readonly volume: 0.18;
113
+ };
114
+ /**
115
+ * A value's pitch, on a semitone scale rather than a linear sweep of hertz.
116
+ *
117
+ * This is the single most important decision in the file. Pitch perception is
118
+ * logarithmic: the ear hears a RATIO, not a difference. 220 Hz to 440 Hz is one
119
+ * octave and so is 440 Hz to 880 Hz, even though the second gap is twice as
120
+ * many hertz. Map values linearly onto hertz and the consequence is immediate —
121
+ * the bottom half of the data spreads across most of the perceived range while
122
+ * the top half compresses into a few near-identical tones, so a rise from 900
123
+ * to 1000 sounds like nothing and a rise from 10 to 110 sounds enormous. The
124
+ * chart lied.
125
+ *
126
+ * Mapping the value range onto a fixed span of SEMITONES fixes it: equal steps
127
+ * in the data become equal musical intervals, which is what a listener actually
128
+ * compares. The map is linear-in-value and exponential-in-frequency, so it is
129
+ * the audio equivalent of a linear axis — not a log axis, which would be a
130
+ * different claim about the data.
131
+ *
132
+ * A flat series (`min === max`) sounds at the middle of the range rather than at
133
+ * the floor: flat data is not low data, and a run of bottom notes reads as one.
134
+ *
135
+ * @example
136
+ * valueToFrequency(50, { min: 0, max: 100 }) // 220 * 2 ** 1 = 440 Hz, one octave up
137
+ */
138
+ declare function valueToFrequency(value: number, domain: SonifyDomain, pitch?: Pick<SonifyOptions, 'rootFrequency' | 'semitones'>): number;
139
+ /**
140
+ * The value range every series is measured against, or `null` when nothing is
141
+ * finite.
142
+ *
143
+ * Shared across all series on purpose. These charts draw their series against
144
+ * ONE value axis, so they have to sound against one pitch axis too — normalising
145
+ * each series to its own extremes would make a flat series that never leaves 2%
146
+ * sound exactly like the one that swings across the whole plot, which is the
147
+ * opposite of what the picture says.
148
+ */
149
+ declare function sonifyDomain(series: SonifySeries[]): SonifyDomain | null;
150
+ /**
151
+ * What is said before a series plays.
152
+ *
153
+ * A run of tones with no frame of reference is unreadable — the listener hears
154
+ * a contour and has no idea whether it spans four units or four million, or how
155
+ * many points went by. Naming the count, the span of the category axis and the
156
+ * two extremes costs one sentence and turns the melody into a measurement. The
157
+ * direction of the mapping is stated too, because "higher is more" is a
158
+ * convention, not a fact the sound carries.
159
+ */
160
+ declare function describeSeries(series: SonifySeries, formatValue?: (value: number) => string): string;
161
+ /** One thing that happens at one moment: something said, or a note sounded. */
162
+ type SonifyStep = {
163
+ kind: 'announce';
164
+ atMs: number;
165
+ text: string;
166
+ } | {
167
+ kind: 'note';
168
+ atMs: number;
169
+ point: SonifyPoint;
170
+ frequency: number | null;
171
+ };
172
+ /**
173
+ * The whole run, laid out on a timeline before a single sound is made.
174
+ *
175
+ * Built as data rather than driven by a loop with a timer inside it, because
176
+ * this is the part worth testing: the maths that decides when each note lands
177
+ * is checkable without a speaker, and pause/resume becomes "walk the same array
178
+ * from a different index" rather than a second, subtly different code path.
179
+ *
180
+ * Several series play **in sequence, each introduced by name**, rather than
181
+ * simultaneously with one panned to each ear. Hard panning is the more
182
+ * impressive demo and the wrong choice: it assumes a stereo output and two
183
+ * usable ears, so a mono speaker, a single hearing aid, or unilateral hearing
184
+ * loss collapses both series into one interleaved melody the listener cannot
185
+ * unpick — and it caps the feature at two series, when four is ordinary. Played
186
+ * in sequence, the worst case is that the run takes longer, which is a cost the
187
+ * listener can see coming and a caller can shorten with `noteMs`.
188
+ */
189
+ declare function sonifyTimeline(series: SonifySeries[], options?: SonifyOptions): SonifyStep[];
190
+ /** How long the whole run takes, in milliseconds. */
191
+ declare function sonifyDuration(series: SonifySeries[], options?: SonifyOptions): number;
192
+ /**
193
+ * Whether this environment can make a sound at all.
194
+ *
195
+ * Safe on a server: it reads `window` from inside a function, so importing this
196
+ * module during a server render touches nothing.
197
+ */
198
+ declare function hasAudioSupport(): boolean;
199
+ interface SonifyHandlers {
200
+ /** The point currently sounding, so a chart can mark it. `null` between series. */
201
+ onPoint?: (point: SonifyPoint | null) => void;
202
+ /** Text meant for a live region — the range announcement before each series. */
203
+ onAnnounce?: (text: string) => void;
204
+ onStateChange?: (state: SonifyState) => void;
205
+ /** Fires once the last note has sounded, not when it was scheduled. */
206
+ onEnd?: () => void;
207
+ }
208
+ interface SonifyInput extends SonifyHandlers {
209
+ series: SonifySeries[];
210
+ options?: SonifyOptions;
211
+ }
212
+ interface SonifyController {
213
+ /**
214
+ * Starts, or resumes from where a pause left off.
215
+ *
216
+ * MUST be called from a user gesture. That is a browser rule, not a
217
+ * preference — and it is also the accessibility rule this feature is built
218
+ * on, so there is no way to reach this except from something the reader did.
219
+ */
220
+ play: () => void;
221
+ /** Holds position. The current note is faded out rather than cut. */
222
+ pause: () => void;
223
+ /** Stops and rewinds to the beginning. */
224
+ stop: () => void;
225
+ /** Stops and releases the audio hardware. Call on unmount. */
226
+ destroy: () => void;
227
+ readonly state: SonifyState;
228
+ /** The whole run, in milliseconds. */
229
+ readonly durationMs: number;
230
+ }
231
+ /**
232
+ * A run of one chart's series, ready to play.
233
+ *
234
+ * Constructing one allocates nothing but an array — no `AudioContext`, no
235
+ * timer, no node. That matters because a chart builds its controller during
236
+ * render, where creating a context would both break a server render and burn
237
+ * one of the browser's limited context slots on a chart nobody listens to.
238
+ *
239
+ * @example
240
+ * const run = createSonification({
241
+ * series: [{ key: 'desktop', label: 'Desktop', values: [186, 305, 237], categories: ['Jan', 'Feb', 'Mar'] }],
242
+ * onPoint: (point) => setActive(point),
243
+ * })
244
+ * button.addEventListener('click', () => run.play())
245
+ */
246
+ declare function createSonification({ series, options, onPoint, onAnnounce, onStateChange, onEnd, }: SonifyInput): SonifyController;
247
+
248
+ export { SONIFY_DEFAULTS, type SonifyController, type SonifyDomain, type SonifyHandlers, type SonifyInput, type SonifyOptions, type SonifyPoint, type SonifySeries, type SonifyState, type SonifyStep, type SonifyWave, createSonification, describeSeries, hasAudioSupport, sonifyDomain, sonifyDuration, sonifyTimeline, valueToFrequency };