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