@orbcharts/plugins-basic 3.0.0-beta.13 → 3.0.0-beta.15

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 (100) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic/src/utils/d3Utils.d.ts +1 -0
  3. package/dist/orbcharts-plugins-basic.es.js +5558 -5443
  4. package/dist/orbcharts-plugins-basic.umd.js +159 -78
  5. package/lib/core-types.ts +7 -7
  6. package/lib/core.ts +6 -6
  7. package/lib/plugins-basic-types.ts +6 -6
  8. package/package.json +44 -44
  9. package/src/base/BaseBars.ts +765 -765
  10. package/src/base/BaseBarsTriangle.ts +676 -676
  11. package/src/base/BaseDots.ts +464 -464
  12. package/src/base/BaseGroupAxis.ts +679 -679
  13. package/src/base/BaseLegend.ts +684 -684
  14. package/src/base/BaseLineAreas.ts +629 -629
  15. package/src/base/BaseLines.ts +706 -706
  16. package/src/base/BaseStackedBar.ts +782 -782
  17. package/src/base/BaseTooltip.ts +385 -385
  18. package/src/base/BaseValueAxis.ts +583 -583
  19. package/src/base/types.ts +2 -2
  20. package/src/const.ts +30 -30
  21. package/src/grid/defaults.ts +250 -246
  22. package/src/grid/gridObservables.ts +554 -554
  23. package/src/grid/index.ts +16 -16
  24. package/src/grid/plugins/Bars.ts +69 -69
  25. package/src/grid/plugins/BarsPN.ts +66 -66
  26. package/src/grid/plugins/BarsTriangle.ts +73 -73
  27. package/src/grid/plugins/Dots.ts +68 -68
  28. package/src/grid/plugins/GridLegend.ts +107 -107
  29. package/src/grid/plugins/GridTooltip.ts +66 -66
  30. package/src/grid/plugins/GridZoom.ts +218 -218
  31. package/src/grid/plugins/GroupAux.ts +1103 -1103
  32. package/src/grid/plugins/GroupAxis.ts +97 -97
  33. package/src/grid/plugins/LineAreas.ts +65 -65
  34. package/src/grid/plugins/Lines.ts +59 -59
  35. package/src/grid/plugins/StackedBar.ts +64 -64
  36. package/src/grid/plugins/StackedValueAxis.ts +96 -96
  37. package/src/grid/plugins/ValueAxis.ts +94 -94
  38. package/src/index.ts +6 -6
  39. package/src/multiGrid/defaults.ts +228 -224
  40. package/src/multiGrid/index.ts +14 -14
  41. package/src/multiGrid/multiGridObservables.ts +49 -49
  42. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  43. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  44. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  45. package/src/multiGrid/plugins/MultiGridLegend.ts +159 -159
  46. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  47. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  48. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  49. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  50. package/src/multiGrid/plugins/MultiStackedBar.ts +106 -106
  51. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +134 -134
  52. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  53. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +299 -299
  54. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  55. package/src/multiValue/defaults.ts +166 -166
  56. package/src/multiValue/index.ts +8 -8
  57. package/src/multiValue/multiValueObservables.ts +297 -297
  58. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  59. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  60. package/src/multiValue/plugins/Scatter.ts +426 -426
  61. package/src/multiValue/plugins/ScatterBubbles.ts +554 -554
  62. package/src/multiValue/plugins/XYAux.ts +681 -681
  63. package/src/multiValue/plugins/XYAxes.ts +684 -684
  64. package/src/multiValue/plugins/XYZoom.ts +299 -299
  65. package/src/noneData/defaults.ts +102 -102
  66. package/src/noneData/index.ts +3 -3
  67. package/src/noneData/plugins/Container.ts +27 -27
  68. package/src/noneData/plugins/Tooltip.ts +373 -373
  69. package/src/relationship/defaults.ts +218 -196
  70. package/src/relationship/index.ts +5 -5
  71. package/src/relationship/plugins/ForceDirected.ts +1168 -1168
  72. package/src/relationship/plugins/ForceDirectedBubbles.ts +1403 -1395
  73. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  74. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  75. package/src/relationship/relationshipObservables.ts +49 -49
  76. package/src/series/defaults.ts +230 -207
  77. package/src/series/index.ts +9 -9
  78. package/src/series/plugins/Bubbles.ts +620 -606
  79. package/src/series/plugins/Pie.ts +623 -623
  80. package/src/series/plugins/PieEventTexts.ts +284 -284
  81. package/src/series/plugins/PieLabels.ts +640 -640
  82. package/src/series/plugins/Rose.ts +516 -516
  83. package/src/series/plugins/RoseLabels.ts +600 -600
  84. package/src/series/plugins/SeriesLegend.ts +107 -107
  85. package/src/series/plugins/SeriesTooltip.ts +66 -66
  86. package/src/series/seriesObservables.ts +145 -145
  87. package/src/series/seriesUtils.ts +51 -51
  88. package/src/tree/defaults.ts +100 -78
  89. package/src/tree/index.ts +4 -4
  90. package/src/tree/plugins/TreeLegend.ts +100 -100
  91. package/src/tree/plugins/TreeMap.ts +341 -333
  92. package/src/tree/plugins/TreeTooltip.ts +66 -66
  93. package/src/utils/commonUtils.ts +21 -21
  94. package/src/utils/d3Graphics.ts +174 -174
  95. package/src/utils/d3Utils.ts +92 -74
  96. package/src/utils/observables.ts +14 -14
  97. package/src/utils/orbchartsUtils.ts +129 -115
  98. package/tsconfig.base.json +13 -13
  99. package/tsconfig.json +2 -2
  100. package/vite.config.js +22 -22
