energy-visualization-sankey 1.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/README.md +497 -0
- package/babel.config.cjs +28 -0
- package/coverage/clover.xml +6 -0
- package/coverage/coverage-final.json +1 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +101 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +0 -0
- package/demo-caching.js +68 -0
- package/dist/core/Sankey.d.ts +294 -0
- package/dist/core/Sankey.d.ts.map +1 -0
- package/dist/core/events/EventBus.d.ts +195 -0
- package/dist/core/events/EventBus.d.ts.map +1 -0
- package/dist/core/types/events.d.ts +42 -0
- package/dist/core/types/events.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/sankey.esm.js +5212 -0
- package/dist/sankey.esm.js.map +1 -0
- package/dist/sankey.standalone.esm.js +9111 -0
- package/dist/sankey.standalone.esm.js.map +1 -0
- package/dist/sankey.standalone.min.js +2 -0
- package/dist/sankey.standalone.min.js.map +1 -0
- package/dist/sankey.standalone.umd.js +9119 -0
- package/dist/sankey.standalone.umd.js.map +1 -0
- package/dist/sankey.umd.js +5237 -0
- package/dist/sankey.umd.js.map +1 -0
- package/dist/sankey.umd.min.js +2 -0
- package/dist/sankey.umd.min.js.map +1 -0
- package/dist/services/AnimationService.d.ts +229 -0
- package/dist/services/AnimationService.d.ts.map +1 -0
- package/dist/services/ConfigurationService.d.ts +173 -0
- package/dist/services/ConfigurationService.d.ts.map +1 -0
- package/dist/services/InteractionService.d.ts +377 -0
- package/dist/services/InteractionService.d.ts.map +1 -0
- package/dist/services/RenderingService.d.ts +152 -0
- package/dist/services/RenderingService.d.ts.map +1 -0
- package/dist/services/calculation/GraphService.d.ts +111 -0
- package/dist/services/calculation/GraphService.d.ts.map +1 -0
- package/dist/services/calculation/SummaryService.d.ts +149 -0
- package/dist/services/calculation/SummaryService.d.ts.map +1 -0
- package/dist/services/data/DataService.d.ts +167 -0
- package/dist/services/data/DataService.d.ts.map +1 -0
- package/dist/services/data/DataValidationService.d.ts +48 -0
- package/dist/services/data/DataValidationService.d.ts.map +1 -0
- package/dist/types/index.d.ts +189 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/Logger.d.ts +88 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/jest.config.cjs +20 -0
- package/package.json +68 -0
- package/rollup.config.js +131 -0
- package/scripts/performance-validation-real.js +411 -0
- package/scripts/validate-optimization.sh +147 -0
- package/scripts/visual-validation-real-data.js +374 -0
- package/src/core/Sankey.ts +1039 -0
- package/src/core/events/EventBus.ts +488 -0
- package/src/core/types/events.ts +80 -0
- package/src/index.ts +35 -0
- package/src/services/AnimationService.ts +983 -0
- package/src/services/ConfigurationService.ts +497 -0
- package/src/services/InteractionService.ts +920 -0
- package/src/services/RenderingService.ts +484 -0
- package/src/services/calculation/GraphService.ts +616 -0
- package/src/services/calculation/SummaryService.ts +394 -0
- package/src/services/data/DataService.ts +380 -0
- package/src/services/data/DataValidationService.ts +155 -0
- package/src/styles/controls.css +184 -0
- package/src/styles/sankey.css +211 -0
- package/src/types/index.ts +220 -0
- package/src/utils/Logger.ts +105 -0
- package/tests/numerical-validation.test.js +575 -0
- package/tests/setup.js +53 -0
- package/tsconfig.json +54 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import {EventBus} from '@/core/events/EventBus';
|
|
2
|
+
import {DataService} from "@/services/data/DataService";
|
|
3
|
+
import {AnimationService} from "@/services/AnimationService";
|
|
4
|
+
import {Logger} from "@/utils/Logger";
|
|
5
|
+
|
|
6
|
+
// ==================== INTERACTION INTERFACES ====================
|
|
7
|
+
|
|
8
|
+
type D3SVGSelection = d3.Selection<SVGSVGElement, unknown, HTMLElement, any>;
|
|
9
|
+
type D3DivSelection = d3.Selection<HTMLDivElement, unknown, HTMLElement, any>;
|
|
10
|
+
|
|
11
|
+
interface InteractionState {
|
|
12
|
+
isMouseDown: boolean;
|
|
13
|
+
lastMousePosition: { x: number; y: number };
|
|
14
|
+
selectedElement: Element | null;
|
|
15
|
+
isDragging: boolean;
|
|
16
|
+
touchStartTime: number;
|
|
17
|
+
keyboardShortcutsEnabled: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface InteractionHandlers {
|
|
21
|
+
onElementHover?: (element: Element, event: MouseEvent) => void;
|
|
22
|
+
onElementClick?: (element: Element, event: MouseEvent) => void;
|
|
23
|
+
onKeyboardNavigation?: (key: string, event: KeyboardEvent) => void;
|
|
24
|
+
onSliderInteraction?: (value: number, event: Event) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interaction Service - Advanced User Interface & Accessibility Management
|
|
29
|
+
*
|
|
30
|
+
* ARCHITECTURAL RESPONSIBILITY: Comprehensive User Interaction & Accessibility Framework
|
|
31
|
+
*
|
|
32
|
+
* This service implements sophisticated interaction patterns for multi-platform energy
|
|
33
|
+
* visualization, providing seamless mouse, touch, keyboard, and accessibility support.
|
|
34
|
+
* Ensures inclusive design principles and responsive user experience across all devices.
|
|
35
|
+
*
|
|
36
|
+
* ADVANCED INTERACTION PATTERNS:
|
|
37
|
+
* 1. **Multi-Modal Input**: Mouse, touch, and keyboard with context switching
|
|
38
|
+
* 2. **Event Delegation**: Efficient handling of dynamic SVG elements
|
|
39
|
+
* 3. **Accessibility Integration**: WCAG 2.1 compliance with screen reader support
|
|
40
|
+
* 4. **Cross-Platform Compatibility**: Desktop, tablet, and mobile optimization
|
|
41
|
+
* 5. **Performance Optimization**: Event debouncing and efficient listener management
|
|
42
|
+
* 6. **State Management**: Centralized interaction state for coordinated responses
|
|
43
|
+
*
|
|
44
|
+
* USER EXPERIENCE ARCHITECTURE:
|
|
45
|
+
* - **Mouse Interactions**: Precise hover, click, and drag operations
|
|
46
|
+
* - **Touch Interactions**: Gesture recognition with mobile-optimized responses
|
|
47
|
+
* - **Keyboard Navigation**: Full accessibility with arrow keys, space, enter
|
|
48
|
+
* - **Screen Reader Support**: ARIA labels and semantic markup integration
|
|
49
|
+
* - **Visual Feedback**: Immediate response to all user interactions
|
|
50
|
+
*
|
|
51
|
+
* ACCESSIBILITY FEATURES:
|
|
52
|
+
* - Keyboard-only navigation through all interactive elements
|
|
53
|
+
* - Screen reader compatibility with descriptive ARIA labels
|
|
54
|
+
* - High contrast support and focus management
|
|
55
|
+
* - Mobile accessibility with proper touch target sizing
|
|
56
|
+
* - Semantic HTML structure for assistive technologies
|
|
57
|
+
*
|
|
58
|
+
* EVENT-DRIVEN INTEGRATION:
|
|
59
|
+
* Communicates through event bus for loose coupling with other services,
|
|
60
|
+
* enabling coordinated responses without direct dependencies.
|
|
61
|
+
*/
|
|
62
|
+
export class InteractionService {
|
|
63
|
+
// INTERACTION STATE MANAGEMENT: Centralized multi-modal input tracking
|
|
64
|
+
// Maintains state for mouse, touch, keyboard, and accessibility interactions
|
|
65
|
+
private state: InteractionState = {
|
|
66
|
+
isMouseDown: false, // Mouse button state for drag detection
|
|
67
|
+
lastMousePosition: {x: 0, y: 0}, // Cursor position for tooltip positioning
|
|
68
|
+
selectedElement: null, // Currently focused/selected element
|
|
69
|
+
isDragging: false, // Drag operation state flag
|
|
70
|
+
touchStartTime: 0, // Touch gesture timing for tap vs drag
|
|
71
|
+
keyboardShortcutsEnabled: true // Global keyboard accessibility state
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// INTERACTION HANDLER REGISTRY: Customizable interaction callbacks
|
|
75
|
+
// Enables flexible response patterns for different interaction types
|
|
76
|
+
private handlers: InteractionHandlers = {};
|
|
77
|
+
|
|
78
|
+
// EVENT LISTENER MANAGEMENT: Centralized cleanup for memory leak prevention
|
|
79
|
+
// Tracks all registered listeners for proper disposal during service cleanup
|
|
80
|
+
private eventListeners: Array<{
|
|
81
|
+
element: Element | Document | Window;
|
|
82
|
+
event: string;
|
|
83
|
+
handler: EventListener
|
|
84
|
+
}> = [];
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
private animationControlService: AnimationService,
|
|
88
|
+
private dataService: DataService,
|
|
89
|
+
private eventBus: EventBus,
|
|
90
|
+
private logger: Logger,
|
|
91
|
+
) {
|
|
92
|
+
// INTERACTION SERVICE READY: Multi-platform user interface management initialized
|
|
93
|
+
// All interaction patterns and accessibility features are now available
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize Comprehensive Multi-Platform User Interactions
|
|
98
|
+
*
|
|
99
|
+
* INITIALIZATION RESPONSIBILITY: Complete interaction system setup across all input modalities
|
|
100
|
+
*
|
|
101
|
+
* This method orchestrates the setup of all user interaction capabilities, creating
|
|
102
|
+
* a comprehensive interface layer that supports mouse, touch, keyboard, and accessibility
|
|
103
|
+
* interactions for the energy visualization.
|
|
104
|
+
*
|
|
105
|
+
* MULTI-MODAL INTERACTION INITIALIZATION:
|
|
106
|
+
* 1. **Mouse Interactions**: Hover effects, click handling, drag operations
|
|
107
|
+
* 2. **Touch Interactions**: Mobile gestures with responsive feedback
|
|
108
|
+
* 3. **Keyboard Navigation**: Full accessibility with arrow key navigation
|
|
109
|
+
* 4. **Slider Controls**: Timeline interaction with precise positioning
|
|
110
|
+
* 5. **Button Controls**: Play/pause and control button event handling
|
|
111
|
+
* 6. **Accessibility Features**: WCAG 2.1 compliance with screen reader support
|
|
112
|
+
*
|
|
113
|
+
* CROSS-PLATFORM COMPATIBILITY:
|
|
114
|
+
* Automatically detects device capabilities (touch support, keyboard availability)
|
|
115
|
+
* and adapts interaction patterns for optimal user experience on each platform.
|
|
116
|
+
*
|
|
117
|
+
* EVENT SYSTEM INTEGRATION:
|
|
118
|
+
* Broadcasts initialization completion to coordinate with other services,
|
|
119
|
+
* enabling system-wide awareness of interaction capability readiness.
|
|
120
|
+
*/
|
|
121
|
+
public initializeInteractions(svg: D3SVGSelection, tooltip: D3DivSelection): void {
|
|
122
|
+
this.logger.log('InteractionService: Initializing comprehensive multi-platform interactions...');
|
|
123
|
+
|
|
124
|
+
// MOUSE INTERACTION SYSTEM: Desktop precision interactions
|
|
125
|
+
// Handles hover effects, precise clicking, and drag operations
|
|
126
|
+
this.setupMouseInteractions(svg, tooltip);
|
|
127
|
+
|
|
128
|
+
// TOUCH INTERACTION SYSTEM: Mobile-optimized gesture recognition
|
|
129
|
+
// Provides responsive touch feedback and mobile-friendly interactions
|
|
130
|
+
this.setupTouchInteractions(svg, tooltip);
|
|
131
|
+
|
|
132
|
+
// KEYBOARD ACCESSIBILITY SYSTEM: Full navigation without mouse
|
|
133
|
+
// Enables complete visualization control through keyboard shortcuts
|
|
134
|
+
this.enableKeyboardNavigation();
|
|
135
|
+
|
|
136
|
+
// TIMELINE SLIDER SYSTEM: Interactive temporal navigation
|
|
137
|
+
// Provides precise year selection and smooth timeline scrubbing
|
|
138
|
+
this.setupSliderInteractions();
|
|
139
|
+
|
|
140
|
+
// CONTROL BUTTON SYSTEM: Play/pause and interface controls
|
|
141
|
+
// Handles all button interactions with proper state management
|
|
142
|
+
this.setupButtonInteractions();
|
|
143
|
+
|
|
144
|
+
// ACCESSIBILITY COMPLIANCE SYSTEM: WCAG 2.1 standards implementation
|
|
145
|
+
// Ensures screen reader compatibility and inclusive design
|
|
146
|
+
// this.setupAccessibilityFeatures(svg);
|
|
147
|
+
|
|
148
|
+
// EVENT SYSTEM NOTIFICATION: Broadcast interaction readiness
|
|
149
|
+
// Enables coordinated initialization across the visualization system
|
|
150
|
+
this.eventBus.emit({
|
|
151
|
+
type: 'system.initialized',
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
source: 'InteractionService',
|
|
154
|
+
data: {
|
|
155
|
+
mouseEnabled: true, // Desktop mouse interactions active
|
|
156
|
+
touchEnabled: 'ontouchstart' in window, // Touch capability detection
|
|
157
|
+
keyboardEnabled: this.state.keyboardShortcutsEnabled // Accessibility navigation active
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.logger.log('InteractionService: All interaction modalities initialized successfully');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Set up mouse event handlers
|
|
166
|
+
* Provides hover effects, click handling, and drag support
|
|
167
|
+
*/
|
|
168
|
+
private setupMouseInteractions(svg: D3SVGSelection, tooltip: D3DivSelection): void {
|
|
169
|
+
// SVG mouse events
|
|
170
|
+
const svgElement = svg.node();
|
|
171
|
+
if (!svgElement) return;
|
|
172
|
+
|
|
173
|
+
// Mouse move for tooltips and hover effects
|
|
174
|
+
const mouseMoveHandler = (event: MouseEvent) => {
|
|
175
|
+
this.state.lastMousePosition = {x: event.clientX, y: event.clientY};
|
|
176
|
+
|
|
177
|
+
// Handle element hovering
|
|
178
|
+
const target = event.target as Element;
|
|
179
|
+
if (target && (target.classList.contains('flow') || target.classList.contains('box'))) {
|
|
180
|
+
this.handleElementHover(target, event);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Mouse click for element selection
|
|
185
|
+
const mouseClickHandler = (event: MouseEvent) => {
|
|
186
|
+
const target = event.target as Element;
|
|
187
|
+
if (target) {
|
|
188
|
+
this.handleElementClick(target, event);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Mouse down for drag start
|
|
193
|
+
const mouseDownHandler = (event: MouseEvent) => {
|
|
194
|
+
this.state.isMouseDown = true;
|
|
195
|
+
this.state.selectedElement = event.target as Element;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Mouse up for drag end
|
|
199
|
+
const mouseUpHandler = () => {
|
|
200
|
+
this.state.isMouseDown = false;
|
|
201
|
+
this.state.selectedElement = null;
|
|
202
|
+
this.state.isDragging = false;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Add event listeners
|
|
206
|
+
this.addEventListener(svgElement, 'mousemove', mouseMoveHandler as EventListener);
|
|
207
|
+
this.addEventListener(svgElement, 'click', mouseClickHandler as EventListener);
|
|
208
|
+
this.addEventListener(svgElement, 'mousedown', mouseDownHandler as EventListener);
|
|
209
|
+
this.addEventListener(document, 'mouseup', mouseUpHandler);
|
|
210
|
+
|
|
211
|
+
this.logger.log('InteractionService: Mouse interactions enabled');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set up touch event handlers
|
|
216
|
+
* Provides mobile-friendly touch interactions
|
|
217
|
+
*/
|
|
218
|
+
private setupTouchInteractions(svg: D3SVGSelection, tooltip: D3DivSelection): void {
|
|
219
|
+
const svgElement = svg.node();
|
|
220
|
+
if (!svgElement || !('ontouchstart' in window)) return;
|
|
221
|
+
|
|
222
|
+
// Touch start
|
|
223
|
+
const touchStartHandler = (event: TouchEvent) => {
|
|
224
|
+
this.state.touchStartTime = Date.now();
|
|
225
|
+
const touch = event.touches[0];
|
|
226
|
+
if (touch) {
|
|
227
|
+
this.state.lastMousePosition = {x: touch.clientX, y: touch.clientY};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Touch end - handle as click if short duration
|
|
232
|
+
const touchEndHandler = (event: TouchEvent) => {
|
|
233
|
+
const touchDuration = Date.now() - this.state.touchStartTime;
|
|
234
|
+
|
|
235
|
+
// Treat short touches as clicks
|
|
236
|
+
if (touchDuration < 300) {
|
|
237
|
+
const touch = event.changedTouches[0];
|
|
238
|
+
if (touch) {
|
|
239
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
240
|
+
if (target) {
|
|
241
|
+
// Create synthetic mouse event for compatibility
|
|
242
|
+
const syntheticEvent = new MouseEvent('click', {
|
|
243
|
+
clientX: touch.clientX,
|
|
244
|
+
clientY: touch.clientY,
|
|
245
|
+
bubbles: true
|
|
246
|
+
});
|
|
247
|
+
this.handleElementClick(target, syntheticEvent);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Touch move for dragging
|
|
254
|
+
const touchMoveHandler = (event: TouchEvent) => {
|
|
255
|
+
event.preventDefault(); // Prevent scrolling
|
|
256
|
+
const touch = event.touches[0];
|
|
257
|
+
if (touch) {
|
|
258
|
+
this.state.lastMousePosition = {x: touch.clientX, y: touch.clientY};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Add touch event listeners
|
|
263
|
+
this.addEventListener(svgElement, 'touchstart', touchStartHandler as EventListener);
|
|
264
|
+
this.addEventListener(svgElement, 'touchend', touchEndHandler as EventListener);
|
|
265
|
+
this.addEventListener(svgElement, 'touchmove', touchMoveHandler as EventListener);
|
|
266
|
+
|
|
267
|
+
this.logger.log('InteractionService: Touch interactions enabled');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Enable keyboard navigation shortcuts
|
|
272
|
+
*
|
|
273
|
+
* **Accessibility Enhancement Responsibility:**
|
|
274
|
+
* - WCAG 2.1 AA compliance for keyboard navigation
|
|
275
|
+
* - Document-level keyboard event delegation
|
|
276
|
+
* - Form input safety (prevents interference)
|
|
277
|
+
* - Global shortcut system activation
|
|
278
|
+
*
|
|
279
|
+
* **Keyboard Navigation Architecture:**
|
|
280
|
+
* - Document Event Delegation: Single handler for all keyboard events
|
|
281
|
+
* - Input Safety: Excludes form fields from shortcut processing
|
|
282
|
+
* - State Management: Prevents duplicate handler registration
|
|
283
|
+
* - Event System Integration: Coordinates with EventBus
|
|
284
|
+
*
|
|
285
|
+
* **Performance Optimization:**
|
|
286
|
+
* - Single document listener (efficient delegation pattern)
|
|
287
|
+
* - Early return for duplicate activation
|
|
288
|
+
* - Form input filtering (performance + UX)
|
|
289
|
+
*
|
|
290
|
+
* **Accessibility Standards:**
|
|
291
|
+
* - Follows ARIA keyboard navigation patterns
|
|
292
|
+
* - Provides alternative to mouse interaction
|
|
293
|
+
* - Ensures consistent cross-platform behavior
|
|
294
|
+
*/
|
|
295
|
+
public enableKeyboardNavigation(): void {
|
|
296
|
+
if (this.state.keyboardShortcutsEnabled) return;
|
|
297
|
+
|
|
298
|
+
const keydownHandler = (event: KeyboardEvent) => {
|
|
299
|
+
// Don't interfere with form inputs - accessibility best practice
|
|
300
|
+
// Prevents shortcuts from disrupting user typing in form fields
|
|
301
|
+
if (event.target instanceof HTMLInputElement ||
|
|
302
|
+
event.target instanceof HTMLTextAreaElement ||
|
|
303
|
+
event.target instanceof HTMLSelectElement) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.handleKeyboardNavigation(event);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.addEventListener(document, 'keydown', keydownHandler as EventListener);
|
|
311
|
+
this.state.keyboardShortcutsEnabled = true;
|
|
312
|
+
|
|
313
|
+
this.logger.log('InteractionService: Keyboard navigation enabled');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Disable keyboard navigation
|
|
318
|
+
*
|
|
319
|
+
* **Resource Management Responsibility:**
|
|
320
|
+
* - Graceful keyboard navigation shutdown
|
|
321
|
+
* - State cleanup coordination
|
|
322
|
+
* - Memory leak prevention (handlers cleaned by cleanup())
|
|
323
|
+
*
|
|
324
|
+
* **Accessibility Pattern:**
|
|
325
|
+
* - Allows dynamic keyboard navigation toggling
|
|
326
|
+
* - Maintains system state consistency
|
|
327
|
+
* - Prepares for service disposal
|
|
328
|
+
*/
|
|
329
|
+
public disableKeyboardNavigation(): void {
|
|
330
|
+
this.state.keyboardShortcutsEnabled = false;
|
|
331
|
+
// Event listeners will be cleaned up by cleanup() method
|
|
332
|
+
this.logger.log('InteractionService: Keyboard navigation disabled');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Handle keyboard navigation events
|
|
337
|
+
*
|
|
338
|
+
* **Keyboard Shortcuts Responsibility:**
|
|
339
|
+
* - Comprehensive animation control via keyboard
|
|
340
|
+
* - Standard accessibility key mapping
|
|
341
|
+
* - Cross-platform keyboard compatibility
|
|
342
|
+
* - Event delegation and custom handler integration
|
|
343
|
+
*
|
|
344
|
+
* **Standard Keyboard Mappings (WCAG 2.1 AA):**
|
|
345
|
+
* - Space/Enter: Play/Pause toggle (standard media controls)
|
|
346
|
+
* - Arrow Left/Right: Year navigation (standard timeline controls)
|
|
347
|
+
* - Home/End: First/Last year navigation (standard list navigation)
|
|
348
|
+
* - Escape: Pause/Stop action (standard escape behavior)
|
|
349
|
+
*
|
|
350
|
+
* **Event-Driven Architecture Integration:**
|
|
351
|
+
* - Emits structured keyboard events to EventBus
|
|
352
|
+
* - Captures modifier keys (Ctrl, Shift, Alt) for advanced interactions
|
|
353
|
+
* - Integrates with AnimationService for timeline control
|
|
354
|
+
* - Supports custom keyboard handlers via callback pattern
|
|
355
|
+
*
|
|
356
|
+
* **Accessibility Design Principles:**
|
|
357
|
+
* - Follows operating system keyboard conventions
|
|
358
|
+
* - Provides equivalent functionality to mouse interactions
|
|
359
|
+
* - Preventable default behavior (event.preventDefault())
|
|
360
|
+
* - Comprehensive modifier key support for power users
|
|
361
|
+
*/
|
|
362
|
+
private handleKeyboardNavigation(event: KeyboardEvent): void {
|
|
363
|
+
const key = event.key.toLowerCase();
|
|
364
|
+
|
|
365
|
+
// Animation controls - Standard accessibility keyboard shortcuts
|
|
366
|
+
// Following WCAG 2.1 AA guidelines for media and timeline controls
|
|
367
|
+
switch (key) {
|
|
368
|
+
case ' ':
|
|
369
|
+
case 'enter':
|
|
370
|
+
// Space or Enter: Toggle play/pause (standard media control)
|
|
371
|
+
// Equivalent to clicking play button - primary interaction
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
if (this.animationControlService.isPlaying()) {
|
|
374
|
+
this.animationControlService.pause();
|
|
375
|
+
} else {
|
|
376
|
+
this.animationControlService.play();
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'arrowleft':
|
|
381
|
+
// Left arrow: Previous year (standard timeline navigation)
|
|
382
|
+
// Provides granular control for year-by-year analysis
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
this.animationControlService.previousYear();
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
case 'arrowright':
|
|
388
|
+
// Right arrow: Next year (standard timeline navigation)
|
|
389
|
+
// Provides granular control for year-by-year analysis
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
this.animationControlService.nextYear();
|
|
392
|
+
break;
|
|
393
|
+
|
|
394
|
+
case 'home':
|
|
395
|
+
// Home: Go to first year (standard list/timeline navigation)
|
|
396
|
+
// Quick jump to beginning of energy data timeline
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
this.animationControlService.setYear(this.dataService.firstYear);
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
case 'end':
|
|
402
|
+
// End: Go to last year (standard list/timeline navigation)
|
|
403
|
+
// Quick jump to most recent energy data point
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
this.animationControlService.setYear(this.dataService.lastYear);
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'escape':
|
|
409
|
+
// Escape: Pause animation (standard escape behavior)
|
|
410
|
+
// Emergency stop functionality - accessibility requirement
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
this.animationControlService.pause();
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Emit keyboard navigation event - Event-Driven Architecture Integration
|
|
417
|
+
// Enables other services to react to keyboard interactions
|
|
418
|
+
// Provides rich event context (key + modifiers) for advanced interactions
|
|
419
|
+
this.eventBus.emit({
|
|
420
|
+
type: 'interaction.keypress',
|
|
421
|
+
timestamp: Date.now(),
|
|
422
|
+
source: 'InteractionService',
|
|
423
|
+
data: {
|
|
424
|
+
key, // Primary key pressed (normalized to lowercase)
|
|
425
|
+
ctrlKey: event.ctrlKey, // Enables Ctrl+key shortcuts
|
|
426
|
+
shiftKey: event.shiftKey, // Enables Shift+key shortcuts
|
|
427
|
+
altKey: event.altKey // Enables Alt+key shortcuts (power user features)
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Call custom handler if provided - Extensibility Pattern
|
|
432
|
+
// Allows external code to extend keyboard navigation behavior
|
|
433
|
+
// Supports application-specific keyboard shortcuts beyond standard set
|
|
434
|
+
if (this.handlers.onKeyboardNavigation) {
|
|
435
|
+
this.handlers.onKeyboardNavigation(key, event);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Set up slider interactions
|
|
441
|
+
*
|
|
442
|
+
* **Timeline Control Responsibility:**
|
|
443
|
+
* - Direct year selection via range slider
|
|
444
|
+
* - Real-time year value feedback
|
|
445
|
+
* - Smooth animation coordination
|
|
446
|
+
* - Event-driven timeline synchronization
|
|
447
|
+
*
|
|
448
|
+
* **User Control Patterns:**
|
|
449
|
+
* - HTML5 range input integration (native accessibility)
|
|
450
|
+
* - Continuous value updates (smooth interaction)
|
|
451
|
+
* - Animation service coordination (timeline sync)
|
|
452
|
+
* - Custom event emission (extensibility)
|
|
453
|
+
*
|
|
454
|
+
* **Accessibility Features:**
|
|
455
|
+
* - Native keyboard support (arrow keys, page up/down)
|
|
456
|
+
* - Screen reader compatibility (range slider semantics)
|
|
457
|
+
* - Touch-friendly interaction (mobile/tablet support)
|
|
458
|
+
* - Visual feedback during interaction
|
|
459
|
+
*
|
|
460
|
+
* **Performance Considerations:**
|
|
461
|
+
* - Direct DOM element access (cached reference)
|
|
462
|
+
* - Efficient value parsing (parseFloat optimization)
|
|
463
|
+
* - Event delegation pattern (single handler)
|
|
464
|
+
*/
|
|
465
|
+
private setupSliderInteractions(): void {
|
|
466
|
+
const rangeSlider = document.getElementById('rangeSlider') as HTMLInputElement;
|
|
467
|
+
if (!rangeSlider) return;
|
|
468
|
+
|
|
469
|
+
const sliderInputHandler = (event: Event) => {
|
|
470
|
+
const target = event.target as HTMLInputElement;
|
|
471
|
+
const year = parseFloat(target.value); // Convert string value to numeric year
|
|
472
|
+
|
|
473
|
+
// Update animation to selected year - Direct timeline control
|
|
474
|
+
// Triggers visualization update to show energy data for selected year
|
|
475
|
+
this.animationControlService.setYear(year);
|
|
476
|
+
|
|
477
|
+
// Emit slider interaction event - Event-Driven Architecture
|
|
478
|
+
// Enables other services to react to timeline position changes
|
|
479
|
+
// Provides structured event data for logging and analytics
|
|
480
|
+
this.eventBus.emit({
|
|
481
|
+
type: 'interaction.slider',
|
|
482
|
+
timestamp: Date.now(),
|
|
483
|
+
source: 'InteractionService',
|
|
484
|
+
data: {
|
|
485
|
+
year, // Selected year value (numeric)
|
|
486
|
+
value: year // Duplicate for backwards compatibility
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Call custom handler if provided - Extensibility Pattern
|
|
491
|
+
// Supports application-specific slider interaction behavior
|
|
492
|
+
// Enables custom year selection logic or additional UI updates
|
|
493
|
+
if (this.handlers.onSliderInteraction) {
|
|
494
|
+
this.handlers.onSliderInteraction(year, event);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
this.addEventListener(rangeSlider, 'input', sliderInputHandler);
|
|
499
|
+
this.logger.log('InteractionService: Slider interactions enabled');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Set up button interactions
|
|
504
|
+
*
|
|
505
|
+
* **Control Button Responsibility:**
|
|
506
|
+
* - Play/pause animation control
|
|
507
|
+
* - Speed adjustment controls
|
|
508
|
+
* - Animation state management
|
|
509
|
+
* - User feedback coordination
|
|
510
|
+
*
|
|
511
|
+
* **User Interface Patterns:**
|
|
512
|
+
* - Primary action button (play/pause toggle)
|
|
513
|
+
* - Secondary action buttons (speed controls)
|
|
514
|
+
* - Event delegation for button groups
|
|
515
|
+
* - State-aware button behavior
|
|
516
|
+
*
|
|
517
|
+
* **Accessibility Integration:**
|
|
518
|
+
* - Click event handling (mouse + keyboard activation)
|
|
519
|
+
* - Focus management (keyboard navigation)
|
|
520
|
+
* - Screen reader button semantics
|
|
521
|
+
* - Touch-friendly tap targets
|
|
522
|
+
*
|
|
523
|
+
* **Animation Control Architecture:**
|
|
524
|
+
* - Direct AnimationService integration
|
|
525
|
+
* - State synchronization (playing/paused)
|
|
526
|
+
* - Speed control coordination
|
|
527
|
+
* - Event emission for system coordination
|
|
528
|
+
*/
|
|
529
|
+
private setupButtonInteractions(): void {
|
|
530
|
+
// Play/pause button - Primary animation control
|
|
531
|
+
const playButton = document.getElementById('play-button');
|
|
532
|
+
if (playButton) {
|
|
533
|
+
const playButtonHandler = (event: Event) => {
|
|
534
|
+
event.preventDefault(); // Prevent default button behavior
|
|
535
|
+
|
|
536
|
+
// Toggle animation state - Primary user interaction
|
|
537
|
+
// Provides immediate visual feedback through state change
|
|
538
|
+
if (this.animationControlService.isPlaying()) {
|
|
539
|
+
this.animationControlService.pause();
|
|
540
|
+
} else {
|
|
541
|
+
this.animationControlService.play();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Emit button interaction event - Event-Driven Architecture
|
|
545
|
+
// Enables other services to react to animation state changes
|
|
546
|
+
// Note: Action reflects the RESULT of the button press, not the current state
|
|
547
|
+
this.eventBus.emit({
|
|
548
|
+
type: 'interaction.button',
|
|
549
|
+
timestamp: Date.now(),
|
|
550
|
+
source: 'InteractionService',
|
|
551
|
+
data: {
|
|
552
|
+
buttonId: 'play-button',
|
|
553
|
+
action: this.animationControlService.isPlaying() ? 'play' : 'pause'
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
this.addEventListener(playButton, 'click', playButtonHandler);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Speed control buttons (if they exist) - Secondary animation controls
|
|
562
|
+
// Supports variable animation speeds for different analysis needs
|
|
563
|
+
const speedButtons = document.querySelectorAll('[data-speed]');
|
|
564
|
+
speedButtons.forEach(button => {
|
|
565
|
+
const speedButtonHandler = (event: Event) => {
|
|
566
|
+
const target = event.target as HTMLElement;
|
|
567
|
+
const speed = parseInt(target.dataset.speed || '200'); // Default 200ms if no speed specified
|
|
568
|
+
|
|
569
|
+
// Update animation speed - Performance and UX customization
|
|
570
|
+
// Allows users to adjust viewing pace for their analysis needs
|
|
571
|
+
this.animationControlService.setSpeed(speed);
|
|
572
|
+
|
|
573
|
+
// Emit speed change event - Event-Driven Architecture
|
|
574
|
+
// Enables UI updates (active button state, speed indicator)
|
|
575
|
+
this.eventBus.emit({
|
|
576
|
+
type: 'interaction.button',
|
|
577
|
+
timestamp: Date.now(),
|
|
578
|
+
source: 'InteractionService',
|
|
579
|
+
data: {
|
|
580
|
+
buttonId: target.id,
|
|
581
|
+
action: 'speed-change',
|
|
582
|
+
speed // New animation speed in milliseconds
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
this.addEventListener(button, 'click', speedButtonHandler);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
this.logger.log('InteractionService: Button interactions enabled');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Set up accessibility features
|
|
595
|
+
*
|
|
596
|
+
* **WCAG 2.1 AA Compliance Responsibility:**
|
|
597
|
+
* - ARIA labels and semantic roles
|
|
598
|
+
* - Keyboard focus management
|
|
599
|
+
* - Screen reader support
|
|
600
|
+
* - Visual focus indicators
|
|
601
|
+
* - Interactive element accessibility
|
|
602
|
+
*
|
|
603
|
+
* **Accessibility Standards Implementation:**
|
|
604
|
+
* - SVG accessibility (role="img", aria-label)
|
|
605
|
+
* - Keyboard navigation (tabindex, focus/blur)
|
|
606
|
+
* - Control accessibility (slider, button roles)
|
|
607
|
+
* - Focus indicators (visual outline feedback)
|
|
608
|
+
* - Screen reader descriptions (meaningful labels)
|
|
609
|
+
*
|
|
610
|
+
* **Universal Design Principles:**
|
|
611
|
+
* - Perceivable: Visual focus indicators, semantic structure
|
|
612
|
+
* - Operable: Keyboard navigation, touch targets
|
|
613
|
+
* - Understandable: Clear labels, consistent behavior
|
|
614
|
+
* - Robust: Cross-platform compatibility, assistive technology support
|
|
615
|
+
*
|
|
616
|
+
* **Focus Management Architecture:**
|
|
617
|
+
* - Logical tab order (sequential navigation)
|
|
618
|
+
* - Visual focus indicators (CSS outline styling)
|
|
619
|
+
* - Focus trap prevention (proper event handling)
|
|
620
|
+
* - Screen reader announcements (ARIA integration)
|
|
621
|
+
*/
|
|
622
|
+
private setupAccessibilityFeatures(svg: D3SVGSelection): void {
|
|
623
|
+
const svgElement = svg.node();
|
|
624
|
+
if (!svgElement) return;
|
|
625
|
+
|
|
626
|
+
// Add ARIA label to SVG - Screen Reader Support
|
|
627
|
+
// Provides semantic meaning for complex data visualization
|
|
628
|
+
svgElement.setAttribute('role', 'img');
|
|
629
|
+
svgElement.setAttribute('aria-label', 'Interactive U.S. Energy Usage Sankey Diagram');
|
|
630
|
+
|
|
631
|
+
// Add tabindex for keyboard navigation - Focus Management
|
|
632
|
+
// Makes SVG focusable for keyboard users (WCAG 2.1 requirement)
|
|
633
|
+
svgElement.setAttribute('tabindex', '0');
|
|
634
|
+
|
|
635
|
+
// Add focus indicators - Visual Accessibility
|
|
636
|
+
// Provides clear visual feedback when SVG receives keyboard focus
|
|
637
|
+
const focusHandler = () => {
|
|
638
|
+
svgElement.style.outline = '2px solid #007cba'; // High contrast focus indicator
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const blurHandler = () => {
|
|
642
|
+
svgElement.style.outline = 'none'; // Remove outline when focus lost
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
this.addEventListener(svgElement, 'focus', focusHandler);
|
|
646
|
+
this.addEventListener(svgElement, 'blur', blurHandler);
|
|
647
|
+
|
|
648
|
+
// Add accessibility to controls - Control Semantic Enhancement
|
|
649
|
+
// Range slider accessibility - Timeline control semantics
|
|
650
|
+
const rangeSlider = document.getElementById('rangeSlider');
|
|
651
|
+
if (rangeSlider) {
|
|
652
|
+
rangeSlider.setAttribute('aria-label', 'Select year for energy data visualization');
|
|
653
|
+
rangeSlider.setAttribute('role', 'slider'); // Explicit slider semantics
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Play button accessibility - Animation control semantics
|
|
657
|
+
const playButton = document.getElementById('play-button');
|
|
658
|
+
if (playButton) {
|
|
659
|
+
playButton.setAttribute('aria-label', 'Play or pause animation');
|
|
660
|
+
playButton.setAttribute('role', 'button'); // Explicit button semantics
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.logger.log('InteractionService: Accessibility features enabled');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Handle element hover events
|
|
668
|
+
*
|
|
669
|
+
* **Visual Feedback Responsibility:**
|
|
670
|
+
* - Interactive element highlighting
|
|
671
|
+
* - Hover state management
|
|
672
|
+
* - Multi-element coordination (exclusive hover)
|
|
673
|
+
* - Event-driven hover notifications
|
|
674
|
+
*
|
|
675
|
+
* **User Experience Patterns:**
|
|
676
|
+
* - Visual Affordance: Immediate hover feedback via CSS classes
|
|
677
|
+
* - Exclusive Interaction: Single element hover state (removes others)
|
|
678
|
+
* - Event Context: Rich hover event data (element type, fuel, sector)
|
|
679
|
+
* - Mouse Position: Coordinates for tooltip positioning
|
|
680
|
+
*
|
|
681
|
+
* **Interaction Architecture:**
|
|
682
|
+
* - CSS Class-Based Styling: .hovered class for visual feedback
|
|
683
|
+
* - Event Bus Integration: Structured hover events for system coordination
|
|
684
|
+
* - Custom Handler Support: Extensible hover behavior
|
|
685
|
+
* - Element Classification: Flow vs Box element detection
|
|
686
|
+
*
|
|
687
|
+
* **Performance Considerations:**
|
|
688
|
+
* - Efficient DOM querying (.hovered class removal)
|
|
689
|
+
* - Minimal DOM manipulation (add/remove classes)
|
|
690
|
+
* - Event data extraction (cached attribute access)
|
|
691
|
+
*/
|
|
692
|
+
private handleElementHover(element: Element, event: MouseEvent): void {
|
|
693
|
+
// Add hover class for styling - Visual Feedback
|
|
694
|
+
// Triggers CSS styles for immediate visual response
|
|
695
|
+
element.classList.add('hovered');
|
|
696
|
+
|
|
697
|
+
// Remove hover class from other elements - Exclusive Hover Pattern
|
|
698
|
+
// Ensures only one element shows hover state at a time (better UX)
|
|
699
|
+
document.querySelectorAll('.hovered').forEach(el => {
|
|
700
|
+
if (el !== element) {
|
|
701
|
+
el.classList.remove('hovered');
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Emit hover event - Event-Driven Architecture
|
|
706
|
+
// Provides rich context for tooltip display, analytics, and custom behavior
|
|
707
|
+
this.eventBus.emit({
|
|
708
|
+
type: 'interaction.hover',
|
|
709
|
+
timestamp: Date.now(),
|
|
710
|
+
source: 'InteractionService',
|
|
711
|
+
data: {
|
|
712
|
+
elementType: element.classList.contains('flow') ? 'flow' : 'box', // Element classification
|
|
713
|
+
fuel: element.getAttribute('data-fuel'), // Energy source identification
|
|
714
|
+
sector: element.getAttribute('data-sector'), // Consumption sector identification
|
|
715
|
+
mousePosition: {x: event.clientX, y: event.clientY} // Coordinates for tooltip positioning
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Call custom handler if provided - Extensibility Pattern
|
|
720
|
+
// Supports application-specific hover behavior
|
|
721
|
+
// Enables custom tooltip content, highlighting, or data display
|
|
722
|
+
if (this.handlers.onElementHover) {
|
|
723
|
+
this.handlers.onElementHover(element, event);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Handle element click events
|
|
729
|
+
*
|
|
730
|
+
* **Selection Management Responsibility:**
|
|
731
|
+
* - Element selection state management
|
|
732
|
+
* - Exclusive selection pattern (single selection)
|
|
733
|
+
* - Visual selection feedback
|
|
734
|
+
* - Event-driven selection notifications
|
|
735
|
+
*
|
|
736
|
+
* **User Interaction Patterns:**
|
|
737
|
+
* - Click-to-Select: Primary selection mechanism
|
|
738
|
+
* - Exclusive Selection: Single element selected at a time
|
|
739
|
+
* - Visual Feedback: .selected class for styling
|
|
740
|
+
* - Context Preservation: Rich click event data
|
|
741
|
+
*
|
|
742
|
+
* **State Management Architecture:**
|
|
743
|
+
* - CSS Class-Based Selection: .selected class for visual state
|
|
744
|
+
* - DOM State Management: Clear previous selections
|
|
745
|
+
* - Event Data Capture: Element type, fuel, sector, coordinates
|
|
746
|
+
* - Extensible Handler Support: Custom click behavior
|
|
747
|
+
*
|
|
748
|
+
* **Integration Benefits:**
|
|
749
|
+
* - Analytics Support: Click tracking and user behavior
|
|
750
|
+
* - Tooltip Coordination: Click-based detailed information display
|
|
751
|
+
* - Custom Behavior: Application-specific selection actions
|
|
752
|
+
*/
|
|
753
|
+
private handleElementClick(element: Element, event: MouseEvent): void {
|
|
754
|
+
// Clear all previous selections and add selected class - Exclusive Selection Pattern
|
|
755
|
+
// Ensures only one element is selected at a time for focused analysis
|
|
756
|
+
// document.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
|
|
757
|
+
// element.classList.add('selected');
|
|
758
|
+
|
|
759
|
+
// Emit click event - Event-Driven Architecture
|
|
760
|
+
// Provides comprehensive click context for analytics, tooltips, and custom behavior
|
|
761
|
+
this.eventBus.emit({
|
|
762
|
+
type: 'interaction.click',
|
|
763
|
+
timestamp: Date.now(),
|
|
764
|
+
source: 'InteractionService',
|
|
765
|
+
data: {
|
|
766
|
+
elementType: element.classList.contains('flow') ? 'flow' : 'box', // Element classification
|
|
767
|
+
fuel: element.getAttribute('data-fuel'), // Energy source identification
|
|
768
|
+
sector: element.getAttribute('data-sector'), // Consumption sector identification
|
|
769
|
+
mousePosition: {x: event.clientX, y: event.clientY} // Click coordinates for UI positioning
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Call custom handler if provided - Extensibility Pattern
|
|
774
|
+
// Supports application-specific click behavior
|
|
775
|
+
// Enables detailed data display, drill-down functionality, or custom actions
|
|
776
|
+
if (this.handlers.onElementClick) {
|
|
777
|
+
this.handlers.onElementClick(element, event);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Set custom interaction handlers
|
|
783
|
+
*/
|
|
784
|
+
public setInteractionHandlers(handlers: InteractionHandlers): void {
|
|
785
|
+
this.handlers = {...this.handlers, ...handlers};
|
|
786
|
+
this.logger.log('InteractionService: Custom handlers updated');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Helper method to add event listener and track for cleanup
|
|
791
|
+
*/
|
|
792
|
+
private addEventListener(
|
|
793
|
+
element: Element | Document | Window,
|
|
794
|
+
event: string,
|
|
795
|
+
handler: EventListener
|
|
796
|
+
): void {
|
|
797
|
+
element.addEventListener(event, handler);
|
|
798
|
+
this.eventListeners.push({element, event, handler});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Clean up all interactions and event listeners
|
|
803
|
+
*
|
|
804
|
+
* **Resource Management Responsibility:**
|
|
805
|
+
* - Complete memory cleanup and leak prevention
|
|
806
|
+
* - Event listener disposal
|
|
807
|
+
* - State reset and consistency
|
|
808
|
+
* - DOM class cleanup
|
|
809
|
+
*
|
|
810
|
+
* **Memory Management Architecture:**
|
|
811
|
+
* - Event Listener Cleanup: Remove all tracked listeners to prevent leaks
|
|
812
|
+
* - State Reset: Return to initial state for consistent disposal
|
|
813
|
+
* - Handler Clearing: Remove all custom handlers
|
|
814
|
+
* - DOM Cleanup: Remove visual state classes (.hovered, .selected)
|
|
815
|
+
*
|
|
816
|
+
* **Cleanup Pattern Benefits:**
|
|
817
|
+
* - Memory Leak Prevention: Proper event listener disposal
|
|
818
|
+
* - State Consistency: Clean slate for reinitialization
|
|
819
|
+
* - Resource Optimization: Free unused memory and references
|
|
820
|
+
* - Visual Cleanup: Remove transient UI states
|
|
821
|
+
*
|
|
822
|
+
* **Architecture Integration:**
|
|
823
|
+
* - Service Lifecycle: Proper service disposal pattern
|
|
824
|
+
* - Event-Driven Cleanup: Coordinated with other services
|
|
825
|
+
* - DOM State Management: Visual consistency maintenance
|
|
826
|
+
* - Performance Optimization: Resource deallocation
|
|
827
|
+
*/
|
|
828
|
+
public cleanup(): void {
|
|
829
|
+
// Remove all event listeners - Memory Leak Prevention
|
|
830
|
+
// Critical: Dispose of all tracked event listeners to prevent memory leaks
|
|
831
|
+
this.eventListeners.forEach(({element, event, handler}) => {
|
|
832
|
+
element.removeEventListener(event, handler);
|
|
833
|
+
});
|
|
834
|
+
this.eventListeners = []; // Clear tracking array
|
|
835
|
+
|
|
836
|
+
// Reset state - State Consistency
|
|
837
|
+
// Return to initial state for clean service disposal/reinitialization
|
|
838
|
+
this.state = {
|
|
839
|
+
isMouseDown: false,
|
|
840
|
+
lastMousePosition: {x: 0, y: 0},
|
|
841
|
+
selectedElement: null,
|
|
842
|
+
isDragging: false,
|
|
843
|
+
touchStartTime: 0,
|
|
844
|
+
keyboardShortcutsEnabled: false
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// Clear handlers - Reference Cleanup
|
|
848
|
+
// Remove all custom handler references to prevent memory retention
|
|
849
|
+
this.handlers = {};
|
|
850
|
+
|
|
851
|
+
// Remove hover and selected classes - Visual State Cleanup
|
|
852
|
+
// Clean up transient visual states from DOM elements
|
|
853
|
+
document.querySelectorAll('.hovered, .selected').forEach(el => {
|
|
854
|
+
el.classList.remove('hovered', 'selected');
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
this.logger.log('InteractionService: Cleanup completed');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ==================== UTILITY METHODS ====================
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Get current interaction state
|
|
864
|
+
*/
|
|
865
|
+
public getInteractionState(): Readonly<InteractionState> {
|
|
866
|
+
return {...this.state};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Check if interactions are enabled
|
|
871
|
+
*/
|
|
872
|
+
public isInteractionEnabled(): boolean {
|
|
873
|
+
return this.eventListeners.length > 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Get interaction statistics
|
|
878
|
+
*/
|
|
879
|
+
public getInteractionStats(): {
|
|
880
|
+
eventListeners: number;
|
|
881
|
+
keyboardEnabled: boolean;
|
|
882
|
+
touchSupported: boolean;
|
|
883
|
+
lastInteraction: { x: number; y: number };
|
|
884
|
+
} {
|
|
885
|
+
return {
|
|
886
|
+
eventListeners: this.eventListeners.length,
|
|
887
|
+
keyboardEnabled: this.state.keyboardShortcutsEnabled,
|
|
888
|
+
touchSupported: 'ontouchstart' in window,
|
|
889
|
+
lastInteraction: this.state.lastMousePosition
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Programmatically trigger element hover
|
|
895
|
+
*/
|
|
896
|
+
public triggerElementHover(element: Element): void {
|
|
897
|
+
if (element) {
|
|
898
|
+
const syntheticEvent = new MouseEvent('mouseover', {
|
|
899
|
+
bubbles: true,
|
|
900
|
+
clientX: this.state.lastMousePosition.x,
|
|
901
|
+
clientY: this.state.lastMousePosition.y
|
|
902
|
+
});
|
|
903
|
+
this.handleElementHover(element, syntheticEvent);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Programmatically trigger element click
|
|
909
|
+
*/
|
|
910
|
+
public triggerElementClick(element: Element): void {
|
|
911
|
+
if (element) {
|
|
912
|
+
const syntheticEvent = new MouseEvent('click', {
|
|
913
|
+
bubbles: true,
|
|
914
|
+
clientX: this.state.lastMousePosition.x,
|
|
915
|
+
clientY: this.state.lastMousePosition.y
|
|
916
|
+
});
|
|
917
|
+
this.handleElementClick(element, syntheticEvent);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|