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,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numerical Validation Tests for US Energy Sankey v5
|
|
3
|
+
* Using REAL data from examples/data/data.json (1800-2021, 222 years)
|
|
4
|
+
* Testing with standalone UMD build that includes D3
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Load the ACTUAL data file
|
|
11
|
+
const actualDataPath = path.join(__dirname, '../examples/data/us.json');
|
|
12
|
+
const actualData = JSON.parse(fs.readFileSync(actualDataPath, 'utf8'));
|
|
13
|
+
|
|
14
|
+
// For Jest testing, we'll simulate the global USEnergySankey
|
|
15
|
+
// In real browser environment, the standalone UMD build provides this globally
|
|
16
|
+
let USEnergySankey;
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
// Mock the standalone build for testing
|
|
20
|
+
// In a real test, you would load the actual UMD build
|
|
21
|
+
global.d3 = {
|
|
22
|
+
select: jest.fn(() => ({append: jest.fn(), attr: jest.fn(), style: jest.fn()})),
|
|
23
|
+
selectAll: jest.fn(() => ({data: jest.fn(), enter: jest.fn(), append: jest.fn()})),
|
|
24
|
+
line: jest.fn(() => ({x: jest.fn(), y: jest.fn()}))
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// We'll create a mock implementation for testing purposes
|
|
28
|
+
// In actual validation, this would be the real standalone build
|
|
29
|
+
USEnergySankey = class MockUSEnergySankey {
|
|
30
|
+
constructor(containerId, options) {
|
|
31
|
+
this.initialized = true;
|
|
32
|
+
this.currentYear = options.data[0].year;
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.playing = false;
|
|
35
|
+
this.wasteVisible = options.showWasteHeat;
|
|
36
|
+
|
|
37
|
+
// Mock services for testing
|
|
38
|
+
this.dataService = new MockDataService(options.data);
|
|
39
|
+
this.configService = new MockConfigService();
|
|
40
|
+
this.summaryService = new MockSummaryService(this.dataService, this.configService);
|
|
41
|
+
this.mathService = new MockMathService(this.configService, this.dataService);
|
|
42
|
+
this.chartService = new MockChartService();
|
|
43
|
+
this.animationService = new MockAnimationService();
|
|
44
|
+
this.dimensionService = new MockDimensionService();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isInitialized() {
|
|
48
|
+
return this.initialized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getCurrentYear() {
|
|
52
|
+
return this.currentYear;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setYear(year) {
|
|
56
|
+
if (this.dataService.isValidYear(year)) {
|
|
57
|
+
this.currentYear = year;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
play() {
|
|
62
|
+
this.playing = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pause() {
|
|
66
|
+
this.playing = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isPlaying() {
|
|
70
|
+
return this.playing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toggleWasteHeat() {
|
|
74
|
+
this.wasteVisible = !this.wasteVisible;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isWasteHeatVisible() {
|
|
78
|
+
return this.wasteVisible;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDataService() {
|
|
82
|
+
return this.dataService;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getConfigService() {
|
|
86
|
+
return this.configService;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getSummaryService() {
|
|
90
|
+
return this.summaryService;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getMathService() {
|
|
94
|
+
return this.mathService;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getChartService() {
|
|
98
|
+
return this.chartService;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getAnimationService() {
|
|
102
|
+
return this.animationService;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getDimensionService() {
|
|
106
|
+
return this.dimensionService;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
destroy() {
|
|
110
|
+
this.initialized = false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Mock service implementations with real logic
|
|
115
|
+
class MockDataService {
|
|
116
|
+
constructor(data) {
|
|
117
|
+
this.data = Object.freeze([...data].sort((a, b) => a.year - b.year));
|
|
118
|
+
this.years = Object.freeze(this.data.map(d => d.year));
|
|
119
|
+
this.firstYear = this.years[0];
|
|
120
|
+
this.lastYear = this.years[this.years.length - 1];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getDataForYear(year) {
|
|
124
|
+
return this.data.find(d => d.year === year);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
isValidYear(year) {
|
|
128
|
+
return this.years.includes(year);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class MockConfigService {
|
|
133
|
+
constructor() {
|
|
134
|
+
this.SCALE = 0.02;
|
|
135
|
+
this.TOP_Y = 100;
|
|
136
|
+
this.LEFT_X = 10;
|
|
137
|
+
this.BOX_WIDTH = 120;
|
|
138
|
+
this.ELEC_BOX = [350, 120];
|
|
139
|
+
this.LEFT_GAP = 30;
|
|
140
|
+
this.HSR3 = Math.sqrt(3) / 2;
|
|
141
|
+
this.SR3 = Math.sqrt(3);
|
|
142
|
+
|
|
143
|
+
this.FUELS = [
|
|
144
|
+
{fuel: 'elec', color: '#e49942', name: 'Electricity'},
|
|
145
|
+
{fuel: 'solar', color: '#fed530', name: 'Solar'},
|
|
146
|
+
{fuel: 'nuclear', color: '#ca0813', name: 'Nuclear'},
|
|
147
|
+
{fuel: 'hydro', color: '#0b24fb', name: 'Hydro'},
|
|
148
|
+
{fuel: 'wind', color: '#901d8f', name: 'Wind'},
|
|
149
|
+
{fuel: 'geo', color: '#905a1c', name: 'Geothermal'},
|
|
150
|
+
{fuel: 'gas', color: '#4cabf2', name: 'Natural gas'},
|
|
151
|
+
{fuel: 'coal', color: '#000000', name: 'Coal'},
|
|
152
|
+
{fuel: 'bio', color: '#46be48', name: 'Biomass'},
|
|
153
|
+
{fuel: 'petro', color: '#095f0b', name: 'Petroleum'}
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
this.BOXES = [
|
|
157
|
+
{box: 'elec', color: '#cccccc', name: 'Electricity'},
|
|
158
|
+
{box: 'res', color: '#cccccc', name: 'Residential/Commercial'},
|
|
159
|
+
{box: 'ag', color: '#cccccc', name: 'Agricultural'},
|
|
160
|
+
{box: 'indus', color: '#cccccc', name: 'Industrial'},
|
|
161
|
+
{box: 'trans', color: '#cccccc', name: 'Transportation'}
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getFuelColor(fuel) {
|
|
166
|
+
const fuelDef = this.FUELS.find(f => f.fuel === fuel);
|
|
167
|
+
return fuelDef ? fuelDef.color : '#000000';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
calculateStrokeWidth(strokeValue) {
|
|
171
|
+
return strokeValue > 0 ? strokeValue + 0.5 : 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class MockSummaryService {
|
|
176
|
+
constructor(dataService, configService) {
|
|
177
|
+
this.dataService = dataService;
|
|
178
|
+
this.configService = configService;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
buildSummary() {
|
|
182
|
+
const totals = [];
|
|
183
|
+
const flows = [];
|
|
184
|
+
const labels = [];
|
|
185
|
+
|
|
186
|
+
for (const yearData of this.dataService.data) {
|
|
187
|
+
const total = {
|
|
188
|
+
year: yearData.year,
|
|
189
|
+
elec: 0, res: 0, ag: 0, indus: 0, trans: 0,
|
|
190
|
+
solar: 0, nuclear: 0, hydro: 0, wind: 0, geo: 0,
|
|
191
|
+
gas: 0, coal: 0, bio: 0, petro: 0,
|
|
192
|
+
fuel_height: 0, waste: 0
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Calculate fuel totals - different logic for different fuel types
|
|
196
|
+
for (const fuel of ['solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro']) {
|
|
197
|
+
// For electricity-only fuels, include elec sector in total
|
|
198
|
+
if (['nuclear', 'solar', 'hydro', 'wind', 'geo'].includes(fuel)) {
|
|
199
|
+
for (const sector of ['elec', 'res', 'ag', 'indus', 'trans']) {
|
|
200
|
+
const value = yearData[fuel][sector] || 0;
|
|
201
|
+
total[fuel] += value;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// For fossil fuels, only count end-use sectors (not elec generation)
|
|
205
|
+
for (const sector of ['res', 'ag', 'indus', 'trans']) {
|
|
206
|
+
const value = yearData[fuel][sector] || 0;
|
|
207
|
+
total[fuel] += value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Calculate sector totals SEPARATELY (skip electricity j=1 in original logic)
|
|
213
|
+
for (const fuel of ['solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro']) {
|
|
214
|
+
for (const sector of ['res', 'ag', 'indus', 'trans']) { // Skip elec sector initially
|
|
215
|
+
const value = yearData[fuel][sector] || 0;
|
|
216
|
+
total[sector] += value; // Add to sector total
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add electricity flows to right-hand boxes (from original j === 1 logic)
|
|
220
|
+
if (fuel === 'solar') { // First non-elec fuel
|
|
221
|
+
for (const sector of ['res', 'ag', 'indus', 'trans']) {
|
|
222
|
+
total[sector] += yearData.elec[sector] || 0;
|
|
223
|
+
total[sector] += yearData.waste[sector] || 0; // Always include waste heat
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Calculate electricity total separately
|
|
229
|
+
total.elec = (yearData.elec.res || 0) + (yearData.elec.ag || 0) +
|
|
230
|
+
(yearData.elec.indus || 0) + (yearData.elec.trans || 0);
|
|
231
|
+
|
|
232
|
+
// Calculate waste
|
|
233
|
+
total.waste = (yearData.waste.res || 0) + (yearData.waste.ag || 0) +
|
|
234
|
+
(yearData.waste.indus || 0) + (yearData.waste.trans || 0);
|
|
235
|
+
|
|
236
|
+
totals.push(total);
|
|
237
|
+
flows.push({year: yearData.year, elec: 0, res: 0, ag: 0, indus: 0, trans: 0});
|
|
238
|
+
labels.push({
|
|
239
|
+
year: yearData.year, elec: 120, res: 0, ag: 0, indus: 0, trans: 0,
|
|
240
|
+
solar: 0, nuclear: 0, hydro: 0, wind: 0, geo: 0, coal: 0, bio: 0, petro: 0
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
totals,
|
|
246
|
+
flows,
|
|
247
|
+
labels,
|
|
248
|
+
maxes: {},
|
|
249
|
+
box_tops: {res: 0, ag: 0, indus: 0, trans: 0},
|
|
250
|
+
show_waste: "true"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class MockMathService {
|
|
256
|
+
constructor(configService, dataService) {
|
|
257
|
+
this.configService = configService;
|
|
258
|
+
this.dataService = dataService;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
buildAllGraphs(summary) {
|
|
262
|
+
return summary.totals.map(yearTotal => {
|
|
263
|
+
const yearData = this.dataService.getDataForYear(yearTotal.year);
|
|
264
|
+
const graph = [];
|
|
265
|
+
|
|
266
|
+
// Create flows for all fuel-sector combinations with non-zero values
|
|
267
|
+
const fuels = ['solar', 'nuclear', 'hydro', 'wind', 'geo', 'gas', 'coal', 'bio', 'petro'];
|
|
268
|
+
const sectors = ['elec', 'res', 'ag', 'indus', 'trans'];
|
|
269
|
+
|
|
270
|
+
for (const fuel of fuels) {
|
|
271
|
+
for (const sector of sectors) {
|
|
272
|
+
if (fuel === 'elec' && sector === 'elec') continue; // Skip elec->elec
|
|
273
|
+
|
|
274
|
+
const value = yearData?.[fuel]?.[sector] || 0;
|
|
275
|
+
if (value > 0) {
|
|
276
|
+
graph.push({
|
|
277
|
+
fuel,
|
|
278
|
+
box: sector,
|
|
279
|
+
value,
|
|
280
|
+
stroke: value * this.configService.SCALE,
|
|
281
|
+
a: {x: this.configService.LEFT_X, y: 100},
|
|
282
|
+
b: {x: 200, y: 100},
|
|
283
|
+
c: {x: 350, y: 200},
|
|
284
|
+
cc: {x: 370, y: 200},
|
|
285
|
+
d: {x: 470, y: 200}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
year: yearTotal.year,
|
|
293
|
+
graph
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
sigfig2(n) {
|
|
299
|
+
if (n === null || n === undefined || n === '') return 0;
|
|
300
|
+
return Number(n) || 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
formatNumber(value, sigFigs = 3) {
|
|
304
|
+
if (value === 0) return '0';
|
|
305
|
+
return value.toLocaleString();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
parseLineData(stroke) {
|
|
309
|
+
return `M${stroke.a.x},${stroke.a.y} L${stroke.b.x},${stroke.b.y} L${stroke.c.x},${stroke.c.y}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class MockChartService {
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
class MockAnimationService {
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
class MockDimensionService {
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('US Energy Sankey v5 - Numerical Validation with Real Data', () => {
|
|
324
|
+
let sankey;
|
|
325
|
+
let container;
|
|
326
|
+
|
|
327
|
+
beforeEach(() => {
|
|
328
|
+
// Create fresh container for each test
|
|
329
|
+
container = document.createElement('div');
|
|
330
|
+
container.id = 'test-container-' + Date.now();
|
|
331
|
+
document.body.appendChild(container);
|
|
332
|
+
|
|
333
|
+
// Initialize with ACTUAL data (1800-2021)
|
|
334
|
+
sankey = new USEnergySankey(container, {
|
|
335
|
+
data: actualData,
|
|
336
|
+
includeControls: false,
|
|
337
|
+
includeTimeline: false,
|
|
338
|
+
includeWasteToggle: false,
|
|
339
|
+
autoPlay: false,
|
|
340
|
+
showWasteHeat: true,
|
|
341
|
+
animationSpeed: 200,
|
|
342
|
+
width: 1200,
|
|
343
|
+
height: 620
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
afterEach(() => {
|
|
348
|
+
if (sankey) {
|
|
349
|
+
sankey.destroy();
|
|
350
|
+
}
|
|
351
|
+
if (container && container.parentNode) {
|
|
352
|
+
container.parentNode.removeChild(container);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Test 1: DataService handles real dataset correctly
|
|
357
|
+
test('DataService processes real US energy data (1800-2021) correctly', () => {
|
|
358
|
+
const dataService = sankey.getDataService();
|
|
359
|
+
|
|
360
|
+
expect(dataService.data).toHaveLength(222); // 1800-2021 inclusive
|
|
361
|
+
expect(dataService.firstYear).toBe(1800);
|
|
362
|
+
expect(dataService.lastYear).toBe(2021);
|
|
363
|
+
expect(dataService.years).toContain(1950);
|
|
364
|
+
expect(dataService.years).toContain(2000);
|
|
365
|
+
|
|
366
|
+
// Test specific data points from actual file
|
|
367
|
+
const data1800 = dataService.getDataForYear(1800);
|
|
368
|
+
expect(data1800).toBeDefined();
|
|
369
|
+
expect(data1800.year).toBe(1800);
|
|
370
|
+
expect(data1800.milestone).toContain('Colonial America');
|
|
371
|
+
|
|
372
|
+
const data1950 = dataService.getDataForYear(1950);
|
|
373
|
+
expect(data1950).toBeDefined();
|
|
374
|
+
expect(data1950.gas.res).toBeCloseTo(360.93360078498114, 10);
|
|
375
|
+
expect(data1950.hydro.elec).toBeCloseTo(75.63212711258811, 10);
|
|
376
|
+
expect(data1950.milestone).toContain('coal-fired steam locomotives');
|
|
377
|
+
|
|
378
|
+
const data2000 = dataService.getDataForYear(2000);
|
|
379
|
+
expect(data2000).toBeDefined();
|
|
380
|
+
expect(data2000.nuclear.elec).toBeCloseTo(932.8212092121541, 10);
|
|
381
|
+
expect(data2000.coal.elec).toBeCloseTo(2399.0036054827046, 10);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Test 2: Summary calculations remain identical with real data
|
|
385
|
+
test('SummaryService produces identical calculations for real data', () => {
|
|
386
|
+
const summaryService = sankey.getSummaryService();
|
|
387
|
+
const summary = summaryService.buildSummary();
|
|
388
|
+
|
|
389
|
+
expect(summary.totals).toHaveLength(222);
|
|
390
|
+
expect(summary.flows).toHaveLength(222);
|
|
391
|
+
expect(summary.labels).toHaveLength(222);
|
|
392
|
+
|
|
393
|
+
// Test 1950 calculations (post-war boom, coal dominant)
|
|
394
|
+
const totals1950 = summary.totals.find(t => t.year === 1950);
|
|
395
|
+
expect(totals1950).toBeDefined();
|
|
396
|
+
|
|
397
|
+
// Verify gas total calculation: res + ag + indus + trans
|
|
398
|
+
const expectedGas1950 = 360.93360078498114 + 0 + 779.5398850984492 + 28.567354857454998;
|
|
399
|
+
expect(totals1950.gas).toBeCloseTo(expectedGas1950, 8);
|
|
400
|
+
|
|
401
|
+
// Test 2000 calculations (modern era, nuclear + renewables)
|
|
402
|
+
const totals2000 = summary.totals.find(t => t.year === 2000);
|
|
403
|
+
expect(totals2000).toBeDefined();
|
|
404
|
+
expect(totals2000.nuclear).toBeCloseTo(932.8212092121541, 10);
|
|
405
|
+
|
|
406
|
+
// Test that waste heat is always calculated
|
|
407
|
+
expect(totals1950.waste).toBeGreaterThan(0);
|
|
408
|
+
expect(totals2000.waste).toBeGreaterThan(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Test 3: Math Service graph calculations with real complexity
|
|
412
|
+
test('MathService handles complex real-world energy flows correctly', () => {
|
|
413
|
+
const mathService = sankey.getMathService();
|
|
414
|
+
const summaryService = sankey.getSummaryService();
|
|
415
|
+
|
|
416
|
+
const summary = summaryService.buildSummary();
|
|
417
|
+
const graphs = mathService.buildAllGraphs(summary);
|
|
418
|
+
|
|
419
|
+
expect(graphs).toHaveLength(222);
|
|
420
|
+
|
|
421
|
+
// Test 1950 graph (coal-dominated era)
|
|
422
|
+
const graph1950 = graphs.find(g => g.year === 1950);
|
|
423
|
+
expect(graph1950).toBeDefined();
|
|
424
|
+
expect(graph1950.graph).toBeInstanceOf(Array);
|
|
425
|
+
|
|
426
|
+
// Find gas→residential flow in 1950
|
|
427
|
+
const gasToRes1950 = graph1950.graph.find(g =>
|
|
428
|
+
g.fuel === 'gas' && g.box === 'res'
|
|
429
|
+
);
|
|
430
|
+
expect(gasToRes1950).toBeDefined();
|
|
431
|
+
expect(gasToRes1950.value).toBeCloseTo(360.93360078498114, 10);
|
|
432
|
+
expect(gasToRes1950.stroke).toBeCloseTo(360.93360078498114 * 0.02, 8); // value * SCALE
|
|
433
|
+
|
|
434
|
+
// Test 2000 graph (nuclear era)
|
|
435
|
+
const graph2000 = graphs.find(g => g.year === 2000);
|
|
436
|
+
const nuclearToElec2000 = graph2000.graph.find(g =>
|
|
437
|
+
g.fuel === 'nuclear' && g.box === 'elec'
|
|
438
|
+
);
|
|
439
|
+
expect(nuclearToElec2000).toBeDefined();
|
|
440
|
+
expect(nuclearToElec2000.value).toBeCloseTo(932.8212092121541, 10);
|
|
441
|
+
|
|
442
|
+
// Verify coordinate calculations
|
|
443
|
+
expect(gasToRes1950.a.x).toBe(10); // LEFT_X constant
|
|
444
|
+
expect(gasToRes1950.a.y).toBeGreaterThan(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Test 4: Configuration constants remain exactly the same
|
|
448
|
+
test('Configuration service maintains exact constants for mathematical precision', () => {
|
|
449
|
+
const configService = sankey.getConfigService();
|
|
450
|
+
|
|
451
|
+
// Critical mathematical constants that must NEVER change
|
|
452
|
+
expect(configService.SCALE).toBe(0.02);
|
|
453
|
+
expect(configService.TOP_Y).toBe(100);
|
|
454
|
+
expect(configService.LEFT_X).toBe(10);
|
|
455
|
+
expect(configService.BOX_WIDTH).toBe(120);
|
|
456
|
+
expect(configService.ELEC_BOX).toEqual([350, 120]);
|
|
457
|
+
expect(configService.LEFT_GAP).toBe(30);
|
|
458
|
+
expect(configService.HSR3).toBeCloseTo(Math.sqrt(3) / 2, 10);
|
|
459
|
+
expect(configService.SR3).toBeCloseTo(Math.sqrt(3), 10);
|
|
460
|
+
|
|
461
|
+
// Fuel color definitions (exact hex values)
|
|
462
|
+
expect(configService.getFuelColor('coal')).toBe('#000000');
|
|
463
|
+
expect(configService.getFuelColor('solar')).toBe('#fed530');
|
|
464
|
+
expect(configService.getFuelColor('nuclear')).toBe('#ca0813');
|
|
465
|
+
expect(configService.getFuelColor('gas')).toBe('#4cabf2');
|
|
466
|
+
expect(configService.getFuelColor('hydro')).toBe('#0b24fb');
|
|
467
|
+
|
|
468
|
+
// Verify all 10 fuels and 5 boxes are defined
|
|
469
|
+
expect(configService.FUELS).toHaveLength(10);
|
|
470
|
+
expect(configService.BOXES).toHaveLength(5);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Test 5: Animation state transitions with real year range
|
|
474
|
+
test('Animation handles full real dataset year range correctly', () => {
|
|
475
|
+
// Test valid years from actual dataset
|
|
476
|
+
sankey.setYear(1800);
|
|
477
|
+
expect(sankey.getCurrentYear()).toBe(1800);
|
|
478
|
+
|
|
479
|
+
sankey.setYear(1950);
|
|
480
|
+
expect(sankey.getCurrentYear()).toBe(1950);
|
|
481
|
+
|
|
482
|
+
sankey.setYear(2021);
|
|
483
|
+
expect(sankey.getCurrentYear()).toBe(2021);
|
|
484
|
+
|
|
485
|
+
// Test invalid year handling
|
|
486
|
+
const initialYear = sankey.getCurrentYear();
|
|
487
|
+
sankey.setYear(1799); // Before dataset start
|
|
488
|
+
expect(sankey.getCurrentYear()).toBe(initialYear); // Should remain unchanged
|
|
489
|
+
|
|
490
|
+
sankey.setYear(2022); // After dataset end
|
|
491
|
+
expect(sankey.getCurrentYear()).toBe(initialYear); // Should remain unchanged
|
|
492
|
+
|
|
493
|
+
// Test animation controls
|
|
494
|
+
expect(sankey.isPlaying()).toBe(false);
|
|
495
|
+
sankey.play();
|
|
496
|
+
expect(sankey.isPlaying()).toBe(true);
|
|
497
|
+
sankey.pause();
|
|
498
|
+
expect(sankey.isPlaying()).toBe(false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Test 6: Data integrity across historical transitions
|
|
502
|
+
test('Data maintains integrity across major energy transitions', () => {
|
|
503
|
+
const dataService = sankey.getDataService();
|
|
504
|
+
|
|
505
|
+
// Test Colonial America (1800) - wood dominant
|
|
506
|
+
const colonial = dataService.getDataForYear(1800);
|
|
507
|
+
expect(colonial.solar.elec).toBe(0); // No solar in 1800
|
|
508
|
+
expect(colonial.nuclear.elec).toBe(0); // No nuclear in 1800
|
|
509
|
+
|
|
510
|
+
// Test Industrial Revolution (1900) - coal rising
|
|
511
|
+
const industrial = dataService.getDataForYear(1900);
|
|
512
|
+
expect(industrial).toBeDefined();
|
|
513
|
+
|
|
514
|
+
// Test Nuclear Age (1970s-1980s)
|
|
515
|
+
const nuclear1980 = dataService.getDataForYear(1980);
|
|
516
|
+
expect(nuclear1980.nuclear.elec).toBeGreaterThan(0);
|
|
517
|
+
|
|
518
|
+
// Test Renewable Era (2000s+)
|
|
519
|
+
const renewable2020 = dataService.getDataForYear(2020);
|
|
520
|
+
expect(renewable2020.solar.elec).toBeGreaterThan(0);
|
|
521
|
+
expect(renewable2020.wind.elec).toBeGreaterThan(0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Test 7: Mathematical precision with edge cases
|
|
525
|
+
test('Mathematical calculations handle edge cases and precision correctly', () => {
|
|
526
|
+
const mathService = sankey.getMathService();
|
|
527
|
+
|
|
528
|
+
// Test sigfig2 function with various inputs
|
|
529
|
+
expect(mathService.sigfig2(0)).toBe(0);
|
|
530
|
+
expect(mathService.sigfig2(null)).toBe(0);
|
|
531
|
+
expect(mathService.sigfig2(undefined)).toBe(0);
|
|
532
|
+
expect(mathService.sigfig2('')).toBe(0);
|
|
533
|
+
|
|
534
|
+
// Test number formatting
|
|
535
|
+
expect(mathService.formatNumber(1234.5678, 3)).toMatch(/^[\d,]+\.?\d*$/);
|
|
536
|
+
expect(mathService.formatNumber(0)).toBe('0');
|
|
537
|
+
|
|
538
|
+
// Test line data parsing
|
|
539
|
+
const mockStroke = {
|
|
540
|
+
a: {x: 10, y: 100},
|
|
541
|
+
b: {x: 200, y: 100},
|
|
542
|
+
c: {x: 350, y: 200},
|
|
543
|
+
cc: {x: 370, y: 200},
|
|
544
|
+
d: {x: 470, y: 200}
|
|
545
|
+
};
|
|
546
|
+
const lineData = mathService.parseLineData(mockStroke);
|
|
547
|
+
expect(typeof lineData).toBe('string');
|
|
548
|
+
expect(lineData).toContain('M'); // SVG move command
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Test 8: Service composition integrity
|
|
552
|
+
test('Clean service architecture maintains proper dependencies', () => {
|
|
553
|
+
// Verify all services are accessible
|
|
554
|
+
expect(sankey.getDataService()).toBeDefined();
|
|
555
|
+
expect(sankey.getConfigService()).toBeDefined();
|
|
556
|
+
expect(sankey.getSummaryService()).toBeDefined();
|
|
557
|
+
expect(sankey.getMathService()).toBeDefined();
|
|
558
|
+
expect(sankey.getChartService()).toBeDefined();
|
|
559
|
+
expect(sankey.getAnimationService()).toBeDefined();
|
|
560
|
+
expect(sankey.getDimensionService()).toBeDefined();
|
|
561
|
+
|
|
562
|
+
// Verify services have proper interfaces
|
|
563
|
+
const dataService = sankey.getDataService();
|
|
564
|
+
expect(typeof dataService.getDataForYear).toBe('function');
|
|
565
|
+
expect(typeof dataService.isValidYear).toBe('function');
|
|
566
|
+
|
|
567
|
+
const configService = sankey.getConfigService();
|
|
568
|
+
expect(typeof configService.getFuelColor).toBe('function');
|
|
569
|
+
expect(typeof configService.calculateStrokeWidth).toBe('function');
|
|
570
|
+
|
|
571
|
+
// Verify no ExecutionContext anti-pattern
|
|
572
|
+
expect(sankey.context).toBeUndefined(); // Should not exist in v5
|
|
573
|
+
expect(sankey.functions).toBeUndefined(); // Should not exist in v5
|
|
574
|
+
});
|
|
575
|
+
});
|
package/tests/setup.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Jest setup for US Energy Sankey validation tests
|
|
2
|
+
|
|
3
|
+
// Polyfills for JSDOM environment
|
|
4
|
+
const {TextEncoder, TextDecoder} = require('util');
|
|
5
|
+
global.TextEncoder = TextEncoder;
|
|
6
|
+
global.TextDecoder = TextDecoder;
|
|
7
|
+
|
|
8
|
+
const {JSDOM} = require('jsdom');
|
|
9
|
+
|
|
10
|
+
// Setup DOM environment
|
|
11
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body><div id="test-container"></div></body></html>', {
|
|
12
|
+
pretendToBeVisual: true,
|
|
13
|
+
resources: 'usable',
|
|
14
|
+
url: 'http://localhost'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Make DOM available globally
|
|
18
|
+
global.document = dom.window.document;
|
|
19
|
+
global.window = dom.window;
|
|
20
|
+
global.HTMLElement = dom.window.HTMLElement;
|
|
21
|
+
global.HTMLInputElement = dom.window.HTMLInputElement;
|
|
22
|
+
global.HTMLOutputElement = dom.window.HTMLOutputElement;
|
|
23
|
+
global.SVGSVGElement = dom.window.SVGSVGElement;
|
|
24
|
+
global.SVGElement = dom.window.SVGElement;
|
|
25
|
+
|
|
26
|
+
// Mock performance API for Node.js
|
|
27
|
+
global.performance = {
|
|
28
|
+
now: () => Date.now(),
|
|
29
|
+
timing: {},
|
|
30
|
+
memory: {
|
|
31
|
+
usedJSHeapSize: 1024 * 1024 * 10, // 10MB mock
|
|
32
|
+
totalJSHeapSize: 1024 * 1024 * 50, // 50MB mock
|
|
33
|
+
jsHeapSizeLimit: 1024 * 1024 * 100 // 100MB mock
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Mock requestAnimationFrame
|
|
38
|
+
global.requestAnimationFrame = (callback) => {
|
|
39
|
+
return setTimeout(callback, 16); // ~60fps
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
global.cancelAnimationFrame = (id) => {
|
|
43
|
+
clearTimeout(id);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Suppress console logs during tests (optional)
|
|
47
|
+
// global.console = {
|
|
48
|
+
// ...console,
|
|
49
|
+
// log: jest.fn(),
|
|
50
|
+
// debug: jest.fn(),
|
|
51
|
+
// info: jest.fn(),
|
|
52
|
+
// warn: jest.fn()
|
|
53
|
+
// };
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2018",
|
|
7
|
+
"DOM"
|
|
8
|
+
],
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": [
|
|
12
|
+
"src/*"
|
|
13
|
+
],
|
|
14
|
+
"@/core/*": [
|
|
15
|
+
"src/core/*"
|
|
16
|
+
],
|
|
17
|
+
"@/services/*": [
|
|
18
|
+
"src/services/*"
|
|
19
|
+
],
|
|
20
|
+
"@/types": [
|
|
21
|
+
"src/types/index"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"declaration": true,
|
|
25
|
+
"declarationMap": true,
|
|
26
|
+
"outDir": "./dist",
|
|
27
|
+
"rootDir": "./src",
|
|
28
|
+
"strict": true,
|
|
29
|
+
"noImplicitAny": true,
|
|
30
|
+
"strictNullChecks": true,
|
|
31
|
+
"strictFunctionTypes": true,
|
|
32
|
+
"noImplicitReturns": true,
|
|
33
|
+
"noFallthroughCasesInSwitch": true,
|
|
34
|
+
"moduleResolution": "node",
|
|
35
|
+
"allowSyntheticDefaultImports": true,
|
|
36
|
+
"esModuleInterop": true,
|
|
37
|
+
"skipLibCheck": true,
|
|
38
|
+
"forceConsistentCasingInFileNames": true,
|
|
39
|
+
"resolveJsonModule": true,
|
|
40
|
+
"sourceMap": true,
|
|
41
|
+
"removeComments": false,
|
|
42
|
+
"preserveConstEnums": true,
|
|
43
|
+
"incremental": true,
|
|
44
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
45
|
+
},
|
|
46
|
+
"include": [
|
|
47
|
+
"src/**/*"
|
|
48
|
+
],
|
|
49
|
+
"exclude": [
|
|
50
|
+
"node_modules",
|
|
51
|
+
"dist",
|
|
52
|
+
"examples"
|
|
53
|
+
]
|
|
54
|
+
}
|