aiphoria 0.0.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

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 (42) hide show
  1. aiphoria/__init__.py +59 -0
  2. aiphoria/core/__init__.py +55 -0
  3. aiphoria/core/builder.py +305 -0
  4. aiphoria/core/datachecker.py +1808 -0
  5. aiphoria/core/dataprovider.py +806 -0
  6. aiphoria/core/datastructures.py +1686 -0
  7. aiphoria/core/datavisualizer.py +431 -0
  8. aiphoria/core/datavisualizer_data/LICENSE +21 -0
  9. aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
  10. aiphoria/core/datavisualizer_data/pako.min.js +2 -0
  11. aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
  12. aiphoria/core/flowmodifiersolver.py +1754 -0
  13. aiphoria/core/flowsolver.py +1472 -0
  14. aiphoria/core/logger.py +113 -0
  15. aiphoria/core/network_graph.py +136 -0
  16. aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
  17. aiphoria/core/network_graph_data/echarts_min.js +45 -0
  18. aiphoria/core/network_graph_data/network_graph.html +76 -0
  19. aiphoria/core/network_graph_data/network_graph.js +1391 -0
  20. aiphoria/core/parameters.py +269 -0
  21. aiphoria/core/types.py +20 -0
  22. aiphoria/core/utils.py +362 -0
  23. aiphoria/core/visualizer_parameters.py +7 -0
  24. aiphoria/data/example_scenario.xlsx +0 -0
  25. aiphoria/example.py +66 -0
  26. aiphoria/lib/docs/dynamic_stock.py +124 -0
  27. aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
  28. aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
  29. aiphoria/lib/odym/modules/__init__.py +1 -0
  30. aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
  31. aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
  32. aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
  33. aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
  34. aiphoria/lib/odym/modules/test/__init__.py +2 -0
  35. aiphoria/runner.py +678 -0
  36. aiphoria-0.8.0.dist-info/METADATA +119 -0
  37. aiphoria-0.8.0.dist-info/RECORD +40 -0
  38. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
  39. aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
  40. aiphoria-0.0.1.dist-info/METADATA +0 -5
  41. aiphoria-0.0.1.dist-info/RECORD +0 -5
  42. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1391 @@
