@orbcharts/plugins-basic 3.0.0-beta.18 → 3.0.0-beta.19

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 (113) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +2220 -2216
  3. package/dist/orbcharts-plugins-basic.umd.js +36 -34
  4. package/dist/src/grid/defaults.d.ts +2 -2
  5. package/dist/src/multiGrid/defaults.d.ts +1 -1
  6. package/dist/src/multiValue/defaults.d.ts +1 -1
  7. package/lib/core-types.ts +7 -7
  8. package/lib/core.ts +6 -6
  9. package/lib/plugins-basic-types.ts +6 -6
  10. package/package.json +44 -44
  11. package/src/base/BaseBars.ts +765 -765
  12. package/src/base/BaseBarsTriangle.ts +676 -676
  13. package/src/base/BaseDots.ts +464 -464
  14. package/src/base/BaseGroupAxis.ts +691 -691
  15. package/src/base/BaseLegend.ts +684 -684
  16. package/src/base/BaseLineAreas.ts +629 -629
  17. package/src/base/BaseLines.ts +706 -706
  18. package/src/base/BaseRacingBars.ts +551 -551
  19. package/src/base/BaseRacingLabels.ts +396 -396
  20. package/src/base/BaseRacingValueLabels.ts +403 -403
  21. package/src/base/BaseStackedBars.ts +782 -782
  22. package/src/base/BaseTooltip.ts +386 -386
  23. package/src/base/BaseValueAxis.ts +600 -600
  24. package/src/base/BaseXAxis.ts +427 -427
  25. package/src/base/BaseYAxis.ts +389 -389
  26. package/src/base/types.ts +2 -2
  27. package/src/const.ts +30 -30
  28. package/src/grid/defaults.ts +213 -213
  29. package/src/grid/gridObservables.ts +612 -612
  30. package/src/grid/index.ts +16 -16
  31. package/src/grid/plugins/Bars.ts +69 -69
  32. package/src/grid/plugins/BarsPN.ts +66 -66
  33. package/src/grid/plugins/BarsTriangle.ts +73 -73
  34. package/src/grid/plugins/Dots.ts +68 -68
  35. package/src/grid/plugins/GridLegend.ts +107 -107
  36. package/src/grid/plugins/GridTooltip.ts +66 -66
  37. package/src/grid/plugins/GroupAux.ts +1120 -1120
  38. package/src/grid/plugins/GroupAxis.ts +73 -73
  39. package/src/grid/plugins/GroupZoom.ts +218 -218
  40. package/src/grid/plugins/LineAreas.ts +65 -65
  41. package/src/grid/plugins/Lines.ts +59 -59
  42. package/src/grid/plugins/StackedBars.ts +64 -64
  43. package/src/grid/plugins/StackedValueAxis.ts +96 -96
  44. package/src/grid/plugins/ValueAxis.ts +94 -94
  45. package/src/index.ts +6 -6
  46. package/src/multiGrid/defaults.ts +244 -244
  47. package/src/multiGrid/index.ts +14 -14
  48. package/src/multiGrid/multiGridObservables.ts +50 -50
  49. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  50. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  51. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  52. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
  53. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  54. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  55. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  56. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  57. package/src/multiGrid/plugins/MultiStackedBars.ts +106 -106
  58. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +134 -134
  59. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  60. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
  61. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  62. package/src/multiValue/defaults.ts +391 -388
  63. package/src/multiValue/index.ts +12 -12
  64. package/src/multiValue/multiValueObservables.ts +666 -666
  65. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  66. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  67. package/src/multiValue/plugins/RacingBars.ts +373 -373
  68. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
  69. package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
  70. package/src/multiValue/plugins/RankingAxis_legacy.ts +109 -109
  71. package/src/multiValue/plugins/Scatter.ts +426 -426
  72. package/src/multiValue/plugins/ScatterBubbles.ts +554 -554
  73. package/src/multiValue/plugins/XAxis.ts +107 -107
  74. package/src/multiValue/plugins/XYAux.ts +682 -682
  75. package/src/multiValue/plugins/XYAxes.ts +194 -194
  76. package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
  77. package/src/multiValue/plugins/XZoom.ts +299 -299
  78. package/src/noneData/defaults.ts +102 -102
  79. package/src/noneData/index.ts +3 -3
  80. package/src/noneData/plugins/Container.ts +27 -27
  81. package/src/noneData/plugins/Tooltip.ts +373 -373
  82. package/src/relationship/defaults.ts +221 -221
  83. package/src/relationship/index.ts +5 -5
  84. package/src/relationship/plugins/ForceDirected.ts +1173 -1173
  85. package/src/relationship/plugins/ForceDirectedBubbles.ts +1411 -1411
  86. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  87. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  88. package/src/relationship/relationshipObservables.ts +49 -49
  89. package/src/series/defaults.ts +221 -221
  90. package/src/series/index.ts +9 -9
  91. package/src/series/plugins/Bubbles.ts +636 -636
  92. package/src/series/plugins/Pie.ts +623 -623
  93. package/src/series/plugins/PieEventTexts.ts +284 -284
  94. package/src/series/plugins/PieLabels.ts +640 -640
  95. package/src/series/plugins/Rose.ts +516 -516
  96. package/src/series/plugins/RoseLabels.ts +600 -600
  97. package/src/series/plugins/SeriesLegend.ts +107 -107
  98. package/src/series/plugins/SeriesTooltip.ts +66 -66
  99. package/src/series/seriesObservables.ts +145 -145
  100. package/src/series/seriesUtils.ts +51 -51
  101. package/src/tree/defaults.ts +102 -102
  102. package/src/tree/index.ts +4 -4
  103. package/src/tree/plugins/TreeLegend.ts +100 -100
  104. package/src/tree/plugins/TreeMap.ts +341 -341
  105. package/src/tree/plugins/TreeTooltip.ts +66 -66
  106. package/src/utils/commonUtils.ts +31 -31
  107. package/src/utils/d3Graphics.ts +176 -176
  108. package/src/utils/d3Utils.ts +92 -92
  109. package/src/utils/observables.ts +14 -14
  110. package/src/utils/orbchartsUtils.ts +129 -129
  111. package/tsconfig.base.json +13 -13
  112. package/tsconfig.json +2 -2
  113. package/vite.config.js +22 -22
