@orbcharts/plugins-basic 3.0.0-alpha.49 → 3.0.0-alpha.51

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