1
+ const chart = echarts.init(document.getElementById("main"));
2
+
3
+ // ***************
4
+ // * Global data *
5
+ // ***************
6
+
7
+ // Global variables
8
+ const globals = {
9
+ // Scenario data, contains baseline unit name and value
10
+ scenarioData: {scenario_data},
11
+
12
+ // Original data, this will get replaced with JSON object from Python
13
+ originalYearToData: {year_to_data},
14
+
15
+ // Updated year to data: contains various mappings e.g. node ID to position
16
+ yearToData: new Map(),
17
+
18
+ // Data mappings
19
+ yearToProcessIdToProcess: new Map(),
20
+ yearToProcessIdToFlowIds: new Map(),
21
+ yearToFlowIdToFlow: new Map(),
22
+
23
+ // Edge colors
24
+ edgeColors: {
25
+ absolute: "rgba(59, 162, 114, 1)",
26
+ relative: "rgba(255, 50, 50, 1)",
27
+ },
28
+
29
+ virtualNodeColor: "rgba(100, 100, 100, 0.8)",
30
+ virtualFlowColor: "rgba(100, 100, 100, 0.8)",
31
+
32
+ // Transformation stage name to color mapping, build in initialize()
33
+ transformationStageNameToColor: new Map(),
34
+
35
+ // Scenario name
36
+ scenarioName: "Scenario",
37
+
38
+ // List of all years
39
+ years: [],
40
+
41
+ // Current timeline year
42
+ currentYear: 0,
43
+ currentYearIndex: 0,
44
+
45
+ // Current in-use year data
46
+ graphData: {
47
+ data: [],
48
+ links: [],
49
+ categories: [],
50
+ legendData: [],
51
+ },
52
+
53
+ selectedNodeIndex: null,
54
+
55
+ // All data for timeline years
56
+ initialOption: {},
57
+
58
+ // ***********************
59
+ // * Changeable settings *
60
+ // ***********************
61
+ // If true, use process_id as label, otherwise use process_label
62
+ useProcessIdAsLabel: true,
63
+
64
+ // If true, use flow type (ABS/%) as flow label, otherwise use flow value
65
+ // useFlowTypeAsLabel: true,
66
+ useFlowTypeAsLabel: false,
67
+
68
+ // If true, color processes by their transformation stage
69
+ // If transformation stage color mapping is not found then use default color palette
70
+ useTransformationStageColors: true,
71
+
72
+ // If true, hide processes that have no inflows and outflows
73
+ hideUnconnectedProcesses: false,
74
+
75
+ // Freeze node positions, disabled by default to allow force layout
76
+ // to find node positions
77
+ freezeNodePositions: false,
78
+ };
79
+
80
+ // **************
81
+ // * Formatters *
82
+ // **************
83
+
84
+ function formatValue(val, options = { numDecimals: 3}) {
85
+ // Format value to fixed number of digits (defaults to 3)
86
+ return parseFloat(val.toFixed(options.numDecimals))
87
+ }
88
+
89
+ function isSameValue(a, b, eps = 0.001) {
90
+ // Check if value is same with maximum allowed difference (= epsilon)
91
+ return Math.abs(a - b) < eps
92
+ }
93
+
94
+ function getTooltipFormatter(params) {
95
+ const isLegend = params.componentType == "legend"
96
+ const isNode = params.dataType == "node"
97
+ const isLink = params.dataType == "edge"
98
+
99
+ let result = ""
100
+ result += `
101
+ <style>
102
+ .tooltip-wrapper {
103
+ min-width: 300px;
104
+ min-height: 100px;
105
+ }
106
+
107
+ .tooltip-title {
108
+ font-size: 16px;
109
+ font-weight: bold;
110
+ }
111
+
112
+ .tooltip-type-title {
113
+ font-size: 12px;
114
+ }
115
+
116
+ .tooltip-body-wrapper {
117
+ display: flex;
118
+ flex-direction: row;
119
+ margin-right: 1rem;
120
+ column-gap: 10px;
121
+ }
122
+
123
+ .tooltip-col {
124
+ font-size: 14px;
125
+ font-weight: normal;
126
+ /*flex-grow: 1;*/
127
+ /*height: 100%;*/
128
+ }
129
+
130
+ .tooltip-table {
131
+ --border: solid 1px #ccc;
132
+ --font-size-header: 12px;
133
+ --font-size-data: 12px;
134
+ border: var(--border);
135
+ border-collapse: collapse;
136
+ }
137
+
138
+ .tooltip-table-title {
139
+ font-size: 14px;
140
+ font-weight: bold;
141
+ }
142
+
143
+ /* Table headings */
144
+ .tooltip-table > thead > tr > th {
145
+ font-size: var(--font-size-header);
146
+ font-weight: bold;
147
+ text-align: left;
148
+ border: var(--border);
149
+ width: 100%;
150
+ padding: 0 4px 0 4px;
151
+ background: #eee;
152
+ }
153
+
154
+ /* Table rows */
155
+ .tooltip-table > tbody > tr > td {
156
+ font-size: var(--font-size-data);
157
+ text-align: left;
158
+ border: var(--border);
159
+ padding: 0 4px 0 4px;
160
+ }
161
+ </style>
162
+ `
163
+
164
+ // TODO: Check with || isLegend and show tooltip when hovering over legend items
165
+ if (isNode) {
166
+ // let nodeData = null
167
+ // let nodeId = null
168
+ // if(isNode) {
169
+ // nodeData = params.data
170
+ // nodeId = nodeData.id
171
+ // }
172
+ // if(isLegend) {
173
+ // nodeId = params.name
174
+ // }
175
+
176
+ const nodeData = params.data
177
+ const nodeId = nodeData.id;
178
+
179
+ // Get flow IDs
180
+ const year = globals.currentYear;
181
+ const processId = nodeId
182
+ const process = globals.yearToProcessIdToProcess.get(year).get(processId)
183
+ const processIdToFlowIds =
184
+ globals.yearToProcessIdToFlowIds.get(year);
185
+ const flowIdToFlow = globals.yearToFlowIdToFlow.get(year)
186
+ const inflowIds = processIdToFlowIds.get(nodeId).in;
187
+ const outflowIds = processIdToFlowIds.get(nodeId).out;
188
+
189
+ // Build stock info
190
+ let stockInfoHTML = ""
191
+ const hasStock = nodeData.isStock
192
+ if (hasStock) {
193
+ let paramsHTML = ""
194
+ const stockDistributionParams = nodeData.stockDistributionParams
195
+ let hasStockParams = !(stockDistributionParams == undefined || stockDistributionParams == null)
196
+
197
+ // Unpack stock distribution params
198
+ if (hasStockParams && typeof stockDistributionParams == "object") {
199
+ const params = []
200
+ for (const [k, v] of Object.entries(stockDistributionParams)) {
201
+ params.push(`${k}=${v}`)
202
+ }
203
+ paramsHTML = params.join(", ")
204
+ } else {
205
+ paramsHTML = stockDistributionParams
206
+ }
207
+
208
+ stockInfoHTML = `
209
+ <table class="tooltip-table">
210
+ <span class="tooltip-table-title">Stock</span>
211
+ <thead>
212
+ <tr>
213
+ <th>Type</th>
214
+ <th>Lifetime</th>
215
+ ${hasStockParams ? "<th>Parameters</th>" : ""}
216
+ </tr>
217
+ </thead>
218
+ <tbody>
219
+ <tr>
220
+ <td>${nodeData.stockDistributionType}</td>
221
+ <td>${nodeData.stockLifetime}</td>
222
+ ${hasStockParams ? `<td>${paramsHTML}</td>` : ""}
223
+ </tr>
224
+ </tbody>
225
+ </table>
226
+ <br/>
227
+ `
228
+ }
229
+
230
+ // Build list of inflow IDs, outflow IDs, total inflows and total outflows
231
+ let totalInflowsBaseline = 0.0
232
+ let totalInflowsIndicators = new Map()
233
+ const inflows = [];
234
+ for (const flowId of inflowIds) {
235
+ const flow = flowIdToFlow.get(flowId)
236
+ totalInflowsBaseline += flow.evaluated_value
237
+ for (const [k, v] of Object.entries(flow.indicators)) {
238
+ if (!totalInflowsIndicators.has(k)) {
239
+ totalInflowsIndicators.set(k, 0.0)
240
+ }
241
+ const prevTotal = totalInflowsIndicators.get(k)
242
+ const newTotal = prevTotal + v
243
+ totalInflowsIndicators.set(k, newTotal)
244
+ }
245
+ inflows.push(flow);
246
+ }
247
+
248
+ let totalOutflowsBaseline = 0.0
249
+ let totalOutflowsIndicators = new Map()
250
+ const outflows = [];
251
+ for (const flowId of outflowIds) {
252
+ const flow = flowIdToFlow.get(flowId)
253
+ totalOutflowsBaseline += flow.evaluated_value
254
+ for (const [k, v] of Object.entries(flow.indicators)) {
255
+ if (!totalOutflowsIndicators.has(k)) {
256
+ totalOutflowsIndicators.set(k, 0.0)
257
+ }
258
+ const prevTotal = totalOutflowsIndicators.get(k)
259
+ const newTotal = prevTotal + v
260
+ totalOutflowsIndicators.set(k, newTotal)
261
+ }
262
+ outflows.push(flow);
263
+ }
264
+
265
+ // Get indicator names from either inflows or from outflows
266
+ const indicatorNames = [];
267
+ const hasInflows = inflows.length > 0;
268
+ const hasOutflows = outflows.length > 0;
269
+ if (hasInflows && !indicatorNames.length) {
270
+ for (const key of Object.keys(inflows[0].indicators)) {
271
+ indicatorNames.push(key);
272
+ }
273
+ }
274
+ if (hasOutflows && !indicatorNames.length) {
275
+ for (const key of Object.keys(outflows[0].indicators)) {
276
+ indicatorNames.push(key);
277
+ }
278
+ }
279
+
280
+ // Make headers for inflows and outflows columns
281
+ const baseline = globals.scenarioData.baseline_value_name
282
+ const inflowHeaders = ["Source", baseline, ...indicatorNames.map(elem => elem)]
283
+ const outflowHeaders = ["Target", baseline, ...indicatorNames.map(elem => elem)]
284
+
285
+ // Build inflow headers and inflow items
286
+ let inflowHeadersHTML = ""
287
+ inflowHeadersHTML += "<tr>"
288
+ for (const elem of inflowHeaders) {
289
+ inflowHeadersHTML += `<th>${elem}</th>`
290
+ }
291
+ inflowHeadersHTML += "</tr>"
292
+
293
+ // Build inflow items as HTML
294
+ let inflowItemsHTML = ""
295
+ for (const flow of inflows) {
296
+ inflowItemsHTML += "<tr>"
297
+ inflowItemsHTML += `<td>${flow.source_process_id}</td>`
298
+ inflowItemsHTML += `<td>${formatValue(flow.evaluated_value)}</td>`
299
+ for (const name of indicatorNames) {
300
+ inflowItemsHTML += `<td>${formatValue(flow.indicators[name])}</td>`
301
+ }
302
+ inflowItemsHTML += "</tr>"
303
+ }
304
+ if (hasInflows) {
305
+ // Add total row
306
+ inflowItemsHTML += "<tr>"
307
+ inflowItemsHTML += "<td><span style='font-weight: bold'>Total</span></td>"
308
+ inflowItemsHTML += `<td><span style='font-weight: bold'>${formatValue(totalInflowsBaseline)}</span></td>`
309
+ for (const [k, v] of totalInflowsIndicators.entries()) {
310
+ inflowItemsHTML += `<td><span style='font-weight: bold'>${formatValue(v)}</span></td>`
311
+ }
312
+ inflowItemsHTML += "</tr>"
313
+ } else {
314
+ inflowHeadersHTML = "<tr>No inflows</tr>"
315
+ }
316
+
317
+ // Build outflow headers and inflow items
318
+ let outflowHeadersHTML = ""
319
+ outflowHeadersHTML += "<tr>"
320
+ for (const elem of outflowHeaders) {
321
+ outflowHeadersHTML += `<th>${elem}</th>`
322
+ }
323
+ outflowHeadersHTML += "</tr>"
324
+
325
+ // Build inflow items as HTML
326
+ let outflowItemsHTML = ""
327
+ for (const flow of outflows) {
328
+ outflowItemsHTML += "<tr>"
329
+ outflowItemsHTML += `<td>${flow.target_process_id}</td>`
330
+ outflowItemsHTML += `<td>${formatValue(flow.evaluated_value)}</td>`
331
+ for (const name of indicatorNames) {
332
+ outflowItemsHTML += `<td>${formatValue(flow.indicators[name])}</td>`
333
+ }
334
+ outflowItemsHTML += "</tr>"
335
+ }
336
+ if (hasOutflows) {
337
+ // Add total row
338
+ outflowItemsHTML += "<tr>"
339
+ outflowItemsHTML += "<td><span style='font-weight: bold'>Total</span></td>"
340
+ outflowItemsHTML += `<td><span style='font-weight: bold'>${formatValue(totalOutflowsBaseline)}</span></td>`
341
+ for (const [k, v] of totalOutflowsIndicators.entries()) {
342
+ outflowItemsHTML += `<td><span style='font-weight: bold'>${formatValue(v)}</span></td>`
343
+ }
344
+ outflowItemsHTML += "</tr>"
345
+ } else {
346
+ outflowHeadersHTML = "<tr>No outflows</tr>"
347
+ }
348
+
349
+ // Determine type for process
350
+ let nodeType = "Process"
351
+ if (hasStock) {
352
+ nodeType = "Stock"
353
+ }
354
+ if (nodeData.isVirtual) {
355
+ nodeType = "Virtual process"
356
+ }
357
+
358
+ // Node tooltip result
359
+ result += `
360
+ <div class="tooltip-wrapper">
361
+ <div class="tooltip-title">${nodeData.name}</div>
362
+ <span class="tooltip-type-title">Type: ${nodeType}</span><br/>
363
+ <span class="tooltip-type-title">ID: ${nodeId}</span><br/>
364
+ <br/>
365
+ <div class="tooltip-body-wrapper">
366
+ <div class="tooltip-col">
367
+ ${hasStock ? stockInfoHTML : ""}
368
+ </div>
369
+ </div>
370
+ <div class="tooltip-body-wrapper">
371
+ <div class="tooltip-col">
372
+ <table class="tooltip-table">
373
+ <span class="tooltip-table-title">Inflows</span><br/>
374
+ <thead>
375
+ ${inflowHeadersHTML}
376
+ </thead>
377
+ <tbody>
378
+ ${inflowItemsHTML}
379
+ </tbody>
380
+ </table>
381
+ </div>
382
+ <br/>
383
+ <div class="tooltip-col">
384
+ <span class="tooltip-table-title">Outflows</span><br/>
385
+ <table class="tooltip-table">
386
+ <thead>
387
+ ${outflowHeadersHTML}
388
+ </thead>
389
+ <tbody>
390
+ ${outflowItemsHTML}
391
+ </tbody>
392
+ </table>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ `;
397
+
398
+ return result;
399
+ }
400
+
401
+ if (isLink) {
402
+ const linkData = params.data
403
+ const linkId = params.data.id
404
+ const year = globals.currentYear;
405
+ const flow = globals.yearToFlowIdToFlow.get(year).get(linkId)
406
+
407
+ // Flow indicators
408
+ // Show flow indicators that have value more than 0.0
409
+ const epsilon = 0.001
410
+ const indicatorNameToValue = new Map()
411
+ for(const [k, v] of Object.entries(flow.indicators)) {
412
+ console.log(k, v)
413
+ if(!isSameValue(v, 0.0, epsilon)) {
414
+ indicatorNameToValue.set(k, v)
415
+ }
416
+ }
417
+
418
+ const baseline = globals.scenarioData.baseline_value_name
419
+ const headers = ["Source", "Target", baseline, ...indicatorNameToValue.keys().map(elem => elem)]
420
+
421
+ // Build headers
422
+ let headersHTML = ""
423
+ headersHTML += "<tr>"
424
+ for(const entry of headers) {
425
+ headersHTML += `<th>${entry}</th>`
426
+ }
427
+ headersHTML += "</tr>"
428
+
429
+ // Build data rows
430
+ let bodyHTML = ""
431
+ bodyHTML += `<td>${flow.source_process_id}</td>`
432
+ bodyHTML += `<td>${flow.target_process_id}</td>`
433
+ bodyHTML += `<td>${formatValue(flow.evaluated_value)}</td>`
434
+ for(const [k, v] of indicatorNameToValue.entries()) {
435
+ bodyHTML += `<td>${formatValue(v)}</td>`
436
+ }
437
+
438
+ // Determine the type of the link
439
+ let linkType = "Flow"
440
+ if (linkData.isVirtual) {
441
+ linkType = "Virtual flow"
442
+ }
443
+
444
+ // Flow tooltip result
445
+ result += `
446
+ <div class="tooltip-wrapper">
447
+ <div class="tooltip-title">${linkData.name}</div>
448
+ <span class="tooltip-type-title">Type: ${linkType}</span><br/>
449
+ <span class="tooltip-type-title">ID: ${linkId}</span><br/>
450
+ <br/>
451
+ <div class="tooltip-body-wrapper">
452
+ <div class="tooltip-col">
453
+ <table class="tooltip-table">
454
+ <span class="tooltip-table-title">Flow</span><br/>
455
+ <thead>
456
+ ${headersHTML}
457
+ </thead>
458
+ <tbody>
459
+ ${bodyHTML}
460
+ </tbody>
461
+ </table>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ `;
466
+ return result
467
+ }
468
+ }
469
+
470
+ // *******************
471
+ // * Event listeners *
472
+ // *******************
473
+
474
+ // Resize chart when window size has changed
475
+ addEventListener("resize", (e) => {
476
+ chart.resize();
477
+ });
478
+
479
+ addEventListener("keydown", (e) => {
480
+ // Change year using keyboard
481
+ // Arrow left = previous year
482
+ // Arrow right = next year
483
+ // Home = First year
484
+ // End = Last year
485
+ let nextYearIndex = -1
486
+ if(e.code == "ArrowLeft") {
487
+ const currentYearIndex = globals.currentYearIndex
488
+ nextYearIndex = currentYearIndex - 1
489
+ }
490
+ if(e.code == "ArrowRight") {
491
+ const currentYearIndex = globals.currentYearIndex
492
+ nextYearIndex = currentYearIndex + 1
493
+ }
494
+ if(e.code == "Home") {
495
+ nextYearIndex = 0
496
+ }
497
+ if(e.code == "End") {
498
+ nextYearIndex = globals.years.length - 1
499
+ }
500
+
501
+ if(nextYearIndex < 0 || nextYearIndex >= globals.years.length) {
502
+ return
503
+ }
504
+
505
+ chart.dispatchAction({
506
+ type: "timelineChange",
507
+ currentIndex: nextYearIndex,
508
+ })
509
+ })
510
+
511
+ // Listen when user changes process label type
512
+ document
513
+ .getElementById("processLabelType")
514
+ .addEventListener("change", (event) => {
515
+ const value = event.target.options[event.target.selectedIndex].value;
516
+ switch (value) {
517
+ case "id":
518
+ globals.useProcessIdAsLabel = true;
519
+ break;
520
+
521
+ case "label":
522
+ globals.useProcessIdAsLabel = false;
523
+ break;
524
+ }
525
+
526
+ // Toggle between node ID and node label for all years
527
+ for (const year of globals.years) {
528
+ const yearData = getYearData(year);
529
+ for (const [nodeIndex, nodeData] of yearData.data.entries()) {
530
+ if (globals.useProcessIdAsLabel) {
531
+ nodeData.name = nodeData.id
532
+ } else {
533
+ if (nodeData.label) {
534
+ nodeData.name = nodeData.label
535
+ } else {
536
+ nodeData.name = `Missing label (${nodeData.id})`;
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+ chart.setOption(globals.initialOption);
543
+ });
544
+
545
+ document
546
+ .getElementById("flowLabelType")
547
+ .addEventListener("change", (event) => {
548
+ const value = event.target.options[event.target.selectedIndex].value;
549
+ switch (value) {
550
+ case "type":
551
+ globals.useFlowTypeAsLabel = true
552
+ break;
553
+
554
+ case "value":
555
+ globals.useFlowTypeAsLabel = false
556
+ break;
557
+ }
558
+
559
+ chart.setOption(globals.initialOption);
560
+ });
561
+
562
+ document
563
+ .getElementById("useTransformationStageColors")
564
+ .addEventListener("change", (event) => {
565
+ const value = event.target.options[event.target.selectedIndex].value;
566
+ switch (value) {
567
+ case "yes":
568
+ globals.useTransformationStageColors = true
569
+ break;
570
+
571
+ case "no":
572
+ globals.useTransformationStageColors = false
573
+ break;
574
+ }
575
+
576
+ update({resetView: false})
577
+ });
578
+
579
+ document
580
+ .getElementById("hideUnconnectedProcesses")
581
+ .addEventListener("change", (event) => {
582
+ const value = event.target.options[event.target.selectedIndex].value;
583
+ switch (value) {
584
+ case "yes":
585
+ globals.hideUnconnectedProcesses = true;
586
+ break;
587
+ case "no":
588
+ globals.hideUnconnectedProcesses = false;
589
+ break;
590
+ }
591
+
592
+ globals.initialOption.options = [];
593
+ for (const year of globals.years) {
594
+ const graphData = buildGraphDataForYear(year, {});
595
+ const newSeries = {
596
+ series: [
597
+ {
598
+ data: graphData.data,
599
+ links: graphData.links,
600
+ categories: graphData.categories,
601
+ },
602
+ ],
603
+ };
604
+ globals.initialOption.options.push(newSeries);
605
+ }
606
+
607
+ const currentYearData = buildGraphDataForYear(globals.currentYear);
608
+ globals.initialOption.baseOption.legend[0].data =
609
+ currentYearData.legendData;
610
+ globals.initialOption.baseOption.timeline.data = globals.years;
611
+ chart.setOption(globals.initialOption);
612
+
613
+ // // Same as update
614
+ // const graphData = buildGraphDataForYear(globals.currentYear)
615
+ // const option = buildOption(graphData, { resetView: false })
616
+ });
617
+
618
+ document.getElementById("resetView").addEventListener("click", (event) => {
619
+ // Set default unfreezed state for nodes
620
+ setFreezeNodePositionButtonState("Freeze", false);
621
+ update({resetView: true});
622
+ });
623
+
624
+ document
625
+ .getElementById("freezeNodePositions")
626
+ .addEventListener("click", (event) => {
627
+ const nextState = !globals.freezeNodePositions;
628
+ if (nextState) {
629
+ setFreezeNodePositionButtonState("Unfreeze", nextState);
630
+ freezeNodePositions();
631
+ } else {
632
+ setFreezeNodePositionButtonState("Freeze", nextState);
633
+ unfreezeNodePositions();
634
+ }
635
+ });
636
+
637
+ // **************************
638
+ // * ECharts event handlers *
639
+ // **************************
640
+
641
+ chart.on("timelinechanged", function (params) {
642
+ const targetYear = globals.years[params.currentIndex];
643
+ changeCurrentYear(targetYear);
644
+ });
645
+
646
+ chart.on("mousedown", {dataType: "node"}, (params) => {
647
+ globals.selectedNodeIndex = params.dataIndex;
648
+ });
649
+
650
+ chart.on("mousemove", {dataType: "node"}, (params) => {
651
+ if (!globals.selectedNodeIndex) {
652
+ return;
653
+ }
654
+
655
+ const yearData = getYearData(globals.currentYear);
656
+ const nodeId = yearData.data[globals.selectedNodeIndex].id;
657
+ const nodePosition = calculateNodePosition(nodeId);
658
+ setNodePosition(globals.currentYear, nodeId, nodePosition);
659
+ });
660
+
661
+ chart.on("mouseup", {dataType: "node"}, (params) => {
662
+ globals.selectedNodeIndex = null;
663
+ });
664
+
665
+ // *************
666
+ // * Functions *
667
+ // *************
668
+
669
+ function getYearIndex(year) {
670
+ return parseInt(year - globals.years[0]);
671
+ }
672
+
673
+ function getYearData(year) {
674
+ const yearIndex = getYearIndex(year);
675
+ return globals.initialOption.options[yearIndex].series[0];
676
+ }
677
+
678
+ function setFreezeNodePositionButtonState(title, state) {
679
+ const label = document.getElementById("freezeNodePositionsButtonLabel");
680
+ globals.freezeNodePositions = state;
681
+ label.innerHTML = title;
682
+ }
683
+
684
+ function getGraphNodeFromNodeData(year, nodeIndex) {
685
+ // Unpack Process data as Node data
686
+ const yearData = globals.yearToData.get(year);
687
+ const nodeIndexToData = yearData.get("nodeIndexToData");
688
+ const nodeIdToPosition = yearData.get("nodeIdToPosition");
689
+ const nodeData = nodeIndexToData.get(nodeIndex);
690
+ const processId = nodeData.process_id;
691
+ const newGraphNode = {
692
+ id: processId,
693
+ name: processId,
694
+ label: nodeData.process_label,
695
+ category: processId,
696
+ numInflows: parseInt(nodeData.num_inflows),
697
+ numOutflows: parseInt(nodeData.num_outflows),
698
+ value: 0,
699
+ text: `Process ${processId}`,
700
+
701
+ transformationStage: nodeData.transformation_stage,
702
+ isStock: nodeData.is_stock,
703
+ stockLifetime: nodeData.stock_lifetime,
704
+ stockDistributionType: nodeData.stock_distribution_type,
705
+ stockDistributionParams: nodeData.stock_distribution_params,
706
+
707
+ // Colors, injected at initialize
708
+ colorNormal: nodeData.color_normal,
709
+ colorTransformationStage: nodeData.color_transformation_stage,
710
+
711
+ // Default color, created at startup
712
+ isVirtual: nodeData.is_virtual,
713
+
714
+ // ECharts related
715
+ itemStyle: {}
716
+ };
717
+
718
+ // Make virtual nodes dark grey
719
+ if (newGraphNode.isVirtual) {
720
+ newGraphNode.itemStyle = {
721
+ color: globals.virtualNodeColor,
722
+ };
723
+ }
724
+
725
+ // Make border for nodes that has stock
726
+ if (newGraphNode.isStock) {
727
+ newGraphNode.itemStyle.borderColor = "#333"
728
+ newGraphNode.itemStyle.borderWidth = 3
729
+ }
730
+
731
+ if (nodeIdToPosition.has(processId)) {
732
+ const nodePosition = nodeIdToPosition.get(processId);
733
+ newGraphNode.x = nodePosition.x;
734
+ newGraphNode.y = nodePosition.y;
735
+ }
736
+
737
+ const hasColorNormal = newGraphNode.colorNormal !== undefined
738
+ const hasColorTransformationStage = newGraphNode.colorTransformationStage !== undefined
739
+ if(!newGraphNode.isVirtual) {
740
+ if(globals.useTransformationStageColors) {
741
+ if(hasColorTransformationStage) {
742
+ newGraphNode.itemStyle.color = newGraphNode.colorTransformationStage
743
+ }
744
+ } else {
745
+ if(hasColorNormal) {
746
+ newGraphNode.itemStyle.color = newGraphNode.colorNormal
747
+ }
748
+ }
749
+ }
750
+
751
+ return newGraphNode;
752
+ }
753
+
754
+ function getGraphEdgeFromEdgeData(edgeIndex, year) {
755
+ const yearData = globals.yearToData.get(year);
756
+ const edgeIndexToData = yearData.get("edgeIndexToData");
757
+ const edgeData = edgeIndexToData.get(edgeIndex);
758
+
759
+ const flowId = edgeData.flow_id;
760
+ const sourceProcessId = edgeData.source_process_id;
761
+ const targetProcessId = edgeData.target_process_id;
762
+ const isUnitAbsoluteValue = edgeData.is_unit_absolute_value;
763
+ const value = edgeData.value;
764
+ const unit = edgeData.unit;
765
+
766
+ const newGraphEdge = {
767
+ id: flowId,
768
+ name: flowId,
769
+ source: sourceProcessId,
770
+ target: targetProcessId,
771
+ label: {
772
+ id: edgeData.flow_id,
773
+ show: true,
774
+ position: "middle",
775
+ formatter: (params) => {
776
+ // Format text for links
777
+ if (globals.useFlowTypeAsLabel) {
778
+ return isUnitAbsoluteValue ? "ABS" : "%";
779
+ } else {
780
+ return formatValue(edgeData.evaluated_value)
781
+ }
782
+ },
783
+ },
784
+ lineStyle: {
785
+ color: isUnitAbsoluteValue
786
+ ? globals.edgeColors.absolute
787
+ : globals.edgeColors.relative,
788
+ // width can be used to change line width
789
+ },
790
+
791
+ // Custom data
792
+ text: isUnitAbsoluteValue ? "Absolute flow" : "Relative flow",
793
+ value: `${value} ${unit}`,
794
+
795
+ isVirtual: edgeData.is_virtual,
796
+ };
797
+
798
+ if (newGraphEdge.isVirtual) {
799
+ newGraphEdge.lineStyle.color = globals.virtualFlowColor;
800
+ }
801
+
802
+ return newGraphEdge;
803
+ }
804
+
805
+ function buildGraphDataForYear(year, updateOptions = {}) {
806
+ const graphData = {
807
+ data: [],
808
+ links: [],
809
+ categories: [],
810
+ legendData: [],
811
+ };
812
+
813
+ const yearData = globals.yearToData.get(year);
814
+
815
+ const nodeIndexToData = yearData.get("nodeIndexToData");
816
+ for (const [nodeIndex, nodeData] of nodeIndexToData.entries()) {
817
+ const graphNode = getGraphNodeFromNodeData(year, nodeIndex);
818
+ if (globals.hideUnconnectedProcesses) {
819
+ const hasNoInflows = graphNode.numInflows === 0;
820
+ const hasNoOutflows = graphNode.numOutflows === 0;
821
+ if (hasNoInflows && hasNoOutflows) {
822
+ continue;
823
+ }
824
+ }
825
+
826
+ if (globals.useProcessIdAsLabel) {
827
+ graphNode.name = graphNode.id;
828
+ } else {
829
+ if (graphNode.label) {
830
+ graphNode.name = graphNode.label;
831
+ } else {
832
+ graphNode.name = `Missing label (${graphNode.id})`;
833
+ }
834
+ }
835
+
836
+ graphData.data.push(graphNode);
837
+ }
838
+
839
+ // Sort graphs in alphabetical order so legend is also in alphabetical order
840
+ graphData.data.sort((a, b) => a.name > b.name ? 1 : -1);
841
+
842
+ const edgeIndexToData = yearData.get("edgeIndexToData");
843
+ for (const [edgeIndex, edgeData] of edgeIndexToData.entries()) {
844
+ const graphEdge = getGraphEdgeFromEdgeData(edgeIndex, year);
845
+ graphData.links.push(graphEdge);
846
+ }
847
+
848
+ // Color nodes by categories
849
+ for (const node of graphData.data) {
850
+ const newCategory = {
851
+ name: node.id,
852
+ itemStyle: {}
853
+ };
854
+
855
+ const hasColorNormal = node.colorNormal !== undefined
856
+ const hasColorTransformationStage = node.colorTransformationStage !== undefined
857
+ if(!node.isVirtual) {
858
+ if(globals.useTransformationStageColors) {
859
+ if(hasColorTransformationStage) {
860
+ newCategory.itemStyle.color = node.colorTransformationStage
861
+ }
862
+ } else {
863
+ if(hasColorNormal) {
864
+ newCategory.itemStyle.color = node.colorNormal
865
+ }
866
+ }
867
+ }
868
+
869
+ graphData.categories.push(newCategory);
870
+ }
871
+
872
+ // Create legend from all visible nodes
873
+ const legendData = []
874
+ for (const node of graphData.data) {
875
+ const newEntry = {
876
+ name: node.id,
877
+ itemStyle: node.itemStyle,
878
+ tooltip: {
879
+ show: true,
880
+ formatter: getTooltipFormatter,
881
+ }
882
+ }
883
+ legendData.push(newEntry)
884
+ }
885
+
886
+ // Build legend data - ECharts uses automatically node ID with this
887
+ graphData.legendData = legendData;
888
+ return graphData;
889
+ }
890
+
891
+ function buildOption(graphData, updateOptions = {resetView: false}) {
892
+ const center = ["50%", "50%"];
893
+ if (!updateOptions.resetView) {
894
+ // Use previous center
895
+ const prevOption = chart.getOption();
896
+ const prevCenter = prevOption.series[0].center;
897
+ center[0] = prevCenter[0];
898
+ center[1] = prevCenter[1];
899
+ }
900
+
901
+ let layout = globals.freezeNodePositions ? "none" : "force";
902
+ if (updateOptions.resetView) {
903
+ layout = "force";
904
+ }
905
+ const option = {
906
+ options: [{
907
+ series: [{
908
+ name: "Process flows test",
909
+ // type: "graph",
910
+ // layout: layout,
911
+ data: graphData.data,
912
+ links: graphData.links,
913
+ categories: graphData.categories,
914
+ // center: center,
915
+ // zoom: zoom: 2,
916
+ // draggable: true,
917
+ // symbolSize: 40,
918
+ // symbol: "circle", // 'rect'
919
+ // label: {
920
+ // show: true, // node name to be shown in circle
921
+ // },
922
+ // edgeSymbol: ["circle", "arrow"], // for arrow from one to another
923
+ // edgeSymbolSize: [0, 15],
924
+ // emphasis: {
925
+ // focus: "adjacency",
926
+ // label: {
927
+ // show: true,
928
+ // },
929
+ // // disabled: true,
930
+ // },
931
+ // roam: true,
932
+ // force: {
933
+ // repulsion: [500, 1000, 2000],
934
+ // edgeLength: 50,
935
+ // },
936
+ // },
937
+ }]
938
+ }]
939
+ }
940
+
941
+ // // graphData is year-specific data
942
+ // const option = {
943
+ // baseOption: {
944
+ // title: {
945
+ // text: "Process connection graph",
946
+ // subtext: `Year ${globals.currentYear}`,
947
+ // },
948
+ // tooltip: {
949
+ // formatter: getTooltipFormatter,
950
+ // },
951
+ // legend: [
952
+ // {
953
+ // type: "scroll",
954
+ // data: graphData.legendData,
955
+ // position: "left",
956
+ // orient: "vertical",
957
+ // right: 10,
958
+ // top: 50,
959
+ // height: "88%",
960
+ // },
961
+ // ],
962
+ // timeline: {
963
+ // show: true,
964
+ // type: "slider",
965
+ // axisType: "category",
966
+ // data: globals.years,
967
+ // left: "20px",
968
+ // right: "20px",
969
+ // },
970
+ // series: [
971
+ // {
972
+ // name: "Process flows",
973
+ // type: "graph",
974
+ // layout: layout,
975
+ // data: graphData.data,
976
+ // links: graphData.links,
977
+ // categories: graphData.categories,
978
+ // center: center,
979
+ // zoom: 2,
980
+ // draggable: true,
981
+ // symbolSize: 40,
982
+ // symbol: "circle", // 'rect'
983
+ // label: {
984
+ // show: true, // node name to be shown in circle
985
+ // },
986
+ // edgeSymbol: ["circle", "arrow"], // for arrow from one to another
987
+ // edgeSymbolSize: [0, 15],
988
+ // emphasis: {
989
+ // focus: "adjacency",
990
+ // label: {
991
+ // show: true,
992
+ // },
993
+ // // disabled: true,
994
+ // },
995
+ // roam: true,
996
+ // force: {
997
+ // repulsion: [500, 1000, 2000],
998
+ // edgeLength: 50,
999
+ // },
1000
+ // },
1001
+ // ],
1002
+ // },
1003
+ // options: [],
1004
+ // };
1005
+
1006
+ return option;
1007
+ }
1008
+
1009
+ // Initialize data
1010
+ function initialize() {
1011
+ globals.years = Object.keys(globals.originalYearToData);
1012
+ globals.currentYear = globals.years[0];
1013
+ globals.currentYearIndex = 0
1014
+
1015
+ const yearToProcessIdToProcess = new Map();
1016
+ const yearToProcessIdToFlowIds = new Map();
1017
+ const yearToFlowIdToFlow = new Map();
1018
+
1019
+ // Create data mapping for each year
1020
+ const yearToData = new Map();
1021
+ for (const year of globals.years) {
1022
+ const yearData = new Map();
1023
+ const nodeData = {
1024
+ ...globals.originalYearToData[year]["node_index_to_data"],
1025
+ };
1026
+ const edgeData = {
1027
+ ...globals.originalYearToData[year]["edge_index_to_data"],
1028
+ };
1029
+
1030
+ // Year -> Process Id and Flow ID -> Process/Flow
1031
+ yearToProcessIdToProcess.set(year, new Map());
1032
+ yearToFlowIdToFlow.set(year, new Map());
1033
+ yearToProcessIdToFlowIds.set(year, new Map());
1034
+
1035
+ // Map node index to node data
1036
+ const nodeIdToNodeIndex = new Map();
1037
+ const nodeIndexToData = new Map();
1038
+ for (const key of Object.keys(nodeData)) {
1039
+ const nodeIndex = parseInt(key);
1040
+ const node = nodeData[nodeIndex];
1041
+ const nodeId = node.process_id;
1042
+ nodeIndexToData.set(nodeIndex, node);
1043
+ nodeIdToNodeIndex.set(nodeId, nodeIndex);
1044
+
1045
+ // Year -> Process ID -> Process
1046
+ const processIdToProcess = yearToProcessIdToProcess.get(year);
1047
+ processIdToProcess.set(nodeId, node);
1048
+
1049
+ // Year -> Process ID -> Flow IDs
1050
+ const processIdToFlowIds = yearToProcessIdToFlowIds.get(year);
1051
+ processIdToFlowIds.set(nodeId, {in: [], out: []});
1052
+ }
1053
+
1054
+ // Map edge index to edge data
1055
+ const edgeIndexToData = new Map();
1056
+ const edgeIdToData = new Map();
1057
+ for (const key of Object.keys(edgeData)) {
1058
+ const edgeIndex = parseInt(key);
1059
+ const edge = edgeData[edgeIndex];
1060
+ const edgeId = edge.flow_id;
1061
+ edgeIndexToData.set(edgeIndex, edge);
1062
+ edgeIdToData.set(edgeId, edge);
1063
+
1064
+ // Year -> Flow ID -> Flow
1065
+ const flowIdToFlow = yearToFlowIdToFlow.get(year);
1066
+ flowIdToFlow.set(edgeId, edge);
1067
+ }
1068
+
1069
+ // Year -> Process ID -> Flow IDs
1070
+ for (const [flowId, flow] of yearToFlowIdToFlow.get(year).entries()) {
1071
+ const sourceProcessId = flow.source_process_id;
1072
+ const sourceProcessEntry = yearToProcessIdToFlowIds
1073
+ .get(year)
1074
+ .get(sourceProcessId);
1075
+ sourceProcessEntry.out.push(flowId);
1076
+
1077
+ const targetProcessId = flow.target_process_id;
1078
+ const targetProcessEntry = yearToProcessIdToFlowIds
1079
+ .get(year)
1080
+ .get(targetProcessId);
1081
+ targetProcessEntry.in.push(flowId);
1082
+ }
1083
+
1084
+ yearData.set("nodeIndexToData", nodeIndexToData);
1085
+ yearData.set("nodeIdToIndex", nodeIdToNodeIndex);
1086
+ yearData.set("edgeIndexToData", edgeIndexToData);
1087
+ yearData.set("edgeIdToData", edgeIdToData);
1088
+ yearData.set("nodeIdToPosition", new Map([]));
1089
+ yearToData.set(year, yearData);
1090
+ }
1091
+
1092
+ // Build transformation stage name to color mapping
1093
+ for(const [k, v] of Object.entries(globals.scenarioData.transformation_stage_name_to_color)) {
1094
+ globals.transformationStageNameToColor.set(k, v)
1095
+ }
1096
+
1097
+ // Update scenario name
1098
+ globals.scenarioName = globals.scenarioData.scenario_name
1099
+
1100
+ globals.yearToData = yearToData;
1101
+ globals.yearToProcessIdToProcess = yearToProcessIdToProcess;
1102
+ globals.yearToFlowIdToFlow = yearToFlowIdToFlow;
1103
+ globals.yearToProcessIdToFlowIds = yearToProcessIdToFlowIds;
1104
+
1105
+ // Create option for ECharts
1106
+ const center = ["50%", "50%"];
1107
+ let layout = globals.freezeNodePositions ? "none" : "force";
1108
+ const option = {
1109
+ baseOption: {
1110
+ title: {
1111
+ text: `${globals.scenarioName}`,
1112
+ subtext: `Year ${globals.currentYear}`,
1113
+ textStyle: {
1114
+ color: "#000",
1115
+ fontSize: 20,
1116
+ fontWeight: 'bold',
1117
+ },
1118
+ subtextStyle: {
1119
+ color: "#000",
1120
+ fontSize: 16,
1121
+ fontWeight: 'bold',
1122
+ }
1123
+ },
1124
+ tooltip: {
1125
+ formatter: getTooltipFormatter,
1126
+ },
1127
+ legend: [
1128
+ {
1129
+ type: "scroll",
1130
+ data: [],
1131
+ position: "left",
1132
+ orient: "vertical",
1133
+ right: 10,
1134
+ top: 50,
1135
+ height: "88%",
1136
+ },
1137
+ ],
1138
+ timeline: {
1139
+ show: true,
1140
+ type: "slider",
1141
+ currentIndex: 0,
1142
+ axisType: "category",
1143
+ data: [],
1144
+ left: "20px",
1145
+ right: "20px",
1146
+ },
1147
+ series: [
1148
+ {
1149
+ name: "Process flows",
1150
+ type: "graph",
1151
+ layout: "force",
1152
+ data: [],
1153
+ links: [],
1154
+ categories: [],
1155
+ center: center,
1156
+ zoom: 2,
1157
+ draggable: true,
1158
+ symbolSize: 40,
1159
+ symbol: "circle", // 'rect'
1160
+ label: {
1161
+ show: true, // node name to be shown in circle
1162
+ },
1163
+ edgeSymbol: ["circle", "arrow"], // for arrow from one to another
1164
+ edgeSymbolSize: [0, 15],
1165
+ emphasis: {
1166
+ focus: "adjacency",
1167
+ label: {
1168
+ show: true,
1169
+ },
1170
+ // disabled: true,
1171
+ },
1172
+ roam: true,
1173
+ force: {
1174
+ repulsion: [500, 1000, 2000],
1175
+ edgeLength: 50,
1176
+ },
1177
+ },
1178
+ ],
1179
+ },
1180
+ options: [],
1181
+ };
1182
+
1183
+ // Build years and insert to options
1184
+ for (const year of globals.years) {
1185
+ const graphData = buildGraphDataForYear(year, {});
1186
+ const newSeries = {
1187
+ series: [
1188
+ {
1189
+ data: graphData.data,
1190
+ links: graphData.links,
1191
+ categories: graphData.categories,
1192
+ },
1193
+ ],
1194
+ };
1195
+ option.options.push(newSeries);
1196
+ }
1197
+
1198
+ // Get first year data and activate it
1199
+ const currentYearData = buildGraphDataForYear(globals.currentYear);
1200
+ option.baseOption.timeline.currentIndex = 0
1201
+ option.baseOption.legend[0].data = currentYearData.legendData;
1202
+ option.baseOption.timeline.data = globals.years;
1203
+
1204
+ // Store reference to original option
1205
+ // This is needed when changed years
1206
+ globals.initialOption = option;
1207
+ chart.setOption(globals.initialOption);
1208
+
1209
+ // Build node colors by reading back the default node colors after rendering
1210
+ // first time and retrieve node colors
1211
+ const nodeInfos = []
1212
+ const model = chart.getModel()
1213
+ const series = model.getSeriesByIndex(0)
1214
+ const data = series.getData()
1215
+ data.each((index) => {
1216
+ const name = data.getName(index)
1217
+ const color = data.getItemVisual(index, "style").fill
1218
+ nodeInfos.push({ id: name, color: color })
1219
+ })
1220
+
1221
+ // Set colors for every year
1222
+ for(const year of globals.years) {
1223
+ const yearData = globals.yearToData.get(year);
1224
+ const nodeIdToIndex = yearData.get("nodeIdToIndex")
1225
+ const nodeIndexToData = yearData.get("nodeIndexToData")
1226
+ for(const entry of nodeInfos) {
1227
+ const nodeId = entry.id
1228
+ const nodeColor = entry.color
1229
+ const nodeIndex = nodeIdToIndex.get(nodeId)
1230
+ const nodeData = nodeIndexToData.get(nodeIndex)
1231
+
1232
+ // TODO: In year 1963 there is "Dissolving_pulp:Export" but that is not defined
1233
+ // in the original data?
1234
+ // Inject properties "color_normal" and "color_transformation_stage" to original node data
1235
+ if(nodeData == undefined) {
1236
+ console.log(nodeId)
1237
+ console.log(nodeIdToIndex.keys())
1238
+ console.log(year)
1239
+ }
1240
+
1241
+ const transformationStageName = nodeData.transformation_stage
1242
+ nodeData["color_normal"] = nodeColor
1243
+ nodeData["color_transformation_stage"] = globals.transformationStageNameToColor.get(transformationStageName)
1244
+ }
1245
+ }
1246
+
1247
+ if(globals.useTransformationStageColors) {
1248
+ update({ resetView: false })
1249
+ }
1250
+ }
1251
+
1252
+ function update(updateOptions = {resetView: false}) {
1253
+ // // TODO: This is called from reset resetView and formatter for
1254
+ // const graphData = buildGraphDataForYear(globals.currentYear);
1255
+ // const option = buildOption(graphData, updateOptions);
1256
+ // chart.setOption(option);
1257
+ // // chart.setOption(option, { notMerge: true });
1258
+ // // chart.setOption(option, { replaceMerge: ["options"] });
1259
+ // globals.graphData = graphData;
1260
+
1261
+ // Update current year data used in globals.initialOption
1262
+ const graphData = buildGraphDataForYear(globals.currentYear);
1263
+ const option = globals.initialOption.options[globals.currentYearIndex]
1264
+ option.series[0].data = graphData.data
1265
+ option.series[0].links = graphData.links
1266
+ option.series[0].categories = graphData.categories
1267
+ chart.setOption(globals.initialOption)
1268
+ globals.graphData = graphData;
1269
+ }
1270
+
1271
+ function freezeNodePositions() {
1272
+ // Update current year nodes' position
1273
+ const yearData = getYearData(globals.currentYear);
1274
+ for (const [nodeIndex, nodeData] of yearData.data.entries()) {
1275
+ const nodePosition = calculateNodePosition(nodeData.id);
1276
+ setNodePosition(globals.currentYear, nodeData.id, nodePosition);
1277
+ }
1278
+
1279
+ const model = chart.getModel()
1280
+ const series = model.getSeriesByIndex(0)
1281
+ const coordSys = series.coordinateSystem
1282
+ const zoom = coordSys.getZoom()
1283
+ const center = coordSys.getCenter()
1284
+ globals.initialOption.baseOption.series[0].layout = "none";
1285
+ globals.initialOption.baseOption.series[0].zoom = zoom
1286
+ globals.initialOption.baseOption.series[0].center = center
1287
+ chart.setOption(globals.initialOption);
1288
+ }
1289
+
1290
+ function unfreezeNodePositions() {
1291
+ // Update current year nodes' position
1292
+ const yearData = getYearData(globals.currentYear);
1293
+ for (const [nodeIndex, nodeData] of yearData.data.entries()) {
1294
+ const nodePosition = calculateNodePosition(nodeData.id);
1295
+ setNodePosition(globals.currentYear, nodeData.id, nodePosition);
1296
+ }
1297
+
1298
+ const model = chart.getModel()
1299
+ const series = model.getSeriesByIndex(0)
1300
+ const coordSys = series.coordinateSystem
1301
+ const zoom = coordSys.getZoom()
1302
+ const center = coordSys.getCenter()
1303
+ globals.initialOption.baseOption.series[0].layout = "force";
1304
+ globals.initialOption.baseOption.series[0].zoom = zoom
1305
+ globals.initialOption.baseOption.series[0].center = center
1306
+ chart.setOption(globals.initialOption);
1307
+ }
1308
+
1309
+ function getNodePosition(year, nodeId) {
1310
+ const yearIndex = getYearIndex(year);
1311
+ const yearData = globals.initialOption.options[yearIndex].series[0];
1312
+ for (const [nodeIndex, nodeData] of yearData.data.entries()) {
1313
+ if (nodeData.id == nodeId) {
1314
+ return {x: nodeData.x, y: nodeData.y};
1315
+ }
1316
+ }
1317
+
1318
+ console.log(`No node ${nodeId} at year ${year}`);
1319
+ }
1320
+
1321
+ function setNodePosition(year, nodeId, nodePosition) {
1322
+ const yearIndex = getYearIndex(year);
1323
+ const yearData = globals.initialOption.options[yearIndex].series[0];
1324
+ for (const [nodeIndex, nodeData] of yearData.data.entries()) {
1325
+ if (nodeData.id == nodeId) {
1326
+ nodeData.x = nodePosition.x;
1327
+ nodeData.y = nodePosition.y;
1328
+ return;
1329
+ }
1330
+ }
1331
+
1332
+ // NOTE: Virtual flows might not be found for every year so no error here
1333
+ // console.log(`No node ${nodeId} at year ${year}`);
1334
+ }
1335
+
1336
+ function calculateNodePosition(nodeId) {
1337
+ const nodeIdToPosition = new Map();
1338
+
1339
+ const nodes = chart.getModel().getSeriesByIndex(0).getData();
1340
+ nodes.each(function (nodeIndex) {
1341
+ const nodeData = nodes.getRawDataItem(nodeIndex);
1342
+ const nodeLayout = nodes.getItemLayout(nodeIndex);
1343
+ const nodePosition = {x: nodeLayout[0], y: nodeLayout[1]};
1344
+ nodeIdToPosition.set(nodeData.id, nodePosition);
1345
+ });
1346
+
1347
+ if (!nodeIdToPosition.has(nodeId)) {
1348
+ // NOTE: Virtual flows might not be found for every year so
1349
+ // console.error(`No node ${nodeId} found in year ${globals.currentYear}!`);
1350
+ return {x: 0.0, y: 0.0};
1351
+ }
1352
+
1353
+ const nodePosition = nodeIdToPosition.get(nodeId);
1354
+ return {x: nodePosition.x, y: nodePosition.y};
1355
+ }
1356
+
1357
+ function changeCurrentYear(targetYear) {
1358
+ // Save current year nodes' position
1359
+ const currentYearData = getYearData(globals.currentYear);
1360
+ for (const [nodeIndex, nodeData] of currentYearData.data.entries()) {
1361
+ const nodePosition = calculateNodePosition(nodeData.id);
1362
+ setNodePosition(globals.currentYear, nodeData.id, nodePosition);
1363
+ }
1364
+
1365
+ const targetYearData = getYearData(targetYear);
1366
+ for (const [nodeIndex, nodeData] of currentYearData.data.entries()) {
1367
+ const nodePosition = getNodePosition(targetYear, nodeData.id);
1368
+ setNodePosition(targetYear, nodeData.id, nodePosition);
1369
+ }
1370
+
1371
+ // Update year and chart
1372
+ const yearIndex = getYearIndex(targetYear);
1373
+ globals.currentYear = targetYear;
1374
+ globals.currentYearIndex = yearIndex
1375
+ globals.initialOption.baseOption.timeline.currentIndex = yearIndex;
1376
+ globals.initialOption.baseOption.title.subtext = `Year ${globals.currentYear}`;
1377
+ update({ resetView: false})
1378
+ }
1379
+
1380
+ console.log("Initializing...");
1381
+ initialize();
1382
+ console.log("Done.");
1383
+
1384
+ // chart.dispatchAction({
1385
+ // type: 'highlight',
1386
+ // batch: [
1387
+ // { dataType: 'node', dataIndex: nodeDataIndex},
1388
+ // { dataType: 'edge', dataIndex: edgeDataIndex, notBlur: true},
1389
+ // ],
1390
+ // })
1391
+ // })