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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
  })