@orbcharts/plugins-basic 3.0.0-beta.2 → 3.0.0-beta.20

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 (182) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +15169 -10684
  3. package/dist/orbcharts-plugins-basic.umd.js +296 -83
  4. package/dist/src/base/BaseBars.d.ts +4 -4
  5. package/dist/src/base/BaseBarsTriangle.d.ts +4 -4
  6. package/dist/src/base/BaseDots.d.ts +4 -4
  7. package/dist/src/base/BaseGroupAxis.d.ts +2 -2
  8. package/dist/src/base/BaseLegend.d.ts +1 -1
  9. package/dist/src/base/BaseLineAreas.d.ts +5 -5
  10. package/dist/src/base/BaseLines.d.ts +5 -5
  11. package/dist/src/base/BaseRacingBars.d.ts +23 -0
  12. package/dist/src/base/BaseRacingLabels.d.ts +20 -0
  13. package/dist/src/base/BaseRacingValueLabels.d.ts +20 -0
  14. package/dist/src/base/{BaseBarStack.d.ts → BaseStackedBars.d.ts} +8 -8
  15. package/dist/src/base/BaseValueAxis.d.ts +6 -8
  16. package/dist/src/base/BaseXAxis.d.ts +20 -0
  17. package/dist/src/base/BaseYAxis.d.ts +18 -0
  18. package/dist/src/grid/defaults.d.ts +5 -5
  19. package/dist/src/grid/gridObservables.d.ts +12 -5
  20. package/dist/src/grid/index.d.ts +3 -3
  21. package/dist/src/grid/plugins/GroupZoom.d.ts +1 -0
  22. package/dist/src/grid/plugins/StackedBars.d.ts +1 -0
  23. package/dist/src/grid/plugins/StackedValueAxis.d.ts +1 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/multiGrid/defaults.d.ts +4 -4
  26. package/dist/src/multiGrid/index.d.ts +3 -3
  27. package/dist/src/multiGrid/plugins/MultiStackedBars.d.ts +1 -0
  28. package/dist/src/multiGrid/plugins/MultiStackedValueAxis.d.ts +1 -0
  29. package/dist/src/multiGrid/plugins/OverlappingStackedValueAxes.d.ts +1 -0
  30. package/dist/src/multiValue/defaults.d.ts +13 -0
  31. package/dist/src/multiValue/index.d.ts +12 -0
  32. package/dist/src/multiValue/multiValueObservables.d.ts +56 -0
  33. package/dist/src/multiValue/plugins/MultiValueLegend.d.ts +1 -0
  34. package/dist/src/multiValue/plugins/MultiValueTooltip.d.ts +1 -0
  35. package/dist/src/multiValue/plugins/RacingBars.d.ts +1 -0
  36. package/dist/src/multiValue/plugins/RacingCounterTexts.d.ts +3 -0
  37. package/dist/src/multiValue/plugins/RacingValueAxis.d.ts +1 -0
  38. package/dist/src/multiValue/plugins/Scatter.d.ts +3 -0
  39. package/dist/src/multiValue/plugins/ScatterBubbles.d.ts +3 -0
  40. package/dist/src/multiValue/plugins/XAxis.d.ts +1 -0
  41. package/dist/src/multiValue/plugins/XYAux.d.ts +3 -0
  42. package/dist/src/multiValue/plugins/XYAxes.d.ts +1 -0
  43. package/dist/src/multiValue/plugins/XYAxes_legacy.d.ts +3 -0
  44. package/dist/src/multiValue/plugins/XZoom.d.ts +1 -0
  45. package/dist/src/noneData/plugins/Container.d.ts +0 -1
  46. package/dist/src/noneData/plugins/Tooltip.d.ts +0 -3
  47. package/dist/src/relationship/defaults.d.ts +6 -0
  48. package/dist/src/relationship/index.d.ts +5 -0
  49. package/dist/src/relationship/plugins/ForceDirected.d.ts +3 -0
  50. package/dist/src/relationship/plugins/ForceDirectedBubbles.d.ts +3 -0
  51. package/dist/src/relationship/plugins/RelationshipLegend.d.ts +1 -0
  52. package/dist/src/relationship/plugins/RelationshipTooltip.d.ts +1 -0
  53. package/dist/src/relationship/relationshipObservables.d.ts +1 -0
  54. package/dist/src/series/seriesObservables.d.ts +3 -3
  55. package/dist/src/utils/commonUtils.d.ts +2 -1
  56. package/dist/src/utils/d3Graphics.d.ts +2 -1
  57. package/dist/src/utils/d3Utils.d.ts +3 -2
  58. package/dist/src/utils/observables.d.ts +1 -1
  59. package/dist/src/utils/orbchartsUtils.d.ts +3 -3
  60. package/lib/core-types.ts +7 -7
  61. package/lib/core.ts +6 -6
  62. package/lib/plugins-basic-types.ts +6 -6
  63. package/package.json +44 -44
  64. package/src/base/BaseBars.ts +765 -765
  65. package/src/base/BaseBarsTriangle.ts +676 -674
  66. package/src/base/BaseDots.ts +464 -515
  67. package/src/base/BaseGroupAxis.ts +691 -679
  68. package/src/base/BaseLegend.ts +684 -680
  69. package/src/base/BaseLineAreas.ts +629 -629
  70. package/src/base/BaseLines.ts +706 -706
  71. package/src/base/BaseRacingBars.ts +552 -0
  72. package/src/base/BaseRacingLabels.ts +396 -0
  73. package/src/base/BaseRacingValueLabels.ts +403 -0
  74. package/src/base/{BaseBarStack.ts → BaseStackedBars.ts} +782 -780
  75. package/src/base/BaseTooltip.ts +386 -385
  76. package/src/base/BaseValueAxis.ts +600 -580
  77. package/src/base/BaseXAxis.ts +427 -0
  78. package/src/base/BaseYAxis.ts +389 -0
  79. package/src/base/types.ts +2 -2
  80. package/src/const.ts +30 -30
  81. package/src/grid/defaults.ts +213 -244
  82. package/src/grid/gridObservables.ts +612 -545
  83. package/src/grid/index.ts +16 -16
  84. package/src/grid/plugins/Bars.ts +69 -69
  85. package/src/grid/plugins/BarsPN.ts +66 -66
  86. package/src/grid/plugins/BarsTriangle.ts +73 -73
  87. package/src/grid/plugins/Dots.ts +68 -68
  88. package/src/grid/plugins/GridLegend.ts +107 -107
  89. package/src/grid/plugins/GridTooltip.ts +66 -66
  90. package/src/grid/plugins/GroupAux.ts +1120 -1098
  91. package/src/grid/plugins/GroupAxis.ts +73 -97
  92. package/src/grid/plugins/{GridZoom.ts → GroupZoom.ts} +218 -218
  93. package/src/grid/plugins/LineAreas.ts +65 -65
  94. package/src/grid/plugins/Lines.ts +59 -59
  95. package/src/grid/plugins/{BarStack.ts → StackedBars.ts} +64 -64
  96. package/src/grid/plugins/{ValueStackAxis.ts → StackedValueAxis.ts} +96 -95
  97. package/src/grid/plugins/ValueAxis.ts +94 -93
  98. package/src/index.ts +6 -9
  99. package/src/multiGrid/defaults.ts +244 -224
  100. package/src/multiGrid/index.ts +15 -15
  101. package/src/multiGrid/multiGridObservables.ts +50 -49
  102. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  103. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  104. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  105. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -159
  106. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  107. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  108. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  109. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  110. package/src/multiGrid/plugins/{MultiBarStack.ts → MultiStackedBars.ts} +106 -106
  111. package/src/multiGrid/plugins/{MultiValueStackAxis.ts → MultiStackedValueAxis.ts} +134 -133
  112. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -133
  113. package/src/multiGrid/plugins/{OverlappingValueStackAxes.ts → OverlappingStackedValueAxes.ts} +300 -298
  114. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -299
  115. package/src/multiValue/defaults.ts +432 -0
  116. package/src/multiValue/index.ts +13 -0
  117. package/src/multiValue/multiValueObservables.ts +667 -0
  118. package/src/multiValue/plugins/MultiValueLegend.ts +107 -0
  119. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -0
  120. package/src/multiValue/plugins/RacingBars.ts +373 -0
  121. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -0
  122. package/src/multiValue/plugins/RacingValueAxis.ts +115 -0
  123. package/src/multiValue/plugins/RankingAxis_legacy.ts +109 -0
  124. package/src/multiValue/plugins/Scatter.ts +426 -0
  125. package/src/multiValue/plugins/ScatterBubbles.ts +554 -0
  126. package/src/multiValue/plugins/XAxis.ts +108 -0
  127. package/src/multiValue/plugins/XYAux.ts +683 -0
  128. package/src/multiValue/plugins/XYAxes.ts +195 -0
  129. package/src/multiValue/plugins/XYAxes_legacy.ts +684 -0
  130. package/src/multiValue/plugins/XZoom.ts +300 -0
  131. package/src/noneData/defaults.ts +102 -102
  132. package/src/noneData/index.ts +3 -3
  133. package/src/noneData/plugins/Container.ts +28 -28
  134. package/src/noneData/plugins/Tooltip.ts +374 -374
  135. package/src/relationship/defaults.ts +222 -0
  136. package/src/relationship/index.ts +5 -0
  137. package/src/relationship/plugins/ForceDirected.ts +1174 -0
  138. package/src/relationship/plugins/ForceDirectedBubbles.ts +1412 -0
  139. package/src/relationship/plugins/RelationshipLegend.ts +100 -0
  140. package/src/relationship/plugins/RelationshipTooltip.ts +66 -0
  141. package/src/relationship/relationshipObservables.ts +50 -0
  142. package/src/series/defaults.ts +221 -206
  143. package/src/series/index.ts +9 -9
  144. package/src/series/plugins/Bubbles.ts +636 -603
  145. package/src/series/plugins/Pie.ts +623 -623
  146. package/src/series/plugins/PieEventTexts.ts +284 -283
  147. package/src/series/plugins/PieLabels.ts +640 -640
  148. package/src/series/plugins/Rose.ts +516 -516
  149. package/src/series/plugins/RoseLabels.ts +600 -600
  150. package/src/series/plugins/SeriesLegend.ts +107 -107
  151. package/src/series/plugins/SeriesTooltip.ts +66 -66
  152. package/src/series/seriesObservables.ts +145 -145
  153. package/src/series/seriesUtils.ts +51 -51
  154. package/src/tree/defaults.ts +102 -78
  155. package/src/tree/index.ts +4 -4
  156. package/src/tree/plugins/TreeLegend.ts +100 -107
  157. package/src/tree/plugins/TreeMap.ts +341 -333
  158. package/src/tree/plugins/TreeTooltip.ts +66 -66
  159. package/src/utils/commonUtils.ts +31 -22
  160. package/src/utils/d3Graphics.ts +176 -174
  161. package/src/utils/d3Utils.ts +92 -73
  162. package/src/utils/observables.ts +14 -14
  163. package/src/utils/orbchartsUtils.ts +130 -100
  164. package/tsconfig.base.json +13 -13
  165. package/tsconfig.json +2 -2
  166. package/vite.config.js +22 -22
  167. package/dist/src/grid/plugins/BarStack.d.ts +0 -1
  168. package/dist/src/grid/plugins/GridZoom.d.ts +0 -1
  169. package/dist/src/grid/plugins/ValueStackAxis.d.ts +0 -1
  170. package/dist/src/multiGrid/plugins/MultiBarStack.d.ts +0 -1
  171. package/dist/src/multiGrid/plugins/MultiValueStackAxis.d.ts +0 -1
  172. package/dist/src/multiGrid/plugins/OverlappingValueStackAxes.d.ts +0 -1
  173. package/src/grid/plugins/RankingAxis.ts +0 -0
  174. package/src/multiValue/plugins/ScatterAxes.ts +0 -0
  175. package/src/relationship/plugins/Relationship.ts +0 -0
  176. /package/dist/src/{base/BaseGroupArea.d.ts → multiValue/plugins/OrdinalBubbles.d.ts} +0 -0
  177. /package/dist/src/{grid/plugins/Ranking.d.ts → multiValue/plugins/OrdinalXAxis.d.ts} +0 -0
  178. /package/dist/src/{grid/plugins/RankingAxis.d.ts → multiValue/plugins/RacingCounterAxis.d.ts} +0 -0
  179. /package/dist/src/multiValue/plugins/{ScatterAxes.d.ts → RankingAxis_legacy.d.ts} +0 -0
  180. /package/{dist/src/relationship/plugins/Relationship.d.ts → src/multiValue/plugins/OrdinalBubbles.ts} +0 -0
  181. /package/src/{base/BaseGroupArea.ts → multiValue/plugins/OrdinalXAxis.ts} +0 -0
  182. /package/src/{grid/plugins/Ranking.ts → multiValue/plugins/RacingCounterAxis.ts} +0 -0
