@orbcharts/plugins-basic 3.0.0-alpha.42 → 3.0.0-alpha.43

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +5477 -5426
  3. package/dist/orbcharts-plugins-basic.umd.js +8 -8
  4. package/dist/src/base/BaseBarStack.d.ts +1 -1
  5. package/dist/src/base/BaseBars.d.ts +1 -1
  6. package/dist/src/base/BaseBarsTriangle.d.ts +1 -1
  7. package/dist/src/base/BaseDots.d.ts +1 -1
  8. package/dist/src/base/BaseLineAreas.d.ts +1 -1
  9. package/dist/src/base/BaseLines.d.ts +1 -1
  10. package/dist/src/multiGrid/defaults.d.ts +2 -1
  11. package/dist/src/multiGrid/index.d.ts +1 -0
  12. package/dist/src/multiGrid/plugins/MultiLineAreas.d.ts +1 -0
  13. package/dist/src/multiGrid/types.d.ts +4 -4
  14. package/package.json +42 -42
  15. package/src/base/BaseBarStack.ts +881 -879
  16. package/src/base/BaseBars.ts +750 -748
  17. package/src/base/BaseBarsTriangle.ts +659 -657
  18. package/src/base/BaseDots.ts +639 -637
  19. package/src/base/BaseGroupAxis.ts +496 -496
  20. package/src/base/BaseLegend.ts +636 -636
  21. package/src/base/BaseLineAreas.ts +621 -624
  22. package/src/base/BaseLines.ts +692 -695
  23. package/src/base/BaseValueAxis.ts +479 -479
  24. package/src/base/types.ts +2 -2
  25. package/src/grid/defaults.ts +121 -121
  26. package/src/grid/gridObservables.ts +263 -263
  27. package/src/grid/index.ts +15 -15
  28. package/src/grid/plugins/BarStack.ts +37 -37
  29. package/src/grid/plugins/Bars.ts +37 -37
  30. package/src/grid/plugins/BarsDiverging.ts +39 -39
  31. package/src/grid/plugins/BarsTriangle.ts +34 -34
  32. package/src/grid/plugins/Dots.ts +35 -35
  33. package/src/grid/plugins/GridLegend.ts +58 -58
  34. package/src/grid/plugins/GroupAux.ts +643 -643
  35. package/src/grid/plugins/GroupAxis.ts +30 -30
  36. package/src/grid/plugins/LineAreas.ts +36 -36
  37. package/src/grid/plugins/Lines.ts +35 -35
  38. package/src/grid/plugins/ScalingArea.ts +174 -174
  39. package/src/grid/plugins/ValueAxis.ts +31 -31
  40. package/src/grid/plugins/ValueStackAxis.ts +70 -70
  41. package/src/grid/types.ts +120 -120
  42. package/src/index.ts +9 -9
  43. package/src/multiGrid/defaults.ts +147 -140
  44. package/src/multiGrid/index.ts +11 -10
  45. package/src/multiGrid/multiGridObservables.ts +289 -278
  46. package/src/multiGrid/plugins/MultiBarStack.ts +60 -60
  47. package/src/multiGrid/plugins/MultiBars.ts +59 -59
  48. package/src/multiGrid/plugins/MultiBarsTriangle.ts +58 -58
  49. package/src/multiGrid/plugins/MultiDots.ts +58 -58
  50. package/src/multiGrid/plugins/MultiGridLegend.ts +88 -88
  51. package/src/multiGrid/plugins/MultiGroupAxis.ts +53 -53
  52. package/src/multiGrid/plugins/MultiLineAreas.ts +59 -0
  53. package/src/multiGrid/plugins/MultiLines.ts +58 -58
  54. package/src/multiGrid/plugins/MultiValueAxis.ts +53 -53
  55. package/src/multiGrid/plugins/OverlappingValueAxes.ts +164 -165
  56. package/src/multiGrid/types.ts +67 -67
  57. package/src/noneData/defaults.ts +61 -61
  58. package/src/noneData/index.ts +3 -3
  59. package/src/noneData/plugins/Container.ts +10 -10
  60. package/src/noneData/plugins/Tooltip.ts +304 -304
  61. package/src/noneData/types.ts +26 -26
  62. package/src/series/defaults.ts +99 -99
  63. package/src/series/index.ts +6 -6
  64. package/src/series/plugins/Bubbles.ts +551 -549
  65. package/src/series/plugins/Pie.ts +600 -598
  66. package/src/series/plugins/PieEventTexts.ts +194 -194
  67. package/src/series/plugins/PieLabels.ts +288 -285
  68. package/src/series/plugins/SeriesLegend.ts +58 -58
  69. package/src/series/seriesUtils.ts +50 -50
  70. package/src/series/types.ts +67 -67
  71. package/src/tree/defaults.ts +22 -22
  72. package/src/tree/index.ts +3 -3
  73. package/src/tree/plugins/TreeLegend.ts +58 -58
  74. package/src/tree/plugins/TreeMap.ts +302 -300
  75. package/src/tree/types.ts +23 -23
  76. package/src/utils/commonUtils.ts +21 -21
  77. package/src/utils/d3Graphics.ts +124 -124
  78. package/src/utils/d3Utils.ts +73 -73
  79. package/src/utils/observables.ts +14 -14
  80. package/src/utils/orbchartsUtils.ts +100 -100
  81. package/tsconfig.dev.json +16 -16
  82. package/tsconfig.json +13 -13
  83. package/tsconfig.prod.json +13 -13
  84. package/vite.config.js +49 -49