@@ -1,1174 +1,1174 @@
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
- // var 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
-
682
- // function renderBubbles ({ selection, bubblesData, fullParams, sumSeries }: {
683
- // selection: d3.Selection<SVGGElement, any, any, any>
684
- // bubblesData: BubblesDatum[]
685
- // fullParams: BubblesParams
686
- // sumSeries: boolean
687
- // }) {
688
- // const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
689
- // .data(bubblesData, (d) => d.id)
690
- // .join(
691
- // enter => {
692
- // const enterSelection = enter
693
- // .append('g')
694
- // .attr('cursor', 'pointer')
695
- // .attr('font-size', 12)
696
- // .style('fill', '#ffffff')
697
- // .attr("text-anchor", "middle")
698
-
699
- // enterSelection
700
- // .append("circle")
701
- // .attr("class", "node")
702
- // .attr("cx", 0)
703
- // .attr("cy", 0)
704
- // // .attr("r", 1e-6)
705
- // .attr('fill', (d) => d.color)
706
- // // .transition()
707
- // // .duration(500)
708
-
709
- // enterSelection
710
- // .append('text')
711
- // .style('opacity', 0.8)
712
- // .attr('pointer-events', 'none')
713
-
714
- // return enterSelection
715
- // },
716
- // update => {
717
- // return update
718
- // },
719
- // exit => {
720
- // return exit
721
- // .remove()
722
- // }
723
- // )
724
- // .attr("transform", (d) => {
725
- // return `translate(${d.x},${d.y})`
726
- // })
727
-
728
- // // 泡泡文字要使用的的資料欄位
729
- // const textDataColumn = sumSeries ? 'seriesLabel' : 'label'// 如果有合併series則使用seriesLabel
730
-
731
- // bubblesSelection.select('circle')
732
- // .transition()
733
- // .duration(200)
734
- // .attr("r", (d) => d.r)
735
- // .attr('fill', (d) => d.color)
736
- // bubblesSelection
737
- // .each((d,i,g) => {
738
- // const gSelection = d3.select(g[i])
739
- // let breakAll = true
740
- // if (d[textDataColumn].length <= fullParams.label.maxLineLength) {
741
- // breakAll = false
742
- // }
743
- // gSelection.call(renderCircleText, {
744
- // text: d[textDataColumn],
745
- // radius: d.r * fullParams.label.fillRate,
746
- // lineHeight: fullParams.label.lineHeight,
747
- // isBreakAll: breakAll
748
- // })
749
-
750
- // })
751
-
752
- // return bubblesSelection
753
- // }
754
-
755
- // function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
756
- // data: BubblesDatum[]
757
- // // fullParams: BubblesParams
758
- // highlightRIncrease: number
759
- // highlightIds: string[]
760
- // }) {
761
- // if (highlightRIncrease == 0) {
762
- // return
763
- // }
764
- // if (!highlightIds.length) {
765
- // data.forEach(d => d.r = d._originR)
766
- // return
767
- // }
768
- // data.forEach(d => {
769
- // if (highlightIds.includes(d.id)) {
770
- // d.r = d._originR + highlightRIncrease
771
- // } else {
772
- // d.r = d._originR
773
- // }
774
- // })
775
- // }
776
-
777
-
778
- // function groupBubbles ({ fullParams, SeriesContainerPositionMap }: {
779
- // fullParams: BubblesParams
780
- // // graphicWidth: number
781
- // // graphicHeight: number
782
- // SeriesContainerPositionMap: Map<string, ContainerPosition>
783
- // }) {
784
- // // console.log('groupBubbles')
785
- // force!
786
- // // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
787
- // // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
788
- // .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
789
- // return SeriesContainerPositionMap.get(data.seriesLabel)!.centerX
790
- // }))
791
- // .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
792
- // return SeriesContainerPositionMap.get(data.seriesLabel)!.centerY
793
- // }))
794
-
795
- // force!.alpha(1).restart()
796
- // }
797
-
798
- function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
799
- nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
800
- edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
801
- fullChartParams: ChartParams
802
- highlightIds: string[]
803
- }) {
804
- nodeGSelection.interrupt('highlight')
805
- edgeGSelection.interrupt('highlight')
806
- // console.log(highlightIds)
807
- if (!highlightIds.length) {
808
- nodeGSelection
809
- .transition('highlight')
810
- .style('opacity', 1)
811
- edgeGSelection
812
- .transition('highlight')
813
- .style('opacity', 1)
814
- return
815
- }
816
-
817
- edgeGSelection
818
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
819
-
820
- nodeGSelection.each((d, i, n) => {
821
- const segment = d3.select(n[i])
822
-
823
- if (highlightIds.includes(d.id)) {
824
- segment
825
- .style('opacity', 1)
826
- .transition('highlight')
827
- .ease(d3.easeElastic)
828
- .duration(500)
829
- } else {
830
- // 取消
831
- segment
832
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
833
- }
834
- })
835
- }
836
-
837
- // 暫不處理edge的highlight
838
- // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
839
- // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
840
- // fullChartParams: ChartParams
841
- // highlightIds: string[]
842
- // }) {
843
- // edgeGSelection.interrupt('highlight')
844
-
845
- // if (!highlightIds.length) {
846
- // edgeGSelection
847
- // .transition('highlight')
848
- // .style('opacity', 1)
849
- // return
850
- // }
851
-
852
- // edgeGSelection.each((d, i, n) => {
853
- // const segment = d3.select(n[i])
854
-
855
- // if (highlightIds.includes(d.id)) {
856
- // segment
857
- // .style('opacity', 1)
858
- // .transition('highlight')
859
- // .ease(d3.easeElastic)
860
- // .duration(500)
861
- // } else {
862
- // // 取消放大
863
- // segment
864
- // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
865
- // }
866
- // })
867
- // }
868
-
869
- export const ForceDirected = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
870
-
871
- const destroy$ = new Subject()
872
-
873
- const gSelection = selection.append('g').classed(gSelectionClassName, true)
874
- const defsSelection = gSelection.append('defs')
875
- const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
876
- const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
877
-
878
- let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
879
- let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
880
- let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
881
- let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
882
- let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
883
- let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
884
- let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
885
- let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
886
-
887
- const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
888
- const mouseEvent$ = new Subject<EventRelationship>()
889
-
890
- // <marker> marker selection
891
- combineLatest({
892
- fullParams: observer.fullParams$,
893
- fullChartParams: observer.fullChartParams$
894
- }).pipe(
895
- takeUntil(destroy$),
896
- switchMap(async d => d),
897
- map(({ fullParams, fullChartParams }) => {
898
- return renderArrowMarker(defsSelection, fullParams, fullChartParams)
899
- })
900
- ).subscribe()
901
-
902
- // init zoom
903
- const d3Zoom$ = observer.fullParams$.pipe(
904
- takeUntil(destroy$),
905
- // map(d => d.scaleExtent),
906
- // distinctUntilChanged((a, b) => String(a) === String(b)),
907
- // first(),
908
- map(data => {
909
- let d3Zoom = data.zoomable
910
- ? d3.zoom().on('zoom', (event) => {
911
- // console.log(event)
912
- // this.svgGroup.attr('transform', `translate(
913
- // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
914
- // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
915
- // ) scale(
916
- // ${event.transform.k}
917
- // )`)
918
- gSelection.attr('transform', `translate(
919
- ${event.transform.x},
920
- ${event.transform.y}
921
- ) scale(
922
- ${event.transform.k}
923
- )`)
924
-
925
- if (data.dotLabel.sizeFixed && nodeLabelSelection) {
926
- // 反向 scale 抵消掉放大縮小
927
- nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
928
- }
929
- if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
930
- // 反向 scale 抵消掉放大縮小
931
- edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
932
- }
933
- })
934
- : d3.zoom().on('zoom', null)
935
- if (data.scaleExtent) {
936
- d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
937
- }
938
- rootSelection.call(d3Zoom)
939
-
940
- return d3Zoom
941
- }),
942
- // shareReplay(1)
943
- )
944
-
945
- // zoom transform
946
- combineLatest({
947
- d3Zoom: d3Zoom$,
948
- transform: observer.fullParams$.pipe(
949
- takeUntil(destroy$),
950
- map(d => d.transform),
951
- )
952
- }).pipe(
953
- takeUntil(destroy$),
954
- switchMap(async d => d)
955
- ).subscribe(data => {
956
- // console.log('call')
957
- selection.call(
958
- data.d3Zoom.transform, d3.zoomIdentity
959
- .translate(data.transform.x, data.transform.y)
960
- .scale(data.transform.k)
961
- )
962
- })
963
-
964
-
965
- const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
966
- layout: observer.layout$.pipe(
967
- first() // 只使用第一次的尺寸(置中)
968
- ),
969
- fullParams: observer.fullParams$
970
- }).pipe(
971
- takeUntil(destroy$),
972
- switchMap(async d => d),
973
- map(data => createSimulation(data.layout, data.fullParams)),
974
- shareReplay(1)
975
- )
976
-
977
- const renderData$: Observable<RenderData> = observer.visibleComputedData$.pipe(
978
- takeUntil(destroy$),
979
- map(data => {
980
- return {
981
- nodes: data.nodes,
982
- edges: data.edges.map(_d => {
983
- let d: RenderEdge = _d as RenderEdge
984
- d.source = _d.startNode // reference
985
- d.target = _d.endNode
986
- return d
987
- })
988
- }
989
- }),
990
- shareReplay(1)
991
- )
992
-
993
- combineLatest({
994
- renderData: renderData$,
995
- computedData: observer.computedData$,
996
- CategoryNodeMap: observer.CategoryNodeMap$,
997
- simulation: simulation$,
998
- fullParams: observer.fullParams$,
999
- fullChartParams: observer.fullChartParams$
1000
- }).pipe(
1001
- takeUntil(destroy$),
1002
- switchMap(async d => d),
1003
- ).subscribe(data => {
1004
-
1005
- nodeGSelection = renderNodeG({
1006
- nodeListGSelection: nodeListGSelection,
1007
- nodes: data.renderData.nodes,
1008
- })
1009
-
1010
- nodeCircleSelection = renderNodeCircle({
1011
- nodeGSelection: nodeGSelection,
1012
- fullParams: data.fullParams,
1013
- fullChartParams: data.fullChartParams
1014
- })
1015
- nodeGSelection.call(drag(data.simulation, dragStatus$))
1016
-
1017
- nodeLabelGSelection = renderNodeLabelG({
1018
- nodeGSelection: nodeGSelection,
1019
- fullParams: data.fullParams
1020
- })
1021
-
1022
- nodeLabelSelection = renderNodeLabel({
1023
- nodeLabelGSelection: nodeLabelGSelection,
1024
- fullParams: data.fullParams,
1025
- fullChartParams: data.fullChartParams
1026
- })
1027
-
1028
- edgeGSelection = renderEdgeG({
1029
- edgeListGSelection: edgeListGSelection,
1030
- edges: data.renderData.edges
1031
- })
1032
-
1033
- edgeArrowSelection = renderEdgeArrowPath({
1034
- edgeGSelection: edgeGSelection,
1035
- fullParams: data.fullParams,
1036
- fullChartParams: data.fullChartParams
1037
- })
1038
-
1039
- edgeLabelGSelection = renderEdgeLabelG({
1040
- edgeGSelection: edgeGSelection,
1041
- })
1042
-
1043
- edgeLabelSelection = renderEdgeLabel({
1044
- edgeLabelGSelection: edgeLabelGSelection,
1045
- fullParams: data.fullParams,
1046
- fullChartParams: data.fullChartParams
1047
- })
1048
-
1049
- data.simulation.nodes(data.renderData.nodes)
1050
- .on('tick', () => {
1051
- edgeArrowSelection.attr('d', linkArcFn)
1052
- nodeGSelection.attr('transform', translateFn)
1053
- // nodeLabelGSelection.attr('transform', d => translateFn({
1054
- // x: d.x,
1055
- // y: d.y - data.fullParams.dot.radius - 10
1056
- // }))
1057
- edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
1058
- })
1059
- ;(data.simulation.force("link") as any).links(data.renderData.edges)
1060
-
1061
- data.simulation.alpha(0.3).restart()
1062
-
1063
- nodeCircleSelection
1064
- .on('mouseover', (event, datum) => {
1065
- event.stopPropagation()
1066
-
1067
- mouseEvent$.next({
1068
- type: 'relationship',
1069
- eventName: 'mouseover',
1070
- pluginName,
1071
- highlightTarget: data.fullChartParams.highlightTarget,
1072
- datum: datum,
1073
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1074
- categoryIndex: datum.categoryIndex,
1075
- categoryLabel: datum.categoryLabel,
1076
- event,
1077
- data: data.computedData
1078
- })
1079
- })
1080
- .on('mousemove', (event, datum) => {
1081
- event.stopPropagation()
1082
-
1083
- mouseEvent$.next({
1084
- type: 'relationship',
1085
- eventName: 'mousemove',
1086
- pluginName,
1087
- highlightTarget: data.fullChartParams.highlightTarget,
1088
- datum: datum,
1089
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1090
- categoryIndex: datum.categoryIndex,
1091
- categoryLabel: datum.categoryLabel,
1092
- event,
1093
- data: data.computedData
1094
- })
1095
- })
1096
- .on('mouseout', (event, datum) => {
1097
- event.stopPropagation()
1098
-
1099
- mouseEvent$.next({
1100
- type: 'relationship',
1101
- eventName: 'mouseout',
1102
- pluginName,
1103
- highlightTarget: data.fullChartParams.highlightTarget,
1104
- datum: datum,
1105
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1106
- categoryIndex: datum.categoryIndex,
1107
- categoryLabel: datum.categoryLabel,
1108
- event,
1109
- data: data.computedData
1110
- })
1111
- })
1112
- .on('click', (event, datum) => {
1113
- event.stopPropagation()
1114
-
1115
- mouseEvent$.next({
1116
- type: 'relationship',
1117
- eventName: 'click',
1118
- pluginName,
1119
- highlightTarget: data.fullChartParams.highlightTarget,
1120
- datum: datum,
1121
- category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1122
- categoryIndex: datum.categoryIndex,
1123
- categoryLabel: datum.categoryLabel,
1124
- event,
1125
- data: data.computedData
1126
- })
1127
- })
1128
- })
1129
-
1130
- dragStatus$.pipe(
1131
- distinctUntilChanged((a, b) => a === b),
1132
- // 只有沒有托曳時才執行
1133
- switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1134
- ).subscribe(data => {
1135
- subject.event$.next(data)
1136
- })
1137
-
1138
- combineLatest({
1139
- renderData: renderData$,
1140
- highlightNodes: observer.relationshipHighlightNodes$.pipe(
1141
- map(data => data.map(d => d.id))
1142
- ),
1143
- highlightEdges: observer.relationshipHighlightEdges$.pipe(
1144
- map(data => data.map(d => d.id))
1145
- ),
1146
- fullChartParams: observer.fullChartParams$,
1147
- fullParams: observer.fullParams$,
1148
- }).pipe(
1149
- takeUntil(destroy$),
1150
- switchMap(async d => d)
1151
- ).subscribe(data => {
1152
- if (!nodeGSelection || !edgeGSelection) {
1153
- return
1154
- }
1155
-
1156
- highlightNodes({
1157
- nodeGSelection,
1158
- edgeGSelection,
1159
- highlightIds: data.highlightNodes,
1160
- fullChartParams: data.fullChartParams
1161
- })
1162
- // highlightEdges({
1163
- // edgeGSelection,
1164
- // highlightIds: data.highlightEdges,
1165
- // fullChartParams: data.fullChartParams
1166
- // })
1167
- })
1168
-
1169
-
1170
-
1171
- return () => {
1172
- destroy$.next(undefined)
1173
- }
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
+ // var 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
+
682
+ // function renderBubbles ({ selection, bubblesData, fullParams, sumSeries }: {
683
+ // selection: d3.Selection<SVGGElement, any, any, any>
684
+ // bubblesData: BubblesDatum[]
685
+ // fullParams: BubblesParams
686
+ // sumSeries: boolean
687
+ // }) {
688
+ // const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
689
+ // .data(bubblesData, (d) => d.id)
690
+ // .join(
691
+ // enter => {
692
+ // const enterSelection = enter
693
+ // .append('g')
694
+ // .attr('cursor', 'pointer')
695
+ // .attr('font-size', 12)
696
+ // .style('fill', '#ffffff')
697
+ // .attr("text-anchor", "middle")
698
+
699
+ // enterSelection
700
+ // .append("circle")
701
+ // .attr("class", "node")
702
+ // .attr("cx", 0)
703
+ // .attr("cy", 0)
704
+ // // .attr("r", 1e-6)
705
+ // .attr('fill', (d) => d.color)
706
+ // // .transition()
707
+ // // .duration(500)
708
+
709
+ // enterSelection
710
+ // .append('text')
711
+ // .style('opacity', 0.8)
712
+ // .attr('pointer-events', 'none')
713
+
714
+ // return enterSelection
715
+ // },
716
+ // update => {
717
+ // return update
718
+ // },
719
+ // exit => {
720
+ // return exit
721
+ // .remove()
722
+ // }
723
+ // )
724
+ // .attr("transform", (d) => {
725
+ // return `translate(${d.x},${d.y})`
726
+ // })
727
+
728
+ // // 泡泡文字要使用的的資料欄位
729
+ // const textDataColumn = sumSeries ? 'seriesLabel' : 'label'// 如果有合併series則使用seriesLabel
730
+
731
+ // bubblesSelection.select('circle')
732
+ // .transition()
733
+ // .duration(200)
734
+ // .attr("r", (d) => d.r)
735
+ // .attr('fill', (d) => d.color)
736
+ // bubblesSelection
737
+ // .each((d,i,g) => {
738
+ // const gSelection = d3.select(g[i])
739
+ // let breakAll = true
740
+ // if (d[textDataColumn].length <= fullParams.label.maxLineLength) {
741
+ // breakAll = false
742
+ // }
743
+ // gSelection.call(renderCircleText, {
744
+ // text: d[textDataColumn],
745
+ // radius: d.r * fullParams.label.fillRate,
746
+ // lineHeight: fullParams.label.lineHeight,
747
+ // isBreakAll: breakAll
748
+ // })
749
+
750
+ // })
751
+
752
+ // return bubblesSelection
753
+ // }
754
+
755
+ // function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
756
+ // data: BubblesDatum[]
757
+ // // fullParams: BubblesParams
758
+ // highlightRIncrease: number
759
+ // highlightIds: string[]
760
+ // }) {
761
+ // if (highlightRIncrease == 0) {
762
+ // return
763
+ // }
764
+ // if (!highlightIds.length) {
765
+ // data.forEach(d => d.r = d._originR)
766
+ // return
767
+ // }
768
+ // data.forEach(d => {
769
+ // if (highlightIds.includes(d.id)) {
770
+ // d.r = d._originR + highlightRIncrease
771
+ // } else {
772
+ // d.r = d._originR
773
+ // }
774
+ // })
775
+ // }
776
+
777
+
778
+ // function groupBubbles ({ fullParams, SeriesContainerPositionMap }: {
779
+ // fullParams: BubblesParams
780
+ // // graphicWidth: number
781
+ // // graphicHeight: number
782
+ // SeriesContainerPositionMap: Map<string, ContainerPosition>
783
+ // }) {
784
+ // // console.log('groupBubbles')
785
+ // force!
786
+ // // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
787
+ // // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
788
+ // .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
789
+ // return SeriesContainerPositionMap.get(data.seriesLabel)!.centerX
790
+ // }))
791
+ // .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
792
+ // return SeriesContainerPositionMap.get(data.seriesLabel)!.centerY
793
+ // }))
794
+
795
+ // force!.alpha(1).restart()
796
+ // }
797
+
798
+ function highlightNodes ({ nodeGSelection, edgeGSelection, highlightIds, fullChartParams }: {
799
+ nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any>
800
+ edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
801
+ fullChartParams: ChartParams
802
+ highlightIds: string[]
803
+ }) {
804
+ nodeGSelection.interrupt('highlight')
805
+ edgeGSelection.interrupt('highlight')
806
+ // console.log(highlightIds)
807
+ if (!highlightIds.length) {
808
+ nodeGSelection
809
+ .transition('highlight')
810
+ .style('opacity', 1)
811
+ edgeGSelection
812
+ .transition('highlight')
813
+ .style('opacity', 1)
814
+ return
815
+ }
816
+
817
+ edgeGSelection
818
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
819
+
820
+ nodeGSelection.each((d, i, n) => {
821
+ const segment = d3.select(n[i])
822
+
823
+ if (highlightIds.includes(d.id)) {
824
+ segment
825
+ .style('opacity', 1)
826
+ .transition('highlight')
827
+ .ease(d3.easeElastic)
828
+ .duration(500)
829
+ } else {
830
+ // 取消
831
+ segment
832
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
833
+ }
834
+ })
835
+ }
836
+
837
+ // 暫不處理edge的highlight
838
+ // function highlightEdges ({ edgeGSelection, highlightIds, fullChartParams }: {
839
+ // edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any>
840
+ // fullChartParams: ChartParams
841
+ // highlightIds: string[]
842
+ // }) {
843
+ // edgeGSelection.interrupt('highlight')
844
+
845
+ // if (!highlightIds.length) {
846
+ // edgeGSelection
847
+ // .transition('highlight')
848
+ // .style('opacity', 1)
849
+ // return
850
+ // }
851
+
852
+ // edgeGSelection.each((d, i, n) => {
853
+ // const segment = d3.select(n[i])
854
+
855
+ // if (highlightIds.includes(d.id)) {
856
+ // segment
857
+ // .style('opacity', 1)
858
+ // .transition('highlight')
859
+ // .ease(d3.easeElastic)
860
+ // .duration(500)
861
+ // } else {
862
+ // // 取消放大
863
+ // segment
864
+ // .style('opacity', fullChartParams.styles.unhighlightedOpacity)
865
+ // }
866
+ // })
867
+ // }
868
+
869
+ export const ForceDirected = defineRelationshipPlugin(pluginConfig)(({ selection, rootSelection, name, observer, subject }) => {
870
+
871
+ const destroy$ = new Subject()
872
+
873
+ const gSelection = selection.append('g').classed(gSelectionClassName, true)
874
+ const defsSelection = gSelection.append('defs')
875
+ const edgeListGSelection = gSelection.append('g').classed(edgeListGClassName, true)
876
+ const nodeListGSelection = gSelection.append('g').classed(nodeListGClassName, true)
877
+
878
+ let nodeGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
879
+ let nodeCircleSelection: d3.Selection<SVGCircleElement, RenderNode, SVGGElement, any> | undefined
880
+ let nodeLabelGSelection: d3.Selection<SVGGElement, RenderNode, SVGGElement, any> | undefined
881
+ let nodeLabelSelection: d3.Selection<SVGTextElement, RenderNode, SVGGElement, any> | undefined
882
+ let edgeGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
883
+ let edgeArrowSelection: d3.Selection<SVGPathElement, RenderEdge, SVGGElement, any> | undefined
884
+ let edgeLabelGSelection: d3.Selection<SVGGElement, RenderEdge, SVGGElement, any> | undefined
885
+ let edgeLabelSelection: d3.Selection<SVGTextElement, RenderEdge, SVGGElement, any> | undefined
886
+
887
+ const dragStatus$ = new BehaviorSubject<DragStatus>('end') // start, drag, end
888
+ const mouseEvent$ = new Subject<EventRelationship>()
889
+
890
+ // <marker> marker selection
891
+ combineLatest({
892
+ fullParams: observer.fullParams$,
893
+ fullChartParams: observer.fullChartParams$
894
+ }).pipe(
895
+ takeUntil(destroy$),
896
+ switchMap(async d => d),
897
+ map(({ fullParams, fullChartParams }) => {
898
+ return renderArrowMarker(defsSelection, fullParams, fullChartParams)
899
+ })
900
+ ).subscribe()
901
+
902
+ // init zoom
903
+ const d3Zoom$ = observer.fullParams$.pipe(
904
+ takeUntil(destroy$),
905
+ // map(d => d.scaleExtent),
906
+ // distinctUntilChanged((a, b) => String(a) === String(b)),
907
+ // first(),
908
+ map(data => {
909
+ let d3Zoom = data.zoomable
910
+ ? d3.zoom().on('zoom', (event) => {
911
+ // console.log(event)
912
+ // this.svgGroup.attr('transform', `translate(
913
+ // ${event.transform.x + (this.zoom.xOffset * event.transform.k)},
914
+ // ${event.transform.y + (this.zoom.yOffset * event.transform.k)}
915
+ // ) scale(
916
+ // ${event.transform.k}
917
+ // )`)
918
+ gSelection.attr('transform', `translate(
919
+ ${event.transform.x},
920
+ ${event.transform.y}
921
+ ) scale(
922
+ ${event.transform.k}
923
+ )`)
924
+
925
+ if (data.dotLabel.sizeFixed && nodeLabelSelection) {
926
+ // 反向 scale 抵消掉放大縮小
927
+ nodeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
928
+ }
929
+ if (data.arrowLabel.sizeFixed && edgeLabelSelection) {
930
+ // 反向 scale 抵消掉放大縮小
931
+ edgeLabelSelection.attr('transform', `scale(${1 / event.transform.k})`)
932
+ }
933
+ })
934
+ : d3.zoom().on('zoom', null)
935
+ if (data.scaleExtent) {
936
+ d3Zoom.scaleExtent([data.scaleExtent.min, data.scaleExtent.max])
937
+ }
938
+ rootSelection.call(d3Zoom)
939
+
940
+ return d3Zoom
941
+ }),
942
+ // shareReplay(1)
943
+ )
944
+
945
+ // zoom transform
946
+ combineLatest({
947
+ d3Zoom: d3Zoom$,
948
+ transform: observer.fullParams$.pipe(
949
+ takeUntil(destroy$),
950
+ map(d => d.transform),
951
+ )
952
+ }).pipe(
953
+ takeUntil(destroy$),
954
+ switchMap(async d => d)
955
+ ).subscribe(data => {
956
+ // console.log('call')
957
+ selection.call(
958
+ data.d3Zoom.transform, d3.zoomIdentity
959
+ .translate(data.transform.x, data.transform.y)
960
+ .scale(data.transform.k)
961
+ )
962
+ })
963
+
964
+
965
+ const simulation$: Observable<d3.Simulation<d3.SimulationNodeDatum, undefined>> = combineLatest({
966
+ layout: observer.layout$.pipe(
967
+ first() // 只使用第一次的尺寸(置中)
968
+ ),
969
+ fullParams: observer.fullParams$
970
+ }).pipe(
971
+ takeUntil(destroy$),
972
+ switchMap(async d => d),
973
+ map(data => createSimulation(data.layout, data.fullParams)),
974
+ shareReplay(1)
975
+ )
976
+
977
+ const renderData$: Observable<RenderData> = observer.visibleComputedData$.pipe(
978
+ takeUntil(destroy$),
979
+ map(data => {
980
+ return {
981
+ nodes: data.nodes,
982
+ edges: data.edges.map(_d => {
983
+ let d: RenderEdge = _d as RenderEdge
984
+ d.source = _d.startNode // reference
985
+ d.target = _d.endNode
986
+ return d
987
+ })
988
+ }
989
+ }),
990
+ shareReplay(1)
991
+ )
992
+
993
+ combineLatest({
994
+ renderData: renderData$,
995
+ computedData: observer.computedData$,
996
+ CategoryNodeMap: observer.CategoryNodeMap$,
997
+ simulation: simulation$,
998
+ fullParams: observer.fullParams$,
999
+ fullChartParams: observer.fullChartParams$
1000
+ }).pipe(
1001
+ takeUntil(destroy$),
1002
+ switchMap(async d => d),
1003
+ ).subscribe(data => {
1004
+
1005
+ nodeGSelection = renderNodeG({
1006
+ nodeListGSelection: nodeListGSelection,
1007
+ nodes: data.renderData.nodes,
1008
+ })
1009
+
1010
+ nodeCircleSelection = renderNodeCircle({
1011
+ nodeGSelection: nodeGSelection,
1012
+ fullParams: data.fullParams,
1013
+ fullChartParams: data.fullChartParams
1014
+ })
1015
+ nodeGSelection.call(drag(data.simulation, dragStatus$))
1016
+
1017
+ nodeLabelGSelection = renderNodeLabelG({
1018
+ nodeGSelection: nodeGSelection,
1019
+ fullParams: data.fullParams
1020
+ })
1021
+
1022
+ nodeLabelSelection = renderNodeLabel({
1023
+ nodeLabelGSelection: nodeLabelGSelection,
1024
+ fullParams: data.fullParams,
1025
+ fullChartParams: data.fullChartParams
1026
+ })
1027
+
1028
+ edgeGSelection = renderEdgeG({
1029
+ edgeListGSelection: edgeListGSelection,
1030
+ edges: data.renderData.edges
1031
+ })
1032
+
1033
+ edgeArrowSelection = renderEdgeArrowPath({
1034
+ edgeGSelection: edgeGSelection,
1035
+ fullParams: data.fullParams,
1036
+ fullChartParams: data.fullChartParams
1037
+ })
1038
+
1039
+ edgeLabelGSelection = renderEdgeLabelG({
1040
+ edgeGSelection: edgeGSelection,
1041
+ })
1042
+
1043
+ edgeLabelSelection = renderEdgeLabel({
1044
+ edgeLabelGSelection: edgeLabelGSelection,
1045
+ fullParams: data.fullParams,
1046
+ fullChartParams: data.fullChartParams
1047
+ })
1048
+
1049
+ data.simulation.nodes(data.renderData.nodes)
1050
+ .on('tick', () => {
1051
+ edgeArrowSelection.attr('d', linkArcFn)
1052
+ nodeGSelection.attr('transform', translateFn)
1053
+ // nodeLabelGSelection.attr('transform', d => translateFn({
1054
+ // x: d.x,
1055
+ // y: d.y - data.fullParams.dot.radius - 10
1056
+ // }))
1057
+ edgeLabelGSelection.attr('transform', d => translateCenterFn(d))
1058
+ })
1059
+ ;(data.simulation.force("link") as any).links(data.renderData.edges)
1060
+
1061
+ data.simulation.alpha(0.3).restart()
1062
+
1063
+ nodeCircleSelection
1064
+ .on('mouseover', (event, datum) => {
1065
+ event.stopPropagation()
1066
+
1067
+ mouseEvent$.next({
1068
+ type: 'relationship',
1069
+ eventName: 'mouseover',
1070
+ pluginName,
1071
+ highlightTarget: data.fullChartParams.highlightTarget,
1072
+ datum: datum,
1073
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1074
+ categoryIndex: datum.categoryIndex,
1075
+ categoryLabel: datum.categoryLabel,
1076
+ event,
1077
+ data: data.computedData
1078
+ })
1079
+ })
1080
+ .on('mousemove', (event, datum) => {
1081
+ event.stopPropagation()
1082
+
1083
+ mouseEvent$.next({
1084
+ type: 'relationship',
1085
+ eventName: 'mousemove',
1086
+ pluginName,
1087
+ highlightTarget: data.fullChartParams.highlightTarget,
1088
+ datum: datum,
1089
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1090
+ categoryIndex: datum.categoryIndex,
1091
+ categoryLabel: datum.categoryLabel,
1092
+ event,
1093
+ data: data.computedData
1094
+ })
1095
+ })
1096
+ .on('mouseout', (event, datum) => {
1097
+ event.stopPropagation()
1098
+
1099
+ mouseEvent$.next({
1100
+ type: 'relationship',
1101
+ eventName: 'mouseout',
1102
+ pluginName,
1103
+ highlightTarget: data.fullChartParams.highlightTarget,
1104
+ datum: datum,
1105
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1106
+ categoryIndex: datum.categoryIndex,
1107
+ categoryLabel: datum.categoryLabel,
1108
+ event,
1109
+ data: data.computedData
1110
+ })
1111
+ })
1112
+ .on('click', (event, datum) => {
1113
+ event.stopPropagation()
1114
+
1115
+ mouseEvent$.next({
1116
+ type: 'relationship',
1117
+ eventName: 'click',
1118
+ pluginName,
1119
+ highlightTarget: data.fullChartParams.highlightTarget,
1120
+ datum: datum,
1121
+ category: data.CategoryNodeMap.get(datum.categoryLabel)!,
1122
+ categoryIndex: datum.categoryIndex,
1123
+ categoryLabel: datum.categoryLabel,
1124
+ event,
1125
+ data: data.computedData
1126
+ })
1127
+ })
1128
+ })
1129
+
1130
+ dragStatus$.pipe(
1131
+ distinctUntilChanged((a, b) => a === b),
1132
+ // 只有沒有托曳時才執行
1133
+ switchMap(d => iif(() => d === 'end', mouseEvent$, EMPTY))
1134
+ ).subscribe(data => {
1135
+ subject.event$.next(data)
1136
+ })
1137
+
1138
+ combineLatest({
1139
+ renderData: renderData$,
1140
+ highlightNodes: observer.relationshipHighlightNodes$.pipe(
1141
+ map(data => data.map(d => d.id))
1142
+ ),
1143
+ highlightEdges: observer.relationshipHighlightEdges$.pipe(
1144
+ map(data => data.map(d => d.id))
1145
+ ),
1146
+ fullChartParams: observer.fullChartParams$,
1147
+ fullParams: observer.fullParams$,
1148
+ }).pipe(
1149
+ takeUntil(destroy$),
1150
+ switchMap(async d => d)
1151
+ ).subscribe(data => {
1152
+ if (!nodeGSelection || !edgeGSelection) {
1153
+ return
1154
+ }
1155
+
1156
+ highlightNodes({
1157
+ nodeGSelection,
1158
+ edgeGSelection,
1159
+ highlightIds: data.highlightNodes,
1160
+ fullChartParams: data.fullChartParams
1161
+ })
1162
+ // highlightEdges({
1163
+ // edgeGSelection,
1164
+ // highlightIds: data.highlightEdges,
1165
+ // fullChartParams: data.fullChartParams
1166
+ // })
1167
+ })
1168
+
1169
+
1170
+
1171
+ return () => {
1172
+ destroy$.next(undefined)
1173
+ }
1174
1174
  })