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.
Files changed (79) hide show
  1. package/README.md +497 -0
  2. package/babel.config.cjs +28 -0
  3. package/coverage/clover.xml +6 -0
  4. package/coverage/coverage-final.json +1 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/favicon.png +0 -0
  8. package/coverage/lcov-report/index.html +101 -0
  9. package/coverage/lcov-report/prettify.css +1 -0
  10. package/coverage/lcov-report/prettify.js +2 -0
  11. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  12. package/coverage/lcov-report/sorter.js +210 -0
  13. package/coverage/lcov.info +0 -0
  14. package/demo-caching.js +68 -0
  15. package/dist/core/Sankey.d.ts +294 -0
  16. package/dist/core/Sankey.d.ts.map +1 -0
  17. package/dist/core/events/EventBus.d.ts +195 -0
  18. package/dist/core/events/EventBus.d.ts.map +1 -0
  19. package/dist/core/types/events.d.ts +42 -0
  20. package/dist/core/types/events.d.ts.map +1 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/sankey.esm.js +5212 -0
  24. package/dist/sankey.esm.js.map +1 -0
  25. package/dist/sankey.standalone.esm.js +9111 -0
  26. package/dist/sankey.standalone.esm.js.map +1 -0
  27. package/dist/sankey.standalone.min.js +2 -0
  28. package/dist/sankey.standalone.min.js.map +1 -0
  29. package/dist/sankey.standalone.umd.js +9119 -0
  30. package/dist/sankey.standalone.umd.js.map +1 -0
  31. package/dist/sankey.umd.js +5237 -0
  32. package/dist/sankey.umd.js.map +1 -0
  33. package/dist/sankey.umd.min.js +2 -0
  34. package/dist/sankey.umd.min.js.map +1 -0
  35. package/dist/services/AnimationService.d.ts +229 -0
  36. package/dist/services/AnimationService.d.ts.map +1 -0
  37. package/dist/services/ConfigurationService.d.ts +173 -0
  38. package/dist/services/ConfigurationService.d.ts.map +1 -0
  39. package/dist/services/InteractionService.d.ts +377 -0
  40. package/dist/services/InteractionService.d.ts.map +1 -0
  41. package/dist/services/RenderingService.d.ts +152 -0
  42. package/dist/services/RenderingService.d.ts.map +1 -0
  43. package/dist/services/calculation/GraphService.d.ts +111 -0
  44. package/dist/services/calculation/GraphService.d.ts.map +1 -0
  45. package/dist/services/calculation/SummaryService.d.ts +149 -0
  46. package/dist/services/calculation/SummaryService.d.ts.map +1 -0
  47. package/dist/services/data/DataService.d.ts +167 -0
  48. package/dist/services/data/DataService.d.ts.map +1 -0
  49. package/dist/services/data/DataValidationService.d.ts +48 -0
  50. package/dist/services/data/DataValidationService.d.ts.map +1 -0
  51. package/dist/types/index.d.ts +189 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/utils/Logger.d.ts +88 -0
  54. package/dist/utils/Logger.d.ts.map +1 -0
  55. package/jest.config.cjs +20 -0
  56. package/package.json +68 -0
  57. package/rollup.config.js +131 -0
  58. package/scripts/performance-validation-real.js +411 -0
  59. package/scripts/validate-optimization.sh +147 -0
  60. package/scripts/visual-validation-real-data.js +374 -0
  61. package/src/core/Sankey.ts +1039 -0
  62. package/src/core/events/EventBus.ts +488 -0
  63. package/src/core/types/events.ts +80 -0
  64. package/src/index.ts +35 -0
  65. package/src/services/AnimationService.ts +983 -0
  66. package/src/services/ConfigurationService.ts +497 -0
  67. package/src/services/InteractionService.ts +920 -0
  68. package/src/services/RenderingService.ts +484 -0
  69. package/src/services/calculation/GraphService.ts +616 -0
  70. package/src/services/calculation/SummaryService.ts +394 -0
  71. package/src/services/data/DataService.ts +380 -0
  72. package/src/services/data/DataValidationService.ts +155 -0
  73. package/src/styles/controls.css +184 -0
  74. package/src/styles/sankey.css +211 -0
  75. package/src/types/index.ts +220 -0
  76. package/src/utils/Logger.ts +105 -0
  77. package/tests/numerical-validation.test.js +575 -0
  78. package/tests/setup.js +53 -0
  79. package/tsconfig.json +54 -0
