@orbcharts/plugins-basic 3.0.8 → 3.0.9

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