@orbcharts/plugins-basic 3.0.8 → 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 (120) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +6177 -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 +292 -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,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
  })