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,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
|
+
}
|