mintwaterfall 0.8.6

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 (38) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/CONTRIBUTING.md +199 -0
  3. package/README.md +363 -0
  4. package/dist/index.d.ts +149 -0
  5. package/dist/mintwaterfall.cjs.js +7978 -0
  6. package/dist/mintwaterfall.esm.js +7907 -0
  7. package/dist/mintwaterfall.min.js +7 -0
  8. package/dist/mintwaterfall.umd.js +7978 -0
  9. package/index.d.ts +149 -0
  10. package/package.json +126 -0
  11. package/src/enterprise/enterprise-core.js +0 -0
  12. package/src/enterprise/enterprise-feature-template.js +0 -0
  13. package/src/enterprise/feature-registry.js +0 -0
  14. package/src/enterprise/features/breakdown.js +0 -0
  15. package/src/features/breakdown.js +0 -0
  16. package/src/features/conditional-formatting.js +0 -0
  17. package/src/index.js +111 -0
  18. package/src/mintwaterfall-accessibility.ts +680 -0
  19. package/src/mintwaterfall-advanced-data.ts +1034 -0
  20. package/src/mintwaterfall-advanced-interactions.ts +649 -0
  21. package/src/mintwaterfall-advanced-performance.ts +582 -0
  22. package/src/mintwaterfall-animations.ts +595 -0
  23. package/src/mintwaterfall-brush.ts +471 -0
  24. package/src/mintwaterfall-chart-core.ts +296 -0
  25. package/src/mintwaterfall-chart.ts +1915 -0
  26. package/src/mintwaterfall-data.ts +1100 -0
  27. package/src/mintwaterfall-export.ts +475 -0
  28. package/src/mintwaterfall-hierarchical-layouts.ts +724 -0
  29. package/src/mintwaterfall-layouts.ts +647 -0
  30. package/src/mintwaterfall-performance.ts +573 -0
  31. package/src/mintwaterfall-scales.ts +437 -0
  32. package/src/mintwaterfall-shapes.ts +385 -0
  33. package/src/mintwaterfall-statistics.ts +821 -0
  34. package/src/mintwaterfall-themes.ts +391 -0
  35. package/src/mintwaterfall-tooltip.ts +450 -0
  36. package/src/mintwaterfall-zoom.ts +399 -0
  37. package/src/types/js-modules.d.ts +25 -0
  38. package/src/utils/compatibility-layer.js +0 -0
