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