@orbcharts/plugins-basic 3.0.8 → 3.0.10

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 (120) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +6182 -6004
  3. package/dist/orbcharts-plugins-basic.umd.js +44 -44
  4. package/dist/src/series/defaults.d.ts +2 -1
  5. package/dist/src/series/index.d.ts +1 -0
  6. package/dist/src/series/plugins/Indicator.d.ts +3 -0
  7. package/lib/core-types.ts +7 -7
  8. package/lib/core.ts +6 -6
  9. package/lib/gridObservables.ts +6 -6
  10. package/lib/plugins-basic-types.ts +6 -6
  11. package/package.json +48 -48
  12. package/src/base/BaseBars.ts +765 -765
  13. package/src/base/BaseBarsTriangle.ts +676 -676
  14. package/src/base/BaseDots.ts +464 -464
  15. package/src/base/BaseGroupAxis.ts +691 -691
  16. package/src/base/BaseLegend.ts +684 -684
  17. package/src/base/BaseLineAreas.ts +629 -629
  18. package/src/base/BaseLines.ts +706 -706
  19. package/src/base/BaseOrdinalBubbles.ts +729 -729
  20. package/src/base/BaseRacingBars.ts +582 -582
  21. package/src/base/BaseRacingLabels.ts +404 -404
  22. package/src/base/BaseRacingValueLabels.ts +403 -403
  23. package/src/base/BaseStackedBars.ts +793 -793
  24. package/src/base/BaseTooltip.ts +408 -408
  25. package/src/base/BaseValueAxis.ts +600 -600
  26. package/src/base/BaseXAxis.ts +427 -427
  27. package/src/base/BaseXZoom.ts +241 -241
  28. package/src/base/BaseYAxis.ts +389 -389
  29. package/src/base/types.ts +2 -2
  30. package/src/const.ts +30 -30
  31. package/src/grid/defaults.ts +213 -213
  32. package/src/grid/gridObservables.ts +635 -635
  33. package/src/grid/index.ts +16 -16
  34. package/src/grid/plugins/Bars.ts +69 -69
  35. package/src/grid/plugins/BarsPN.ts +66 -66
  36. package/src/grid/plugins/BarsTriangle.ts +73 -73
  37. package/src/grid/plugins/Dots.ts +68 -68
  38. package/src/grid/plugins/GridLegend.ts +107 -107
  39. package/src/grid/plugins/GridTooltip.ts +66 -66
  40. package/src/grid/plugins/GroupAux.ts +1095 -1095
  41. package/src/grid/plugins/GroupAxis.ts +73 -73
  42. package/src/grid/plugins/GroupZoom.ts +218 -218
  43. package/src/grid/plugins/LineAreas.ts +65 -65
  44. package/src/grid/plugins/Lines.ts +59 -59
  45. package/src/grid/plugins/StackedBars.ts +66 -66
  46. package/src/grid/plugins/StackedValueAxis.ts +97 -97
  47. package/src/grid/plugins/ValueAxis.ts +94 -94
  48. package/src/index.ts +6 -6
  49. package/src/multiGrid/defaults.ts +244 -244
  50. package/src/multiGrid/index.ts +14 -14
  51. package/src/multiGrid/multiGridObservables.ts +50 -50
  52. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  53. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  54. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  55. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
  56. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  57. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  58. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  59. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  60. package/src/multiGrid/plugins/MultiStackedBars.ts +109 -109
  61. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +135 -135
  62. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  63. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
  64. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  65. package/src/multiValue/defaults.ts +523 -523
  66. package/src/multiValue/index.ts +16 -16
  67. package/src/multiValue/multiValueObservables.ts +781 -781
  68. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  69. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  70. package/src/multiValue/plugins/OrdinalAux.ts +660 -660
  71. package/src/multiValue/plugins/OrdinalAxis.ts +524 -524
  72. package/src/multiValue/plugins/OrdinalBubbles.ts +226 -226
  73. package/src/multiValue/plugins/OrdinalZoom.ts +57 -57
  74. package/src/multiValue/plugins/RacingBars.ts +375 -375
  75. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
  76. package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
  77. package/src/multiValue/plugins/Scatter.ts +486 -486
  78. package/src/multiValue/plugins/ScatterBubbles.ts +635 -635
  79. package/src/multiValue/plugins/XAxis.ts +107 -107
  80. package/src/multiValue/plugins/XYAux.ts +683 -683
  81. package/src/multiValue/plugins/XYAxes.ts +194 -194
  82. package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
  83. package/src/multiValue/plugins/XZoom.ts +40 -40
  84. package/src/noneData/defaults.ts +102 -102
  85. package/src/noneData/index.ts +3 -3
  86. package/src/noneData/plugins/Container.ts +27 -27
  87. package/src/noneData/plugins/Tooltip.ts +373 -373
  88. package/src/relationship/defaults.ts +221 -221
  89. package/src/relationship/index.ts +5 -5
  90. package/src/relationship/plugins/ForceDirected.ts +1056 -1056
  91. package/src/relationship/plugins/ForceDirectedBubbles.ts +1294 -1294
  92. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  93. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  94. package/src/relationship/relationshipObservables.ts +49 -49
  95. package/src/series/defaults.ts +236 -224
  96. package/src/series/index.ts +10 -9
  97. package/src/series/plugins/Bubbles.ts +784 -784
  98. package/src/series/plugins/Indicator.ts +298 -0
  99. package/src/series/plugins/Pie.ts +622 -622
  100. package/src/series/plugins/PieEventTexts.ts +283 -283
  101. package/src/series/plugins/PieLabels.ts +639 -639
  102. package/src/series/plugins/Rose.ts +515 -515
  103. package/src/series/plugins/RoseLabels.ts +599 -599
  104. package/src/series/plugins/SeriesLegend.ts +107 -107
  105. package/src/series/plugins/SeriesTooltip.ts +66 -66
  106. package/src/series/seriesObservables.ts +168 -168
  107. package/src/series/seriesUtils.ts +51 -51
  108. package/src/tree/defaults.ts +102 -102
  109. package/src/tree/index.ts +4 -4
  110. package/src/tree/plugins/TreeLegend.ts +100 -100
  111. package/src/tree/plugins/TreeMap.ts +341 -341
  112. package/src/tree/plugins/TreeTooltip.ts +66 -66
  113. package/src/utils/commonUtils.ts +31 -31
  114. package/src/utils/d3Graphics.ts +176 -176
  115. package/src/utils/d3Utils.ts +92 -92
  116. package/src/utils/observables.ts +14 -14
  117. package/src/utils/orbchartsUtils.ts +129 -129
  118. package/tsconfig.base.json +13 -13
  119. package/tsconfig.json +2 -2
  120. 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
  })