@@ -0,0 +1,680 @@
1
+ // MintWaterfall Accessibility System - TypeScript Version
2
+ // Provides WCAG 2.1 AA compliance features for screen readers and keyboard navigation with full type safety
3
+
4
+ import * as d3 from 'd3';
5
+ import { rgb } from 'd3-color';
6
+
7
+ // Type definitions for accessibility system
8
+ export interface AccessibilityConfig {
9
+ title?: string;
10
+ summary?: string;
11
+ totalItems?: number;
12
+ showTotal?: boolean;
13
+ formatNumber?: (n: number) => string;
14
+ }
15
+
16
+ export interface WaterfallDataItem {
17
+ label: string;
18
+ stacks: StackItem[];
19
+ cumulative?: number;
20
+ }
21
+
22
+ export interface StackItem {
23
+ label?: string;
24
+ value: number;
25
+ }
26
+
27
+ export interface HierarchicalData {
28
+ children?: HierarchicalData[];
29
+ value?: number;
30
+ }
31
+
32
+ export interface ContrastResult {
33
+ ratio: number;
34
+ passesAA: boolean;
35
+ passesAAA: boolean;
36
+ }
37
+
38
+ export interface AccessibilityResult {
39
+ bars: d3.Selection<d3.BaseType, any, any, any>;
40
+ focusableElements: number;
41
+ }
42
+
43
+ export interface ChartAccessibilityResult {
44
+ accessibilitySystem: AccessibilitySystem;
45
+ descriptionId: string;
46
+ focusableElements: number;
47
+ }
48
+
49
+ export interface AccessibilitySystem {
50
+ createLiveRegion(container: d3.Selection<d3.BaseType, any, any, any>): d3.Selection<HTMLDivElement, any, any, any>;
51
+ createChartDescription(container: d3.Selection<d3.BaseType, any, any, any>, data: WaterfallDataItem[] | HierarchicalData, config?: AccessibilityConfig): string;
52
+ makeAccessible(chartContainer: d3.Selection<d3.BaseType, any, any, any>, data: WaterfallDataItem[], config?: AccessibilityConfig): AccessibilityResult;
53
+ handleChartKeydown(event: KeyboardEvent, data: WaterfallDataItem[], config: AccessibilityConfig): void;
54
+ handleBarKeydown(event: KeyboardEvent, barData: WaterfallDataItem, index: number, allData: WaterfallDataItem[], config: AccessibilityConfig): void;
55
+ moveFocus(direction: number, data: WaterfallDataItem[], config: AccessibilityConfig): void;
56
+ focusElement(index: number, data: WaterfallDataItem[], config: AccessibilityConfig): void;
57
+ announce(message: string): void;
58
+ detectHighContrast(): boolean;
59
+ applyHighContrastStyles(chartContainer: d3.Selection<d3.BaseType, any, any, any>): void;
60
+ injectForcedColorsCSS(): void;
61
+ respectsReducedMotion(): boolean;
62
+ getAccessibleAnimationDuration(defaultDuration: number): number;
63
+ validateColorContrast(foreground: string, background: string): ContrastResult;
64
+ setAnnounceFunction(fn: (message: string) => void): AccessibilitySystem;
65
+ getCurrentFocus(): number;
66
+ getFocusableCount(): number;
67
+ }
68
+
69
+ export function createAccessibilitySystem(): AccessibilitySystem {
70
+
71
+ let currentFocusIndex: number = -1;
72
+ let focusableElements: any[] = [];
73
+ let announceFunction: ((message: string) => void) | null = null;
74
+ let descriptionId: string | null = null;
75
+
76
+ // ARIA live region for dynamic announcements
77
+ function createLiveRegion(container: d3.Selection<d3.BaseType, any, any, any>): d3.Selection<HTMLDivElement, any, any, any> {
78
+ const liveRegion = container.append<HTMLDivElement>("div")
79
+ .attr("id", "waterfall-live-region")
80
+ .attr("aria-live", "polite")
81
+ .attr("aria-atomic", "true")
82
+ .style("position", "absolute")
83
+ .style("left", "-10000px")
84
+ .style("width", "1px")
85
+ .style("height", "1px")
86
+ .style("overflow", "hidden");
87
+
88
+ return liveRegion;
89
+ }
90
+
91
+ // Create chart description for screen readers
92
+ function createChartDescription(
93
+ container: d3.Selection<d3.BaseType, any, any, any>,
94
+ data: WaterfallDataItem[] | HierarchicalData,
95
+ config: AccessibilityConfig = {}
96
+ ): string {
97
+ const {
98
+ title = "Waterfall Chart",
99
+ summary = "Interactive waterfall chart showing data progression",
100
+ totalItems = Array.isArray(data) ? data.length : 1,
101
+ showTotal: hasTotal = false
102
+ } = config;
103
+
104
+ const descId = "waterfall-description-" + Math.random().toString(36).substr(2, 9);
105
+
106
+ const description = container.append("div")
107
+ .attr("id", descId)
108
+ .attr("class", "sr-only")
109
+ .style("position", "absolute")
110
+ .style("left", "-10000px")
111
+ .style("width", "1px")
112
+ .style("height", "1px")
113
+ .style("overflow", "hidden");
114
+
115
+ // Calculate summary statistics based on data structure
116
+ let totalValue = 0;
117
+ let positiveCount = 0;
118
+ let negativeCount = 0;
119
+
120
+ if (Array.isArray(data)) {
121
+ // Waterfall chart data structure
122
+ totalValue = data.reduce((sum, item) => {
123
+ if (item.stacks && Array.isArray(item.stacks)) {
124
+ return sum + item.stacks.reduce((stackSum, stack) => stackSum + (stack.value || 0), 0);
125
+ }
126
+ return sum;
127
+ }, 0);
128
+
129
+ positiveCount = data.filter(item =>
130
+ item.stacks && item.stacks.some(stack => (stack.value || 0) > 0)
131
+ ).length;
132
+
133
+ negativeCount = data.filter(item =>
134
+ item.stacks && item.stacks.some(stack => stack.value < 0)
135
+ ).length;
136
+ } else if (data && typeof data === "object" && (data as HierarchicalData).children) {
137
+ // Hierarchical chart data structure
138
+ function calculateHierarchicalStats(node: HierarchicalData): number {
139
+ if (node.children && Array.isArray(node.children)) {
140
+ return node.children.reduce((sum, child) => sum + calculateHierarchicalStats(child), 0);
141
+ } else {
142
+ return node.value || 0;
143
+ }
144
+ }
145
+
146
+ totalValue = calculateHierarchicalStats(data as HierarchicalData);
147
+ positiveCount = 1; // For hierarchical data, we consider it as one positive entity
148
+ negativeCount = 0;
149
+ }
150
+
151
+ description.html(`
152
+ <h3>${title}</h3>
153
+ <p>${summary}</p>
154
+ <p>This chart contains ${totalItems} data categories${hasTotal ? " plus a total bar" : ""}.</p>
155
+ <p>Total value: ${config.formatNumber ? config.formatNumber(totalValue) : totalValue}</p>
156
+ <p>${positiveCount} categories have positive values, ${negativeCount} have negative values.</p>
157
+ <p>Use Tab to navigate between bars, Enter to hear details, and Arrow keys to move between bars.</p>
158
+ <p>Press Escape to return focus to the chart container.</p>
159
+ `);
160
+
161
+ descriptionId = descId;
162
+ return descId;
163
+ }
164
+
165
+ // Make chart elements keyboard accessible
166
+ function makeAccessible(
167
+ chartContainer: d3.Selection<d3.BaseType, any, any, any>,
168
+ data: WaterfallDataItem[],
169
+ config: AccessibilityConfig = {}
170
+ ): AccessibilityResult {
171
+ const svg = chartContainer.select("svg");
172
+
173
+ // Add main chart ARIA attributes
174
+ svg.attr("role", "img")
175
+ .attr("aria-labelledby", descriptionId)
176
+ .attr("tabindex", "0")
177
+ .attr("aria-describedby", descriptionId);
178
+
179
+ // Add keyboard event handlers to main SVG
180
+ svg.on("keydown", function(event: KeyboardEvent) {
181
+ handleChartKeydown(event, data, config);
182
+ });
183
+
184
+ // Make individual bars focusable and accessible
185
+ const bars = svg.selectAll(".bar-group");
186
+
187
+ bars.each(function(d: any, i: number) {
188
+ const bar = d3.select(this);
189
+ const data = d as WaterfallDataItem;
190
+
191
+ bar.attr("role", "button")
192
+ .attr("tabindex", "-1")
193
+ .attr("aria-label", createBarAriaLabel(data, i, config))
194
+ .attr("aria-describedby", `bar-description-${i}`)
195
+ .on("keydown", function(event: KeyboardEvent) {
196
+ handleBarKeydown(event, data, i, [data], config);
197
+ })
198
+ .on("focus", function() {
199
+ currentFocusIndex = i;
200
+ const element = this as Element;
201
+ if (element) {
202
+ highlightFocusedElement(element);
203
+ }
204
+ })
205
+ .on("blur", function() {
206
+ const element = this as Element;
207
+ if (element) {
208
+ removeFocusHighlight(element);
209
+ }
210
+ });
211
+ });
212
+
213
+ // Store focusable elements
214
+ focusableElements = bars && bars.nodes && typeof bars.nodes === 'function'
215
+ ? bars.nodes().filter(node => node !== null)
216
+ : [];
217
+
218
+ return {
219
+ bars,
220
+ focusableElements: focusableElements.length
221
+ };
222
+ }
223
+
224
+ // Create ARIA label for individual bars
225
+ function createBarAriaLabel(data: WaterfallDataItem, index: number, config: AccessibilityConfig = {}): string {
226
+ if (!data || !data.stacks || !Array.isArray(data.stacks)) {
227
+ return `Item ${index + 1}: Invalid data`;
228
+ }
229
+
230
+ const totalValue = data.stacks.reduce((sum, stack) => sum + stack.value, 0);
231
+ const stackCount = data.stacks.length;
232
+ const formatNumber = config.formatNumber || ((n: number) => n.toString());
233
+
234
+ let label = `${data.label}: ${formatNumber(totalValue)}`;
235
+
236
+ if (stackCount > 1) {
237
+ label += `, ${stackCount} segments`;
238
+ }
239
+
240
+ if (data.cumulative !== undefined) {
241
+ label += `, cumulative total: ${formatNumber(data.cumulative)}`;
242
+ }
243
+
244
+ label += ". Press Enter for details.";
245
+
246
+ return label;
247
+ }
248
+
249
+ // Handle keyboard navigation on chart level
250
+ function handleChartKeydown(event: KeyboardEvent, data: WaterfallDataItem[], config: AccessibilityConfig): void {
251
+ switch(event.key) {
252
+ case "Tab":
253
+ // Let default tab behavior work
254
+ break;
255
+
256
+ case "ArrowRight":
257
+ case "ArrowDown":
258
+ event.preventDefault();
259
+ moveFocus(1, data, config);
260
+ break;
261
+
262
+ case "ArrowLeft":
263
+ case "ArrowUp":
264
+ event.preventDefault();
265
+ moveFocus(-1, data, config);
266
+ break;
267
+
268
+ case "Home":
269
+ event.preventDefault();
270
+ focusElement(0, data, config);
271
+ break;
272
+
273
+ case "End":
274
+ event.preventDefault();
275
+ focusElement(focusableElements.length - 1, data, config);
276
+ break;
277
+
278
+ case "Enter":
279
+ case " ":
280
+ event.preventDefault();
281
+ if (currentFocusIndex >= 0) {
282
+ announceBarDetails(data[currentFocusIndex], currentFocusIndex, config);
283
+ } else {
284
+ announceChartSummary(data, config);
285
+ }
286
+ break;
287
+
288
+ case "Escape":
289
+ event.preventDefault();
290
+ returnFocusToChart();
291
+ break;
292
+ }
293
+ }
294
+
295
+ // Handle keyboard events on individual bars
296
+ function handleBarKeydown(
297
+ event: KeyboardEvent,
298
+ barData: WaterfallDataItem,
299
+ index: number,
300
+ allData: WaterfallDataItem[],
301
+ config: AccessibilityConfig
302
+ ): void {
303
+ switch(event.key) {
304
+ case "Enter":
305
+ case " ":
306
+ event.preventDefault();
307
+ announceBarDetails(barData, index, config);
308
+ // Trigger click event for compatibility
309
+ d3.select(event.target as Element).dispatch("click");
310
+ break;
311
+
312
+ case "ArrowRight":
313
+ case "ArrowDown":
314
+ event.preventDefault();
315
+ moveFocus(1, allData, config);
316
+ break;
317
+
318
+ case "ArrowLeft":
319
+ case "ArrowUp":
320
+ event.preventDefault();
321
+ moveFocus(-1, allData, config);
322
+ break;
323
+ }
324
+ }
325
+
326
+ // Move focus between chart elements
327
+ function moveFocus(direction: number, data: WaterfallDataItem[], config: AccessibilityConfig): void {
328
+ if (focusableElements.length === 0) return;
329
+
330
+ let newIndex = currentFocusIndex + direction;
331
+
332
+ // Wrap around
333
+ if (newIndex >= focusableElements.length) {
334
+ newIndex = 0;
335
+ } else if (newIndex < 0) {
336
+ newIndex = focusableElements.length - 1;
337
+ }
338
+
339
+ focusElement(newIndex, data, config);
340
+ }
341
+
342
+ // Focus specific element by index
343
+ function focusElement(index: number, data: WaterfallDataItem[], config: AccessibilityConfig): void {
344
+ if (index < 0 || index >= focusableElements.length) return;
345
+
346
+ currentFocusIndex = index;
347
+ const element = focusableElements[index] as HTMLElement;
348
+ if (element && element.focus) {
349
+ element.focus();
350
+ }
351
+
352
+ // Announce the focused element
353
+ const barData = data[index];
354
+ announceBarFocus(barData, index, config);
355
+ }
356
+
357
+ // Return focus to main chart container
358
+ function returnFocusToChart(): void {
359
+ const svg = d3.select("svg[role='img']");
360
+ if (!svg.empty()) {
361
+ const svgNode = svg.node() as HTMLElement;
362
+ if (svgNode) {
363
+ svgNode.focus();
364
+ }
365
+ currentFocusIndex = -1;
366
+ }
367
+ }
368
+
369
+ // Visual focus indicators
370
+ function highlightFocusedElement(element: Element): void {
371
+ d3.select(element)
372
+ .style("outline", "3px solid #4A90E2")
373
+ .style("outline-offset", "2px");
374
+ }
375
+
376
+ function removeFocusHighlight(element: Element): void {
377
+ d3.select(element)
378
+ .style("outline", null)
379
+ .style("outline-offset", null);
380
+ }
381
+
382
+ // Screen reader announcements
383
+ function announceBarFocus(data: WaterfallDataItem, index: number, config: AccessibilityConfig): void {
384
+ const formatNumber = config.formatNumber || ((n: number) => n.toString());
385
+ const totalValue = data.stacks.reduce((sum, stack) => sum + stack.value, 0);
386
+
387
+ const message = `Focused on ${data.label}, value ${formatNumber(totalValue)}`;
388
+ announce(message);
389
+ }
390
+
391
+ function announceBarDetails(data: WaterfallDataItem, index: number, config: AccessibilityConfig): void {
392
+ const formatNumber = config.formatNumber || ((n: number) => n.toString());
393
+ const totalValue = data.stacks.reduce((sum, stack) => sum + stack.value, 0);
394
+
395
+ let message = `${data.label}: Total value ${formatNumber(totalValue)}`;
396
+
397
+ if (data.stacks.length > 1) {
398
+ message += `. Contains ${data.stacks.length} segments: `;
399
+ const segments = data.stacks.map(stack =>
400
+ `${stack.label || formatNumber(stack.value)}`
401
+ ).join(", ");
402
+ message += segments;
403
+ }
404
+
405
+ if (data.cumulative !== undefined) {
406
+ message += `. Cumulative total: ${formatNumber(data.cumulative)}`;
407
+ }
408
+
409
+ announce(message);
410
+ }
411
+
412
+ function announceChartSummary(data: WaterfallDataItem[], config: AccessibilityConfig): void {
413
+ const formatNumber = config.formatNumber || ((n: number) => n.toString());
414
+ const totalValue = data.reduce((sum, item) => {
415
+ return sum + item.stacks.reduce((stackSum, stack) => stackSum + stack.value, 0);
416
+ }, 0);
417
+
418
+ const message = `Waterfall chart with ${data.length} categories. Total value: ${formatNumber(totalValue)}. Use arrow keys to navigate between bars.`;
419
+ announce(message);
420
+ }
421
+
422
+ // Announce message to screen readers
423
+ function announce(message: string): void {
424
+ const liveRegion = d3.select("#waterfall-live-region");
425
+ if (!liveRegion.empty()) {
426
+ liveRegion.text(message);
427
+ }
428
+
429
+ // Also call custom announce function if provided
430
+ if (announceFunction) {
431
+ announceFunction(message);
432
+ }
433
+ }
434
+
435
+ // High contrast mode detection and support (Updated: 2025-08-28)
436
+ function detectHighContrast(): boolean {
437
+ // Check for modern forced colors mode and high contrast preferences
438
+ if (window.matchMedia) {
439
+ // First check for modern forced-colors mode (preferred)
440
+ if (window.matchMedia("(forced-colors: active)").matches) {
441
+ return true;
442
+ }
443
+
444
+ // Then check for prefers-contrast
445
+ if (window.matchMedia("(prefers-contrast: high)").matches) {
446
+ return true;
447
+ }
448
+
449
+ // Additional modern checks for high contrast scenarios
450
+ if (window.matchMedia("(prefers-contrast: more)").matches) {
451
+ return true;
452
+ }
453
+
454
+ // Check for inverted colors which often indicates high contrast mode
455
+ if (window.matchMedia("(inverted-colors: inverted)").matches) {
456
+ return true;
457
+ }
458
+
459
+ // Fallback: detect if system colors are being used (indicates forced colors)
460
+ try {
461
+ const testElement = document.createElement("div");
462
+ testElement.style.color = "rgb(1, 2, 3)";
463
+ testElement.style.position = "absolute";
464
+ testElement.style.visibility = "hidden";
465
+ document.body.appendChild(testElement);
466
+
467
+ const computedColor = window.getComputedStyle(testElement).color;
468
+ document.body.removeChild(testElement);
469
+
470
+ // If the computed color doesn't match what we set, forced colors is likely active
471
+ return computedColor !== "rgb(1, 2, 3)";
472
+ } catch (e) {
473
+ // If detection fails, assume no high contrast for safety
474
+ return false;
475
+ }
476
+ }
477
+ return false;
478
+ }
479
+
480
+ function applyHighContrastStyles(chartContainer: d3.Selection<d3.BaseType, any, any, any>): void {
481
+ if (!detectHighContrast()) return;
482
+
483
+ const svg = chartContainer.select("svg");
484
+
485
+ // Apply modern forced colors mode compatible styles using CSS system colors
486
+ svg.selectAll(".bar-group rect")
487
+ .style("stroke", "CanvasText")
488
+ .style("stroke-width", "2px")
489
+ .style("fill", "ButtonFace");
490
+
491
+ svg.selectAll(".x-axis, .y-axis")
492
+ .style("stroke", "CanvasText")
493
+ .style("stroke-width", "2px");
494
+
495
+ svg.selectAll("text")
496
+ .style("fill", "CanvasText")
497
+ .style("font-weight", "bold");
498
+
499
+ // Apply high contrast styles to trend lines if present
500
+ svg.selectAll(".trend-line")
501
+ .style("stroke", "Highlight")
502
+ .style("stroke-width", "3px");
503
+
504
+ // Ensure tooltips work in forced colors mode
505
+ svg.selectAll(".tooltip")
506
+ .style("background", "Canvas")
507
+ .style("border", "2px solid CanvasText")
508
+ .style("color", "CanvasText");
509
+ }
510
+
511
+ // Inject CSS for forced colors mode support
512
+ function injectForcedColorsCSS(): void {
513
+ // Check if we're in a browser environment
514
+ if (typeof document === "undefined") return; // Node.js environment
515
+
516
+ const cssId = "mintwaterfall-forced-colors-css";
517
+ if (document.getElementById(cssId)) return; // Already injected
518
+
519
+ const css = `
520
+ @media (forced-colors: active) {
521
+ .mintwaterfall-chart svg {
522
+ forced-color-adjust: none;
523
+ }
524
+
525
+ .mintwaterfall-chart .bar-group rect {
526
+ stroke: CanvasText !important;
527
+ stroke-width: 2px !important;
528
+ }
529
+
530
+ .mintwaterfall-chart .x-axis,
531
+ .mintwaterfall-chart .y-axis {
532
+ stroke: CanvasText !important;
533
+ stroke-width: 2px !important;
534
+ }
535
+
536
+ .mintwaterfall-chart text {
537
+ fill: CanvasText !important;
538
+ font-weight: bold !important;
539
+ }
540
+
541
+ .mintwaterfall-chart .trend-line {
542
+ stroke: Highlight !important;
543
+ stroke-width: 3px !important;
544
+ }
545
+
546
+ .mintwaterfall-tooltip {
547
+ background: Canvas !important;
548
+ border: 2px solid CanvasText !important;
549
+ color: CanvasText !important;
550
+ forced-color-adjust: none;
551
+ }
552
+ }
553
+
554
+ @media (prefers-contrast: high) {
555
+ .mintwaterfall-chart .bar-group rect {
556
+ stroke-width: 2px !important;
557
+ }
558
+
559
+ .mintwaterfall-chart text {
560
+ font-weight: bold !important;
561
+ }
562
+ }
563
+ `;
564
+
565
+ const style = document.createElement("style");
566
+ style.id = cssId;
567
+ style.textContent = css;
568
+ document.head.appendChild(style);
569
+ }
570
+
571
+ // Reduced motion support
572
+ function respectsReducedMotion(): boolean {
573
+ if (window.matchMedia) {
574
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
575
+ }
576
+ return false;
577
+ }
578
+
579
+ function getAccessibleAnimationDuration(defaultDuration: number): number {
580
+ return respectsReducedMotion() ? 0 : defaultDuration;
581
+ }
582
+
583
+ // Color contrast validation
584
+ function validateColorContrast(foreground: string, background: string): ContrastResult {
585
+ // Simplified contrast ratio calculation
586
+ // In production, use a proper color contrast library
587
+ const getLuminance = (color: string): number => {
588
+ // This is a simplified version - use a proper color library
589
+ const colorRgb = rgb(color);
590
+ if (!colorRgb) return 0;
591
+ return (0.299 * colorRgb.r + 0.587 * colorRgb.g + 0.114 * colorRgb.b) / 255;
592
+ };
593
+
594
+ const l1 = getLuminance(foreground);
595
+ const l2 = getLuminance(background);
596
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
597
+
598
+ return {
599
+ ratio,
600
+ passesAA: ratio >= 4.5,
601
+ passesAAA: ratio >= 7
602
+ };
603
+ }
604
+
605
+ // Public API
606
+ const accessibilitySystem: AccessibilitySystem = {
607
+ createLiveRegion,
608
+ createChartDescription,
609
+ makeAccessible,
610
+ handleChartKeydown,
611
+ handleBarKeydown,
612
+ moveFocus,
613
+ focusElement,
614
+ announce,
615
+ detectHighContrast,
616
+ applyHighContrastStyles,
617
+ injectForcedColorsCSS,
618
+ respectsReducedMotion,
619
+ getAccessibleAnimationDuration,
620
+ validateColorContrast,
621
+
622
+ // Configuration
623
+ setAnnounceFunction(fn: (message: string) => void): AccessibilitySystem {
624
+ announceFunction = fn;
625
+ return this;
626
+ },
627
+
628
+ getCurrentFocus(): number {
629
+ return currentFocusIndex;
630
+ },
631
+
632
+ getFocusableCount(): number {
633
+ return focusableElements.length;
634
+ }
635
+ };
636
+
637
+ return accessibilitySystem;
638
+ }
639
+
640
+ // Global accessibility system instance
641
+ export const accessibilitySystem = createAccessibilitySystem();
642
+
643
+ // Inject CSS support immediately for global instance (only in browser)
644
+ if (typeof document !== "undefined") {
645
+ accessibilitySystem.injectForcedColorsCSS();
646
+ }
647
+
648
+ // Utility function to make any chart accessible
649
+ export function makeChartAccessible(
650
+ chartContainer: d3.Selection<d3.BaseType, any, any, any>,
651
+ data: WaterfallDataItem[],
652
+ config: AccessibilityConfig = {}
653
+ ): ChartAccessibilityResult {
654
+ const a11y = createAccessibilitySystem();
655
+
656
+ // Inject forced colors CSS support (only in browser)
657
+ if (typeof document !== "undefined") {
658
+ a11y.injectForcedColorsCSS();
659
+ }
660
+
661
+ // Create live region for announcements
662
+ a11y.createLiveRegion(d3.select("body"));
663
+
664
+ // Create chart description
665
+ const descId = a11y.createChartDescription(chartContainer, data, config);
666
+
667
+ // Make chart elements accessible
668
+ const result = a11y.makeAccessible(chartContainer, data, config);
669
+
670
+ // Apply high contrast styles if needed
671
+ a11y.applyHighContrastStyles(chartContainer);
672
+
673
+ return {
674
+ accessibilitySystem: a11y,
675
+ descriptionId: descId,
676
+ focusableElements: result.focusableElements
677
+ };
678
+ }
679
+
680
+ export default createAccessibilitySystem;