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.
- package/.claude/mem/TIMELINE.md +36 -0
- package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-external-script-solution.md +24 -0
- package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-session-summary.md +43 -0
- package/.claude/mem/notes/2026-02-11-cli-fix-editor-command-alias.md +26 -0
- package/.claude/mem/notes/2026-02-11-phase-3-developer-experience-completed.md +51 -0
- package/.claude/mem/notes/2026-02-11-phase-4-web-workers-implementation-complete.md +59 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-2-3d-visualization-complete.md +50 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-2-committed-ready-for-phase-3.md +33 -0
- package/.claude/mem/notes/2026-02-11-visualify-phase-3-complete-developer-experience.md +52 -0
- package/.claude/mem/notes/2026-02-11-visualify-repository-cleanup-complete.md +28 -0
- package/.claude/mem/notes/2026-02-18-codebase-cleanup-docsify-plugin-documentation.md +37 -0
- package/.claude/mem/notes/2026-02-19-css-grid-layout-fix-displaycontents-on-vcontroller.md +18 -0
- package/.claude/mem/notes/2026-02-19-docsify-plugin-fixes-latex-and-visualify-code-bloc.md +26 -0
- package/.claude/mem/notes/2026-02-19-page-mode-docs-update-decisions.md +23 -0
- package/.claude/mem/notes/2026-02-19-react-context-infinite-re-render-loop-fix-pattern.md +31 -0
- package/.claude/mem/notes/2026-02-19-version-300-bump-and-build-fixes.md +32 -0
- package/.claude/mem/notes/2026-02-19-visualify-build-deployment-architecture-bug-fixes.md +25 -0
- package/.claude/mem/notes/2026-02-19-visualify-dist-iife-self-contained-build-config.md +30 -0
- package/.claude/mem/notes/2026-02-19-visualify-infinite-loop-i18n-fixes.md +31 -0
- package/.claude/mem/notes/2026-02-19-visualify-v3-bundle-splitting-docs-restructuring.md +32 -0
- package/.claude/mem/notes/2026-02-20-bundle-externalization-final-architecture.md +29 -0
- package/.claude/mem/notes/2026-02-20-chromium-page-fix-unstable-keys-and-double-event-b.md +27 -0
- package/.claude/mem/notes/2026-02-20-console-cleanup-bundle-optimization-commit.md +20 -0
- package/.claude/mem/notes/2026-02-20-dotbio-dot-plot-fix-useeffect-dependency.md +21 -0
- package/.claude/mem/notes/2026-02-20-public-folder-cleanup-and-readme-rewrite.md +25 -0
- package/.claude/mem/notes/2026-02-20-v300-release-and-beta-channel-strategy.md +29 -0
- package/.claude/mem/notes/2026-02-20-visium-background-image-unknown-legend-fix.md +19 -0
- package/.claude/mem/notes/2026-02-20-visualify-cdn-loader-bundle-externalization.md +34 -0
- package/.claude/mem/sessions/session-2026-02-20-031524.md +54 -0
- package/.claude/settings.local.json +21 -0
- package/.github/workflows/static.yml.bak +51 -51
- package/.sisyphus/boulder.json +65 -0
- package/.sisyphus/plans/phase-4-advanced-optimizations.md +217 -0
- package/LICENSE +674 -674
- package/README.md +94 -59
- package/config-overrides.js +31 -31
- package/dist/stats.html +4949 -0
- package/dist/visualify-3d.esm.js +1 -0
- package/dist/visualify-3d.js +1 -0
- package/dist/visualify-core.esm.js +1 -0
- package/dist/visualify-core.js +1 -0
- package/dist/visualify-docs.esm.js +1 -0
- package/dist/visualify-docs.js +1 -0
- package/dist/visualify-loader.js +1 -0
- package/dist/visualify-pages.esm.js +1 -0
- package/dist/visualify-pages.js +1 -0
- package/dist/visualify-portal.esm.js +1 -0
- package/dist/visualify-portal.js +1 -0
- package/dist/visualify-shared.js +26571 -0
- package/dist/visualify.js +1 -188
- package/docs/CHANGELOG.md +148 -0
- package/docs/cli/commands.md +513 -0
- package/docs/configuration/visualify-json.md +474 -0
- package/docs/docs/3d-visualization.md +374 -0
- package/docs/docs/CLI.md +303 -34
- package/docs/docs/README.md +65 -65
- package/docs/docs/Rechart/bar.md +190 -190
- package/docs/docs/Rechart/funnel.md +241 -193
- package/docs/docs/Rechart/line.md +355 -355
- package/docs/docs/Rechart/pie.md +225 -225
- package/docs/docs/Rechart/radar.md +253 -253
- package/docs/docs/Rechart/scatter.md +298 -0
- package/docs/docs/_404.md +51 -51
- package/docs/docs/_coverpage.md +11 -11
- package/docs/docs/_sidebar.md +54 -43
- package/docs/docs/components/dotBio.md +87 -34
- package/docs/docs/components/echart.md +171 -82
- package/docs/docs/components/html.md +61 -34
- package/docs/docs/components/macaron.md +156 -145
- package/docs/docs/components/markdown.md +42 -0
- package/docs/docs/components/more.md +183 -142
- package/docs/docs/components/plotly.md +132 -62
- package/docs/docs/components/scatterL.md +171 -70
- package/docs/docs/components/visium.md +112 -57
- package/docs/docs/configuration.md +121 -123
- package/docs/docs/deploy.md +31 -31
- package/docs/docs/docsify-plugin.md +655 -0
- package/docs/docs/hmr.md +165 -0
- package/docs/docs/i18n.md +332 -0
- package/docs/docs/log.md +30 -1
- package/docs/docs/more-pages.md +23 -23
- package/docs/docs/quickstart.md +148 -119
- package/docs/docs/rechart-attributes.md +74 -74
- package/docs/docs/rechart-basic-usage.md +160 -162
- package/docs/docs/theme.md +5 -5
- package/docs/docs/typescript.md +306 -0
- package/docs/docs/visual-editor.md +359 -0
- package/docs/index.html +85 -71
- package/docs/manifest.json +23 -23
- package/docs/migration/v3-migration.md +392 -0
- package/docs/static/css/fluff-stuff.css +169 -169
- package/docs/static/css/font-awesome.min.css +4 -4
- package/docs/static/css/visualify.css +6 -25
- package/docs/static/js/3d-viz-examples.js +181 -0
- package/docs/static/js/configuration.js +630 -448
- package/docs/static/js/visualify.js +1 -188
- package/package.json +106 -84
- package/rollup.config.mjs +766 -76
- package/src/_css/404.css +115 -115
- package/src/_css/App.css +37 -37
- package/src/_css/autoSuggestion.css +26 -26
- package/src/_css/circular-progress.css +32 -32
- package/src/_css/index.css +36 -36
- package/src/_css/modern.css +350 -25
- package/src/_media/corner.svg +8 -8
- package/src/_media/download.svg +3 -3
- package/src/_media/logo.svg +14 -14
- package/src/_test/App.test.js +15 -15
- package/src/_utils/reportWebVitals.js +13 -13
- package/src/a11y/README.md +177 -0
- package/src/a11y/aria-labels.js +339 -0
- package/src/a11y/color-contrast.js +535 -0
- package/src/a11y/index.js +197 -0
- package/src/a11y/keyboard-nav.js +523 -0
- package/src/a11y/styles.css +165 -0
- package/src/cli/commands/dev.js +214 -0
- package/src/cli/commands/docs.js +521 -0
- package/src/cli/commands/edit.js +379 -0
- package/src/cli/commands/init.js +213 -0
- package/src/cli/commands/portal.js +236 -0
- package/src/cli/dev-server.js +530 -0
- package/src/cli/hmr.js +456 -0
- package/src/cli/index.js +180 -0
- package/src/cli/utils/config.js +207 -0
- package/src/cli/utils/logger.js +241 -0
- package/src/config/defaults.ts +122 -0
- package/src/config/index.ts +72 -0
- package/src/config/loader.ts +478 -0
- package/src/config/schema.ts +227 -0
- package/src/config/validator.ts +337 -0
- package/src/core/appContext.js +34 -27
- package/src/core/components/Bar.js +383 -0
- package/src/core/components/Bar3D.js +473 -0
- package/src/core/components/LargeDatasetChart.js +296 -0
- package/src/core/components/Line3D.js +310 -0
- package/src/core/components/Scatter.js +392 -188
- package/src/core/components/Scatter3D.js +455 -0
- package/src/core/components/ScatterBio.js +601 -572
- package/src/core/components/Surface3D.js +326 -0
- package/src/core/components/ThreeCustom.js +648 -0
- package/src/core/components/ThreeScene.js +459 -0
- package/src/core/components/VisiumPlot.js +191 -165
- package/src/core/components/browser.js +42 -42
- package/src/core/components/dotplot.js +413 -413
- package/src/core/components/html.js +29 -29
- package/src/core/components/list.js +178 -178
- package/src/core/components/macaron.js +206 -201
- package/src/core/components/markdown.js +56 -56
- package/src/core/components/parser.scatterBio.js +582 -579
- package/src/core/components/ratio.js +82 -80
- package/src/core/components/scatterL.js +206 -173
- package/src/core/components/searchbar.js +156 -131
- package/src/core/components/selection.js +310 -193
- package/src/core/components/timeline.js +236 -281
- package/src/core/components/visium.js +114 -97
- package/src/core/data-processor.js +413 -0
- package/src/core/fetch/condfetch.js +82 -82
- package/src/core/fetch/fetch.js +92 -92
- package/src/core/fetch/json.js +29 -29
- package/src/core/fetch/vfetch.js +42 -42
- package/src/core/hmr-client.js +724 -0
- package/src/core/liveEditor.js +44 -44
- package/src/core/modules/codeEditorWithPreview.js +104 -104
- package/src/core/modules/echarts/common.js +20 -20
- package/src/core/modules/echarts/gl.js +228 -0
- package/src/core/modules/echarts/presetHandler.js +41 -41
- package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
- package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
- package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
- package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
- package/src/core/modules/echarts.js +70 -71
- package/src/core/modules/echartsUtils.js +43 -43
- package/src/core/modules/echartswitcher.js +227 -152
- package/src/core/modules/replotly/presetHandler.js +24 -24
- package/src/core/modules/replotly/presets/minimum.js +18 -18
- package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
- package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
- package/src/core/modules/replotly.js +74 -71
- package/src/core/modules/threejs/Camera.js +373 -0
- package/src/core/modules/threejs/Lighting.js +459 -0
- package/src/core/modules/threejs/Renderer.js +364 -0
- package/src/core/modules/threejs/Scene.js +266 -0
- package/src/core/modules/threejs/index.js +155 -0
- package/src/core/pages/404.js +50 -50
- package/src/core/pages/error.js +27 -27
- package/src/core/pages/jsonPage.js +62 -62
- package/src/core/pages/loading.js +44 -44
- package/src/core/parser/echart.data.js +204 -183
- package/src/core/parser/echart.features.js +125 -125
- package/src/core/parser/echart.general.js +147 -143
- package/src/core/parser/echart.hilbert.js +57 -57
- package/src/core/parser/echart.parser.js +210 -210
- package/src/core/parser/echart.series.js +67 -67
- package/src/core/parser/echart.types.js +76 -76
- package/src/core/parser/plotly.config.js +10 -10
- package/src/core/parser/plotly.data.js +132 -132
- package/src/core/parser/plotly.layout.js +9 -9
- package/src/core/parser/plotly.violin.js +18 -18
- package/src/core/recharts.js +361 -62
- package/src/core/router/alias.js +49 -49
- package/src/core/router/jsonRouter.js +31 -31
- package/src/core/themes/modern.js +32 -32
- package/src/core/themes/themeSelector.js +33 -33
- package/src/core/visualify.js +213 -47
- package/src/core/widgets/circularProgress.js +23 -23
- package/src/core/widgets/controller.js +116 -83
- package/src/core/widgets/errorBoundary.js +36 -36
- package/src/core/widgets/footer.js +185 -177
- package/src/core/widgets/header.js +238 -234
- package/src/core/widgets/layout/Grid.js +31 -31
- package/src/core/widgets/layout.js +36 -36
- package/src/core/widgets/mapping.js +56 -42
- package/src/core/workers/data-worker.js +349 -0
- package/src/core/workers/worker-pool.js +396 -0
- package/src/docsify/bundle.js +215 -0
- package/src/docsify/markdown.js +271 -0
- package/src/docsify/plugin.js +268 -0
- package/src/editor/README.md +172 -0
- package/src/editor/components/ChartBuilder.jsx +341 -0
- package/src/editor/components/ChartTypeSidebar.jsx +91 -0
- package/src/editor/components/Editor.jsx +367 -0
- package/src/editor/components/Preview.jsx +446 -0
- package/src/editor/components/PropertyPanel.jsx +468 -0
- package/src/editor/components/StatusBar.jsx +85 -0
- package/src/editor/context/EditorContext.js +248 -0
- package/src/editor/hooks/useDebounce.js +32 -0
- package/src/editor/index.js +315 -0
- package/src/editor/styles/editor.css +637 -0
- package/src/editor/utils/chartValidator.js +263 -0
- package/src/entries/charts3d.js +70 -0
- package/src/entries/core.js +78 -0
- package/src/entries/docs.js +154 -0
- package/src/entries/pages.js +93 -0
- package/src/entries/portal.js +204 -0
- package/src/entries/shared.js +50 -0
- package/src/i18n/formatters.js +455 -0
- package/src/i18n/index.js +169 -0
- package/src/i18n/locales/ar.json +137 -0
- package/src/i18n/locales/de.json +137 -0
- package/src/i18n/locales/en.json +137 -0
- package/src/i18n/locales/es.json +137 -0
- package/src/i18n/locales/he.json +137 -0
- package/src/i18n/locales/zh.json +137 -0
- package/src/i18n/rtl.css +183 -0
- package/src/index.js +82 -62
- package/src/loader.js +103 -0
- package/src/setupTests.js +5 -5
- package/tsconfig.json +51 -0
- package/types/charts.d.ts +569 -0
- package/types/components.d.ts +441 -0
- package/types/config.d.ts +199 -0
- 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
|
+
};
|