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,147 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Master Validation Script for US Energy Sankey v5
|
|
4
|
+
# Comprehensive validation with real data (1800-2021)
|
|
5
|
+
# Ensures pixel-perfect accuracy during optimization
|
|
6
|
+
|
|
7
|
+
echo "๐ US Energy Sankey v5 - Comprehensive Validation"
|
|
8
|
+
echo "================================================="
|
|
9
|
+
echo "Testing with real US energy data: 1800-2021 (222 years)"
|
|
10
|
+
echo ""
|
|
11
|
+
|
|
12
|
+
# Colors for output
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
BLUE='\033[0;34m'
|
|
17
|
+
NC='\033[0m' # No Color
|
|
18
|
+
|
|
19
|
+
# Track validation results
|
|
20
|
+
NUMERICAL_RESULT=0
|
|
21
|
+
VISUAL_RESULT=0
|
|
22
|
+
PERFORMANCE_RESULT=0
|
|
23
|
+
BUILD_RESULT=0
|
|
24
|
+
|
|
25
|
+
# Function to print status
|
|
26
|
+
print_status() {
|
|
27
|
+
if [ $1 -eq 0 ]; then
|
|
28
|
+
echo -e "${GREEN}โ
$2 PASSED${NC}"
|
|
29
|
+
else
|
|
30
|
+
echo -e "${RED}โ $2 FAILED${NC}"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Function to cleanup background processes
|
|
35
|
+
cleanup() {
|
|
36
|
+
echo -e "\n๐งน Cleaning up..."
|
|
37
|
+
if [ ! -z "$SERVER_PID" ]; then
|
|
38
|
+
kill $SERVER_PID 2>/dev/null
|
|
39
|
+
wait $SERVER_PID 2>/dev/null
|
|
40
|
+
echo " Server stopped"
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Trap to ensure cleanup on exit
|
|
45
|
+
trap cleanup EXIT INT TERM
|
|
46
|
+
|
|
47
|
+
# Step 1: Build the project
|
|
48
|
+
echo -e "${BLUE}๐ฆ Step 1: Building project...${NC}"
|
|
49
|
+
npm run build
|
|
50
|
+
BUILD_RESULT=$?
|
|
51
|
+
print_status $BUILD_RESULT "Build"
|
|
52
|
+
|
|
53
|
+
if [ $BUILD_RESULT -ne 0 ]; then
|
|
54
|
+
echo -e "${RED}Build failed. Cannot proceed with validation.${NC}"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Verify required build files exist
|
|
59
|
+
echo "๐ Verifying build artifacts..."
|
|
60
|
+
if [ ! -f "dist/us-energy-sankey-v5.standalone.esm.js" ]; then
|
|
61
|
+
echo -e "${RED}โ Required standalone ESM build not found${NC}"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
if [ ! -f "examples/data/data.json" ]; then
|
|
65
|
+
echo -e "${RED}โ Required data file not found${NC}"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
echo -e "${GREEN}โ
All required files present${NC}"
|
|
69
|
+
|
|
70
|
+
# Step 2: Run numerical validation
|
|
71
|
+
echo -e "\n${BLUE}๐ Step 2: Running numerical validation...${NC}"
|
|
72
|
+
echo "Testing mathematical consistency with real data"
|
|
73
|
+
npm run test:numerical
|
|
74
|
+
NUMERICAL_RESULT=$?
|
|
75
|
+
print_status $NUMERICAL_RESULT "Numerical Validation"
|
|
76
|
+
|
|
77
|
+
# Step 3: Start server for visual/performance tests
|
|
78
|
+
echo -e "\n${BLUE}๐ Step 3: Starting local server...${NC}"
|
|
79
|
+
npm run serve &
|
|
80
|
+
SERVER_PID=$!
|
|
81
|
+
echo "Server PID: $SERVER_PID"
|
|
82
|
+
|
|
83
|
+
# Wait for server to start
|
|
84
|
+
echo "Waiting for server to start..."
|
|
85
|
+
sleep 3
|
|
86
|
+
|
|
87
|
+
# Check if server is running
|
|
88
|
+
if ! curl -s http://localhost:8080 > /dev/null; then
|
|
89
|
+
echo -e "${RED}โ Server failed to start${NC}"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
echo -e "${GREEN}โ
Server running on http://localhost:8080${NC}"
|
|
93
|
+
|
|
94
|
+
# Step 4: Run visual validation
|
|
95
|
+
echo -e "\n${BLUE}๐๏ธ Step 4: Running visual validation...${NC}"
|
|
96
|
+
echo "Testing pixel-perfect accuracy across energy eras"
|
|
97
|
+
npm run test:visual
|
|
98
|
+
VISUAL_RESULT=$?
|
|
99
|
+
print_status $VISUAL_RESULT "Visual Validation"
|
|
100
|
+
|
|
101
|
+
# Step 5: Run performance validation
|
|
102
|
+
echo -e "\n${BLUE}โก Step 5: Running performance validation...${NC}"
|
|
103
|
+
echo "Testing performance with 222 years of real data"
|
|
104
|
+
npm run test:performance
|
|
105
|
+
PERFORMANCE_RESULT=$?
|
|
106
|
+
print_status $PERFORMANCE_RESULT "Performance Validation"
|
|
107
|
+
|
|
108
|
+
# Generate final report
|
|
109
|
+
echo -e "\n๐ VALIDATION SUMMARY"
|
|
110
|
+
echo "===================="
|
|
111
|
+
print_status $BUILD_RESULT "Build"
|
|
112
|
+
print_status $NUMERICAL_RESULT "Numerical Validation (Real Data Calculations)"
|
|
113
|
+
print_status $VISUAL_RESULT "Visual Validation (Pixel-Perfect Screenshots)"
|
|
114
|
+
print_status $PERFORMANCE_RESULT "Performance Validation (Speed & Memory)"
|
|
115
|
+
|
|
116
|
+
# Calculate overall result
|
|
117
|
+
OVERALL_RESULT=$((BUILD_RESULT + NUMERICAL_RESULT + VISUAL_RESULT + PERFORMANCE_RESULT))
|
|
118
|
+
|
|
119
|
+
if [ $OVERALL_RESULT -eq 0 ]; then
|
|
120
|
+
echo -e "\n${GREEN}๐ ALL VALIDATIONS PASSED!${NC}"
|
|
121
|
+
echo -e "${GREEN}โ
Your optimization maintains identical results${NC}"
|
|
122
|
+
echo -e "${GREEN}โ
Ready for production deployment${NC}"
|
|
123
|
+
else
|
|
124
|
+
echo -e "\n${RED}โ VALIDATION FAILURES DETECTED${NC}"
|
|
125
|
+
echo -e "${RED}โ ๏ธ Do not proceed with optimization until all tests pass${NC}"
|
|
126
|
+
|
|
127
|
+
# Provide specific guidance
|
|
128
|
+
if [ $NUMERICAL_RESULT -ne 0 ]; then
|
|
129
|
+
echo -e "${YELLOW}๐ก Numerical failures: Check mathematical calculations and constants${NC}"
|
|
130
|
+
fi
|
|
131
|
+
if [ $VISUAL_RESULT -ne 0 ]; then
|
|
132
|
+
echo -e "${YELLOW}๐ก Visual failures: Check validation/diffs/ for pixel differences${NC}"
|
|
133
|
+
fi
|
|
134
|
+
if [ $PERFORMANCE_RESULT -ne 0 ]; then
|
|
135
|
+
echo -e "${YELLOW}๐ก Performance failures: Check if performance thresholds were exceeded${NC}"
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Show validation artifacts
|
|
140
|
+
echo -e "\n๐ VALIDATION ARTIFACTS"
|
|
141
|
+
echo "======================"
|
|
142
|
+
echo "๐ Numerical test results: Jest output above"
|
|
143
|
+
echo "๐ธ Visual baselines: validation/baselines/"
|
|
144
|
+
echo "๐ Visual differences: validation/diffs/"
|
|
145
|
+
echo "โก Performance data: validation/performance/"
|
|
146
|
+
|
|
147
|
+
exit $OVERALL_RESULT
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Validation Script for US Energy Sankey v5
|
|
3
|
+
* Using REAL data from examples/data/data.json (1800-2021)
|
|
4
|
+
* Pixel-perfect comparison across key historical periods
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {chromium} from 'playwright';
|
|
8
|
+
import pixelmatch from 'pixelmatch';
|
|
9
|
+
import {PNG} from 'pngjs';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
class RealDataVisualValidator {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.browser = null;
|
|
20
|
+
this.baselineDir = path.join(__dirname, '../validation/baselines');
|
|
21
|
+
this.outputDir = path.join(__dirname, '../validation/output');
|
|
22
|
+
this.diffDir = path.join(__dirname, '../validation/diffs');
|
|
23
|
+
|
|
24
|
+
// Test scenarios based on ACTUAL US energy history
|
|
25
|
+
this.testScenarios = [
|
|
26
|
+
{
|
|
27
|
+
name: 'colonial-1800.png',
|
|
28
|
+
year: 1800,
|
|
29
|
+
description: 'Colonial America - Wood burning era',
|
|
30
|
+
significance: 'Baseline minimal energy usage'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'industrial-1900.png',
|
|
34
|
+
year: 1900,
|
|
35
|
+
description: 'Industrial Revolution - Coal emergence',
|
|
36
|
+
significance: 'Industrial transformation period'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'postwar-1950.png',
|
|
40
|
+
year: 1950,
|
|
41
|
+
description: 'Post-war boom - Coal dominant + steam locomotives retired',
|
|
42
|
+
significance: 'Transportation electrification milestone'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'nuclear-1980.png',
|
|
46
|
+
year: 1980,
|
|
47
|
+
description: 'Nuclear age - Atomic energy adoption',
|
|
48
|
+
significance: 'Nuclear power peak period'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'modern-2000.png',
|
|
52
|
+
year: 2000,
|
|
53
|
+
description: 'Modern era - Diversified energy mix',
|
|
54
|
+
significance: 'Pre-renewable diversity'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'renewable-2020.png',
|
|
58
|
+
year: 2020,
|
|
59
|
+
description: 'Renewable era - Solar and wind growth',
|
|
60
|
+
significance: 'Clean energy transition'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'current-2021.png',
|
|
64
|
+
year: 2021,
|
|
65
|
+
description: 'Latest available data',
|
|
66
|
+
significance: 'Most recent energy snapshot'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'waste-visible-2000.png',
|
|
70
|
+
year: 2000,
|
|
71
|
+
wasteVisible: true,
|
|
72
|
+
description: 'Waste heat visible - Modern era',
|
|
73
|
+
significance: 'Full energy flow visualization'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'waste-hidden-2000.png',
|
|
77
|
+
year: 2000,
|
|
78
|
+
wasteVisible: false,
|
|
79
|
+
description: 'Waste heat hidden - Modern era',
|
|
80
|
+
significance: 'Clean flow visualization'
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
this.ensureDirectories();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ensureDirectories() {
|
|
88
|
+
[this.baselineDir, this.outputDir, this.diffDir].forEach(dir => {
|
|
89
|
+
if (!fs.existsSync(dir)) {
|
|
90
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
91
|
+
console.log(`๐ Created directory: ${dir}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async setup() {
|
|
97
|
+
console.log('๐ Setting up browser for visual validation...');
|
|
98
|
+
this.browser = await chromium.launch({
|
|
99
|
+
headless: true,
|
|
100
|
+
args: [
|
|
101
|
+
'--no-sandbox',
|
|
102
|
+
'--disable-dev-shm-usage',
|
|
103
|
+
'--disable-web-security',
|
|
104
|
+
'--allow-running-insecure-content'
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async captureScenario(scenario) {
|
|
110
|
+
const page = await this.browser.newPage();
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Set consistent viewport for all tests
|
|
114
|
+
await page.setViewportSize({width: 1400, height: 900});
|
|
115
|
+
|
|
116
|
+
// Navigate to the test page
|
|
117
|
+
console.log(`๐ธ Capturing: ${scenario.description}`);
|
|
118
|
+
await page.goto('http://localhost:8080/examples/visual-test.html', {
|
|
119
|
+
waitUntil: 'networkidle',
|
|
120
|
+
timeout: 30000
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Wait for sankey initialization with REAL data validation
|
|
124
|
+
await page.waitForFunction(() => {
|
|
125
|
+
return window.sankey &&
|
|
126
|
+
window.sankey.isInitialized() &&
|
|
127
|
+
window.sankey.getYears().length === 222 && // Verify all 222 years loaded
|
|
128
|
+
window.sankey.getYears().includes(1800) &&
|
|
129
|
+
window.sankey.getYears().includes(2021);
|
|
130
|
+
}, {timeout: 20000});
|
|
131
|
+
|
|
132
|
+
// Set specific year from real dataset
|
|
133
|
+
if (scenario.year) {
|
|
134
|
+
await page.evaluate((year) => {
|
|
135
|
+
console.log(`Setting year to ${year}`);
|
|
136
|
+
window.sankey.setYear(year);
|
|
137
|
+
}, scenario.year);
|
|
138
|
+
|
|
139
|
+
// Wait for year transition to complete
|
|
140
|
+
await page.waitForTimeout(1000);
|
|
141
|
+
|
|
142
|
+
// Verify year was set correctly
|
|
143
|
+
const actualYear = await page.evaluate(() => {
|
|
144
|
+
return window.sankey.getCurrentYear();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (actualYear !== scenario.year) {
|
|
148
|
+
console.warn(`โ ๏ธ Year mismatch: expected ${scenario.year}, got ${actualYear}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle waste heat visibility
|
|
153
|
+
if (scenario.wasteVisible !== undefined) {
|
|
154
|
+
await page.evaluate((visible) => {
|
|
155
|
+
const currentState = window.sankey.isWasteHeatVisible();
|
|
156
|
+
console.log(`Waste heat: current=${currentState}, target=${visible}`);
|
|
157
|
+
if (currentState !== visible) {
|
|
158
|
+
window.sankey.toggleWasteHeat();
|
|
159
|
+
}
|
|
160
|
+
}, scenario.wasteVisible);
|
|
161
|
+
|
|
162
|
+
// Wait for waste heat toggle to complete
|
|
163
|
+
await page.waitForTimeout(500);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Wait for any final rendering
|
|
167
|
+
await page.waitForTimeout(1500);
|
|
168
|
+
|
|
169
|
+
// Additional wait for stability
|
|
170
|
+
await page.waitForLoadState('networkidle');
|
|
171
|
+
|
|
172
|
+
// Capture screenshot
|
|
173
|
+
const screenshot = await page.screenshot({
|
|
174
|
+
path: path.join(this.outputDir, scenario.name),
|
|
175
|
+
fullPage: false,
|
|
176
|
+
clip: {x: 0, y: 0, width: 1400, height: 1000} // Consistent clipping
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
console.log(`โ
Captured: ${scenario.name} (${scenario.year || 'default'})`);
|
|
180
|
+
return screenshot;
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`โ Failed to capture ${scenario.name}:`, error.message);
|
|
184
|
+
throw error;
|
|
185
|
+
} finally {
|
|
186
|
+
await page.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async compareWithBaseline(filename) {
|
|
191
|
+
const baselinePath = path.join(this.baselineDir, filename);
|
|
192
|
+
const outputPath = path.join(this.outputDir, filename);
|
|
193
|
+
const diffPath = path.join(this.diffDir, filename);
|
|
194
|
+
|
|
195
|
+
if (!fs.existsSync(baselinePath)) {
|
|
196
|
+
console.log(`๐ Creating new baseline: ${filename}`);
|
|
197
|
+
fs.copyFileSync(outputPath, baselinePath);
|
|
198
|
+
return {match: true, isNewBaseline: true};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
|
|
203
|
+
const current = PNG.sync.read(fs.readFileSync(outputPath));
|
|
204
|
+
|
|
205
|
+
const {width, height} = baseline;
|
|
206
|
+
const diff = new PNG({width, height});
|
|
207
|
+
|
|
208
|
+
const pixelDiffCount = pixelmatch(
|
|
209
|
+
baseline.data,
|
|
210
|
+
current.data,
|
|
211
|
+
diff.data,
|
|
212
|
+
width,
|
|
213
|
+
height,
|
|
214
|
+
{
|
|
215
|
+
threshold: 0.005, // Very strict threshold for energy visualization
|
|
216
|
+
includeAA: false,
|
|
217
|
+
alpha: 0.1,
|
|
218
|
+
diffColor: [255, 0, 0] // Red diff highlights
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const totalPixels = width * height;
|
|
223
|
+
const diffPercentage = (pixelDiffCount / totalPixels) * 100;
|
|
224
|
+
|
|
225
|
+
if (pixelDiffCount > 0) {
|
|
226
|
+
fs.writeFileSync(diffPath, PNG.sync.write(diff));
|
|
227
|
+
} else {
|
|
228
|
+
// Remove diff file if no differences
|
|
229
|
+
if (fs.existsSync(diffPath)) {
|
|
230
|
+
fs.unlinkSync(diffPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
match: pixelDiffCount === 0,
|
|
236
|
+
pixelDiffCount,
|
|
237
|
+
diffPercentage,
|
|
238
|
+
totalPixels,
|
|
239
|
+
diffPath: pixelDiffCount > 0 ? diffPath : null
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`โ Error comparing ${filename}:`, error.message);
|
|
244
|
+
return {
|
|
245
|
+
match: false,
|
|
246
|
+
error: error.message
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async validateAllScenarios() {
|
|
252
|
+
console.log('๐ฏ Starting visual validation with REAL US energy data (1800-2021)...\n');
|
|
253
|
+
console.log(`Testing ${this.testScenarios.length} scenarios across 222 years of energy history\n`);
|
|
254
|
+
|
|
255
|
+
const results = [];
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < this.testScenarios.length; i++) {
|
|
258
|
+
const scenario = this.testScenarios[i];
|
|
259
|
+
console.log(`[${i + 1}/${this.testScenarios.length}] ${scenario.description}`);
|
|
260
|
+
console.log(` Significance: ${scenario.significance}`);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await this.captureScenario(scenario);
|
|
264
|
+
const comparison = await this.compareWithBaseline(scenario.name);
|
|
265
|
+
|
|
266
|
+
results.push({
|
|
267
|
+
scenario: scenario.name,
|
|
268
|
+
description: scenario.description,
|
|
269
|
+
year: scenario.year,
|
|
270
|
+
significance: scenario.significance,
|
|
271
|
+
...comparison
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (comparison.error) {
|
|
275
|
+
console.error(`โ ${scenario.name}: ${comparison.error}`);
|
|
276
|
+
} else if (!comparison.match && !comparison.isNewBaseline) {
|
|
277
|
+
console.error(`โ ${scenario.name}: ${comparison.pixelDiffCount} pixels different (${comparison.diffPercentage.toFixed(3)}%)`);
|
|
278
|
+
console.error(` Diff saved to: ${comparison.diffPath}`);
|
|
279
|
+
} else if (comparison.isNewBaseline) {
|
|
280
|
+
console.log(`๐ ${scenario.name}: New baseline created`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`โ
${scenario.name}: Perfect pixel match`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error(`๐ฅ ${scenario.name}: Capture failed - ${error.message}`);
|
|
287
|
+
results.push({
|
|
288
|
+
scenario: scenario.name,
|
|
289
|
+
description: scenario.description,
|
|
290
|
+
year: scenario.year,
|
|
291
|
+
match: false,
|
|
292
|
+
error: error.message
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(''); // Empty line for readability
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async teardown() {
|
|
303
|
+
if (this.browser) {
|
|
304
|
+
await this.browser.close();
|
|
305
|
+
console.log('๐งน Browser closed');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
generateReport(results) {
|
|
310
|
+
console.log(`\n๐ VISUAL VALIDATION REPORT`);
|
|
311
|
+
console.log(`${'='.repeat(50)}`);
|
|
312
|
+
|
|
313
|
+
const total = results.length;
|
|
314
|
+
const newBaselines = results.filter(r => r.isNewBaseline).length;
|
|
315
|
+
const passed = results.filter(r => r.match && !r.isNewBaseline).length;
|
|
316
|
+
const failed = results.filter(r => !r.match && !r.isNewBaseline).length;
|
|
317
|
+
const errors = results.filter(r => r.error).length;
|
|
318
|
+
|
|
319
|
+
console.log(`Total scenarios: ${total}`);
|
|
320
|
+
console.log(`New baselines: ${newBaselines}`);
|
|
321
|
+
console.log(`Passed: ${passed}`);
|
|
322
|
+
console.log(`Failed: ${failed}`);
|
|
323
|
+
console.log(`Errors: ${errors}`);
|
|
324
|
+
|
|
325
|
+
if (failed > 0) {
|
|
326
|
+
console.log(`\nโ FAILED SCENARIOS:`);
|
|
327
|
+
results.filter(r => !r.match && !r.isNewBaseline && !r.error).forEach(r => {
|
|
328
|
+
console.log(` ${r.scenario} (${r.year}): ${r.diffPercentage.toFixed(3)}% difference`);
|
|
329
|
+
console.log(` ${r.description}`);
|
|
330
|
+
console.log(` Diff: ${r.diffPath}`);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (errors > 0) {
|
|
335
|
+
console.log(`\n๐ฅ ERROR SCENARIOS:`);
|
|
336
|
+
results.filter(r => r.error).forEach(r => {
|
|
337
|
+
console.log(` ${r.scenario}: ${r.error}`);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {total, newBaselines, passed, failed, errors};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Main execution
|
|
346
|
+
async function main() {
|
|
347
|
+
const validator = new RealDataVisualValidator();
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
await validator.setup();
|
|
351
|
+
const results = await validator.validateAllScenarios();
|
|
352
|
+
const summary = validator.generateReport(results);
|
|
353
|
+
|
|
354
|
+
if (summary.failed > 0 || summary.errors > 0) {
|
|
355
|
+
console.error(`\nโ Visual validation failed: ${summary.failed} failures, ${summary.errors} errors`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
} else if (summary.newBaselines > 0) {
|
|
358
|
+
console.log(`\n๐ ${summary.newBaselines} new baselines created. Re-run to validate against them.`);
|
|
359
|
+
} else {
|
|
360
|
+
console.log(`\nโ
All ${summary.passed} visual validations passed! Pixel-perfect accuracy maintained.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('โ Visual validation crashed:', error);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
} finally {
|
|
367
|
+
await validator.teardown();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Run if called directly
|
|
372
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
373
|
+
main();
|
|
374
|
+
}
|