@orbcharts/plugins-basic 3.0.0-alpha.68 → 3.0.0-alpha.70

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