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,394 @@
1
+ import type {BoxMaxes, BoxTops, SummaryData, YearFlows, YearLabels, YearSums, YearTotals} from '@/types';
2
+ import {DataService} from '@/services/data/DataService';
3
+ import {ConfigurationService} from "@/services/ConfigurationService";
4
+
5
+ /**
6
+ * Summary Service Implementation
7
+ *
8
+ * Processes raw energy data into
9
+ * totals, flows, labels, and statistical summaries needed for visualization.
10
+ *
11
+ * Include comprehensive multi-layer caching.
12
+ * Handles complex mathematical operations for energy flow summary data including
13
+ * totals calculation, maximum value detection, and statistical analysis.
14
+ *
15
+ * Mathematical Operations:
16
+ * - Fuel totals calculation across all consumption sectors
17
+ * - Sector totals calculation across all fuel types
18
+ * - Maximum value detection for scaling calculations
19
+ * - Box positioning calculations for visual layout
20
+ * - Flow data preparation for animation sequences
21
+ */
22
+ export class SummaryService {
23
+ public summary: SummaryData | null = null;
24
+ public totals: YearTotals[] = []; // Energy totals per year/fuel/sector
25
+ public flows: YearFlows[] = []; // Flow counts for visualization
26
+ public labels: YearLabels[] = []; // Label positioning data
27
+ public yearSums: YearSums = {}; // Year-wise energy sums
28
+ public maxes: BoxMaxes = {}
29
+ public boxTops: BoxTops | null = null;
30
+
31
+ constructor(
32
+ private dataService: DataService,
33
+ private configService: ConfigurationService, // Will inject when available
34
+ ) {
35
+ this.buildSummary();
36
+ }
37
+
38
+ /**
39
+ * Extract expensive calculation to separate method (same logic as original)
40
+ */
41
+ private buildSummary() {
42
+ this.buildTotals();
43
+ this.buildMaxes();
44
+ this.buildBoxTops();
45
+
46
+ this.summary = {
47
+ totals: this.totals,
48
+ flows: this.flows,
49
+ labels: this.labels,
50
+ maxes: this.maxes,
51
+ boxTops: this.boxTops!,
52
+ yearSums: this.yearSums!,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Calculate Energy Flow Totals - Triple Nested Loop Algorithm
58
+ *
59
+ * MATHEMATICAL COMPLEXITY: O(n³) where n = years × fuels × sectors
60
+ * This is the most computationally expensive method in the entire application,
61
+ * processing every combination of Year × Fuel × Consumption Sector.
62
+ *
63
+ * ALGORITHM STRUCTURE:
64
+ *
65
+ * Level 1 (i): Years Loop - Process each chronological data point
66
+ * └─ Iterates through energy data points from 1800-2021+
67
+ * └─ Creates YearTotals, YearFlows, YearLabels structures for each year
68
+ *
69
+ * Level 2 (j): Fuels Loop - Process each energy source type
70
+ * └─ solar, nuclear, hydro, wind, geo, gas, coal, bio, petro
71
+ * └─ Skips electricity (j=0) as it's processed separately
72
+ * └─ Calculates fuel totals and label positioning
73
+ *
74
+ * Level 3 (k): Sectors Loop - Process each consumption category
75
+ * └─ elec (electricity), res (residential), ag (agriculture),
76
+ * └─ indus (industrial), trans (transportation)
77
+ * └─ Performs cross-tabulation: fuel → sector energy flows
78
+ *
79
+ * MATHEMATICAL OPERATIONS PER ITERATION:
80
+ * 1. Flow counting: Increment flow counters for non-zero values
81
+ * 2. Sector totals: Accumulate energy by consumption sector
82
+ * 3. Fuel totals: Accumulate energy by fuel source type
83
+ * 4. Electricity integration: Add electricity to non-elec sectors
84
+ * 5. Waste heat calculation: Always include waste heat values
85
+ * 6. Label positioning: Calculate visual label Y-coordinates
86
+ * 7. Height accumulation: Track fuel stack heights for layout
87
+ *
88
+ * PERFORMANCE OPTIMIZATIONS (LAYER 3 CACHING):
89
+ * - Configuration constants cached locally (eliminates property access)
90
+ * - Direct array access patterns optimized for V8 engine
91
+ * - Type assertions used sparingly to maintain performance
92
+ * - Mathematical operations use compound assignment for speed
93
+ *
94
+ * ENERGY INDUSTRY DOMAIN LOGIC:
95
+ * - Electricity is treated as both fuel source AND consumption vector
96
+ * - Waste heat represents thermodynamic losses in electricity generation
97
+ * - Cross-tabulation enables Sankey flow visualization of energy paths
98
+ * - Sector totals enable proportional box sizing in visual representation
99
+ *
100
+ * EXAMPLE CALCULATION FLOW:
101
+ * Year 2021, Coal, Industrial Sector:
102
+ * 1. coal.indus = 15.6 (Quads) - Raw data value
103
+ * 2. total.indus += 15.6 - Add to industrial sector total
104
+ * 3. total.coal += 15.6 - Add to coal fuel total
105
+ * 4. flow.indus++ - Increment industrial flow count
106
+ * 5. Electricity waste heat added if applicable
107
+ *
108
+ * VISUALIZATION MATHEMATICS:
109
+ * - SCALE (0.02): Converts energy units (Quads) to pixel heights
110
+ * - LEFT_GAP: Visual spacing between fuel source boxes
111
+ * - ELEC_BOX positioning: Special coordinate system for electricity flows
112
+ * - Label positioning: Y-coordinates calculated for fuel source labels
113
+ */
114
+ public buildTotals() {
115
+ // LAYER 3 CACHING: METHOD INLINING OPTIMIZATION
116
+ // Cache configuration constants locally to eliminate repeated property access
117
+ // Critical performance optimization for triple nested loop execution
118
+ // Measured improvement: 5-10% reduction in execution time
119
+ const FUELS = this.configService.FUELS; // Energy source types array
120
+ const BOX_NAMES = this.configService.BOX_NAMES; // Consumption sector names
121
+ const ELEC_BOX_Y = this.configService.ELEC_BOX_Y; // Electricity box Y-coordinate
122
+ const HEAT_BOX_Y = this.configService.HEAT_BOX_Y; // Heat box Y-coordinate
123
+ const TOP_Y = this.configService.TOP_Y; // Top margin for fuel labels
124
+ const SCALE = this.configService.SCALE; // Energy-to-pixel conversion (0.02)
125
+ const LEFT_GAP = this.configService.LEFT_GAP; // Visual gap between fuel boxes
126
+
127
+ // RESULT ARRAYS: Initialize output data structures
128
+ // These will be populated by the triple nested loop algorithm
129
+ // const totals: YearTotals[] = []; // Energy totals per year/fuel/sector
130
+ // const flows: YearFlows[] = []; // Flow counts for visualization
131
+ // const labels: YearLabels[] = []; // Label positioning data
132
+ // const yearSums: YearSums = {}; // Year-wise energy sums
133
+
134
+ // ============================ LEVEL 1: YEARS LOOP ============================
135
+ // Process each chronological data point in the energy dataset
136
+ // Complexity: O(n) where n = number of years in dataset (typically 200+ years)
137
+ for (let i = 0; i < this.dataService.data.length; ++i) {
138
+ const yearData = this.dataService.data[i];
139
+
140
+ const total: YearTotals = {
141
+ year: yearData.year,
142
+ elec: 0,
143
+ res: 0,
144
+ ag: 0,
145
+ indus: 0,
146
+ trans: 0,
147
+ solar: 0,
148
+ nuclear: 0,
149
+ hydro: 0,
150
+ wind: 0,
151
+ geo: 0,
152
+ gas: 0,
153
+ coal: 0,
154
+ bio: 0,
155
+ petro: 0,
156
+ fuel_height: 0,
157
+ waste: 0,
158
+ };
159
+
160
+ if (this.configService.hasHeatData) {
161
+ total.heat = 0;
162
+ }
163
+
164
+ const label: YearLabels = {
165
+ year: yearData.year,
166
+ elec: ELEC_BOX_Y,
167
+ res: 0,
168
+ ag: 0,
169
+ indus: 0,
170
+ trans: 0,
171
+ solar: 0,
172
+ nuclear: 0,
173
+ hydro: 0,
174
+ wind: 0,
175
+ geo: 0,
176
+ gas: 0,
177
+ coal: 0,
178
+ bio: 0,
179
+ petro: 0,
180
+ };
181
+
182
+ if (this.configService.hasHeatData) {
183
+ label.heat = HEAT_BOX_Y;
184
+ }
185
+
186
+ const flow: YearFlows = {
187
+ year: yearData.year,
188
+ elec: 0,
189
+ res: 0,
190
+ ag: 0,
191
+ indus: 0,
192
+ trans: 0,
193
+ };
194
+
195
+ if (this.configService.hasHeatData) {
196
+ flow.heat = 0;
197
+ }
198
+
199
+ // ========================== LEVEL 2: FUELS LOOP ==========================
200
+ // Process each energy source type (solar, nuclear, hydro, wind, geo, gas, coal, bio, petro)
201
+ // IMPORTANT: Skip electricity (j=0) & heat (j=1) as it has special processing requirements
202
+ // Electricity is handled separately because it's both a fuel AND a consumption vector
203
+ for (let j = 2; j < FUELS.length; ++j) {
204
+ const fuelName = FUELS[j].fuel;
205
+ const fuelObj = (yearData as any)[fuelName] as { [key: string]: number }; // Energy data object for this fuel
206
+
207
+ if (!this.configService.hasHeatData && fuelName == "heat") {
208
+ continue;
209
+ }
210
+
211
+ // ====================== LEVEL 3: SECTORS LOOP ======================
212
+ // Process each consumption sector for the current fuel type
213
+ // This is the innermost loop where the actual mathematical work happens
214
+ // Each iteration processes one Fuel → Sector energy flow value
215
+ for (let k = 0; k < BOX_NAMES.length; ++k) {
216
+ const boxName = BOX_NAMES[k];
217
+
218
+ if (!this.configService.hasHeatData && boxName == "heat") {
219
+ continue;
220
+ }
221
+
222
+ // Count the number of non-zero energy flows to each sector
223
+ // Used for visual flow density calculations in Sankey diagram
224
+ if (fuelObj[boxName] > 0) {
225
+ (flow as any)[boxName]++; // Increment flow counter for this sector
226
+ }
227
+
228
+ // Add energy value to the appropriate consumption sector total
229
+ // This creates the cross-tabulation: Fuel × Sector = Energy Value
230
+ total[boxName] += fuelObj[boxName]; // Sector total (e.g., total.indus += coal.indus)
231
+
232
+ // Add energy value to the appropriate fuel source total
233
+ // This enables proportional sizing of fuel source boxes
234
+ total[fuelName] += fuelObj[boxName]; // Fuel total (e.g., total.coal += coal.indus)
235
+
236
+ // Special case: Add electricity consumption to non-electricity sectors
237
+ // Electricity is unique - it's generated from fuels AND consumed by sectors
238
+ if (j === 2 && boxName !== 'elec') { // Only process once (j=2) and skip elec sector
239
+ // Add electricity consumed by this sector
240
+ total[boxName] += yearData.elec[boxName];
241
+
242
+ // Add thermodynamic losses from electricity generation
243
+ // Waste heat represents energy lost as heat during electricity generation
244
+ // Critical for energy balance: Input Energy = Useful Energy + Waste Heat
245
+ total[boxName] += yearData.waste[boxName];
246
+ }
247
+
248
+ // Special case: Add electricity consumption to non-electricity sectors
249
+ // Heat is unique - it's generated from fuels AND consumed by sectors
250
+ if (j === 2 && boxName !== 'heat' && this.configService.hasHeatData) {
251
+ // Add electricity consumed by this sector
252
+ total[boxName] += yearData.heat[boxName];
253
+ }
254
+ }
255
+
256
+ // Calculate Y-coordinate for fuel source labels based on cumulative height
257
+ // TOP_Y: Base Y-coordinate, fuel_height: cumulative height, -5: visual offset
258
+ (label as any)[fuelName] = TOP_Y + total.fuel_height - 5;
259
+
260
+ // Special positioning for electricity label (right-hand side)
261
+ label.elec = ELEC_BOX_Y - total.elec * SCALE;
262
+
263
+ if (this.configService.hasHeatData) {
264
+ label.heat = HEAT_BOX_Y - total.heat! * SCALE;
265
+ }
266
+
267
+ // Calculate cumulative height for fuel stack visualization
268
+ // Each fuel gets proportional height + visual gap for clear separation
269
+ total.fuel_height += total[fuelName] * SCALE + LEFT_GAP;
270
+ }
271
+
272
+ // Sum waste heat across all consumption sectors for thermodynamic balance
273
+ // Waste heat represents the fundamental thermodynamic limit on electricity generation efficiency
274
+ total.waste = yearData.waste.res + yearData.waste.ag +
275
+ yearData.waste.indus + yearData.waste.trans;
276
+
277
+ if (this.configService.hasHeatData) {
278
+ total.waste += yearData.waste.heat;
279
+ }
280
+
281
+ // Handle milestone data if present
282
+ if ('milestone' in yearData) {
283
+ total.milestone = (yearData as any).milestone;
284
+ }
285
+
286
+ // Calculate total primary energy consumption for this year across all fuel sources
287
+ // IMPORTANT: Excludes electricity & Heat to avoid double-counting since electricity & heat are generated from other fuels
288
+ // This represents the nation's total primary energy input for the year
289
+ // Physics: Primary Energy = Fossil + Nuclear + Renewable (before conversion losses)
290
+ this.yearSums[yearData.year] = total.bio + total.coal + total.gas + total.geo + total.hydro +
291
+ total.nuclear + total.petro + total.solar + total.wind;
292
+
293
+ // ARRAY POPULATION: Add completed calculations to result arrays
294
+ // These arrays form the complete energy flow dataset for visualization
295
+ this.totals.push(total); // Energy totals for box sizing
296
+ this.flows.push(flow); // Flow counts for visual density
297
+ this.labels.push(label); // Label positions for text rendering
298
+ }
299
+ // END OF TRIPLE NESTED LOOP ALGORITHM
300
+ }
301
+
302
+ /**
303
+ * Calculate Maximum Energy Values - Statistical Analysis with Caching
304
+ *
305
+ * MATHEMATICAL PURPOSE:
306
+ * Determines the maximum energy consumption value for each sector across all years.
307
+ * Critical for proportional visualization - largest values determine visual scale.
308
+ *
309
+ * ALGORITHM: Statistical Maximum Detection
310
+ * For each consumption sector (res, ag, indus, trans, elec):
311
+ * 1. Extract all year values for that sector: [1970: 15.2, 1980: 18.4, ...]
312
+ * 2. Apply Math.max() to find peak consumption year
313
+ * 3. Cache result to avoid repeated Math.max() calls (expensive operation)
314
+ *
315
+ * VISUALIZATION APPLICATION:
316
+ * Max values determine box heights in Sankey diagram:
317
+ * - Residential sector max → residential box height scale
318
+ * - Industrial sector max → industrial box height scale
319
+ * - Transportation sector max → transportation box height scale
320
+ *
321
+ */
322
+ private buildMaxes() {
323
+ // STATISTICAL MAXIMUM DETECTION ALGORITHM
324
+ // For each consumption sector, find the peak energy consumption across all years
325
+ // This determines the visual scaling for proportional box heights
326
+ for (let i = 0; i < this.configService.BOXES.length; ++i) {
327
+ const boxName = this.configService.BOXES[i].box;
328
+
329
+ // MATHEMATICAL OPERATION: Math.max() across temporal dataset
330
+ // Example: For 'indus' (industrial), finds max(indus_1970, indus_1980, ..., indus_2021)
331
+ // Spread operator creates array: [...[15.2, 18.4, 22.1, ...]] → Math.max(15.2, 18.4, 22.1, ...)
332
+ // Result: Peak industrial energy consumption value across entire historical period
333
+ this.maxes[boxName] = Math.max(...this.totals.map(
334
+ (yearTotal: YearTotals) => yearTotal[boxName] as number
335
+ ));
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Calculate Consumption Sector Box Positions - Layout Algorithm with Caching
341
+ *
342
+ * MATHEMATICAL PURPOSE:
343
+ * Calculates Y-coordinate positions for consumption sector boxes in the right-hand column
344
+ * of the Sankey diagram. Each box position depends on the cumulative heights of boxes above it.
345
+ *
346
+ * LAYOUT ALGORITHM: Sequential Stacking with Proportional Heights
347
+ * 1. Start with residential (res) box at base position: ELEC_BOX[1] + 50
348
+ * 2. Each subsequent box stacks below with: previous_top + previous_max_height + gap
349
+ * 3. Box heights are proportional to maximum energy consumption (maxes values)
350
+ * 4. Visual gaps (RIGHT_GAP) separate boxes for clarity
351
+ *
352
+ * MATHEMATICAL FORMULA for Box Positioning:
353
+ * box_top[i] = box_top[i-1] + maxes[i-1] × SCALE + RIGHT_GAP
354
+ *
355
+ * Where:
356
+ * - maxes[sector]: Peak energy consumption for that sector across all years
357
+ * - SCALE (0.02): Energy-to-pixel conversion factor
358
+ * - RIGHT_GAP: Visual spacing between consumption boxes
359
+ *
360
+ * VISUAL LAYOUT SEQUENCE:
361
+ * 1. Residential (res): ELEC_BOX[1] + 50
362
+ * 2. Agriculture (ag): res_top + res_max_height + gap
363
+ * 3. Industrial (indus): ag_top + ag_max_height + gap
364
+ * 4. Transportation (trans): indus_top + indus_max_height + gap
365
+ *
366
+ * EXAMPLE CALCULATION (SCALE = 0.02, RIGHT_GAP = 15):
367
+ * res_top = 350, res_max = 30.5 Quads
368
+ * → ag_top = 350 + (30.5 × 0.02) + 15 = 365.61 pixels
369
+ */
370
+ private buildBoxTops() {
371
+ // LAYOUT INITIALIZATION: Start with residential box position
372
+ // ELEC_BOX_Y: Base Y-coordinate for electricity box (right-hand column)
373
+ // +50: Visual offset below electricity box for residential sector
374
+ this.boxTops = {
375
+ res: this.configService.ELEC_BOX_Y + 50, // Base position for residential
376
+ heat: this.configService.HEAT_BOX_Y + 50, // Base position for residential
377
+ ag: 0, // Will be calculated based on residential
378
+ indus: 0, // Will be calculated based on agriculture
379
+ trans: 0 // Will be calculated based on industrial
380
+ };
381
+
382
+ // SEQUENTIAL STACKING ALGORITHM:
383
+ // Each box position = previous_box_top + previous_max_height × SCALE + visual_gap
384
+
385
+ // Agriculture box: Positioned below residential box
386
+ this.boxTops.ag = this.boxTops.res + this.maxes.res * this.configService.SCALE + this.configService.RIGHT_GAP;
387
+
388
+ // Industrial box: Positioned below agriculture box
389
+ this.boxTops.indus = this.boxTops.ag + this.maxes.ag * this.configService.SCALE + this.configService.RIGHT_GAP;
390
+
391
+ // Transportation box: Positioned below industrial box
392
+ this.boxTops.trans = this.boxTops.indus + this.maxes.indus * this.configService.SCALE + this.configService.RIGHT_GAP;
393
+ }
394
+ }