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

Sign up to get free protection for your applications and to get access to all the features.
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
  })