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,484 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
import {GraphPoint, GraphStroke, YearTotals} from '@/types';
|
|
3
|
+
import {EventBus} from '@/core/events/EventBus';
|
|
4
|
+
import {ConfigurationService} from '@/services/ConfigurationService';
|
|
5
|
+
import {SummaryService} from '@/services/calculation/SummaryService';
|
|
6
|
+
import {GraphService} from '@/services/calculation/GraphService';
|
|
7
|
+
import {DataService} from "@/services/data/DataService";
|
|
8
|
+
|
|
9
|
+
// ==================== D3 SELECTION TYPES ====================
|
|
10
|
+
|
|
11
|
+
type D3SVGSelection = d3.Selection<SVGSVGElement, unknown, HTMLElement, any>;
|
|
12
|
+
type D3DivSelection = d3.Selection<HTMLDivElement, unknown, HTMLElement, any>;
|
|
13
|
+
|
|
14
|
+
// ==================== RENDERING DATA INTERFACES ====================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Chart Rendering Service - D3.js SVG Generation & Visual Rendering Engine
|
|
18
|
+
*
|
|
19
|
+
* ARCHITECTURAL RESPONSIBILITY: Mathematical Data → Visual SVG Transformation
|
|
20
|
+
*
|
|
21
|
+
* This service implements sophisticated D3.js patterns to transform mathematical energy flow
|
|
22
|
+
* calculations into interactive SVG visualizations. It manages the complete visual rendering
|
|
23
|
+
* pipeline from raw data to user-ready interactive charts.
|
|
24
|
+
*
|
|
25
|
+
* D3.JS DESIGN PATTERNS IMPLEMENTED:
|
|
26
|
+
* 1. **Selection Patterns**: Efficient DOM element selection and manipulation
|
|
27
|
+
* 2. **Data Binding**: Binding energy data to SVG elements with enter/update/exit patterns
|
|
28
|
+
* 3. **Method Chaining**: Fluent D3 API usage for concise and readable code
|
|
29
|
+
* 4. **Event Handling**: Mouse interactions with tooltip integration
|
|
30
|
+
* 5. **Transition Management**: Smooth animations for year-to-year transitions
|
|
31
|
+
* 6. **Scale Management**: Coordinate transformations and responsive scaling
|
|
32
|
+
*
|
|
33
|
+
* SVG GENERATION ARCHITECTURE:
|
|
34
|
+
* - **Fuel Boxes**: Left column showing energy sources (solar, coal, etc.)
|
|
35
|
+
* - **Sector Boxes**: Right column showing consumption sectors (residential, industrial, etc.)
|
|
36
|
+
* - **Flow Paths**: connecting fuel sources to consumption sectors
|
|
37
|
+
* - **Labels & Text**: Dynamic text elements with data-driven content
|
|
38
|
+
* - **Tooltips**: Interactive information overlays with precise positioning
|
|
39
|
+
*
|
|
40
|
+
* VISUAL RENDERING PIPELINE:
|
|
41
|
+
* Mathematical Data → D3 Selections → SVG Elements → Interactive Features → User Interface
|
|
42
|
+
*
|
|
43
|
+
* PERFORMANCE OPTIMIZATIONS:
|
|
44
|
+
* - Efficient DOM manipulation using D3 selections
|
|
45
|
+
* - Minimal DOM re-creation during year transitions
|
|
46
|
+
* - Event delegation for tooltip management
|
|
47
|
+
* - CSS class-based styling for performance
|
|
48
|
+
* - Cached line generator for path creation
|
|
49
|
+
*/
|
|
50
|
+
export class RenderingService {
|
|
51
|
+
private lineGenerator: d3.Line<GraphPoint>;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
private configService: ConfigurationService,
|
|
55
|
+
private summaryCalculationService: SummaryService,
|
|
56
|
+
private graphCalculationService: GraphService,
|
|
57
|
+
private dataService: DataService,
|
|
58
|
+
private eventBus: EventBus
|
|
59
|
+
) {
|
|
60
|
+
// Initialize D3 line generator for smooth rendering
|
|
61
|
+
this.lineGenerator = d3.line<GraphPoint>()
|
|
62
|
+
.x((d: GraphPoint) => d.x)
|
|
63
|
+
.y((d: GraphPoint) => d.y);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draws the main title, subtitle, and affiliations in separate title container
|
|
68
|
+
*/
|
|
69
|
+
public drawHeader(): void {
|
|
70
|
+
// Use separate title container
|
|
71
|
+
const titleContainer = d3.select('.title_container')
|
|
72
|
+
.style('height', '58px');
|
|
73
|
+
|
|
74
|
+
const titleSvg = titleContainer
|
|
75
|
+
.append('svg')
|
|
76
|
+
.attr('id', 'title')
|
|
77
|
+
.attr('width', this.configService.WIDTH)
|
|
78
|
+
.attr('height', 58);
|
|
79
|
+
|
|
80
|
+
const country = this.configService.options.country
|
|
81
|
+
|
|
82
|
+
// Draw title of graph
|
|
83
|
+
const svg_title = titleSvg.append('text')
|
|
84
|
+
.text(`${country} energy usage`)
|
|
85
|
+
.attr('text-anchor', 'end')
|
|
86
|
+
.attr('x', this.configService.ELEC_BOX_X - 10)
|
|
87
|
+
.attr('y', '1.5em')
|
|
88
|
+
.attr('class', 'title');
|
|
89
|
+
|
|
90
|
+
// Draw units of graph (will be populated by animation)
|
|
91
|
+
svg_title.append('tspan')
|
|
92
|
+
.text('0 W/capita') // Will be updated by animation to show actual energy usage
|
|
93
|
+
.attr('text-anchor', 'end')
|
|
94
|
+
.attr('x', this.configService.ELEC_BOX_X - 13)
|
|
95
|
+
.attr('dy', '1.4em')
|
|
96
|
+
.attr('class', 'unit year-total animate title-bottom')
|
|
97
|
+
.attr('data-incr', '0')
|
|
98
|
+
.attr('data-value', '0');
|
|
99
|
+
|
|
100
|
+
// Draw year (will be populated by animation)
|
|
101
|
+
const firstYear = this.dataService.firstYear!;
|
|
102
|
+
const lastYear = this.dataService.lastYear!;
|
|
103
|
+
svg_title.append('tspan')
|
|
104
|
+
.text(firstYear.toString())
|
|
105
|
+
.attr('text-anchor', 'start')
|
|
106
|
+
.attr('x', this.configService.ELEC_BOX_X)
|
|
107
|
+
.attr('dy', '0em')
|
|
108
|
+
.attr('class', 'year animate')
|
|
109
|
+
.attr('data-incr', '0')
|
|
110
|
+
.attr('data-value', firstYear);
|
|
111
|
+
|
|
112
|
+
// Add subtitle
|
|
113
|
+
titleSvg.append('text')
|
|
114
|
+
.text(`Energy Transitions in ${country} History, ${firstYear}-${lastYear}`)
|
|
115
|
+
.attr('x', this.configService.ELEC_BOX_X + this.configService.BOX_WIDTH + 25)
|
|
116
|
+
.attr('y', '1.5em')
|
|
117
|
+
.attr('class', 'affiliation')
|
|
118
|
+
.append('tspan')
|
|
119
|
+
.text('Suits, Matteson, and Moyer (2020)')
|
|
120
|
+
.attr('class', 'affiliation-bottom')
|
|
121
|
+
.attr('x', this.configService.ELEC_BOX_X + this.configService.BOX_WIDTH + 25)
|
|
122
|
+
.attr('dy', '1.4em');
|
|
123
|
+
|
|
124
|
+
// Draw affiliations
|
|
125
|
+
const affiliationX = this.configService.ELEC_BOX_X + this.configService.BOX_WIDTH +
|
|
126
|
+
(this.configService.WIDTH - (this.configService.ELEC_BOX_X + this.configService.BOX_WIDTH)) / 2 + 50;
|
|
127
|
+
|
|
128
|
+
titleSvg.append('text')
|
|
129
|
+
.text('Center for Robust Decision-making on')
|
|
130
|
+
.attr('x', affiliationX)
|
|
131
|
+
.attr('y', '1.5em')
|
|
132
|
+
.attr('class', 'affiliation')
|
|
133
|
+
.append('tspan')
|
|
134
|
+
.text('Climate and Energy Policy, UChicago')
|
|
135
|
+
.attr('class', 'affiliation-bottom')
|
|
136
|
+
.attr('x', affiliationX)
|
|
137
|
+
.attr('dy', '1.4em');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Draw Fuel Source Labels - Left Column Energy Source Visualization
|
|
142
|
+
*
|
|
143
|
+
* RENDERING RESPONSIBILITY: Create dynamic fuel source labels with proportional positioning
|
|
144
|
+
*
|
|
145
|
+
* This method implements D3.js text element creation for fuel source identification.
|
|
146
|
+
* Label positions are calculated dynamically based on energy totals to maintain
|
|
147
|
+
* accurate visual alignment with proportional fuel box heights.
|
|
148
|
+
*
|
|
149
|
+
* D3.JS TEXT RENDERING PATTERNS:
|
|
150
|
+
* - Dynamic content from configuration and energy data
|
|
151
|
+
* - Conditional visibility based on energy values (hidden if zero)
|
|
152
|
+
* - Data attributes for animation and programmatic access
|
|
153
|
+
* - CSS classes for styling and state management
|
|
154
|
+
*
|
|
155
|
+
* PROPORTIONAL POSITIONING ALGORITHM:
|
|
156
|
+
* Y-position calculated dynamically: TOP_Y + cumulative_energy_heights + gaps
|
|
157
|
+
* Ensures perfect alignment between fuel labels and their corresponding flow origins
|
|
158
|
+
*/
|
|
159
|
+
public drawLeftLabels(svg: D3SVGSelection, totals: YearTotals): void {
|
|
160
|
+
// Electricity & Heat (index 0, 1) are handled separately due to special positioning requirements
|
|
161
|
+
const leftFuels = this.configService.FUELS.slice(2); // Remove electricity & heat from array
|
|
162
|
+
|
|
163
|
+
svg.selectAll('.fuel-label-left')
|
|
164
|
+
.data(leftFuels)
|
|
165
|
+
.join('text')
|
|
166
|
+
.attr('class', (d: any) => `label animate fuel ${d.fuel} fuel-label-left`) // CSS classes for styling
|
|
167
|
+
.text((d: any) => d.name) // Human-readable fuel name
|
|
168
|
+
.attr('x', this.configService.LEFT_X) // X-position: left column alignment
|
|
169
|
+
.attr('y', (d: any, i: number) => { // Y-position: calculated per fuel
|
|
170
|
+
// Calculate cumulative Y position for this fuel
|
|
171
|
+
let cumulativeTop = this.configService.TOP_Y;
|
|
172
|
+
for (let j = 0; j < i; j++) {
|
|
173
|
+
const prevFuel = leftFuels[j];
|
|
174
|
+
const prevFuelTotal = totals[prevFuel.fuel] || 0;
|
|
175
|
+
cumulativeTop += prevFuelTotal * this.configService.SCALE + this.configService.LEFT_GAP;
|
|
176
|
+
}
|
|
177
|
+
return cumulativeTop - 5; // Apply visual offset
|
|
178
|
+
})
|
|
179
|
+
.attr('data-incr', '0') // Animation increment tracking
|
|
180
|
+
.attr('data-fuel', (d: any) => d.fuel) // Fuel identifier for interactions
|
|
181
|
+
.attr('data-value', (d: any) => { // Formatted energy value
|
|
182
|
+
const fuelTotal = totals[d.fuel] || 0;
|
|
183
|
+
return this.graphCalculationService.sigfig2(fuelTotal);
|
|
184
|
+
})
|
|
185
|
+
.classed('hidden', (d: any) => { // Hide labels for zero-energy fuels
|
|
186
|
+
const fuelTotal = totals[d.fuel] || 0;
|
|
187
|
+
return fuelTotal === 0;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Draw Energy Sector Boxes & Labels - Right Column Consumption Visualization
|
|
193
|
+
*
|
|
194
|
+
* RENDERING RESPONSIBILITY: Create interactive sector boxes with labels and totals
|
|
195
|
+
*
|
|
196
|
+
* This method implements the most complex D3.js element creation patterns, generating
|
|
197
|
+
* rectangular sector boxes with multi-line labels and dynamic energy totals.
|
|
198
|
+
* Demonstrates advanced SVG composition with nested text elements.
|
|
199
|
+
*
|
|
200
|
+
* ADVANCED D3.JS PATTERNS DEMONSTRATED:
|
|
201
|
+
* 1. **Conditional Positioning**: Different algorithms for electricity vs. sector boxes
|
|
202
|
+
* 2. **Complex Text Composition**: Multi-line labels using tspan elements
|
|
203
|
+
* 3. **Dynamic Sizing**: Proportional box heights based on energy consumption
|
|
204
|
+
* 4. **Data-Driven Visibility**: Conditional hiding based on energy values
|
|
205
|
+
* 5. **Nested Element Creation**: Text elements with multiple tspan children
|
|
206
|
+
* 6. **Special Case Handling**: Residential/Commercial label splitting
|
|
207
|
+
*
|
|
208
|
+
* SVG ELEMENT ARCHITECTURE:
|
|
209
|
+
* Each sector generates: Rectangle (visual box) + Text (label + total + waste)
|
|
210
|
+
* - Rectangle: Proportionally sized based on energy consumption
|
|
211
|
+
* - Text Label: Human-readable sector name with special formatting
|
|
212
|
+
* - Energy Total: Numeric display of energy consumption value
|
|
213
|
+
* - Waste Total: Thermodynamic losses for electricity-consuming sectors
|
|
214
|
+
*/
|
|
215
|
+
public drawBoxes(
|
|
216
|
+
svg: D3SVGSelection,
|
|
217
|
+
totals: YearTotals,
|
|
218
|
+
): void {
|
|
219
|
+
const boxtops = this.summaryCalculationService.summary!.boxTops; // Calculated box positions
|
|
220
|
+
|
|
221
|
+
// SECTOR BOX GENERATION LOOP: Create visual elements for each consumption sector
|
|
222
|
+
for (let i = 0; i < this.configService.BOXES.length; i++) {
|
|
223
|
+
const boxConfig = this.configService.BOXES[i];
|
|
224
|
+
|
|
225
|
+
let x: number; // Calculated X-coordinate
|
|
226
|
+
let y: number; // Calculated Y-coordinate
|
|
227
|
+
|
|
228
|
+
if (!this.configService.hasHeatData && boxConfig.box == "heat") {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// CONDITIONAL POSITIONING ALGORITHM: Electricity vs. Regular Sectors
|
|
233
|
+
if (boxConfig.box === 'elec') {
|
|
234
|
+
// ELECTRICITY BOX: Special central positioning
|
|
235
|
+
x = this.configService.ELEC_BOX_X; // Configured X-position
|
|
236
|
+
y = this.configService.ELEC_BOX_Y - totals.elec * this.configService.SCALE; // Dynamic Y-position
|
|
237
|
+
} else if (boxConfig.box === 'heat') {
|
|
238
|
+
// ELECTRICITY BOX: Special central positioning
|
|
239
|
+
x = this.configService.HEAT_BOX_X; // Configured X-position
|
|
240
|
+
y = this.configService.HEAT_BOX_Y - totals.heat! * this.configService.SCALE; // Dynamic Y-position
|
|
241
|
+
} else {
|
|
242
|
+
// CONSUMPTION SECTORS: Right column with calculated positioning
|
|
243
|
+
x = this.configService.WIDTH - this.configService.BOX_WIDTH; // Right-aligned positioning
|
|
244
|
+
y = boxtops[boxConfig.box] || 0; // Pre-calculated Y-positions
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const boxTotal = (totals as any)[boxConfig.box] || 0; // Energy consumption for this sector
|
|
248
|
+
|
|
249
|
+
// D3.JS RECTANGLE CREATION: Visual sector box with proportional sizing
|
|
250
|
+
svg.append('rect') // Create SVG rectangle
|
|
251
|
+
.attr('x', x) // Horizontal position
|
|
252
|
+
.attr('y', y) // Vertical position
|
|
253
|
+
.attr('width', this.configService.BOX_WIDTH) // Standard box width
|
|
254
|
+
.attr('height', boxTotal > 0 ? boxTotal * this.configService.SCALE + this.configService.BLEED : 0) // Proportional height
|
|
255
|
+
.attr('class', `box sector animate ${boxConfig.box}`) // CSS classes for styling
|
|
256
|
+
.classed('fuel', ['elec', 'heat'].includes(boxConfig.box)) // Special class for electricity
|
|
257
|
+
.attr('data-sector', boxConfig.box) // Sector identifier
|
|
258
|
+
.attr('data-fuel', ['elec', 'heat'].includes(boxConfig.box) ? boxConfig.box : '')
|
|
259
|
+
.attr('data-incr', '0'); // Animation increment tracking
|
|
260
|
+
|
|
261
|
+
// COMPLEX TEXT ELEMENT CREATION: Multi-line label with totals
|
|
262
|
+
const text = svg.append('text') // Create main text element
|
|
263
|
+
.text(boxConfig.box === 'res' ? 'Residential' : boxConfig.name) // Primary label text
|
|
264
|
+
.attr('x', x) // Horizontal alignment
|
|
265
|
+
.attr('y', y - 5) // Vertical position above box
|
|
266
|
+
.attr('dy', boxConfig.box === 'res' ? '-1.8em' : '-0.8em') // Line spacing adjustment
|
|
267
|
+
.attr('data-sector', boxConfig.box) // Sector identification
|
|
268
|
+
.attr('data-fuel', ['elec', 'heat'].includes(boxConfig.box) ? boxConfig.box : '')
|
|
269
|
+
.attr('class', `label sector animate ${boxConfig.box}`) // CSS classes
|
|
270
|
+
.classed('hidden', boxTotal === 0) // Conditional visibility
|
|
271
|
+
.classed('fuel', ['elec', 'heat'].includes(boxConfig.box)); // Special class for electricity
|
|
272
|
+
|
|
273
|
+
// SPECIAL CASE: RESIDENTIAL SECTOR MULTI-LINE LABEL
|
|
274
|
+
if (boxConfig.box === 'res') {
|
|
275
|
+
text.append('tspan') // Create second line
|
|
276
|
+
.text('/Commercial') // Complete sector name
|
|
277
|
+
.attr('x', x) // Horizontal alignment
|
|
278
|
+
.attr('dy', '1em') // Line spacing
|
|
279
|
+
.attr('data-incr', '0'); // Animation tracking
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ENERGY TOTAL DISPLAY: Numerical energy consumption value
|
|
283
|
+
text.append('tspan') // Create total value tspan
|
|
284
|
+
.attr('class', `total sector animate ${boxConfig.box}`) // CSS classes for styling
|
|
285
|
+
.attr('data-sector', boxConfig.box) // Sector identification
|
|
286
|
+
.attr('data-value', boxTotal) // Raw energy value
|
|
287
|
+
.text(this.graphCalculationService.sigfig2(boxTotal)) // Formatted display value
|
|
288
|
+
.attr('x', x) // Horizontal position
|
|
289
|
+
.attr('dy', '1.2em') // Vertical offset
|
|
290
|
+
.attr('data-incr', '0'); // Animation increment
|
|
291
|
+
|
|
292
|
+
// WASTE HEAT DISPLAY: Thermodynamic losses for electricity consumption
|
|
293
|
+
text.append('tspan') // Create waste total tspan
|
|
294
|
+
.attr('class', `total waste-level sector animate ${boxConfig.box}`) // CSS classes
|
|
295
|
+
.attr('data-sector', boxConfig.box) // Sector identification
|
|
296
|
+
.attr('data-value', '0') // Initial waste value
|
|
297
|
+
.text(this.graphCalculationService.sigfig2(0)) // Formatted waste display
|
|
298
|
+
.attr('x', x + this.configService.BOX_WIDTH) // Right-aligned positioning
|
|
299
|
+
.attr('dy', '0') // Same line as totals
|
|
300
|
+
.attr('text-anchor', 'end') // Right text alignment
|
|
301
|
+
.attr('data-incr', '0'); // Animation tracking
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Draw Interactive Energy Flow Paths - Advanced D3.js SVG Path Rendering
|
|
307
|
+
*
|
|
308
|
+
* RENDERING RESPONSIBILITY: Transform Mathematical Flow Data → Interactive SVG Paths
|
|
309
|
+
*
|
|
310
|
+
* This method implements the most sophisticated D3.js rendering patterns in the entire
|
|
311
|
+
* visualization system, creating interactive paths that represent energy
|
|
312
|
+
* flows between fuel sources and consumption sectors.
|
|
313
|
+
*
|
|
314
|
+
* ADVANCED D3.JS PATTERNS IMPLEMENTED:
|
|
315
|
+
* 1. **Complex Path Generation**: Mathematical GraphStroke data → SVG path strings
|
|
316
|
+
* 2. **Interactive Event Binding**: Mouse event handlers for tooltip interactions
|
|
317
|
+
* 3. **Dynamic Content Creation**: Data-driven SVG element creation with unique attributes
|
|
318
|
+
* 4. **Transition Management**: Smooth fade-in/fade-out tooltip animations
|
|
319
|
+
* 5. **Context-Aware Selection**: Targeting existing fuel group containers
|
|
320
|
+
* 6. **Performance Optimization**: Efficient DOM manipulation and event delegation
|
|
321
|
+
*
|
|
322
|
+
* ENERGY FLOW VISUALIZATION ARCHITECTURE:
|
|
323
|
+
* Each energy flow (Fuel → Sector) becomes an SVG <path> element with:
|
|
324
|
+
* - Geometry calculated by GraphService
|
|
325
|
+
* - Stroke width proportional to energy quantity (visual data encoding)
|
|
326
|
+
* - Interactive tooltip showing detailed energy values on hover
|
|
327
|
+
* - CSS classes enabling styling and animation hooks
|
|
328
|
+
* - Data attributes for programmatic access and filtering
|
|
329
|
+
*
|
|
330
|
+
* TOOLTIP INTERACTION DESIGN:
|
|
331
|
+
* - Mouseover: 200ms fade-in with energy flow details
|
|
332
|
+
* - Mouseout: 500ms fade-out for smooth visual transitions
|
|
333
|
+
* - Dynamic positioning: Follows cursor with offset for readability
|
|
334
|
+
* - Content formatting: "Fuel → Sector" with numerical energy value
|
|
335
|
+
*
|
|
336
|
+
* SVG PATH GENERATION PIPELINE:
|
|
337
|
+
* Mathematical Coordinates → parseLineData() → SVG Path String → Interactive Path Element
|
|
338
|
+
*/
|
|
339
|
+
public drawFlows(svg: D3SVGSelection, yearIndex: number, tooltip: D3DivSelection): void {
|
|
340
|
+
// DATA RETRIEVAL: Get mathematical flow calculations for target year
|
|
341
|
+
const graphs = this.graphCalculationService.graphs;
|
|
342
|
+
|
|
343
|
+
const graphData = graphs[yearIndex];
|
|
344
|
+
|
|
345
|
+
console.log("graphData.graph", graphData.graph)
|
|
346
|
+
|
|
347
|
+
// Filter valid strokes and group by fuel for optimized rendering
|
|
348
|
+
const validStrokes = graphData.graph.filter(stroke =>
|
|
349
|
+
stroke.b.x !== null && stroke.b.x !== undefined
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Group strokes by fuel for efficient fuel-based rendering
|
|
353
|
+
const strokesByFuel = validStrokes.reduce((groups: any, stroke: any) => {
|
|
354
|
+
if (!groups[stroke.fuel]) groups[stroke.fuel] = [];
|
|
355
|
+
groups[stroke.fuel].push(stroke);
|
|
356
|
+
return groups;
|
|
357
|
+
}, {});
|
|
358
|
+
|
|
359
|
+
console.log("strokesByFuel", strokesByFuel)
|
|
360
|
+
|
|
361
|
+
Object.entries(strokesByFuel).forEach(([fuel, strokes]: [string, any]) => {
|
|
362
|
+
svg.select(`.fuel.${fuel}`) // Target existing fuel group container
|
|
363
|
+
.selectAll('.flow-path') // Select flow paths within fuel group
|
|
364
|
+
.data(strokes) // Bind stroke data
|
|
365
|
+
.join('path')
|
|
366
|
+
.attr('class', (d: any) => `flow animate ${d.fuel} ${d.box} flow-path`) // CSS classes
|
|
367
|
+
.attr('d', (d: any) => this.parseLineData(d)) // Set path geometry
|
|
368
|
+
.attr('stroke-width', (d: any) => d.stroke > 0 ? d.stroke + this.configService.BLEED : 0) // Visual thickness
|
|
369
|
+
.attr('data-fuel', (d: any) => d.fuel) // Data attribute for fuel identification
|
|
370
|
+
.attr('data-sector', (d: any) => d.box) // Data attribute for sector identification
|
|
371
|
+
.attr('data-incr', '0') // Animation increment tracking
|
|
372
|
+
.attr('stroke-linejoin', (d: any) => d.fuel !== 'waste' ? 'round' : '') // Rounded joins
|
|
373
|
+
|
|
374
|
+
// ================= INTERACTIVE TOOLTIP SYSTEM =================
|
|
375
|
+
|
|
376
|
+
// MOUSEOVER EVENT: Show detailed energy flow information
|
|
377
|
+
.on('mouseover', (event: any, d: any) => {
|
|
378
|
+
// Clear any existing tooltip styles for clean state
|
|
379
|
+
tooltip.attr('style', '');
|
|
380
|
+
|
|
381
|
+
// D3.JS TRANSITION: Smooth fade-in animation (200ms)
|
|
382
|
+
tooltip.transition()
|
|
383
|
+
.duration(200) // Fast appearance for responsive feel
|
|
384
|
+
.style('opacity', 0.9); // Nearly opaque for readability
|
|
385
|
+
|
|
386
|
+
// TOOLTIP CONTENT GENERATION: Format energy flow information
|
|
387
|
+
const fuelName = this.configService.getFuelDisplayName(d.fuel); // Human-readable fuel name
|
|
388
|
+
const sectorName = this.configService.getBoxDisplayName(d.box); // Human-readable sector name
|
|
389
|
+
const value = this.graphCalculationService.sigfig2(d.value); // Formatted energy value
|
|
390
|
+
|
|
391
|
+
// CROSS-BROWSER MOUSE POSITION: Handle D3 v7 event changes
|
|
392
|
+
// Get mouse position from the event parameter
|
|
393
|
+
const mouseX = event?.pageX || 0;
|
|
394
|
+
const mouseY = event?.pageY || 0;
|
|
395
|
+
|
|
396
|
+
// TOOLTIP POSITIONING & CONTENT: Set HTML content and position
|
|
397
|
+
tooltip.html(`${fuelName} → ${sectorName}<div class='fuel_value'>${value}</div>`)
|
|
398
|
+
.style('left', `${mouseX}px`) // Horizontal position follows cursor
|
|
399
|
+
.style('top', `${mouseY - 28}px`); // Vertical offset prevents cursor overlap
|
|
400
|
+
|
|
401
|
+
this.highlightFuel(svg, fuelName)
|
|
402
|
+
}) // Bind context for access to service methods
|
|
403
|
+
|
|
404
|
+
// MOUSEOUT EVENT: Hide tooltip with smooth transition
|
|
405
|
+
.on('mouseout', () => {
|
|
406
|
+
// D3.JS TRANSITION: Smooth fade-out animation (500ms)
|
|
407
|
+
tooltip.transition()
|
|
408
|
+
.duration(500) // Slower fade-out allows reading time
|
|
409
|
+
.style('opacity', 0); // Fade to transparent
|
|
410
|
+
|
|
411
|
+
this.resetHighlight(svg)
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// EVENT SYSTEM INTEGRATION: Notify other services of rendering completion
|
|
416
|
+
// Enables coordinated updates across the visualization system
|
|
417
|
+
this.eventBus.emit({
|
|
418
|
+
type: 'rendering.completed',
|
|
419
|
+
timestamp: Date.now(),
|
|
420
|
+
source: 'ChartRenderingService',
|
|
421
|
+
data: {
|
|
422
|
+
type: 'flows',
|
|
423
|
+
year: graphData.year,
|
|
424
|
+
flowCount: graphData.graph.length
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Clear chart content
|
|
431
|
+
*/
|
|
432
|
+
public clearChart(svg: D3SVGSelection): void {
|
|
433
|
+
svg.selectAll('*').remove();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public drawInitialChart(svg: D3SVGSelection, tooltip: D3DivSelection): boolean {
|
|
437
|
+
const firstYearIndex = 0; // First year index
|
|
438
|
+
|
|
439
|
+
const graphs = this.graphCalculationService.graphs;
|
|
440
|
+
|
|
441
|
+
// Add fuel layers initialization
|
|
442
|
+
// creates these groups and drawFlows() selects them
|
|
443
|
+
for (const fuel of this.configService.FUELS) {
|
|
444
|
+
svg.append('g').attr('class', `fuel ${fuel.fuel}`);
|
|
445
|
+
}
|
|
446
|
+
svg.append('g').attr('class', 'fuel waste');
|
|
447
|
+
|
|
448
|
+
// Draw title with header information
|
|
449
|
+
this.drawHeader();
|
|
450
|
+
|
|
451
|
+
this.drawFlows(svg, firstYearIndex, tooltip);
|
|
452
|
+
this.drawLeftLabels(svg, graphs[firstYearIndex].totals as YearTotals);
|
|
453
|
+
this.drawBoxes(svg, graphs[firstYearIndex].totals as YearTotals);
|
|
454
|
+
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Highlight specific fuel flows
|
|
460
|
+
*/
|
|
461
|
+
public highlightFuel(svg: D3SVGSelection, fuelName: string): void {
|
|
462
|
+
svg.selectAll('.flow')
|
|
463
|
+
.style('opacity', function (this: any) {
|
|
464
|
+
const fuel = d3.select(this).attr('data-fuel');
|
|
465
|
+
return fuel === fuelName ? 1.0 : 0.3;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Reset highlighting
|
|
471
|
+
*/
|
|
472
|
+
public resetHighlight(svg: D3SVGSelection): void {
|
|
473
|
+
svg.selectAll('.flow')
|
|
474
|
+
.style('opacity', function (this: any) {
|
|
475
|
+
const fuel = d3.select(this).attr('data-fuel');
|
|
476
|
+
return fuel === 'waste' ? 0.6 : 0.8;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
public parseLineData(stroke: GraphStroke): string {
|
|
481
|
+
const points: GraphPoint[] = [stroke.a, stroke.b, stroke.c, stroke.d];
|
|
482
|
+
return this.lineGenerator(points) || '';
|
|
483
|
+
}
|
|
484
|
+
}
|