@orbcharts/plugins-basic 3.0.0-alpha.60 → 3.0.0-alpha.62

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +1 -1
  3. package/dist/orbcharts-plugins-basic.umd.js +1 -1
  4. package/package.json +42 -42
  5. package/src/base/BaseBarStack.ts +778 -778
  6. package/src/base/BaseBars.ts +764 -764
  7. package/src/base/BaseBarsTriangle.ts +672 -672
  8. package/src/base/BaseDots.ts +513 -513
  9. package/src/base/BaseGroupAxis.ts +558 -558
  10. package/src/base/BaseLegend.ts +641 -641
  11. package/src/base/BaseLineAreas.ts +628 -628
  12. package/src/base/BaseLines.ts +704 -704
  13. package/src/base/BaseValueAxis.ts +480 -478
  14. package/src/base/types.ts +2 -2
  15. package/src/grid/defaults.ts +128 -128
  16. package/src/grid/gridObservables.ts +541 -541
  17. package/src/grid/index.ts +15 -15
  18. package/src/grid/plugins/BarStack.ts +43 -43
  19. package/src/grid/plugins/Bars.ts +44 -44
  20. package/src/grid/plugins/BarsPN.ts +41 -41
  21. package/src/grid/plugins/BarsTriangle.ts +42 -42
  22. package/src/grid/plugins/Dots.ts +37 -37
  23. package/src/grid/plugins/GridLegend.ts +59 -59
  24. package/src/grid/plugins/GroupAux.ts +976 -976
  25. package/src/grid/plugins/GroupAxis.ts +35 -35
  26. package/src/grid/plugins/LineAreas.ts +40 -40
  27. package/src/grid/plugins/Lines.ts +40 -40
  28. package/src/grid/plugins/ScalingArea.ts +173 -173
  29. package/src/grid/plugins/ValueAxis.ts +36 -36
  30. package/src/grid/plugins/ValueStackAxis.ts +38 -38
  31. package/src/grid/types.ts +123 -123
  32. package/src/index.ts +9 -9
  33. package/src/multiGrid/defaults.ts +158 -158
  34. package/src/multiGrid/index.ts +13 -13
  35. package/src/multiGrid/multiGridObservables.ts +49 -49
  36. package/src/multiGrid/plugins/MultiBarStack.ts +78 -78
  37. package/src/multiGrid/plugins/MultiBars.ts +77 -77
  38. package/src/multiGrid/plugins/MultiBarsTriangle.ts +77 -77
  39. package/src/multiGrid/plugins/MultiDots.ts +65 -65
  40. package/src/multiGrid/plugins/MultiGridLegend.ts +89 -89
  41. package/src/multiGrid/plugins/MultiGroupAxis.ts +69 -69
  42. package/src/multiGrid/plugins/MultiLineAreas.ts +77 -77
  43. package/src/multiGrid/plugins/MultiLines.ts +77 -77
  44. package/src/multiGrid/plugins/MultiValueAxis.ts +69 -69
  45. package/src/multiGrid/plugins/MultiValueStackAxis.ts +69 -69
  46. package/src/multiGrid/plugins/OverlappingValueAxes.ts +167 -167
  47. package/src/multiGrid/plugins/OverlappingValueStackAxes.ts +168 -168
  48. package/src/multiGrid/types.ts +72 -72
  49. package/src/noneData/defaults.ts +102 -102
  50. package/src/noneData/index.ts +3 -3
  51. package/src/noneData/plugins/Container.ts +10 -10
  52. package/src/noneData/plugins/Tooltip.ts +310 -310
  53. package/src/noneData/types.ts +26 -26
  54. package/src/series/defaults.ts +144 -144
  55. package/src/series/index.ts +9 -9
  56. package/src/series/plugins/Bubbles.ts +545 -545
  57. package/src/series/plugins/Pie.ts +576 -576
  58. package/src/series/plugins/PieEventTexts.ts +262 -262
  59. package/src/series/plugins/PieLabels.ts +304 -304
  60. package/src/series/plugins/Rose.ts +472 -472
  61. package/src/series/plugins/RoseLabels.ts +362 -362
  62. package/src/series/plugins/SeriesLegend.ts +59 -59
  63. package/src/series/seriesObservables.ts +145 -145
  64. package/src/series/seriesUtils.ts +51 -51
  65. package/src/series/types.ts +83 -83
  66. package/src/tree/defaults.ts +23 -23
  67. package/src/tree/index.ts +3 -3
  68. package/src/tree/plugins/TreeLegend.ts +59 -59
  69. package/src/tree/plugins/TreeMap.ts +305 -305
  70. package/src/tree/types.ts +23 -23
  71. package/src/utils/commonUtils.ts +21 -21
  72. package/src/utils/d3Graphics.ts +124 -124
  73. package/src/utils/d3Utils.ts +73 -73
  74. package/src/utils/observables.ts +14 -14
  75. package/src/utils/orbchartsUtils.ts +100 -100
  76. package/tsconfig.base.json +13 -13
  77. package/tsconfig.json +2 -2
  78. 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
  })