energy-visualization-sankey 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +497 -0
  2. package/babel.config.cjs +28 -0
  3. package/coverage/clover.xml +6 -0
  4. package/coverage/coverage-final.json +1 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/favicon.png +0 -0
  8. package/coverage/lcov-report/index.html +101 -0
  9. package/coverage/lcov-report/prettify.css +1 -0
  10. package/coverage/lcov-report/prettify.js +2 -0
  11. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  12. package/coverage/lcov-report/sorter.js +210 -0
  13. package/coverage/lcov.info +0 -0
  14. package/demo-caching.js +68 -0
  15. package/dist/core/Sankey.d.ts +294 -0
  16. package/dist/core/Sankey.d.ts.map +1 -0
  17. package/dist/core/events/EventBus.d.ts +195 -0
  18. package/dist/core/events/EventBus.d.ts.map +1 -0
  19. package/dist/core/types/events.d.ts +42 -0
  20. package/dist/core/types/events.d.ts.map +1 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/sankey.esm.js +5212 -0
  24. package/dist/sankey.esm.js.map +1 -0
  25. package/dist/sankey.standalone.esm.js +9111 -0
  26. package/dist/sankey.standalone.esm.js.map +1 -0
  27. package/dist/sankey.standalone.min.js +2 -0
  28. package/dist/sankey.standalone.min.js.map +1 -0
  29. package/dist/sankey.standalone.umd.js +9119 -0
  30. package/dist/sankey.standalone.umd.js.map +1 -0
  31. package/dist/sankey.umd.js +5237 -0
  32. package/dist/sankey.umd.js.map +1 -0
  33. package/dist/sankey.umd.min.js +2 -0
  34. package/dist/sankey.umd.min.js.map +1 -0
  35. package/dist/services/AnimationService.d.ts +229 -0
  36. package/dist/services/AnimationService.d.ts.map +1 -0
  37. package/dist/services/ConfigurationService.d.ts +173 -0
  38. package/dist/services/ConfigurationService.d.ts.map +1 -0
  39. package/dist/services/InteractionService.d.ts +377 -0
  40. package/dist/services/InteractionService.d.ts.map +1 -0
  41. package/dist/services/RenderingService.d.ts +152 -0
  42. package/dist/services/RenderingService.d.ts.map +1 -0
  43. package/dist/services/calculation/GraphService.d.ts +111 -0
  44. package/dist/services/calculation/GraphService.d.ts.map +1 -0
  45. package/dist/services/calculation/SummaryService.d.ts +149 -0
  46. package/dist/services/calculation/SummaryService.d.ts.map +1 -0
  47. package/dist/services/data/DataService.d.ts +167 -0
  48. package/dist/services/data/DataService.d.ts.map +1 -0
  49. package/dist/services/data/DataValidationService.d.ts +48 -0
  50. package/dist/services/data/DataValidationService.d.ts.map +1 -0
  51. package/dist/types/index.d.ts +189 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/utils/Logger.d.ts +88 -0
  54. package/dist/utils/Logger.d.ts.map +1 -0
  55. package/jest.config.cjs +20 -0
  56. package/package.json +68 -0
  57. package/rollup.config.js +131 -0
  58. package/scripts/performance-validation-real.js +411 -0
  59. package/scripts/validate-optimization.sh +147 -0
  60. package/scripts/visual-validation-real-data.js +374 -0
  61. package/src/core/Sankey.ts +1039 -0
  62. package/src/core/events/EventBus.ts +488 -0
  63. package/src/core/types/events.ts +80 -0
  64. package/src/index.ts +35 -0
  65. package/src/services/AnimationService.ts +983 -0
  66. package/src/services/ConfigurationService.ts +497 -0
  67. package/src/services/InteractionService.ts +920 -0
  68. package/src/services/RenderingService.ts +484 -0
  69. package/src/services/calculation/GraphService.ts +616 -0
  70. package/src/services/calculation/SummaryService.ts +394 -0
  71. package/src/services/data/DataService.ts +380 -0
  72. package/src/services/data/DataValidationService.ts +155 -0
  73. package/src/styles/controls.css +184 -0
  74. package/src/styles/sankey.css +211 -0
  75. package/src/types/index.ts +220 -0
  76. package/src/utils/Logger.ts +105 -0
  77. package/tests/numerical-validation.test.js +575 -0
  78. package/tests/setup.js +53 -0
  79. package/tsconfig.json +54 -0
@@ -0,0 +1,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
+ }