visualifyjs 2.5.3 → 3.0.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 (252) hide show
  1. package/.claude/mem/TIMELINE.md +36 -0
  2. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-external-script-solution.md +24 -0
  3. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-session-summary.md +43 -0
  4. package/.claude/mem/notes/2026-02-11-cli-fix-editor-command-alias.md +26 -0
  5. package/.claude/mem/notes/2026-02-11-phase-3-developer-experience-completed.md +51 -0
  6. package/.claude/mem/notes/2026-02-11-phase-4-web-workers-implementation-complete.md +59 -0
  7. package/.claude/mem/notes/2026-02-11-visualify-phase-2-3d-visualization-complete.md +50 -0
  8. package/.claude/mem/notes/2026-02-11-visualify-phase-2-committed-ready-for-phase-3.md +33 -0
  9. package/.claude/mem/notes/2026-02-11-visualify-phase-3-complete-developer-experience.md +52 -0
  10. package/.claude/mem/notes/2026-02-11-visualify-repository-cleanup-complete.md +28 -0
  11. package/.claude/mem/notes/2026-02-18-codebase-cleanup-docsify-plugin-documentation.md +37 -0
  12. package/.claude/mem/notes/2026-02-19-css-grid-layout-fix-displaycontents-on-vcontroller.md +18 -0
  13. package/.claude/mem/notes/2026-02-19-docsify-plugin-fixes-latex-and-visualify-code-bloc.md +26 -0
  14. package/.claude/mem/notes/2026-02-19-page-mode-docs-update-decisions.md +23 -0
  15. package/.claude/mem/notes/2026-02-19-react-context-infinite-re-render-loop-fix-pattern.md +31 -0
  16. package/.claude/mem/notes/2026-02-19-version-300-bump-and-build-fixes.md +32 -0
  17. package/.claude/mem/notes/2026-02-19-visualify-build-deployment-architecture-bug-fixes.md +25 -0
  18. package/.claude/mem/notes/2026-02-19-visualify-dist-iife-self-contained-build-config.md +30 -0
  19. package/.claude/mem/notes/2026-02-19-visualify-infinite-loop-i18n-fixes.md +31 -0
  20. package/.claude/mem/notes/2026-02-19-visualify-v3-bundle-splitting-docs-restructuring.md +32 -0
  21. package/.claude/mem/notes/2026-02-20-bundle-externalization-final-architecture.md +29 -0
  22. package/.claude/mem/notes/2026-02-20-chromium-page-fix-unstable-keys-and-double-event-b.md +27 -0
  23. package/.claude/mem/notes/2026-02-20-console-cleanup-bundle-optimization-commit.md +20 -0
  24. package/.claude/mem/notes/2026-02-20-dotbio-dot-plot-fix-useeffect-dependency.md +21 -0
  25. package/.claude/mem/notes/2026-02-20-public-folder-cleanup-and-readme-rewrite.md +25 -0
  26. package/.claude/mem/notes/2026-02-20-v300-release-and-beta-channel-strategy.md +29 -0
  27. package/.claude/mem/notes/2026-02-20-visium-background-image-unknown-legend-fix.md +19 -0
  28. package/.claude/mem/notes/2026-02-20-visualify-cdn-loader-bundle-externalization.md +34 -0
  29. package/.claude/mem/sessions/session-2026-02-20-031524.md +54 -0
  30. package/.claude/settings.local.json +21 -0
  31. package/.github/workflows/static.yml.bak +51 -51
  32. package/.sisyphus/boulder.json +65 -0
  33. package/.sisyphus/plans/phase-4-advanced-optimizations.md +217 -0
  34. package/LICENSE +674 -674
  35. package/README.md +94 -59
  36. package/config-overrides.js +31 -31
  37. package/dist/stats.html +4949 -0
  38. package/dist/visualify-3d.esm.js +1 -0
  39. package/dist/visualify-3d.js +1 -0
  40. package/dist/visualify-core.esm.js +1 -0
  41. package/dist/visualify-core.js +1 -0
  42. package/dist/visualify-docs.esm.js +1 -0
  43. package/dist/visualify-docs.js +1 -0
  44. package/dist/visualify-loader.js +1 -0
  45. package/dist/visualify-pages.esm.js +1 -0
  46. package/dist/visualify-pages.js +1 -0
  47. package/dist/visualify-portal.esm.js +1 -0
  48. package/dist/visualify-portal.js +1 -0
  49. package/dist/visualify-shared.js +26571 -0
  50. package/dist/visualify.js +1 -188
  51. package/docs/CHANGELOG.md +148 -0
  52. package/docs/cli/commands.md +513 -0
  53. package/docs/configuration/visualify-json.md +474 -0
  54. package/docs/docs/3d-visualization.md +374 -0
  55. package/docs/docs/CLI.md +303 -34
  56. package/docs/docs/README.md +65 -65
  57. package/docs/docs/Rechart/bar.md +190 -190
  58. package/docs/docs/Rechart/funnel.md +241 -193
  59. package/docs/docs/Rechart/line.md +355 -355
  60. package/docs/docs/Rechart/pie.md +225 -225
  61. package/docs/docs/Rechart/radar.md +253 -253
  62. package/docs/docs/Rechart/scatter.md +298 -0
  63. package/docs/docs/_404.md +51 -51
  64. package/docs/docs/_coverpage.md +11 -11
  65. package/docs/docs/_sidebar.md +54 -43
  66. package/docs/docs/components/dotBio.md +87 -34
  67. package/docs/docs/components/echart.md +171 -82
  68. package/docs/docs/components/html.md +61 -34
  69. package/docs/docs/components/macaron.md +156 -145
  70. package/docs/docs/components/markdown.md +42 -0
  71. package/docs/docs/components/more.md +183 -142
  72. package/docs/docs/components/plotly.md +132 -62
  73. package/docs/docs/components/scatterL.md +171 -70
  74. package/docs/docs/components/visium.md +112 -57
  75. package/docs/docs/configuration.md +121 -123
  76. package/docs/docs/deploy.md +31 -31
  77. package/docs/docs/docsify-plugin.md +655 -0
  78. package/docs/docs/hmr.md +165 -0
  79. package/docs/docs/i18n.md +332 -0
  80. package/docs/docs/log.md +30 -1
  81. package/docs/docs/more-pages.md +23 -23
  82. package/docs/docs/quickstart.md +148 -119
  83. package/docs/docs/rechart-attributes.md +74 -74
  84. package/docs/docs/rechart-basic-usage.md +160 -162
  85. package/docs/docs/theme.md +5 -5
  86. package/docs/docs/typescript.md +306 -0
  87. package/docs/docs/visual-editor.md +359 -0
  88. package/docs/index.html +85 -71
  89. package/docs/manifest.json +23 -23
  90. package/docs/migration/v3-migration.md +392 -0
  91. package/docs/static/css/fluff-stuff.css +169 -169
  92. package/docs/static/css/font-awesome.min.css +4 -4
  93. package/docs/static/css/visualify.css +6 -25
  94. package/docs/static/js/3d-viz-examples.js +181 -0
  95. package/docs/static/js/configuration.js +630 -448
  96. package/docs/static/js/visualify.js +1 -188
  97. package/package.json +106 -84
  98. package/rollup.config.mjs +766 -76
  99. package/src/_css/404.css +115 -115
  100. package/src/_css/App.css +37 -37
  101. package/src/_css/autoSuggestion.css +26 -26
  102. package/src/_css/circular-progress.css +32 -32
  103. package/src/_css/index.css +36 -36
  104. package/src/_css/modern.css +350 -25
  105. package/src/_media/corner.svg +8 -8
  106. package/src/_media/download.svg +3 -3
  107. package/src/_media/logo.svg +14 -14
  108. package/src/_test/App.test.js +15 -15
  109. package/src/_utils/reportWebVitals.js +13 -13
  110. package/src/a11y/README.md +177 -0
  111. package/src/a11y/aria-labels.js +339 -0
  112. package/src/a11y/color-contrast.js +535 -0
  113. package/src/a11y/index.js +197 -0
  114. package/src/a11y/keyboard-nav.js +523 -0
  115. package/src/a11y/styles.css +165 -0
  116. package/src/cli/commands/dev.js +214 -0
  117. package/src/cli/commands/docs.js +521 -0
  118. package/src/cli/commands/edit.js +379 -0
  119. package/src/cli/commands/init.js +213 -0
  120. package/src/cli/commands/portal.js +236 -0
  121. package/src/cli/dev-server.js +530 -0
  122. package/src/cli/hmr.js +456 -0
  123. package/src/cli/index.js +180 -0
  124. package/src/cli/utils/config.js +207 -0
  125. package/src/cli/utils/logger.js +241 -0
  126. package/src/config/defaults.ts +122 -0
  127. package/src/config/index.ts +72 -0
  128. package/src/config/loader.ts +478 -0
  129. package/src/config/schema.ts +227 -0
  130. package/src/config/validator.ts +337 -0
  131. package/src/core/appContext.js +34 -27
  132. package/src/core/components/Bar.js +383 -0
  133. package/src/core/components/Bar3D.js +473 -0
  134. package/src/core/components/LargeDatasetChart.js +296 -0
  135. package/src/core/components/Line3D.js +310 -0
  136. package/src/core/components/Scatter.js +392 -188
  137. package/src/core/components/Scatter3D.js +455 -0
  138. package/src/core/components/ScatterBio.js +601 -572
  139. package/src/core/components/Surface3D.js +326 -0
  140. package/src/core/components/ThreeCustom.js +648 -0
  141. package/src/core/components/ThreeScene.js +459 -0
  142. package/src/core/components/VisiumPlot.js +191 -165
  143. package/src/core/components/browser.js +42 -42
  144. package/src/core/components/dotplot.js +413 -413
  145. package/src/core/components/html.js +29 -29
  146. package/src/core/components/list.js +178 -178
  147. package/src/core/components/macaron.js +206 -201
  148. package/src/core/components/markdown.js +56 -56
  149. package/src/core/components/parser.scatterBio.js +582 -579
  150. package/src/core/components/ratio.js +82 -80
  151. package/src/core/components/scatterL.js +206 -173
  152. package/src/core/components/searchbar.js +156 -131
  153. package/src/core/components/selection.js +310 -193
  154. package/src/core/components/timeline.js +236 -281
  155. package/src/core/components/visium.js +114 -97
  156. package/src/core/data-processor.js +413 -0
  157. package/src/core/fetch/condfetch.js +82 -82
  158. package/src/core/fetch/fetch.js +92 -92
  159. package/src/core/fetch/json.js +29 -29
  160. package/src/core/fetch/vfetch.js +42 -42
  161. package/src/core/hmr-client.js +724 -0
  162. package/src/core/liveEditor.js +44 -44
  163. package/src/core/modules/codeEditorWithPreview.js +104 -104
  164. package/src/core/modules/echarts/common.js +20 -20
  165. package/src/core/modules/echarts/gl.js +228 -0
  166. package/src/core/modules/echarts/presetHandler.js +41 -41
  167. package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
  168. package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
  169. package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
  170. package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
  171. package/src/core/modules/echarts.js +70 -71
  172. package/src/core/modules/echartsUtils.js +43 -43
  173. package/src/core/modules/echartswitcher.js +227 -152
  174. package/src/core/modules/replotly/presetHandler.js +24 -24
  175. package/src/core/modules/replotly/presets/minimum.js +18 -18
  176. package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
  177. package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
  178. package/src/core/modules/replotly.js +74 -71
  179. package/src/core/modules/threejs/Camera.js +373 -0
  180. package/src/core/modules/threejs/Lighting.js +459 -0
  181. package/src/core/modules/threejs/Renderer.js +364 -0
  182. package/src/core/modules/threejs/Scene.js +266 -0
  183. package/src/core/modules/threejs/index.js +155 -0
  184. package/src/core/pages/404.js +50 -50
  185. package/src/core/pages/error.js +27 -27
  186. package/src/core/pages/jsonPage.js +62 -62
  187. package/src/core/pages/loading.js +44 -44
  188. package/src/core/parser/echart.data.js +204 -183
  189. package/src/core/parser/echart.features.js +125 -125
  190. package/src/core/parser/echart.general.js +147 -143
  191. package/src/core/parser/echart.hilbert.js +57 -57
  192. package/src/core/parser/echart.parser.js +210 -210
  193. package/src/core/parser/echart.series.js +67 -67
  194. package/src/core/parser/echart.types.js +76 -76
  195. package/src/core/parser/plotly.config.js +10 -10
  196. package/src/core/parser/plotly.data.js +132 -132
  197. package/src/core/parser/plotly.layout.js +9 -9
  198. package/src/core/parser/plotly.violin.js +18 -18
  199. package/src/core/recharts.js +361 -62
  200. package/src/core/router/alias.js +49 -49
  201. package/src/core/router/jsonRouter.js +31 -31
  202. package/src/core/themes/modern.js +32 -32
  203. package/src/core/themes/themeSelector.js +33 -33
  204. package/src/core/visualify.js +213 -47
  205. package/src/core/widgets/circularProgress.js +23 -23
  206. package/src/core/widgets/controller.js +116 -83
  207. package/src/core/widgets/errorBoundary.js +36 -36
  208. package/src/core/widgets/footer.js +185 -177
  209. package/src/core/widgets/header.js +238 -234
  210. package/src/core/widgets/layout/Grid.js +31 -31
  211. package/src/core/widgets/layout.js +36 -36
  212. package/src/core/widgets/mapping.js +56 -42
  213. package/src/core/workers/data-worker.js +349 -0
  214. package/src/core/workers/worker-pool.js +396 -0
  215. package/src/docsify/bundle.js +215 -0
  216. package/src/docsify/markdown.js +271 -0
  217. package/src/docsify/plugin.js +268 -0
  218. package/src/editor/README.md +172 -0
  219. package/src/editor/components/ChartBuilder.jsx +341 -0
  220. package/src/editor/components/ChartTypeSidebar.jsx +91 -0
  221. package/src/editor/components/Editor.jsx +367 -0
  222. package/src/editor/components/Preview.jsx +446 -0
  223. package/src/editor/components/PropertyPanel.jsx +468 -0
  224. package/src/editor/components/StatusBar.jsx +85 -0
  225. package/src/editor/context/EditorContext.js +248 -0
  226. package/src/editor/hooks/useDebounce.js +32 -0
  227. package/src/editor/index.js +315 -0
  228. package/src/editor/styles/editor.css +637 -0
  229. package/src/editor/utils/chartValidator.js +263 -0
  230. package/src/entries/charts3d.js +70 -0
  231. package/src/entries/core.js +78 -0
  232. package/src/entries/docs.js +154 -0
  233. package/src/entries/pages.js +93 -0
  234. package/src/entries/portal.js +204 -0
  235. package/src/entries/shared.js +50 -0
  236. package/src/i18n/formatters.js +455 -0
  237. package/src/i18n/index.js +169 -0
  238. package/src/i18n/locales/ar.json +137 -0
  239. package/src/i18n/locales/de.json +137 -0
  240. package/src/i18n/locales/en.json +137 -0
  241. package/src/i18n/locales/es.json +137 -0
  242. package/src/i18n/locales/he.json +137 -0
  243. package/src/i18n/locales/zh.json +137 -0
  244. package/src/i18n/rtl.css +183 -0
  245. package/src/index.js +82 -62
  246. package/src/loader.js +103 -0
  247. package/src/setupTests.js +5 -5
  248. package/tsconfig.json +51 -0
  249. package/types/charts.d.ts +569 -0
  250. package/types/components.d.ts +441 -0
  251. package/types/config.d.ts +199 -0
  252. package/types/index.d.ts +353 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Accessibility (a11y) Module for Visualify.js
