@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,782 +1,785 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- switchMap,
6
- first,
7
- takeUntil,
8
- debounceTime,
9
- of,
10
- iif,
11
- Subject,
12
- Observable,
13
- distinctUntilChanged,
14
- shareReplay} from 'rxjs'
15
- import type { DefinePluginConfig } from '../../../lib/core-types'
16
- import type {
17
- ChartParams,
18
- DatumValue,
19
- DataSeries,
20
- EventName,
21
- ComputedDataSeries,
22
- ComputedDatumSeries,
23
- ContainerPosition } from '../../../lib/core-types'
24
- import {
25
- defineSeriesPlugin } from '../../../lib/core'
26
- import type { BubblesParams, ArcScaleType } from '../../../lib/plugins-basic-types'
27
- import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
28
- import { renderCircleText } from '../../utils/d3Graphics'
29
- import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
30
- import { getDatumColor } from '../../utils/orbchartsUtils'
31
-
32
- interface BubblesDatum extends ComputedDatumSeries {
33
- x: number
34
- y: number
35
- r: number
36
- renderLabel: string
37
- _originR: number // 紀錄變化前的r
38
- }
39
-
40
- type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
41
-
42
- const pluginName = 'Bubbles'
43
-
44
- const baseLineHeight = 12 // 未變形前的字體大小(代入計算用而已,數字多少都不會有影響)
45
-
46
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_BUBBLES_PARAMS> = {
47
- name: pluginName,
48
- defaultParams: DEFAULT_BUBBLES_PARAMS,
49
- layerIndex: LAYER_INDEX_OF_GRAPHIC,
50
- validator: (params, { validateColumns }) => {
51
- const result = validateColumns(params, {
52
- force: {
53
- toBeTypes: ['object']
54
- },
55
- bubbleLabel: {
56
- toBeTypes: ['object']
57
- },
58
- arcScaleType: {
59
- toBe: '"area" | "radius"',
60
- test: (value) => value === 'area' || value === 'radius'
61
- }
62
- })
63
- if (params.force) {
64
- const forceResult = validateColumns(params.force, {
65
- velocityDecay: {
66
- toBeTypes: ['number']
67
- },
68
- collisionSpacing: {
69
- toBeTypes: ['number']
70
- },
71
- strength: {
72
- toBeTypes: ['number']
73
- },
74
- })
75
- if (forceResult.status === 'error') {
76
- return forceResult
77
- }
78
- }
79
- if (params.bubbleLabel) {
80
- const bubbleLabelResult = validateColumns(params.bubbleLabel, {
81
- labelFn: {
82
- toBeTypes: ['Function'],
83
- },
84
- colorType: {
85
- toBeOption: 'ColorType'
86
- },
87
- fillRate: {
88
- toBeTypes: ['number']
89
- },
90
- lineHeight: {
91
- toBeTypes: ['number']
92
- },
93
- maxLineLength: {
94
- toBeTypes: ['number']
95
- },
96
- })
97
- if (bubbleLabelResult.status === 'error') {
98
- return bubbleLabelResult
99
- }
100
- }
101
- return result
102
- }
103
- }
104
-
105
-
106
- // let isRunning = false
107
-
108
- function createSimulation (bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>, fullParams: BubblesParams) {
109
- return d3.forceSimulation()
110
- .velocityDecay(fullParams.force!.velocityDecay!)
111
- // .alphaDecay(0.2)
112
- .force(
113
- "collision",
114
- d3.forceCollide()
115
- .radius((d: d3.SimulationNodeDatum & BubblesDatum) => {
116
- return d.r + fullParams.force!.collisionSpacing
117
- })
118
- // .strength(0.01)
119
- )
120
- .force("charge", d3.forceManyBody().strength((d: d3.SimulationNodeDatum & BubblesDatum) => {
121
- return - Math.pow(d.r, 2.0) * fullParams.force!.strength
122
- }))
123
- // .force("charge", d3.forceManyBody().strength(-2000))
124
- // .force("collision", d3.forceCollide(60).strength(1)) // @Q@ 60為泡泡的R,暫時是先寫死的
125
- // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
126
- // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
127
- .on("tick", () => {
128
- // if (!bubblesSelection) {
129
- // return
130
- // }
131
- bubblesSelection
132
- .attr("transform", (d) => {
133
- return `translate(${d.x},${d.y})`
134
- })
135
- // .attr("cx", (d) => d.x)
136
- // .attr("cy", (d) => d.y)
137
-
138
-
139
- })
140
- // .on("end", () => {
141
-
142
- // })
143
-
144
- }
145
-
146
-
147
- // // 計算最大泡泡的半徑
148
- // function getMaxR ({ data, totalR, maxValue, avgValue }: {
149
- // data: DatumValue[]
150
- // totalR: number
151
- // maxValue: number
152
- // avgValue: number
153
- // }) {
154
- // // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
155
- // const avgR = totalR / Math.sqrt(data.length)
156
- // const avgSize = avgR * avgR * Math.PI
157
- // const sizeRate = avgSize / avgValue
158
- // const maxSize = maxValue * sizeRate
159
- // const maxR = Math.pow(maxSize / Math.PI, 0.5)
160
-
161
- // const modifier = 0.785 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
162
- // return maxR * modifier
163
- // }
164
-
165
- // function createBubblesData ({ visibleComputedSortedData, LastBubbleDataMap, fullParams, graphicWidth, graphicHeight, DatumContainerPositionMap, scaleType }: {
166
- // visibleComputedSortedData: ComputedDataSeries
167
- // LastBubbleDataMap: Map<string, BubblesDatum>
168
- // fullParams: BubblesParams
169
- // graphicWidth: number
170
- // graphicHeight: number
171
- // DatumContainerPositionMap: Map<string, ContainerPosition>
172
- // scaleType: ArcScaleType
173
- // // highlightIds: string[]
174
- // }): BubblesDatum[] {
175
- // // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
176
- // const totalR = Math.min(...[graphicWidth, graphicHeight]) / 2
177
-
178
- // const data = visibleComputedSortedData.flat()
179
-
180
- // const totalValue = data.reduce((acc, current) => acc + current.value, 0)
181
-
182
- // // 半徑比例尺
183
- // const radiusScale = d3.scalePow()
184
- // .domain([0, totalValue])
185
- // .range([0, totalR])
186
- // .exponent(scaleType === 'area'
187
- // ? 0.5 // 數值映射面積(0.5為取平方根)
188
- // : 1 // 數值映射半徑
189
- // )
190
-
191
- // // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
192
- // const scaleFactor = scaleType === 'area'
193
- // ? 1
194
- // // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
195
- // : (() => {
196
- // const totalArea = totalR * totalR * Math.PI
197
- // return Math.sqrt(totalArea / d3.sum(data, d => Math.PI * Math.pow(radiusScale(d.value), 2)))
198
- // })()
199
-
200
- // // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
201
- // const adjustmentFactor = 0.9
202
-
203
- // return data.map((_d) => {
204
- // const d: BubblesDatum = _d as BubblesDatum
205
-
206
- // d.renderLabel = fullParams.bubbleLabel.labelFn(d)
207
-
208
- // const existDatum = LastBubbleDataMap.get(d.id)
209
-
210
- // if (existDatum) {
211
- // // 使用現有的座標
212
- // d.x = existDatum.x
213
- // d.y = existDatum.y
214
- // } else {
215
- // const seriesContainerPosition = DatumContainerPositionMap.get(d.id)!
216
- // d.x = Math.random() * seriesContainerPosition.width
217
- // d.y = Math.random() * seriesContainerPosition.height
218
- // }
219
- // const r = radiusScale!(d.value ?? 0)! * scaleFactor * adjustmentFactor
220
- // d.r = r
221
- // d._originR = r
222
-
223
- // return d
224
- // })
225
- // }
226
-
227
- function renderBubbles ({ selection, bubblesData, fullParams, fullChartParams }: {
228
- selection: d3.Selection<SVGGElement, any, any, any>
229
- bubblesData: BubblesDatum[]
230
- fullParams: BubblesParams
231
- fullChartParams: ChartParams
232
- }) {
233
- const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
234
- .data(bubblesData, (d) => d.id)
235
- .join(
236
- enter => {
237
- const enterSelection = enter
238
- .append('g')
239
- .attr('cursor', 'pointer')
240
- .attr('font-size', baseLineHeight)
241
- .style('fill', '#ffffff')
242
- .attr("text-anchor", "middle")
243
-
244
- enterSelection
245
- .append("circle")
246
- .attr("class", "node")
247
- .attr("cx", 0)
248
- .attr("cy", 0)
249
- // .attr("r", 1e-6)
250
- .attr('fill', (d) => d.color)
251
- // .transition()
252
- // .duration(500)
253
-
254
- enterSelection
255
- .append('text')
256
- .style('opacity', 0.8)
257
- .attr('pointer-events', 'none')
258
-
259
- return enterSelection
260
- },
261
- update => {
262
- return update
263
- },
264
- exit => {
265
- return exit
266
- .remove()
267
- }
268
- )
269
- .attr("transform", (d) => {
270
- return `translate(${d.x},${d.y})`
271
- })
272
-
273
- bubblesSelection.select('circle')
274
- .transition()
275
- .duration(200)
276
- // .ease(d3.easeLinear)
277
- .attr("r", (d) => d.r)
278
- .attr('fill', (d) => d.color)
279
- bubblesSelection
280
- .each((d,i,g) => {
281
- const gSelection = d3.select(g[i])
282
- const text = d.renderLabel
283
-
284
- gSelection.call(renderCircleText, {
285
- text,
286
- radius: d.r * fullParams.bubbleLabel.fillRate,
287
- lineHeight: baseLineHeight * fullParams.bubbleLabel.lineHeight,
288
- isBreakAll: text.length <= fullParams.bubbleLabel.maxLineLength
289
- ? false
290
- : fullParams.bubbleLabel.wordBreakAll
291
- })
292
-
293
- // -- text color --
294
- gSelection.select('text').attr('fill', _ => getDatumColor({
295
- datum: d,
296
- colorType: fullParams.bubbleLabel.colorType,
297
- fullChartParams: fullChartParams
298
- }))
299
-
300
- })
301
-
302
- return bubblesSelection
303
- }
304
-
305
- function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
306
- data: BubblesDatum[]
307
- // fullParams: BubblesParams
308
- highlightRIncrease: number
309
- highlightIds: string[]
310
- }) {
311
- if (highlightRIncrease == 0) {
312
- return
313
- }
314
- if (!highlightIds.length) {
315
- data.forEach(d => d.r = d._originR)
316
- return
317
- }
318
- data.forEach(d => {
319
- if (highlightIds.includes(d.id)) {
320
- d.r = d._originR + highlightRIncrease
321
- } else {
322
- d.r = d._originR
323
- }
324
- })
325
- }
326
-
327
- function drag (_simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>): d3.DragBehavior<Element, unknown, unknown> {
328
- return d3.drag()
329
- .on("start", (event, d: any) => {
330
- if (!event.active) {
331
- _simulation!.alpha(1).restart()
332
- }
333
- d.fx = d.x
334
- d.fy = d.y
335
- })
336
- .on("drag", (event, d: any) => {
337
- if (!event.active) {
338
- _simulation!.alphaTarget(0)
339
- }
340
- d.fx = event.x
341
- d.fy = event.y
342
- })
343
- .on("end", (event, d: any) => {
344
- d.fx = null
345
- d.fy = null
346
- _simulation!.alpha(1).restart()
347
- })
348
- }
349
-
350
-
351
- // private nodeTypePos (d: any) {
352
- // console.log(d)
353
- // console.log(this.TypeCenters.get(d.type)!)
354
- // const typeCenter = this.TypeCenters.get(d.type)!
355
- // return typeCenter ? typeCenter.x : 0
356
- // }
357
-
358
- function groupBubbles ({ _simulation, fullParams, DatumContainerPositionMap }: {
359
- _simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>
360
- fullParams: BubblesParams
361
- // graphicWidth: number
362
- // graphicHeight: number
363
- DatumContainerPositionMap: Map<string, ContainerPosition>
364
- }) {
365
- // console.log('groupBubbles')
366
-
367
- _simulation!
368
- // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
369
- // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
370
- .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
371
- let position = DatumContainerPositionMap.get(data.id)!
372
- if (!position) {
373
- // 有時候可能會因為時間差而找不到,這時候取第一筆
374
- position = DatumContainerPositionMap.get(Array.from(DatumContainerPositionMap.keys())[0])
375
- }
376
-
377
- return position?.centerX ?? null
378
- }))
379
- .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
380
- let position = DatumContainerPositionMap.get(data.id)!
381
- if (!position) {
382
- position = DatumContainerPositionMap.get(Array.from(DatumContainerPositionMap.keys())[0])
383
- }
384
-
385
- return position?.centerY ?? null
386
- }))
387
-
388
- // force!.alpha(1).restart()
389
- }
390
-
391
- function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
392
- bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
393
- fullChartParams: ChartParams
394
- highlightIds: string[]
395
- }) {
396
- bubblesSelection.interrupt('highlight')
397
-
398
- if (!highlightIds.length) {
399
- bubblesSelection
400
- .transition('highlight')
401
- .style('opacity', 1)
402
- return
403
- }
404
-
405
- bubblesSelection.each((d, i, n) => {
406
- const segment = d3.select(n[i])
407
-
408
- if (highlightIds.includes(d.id)) {
409
- segment
410
- .style('opacity', 1)
411
- .transition('highlight')
412
- .ease(d3.easeElastic)
413
- .duration(500)
414
- } else {
415
- // 取消放大
416
- segment
417
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
418
- }
419
- })
420
- }
421
-
422
-
423
- export const Bubbles = defineSeriesPlugin(pluginConfig)(({ selection, name, observer, subject }) => {
424
-
425
- const destroy$ = new Subject()
426
-
427
- let simulation: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
428
-
429
- // 紀錄前一次bubble data
430
- let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
431
-
432
-
433
- const scaleType$ = observer.fullParams$.pipe(
434
- takeUntil(destroy$),
435
- map(d => d.arcScaleType),
436
- distinctUntilChanged(),
437
- shareReplay(1)
438
- )
439
-
440
- // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
441
- const totalR$ = observer.layout$.pipe(
442
- takeUntil(destroy$),
443
- map(d => Math.min(d.width, d.height) / 2),
444
- distinctUntilChanged(),
445
- shareReplay(1)
446
- )
447
-
448
- const totalValue$ = observer.visibleComputedSortedData$.pipe(
449
- takeUntil(destroy$),
450
- map(d => d.flat().reduce((acc, current) => acc + current.value, 0)),
451
- distinctUntilChanged(),
452
- shareReplay(1)
453
- )
454
-
455
- // 半徑比例尺
456
- const radiusScale$ = combineLatest({
457
- totalR: totalR$,
458
- totalValue: totalValue$,
459
- scaleType: scaleType$
460
- }).pipe(
461
- takeUntil(destroy$),
462
- switchMap(async (d) => d),
463
- map(data => {
464
- return d3.scalePow()
465
- .domain([0, data.totalValue])
466
- .range([0, data.totalR])
467
- .exponent(data.scaleType === 'area'
468
- ? 0.5 // 數值映射面積(0.5為取平方根)
469
- : 1 // 數值映射半徑
470
- )
471
- }),
472
- shareReplay(1)
473
- )
474
-
475
- // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
476
- const scaleFactor$ = scaleType$.pipe(
477
- takeUntil(destroy$),
478
- switchMap(scaleType => {
479
- return iif(
480
- () => scaleType === 'area',
481
- of(1),
482
- combineLatest({
483
- totalR: totalR$,
484
- radiusScale: radiusScale$,
485
- visibleComputedSortedData: observer.visibleComputedSortedData$
486
- }).pipe(
487
- switchMap(async (d) => d),
488
- map(data => {
489
- // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
490
- const totalArea = data.totalR * data.totalR * Math.PI
491
- return Math.sqrt(totalArea / d3.sum(data.visibleComputedSortedData.flat(), d => Math.PI * Math.pow(data.radiusScale(d.value), 2)))
492
- })
493
- )
494
- )
495
- })
496
- )
497
-
498
- const DatumRMap$ = combineLatest({
499
- visibleComputedSortedData: observer.visibleComputedSortedData$,
500
- radiusScale: radiusScale$,
501
- scaleFactor: scaleFactor$
502
- }).pipe(
503
- takeUntil(destroy$),
504
- switchMap(async (d) => d),
505
- map(data => {
506
- // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
507
- const adjustmentFactor = 0.9
508
-
509
- return new Map<string, number>(
510
- data.visibleComputedSortedData
511
- .flat()
512
- .map(d => [d.id, data.radiusScale(d.value ?? 0) * data.scaleFactor * adjustmentFactor])
513
- )
514
- }),
515
- shareReplay(1)
516
- )
517
-
518
- // 初始座標
519
- const DatumInitXYMap$ = observer.DatumContainerPositionMap$.pipe(
520
- takeUntil(destroy$),
521
- map(data => {
522
- return new Map<string, { x: number, y: number }>(
523
- Array.from(data).map(([id, position]) => {
524
- return [
525
- id,
526
- {
527
- x: position.startX + (position.width * Math.random()),
528
- y: position.startY + (position.height * Math.random())
529
- }
530
- ]
531
- })
532
- )
533
- }),
534
- first(), // 只算一次
535
- shareReplay(1)
536
- )
537
-
538
- const bubblesData$ = combineLatest({
539
- visibleComputedSortedData: observer.visibleComputedSortedData$,
540
- DatumRMap: DatumRMap$,
541
- DatumInitXYMap: DatumInitXYMap$,
542
- fullParams: observer.fullParams$,
543
- }).pipe(
544
- takeUntil(destroy$),
545
- switchMap(async (d) => d),
546
- map(data => {
547
- return data.visibleComputedSortedData
548
- .flat()
549
- .map(_d => {
550
- // 傳址,附加計算的欄位資料會 reference 到始資料上
551
- const d: BubblesDatum = _d as BubblesDatum
552
-
553
- // 第一次計算時沒有 x, y 座標,取得預設座標。第二次之後計算使用原有的座標
554
- if (d.x === undefined || d.y === undefined) {
555
- let xy = data.DatumInitXYMap.get(d.id)!
556
- if (!xy) {
557
- xy = data.DatumInitXYMap.get(Array.from(data.DatumInitXYMap.keys())[0])
558
- }
559
- d.x = xy.x
560
- d.y = xy.y
561
- }
562
- d.r = data.DatumRMap.get(d.id)!
563
- d._originR = d.r
564
- d.renderLabel = data.fullParams.bubbleLabel.labelFn(d)
565
- return d
566
- })
567
- }),
568
- shareReplay(1)
569
- )
570
-
571
- // const bubblesData$ = combineLatest({
572
- // layout: observer.layout$,
573
- // fullParams: observer.fullParams$,
574
- // DatumContainerPositionMap: observer.DatumContainerPositionMap$,
575
- // visibleComputedSortedData: observer.visibleComputedSortedData$,
576
- // scaleType: scaleType$,
577
- // }).pipe(
578
- // takeUntil(destroy$),
579
- // switchMap(async (d) => d),
580
- // map(data => {
581
- // // console.log(data.visibleComputedSortedData)
582
- // return createBubblesData({
583
- // visibleComputedSortedData: data.visibleComputedSortedData,
584
- // LastBubbleDataMap,
585
- // fullParams: data.fullParams,
586
- // graphicWidth: data.layout.width,
587
- // graphicHeight: data.layout.height,
588
- // DatumContainerPositionMap: data.DatumContainerPositionMap,
589
- // scaleType: data.scaleType
590
- // })
591
- // }),
592
- // shareReplay(1)
593
- // )
594
-
595
- // // 紀錄前一次bubble data
596
- // bubblesData$.subscribe(d => {
597
- // LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
598
- // })
599
-
600
- const highlightTarget$ = observer.fullChartParams$.pipe(
601
- takeUntil(destroy$),
602
- map(d => d.highlightTarget),
603
- distinctUntilChanged()
604
- )
605
-
606
- const bubblesSelection$ = combineLatest({
607
- bubblesData: bubblesData$,
608
- fullParams: observer.fullParams$,
609
- fullChartParams: observer.fullChartParams$,
610
- DatumContainerPositionMap: observer.DatumContainerPositionMap$,
611
- }).pipe(
612
- takeUntil(destroy$),
613
- switchMap(async (d) => d),
614
- map(data => {
615
- if (simulation) {
616
- // 先停止,重新計算之後再restart
617
- simulation.stop()
618
- }
619
-
620
- const bubblesSelection = renderBubbles({
621
- selection,
622
- bubblesData: data.bubblesData,
623
- fullParams: data.fullParams,
624
- fullChartParams: data.fullChartParams,
625
- })
626
-
627
- simulation = createSimulation(bubblesSelection, data.fullParams)
628
-
629
- simulation.nodes(data.bubblesData)
630
-
631
- groupBubbles({
632
- _simulation: simulation,
633
- fullParams: data.fullParams,
634
- DatumContainerPositionMap: data.DatumContainerPositionMap
635
- // graphicWidth: data.layout.width,
636
- // graphicHeight: data.layout.height
637
- })
638
-
639
- simulation!.alpha(1).restart()
640
-
641
- return bubblesSelection
642
- }),
643
- shareReplay(1)
644
- )
645
-
646
- combineLatest({
647
- bubblesSelection: bubblesSelection$,
648
- computedData: observer.computedData$,
649
- SeriesDataMap: observer.SeriesDataMap$,
650
- highlightTarget: highlightTarget$,
651
- }).pipe(
652
- takeUntil(destroy$),
653
- switchMap(async (d) => d)
654
- ).subscribe(data => {
655
-
656
- data.bubblesSelection
657
- .on('mouseover', (event, datum) => {
658
- // this.tooltip!.setDatum({
659
- // data: d,
660
- // x: d3.event.clientX,
661
- // y: d3.event.clientY
662
- // })
663
-
664
- subject.event$.next({
665
- type: 'series',
666
- eventName: 'mouseover',
667
- pluginName: name,
668
- highlightTarget: data.highlightTarget,
669
- datum,
670
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
671
- seriesIndex: datum.seriesIndex,
672
- seriesLabel: datum.seriesLabel,
673
- event,
674
- data: data.computedData
675
- })
676
- })
677
- .on('mousemove', (event, datum) => {
678
- // this.tooltip!.setDatum({
679
- // x: d3.event.clientX,
680
- // y: d3.event.clientY
681
- // })
682
-
683
- subject.event$.next({
684
- type: 'series',
685
- eventName: 'mousemove',
686
- pluginName: name,
687
- highlightTarget: data.highlightTarget,
688
- datum,
689
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
690
- seriesIndex: datum.seriesIndex,
691
- seriesLabel: datum.seriesLabel,
692
- event,
693
- data: data.computedData
694
- })
695
- })
696
- .on('mouseout', (event, datum) => {
697
- // this.tooltip!.remove()
698
-
699
- subject.event$.next({
700
- type: 'series',
701
- eventName: 'mouseout',
702
- pluginName: name,
703
- highlightTarget: data.highlightTarget,
704
- datum,
705
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
706
- seriesIndex: datum.seriesIndex,
707
- seriesLabel: datum.seriesLabel,
708
- event,
709
- data: data.computedData
710
- })
711
- })
712
- .on('click', (event, datum) => {
713
-
714
- subject.event$.next({
715
- type: 'series',
716
- eventName: 'click',
717
- pluginName: name,
718
- highlightTarget: data.highlightTarget,
719
- datum,
720
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
721
- seriesIndex: datum.seriesIndex,
722
- seriesLabel: datum.seriesLabel,
723
- event,
724
- data: data.computedData
725
- })
726
- })
727
- .call(drag(simulation) as any)
728
-
729
-
730
- })
731
-
732
- combineLatest({
733
- bubblesSelection: bubblesSelection$,
734
- // bubblesData: bubblesData$,
735
- highlight: observer.seriesHighlight$.pipe(
736
- map(data => data.map(d => d.id))
737
- ),
738
- fullChartParams: observer.fullChartParams$,
739
- // fullParams: observer.fullParams$,
740
- // sumSeries: sumSeries$,
741
- // // layout: observer.layout$,
742
- // DatumContainerPositionMap: observer.DatumContainerPositionMap$,
743
- }).pipe(
744
- takeUntil(destroy$),
745
- switchMap(async d => d)
746
- ).subscribe(data => {
747
- highlight({
748
- bubblesSelection: data.bubblesSelection,
749
- highlightIds: data.highlight,
750
- fullChartParams: data.fullChartParams
751
- })
752
-
753
- // if (data.fullParams.highlightRIncrease) {
754
- // setHighlightData ({
755
- // data: data.bubblesData,
756
- // highlightRIncrease: data.fullParams.highlightRIncrease,
757
- // highlightIds: data.highlight
758
- // })
759
- // data.bubblesSelection.select('circle')
760
- // // .transition()
761
- // // .duration(200)
762
- // .attr("r", (d) => d.r)
763
-
764
- // force!.nodes(data.bubblesData)
765
-
766
- // groupBubbles({
767
- // fullParams: data.fullParams,
768
- // DatumContainerPositionMap: data.DatumContainerPositionMap
769
- // })
770
- // }
771
-
772
- })
773
-
774
-
775
- return () => {
776
- destroy$.next(undefined)
777
- if (simulation) {
778
- simulation.stop()
779
- simulation = undefined
780
- }
781
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ filter,
6
+ switchMap,
7
+ first,
8
+ takeUntil,
9
+ takeWhile,
10
+ debounceTime,
11
+ of,
12
+ iif,
13
+ Subject,
14
+ Observable,
15
+ distinctUntilChanged,
16
+ shareReplay} from 'rxjs'
17
+ import type { DefinePluginConfig } from '../../../lib/core-types'
18
+ import type {
19
+ ChartParams,
20
+ DatumValue,
21
+ DataSeries,
22
+ EventName,
23
+ ComputedDataSeries,
24
+ ComputedDatumSeries,
25
+ ContainerPosition } from '../../../lib/core-types'
26
+ import {
27
+ defineSeriesPlugin } from '../../../lib/core'
28
+ import type { BubblesParams, ArcScaleType } from '../../../lib/plugins-basic-types'
29
+ import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
30
+ import { renderCircleText } from '../../utils/d3Graphics'
31
+ import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
32
+ import { getDatumColor } from '../../utils/orbchartsUtils'
33
+
34
+ interface BubblesDatum extends ComputedDatumSeries {
35
+ x: number
36
+ y: number
37
+ r: number
38
+ renderLabel: string
39
+ _originR: number // 紀錄變化前的r
40
+ }
41
+
42
+ type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
43
+
44
+ const pluginName = 'Bubbles'
45
+
46
+ const baseLineHeight = 12 // 未變形前的字體大小(代入計算用而已,數字多少都不會有影響)
47
+
48
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_BUBBLES_PARAMS> = {
49
+ name: pluginName,
50
+ defaultParams: DEFAULT_BUBBLES_PARAMS,
51
+ layerIndex: LAYER_INDEX_OF_GRAPHIC,
52
+ validator: (params, { validateColumns }) => {
53
+ const result = validateColumns(params, {
54
+ force: {
55
+ toBeTypes: ['object']
56
+ },
57
+ bubbleLabel: {
58
+ toBeTypes: ['object']
59
+ },
60
+ arcScaleType: {
61
+ toBe: '"area" | "radius"',
62
+ test: (value) => value === 'area' || value === 'radius'
63
+ }
64
+ })
65
+ if (params.force) {
66
+ const forceResult = validateColumns(params.force, {
67
+ velocityDecay: {
68
+ toBeTypes: ['number']
69
+ },
70
+ collisionSpacing: {
71
+ toBeTypes: ['number']
72
+ },
73
+ strength: {
74
+ toBeTypes: ['number']
75
+ },
76
+ })
77
+ if (forceResult.status === 'error') {
78
+ return forceResult
79
+ }
80
+ }
81
+ if (params.bubbleLabel) {
82
+ const bubbleLabelResult = validateColumns(params.bubbleLabel, {
83
+ labelFn: {
84
+ toBeTypes: ['Function'],
85
+ },
86
+ colorType: {
87
+ toBeOption: 'ColorType'
88
+ },
89
+ fillRate: {
90
+ toBeTypes: ['number']
91
+ },
92
+ lineHeight: {
93
+ toBeTypes: ['number']
94
+ },
95
+ maxLineLength: {
96
+ toBeTypes: ['number']
97
+ },
98
+ })
99
+ if (bubbleLabelResult.status === 'error') {
100
+ return bubbleLabelResult
101
+ }
102
+ }
103
+ return result
104
+ }
105
+ }
106
+
107
+
108
+ // let isRunning = false
109
+
110
+ function createSimulation (bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>, fullParams: BubblesParams) {
111
+ return d3.forceSimulation()
112
+ .velocityDecay(fullParams.force!.velocityDecay!)
113
+ // .alphaDecay(0.2)
114
+ .force(
115
+ "collision",
116
+ d3.forceCollide()
117
+ .radius((d: d3.SimulationNodeDatum & BubblesDatum) => {
118
+ return d.r + fullParams.force!.collisionSpacing
119
+ })
120
+ // .strength(0.01)
121
+ )
122
+ .force("charge", d3.forceManyBody().strength((d: d3.SimulationNodeDatum & BubblesDatum) => {
123
+ return - Math.pow(d.r, 2.0) * fullParams.force!.strength
124
+ }))
125
+ // .force("charge", d3.forceManyBody().strength(-2000))
126
+ // .force("collision", d3.forceCollide(60).strength(1)) // @Q@ 60為泡泡的R,暫時是先寫死的
127
+ // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
128
+ // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
129
+ .on("tick", () => {
130
+ // if (!bubblesSelection) {
131
+ // return
132
+ // }
133
+ bubblesSelection
134
+ .attr("transform", (d) => {
135
+ return `translate(${d.x},${d.y})`
136
+ })
137
+ // .attr("cx", (d) => d.x)
138
+ // .attr("cy", (d) => d.y)
139
+
140
+
141
+ })
142
+ // .on("end", () => {
143
+
144
+ // })
145
+
146
+ }
147
+
148
+
149
+ // // 計算最大泡泡的半徑
150
+ // function getMaxR ({ data, totalR, maxValue, avgValue }: {
151
+ // data: DatumValue[]
152
+ // totalR: number
153
+ // maxValue: number
154
+ // avgValue: number
155
+ // }) {
156
+ // // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
157
+ // const avgR = totalR / Math.sqrt(data.length)
158
+ // const avgSize = avgR * avgR * Math.PI
159
+ // const sizeRate = avgSize / avgValue
160
+ // const maxSize = maxValue * sizeRate
161
+ // const maxR = Math.pow(maxSize / Math.PI, 0.5)
162
+
163
+ // const modifier = 0.785 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
164
+ // return maxR * modifier
165
+ // }
166
+
167
+ // function createBubblesData ({ visibleComputedSortedData, LastBubbleDataMap, fullParams, graphicWidth, graphicHeight, DatumContainerPositionMap, scaleType }: {
168
+ // visibleComputedSortedData: ComputedDataSeries
169
+ // LastBubbleDataMap: Map<string, BubblesDatum>
170
+ // fullParams: BubblesParams
171
+ // graphicWidth: number
172
+ // graphicHeight: number
173
+ // DatumContainerPositionMap: Map<string, ContainerPosition>
174
+ // scaleType: ArcScaleType
175
+ // // highlightIds: string[]
176
+ // }): BubblesDatum[] {
177
+ // // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
178
+ // const totalR = Math.min(...[graphicWidth, graphicHeight]) / 2
179
+
180
+ // const data = visibleComputedSortedData.flat()
181
+
182
+ // const totalValue = data.reduce((acc, current) => acc + current.value, 0)
183
+
184
+ // // 半徑比例尺
185
+ // const radiusScale = d3.scalePow()
186
+ // .domain([0, totalValue])
187
+ // .range([0, totalR])
188
+ // .exponent(scaleType === 'area'
189
+ // ? 0.5 // 數值映射面積(0.5為取平方根)
190
+ // : 1 // 數值映射半徑
191
+ // )
192
+
193
+ // // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
194
+ // const scaleFactor = scaleType === 'area'
195
+ // ? 1
196
+ // // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
197
+ // : (() => {
198
+ // const totalArea = totalR * totalR * Math.PI
199
+ // return Math.sqrt(totalArea / d3.sum(data, d => Math.PI * Math.pow(radiusScale(d.value), 2)))
200
+ // })()
201
+
202
+ // // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
203
+ // const adjustmentFactor = 0.9
204
+
205
+ // return data.map((_d) => {
206
+ // const d: BubblesDatum = _d as BubblesDatum
207
+
208
+ // d.renderLabel = fullParams.bubbleLabel.labelFn(d)
209
+
210
+ // const existDatum = LastBubbleDataMap.get(d.id)
211
+
212
+ // if (existDatum) {
213
+ // // 使用現有的座標
214
+ // d.x = existDatum.x
215
+ // d.y = existDatum.y
216
+ // } else {
217
+ // const seriesContainerPosition = DatumContainerPositionMap.get(d.id)!
218
+ // d.x = Math.random() * seriesContainerPosition.width
219
+ // d.y = Math.random() * seriesContainerPosition.height
220
+ // }
221
+ // const r = radiusScale!(d.value ?? 0)! * scaleFactor * adjustmentFactor
222
+ // d.r = r
223
+ // d._originR = r
224
+
225
+ // return d
226
+ // })
227
+ // }
228
+
229
+ function renderBubbles ({ selection, bubblesData, fullParams, fullChartParams }: {
230
+ selection: d3.Selection<SVGGElement, any, any, any>
231
+ bubblesData: BubblesDatum[]
232
+ fullParams: BubblesParams
233
+ fullChartParams: ChartParams
234
+ }) {
235
+ const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
236
+ .data(bubblesData, (d) => d.id)
237
+ .join(
238
+ enter => {
239
+ const enterSelection = enter
240
+ .append('g')
241
+ .attr('cursor', 'pointer')
242
+ .attr('font-size', baseLineHeight)
243
+ .style('fill', '#ffffff')
244
+ .attr("text-anchor", "middle")
245
+
246
+ enterSelection
247
+ .append("circle")
248
+ .attr("class", "node")
249
+ .attr("cx", 0)
250
+ .attr("cy", 0)
251
+ // .attr("r", 1e-6)
252
+ .attr('fill', (d) => d.color)
253
+ // .transition()
254
+ // .duration(500)
255
+
256
+ enterSelection
257
+ .append('text')
258
+ .style('opacity', 0.8)
259
+ .attr('pointer-events', 'none')
260
+
261
+ return enterSelection
262
+ },
263
+ update => {
264
+ return update
265
+ },
266
+ exit => {
267
+ return exit
268
+ .remove()
269
+ }
270
+ )
271
+ .attr("transform", (d) => {
272
+ return `translate(${d.x},${d.y})`
273
+ })
274
+
275
+ bubblesSelection.select('circle')
276
+ .transition()
277
+ .duration(200)
278
+ // .ease(d3.easeLinear)
279
+ .attr("r", (d) => d.r)
280
+ .attr('fill', (d) => d.color)
281
+ bubblesSelection
282
+ .each((d,i,g) => {
283
+ const gSelection = d3.select(g[i])
284
+ const text = d.renderLabel
285
+
286
+ gSelection.call(renderCircleText, {
287
+ text,
288
+ radius: d.r * fullParams.bubbleLabel.fillRate,
289
+ lineHeight: baseLineHeight * fullParams.bubbleLabel.lineHeight,
290
+ isBreakAll: text.length <= fullParams.bubbleLabel.maxLineLength
291
+ ? false
292
+ : fullParams.bubbleLabel.wordBreakAll
293
+ })
294
+
295
+ // -- text color --
296
+ gSelection.select('text').attr('fill', _ => getDatumColor({
297
+ datum: d,
298
+ colorType: fullParams.bubbleLabel.colorType,
299
+ fullChartParams: fullChartParams
300
+ }))
301
+
302
+ })
303
+
304
+ return bubblesSelection
305
+ }
306
+
307
+ function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
308
+ data: BubblesDatum[]
309
+ // fullParams: BubblesParams
310
+ highlightRIncrease: number
311
+ highlightIds: string[]
312
+ }) {
313
+ if (highlightRIncrease == 0) {
314
+ return
315
+ }
316
+ if (!highlightIds.length) {
317
+ data.forEach(d => d.r = d._originR)
318
+ return
319
+ }
320
+ data.forEach(d => {
321
+ if (highlightIds.includes(d.id)) {
322
+ d.r = d._originR + highlightRIncrease
323
+ } else {
324
+ d.r = d._originR
325
+ }
326
+ })
327
+ }
328
+
329
+ function drag (_simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>): d3.DragBehavior<Element, unknown, unknown> {
330
+ return d3.drag()
331
+ .on("start", (event, d: any) => {
332
+ if (!event.active) {
333
+ _simulation!.alpha(1).restart()
334
+ }
335
+ d.fx = d.x
336
+ d.fy = d.y
337
+ })
338
+ .on("drag", (event, d: any) => {
339
+ if (!event.active) {
340
+ _simulation!.alphaTarget(0)
341
+ }
342
+ d.fx = event.x
343
+ d.fy = event.y
344
+ })
345
+ .on("end", (event, d: any) => {
346
+ d.fx = null
347
+ d.fy = null
348
+ _simulation!.alpha(1).restart()
349
+ })
350
+ }
351
+
352
+
353
+ // private nodeTypePos (d: any) {
354
+ // console.log(d)
355
+ // console.log(this.TypeCenters.get(d.type)!)
356
+ // const typeCenter = this.TypeCenters.get(d.type)!
357
+ // return typeCenter ? typeCenter.x : 0
358
+ // }
359
+
360
+ function groupBubbles ({ _simulation, fullParams, DatumContainerPositionMap }: {
361
+ _simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>
362
+ fullParams: BubblesParams
363
+ // graphicWidth: number
364
+ // graphicHeight: number
365
+ DatumContainerPositionMap: Map<string, ContainerPosition>
366
+ }) {
367
+ // console.log('groupBubbles')
368
+
369
+ _simulation!
370
+ // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
371
+ // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
372
+ .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
373
+ let position = DatumContainerPositionMap.get(data.id)!
374
+ if (!position) {
375
+ // 有時候可能會因為時間差而找不到,這時候取第一筆
376
+ position = DatumContainerPositionMap.get(Array.from(DatumContainerPositionMap.keys())[0])
377
+ }
378
+
379
+ return position?.centerX ?? null
380
+ }))
381
+ .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
382
+ let position = DatumContainerPositionMap.get(data.id)!
383
+ if (!position) {
384
+ position = DatumContainerPositionMap.get(Array.from(DatumContainerPositionMap.keys())[0])
385
+ }
386
+
387
+ return position?.centerY ?? null
388
+ }))
389
+
390
+ // force!.alpha(1).restart()
391
+ }
392
+
393
+ function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
394
+ bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
395
+ fullChartParams: ChartParams
396
+ highlightIds: string[]
397
+ }) {
398
+ bubblesSelection.interrupt('highlight')
399
+
400
+ if (!highlightIds.length) {
401
+ bubblesSelection
402
+ .transition('highlight')
403
+ .style('opacity', 1)
404
+ return
405
+ }
406
+
407
+ bubblesSelection.each((d, i, n) => {
408
+ const segment = d3.select(n[i])
409
+
410
+ if (highlightIds.includes(d.id)) {
411
+ segment
412
+ .style('opacity', 1)
413
+ .transition('highlight')
414
+ .ease(d3.easeElastic)
415
+ .duration(500)
416
+ } else {
417
+ // 取消放大
418
+ segment
419
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
420
+ }
421
+ })
422
+ }
423
+
424
+
425
+ export const Bubbles = defineSeriesPlugin(pluginConfig)(({ selection, name, observer, subject }) => {
426
+
427
+ const destroy$ = new Subject()
428
+
429
+ let simulation: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
430
+
431
+ // 紀錄前一次bubble data
432
+ let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
433
+
434
+
435
+ const scaleType$ = observer.fullParams$.pipe(
436
+ takeUntil(destroy$),
437
+ map(d => d.arcScaleType),
438
+ distinctUntilChanged(),
439
+ shareReplay(1)
440
+ )
441
+
442
+ // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
443
+ const totalR$ = observer.layout$.pipe(
444
+ takeUntil(destroy$),
445
+ map(d => Math.min(d.width, d.height) / 2),
446
+ distinctUntilChanged(),
447
+ shareReplay(1)
448
+ )
449
+
450
+ const totalValue$ = observer.visibleComputedSortedData$.pipe(
451
+ takeUntil(destroy$),
452
+ map(d => d.flat().reduce((acc, current) => acc + current.value, 0)),
453
+ distinctUntilChanged(),
454
+ shareReplay(1)
455
+ )
456
+
457
+ // 半徑比例尺
458
+ const radiusScale$ = combineLatest({
459
+ totalR: totalR$,
460
+ totalValue: totalValue$,
461
+ scaleType: scaleType$
462
+ }).pipe(
463
+ takeUntil(destroy$),
464
+ switchMap(async (d) => d),
465
+ map(data => {
466
+ return d3.scalePow()
467
+ .domain([0, data.totalValue])
468
+ .range([0, data.totalR])
469
+ .exponent(data.scaleType === 'area'
470
+ ? 0.5 // 數值映射面積(0.5為取平方根)
471
+ : 1 // 數值映射半徑
472
+ )
473
+ }),
474
+ shareReplay(1)
475
+ )
476
+
477
+ // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
478
+ const scaleFactor$ = scaleType$.pipe(
479
+ takeUntil(destroy$),
480
+ switchMap(scaleType => {
481
+ return iif(
482
+ () => scaleType === 'area',
483
+ of(1),
484
+ combineLatest({
485
+ totalR: totalR$,
486
+ radiusScale: radiusScale$,
487
+ visibleComputedSortedData: observer.visibleComputedSortedData$
488
+ }).pipe(
489
+ switchMap(async (d) => d),
490
+ map(data => {
491
+ // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
492
+ const totalArea = data.totalR * data.totalR * Math.PI
493
+ return Math.sqrt(totalArea / d3.sum(data.visibleComputedSortedData.flat(), d => Math.PI * Math.pow(data.radiusScale(d.value), 2)))
494
+ })
495
+ )
496
+ )
497
+ })
498
+ )
499
+
500
+ const DatumRMap$ = combineLatest({
501
+ visibleComputedSortedData: observer.visibleComputedSortedData$,
502
+ radiusScale: radiusScale$,
503
+ scaleFactor: scaleFactor$
504
+ }).pipe(
505
+ takeUntil(destroy$),
506
+ switchMap(async (d) => d),
507
+ map(data => {
508
+ // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
509
+ const adjustmentFactor = 0.9
510
+
511
+ return new Map<string, number>(
512
+ data.visibleComputedSortedData
513
+ .flat()
514
+ .map(d => [d.id, data.radiusScale(d.value ?? 0) * data.scaleFactor * adjustmentFactor])
515
+ )
516
+ }),
517
+ shareReplay(1)
518
+ )
519
+
520
+ // 初始座標
521
+ const DatumInitXYMap$ = observer.DatumContainerPositionMap$.pipe(
522
+ takeUntil(destroy$),
523
+ filter(data => data.size > 0), // 至少要有一筆資料
524
+ map(data => {
525
+ return new Map<string, { x: number, y: number }>(
526
+ Array.from(data).map(([id, position]) => {
527
+ return [
528
+ id,
529
+ {
530
+ x: position.startX + (position.width * Math.random()),
531
+ y: position.startY + (position.height * Math.random())
532
+ }
533
+ ]
534
+ })
535
+ )
536
+ }),
537
+ first(), // 只算一次
538
+ shareReplay(1)
539
+ )
540
+
541
+ const bubblesData$ = combineLatest({
542
+ visibleComputedSortedData: observer.visibleComputedSortedData$,
543
+ DatumRMap: DatumRMap$,
544
+ DatumInitXYMap: DatumInitXYMap$,
545
+ fullParams: observer.fullParams$,
546
+ }).pipe(
547
+ takeUntil(destroy$),
548
+ switchMap(async (d) => d),
549
+ map(data => {
550
+ return data.visibleComputedSortedData
551
+ .flat()
552
+ .map(_d => {
553
+ // 傳址,附加計算的欄位資料會 reference 到始資料上
554
+ const d: BubblesDatum = _d as BubblesDatum
555
+
556
+ // 第一次計算時沒有 x, y 座標,取得預設座標。第二次之後計算使用原有的座標
557
+ if (d.x === undefined || d.y === undefined) {
558
+ let xy = data.DatumInitXYMap.get(d.id)!
559
+ if (!xy) {
560
+ xy = data.DatumInitXYMap.get(Array.from(data.DatumInitXYMap.keys())[0])
561
+ }
562
+ d.x = xy.x
563
+ d.y = xy.y
564
+ }
565
+ d.r = data.DatumRMap.get(d.id)!
566
+ d._originR = d.r
567
+ d.renderLabel = data.fullParams.bubbleLabel.labelFn(d)
568
+ return d
569
+ })
570
+ }),
571
+ shareReplay(1)
572
+ )
573
+
574
+ // const bubblesData$ = combineLatest({
575
+ // layout: observer.layout$,
576
+ // fullParams: observer.fullParams$,
577
+ // DatumContainerPositionMap: observer.DatumContainerPositionMap$,
578
+ // visibleComputedSortedData: observer.visibleComputedSortedData$,
579
+ // scaleType: scaleType$,
580
+ // }).pipe(
581
+ // takeUntil(destroy$),
582
+ // switchMap(async (d) => d),
583
+ // map(data => {
584
+ // // console.log(data.visibleComputedSortedData)
585
+ // return createBubblesData({
586
+ // visibleComputedSortedData: data.visibleComputedSortedData,
587
+ // LastBubbleDataMap,
588
+ // fullParams: data.fullParams,
589
+ // graphicWidth: data.layout.width,
590
+ // graphicHeight: data.layout.height,
591
+ // DatumContainerPositionMap: data.DatumContainerPositionMap,
592
+ // scaleType: data.scaleType
593
+ // })
594
+ // }),
595
+ // shareReplay(1)
596
+ // )
597
+
598
+ // // 紀錄前一次bubble data
599
+ // bubblesData$.subscribe(d => {
600
+ // LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
601
+ // })
602
+
603
+ const highlightTarget$ = observer.fullChartParams$.pipe(
604
+ takeUntil(destroy$),
605
+ map(d => d.highlightTarget),
606
+ distinctUntilChanged()
607
+ )
608
+
609
+ const bubblesSelection$ = combineLatest({
610
+ bubblesData: bubblesData$,
611
+ fullParams: observer.fullParams$,
612
+ fullChartParams: observer.fullChartParams$,
613
+ DatumContainerPositionMap: observer.DatumContainerPositionMap$,
614
+ }).pipe(
615
+ takeUntil(destroy$),
616
+ switchMap(async (d) => d),
617
+ map(data => {
618
+ if (simulation) {
619
+ // 先停止,重新計算之後再restart
620
+ simulation.stop()
621
+ }
622
+
623
+ const bubblesSelection = renderBubbles({
624
+ selection,
625
+ bubblesData: data.bubblesData,
626
+ fullParams: data.fullParams,
627
+ fullChartParams: data.fullChartParams,
628
+ })
629
+
630
+ simulation = createSimulation(bubblesSelection, data.fullParams)
631
+
632
+ simulation.nodes(data.bubblesData)
633
+
634
+ groupBubbles({
635
+ _simulation: simulation,
636
+ fullParams: data.fullParams,
637
+ DatumContainerPositionMap: data.DatumContainerPositionMap
638
+ // graphicWidth: data.layout.width,
639
+ // graphicHeight: data.layout.height
640
+ })
641
+
642
+ simulation!.alpha(1).restart()
643
+
644
+ return bubblesSelection
645
+ }),
646
+ shareReplay(1)
647
+ )
648
+
649
+ combineLatest({
650
+ bubblesSelection: bubblesSelection$,
651
+ computedData: observer.computedData$,
652
+ SeriesDataMap: observer.SeriesDataMap$,
653
+ highlightTarget: highlightTarget$,
654
+ }).pipe(
655
+ takeUntil(destroy$),
656
+ switchMap(async (d) => d)
657
+ ).subscribe(data => {
658
+
659
+ data.bubblesSelection
660
+ .on('mouseover', (event, datum) => {
661
+ // this.tooltip!.setDatum({
662
+ // data: d,
663
+ // x: d3.event.clientX,
664
+ // y: d3.event.clientY
665
+ // })
666
+
667
+ subject.event$.next({
668
+ type: 'series',
669
+ eventName: 'mouseover',
670
+ pluginName: name,
671
+ highlightTarget: data.highlightTarget,
672
+ datum,
673
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
674
+ seriesIndex: datum.seriesIndex,
675
+ seriesLabel: datum.seriesLabel,
676
+ event,
677
+ data: data.computedData
678
+ })
679
+ })
680
+ .on('mousemove', (event, datum) => {
681
+ // this.tooltip!.setDatum({
682
+ // x: d3.event.clientX,
683
+ // y: d3.event.clientY
684
+ // })
685
+
686
+ subject.event$.next({
687
+ type: 'series',
688
+ eventName: 'mousemove',
689
+ pluginName: name,
690
+ highlightTarget: data.highlightTarget,
691
+ datum,
692
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
693
+ seriesIndex: datum.seriesIndex,
694
+ seriesLabel: datum.seriesLabel,
695
+ event,
696
+ data: data.computedData
697
+ })
698
+ })
699
+ .on('mouseout', (event, datum) => {
700
+ // this.tooltip!.remove()
701
+
702
+ subject.event$.next({
703
+ type: 'series',
704
+ eventName: 'mouseout',
705
+ pluginName: name,
706
+ highlightTarget: data.highlightTarget,
707
+ datum,
708
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
709
+ seriesIndex: datum.seriesIndex,
710
+ seriesLabel: datum.seriesLabel,
711
+ event,
712
+ data: data.computedData
713
+ })
714
+ })
715
+ .on('click', (event, datum) => {
716
+
717
+ subject.event$.next({
718
+ type: 'series',
719
+ eventName: 'click',
720
+ pluginName: name,
721
+ highlightTarget: data.highlightTarget,
722
+ datum,
723
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
724
+ seriesIndex: datum.seriesIndex,
725
+ seriesLabel: datum.seriesLabel,
726
+ event,
727
+ data: data.computedData
728
+ })
729
+ })
730
+ .call(drag(simulation) as any)
731
+
732
+
733
+ })
734
+
735
+ combineLatest({
736
+ bubblesSelection: bubblesSelection$,
737
+ // bubblesData: bubblesData$,
738
+ highlight: observer.seriesHighlight$.pipe(
739
+ map(data => data.map(d => d.id))
740
+ ),
741
+ fullChartParams: observer.fullChartParams$,
742
+ // fullParams: observer.fullParams$,
743
+ // sumSeries: sumSeries$,
744
+ // // layout: observer.layout$,
745
+ // DatumContainerPositionMap: observer.DatumContainerPositionMap$,
746
+ }).pipe(
747
+ takeUntil(destroy$),
748
+ switchMap(async d => d)
749
+ ).subscribe(data => {
750
+ highlight({
751
+ bubblesSelection: data.bubblesSelection,
752
+ highlightIds: data.highlight,
753
+ fullChartParams: data.fullChartParams
754
+ })
755
+
756
+ // if (data.fullParams.highlightRIncrease) {
757
+ // setHighlightData ({
758
+ // data: data.bubblesData,
759
+ // highlightRIncrease: data.fullParams.highlightRIncrease,
760
+ // highlightIds: data.highlight
761
+ // })
762
+ // data.bubblesSelection.select('circle')
763
+ // // .transition()
764
+ // // .duration(200)
765
+ // .attr("r", (d) => d.r)
766
+
767
+ // force!.nodes(data.bubblesData)
768
+
769
+ // groupBubbles({
770
+ // fullParams: data.fullParams,
771
+ // DatumContainerPositionMap: data.DatumContainerPositionMap
772
+ // })
773
+ // }
774
+
775
+ })
776
+
777
+
778
+ return () => {
779
+ destroy$.next(undefined)
780
+ if (simulation) {
781
+ simulation.stop()
782
+ simulation = undefined
783
+ }
784
+ }
782
785
  })