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,380 @@
|
|
|
1
|
+
import type {EnergyDataPoint} from '@/types';
|
|
2
|
+
import {DataValidationService} from "@/services/data/DataValidationService";
|
|
3
|
+
import {EventBus} from "@/core/events/EventBus";
|
|
4
|
+
import {Logger} from "@/utils/Logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Data Service
|
|
8
|
+
*
|
|
9
|
+
* Core data access and management service providing validated, sorted energy data
|
|
10
|
+
* with comprehensive query capabilities. Handles input validation, data sorting,
|
|
11
|
+
* year-based navigation, and statistical analysis.
|
|
12
|
+
*
|
|
13
|
+
* Provides validated, indexed access to energy flow data with comprehensive
|
|
14
|
+
* query capabilities and statistical analysis. Maintains data integrity
|
|
15
|
+
* through immutable structures and validation at construction time.
|
|
16
|
+
*
|
|
17
|
+
* Key Responsibilities:
|
|
18
|
+
* - Input validation and data structure verification
|
|
19
|
+
* - Chronological data sorting and indexing
|
|
20
|
+
* - Year-based data retrieval and navigation
|
|
21
|
+
* - Fuel and sector totals calculation
|
|
22
|
+
* - Milestone data management
|
|
23
|
+
* - Dataset statistics and metadata
|
|
24
|
+
*
|
|
25
|
+
* Data Structure:
|
|
26
|
+
* Manages EnergyDataPoint arrays with comprehensive energy flow data
|
|
27
|
+
* including all major fuel types (solar, nuclear, hydro, wind, etc.)
|
|
28
|
+
* and consumption sectors (electricity, residential, industrial, etc.)
|
|
29
|
+
*
|
|
30
|
+
* Performance Features:
|
|
31
|
+
* - Immutable data structures for safety
|
|
32
|
+
* - Pre-computed indices for fast lookups
|
|
33
|
+
* - Efficient year-based navigation
|
|
34
|
+
* - Cached statistics calculations
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
export class DataService {
|
|
38
|
+
// Core immutable data structures
|
|
39
|
+
public readonly data: readonly EnergyDataPoint[];
|
|
40
|
+
public readonly years: readonly number[];
|
|
41
|
+
public readonly yearsLength: number;
|
|
42
|
+
public readonly firstYear: number;
|
|
43
|
+
public readonly lastYear: number;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
energyData: EnergyDataPoint[],
|
|
47
|
+
private validationService: DataValidationService,
|
|
48
|
+
private eventBus: EventBus,
|
|
49
|
+
private logger: Logger,
|
|
50
|
+
) {
|
|
51
|
+
const startTime = performance.now(); // Performance monitoring for data service initialization
|
|
52
|
+
|
|
53
|
+
// Validate input data structure and content - Data Integrity Assurance
|
|
54
|
+
// Critical first step: ensures all downstream calculations receive valid data
|
|
55
|
+
// Validation service performs comprehensive structure and content verification
|
|
56
|
+
this.validationService.validateData(energyData);
|
|
57
|
+
|
|
58
|
+
// Sort data chronologically and create immutable reference - Data Structure Optimization
|
|
59
|
+
// Chronological sorting enables efficient year-based navigation and animation
|
|
60
|
+
// Immutability prevents accidental data corruption throughout application lifecycle
|
|
61
|
+
const sortedData = [...energyData].sort((a, b) => a.year - b.year);
|
|
62
|
+
this.data = Object.freeze(sortedData);
|
|
63
|
+
|
|
64
|
+
// Extract and freeze year indices for efficient navigation - Performance Optimization
|
|
65
|
+
// Pre-computed year array enables O(1) year lookups and efficient navigation
|
|
66
|
+
// Frozen arrays prevent mutation while maintaining reference stability
|
|
67
|
+
this.years = Object.freeze(this.data.map(d => d.year));
|
|
68
|
+
this.yearsLength = this.years.length;
|
|
69
|
+
this.firstYear = this.years[0]; // Timeline start boundary
|
|
70
|
+
this.lastYear = this.years[this.yearsLength - 1]; // Timeline end boundary
|
|
71
|
+
|
|
72
|
+
const endTime = performance.now();
|
|
73
|
+
|
|
74
|
+
// Emit data loaded event for service coordination - Event-Driven Architecture
|
|
75
|
+
// Notifies calculation and visualization services that validated data is available
|
|
76
|
+
// Includes metadata for capacity planning and performance monitoring
|
|
77
|
+
this.eventBus.emit({
|
|
78
|
+
type: 'data.loaded',
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
source: 'DataService',
|
|
81
|
+
data: {
|
|
82
|
+
dataPoints: [...this.data], // Complete dataset for processing
|
|
83
|
+
yearCount: this.yearsLength, // Timeline scope information
|
|
84
|
+
yearRange: [this.firstYear, this.lastYear] // Temporal boundaries
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Debug logging (controlled by library configuration when available) - Performance Monitoring
|
|
89
|
+
// Tracks initialization performance and data characteristics for optimization
|
|
90
|
+
this.logger.log(`DataService: ${this.data.length} data points loaded and sorted in ${(endTime - startTime).toFixed(2)}ms`);
|
|
91
|
+
this.logger.log(`DataService: Year range ${this.firstYear}-${this.lastYear}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ==================== DATA ACCESS METHODS ====================
|
|
95
|
+
// Core data retrieval and navigation methods
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get data for specific year
|
|
99
|
+
* @param year - Target year to retrieve
|
|
100
|
+
* @returns Energy data for the specified year, or undefined if not found
|
|
101
|
+
*/
|
|
102
|
+
public getYearData(year: number): EnergyDataPoint | undefined {
|
|
103
|
+
return this.data.find(d => d.year === year);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get index of year in chronological sequence
|
|
108
|
+
* @param year - Year to locate
|
|
109
|
+
* @returns Zero-based index of year, or -1 if not found
|
|
110
|
+
*/
|
|
111
|
+
public getYearIndex(year: number): number {
|
|
112
|
+
return this.years.indexOf(year);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if year exists in dataset
|
|
117
|
+
* @param year - Year to check
|
|
118
|
+
* @returns True if year has data available
|
|
119
|
+
*/
|
|
120
|
+
public hasYear(year: number): boolean {
|
|
121
|
+
return this.years.includes(year);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get data by chronological index
|
|
126
|
+
* @param index - Zero-based index in sorted data array
|
|
127
|
+
* @returns Energy data at specified index, or undefined if out of bounds
|
|
128
|
+
*/
|
|
129
|
+
public getYearDataByIndex(index: number): EnergyDataPoint | undefined {
|
|
130
|
+
if (index < 0 || index >= this.data.length) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return this.data[index];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if year is valid and within dataset range
|
|
138
|
+
* @param year - Year to validate
|
|
139
|
+
* @returns True if year exists and is within valid range
|
|
140
|
+
*/
|
|
141
|
+
public isValidYear(year: number): boolean {
|
|
142
|
+
return year >= this.firstYear && year <= this.lastYear && this.hasYear(year);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ==================== NAVIGATION METHODS ====================
|
|
146
|
+
// Year-based navigation and range operations
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get year range as tuple
|
|
150
|
+
* @returns Tuple containing [firstYear, lastYear]
|
|
151
|
+
*/
|
|
152
|
+
public getYearRange(): [number, number] {
|
|
153
|
+
return [this.firstYear, this.lastYear];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get total number of data points
|
|
158
|
+
* @returns Count of available years in dataset
|
|
159
|
+
*/
|
|
160
|
+
public getDataCount(): number {
|
|
161
|
+
return this.data.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get chronologically next year
|
|
166
|
+
* @param currentYear - Starting year
|
|
167
|
+
* @returns Next available year, or null if at end of dataset
|
|
168
|
+
*/
|
|
169
|
+
public getNextYear(currentYear: number): number | null {
|
|
170
|
+
const currentIndex = this.getYearIndex(currentYear);
|
|
171
|
+
if (currentIndex === -1 || currentIndex === this.yearsLength - 1) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return this.years[currentIndex + 1];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get chronologically previous year
|
|
179
|
+
* @param currentYear - Starting year
|
|
180
|
+
* @returns Previous available year, or null if at beginning of dataset
|
|
181
|
+
*/
|
|
182
|
+
public getPreviousYear(currentYear: number): number | null {
|
|
183
|
+
const currentIndex = this.getYearIndex(currentYear);
|
|
184
|
+
if (currentIndex <= 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return this.years[currentIndex - 1];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ==================== CALCULATION METHODS ====================
|
|
191
|
+
// Fuel and sector totals calculation methods
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get energy total for specific year, fuel, and sector combination
|
|
195
|
+
* @param year - Target year
|
|
196
|
+
* @param fuel - Fuel type (e.g., 'coal', 'gas', 'solar')
|
|
197
|
+
* @param sector - Consumption sector (e.g., 'elec', 'trans', 'indus')
|
|
198
|
+
* @returns Energy value for the specified combination
|
|
199
|
+
*/
|
|
200
|
+
public getTotalForYear(year: number, fuel: string, sector: string): number {
|
|
201
|
+
const yearData = this.getYearData(year);
|
|
202
|
+
if (!yearData) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fuelData = (yearData as any)[fuel];
|
|
207
|
+
if (!fuelData) {
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return fuelData[sector] || 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get total energy consumption for specific fuel across all sectors
|
|
216
|
+
*
|
|
217
|
+
* **Energy Industry Analysis Method:**
|
|
218
|
+
* - Aggregates fuel consumption across all end-use sectors
|
|
219
|
+
* - Provides fuel-specific energy totals for analysis and visualization
|
|
220
|
+
* - Follows EIA energy accounting standards for sector summation
|
|
221
|
+
*
|
|
222
|
+
* @param year - Target year
|
|
223
|
+
* @param fuel - Fuel type to sum (e.g., 'coal', 'gas', 'solar')
|
|
224
|
+
* @returns Total energy consumption for fuel across all sectors
|
|
225
|
+
*/
|
|
226
|
+
public getYearTotalForFuel(year: number, fuel: string): number {
|
|
227
|
+
const yearData = this.getYearData(year);
|
|
228
|
+
if (!yearData) {
|
|
229
|
+
return 0; // Graceful degradation for missing year data
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const fuelData = (yearData as any)[fuel];
|
|
233
|
+
if (!fuelData) {
|
|
234
|
+
return 0; // Graceful degradation for missing fuel data
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sum across all major consumption sectors - Energy Industry Standard
|
|
238
|
+
// Covers complete energy end-use landscape: electricity generation, residential,
|
|
239
|
+
// agriculture, industrial, and transportation sectors
|
|
240
|
+
return (fuelData.elec || 0) + (fuelData.res || 0) + (fuelData.ag || 0) +
|
|
241
|
+
(fuelData.indus || 0) + (fuelData.trans || 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get total energy consumption for specific sector across all fuels
|
|
246
|
+
*
|
|
247
|
+
* **Sectoral Energy Analysis Method:**
|
|
248
|
+
* - Aggregates energy consumption by end-use sector across all fuel types
|
|
249
|
+
* - Handles electricity as both source and consumption (special case logic)
|
|
250
|
+
* - Provides sectoral energy totals for economic and policy analysis
|
|
251
|
+
*
|
|
252
|
+
* @param year - Target year
|
|
253
|
+
* @param sector - Sector to sum (e.g., 'trans' for transportation)
|
|
254
|
+
* @returns Total energy consumption for sector across all fuel types
|
|
255
|
+
*/
|
|
256
|
+
public getYearTotalForSector(year: number, sector: string): number {
|
|
257
|
+
const yearData = this.getYearData(year);
|
|
258
|
+
if (!yearData) {
|
|
259
|
+
return 0; // Graceful degradation for missing year data
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let total = 0;
|
|
263
|
+
// Standard fuel types for comprehensive calculation - Energy Source Categories
|
|
264
|
+
// Covers all primary energy sources in US energy system per EIA classification
|
|
265
|
+
const fuels = ['solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro'];
|
|
266
|
+
|
|
267
|
+
for (const fuel of fuels) {
|
|
268
|
+
const fuelData = (yearData as any)[fuel];
|
|
269
|
+
if (fuelData && fuelData[sector]) {
|
|
270
|
+
total += fuelData[sector]; // Direct fuel consumption in target sector
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Add electricity consumption for non-electricity sectors - Special Case Handling
|
|
275
|
+
// Electricity is both a fuel source (for generation) and consumption medium
|
|
276
|
+
// For non-electricity sectors, add electricity consumption to capture total energy use
|
|
277
|
+
if (sector !== 'elec' && yearData.elec[sector as keyof typeof yearData.elec]) {
|
|
278
|
+
total += yearData.elec[sector as keyof typeof yearData.elec];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return total;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ==================== MILESTONE METHODS ====================
|
|
285
|
+
// Historical milestone data retrieval methods
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get historical milestone description for specific year
|
|
289
|
+
* @param year - Target year
|
|
290
|
+
* @returns Milestone description string, or undefined if no milestone
|
|
291
|
+
*/
|
|
292
|
+
public getMilestoneForYear(year: number): string | undefined {
|
|
293
|
+
const yearData = this.getYearData(year);
|
|
294
|
+
return yearData?.milestone;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get array of all years containing historical milestones
|
|
299
|
+
* @returns Array of years with milestone data available
|
|
300
|
+
*/
|
|
301
|
+
public getYearsWithMilestones(): number[] {
|
|
302
|
+
return this.data
|
|
303
|
+
.filter(d => d.milestone)
|
|
304
|
+
.map(d => d.year);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ==================== ANALYSIS METHODS ====================
|
|
308
|
+
// Dataset analysis and metadata extraction methods
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get list of all fuel types available in dataset
|
|
312
|
+
* @returns Array of fuel type names (e.g., ['solar', 'coal', 'gas'])
|
|
313
|
+
*/
|
|
314
|
+
public getAvailableFuels(): string[] {
|
|
315
|
+
if (this.data.length === 0) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Extract all fuel types from first data point (structure should be consistent)
|
|
320
|
+
const firstPoint = this.data[0];
|
|
321
|
+
return Object.keys(firstPoint).filter(key =>
|
|
322
|
+
key !== 'year' &&
|
|
323
|
+
key !== 'milestone' &&
|
|
324
|
+
typeof (firstPoint as any)[key] === 'object'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get list of all consumption sectors available in dataset
|
|
330
|
+
* @returns Array of sector names (e.g., ['elec', 'trans', 'indus'])
|
|
331
|
+
*/
|
|
332
|
+
public getAvailableSectors(): string[] {
|
|
333
|
+
if (this.data.length === 0) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Extract sectors from first fuel object (structure should be consistent)
|
|
338
|
+
const firstPoint = this.data[0];
|
|
339
|
+
const firstFuel = (firstPoint as any)['elec']; // Use elec as reference
|
|
340
|
+
if (!firstFuel) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return Object.keys(firstFuel);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get comprehensive statistics about the dataset
|
|
349
|
+
* Provides metadata useful for analysis, debugging, and performance monitoring
|
|
350
|
+
* @returns Object containing dataset statistics and metadata
|
|
351
|
+
*/
|
|
352
|
+
public getDataStatistics(): {
|
|
353
|
+
totalDataPoints: number;
|
|
354
|
+
yearRange: [number, number];
|
|
355
|
+
averageYearGap: number;
|
|
356
|
+
milestonesCount: number;
|
|
357
|
+
fuelsCount: number;
|
|
358
|
+
sectorsCount: number;
|
|
359
|
+
} {
|
|
360
|
+
const milestonesCount = this.getYearsWithMilestones().length;
|
|
361
|
+
const fuelsCount = this.getAvailableFuels().length;
|
|
362
|
+
const sectorsCount = this.getAvailableSectors().length;
|
|
363
|
+
|
|
364
|
+
// Calculate average gap between consecutive years
|
|
365
|
+
let totalGap = 0;
|
|
366
|
+
for (let i = 1; i < this.yearsLength; i++) {
|
|
367
|
+
totalGap += this.years[i] - this.years[i - 1];
|
|
368
|
+
}
|
|
369
|
+
const averageYearGap = this.yearsLength > 1 ? totalGap / (this.yearsLength - 1) : 0;
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
totalDataPoints: this.data.length,
|
|
373
|
+
yearRange: [this.firstYear, this.lastYear],
|
|
374
|
+
averageYearGap,
|
|
375
|
+
milestonesCount,
|
|
376
|
+
fuelsCount,
|
|
377
|
+
sectorsCount
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {EnergyDataPoint} from '@/types';
|
|
2
|
+
import {DataValidationError} from '@/types';
|
|
3
|
+
import {EventBus} from '@/core/events/EventBus';
|
|
4
|
+
import {Logger} from "@/utils/Logger";
|
|
5
|
+
import {ConfigurationService} from "@/services/ConfigurationService";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Data Validation Service
|
|
9
|
+
*
|
|
10
|
+
* Comprehensive validation service ensuring data integrity and structure
|
|
11
|
+
* compliance for energy flow datasets. Performs multi-level validation
|
|
12
|
+
* including structure, content, and value range verification.
|
|
13
|
+
*
|
|
14
|
+
* Validation Levels:
|
|
15
|
+
* 1. Structure Validation - Array format and basic structure checks
|
|
16
|
+
* 2. Content Validation - Required sectors and breakdown verification
|
|
17
|
+
* 3. Type Validation - Ensures proper data types for all values
|
|
18
|
+
* 4. Range Validation - Warns about unusual values or potential errors
|
|
19
|
+
* 5. Duplicate Detection - Prevents duplicate years in dataset
|
|
20
|
+
*
|
|
21
|
+
* Error Handling:
|
|
22
|
+
* Throws DataValidationError with detailed context for failed validations,
|
|
23
|
+
* including field names, indices, and descriptive error messages.
|
|
24
|
+
* Emits validation events for service coordination and monitoring.
|
|
25
|
+
*
|
|
26
|
+
* Required Data Structure:
|
|
27
|
+
* Each data point must contain all major fuel types (solar, nuclear, hydro,
|
|
28
|
+
* wind, geothermal, gas, coal, biomass, petroleum) with consumption
|
|
29
|
+
* breakdowns across sectors (electricity, residential, agriculture,
|
|
30
|
+
* industrial, transportation).
|
|
31
|
+
*
|
|
32
|
+
* Provides comprehensive validation for energy flow datasets with
|
|
33
|
+
* multi-level checks ensuring data integrity, proper structure,
|
|
34
|
+
* and content completeness. Maintains strict validation rules
|
|
35
|
+
* to prevent downstream calculation errors.
|
|
36
|
+
*/
|
|
37
|
+
export class DataValidationService {
|
|
38
|
+
constructor(private configurationService: ConfigurationService, private eventBus: EventBus, private logger: Logger) {
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate complete data array with comprehensive checks
|
|
43
|
+
* Performs structure, content, and type validation for entire dataset
|
|
44
|
+
* @param data - Array of energy data points to validate
|
|
45
|
+
* @throws DataValidationError - If any validation check fails
|
|
46
|
+
*/
|
|
47
|
+
public validateData(data: EnergyDataPoint[]): void {
|
|
48
|
+
const startTime = performance.now(); // Performance monitoring for validation efficiency
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Basic array structure validation - Critical first line of defense
|
|
52
|
+
// Prevents downstream calculation errors from malformed input
|
|
53
|
+
if (!Array.isArray(data)) {
|
|
54
|
+
throw new DataValidationError('Data must be an array', 'data');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data.length === 0) {
|
|
58
|
+
throw new DataValidationError('Data array cannot be empty', 'data');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate all required fuel sectors are present - Energy Industry Standard Compliance
|
|
62
|
+
// These sectors represent the complete US energy landscape per EIA standards
|
|
63
|
+
// Missing sectors would cause calculation failures in downstream services
|
|
64
|
+
const requiredSectors = ['elec', 'waste', 'solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro'];
|
|
65
|
+
|
|
66
|
+
// Validate consumption breakdown for each sector - Mathematical Integrity Check
|
|
67
|
+
// Each fuel sector must have consumption across all major sectors:
|
|
68
|
+
// elec (electricity), res (residential), ag (agriculture), indus (industrial), trans (transportation)
|
|
69
|
+
// This structure is required for the triple nested loop calculations in SummaryService
|
|
70
|
+
const requiredBreakdown = ['elec', 'res', 'ag', 'indus', 'trans'];
|
|
71
|
+
|
|
72
|
+
let hasHeatData = false;
|
|
73
|
+
|
|
74
|
+
// Validate each data point in the array - Comprehensive structural integrity check
|
|
75
|
+
// O(n) iteration ensures every data point meets strict requirements
|
|
76
|
+
for (let i = 0; i < data.length; i++) {
|
|
77
|
+
const point = data[i];
|
|
78
|
+
|
|
79
|
+
// Year validation - Critical for timeline functionality
|
|
80
|
+
// Year must be numeric for chronological sorting and animation
|
|
81
|
+
if (!point.year) {
|
|
82
|
+
throw new DataValidationError(`Invalid year at index ${i}`, 'year');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const sector of requiredSectors) {
|
|
86
|
+
if (!(sector in point)) {
|
|
87
|
+
throw new DataValidationError(`Missing sector '${sector}' in data point for year ${point.year}`, sector);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sectorData = (point as any)[sector];
|
|
91
|
+
if (!sectorData || typeof sectorData !== 'object') {
|
|
92
|
+
throw new DataValidationError(`Invalid sector data for '${sector}' in year ${point.year}`, sector);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let currentRequiredBreakdown = [...requiredBreakdown];
|
|
96
|
+
if ("heat" in point) {
|
|
97
|
+
hasHeatData = true;
|
|
98
|
+
currentRequiredBreakdown.push("heat");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const breakdown of currentRequiredBreakdown) {
|
|
102
|
+
if (!(breakdown in sectorData) || typeof sectorData[breakdown] !== 'number') {
|
|
103
|
+
throw new DataValidationError(`Invalid breakdown '${breakdown}' for sector '${sector}' in year ${point.year}`, `${sector}.${breakdown}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (hasHeatData) {
|
|
110
|
+
this.configurationService.hasHeatData = hasHeatData;
|
|
111
|
+
}
|
|
112
|
+
// this.configurationService.updateFuelAndBoxNames()
|
|
113
|
+
|
|
114
|
+
const endTime = performance.now();
|
|
115
|
+
|
|
116
|
+
// Emit validation success event for service coordination - Event-Driven Architecture
|
|
117
|
+
// This notifies other services that data is validated and safe to use
|
|
118
|
+
// Provides validation metadata for performance monitoring and debugging
|
|
119
|
+
this.eventBus.emit({
|
|
120
|
+
type: 'data.validated',
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
source: 'DataValidationService',
|
|
123
|
+
data: {
|
|
124
|
+
dataPointCount: data.length, // Dataset size for capacity planning
|
|
125
|
+
yearRange: data.length > 0 ? [data[0].year, data[data.length - 1].year] : [0, 0], // Temporal scope
|
|
126
|
+
validationTime: endTime - startTime // Performance metrics for optimization
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Debug logging when enabled - Performance Monitoring
|
|
131
|
+
// Tracks validation performance to identify potential bottlenecks
|
|
132
|
+
this.logger.log(`DataValidationService: ${data.length} data points validated in ${(endTime - startTime).toFixed(2)}ms`);
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Emit validation error event for error handling - Error Recovery Architecture
|
|
136
|
+
// Enables centralized error handling and user notification systems
|
|
137
|
+
// Marks validation errors as non-recoverable (require data fix)
|
|
138
|
+
this.eventBus.emit({
|
|
139
|
+
type: 'system.error',
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
source: 'DataValidationService',
|
|
142
|
+
data: {
|
|
143
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
144
|
+
context: 'data_validation', // Error categorization for debugging
|
|
145
|
+
recoverable: false // Validation errors require data correction
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Re-throw error to maintain validation contract - Fail-Fast Pattern
|
|
150
|
+
// Prevents invalid data from propagating to calculation services
|
|
151
|
+
// Maintains data integrity throughout the system
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|