@orbcharts/plugins-basic 3.0.7 → 3.0.9

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 (121) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +6837 -6656
  3. package/dist/orbcharts-plugins-basic.umd.js +45 -45
  4. package/dist/src/base/BaseStackedBars.d.ts +2 -0
  5. package/dist/src/series/defaults.d.ts +2 -1
  6. package/dist/src/series/index.d.ts +1 -0
  7. package/dist/src/series/plugins/Indicator.d.ts +3 -0
  8. package/lib/core-types.ts +7 -7
  9. package/lib/core.ts +6 -6
  10. package/lib/gridObservables.ts +6 -6
  11. package/lib/plugins-basic-types.ts +6 -6
  12. package/package.json +48 -48
  13. package/src/base/BaseBars.ts +765 -765
  14. package/src/base/BaseBarsTriangle.ts +676 -676
  15. package/src/base/BaseDots.ts +464 -464
  16. package/src/base/BaseGroupAxis.ts +691 -691
  17. package/src/base/BaseLegend.ts +684 -684
  18. package/src/base/BaseLineAreas.ts +629 -629
  19. package/src/base/BaseLines.ts +706 -706
  20. package/src/base/BaseOrdinalBubbles.ts +729 -729
  21. package/src/base/BaseRacingBars.ts +582 -582
  22. package/src/base/BaseRacingLabels.ts +404 -404
  23. package/src/base/BaseRacingValueLabels.ts +403 -403
  24. package/src/base/BaseStackedBars.ts +793 -782
  25. package/src/base/BaseTooltip.ts +408 -408
  26. package/src/base/BaseValueAxis.ts +600 -600
  27. package/src/base/BaseXAxis.ts +427 -427
  28. package/src/base/BaseXZoom.ts +241 -241
  29. package/src/base/BaseYAxis.ts +389 -389
  30. package/src/base/types.ts +2 -2
  31. package/src/const.ts +30 -30
  32. package/src/grid/defaults.ts +213 -213
  33. package/src/grid/gridObservables.ts +635 -635
  34. package/src/grid/index.ts +16 -16
  35. package/src/grid/plugins/Bars.ts +69 -69
  36. package/src/grid/plugins/BarsPN.ts +66 -66
  37. package/src/grid/plugins/BarsTriangle.ts +73 -73
  38. package/src/grid/plugins/Dots.ts +68 -68
  39. package/src/grid/plugins/GridLegend.ts +107 -107
  40. package/src/grid/plugins/GridTooltip.ts +66 -66
  41. package/src/grid/plugins/GroupAux.ts +1095 -1095
  42. package/src/grid/plugins/GroupAxis.ts +73 -73
  43. package/src/grid/plugins/GroupZoom.ts +218 -218
  44. package/src/grid/plugins/LineAreas.ts +65 -65
  45. package/src/grid/plugins/Lines.ts +59 -59
  46. package/src/grid/plugins/StackedBars.ts +66 -64
  47. package/src/grid/plugins/StackedValueAxis.ts +97 -96
  48. package/src/grid/plugins/ValueAxis.ts +94 -94
  49. package/src/index.ts +6 -6
  50. package/src/multiGrid/defaults.ts +244 -244
  51. package/src/multiGrid/index.ts +14 -14
  52. package/src/multiGrid/multiGridObservables.ts +50 -50
  53. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  54. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  55. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  56. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
  57. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  58. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  59. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  60. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  61. package/src/multiGrid/plugins/MultiStackedBars.ts +109 -106
  62. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +135 -134
  63. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  64. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
  65. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  66. package/src/multiValue/defaults.ts +523 -523
  67. package/src/multiValue/index.ts +16 -16
  68. package/src/multiValue/multiValueObservables.ts +781 -781
  69. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  70. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  71. package/src/multiValue/plugins/OrdinalAux.ts +660 -660
  72. package/src/multiValue/plugins/OrdinalAxis.ts +524 -524
  73. package/src/multiValue/plugins/OrdinalBubbles.ts +226 -226
  74. package/src/multiValue/plugins/OrdinalZoom.ts +57 -57
  75. package/src/multiValue/plugins/RacingBars.ts +375 -375
  76. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
  77. package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
  78. package/src/multiValue/plugins/Scatter.ts +486 -486
  79. package/src/multiValue/plugins/ScatterBubbles.ts +635 -635
  80. package/src/multiValue/plugins/XAxis.ts +107 -107
  81. package/src/multiValue/plugins/XYAux.ts +683 -683
  82. package/src/multiValue/plugins/XYAxes.ts +194 -194
  83. package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
  84. package/src/multiValue/plugins/XZoom.ts +40 -40
  85. package/src/noneData/defaults.ts +102 -102
  86. package/src/noneData/index.ts +3 -3
  87. package/src/noneData/plugins/Container.ts +27 -27
  88. package/src/noneData/plugins/Tooltip.ts +373 -373
  89. package/src/relationship/defaults.ts +221 -221
  90. package/src/relationship/index.ts +5 -5
  91. package/src/relationship/plugins/ForceDirected.ts +1056 -1056
  92. package/src/relationship/plugins/ForceDirectedBubbles.ts +1294 -1294
  93. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  94. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  95. package/src/relationship/relationshipObservables.ts +49 -49
  96. package/src/series/defaults.ts +236 -224
  97. package/src/series/index.ts +10 -9
  98. package/src/series/plugins/Bubbles.ts +784 -784
  99. package/src/series/plugins/Indicator.ts +292 -0
  100. package/src/series/plugins/Pie.ts +622 -622
  101. package/src/series/plugins/PieEventTexts.ts +283 -283
  102. package/src/series/plugins/PieLabels.ts +639 -639
  103. package/src/series/plugins/Rose.ts +515 -515
  104. package/src/series/plugins/RoseLabels.ts +599 -599
  105. package/src/series/plugins/SeriesLegend.ts +107 -107
  106. package/src/series/plugins/SeriesTooltip.ts +66 -66
  107. package/src/series/seriesObservables.ts +168 -168
  108. package/src/series/seriesUtils.ts +51 -51
  109. package/src/tree/defaults.ts +102 -102
  110. package/src/tree/index.ts +4 -4
  111. package/src/tree/plugins/TreeLegend.ts +100 -100
  112. package/src/tree/plugins/TreeMap.ts +341 -341
  113. package/src/tree/plugins/TreeTooltip.ts +66 -66
  114. package/src/utils/commonUtils.ts +31 -31
  115. package/src/utils/d3Graphics.ts +176 -176
  116. package/src/utils/d3Utils.ts +92 -92
  117. package/src/utils/observables.ts +14 -14
  118. package/src/utils/orbchartsUtils.ts +129 -129
  119. package/tsconfig.base.json +13 -13
  120. package/tsconfig.json +2 -2
  121. package/vite.config.js +22 -22
