@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,1057 +1,1057 @@
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 } from '../../../lib/core'
35
- import type { BubblesParams, ArcScaleType, ForceDirectedParams } from '../../../lib/plugins-basic-types'
36
- import { getColor, getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
37
- import { DEFAULT_FORCE_DIRECTED_PARAMS } from '../defaults'
38
- // import { renderCircleText } from '../../utils/d3Graphics'
39
- import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
40
- import { d3EventObservable } from '../../utils/observables'
41
-
42
- // interface BubblesDatum extends ComputedNode {
43
- // x: number
44
- // y: number
45
- // r: number
46
- // _originR: number // 紀錄變化前的r
47
- // }
48
-
49
- type Zoom = {
50
- xOffset: number
51
- yOffset: number
52
- scaleExtent: {
53
- min: number
54
- max: number
55
- }
56
- }
57
-
58
- // d3 forceSimulation使用的node資料
59
- type RenderNode = d3.SimulationNodeDatum & ComputedNode
60
-
61
- // d3 forceSimulation使用的edge資料
62
- interface RenderEdge extends ComputedEdge {
63
- source: RenderNode
64
- target: RenderNode
65
- }
66
-
67
- // d3 forceSimulation使用的資料
68
- type RenderData = {
69
- nodes: (ComputedNode | RenderNode)[] // 經過d3 forceSimulation計算後的node才有座標資訊
70
- edges: RenderEdge[]
71
- }
72
-
73
- interface D3DragEvent {
74
- active: number
75
- dx: number
76
- dy: number
77
- identifier: string
78
- sourceEvent: MouseEvent
79
- subject: RenderNode
80
- target: any
81
- type: string
82
- x: number
83
- y: number
84
- }
85
-
86
- type DragStatus = 'start' | 'drag' | 'end'
87
-
88
- // type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
89
-
90
- const pluginName = 'ForceDirected'
91
-
92
- const gSelectionClassName = getClassName(pluginName, 'zoom-area')
93
- const defsArrowMarkerId = getUniID(pluginName, 'arrow')
94
- const defsArrowMarkerClassName = getClassName(pluginName, 'arrow-marker')
95
- const edgeListGClassName = getClassName(pluginName, 'edge-list-g')
96
- const edgeGClassName = getClassName(pluginName, 'edge-g')
97
- const edgeArrowPathClassName = getClassName(pluginName, 'edge-arrow-path')
98
- const edgeLabelGClassName = getClassName(pluginName, 'edge-label-g')
99
- const edgeLabelClassName = getClassName(pluginName, 'edge-label')
100
- const nodeListGClassName = getClassName(pluginName, 'node-list-g')
101
- const nodeGClassName = getClassName(pluginName, 'node-g')
102
- const nodeCircleClassName = getClassName(pluginName, 'node-circle')
103
- const nodeLabelGClassName = getClassName(pluginName, 'node-label-g')
104
- const nodeLabelClassName = getClassName(pluginName, 'node-label')
105
-
106
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_FORCE_DIRECTED_PARAMS> = {
107
- name: pluginName,
108
- defaultParams: DEFAULT_FORCE_DIRECTED_PARAMS,
109
- layerIndex: LAYER_INDEX_OF_GRAPHIC,
110
- validator: (params, { validateColumns }) => {
111
- const result = validateColumns(params, {
112
- dot: {
113
- toBeTypes: ['object']
114
- },
115
- dotLabel: {
116
- toBeTypes: ['object']
117
- },
118
- arrow: {
119
- toBeTypes: ['object']
120
- },
121
- arrowLabel: {
122
- toBeTypes: ['object']
123
- },
124
- force: {
125
- toBeTypes: ['object']
126
- },
127
- zoomable: {
128
- toBeTypes: ['boolean']
129
- },
130
- transform: {
131
- toBeTypes: ['object']
132
- },
133
- scaleExtent: {
134
- toBeTypes: ['object']
135
- }
136
- })
137
- if (params.dot) {
138
- const dotResult = validateColumns(params.dot, {
139
- radius: {
140
- toBeTypes: ['number']
141
- },
142
- fillColorType: {
143
- toBeOption: 'ColorType'
144
- },
145
- strokeColorType: {
146
- toBeOption: 'ColorType'
147
- },
148
- strokeWidth: {
149
- toBeTypes: ['number']
150
- },
151
- styleFn: {
152
- toBeTypes: ['Function']
153
- },
154
- })
155
- if (dotResult.status === 'error') {
156
- return dotResult
157
- }
158
- }
159
- if (params.dotLabel) {
160
- const dotLabelResult = validateColumns(params.dotLabel, {
161
- colorType: {
162
- toBeOption: 'ColorType'
163
- },
164
- sizeFixed: {
165
- toBeTypes: ['boolean']
166
- },
167
- styleFn: {
168
- toBeTypes: ['Function']
169
- },
170
- })
171
- if (dotLabelResult.status === 'error') {
172
- return dotLabelResult
173
- }
174
- }
175
- if (params.arrow) {
176
- const arrowResult = validateColumns(params.arrow, {
177
- colorType: {
178
- toBeOption: 'ColorType'
179
- },
180
- strokeWidth: {
181
- toBeTypes: ['number']
182
- },
183
- pointerWidth: {
184
- toBeTypes: ['number']
185
- },
186
- pointerHeight: {
187
- toBeTypes: ['number']
188
- },
189
- styleFn: {
190
- toBeTypes: ['Function']
191
- },
192
- })
193
- if (arrowResult.status === 'error') {
194
- return arrowResult
195
- }
196
- }
197
- if (params.arrowLabel) {
198
- const arrowLabelResult = validateColumns(params.arrowLabel, {
199
- colorType: {
200
- toBeOption: 'ColorType'
201
- },
202
- sizeFixed: {
203
- toBeTypes: ['boolean']
204
- },
205
- styleFn: {
206
- toBeTypes: ['Function']
207
- },
208
- })
209
- if (arrowLabelResult.status === 'error') {
210
- return arrowLabelResult
211
- }
212
- }
213
- if (params.force) {
214
- const forceResult = validateColumns(params.force, {
215
- nodeStrength: {
216
- toBeTypes: ['number']
217
- },
218
- linkDistance: {
219
- toBeTypes: ['number']
220
- },
221
- velocityDecay: {
222
- toBeTypes: ['number']
223
- },
224
- alphaDecay: {
225
- toBeTypes: ['number']
226
- },
227
- })
228
- if (forceResult.status === 'error') {
229
- return forceResult
230
- }
231
- }
232
- if (params.transform) {
233
- const transformResult = validateColumns(params.transform, {
234
- x: {
235
- toBeTypes: ['number']
236
- },
237
- y: {
238
- toBeTypes: ['number']
239
- },
240
- k: {
241
- toBeTypes: ['number']
242
- },
243
- })
244
- if (transformResult.status === 'error') {
245
- return transformResult
246
- }
247
- }
248
- if (params.scaleExtent) {
249
- const scaleExtentResult = validateColumns(params.scaleExtent, {
250
- min: {
251
- toBeTypes: ['number']
252
- },
253
- max: {
254
- toBeTypes: ['number']
255
- },
256
- })
257
- if (scaleExtentResult.status === 'error') {
258
- return scaleExtentResult
259
- }
260
- }
261
- return result
262
- }
263
- }
264
-
265
- // let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
266
-
267
- function createSimulation (layout: Layout, fullParams: ForceDirectedParams) {
268
- return d3.forceSimulation()
269
- .velocityDecay(fullParams.force.velocityDecay)
270
- .alphaDecay(fullParams.force.alphaDecay)
271
- .force(
272
- "link",
273
- d3.forceLink()
274
- .id((d: d3.SimulationNodeDatum & ComputedNode) => d.id)
275
- .strength(1)
276
- .distance((d: d3.SimulationLinkDatum<d3.SimulationNodeDatum & ComputedNode>) => {
277
- // if (d.direction === 'top') {
278
- // return 200
279
- // } else {
280
- // return 250
281
- // }
282
- return fullParams.force.linkDistance
283
- })
284
- )
285
- .force("charge", d3.forceManyBody().strength(fullParams.force.nodeStrength))
286
- .force("collision", d3.forceCollide(fullParams.dot.radius).strength(1))
287
- .force("center", d3.forceCenter(layout.width / 2, layout.height / 2))
288
-
289
- }
290
-
291
- function translateFn (d: any): string {
292
- // console.log('translateFn', d)
293
- return "translate(" + d.x + "," + d.y + ")";
294
- }
295
-
296
- function translateCenterFn (d: any): string {
297
- // console.log('translateCenterFn', d)
298
- const x = d.source.x + ((d.target.x - d.source.x) / 2) // 置中的話除2
299
- const y = d.source.y + ((d.target.y - d.source.y) / 2) // 置中的話除2
300
- return "translate(" + x + "," + y + ")";
301
- }
302
-
303
- function linkArcFn (d: RenderEdge): string {
304
- // console.log('linkArcFn', d)
305
-
306
- // const dx = d.target.x - d.source.x,
307
- // dy = d.target.y - d.source.y
308
- // dr讓方向線變成有弧度的
309
- // dr = Math.sqrt(dx * dx + dy * dy);
310
- // return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
311
-
312
- // 直線
313
- return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y;
314
-
315
-
316
- }
317
-
318
-
319
-
320
- function renderArrowMarker (defsSelection: d3.Selection<SVGDefsElement, any, any, unknown>, fullParams: ForceDirectedParams, fullChartParams: ChartParams) {
321
- return defsSelection
322
- .selectAll<SVGMarkerElement, any>(`marker.${defsArrowMarkerClassName}`)
323
- .data([fullParams])
324
- .join(
325
- enter => {
326
- const enterSelection = enter
327
- .append("marker")
328
- .classed(defsArrowMarkerClassName, true)
329
- .attr('id', defsArrowMarkerId)
330
- .attr('fill', d => getColor(fullParams.arrow.colorType, fullChartParams ))
331
- .attr("viewBox", d => `-${d.arrow.pointerWidth} -${d.arrow.pointerHeight / 2} ${d.arrow.pointerWidth} ${d.arrow.pointerHeight}`)
332
- .attr("orient", "auto")
333
- enterSelection.append("path")
334
- .attr("d", d => `M${-d.arrow.pointerWidth},${-d.arrow.pointerHeight / 2}L0,0L${-d.arrow.pointerWidth},${d.arrow.pointerHeight / 2}`) // 箭頭的尖端為(0,0)
335
- return enterSelection
336
- },
337
- update => {
338
- return update
339
- },
340
- exit => {
341
- return exit.remove()
342
- }
343
- )
344
- .attr("markerWidth", d => d.arrow.pointerWidth)
345
- .attr("markerHeight", d => d.arrow.pointerHeight)
346
- /* refX:修正marker位置(計算出和circle半徑相等的寬度)
347
- (1)circle半徑需加上 strokeWidth/2 是因為框線是以 circle 的邊緣往內及往外擴展,所以 stroke 多出來的寬度是一半而已
348
- (2)circle半徑需除以 path 寬度是因為「marker 的位置會受到 path 的stroke-width影響」,所以要進行修正
349
- (3)- 1 是要修正奇怪的誤差(不知原因)
350
- */
351
- .attr('refX', d => ((d.dot.radius + (fullParams.dot.strokeWidth / 2)) / d.arrow.strokeWidth) - 1)
352
- .attr("refY", 0)
353
-
354
- }
355
-
356
- // function drag (): d3.DragBehavior<Element, unknown, unknown> {
357
- // let originHighlightLockMode: boolean // 拖拽前的highlightLockMode
358
-
359
- // return d3.drag()
360
- // .on("start", (event: D3DragEvent) => {
361
- // console.log('start', event.sourceEvent)
362
- // // if (this.params.lockMode) {
363
- // // return
364
- // // }
365
- // // if (!d3.event.active) {
366
- // // this.forceRestart()
367
- // // }
368
- // // d.fx = d.x
369
- // // d.fy = d.y
370
-
371
- // // // 鎖定模式才不會在拖拽過程式觸發到其他事件造成衝突
372
- // // originHighlightLockMode = this.highlightLockMode
373
- // // this.highlightLockMode = true
374
- // // this.noneStopMode = true
375
- // // // 動畫會有點卡住所以乾脆拿掉
376
- // // if(this.tooltip != null) {
377
- // // this.tooltip.remove()
378
- // // }
379
- // })
380
- // .on("drag", function (event: D3DragEvent) {
381
- // console.log('drag', event)
382
- // // if (this.params.lockMode) {
383
- // // return
384
- // // }
385
- // // if (!d3.event.active) {
386
- // // this.force.alphaTarget(0)
387
- // // }
388
- // // d.fx = d3.event.x
389
- // // d.fy = d3.event.y
390
- // // d3.select(this).attr({
391
- // // 'cx': event.x,
392
- // // 'cy': event.y,
393
- // // })
394
- // d3.select(this)
395
- // .attr('fx', event.x)
396
- // .attr('fy', event.y)
397
- // })
398
- // .on("end", (event: D3DragEvent) => {
399
- // console.log('end', event)
400
- // // if (this.params.lockMode) {
401
- // // return
402
- // // }
403
- // // d.fx = null
404
- // // d.fy = null
405
-
406
- // // this.highlightLockMode = originHighlightLockMode // 還原拖拽前的highlightLockMode
407
- // // this.noneStopMode = false
408
- // // if (this.highlightLockMode) {
409
- // // this.forceStop()
410
- // // }
411
- // })
412
- // }
413
-
414
- function drag (simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>, dragStatus$: BehaviorSubject<DragStatus>) {
415
- function dragstarted (event: D3DragEvent, node: RenderNode) {
416
- if (!event.active) simulation.alphaTarget(0.3).restart()
417
- event.subject.fx = event.x
418
- event.subject.fy = event.y
419
-
420
- dragStatus$.next('start')
421
- }
422
-
423
- function dragged (event: D3DragEvent, node: RenderNode) {
424
- event.subject.fx = event.x
425
- event.subject.fy = event.y
426
-
427
- dragStatus$.next('drag')
428
- }
429
-
430
- function dragended (event: D3DragEvent, node: RenderNode) {
431
- if (!event.active) simulation.alphaTarget(0);
432
- event.subject.fx = null
433
- event.subject.fy = null
434
-
435
- dragStatus$.next('end')
436
- }
437
-
438
- return d3.drag()
439
- .on("start", dragstarted)
440
- .on("drag", dragged)
441
- .on("end", dragended)
442
- }
443
-
444
- function renderNodeG ({ nodeListGSelection, nodes }: {
445
- nodeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
446
- nodes: RenderNode[]
447
- }) {
448
- return nodeListGSelection.selectAll<SVGGElement, RenderNode>('g')
449
- .data(nodes, d => d.id)
450
- .join(
451
- enter => {
452
- const enterSelection = enter
453
- .append('g')
454
- .classed(nodeGClassName, true)
455
- // .attr('cursor', 'pointer')
456
- return enterSelection
457
- },
458
- update => {
459
- return update
460
- },
461
- exit => {
462
- return exit.remove()
463
- }
464
- )
465
- }
466
-
467
- function renderNodeCircle ({ nodeGSelection, fullParams, fullChartParams }: {
468
- nodeGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
469
- fullParams: ForceDirectedParams
470
- fullChartParams: ChartParams
471
- }) {
472
- nodeGSelection.each((data,i,g) => {
473
- const gSelection = d3.select(g[i])
474
- gSelection.selectAll<SVGCircleElement, ComputedEdge>('circle')
475
- .data([data])
476
- .join(
477
- enter => {
478
- const enterSelection = enter
479
- .append('circle')
480
- .classed(nodeCircleClassName, true)
481
- .attr('cursor', 'pointer')
482
- return enterSelection
483
- },
484
- update => {
485
- return update
486
- },
487
- exit => {
488
- return exit.remove()
489
- }
490
- )
491
- .attr('r', fullParams.dot.radius)
492
- .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.dot.fillColorType, fullChartParams }))
493
- .attr('stroke', d => getDatumColor({ datum: d, colorType: fullParams.dot.strokeColorType, fullChartParams }))
494
- .attr('stroke-width', fullParams.dot.strokeWidth)
495
- .attr('style', d => fullParams.dot.styleFn(d))
496
- })
497
-
498
- return nodeGSelection.select<SVGCircleElement>(`circle.${nodeCircleClassName}`)
499
- }
500
-
501
- function renderNodeLabelG ({ nodeGSelection, fullParams }: {
502
- nodeGSelection: d3.Selection<SVGGElement, any, any, unknown>
503
- fullParams: ForceDirectedParams
504
- }) {
505
- nodeGSelection.each((data,i,g) => {
506
- const gSelection = d3.select(g[i])
507
- gSelection.selectAll<SVGGElement, RenderNode>('g')
508
- .data([data])
509
- .join(
510
- enter => {
511
- const enterSelection = enter
512
- .append('g')
513
- .classed(nodeLabelGClassName, true)
514
- // .attr('cursor', 'pointer')
515
- return enterSelection
516
- },
517
- update => {
518
- return update
519
- },
520
- exit => {
521
- return exit.remove()
522
- }
523
- )
524
- .attr('transform', `translate(0, ${- fullParams.dot.radius - 10})`)
525
- })
526
-
527
- return nodeGSelection.select<SVGTextElement>(`g.${nodeLabelGClassName}`)
528
- }
529
-
530
- function renderNodeLabel ({ nodeLabelGSelection, fullParams, fullChartParams }: {
531
- nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
532
- fullParams: ForceDirectedParams
533
- fullChartParams: ChartParams
534
- }) {
535
- nodeLabelGSelection.each((data,i,g) => {
536
- const gSelection = d3.select(g[i])
537
- gSelection.selectAll<SVGTextElement, RenderNode>('text')
538
- .data([data], d => d.id)
539
- .join(
540
- enter => {
541
- const enterSelection = enter
542
- .append('text')
543
- .classed(nodeLabelClassName, true)
544
- // .attr('cursor', 'pointer')
545
- .attr('text-anchor', 'middle')
546
- .attr('pointer-events', 'none')
547
- return enterSelection
548
- },
549
- update => {
550
- return update
551
- },
552
- exit => {
553
- return exit.remove()
554
- }
555
- )
556
- .text(d => d.label)
557
- .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.dotLabel.colorType, fullChartParams }))
558
- .attr('font-size', fullChartParams.styles.textSize)
559
- .attr('style', d => fullParams.dotLabel.styleFn(d))
560
- })
561
-
562
- return nodeLabelGSelection.select<SVGTextElement>(`text.${nodeLabelClassName}`)
563
- }
564
-
565
- function renderEdgeG ({ edgeListGSelection, edges }: {
566
- edgeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
567
- edges: RenderEdge[]
568
- }) {
569
- return edgeListGSelection.selectAll<SVGGElement, RenderEdge>('g')
570
- .data(edges, d => d.id)
571
- .join(
572
- enter => {
573
- const enterSelection = enter
574
- .append('g')
575
- .classed(edgeGClassName, true)
576
- // .attr('cursor', 'pointer')
577
- return enterSelection
578
- },
579
- update => {
580
- return update
581
- },
582
- exit => {
583
- return exit.remove()
584
- }
585
- )
586
- }
587
-
588
- function renderEdgeArrowPath ({ edgeGSelection, fullParams, fullChartParams }: {
589
- edgeGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
590
- fullParams: ForceDirectedParams
591
- fullChartParams: ChartParams
592
- }) {
593
- edgeGSelection.each((data,i,g) => {
594
- const gSelection = d3.select(g[i])
595
- gSelection.selectAll<SVGPathElement, ComputedEdge>('path')
596
- .data([data])
597
- .join(
598
- enter => {
599
- return enter
600
- .append('path')
601
- .classed(edgeArrowPathClassName, true)
602
- .attr('marker-end', `url(#${defsArrowMarkerId})`)
603
- },
604
- update => {
605
- return update
606
- },
607
- exit => {
608
- return exit.remove()
609
- }
610
- )
611
- .attr('stroke', d => getDatumColor({ datum: d.data, colorType: fullParams.arrow.colorType, fullChartParams }))
612
- .attr('stroke-width', fullParams.arrow.strokeWidth)
613
- .attr('style', d => fullParams.arrow.styleFn(d))
614
- })
615
-
616
- return edgeGSelection.select<SVGPathElement>(`path.${edgeArrowPathClassName}`)
617
- }
618
-
619
- function renderEdgeLabelG ({ edgeGSelection }: {
620
- edgeGSelection: d3.Selection<SVGGElement, any, any, unknown>
621
- }) {
622
- edgeGSelection.each((data,i,g) => {
623
- const gSelection = d3.select(g[i])
624
- gSelection.selectAll<SVGGElement, RenderEdge>('g')
625
- .data([data])
626
- .join(
627
- enter => {
628
- const enterSelection = enter
629
- .append('g')
630
- .classed(edgeLabelGClassName, true)
631
- // .attr('cursor', 'pointer')
632
- return enterSelection
633
- },
634
- update => {
635
- return update
636
- },
637
- exit => {
638
- return exit.remove()
639
- }
640
- )
641
- })
642
-
643
- return edgeGSelection.select<SVGTextElement>(`g.${edgeLabelGClassName}`)
644
- }
645
-
646
- function renderEdgeLabel ({ edgeLabelGSelection, fullParams, fullChartParams }: {
647
- edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
648
- fullParams: ForceDirectedParams
649
- fullChartParams: ChartParams
650
- }) {
651
- edgeLabelGSelection.each((data,i,g) => {
652
- const gSelection = d3.select(g[i])
653
- gSelection.selectAll<SVGTextElement, RenderEdge>('text')
654
- .data([data], d => d.id)
655
- .join(
656
- enter => {
657
- const enterSelection = enter
658
- .append('text')
659
- .classed(edgeLabelClassName, true)
660
- // .attr('cursor', 'pointer')
661
- .attr('text-anchor', 'middle')
662
- .attr('pointer-events', 'none')
663
- return enterSelection
664
- },
665
- update => {
666
- return update
667
- },
668
- exit => {
669
- return exit.remove()
670
- }
671
- )
672
- .text(d => d.label)
673
- .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.arrowLabel.colorType, fullChartParams }))
674
- .attr('font-size', fullChartParams.styles.textSize)
675
- .attr('style', d => fullParams.arrowLabel.styleFn(d))
676
- })
677
-
678
- return edgeLabelGSelection.select<SVGTextElement>(`text.${edgeLabelClassName}`)
679
- }
680
-
681
- function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
682
- nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
683
- edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
684
- fullChartParams: ChartParams
685
- highlightIds: string[]
686
- }) {
687
- nodeGSelection.interrupt('highlight')
688
- edgeGSelection.interrupt('highlight')
689
- // console.log(highlightIds)
690
- if (!highlightIds.length) {
691
- nodeGSelection
692
- .transition('highlight')
693
- .style('opacity', 1)
694
- edgeGSelection
695
- .transition('highlight')
696
- .style('opacity', 1)
697
- return
698
- }
699
-
700
- edgeGSelection
701
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
702
-
703
- nodeGSelection.each((d, i, n) => {
704
- const segment = d3.select(n[i])
705
-
706
- if (highlightIds.includes(d.id)) {
707
- segment
708
- .style('opacity', 1)
709
- .transition('highlight')
710
- .ease(d3.easeElastic)
711
- .duration(500)
712
- } else {
713
- // 取消
714
- segment
715
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
716
- }
717
- })
718
- }
719
-
720
- // 暫不處理edge的highlight
721
- // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
722
- // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
723
- // fullChartParams: ChartParams
724
- // highlightIds: string[]
725
- // }) {
726
- // edgeGSelection.interrupt('highlight')
727
-
728
- // if (!highlightIds.length) {
729
- // edgeGSelection
730
- // .transition('highlight')
731
- // .style('opacity', 1)
732
- // return
733
- // }
734
-
735
- // edgeGSelection.each((d, i, n) => {
736
- // const segment = d3.select(n[i])
737
-
738
- // if (highlightIds.includes(d.id)) {
739
- // segment
740
- // .style('opacity', 1)
741
- // .transition('highlight')
742
- // .ease(d3.easeElastic)
743
- // .duration(500)
744
- // } else {
745
- // // 取消放大
746
- // segment
747
- // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
748
- // }
749
- // })
750
- // }
751
-
752
- export const ForceDirected = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
753
-
754
- const destroy$ = new Subject()
755
-
756
- const gSelection = selection.append('g').classed(gSelectionClassName, true)
757
- const defsSelection = gSelection.append('defs')
758
- const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
759
- const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
760
-
761
- let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
762
- let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
763
- let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
764
- let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
765
- let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
766
- let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
767
- let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
768
- let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
769
-
770
- const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
771
- const mouseEvent$ = new Subject<EventRelationship>()
772
-
773
- // <marker> marker selection
774
- combineLatest({
775
- fullParams: observer.fullParams$,
776
- fullChartParams: observer.fullChartParams$
777
- }).pipe(
778
- takeUntil(destroy$),
779
- switchMap(async d => d),
780
- map(({ fullParams, fullChartParams }) => {
781
- return renderArrowMarker(defsSelection, fullParams, fullChartParams)
782
- })
783
- ).subscribe()
784
-
785
- // init zoom
786
- const d3Zoom$ = observer.fullParams$.pipe(
787
- takeUntil(destroy$),
788
- // map(d => d.scaleExtent),
789
- // distinctUntilChanged((a, b) => String(a) === String(b)),
790
- // first(),
791
- map(data => {
792
- let d3Zoom = data.zoomable
793
- ? d3.zoom().on('zoom', (event) => {
794
- // console.log(event)
795
- // this.svgGroup.attr('transform', `translate(
796
- // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
797
- // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
798
- // ) scale(
799
- // ${event.transform.k}
800
- // )`)
801
- gSelection.attr('transform', `translate(
802
- ${event.transform.x},
803
- ${event.transform.y}
804
- ) scale(
805
- ${event.transform.k}
806
- )`)
807
-
808
- if (data.dotLabel.sizeFixed && nodeLabelSelection) {
809
- // 反向 scale 抵消掉放大縮小
810
- nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
811
- }
812
- if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
813
- // 反向 scale 抵消掉放大縮小
814
- edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
815
- }
816
- })
817
- : d3.zoom().on('zoom', null)
818
- if (data.scaleExtent) {
819
- d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
820
- }
821
- rootSelection.call(d3Zoom)
822
-
823
- return d3Zoom
824
- }),
825
- // shareReplay(1)
826
- )
827
-
828
- // zoom transform
829
- combineLatest({
830
- d3Zoom: d3Zoom$,
831
- transform: observer.fullParams$.pipe(
832
- takeUntil(destroy$),
833
- map(d => d.transform),
834
- )
835
- }).pipe(
836
- takeUntil(destroy$),
837
- switchMap(async d => d)
838
- ).subscribe(data => {
839
- // console.log('call')
840
- selection.call(
841
- data.d3Zoom.transform, d3.zoomIdentity
842
- .translate(data.transform.x, data.transform.y)
843
- .scale(data.transform.k)
844
- )
845
- })
846
-
847
-
848
- const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
849
- layout: observer.layout$.pipe(
850
- first() // 只使用第一次的尺寸(置中)
851
- ),
852
- fullParams: observer.fullParams$
853
- }).pipe(
854
- takeUntil(destroy$),
855
- switchMap(async d => d),
856
- map(data => createSimulation(data.layout, data.fullParams)),
857
- shareReplay(1)
858
- )
859
-
860
- const renderData$: Observable<RenderData> = observer.visibleComputedData$.pipe(
861
- takeUntil(destroy$),
862
- map(data => {
863
- return {
864
- nodes: data.nodes,
865
- edges: data.edges.map(_d => {
866
- let d: RenderEdge = _d as RenderEdge
867
- d.source = _d.startNode // reference
868
- d.target = _d.endNode
869
- return d
870
- })
871
- }
872
- }),
873
- shareReplay(1)
874
- )
875
-
876
- combineLatest({
877
- renderData: renderData$,
878
- computedData: observer.computedData$,
879
- CategoryNodeMap: observer.CategoryNodeMap$,
880
- simulation: simulation$,
881
- fullParams: observer.fullParams$,
882
- fullChartParams: observer.fullChartParams$
883
- }).pipe(
884
- takeUntil(destroy$),
885
- switchMap(async d => d),
886
- ).subscribe(data => {
887
-
888
- nodeGSelection = renderNodeG({
889
- nodeListGSelection: nodeListGSelection,
890
- nodes: data.renderData.nodes,
891
- })
892
-
893
- nodeCircleSelection = renderNodeCircle({
894
- nodeGSelection: nodeGSelection,
895
- fullParams: data.fullParams,
896
- fullChartParams: data.fullChartParams
897
- })
898
- nodeGSelection.call(drag(data.simulation, dragStatus$))
899
-
900
- nodeLabelGSelection = renderNodeLabelG({
901
- nodeGSelection: nodeGSelection,
902
- fullParams: data.fullParams
903
- })
904
-
905
- nodeLabelSelection = renderNodeLabel({
906
- nodeLabelGSelection: nodeLabelGSelection,
907
- fullParams: data.fullParams,
908
- fullChartParams: data.fullChartParams
909
- })
910
-
911
- edgeGSelection = renderEdgeG({
912
- edgeListGSelection: edgeListGSelection,
913
- edges: data.renderData.edges
914
- })
915
-
916
- edgeArrowSelection = renderEdgeArrowPath({
917
- edgeGSelection: edgeGSelection,
918
- fullParams: data.fullParams,
919
- fullChartParams: data.fullChartParams
920
- })
921
-
922
- edgeLabelGSelection = renderEdgeLabelG({
923
- edgeGSelection: edgeGSelection,
924
- })
925
-
926
- edgeLabelSelection = renderEdgeLabel({
927
- edgeLabelGSelection: edgeLabelGSelection,
928
- fullParams: data.fullParams,
929
- fullChartParams: data.fullChartParams
930
- })
931
-
932
- data.simulation.nodes(data.renderData.nodes)
933
- .on('tick', () => {
934
- edgeArrowSelection.attr('d', linkArcFn)
935
- nodeGSelection.attr('transform', translateFn)
936
- // nodeLabelGSelection.attr('transform', d => translateFn({
937
- // x: d.x,
938
- // y: d.y - data.fullParams.dot.radius - 10
939
- // }))
940
- edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
941
- })
942
- ;(data.simulation.force("link") as any).links(data.renderData.edges)
943
-
944
- data.simulation.alpha(0.3).restart()
945
-
946
- nodeCircleSelection
947
- .on('mouseover', (event, datum) => {
948
- event.stopPropagation()
949
-
950
- mouseEvent$.next({
951
- type: 'relationship',
952
- eventName: 'mouseover',
953
- pluginName,
954
- highlightTarget: data.fullChartParams.highlightTarget,
955
- datum: datum,
956
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
957
- categoryIndex: datum.categoryIndex,
958
- categoryLabel: datum.categoryLabel,
959
- event,
960
- data: data.computedData
961
- })
962
- })
963
- .on('mousemove', (event, datum) => {
964
- event.stopPropagation()
965
-
966
- mouseEvent$.next({
967
- type: 'relationship',
968
- eventName: 'mousemove',
969
- pluginName,
970
- highlightTarget: data.fullChartParams.highlightTarget,
971
- datum: datum,
972
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
973
- categoryIndex: datum.categoryIndex,
974
- categoryLabel: datum.categoryLabel,
975
- event,
976
- data: data.computedData
977
- })
978
- })
979
- .on('mouseout', (event, datum) => {
980
- event.stopPropagation()
981
-
982
- mouseEvent$.next({
983
- type: 'relationship',
984
- eventName: 'mouseout',
985
- pluginName,
986
- highlightTarget: data.fullChartParams.highlightTarget,
987
- datum: datum,
988
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
989
- categoryIndex: datum.categoryIndex,
990
- categoryLabel: datum.categoryLabel,
991
- event,
992
- data: data.computedData
993
- })
994
- })
995
- .on('click', (event, datum) => {
996
- event.stopPropagation()
997
-
998
- mouseEvent$.next({
999
- type: 'relationship',
1000
- eventName: 'click',
1001
- pluginName,
1002
- highlightTarget: data.fullChartParams.highlightTarget,
1003
- datum: datum,
1004
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1005
- categoryIndex: datum.categoryIndex,
1006
- categoryLabel: datum.categoryLabel,
1007
- event,
1008
- data: data.computedData
1009
- })
1010
- })
1011
- })
1012
-
1013
- dragStatus$.pipe(
1014
- distinctUntilChanged((a, b) => a === b),
1015
- // 只有沒有托曳時才執行
1016
- switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1017
- ).subscribe(data => {
1018
- subject.event$.next(data)
1019
- })
1020
-
1021
- combineLatest({
1022
- renderData: renderData$,
1023
- highlightNodes: observer.relationshipHighlightNodes$.pipe(
1024
- map(data => data.map(d => d.id))
1025
- ),
1026
- highlightEdges: observer.relationshipHighlightEdges$.pipe(
1027
- map(data => data.map(d => d.id))
1028
- ),
1029
- fullChartParams: observer.fullChartParams$,
1030
- fullParams: observer.fullParams$,
1031
- }).pipe(
1032
- takeUntil(destroy$),
1033
- switchMap(async d => d)
1034
- ).subscribe(data => {
1035
- if (!nodeGSelection || !edgeGSelection) {
1036
- return
1037
- }
1038
-
1039
- highlightNodes({
1040
- nodeGSelection,
1041
- edgeGSelection,
1042
- highlightIds: data.highlightNodes,
1043
- fullChartParams: data.fullChartParams
1044
- })
1045
- // highlightEdges({
1046
- // edgeGSelection,
1047
- // highlightIds: data.highlightEdges,
1048
- // fullChartParams: data.fullChartParams
1049
- // })
1050
- })
1051
-
1052
-
1053
-
1054
- return () => {
1055
- destroy$.next(undefined)
1056
- }
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 } from '../../../lib/core'
35
+ import type { BubblesParams, ArcScaleType, ForceDirectedParams } from '../../../lib/plugins-basic-types'
36
+ import { getColor, getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
37
+ import { DEFAULT_FORCE_DIRECTED_PARAMS } from '../defaults'
38
+ // import { renderCircleText } from '../../utils/d3Graphics'
39
+ import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
40
+ import { d3EventObservable } from '../../utils/observables'
41
+
42
+ // interface BubblesDatum extends ComputedNode {
43
+ // x: number
44
+ // y: number
45
+ // r: number
46
+ // _originR: number // 紀錄變化前的r
47
+ // }
48
+
49
+ type Zoom = {
50
+ xOffset: number
51
+ yOffset: number
52
+ scaleExtent: {
53
+ min: number
54
+ max: number
55
+ }
56
+ }
57
+
58
+ // d3 forceSimulation使用的node資料
59
+ type RenderNode = d3.SimulationNodeDatum & ComputedNode
60
+
61
+ // d3 forceSimulation使用的edge資料
62
+ interface RenderEdge extends ComputedEdge {
63
+ source: RenderNode
64
+ target: RenderNode
65
+ }
66
+
67
+ // d3 forceSimulation使用的資料
68
+ type RenderData = {
69
+ nodes: (ComputedNode | RenderNode)[] // 經過d3 forceSimulation計算後的node才有座標資訊
70
+ edges: RenderEdge[]
71
+ }
72
+
73
+ interface D3DragEvent {
74
+ active: number
75
+ dx: number
76
+ dy: number
77
+ identifier: string
78
+ sourceEvent: MouseEvent
79
+ subject: RenderNode
80
+ target: any
81
+ type: string
82
+ x: number
83
+ y: number
84
+ }
85
+
86
+ type DragStatus = 'start' | 'drag' | 'end'
87
+
88
+ // type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
89
+
90
+ const pluginName = 'ForceDirected'
91
+
92
+ const gSelectionClassName = getClassName(pluginName, 'zoom-area')
93
+ const defsArrowMarkerId = getUniID(pluginName, 'arrow')
94
+ const defsArrowMarkerClassName = getClassName(pluginName, 'arrow-marker')
95
+ const edgeListGClassName = getClassName(pluginName, 'edge-list-g')
96
+ const edgeGClassName = getClassName(pluginName, 'edge-g')
97
+ const edgeArrowPathClassName = getClassName(pluginName, 'edge-arrow-path')
98
+ const edgeLabelGClassName = getClassName(pluginName, 'edge-label-g')
99
+ const edgeLabelClassName = getClassName(pluginName, 'edge-label')
100
+ const nodeListGClassName = getClassName(pluginName, 'node-list-g')
101
+ const nodeGClassName = getClassName(pluginName, 'node-g')
102
+ const nodeCircleClassName = getClassName(pluginName, 'node-circle')
103
+ const nodeLabelGClassName = getClassName(pluginName, 'node-label-g')
104
+ const nodeLabelClassName = getClassName(pluginName, 'node-label')
105
+
106
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_FORCE_DIRECTED_PARAMS> = {
107
+ name: pluginName,
108
+ defaultParams: DEFAULT_FORCE_DIRECTED_PARAMS,
109
+ layerIndex: LAYER_INDEX_OF_GRAPHIC,
110
+ validator: (params, { validateColumns }) => {
111
+ const result = validateColumns(params, {
112
+ dot: {
113
+ toBeTypes: ['object']
114
+ },
115
+ dotLabel: {
116
+ toBeTypes: ['object']
117
+ },
118
+ arrow: {
119
+ toBeTypes: ['object']
120
+ },
121
+ arrowLabel: {
122
+ toBeTypes: ['object']
123
+ },
124
+ force: {
125
+ toBeTypes: ['object']
126
+ },
127
+ zoomable: {
128
+ toBeTypes: ['boolean']
129
+ },
130
+ transform: {
131
+ toBeTypes: ['object']
132
+ },
133
+ scaleExtent: {
134
+ toBeTypes: ['object']
135
+ }
136
+ })
137
+ if (params.dot) {
138
+ const dotResult = validateColumns(params.dot, {
139
+ radius: {
140
+ toBeTypes: ['number']
141
+ },
142
+ fillColorType: {
143
+ toBeOption: 'ColorType'
144
+ },
145
+ strokeColorType: {
146
+ toBeOption: 'ColorType'
147
+ },
148
+ strokeWidth: {
149
+ toBeTypes: ['number']
150
+ },
151
+ styleFn: {
152
+ toBeTypes: ['Function']
153
+ },
154
+ })
155
+ if (dotResult.status === 'error') {
156
+ return dotResult
157
+ }
158
+ }
159
+ if (params.dotLabel) {
160
+ const dotLabelResult = validateColumns(params.dotLabel, {
161
+ colorType: {
162
+ toBeOption: 'ColorType'
163
+ },
164
+ sizeFixed: {
165
+ toBeTypes: ['boolean']
166
+ },
167
+ styleFn: {
168
+ toBeTypes: ['Function']
169
+ },
170
+ })
171
+ if (dotLabelResult.status === 'error') {
172
+ return dotLabelResult
173
+ }
174
+ }
175
+ if (params.arrow) {
176
+ const arrowResult = validateColumns(params.arrow, {
177
+ colorType: {
178
+ toBeOption: 'ColorType'
179
+ },
180
+ strokeWidth: {
181
+ toBeTypes: ['number']
182
+ },
183
+ pointerWidth: {
184
+ toBeTypes: ['number']
185
+ },
186
+ pointerHeight: {
187
+ toBeTypes: ['number']
188
+ },
189
+ styleFn: {
190
+ toBeTypes: ['Function']
191
+ },
192
+ })
193
+ if (arrowResult.status === 'error') {
194
+ return arrowResult
195
+ }
196
+ }
197
+ if (params.arrowLabel) {
198
+ const arrowLabelResult = validateColumns(params.arrowLabel, {
199
+ colorType: {
200
+ toBeOption: 'ColorType'
201
+ },
202
+ sizeFixed: {
203
+ toBeTypes: ['boolean']
204
+ },
205
+ styleFn: {
206
+ toBeTypes: ['Function']
207
+ },
208
+ })
209
+ if (arrowLabelResult.status === 'error') {
210
+ return arrowLabelResult
211
+ }
212
+ }
213
+ if (params.force) {
214
+ const forceResult = validateColumns(params.force, {
215
+ nodeStrength: {
216
+ toBeTypes: ['number']
217
+ },
218
+ linkDistance: {
219
+ toBeTypes: ['number']
220
+ },
221
+ velocityDecay: {
222
+ toBeTypes: ['number']
223
+ },
224
+ alphaDecay: {
225
+ toBeTypes: ['number']
226
+ },
227
+ })
228
+ if (forceResult.status === 'error') {
229
+ return forceResult
230
+ }
231
+ }
232
+ if (params.transform) {
233
+ const transformResult = validateColumns(params.transform, {
234
+ x: {
235
+ toBeTypes: ['number']
236
+ },
237
+ y: {
238
+ toBeTypes: ['number']
239
+ },
240
+ k: {
241
+ toBeTypes: ['number']
242
+ },
243
+ })
244
+ if (transformResult.status === 'error') {
245
+ return transformResult
246
+ }
247
+ }
248
+ if (params.scaleExtent) {
249
+ const scaleExtentResult = validateColumns(params.scaleExtent, {
250
+ min: {
251
+ toBeTypes: ['number']
252
+ },
253
+ max: {
254
+ toBeTypes: ['number']
255
+ },
256
+ })
257
+ if (scaleExtentResult.status === 'error') {
258
+ return scaleExtentResult
259
+ }
260
+ }
261
+ return result
262
+ }
263
+ }
264
+
265
+ // let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
266
+
267
+ function createSimulation (layout: Layout, fullParams: ForceDirectedParams) {
268
+ return d3.forceSimulation()
269
+ .velocityDecay(fullParams.force.velocityDecay)
270
+ .alphaDecay(fullParams.force.alphaDecay)
271
+ .force(
272
+ "link",
273
+ d3.forceLink()
274
+ .id((d: d3.SimulationNodeDatum & ComputedNode) => d.id)
275
+ .strength(1)
276
+ .distance((d: d3.SimulationLinkDatum<d3.SimulationNodeDatum & ComputedNode>) => {
277
+ // if (d.direction === 'top') {
278
+ // return 200
279
+ // } else {
280
+ // return 250
281
+ // }
282
+ return fullParams.force.linkDistance
283
+ })
284
+ )
285
+ .force("charge", d3.forceManyBody().strength(fullParams.force.nodeStrength))
286
+ .force("collision", d3.forceCollide(fullParams.dot.radius).strength(1))
287
+ .force("center", d3.forceCenter(layout.width / 2, layout.height / 2))
288
+
289
+ }
290
+
291
+ function translateFn (d: any): string {
292
+ // console.log('translateFn', d)
293
+ return "translate(" + d.x + "," + d.y + ")";
294
+ }
295
+
296
+ function translateCenterFn (d: any): string {
297
+ // console.log('translateCenterFn', d)
298
+ const x = d.source.x + ((d.target.x - d.source.x) / 2) // 置中的話除2
299
+ const y = d.source.y + ((d.target.y - d.source.y) / 2) // 置中的話除2
300
+ return "translate(" + x + "," + y + ")";
301
+ }
302
+
303
+ function linkArcFn (d: RenderEdge): string {
304
+ // console.log('linkArcFn', d)
305
+
306
+ // const dx = d.target.x - d.source.x,
307
+ // dy = d.target.y - d.source.y
308
+ // dr讓方向線變成有弧度的
309
+ // dr = Math.sqrt(dx * dx + dy * dy);
310
+ // return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
311
+
312
+ // 直線
313
+ return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y;
314
+
315
+
316
+ }
317
+
318
+
319
+
320
+ function renderArrowMarker (defsSelection: d3.Selection<SVGDefsElement, any, any, unknown>, fullParams: ForceDirectedParams, fullChartParams: ChartParams) {
321
+ return defsSelection
322
+ .selectAll<SVGMarkerElement, any>(`marker.${defsArrowMarkerClassName}`)
323
+ .data([fullParams])
324
+ .join(
325
+ enter => {
326
+ const enterSelection = enter
327
+ .append("marker")
328
+ .classed(defsArrowMarkerClassName, true)
329
+ .attr('id', defsArrowMarkerId)
330
+ .attr('fill', d => getColor(fullParams.arrow.colorType, fullChartParams ))
331
+ .attr("viewBox", d => `-${d.arrow.pointerWidth} -${d.arrow.pointerHeight / 2} ${d.arrow.pointerWidth} ${d.arrow.pointerHeight}`)
332
+ .attr("orient", "auto")
333
+ enterSelection.append("path")
334
+ .attr("d", d => `M${-d.arrow.pointerWidth},${-d.arrow.pointerHeight / 2}L0,0L${-d.arrow.pointerWidth},${d.arrow.pointerHeight / 2}`) // 箭頭的尖端為(0,0)
335
+ return enterSelection
336
+ },
337
+ update => {
338
+ return update
339
+ },
340
+ exit => {
341
+ return exit.remove()
342
+ }
343
+ )
344
+ .attr("markerWidth", d => d.arrow.pointerWidth)
345
+ .attr("markerHeight", d => d.arrow.pointerHeight)
346
+ /* refX:修正marker位置(計算出和circle半徑相等的寬度)
347
+ (1)circle半徑需加上 strokeWidth/2 是因為框線是以 circle 的邊緣往內及往外擴展,所以 stroke 多出來的寬度是一半而已
348
+ (2)circle半徑需除以 path 寬度是因為「marker 的位置會受到 path 的stroke-width影響」,所以要進行修正
349
+ (3)- 1 是要修正奇怪的誤差(不知原因)
350
+ */
351
+ .attr('refX', d => ((d.dot.radius + (fullParams.dot.strokeWidth / 2)) / d.arrow.strokeWidth) - 1)
352
+ .attr("refY", 0)
353
+
354
+ }
355
+
356
+ // function drag (): d3.DragBehavior<Element, unknown, unknown> {
357
+ // let originHighlightLockMode: boolean // 拖拽前的highlightLockMode
358
+
359
+ // return d3.drag()
360
+ // .on("start", (event: D3DragEvent) => {
361
+ // console.log('start', event.sourceEvent)
362
+ // // if (this.params.lockMode) {
363
+ // // return
364
+ // // }
365
+ // // if (!d3.event.active) {
366
+ // // this.forceRestart()
367
+ // // }
368
+ // // d.fx = d.x
369
+ // // d.fy = d.y
370
+
371
+ // // // 鎖定模式才不會在拖拽過程式觸發到其他事件造成衝突
372
+ // // originHighlightLockMode = this.highlightLockMode
373
+ // // this.highlightLockMode = true
374
+ // // this.noneStopMode = true
375
+ // // // 動畫會有點卡住所以乾脆拿掉
376
+ // // if(this.tooltip != null) {
377
+ // // this.tooltip.remove()
378
+ // // }
379
+ // })
380
+ // .on("drag", function (event: D3DragEvent) {
381
+ // console.log('drag', event)
382
+ // // if (this.params.lockMode) {
383
+ // // return
384
+ // // }
385
+ // // if (!d3.event.active) {
386
+ // // this.force.alphaTarget(0)
387
+ // // }
388
+ // // d.fx = d3.event.x
389
+ // // d.fy = d3.event.y
390
+ // // d3.select(this).attr({
391
+ // // 'cx': event.x,
392
+ // // 'cy': event.y,
393
+ // // })
394
+ // d3.select(this)
395
+ // .attr('fx', event.x)
396
+ // .attr('fy', event.y)
397
+ // })
398
+ // .on("end", (event: D3DragEvent) => {
399
+ // console.log('end', event)
400
+ // // if (this.params.lockMode) {
401
+ // // return
402
+ // // }
403
+ // // d.fx = null
404
+ // // d.fy = null
405
+
406
+ // // this.highlightLockMode = originHighlightLockMode // 還原拖拽前的highlightLockMode
407
+ // // this.noneStopMode = false
408
+ // // if (this.highlightLockMode) {
409
+ // // this.forceStop()
410
+ // // }
411
+ // })
412
+ // }
413
+
414
+ function drag (simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>, dragStatus$: BehaviorSubject<DragStatus>) {
415
+ function dragstarted (event: D3DragEvent, node: RenderNode) {
416
+ if (!event.active) simulation.alphaTarget(0.3).restart()
417
+ event.subject.fx = event.x
418
+ event.subject.fy = event.y
419
+
420
+ dragStatus$.next('start')
421
+ }
422
+
423
+ function dragged (event: D3DragEvent, node: RenderNode) {
424
+ event.subject.fx = event.x
425
+ event.subject.fy = event.y
426
+
427
+ dragStatus$.next('drag')
428
+ }
429
+
430
+ function dragended (event: D3DragEvent, node: RenderNode) {
431
+ if (!event.active) simulation.alphaTarget(0);
432
+ event.subject.fx = null
433
+ event.subject.fy = null
434
+
435
+ dragStatus$.next('end')
436
+ }
437
+
438
+ return d3.drag()
439
+ .on("start", dragstarted)
440
+ .on("drag", dragged)
441
+ .on("end", dragended)
442
+ }
443
+
444
+ function renderNodeG ({ nodeListGSelection, nodes }: {
445
+ nodeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
446
+ nodes: RenderNode[]
447
+ }) {
448
+ return nodeListGSelection.selectAll<SVGGElement, RenderNode>('g')
449
+ .data(nodes, d => d.id)
450
+ .join(
451
+ enter => {
452
+ const enterSelection = enter
453
+ .append('g')
454
+ .classed(nodeGClassName, true)
455
+ // .attr('cursor', 'pointer')
456
+ return enterSelection
457
+ },
458
+ update => {
459
+ return update
460
+ },
461
+ exit => {
462
+ return exit.remove()
463
+ }
464
+ )
465
+ }
466
+
467
+ function renderNodeCircle ({ nodeGSelection, fullParams, fullChartParams }: {
468
+ nodeGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
469
+ fullParams: ForceDirectedParams
470
+ fullChartParams: ChartParams
471
+ }) {
472
+ nodeGSelection.each((data,i,g) => {
473
+ const gSelection = d3.select(g[i])
474
+ gSelection.selectAll<SVGCircleElement, ComputedEdge>('circle')
475
+ .data([data])
476
+ .join(
477
+ enter => {
478
+ const enterSelection = enter
479
+ .append('circle')
480
+ .classed(nodeCircleClassName, true)
481
+ .attr('cursor', 'pointer')
482
+ return enterSelection
483
+ },
484
+ update => {
485
+ return update
486
+ },
487
+ exit => {
488
+ return exit.remove()
489
+ }
490
+ )
491
+ .attr('r', fullParams.dot.radius)
492
+ .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.dot.fillColorType, fullChartParams }))
493
+ .attr('stroke', d => getDatumColor({ datum: d, colorType: fullParams.dot.strokeColorType, fullChartParams }))
494
+ .attr('stroke-width', fullParams.dot.strokeWidth)
495
+ .attr('style', d => fullParams.dot.styleFn(d))
496
+ })
497
+
498
+ return nodeGSelection.select<SVGCircleElement>(`circle.${nodeCircleClassName}`)
499
+ }
500
+
501
+ function renderNodeLabelG ({ nodeGSelection, fullParams }: {
502
+ nodeGSelection: d3.Selection<SVGGElement, any, any, unknown>
503
+ fullParams: ForceDirectedParams
504
+ }) {
505
+ nodeGSelection.each((data,i,g) => {
506
+ const gSelection = d3.select(g[i])
507
+ gSelection.selectAll<SVGGElement, RenderNode>('g')
508
+ .data([data])
509
+ .join(
510
+ enter => {
511
+ const enterSelection = enter
512
+ .append('g')
513
+ .classed(nodeLabelGClassName, true)
514
+ // .attr('cursor', 'pointer')
515
+ return enterSelection
516
+ },
517
+ update => {
518
+ return update
519
+ },
520
+ exit => {
521
+ return exit.remove()
522
+ }
523
+ )
524
+ .attr('transform', `translate(0, ${- fullParams.dot.radius - 10})`)
525
+ })
526
+
527
+ return nodeGSelection.select<SVGTextElement>(`g.${nodeLabelGClassName}`)
528
+ }
529
+
530
+ function renderNodeLabel ({ nodeLabelGSelection, fullParams, fullChartParams }: {
531
+ nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, any, unknown>
532
+ fullParams: ForceDirectedParams
533
+ fullChartParams: ChartParams
534
+ }) {
535
+ nodeLabelGSelection.each((data,i,g) => {
536
+ const gSelection = d3.select(g[i])
537
+ gSelection.selectAll<SVGTextElement, RenderNode>('text')
538
+ .data([data], d => d.id)
539
+ .join(
540
+ enter => {
541
+ const enterSelection = enter
542
+ .append('text')
543
+ .classed(nodeLabelClassName, true)
544
+ // .attr('cursor', 'pointer')
545
+ .attr('text-anchor', 'middle')
546
+ .attr('pointer-events', 'none')
547
+ return enterSelection
548
+ },
549
+ update => {
550
+ return update
551
+ },
552
+ exit => {
553
+ return exit.remove()
554
+ }
555
+ )
556
+ .text(d => d.label)
557
+ .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.dotLabel.colorType, fullChartParams }))
558
+ .attr('font-size', fullChartParams.styles.textSize)
559
+ .attr('style', d => fullParams.dotLabel.styleFn(d))
560
+ })
561
+
562
+ return nodeLabelGSelection.select<SVGTextElement>(`text.${nodeLabelClassName}`)
563
+ }
564
+
565
+ function renderEdgeG ({ edgeListGSelection, edges }: {
566
+ edgeListGSelection: d3.Selection<SVGGElement, any, any, unknown>
567
+ edges: RenderEdge[]
568
+ }) {
569
+ return edgeListGSelection.selectAll<SVGGElement, RenderEdge>('g')
570
+ .data(edges, d => d.id)
571
+ .join(
572
+ enter => {
573
+ const enterSelection = enter
574
+ .append('g')
575
+ .classed(edgeGClassName, true)
576
+ // .attr('cursor', 'pointer')
577
+ return enterSelection
578
+ },
579
+ update => {
580
+ return update
581
+ },
582
+ exit => {
583
+ return exit.remove()
584
+ }
585
+ )
586
+ }
587
+
588
+ function renderEdgeArrowPath ({ edgeGSelection, fullParams, fullChartParams }: {
589
+ edgeGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
590
+ fullParams: ForceDirectedParams
591
+ fullChartParams: ChartParams
592
+ }) {
593
+ edgeGSelection.each((data,i,g) => {
594
+ const gSelection = d3.select(g[i])
595
+ gSelection.selectAll<SVGPathElement, ComputedEdge>('path')
596
+ .data([data])
597
+ .join(
598
+ enter => {
599
+ return enter
600
+ .append('path')
601
+ .classed(edgeArrowPathClassName, true)
602
+ .attr('marker-end', `url(#${defsArrowMarkerId})`)
603
+ },
604
+ update => {
605
+ return update
606
+ },
607
+ exit => {
608
+ return exit.remove()
609
+ }
610
+ )
611
+ .attr('stroke', d => getDatumColor({ datum: d.data, colorType: fullParams.arrow.colorType, fullChartParams }))
612
+ .attr('stroke-width', fullParams.arrow.strokeWidth)
613
+ .attr('style', d => fullParams.arrow.styleFn(d))
614
+ })
615
+
616
+ return edgeGSelection.select<SVGPathElement>(`path.${edgeArrowPathClassName}`)
617
+ }
618
+
619
+ function renderEdgeLabelG ({ edgeGSelection }: {
620
+ edgeGSelection: d3.Selection<SVGGElement, any, any, unknown>
621
+ }) {
622
+ edgeGSelection.each((data,i,g) => {
623
+ const gSelection = d3.select(g[i])
624
+ gSelection.selectAll<SVGGElement, RenderEdge>('g')
625
+ .data([data])
626
+ .join(
627
+ enter => {
628
+ const enterSelection = enter
629
+ .append('g')
630
+ .classed(edgeLabelGClassName, true)
631
+ // .attr('cursor', 'pointer')
632
+ return enterSelection
633
+ },
634
+ update => {
635
+ return update
636
+ },
637
+ exit => {
638
+ return exit.remove()
639
+ }
640
+ )
641
+ })
642
+
643
+ return edgeGSelection.select<SVGTextElement>(`g.${edgeLabelGClassName}`)
644
+ }
645
+
646
+ function renderEdgeLabel ({ edgeLabelGSelection, fullParams, fullChartParams }: {
647
+ edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, any, unknown>
648
+ fullParams: ForceDirectedParams
649
+ fullChartParams: ChartParams
650
+ }) {
651
+ edgeLabelGSelection.each((data,i,g) => {
652
+ const gSelection = d3.select(g[i])
653
+ gSelection.selectAll<SVGTextElement, RenderEdge>('text')
654
+ .data([data], d => d.id)
655
+ .join(
656
+ enter => {
657
+ const enterSelection = enter
658
+ .append('text')
659
+ .classed(edgeLabelClassName, true)
660
+ // .attr('cursor', 'pointer')
661
+ .attr('text-anchor', 'middle')
662
+ .attr('pointer-events', 'none')
663
+ return enterSelection
664
+ },
665
+ update => {
666
+ return update
667
+ },
668
+ exit => {
669
+ return exit.remove()
670
+ }
671
+ )
672
+ .text(d => d.label)
673
+ .attr('fill', d => getDatumColor({ datum: d, colorType: fullParams.arrowLabel.colorType, fullChartParams }))
674
+ .attr('font-size', fullChartParams.styles.textSize)
675
+ .attr('style', d => fullParams.arrowLabel.styleFn(d))
676
+ })
677
+
678
+ return edgeLabelGSelection.select<SVGTextElement>(`text.${edgeLabelClassName}`)
679
+ }
680
+
681
+ function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
682
+ nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
683
+ edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
684
+ fullChartParams: ChartParams
685
+ highlightIds: string[]
686
+ }) {
687
+ nodeGSelection.interrupt('highlight')
688
+ edgeGSelection.interrupt('highlight')
689
+ // console.log(highlightIds)
690
+ if (!highlightIds.length) {
691
+ nodeGSelection
692
+ .transition('highlight')
693
+ .style('opacity', 1)
694
+ edgeGSelection
695
+ .transition('highlight')
696
+ .style('opacity', 1)
697
+ return
698
+ }
699
+
700
+ edgeGSelection
701
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
702
+
703
+ nodeGSelection.each((d, i, n) => {
704
+ const segment = d3.select(n[i])
705
+
706
+ if (highlightIds.includes(d.id)) {
707
+ segment
708
+ .style('opacity', 1)
709
+ .transition('highlight')
710
+ .ease(d3.easeElastic)
711
+ .duration(500)
712
+ } else {
713
+ // 取消
714
+ segment
715
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
716
+ }
717
+ })
718
+ }
719
+
720
+ // 暫不處理edge的highlight
721
+ // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
722
+ // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
723
+ // fullChartParams: ChartParams
724
+ // highlightIds: string[]
725
+ // }) {
726
+ // edgeGSelection.interrupt('highlight')
727
+
728
+ // if (!highlightIds.length) {
729
+ // edgeGSelection
730
+ // .transition('highlight')
731
+ // .style('opacity', 1)
732
+ // return
733
+ // }
734
+
735
+ // edgeGSelection.each((d, i, n) => {
736
+ // const segment = d3.select(n[i])
737
+
738
+ // if (highlightIds.includes(d.id)) {
739
+ // segment
740
+ // .style('opacity', 1)
741
+ // .transition('highlight')
742
+ // .ease(d3.easeElastic)
743
+ // .duration(500)
744
+ // } else {
745
+ // // 取消放大
746
+ // segment
747
+ // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
748
+ // }
749
+ // })
750
+ // }
751
+
752
+ export const ForceDirected = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
753
+
754
+ const destroy$ = new Subject()
755
+
756
+ const gSelection = selection.append('g').classed(gSelectionClassName, true)
757
+ const defsSelection = gSelection.append('defs')
758
+ const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
759
+ const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
760
+
761
+ let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
762
+ let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
763
+ let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
764
+ let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
765
+ let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
766
+ let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
767
+ let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
768
+ let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
769
+
770
+ const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
771
+ const mouseEvent$ = new Subject<EventRelationship>()
772
+
773
+ // <marker> marker selection
774
+ combineLatest({
775
+ fullParams: observer.fullParams$,
776
+ fullChartParams: observer.fullChartParams$
777
+ }).pipe(
778
+ takeUntil(destroy$),
779
+ switchMap(async d => d),
780
+ map(({ fullParams, fullChartParams }) => {
781
+ return renderArrowMarker(defsSelection, fullParams, fullChartParams)
782
+ })
783
+ ).subscribe()
784
+
785
+ // init zoom
786
+ const d3Zoom$ = observer.fullParams$.pipe(
787
+ takeUntil(destroy$),
788
+ // map(d => d.scaleExtent),
789
+ // distinctUntilChanged((a, b) => String(a) === String(b)),
790
+ // first(),
791
+ map(data => {
792
+ let d3Zoom = data.zoomable
793
+ ? d3.zoom().on('zoom', (event) => {
794
+ // console.log(event)
795
+ // this.svgGroup.attr('transform', `translate(
796
+ // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
797
+ // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
798
+ // ) scale(
799
+ // ${event.transform.k}
800
+ // )`)
801
+ gSelection.attr('transform', `translate(
802
+ ${event.transform.x},
803
+ ${event.transform.y}
804
+ ) scale(
805
+ ${event.transform.k}
806
+ )`)
807
+
808
+ if (data.dotLabel.sizeFixed && nodeLabelSelection) {
809
+ // 反向 scale 抵消掉放大縮小
810
+ nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
811
+ }
812
+ if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
813
+ // 反向 scale 抵消掉放大縮小
814
+ edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
815
+ }
816
+ })
817
+ : d3.zoom().on('zoom', null)
818
+ if (data.scaleExtent) {
819
+ d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
820
+ }
821
+ rootSelection.call(d3Zoom)
822
+
823
+ return d3Zoom
824
+ }),
825
+ // shareReplay(1)
826
+ )
827
+
828
+ // zoom transform
829
+ combineLatest({
830
+ d3Zoom: d3Zoom$,
831
+ transform: observer.fullParams$.pipe(
832
+ takeUntil(destroy$),
833
+ map(d => d.transform),
834
+ )
835
+ }).pipe(
836
+ takeUntil(destroy$),
837
+ switchMap(async d => d)
838
+ ).subscribe(data => {
839
+ // console.log('call')
840
+ selection.call(
841
+ data.d3Zoom.transform, d3.zoomIdentity
842
+ .translate(data.transform.x, data.transform.y)
843
+ .scale(data.transform.k)
844
+ )
845
+ })
846
+
847
+
848
+ const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
849
+ layout: observer.layout$.pipe(
850
+ first() // 只使用第一次的尺寸(置中)
851
+ ),
852
+ fullParams: observer.fullParams$
853
+ }).pipe(
854
+ takeUntil(destroy$),
855
+ switchMap(async d => d),
856
+ map(data => createSimulation(data.layout, data.fullParams)),
857
+ shareReplay(1)
858
+ )
859
+
860
+ const renderData$: Observable<RenderData> = observer.visibleComputedData$.pipe(
861
+ takeUntil(destroy$),
862
+ map(data => {
863
+ return {
864
+ nodes: data.nodes,
865
+ edges: data.edges.map(_d => {
866
+ let d: RenderEdge = _d as RenderEdge
867
+ d.source = _d.startNode // reference
868
+ d.target = _d.endNode
869
+ return d
870
+ })
871
+ }
872
+ }),
873
+ shareReplay(1)
874
+ )
875
+
876
+ combineLatest({
877
+ renderData: renderData$,
878
+ computedData: observer.computedData$,
879
+ CategoryNodeMap: observer.CategoryNodeMap$,
880
+ simulation: simulation$,
881
+ fullParams: observer.fullParams$,
882
+ fullChartParams: observer.fullChartParams$
883
+ }).pipe(
884
+ takeUntil(destroy$),
885
+ switchMap(async d => d),
886
+ ).subscribe(data => {
887
+
888
+ nodeGSelection = renderNodeG({
889
+ nodeListGSelection: nodeListGSelection,
890
+ nodes: data.renderData.nodes,
891
+ })
892
+
893
+ nodeCircleSelection = renderNodeCircle({
894
+ nodeGSelection: nodeGSelection,
895
+ fullParams: data.fullParams,
896
+ fullChartParams: data.fullChartParams
897
+ })
898
+ nodeGSelection.call(drag(data.simulation, dragStatus$))
899
+
900
+ nodeLabelGSelection = renderNodeLabelG({
901
+ nodeGSelection: nodeGSelection,
902
+ fullParams: data.fullParams
903
+ })
904
+
905
+ nodeLabelSelection = renderNodeLabel({
906
+ nodeLabelGSelection: nodeLabelGSelection,
907
+ fullParams: data.fullParams,
908
+ fullChartParams: data.fullChartParams
909
+ })
910
+
911
+ edgeGSelection = renderEdgeG({
912
+ edgeListGSelection: edgeListGSelection,
913
+ edges: data.renderData.edges
914
+ })
915
+
916
+ edgeArrowSelection = renderEdgeArrowPath({
917
+ edgeGSelection: edgeGSelection,
918
+ fullParams: data.fullParams,
919
+ fullChartParams: data.fullChartParams
920
+ })
921
+
922
+ edgeLabelGSelection = renderEdgeLabelG({
923
+ edgeGSelection: edgeGSelection,
924
+ })
925
+
926
+ edgeLabelSelection = renderEdgeLabel({
927
+ edgeLabelGSelection: edgeLabelGSelection,
928
+ fullParams: data.fullParams,
929
+ fullChartParams: data.fullChartParams
930
+ })
931
+
932
+ data.simulation.nodes(data.renderData.nodes)
933
+ .on('tick', () => {
934
+ edgeArrowSelection.attr('d', linkArcFn)
935
+ nodeGSelection.attr('transform', translateFn)
936
+ // nodeLabelGSelection.attr('transform', d => translateFn({
937
+ // x: d.x,
938
+ // y: d.y - data.fullParams.dot.radius - 10
939
+ // }))
940
+ edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
941
+ })
942
+ ;(data.simulation.force("link") as any).links(data.renderData.edges)
943
+
944
+ data.simulation.alpha(0.3).restart()
945
+
946
+ nodeCircleSelection
947
+ .on('mouseover', (event, datum) => {
948
+ event.stopPropagation()
949
+
950
+ mouseEvent$.next({
951
+ type: 'relationship',
952
+ eventName: 'mouseover',
953
+ pluginName,
954
+ highlightTarget: data.fullChartParams.highlightTarget,
955
+ datum: datum,
956
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
957
+ categoryIndex: datum.categoryIndex,
958
+ categoryLabel: datum.categoryLabel,
959
+ event,
960
+ data: data.computedData
961
+ })
962
+ })
963
+ .on('mousemove', (event, datum) => {
964
+ event.stopPropagation()
965
+
966
+ mouseEvent$.next({
967
+ type: 'relationship',
968
+ eventName: 'mousemove',
969
+ pluginName,
970
+ highlightTarget: data.fullChartParams.highlightTarget,
971
+ datum: datum,
972
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
973
+ categoryIndex: datum.categoryIndex,
974
+ categoryLabel: datum.categoryLabel,
975
+ event,
976
+ data: data.computedData
977
+ })
978
+ })
979
+ .on('mouseout', (event, datum) => {
980
+ event.stopPropagation()
981
+
982
+ mouseEvent$.next({
983
+ type: 'relationship',
984
+ eventName: 'mouseout',
985
+ pluginName,
986
+ highlightTarget: data.fullChartParams.highlightTarget,
987
+ datum: datum,
988
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
989
+ categoryIndex: datum.categoryIndex,
990
+ categoryLabel: datum.categoryLabel,
991
+ event,
992
+ data: data.computedData
993
+ })
994
+ })
995
+ .on('click', (event, datum) => {
996
+ event.stopPropagation()
997
+
998
+ mouseEvent$.next({
999
+ type: 'relationship',
1000
+ eventName: 'click',
1001
+ pluginName,
1002
+ highlightTarget: data.fullChartParams.highlightTarget,
1003
+ datum: datum,
1004
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1005
+ categoryIndex: datum.categoryIndex,
1006
+ categoryLabel: datum.categoryLabel,
1007
+ event,
1008
+ data: data.computedData
1009
+ })
1010
+ })
1011
+ })
1012
+
1013
+ dragStatus$.pipe(
1014
+ distinctUntilChanged((a, b) => a === b),
1015
+ // 只有沒有托曳時才執行
1016
+ switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1017
+ ).subscribe(data => {
1018
+ subject.event$.next(data)
1019
+ })
1020
+
1021
+ combineLatest({
1022
+ renderData: renderData$,
1023
+ highlightNodes: observer.relationshipHighlightNodes$.pipe(
1024
+ map(data => data.map(d => d.id))
1025
+ ),
1026
+ highlightEdges: observer.relationshipHighlightEdges$.pipe(
1027
+ map(data => data.map(d => d.id))
1028
+ ),
1029
+ fullChartParams: observer.fullChartParams$,
1030
+ fullParams: observer.fullParams$,
1031
+ }).pipe(
1032
+ takeUntil(destroy$),
1033
+ switchMap(async d => d)
1034
+ ).subscribe(data => {
1035
+ if (!nodeGSelection || !edgeGSelection) {
1036
+ return
1037
+ }
1038
+
1039
+ highlightNodes({
1040
+ nodeGSelection,
1041
+ edgeGSelection,
1042
+ highlightIds: data.highlightNodes,
1043
+ fullChartParams: data.fullChartParams
1044
+ })
1045
+ // highlightEdges({
1046
+ // edgeGSelection,
1047
+ // highlightIds: data.highlightEdges,
1048
+ // fullChartParams: data.fullChartParams
1049
+ // })
1050
+ })
1051
+
1052
+
1053
+
1054
+ return () => {
1055
+ destroy$.next(undefined)
1056
+ }
1057
1057
  })