3
+ * Provides comprehensive accessibility support for charts
4
+ * @module a11y
5
+ */
6
+
7
+ export * from './aria-labels';
8
+ export * from './keyboard-nav';
9
+ export * from './color-contrast';
10
+
11
+ // CSS class for screen reader only content
12
+ export const SR_ONLY_CLASS = 'sr-only';
13
+
14
+ // Default ARIA attributes for chart containers
15
+ export const DEFAULT_CHART_ARIA = {
16
+ role: 'img',
17
+ tabIndex: 0,
18
+ };
19
+
20
+ // Focus styles for keyboard navigation
21
+ export const FOCUS_STYLES = {
22
+ outline: '3px solid #4A90E2',
23
+ outlineOffset: '2px',
24
+ };
25
+
26
+ /**
27
+ * CSS styles for screen reader only content
28
+ * Include this in your global CSS or component styles
29
+ */
30
+ export const srOnlyStyles = `
31
+ .sr-only {
32
+ position: absolute;
33
+ width: 1px;
34
+ height: 1px;
35
+ padding: 0;
36
+ margin: -1px;
37
+ overflow: hidden;
38
+ clip: rect(0, 0, 0, 0);
39
+ white-space: nowrap;
40
+ border: 0;
41
+ }
42
+
43
+ /* Focus indicators for keyboard navigation */
44
+ .visualify-chart:focus,
45
+ .visualify-chart:focus-visible {
46
+ outline: 3px solid #4A90E2;
47
+ outline-offset: 2px;
48
+ }
49
+
50
+ .visualify-chart:focus:not(:focus-visible) {
51
+ outline: none;
52
+ }
53
+
54
+ /* High contrast mode support */
55
+ @media (prefers-contrast: high) {
56
+ .visualify-chart {
57
+ border: 2px solid currentColor;
58
+ }
59
+ }
60
+
61
+ /* Reduced motion support */
62
+ @media (prefers-reduced-motion: reduce) {
63
+ .visualify-chart,
64
+ .visualify-chart * {
65
+ animation-duration: 0.01ms !important;
66
+ animation-iteration-count: 1 !important;
67
+ transition-duration: 0.01ms !important;
68
+ }
69
+ }
70
+ `;
71
+
72
+ /**
73
+ * Applies screen reader only styles to a style element
74
+ * Call this once in your application initialization
75
+ */
76
+ export const applyA11yStyles = () => {
77
+ if (typeof document === 'undefined') return;
78
+
79
+ const styleId = 'visualify-a11y-styles';
80
+ if (document.getElementById(styleId)) return;
81
+
82
+ const styleEl = document.createElement('style');
83
+ styleEl.id = styleId;
84
+ styleEl.textContent = srOnlyStyles;
85
+ document.head.appendChild(styleEl);
86
+ };
87
+
88
+ /**
89
+ * Creates a hidden data table for screen readers
90
+ * @param {Object} tableData - Table data from generateDataTable
91
+ * @param {string} id - Unique ID for the table
92
+ * @returns {JSX.Element} Hidden table element
93
+ */
94
+ export const createScreenReaderTable = (tableData, id) => {
95
+ if (!tableData || !tableData.rows || tableData.rows.length === 0) {
96
+ return null;
97
+ }
98
+
99
+ return {
100
+ type: 'table',
101
+ props: {
102
+ id,
103
+ className: SR_ONLY_CLASS,
104
+ 'aria-label': `${tableData.caption} - Data table`,
105
+ },
106
+ children: [
107
+ {
108
+ type: 'caption',
109
+ props: {},
110
+ children: tableData.caption,
111
+ },
112
+ {
113
+ type: 'thead',
114
+ props: {},
115
+ children: {
116
+ type: 'tr',
117
+ props: {},
118
+ children: tableData.headers.map((header, i) => ({
119
+ type: 'th',
120
+ props: { key: i, scope: 'col' },
121
+ children: header,
122
+ })),
123
+ },
124
+ },
125
+ {
126
+ type: 'tbody',
127
+ props: {},
128
+ children: tableData.rows.map((row) => ({
129
+ type: 'tr',
130
+ props: { key: row.id },
131
+ children: row.cells.map((cell, i) => ({
132
+ type: 'td',
133
+ props: { key: i },
134
+ children: cell,
135
+ })),
136
+ })),
137
+ },
138
+ ],
139
+ };
140
+ };
141
+
142
+ /**
143
+ * Complete accessibility configuration for charts
144
+ */
145
+ export const defaultA11yConfig = {
146
+ // ARIA settings
147
+ aria: {
148
+ enabled: true,
149
+ role: 'img',
150
+ describeData: true,
151
+ },
152
+
153
+ // Keyboard navigation
154
+ keyboard: {
155
+ enabled: true,
156
+ arrowNavigation: true,
157
+ enterActivation: true,
158
+ wrapAround: true,
159
+ announceChanges: true,
160
+ },
161
+
162
+ // Screen reader
163
+ screenReader: {
164
+ enabled: true,
165
+ dataTable: true,
166
+ liveRegion: true,
167
+ },
168
+
169
+ // Color contrast
170
+ contrast: {
171
+ enabled: true,
172
+ autoFix: true,
173
+ minRatio: 4.5,
174
+ patterns: false,
175
+ },
176
+
177
+ // Focus management
178
+ focus: {
179
+ visible: true,
180
+ trapInModal: true,
181
+ },
182
+
183
+ // Motion
184
+ motion: {
185
+ respectPrefersReducedMotion: true,
186
+ },
187
+ };
188
+
189
+ export default {
190
+ SR_ONLY_CLASS,
191
+ DEFAULT_CHART_ARIA,
192
+ FOCUS_STYLES,
193
+ srOnlyStyles,
194
+ applyA11yStyles,
195
+ createScreenReaderTable,
196
+ defaultA11yConfig,
197
+ };
@@ -0,0 +1,523 @@
1
+ /**
2
+ * Keyboard Navigation Handlers for Visualify.js
3
+ * Provides keyboard accessibility for chart interactions
4
+ * @module a11y/keyboard-nav
5
+ */
6
+
7
+ import { useState, useRef, useCallback } from 'react';
8
+ import { announceToScreenReader, generateDataPointLabel } from './aria-labels';
9
+
10
+ /**
11
+ * Default keyboard navigation configuration
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ enableArrowNavigation: true,
15
+ enableTabNavigation: true,
16
+ enableEnterActivation: true,
17
+ wrapAround: true,
18
+ announceChanges: true,
19
+ };
20
+
21
+ /**
22
+ * Creates a keyboard navigation handler for charts
23
+ * @param {Object} options - Configuration options
24
+ * @param {Array} options.data - Chart data array
25
+ * @param {Function} options.onFocusChange - Callback when focus changes
26
+ * @param {Function} options.onActivate - Callback when item is activated
27
+ * @param {Function} options.onEscape - Callback when escape is pressed
28
+ * @param {Object} options.config - Chart configuration
29
+ * @returns {Object} Keyboard navigation state and handlers
30
+ */
31
+ export const useKeyboardNavigation = (options = {}) => {
32
+ const {
33
+ data = [],
34
+ onFocusChange,
35
+ onActivate,
36
+ onEscape,
37
+ config = {},
38
+ navConfig = {},
39
+ } = options;
40
+
41
+ const navigationConfig = { ...DEFAULT_CONFIG, ...navConfig };
42
+ const [focusedIndex, setFocusedIndex] = useState(-1);
43
+ const containerRef = useRef(null);
44
+
45
+ /**
46
+ * Moves focus to a specific data point
47
+ * @param {number} index - Index of data point to focus
48
+ */
49
+ const focusDataPoint = (index) => {
50
+ if (index < 0 || index >= data.length) {
51
+ if (navigationConfig.wrapAround) {
52
+ // Wrap around
53
+ if (index < 0) {
54
+ index = data.length - 1;
55
+ } else {
56
+ index = 0;
57
+ }
58
+ } else {
59
+ return;
60
+ }
61
+ }
62
+
63
+ setFocusedIndex(index);
64
+
65
+ if (navigationConfig.announceChanges && index >= 0) {
66
+ const label = generateDataPointLabel(data[index], index, config);
67
+ announceToScreenReader(label, 'polite');
68
+ }
69
+
70
+ if (onFocusChange) {
71
+ onFocusChange(index, data[index]);
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Activates the currently focused data point
77
+ */
78
+ const activateDataPoint = () => {
79
+ if (focusedIndex >= 0 && focusedIndex < data.length) {
80
+ if (navigationConfig.announceChanges) {
81
+ announceToScreenReader(`Activated ${generateDataPointLabel(data[focusedIndex], focusedIndex, config)}`, 'polite');
82
+ }
83
+
84
+ if (onActivate) {
85
+ onActivate(focusedIndex, data[focusedIndex]);
86
+ }
87
+ }
88
+ };
89
+
90
+ /**
91
+ * Clears the current focus
92
+ */
93
+ const clearFocus = () => {
94
+ setFocusedIndex(-1);
95
+ if (onEscape) {
96
+ onEscape();
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Handles keyboard events for chart navigation
102
+ * @param {KeyboardEvent} event - Keyboard event
103
+ */
104
+ const handleKeyDown = (event) => {
105
+ if (!navigationConfig.enableArrowNavigation && !navigationConfig.enableEnterActivation) {
106
+ return;
107
+ }
108
+
109
+ switch (event.key) {
110
+ case 'ArrowRight':
111
+ case 'ArrowDown':
112
+ event.preventDefault();
113
+ if (navigationConfig.enableArrowNavigation) {
114
+ focusDataPoint(focusedIndex + 1);
115
+ }
116
+ break;
117
+
118
+ case 'ArrowLeft':
119
+ case 'ArrowUp':
120
+ event.preventDefault();
121
+ if (navigationConfig.enableArrowNavigation) {
122
+ focusDataPoint(focusedIndex - 1);
123
+ }
124
+ break;
125
+
126
+ case 'Enter':
127
+ case ' ':
128
+ event.preventDefault();
129
+ if (navigationConfig.enableEnterActivation) {
130
+ activateDataPoint();
131
+ }
132
+ break;
133
+
134
+ case 'Escape':
135
+ event.preventDefault();
136
+ clearFocus();
137
+ // Move focus back to container
138
+ if (containerRef.current) {
139
+ containerRef.current.focus();
140
+ }
141
+ break;
142
+
143
+ case 'Home':
144
+ event.preventDefault();
145
+ if (navigationConfig.enableArrowNavigation) {
146
+ focusDataPoint(0);
147
+ }
148
+ break;
149
+
150
+ case 'End':
151
+ event.preventDefault();
152
+ if (navigationConfig.enableArrowNavigation) {
153
+ focusDataPoint(data.length - 1);
154
+ }
155
+ break;
156
+
157
+ case 'PageUp':
158
+ event.preventDefault();
159
+ if (navigationConfig.enableArrowNavigation) {
160
+ focusDataPoint(Math.max(0, focusedIndex - 10));
161
+ }
162
+ break;
163
+
164
+ case 'PageDown':
165
+ event.preventDefault();
166
+ if (navigationConfig.enableArrowNavigation) {
167
+ focusDataPoint(Math.min(data.length - 1, focusedIndex + 10));
168
+ }
169
+ break;
170
+
171
+ default:
172
+ break;
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Handles focus entering the chart
178
+ */
179
+ const handleFocus = () => {
180
+ if (focusedIndex === -1 && data.length > 0) {
181
+ // Auto-focus first item when tabbing into chart
182
+ focusDataPoint(0);
183
+ }
184
+ };
185
+
186
+ /**
187
+ * Handles blur leaving the chart
188
+ */
189
+ const handleBlur = (event) => {
190
+ // Check if focus is moving outside the chart container
191
+ if (containerRef.current && !containerRef.current.contains(event.relatedTarget)) {
192
+ clearFocus();
193
+ }
194
+ };
195
+
196
+ return {
197
+ focusedIndex,
198
+ setFocusedIndex,
199
+ containerRef,
200
+ handleKeyDown,
201
+ handleFocus,
202
+ handleBlur,
203
+ focusDataPoint,
204
+ activateDataPoint,
205
+ clearFocus,
206
+ };
207
+ };
208
+
209
+ /**
210
+ * React hook for chart keyboard navigation
211
+ * @param {Object} options - Configuration options
212
+ * @returns {Object} Navigation props and state
213
+ */
214
+ export const useChartKeyboardNav = (options = {}) => {
215
+ const [focusedIndex, setFocusedIndex] = useState(-1);
216
+ const containerRef = useRef(null);
217
+
218
+ const {
219
+ data = [],
220
+ onFocusChange,
221
+ onActivate,
222
+ onEscape,
223
+ config = {},
224
+ navConfig = {},
225
+ } = options;
226
+
227
+ const navigationConfig = { ...DEFAULT_CONFIG, ...navConfig };
228
+
229
+ const focusDataPoint = useCallback((index) => {
230
+ if (index < 0 || index >= data.length) {
231
+ if (navigationConfig.wrapAround) {
232
+ if (index < 0) {
233
+ index = data.length - 1;
234
+ } else {
235
+ index = 0;
236
+ }
237
+ } else {
238
+ return;
239
+ }
240
+ }
241
+
242
+ setFocusedIndex(index);
243
+
244
+ if (navigationConfig.announceChanges && index >= 0) {
245
+ const label = generateDataPointLabel(data[index], index, config);
246
+ announceToScreenReader(label, 'polite');
247
+ }
248
+
249
+ if (onFocusChange) {
250
+ onFocusChange(index, data[index]);
251
+ }
252
+ }, [data, config, navigationConfig, onFocusChange]);
253
+
254
+ const activateDataPoint = useCallback(() => {
255
+ if (focusedIndex >= 0 && focusedIndex < data.length) {
256
+ if (navigationConfig.announceChanges) {
257
+ announceToScreenReader(
258
+ `Activated ${generateDataPointLabel(data[focusedIndex], focusedIndex, config)}`,
259
+ 'polite'
260
+ );
261
+ }
262
+
263
+ if (onActivate) {
264
+ onActivate(focusedIndex, data[focusedIndex]);
265
+ }
266
+ }
267
+ }, [focusedIndex, data, config, navigationConfig, onActivate]);
268
+
269
+ const clearFocus = useCallback(() => {
270
+ setFocusedIndex(-1);
271
+ if (onEscape) {
272
+ onEscape();
273
+ }
274
+ }, [onEscape]);
275
+
276
+ const handleKeyDown = useCallback((event) => {
277
+ if (!navigationConfig.enableArrowNavigation && !navigationConfig.enableEnterActivation) {
278
+ return;
279
+ }
280
+
281
+ switch (event.key) {
282
+ case 'ArrowRight':
283
+ case 'ArrowDown':
284
+ event.preventDefault();
285
+ if (navigationConfig.enableArrowNavigation) {
286
+ focusDataPoint(focusedIndex + 1);
287
+ }
288
+ break;
289
+
290
+ case 'ArrowLeft':
291
+ case 'ArrowUp':
292
+ event.preventDefault();
293
+ if (navigationConfig.enableArrowNavigation) {
294
+ focusDataPoint(focusedIndex - 1);
295
+ }
296
+ break;
297
+
298
+ case 'Enter':
299
+ case ' ':
300
+ event.preventDefault();
301
+ if (navigationConfig.enableEnterActivation) {
302
+ activateDataPoint();
303
+ }
304
+ break;
305
+
306
+ case 'Escape':
307
+ event.preventDefault();
308
+ clearFocus();
309
+ if (containerRef.current) {
310
+ containerRef.current.focus();
311
+ }
312
+ break;
313
+
314
+ case 'Home':
315
+ event.preventDefault();
316
+ if (navigationConfig.enableArrowNavigation) {
317
+ focusDataPoint(0);
318
+ }
319
+ break;
320
+
321
+ case 'End':
322
+ event.preventDefault();
323
+ if (navigationConfig.enableArrowNavigation) {
324
+ focusDataPoint(data.length - 1);
325
+ }
326
+ break;
327
+
328
+ case 'PageUp':
329
+ event.preventDefault();
330
+ if (navigationConfig.enableArrowNavigation) {
331
+ focusDataPoint(Math.max(0, focusedIndex - 10));
332
+ }
333
+ break;
334
+
335
+ case 'PageDown':
336
+ event.preventDefault();
337
+ if (navigationConfig.enableArrowNavigation) {
338
+ focusDataPoint(Math.min(data.length - 1, focusedIndex + 10));
339
+ }
340
+ break;
341
+
342
+ default:
343
+ break;
344
+ }
345
+ }, [focusedIndex, data.length, navigationConfig, focusDataPoint, activateDataPoint, clearFocus]);
346
+
347
+ const handleFocus = useCallback(() => {
348
+ if (focusedIndex === -1 && data.length > 0) {
349
+ focusDataPoint(0);
350
+ }
351
+ }, [focusedIndex, data.length, focusDataPoint]);
352
+
353
+ const handleBlur = useCallback((event) => {
354
+ if (containerRef.current && !containerRef.current.contains(event.relatedTarget)) {
355
+ clearFocus();
356
+ }
357
+ }, [clearFocus]);
358
+
359
+ return {
360
+ focusedIndex,
361
+ containerRef,
362
+ handleKeyDown,
363
+ handleFocus,
364
+ handleBlur,
365
+ focusDataPoint,
366
+ activateDataPoint,
367
+ clearFocus,
368
+ // Props to spread on container
369
+ containerProps: {
370
+ ref: containerRef,
371
+ tabIndex: 0,
372
+ onKeyDown: handleKeyDown,
373
+ onFocus: handleFocus,
374
+ onBlur: handleBlur,
375
+ role: 'application',
376
+ 'aria-label': config.title || 'Interactive chart',
377
+ },
378
+ };
379
+ };
380
+
381
+ /**
382
+ * Creates keyboard handlers for the echarts switcher component
383
+ * @param {Object} chartRef - Reference to the chart instance
384
+ * @param {Object} config - Chart configuration
385
+ * @returns {Object} Keyboard event handlers
386
+ */
387
+ export const createEChartsSwitcherKeyboardHandlers = (chartRef, config = {}) => {
388
+ const handleKeyDown = (event) => {
389
+ switch (event.key) {
390
+ case 'Enter':
391
+ case ' ':
392
+ event.preventDefault();
393
+ // Trigger chart click event
394
+ if (chartRef?.current) {
395
+ const chart = chartRef.current.getEchartsInstance?.();
396
+ if (chart) {
397
+ // Dispatch a click event on the chart
398
+ chart.dispatchAction({
399
+ type: 'highlight',
400
+ seriesIndex: 0,
401
+ dataIndex: 0,
402
+ });
403
+ }
404
+ }
405
+ break;
406
+
407
+ case 'r':
408
+ case 'R':
409
+ // Refresh chart data (Ctrl+R or Cmd+R is handled by browser)
410
+ if (event.ctrlKey || event.metaKey) {
411
+ return;
412
+ }
413
+ event.preventDefault();
414
+ if (chartRef?.current) {
415
+ const chart = chartRef.current.getEchartsInstance?.();
416
+ if (chart) {
417
+ chart.resize();
418
+ announceToScreenReader('Chart refreshed', 'polite');
419
+ }
420
+ }
421
+ break;
422
+
423
+ case 'Escape':
424
+ event.preventDefault();
425
+ // Clear any selections
426
+ if (chartRef?.current) {
427
+ const chart = chartRef.current.getEchartsInstance?.();
428
+ if (chart) {
429
+ chart.dispatchAction({
430
+ type: 'downplay',
431
+ });
432
+ }
433
+ }
434
+ break;
435
+
436
+ default:
437
+ break;
438
+ }
439
+ };
440
+
441
+ return {
442
+ handleKeyDown,
443
+ };
444
+ };
445
+
446
+ /**
447
+ * Traps focus within a modal or overlay element
448
+ * @param {HTMLElement} element - Container element
449
+ * @returns {Function} Cleanup function
450
+ */
451
+ export const trapFocus = (element) => {
452
+ if (!element) return () => {};
453
+
454
+ const focusableElements = element.querySelectorAll(
455
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
456
+ );
457
+
458
+ const firstFocusable = focusableElements[0];
459
+ const lastFocusable = focusableElements[focusableElements.length - 1];
460
+
461
+ const handleTabKey = (e) => {
462
+ if (e.key !== 'Tab') return;
463
+
464
+ if (e.shiftKey) {
465
+ if (document.activeElement === firstFocusable) {
466
+ lastFocusable.focus();
467
+ e.preventDefault();
468
+ }
469
+ } else {
470
+ if (document.activeElement === lastFocusable) {
471
+ firstFocusable.focus();
472
+ e.preventDefault();
473
+ }
474
+ }
475
+ };
476
+
477
+ element.addEventListener('keydown', handleTabKey);
478
+
479
+ // Focus first element
480
+ if (firstFocusable) {
481
+ firstFocusable.focus();
482
+ }
483
+
484
+ return () => {
485
+ element.removeEventListener('keydown', handleTabKey);
486
+ };
487
+ };
488
+
489
+ /**
490
+ * Checks if the user prefers reduced motion
491
+ * @returns {boolean} True if reduced motion is preferred
492
+ */
493
+ export const prefersReducedMotion = () => {
494
+ if (typeof window === 'undefined') return false;
495
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
496
+ };
497
+
498
+ /**
499
+ * Gets appropriate animation settings based on user preferences
500
+ * @param {Object} animationConfig - Desired animation configuration
501
+ * @returns {Object} Animation config respecting user preferences
502
+ */
503
+ export const getAccessibleAnimation = (animationConfig = {}) => {
504
+ if (prefersReducedMotion()) {
505
+ return {
506
+ duration: 0,
507
+ delay: 0,
508
+ easing: 'linear',
509
+ ...animationConfig.reducedMotion,
510
+ };
511
+ }
512
+
513
+ return animationConfig;
514
+ };
515
+
516
+ export default {
517
+ useKeyboardNavigation,
518
+ useChartKeyboardNav,
519
+ createEChartsSwitcherKeyboardHandlers,
520
+ trapFocus,
521
+ prefersReducedMotion,
522
+ getAccessibleAnimation,
523
+ };