@orbcharts/plugins-basic 3.0.6 → 3.0.8

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