@@ -1,554 +1,554 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- filter,
6
- switchMap,
7
- takeUntil,
8
- distinctUntilChanged,
9
- Observable,
10
- Subject,
11
- share,
12
- shareReplay
13
- } from 'rxjs'
14
- import type {
15
- ComputedDatumMultiValue,
16
- ComputedDataMultiValue,
17
- ComputedLayoutDatumMultiValue,
18
- ComputedLayoutDataMultiValue,
19
- DefinePluginConfig,
20
- EventMultiValue,
21
- ChartParams,
22
- ContainerPositionScaled,
23
- Layout,
24
- TransformData,
25
- ColorType
26
- } from '../../../lib/core-types'
27
- import {
28
- defineMultiValuePlugin,
29
- getMinMax
30
- } from '../../../lib/core'
31
- import type { ScatterBubblesParams } from '../../../lib/plugins-basic-types'
32
- import { DEFAULT_SCATTER_BUBBLES_PARAMS } from '../defaults'
33
- import { LAYER_INDEX_OF_GRAPHIC_COVER } from '../../const'
34
- import { getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
35
- import { multiValueSelectionsObservable } from '../multiValueObservables'
36
-
37
- type ClipPathDatum = {
38
- id: string;
39
- // x: number;
40
- // y: number;
41
- width: number;
42
- height: number;
43
- }
44
-
45
- interface BubbleDatum extends ComputedLayoutDatumMultiValue {
46
- r: number
47
- opacity: number
48
- }
49
-
50
- const pluginName = 'ScatterBubbles'
51
-
52
- // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
53
- const adjustmentFactor = 0.9
54
-
55
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_SCATTER_BUBBLES_PARAMS> = {
56
- name: pluginName,
57
- defaultParams: DEFAULT_SCATTER_BUBBLES_PARAMS,
58
- layerIndex: LAYER_INDEX_OF_GRAPHIC_COVER,
59
- validator: (params, { validateColumns }) => {
60
- const result = validateColumns(params, {
61
- // radius: {
62
- // toBeTypes: ['number']
63
- // },
64
- fillColorType: {
65
- toBeOption: 'ColorType',
66
- },
67
- strokeColorType: {
68
- toBeOption: 'ColorType',
69
- },
70
- strokeWidth: {
71
- toBeTypes: ['number']
72
- },
73
- valueLinearOpacity: {
74
- toBeTypes: ['number[]']
75
- },
76
- arcScaleType: {
77
- toBe: 'ArcScaleType',
78
- test: (value) => {
79
- return value === 'area' || value === 'radius'
80
- }
81
- },
82
- sizeAdjust: {
83
- toBeTypes: ['number']
84
- }
85
- })
86
- return result
87
- }
88
- }
89
-
90
- function renderDots ({ graphicGSelection, circleGClassName, circleClassName, bubbleData, fullParams, fullChartParams, graphicReverseScale }: {
91
- graphicGSelection: d3.Selection<SVGGElement, any, any, any>
92
- circleGClassName: string
93
- circleClassName: string
94
- // visibleComputedLayoutData: ComputedLayoutDataMultiValue
95
- bubbleData: BubbleDatum[][]
96
- fullParams: ScatterBubblesParams
97
- fullChartParams: ChartParams
98
- graphicReverseScale: [number, number][]
99
- }) {
100
- const createEnterDuration = (enter: d3.Selection<d3.EnterElement, ComputedDatumMultiValue, SVGGElement, any>) => {
101
- const enterSize = enter.size()
102
- const eachDuration = fullChartParams.transitionDuration / enterSize
103
- return eachDuration
104
- }
105
- // enterDuration
106
- let enterDuration = 0
107
-
108
- graphicGSelection
109
- .each((categoryData, categoryIndex, g) => {
110
- d3.select(g[categoryIndex])
111
- .selectAll<SVGGElement, ComputedDatumMultiValue>('g')
112
- .data(bubbleData[categoryIndex], d => d.id)
113
- .join(
114
- enter => {
115
- // enterDuration
116
- enterDuration = createEnterDuration(enter)
117
-
118
- return enter
119
- .append('g')
120
- .classed(circleGClassName, true)
121
- },
122
- update => update,
123
- exit => exit.remove()
124
- )
125
- .attr('transform', d => `translate(${d.axisX}, ${d.axisY})`)
126
- .each((d, i, g) => {
127
- const circle = d3.select(g[i])
128
- .selectAll('circle')
129
- .data([d])
130
- .join(
131
- enter => {
132
- return enter
133
- .append('circle')
134
- .style('cursor', 'pointer')
135
- .style('vector-effect', 'non-scaling-stroke')
136
- .classed(circleClassName, true)
137
- .attr('opacity', 0)
138
- .transition()
139
- .delay((_d, _i) => {
140
- return i * enterDuration
141
- })
142
- .attr('opacity', 0.8)
143
- },
144
- update => {
145
- return update
146
- .transition()
147
- .duration(50)
148
- // .attr('cx', d => d.axisX)
149
- // .attr('cy', d => d.axisY)
150
- .attr('opacity', 0.8)
151
- },
152
- exit => exit.remove()
153
- )
154
- .attr('r', d => d.r)
155
- .attr('fill', (d, i) => getDatumColor({ datum: d, colorType: fullParams.fillColorType, fullChartParams }))
156
- .attr('stroke', (d, i) => getDatumColor({ datum: d, colorType: fullParams.strokeColorType, fullChartParams }))
157
- .attr('stroke-width', fullParams.strokeWidth)
158
- .attr('transform', `scale(${graphicReverseScale[categoryIndex][0] ?? 1}, ${graphicReverseScale[categoryIndex][1] ?? 1})`)
159
- })
160
- })
161
-
162
- const graphicCircleSelection: d3.Selection<SVGRectElement, BubbleDatum, SVGGElement, unknown> = graphicGSelection.selectAll(`circle.${circleClassName}`)
163
-
164
- return graphicCircleSelection
165
- }
166
-
167
-
168
- function highlightBubbles ({ selection, ids, fullChartParams }: {
169
- selection: d3.Selection<SVGGElement, BubbleDatum, any, any>
170
- ids: string[]
171
- fullChartParams: ChartParams
172
- }) {
173
- selection.interrupt('highlight')
174
- if (!ids.length) {
175
- // remove highlight
176
- selection
177
- .transition('highlight')
178
- .duration(200)
179
- .style('opacity', d => d.opacity)
180
- // selection
181
- // .attr('stroke-width', fullParams.strokeWidth)
182
-
183
- return
184
- }
185
-
186
- selection
187
- .each((d, i, n) => {
188
- if (ids.includes(d.id)) {
189
- const dot = d3.select<SVGGElement, BubbleDatum>(n[i])
190
- dot
191
- .style('opacity', d => d.opacity)
192
- .transition('highlight')
193
- .duration(200)
194
- // dot
195
- // .attr('stroke-width', fullParams.strokeWidthWhileHighlight)
196
- } else {
197
- const dot = d3.select(n[i])
198
- dot
199
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
200
- .transition('highlight')
201
- .duration(200)
202
- // dot
203
- // .attr('stroke-width', fullParams.strokeWidth)
204
-
205
- }
206
- })
207
- }
208
-
209
- function renderClipPath ({ defsSelection, clipPathData }: {
210
- defsSelection: d3.Selection<SVGDefsElement, any, any, any>
211
- clipPathData: ClipPathDatum[]
212
- }) {
213
- const clipPath = defsSelection
214
- .selectAll<SVGClipPathElement, Layout>('clipPath')
215
- .data(clipPathData)
216
- .join(
217
- enter => {
218
- return enter
219
- .append('clipPath')
220
- },
221
- update => update,
222
- exit => exit.remove()
223
- )
224
- .attr('id', d => d.id)
225
- .each((d, i, g) => {
226
- const rect = d3.select(g[i])
227
- .selectAll<SVGRectElement, typeof d>('rect')
228
- .data([d])
229
- .join('rect')
230
- .attr('x', 0)
231
- .attr('y', 0)
232
- .attr('width', _d => _d.width)
233
- .attr('height', _d => _d.height)
234
- })
235
-
236
- }
237
-
238
-
239
- export const ScatterBubbles = defineMultiValuePlugin(pluginConfig)(({ selection, name, subject, observer }) => {
240
-
241
- const destroy$ = new Subject()
242
-
243
- const clipPathID = getUniID(pluginName, 'clipPath-box')
244
- const circleGClassName = getClassName(pluginName, 'circleG')
245
- const circleClassName = getClassName(pluginName, 'circle')
246
-
247
- const {
248
- categorySelection$,
249
- axesSelection$,
250
- defsSelection$,
251
- graphicGSelection$
252
- } = multiValueSelectionsObservable({
253
- selection,
254
- pluginName,
255
- clipPathID,
256
- categoryLabels$: observer.categoryLabels$,
257
- multiValueContainerPosition$: observer.multiValueContainerPosition$,
258
- multiValueGraphicTransform$: observer.multiValueGraphicTransform$
259
- })
260
-
261
- const graphicReverseScale$: Observable<[number, number][]> = combineLatest({
262
- computedData: observer.computedData$,
263
- multiValueGraphicReverseScale: observer.multiValueGraphicReverseScale$
264
- }).pipe(
265
- takeUntil(destroy$),
266
- switchMap(async data => data),
267
- map(data => {
268
- return data.computedData.map((series, categoryIndex) => {
269
- return data.multiValueGraphicReverseScale[categoryIndex]
270
- })
271
- })
272
- )
273
-
274
- const clipPathSubscription = combineLatest({
275
- defsSelection: defsSelection$,
276
- layout: observer.layout$,
277
- }).pipe(
278
- takeUntil(destroy$),
279
- switchMap(async (d) => d),
280
- ).subscribe(data => {
281
- // 外層的遮罩
282
- const clipPathData = [{
283
- id: clipPathID,
284
- width: data.layout.width,
285
- height: data.layout.height
286
- }]
287
- renderClipPath({
288
- defsSelection: data.defsSelection,
289
- clipPathData,
290
- })
291
- })
292
-
293
- const filteredValueList$ = observer.filteredMinMaxXYData$.pipe(
294
- takeUntil(destroy$),
295
- map(data => data.datumList.flat().map(d => d.value[2] ?? 0)),
296
- shareReplay(1)
297
- )
298
-
299
- const filteredMinMaxValue$ = observer.filteredMinMaxXYData$.pipe(
300
- takeUntil(destroy$),
301
- map(data => {
302
- return getMinMax(data.datumList.flat().map(d => d.value[2] ?? 0))
303
- })
304
- )
305
-
306
- const opacityScale$ = combineLatest({
307
- filteredMinMaxValue: filteredMinMaxValue$,
308
- fullParams: observer.fullParams$
309
- }).pipe(
310
- takeUntil(destroy$),
311
- switchMap(async (d) => d),
312
- map(data => {
313
- return d3.scaleLinear()
314
- .domain(data.filteredMinMaxValue)
315
- .range(data.fullParams.valueLinearOpacity)
316
- })
317
- )
318
-
319
- // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
320
- const totalR$ = combineLatest({
321
- layout: observer.layout$,
322
- fullParams: observer.fullParams$
323
- }).pipe(
324
- takeUntil(destroy$),
325
- map(data => {
326
- // 場景最短邊
327
- const fullRadius = Math.min(...[data.layout.width, data.layout.height]) / 2
328
- return fullRadius * data.fullParams.sizeAdjust
329
- })
330
- )
331
-
332
- const totalValue$ = filteredValueList$.pipe(
333
- takeUntil(destroy$),
334
- map(data => {
335
- return data.reduce((acc, current) => acc + current, 0)
336
- }),
337
- filter(data => data > 0) // 避免後續計算scale的時候發生問題
338
- )
339
-
340
- const radiusScale$ = combineLatest({
341
- totalR: totalR$,
342
- totalValue: totalValue$,
343
- fullParams: observer.fullParams$
344
- }).pipe(
345
- takeUntil(destroy$),
346
- switchMap(async (d) => d),
347
- map(data => {
348
- // console.log({ totalR: data.totalR, totalValue: data.totalValue })
349
- return d3.scalePow()
350
- .domain([0, data.totalValue])
351
- .range([0, data.totalR])
352
- .exponent(data.fullParams.arcScaleType === 'area'
353
- ? 0.5 // 數值映射面積(0.5為取平方根)
354
- : 1 // 數值映射半徑
355
- )
356
- })
357
- )
358
-
359
- const scaleFactor$ = combineLatest({
360
- radiusScale: radiusScale$,
361
- fullParams: observer.fullParams$,
362
- totalR: totalR$,
363
- filteredValueList: filteredValueList$
364
- }).pipe(
365
- takeUntil(destroy$),
366
- switchMap(async (d) => d),
367
- map(data => {
368
- return data.fullParams.arcScaleType === 'area'
369
- ? 1
370
- // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
371
- : (() => {
372
- const totalArea = data.totalR * data.totalR * Math.PI
373
- return Math.sqrt(totalArea / d3.sum(data.filteredValueList, d => Math.PI * Math.pow(data.radiusScale(d), 2)))
374
- })()
375
- })
376
- )
377
-
378
- const bubbleData$ = combineLatest({
379
- computedLayoutData: observer.computedLayoutData$,
380
- opacityScale: opacityScale$,
381
- radiusScale: radiusScale$,
382
- scaleFactor: scaleFactor$,
383
- fullParams: observer.fullParams$
384
- }).pipe(
385
- takeUntil(destroy$),
386
- switchMap(async (d) => d),
387
- map(data => {
388
- return data.computedLayoutData.map(category => {
389
- return category.map(_d => {
390
- const d: BubbleDatum = _d as BubbleDatum
391
- d.r = data.radiusScale(d.value[2]) * data.scaleFactor * adjustmentFactor
392
- d.opacity = data.opacityScale(d.value[2])
393
- return d
394
- })
395
- })
396
- })
397
- )
398
-
399
-
400
- const graphicSelection$ = combineLatest({
401
- graphicGSelection: graphicGSelection$,
402
- // visibleComputedLayoutData: observer.visibleComputedLayoutData$,
403
- bubbleData: bubbleData$,
404
- graphicReverseScale: graphicReverseScale$,
405
- fullChartParams: observer.fullChartParams$,
406
- fullParams: observer.fullParams$,
407
- }).pipe(
408
- takeUntil(destroy$),
409
- switchMap(async (d) => d),
410
- map(data => {
411
- return renderDots({
412
- graphicGSelection: data.graphicGSelection,
413
- circleGClassName,
414
- circleClassName,
415
- bubbleData: data.bubbleData,
416
- fullParams: data.fullParams,
417
- fullChartParams: data.fullChartParams,
418
- graphicReverseScale: data.graphicReverseScale
419
- })
420
- })
421
- )
422
-
423
- const highlightTarget$ = observer.fullChartParams$.pipe(
424
- takeUntil(destroy$),
425
- map(d => d.highlightTarget),
426
- distinctUntilChanged()
427
- )
428
-
429
- combineLatest({
430
- graphicSelection: graphicSelection$,
431
- computedData: observer.computedData$,
432
- CategoryDataMap: observer.CategoryDataMap$,
433
- highlightTarget: highlightTarget$
434
- }).pipe(
435
- takeUntil(destroy$),
436
- switchMap(async (d) => d),
437
- ).subscribe(data => {
438
-
439
- data.graphicSelection
440
- .on('mouseover', (event, datum) => {
441
- // event.stopPropagation()
442
- // console.log({
443
- // type: 'multiValue',
444
- // eventName: 'mouseover',
445
- // pluginName,
446
- // highlightTarget: data.highlightTarget,
447
- // datum,
448
- // category: data.CategoryDataMap.get(datum.categoryLabel)!,
449
- // categoryIndex: datum.categoryIndex,
450
- // categoryLabel: datum.categoryLabel,
451
- // data: data.computedData,
452
- // event,
453
- // })
454
- subject.event$.next({
455
- type: 'multiValue',
456
- eventName: 'mouseover',
457
- pluginName,
458
- highlightTarget: data.highlightTarget,
459
- datum,
460
- category: data.CategoryDataMap.get(datum.categoryLabel)!,
461
- categoryIndex: datum.categoryIndex,
462
- categoryLabel: datum.categoryLabel,
463
- data: data.computedData,
464
- event,
465
- })
466
- })
467
- .on('mousemove', (event, datum) => {
468
- // event.stopPropagation()
469
-
470
- subject.event$.next({
471
- type: 'multiValue',
472
- eventName: 'mousemove',
473
- pluginName,
474
- highlightTarget: data.highlightTarget,
475
- datum,
476
- category: data.CategoryDataMap.get(datum.categoryLabel)!,
477
- categoryIndex: datum.categoryIndex,
478
- categoryLabel: datum.categoryLabel,
479
- data: data.computedData,
480
- event,
481
- })
482
- })
483
- .on('mouseout', (event, datum) => {
484
- // event.stopPropagation()
485
-
486
- subject.event$.next({
487
- type: 'multiValue',
488
- eventName: 'mouseout',
489
- pluginName,
490
- highlightTarget: data.highlightTarget,
491
- datum,
492
- category: data.CategoryDataMap.get(datum.categoryLabel)!,
493
- categoryIndex: datum.categoryIndex,
494
- categoryLabel: datum.categoryLabel,
495
- data: data.computedData,
496
- event,
497
- })
498
- })
499
- .on('click', (event, datum) => {
500
- // event.stopPropagation()
501
-
502
- subject.event$.next({
503
- type: 'multiValue',
504
- eventName: 'click',
505
- pluginName,
506
- highlightTarget: data.highlightTarget,
507
- datum,
508
- category: data.CategoryDataMap.get(datum.categoryLabel)!,
509
- categoryIndex: datum.categoryIndex,
510
- categoryLabel: datum.categoryLabel,
511
- data: data.computedData,
512
- event,
513
- })
514
- })
515
-
516
- })
517
-
518
- combineLatest({
519
- graphicSelection: graphicSelection$,
520
- highlight: observer.multiValueHighlight$.pipe(
521
- map(data => data.map(d => d.id))
522
- ),
523
- fullChartParams: observer.fullChartParams$
524
- }).pipe(
525
- takeUntil(destroy$),
526
- switchMap(async d => d)
527
- ).subscribe(data => {
528
- highlightBubbles({
529
- selection: data.graphicSelection,
530
- ids: data.highlight,
531
- fullChartParams: data.fullChartParams
532
- })
533
- })
534
-
535
- // graphicGSelection$.subscribe(data => {
536
- // console.log('graphicGSelection$', data)
537
- // })
538
-
539
- // observer.visibleComputedLayoutData$.subscribe(data => {
540
- // console.log('visibleComputedLayoutData$', data)
541
- // })
542
-
543
- // observer.fullChartParams$.subscribe(data => {
544
- // console.log('fullChartParams$', data)
545
- // })
546
-
547
- // observer.fullParams$.subscribe(data => {
548
- // console.log('fullParams$', data)
549
- // })
550
-
551
- return () => {
552
- destroy$.next(undefined)
553
- }
554
- })
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ filter,
6
+ switchMap,
7
+ takeUntil,
8
+ distinctUntilChanged,
9
+ Observable,
10
+ Subject,
11
+ share,
12
+ shareReplay
13
+ } from 'rxjs'
14
+ import type {
15
+ ComputedDatumMultiValue,
16
+ ComputedDataMultiValue,
17
+ ComputedLayoutDatumMultiValue,
18
+ ComputedLayoutDataMultiValue,
19
+ DefinePluginConfig,
20
+ EventMultiValue,
21
+ ChartParams,
22
+ ContainerPositionScaled,
23
+ Layout,
24
+ TransformData,
25
+ ColorType
26
+ } from '../../../lib/core-types'
27
+ import {
28
+ defineMultiValuePlugin,
29
+ getMinMax
30
+ } from '../../../lib/core'
31
+ import type { ScatterBubblesParams } from '../../../lib/plugins-basic-types'
32
+ import { DEFAULT_SCATTER_BUBBLES_PARAMS } from '../defaults'
33
+ import { LAYER_INDEX_OF_GRAPHIC_COVER } from '../../const'
34
+ import { getDatumColor, getClassName, getUniID } from '../../utils/orbchartsUtils'
35
+ import { multiValueSelectionsObservable } from '../multiValueObservables'
36
+
37
+ type ClipPathDatum = {
38
+ id: string;
39
+ // x: number;
40
+ // y: number;
41
+ width: number;
42
+ height: number;
43
+ }
44
+
45
+ interface BubbleDatum extends ComputedLayoutDatumMultiValue {
46
+ r: number
47
+ opacity: number
48
+ }
49
+
50
+ const pluginName = 'ScatterBubbles'
51
+
52
+ // 調整係數 - 因為圓和圓之間的空隙造成聚合起來的大圓會略大,所以稍作微調
53
+ const adjustmentFactor = 0.9
54
+
55
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_SCATTER_BUBBLES_PARAMS> = {
56
+ name: pluginName,
57
+ defaultParams: DEFAULT_SCATTER_BUBBLES_PARAMS,
58
+ layerIndex: LAYER_INDEX_OF_GRAPHIC_COVER,
59
+ validator: (params, { validateColumns }) => {
60
+ const result = validateColumns(params, {
61
+ // radius: {
62
+ // toBeTypes: ['number']
63
+ // },
64
+ fillColorType: {
65
+ toBeOption: 'ColorType',
66
+ },
67
+ strokeColorType: {
68
+ toBeOption: 'ColorType',
69
+ },
70
+ strokeWidth: {
71
+ toBeTypes: ['number']
72
+ },
73
+ valueLinearOpacity: {
74
+ toBeTypes: ['number[]']
75
+ },
76
+ arcScaleType: {
77
+ toBe: 'ArcScaleType',
78
+ test: (value) => {
79
+ return value === 'area' || value === 'radius'
80
+ }
81
+ },
82
+ sizeAdjust: {
83
+ toBeTypes: ['number']
84
+ }
85
+ })
86
+ return result
87
+ }
88
+ }
89
+
90
+ function renderDots ({ graphicGSelection, circleGClassName, circleClassName, bubbleData, fullParams, fullChartParams, graphicReverseScale }: {
91
+ graphicGSelection: d3.Selection<SVGGElement, any, any, any>
92
+ circleGClassName: string
93
+ circleClassName: string
94
+ // visibleComputedLayoutData: ComputedLayoutDataMultiValue
95
+ bubbleData: BubbleDatum[][]
96
+ fullParams: ScatterBubblesParams
97
+ fullChartParams: ChartParams
98
+ graphicReverseScale: [number, number][]
99
+ }) {
100
+ const createEnterDuration = (enter: d3.Selection<d3.EnterElement, ComputedDatumMultiValue, SVGGElement, any>) => {
101
+ const enterSize = enter.size()
102
+ const eachDuration = fullChartParams.transitionDuration / enterSize
103
+ return eachDuration
104
+ }
105
+ // enterDuration
106
+ let enterDuration = 0
107
+
108
+ graphicGSelection
109
+ .each((categoryData, categoryIndex, g) => {
110
+ d3.select(g[categoryIndex])
111
+ .selectAll<SVGGElement, ComputedDatumMultiValue>('g')
112
+ .data(bubbleData[categoryIndex], d => d.id)
113
+ .join(
114
+ enter => {
115
+ // enterDuration
116
+ enterDuration = createEnterDuration(enter)
117
+
118
+ return enter
119
+ .append('g')
120
+ .classed(circleGClassName, true)
121
+ },
122
+ update => update,
123
+ exit => exit.remove()
124
+ )
125
+ .attr('transform', d => `translate(${d.axisX}, ${d.axisY})`)
126
+ .each((d, i, g) => {
127
+ const circle = d3.select(g[i])
128
+ .selectAll('circle')
129
+ .data([d])
130
+ .join(
131
+ enter => {
132
+ return enter
133
+ .append('circle')
134
+ .style('cursor', 'pointer')
135
+ .style('vector-effect', 'non-scaling-stroke')
136
+ .classed(circleClassName, true)
137
+ .attr('opacity', 0)
138
+ .transition()
139
+ .delay((_d, _i) => {
140
+ return i * enterDuration
141
+ })
142
+ .attr('opacity', 0.8)
143
+ },
144
+ update => {
145
+ return update
146
+ .transition()
147
+ .duration(50)
148
+ // .attr('cx', d => d.axisX)
149
+ // .attr('cy', d => d.axisY)
150
+ .attr('opacity', 0.8)
151
+ },
152
+ exit => exit.remove()
153
+ )
154
+ .attr('r', d => d.r)
155
+ .attr('fill', (d, i) => getDatumColor({ datum: d, colorType: fullParams.fillColorType, fullChartParams }))
156
+ .attr('stroke', (d, i) => getDatumColor({ datum: d, colorType: fullParams.strokeColorType, fullChartParams }))
157
+ .attr('stroke-width', fullParams.strokeWidth)
158
+ .attr('transform', `scale(${graphicReverseScale[categoryIndex][0] ?? 1}, ${graphicReverseScale[categoryIndex][1] ?? 1})`)
159
+ })
160
+ })
161
+
162
+ const graphicCircleSelection: d3.Selection<SVGRectElement, BubbleDatum, SVGGElement, unknown> = graphicGSelection.selectAll(`circle.${circleClassName}`)
163
+
164
+ return graphicCircleSelection
165
+ }
166
+
167
+
168
+ function highlightBubbles ({ selection, ids, fullChartParams }: {
169
+ selection: d3.Selection<SVGGElement, BubbleDatum, any, any>
170
+ ids: string[]
171
+ fullChartParams: ChartParams
172
+ }) {
173
+ selection.interrupt('highlight')
174
+ if (!ids.length) {
175
+ // remove highlight
176
+ selection
177
+ .transition('highlight')
178
+ .duration(200)
179
+ .style('opacity', d => d.opacity)
180
+ // selection
181
+ // .attr('stroke-width', fullParams.strokeWidth)
182
+
183
+ return
184
+ }
185
+
186
+ selection
187
+ .each((d, i, n) => {
188
+ if (ids.includes(d.id)) {
189
+ const dot = d3.select<SVGGElement, BubbleDatum>(n[i])
190
+ dot
191
+ .style('opacity', d => d.opacity)
192
+ .transition('highlight')
193
+ .duration(200)
194
+ // dot
195
+ // .attr('stroke-width', fullParams.strokeWidthWhileHighlight)
196
+ } else {
197
+ const dot = d3.select(n[i])
198
+ dot
199
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
200
+ .transition('highlight')
201
+ .duration(200)
202
+ // dot
203
+ // .attr('stroke-width', fullParams.strokeWidth)
204
+
205
+ }
206
+ })
207
+ }
208
+
209
+ function renderClipPath ({ defsSelection, clipPathData }: {
210
+ defsSelection: d3.Selection<SVGDefsElement, any, any, any>
211
+ clipPathData: ClipPathDatum[]
212
+ }) {
213
+ const clipPath = defsSelection
214
+ .selectAll<SVGClipPathElement, Layout>('clipPath')
215
+ .data(clipPathData)
216
+ .join(
217
+ enter => {
218
+ return enter
219
+ .append('clipPath')
220
+ },
221
+ update => update,
222
+ exit => exit.remove()
223
+ )
224
+ .attr('id', d => d.id)
225
+ .each((d, i, g) => {
226
+ const rect = d3.select(g[i])
227
+ .selectAll<SVGRectElement, typeof d>('rect')
228
+ .data([d])
229
+ .join('rect')
230
+ .attr('x', 0)
231
+ .attr('y', 0)
232
+ .attr('width', _d => _d.width)
233
+ .attr('height', _d => _d.height)
234
+ })
235
+
236
+ }
237
+
238
+
239
+ export const ScatterBubbles = defineMultiValuePlugin(pluginConfig)(({ selection, name, subject, observer }) => {
240
+
241
+ const destroy$ = new Subject()
242
+
243
+ const clipPathID = getUniID(pluginName, 'clipPath-box')
244
+ const circleGClassName = getClassName(pluginName, 'circleG')
245
+ const circleClassName = getClassName(pluginName, 'circle')
246
+
247
+ const {
248
+ categorySelection$,
249
+ axesSelection$,
250
+ defsSelection$,
251
+ graphicGSelection$
252
+ } = multiValueSelectionsObservable({
253
+ selection,
254
+ pluginName,
255
+ clipPathID,
256
+ categoryLabels$: observer.categoryLabels$,
257
+ multiValueContainerPosition$: observer.multiValueContainerPosition$,
258
+ multiValueGraphicTransform$: observer.multiValueGraphicTransform$
259
+ })
260
+
261
+ const graphicReverseScale$: Observable<[number, number][]> = combineLatest({
262
+ computedData: observer.computedData$,
263
+ multiValueGraphicReverseScale: observer.multiValueGraphicReverseScale$
264
+ }).pipe(
265
+ takeUntil(destroy$),
266
+ switchMap(async data => data),
267
+ map(data => {
268
+ return data.computedData.map((series, categoryIndex) => {
269
+ return data.multiValueGraphicReverseScale[categoryIndex]
270
+ })
271
+ })
272
+ )
273
+
274
+ const clipPathSubscription = combineLatest({
275
+ defsSelection: defsSelection$,
276
+ layout: observer.layout$,
277
+ }).pipe(
278
+ takeUntil(destroy$),
279
+ switchMap(async (d) => d),
280
+ ).subscribe(data => {
281
+ // 外層的遮罩
282
+ const clipPathData = [{
283
+ id: clipPathID,
284
+ width: data.layout.width,
285
+ height: data.layout.height
286
+ }]
287
+ renderClipPath({
288
+ defsSelection: data.defsSelection,
289
+ clipPathData,
290
+ })
291
+ })
292
+
293
+ const filteredValueList$ = observer.filteredMinMaxXYData$.pipe(
294
+ takeUntil(destroy$),
295
+ map(data => data.datumList.flat().map(d => d.value[2] ?? 0)),
296
+ shareReplay(1)
297
+ )
298
+
299
+ const filteredMinMaxValue$ = observer.filteredMinMaxXYData$.pipe(
300
+ takeUntil(destroy$),
301
+ map(data => {
302
+ return getMinMax(data.datumList.flat().map(d => d.value[2] ?? 0))
303
+ })
304
+ )
305
+
306
+ const opacityScale$ = combineLatest({
307
+ filteredMinMaxValue: filteredMinMaxValue$,
308
+ fullParams: observer.fullParams$
309
+ }).pipe(
310
+ takeUntil(destroy$),
311
+ switchMap(async (d) => d),
312
+ map(data => {
313
+ return d3.scaleLinear()
314
+ .domain(data.filteredMinMaxValue)
315
+ .range(data.fullParams.valueLinearOpacity)
316
+ })
317
+ )
318
+
319
+ // 虛擬大圓(所有小圓聚合起來的大圓)的半徑
320
+ const totalR$ = combineLatest({
321
+ layout: observer.layout$,
322
+ fullParams: observer.fullParams$
323
+ }).pipe(
324
+ takeUntil(destroy$),
325
+ map(data => {
326
+ // 場景最短邊
327
+ const fullRadius = Math.min(...[data.layout.width, data.layout.height]) / 2
328
+ return fullRadius * data.fullParams.sizeAdjust
329
+ })
330
+ )
331
+
332
+ const totalValue$ = filteredValueList$.pipe(
333
+ takeUntil(destroy$),
334
+ map(data => {
335
+ return data.reduce((acc, current) => acc + current, 0)
336
+ }),
337
+ filter(data => data > 0) // 避免後續計算scale的時候發生問題
338
+ )
339
+
340
+ const radiusScale$ = combineLatest({
341
+ totalR: totalR$,
342
+ totalValue: totalValue$,
343
+ fullParams: observer.fullParams$
344
+ }).pipe(
345
+ takeUntil(destroy$),
346
+ switchMap(async (d) => d),
347
+ map(data => {
348
+ // console.log({ totalR: data.totalR, totalValue: data.totalValue })
349
+ return d3.scalePow()
350
+ .domain([0, data.totalValue])
351
+ .range([0, data.totalR])
352
+ .exponent(data.fullParams.arcScaleType === 'area'
353
+ ? 0.5 // 數值映射面積(0.5為取平方根)
354
+ : 1 // 數值映射半徑
355
+ )
356
+ })
357
+ )
358
+
359
+ const scaleFactor$ = combineLatest({
360
+ radiusScale: radiusScale$,
361
+ fullParams: observer.fullParams$,
362
+ totalR: totalR$,
363
+ filteredValueList: filteredValueList$
364
+ }).pipe(
365
+ takeUntil(destroy$),
366
+ switchMap(async (d) => d),
367
+ map(data => {
368
+ return data.fullParams.arcScaleType === 'area'
369
+ ? 1
370
+ // 當數值映射半徑時,多個小圓的總面積會小於大圓的面積,所以要計算縮放比例
371
+ : (() => {
372
+ const totalArea = data.totalR * data.totalR * Math.PI
373
+ return Math.sqrt(totalArea / d3.sum(data.filteredValueList, d => Math.PI * Math.pow(data.radiusScale(d), 2)))
374
+ })()
375
+ })
376
+ )
377
+
378
+ const bubbleData$ = combineLatest({
379
+ computedLayoutData: observer.computedLayoutData$,
380
+ opacityScale: opacityScale$,
381
+ radiusScale: radiusScale$,
382
+ scaleFactor: scaleFactor$,
383
+ fullParams: observer.fullParams$
384
+ }).pipe(
385
+ takeUntil(destroy$),
386
+ switchMap(async (d) => d),
387
+ map(data => {
388
+ return data.computedLayoutData.map(category => {
389
+ return category.map(_d => {
390
+ const d: BubbleDatum = _d as BubbleDatum
391
+ d.r = data.radiusScale(d.value[2]) * data.scaleFactor * adjustmentFactor
392
+ d.opacity = data.opacityScale(d.value[2])
393
+ return d
394
+ })
395
+ })
396
+ })
397
+ )
398
+
399
+
400
+ const graphicSelection$ = combineLatest({
401
+ graphicGSelection: graphicGSelection$,
402
+ // visibleComputedLayoutData: observer.visibleComputedLayoutData$,
403
+ bubbleData: bubbleData$,
404
+ graphicReverseScale: graphicReverseScale$,
405
+ fullChartParams: observer.fullChartParams$,
406
+ fullParams: observer.fullParams$,
407
+ }).pipe(
408
+ takeUntil(destroy$),
409
+ switchMap(async (d) => d),
410
+ map(data => {
411
+ return renderDots({
412
+ graphicGSelection: data.graphicGSelection,
413
+ circleGClassName,
414
+ circleClassName,
415
+ bubbleData: data.bubbleData,
416
+ fullParams: data.fullParams,
417
+ fullChartParams: data.fullChartParams,
418
+ graphicReverseScale: data.graphicReverseScale
419
+ })
420
+ })
421
+ )
422
+
423
+ const highlightTarget$ = observer.fullChartParams$.pipe(
424
+ takeUntil(destroy$),
425
+ map(d => d.highlightTarget),
426
+ distinctUntilChanged()
427
+ )
428
+
429
+ combineLatest({
430
+ graphicSelection: graphicSelection$,
431
+ computedData: observer.computedData$,
432
+ CategoryDataMap: observer.CategoryDataMap$,
433
+ highlightTarget: highlightTarget$
434
+ }).pipe(
435
+ takeUntil(destroy$),
436
+ switchMap(async (d) => d),
437
+ ).subscribe(data => {
438
+
439
+ data.graphicSelection
440
+ .on('mouseover', (event, datum) => {
441
+ // event.stopPropagation()
442
+ // console.log({
443
+ // type: 'multiValue',
444
+ // eventName: 'mouseover',
445
+ // pluginName,
446
+ // highlightTarget: data.highlightTarget,
447
+ // datum,
448
+ // category: data.CategoryDataMap.get(datum.categoryLabel)!,
449
+ // categoryIndex: datum.categoryIndex,
450
+ // categoryLabel: datum.categoryLabel,
451
+ // data: data.computedData,
452
+ // event,
453
+ // })
454
+ subject.event$.next({
455
+ type: 'multiValue',
456
+ eventName: 'mouseover',
457
+ pluginName,
458
+ highlightTarget: data.highlightTarget,
459
+ datum,
460
+ category: data.CategoryDataMap.get(datum.categoryLabel)!,
461
+ categoryIndex: datum.categoryIndex,
462
+ categoryLabel: datum.categoryLabel,
463
+ data: data.computedData,
464
+ event,
465
+ })
466
+ })
467
+ .on('mousemove', (event, datum) => {
468
+ // event.stopPropagation()
469
+
470
+ subject.event$.next({
471
+ type: 'multiValue',
472
+ eventName: 'mousemove',
473
+ pluginName,
474
+ highlightTarget: data.highlightTarget,
475
+ datum,
476
+ category: data.CategoryDataMap.get(datum.categoryLabel)!,
477
+ categoryIndex: datum.categoryIndex,
478
+ categoryLabel: datum.categoryLabel,
479
+ data: data.computedData,
480
+ event,
481
+ })
482
+ })
483
+ .on('mouseout', (event, datum) => {
484
+ // event.stopPropagation()
485
+
486
+ subject.event$.next({
487
+ type: 'multiValue',
488
+ eventName: 'mouseout',
489
+ pluginName,
490
+ highlightTarget: data.highlightTarget,
491
+ datum,
492
+ category: data.CategoryDataMap.get(datum.categoryLabel)!,
493
+ categoryIndex: datum.categoryIndex,
494
+ categoryLabel: datum.categoryLabel,
495
+ data: data.computedData,
496
+ event,
497
+ })
498
+ })
499
+ .on('click', (event, datum) => {
500
+ // event.stopPropagation()
501
+
502
+ subject.event$.next({
503
+ type: 'multiValue',
504
+ eventName: 'click',
505
+ pluginName,
506
+ highlightTarget: data.highlightTarget,
507
+ datum,
508
+ category: data.CategoryDataMap.get(datum.categoryLabel)!,
509
+ categoryIndex: datum.categoryIndex,
510
+ categoryLabel: datum.categoryLabel,
511
+ data: data.computedData,
512
+ event,
513
+ })
514
+ })
515
+
516
+ })
517
+
518
+ combineLatest({
519
+ graphicSelection: graphicSelection$,
520
+ highlight: observer.multiValueHighlight$.pipe(
521
+ map(data => data.map(d => d.id))
522
+ ),
523
+ fullChartParams: observer.fullChartParams$
524
+ }).pipe(
525
+ takeUntil(destroy$),
526
+ switchMap(async d => d)
527
+ ).subscribe(data => {
528
+ highlightBubbles({
529
+ selection: data.graphicSelection,
530
+ ids: data.highlight,
531
+ fullChartParams: data.fullChartParams
532
+ })
533
+ })
534
+
535
+ // graphicGSelection$.subscribe(data => {
536
+ // console.log('graphicGSelection$', data)
537
+ // })
538
+
539
+ // observer.visibleComputedLayoutData$.subscribe(data => {
540
+ // console.log('visibleComputedLayoutData$', data)
541
+ // })
542
+
543
+ // observer.fullChartParams$.subscribe(data => {
544
+ // console.log('fullChartParams$', data)
545
+ // })
546
+
547
+ // observer.fullParams$.subscribe(data => {
548
+ // console.log('fullParams$', data)
549
+ // })
550
+
551
+ return () => {
552
+ destroy$.next(undefined)
553
+ }
554
+ })