@@ -1,1295 +1,1295 @@
1
- import * as d3 from 'd3'
2
- import {
3
- of,
4
- combineLatest,
5
- map,
6
- switchMap,
7
- first,
8
- takeUntil,
9
- Subject,
10
- BehaviorSubject,
11
- Observable,
12
- distinctUntilChanged,
13
- shareReplay,
14
- take,
15
- share,
16
- filter,
17
- iif,
18
- EMPTY
19
- } from 'rxjs'
20
- import type { DefinePluginConfig } from '../../../lib/core-types'
21
- import type {
22
- ChartParams,
23
- DatumValue,
24
- DataSeries,
25
- EventName,
26
- EventRelationship,
27
- ComputedDataSeries,
28
- ComputedNode,
29
- ComputedEdge,
30
- ContainerPosition,
31
- Layout
32
- } from '../../../lib/core-types'
33
- import {
34
- defineRelationshipPlugin,
35
- getMinMax
36
- } from '../../../lib/core'
37
- import type { BubblesParams, ArcScaleType, ForceDirectedBubblesParams } from '../../../lib/plugins-basic-types'
38
- import { getColor, getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
39
- import { DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS } from '../defaults'
40
- // import { renderCircleText } from '../../utils/d3Graphics'
41
- import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
42
- import { renderCircleText } from '../../utils/d3Graphics'
43
-
44
- // interface BubblesDatum extends ComputedNode {
45
- // x: number
46
- // y: number
47
- // r: number
48
- // _originR: number // 紀錄變化前的r
49
- // }
50
-
51
- type Zoom = {
52
- xOffset: number
53
- yOffset: number
54
- scaleExtent: {
55
- min: number
56
- max: number
57
- }
58
- }
59
-
60
- // d3 forceSimulation使用的node資料
61
- type RenderNode = d3.SimulationNodeDatum & ComputedNode & {
62
- r: number
63
- }
64
-
65
- // d3 forceSimulation使用的edge資料
66
- interface RenderEdge extends ComputedEdge {
67
- source: RenderNode
68
- target: RenderNode
69
- strokeWidth: number
70
- markerId: string
71
- }
72
-
73
- // d3 forceSimulation使用的資料
74
- type RenderData = {
75
- nodes: (ComputedNode | RenderNode)[] // 經過d3 forceSimulation計算後的node才有座標資訊
76
- edges: RenderEdge[]
77
- }
78
-
79
- interface D3DragEvent {
80
- active: number
81
- dx: number
82
- dy: number
83
- identifier: string
84
- sourceEvent: MouseEvent
85
- subject: RenderNode
86
- target: any
87
- type: string
88
- x: number
89
- y: number
90
- }
91
-
92
- type DragStatus = 'start' | 'drag' | 'end'
93
-
94
- interface MarkerParams {
95
- viewBox: string
96
- d: string
97
- pointerWidth: number
98
- pointerHeight: number
99
- // refX: number
100
- }
101
-
102
- interface MarkerDatum {
103
- id: string
104
- edgeId: string
105
- strokeWidth: number
106
- refX: number
107
- }
108
-
109
- // type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
110
-
111
- const pluginName = 'ForceDirectedBubbles'
112
-
113
- const baseLineHeight = 12 // 未變形前的字體大小(代入計算用而已,數字多少都不會有影響)
114
-
115
- const gSelectionClassName = getClassName(pluginName, 'zoom-area')
116
- const defsArrowMarkerId = getUniID(pluginName, 'arrow')
117
- const defsArrowMarkerClassName = getClassName(pluginName, 'arrow-marker')
118
- const edgeListGClassName = getClassName(pluginName, 'edge-list-g')
119
- const edgeGClassName = getClassName(pluginName, 'edge-g')
120
- const edgeArrowPathClassName = getClassName(pluginName, 'edge-arrow-path')
121
- const edgeLabelGClassName = getClassName(pluginName, 'edge-label-g')
122
- const edgeLabelClassName = getClassName(pluginName, 'edge-label')
123
- const nodeListGClassName = getClassName(pluginName, 'node-list-g')
124
- const nodeGClassName = getClassName(pluginName, 'node-g')
125
- const nodeCircleClassName = getClassName(pluginName, 'node-circle')
126
- // const nodeLabelGClassName = getClassName(pluginName, 'node-label-g')
127
- // const nodeLabelClassName = getClassName(pluginName, 'node-label')
128
-
129
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS> = {
130
- name: pluginName,
131
- defaultParams: DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS,
132
- layerIndex: LAYER_INDEX_OF_GRAPHIC,
133
- validator: (params, { validateColumns }) => {
134
- const result = validateColumns(params, {
135
- bubble: {
136
- toBeTypes: ['object']
137
- },
138
- bubbleLabel: {
139
- toBeTypes: ['object']
140
- },
141
- arrow: {
142
- toBeTypes: ['object']
143
- },
144
- arrowLabel: {
145
- toBeTypes: ['object']
146
- },
147
- force: {
148
- toBeTypes: ['object']
149
- },
150
- zoomable: {
151
- toBeTypes: ['boolean']
152
- },
153
- transform: {
154
- toBeTypes: ['object']
155
- },
156
- scaleExtent: {
157
- toBeTypes: ['object']
158
- }
159
- })
160
- if (params.bubble) {
161
- const bubbleResult = validateColumns(params.bubble, {
162
- radiusMin: {
163
- toBeTypes: ['number']
164
- },
165
- radiusMax: {
166
- toBeTypes: ['number']
167
- },
168
- arcScaleType: {
169
- toBe: '"area" | "radius"',
170
- test: (value) => value === 'area' || value === 'radius'
171
- },
172
- fillColorType: {
173
- toBeOption: 'ColorType'
174
- },
175
- strokeColorType: {
176
- toBeOption: 'ColorType'
177
- },
178
- strokeWidth: {
179
- toBeTypes: ['number']
180
- },
181
- styleFn: {
182
- toBeTypes: ['Function']
183
- },
184
- })
185
- if (bubbleResult.status === 'error') {
186
- return bubbleResult
187
- }
188
- }
189
- if (params.bubbleLabel) {
190
- const bubbleLabelResult = validateColumns(params.bubbleLabel, {
191
- fillRate: {
192
- toBeTypes: ['number']
193
- },
194
- lineHeight: {
195
- toBeTypes: ['number']
196
- },
197
- maxLineLength: {
198
- toBeTypes: ['number']
199
- },
200
- colorType: {
201
- toBeOption: 'ColorType'
202
- },
203
- styleFn: {
204
- toBeTypes: ['Function']
205
- },
206
- })
207
- if (bubbleLabelResult.status === 'error') {
208
- return bubbleLabelResult
209
- }
210
- }
211
- if (params.arrow) {
212
- const arrowResult = validateColumns(params.arrow, {
213
- colorType: {
214
- toBeOption: 'ColorType'
215
- },
216
- strokeWidthMin: {
217
- toBeTypes: ['number']
218
- },
219
- strokeWidthMax: {
220
- toBeTypes: ['number']
221
- },
222
- pointerWidth: {
223
- toBeTypes: ['number']
224
- },
225
- pointerHeight: {
226
- toBeTypes: ['number']
227
- },
228
- styleFn: {
229
- toBeTypes: ['Function']
230
- },
231
- })
232
- if (arrowResult.status === 'error') {
233
- return arrowResult
234
- }
235
- }
236
- if (params.arrowLabel) {
237
- const arrowLabelResult = validateColumns(params.arrowLabel, {
238
- colorType: {
239
- toBeOption: 'ColorType'
240
- },
241
- sizeFixed: {
242
- toBeTypes: ['boolean']
243
- },
244
- styleFn: {
245
- toBeTypes: ['Function']
246
- },
247
- })
248
- if (arrowLabelResult.status === 'error') {
249
- return arrowLabelResult
250
- }
251
- }
252
- if (params.force) {
253
- const forceResult = validateColumns(params.force, {
254
- nodeStrength: {
255
- toBeTypes: ['number']
256
- },
257
- linkDistance: {
258
- toBeTypes: ['number']
259
- },
260
- velocityDecay: {
261
- toBeTypes: ['number']
262
- },
263
- alphaDecay: {
264
- toBeTypes: ['number']
265
- },
266
- })
267
- if (forceResult.status === 'error') {
268
- return forceResult
269
- }
270
- }
271
- if (params.transform) {
272
- const transformResult = validateColumns(params.transform, {
273
- x: {
274
- toBeTypes: ['number']
275
- },
276
- y: {
277
- toBeTypes: ['number']
278
- },
279
- k: {
280
- toBeTypes: ['number']
281
- },
282
- })
283
- if (transformResult.status === 'error') {
284
- return transformResult
285
- }
286
- }
287
- if (params.scaleExtent) {
288
- const scaleExtentResult = validateColumns(params.scaleExtent, {
289
- min: {
290
- toBeTypes: ['number']
291
- },
292
- max: {
293
- toBeTypes: ['number']
294
- },
295
- })
296
- if (scaleExtentResult.status === 'error') {
297
- return scaleExtentResult
298
- }
299
- }
300
- return result
301
- }
302
- }
303
-
304
- // let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
305
-
306
- function createSimulation (layout: Layout, fullParams: ForceDirectedBubblesParams) {
307
- return d3.forceSimulation()
308
- .velocityDecay(fullParams.force.velocityDecay)
309
- .alphaDecay(fullParams.force.alphaDecay)
310
- .force(
311
- "link",
312
- d3.forceLink()
313
- .id((d: d3.SimulationNodeDatum & ComputedNode) => d.id)
314
- .strength(1)
315
- .distance((d: d3.SimulationLinkDatum<d3.SimulationNodeDatum & ComputedNode>) => {
316
- // if (d.direction === 'top') {
317
- // return 200
318
- // } else {
319
- // return 250
320
- // }
321
- return fullParams.force.linkDistance
322
- })
323
- )
324
- .force("charge", d3.forceManyBody().strength(fullParams.force.nodeStrength))
325
- .force("collision", d3.forceCollide(fullParams.bubble.radiusMax).strength(1)) // 先以最大值設定
326
- .force("center", d3.forceCenter(layout.width / 2, layout.height / 2))
327
-
328
- }
329
-
330
- function translateFn (d: any): string {
331
- // console.log('translateFn', d)
332
- return "translate(" + d.x + "," + d.y + ")";
333
- }
334
-
335
- function translateCenterFn (d: any): string {
336
- // console.log('translateCenterFn', d)
337
- const x = d.source.x + ((d.target.x - d.source.x) / 2) // 置中的話除2
338
- const y = d.source.y + ((d.target.y - d.source.y) / 2) // 置中的話除2
339
- return "translate(" + x + "," + y + ")";
340
- }
341
-
342
- function linkArcFn (d: RenderEdge): string {
343
- // console.log('linkArcFn', d)
344
-
345
- // const dx = d.target.x - d.source.x,
346
- // dy = d.target.y - d.source.y
347
- // dr讓方向線變成有弧度的
348
- // dr = Math.sqrt(dx * dx + dy * dy);
349
- // return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
350
-
351
- // 直線
352
- return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y;
353
-
354
-
355
- }
356
-
357
-
358
-
359
- function renderArrowMarker ({ defsSelection, markerParams, markerData, fullParams, fullChartParams }: {
360
- defsSelection: d3.Selection<SVGDefsElement, any, any, unknown>
361
- markerParams: MarkerParams
362
- markerData: MarkerDatum[]
363
- fullParams: ForceDirectedBubblesParams
364
- fullChartParams: ChartParams
365
- }) {
366
- return defsSelection
367
- .selectAll<SVGMarkerElement, any>(`marker.${defsArrowMarkerClassName}`)
368
- .data(markerData)
369
- .join(
370
- enter => {
371
- const enterSelection = enter
372
- .append("marker")
373
- .classed(defsArrowMarkerClassName, true)
374
- .attr("viewBox", markerParams.viewBox)
375
- .attr("orient", "auto")
376
- enterSelection.append("path")
377
- .attr("d", markerParams.d)
378
- return enterSelection
379
- },
380
- update => {
381
- return update
382
- },
383
- exit => {
384
- return exit.remove()
385
- }
386
- )
387
- .attr('id', d => d.id)
388
- .attr('fill', d => getColor(fullParams.arrow.colorType, fullChartParams ))
389
- .attr("markerWidth", markerParams.pointerWidth)
390
- .attr("markerHeight", markerParams.pointerHeight)
391
- .attr('refX', d => d.refX)
392
- .attr("refY", 0)
393
-
394
-
395
- }
396
-
397
- // function drag (): d3.DragBehavior<Element, unknown, unknown> {
398
- // let originHighlightLockMode: boolean // 拖拽前的highlightLockMode
399
-
400
- // return d3.drag()
401
- // .on("start", (event: D3DragEvent) => {
402
- // console.log('start', event.sourceEvent)
403
- // // if (this.params.lockMode) {
404
- // // return
405
- // // }
406
- // // if (!d3.event.active) {
407
- // // this.forceRestart()
408
- // // }
409
- // // d.fx = d.x
410
- // // d.fy = d.y
411
-
412
- // // // 鎖定模式才不會在拖拽過程式觸發到其他事件造成衝突
413
- // // originHighlightLockMode = this.highlightLockMode
414
- // // this.highlightLockMode = true
415
- // // this.noneStopMode = true
416
- // // // 動畫會有點卡住所以乾脆拿掉
417
- // // if(this.tooltip != null) {
418
- // // this.tooltip.remove()
419
- // // }
420
- // })
421
- // .on("drag", function (event: D3DragEvent) {
422
- // console.log('drag', event)
423
- // // if (this.params.lockMode) {
424
- // // return
425
- // // }
426
- // // if (!d3.event.active) {
427
- // // this.force.alphaTarget(0)
428
- // // }
429
- // // d.fx = d3.event.x
430
- // // d.fy = d3.event.y
431
- // // d3.select(this).attr({
432
- // // 'cx': event.x,
433
- // // 'cy': event.y,
434
- // // })
435
- // d3.select(this)
436
- // .attr('fx', event.x)
437
- // .attr('fy', event.y)
438
- // })
439
- // .on("end", (event: D3DragEvent) => {
440
- // console.log('end', event)
441
- // // if (this.params.lockMode) {
442
- // // return
443
- // // }
444
- // // d.fx = null
445
- // // d.fy = null
446
-
447
- // // this.highlightLockMode = originHighlightLockMode // 還原拖拽前的highlightLockMode
448
- // // this.noneStopMode = false
449
- // // if (this.highlightLockMode) {
450
- // // this.forceStop()
451
- // // }
452
- // })
453
- // }
454
-
455
- function drag (simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>, dragStatus$: BehaviorSubject<DragStatus>) {
456
- function dragstarted (event: D3DragEvent, node: RenderNode) {
457
- if (!event.active) {
458
- simulation.alphaTarget(0.3).restart()
459
- }
460
- event.subject.fx = event.subject.x
461
- event.subject.fy = event.subject.y
462
-
463
- dragStatus$.next('start')
464
- }
465
-
466
- function dragged (event: D3DragEvent, node: RenderNode) {
467
- event.subject.fx = event.x
468
- event.subject.fy = event.y
469
-
470
- dragStatus$.next('drag')
471
- }
472
-
473
- function dragended (event: D3DragEvent, node: RenderNode) {
474
- if (!event.active) {
475
- simulation.alphaTarget(0)
476
- }
477
- event.subject.fx = null
478
- event.subject.fy = null
479
-
480
- dragStatus$.next('end')
481
- }
482
-
483
- return d3.drag()
484
- .on("start", dragstarted)
485
- .on("drag", dragged)
486
- .on("end", dragended)
487
- }
488
-
489
- function renderNodeG ({ nodeListGSelection, nodes }: {
490
- nodeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
491
- nodes: RenderNode[]
492
- }) {
493
- return nodeListGSelection.selectAll<SVGGElement, RenderNode>('g')
494
- .data(nodes, d => d.id)
495
- .join(
496
- enter => {
497
- const enterSelection = enter
498
- .append('g')
499
- .classed(nodeGClassName, true)
500
- // .attr('cursor', 'pointer')
501
- return enterSelection
502
- },
503
- update => {
504
- return update
505
- },
506
- exit => {
507
- return exit.remove()
508
- }
509
- )
510
- }
511
-
512
- function renderNodeCircle ({ nodeGSelection, fullParams, fullChartParams }: {
513
- nodeGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
514
- fullParams: ForceDirectedBubblesParams
515
- fullChartParams: ChartParams
516
- }) {
517
- nodeGSelection
518
- .each((data,i,g) => {
519
- const gSelection = d3.select(g[i])
520
- gSelection.selectAll<SVGCircleElement, ComputedEdge>('circle')
521
- .data([data])
522
- .join(
523
- enter => {
524
- const enterSelection = enter
525
- .append('circle')
526
- .classed(nodeCircleClassName, true)
527
- .attr('cursor', 'pointer')
528
- return enterSelection
529
- },
530
- update => {
531
- return update
532
- },
533
- exit => {
534
- return exit.remove()
535
- }
536
- )
537
- .attr('r', d => d.r)
538
- .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.bubble.fillColorType, fullChartParams }))
539
- .attr('stroke', d => getDatumColor({ datum: d, colorType: fullParams.bubble.strokeColorType, fullChartParams }))
540
- .attr('stroke-width', fullParams.bubble.strokeWidth)
541
- .attr('style', d => fullParams.bubble.styleFn(d))
542
-
543
- })
544
- .attr("text-anchor", "middle")
545
- .attr('font-size', baseLineHeight)
546
- .each((d,i,g) => {
547
- const gSelection = d3.select(g[i])
548
-
549
- gSelection.call(renderCircleText, {
550
- text: d.label,
551
- radius: d.r * fullParams.bubbleLabel.fillRate,
552
- lineHeight: baseLineHeight * fullParams.bubbleLabel.lineHeight,
553
- isBreakAll: d.label.length <= fullParams.bubbleLabel.maxLineLength
554
- ? false
555
- : fullParams.bubbleLabel.wordBreakAll
556
- })
557
-
558
- // -- text color --
559
- gSelection.select('text').attr('fill', _ => getDatumColor({
560
- datum: d,
561
- colorType: fullParams.bubbleLabel.colorType,
562
- fullChartParams: fullChartParams
563
- }))
564
-
565
-
566
- })
567
-
568
- nodeGSelection.select('text')
569
- .attr('pointer-events', 'none')
570
- .attr('style', d => fullParams.bubbleLabel.styleFn(d))
571
-
572
- return nodeGSelection.select<SVGCircleElement>(`circle.${nodeCircleClassName}`)
573
- }
574
-
575
- // function renderNodeLabelG ({ nodeGSelection }: {
576
- // nodeGSelection: d3.Selection<SVGGElement, any, any, unknown>
577
- // }) {
578
- // nodeGSelection.each((data,i,g) => {
579
- // const gSelection = d3.select(g[i])
580
- // gSelection.selectAll<SVGGElement, RenderNode>('g')
581
- // .data([data])
582
- // .join(
583
- // enter => {
584
- // const enterSelection = enter
585
- // .append('g')
586
- // .classed(nodeLabelGClassName, true)
587
- // // .attr('cursor', 'pointer')
588
- // return enterSelection
589
- // },
590
- // update => {
591
- // return update
592
- // },
593
- // exit => {
594
- // return exit.remove()
595
- // }
596
- // )
597
- // })
598
-
599
- // return nodeGSelection.select<SVGTextElement>(`g.${nodeLabelGClassName}`)
600
- // }
601
-
602
- // function renderNodeLabel ({ nodeLabelGSelection, fullParams, fullChartParams }: {
603
- // nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
604
- // fullParams: ForceDirectedBubblesParams
605
- // fullChartParams: ChartParams
606
- // }) {
607
- // nodeLabelGSelection.each((data,i,g) => {
608
- // const gSelection = d3.select(g[i])
609
- // gSelection.selectAll<SVGTextElement, RenderNode>('text')
610
- // .data([data], d => d.id)
611
- // .join(
612
- // enter => {
613
- // const enterSelection = enter
614
- // .append('text')
615
- // .classed(nodeLabelClassName, true)
616
- // // .attr('cursor', 'pointer')
617
- // .attr('text-anchor', 'middle')
618
- // .attr('pointer-events', 'none')
619
- // return enterSelection
620
- // },
621
- // update => {
622
- // return update
623
- // },
624
- // exit => {
625
- // return exit.remove()
626
- // }
627
- // )
628
- // .text(d => d.label)
629
- // .attr('transform', d => `translate(0, ${- d.r - 10})`)
630
- // .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.node.labelColorType, fullChartParams }))
631
- // .attr('font-size', fullChartParams.styles.textSize)
632
- // .attr('style', d => fullParams.node.labelStyleFn(d))
633
- // })
634
-
635
- // return nodeLabelGSelection.select<SVGTextElement>(`text.${nodeLabelClassName}`)
636
- // }
637
-
638
- function renderEdgeG ({ edgeListGSelection, edges }: {
639
- edgeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
640
- edges: RenderEdge[]
641
- }) {
642
- return edgeListGSelection.selectAll<SVGGElement, RenderEdge>('g')
643
- .data(edges, d => d.id)
644
- .join(
645
- enter => {
646
- const enterSelection = enter
647
- .append('g')
648
- .classed(edgeGClassName, true)
649
- // .attr('cursor', 'pointer')
650
- return enterSelection
651
- },
652
- update => {
653
- return update
654
- },
655
- exit => {
656
- return exit.remove()
657
- }
658
- )
659
- }
660
-
661
- function renderEdgeArrowPath ({ edgeGSelection, fullParams, fullChartParams }: {
662
- edgeGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
663
- fullParams: ForceDirectedBubblesParams
664
- fullChartParams: ChartParams
665
- }) {
666
- edgeGSelection.each((data,i,g) => {
667
- const gSelection = d3.select(g[i])
668
- gSelection.selectAll<SVGPathElement, ComputedEdge>('path')
669
- .data([data])
670
- .join(
671
- enter => {
672
- return enter
673
- .append('path')
674
- .classed(edgeArrowPathClassName, true)
675
- },
676
- update => {
677
- return update
678
- },
679
- exit => {
680
- return exit.remove()
681
- }
682
- )
683
- .attr('marker-end', d => `url(#${d.markerId})`)
684
- .attr('stroke', d => getDatumColor({ datum: d.data, colorType: fullParams.arrow.colorType, fullChartParams }))
685
- .attr('stroke-width', d => d.strokeWidth)
686
- .attr('style', d => fullParams.arrow.styleFn(d))
687
- })
688
-
689
- return edgeGSelection.select<SVGPathElement>(`path.${edgeArrowPathClassName}`)
690
- }
691
-
692
- function renderEdgeLabelG ({ edgeGSelection }: {
693
- edgeGSelection: d3.Selection<SVGGElement, any, any, unknown>
694
- }) {
695
- edgeGSelection.each((data,i,g) => {
696
- const gSelection = d3.select(g[i])
697
- gSelection.selectAll<SVGGElement, RenderEdge>('g')
698
- .data([data])
699
- .join(
700
- enter => {
701
- const enterSelection = enter
702
- .append('g')
703
- .classed(edgeLabelGClassName, true)
704
- // .attr('cursor', 'pointer')
705
- return enterSelection
706
- },
707
- update => {
708
- return update
709
- },
710
- exit => {
711
- return exit.remove()
712
- }
713
- )
714
- })
715
-
716
- return edgeGSelection.select<SVGTextElement>(`g.${edgeLabelGClassName}`)
717
- }
718
-
719
- function renderEdgeLabel ({ edgeLabelGSelection, fullParams, fullChartParams }: {
720
- edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
721
- fullParams: ForceDirectedBubblesParams
722
- fullChartParams: ChartParams
723
- }) {
724
- edgeLabelGSelection.each((data,i,g) => {
725
- const gSelection = d3.select(g[i])
726
- gSelection.selectAll<SVGTextElement, RenderEdge>('text')
727
- .data([data], d => d.id)
728
- .join(
729
- enter => {
730
- const enterSelection = enter
731
- .append('text')
732
- .classed(edgeLabelClassName, true)
733
- // .attr('cursor', 'pointer')
734
- .attr('text-anchor', 'middle')
735
- .attr('pointer-events', 'none')
736
- return enterSelection
737
- },
738
- update => {
739
- return update
740
- },
741
- exit => {
742
- return exit.remove()
743
- }
744
- )
745
- .text(d => d.label)
746
- .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.arrowLabel.colorType, fullChartParams }))
747
- .attr('font-size', fullChartParams.styles.textSize)
748
- .attr('style', d => fullParams.arrowLabel.styleFn(d))
749
- })
750
-
751
- return edgeLabelGSelection.select<SVGTextElement>(`text.${edgeLabelClassName}`)
752
- }
753
-
754
- function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
755
- nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
756
- edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
757
- fullChartParams: ChartParams
758
- highlightIds: string[]
759
- }) {
760
- nodeGSelection.interrupt('highlight')
761
- edgeGSelection.interrupt('highlight')
762
- // console.log(highlightIds)
763
- if (!highlightIds.length) {
764
- nodeGSelection
765
- .transition('highlight')
766
- .style('opacity', 1)
767
- edgeGSelection
768
- .transition('highlight')
769
- .style('opacity', 1)
770
- return
771
- }
772
-
773
- edgeGSelection
774
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
775
-
776
- nodeGSelection.each((d, i, n) => {
777
- const segment = d3.select(n[i])
778
-
779
- if (highlightIds.includes(d.id)) {
780
- segment
781
- .style('opacity', 1)
782
- .transition('highlight')
783
- .ease(d3.easeElastic)
784
- .duration(500)
785
- } else {
786
- // 取消
787
- segment
788
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
789
- }
790
- })
791
- }
792
-
793
- // 暫不處理edge的highlight
794
- // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
795
- // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
796
- // fullChartParams: ChartParams
797
- // highlightIds: string[]
798
- // }) {
799
- // edgeGSelection.interrupt('highlight')
800
-
801
- // if (!highlightIds.length) {
802
- // edgeGSelection
803
- // .transition('highlight')
804
- // .style('opacity', 1)
805
- // return
806
- // }
807
-
808
- // edgeGSelection.each((d, i, n) => {
809
- // const segment = d3.select(n[i])
810
-
811
- // if (highlightIds.includes(d.id)) {
812
- // segment
813
- // .style('opacity', 1)
814
- // .transition('highlight')
815
- // .ease(d3.easeElastic)
816
- // .duration(500)
817
- // } else {
818
- // // 取消放大
819
- // segment
820
- // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
821
- // }
822
- // })
823
- // }
824
-
825
- export const ForceDirectedBubbles = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
826
-
827
- const destroy$ = new Subject()
828
-
829
- const gSelection = selection.append('g').classed(gSelectionClassName, true)
830
- const defsSelection = gSelection.append('defs')
831
- const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
832
- const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
833
-
834
- let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
835
- let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
836
- // let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
837
- // let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
838
- let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
839
- let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
840
- let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
841
- let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
842
-
843
- const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
844
- const mouseEvent$ = new Subject<EventRelationship>()
845
-
846
-
847
- // init zoom
848
- const d3Zoom$ = observer.fullParams$.pipe(
849
- takeUntil(destroy$),
850
- // map(d => d.scaleExtent),
851
- // distinctUntilChanged((a, b) => String(a) === String(b)),
852
- // first(),
853
- map(data => {
854
- let d3Zoom = data.zoomable
855
- ? d3.zoom().on('zoom', (event) => {
856
- // console.log(event)
857
- // this.svgGroup.attr('transform', `translate(
858
- // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
859
- // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
860
- // ) scale(
861
- // ${event.transform.k}
862
- // )`)
863
- gSelection.attr('transform', `translate(
864
- ${event.transform.x},
865
- ${event.transform.y}
866
- ) scale(
867
- ${event.transform.k}
868
- )`)
869
-
870
- // if (data.node.labelSizeFixed && nodeLabelSelection) {
871
- // // 反向 scale 抵消掉放大縮小
872
- // nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
873
- // }
874
- if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
875
- // 反向 scale 抵消掉放大縮小
876
- edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
877
- }
878
- })
879
- : d3.zoom().on('zoom', null)
880
- if (data.scaleExtent) {
881
- d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
882
- }
883
- rootSelection.call(d3Zoom)
884
-
885
- return d3Zoom
886
- }),
887
- // shareReplay(1)
888
- )
889
-
890
- // zoom transform
891
- combineLatest({
892
- d3Zoom: d3Zoom$,
893
- transform: observer.fullParams$.pipe(
894
- takeUntil(destroy$),
895
- map(d => d.transform),
896
- )
897
- }).pipe(
898
- takeUntil(destroy$),
899
- switchMap(async d => d)
900
- ).subscribe(data => {
901
- // console.log('call')
902
- selection.call(
903
- data.d3Zoom.transform, d3.zoomIdentity
904
- .translate(data.transform.x, data.transform.y)
905
- .scale(data.transform.k)
906
- )
907
- })
908
-
909
-
910
- const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
911
- layout: observer.layout$.pipe(
912
- first() // 只使用第一次的尺寸(置中)
913
- ),
914
- fullParams: observer.fullParams$
915
- }).pipe(
916
- takeUntil(destroy$),
917
- switchMap(async d => d),
918
- map(data => createSimulation(data.layout, data.fullParams)),
919
- shareReplay(1)
920
- )
921
-
922
- const nodeMinMaxValue$ = observer.computedData$.pipe(
923
- takeUntil(destroy$),
924
- map(data => {
925
- const hadValueData = data.nodes.filter(d => d.value != undefined)
926
- if (!hadValueData.length) {
927
- return [0, 2] // 給預設值
928
- }
929
- const minMax = getMinMax(data.nodes.map(d => d.value))
930
- if (hadValueData.length == 1 || minMax[0] === minMax[1]) {
931
- return [minMax[0] - 1, minMax[1] + 1] // 避免最大最小值相同
932
- }
933
- return minMax
934
- }),
935
- shareReplay(1)
936
- )
937
-
938
- const edgeMinMaxValue$ = observer.computedData$.pipe(
939
- takeUntil(destroy$),
940
- map(data => {
941
- const hadValueData = data.edges.filter(d => d.value != undefined)
942
- if (!hadValueData.length) {
943
- return [0, 2] // 給預設值
944
- }
945
- const minMax = getMinMax(data.edges.map(d => d.value))
946
- if (hadValueData.length == 1 || minMax[0] === minMax[1]) {
947
- return [minMax[0] - 1, minMax[1] + 1] // 避免最大最小值相同
948
- }
949
- return minMax
950
- }),
951
- shareReplay(1)
952
- )
953
-
954
- // 當無value時給的預設值
955
- const defaultNodeValue$ = nodeMinMaxValue$.pipe(
956
- takeUntil(destroy$),
957
- map(data => (data[1] - data[0]) / 2) // 預設值為最大及最小的中間值
958
- )
959
-
960
- // 當無value時給的預設值
961
- const defaultEdgeValue$ = edgeMinMaxValue$.pipe(
962
- takeUntil(destroy$),
963
- map(data => (data[1] - data[0]) / 2) // 預設值為最大及最小的中間值
964
- )
965
-
966
- const radiusScale$ = combineLatest({
967
- nodeMinMaxValue: nodeMinMaxValue$,
968
- fullParams: observer.fullParams$
969
- }).pipe(
970
- takeUntil(destroy$),
971
- switchMap(async (d) => d),
972
- map(data => {
973
- // console.log({ totalR: data.totalR, totalValue: data.totalValue })
974
- return d3.scalePow()
975
- .domain(data.nodeMinMaxValue)
976
- .range([data.fullParams.bubble.radiusMin, data.fullParams.bubble.radiusMax])
977
- .exponent(data.fullParams.bubble.arcScaleType === 'area'
978
- ? 0.5 // 數值映射面積(0.5為取平方根)
979
- : 1 // 數值映射半徑
980
- )
981
- })
982
- )
983
-
984
- const strokeWidthScale$ = combineLatest({
985
- edgeMinMaxValue: edgeMinMaxValue$,
986
- fullParams: observer.fullParams$
987
- }).pipe(
988
- takeUntil(destroy$),
989
- switchMap(async (d) => d),
990
- map(data => {
991
- return d3.scaleLinear()
992
- .domain(data.edgeMinMaxValue)
993
- .range([data.fullParams.arrow.strokeWidthMin, data.fullParams.arrow.strokeWidthMax])
994
- })
995
- )
996
-
997
- // 先將未篩選的資料全部儲起來,就不會因為 visibleFilter 而重新計算
998
- const RenderNodeMap$: Observable<Map<string, RenderNode>> = combineLatest({
999
- computedData: observer.computedData$,
1000
- radiusScale: radiusScale$,
1001
- defaultNodeValue: defaultNodeValue$,
1002
- }).pipe(
1003
- takeUntil(destroy$),
1004
- switchMap(async (d) => d),
1005
- map(data => {
1006
- return new Map(
1007
- data.computedData.nodes.map(_d => {
1008
- let d: RenderNode = _d as RenderNode
1009
- d.r = data.radiusScale(d.value ?? data.defaultNodeValue)
1010
- return [d.id, d]
1011
- })
1012
- )
1013
- }),
1014
- )
1015
-
1016
- // 先將未篩選的資料全部儲起來,就不會因為 visibleFilter 而重新計算
1017
- const RenderEdgeMap$: Observable<Map<string, RenderEdge>> = combineLatest({
1018
- computedData: observer.computedData$,
1019
- strokeWidthScale: strokeWidthScale$,
1020
- defaultEdgeValue: defaultEdgeValue$
1021
- }).pipe(
1022
- takeUntil(destroy$),
1023
- switchMap(async (d) => d),
1024
- map(data => {
1025
- return new Map(
1026
- data.computedData.edges.map(_d => {
1027
- let d: any = _d as RenderEdge
1028
- d.source = _d.startNode // reference
1029
- d.target = _d.endNode
1030
- d.strokeWidth = data.strokeWidthScale(d.value ?? data.defaultEdgeValue)
1031
- d.markerId = `${defsArrowMarkerId}__${d.id}`
1032
- return [d.id, d]
1033
- })
1034
- )
1035
- }),
1036
- )
1037
-
1038
- const renderData$: Observable<RenderData> = combineLatest({
1039
- visibleComputedData: observer.visibleComputedData$,
1040
- RenderNodeMap: RenderNodeMap$,
1041
- RenderEdgeMap: RenderEdgeMap$,
1042
- }).pipe(
1043
- takeUntil(destroy$),
1044
- switchMap(async (d) => d),
1045
- map(data => {
1046
- return {
1047
- nodes: data.visibleComputedData.nodes.map(d => data.RenderNodeMap.get(d.id)!),
1048
- edges: data.visibleComputedData.edges.map(d => data.RenderEdgeMap.get(d.id)!),
1049
- }
1050
- }),
1051
- shareReplay(1)
1052
- )
1053
-
1054
- const markerParams$: Observable<MarkerParams> = observer.fullParams$.pipe(
1055
- takeUntil(destroy$),
1056
- map(fullParams => {
1057
- return {
1058
- viewBox: `-${fullParams.arrow.pointerWidth} -${fullParams.arrow.pointerHeight / 2} ${fullParams.arrow.pointerWidth} ${fullParams.arrow.pointerHeight}`,
1059
- d: `M${-fullParams.arrow.pointerWidth},${-fullParams.arrow.pointerHeight / 2}L0,0L${-fullParams.arrow.pointerWidth},${fullParams.arrow.pointerHeight / 2}`, // 箭頭的尖端為(0,0)
1060
- pointerWidth: fullParams.arrow.pointerWidth,
1061
- pointerHeight: fullParams.arrow.pointerHeight,
1062
- }
1063
- })
1064
- )
1065
-
1066
- const markerData$: Observable<MarkerDatum[]> = combineLatest({
1067
- computedData: observer.computedData$,
1068
- fullParams: observer.fullParams$,
1069
- RenderNodeMap: RenderNodeMap$,
1070
- RenderEdgeMap: RenderEdgeMap$,
1071
- }).pipe(
1072
- takeUntil(destroy$),
1073
- switchMap(async d => d),
1074
- map(data => {
1075
- return data.computedData.edges.map(d => {
1076
- const renderEdge = data.RenderEdgeMap.get(d.id)!
1077
- const renderEndNode = data.RenderNodeMap.get(d.endNode.id)!
1078
- return {
1079
- id: renderEdge.markerId,
1080
- edgeId: d.id,
1081
- strokeWidth: renderEdge.strokeWidth,
1082
- /* refX:修正marker位置(計算出和circle半徑相等的寬度)
1083
- (1)circle半徑需加上 strokeWidth/2 是因為框線是以 circle 的邊緣往內及往外擴展,所以 stroke 多出來的寬度是一半而已
1084
- (2)circle半徑需除以 path 寬度是因為「marker 的位置會受到 path 的stroke-width影響」,所以要進行修正
1085
- (3)- 1 是要修正奇怪的誤差(不知原因)
1086
- */
1087
- refX: ((renderEndNode.r + (data.fullParams.bubble.strokeWidth / 2)) / renderEdge.strokeWidth) - 1
1088
- }
1089
- })
1090
- }),
1091
- )
1092
-
1093
- // <marker> marker selection
1094
- combineLatest({
1095
- defsSelection,
1096
- markerParams: markerParams$,
1097
- markerData: markerData$,
1098
- fullParams: observer.fullParams$,
1099
- fullChartParams: observer.fullChartParams$
1100
- }).pipe(
1101
- takeUntil(destroy$),
1102
- switchMap(async d => d),
1103
- map(data => {
1104
- return renderArrowMarker({
1105
- defsSelection,
1106
- markerParams: data.markerParams,
1107
- markerData: data.markerData,
1108
- fullParams: data.fullParams,
1109
- fullChartParams: data.fullChartParams
1110
- })
1111
- })
1112
- ).subscribe()
1113
-
1114
-
1115
- combineLatest({
1116
- renderData: renderData$,
1117
- computedData: observer.computedData$,
1118
- CategoryNodeMap: observer.CategoryNodeMap$,
1119
- simulation: simulation$,
1120
- fullParams: observer.fullParams$,
1121
- fullChartParams: observer.fullChartParams$
1122
- }).pipe(
1123
- takeUntil(destroy$),
1124
- switchMap(async d => d),
1125
- ).subscribe(data => {
1126
-
1127
- nodeGSelection = renderNodeG({
1128
- nodeListGSelection: nodeListGSelection,
1129
- nodes: data.renderData.nodes as RenderNode[],
1130
- })
1131
-
1132
- nodeCircleSelection = renderNodeCircle({
1133
- nodeGSelection: nodeGSelection,
1134
- fullParams: data.fullParams,
1135
- fullChartParams: data.fullChartParams
1136
- })
1137
- nodeGSelection.call(drag(data.simulation, dragStatus$))
1138
-
1139
- // nodeLabelGSelection = renderNodeLabelG({
1140
- // nodeGSelection: nodeGSelection,
1141
- // })
1142
-
1143
- // nodeLabelSelection = renderNodeLabel({
1144
- // nodeLabelGSelection: nodeLabelGSelection,
1145
- // fullParams: data.fullParams,
1146
- // fullChartParams: data.fullChartParams
1147
- // })
1148
-
1149
- edgeGSelection = renderEdgeG({
1150
- edgeListGSelection: edgeListGSelection,
1151
- edges: data.renderData.edges
1152
- })
1153
-
1154
- edgeArrowSelection = renderEdgeArrowPath({
1155
- edgeGSelection: edgeGSelection,
1156
- fullParams: data.fullParams,
1157
- fullChartParams: data.fullChartParams
1158
- })
1159
-
1160
- edgeLabelGSelection = renderEdgeLabelG({
1161
- edgeGSelection: edgeGSelection,
1162
- })
1163
-
1164
- edgeLabelSelection = renderEdgeLabel({
1165
- edgeLabelGSelection: edgeLabelGSelection,
1166
- fullParams: data.fullParams,
1167
- fullChartParams: data.fullChartParams
1168
- })
1169
-
1170
- data.simulation.nodes(data.renderData.nodes)
1171
- .on('tick', () => {
1172
- edgeArrowSelection.attr('d', linkArcFn)
1173
- nodeGSelection.attr('transform', translateFn)
1174
- // nodeLabelGSelection.attr('transform', d => translateFn({
1175
- // x: d.x,
1176
- // y: d.y - d.r - 10
1177
- // }))
1178
- edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
1179
- })
1180
- ;(data.simulation.force("link") as any).links(data.renderData.edges)
1181
-
1182
- data.simulation.alpha(0.3).restart()
1183
-
1184
- nodeCircleSelection
1185
- .on('mouseover', (event, datum) => {
1186
- event.stopPropagation()
1187
-
1188
- mouseEvent$.next({
1189
- type: 'relationship',
1190
- eventName: 'mouseover',
1191
- pluginName,
1192
- highlightTarget: data.fullChartParams.highlightTarget,
1193
- datum: datum,
1194
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1195
- categoryIndex: datum.categoryIndex,
1196
- categoryLabel: datum.categoryLabel,
1197
- event,
1198
- data: data.computedData
1199
- })
1200
- })
1201
- .on('mousemove', (event, datum) => {
1202
- event.stopPropagation()
1203
-
1204
- mouseEvent$.next({
1205
- type: 'relationship',
1206
- eventName: 'mousemove',
1207
- pluginName,
1208
- highlightTarget: data.fullChartParams.highlightTarget,
1209
- datum: datum,
1210
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1211
- categoryIndex: datum.categoryIndex,
1212
- categoryLabel: datum.categoryLabel,
1213
- event,
1214
- data: data.computedData
1215
- })
1216
- })
1217
- .on('mouseout', (event, datum) => {
1218
- event.stopPropagation()
1219
-
1220
- mouseEvent$.next({
1221
- type: 'relationship',
1222
- eventName: 'mouseout',
1223
- pluginName,
1224
- highlightTarget: data.fullChartParams.highlightTarget,
1225
- datum: datum,
1226
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1227
- categoryIndex: datum.categoryIndex,
1228
- categoryLabel: datum.categoryLabel,
1229
- event,
1230
- data: data.computedData
1231
- })
1232
- })
1233
- .on('click', (event, datum) => {
1234
- event.stopPropagation()
1235
-
1236
- mouseEvent$.next({
1237
- type: 'relationship',
1238
- eventName: 'click',
1239
- pluginName,
1240
- highlightTarget: data.fullChartParams.highlightTarget,
1241
- datum: datum,
1242
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1243
- categoryIndex: datum.categoryIndex,
1244
- categoryLabel: datum.categoryLabel,
1245
- event,
1246
- data: data.computedData
1247
- })
1248
- })
1249
- })
1250
-
1251
- dragStatus$.pipe(
1252
- distinctUntilChanged((a, b) => a === b),
1253
- // 只有沒有托曳時才執行
1254
- switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1255
- ).subscribe(data => {
1256
- subject.event$.next(data)
1257
- })
1258
-
1259
- combineLatest({
1260
- renderData: renderData$,
1261
- highlightNodes: observer.relationshipHighlightNodes$.pipe(
1262
- map(data => data.map(d => d.id))
1263
- ),
1264
- highlightEdges: observer.relationshipHighlightEdges$.pipe(
1265
- map(data => data.map(d => d.id))
1266
- ),
1267
- fullChartParams: observer.fullChartParams$,
1268
- fullParams: observer.fullParams$,
1269
- }).pipe(
1270
- takeUntil(destroy$),
1271
- switchMap(async d => d)
1272
- ).subscribe(data => {
1273
- if (!nodeGSelection || !edgeGSelection) {
1274
- return
1275
- }
1276
-
1277
- highlightNodes({
1278
- nodeGSelection,
1279
- edgeGSelection,
1280
- highlightIds: data.highlightNodes,
1281
- fullChartParams: data.fullChartParams
1282
- })
1283
- // highlightEdges({
1284
- // edgeGSelection,
1285
- // highlightIds: data.highlightEdges,
1286
- // fullChartParams: data.fullChartParams
1287
- // })
1288
- })
1289
-
1290
-
1291
-
1292
- return () => {
1293
- destroy$.next(undefined)
1294
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ of,
4
+ combineLatest,
5
+ map,
6
+ switchMap,
7
+ first,
8
+ takeUntil,
9
+ Subject,
10
+ BehaviorSubject,
11
+ Observable,
12
+ distinctUntilChanged,
13
+ shareReplay,
14
+ take,
15
+ share,
16
+ filter,
17
+ iif,
18
+ EMPTY
19
+ } from 'rxjs'
20
+ import type { DefinePluginConfig } from '../../../lib/core-types'
21
+ import type {
22
+ ChartParams,
23
+ DatumValue,
24
+ DataSeries,
25
+ EventName,
26
+ EventRelationship,
27
+ ComputedDataSeries,
28
+ ComputedNode,
29
+ ComputedEdge,
30
+ ContainerPosition,
31
+ Layout
32
+ } from '../../../lib/core-types'
33
+ import {
34
+ defineRelationshipPlugin,
35
+ getMinMax
36
+ } from '../../../lib/core'
37
+ import type { BubblesParams, ArcScaleType, ForceDirectedBubblesParams } from '../../../lib/plugins-basic-types'
38
+ import { getColor, getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
39
+ import { DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS } from '../defaults'
40
+ // import { renderCircleText } from '../../utils/d3Graphics'
41
+ import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
42
+ import { renderCircleText } from '../../utils/d3Graphics'
43
+
44
+ // interface BubblesDatum extends ComputedNode {
45
+ // x: number
46
+ // y: number
47
+ // r: number
48
+ // _originR: number // 紀錄變化前的r
49
+ // }
50
+
51
+ type Zoom = {
52
+ xOffset: number
53
+ yOffset: number
54
+ scaleExtent: {
55
+ min: number
56
+ max: number
57
+ }
58
+ }
59
+
60
+ // d3 forceSimulation使用的node資料
61
+ type RenderNode = d3.SimulationNodeDatum & ComputedNode & {
62
+ r: number
63
+ }
64
+
65
+ // d3 forceSimulation使用的edge資料
66
+ interface RenderEdge extends ComputedEdge {
67
+ source: RenderNode
68
+ target: RenderNode
69
+ strokeWidth: number
70
+ markerId: string
71
+ }
72
+
73
+ // d3 forceSimulation使用的資料
74
+ type RenderData = {
75
+ nodes: (ComputedNode | RenderNode)[] // 經過d3 forceSimulation計算後的node才有座標資訊
76
+ edges: RenderEdge[]
77
+ }
78
+
79
+ interface D3DragEvent {
80
+ active: number
81
+ dx: number
82
+ dy: number
83
+ identifier: string
84
+ sourceEvent: MouseEvent
85
+ subject: RenderNode
86
+ target: any
87
+ type: string
88
+ x: number
89
+ y: number
90
+ }
91
+
92
+ type DragStatus = 'start' | 'drag' | 'end'
93
+
94
+ interface MarkerParams {
95
+ viewBox: string
96
+ d: string
97
+ pointerWidth: number
98
+ pointerHeight: number
99
+ // refX: number
100
+ }
101
+
102
+ interface MarkerDatum {
103
+ id: string
104
+ edgeId: string
105
+ strokeWidth: number
106
+ refX: number
107
+ }
108
+
109
+ // type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
110
+
111
+ const pluginName = 'ForceDirectedBubbles'
112
+
113
+ const baseLineHeight = 12 // 未變形前的字體大小(代入計算用而已,數字多少都不會有影響)
114
+
115
+ const gSelectionClassName = getClassName(pluginName, 'zoom-area')
116
+ const defsArrowMarkerId = getUniID(pluginName, 'arrow')
117
+ const defsArrowMarkerClassName = getClassName(pluginName, 'arrow-marker')
118
+ const edgeListGClassName = getClassName(pluginName, 'edge-list-g')
119
+ const edgeGClassName = getClassName(pluginName, 'edge-g')
120
+ const edgeArrowPathClassName = getClassName(pluginName, 'edge-arrow-path')
121
+ const edgeLabelGClassName = getClassName(pluginName, 'edge-label-g')
122
+ const edgeLabelClassName = getClassName(pluginName, 'edge-label')
123
+ const nodeListGClassName = getClassName(pluginName, 'node-list-g')
124
+ const nodeGClassName = getClassName(pluginName, 'node-g')
125
+ const nodeCircleClassName = getClassName(pluginName, 'node-circle')
126
+ // const nodeLabelGClassName = getClassName(pluginName, 'node-label-g')
127
+ // const nodeLabelClassName = getClassName(pluginName, 'node-label')
128
+
129
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS> = {
130
+ name: pluginName,
131
+ defaultParams: DEFAULT_FORCE_DIRECTED_BUBBLES_PARAMS,
132
+ layerIndex: LAYER_INDEX_OF_GRAPHIC,
133
+ validator: (params, { validateColumns }) => {
134
+ const result = validateColumns(params, {
135
+ bubble: {
136
+ toBeTypes: ['object']
137
+ },
138
+ bubbleLabel: {
139
+ toBeTypes: ['object']
140
+ },
141
+ arrow: {
142
+ toBeTypes: ['object']
143
+ },
144
+ arrowLabel: {
145
+ toBeTypes: ['object']
146
+ },
147
+ force: {
148
+ toBeTypes: ['object']
149
+ },
150
+ zoomable: {
151
+ toBeTypes: ['boolean']
152
+ },
153
+ transform: {
154
+ toBeTypes: ['object']
155
+ },
156
+ scaleExtent: {
157
+ toBeTypes: ['object']
158
+ }
159
+ })
160
+ if (params.bubble) {
161
+ const bubbleResult = validateColumns(params.bubble, {
162
+ radiusMin: {
163
+ toBeTypes: ['number']
164
+ },
165
+ radiusMax: {
166
+ toBeTypes: ['number']
167
+ },
168
+ arcScaleType: {
169
+ toBe: '"area" | "radius"',
170
+ test: (value) => value === 'area' || value === 'radius'
171
+ },
172
+ fillColorType: {
173
+ toBeOption: 'ColorType'
174
+ },
175
+ strokeColorType: {
176
+ toBeOption: 'ColorType'
177
+ },
178
+ strokeWidth: {
179
+ toBeTypes: ['number']
180
+ },
181
+ styleFn: {
182
+ toBeTypes: ['Function']
183
+ },
184
+ })
185
+ if (bubbleResult.status === 'error') {
186
+ return bubbleResult
187
+ }
188
+ }
189
+ if (params.bubbleLabel) {
190
+ const bubbleLabelResult = validateColumns(params.bubbleLabel, {
191
+ fillRate: {
192
+ toBeTypes: ['number']
193
+ },
194
+ lineHeight: {
195
+ toBeTypes: ['number']
196
+ },
197
+ maxLineLength: {
198
+ toBeTypes: ['number']
199
+ },
200
+ colorType: {
201
+ toBeOption: 'ColorType'
202
+ },
203
+ styleFn: {
204
+ toBeTypes: ['Function']
205
+ },
206
+ })
207
+ if (bubbleLabelResult.status === 'error') {
208
+ return bubbleLabelResult
209
+ }
210
+ }
211
+ if (params.arrow) {
212
+ const arrowResult = validateColumns(params.arrow, {
213
+ colorType: {
214
+ toBeOption: 'ColorType'
215
+ },
216
+ strokeWidthMin: {
217
+ toBeTypes: ['number']
218
+ },
219
+ strokeWidthMax: {
220
+ toBeTypes: ['number']
221
+ },
222
+ pointerWidth: {
223
+ toBeTypes: ['number']
224
+ },
225
+ pointerHeight: {
226
+ toBeTypes: ['number']
227
+ },
228
+ styleFn: {
229
+ toBeTypes: ['Function']
230
+ },
231
+ })
232
+ if (arrowResult.status === 'error') {
233
+ return arrowResult
234
+ }
235
+ }
236
+ if (params.arrowLabel) {
237
+ const arrowLabelResult = validateColumns(params.arrowLabel, {
238
+ colorType: {
239
+ toBeOption: 'ColorType'
240
+ },
241
+ sizeFixed: {
242
+ toBeTypes: ['boolean']
243
+ },
244
+ styleFn: {
245
+ toBeTypes: ['Function']
246
+ },
247
+ })
248
+ if (arrowLabelResult.status === 'error') {
249
+ return arrowLabelResult
250
+ }
251
+ }
252
+ if (params.force) {
253
+ const forceResult = validateColumns(params.force, {
254
+ nodeStrength: {
255
+ toBeTypes: ['number']
256
+ },
257
+ linkDistance: {
258
+ toBeTypes: ['number']
259
+ },
260
+ velocityDecay: {
261
+ toBeTypes: ['number']
262
+ },
263
+ alphaDecay: {
264
+ toBeTypes: ['number']
265
+ },
266
+ })
267
+ if (forceResult.status === 'error') {
268
+ return forceResult
269
+ }
270
+ }
271
+ if (params.transform) {
272
+ const transformResult = validateColumns(params.transform, {
273
+ x: {
274
+ toBeTypes: ['number']
275
+ },
276
+ y: {
277
+ toBeTypes: ['number']
278
+ },
279
+ k: {
280
+ toBeTypes: ['number']
281
+ },
282
+ })
283
+ if (transformResult.status === 'error') {
284
+ return transformResult
285
+ }
286
+ }
287
+ if (params.scaleExtent) {
288
+ const scaleExtentResult = validateColumns(params.scaleExtent, {
289
+ min: {
290
+ toBeTypes: ['number']
291
+ },
292
+ max: {
293
+ toBeTypes: ['number']
294
+ },
295
+ })
296
+ if (scaleExtentResult.status === 'error') {
297
+ return scaleExtentResult
298
+ }
299
+ }
300
+ return result
301
+ }
302
+ }
303
+
304
+ // let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
305
+
306
+ function createSimulation (layout: Layout, fullParams: ForceDirectedBubblesParams) {
307
+ return d3.forceSimulation()
308
+ .velocityDecay(fullParams.force.velocityDecay)
309
+ .alphaDecay(fullParams.force.alphaDecay)
310
+ .force(
311
+ "link",
312
+ d3.forceLink()
313
+ .id((d: d3.SimulationNodeDatum & ComputedNode) => d.id)
314
+ .strength(1)
315
+ .distance((d: d3.SimulationLinkDatum<d3.SimulationNodeDatum & ComputedNode>) => {
316
+ // if (d.direction === 'top') {
317
+ // return 200
318
+ // } else {
319
+ // return 250
320
+ // }
321
+ return fullParams.force.linkDistance
322
+ })
323
+ )
324
+ .force("charge", d3.forceManyBody().strength(fullParams.force.nodeStrength))
325
+ .force("collision", d3.forceCollide(fullParams.bubble.radiusMax).strength(1)) // 先以最大值設定
326
+ .force("center", d3.forceCenter(layout.width / 2, layout.height / 2))
327
+
328
+ }
329
+
330
+ function translateFn (d: any): string {
331
+ // console.log('translateFn', d)
332
+ return "translate(" + d.x + "," + d.y + ")";
333
+ }
334
+
335
+ function translateCenterFn (d: any): string {
336
+ // console.log('translateCenterFn', d)
337
+ const x = d.source.x + ((d.target.x - d.source.x) / 2) // 置中的話除2
338
+ const y = d.source.y + ((d.target.y - d.source.y) / 2) // 置中的話除2
339
+ return "translate(" + x + "," + y + ")";
340
+ }
341
+
342
+ function linkArcFn (d: RenderEdge): string {
343
+ // console.log('linkArcFn', d)
344
+
345
+ // const dx = d.target.x - d.source.x,
346
+ // dy = d.target.y - d.source.y
347
+ // dr讓方向線變成有弧度的
348
+ // dr = Math.sqrt(dx * dx + dy * dy);
349
+ // return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
350
+
351
+ // 直線
352
+ return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y;
353
+
354
+
355
+ }
356
+
357
+
358
+
359
+ function renderArrowMarker ({ defsSelection, markerParams, markerData, fullParams, fullChartParams }: {
360
+ defsSelection: d3.Selection<SVGDefsElement, any, any, unknown>
361
+ markerParams: MarkerParams
362
+ markerData: MarkerDatum[]
363
+ fullParams: ForceDirectedBubblesParams
364
+ fullChartParams: ChartParams
365
+ }) {
366
+ return defsSelection
367
+ .selectAll<SVGMarkerElement, any>(`marker.${defsArrowMarkerClassName}`)
368
+ .data(markerData)
369
+ .join(
370
+ enter => {
371
+ const enterSelection = enter
372
+ .append("marker")
373
+ .classed(defsArrowMarkerClassName, true)
374
+ .attr("viewBox", markerParams.viewBox)
375
+ .attr("orient", "auto")
376
+ enterSelection.append("path")
377
+ .attr("d", markerParams.d)
378
+ return enterSelection
379
+ },
380
+ update => {
381
+ return update
382
+ },
383
+ exit => {
384
+ return exit.remove()
385
+ }
386
+ )
387
+ .attr('id', d => d.id)
388
+ .attr('fill', d => getColor(fullParams.arrow.colorType, fullChartParams ))
389
+ .attr("markerWidth", markerParams.pointerWidth)
390
+ .attr("markerHeight", markerParams.pointerHeight)
391
+ .attr('refX', d => d.refX)
392
+ .attr("refY", 0)
393
+
394
+
395
+ }
396
+
397
+ // function drag (): d3.DragBehavior<Element, unknown, unknown> {
398
+ // let originHighlightLockMode: boolean // 拖拽前的highlightLockMode
399
+
400
+ // return d3.drag()
401
+ // .on("start", (event: D3DragEvent) => {
402
+ // console.log('start', event.sourceEvent)
403
+ // // if (this.params.lockMode) {
404
+ // // return
405
+ // // }
406
+ // // if (!d3.event.active) {
407
+ // // this.forceRestart()
408
+ // // }
409
+ // // d.fx = d.x
410
+ // // d.fy = d.y
411
+
412
+ // // // 鎖定模式才不會在拖拽過程式觸發到其他事件造成衝突
413
+ // // originHighlightLockMode = this.highlightLockMode
414
+ // // this.highlightLockMode = true
415
+ // // this.noneStopMode = true
416
+ // // // 動畫會有點卡住所以乾脆拿掉
417
+ // // if(this.tooltip != null) {
418
+ // // this.tooltip.remove()
419
+ // // }
420
+ // })
421
+ // .on("drag", function (event: D3DragEvent) {
422
+ // console.log('drag', event)
423
+ // // if (this.params.lockMode) {
424
+ // // return
425
+ // // }
426
+ // // if (!d3.event.active) {
427
+ // // this.force.alphaTarget(0)
428
+ // // }
429
+ // // d.fx = d3.event.x
430
+ // // d.fy = d3.event.y
431
+ // // d3.select(this).attr({
432
+ // // 'cx': event.x,
433
+ // // 'cy': event.y,
434
+ // // })
435
+ // d3.select(this)
436
+ // .attr('fx', event.x)
437
+ // .attr('fy', event.y)
438
+ // })
439
+ // .on("end", (event: D3DragEvent) => {
440
+ // console.log('end', event)
441
+ // // if (this.params.lockMode) {
442
+ // // return
443
+ // // }
444
+ // // d.fx = null
445
+ // // d.fy = null
446
+
447
+ // // this.highlightLockMode = originHighlightLockMode // 還原拖拽前的highlightLockMode
448
+ // // this.noneStopMode = false
449
+ // // if (this.highlightLockMode) {
450
+ // // this.forceStop()
451
+ // // }
452
+ // })
453
+ // }
454
+
455
+ function drag (simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>, dragStatus$: BehaviorSubject<DragStatus>) {
456
+ function dragstarted (event: D3DragEvent, node: RenderNode) {
457
+ if (!event.active) {
458
+ simulation.alphaTarget(0.3).restart()
459
+ }
460
+ event.subject.fx = event.subject.x
461
+ event.subject.fy = event.subject.y
462
+
463
+ dragStatus$.next('start')
464
+ }
465
+
466
+ function dragged (event: D3DragEvent, node: RenderNode) {
467
+ event.subject.fx = event.x
468
+ event.subject.fy = event.y
469
+
470
+ dragStatus$.next('drag')
471
+ }
472
+
473
+ function dragended (event: D3DragEvent, node: RenderNode) {
474
+ if (!event.active) {
475
+ simulation.alphaTarget(0)
476
+ }
477
+ event.subject.fx = null
478
+ event.subject.fy = null
479
+
480
+ dragStatus$.next('end')
481
+ }
482
+
483
+ return d3.drag()
484
+ .on("start", dragstarted)
485
+ .on("drag", dragged)
486
+ .on("end", dragended)
487
+ }
488
+
489
+ function renderNodeG ({ nodeListGSelection, nodes }: {
490
+ nodeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
491
+ nodes: RenderNode[]
492
+ }) {
493
+ return nodeListGSelection.selectAll<SVGGElement, RenderNode>('g')
494
+ .data(nodes, d => d.id)
495
+ .join(
496
+ enter => {
497
+ const enterSelection = enter
498
+ .append('g')
499
+ .classed(nodeGClassName, true)
500
+ // .attr('cursor', 'pointer')
501
+ return enterSelection
502
+ },
503
+ update => {
504
+ return update
505
+ },
506
+ exit => {
507
+ return exit.remove()
508
+ }
509
+ )
510
+ }
511
+
512
+ function renderNodeCircle ({ nodeGSelection, fullParams, fullChartParams }: {
513
+ nodeGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
514
+ fullParams: ForceDirectedBubblesParams
515
+ fullChartParams: ChartParams
516
+ }) {
517
+ nodeGSelection
518
+ .each((data,i,g) => {
519
+ const gSelection = d3.select(g[i])
520
+ gSelection.selectAll<SVGCircleElement, ComputedEdge>('circle')
521
+ .data([data])
522
+ .join(
523
+ enter => {
524
+ const enterSelection = enter
525
+ .append('circle')
526
+ .classed(nodeCircleClassName, true)
527
+ .attr('cursor', 'pointer')
528
+ return enterSelection
529
+ },
530
+ update => {
531
+ return update
532
+ },
533
+ exit => {
534
+ return exit.remove()
535
+ }
536
+ )
537
+ .attr('r', d => d.r)
538
+ .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.bubble.fillColorType, fullChartParams }))
539
+ .attr('stroke', d => getDatumColor({ datum: d, colorType: fullParams.bubble.strokeColorType, fullChartParams }))
540
+ .attr('stroke-width', fullParams.bubble.strokeWidth)
541
+ .attr('style', d => fullParams.bubble.styleFn(d))
542
+
543
+ })
544
+ .attr("text-anchor", "middle")
545
+ .attr('font-size', baseLineHeight)
546
+ .each((d,i,g) => {
547
+ const gSelection = d3.select(g[i])
548
+
549
+ gSelection.call(renderCircleText, {
550
+ text: d.label,
551
+ radius: d.r * fullParams.bubbleLabel.fillRate,
552
+ lineHeight: baseLineHeight * fullParams.bubbleLabel.lineHeight,
553
+ isBreakAll: d.label.length <= fullParams.bubbleLabel.maxLineLength
554
+ ? false
555
+ : fullParams.bubbleLabel.wordBreakAll
556
+ })
557
+
558
+ // -- text color --
559
+ gSelection.select('text').attr('fill', _ => getDatumColor({
560
+ datum: d,
561
+ colorType: fullParams.bubbleLabel.colorType,
562
+ fullChartParams: fullChartParams
563
+ }))
564
+
565
+
566
+ })
567
+
568
+ nodeGSelection.select('text')
569
+ .attr('pointer-events', 'none')
570
+ .attr('style', d => fullParams.bubbleLabel.styleFn(d))
571
+
572
+ return nodeGSelection.select<SVGCircleElement>(`circle.${nodeCircleClassName}`)
573
+ }
574
+
575
+ // function renderNodeLabelG ({ nodeGSelection }: {
576
+ // nodeGSelection: d3.Selection<SVGGElement, any, any, unknown>
577
+ // }) {
578
+ // nodeGSelection.each((data,i,g) => {
579
+ // const gSelection = d3.select(g[i])
580
+ // gSelection.selectAll<SVGGElement, RenderNode>('g')
581
+ // .data([data])
582
+ // .join(
583
+ // enter => {
584
+ // const enterSelection = enter
585
+ // .append('g')
586
+ // .classed(nodeLabelGClassName, true)
587
+ // // .attr('cursor', 'pointer')
588
+ // return enterSelection
589
+ // },
590
+ // update => {
591
+ // return update
592
+ // },
593
+ // exit => {
594
+ // return exit.remove()
595
+ // }
596
+ // )
597
+ // })
598
+
599
+ // return nodeGSelection.select<SVGTextElement>(`g.${nodeLabelGClassName}`)
600
+ // }
601
+
602
+ // function renderNodeLabel ({ nodeLabelGSelection, fullParams, fullChartParams }: {
603
+ // nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
604
+ // fullParams: ForceDirectedBubblesParams
605
+ // fullChartParams: ChartParams
606
+ // }) {
607
+ // nodeLabelGSelection.each((data,i,g) => {
608
+ // const gSelection = d3.select(g[i])
609
+ // gSelection.selectAll<SVGTextElement, RenderNode>('text')
610
+ // .data([data], d => d.id)
611
+ // .join(
612
+ // enter => {
613
+ // const enterSelection = enter
614
+ // .append('text')
615
+ // .classed(nodeLabelClassName, true)
616
+ // // .attr('cursor', 'pointer')
617
+ // .attr('text-anchor', 'middle')
618
+ // .attr('pointer-events', 'none')
619
+ // return enterSelection
620
+ // },
621
+ // update => {
622
+ // return update
623
+ // },
624
+ // exit => {
625
+ // return exit.remove()
626
+ // }
627
+ // )
628
+ // .text(d => d.label)
629
+ // .attr('transform', d => `translate(0, ${- d.r - 10})`)
630
+ // .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.node.labelColorType, fullChartParams }))
631
+ // .attr('font-size', fullChartParams.styles.textSize)
632
+ // .attr('style', d => fullParams.node.labelStyleFn(d))
633
+ // })
634
+
635
+ // return nodeLabelGSelection.select<SVGTextElement>(`text.${nodeLabelClassName}`)
636
+ // }
637
+
638
+ function renderEdgeG ({ edgeListGSelection, edges }: {
639
+ edgeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
640
+ edges: RenderEdge[]
641
+ }) {
642
+ return edgeListGSelection.selectAll<SVGGElement, RenderEdge>('g')
643
+ .data(edges, d => d.id)
644
+ .join(
645
+ enter => {
646
+ const enterSelection = enter
647
+ .append('g')
648
+ .classed(edgeGClassName, true)
649
+ // .attr('cursor', 'pointer')
650
+ return enterSelection
651
+ },
652
+ update => {
653
+ return update
654
+ },
655
+ exit => {
656
+ return exit.remove()
657
+ }
658
+ )
659
+ }
660
+
661
+ function renderEdgeArrowPath ({ edgeGSelection, fullParams, fullChartParams }: {
662
+ edgeGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
663
+ fullParams: ForceDirectedBubblesParams
664
+ fullChartParams: ChartParams
665
+ }) {
666
+ edgeGSelection.each((data,i,g) => {
667
+ const gSelection = d3.select(g[i])
668
+ gSelection.selectAll<SVGPathElement, ComputedEdge>('path')
669
+ .data([data])
670
+ .join(
671
+ enter => {
672
+ return enter
673
+ .append('path')
674
+ .classed(edgeArrowPathClassName, true)
675
+ },
676
+ update => {
677
+ return update
678
+ },
679
+ exit => {
680
+ return exit.remove()
681
+ }
682
+ )
683
+ .attr('marker-end', d => `url(#${d.markerId})`)
684
+ .attr('stroke', d => getDatumColor({ datum: d.data, colorType: fullParams.arrow.colorType, fullChartParams }))
685
+ .attr('stroke-width', d => d.strokeWidth)
686
+ .attr('style', d => fullParams.arrow.styleFn(d))
687
+ })
688
+
689
+ return edgeGSelection.select<SVGPathElement>(`path.${edgeArrowPathClassName}`)
690
+ }
691
+
692
+ function renderEdgeLabelG ({ edgeGSelection }: {
693
+ edgeGSelection: d3.Selection<SVGGElement, any, any, unknown>
694
+ }) {
695
+ edgeGSelection.each((data,i,g) => {
696
+ const gSelection = d3.select(g[i])
697
+ gSelection.selectAll<SVGGElement, RenderEdge>('g')
698
+ .data([data])
699
+ .join(
700
+ enter => {
701
+ const enterSelection = enter
702
+ .append('g')
703
+ .classed(edgeLabelGClassName, true)
704
+ // .attr('cursor', 'pointer')
705
+ return enterSelection
706
+ },
707
+ update => {
708
+ return update
709
+ },
710
+ exit => {
711
+ return exit.remove()
712
+ }
713
+ )
714
+ })
715
+
716
+ return edgeGSelection.select<SVGTextElement>(`g.${edgeLabelGClassName}`)
717
+ }
718
+
719
+ function renderEdgeLabel ({ edgeLabelGSelection, fullParams, fullChartParams }: {
720
+ edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
721
+ fullParams: ForceDirectedBubblesParams
722
+ fullChartParams: ChartParams
723
+ }) {
724
+ edgeLabelGSelection.each((data,i,g) => {
725
+ const gSelection = d3.select(g[i])
726
+ gSelection.selectAll<SVGTextElement, RenderEdge>('text')
727
+ .data([data], d => d.id)
728
+ .join(
729
+ enter => {
730
+ const enterSelection = enter
731
+ .append('text')
732
+ .classed(edgeLabelClassName, true)
733
+ // .attr('cursor', 'pointer')
734
+ .attr('text-anchor', 'middle')
735
+ .attr('pointer-events', 'none')
736
+ return enterSelection
737
+ },
738
+ update => {
739
+ return update
740
+ },
741
+ exit => {
742
+ return exit.remove()
743
+ }
744
+ )
745
+ .text(d => d.label)
746
+ .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.arrowLabel.colorType, fullChartParams }))
747
+ .attr('font-size', fullChartParams.styles.textSize)
748
+ .attr('style', d => fullParams.arrowLabel.styleFn(d))
749
+ })
750
+
751
+ return edgeLabelGSelection.select<SVGTextElement>(`text.${edgeLabelClassName}`)
752
+ }
753
+
754
+ function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
755
+ nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
756
+ edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
757
+ fullChartParams: ChartParams
758
+ highlightIds: string[]
759
+ }) {
760
+ nodeGSelection.interrupt('highlight')
761
+ edgeGSelection.interrupt('highlight')
762
+ // console.log(highlightIds)
763
+ if (!highlightIds.length) {
764
+ nodeGSelection
765
+ .transition('highlight')
766
+ .style('opacity', 1)
767
+ edgeGSelection
768
+ .transition('highlight')
769
+ .style('opacity', 1)
770
+ return
771
+ }
772
+
773
+ edgeGSelection
774
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
775
+
776
+ nodeGSelection.each((d, i, n) => {
777
+ const segment = d3.select(n[i])
778
+
779
+ if (highlightIds.includes(d.id)) {
780
+ segment
781
+ .style('opacity', 1)
782
+ .transition('highlight')
783
+ .ease(d3.easeElastic)
784
+ .duration(500)
785
+ } else {
786
+ // 取消
787
+ segment
788
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
789
+ }
790
+ })
791
+ }
792
+
793
+ // 暫不處理edge的highlight
794
+ // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
795
+ // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
796
+ // fullChartParams: ChartParams
797
+ // highlightIds: string[]
798
+ // }) {
799
+ // edgeGSelection.interrupt('highlight')
800
+
801
+ // if (!highlightIds.length) {
802
+ // edgeGSelection
803
+ // .transition('highlight')
804
+ // .style('opacity', 1)
805
+ // return
806
+ // }
807
+
808
+ // edgeGSelection.each((d, i, n) => {
809
+ // const segment = d3.select(n[i])
810
+
811
+ // if (highlightIds.includes(d.id)) {
812
+ // segment
813
+ // .style('opacity', 1)
814
+ // .transition('highlight')
815
+ // .ease(d3.easeElastic)
816
+ // .duration(500)
817
+ // } else {
818
+ // // 取消放大
819
+ // segment
820
+ // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
821
+ // }
822
+ // })
823
+ // }
824
+
825
+ export const ForceDirectedBubbles = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
826
+
827
+ const destroy$ = new Subject()
828
+
829
+ const gSelection = selection.append('g').classed(gSelectionClassName, true)
830
+ const defsSelection = gSelection.append('defs')
831
+ const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
832
+ const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
833
+
834
+ let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
835
+ let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
836
+ // let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
837
+ // let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
838
+ let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
839
+ let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
840
+ let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
841
+ let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
842
+
843
+ const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
844
+ const mouseEvent$ = new Subject<EventRelationship>()
845
+
846
+
847
+ // init zoom
848
+ const d3Zoom$ = observer.fullParams$.pipe(
849
+ takeUntil(destroy$),
850
+ // map(d => d.scaleExtent),
851
+ // distinctUntilChanged((a, b) => String(a) === String(b)),
852
+ // first(),
853
+ map(data => {
854
+ let d3Zoom = data.zoomable
855
+ ? d3.zoom().on('zoom', (event) => {
856
+ // console.log(event)
857
+ // this.svgGroup.attr('transform', `translate(
858
+ // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
859
+ // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
860
+ // ) scale(
861
+ // ${event.transform.k}
862
+ // )`)
863
+ gSelection.attr('transform', `translate(
864
+ ${event.transform.x},
865
+ ${event.transform.y}
866
+ ) scale(
867
+ ${event.transform.k}
868
+ )`)
869
+
870
+ // if (data.node.labelSizeFixed && nodeLabelSelection) {
871
+ // // 反向 scale 抵消掉放大縮小
872
+ // nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
873
+ // }
874
+ if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
875
+ // 反向 scale 抵消掉放大縮小
876
+ edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
877
+ }
878
+ })
879
+ : d3.zoom().on('zoom', null)
880
+ if (data.scaleExtent) {
881
+ d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
882
+ }
883
+ rootSelection.call(d3Zoom)
884
+
885
+ return d3Zoom
886
+ }),
887
+ // shareReplay(1)
888
+ )
889
+
890
+ // zoom transform
891
+ combineLatest({
892
+ d3Zoom: d3Zoom$,
893
+ transform: observer.fullParams$.pipe(
894
+ takeUntil(destroy$),
895
+ map(d => d.transform),
896
+ )
897
+ }).pipe(
898
+ takeUntil(destroy$),
899
+ switchMap(async d => d)
900
+ ).subscribe(data => {
901
+ // console.log('call')
902
+ selection.call(
903
+ data.d3Zoom.transform, d3.zoomIdentity
904
+ .translate(data.transform.x, data.transform.y)
905
+ .scale(data.transform.k)
906
+ )
907
+ })
908
+
909
+
910
+ const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
911
+ layout: observer.layout$.pipe(
912
+ first() // 只使用第一次的尺寸(置中)
913
+ ),
914
+ fullParams: observer.fullParams$
915
+ }).pipe(
916
+ takeUntil(destroy$),
917
+ switchMap(async d => d),
918
+ map(data => createSimulation(data.layout, data.fullParams)),
919
+ shareReplay(1)
920
+ )
921
+
922
+ const nodeMinMaxValue$ = observer.computedData$.pipe(
923
+ takeUntil(destroy$),
924
+ map(data => {
925
+ const hadValueData = data.nodes.filter(d => d.value != undefined)
926
+ if (!hadValueData.length) {
927
+ return [0, 2] // 給預設值
928
+ }
929
+ const minMax = getMinMax(data.nodes.map(d => d.value))
930
+ if (hadValueData.length == 1 || minMax[0] === minMax[1]) {
931
+ return [minMax[0] - 1, minMax[1] + 1] // 避免最大最小值相同
932
+ }
933
+ return minMax
934
+ }),
935
+ shareReplay(1)
936
+ )
937
+
938
+ const edgeMinMaxValue$ = observer.computedData$.pipe(
939
+ takeUntil(destroy$),
940
+ map(data => {
941
+ const hadValueData = data.edges.filter(d => d.value != undefined)
942
+ if (!hadValueData.length) {
943
+ return [0, 2] // 給預設值
944
+ }
945
+ const minMax = getMinMax(data.edges.map(d => d.value))
946
+ if (hadValueData.length == 1 || minMax[0] === minMax[1]) {
947
+ return [minMax[0] - 1, minMax[1] + 1] // 避免最大最小值相同
948
+ }
949
+ return minMax
950
+ }),
951
+ shareReplay(1)
952
+ )
953
+
954
+ // 當無value時給的預設值
955
+ const defaultNodeValue$ = nodeMinMaxValue$.pipe(
956
+ takeUntil(destroy$),
957
+ map(data => (data[1] - data[0]) / 2) // 預設值為最大及最小的中間值
958
+ )
959
+
960
+ // 當無value時給的預設值
961
+ const defaultEdgeValue$ = edgeMinMaxValue$.pipe(
962
+ takeUntil(destroy$),
963
+ map(data => (data[1] - data[0]) / 2) // 預設值為最大及最小的中間值
964
+ )
965
+
966
+ const radiusScale$ = combineLatest({
967
+ nodeMinMaxValue: nodeMinMaxValue$,
968
+ fullParams: observer.fullParams$
969
+ }).pipe(
970
+ takeUntil(destroy$),
971
+ switchMap(async (d) => d),
972
+ map(data => {
973
+ // console.log({ totalR: data.totalR, totalValue: data.totalValue })
974
+ return d3.scalePow()
975
+ .domain(data.nodeMinMaxValue)
976
+ .range([data.fullParams.bubble.radiusMin, data.fullParams.bubble.radiusMax])
977
+ .exponent(data.fullParams.bubble.arcScaleType === 'area'
978
+ ? 0.5 // 數值映射面積(0.5為取平方根)
979
+ : 1 // 數值映射半徑
980
+ )
981
+ })
982
+ )
983
+
984
+ const strokeWidthScale$ = combineLatest({
985
+ edgeMinMaxValue: edgeMinMaxValue$,
986
+ fullParams: observer.fullParams$
987
+ }).pipe(
988
+ takeUntil(destroy$),
989
+ switchMap(async (d) => d),
990
+ map(data => {
991
+ return d3.scaleLinear()
992
+ .domain(data.edgeMinMaxValue)
993
+ .range([data.fullParams.arrow.strokeWidthMin, data.fullParams.arrow.strokeWidthMax])
994
+ })
995
+ )
996
+
997
+ // 先將未篩選的資料全部儲起來,就不會因為 visibleFilter 而重新計算
998
+ const RenderNodeMap$: Observable<Map<string, RenderNode>> = combineLatest({
999
+ computedData: observer.computedData$,
1000
+ radiusScale: radiusScale$,
1001
+ defaultNodeValue: defaultNodeValue$,
1002
+ }).pipe(
1003
+ takeUntil(destroy$),
1004
+ switchMap(async (d) => d),
1005
+ map(data => {
1006
+ return new Map(
1007
+ data.computedData.nodes.map(_d => {
1008
+ let d: RenderNode = _d as RenderNode
1009
+ d.r = data.radiusScale(d.value ?? data.defaultNodeValue)
1010
+ return [d.id, d]
1011
+ })
1012
+ )
1013
+ }),
1014
+ )
1015
+
1016
+ // 先將未篩選的資料全部儲起來,就不會因為 visibleFilter 而重新計算
1017
+ const RenderEdgeMap$: Observable<Map<string, RenderEdge>> = combineLatest({
1018
+ computedData: observer.computedData$,
1019
+ strokeWidthScale: strokeWidthScale$,
1020
+ defaultEdgeValue: defaultEdgeValue$
1021
+ }).pipe(
1022
+ takeUntil(destroy$),
1023
+ switchMap(async (d) => d),
1024
+ map(data => {
1025
+ return new Map(
1026
+ data.computedData.edges.map(_d => {
1027
+ let d: any = _d as RenderEdge
1028
+ d.source = _d.startNode // reference
1029
+ d.target = _d.endNode
1030
+ d.strokeWidth = data.strokeWidthScale(d.value ?? data.defaultEdgeValue)
1031
+ d.markerId = `${defsArrowMarkerId}__${d.id}`
1032
+ return [d.id, d]
1033
+ })
1034
+ )
1035
+ }),
1036
+ )
1037
+
1038
+ const renderData$: Observable<RenderData> = combineLatest({
1039
+ visibleComputedData: observer.visibleComputedData$,
1040
+ RenderNodeMap: RenderNodeMap$,
1041
+ RenderEdgeMap: RenderEdgeMap$,
1042
+ }).pipe(
1043
+ takeUntil(destroy$),
1044
+ switchMap(async (d) => d),
1045
+ map(data => {
1046
+ return {
1047
+ nodes: data.visibleComputedData.nodes.map(d => data.RenderNodeMap.get(d.id)!),
1048
+ edges: data.visibleComputedData.edges.map(d => data.RenderEdgeMap.get(d.id)!),
1049
+ }
1050
+ }),
1051
+ shareReplay(1)
1052
+ )
1053
+
1054
+ const markerParams$: Observable<MarkerParams> = observer.fullParams$.pipe(
1055
+ takeUntil(destroy$),
1056
+ map(fullParams => {
1057
+ return {
1058
+ viewBox: `-${fullParams.arrow.pointerWidth} -${fullParams.arrow.pointerHeight / 2} ${fullParams.arrow.pointerWidth} ${fullParams.arrow.pointerHeight}`,
1059
+ d: `M${-fullParams.arrow.pointerWidth},${-fullParams.arrow.pointerHeight / 2}L0,0L${-fullParams.arrow.pointerWidth},${fullParams.arrow.pointerHeight / 2}`, // 箭頭的尖端為(0,0)
1060
+ pointerWidth: fullParams.arrow.pointerWidth,
1061
+ pointerHeight: fullParams.arrow.pointerHeight,
1062
+ }
1063
+ })
1064
+ )
1065
+
1066
+ const markerData$: Observable<MarkerDatum[]> = combineLatest({
1067
+ computedData: observer.computedData$,
1068
+ fullParams: observer.fullParams$,
1069
+ RenderNodeMap: RenderNodeMap$,
1070
+ RenderEdgeMap: RenderEdgeMap$,
1071
+ }).pipe(
1072
+ takeUntil(destroy$),
1073
+ switchMap(async d => d),
1074
+ map(data => {
1075
+ return data.computedData.edges.map(d => {
1076
+ const renderEdge = data.RenderEdgeMap.get(d.id)!
1077
+ const renderEndNode = data.RenderNodeMap.get(d.endNode.id)!
1078
+ return {
1079
+ id: renderEdge.markerId,
1080
+ edgeId: d.id,
1081
+ strokeWidth: renderEdge.strokeWidth,
1082
+ /* refX:修正marker位置(計算出和circle半徑相等的寬度)
1083
+ (1)circle半徑需加上 strokeWidth/2 是因為框線是以 circle 的邊緣往內及往外擴展,所以 stroke 多出來的寬度是一半而已
1084
+ (2)circle半徑需除以 path 寬度是因為「marker 的位置會受到 path 的stroke-width影響」,所以要進行修正
1085
+ (3)- 1 是要修正奇怪的誤差(不知原因)
1086
+ */
1087
+ refX: ((renderEndNode.r + (data.fullParams.bubble.strokeWidth / 2)) / renderEdge.strokeWidth) - 1
1088
+ }
1089
+ })
1090
+ }),
1091
+ )
1092
+
1093
+ // <marker> marker selection
1094
+ combineLatest({
1095
+ defsSelection,
1096
+ markerParams: markerParams$,
1097
+ markerData: markerData$,
1098
+ fullParams: observer.fullParams$,
1099
+ fullChartParams: observer.fullChartParams$
1100
+ }).pipe(
1101
+ takeUntil(destroy$),
1102
+ switchMap(async d => d),
1103
+ map(data => {
1104
+ return renderArrowMarker({
1105
+ defsSelection,
1106
+ markerParams: data.markerParams,
1107
+ markerData: data.markerData,
1108
+ fullParams: data.fullParams,
1109
+ fullChartParams: data.fullChartParams
1110
+ })
1111
+ })
1112
+ ).subscribe()
1113
+
1114
+
1115
+ combineLatest({
1116
+ renderData: renderData$,
1117
+ computedData: observer.computedData$,
1118
+ CategoryNodeMap: observer.CategoryNodeMap$,
1119
+ simulation: simulation$,
1120
+ fullParams: observer.fullParams$,
1121
+ fullChartParams: observer.fullChartParams$
1122
+ }).pipe(
1123
+ takeUntil(destroy$),
1124
+ switchMap(async d => d),
1125
+ ).subscribe(data => {
1126
+
1127
+ nodeGSelection = renderNodeG({
1128
+ nodeListGSelection: nodeListGSelection,
1129
+ nodes: data.renderData.nodes as RenderNode[],
1130
+ })
1131
+
1132
+ nodeCircleSelection = renderNodeCircle({
1133
+ nodeGSelection: nodeGSelection,
1134
+ fullParams: data.fullParams,
1135
+ fullChartParams: data.fullChartParams
1136
+ })
1137
+ nodeGSelection.call(drag(data.simulation, dragStatus$))
1138
+
1139
+ // nodeLabelGSelection = renderNodeLabelG({
1140
+ // nodeGSelection: nodeGSelection,
1141
+ // })
1142
+
1143
+ // nodeLabelSelection = renderNodeLabel({
1144
+ // nodeLabelGSelection: nodeLabelGSelection,
1145
+ // fullParams: data.fullParams,
1146
+ // fullChartParams: data.fullChartParams
1147
+ // })
1148
+
1149
+ edgeGSelection = renderEdgeG({
1150
+ edgeListGSelection: edgeListGSelection,
1151
+ edges: data.renderData.edges
1152
+ })
1153
+
1154
+ edgeArrowSelection = renderEdgeArrowPath({
1155
+ edgeGSelection: edgeGSelection,
1156
+ fullParams: data.fullParams,
1157
+ fullChartParams: data.fullChartParams
1158
+ })
1159
+
1160
+ edgeLabelGSelection = renderEdgeLabelG({
1161
+ edgeGSelection: edgeGSelection,
1162
+ })
1163
+
1164
+ edgeLabelSelection = renderEdgeLabel({
1165
+ edgeLabelGSelection: edgeLabelGSelection,
1166
+ fullParams: data.fullParams,
1167
+ fullChartParams: data.fullChartParams
1168
+ })
1169
+
1170
+ data.simulation.nodes(data.renderData.nodes)
1171
+ .on('tick', () => {
1172
+ edgeArrowSelection.attr('d', linkArcFn)
1173
+ nodeGSelection.attr('transform', translateFn)
1174
+ // nodeLabelGSelection.attr('transform', d => translateFn({
1175
+ // x: d.x,
1176
+ // y: d.y - d.r - 10
1177
+ // }))
1178
+ edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
1179
+ })
1180
+ ;(data.simulation.force("link") as any).links(data.renderData.edges)
1181
+
1182
+ data.simulation.alpha(0.3).restart()
1183
+
1184
+ nodeCircleSelection
1185
+ .on('mouseover', (event, datum) => {
1186
+ event.stopPropagation()
1187
+
1188
+ mouseEvent$.next({
1189
+ type: 'relationship',
1190
+ eventName: 'mouseover',
1191
+ pluginName,
1192
+ highlightTarget: data.fullChartParams.highlightTarget,
1193
+ datum: datum,
1194
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1195
+ categoryIndex: datum.categoryIndex,
1196
+ categoryLabel: datum.categoryLabel,
1197
+ event,
1198
+ data: data.computedData
1199
+ })
1200
+ })
1201
+ .on('mousemove', (event, datum) => {
1202
+ event.stopPropagation()
1203
+
1204
+ mouseEvent$.next({
1205
+ type: 'relationship',
1206
+ eventName: 'mousemove',
1207
+ pluginName,
1208
+ highlightTarget: data.fullChartParams.highlightTarget,
1209
+ datum: datum,
1210
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1211
+ categoryIndex: datum.categoryIndex,
1212
+ categoryLabel: datum.categoryLabel,
1213
+ event,
1214
+ data: data.computedData
1215
+ })
1216
+ })
1217
+ .on('mouseout', (event, datum) => {
1218
+ event.stopPropagation()
1219
+
1220
+ mouseEvent$.next({
1221
+ type: 'relationship',
1222
+ eventName: 'mouseout',
1223
+ pluginName,
1224
+ highlightTarget: data.fullChartParams.highlightTarget,
1225
+ datum: datum,
1226
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1227
+ categoryIndex: datum.categoryIndex,
1228
+ categoryLabel: datum.categoryLabel,
1229
+ event,
1230
+ data: data.computedData
1231
+ })
1232
+ })
1233
+ .on('click', (event, datum) => {
1234
+ event.stopPropagation()
1235
+
1236
+ mouseEvent$.next({
1237
+ type: 'relationship',
1238
+ eventName: 'click',
1239
+ pluginName,
1240
+ highlightTarget: data.fullChartParams.highlightTarget,
1241
+ datum: datum,
1242
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1243
+ categoryIndex: datum.categoryIndex,
1244
+ categoryLabel: datum.categoryLabel,
1245
+ event,
1246
+ data: data.computedData
1247
+ })
1248
+ })
1249
+ })
1250
+
1251
+ dragStatus$.pipe(
1252
+ distinctUntilChanged((a, b) => a === b),
1253
+ // 只有沒有托曳時才執行
1254
+ switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1255
+ ).subscribe(data => {
1256
+ subject.event$.next(data)
1257
+ })
1258
+
1259
+ combineLatest({
1260
+ renderData: renderData$,
1261
+ highlightNodes: observer.relationshipHighlightNodes$.pipe(
1262
+ map(data => data.map(d => d.id))
1263
+ ),
1264
+ highlightEdges: observer.relationshipHighlightEdges$.pipe(
1265
+ map(data => data.map(d => d.id))
1266
+ ),
1267
+ fullChartParams: observer.fullChartParams$,
1268
+ fullParams: observer.fullParams$,
1269
+ }).pipe(
1270
+ takeUntil(destroy$),
1271
+ switchMap(async d => d)
1272
+ ).subscribe(data => {
1273
+ if (!nodeGSelection || !edgeGSelection) {
1274
+ return
1275
+ }
1276
+
1277
+ highlightNodes({
1278
+ nodeGSelection,
1279
+ edgeGSelection,
1280
+ highlightIds: data.highlightNodes,
1281
+ fullChartParams: data.fullChartParams
1282
+ })
1283
+ // highlightEdges({
1284
+ // edgeGSelection,
1285
+ // highlightIds: data.highlightEdges,
1286
+ // fullChartParams: data.fullChartParams
1287
+ // })
1288
+ })
1289
+
1290
+
1291
+
1292
+ return () => {
1293
+ destroy$.next(undefined)
1294
+ }
1295
1295
  })