@@ -1,550 +1,552 @@
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 } from '@orbcharts/core'
18
- import {
19
- defineSeriesPlugin } from '@orbcharts/core'
20
- import type { BubblesParams, BubbleScaleType } from '../types'
21
- import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
22
- import { renderCircleText } from '../../utils/d3Graphics'
23
-
24
- interface BubblesDatum extends ComputedDatumSeries {
25
- x: number
26
- y: number
27
- r: number
28
- _originR: number // 紀錄變化前的r
29
- }
30
-
31
- let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
32
-
33
- function makeForce (bubblesSelection: d3.Selection<SVGGElement, any, any, any>, fullParams: BubblesParams) {
34
- return d3.forceSimulation()
35
- .velocityDecay(fullParams.force!.velocityDecay!)
36
- // .alphaDecay(0.2)
37
- .force(
38
- "collision",
39
- d3.forceCollide()
40
- .radius(d => {
41
- // @ts-ignore
42
- return d.r + fullParams.force!.collisionSpacing
43
- })
44
- // .strength(0.01)
45
- )
46
- .force("charge", d3.forceManyBody().strength((d) => {
47
- // @ts-ignore
48
- return - Math.pow(d.r, 2.0) * fullParams.force!.strength
49
- }))
50
- // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
51
- // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
52
- .on("tick", () => {
53
- // if (!bubblesSelection) {
54
- // return
55
- // }
56
- bubblesSelection
57
- .attr("transform", (d) => {
58
- return `translate(${d.x},${d.y})`
59
- })
60
- // .attr("cx", (d) => d.x)
61
- // .attr("cy", (d) => d.y)
62
- })
63
- }
64
-
65
-
66
- // 計算最大泡泡的半徑
67
- function getMaxR ({ data, bubbleGroupR, maxValue, avgValue }: {
68
- data: DatumValue[]
69
- bubbleGroupR: number
70
- maxValue: number
71
- avgValue: number
72
- }) {
73
- // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
74
- const avgR = bubbleGroupR / Math.sqrt(data.length)
75
- const avgSize = avgR * avgR * Math.PI
76
- const sizeRate = avgSize / avgValue
77
- const maxSize = maxValue * sizeRate
78
- const maxR = Math.pow(maxSize / Math.PI, 0.5)
79
-
80
- const modifier = 0.75 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
81
- return maxR * modifier
82
- }
83
-
84
- function createBubblesData ({ data, LastBubbleDataMap, graphicWidth, graphicHeight, scaleType }: {
85
- data: ComputedDataSeries
86
- LastBubbleDataMap: Map<string, BubblesDatum>
87
- graphicWidth: number
88
- graphicHeight: number
89
- scaleType: BubbleScaleType
90
- // highlightIds: string[]
91
- }) {
92
- const bubbleGroupR = Math.min(...[graphicWidth, graphicHeight]) / 2
93
-
94
- const filteredData = data
95
- .flat()
96
- .filter(_d => _d.value != null && _d.visible != false)
97
-
98
- const maxValue = Math.max(
99
- ...filteredData.map(_d => _d.value!)
100
- )
101
-
102
- const avgValue = (
103
- filteredData.reduce((prev, current) => prev + (current.value ?? 0), 0)
104
- ) / filteredData.length
105
-
106
- const maxR = getMaxR({ data: filteredData, bubbleGroupR, maxValue, avgValue })
107
-
108
- const exponent = scaleType === 'area'
109
- ? 0.5 // 比例映射面積(0.5為取平方根)
110
- : 1 // 比例映射半徑
111
-
112
- const scaleBubbleR = d3.scalePow()
113
- .domain([0, maxValue])
114
- .range([0, maxR])
115
- .exponent(exponent)
116
-
117
- const bubbleData: BubblesDatum[] = filteredData.map((_d) => {
118
- const d: BubblesDatum = _d as BubblesDatum
119
-
120
- const existDatum = LastBubbleDataMap.get(_d.id)
121
-
122
- if (existDatum) {
123
- // 使用現有的座標
124
- d.x = existDatum.x
125
- d.y = existDatum.y
126
- } else {
127
- d.x = Math.random() * graphicWidth
128
- d.y = Math.random() * graphicHeight
129
- }
130
- const r = scaleBubbleR!(d.value ?? 0)!
131
- d.r = r
132
- d._originR = r
133
-
134
- return d
135
- })
136
-
137
- return bubbleData
138
- }
139
-
140
- function renderBubbles ({ graphicSelection, bubblesData, fullParams }: {
141
- graphicSelection: d3.Selection<SVGGElement, any, any, any>
142
- bubblesData: BubblesDatum[]
143
- fullParams: BubblesParams
144
- }) {
145
- let update = graphicSelection.selectAll<SVGGElement, BubblesDatum>("g")
146
- .data(bubblesData, (d) => d.id)
147
- let enter = update.enter()
148
- .append<SVGGElement>("g")
149
- .attr('cursor', 'pointer')
150
- enter
151
- .style('font-size', 12)
152
- .style('fill', '#ffffff')
153
- .attr("text-anchor", "middle")
154
- .attr("transform", (d) => {
155
- return `translate(${d.x},${d.y})`
156
- })
157
- // .attr("cx", (d) => d.x)
158
- // .attr("cy", (d) => d.y)
159
-
160
- enter
161
- .append("circle")
162
- .attr("class", "node")
163
- // update.merge(enter)
164
- .attr("cx", 0)
165
- .attr("cy", 0)
166
- // .attr("r", 1e-6)
167
- .attr('fill', (d) => d.color)
168
- // .transition()
169
- // .duration(500)
170
-
171
- enter
172
- .append('text')
173
- .style('opacity', 0.8)
174
- .attr('pointer-events', 'none')
175
-
176
- update.exit().remove()
177
-
178
- const bubblesSelection = update.merge(enter)
179
-
180
- bubblesSelection.select('circle')
181
- .transition()
182
- .duration(200)
183
- .attr("r", (d) => d.r)
184
- .attr('fill', (d) => d.color)
185
- bubblesSelection
186
- .each((d,i,g) => {
187
- const gSelection = d3.select(g[i])
188
- let breakAll = true
189
- if (d.label.length <= fullParams.bubbleText.lineLengthMin) {
190
- breakAll = false
191
- }
192
- gSelection.call(renderCircleText, {
193
- text: d.label,
194
- radius: d.r * fullParams.bubbleText.fillRate,
195
- lineHeight: fullParams.bubbleText.lineHeight,
196
- isBreakAll: breakAll
197
- })
198
-
199
- })
200
-
201
- return bubblesSelection
202
- }
203
-
204
- function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
205
- data: BubblesDatum[]
206
- // fullParams: BubblesParams
207
- highlightRIncrease: number
208
- highlightIds: string[]
209
- }) {
210
- if (highlightRIncrease == 0) {
211
- return
212
- }
213
- if (!highlightIds.length) {
214
- data.forEach(d => d.r = d._originR)
215
- return
216
- }
217
- data.forEach(d => {
218
- if (highlightIds.includes(d.id)) {
219
- d.r = d._originR + highlightRIncrease
220
- } else {
221
- d.r = d._originR
222
- }
223
- })
224
- }
225
-
226
- function drag (): d3.DragBehavior<Element, unknown, unknown> {
227
- return d3.drag()
228
- .on("start", (event, d: any) => {
229
- if (!event.active) {
230
- force!.alpha(1).restart();
231
- }
232
- d.fx = d.x
233
- d.fy = d.y
234
- })
235
- .on("drag", (event, d: any) => {
236
- if (!event.active) {
237
- force!.alphaTarget(0)
238
- }
239
- d.fx = event.x
240
- d.fy = event.y
241
- })
242
- .on("end", (event, d: any) => {
243
- d.fx = null
244
- d.fy = null
245
- })
246
- }
247
-
248
-
249
- // private nodeTypePos (d: any) {
250
- // console.log(d)
251
- // console.log(this.TypeCenters.get(d.type)!)
252
- // const typeCenter = this.TypeCenters.get(d.type)!
253
- // return typeCenter ? typeCenter.x : 0
254
- // }
255
-
256
- function groupBubbles ({ fullParams, graphicWidth, graphicHeight }: {
257
- fullParams: BubblesParams
258
- graphicWidth: number
259
- graphicHeight: number
260
- }) {
261
- force!
262
- // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
263
- // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
264
- .force('x', d3.forceX().strength(fullParams.force.strength).x(0))
265
- .force('y', d3.forceY().strength(fullParams.force.strength).y(0))
266
-
267
- force!.alpha(1).restart();
268
- }
269
-
270
- function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
271
- bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
272
- fullChartParams: ChartParams
273
- highlightIds: string[]
274
- }) {
275
- bubblesSelection.interrupt('highlight')
276
-
277
- if (!highlightIds.length) {
278
- bubblesSelection
279
- .transition('highlight')
280
- .style('opacity', 1)
281
- return
282
- }
283
-
284
- bubblesSelection.each((d, i, n) => {
285
- const segment = d3.select(n[i])
286
-
287
- if (highlightIds.includes(d.id)) {
288
- segment
289
- .style('opacity', 1)
290
- .transition('highlight')
291
- .ease(d3.easeElastic)
292
- .duration(500)
293
- } else {
294
- // 取消放大
295
- segment
296
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
297
- }
298
- })
299
- }
300
-
301
- export const Bubbles = defineSeriesPlugin('Bubbles', DEFAULT_BUBBLES_PARAMS)(({ selection, name, observer, subject }) => {
302
-
303
- const destroy$ = new Subject()
304
-
305
- const graphicSelection: d3.Selection<SVGGElement, any, any, any> = selection.append('g')
306
- const bubblesSelection$: Subject<d3.Selection<SVGGElement, BubblesDatum, any, any>> = new Subject()
307
- // 紀錄前一次bubble data
308
- let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
309
-
310
-
311
- // fullParams$.subscribe(d => {
312
- // force = makeForce(bubblesSelection, d)
313
- // })
314
-
315
- observer.layout$
316
- .pipe(
317
- first()
318
- )
319
- .subscribe(size => {
320
- selection
321
- .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
322
- observer.layout$
323
- .pipe(
324
- takeUntil(destroy$)
325
- )
326
- .subscribe(size => {
327
- selection
328
- .transition()
329
- .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
330
- })
331
- })
332
-
333
- // const bubbleGroupR$ = layout$.pipe(
334
- // map(d => {
335
- // const minWidth = Math.min(...[d.width, d.height])
336
- // return minWidth / 2
337
- // })
338
- // )
339
-
340
- // const maxValue$ = computedData$.pipe(
341
- // map(d => Math.max(
342
- // ...d
343
- // .flat()
344
- // .filter(_d => _d.value != null)
345
- // .map(_d => _d.value!)
346
- // )
347
- // )
348
- // )
349
-
350
- // const avgValue$ = computedData$.pipe(
351
- // map(d => {
352
- // const total = d
353
- // .flat()
354
- // .reduce((prev, current) => prev + (current.value ?? 0), 0)
355
- // return total / d.length
356
- // })
357
- // )
358
-
359
- // const SeriesDataMap$ = observer.computedData$.pipe(
360
- // takeUntil(destroy$),
361
- // map(d => makeSeriesDataMap(d))
362
- // )
363
-
364
- const scaleType$ = observer.fullParams$.pipe(
365
- takeUntil(destroy$),
366
- map(d => d.bubbleScaleType),
367
- distinctUntilChanged()
368
- )
369
-
370
- const bubblesData$ = new Observable<BubblesDatum[]>(subscriber => {
371
- combineLatest({
372
- layout: observer.layout$,
373
- computedData: observer.computedData$,
374
- scaleType: scaleType$
375
- }).pipe(
376
- takeUntil(destroy$),
377
- // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
378
- switchMap(async (d) => d),
379
- ).subscribe(data => {
380
- const bubblesData = createBubblesData({
381
- data: data.computedData,
382
- LastBubbleDataMap,
383
- graphicWidth: data.layout.width,
384
- graphicHeight: data.layout.height,
385
- scaleType: data.scaleType
386
- })
387
- subscriber.next(bubblesData)
388
- })
389
- })
390
-
391
- // 紀錄前一次bubble data
392
- bubblesData$.subscribe(d => {
393
- LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
394
- })
395
-
396
- const highlightTarget$ = observer.fullChartParams$.pipe(
397
- takeUntil(destroy$),
398
- map(d => d.highlightTarget),
399
- distinctUntilChanged()
400
- )
401
-
402
- combineLatest({
403
- layout: observer.layout$,
404
- computedData: observer.computedData$,
405
- bubblesData: bubblesData$,
406
- SeriesDataMap: observer.SeriesDataMap$,
407
- fullParams: observer.fullParams$,
408
- highlightTarget: highlightTarget$
409
- // fullChartParams: fullChartParams$
410
- // highlight: highlight$
411
- }).pipe(
412
- takeUntil(destroy$),
413
- // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
414
- switchMap(async (d) => d),
415
- ).subscribe(data => {
416
-
417
- const bubblesSelection = renderBubbles({
418
- graphicSelection,
419
- bubblesData: data.bubblesData,
420
- fullParams: data.fullParams
421
- })
422
-
423
- force = makeForce(bubblesSelection, data.fullParams)
424
-
425
- 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
- force!.nodes(data.bubblesData);
499
-
500
- groupBubbles({
501
- fullParams: data.fullParams,
502
- graphicWidth: data.layout.width,
503
- graphicHeight: data.layout.height
504
- })
505
-
506
- bubblesSelection$.next(bubblesSelection)
507
- })
508
-
509
- combineLatest({
510
- bubblesSelection: bubblesSelection$,
511
- bubblesData: bubblesData$,
512
- highlight: observer.seriesHighlight$,
513
- fullChartParams: observer.fullChartParams$,
514
- fullParams: observer.fullParams$,
515
- layout: observer.layout$
516
- }).pipe(
517
- takeUntil(destroy$),
518
- switchMap(async d => d)
519
- ).subscribe(data => {
520
- highlight({
521
- bubblesSelection: data.bubblesSelection,
522
- highlightIds: data.highlight,
523
- fullChartParams: data.fullChartParams
524
- })
525
-
526
- if (data.fullParams.highlightRIncrease) {
527
- setHighlightData ({
528
- data: data.bubblesData,
529
- highlightRIncrease: data.fullParams.highlightRIncrease,
530
- highlightIds: data.highlight
531
- })
532
- renderBubbles({
533
- graphicSelection,
534
- bubblesData: data.bubblesData,
535
- fullParams: data.fullParams
536
- })
537
- }
538
-
539
- groupBubbles({
540
- fullParams: data.fullParams,
541
- graphicWidth: data.layout.width,
542
- graphicHeight: data.layout.height
543
- })
544
- force!.nodes(data.bubblesData);
545
- })
546
-
547
- return () => {
548
- destroy$.next(undefined)
549
- }
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 } from '@orbcharts/core'
18
+ import {
19
+ defineSeriesPlugin } from '@orbcharts/core'
20
+ import type { BubblesParams, BubbleScaleType } from '../types'
21
+ import { DEFAULT_BUBBLES_PARAMS } from '../defaults'
22
+ import { renderCircleText } from '../../utils/d3Graphics'
23
+
24
+ interface BubblesDatum extends ComputedDatumSeries {
25
+ x: number
26
+ y: number
27
+ r: number
28
+ _originR: number // 紀錄變化前的r
29
+ }
30
+
31
+ let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
32
+
33
+ function makeForce (bubblesSelection: d3.Selection<SVGGElement, any, any, any>, fullParams: BubblesParams) {
34
+ return d3.forceSimulation()
35
+ .velocityDecay(fullParams.force!.velocityDecay!)
36
+ // .alphaDecay(0.2)
37
+ .force(
38
+ "collision",
39
+ d3.forceCollide()
40
+ .radius(d => {
41
+ // @ts-ignore
42
+ return d.r + fullParams.force!.collisionSpacing
43
+ })
44
+ // .strength(0.01)
45
+ )
46
+ .force("charge", d3.forceManyBody().strength((d) => {
47
+ // @ts-ignore
48
+ return - Math.pow(d.r, 2.0) * fullParams.force!.strength
49
+ }))
50
+ // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
51
+ // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
52
+ .on("tick", () => {
53
+ // if (!bubblesSelection) {
54
+ // return
55
+ // }
56
+ bubblesSelection
57
+ .attr("transform", (d) => {
58
+ return `translate(${d.x},${d.y})`
59
+ })
60
+ // .attr("cx", (d) => d.x)
61
+ // .attr("cy", (d) => d.y)
62
+ })
63
+ }
64
+
65
+
66
+ // 計算最大泡泡的半徑
67
+ function getMaxR ({ data, bubbleGroupR, maxValue, avgValue }: {
68
+ data: DatumValue[]
69
+ bubbleGroupR: number
70
+ maxValue: number
71
+ avgValue: number
72
+ }) {
73
+ // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
74
+ const avgR = bubbleGroupR / Math.sqrt(data.length)
75
+ const avgSize = avgR * avgR * Math.PI
76
+ const sizeRate = avgSize / avgValue
77
+ const maxSize = maxValue * sizeRate
78
+ const maxR = Math.pow(maxSize / Math.PI, 0.5)
79
+
80
+ const modifier = 0.75 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
81
+ return maxR * modifier
82
+ }
83
+
84
+ function createBubblesData ({ data, LastBubbleDataMap, graphicWidth, graphicHeight, scaleType }: {
85
+ data: ComputedDataSeries
86
+ LastBubbleDataMap: Map<string, BubblesDatum>
87
+ graphicWidth: number
88
+ graphicHeight: number
89
+ scaleType: BubbleScaleType
90
+ // highlightIds: string[]
91
+ }) {
92
+ const bubbleGroupR = Math.min(...[graphicWidth, graphicHeight]) / 2
93
+
94
+ const filteredData = data
95
+ .flat()
96
+ .filter(_d => _d.value != null && _d.visible != false)
97
+
98
+ const maxValue = Math.max(
99
+ ...filteredData.map(_d => _d.value!)
100
+ )
101
+
102
+ const avgValue = (
103
+ filteredData.reduce((prev, current) => prev + (current.value ?? 0), 0)
104
+ ) / filteredData.length
105
+
106
+ const maxR = getMaxR({ data: filteredData, bubbleGroupR, maxValue, avgValue })
107
+
108
+ const exponent = scaleType === 'area'
109
+ ? 0.5 // 比例映射面積(0.5為取平方根)
110
+ : 1 // 比例映射半徑
111
+
112
+ const scaleBubbleR = d3.scalePow()
113
+ .domain([0, maxValue])
114
+ .range([0, maxR])
115
+ .exponent(exponent)
116
+
117
+ const bubbleData: BubblesDatum[] = filteredData.map((_d) => {
118
+ const d: BubblesDatum = _d as BubblesDatum
119
+
120
+ const existDatum = LastBubbleDataMap.get(_d.id)
121
+
122
+ if (existDatum) {
123
+ // 使用現有的座標
124
+ d.x = existDatum.x
125
+ d.y = existDatum.y
126
+ } else {
127
+ d.x = Math.random() * graphicWidth
128
+ d.y = Math.random() * graphicHeight
129
+ }
130
+ const r = scaleBubbleR!(d.value ?? 0)!
131
+ d.r = r
132
+ d._originR = r
133
+
134
+ return d
135
+ })
136
+
137
+ return bubbleData
138
+ }
139
+
140
+ function renderBubbles ({ graphicSelection, bubblesData, fullParams }: {
141
+ graphicSelection: d3.Selection<SVGGElement, any, any, any>
142
+ bubblesData: BubblesDatum[]
143
+ fullParams: BubblesParams
144
+ }) {
145
+ let update = graphicSelection.selectAll<SVGGElement, BubblesDatum>("g")
146
+ .data(bubblesData, (d) => d.id)
147
+ let enter = update.enter()
148
+ .append<SVGGElement>("g")
149
+ .attr('cursor', 'pointer')
150
+ enter
151
+ .style('font-size', 12)
152
+ .style('fill', '#ffffff')
153
+ .attr("text-anchor", "middle")
154
+ .attr("transform", (d) => {
155
+ return `translate(${d.x},${d.y})`
156
+ })
157
+ // .attr("cx", (d) => d.x)
158
+ // .attr("cy", (d) => d.y)
159
+
160
+ enter
161
+ .append("circle")
162
+ .attr("class", "node")
163
+ // update.merge(enter)
164
+ .attr("cx", 0)
165
+ .attr("cy", 0)
166
+ // .attr("r", 1e-6)
167
+ .attr('fill', (d) => d.color)
168
+ // .transition()
169
+ // .duration(500)
170
+
171
+ enter
172
+ .append('text')
173
+ .style('opacity', 0.8)
174
+ .attr('pointer-events', 'none')
175
+
176
+ update.exit().remove()
177
+
178
+ const bubblesSelection = update.merge(enter)
179
+
180
+ bubblesSelection.select('circle')
181
+ .transition()
182
+ .duration(200)
183
+ .attr("r", (d) => d.r)
184
+ .attr('fill', (d) => d.color)
185
+ bubblesSelection
186
+ .each((d,i,g) => {
187
+ const gSelection = d3.select(g[i])
188
+ let breakAll = true
189
+ if (d.label.length <= fullParams.bubbleText.lineLengthMin) {
190
+ breakAll = false
191
+ }
192
+ gSelection.call(renderCircleText, {
193
+ text: d.label,
194
+ radius: d.r * fullParams.bubbleText.fillRate,
195
+ lineHeight: fullParams.bubbleText.lineHeight,
196
+ isBreakAll: breakAll
197
+ })
198
+
199
+ })
200
+
201
+ return bubblesSelection
202
+ }
203
+
204
+ function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
205
+ data: BubblesDatum[]
206
+ // fullParams: BubblesParams
207
+ highlightRIncrease: number
208
+ highlightIds: string[]
209
+ }) {
210
+ if (highlightRIncrease == 0) {
211
+ return
212
+ }
213
+ if (!highlightIds.length) {
214
+ data.forEach(d => d.r = d._originR)
215
+ return
216
+ }
217
+ data.forEach(d => {
218
+ if (highlightIds.includes(d.id)) {
219
+ d.r = d._originR + highlightRIncrease
220
+ } else {
221
+ d.r = d._originR
222
+ }
223
+ })
224
+ }
225
+
226
+ function drag (): d3.DragBehavior<Element, unknown, unknown> {
227
+ return d3.drag()
228
+ .on("start", (event, d: any) => {
229
+ if (!event.active) {
230
+ force!.alpha(1).restart();
231
+ }
232
+ d.fx = d.x
233
+ d.fy = d.y
234
+ })
235
+ .on("drag", (event, d: any) => {
236
+ if (!event.active) {
237
+ force!.alphaTarget(0)
238
+ }
239
+ d.fx = event.x
240
+ d.fy = event.y
241
+ })
242
+ .on("end", (event, d: any) => {
243
+ d.fx = null
244
+ d.fy = null
245
+ })
246
+ }
247
+
248
+
249
+ // private nodeTypePos (d: any) {
250
+ // console.log(d)
251
+ // console.log(this.TypeCenters.get(d.type)!)
252
+ // const typeCenter = this.TypeCenters.get(d.type)!
253
+ // return typeCenter ? typeCenter.x : 0
254
+ // }
255
+
256
+ function groupBubbles ({ fullParams, graphicWidth, graphicHeight }: {
257
+ fullParams: BubblesParams
258
+ graphicWidth: number
259
+ graphicHeight: number
260
+ }) {
261
+ force!
262
+ // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
263
+ // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
264
+ .force('x', d3.forceX().strength(fullParams.force.strength).x(0))
265
+ .force('y', d3.forceY().strength(fullParams.force.strength).y(0))
266
+
267
+ force!.alpha(1).restart();
268
+ }
269
+
270
+ function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
271
+ bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
272
+ fullChartParams: ChartParams
273
+ highlightIds: string[]
274
+ }) {
275
+ bubblesSelection.interrupt('highlight')
276
+
277
+ if (!highlightIds.length) {
278
+ bubblesSelection
279
+ .transition('highlight')
280
+ .style('opacity', 1)
281
+ return
282
+ }
283
+
284
+ bubblesSelection.each((d, i, n) => {
285
+ const segment = d3.select(n[i])
286
+
287
+ if (highlightIds.includes(d.id)) {
288
+ segment
289
+ .style('opacity', 1)
290
+ .transition('highlight')
291
+ .ease(d3.easeElastic)
292
+ .duration(500)
293
+ } else {
294
+ // 取消放大
295
+ segment
296
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
297
+ }
298
+ })
299
+ }
300
+
301
+ export const Bubbles = defineSeriesPlugin('Bubbles', DEFAULT_BUBBLES_PARAMS)(({ selection, name, observer, subject }) => {
302
+
303
+ const destroy$ = new Subject()
304
+
305
+ const graphicSelection: d3.Selection<SVGGElement, any, any, any> = selection.append('g')
306
+ const bubblesSelection$: Subject<d3.Selection<SVGGElement, BubblesDatum, any, any>> = new Subject()
307
+ // 紀錄前一次bubble data
308
+ let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
309
+
310
+
311
+ // fullParams$.subscribe(d => {
312
+ // force = makeForce(bubblesSelection, d)
313
+ // })
314
+
315
+ observer.layout$
316
+ .pipe(
317
+ first()
318
+ )
319
+ .subscribe(size => {
320
+ selection
321
+ .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
322
+ observer.layout$
323
+ .pipe(
324
+ takeUntil(destroy$)
325
+ )
326
+ .subscribe(size => {
327
+ selection
328
+ .transition()
329
+ .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
330
+ })
331
+ })
332
+
333
+ // const bubbleGroupR$ = layout$.pipe(
334
+ // map(d => {
335
+ // const minWidth = Math.min(...[d.width, d.height])
336
+ // return minWidth / 2
337
+ // })
338
+ // )
339
+
340
+ // const maxValue$ = computedData$.pipe(
341
+ // map(d => Math.max(
342
+ // ...d
343
+ // .flat()
344
+ // .filter(_d => _d.value != null)
345
+ // .map(_d => _d.value!)
346
+ // )
347
+ // )
348
+ // )
349
+
350
+ // const avgValue$ = computedData$.pipe(
351
+ // map(d => {
352
+ // const total = d
353
+ // .flat()
354
+ // .reduce((prev, current) => prev + (current.value ?? 0), 0)
355
+ // return total / d.length
356
+ // })
357
+ // )
358
+
359
+ // const SeriesDataMap$ = observer.computedData$.pipe(
360
+ // takeUntil(destroy$),
361
+ // map(d => makeSeriesDataMap(d))
362
+ // )
363
+
364
+ const scaleType$ = observer.fullParams$.pipe(
365
+ takeUntil(destroy$),
366
+ map(d => d.bubbleScaleType),
367
+ distinctUntilChanged()
368
+ )
369
+
370
+ const bubblesData$ = new Observable<BubblesDatum[]>(subscriber => {
371
+ combineLatest({
372
+ layout: observer.layout$,
373
+ computedData: observer.computedData$,
374
+ scaleType: scaleType$
375
+ }).pipe(
376
+ takeUntil(destroy$),
377
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
378
+ switchMap(async (d) => d),
379
+ ).subscribe(data => {
380
+ const bubblesData = createBubblesData({
381
+ data: data.computedData,
382
+ LastBubbleDataMap,
383
+ graphicWidth: data.layout.width,
384
+ graphicHeight: data.layout.height,
385
+ scaleType: data.scaleType
386
+ })
387
+ subscriber.next(bubblesData)
388
+ })
389
+ })
390
+
391
+ // 紀錄前一次bubble data
392
+ bubblesData$.subscribe(d => {
393
+ LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
394
+ })
395
+
396
+ const highlightTarget$ = observer.fullChartParams$.pipe(
397
+ takeUntil(destroy$),
398
+ map(d => d.highlightTarget),
399
+ distinctUntilChanged()
400
+ )
401
+
402
+ combineLatest({
403
+ layout: observer.layout$,
404
+ computedData: observer.computedData$,
405
+ bubblesData: bubblesData$,
406
+ SeriesDataMap: observer.SeriesDataMap$,
407
+ fullParams: observer.fullParams$,
408
+ highlightTarget: highlightTarget$
409
+ // fullChartParams: fullChartParams$
410
+ // highlight: highlight$
411
+ }).pipe(
412
+ takeUntil(destroy$),
413
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
414
+ switchMap(async (d) => d),
415
+ ).subscribe(data => {
416
+
417
+ const bubblesSelection = renderBubbles({
418
+ graphicSelection,
419
+ bubblesData: data.bubblesData,
420
+ fullParams: data.fullParams
421
+ })
422
+
423
+ force = makeForce(bubblesSelection, data.fullParams)
424
+
425
+ 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
+ force!.nodes(data.bubblesData);
499
+
500
+ groupBubbles({
501
+ fullParams: data.fullParams,
502
+ graphicWidth: data.layout.width,
503
+ graphicHeight: data.layout.height
504
+ })
505
+
506
+ bubblesSelection$.next(bubblesSelection)
507
+ })
508
+
509
+ combineLatest({
510
+ bubblesSelection: bubblesSelection$,
511
+ bubblesData: bubblesData$,
512
+ highlight: observer.seriesHighlight$.pipe(
513
+ map(data => data.map(d => d.id))
514
+ ),
515
+ fullChartParams: observer.fullChartParams$,
516
+ fullParams: observer.fullParams$,
517
+ layout: observer.layout$
518
+ }).pipe(
519
+ takeUntil(destroy$),
520
+ switchMap(async d => d)
521
+ ).subscribe(data => {
522
+ highlight({
523
+ bubblesSelection: data.bubblesSelection,
524
+ highlightIds: data.highlight,
525
+ fullChartParams: data.fullChartParams
526
+ })
527
+
528
+ if (data.fullParams.highlightRIncrease) {
529
+ setHighlightData ({
530
+ data: data.bubblesData,
531
+ highlightRIncrease: data.fullParams.highlightRIncrease,
532
+ highlightIds: data.highlight
533
+ })
534
+ renderBubbles({
535
+ graphicSelection,
536
+ bubblesData: data.bubblesData,
537
+ fullParams: data.fullParams
538
+ })
539
+ }
540
+
541
+ groupBubbles({
542
+ fullParams: data.fullParams,
543
+ graphicWidth: data.layout.width,
544
+ graphicHeight: data.layout.height
545
+ })
546
+ force!.nodes(data.bubblesData);
547
+ })
548
+
549
+ return () => {
550
+ destroy$.next(undefined)
551
+ }
550
552
  })