@@ -1,604 +1,637 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- switchMap,
6
- first,
7
- takeUntil,
8
- Subject,
9
- Observable,
10
- distinctUntilChanged,
11
- shareReplay} from 'rxjs'
12
- import type { DefinePluginConfig } from '../../../lib/core-types'
13
- import type {
14
- ChartParams,
15
- DatumValue,
16
- DataSeries,
17
- EventName,
18
- ComputedDataSeries,
19
- ComputedDatumSeries,
20
- SeriesContainerPosition } from '../../../lib/core-types'
21
- import {
22
- defineSeriesPlugin } from '../../../lib/core'
23
- import type { BubblesParams, ArcScaleType } from '../../../lib/plugins-basic-types'
24
- import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
25
- import { renderCircleText } from '../../utils/d3Graphics'
26
- import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
27
-
28
- interface BubblesDatum extends ComputedDatumSeries {
29
- x: number
30
- y: number
31
- r: number
32
- _originR: number // 紀錄變化前的r
33
- }
34
-
35
- type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
36
-
37
- const pluginName = 'Bubbles'
38
-
39
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_BUBBLES_PARAMS> = {
40
- name: pluginName,
41
- defaultParams: DEFAULT_BUBBLES_PARAMS,
42
- layerIndex: LAYER_INDEX_OF_GRAPHIC,
43
- validator: (params, { validateColumns }) => {
44
- const result = validateColumns(params, {
45
- force: {
46
- toBeTypes: ['object']
47
- },
48
- bubbleText: {
49
- toBeTypes: ['object']
50
- },
51
- arcScaleType: {
52
- toBe: '"area" | "radius"',
53
- test: (value) => value === 'area' || value === 'radius'
54
- }
55
- })
56
- if (params.force) {
57
- const forceResult = validateColumns(params.force, {
58
- velocityDecay: {
59
- toBeTypes: ['number']
60
- },
61
- collisionSpacing: {
62
- toBeTypes: ['number']
63
- },
64
- strength: {
65
- toBeTypes: ['number']
66
- },
67
- })
68
- if (forceResult.status === 'error') {
69
- return forceResult
70
- }
71
- }
72
- if (params.bubbleText) {
73
- const bubbleTextResult = validateColumns(params.bubbleText, {
74
- fillRate: {
75
- toBeTypes: ['number']
76
- },
77
- lineHeight: {
78
- toBeTypes: ['number']
79
- },
80
- lineLengthMin: {
81
- toBeTypes: ['number']
82
- },
83
- })
84
- if (bubbleTextResult.status === 'error') {
85
- return bubbleTextResult
86
- }
87
- }
88
- return result
89
- }
90
- }
91
-
92
- let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
93
-
94
- function makeForce (bubblesSelection: d3.Selection<SVGGElement, any, any, any>, fullParams: BubblesParams) {
95
- return d3.forceSimulation()
96
- .velocityDecay(fullParams.force!.velocityDecay!)
97
- // .alphaDecay(0.2)
98
- .force(
99
- "collision",
100
- d3.forceCollide()
101
- .radius(d => {
102
- // @ts-ignore
103
- return d.r + fullParams.force!.collisionSpacing
104
- })
105
- // .strength(0.01)
106
- )
107
- .force("charge", d3.forceManyBody().strength((d) => {
108
- // @ts-ignore
109
- return - Math.pow(d.r, 2.0) * fullParams.force!.strength
110
- }))
111
- // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
112
- // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
113
- .on("tick", () => {
114
- // if (!bubblesSelection) {
115
- // return
116
- // }
117
- bubblesSelection
118
- .attr("transform", (d) => {
119
- return `translate(${d.x},${d.y})`
120
- })
121
- // .attr("cx", (d) => d.x)
122
- // .attr("cy", (d) => d.y)
123
- })
124
- }
125
-
126
-
127
- // // 計算最大泡泡的半徑
128
- // function getMaxR ({ data, totalR, maxValue, avgValue }: {
129
- // data: DatumValue[]
130
- // totalR: number
131
- // maxValue: number
132
- // avgValue: number
133
- // }) {
134
- // // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
135
- // const avgR = totalR / Math.sqrt(data.length)
136
- // const avgSize = avgR * avgR * Math.PI
137
- // const sizeRate = avgSize / avgValue
138
- // const maxSize = maxValue * sizeRate
139
- // const maxR = Math.pow(maxSize / Math.PI, 0.5)
140
-
141
- // const modifier = 0.785 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
142
- // return maxR * modifier
143
- // }
144
-
145
- function createBubblesData ({ visibleComputedLayoutData, LastBubbleDataMap, graphicWidth, graphicHeight, SeriesContainerPositionMap, scaleType }: {
146
- visibleComputedLayoutData: ComputedDataSeries
147
- LastBubbleDataMap: Map<string, BubblesDatum>
148
- graphicWidth: number
149
- graphicHeight: number
150
- SeriesContainerPositionMap: Map<string, SeriesContainerPosition>
151
- scaleType: ArcScaleType
152
- // highlightIds: string[]
153
- }): BubblesDatum[] {
154
- // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
155
- const totalR = Math.min(...[graphicWidth, graphicHeight]) / 2
156
-
157
- const data = visibleComputedLayoutData.flat()
158
-
159
- const totalValue = data.reduce((acc, current) => acc + current.value, 0)
160
-
161
- // 半徑比例尺
162
- const radiusScale = d3.scalePow()
163
- .domain([0, totalValue])
164
- .range([0, totalR])
165
- .exponent(scaleType === 'area'
166
- ? 0.5 // 數值映射面積(0.5為取平方根)
167
- : 1 // 數值映射半徑
168
- )
169
-
170
- // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
171
- const scaleFactor = scaleType === 'area'
172
- ? 1
173
- // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
174
- : (() => {
175
- const totalArea = totalR * totalR * Math.PI
176
- return Math.sqrt(totalArea / d3.sum(data, d => Math.PI * Math.pow(radiusScale(d.value), 2)))
177
- })()
178
-
179
- // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
180
- const adjustmentFactor = 0.9
181
-
182
- return data.map((_d) => {
183
- const d: BubblesDatum = _d as BubblesDatum
184
-
185
- const existDatum = LastBubbleDataMap.get(d.id)
186
-
187
- if (existDatum) {
188
- // 使用現有的座標
189
- d.x = existDatum.x
190
- d.y = existDatum.y
191
- } else {
192
- const seriesContainerPosition = SeriesContainerPositionMap.get(d.seriesLabel)!
193
- d.x = Math.random() * seriesContainerPosition.width
194
- d.y = Math.random() * seriesContainerPosition.height
195
- }
196
- const r = radiusScale!(d.value ?? 0)! * scaleFactor * adjustmentFactor
197
- d.r = r
198
- d._originR = r
199
-
200
- return d
201
- })
202
- }
203
-
204
- function renderBubbles ({ selection, bubblesData, fullParams, sumSeries }: {
205
- selection: d3.Selection<SVGGElement, any, any, any>
206
- bubblesData: BubblesDatum[]
207
- fullParams: BubblesParams
208
- sumSeries: boolean
209
- }) {
210
- const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
211
- .data(bubblesData, (d) => d.id)
212
- .join(
213
- enter => {
214
- const enterSelection = enter
215
- .append('g')
216
- .attr('cursor', 'pointer')
217
- .attr('font-size', 12)
218
- .style('fill', '#ffffff')
219
- .attr("text-anchor", "middle")
220
-
221
- enterSelection
222
- .append("circle")
223
- .attr("class", "node")
224
- .attr("cx", 0)
225
- .attr("cy", 0)
226
- // .attr("r", 1e-6)
227
- .attr('fill', (d) => d.color)
228
- // .transition()
229
- // .duration(500)
230
-
231
- enterSelection
232
- .append('text')
233
- .style('opacity', 0.8)
234
- .attr('pointer-events', 'none')
235
-
236
- return enterSelection
237
- },
238
- update => {
239
- return update
240
- },
241
- exit => {
242
- return exit
243
- .remove()
244
- }
245
- )
246
- .attr("transform", (d) => {
247
- return `translate(${d.x},${d.y})`
248
- })
249
-
250
- // 泡泡文字要使用的的資料欄位
251
- const textDataColumn = sumSeries ? 'seriesLabel' : 'label'// 如果有合併series則使用seriesLabel
252
-
253
- bubblesSelection.select('circle')
254
- .transition()
255
- .duration(200)
256
- .attr("r", (d) => d.r)
257
- .attr('fill', (d) => d.color)
258
- bubblesSelection
259
- .each((d,i,g) => {
260
- const gSelection = d3.select(g[i])
261
- let breakAll = true
262
- if (d[textDataColumn].length <= fullParams.bubbleText.lineLengthMin) {
263
- breakAll = false
264
- }
265
- gSelection.call(renderCircleText, {
266
- text: d[textDataColumn],
267
- radius: d.r * fullParams.bubbleText.fillRate,
268
- lineHeight: fullParams.bubbleText.lineHeight,
269
- isBreakAll: breakAll
270
- })
271
-
272
- })
273
-
274
- return bubblesSelection
275
- }
276
-
277
- function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
278
- data: BubblesDatum[]
279
- // fullParams: BubblesParams
280
- highlightRIncrease: number
281
- highlightIds: string[]
282
- }) {
283
- if (highlightRIncrease == 0) {
284
- return
285
- }
286
- if (!highlightIds.length) {
287
- data.forEach(d => d.r = d._originR)
288
- return
289
- }
290
- data.forEach(d => {
291
- if (highlightIds.includes(d.id)) {
292
- d.r = d._originR + highlightRIncrease
293
- } else {
294
- d.r = d._originR
295
- }
296
- })
297
- }
298
-
299
- function drag (): d3.DragBehavior<Element, unknown, unknown> {
300
- return d3.drag()
301
- .on("start", (event, d: any) => {
302
- if (!event.active) {
303
- force!.alpha(1).restart()
304
- }
305
- d.fx = d.x
306
- d.fy = d.y
307
- })
308
- .on("drag", (event, d: any) => {
309
- if (!event.active) {
310
- force!.alphaTarget(0)
311
- }
312
- d.fx = event.x
313
- d.fy = event.y
314
- })
315
- .on("end", (event, d: any) => {
316
- d.fx = null
317
- d.fy = null
318
- })
319
- }
320
-
321
-
322
- // private nodeTypePos (d: any) {
323
- // console.log(d)
324
- // console.log(this.TypeCenters.get(d.type)!)
325
- // const typeCenter = this.TypeCenters.get(d.type)!
326
- // return typeCenter ? typeCenter.x : 0
327
- // }
328
-
329
- function groupBubbles ({ fullParams, SeriesContainerPositionMap }: {
330
- fullParams: BubblesParams
331
- // graphicWidth: number
332
- // graphicHeight: number
333
- SeriesContainerPositionMap: Map<string, SeriesContainerPosition>
334
- }) {
335
- // console.log('groupBubbles')
336
- force!
337
- // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
338
- // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
339
- .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
340
- return SeriesContainerPositionMap.get(data.seriesLabel)!.centerX
341
- }))
342
- .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
343
- return SeriesContainerPositionMap.get(data.seriesLabel)!.centerY
344
- }))
345
-
346
- force!.alpha(1).restart()
347
- }
348
-
349
- function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
350
- bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
351
- fullChartParams: ChartParams
352
- highlightIds: string[]
353
- }) {
354
- bubblesSelection.interrupt('highlight')
355
-
356
- if (!highlightIds.length) {
357
- bubblesSelection
358
- .transition('highlight')
359
- .style('opacity', 1)
360
- return
361
- }
362
-
363
- bubblesSelection.each((d, i, n) => {
364
- const segment = d3.select(n[i])
365
-
366
- if (highlightIds.includes(d.id)) {
367
- segment
368
- .style('opacity', 1)
369
- .transition('highlight')
370
- .ease(d3.easeElastic)
371
- .duration(500)
372
- } else {
373
- // 取消放大
374
- segment
375
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
376
- }
377
- })
378
- }
379
-
380
-
381
- export const Bubbles = defineSeriesPlugin(pluginConfig)(({ selection, name, observer, subject }) => {
382
-
383
- const destroy$ = new Subject()
384
-
385
- // 紀錄前一次bubble data
386
- let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
387
-
388
-
389
- const sumSeries$ = observer.fullDataFormatter$.pipe(
390
- map(d => d.sumSeries),
391
- distinctUntilChanged()
392
- )
393
-
394
- const scaleType$ = observer.fullParams$.pipe(
395
- takeUntil(destroy$),
396
- map(d => d.arcScaleType),
397
- distinctUntilChanged()
398
- )
399
-
400
- const bubblesData$ = combineLatest({
401
- layout: observer.layout$,
402
- SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
403
- visibleComputedLayoutData: observer.visibleComputedLayoutData$,
404
- scaleType: scaleType$
405
- }).pipe(
406
- takeUntil(destroy$),
407
- switchMap(async (d) => d),
408
- map(data => {
409
- // console.log(data.visibleComputedLayoutData)
410
- return createBubblesData({
411
- visibleComputedLayoutData: data.visibleComputedLayoutData,
412
- LastBubbleDataMap,
413
- graphicWidth: data.layout.width,
414
- graphicHeight: data.layout.height,
415
- SeriesContainerPositionMap: data.SeriesContainerPositionMap,
416
- scaleType: data.scaleType
417
- })
418
- }),
419
- shareReplay(1)
420
- )
421
-
422
- // 紀錄前一次bubble data
423
- bubblesData$.subscribe(d => {
424
- LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
425
- })
426
-
427
- const highlightTarget$ = observer.fullChartParams$.pipe(
428
- takeUntil(destroy$),
429
- map(d => d.highlightTarget),
430
- distinctUntilChanged()
431
- )
432
-
433
- const bubblesSelection$ = combineLatest({
434
- bubblesData: bubblesData$,
435
- fullParams: observer.fullParams$,
436
- SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
437
- sumSeries: sumSeries$
438
- }).pipe(
439
- takeUntil(destroy$),
440
- switchMap(async (d) => d),
441
- map(data => {
442
- if (force) {
443
- force.stop()
444
- }
445
-
446
- const bubblesSelection = renderBubbles({
447
- selection,
448
- bubblesData: data.bubblesData,
449
- fullParams: data.fullParams,
450
- sumSeries: data.sumSeries
451
- })
452
-
453
- force = makeForce(bubblesSelection, data.fullParams)
454
-
455
- force.nodes(data.bubblesData)
456
-
457
- groupBubbles({
458
- fullParams: data.fullParams,
459
- SeriesContainerPositionMap: data.SeriesContainerPositionMap
460
- // graphicWidth: data.layout.width,
461
- // graphicHeight: data.layout.height
462
- })
463
-
464
- // setTimeout(() => {
465
- // force!.alphaTarget(0)
466
- // force!.alpha(1).restart()
467
- // }, 2000)
468
-
469
- return bubblesSelection
470
- })
471
- )
472
-
473
- combineLatest({
474
- bubblesSelection: bubblesSelection$,
475
- computedData: observer.computedData$,
476
- SeriesDataMap: observer.SeriesDataMap$,
477
- highlightTarget: highlightTarget$,
478
- }).pipe(
479
- takeUntil(destroy$),
480
- switchMap(async (d) => d)
481
- ).subscribe(data => {
482
-
483
- data.bubblesSelection
484
- .on('mouseover', (event, datum) => {
485
- // this.tooltip!.setDatum({
486
- // data: d,
487
- // x: d3.event.clientX,
488
- // y: d3.event.clientY
489
- // })
490
-
491
- subject.event$.next({
492
- type: 'series',
493
- eventName: 'mouseover',
494
- pluginName: name,
495
- highlightTarget: data.highlightTarget,
496
- datum,
497
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
498
- seriesIndex: datum.seriesIndex,
499
- seriesLabel: datum.seriesLabel,
500
- event,
501
- data: data.computedData
502
- })
503
- })
504
- .on('mousemove', (event, datum) => {
505
- // this.tooltip!.setDatum({
506
- // x: d3.event.clientX,
507
- // y: d3.event.clientY
508
- // })
509
-
510
- subject.event$.next({
511
- type: 'series',
512
- eventName: 'mousemove',
513
- pluginName: name,
514
- highlightTarget: data.highlightTarget,
515
- datum,
516
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
517
- seriesIndex: datum.seriesIndex,
518
- seriesLabel: datum.seriesLabel,
519
- event,
520
- data: data.computedData
521
- })
522
- })
523
- .on('mouseout', (event, datum) => {
524
- // this.tooltip!.remove()
525
-
526
- subject.event$.next({
527
- type: 'series',
528
- eventName: 'mouseout',
529
- pluginName: name,
530
- highlightTarget: data.highlightTarget,
531
- datum,
532
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
533
- seriesIndex: datum.seriesIndex,
534
- seriesLabel: datum.seriesLabel,
535
- event,
536
- data: data.computedData
537
- })
538
- })
539
- .on('click', (event, datum) => {
540
-
541
- subject.event$.next({
542
- type: 'series',
543
- eventName: 'click',
544
- pluginName: name,
545
- highlightTarget: data.highlightTarget,
546
- datum,
547
- series: data.SeriesDataMap.get(datum.seriesLabel)!,
548
- seriesIndex: datum.seriesIndex,
549
- seriesLabel: datum.seriesLabel,
550
- event,
551
- data: data.computedData
552
- })
553
- })
554
- .call(drag() as any)
555
-
556
-
557
- })
558
-
559
- combineLatest({
560
- bubblesSelection: bubblesSelection$,
561
- bubblesData: bubblesData$,
562
- highlight: observer.seriesHighlight$.pipe(
563
- map(data => data.map(d => d.id))
564
- ),
565
- fullChartParams: observer.fullChartParams$,
566
- fullParams: observer.fullParams$,
567
- sumSeries: sumSeries$,
568
- // layout: observer.layout$,
569
- SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
570
- }).pipe(
571
- takeUntil(destroy$),
572
- switchMap(async d => d)
573
- ).subscribe(data => {
574
- highlight({
575
- bubblesSelection: data.bubblesSelection,
576
- highlightIds: data.highlight,
577
- fullChartParams: data.fullChartParams
578
- })
579
-
580
- // if (data.fullParams.highlightRIncrease) {
581
- // setHighlightData ({
582
- // data: data.bubblesData,
583
- // highlightRIncrease: data.fullParams.highlightRIncrease,
584
- // highlightIds: data.highlight
585
- // })
586
- // data.bubblesSelection.select('circle')
587
- // // .transition()
588
- // // .duration(200)
589
- // .attr("r", (d) => d.r)
590
-
591
- // force!.nodes(data.bubblesData)
592
-
593
- // groupBubbles({
594
- // fullParams: data.fullParams,
595
- // SeriesContainerPositionMap: data.SeriesContainerPositionMap
596
- // })
597
- // }
598
-
599
- })
600
-
601
- return () => {
602
- destroy$.next(undefined)
603
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ switchMap,
6
+ first,
7
+ takeUntil,
8
+ debounceTime,
9
+ Subject,
10
+ Observable,
11
+ distinctUntilChanged,
12
+ shareReplay} from 'rxjs'
13
+ import type { DefinePluginConfig } from '../../../lib/core-types'
14
+ import type {
15
+ ChartParams,
16
+ DatumValue,
17
+ DataSeries,
18
+ EventName,
19
+ ComputedDataSeries,
20
+ ComputedDatumSeries,
21
+ ContainerPosition } from '../../../lib/core-types'
22
+ import {
23
+ defineSeriesPlugin } from '../../../lib/core'
24
+ import type { BubblesParams, ArcScaleType } from '../../../lib/plugins-basic-types'
25
+ import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
26
+ import { renderCircleText } from '../../utils/d3Graphics'
27
+ import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
28
+ import { getDatumColor } from '../../utils/orbchartsUtils'
29
+
30
+ interface BubblesDatum extends ComputedDatumSeries {
31
+ x: number
32
+ y: number
33
+ r: number
34
+ _originR: number // 紀錄變化前的r
35
+ }
36
+
37
+ type BubblesSimulationDatum = BubblesDatum & d3.SimulationNodeDatum
38
+
39
+ const pluginName = 'Bubbles'
40
+
41
+ const baseLineHeight = 12 // 未變形前的字體大小(代入計算用而已,數字多少都不會有影響)
42
+
43
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_BUBBLES_PARAMS> = {
44
+ name: pluginName,
45
+ defaultParams: DEFAULT_BUBBLES_PARAMS,
46
+ layerIndex: LAYER_INDEX_OF_GRAPHIC,
47
+ validator: (params, { validateColumns }) => {
48
+ const result = validateColumns(params, {
49
+ force: {
50
+ toBeTypes: ['object']
51
+ },
52
+ bubbleLabel: {
53
+ toBeTypes: ['object']
54
+ },
55
+ arcScaleType: {
56
+ toBe: '"area" | "radius"',
57
+ test: (value) => value === 'area' || value === 'radius'
58
+ }
59
+ })
60
+ if (params.force) {
61
+ const forceResult = validateColumns(params.force, {
62
+ velocityDecay: {
63
+ toBeTypes: ['number']
64
+ },
65
+ collisionSpacing: {
66
+ toBeTypes: ['number']
67
+ },
68
+ strength: {
69
+ toBeTypes: ['number']
70
+ },
71
+ })
72
+ if (forceResult.status === 'error') {
73
+ return forceResult
74
+ }
75
+ }
76
+ if (params.bubbleLabel) {
77
+ const bubbleLabelResult = validateColumns(params.bubbleLabel, {
78
+ colorType: {
79
+ toBeOption: 'ColorType'
80
+ },
81
+ fillRate: {
82
+ toBeTypes: ['number']
83
+ },
84
+ lineHeight: {
85
+ toBeTypes: ['number']
86
+ },
87
+ maxLineLength: {
88
+ toBeTypes: ['number']
89
+ },
90
+ })
91
+ if (bubbleLabelResult.status === 'error') {
92
+ return bubbleLabelResult
93
+ }
94
+ }
95
+ return result
96
+ }
97
+ }
98
+
99
+
100
+ // let isRunning = false
101
+
102
+ function createSimulation (bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>, fullParams: BubblesParams) {
103
+ return d3.forceSimulation()
104
+ .velocityDecay(fullParams.force!.velocityDecay!)
105
+ // .alphaDecay(0.2)
106
+ .force(
107
+ "collision",
108
+ d3.forceCollide()
109
+ .radius((d: d3.SimulationNodeDatum & BubblesDatum) => {
110
+ return d.r + fullParams.force!.collisionSpacing
111
+ })
112
+ // .strength(0.01)
113
+ )
114
+ .force("charge", d3.forceManyBody().strength((d: d3.SimulationNodeDatum & BubblesDatum) => {
115
+ return - Math.pow(d.r, 2.0) * fullParams.force!.strength
116
+ }))
117
+ // .force("charge", d3.forceManyBody().strength(-2000))
118
+ // .force("collision", d3.forceCollide(60).strength(1)) // @Q@ 60為泡泡的R,暫時是先寫死的
119
+ // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
120
+ // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
121
+ .on("tick", () => {
122
+ // if (!bubblesSelection) {
123
+ // return
124
+ // }
125
+ bubblesSelection
126
+ .attr("transform", (d) => {
127
+ return `translate(${d.x},${d.y})`
128
+ })
129
+ // .attr("cx", (d) => d.x)
130
+ // .attr("cy", (d) => d.y)
131
+
132
+
133
+ })
134
+ // .on("end", () => {
135
+
136
+ // })
137
+
138
+ }
139
+
140
+
141
+ // // 計算最大泡泡的半徑
142
+ // function getMaxR ({ data, totalR, maxValue, avgValue }: {
143
+ // data: DatumValue[]
144
+ // totalR: number
145
+ // maxValue: number
146
+ // avgValue: number
147
+ // }) {
148
+ // // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
149
+ // const avgR = totalR / Math.sqrt(data.length)
150
+ // const avgSize = avgR * avgR * Math.PI
151
+ // const sizeRate = avgSize / avgValue
152
+ // const maxSize = maxValue * sizeRate
153
+ // const maxR = Math.pow(maxSize / Math.PI, 0.5)
154
+
155
+ // const modifier = 0.785 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
156
+ // return maxR * modifier
157
+ // }
158
+
159
+ function createBubblesData ({ visibleComputedSortedData, LastBubbleDataMap, graphicWidth, graphicHeight, SeriesContainerPositionMap, scaleType }: {
160
+ visibleComputedSortedData: ComputedDataSeries
161
+ LastBubbleDataMap: Map<string, BubblesDatum>
162
+ graphicWidth: number
163
+ graphicHeight: number
164
+ SeriesContainerPositionMap: Map<string, ContainerPosition>
165
+ scaleType: ArcScaleType
166
+ // highlightIds: string[]
167
+ }): BubblesDatum[] {
168
+ // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
169
+ const totalR = Math.min(...[graphicWidth, graphicHeight]) / 2
170
+
171
+ const data = visibleComputedSortedData.flat()
172
+
173
+ const totalValue = data.reduce((acc, current) => acc + current.value, 0)
174
+
175
+ // 半徑比例尺
176
+ const radiusScale = d3.scalePow()
177
+ .domain([0, totalValue])
178
+ .range([0, totalR])
179
+ .exponent(scaleType === 'area'
180
+ ? 0.5 // 數值映射面積(0.5為取平方根)
181
+ : 1 // 數值映射半徑
182
+ )
183
+
184
+ // 縮放比例 - 確保多個小圓的總面積等於大圓的面積
185
+ const scaleFactor = scaleType === 'area'
186
+ ? 1
187
+ // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
188
+ : (() => {
189
+ const totalArea = totalR * totalR * Math.PI
190
+ return Math.sqrt(totalArea / d3.sum(data, d => Math.PI * Math.pow(radiusScale(d.value), 2)))
191
+ })()
192
+
193
+ // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
194
+ const adjustmentFactor = 0.9
195
+
196
+ return data.map((_d) => {
197
+ const d: BubblesDatum = _d as BubblesDatum
198
+
199
+ const existDatum = LastBubbleDataMap.get(d.id)
200
+
201
+ if (existDatum) {
202
+ // 使用現有的座標
203
+ d.x = existDatum.x
204
+ d.y = existDatum.y
205
+ } else {
206
+ const seriesContainerPosition = SeriesContainerPositionMap.get(d.seriesLabel)!
207
+ d.x = Math.random() * seriesContainerPosition.width
208
+ d.y = Math.random() * seriesContainerPosition.height
209
+ }
210
+ const r = radiusScale!(d.value ?? 0)! * scaleFactor * adjustmentFactor
211
+ d.r = r
212
+ d._originR = r
213
+
214
+ return d
215
+ })
216
+ }
217
+
218
+ function renderBubbles ({ selection, bubblesData, fullParams, fullChartParams, sumSeries }: {
219
+ selection: d3.Selection<SVGGElement, any, any, any>
220
+ bubblesData: BubblesDatum[]
221
+ fullParams: BubblesParams
222
+ fullChartParams: ChartParams
223
+ sumSeries: boolean
224
+ }) {
225
+ const bubblesSelection = selection.selectAll<SVGGElement, BubblesDatum>("g")
226
+ .data(bubblesData, (d) => d.id)
227
+ .join(
228
+ enter => {
229
+ const enterSelection = enter
230
+ .append('g')
231
+ .attr('cursor', 'pointer')
232
+ .attr('font-size', baseLineHeight)
233
+ .style('fill', '#ffffff')
234
+ .attr("text-anchor", "middle")
235
+
236
+ enterSelection
237
+ .append("circle")
238
+ .attr("class", "node")
239
+ .attr("cx", 0)
240
+ .attr("cy", 0)
241
+ // .attr("r", 1e-6)
242
+ .attr('fill', (d) => d.color)
243
+ // .transition()
244
+ // .duration(500)
245
+
246
+ enterSelection
247
+ .append('text')
248
+ .style('opacity', 0.8)
249
+ .attr('pointer-events', 'none')
250
+
251
+ return enterSelection
252
+ },
253
+ update => {
254
+ return update
255
+ },
256
+ exit => {
257
+ return exit
258
+ .remove()
259
+ }
260
+ )
261
+ .attr("transform", (d) => {
262
+ return `translate(${d.x},${d.y})`
263
+ })
264
+
265
+ // 泡泡文字要使用的的資料欄位
266
+ const textDataColumn = sumSeries ? 'seriesLabel' : 'label'// 如果有合併series則使用seriesLabel
267
+
268
+ bubblesSelection.select('circle')
269
+ .transition()
270
+ .duration(200)
271
+ .attr("r", (d) => d.r)
272
+ .attr('fill', (d) => d.color)
273
+ bubblesSelection
274
+ .each((d,i,g) => {
275
+ const gSelection = d3.select(g[i])
276
+ const text = d[textDataColumn] ?? ''
277
+
278
+ gSelection.call(renderCircleText, {
279
+ text,
280
+ radius: d.r * fullParams.bubbleLabel.fillRate,
281
+ lineHeight: baseLineHeight * fullParams.bubbleLabel.lineHeight,
282
+ isBreakAll: text.length <= fullParams.bubbleLabel.maxLineLength
283
+ ? false
284
+ : fullParams.bubbleLabel.wordBreakAll
285
+ })
286
+
287
+ // -- text color --
288
+ gSelection.select('text').attr('fill', _ => getDatumColor({
289
+ datum: d,
290
+ colorType: fullParams.bubbleLabel.colorType,
291
+ fullChartParams: fullChartParams
292
+ }))
293
+
294
+ })
295
+
296
+ return bubblesSelection
297
+ }
298
+
299
+ function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
300
+ data: BubblesDatum[]
301
+ // fullParams: BubblesParams
302
+ highlightRIncrease: number
303
+ highlightIds: string[]
304
+ }) {
305
+ if (highlightRIncrease == 0) {
306
+ return
307
+ }
308
+ if (!highlightIds.length) {
309
+ data.forEach(d => d.r = d._originR)
310
+ return
311
+ }
312
+ data.forEach(d => {
313
+ if (highlightIds.includes(d.id)) {
314
+ d.r = d._originR + highlightRIncrease
315
+ } else {
316
+ d.r = d._originR
317
+ }
318
+ })
319
+ }
320
+
321
+ function drag (_simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>): d3.DragBehavior<Element, unknown, unknown> {
322
+ return d3.drag()
323
+ .on("start", (event, d: any) => {
324
+ if (!event.active) {
325
+ _simulation!.alpha(1).restart()
326
+ }
327
+ d.fx = d.x
328
+ d.fy = d.y
329
+ })
330
+ .on("drag", (event, d: any) => {
331
+ if (!event.active) {
332
+ _simulation!.alphaTarget(0)
333
+ }
334
+ d.fx = event.x
335
+ d.fy = event.y
336
+ })
337
+ .on("end", (event, d: any) => {
338
+ d.fx = null
339
+ d.fy = null
340
+ _simulation!.alpha(1).restart()
341
+ })
342
+ }
343
+
344
+
345
+ // private nodeTypePos (d: any) {
346
+ // console.log(d)
347
+ // console.log(this.TypeCenters.get(d.type)!)
348
+ // const typeCenter = this.TypeCenters.get(d.type)!
349
+ // return typeCenter ? typeCenter.x : 0
350
+ // }
351
+
352
+ function groupBubbles ({ _simulation, fullParams, SeriesContainerPositionMap }: {
353
+ _simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>
354
+ fullParams: BubblesParams
355
+ // graphicWidth: number
356
+ // graphicHeight: number
357
+ SeriesContainerPositionMap: Map<string, ContainerPosition>
358
+ }) {
359
+ // console.log('groupBubbles')
360
+ _simulation!
361
+ // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
362
+ // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
363
+ .force('x', d3.forceX().strength(fullParams.force.strength).x((data: BubblesSimulationDatum) => {
364
+ return SeriesContainerPositionMap.get(data.seriesLabel)!.centerX
365
+ }))
366
+ .force('y', d3.forceY().strength(fullParams.force.strength).y((data: BubblesSimulationDatum) => {
367
+ return SeriesContainerPositionMap.get(data.seriesLabel)!.centerY
368
+ }))
369
+
370
+ // force!.alpha(1).restart()
371
+ }
372
+
373
+ function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
374
+ bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
375
+ fullChartParams: ChartParams
376
+ highlightIds: string[]
377
+ }) {
378
+ bubblesSelection.interrupt('highlight')
379
+
380
+ if (!highlightIds.length) {
381
+ bubblesSelection
382
+ .transition('highlight')
383
+ .style('opacity', 1)
384
+ return
385
+ }
386
+
387
+ bubblesSelection.each((d, i, n) => {
388
+ const segment = d3.select(n[i])
389
+
390
+ if (highlightIds.includes(d.id)) {
391
+ segment
392
+ .style('opacity', 1)
393
+ .transition('highlight')
394
+ .ease(d3.easeElastic)
395
+ .duration(500)
396
+ } else {
397
+ // 取消放大
398
+ segment
399
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
400
+ }
401
+ })
402
+ }
403
+
404
+
405
+ export const Bubbles = defineSeriesPlugin(pluginConfig)(({ selection, name, observer, subject }) => {
406
+
407
+ const destroy$ = new Subject()
408
+
409
+ let simulation: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
410
+
411
+ // 紀錄前一次bubble data
412
+ let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
413
+
414
+
415
+ const sumSeries$ = observer.fullDataFormatter$.pipe(
416
+ map(d => d.sumSeries),
417
+ distinctUntilChanged()
418
+ )
419
+
420
+ const scaleType$ = observer.fullParams$.pipe(
421
+ takeUntil(destroy$),
422
+ map(d => d.arcScaleType),
423
+ distinctUntilChanged()
424
+ )
425
+
426
+ const bubblesData$ = combineLatest({
427
+ layout: observer.layout$,
428
+ SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
429
+ visibleComputedSortedData: observer.visibleComputedSortedData$,
430
+ scaleType: scaleType$
431
+ }).pipe(
432
+ takeUntil(destroy$),
433
+ switchMap(async (d) => d),
434
+ map(data => {
435
+ // console.log(data.visibleComputedSortedData)
436
+ return createBubblesData({
437
+ visibleComputedSortedData: data.visibleComputedSortedData,
438
+ LastBubbleDataMap,
439
+ graphicWidth: data.layout.width,
440
+ graphicHeight: data.layout.height,
441
+ SeriesContainerPositionMap: data.SeriesContainerPositionMap,
442
+ scaleType: data.scaleType
443
+ })
444
+ }),
445
+ shareReplay(1)
446
+ )
447
+
448
+ // 紀錄前一次bubble data
449
+ bubblesData$.subscribe(d => {
450
+ LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
451
+ })
452
+
453
+ const highlightTarget$ = observer.fullChartParams$.pipe(
454
+ takeUntil(destroy$),
455
+ map(d => d.highlightTarget),
456
+ distinctUntilChanged()
457
+ )
458
+
459
+ const bubblesSelection$ = combineLatest({
460
+ bubblesData: bubblesData$,
461
+ fullParams: observer.fullParams$,
462
+ fullChartParams: observer.fullChartParams$,
463
+ SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
464
+ sumSeries: sumSeries$
465
+ }).pipe(
466
+ takeUntil(destroy$),
467
+ switchMap(async (d) => d),
468
+ map(data => {
469
+ if (simulation) {
470
+ // 先停止,重新計算之後再restart
471
+ simulation.stop()
472
+ }
473
+
474
+ const bubblesSelection = renderBubbles({
475
+ selection,
476
+ bubblesData: data.bubblesData,
477
+ fullParams: data.fullParams,
478
+ fullChartParams: data.fullChartParams,
479
+ sumSeries: data.sumSeries
480
+ })
481
+
482
+ simulation = createSimulation(bubblesSelection, data.fullParams)
483
+
484
+ simulation.nodes(data.bubblesData)
485
+
486
+ groupBubbles({
487
+ _simulation: simulation,
488
+ fullParams: data.fullParams,
489
+ SeriesContainerPositionMap: data.SeriesContainerPositionMap
490
+ // graphicWidth: data.layout.width,
491
+ // graphicHeight: data.layout.height
492
+ })
493
+
494
+ simulation!.alpha(1).restart()
495
+
496
+ return bubblesSelection
497
+ }),
498
+ shareReplay(1)
499
+ )
500
+
501
+ combineLatest({
502
+ bubblesSelection: bubblesSelection$,
503
+ computedData: observer.computedData$,
504
+ SeriesDataMap: observer.SeriesDataMap$,
505
+ highlightTarget: highlightTarget$,
506
+ }).pipe(
507
+ takeUntil(destroy$),
508
+ switchMap(async (d) => d)
509
+ ).subscribe(data => {
510
+
511
+ data.bubblesSelection
512
+ .on('mouseover', (event, datum) => {
513
+ // this.tooltip!.setDatum({
514
+ // data: d,
515
+ // x: d3.event.clientX,
516
+ // y: d3.event.clientY
517
+ // })
518
+
519
+ subject.event$.next({
520
+ type: 'series',
521
+ eventName: 'mouseover',
522
+ pluginName: name,
523
+ highlightTarget: data.highlightTarget,
524
+ datum,
525
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
526
+ seriesIndex: datum.seriesIndex,
527
+ seriesLabel: datum.seriesLabel,
528
+ event,
529
+ data: data.computedData
530
+ })
531
+ })
532
+ .on('mousemove', (event, datum) => {
533
+ // this.tooltip!.setDatum({
534
+ // x: d3.event.clientX,
535
+ // y: d3.event.clientY
536
+ // })
537
+
538
+ subject.event$.next({
539
+ type: 'series',
540
+ eventName: 'mousemove',
541
+ pluginName: name,
542
+ highlightTarget: data.highlightTarget,
543
+ datum,
544
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
545
+ seriesIndex: datum.seriesIndex,
546
+ seriesLabel: datum.seriesLabel,
547
+ event,
548
+ data: data.computedData
549
+ })
550
+ })
551
+ .on('mouseout', (event, datum) => {
552
+ // this.tooltip!.remove()
553
+
554
+ subject.event$.next({
555
+ type: 'series',
556
+ eventName: 'mouseout',
557
+ pluginName: name,
558
+ highlightTarget: data.highlightTarget,
559
+ datum,
560
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
561
+ seriesIndex: datum.seriesIndex,
562
+ seriesLabel: datum.seriesLabel,
563
+ event,
564
+ data: data.computedData
565
+ })
566
+ })
567
+ .on('click', (event, datum) => {
568
+
569
+ subject.event$.next({
570
+ type: 'series',
571
+ eventName: 'click',
572
+ pluginName: name,
573
+ highlightTarget: data.highlightTarget,
574
+ datum,
575
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
576
+ seriesIndex: datum.seriesIndex,
577
+ seriesLabel: datum.seriesLabel,
578
+ event,
579
+ data: data.computedData
580
+ })
581
+ })
582
+ .call(drag(simulation) as any)
583
+
584
+
585
+ })
586
+
587
+ combineLatest({
588
+ bubblesSelection: bubblesSelection$,
589
+ // bubblesData: bubblesData$,
590
+ highlight: observer.seriesHighlight$.pipe(
591
+ map(data => data.map(d => d.id))
592
+ ),
593
+ fullChartParams: observer.fullChartParams$,
594
+ // fullParams: observer.fullParams$,
595
+ // sumSeries: sumSeries$,
596
+ // // layout: observer.layout$,
597
+ // SeriesContainerPositionMap: observer.SeriesContainerPositionMap$,
598
+ }).pipe(
599
+ takeUntil(destroy$),
600
+ switchMap(async d => d)
601
+ ).subscribe(data => {
602
+ highlight({
603
+ bubblesSelection: data.bubblesSelection,
604
+ highlightIds: data.highlight,
605
+ fullChartParams: data.fullChartParams
606
+ })
607
+
608
+ // if (data.fullParams.highlightRIncrease) {
609
+ // setHighlightData ({
610
+ // data: data.bubblesData,
611
+ // highlightRIncrease: data.fullParams.highlightRIncrease,
612
+ // highlightIds: data.highlight
613
+ // })
614
+ // data.bubblesSelection.select('circle')
615
+ // // .transition()
616
+ // // .duration(200)
617
+ // .attr("r", (d) => d.r)
618
+
619
+ // force!.nodes(data.bubblesData)
620
+
621
+ // groupBubbles({
622
+ // fullParams: data.fullParams,
623
+ // SeriesContainerPositionMap: data.SeriesContainerPositionMap
624
+ // })
625
+ // }
626
+
627
+ })
628
+
629
+
630
+ return () => {
631
+ destroy$.next(undefined)
632
+ if (simulation) {
633
+ simulation.stop()
634
+ simulation = undefined
635
+ }
636
+ }
604
637
  })