@orbcharts/plugins-basic 3.0.7 → 3.0.9

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