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
package/src/_test/App.test.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @Author : Lihao leolihao@arizona.edu
|
|
3
|
-
* @Date : 2023-11-29 15:35:28
|
|
4
|
-
* @FilePath : /visualifyjs/src/_test/App.test.js
|
|
5
|
-
* @Description :
|
|
6
|
-
* Copyright (c) 2023 by Lihao (leolihao@arizona.edu), All Rights Reserved.
|
|
7
|
-
*/
|
|
8
|
-
import { render, screen } from '@testing-library/react';
|
|
9
|
-
import App from '../core/App';
|
|
10
|
-
|
|
11
|
-
test('renders learn react link', () => {
|
|
12
|
-
render(<App />);
|
|
13
|
-
const linkElement = screen.getByText(/learn react/i);
|
|
14
|
-
expect(linkElement).toBeInTheDocument();
|
|
15
|
-
});
|
|
1
|
+
/*
|
|
2
|
+
* @Author : Lihao leolihao@arizona.edu
|
|
3
|
+
* @Date : 2023-11-29 15:35:28
|
|
4
|
+
* @FilePath : /visualifyjs/src/_test/App.test.js
|
|
5
|
+
* @Description :
|
|
6
|
+
* Copyright (c) 2023 by Lihao (leolihao@arizona.edu), All Rights Reserved.
|
|
7
|
+
*/
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import App from '../core/App';
|
|
10
|
+
|
|
11
|
+
test('renders learn react link', () => {
|
|
12
|
+
render(<App />);
|
|
13
|
+
const linkElement = screen.getByText(/learn react/i);
|
|
14
|
+
expect(linkElement).toBeInTheDocument();
|
|
15
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const reportWebVitals = onPerfEntry => {
|
|
2
|
-
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
3
|
-
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
4
|
-
getCLS(onPerfEntry);
|
|
5
|
-
getFID(onPerfEntry);
|
|
6
|
-
getFCP(onPerfEntry);
|
|
7
|
-
getLCP(onPerfEntry);
|
|
8
|
-
getTTFB(onPerfEntry);
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default reportWebVitals;
|
|
1
|
+
const reportWebVitals = onPerfEntry => {
|
|
2
|
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
3
|
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
4
|
+
getCLS(onPerfEntry);
|
|
5
|
+
getFID(onPerfEntry);
|
|
6
|
+
getFCP(onPerfEntry);
|
|
7
|
+
getLCP(onPerfEntry);
|
|
8
|
+
getTTFB(onPerfEntry);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default reportWebVitals;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Accessibility (a11y) Module for Visualify.js
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive accessibility support for Visualify.js charts, ensuring compliance with WCAG 2.1 AA standards and compatibility with screen readers and keyboard navigation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **ARIA Labels**: Automatic generation of descriptive labels for charts
|
|
8
|
+
- **Keyboard Navigation**: Full keyboard support for navigating and interacting with charts
|
|
9
|
+
- **Screen Reader Support**: Data tables and live regions for screen reader announcements
|
|
10
|
+
- **Color Contrast**: WCAG AA compliance checking with automatic color suggestions
|
|
11
|
+
- **Focus Management**: Visible focus indicators and logical tab order
|
|
12
|
+
- **Motion Preferences**: Respects `prefers-reduced-motion` settings
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { applyA11yStyles } from 'visualify/a11y';
|
|
18
|
+
|
|
19
|
+
// Apply accessibility styles once in your app
|
|
20
|
+
applyA11yStyles();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Components with A11y Support
|
|
24
|
+
|
|
25
|
+
The following components have built-in accessibility support:
|
|
26
|
+
|
|
27
|
+
- `Scatter` - Scatter plots with keyboard navigation
|
|
28
|
+
- `Scatter3D` - 3D scatter plots with accessibility features
|
|
29
|
+
- `Bar` - Bar charts with ARIA labels
|
|
30
|
+
- `Bar3D` - 3D bar charts with keyboard navigation
|
|
31
|
+
- `EChartSwitcher` - Dynamic chart switcher with screen reader support
|
|
32
|
+
- `Vcontroller` - Dashboard controller with skip links
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Chart Configuration
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const config = {
|
|
40
|
+
title: 'Sales by Month',
|
|
41
|
+
data: [...],
|
|
42
|
+
|
|
43
|
+
// Accessibility options
|
|
44
|
+
a11y: {
|
|
45
|
+
enabled: true, // Enable accessibility features
|
|
46
|
+
announceLoad: true, // Announce when chart loads
|
|
47
|
+
autoFix: true, // Auto-fix color contrast issues
|
|
48
|
+
enableDownload: true, // Enable CSV download for screen readers
|
|
49
|
+
description: 'Detailed description of the chart',
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Color Contrast
|
|
55
|
+
|
|
56
|
+
The module automatically checks and fixes color contrast issues:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import { validateChartColors, applyAccessibleColors } from 'visualify/a11y';
|
|
60
|
+
|
|
61
|
+
// Validate colors
|
|
62
|
+
const validation = validateChartColors(config);
|
|
63
|
+
if (!validation.valid) {
|
|
64
|
+
console.log('Issues:', validation.issues);
|
|
65
|
+
console.log('Suggestions:', validation.suggestions);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Auto-fix colors
|
|
69
|
+
const accessibleConfig = applyAccessibleColors(config);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Keyboard Navigation
|
|
73
|
+
|
|
74
|
+
Charts support the following keyboard shortcuts:
|
|
75
|
+
|
|
76
|
+
| Key | Action |
|
|
77
|
+
|-----|--------|
|
|
78
|
+
| Tab | Focus the chart |
|
|
79
|
+
| Arrow Right/Down | Navigate to next data point |
|
|
80
|
+
| Arrow Left/Up | Navigate to previous data point |
|
|
81
|
+
| Enter/Space | Activate/select data point |
|
|
82
|
+
| Escape | Clear selection |
|
|
83
|
+
| Home | Jump to first data point |
|
|
84
|
+
| End | Jump to last data point |
|
|
85
|
+
| Page Up | Jump back 10 data points |
|
|
86
|
+
| Page Down | Jump forward 10 data points |
|
|
87
|
+
|
|
88
|
+
## API Reference
|
|
89
|
+
|
|
90
|
+
### ARIA Labels (`aria-labels.js`)
|
|
91
|
+
|
|
92
|
+
#### `generateChartLabel(config, chartType)`
|
|
93
|
+
Generates a descriptive ARIA label for a chart.
|
|
94
|
+
|
|
95
|
+
#### `formatDataForScreenReader(data, config, chartType)`
|
|
96
|
+
Formats chart data for screen reader output.
|
|
97
|
+
|
|
98
|
+
#### `generateDataTable(data, config, chartType)`
|
|
99
|
+
Creates a data table structure for screen readers.
|
|
100
|
+
|
|
101
|
+
#### `announceToScreenReader(message, priority)`
|
|
102
|
+
Announces a message via ARIA live region.
|
|
103
|
+
|
|
104
|
+
### Keyboard Navigation (`keyboard-nav.js`)
|
|
105
|
+
|
|
106
|
+
#### `useChartKeyboardNav(options)`
|
|
107
|
+
React hook for chart keyboard navigation.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
const {
|
|
111
|
+
containerRef,
|
|
112
|
+
handleKeyDown,
|
|
113
|
+
handleFocus,
|
|
114
|
+
focusedIndex,
|
|
115
|
+
containerProps
|
|
116
|
+
} = useChartKeyboardNav({
|
|
117
|
+
data: chartData,
|
|
118
|
+
config: chartConfig,
|
|
119
|
+
onFocusChange: (index, dataPoint) => {},
|
|
120
|
+
onActivate: (index, dataPoint) => {},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Color Contrast (`color-contrast.js`)
|
|
125
|
+
|
|
126
|
+
#### `checkWCAGCompliance(foreground, background, isLargeText)`
|
|
127
|
+
Checks if color combination meets WCAG AA standards.
|
|
128
|
+
|
|
129
|
+
#### `getContrastRatio(color1, color2)`
|
|
130
|
+
Calculates contrast ratio between two colors.
|
|
131
|
+
|
|
132
|
+
#### `suggestAccessibleColor(background, preferredColor, options)`
|
|
133
|
+
Suggests an accessible color for given background.
|
|
134
|
+
|
|
135
|
+
## CSS Classes
|
|
136
|
+
|
|
137
|
+
### `.sr-only`
|
|
138
|
+
Hides content visually but keeps it accessible to screen readers.
|
|
139
|
+
|
|
140
|
+
### `.visualify-chart`
|
|
141
|
+
Applied to all chart containers. Includes focus styles.
|
|
142
|
+
|
|
143
|
+
### `.visualify-controller`
|
|
144
|
+
Applied to the dashboard controller. Includes skip link styles.
|
|
145
|
+
|
|
146
|
+
## Testing Checklist
|
|
147
|
+
|
|
148
|
+
- [ ] Can navigate to charts using Tab key
|
|
149
|
+
- [ ] Focus indicator is clearly visible
|
|
150
|
+
- [ ] Arrow keys navigate between data points
|
|
151
|
+
- [ ] Enter/Space activates data points
|
|
152
|
+
- [ ] Screen reader announces chart title and type
|
|
153
|
+
- [ ] Screen reader announces data values
|
|
154
|
+
- [ ] Data table is accessible to screen readers
|
|
155
|
+
- [ ] Color contrast passes WCAG AA (4.5:1 for normal text)
|
|
156
|
+
- [ ] Works with 200% zoom
|
|
157
|
+
- [ ] Respects reduced motion preferences
|
|
158
|
+
|
|
159
|
+
## Browser Support
|
|
160
|
+
|
|
161
|
+
- Chrome/Edge 80+
|
|
162
|
+
- Firefox 75+
|
|
163
|
+
- Safari 13+
|
|
164
|
+
- IE 11 (with polyfills)
|
|
165
|
+
|
|
166
|
+
## Screen Reader Support
|
|
167
|
+
|
|
168
|
+
- NVDA (Windows)
|
|
169
|
+
- JAWS (Windows)
|
|
170
|
+
- VoiceOver (macOS/iOS)
|
|
171
|
+
- TalkBack (Android)
|
|
172
|
+
|
|
173
|
+
## Resources
|
|
174
|
+
|
|
175
|
+
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
176
|
+
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
|
177
|
+
- [Inclusive Design Principles](https://inclusivedesignprinciples.org/)
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARIA Label Generators for Visualify.js
|
|
3
|
+
* Provides accessible labels and descriptions for charts
|
|
4
|
+
* @module a11y/aria-labels
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates a descriptive ARIA label for a chart
|
|
9
|
+
* @param {Object} config - Chart configuration
|
|
10
|
+
* @param {string} chartType - Type of chart (scatter, bar, line, etc.)
|
|
11
|
+
* @returns {string} Descriptive label for screen readers
|
|
12
|
+
*/
|
|
13
|
+
export const generateChartLabel = (config, chartType = 'chart') => {
|
|
14
|
+
const title = typeof config.title === 'string'
|
|
15
|
+
? config.title
|
|
16
|
+
: config.title?.text || 'Untitled chart';
|
|
17
|
+
|
|
18
|
+
const dataPoints = config.data?.length || config.series?.[0]?.data?.length || 0;
|
|
19
|
+
const xAxisLabel = config.labels?.x || config.xAxis?.name || 'X axis';
|
|
20
|
+
const yAxisLabel = config.labels?.y || config.yAxis?.name || 'Y axis';
|
|
21
|
+
const zAxisLabel = config.zAxis3D?.name || config.zAxis?.name || 'Z axis';
|
|
22
|
+
|
|
23
|
+
let description = `${title}. ${chartType} chart`;
|
|
24
|
+
|
|
25
|
+
if (dataPoints > 0) {
|
|
26
|
+
description += ` with ${dataPoints} data points`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.is3D || chartType.includes('3D')) {
|
|
30
|
+
description += `. Three-dimensional view showing ${xAxisLabel}, ${yAxisLabel}, and ${zAxisLabel}`;
|
|
31
|
+
} else {
|
|
32
|
+
description += `. X axis shows ${xAxisLabel}, Y axis shows ${yAxisLabel}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.description) {
|
|
36
|
+
description += `. ${config.description}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return description;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats data into a readable string for screen readers
|
|
44
|
+
* @param {Array} data - Chart data array
|
|
45
|
+
* @param {Object} config - Chart configuration
|
|
46
|
+
* @param {string} chartType - Type of chart
|
|
47
|
+
* @returns {string} Formatted data description
|
|
48
|
+
*/
|
|
49
|
+
export const formatDataForScreenReader = (data, config = {}, chartType = 'chart') => {
|
|
50
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
51
|
+
return 'No data available';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const maxItems = 10; // Limit to avoid overwhelming screen readers
|
|
55
|
+
const dataLength = data.length;
|
|
56
|
+
|
|
57
|
+
let description = `Data summary: ${dataLength} total items. `;
|
|
58
|
+
|
|
59
|
+
// Format based on data structure
|
|
60
|
+
const sample = data[0];
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(sample)) {
|
|
63
|
+
// Array format: [[x, y], [x, y, z], etc.]
|
|
64
|
+
const dimensions = sample.length;
|
|
65
|
+
description += `Each data point has ${dimensions} dimensions. `;
|
|
66
|
+
|
|
67
|
+
const items = data.slice(0, maxItems).map((item, index) => {
|
|
68
|
+
if (dimensions === 2) {
|
|
69
|
+
return `Point ${index + 1}: ${formatValue(item[0])}, ${formatValue(item[1])}`;
|
|
70
|
+
} else if (dimensions >= 3) {
|
|
71
|
+
return `Point ${index + 1}: ${formatValue(item[0])}, ${formatValue(item[1])}, ${formatValue(item[2])}`;
|
|
72
|
+
}
|
|
73
|
+
return `Point ${index + 1}: ${formatValue(item[0])}`;
|
|
74
|
+
}).join('; ');
|
|
75
|
+
|
|
76
|
+
description += items;
|
|
77
|
+
} else if (typeof sample === 'object') {
|
|
78
|
+
// Object format: {x, y, z, name, value, etc.}
|
|
79
|
+
const items = data.slice(0, maxItems).map((item, index) => {
|
|
80
|
+
const name = item.name || item.label || `Item ${index + 1}`;
|
|
81
|
+
const value = item.value !== undefined ? item.value :
|
|
82
|
+
(item.z !== undefined ? `(${item.x}, ${item.y}, ${item.z})` :
|
|
83
|
+
`(${item.x}, ${item.y})`);
|
|
84
|
+
return `${name}: ${formatValue(value)}`;
|
|
85
|
+
}).join('; ');
|
|
86
|
+
|
|
87
|
+
description += items;
|
|
88
|
+
} else {
|
|
89
|
+
// Simple value array
|
|
90
|
+
const items = data.slice(0, maxItems).map((item, index) =>
|
|
91
|
+
`Item ${index + 1}: ${formatValue(item)}`
|
|
92
|
+
).join('; ');
|
|
93
|
+
|
|
94
|
+
description += items;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (dataLength > maxItems) {
|
|
98
|
+
description += `. And ${dataLength - maxItems} more items`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return description;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generates an accessible name for a data point
|
|
106
|
+
* @param {Object|Array} dataPoint - Single data point
|
|
107
|
+
* @param {number} index - Index of the data point
|
|
108
|
+
* @param {Object} config - Chart configuration
|
|
109
|
+
* @returns {string} Accessible name for the data point
|
|
110
|
+
*/
|
|
111
|
+
export const generateDataPointLabel = (dataPoint, index, config = {}) => {
|
|
112
|
+
if (Array.isArray(dataPoint)) {
|
|
113
|
+
const xLabel = config.labels?.x || 'X';
|
|
114
|
+
const yLabel = config.labels?.y || 'Y';
|
|
115
|
+
const zLabel = config.zAxis3D?.name || config.zAxis?.name || 'Z';
|
|
116
|
+
|
|
117
|
+
if (dataPoint.length >= 3) {
|
|
118
|
+
return `Data point ${index + 1}: ${xLabel} ${formatValue(dataPoint[0])}, ${yLabel} ${formatValue(dataPoint[1])}, ${zLabel} ${formatValue(dataPoint[2])}`;
|
|
119
|
+
}
|
|
120
|
+
return `Data point ${index + 1}: ${xLabel} ${formatValue(dataPoint[0])}, ${yLabel} ${formatValue(dataPoint[1])}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof dataPoint === 'object') {
|
|
124
|
+
const name = dataPoint.name || dataPoint.label || `Data point ${index + 1}`;
|
|
125
|
+
const value = dataPoint.value !== undefined ? formatValue(dataPoint.value) : '';
|
|
126
|
+
const x = dataPoint.x !== undefined ? `${config.labels?.x || 'X'}: ${formatValue(dataPoint.x)}` : '';
|
|
127
|
+
const y = dataPoint.y !== undefined ? `${config.labels?.y || 'Y'}: ${formatValue(dataPoint.y)}` : '';
|
|
128
|
+
const z = dataPoint.z !== undefined ? `${config.zAxis3D?.name || 'Z'}: ${formatValue(dataPoint.z)}` : '';
|
|
129
|
+
|
|
130
|
+
const coords = [x, y, z].filter(Boolean).join(', ');
|
|
131
|
+
|
|
132
|
+
if (value) {
|
|
133
|
+
return `${name}: ${value}${coords ? ` at ${coords}` : ''}`;
|
|
134
|
+
}
|
|
135
|
+
return `${name}${coords ? `: ${coords}` : ''}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `Data point ${index + 1}: ${formatValue(dataPoint)}`;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Formats a value for screen reader output
|
|
143
|
+
* @param {*} value - Value to format
|
|
144
|
+
* @returns {string} Formatted value
|
|
145
|
+
*/
|
|
146
|
+
const formatValue = (value) => {
|
|
147
|
+
if (value === null || value === undefined) {
|
|
148
|
+
return 'unknown';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof value === 'number') {
|
|
152
|
+
// Format large numbers with commas
|
|
153
|
+
if (Math.abs(value) >= 1000) {
|
|
154
|
+
return value.toLocaleString();
|
|
155
|
+
}
|
|
156
|
+
// Round decimals to 2 places
|
|
157
|
+
if (value % 1 !== 0) {
|
|
158
|
+
return value.toFixed(2);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (value instanceof Date) {
|
|
163
|
+
return value.toLocaleDateString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return String(value);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generates ARIA attributes for a chart container
|
|
171
|
+
* @param {Object} config - Chart configuration
|
|
172
|
+
* @param {string} chartType - Type of chart
|
|
173
|
+
* @param {string} uniqueId - Unique identifier for the chart
|
|
174
|
+
* @returns {Object} ARIA attributes object
|
|
175
|
+
*/
|
|
176
|
+
export const generateChartAriaAttributes = (config, chartType = 'chart', uniqueId = '') => {
|
|
177
|
+
const label = generateChartLabel(config, chartType);
|
|
178
|
+
const dataDescription = formatDataForScreenReader(
|
|
179
|
+
config.data || config.series?.[0]?.data,
|
|
180
|
+
config,
|
|
181
|
+
chartType
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const descriptionId = uniqueId ? `${uniqueId}-description` : 'chart-description';
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
role: 'img',
|
|
188
|
+
'aria-label': label,
|
|
189
|
+
'aria-describedby': descriptionId,
|
|
190
|
+
tabIndex: 0,
|
|
191
|
+
'data-description-id': descriptionId,
|
|
192
|
+
'data-chart-type': chartType,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generates a data table for screen readers from chart data
|
|
198
|
+
* @param {Array} data - Chart data
|
|
199
|
+
* @param {Object} config - Chart configuration
|
|
200
|
+
* @param {string} chartType - Type of chart
|
|
201
|
+
* @returns {Object} Table data structure with headers and rows
|
|
202
|
+
*/
|
|
203
|
+
export const generateDataTable = (data, config = {}, chartType = 'chart') => {
|
|
204
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
caption: config.title || 'Chart Data',
|
|
207
|
+
headers: [],
|
|
208
|
+
rows: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sample = data[0];
|
|
213
|
+
let headers = [];
|
|
214
|
+
let rows = [];
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(sample)) {
|
|
217
|
+
// Array format
|
|
218
|
+
const dimensions = sample.length;
|
|
219
|
+
headers = [
|
|
220
|
+
config.labels?.x || 'X',
|
|
221
|
+
config.labels?.y || 'Y',
|
|
222
|
+
...(dimensions > 2 ? [config.zAxis3D?.name || config.zAxis?.name || 'Z'] : []),
|
|
223
|
+
].slice(0, dimensions);
|
|
224
|
+
|
|
225
|
+
rows = data.map((item, index) => ({
|
|
226
|
+
id: `row-${index}`,
|
|
227
|
+
cells: item.map(val => formatValue(val)),
|
|
228
|
+
}));
|
|
229
|
+
} else if (typeof sample === 'object') {
|
|
230
|
+
// Object format
|
|
231
|
+
const keys = Object.keys(sample);
|
|
232
|
+
headers = keys.map(key => {
|
|
233
|
+
switch(key) {
|
|
234
|
+
case 'x': return config.labels?.x || 'X';
|
|
235
|
+
case 'y': return config.labels?.y || 'Y';
|
|
236
|
+
case 'z': return config.zAxis3D?.name || config.zAxis?.name || 'Z';
|
|
237
|
+
case 'name': return 'Name';
|
|
238
|
+
case 'value': return 'Value';
|
|
239
|
+
default: return key.charAt(0).toUpperCase() + key.slice(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
rows = data.map((item, index) => ({
|
|
244
|
+
id: `row-${index}`,
|
|
245
|
+
cells: keys.map(key => formatValue(item[key])),
|
|
246
|
+
}));
|
|
247
|
+
} else {
|
|
248
|
+
// Simple value array
|
|
249
|
+
headers = ['Index', 'Value'];
|
|
250
|
+
rows = data.map((item, index) => ({
|
|
251
|
+
id: `row-${index}`,
|
|
252
|
+
cells: [String(index + 1), formatValue(item)],
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
caption: typeof config.title === 'string' ? config.title : config.title?.text || 'Chart Data',
|
|
258
|
+
headers,
|
|
259
|
+
rows,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Announces a message to screen readers via a live region
|
|
265
|
+
* @param {string} message - Message to announce
|
|
266
|
+
* @param {string} priority - Priority level ('polite' or 'assertive')
|
|
267
|
+
*/
|
|
268
|
+
export const announceToScreenReader = (message, priority = 'polite') => {
|
|
269
|
+
// Look for existing live region or create one
|
|
270
|
+
let liveRegion = document.getElementById('visualify-a11y-announcer');
|
|
271
|
+
|
|
272
|
+
if (!liveRegion) {
|
|
273
|
+
liveRegion = document.createElement('div');
|
|
274
|
+
liveRegion.id = 'visualify-a11y-announcer';
|
|
275
|
+
liveRegion.setAttribute('aria-live', priority);
|
|
276
|
+
liveRegion.setAttribute('aria-atomic', 'true');
|
|
277
|
+
liveRegion.className = 'sr-only';
|
|
278
|
+
document.body.appendChild(liveRegion);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update live region to trigger announcement
|
|
282
|
+
liveRegion.setAttribute('aria-live', priority);
|
|
283
|
+
liveRegion.textContent = message;
|
|
284
|
+
|
|
285
|
+
// Clear after announcement
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
liveRegion.textContent = '';
|
|
288
|
+
}, 1000);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Generates a CSV string from chart data for export
|
|
293
|
+
* @param {Array} data - Chart data
|
|
294
|
+
* @param {Object} config - Chart configuration
|
|
295
|
+
* @returns {string} CSV formatted string
|
|
296
|
+
*/
|
|
297
|
+
export const generateCSV = (data, config = {}) => {
|
|
298
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const table = generateDataTable(data, config);
|
|
303
|
+
const headers = table.headers.join(',');
|
|
304
|
+
const rows = table.rows.map(row => row.cells.join(',')).join('\n');
|
|
305
|
+
|
|
306
|
+
return `${headers}\n${rows}`;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Creates a download link for CSV data
|
|
311
|
+
* @param {string} csv - CSV content
|
|
312
|
+
* @param {string} filename - Download filename
|
|
313
|
+
*/
|
|
314
|
+
export const downloadCSV = (csv, filename = 'chart-data.csv') => {
|
|
315
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
316
|
+
const link = document.createElement('a');
|
|
317
|
+
const url = URL.createObjectURL(blob);
|
|
318
|
+
|
|
319
|
+
link.setAttribute('href', url);
|
|
320
|
+
link.setAttribute('download', filename);
|
|
321
|
+
link.setAttribute('aria-label', `Download data as ${filename}`);
|
|
322
|
+
link.className = 'sr-only';
|
|
323
|
+
|
|
324
|
+
document.body.appendChild(link);
|
|
325
|
+
link.click();
|
|
326
|
+
document.body.removeChild(link);
|
|
327
|
+
URL.revokeObjectURL(url);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export default {
|
|
331
|
+
generateChartLabel,
|
|
332
|
+
formatDataForScreenReader,
|
|
333
|
+
generateDataPointLabel,
|
|
334
|
+
generateChartAriaAttributes,
|
|
335
|
+
generateDataTable,
|
|
336
|
+
announceToScreenReader,
|
|
337
|
+
generateCSV,
|
|
338
|
+
downloadCSV,
|
|
339
|
+
};
|