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,616 @@
1
+ import * as d3 from 'd3';
2
+ import {EnergySectorBreakdown, GraphData, GraphPoint, GraphStroke, Offest} from '@/types';
3
+ import {DataService} from "@/services/data/DataService";
4
+ import {ConfigurationService} from "@/services/ConfigurationService";
5
+ import {SummaryService} from "@/services/calculation/SummaryService";
6
+
7
+
8
+ /**
9
+ * Graph Service
10
+ *
11
+ * Performs complex mathematical calculations for energy flow positioning and routing.
12
+ * Handles the sophisticated algorithms needed to calculate Sankey diagram paths,
13
+ * including triple nested loops for flow positioning and waste heat calculations.
14
+ *
15
+ * Key Algorithms:
16
+ * - Complex flow positioning with mathematical precision
17
+ * - Waste heat cloning and distribution calculations
18
+ * - Multi-layer caching system for performance optimization
19
+ * - D3 line generation for smooth rendering
20
+ * - Graph data structure management and optimization
21
+ */
22
+ export class GraphService {
23
+ public graphs: GraphData[] = [];
24
+
25
+ constructor(
26
+ private configService: ConfigurationService, // Will inject when available
27
+ private dataService: DataService,
28
+ private summaryCalculationService: SummaryService,
29
+ ) {
30
+ this.buildGraphs();
31
+ }
32
+
33
+ /**
34
+ * Extract expensive calculation to separate method (same logic as original)
35
+ */
36
+ private buildGraphs() {
37
+ this.calculateGraphY();
38
+ console.log("calculateGraphY", JSON.stringify(this.graphs));
39
+ this.calculateGraphX();
40
+ console.log("calculateGraphX", JSON.stringify(this.graphs));
41
+
42
+ this.spaceUpsAndDowns();
43
+ console.log("spaceUpsAndDowns", JSON.stringify(this.graphs));
44
+
45
+ // Process waste heat flows
46
+ this.processWasteHeatFlows();
47
+ console.log("processWasteHeatFlows", JSON.stringify(this.graphs));
48
+ }
49
+
50
+ /**
51
+ * Calculate Flow Y-Coordinates - Complex Triple Nested Loop Algorithm
52
+ *
53
+ * COMPUTATIONAL COMPLEXITY: O(n³) - Years × Fuels × Sectors
54
+ * This is the most mathematically sophisticated method in the entire energy visualization system,
55
+ * handling precise flow positioning, coordinate calculations, and waste heat thermodynamics.
56
+ *
57
+ * ALGORITHM STRUCTURE - THREE NESTED LEVELS:
58
+ *
59
+ * Level 1 (i): Years Loop - Process each chronological data point
60
+ * └─ Creates GraphStroke arrays for energy flow paths
61
+ * └─ Manages vertical offset tracking for precise positioning
62
+ *
63
+ * Level 2 (j): Fuels Loop - Process each energy source type
64
+ * └─ Special handling for electricity (j=0) vs. primary fuels (j>0)
65
+ * └─ Calculates fuel-specific positioning and offsets
66
+ *
67
+ * Level 3 (k): Sectors Loop - Process each consumption category
68
+ * └─ Creates GraphStroke objects for each Fuel → Sector flow
69
+ * └─ Applies complex coordinate mathematics for positioning
70
+ *
71
+ * CRITICAL COORDINATE MATHEMATICS:
72
+ * 1. Y-Coordinate Positioning: Uses cumulative offset tracking
73
+ * 2. Stroke Width Calculation: Energy value × SCALE factor
74
+ * 3. Control Points: mathematics for smooth flow
75
+ * 4. Waste Heat Cloning: Deep object cloning with thermodynamic calculations
76
+ *
77
+ * WASTE HEAT PHYSICS IMPLEMENTATION:
78
+ * Implements the fundamental thermodynamic principle that electricity generation
79
+ * produces waste heat according to Carnot efficiency limits. Each electricity
80
+ * flow gets a corresponding waste heat flow with identical path geometry.
81
+ *
82
+ * COORDINATE SYSTEM DETAILS:
83
+ * - SCALE (0.02): Converts energy units (Quads) to pixel heights
84
+ * - ELEC_BOX coordinates: Special positioning for electricity flows
85
+ * - SR3: Slope ratio for smooth transitions (slope = height/3)
86
+ * - PATH_GAP: Visual spacing between parallel flow paths
87
+ * - LEFT_GAP: Spacing between fuel source boxes
88
+ *
89
+ * PERFORMANCE OPTIMIZATIONS:
90
+ * - Method inlining: Configuration constants cached locally
91
+ * - Direct array indexing: Eliminates object property lookups
92
+ * - In-place calculations: Minimizes temporary object creation
93
+ *
94
+ * MATHEMATICAL PRECISION REQUIREMENTS:
95
+ * All calculations must maintain sub-pixel precision to ensure:
96
+ * - Smooth flow animations during year transitions
97
+ * - Perfect alignment between interconnected flows
98
+ * - Accurate proportional representation of energy values
99
+ * - Thermodynamically correct waste heat positioning
100
+ */
101
+ public calculateGraphY() {
102
+ // METHOD INLINING OPTIMIZATION: Cache all configuration constants locally
103
+ // Eliminates repeated property access during O(n³) loop execution
104
+ // Performance improvement: 5-10% reduction in execution time
105
+ const SCALE = this.configService.SCALE; // Energy-to-pixel conversion (0.02)
106
+ const ELEC_BOX_X = this.configService.ELEC_BOX_X; // Electricity box X-coordinate
107
+ const ELEC_BOX_Y = this.configService.ELEC_BOX_Y; // Electricity box Y-coordinate
108
+ const HEAT_BOX_X = this.configService.HEAT_BOX_X; // Heat box X-coordinate
109
+ const HEAT_BOX_Y = this.configService.HEAT_BOX_Y; // Heat box Y-coordinate
110
+ const BOX_WIDTH = this.configService.BOX_WIDTH; // Standard box width
111
+ const LEFT_X = this.configService.LEFT_X; // Left column X-coordinate
112
+ const TOP_Y = this.configService.TOP_Y; // Top margin Y-coordinate
113
+ const SR3 = this.configService.SR3; // Slope ratio (height/3)
114
+ const PATH_GAP = this.configService.PATH_GAP; // Visual gap between flow paths
115
+ const LEFT_GAP = this.configService.LEFT_GAP; // Gap between fuel boxes
116
+ const FUELS = this.configService.FUELS; // Energy source definitions
117
+ const BOX_NAMES = this.configService.BOX_NAMES; // Consumption sector names
118
+ const WIDTH = this.configService.WIDTH; // Canvas width
119
+ const FLOW_PATHS_ORDER = this.configService.FLOW_PATHS_ORDER;
120
+
121
+ const summary = this.summaryCalculationService.summary!;
122
+
123
+ // ======================== LEVEL 1: YEARS LOOP ========================
124
+ // Process each chronological data point in the energy dataset
125
+ // Creates complete flow network for one year before moving to next year
126
+ // Complexity: O(n) where n = number of years (typically 200+ historical data points)
127
+ for (let i = 0; i < this.dataService.data.length; ++i) {
128
+ let graph: GraphStroke[] = []; // GraphStroke array for this year
129
+
130
+ // COORDINATE TRACKING SYSTEM: Precise vertical offset management
131
+ // Critical for maintaining accurate flow positioning and preventing visual overlaps
132
+ let leftY = TOP_Y; // Current Y-position in left column
133
+ let elecY = ELEC_BOX_Y - (summary.totals[i].elec) * SCALE; // Electricity box Y-position
134
+ let heatY;
135
+ if (this.configService.hasHeatData) {
136
+ heatY = HEAT_BOX_Y - (summary.totals[i].heat!) * SCALE; // Heat box Y-position
137
+ }
138
+
139
+ // OFFSET TRACKING MATRICES: Track cumulative positioning offsets
140
+ // Y-offsets: Vertical positioning within each consumption sector box
141
+ // X-offsets: Horizontal positioning for fuel source alignment (future use)
142
+ let offsets: Offest = {
143
+ x: {
144
+ solar: 0,
145
+ nuclear: 0,
146
+ hydro: 0,
147
+ wind: 0,
148
+ geo: 0,
149
+ gas: 0,
150
+ coal: 0,
151
+ bio: 0,
152
+ petro: 0
153
+ },
154
+ y: {
155
+ elec: 0,
156
+ res: 0,
157
+ ag: 0,
158
+ indus: 0,
159
+ trans: 0,
160
+ },
161
+
162
+ };
163
+
164
+ if (this.configService.hasHeatData) {
165
+ offsets.y.heat = 0;
166
+ }
167
+
168
+ // YEAR-SPECIFIC DATA EXTRACTION: Get calculated totals and flows for current year
169
+ const currentYear = this.dataService.data[i].year;
170
+ let totals = summary.totals.filter(d => d.year === currentYear)[0]; // Energy totals for this year
171
+ let flows = summary.flows.filter(d => d.year === currentYear)[0]; // Flow counts for this year
172
+
173
+ // CALCULATION STATE VARIABLES: Track loop state for complex calculations
174
+ let halfStroke: number | null = null; // Half of flow stroke width
175
+ let lastBox: string | null = null; // Previous sector for waste heat logic
176
+
177
+ // WASTE HEAT DATA EXTRACTION: Critical for thermodynamic calculations
178
+ // Waste heat represents energy losses in electricity generation process
179
+ let wasteObj = this.dataService.data[i]['waste']; // Waste heat data object
180
+
181
+ // ======================= LEVEL 2: FUELS LOOP =======================
182
+ // Process each energy source type (electricity, solar, nuclear, hydro, wind, geo, gas, coal, bio, petro)
183
+ // Special handling: Electricity (j=0) & Heat (j=1) vs. Primary Fuels (j>1) require different positioning algorithms
184
+ // Complexity: O(n) where n = number of fuel types (typically 10 fuel categories)
185
+
186
+ for (let j = 0; j < FUELS.length; ++j) {
187
+ let fuelName = FUELS[j].fuel;
188
+ if (!this.configService.hasHeatData && fuelName == "heat") {
189
+ continue;
190
+ }
191
+
192
+ let fuelObj: EnergySectorBreakdown = (this.dataService.data[i] as any)[fuelName];
193
+ fuelObj.total = 0;
194
+
195
+ // =================== LEVEL 3: SECTORS LOOP ===================
196
+ // Process each consumption sector for the current fuel type
197
+ // This is where the core mathematical work happens: Fuel × Sector → Flow
198
+ // Complexity: O(n) where n = number of sectors (typically 5: elec, res, ag, indus, trans)
199
+
200
+ const boxes = [...BOX_NAMES].sort((a, b) => FLOW_PATHS_ORDER[fuelName][a] - FLOW_PATHS_ORDER[fuelName][b])
201
+ for (let k = 0; k < boxes.length; ++k) {
202
+ const boxName = boxes[k];
203
+
204
+ // SPECIAL CASE: Skip electricity→electricity flow (self-loop prevention)
205
+ // SPECIAL CASE: Skip heat→heat flow (self-loop prevention)
206
+ if (fuelName === boxName) {
207
+ continue;
208
+ }
209
+
210
+ if (!this.configService.hasHeatData && boxName == "heat") {
211
+ continue;
212
+ }
213
+
214
+ // GRAPH STROKE OBJECT CREATION: Initialize flow path data structure
215
+ // This object contains all coordinate data needed for rendering
216
+ let g: GraphStroke = {
217
+ fuel: fuelName, // Source fuel type (e.g., 'coal')
218
+ box: boxName, // Target sector (e.g., 'indus')
219
+ stroke: 0, // Flow width in pixels (calculated below)
220
+ value: 0, // Energy value in Quads (calculated below)
221
+ a: {x: 0, y: 0}, // Flow start point coordinates
222
+ b: {x: 0, y: 0}, // First control point
223
+ c: {x: 0, y: 0}, // Second control point
224
+ cc: {x: 0, y: 0}, // Alternative control point (special cases)
225
+ d: {x: 0, y: 0} // Flow end point coordinates
226
+ };
227
+
228
+ // STROKE WIDTH CALCULATION: Convert energy value to visual thickness
229
+ // Energy is converted to pixels using SCALE factor (0.02)
230
+ // Half-stroke is used for center-line positioning mathematics
231
+ halfStroke = fuelObj[boxes[k]] * SCALE / 2; // Half of visual flow thickness
232
+ g.value = fuelObj[boxes[k]]; // Store original energy value
233
+
234
+ // Electricity (j=0) and primary fuels (j>1) have different source positions
235
+ // Heat (j=1) and primary fuels (j>1) have different source positions
236
+
237
+ // ELECTRICITY FLOWS (j=0): That Start from electricity box to the right boxes
238
+ if (j === 0) {
239
+ elecY += halfStroke; // Move down by half stroke width
240
+ g.a.y = elecY; // Set flow start Y-coordinate
241
+ g.a.x = ELEC_BOX_X + BOX_WIDTH; // Set flow start X-coordinate
242
+ elecY += halfStroke; // Move down by remaining half stroke
243
+ }
244
+ // HEAT FLOWS (j=1): That Start from heat box to the right boxes
245
+ else if (j === 1) {
246
+ heatY! += halfStroke; // Move down by half stroke width
247
+ g.a.y = heatY!; // Set flow start Y-coordinate
248
+ g.a.x = HEAT_BOX_X + BOX_WIDTH; // Set flow start X-coordinate
249
+ heatY! += halfStroke; // Move down by remaining half stroke
250
+ }
251
+ // PRIMARY FUEL FLOWS (j>1): Start from fuel boxes on the left
252
+ else {
253
+ leftY += halfStroke; // Move down by half stroke width
254
+ g.a.y = leftY; // Set flow start Y-coordinate
255
+ g.a.x = LEFT_X; // Set flow start X-coordinate
256
+ }
257
+
258
+ offsets.y[boxName as keyof typeof offsets.y] += halfStroke;
259
+ g.stroke = halfStroke * 2;
260
+ g.b.y = g.a.y;
261
+
262
+ // ELECTRICITY FLOWS: That Start from the left to the electricity box
263
+ if (boxName === 'elec') {
264
+ g.d.x = ELEC_BOX_X;
265
+ g.d.y = (ELEC_BOX_Y - totals.elec * SCALE + offsets.y.elec);
266
+
267
+ g.c.x = (ELEC_BOX_X - 20 -
268
+ (totals.elec * SCALE - offsets.y.elec) / SR3 -
269
+ (FUELS.length - j) * PATH_GAP);
270
+ g.b.x = (g.c.x - Math.abs(g.a.y - g.d.y) / SR3);
271
+ }
272
+ // HEAT FLOWS: That Start from the left to the electricity box
273
+ else if (boxName === 'heat') {
274
+ g.d.x = HEAT_BOX_X;
275
+ g.d.y = (HEAT_BOX_Y - totals.heat! * SCALE + offsets.y.heat!);
276
+
277
+ g.c.x = (HEAT_BOX_X - 20 -
278
+ (totals.heat! * SCALE - offsets.y.heat!) / SR3 -
279
+ (FUELS.length - j) * PATH_GAP);
280
+ g.b.x = (g.c.x - Math.abs(g.a.y - g.d.y) / SR3);
281
+ } else {
282
+ g.d.x = WIDTH - BOX_WIDTH;
283
+ g.d.y = (summary.boxTops as any)[boxes[k]] + offsets.y[boxName as keyof typeof offsets.y];
284
+ }
285
+
286
+ g.c.y = g.d.y;
287
+ offsets.y[boxName as keyof typeof offsets.y] += halfStroke;
288
+
289
+ if (j > 1) {
290
+ leftY += halfStroke;
291
+ }
292
+
293
+ lastBox = boxName;
294
+ graph.push(g);
295
+
296
+ // ============== WASTE HEAT CLONING ALGORITHM ==============
297
+ // CRITICAL THERMODYNAMIC IMPLEMENTATION
298
+ // For electricity flows, create corresponding waste heat flows
299
+ // This implements the fundamental physics of electricity generation
300
+ if (j === 0) {
301
+ // DEEP OBJECT CLONING: Create independent copy of GraphStroke object
302
+ // JSON.parse(JSON.stringify()) ensures complete object independence
303
+ // Required because we modify stroke, value, and positioning independently
304
+ // Performance note: This is intentionally expensive for accuracy
305
+ let cloned = JSON.parse(JSON.stringify(g));
306
+
307
+ // THERMODYNAMIC WASTE HEAT CALCULATION
308
+ // Physics: η = W_useful / (W_useful + Q_waste)
309
+ // Where η = efficiency, W = useful work, Q = waste heat
310
+ // Each electricity flow generates corresponding waste heat flow
311
+
312
+ // RESIDENTIAL SECTOR WASTE HEAT
313
+ if (fuelName === 'elec' && lastBox === 'res') {
314
+ // Calculate waste heat stroke width: waste_energy × SCALE ÷ 2
315
+ halfStroke = wasteObj[lastBox] * SCALE / 2;
316
+ elecY += halfStroke * 2; // Update vertical position
317
+ cloned.stroke = halfStroke * 2; // Set waste heat flow width
318
+ offsets.y[lastBox as keyof typeof offsets.y] += halfStroke * 2; // Update position tracking
319
+ cloned.value = wasteObj[lastBox]; // Set thermodynamic waste value
320
+ }
321
+ // AGRICULTURE SECTOR WASTE HEAT
322
+ else if (fuelName === 'elec' && lastBox === 'ag') {
323
+ halfStroke = wasteObj[lastBox] * SCALE / 2; // Same mathematical pattern
324
+ elecY += halfStroke * 2; // for all sectors - this
325
+ cloned.stroke = halfStroke * 2; // repetition preserves
326
+ offsets.y[lastBox as keyof typeof offsets.y] += halfStroke * 2; // exact logic while
327
+ cloned.value = wasteObj[lastBox]; // maintaining clarity
328
+ }
329
+ // INDUSTRIAL SECTOR WASTE HEAT
330
+ else if (fuelName === 'elec' && lastBox === 'indus') {
331
+ halfStroke = wasteObj[lastBox] * SCALE / 2; // Industrial processes have
332
+ elecY += halfStroke * 2; // significant electricity
333
+ cloned.stroke = halfStroke * 2; // consumption and thus
334
+ offsets.y[lastBox as keyof typeof offsets.y] += halfStroke * 2; // substantial waste heat
335
+ cloned.value = wasteObj[lastBox]; // generation
336
+ }
337
+ // TRANSPORTATION SECTOR WASTE HEAT
338
+ else if (fuelName === 'elec' && lastBox === 'trans') {
339
+ halfStroke = wasteObj[lastBox] * SCALE / 2; // Electric vehicles and
340
+ elecY += halfStroke * 2; // electric rail systems
341
+ cloned.stroke = halfStroke * 2; // contribute to waste heat
342
+ offsets.y[lastBox as keyof typeof offsets.y] += halfStroke * 2; // from electricity
343
+ cloned.value = wasteObj[lastBox]; // generation processes
344
+ }
345
+ // TRANSPORTATION SECTOR WASTE HEAT
346
+ else if (fuelName === 'elec' && lastBox === 'heat' && this.configService.hasHeatData) {
347
+ halfStroke = wasteObj[lastBox] * SCALE / 2; // Electric vehicles and
348
+ elecY += halfStroke * 2; // electric rail systems
349
+ cloned.stroke = halfStroke * 2; // contribute to waste heat
350
+ offsets.y[lastBox as keyof typeof offsets.y] += halfStroke * 2; // from electricity
351
+ cloned.value = wasteObj[lastBox]; // generation processes
352
+ }
353
+
354
+ // ADD WASTE HEAT FLOW TO GRAPH: Insert cloned waste heat flow
355
+ // This creates a parallel flow path showing thermodynamic losses
356
+ // Critical for energy balance: Input = Useful Output + Waste Heat
357
+ graph.push(cloned);
358
+ }
359
+ }
360
+
361
+ if (j > 1) {
362
+ leftY += LEFT_GAP;
363
+ }
364
+ }
365
+
366
+ this.graphs.push({
367
+ graph: graph,
368
+ offsets,
369
+ year: this.dataService.data[i].year,
370
+ totals: totals,
371
+ flows: flows
372
+ } as GraphData);
373
+ }
374
+ }
375
+
376
+ public calculateGraphX() {
377
+ // Build summary to get boxTops for positioning calculations
378
+ this.calculateGraphXUps();
379
+ this.calculateGraphXDowns();
380
+ }
381
+
382
+ /**
383
+ * Method Inlined calculateGraphXUps() - eliminates repeated property access
384
+ */
385
+ private calculateGraphXUps() {
386
+ // Cache configuration constants locally to eliminate property lookup overhead
387
+ const WIDTH = this.configService.WIDTH;
388
+ const BOX_WIDTH = this.configService.BOX_WIDTH;
389
+ const SCALE = this.configService.SCALE;
390
+ const SR3 = this.configService.SR3;
391
+ const ELEC_GAP = this.configService.ELEC_GAP;
392
+ const HSR3 = this.configService.HSR3;
393
+
394
+ for (let i = 0; i < this.graphs.length; ++i) {
395
+ let current_box: string | null = null;
396
+ this.graphs[i].graph
397
+ .filter(function (g) {
398
+ return g.a.y > g.d.y && !['elec', 'heat'].includes(g.box);
399
+ })
400
+ .sort(this.sortGraphUp.bind(this))
401
+ .forEach((g, j) => {
402
+ if (g.box !== current_box) {
403
+ (this.graphs[i].offsets.y as any)[g.box] = g.stroke / 2;
404
+ g.c.x = WIDTH - BOX_WIDTH - 20 - g.stroke / 2;
405
+ } else {
406
+ g.c.x = (WIDTH - BOX_WIDTH - 20 - (this.graphs[i].totals[g.box] * SCALE - (this.graphs[i].offsets.y as any)[g.box]) / SR3 - j * ELEC_GAP * HSR3);
407
+ }
408
+ g.b.x = (g.c.x - Math.abs(g.a.y - g.c.y) / SR3);
409
+ g.cc.x = g.c.x - Math.abs(this.graphs[i].totals.fuel_height - g.c.y) / SR3;
410
+ current_box = g.box;
411
+ });
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Method Inlined calculateGraphXDowns() - eliminates repeated property access
417
+ */
418
+ private calculateGraphXDowns() {
419
+ // Cache configuration constants locally to eliminate property lookup overhead
420
+ const WIDTH = this.configService.WIDTH;
421
+ const BOX_WIDTH = this.configService.BOX_WIDTH;
422
+ const SR3 = this.configService.SR3;
423
+ const HSR3 = this.configService.HSR3;
424
+ const ELEC_GAP = this.configService.ELEC_GAP;
425
+ const ELEC_BOX_Y = this.configService.ELEC_BOX_Y;
426
+
427
+ for (let i = 0; i < this.graphs.length; ++i) {
428
+ let current_box: string | null = null;
429
+ this.graphs[i].graph
430
+ .filter(function (g) {
431
+ return g.a.y < g.d.y && !['elec', 'heat'].includes(g.box);
432
+ })
433
+ .sort(this.sortGraphDown.bind(this))
434
+ .forEach((g, j) => {
435
+ if (g.box !== current_box) {
436
+ (this.graphs[i].offsets.y as any)[g.box] = g.stroke / 2;
437
+ g.c.x = WIDTH - BOX_WIDTH - 20 - g.stroke / 2;
438
+ } else {
439
+ g.c.x = (WIDTH - BOX_WIDTH - 20 - ((this.graphs[i].offsets.y as any)[g.box]) / SR3 - j * ELEC_GAP * HSR3);
440
+ }
441
+ g.b.x = (g.c.x - Math.abs(g.a.y - g.c.y) / SR3);
442
+ g.cc.x = g.c.x - Math.abs(ELEC_BOX_Y - g.c.y) / SR3;
443
+ current_box = g.box;
444
+ });
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Method Inlined spaceUpsAndDowns() - eliminates repeated property access
450
+ */
451
+ public spaceUpsAndDowns() {
452
+ const PATH_GAP = this.configService.PATH_GAP;
453
+ const HSR3 = this.configService.HSR3;
454
+ const WIDTH = this.configService.WIDTH;
455
+ const BOX_WIDTH = this.configService.BOX_WIDTH;
456
+
457
+ let prev: GraphStroke | null = null;
458
+ let diff: number | null = null;
459
+ for (let i = 0; i < this.graphs.length; ++i) {
460
+ this.graphs[i].graph.sort(function (a, b) {
461
+ return b.cc.x - a.cc.x;
462
+ });
463
+ this.graphs[i].graph
464
+ .filter(function (g) {
465
+ return !['elec', 'heat'].includes(g.box);
466
+ })
467
+ .forEach((g, j) => {
468
+ if (j === 0) {
469
+ prev = g;
470
+ return;
471
+ }
472
+ let pathGap = PATH_GAP * HSR3;
473
+ if (g.stroke === 0) {
474
+ pathGap = 0;
475
+ }
476
+ diff = pathGap - ((prev!.cc.x - prev!.stroke / 2) - (g.cc.x + g.stroke / 2));
477
+ g.cc.x -= diff;
478
+ g.c.x -= diff;
479
+ g.b.x -= diff;
480
+ prev = g;
481
+ });
482
+ let max_cc = Math.max.apply(Math, this.graphs[i].graph.map(function (o) {
483
+ return o.cc.x;
484
+ }));
485
+ this.graphs[i].graph
486
+ .filter(function (g) {
487
+ return !['elec', 'heat'].includes(g.box);
488
+ })
489
+ .forEach((g) => {
490
+ let diff = max_cc - (WIDTH - BOX_WIDTH - 50);
491
+ g.c.x -= diff;
492
+ g.b.x -= diff;
493
+ });
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Waste heat processing
499
+ */
500
+ public processWasteHeatFlows() {
501
+ // Always process waste heat flows
502
+ for (let i = 0; i < this.graphs.length; ++i) {
503
+ let prev_graph: GraphStroke | null = null;
504
+
505
+ this.graphs[i].graph
506
+ .filter((g: GraphStroke) => g.fuel === 'elec')
507
+ .sort(this.sortGraphDown.bind(this))
508
+ .forEach((g: GraphStroke, j: number) => {
509
+ // Loop through boxes
510
+ if (j % 2 !== 0) {
511
+ g.fuel = 'waste';
512
+ // This is waste --> right side boxes
513
+ if (prev_graph) {
514
+ const total_stroke = Math.abs(prev_graph.stroke + g.stroke);
515
+ g.a.y = prev_graph.a.y + total_stroke / 2;
516
+ g.b.y = g.a.y;
517
+
518
+ g.b.x = prev_graph.b.x - total_stroke / 3.5;
519
+ g.c.x = prev_graph.c.x - total_stroke / 3.5;
520
+ g.c.y = prev_graph.c.y + total_stroke / 2;
521
+ g.d.y = g.c.y;
522
+ }
523
+ } else {
524
+ prev_graph = g;
525
+ }
526
+ });
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Method Inlined sortGraphUp() - eliminates repeated array access
532
+ */
533
+ private sortGraphUp(a: GraphStroke, b: GraphStroke): number {
534
+ // Cache arrays locally to eliminate repeated property access
535
+ const BOX_NAMES = this.configService.BOX_NAMES;
536
+ const FUEL_NAMES = this.configService.FUEL_NAMES;
537
+
538
+ if (BOX_NAMES.indexOf(a.box) < BOX_NAMES.indexOf(b.box)) {
539
+ return 1;
540
+ }
541
+ if (BOX_NAMES.indexOf(a.box) > BOX_NAMES.indexOf(b.box)) {
542
+ return -1;
543
+ }
544
+ if (FUEL_NAMES.indexOf(a.fuel) < FUEL_NAMES.indexOf(b.fuel)) {
545
+ return 1;
546
+ }
547
+ if (FUEL_NAMES.indexOf(a.fuel) > FUEL_NAMES.indexOf(b.fuel)) {
548
+ return -1;
549
+ }
550
+ return 0;
551
+ }
552
+
553
+ /**
554
+ * Method Inlined sortGraphDown() - eliminates repeated array access
555
+ */
556
+ private sortGraphDown(a: GraphStroke, b: GraphStroke): number {
557
+ // Cache arrays locally to eliminate repeated property access
558
+ const BOX_NAMES = this.configService.BOX_NAMES;
559
+ const FUEL_NAMES = this.configService.FUEL_NAMES;
560
+
561
+ if (BOX_NAMES.indexOf(a.box) < BOX_NAMES.indexOf(b.box)) {
562
+ return -1;
563
+ }
564
+ if (BOX_NAMES.indexOf(a.box) > BOX_NAMES.indexOf(b.box)) {
565
+ return 1;
566
+ }
567
+ if (FUEL_NAMES.indexOf(a.fuel) < FUEL_NAMES.indexOf(b.fuel)) {
568
+ return -1;
569
+ }
570
+ if (FUEL_NAMES.indexOf(a.fuel) > FUEL_NAMES.indexOf(b.fuel)) {
571
+ return 1;
572
+ }
573
+ return 0;
574
+ }
575
+
576
+ public sigfig2(n: number | string | undefined | null): number {
577
+ // Add safety check for invalid inputs
578
+ if (n === undefined || n === null || (typeof n === 'string' && n.trim() === '')) {
579
+ console.warn('sigfig2 received invalid input:', n);
580
+ return 0;
581
+ }
582
+
583
+ // Convert to number if it's a string
584
+ let numValue: number;
585
+ if (typeof n === 'string') {
586
+ numValue = parseFloat(n);
587
+ if (isNaN(numValue)) {
588
+ console.warn('sigfig2 could not parse string to number:', n);
589
+ return 0;
590
+ }
591
+ } else {
592
+ numValue = n;
593
+ }
594
+
595
+ if (isNaN(numValue)) {
596
+ console.warn('sigfig2 received NaN:', n);
597
+ return 0;
598
+ }
599
+
600
+ if (numValue > 1 && numValue < 10) {
601
+ return Number.parseFloat(numValue.toPrecision(1));
602
+ } else {
603
+ return Number.parseFloat(numValue.toPrecision(2));
604
+ }
605
+ }
606
+
607
+ public createLine(): d3.Line<GraphPoint> {
608
+ return d3.line<GraphPoint>()
609
+ .x(function (d) {
610
+ return d.x;
611
+ })
612
+ .y(function (d) {
613
+ return d.y;
614
+ });
615
+ }
616
+ }