@@ -0,0 +1,497 @@
1
+ import type {SankeyOptions} from '@/types';
2
+ import {EventBus} from "@/core/events/EventBus";
3
+ import {Logger} from "@/utils/Logger";
4
+
5
+ /**
6
+ * Fuel configuration interface
7
+ */
8
+ export interface FuelConfig {
9
+ readonly fuel: string;
10
+ readonly name: string;
11
+ readonly color: string;
12
+ }
13
+
14
+ /**
15
+ * Sector/Box configuration interface
16
+ */
17
+ export interface BoxConfig {
18
+ readonly box: string;
19
+ readonly name: string;
20
+ readonly color: string;
21
+ }
22
+
23
+ /**
24
+ * Configuration Service - Mathematical Constants & Visual Parameters
25
+ *
26
+ * ARCHITECTURAL RESPONSIBILITY: Central Mathematical Constants Repository
27
+ *
28
+ * This service provides all the mathematical constants, visual parameters, and scaling factors
29
+ * that determine the precise positioning, sizing, and styling of the energy visualization.
30
+ * Every coordinate calculation, energy-to-pixel conversion, and visual spacing depends on
31
+ * these carefully calibrated values.
32
+ *
33
+ * MATHEMATICAL SIGNIFICANCE OF CONSTANTS:
34
+ * Each constant has been precisely calculated to ensure:
35
+ * - Accurate proportional representation of energy values
36
+ * - Optimal visual clarity and readability
37
+ * - Smooth animation transitions between years
38
+ * - Responsive behavior across different screen sizes
39
+ * - Perfect alignment between flows and boxes
40
+ *
41
+ * COORDINATE SYSTEM ARCHITECTURE:
42
+ * The visualization uses a custom coordinate system where:
43
+ * - Origin (0,0) is at top-left of container
44
+ * - X increases rightward (fuel boxes → sector boxes)
45
+ * - Y increases downward (stacked vertically)
46
+ * - All measurements in pixels for precise SVG positioning
47
+ */
48
+ export class ConfigurationService {
49
+
50
+ public hasHeatData = false;
51
+
52
+ // ======================== CORE DIMENSIONAL CONSTANTS ========================
53
+
54
+ // CANVAS DIMENSIONS: Overall visualization space
55
+ public readonly HEIGHT = 620; // Canvas height in pixels - accommodates ~100 years of stacked fuels
56
+
57
+ // BOX GEOMETRY: Standard rectangular dimensions for fuel sources and consumption sectors
58
+ public readonly BOX_WIDTH: number = 120; // Standard width for all energy boxes (fuel & sector)
59
+ public readonly BOX_HEIGHT = 30; // Minimum height for energy boxes (scaled by energy value)
60
+
61
+ // ===================== COORDINATE POSITIONING CONSTANTS =====================
62
+
63
+ // LEFT COLUMN POSITIONING: Fuel source boxes alignment
64
+ public readonly LEFT_X = 10; // X-coordinate for left column (fuel sources)
65
+ public readonly TOP_Y = 100; // Y-coordinate for top margin (visual breathing space)
66
+
67
+ // ==================== MATHEMATICAL SCALING CONSTANTS ====================
68
+
69
+ // ENERGY-TO-PIXEL CONVERSION: Critical scaling factor for proportional representation
70
+ public readonly SCALE = 0.02; // Converts Quads to pixels: 1 Quad = 0.02 pixels height
71
+ // Calibrated for typical US energy consumption (0-100+ Quads)
72
+ // Example: 50 Quads × 0.02 = 1.0 pixel height
73
+
74
+ // ELECTRICITY BOX POSITIONING: Special coordinates for electricity box (bidirectional flows)
75
+ public readonly ELEC_BOX_X = 300 as const;
76
+ public readonly ELEC_BOX_Y = 120 as const;
77
+
78
+ public readonly HEAT_BOX_X = 750 as const;
79
+ public readonly HEAT_BOX_Y = 200 as const;
80
+ // X=350: Positioned between fuel sources (left) and sectors (right)
81
+ // Y=120: Offset below title area for visual clarity
82
+
83
+ // ======================== VISUAL SPACING CONSTANTS ========================
84
+
85
+ // VERTICAL GAPS: Spacing between stacked elements for visual clarity
86
+ public readonly LEFT_GAP: number = 30; // Gap between fuel boxes (left column)
87
+ public readonly RIGHT_GAP: number; // Gap between sector boxes (right column) - computed as LEFT_GAP × 2.1
88
+
89
+ // ANIMATION PARAMETERS: Timing and transition control
90
+ public readonly SPEED: number; // Animation speed in milliseconds (from user options, default 200)
91
+ public readonly BLEED = 0.5; // Edge bleed factor for smooth visual transitions
92
+
93
+ // =================== GEOMETRIC CALCULATION CONSTANTS ===================
94
+
95
+ // MATHEMATICS: Mathematical constants
96
+ public readonly SR3 = Math.sqrt(3); // √3 ≈ 1.732
97
+ // Provides optimal smoothness for flow paths
98
+ public readonly HSR3 = Math.sqrt(3) / 2; // √3/2 ≈ 0.866
99
+
100
+ // FLOW PATH SPACING: Visual separation between parallel energy flows
101
+ public readonly PATH_GAP: number = 20; // Pixel gap between parallel flow paths
102
+ // Prevents visual overlap while maintaining readability
103
+ public readonly ELEC_GAP = 19; // Special gap for electricity flows (slightly smaller)
104
+ // Optimized for electricity box's central position
105
+
106
+ // ====================== ENERGY SOURCE DEFINITIONS ======================
107
+ // Comprehensive fuel type configuration with energy industry standard categorization
108
+ // Colors chosen for maximum visual distinction and intuitive association
109
+ public readonly FUELS: readonly FuelConfig[] = [
110
+ // ELECTRICITY: Special category - both generated from other fuels AND consumed by sectors
111
+ {fuel: 'elec', name: 'Electricity', color: '#e49942'}, // Amber - central energy carrier
112
+ {fuel: 'heat', name: 'Heat', color: '#98002e'}, // Read - central energy carrier
113
+ {fuel: 'solar', name: 'Solar', color: '#fed530'}, // Bright yellow - sun association
114
+ {fuel: 'nuclear', name: 'Nuclear', color: '#ca0813'}, // Red - nuclear energy (caution color)
115
+ {fuel: 'hydro', name: 'Hydro', color: '#0b24fb'}, // Deep blue - water association
116
+ {fuel: 'wind', name: 'Wind', color: '#901d8f'}, // Purple - distinctive for wind power
117
+ {fuel: 'geo', name: 'Geothermal', color: '#905a1c'}, // Earth brown - geothermal heat
118
+ {fuel: 'gas', name: 'Natural Gas', color: '#4cabf2'}, // Light blue - clean-burning fossil fuel
119
+ {fuel: 'coal', name: 'Coal', color: '#000000'}, // Black - coal's natural color
120
+ {fuel: 'bio', name: 'Biomass', color: '#46be48'}, // Green - organic matter, photosynthesis
121
+ {fuel: 'petro', name: 'Petroleum', color: '#095f0b'} // Dark green - oil/petroleum products
122
+ ] as const;
123
+
124
+ // ==================== CONSUMPTION SECTOR DEFINITIONS ====================
125
+ // Major energy consumption categories following US Energy Information Administration (EIA) classification
126
+ // Neutral gray colors to emphasize flows while maintaining sector distinction through positioning
127
+ public readonly BOXES: readonly BoxConfig[] = [
128
+ {box: 'elec', name: 'Electricity', color: '#cccccc'}, // Gray - neutral energy carrier
129
+ {box: 'res', name: 'Residential/Commercial', color: '#cccccc'}, // Buildings: homes, offices, stores
130
+ {box: 'ag', name: 'Agricultural', color: '#cccccc'}, // Farming: irrigation, equipment, processing
131
+ {box: 'indus', name: 'Industrial', color: '#cccccc'}, // Manufacturing: steel, chemicals, cement
132
+ {box: 'trans', name: 'Transportation', color: '#cccccc'}, // Mobility: cars, trucks, planes, ships
133
+ {box: 'heat', name: 'Heat', color: '#cccccc'}, // Gray - neutral energy carrier
134
+ ] as const;
135
+
136
+ public readonly BOXES_DEFAULT_FLOW_PATHS_ORDER = {
137
+ "elec": 1,
138
+ "res": 2,
139
+ "ag": 3,
140
+ "indus": 4,
141
+ "trans": 5,
142
+ "heat": 6,
143
+ }
144
+
145
+ public readonly HEAT_BOX_FIRST_FLOW_PATHS_ORDER = {
146
+ "elec": 1,
147
+ "heat": 2,
148
+ "res": 3,
149
+ "ag": 4,
150
+ "indus": 5,
151
+ "trans": 6,
152
+ }
153
+
154
+ public FLOW_PATHS_ORDER: Record<string, Record<string, number>> = {}
155
+
156
+ // These are computed once for performance optimization
157
+ public readonly BOX_NAMES: readonly string[] = this.BOXES.map(box => box.box);
158
+ public readonly FUEL_NAMES: readonly string[] = this.FUELS.map(fuel => fuel.fuel);
159
+
160
+ constructor(
161
+ private container: HTMLElement,
162
+ public options: SankeyOptions,
163
+ private eventBus: EventBus,
164
+ private logger: Logger
165
+ ) {
166
+ // Calculate computed properties
167
+ this.RIGHT_GAP = this.LEFT_GAP * 2.1;
168
+ this.SPEED = options.animationSpeed || 200;
169
+
170
+ this.FLOW_PATHS_ORDER = {
171
+ "elec": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
172
+ "heat": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
173
+ "solar": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
174
+ "nuclear": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
175
+ "hydro": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
176
+ "wind": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
177
+ "geo": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
178
+ "gas": this.HEAT_BOX_FIRST_FLOW_PATHS_ORDER,
179
+ "coal": this.HEAT_BOX_FIRST_FLOW_PATHS_ORDER,
180
+ "bio": this.BOXES_DEFAULT_FLOW_PATHS_ORDER,
181
+ "petro": this.HEAT_BOX_FIRST_FLOW_PATHS_ORDER,
182
+ }
183
+
184
+ // this.updateFuelAndBoxNames();
185
+
186
+ // Validate configuration consistency
187
+ this.validateConfiguration();
188
+
189
+ // Emit configuration loaded event
190
+ this.eventBus.emit({
191
+ type: 'system.initialized',
192
+ timestamp: Date.now(),
193
+ source: 'ConfigurationService',
194
+ data: {
195
+ fuelsCount: this.FUELS.length,
196
+ sectorsCount: this.BOXES.length,
197
+ dimensions: {width: this.WIDTH, height: this.HEIGHT}
198
+ }
199
+ });
200
+ }
201
+
202
+ // public updateFuelAndBoxNames(): void {
203
+ // this.BOX_NAMES = this.BOXES.filter((box) => {
204
+ // if (!this.hasHeatData && box.box == 'heat') {
205
+ // return false
206
+ // }
207
+ // return true
208
+ // }).map(box => box.box);
209
+ //
210
+ // this.FUEL_NAMES = this.FUELS.filter((fuel) => {
211
+ // if (!this.hasHeatData && fuel.fuel == 'heat') {
212
+ // return false
213
+ // }
214
+ // return true
215
+ // }).map(fuel => fuel.fuel);
216
+ //
217
+ // console.log(this.BOX_NAMES);
218
+ // console.log(this.FUEL_NAMES);
219
+ // }
220
+
221
+ /**
222
+ * Get human-readable display name for fuel
223
+ */
224
+ public getFuelDisplayName(fuel: string): string {
225
+ const fuelConfig = this.FUELS.find(f => f.fuel === fuel);
226
+ if (fuelConfig) {
227
+ return fuelConfig.name;
228
+ }
229
+
230
+ // Fallback for unknown fuels
231
+ const fallbackNames: { [key: string]: string } = {
232
+ 'elec': 'Electricity',
233
+ 'waste': 'Waste Heat'
234
+ };
235
+
236
+ return fallbackNames[fuel] || fuel.charAt(0).toUpperCase() + fuel.slice(1);
237
+ }
238
+
239
+ /**
240
+ * Get human-readable display name for sector/box
241
+ */
242
+ public getBoxDisplayName(sector: string): string {
243
+ const boxConfig = this.BOXES.find(b => b.box === sector);
244
+ if (boxConfig) {
245
+ return boxConfig.name;
246
+ }
247
+
248
+ // Fallback for unknown sectors
249
+ const fallbackNames: { [key: string]: string } = {
250
+ 'res': 'Residential',
251
+ 'ag': 'Agriculture',
252
+ 'indus': 'Industrial',
253
+ 'trans': 'Transportation',
254
+ 'elec': 'Electricity'
255
+ };
256
+
257
+ return fallbackNames[sector] || sector.charAt(0).toUpperCase() + sector.slice(1);
258
+ }
259
+
260
+ /**
261
+ * Get color for fuel type
262
+ */
263
+ public getFuelColor(fuel: string): string {
264
+ const fuelConfig = this.FUELS.find(f => f.fuel === fuel);
265
+ return fuelConfig?.color || '#CCCCCC'; // Default gray for unknown fuels
266
+ }
267
+
268
+ /**
269
+ * Get color for sector type
270
+ */
271
+ public getSectorColor(sector: string): string {
272
+ const boxConfig = this.BOXES.find(b => b.box === sector);
273
+ return boxConfig?.color || '#CCCCCC'; // Default gray for unknown sectors
274
+ }
275
+
276
+ /**
277
+ * Get dynamic width from container ConfigurationService.WIDTH getter
278
+ * This dynamically calculates width based on actual container size
279
+ */
280
+ public get WIDTH(): number {
281
+ // Get container width or use explicit width option
282
+ if (this.options.width) {
283
+ return this.options.width;
284
+ } else {
285
+ // Auto-detect from container
286
+ const containerRect = this.container.getBoundingClientRect();
287
+ let containerWidth = containerRect.width || this.getDefaultWidth();
288
+
289
+ // Ensure minimum dimensions
290
+ containerWidth = Math.max(containerWidth, 400);
291
+
292
+ // Apply responsive adjustments
293
+ return this.applyResponsiveAdjustments(containerWidth);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Get default width when container width cannot be determined
299
+ */
300
+ private getDefaultWidth(): number {
301
+ // Check if we're in a browser environment
302
+ if (typeof window !== 'undefined' && window.innerWidth) {
303
+ // Use 90% of viewport width as fallback, capped at 1200px
304
+ return Math.min(window.innerWidth * 0.9, 1200);
305
+ }
306
+
307
+ // Server-side or fallback
308
+ return 1000;
309
+ }
310
+
311
+ /**
312
+ * Apply responsive adjustments based on screen size
313
+ */
314
+ private applyResponsiveAdjustments(width: number): number {
315
+ // For smaller screens, adjust dimensions
316
+ if (typeof window !== 'undefined') {
317
+ const viewportWidth = window.innerWidth;
318
+
319
+ if (viewportWidth < 768) {
320
+ // Mobile adjustments
321
+ width = Math.min(width, viewportWidth - 40);
322
+ } else if (viewportWidth < 1024) {
323
+ // Tablet adjustments
324
+ width = Math.min(width, viewportWidth - 80);
325
+ }
326
+ }
327
+
328
+ return width;
329
+ }
330
+
331
+ /**
332
+ * Calculate right box X position
333
+ */
334
+ public calculateRightBoxX(): number {
335
+ return this.WIDTH - this.BOX_WIDTH;
336
+ }
337
+
338
+ // ==================== CONFIGURATION VALIDATION ====================
339
+
340
+ /**
341
+ * Validate configuration consistency
342
+ * Ensures all required constants are properly defined
343
+ */
344
+ private validateConfiguration(): void {
345
+ // Validate dimensions
346
+ if (this.WIDTH <= 0 || this.HEIGHT <= 0) {
347
+ throw new Error('ConfigurationService: Invalid dimensions');
348
+ }
349
+
350
+ // Validate box positioning
351
+ if (this.BOX_WIDTH <= 0 || this.BOX_HEIGHT <= 0) {
352
+ throw new Error('ConfigurationService: Invalid box dimensions');
353
+ }
354
+
355
+ // Validate scaling factors
356
+ if (this.SCALE <= 0) {
357
+ throw new Error('ConfigurationService: Invalid scale factor');
358
+ }
359
+
360
+ // Validate electricity box positioning
361
+ if (this.ELEC_BOX_X <= 0 || this.ELEC_BOX_X <= 0) {
362
+ throw new Error('ConfigurationService: Invalid electricity box position');
363
+ }
364
+
365
+ // Validate fuel definitions
366
+ if (this.FUELS.length === 0) {
367
+ throw new Error('ConfigurationService: No fuels defined');
368
+ }
369
+
370
+ // Validate sector definitions
371
+ if (this.BOXES.length === 0) {
372
+ throw new Error('ConfigurationService: No sectors defined');
373
+ }
374
+
375
+ // Validate required fuels exist
376
+ const requiredFuels = ['elec', 'solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro'];
377
+ for (const fuel of requiredFuels) {
378
+ if (!this.FUEL_NAMES.includes(fuel)) {
379
+ console.warn(`ConfigurationService: Missing required fuel: ${fuel}`);
380
+ }
381
+ }
382
+
383
+ // Validate required sectors exist
384
+ const requiredSectors = ['elec', 'res', 'ag', 'indus', 'trans'];
385
+ for (const sector of requiredSectors) {
386
+ if (!this.BOX_NAMES.includes(sector)) {
387
+ console.warn(`ConfigurationService: Missing required sector: ${sector}`);
388
+ }
389
+ }
390
+
391
+ this.logger.log('ConfigurationService: All configuration validated successfully');
392
+ }
393
+
394
+ // ==================== UTILITY METHODS ====================
395
+
396
+ /**
397
+ * Check if fuel is defined in configuration
398
+ */
399
+ public isValidFuel(fuel: string): boolean {
400
+ return this.FUEL_NAMES.includes(fuel);
401
+ }
402
+
403
+ /**
404
+ * Check if sector is defined in configuration
405
+ */
406
+ public isValidSector(sector: string): boolean {
407
+ return this.BOX_NAMES.includes(sector);
408
+ }
409
+
410
+ /**
411
+ * Get all fuel configurations
412
+ */
413
+ public getAllFuels(): readonly FuelConfig[] {
414
+ return this.FUELS;
415
+ }
416
+
417
+ /**
418
+ * Get all sector configurations
419
+ */
420
+ public getAllSectors(): readonly BoxConfig[] {
421
+ return this.BOXES;
422
+ }
423
+
424
+ /**
425
+ * Get fuel configuration by name
426
+ */
427
+ public getFuelConfig(fuel: string): FuelConfig | undefined {
428
+ return this.FUELS.find(f => f.fuel === fuel);
429
+ }
430
+
431
+ /**
432
+ * Get sector configuration by name
433
+ */
434
+ public getSectorConfig(sector: string): BoxConfig | undefined {
435
+ return this.BOXES.find(b => b.box === sector);
436
+ }
437
+
438
+ // ==================== RESPONSIVE CONFIGURATION ====================
439
+
440
+ /**
441
+ * Calculate responsive scaling factor based on container size
442
+ * Maintains aspect ratio while fitting within container
443
+ */
444
+ public calculateResponsiveScale(containerWidth: number, containerHeight: number): {
445
+ scale: number;
446
+ width: number;
447
+ height: number;
448
+ offsetX: number;
449
+ offsetY: number;
450
+ } {
451
+ const aspectRatio = this.WIDTH / this.HEIGHT;
452
+
453
+ // Calculate scale based on container constraints
454
+ const scaleX = containerWidth / this.WIDTH;
455
+ const scaleY = containerHeight / this.HEIGHT;
456
+ const scale = Math.min(scaleX, scaleY);
457
+
458
+ // Calculate actual dimensions
459
+ const scaledWidth = this.WIDTH * scale;
460
+ const scaledHeight = this.HEIGHT * scale;
461
+
462
+ // Calculate centering offsets
463
+ const offsetX = (containerWidth - scaledWidth) / 2;
464
+ const offsetY = (containerHeight - scaledHeight) / 2;
465
+
466
+ return {
467
+ scale,
468
+ width: scaledWidth,
469
+ height: scaledHeight,
470
+ offsetX,
471
+ offsetY
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Get configuration optimized for mobile devices
477
+ */
478
+ public getMobileConfiguration(): Partial<ConfigurationService> {
479
+ // Return modified constants for mobile optimization
480
+ // These maintain visual accuracy while optimizing for small screens
481
+ return {
482
+ BOX_WIDTH: Math.max(40, this.BOX_WIDTH * 0.8),
483
+ LEFT_GAP: Math.max(5, this.LEFT_GAP * 0.7),
484
+ RIGHT_GAP: Math.max(10, this.RIGHT_GAP * 0.7),
485
+ PATH_GAP: Math.max(4, this.PATH_GAP * 0.7),
486
+ SPEED: this.SPEED * 1.2, // Slightly faster animations on mobile
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Check if current viewport suggests mobile device
492
+ */
493
+ public isMobileViewport(): boolean {
494
+ if (typeof window === 'undefined') return false;
495
+ return window.innerWidth <= 768 || window.innerHeight <= 600;
496
+ }
497
+ }