@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,517 +1,517 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- filter,
6
- switchMap,
7
- takeUntil,
8
- distinctUntilChanged,
9
- shareReplay,
10
- Observable,
11
- Subject,
12
- BehaviorSubject } from 'rxjs'
13
- import type { DefinePluginConfig } from '../../../lib/core-types'
14
- import type {
15
- ComputedDataSeries,
16
- ComputedDatumSeries,
17
- ContainerPosition,
18
- ChartParams,
19
- EventSeries,
20
- Layout } from '../../../lib/core-types'
21
- import type { D3PieDatum } from '../seriesUtils'
22
- import type { RoseParams } from '../../../lib/plugins-basic-types'
23
- import {
24
- defineSeriesPlugin } from '../../../lib/core'
25
- import { DEFAULT_ROSE_PARAMS } from '../defaults'
26
- // import { makePieData } from '../seriesUtils'
27
- // import { getD3TransitionEase, makeD3Arc } from '../../utils/d3Utils'
28
- import { getDatumColor, getClassName } from '../../utils/orbchartsUtils'
29
- import { seriesCenterSelectionObservable } from '../seriesObservables'
30
- import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
31
-
32
- // @Q@ 暫時先寫在這裡,之後pie一起重構後再放到seriesUtils
33
- export interface PieDatum extends D3PieDatum {
34
- data: ComputedDatumSeries
35
- id: string
36
- prevValue: number // 補間動畫用的(前次資料的value)
37
- }
38
-
39
- const pluginName = 'Rose'
40
-
41
- const roseInnerRadius = 0
42
- const roseStartAngle = 0
43
- const roseEndAngle = Math.PI * 2
44
-
45
- const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_ROSE_PARAMS> = {
46
- name: pluginName,
47
- defaultParams: DEFAULT_ROSE_PARAMS,
48
- layerIndex: LAYER_INDEX_OF_GRAPHIC,
49
- validator: (params, { validateColumns }) => {
50
- const result = validateColumns(params, {
51
- outerRadius: {
52
- toBeTypes: ['number'],
53
- },
54
- padAngle: {
55
- toBeTypes: ['number'],
56
- },
57
- strokeColorType: {
58
- toBeOption: 'ColorType'
59
- },
60
- strokeWidth: {
61
- toBeTypes: ['number']
62
- },
63
- cornerRadius: {
64
- toBeTypes: ['number']
65
- },
66
- arcScaleType: {
67
- toBe: '"area" | "radius"',
68
- test: (value: any) => value === 'area' || value === 'radius'
69
- },
70
- angleIncreaseWhileHighlight: {
71
- toBeTypes: ['number']
72
- }
73
- })
74
- return result
75
- }
76
- }
77
-
78
- function makeTweenArcFn ({ cornerRadius, outerRadius, axisWidth, maxValue, arcScaleType, fullParams }: {
79
- // interpolateRadius: (t: number) => number
80
- outerRadius: number
81
- cornerRadius: number
82
- axisWidth: number
83
- maxValue: number
84
- arcScaleType: 'radius' | 'area'
85
- fullParams: RoseParams
86
- }): (d: PieDatum) => (t: number) => string {
87
-
88
- const outerRadiusWidth = (axisWidth / 2) * outerRadius
89
-
90
- // const arcScale = d3.scaleLinear()
91
- // .domain([0, maxValue])
92
- // .range([0, outerRadiusWidth])
93
-
94
- const exponent = arcScaleType === 'area'
95
- ? 0.5 // 比例映射面積(0.5為取平方根)
96
- : 1 // 比例映射半徑
97
-
98
- const arcScale = d3.scalePow()
99
- .domain([0, maxValue])
100
- .range([0, outerRadiusWidth])
101
- .exponent(exponent)
102
-
103
- return (d: PieDatum) => {
104
- const prevEachOuterRadius = arcScale(d.prevValue)!
105
- const eachOuterRadius = arcScale(d.value)!
106
-
107
- const interpolateRadius = d3.interpolate(prevEachOuterRadius, eachOuterRadius)
108
-
109
- return (t: number) => {
110
-
111
- const outerRadius = interpolateRadius(t)
112
-
113
- const arc = d3.arc()
114
- .innerRadius(0)
115
- .outerRadius(outerRadius)
116
- .padAngle(fullParams.padAngle)
117
- .padRadius(outerRadius)
118
- .cornerRadius(cornerRadius)
119
-
120
- return arc(d as any)
121
- }
122
- }
123
- }
124
-
125
- // function renderPie ({ selection, data, tweenArc, transitionDuration, pathClassName }: {
126
- // selection: d3.Selection<SVGGElement, unknown, any, unknown>
127
- // data: PieDatum[]
128
- // // arc: d3.Arc<any, d3.DefaultArcObject>
129
- // tweenArc: (d: PieDatum) => (t: number) => string
130
- // transitionDuration: number
131
- // pathClassName: string
132
- // }): d3.Selection<SVGPathElement, PieDatum, any, any> {
133
- // // console.log('data', data)
134
- // const pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any> = selection
135
- // .selectAll<SVGPathElement, PieDatum>('path')
136
- // .data(data, d => d.id)
137
- // .join('path')
138
- // .classed(pathClassName, true)
139
- // .style('cursor', 'pointer')
140
- // .attr('fill', (d, i) => d.data.color)
141
- // pathSelection
142
- // .transition('graphicMove')
143
- // .duration(transitionDuration)
144
- // .attrTween('d', tweenArc)
145
-
146
- // return pathSelection
147
- // }
148
-
149
- function highlight ({ pathSelection, ids, fullParams, fullChartParams, tweenArc }: {
150
- pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any>
151
- ids: string[]
152
- fullParams: RoseParams
153
- fullChartParams: ChartParams
154
- // arc: d3.Arc<any, d3.DefaultArcObject>
155
- tweenArc: (d: PieDatum) => (t: number) => string
156
- }) {
157
- pathSelection.interrupt('highlight')
158
-
159
- if (!ids.length) {
160
- // 取消放大
161
- pathSelection
162
- .transition('highlight')
163
- .style('opacity', 1)
164
- .attr('d', (d: PieDatum) => {
165
- return tweenArc(d)(1)
166
- })
167
- return
168
- }
169
-
170
- pathSelection.each((d, i, n) => {
171
- const segment = d3.select(n[i])
172
-
173
- if (ids.includes(d.data.id)) {
174
- segment
175
- .style('opacity', 1)
176
- .transition('highlight')
177
- .ease(d3.easeElastic)
178
- .duration(500)
179
- // .attr('d', (d: any) => {
180
- // return arc!({
181
- // ...d,
182
- // startAngle: d.startAngle - 0.5,
183
- // endAngle: d.endAngle + 0.5
184
- // })
185
- // })
186
- .attr('d', (d: PieDatum) => {
187
- return tweenArc({
188
- ...d,
189
- startAngle: d.startAngle - fullParams.angleIncreaseWhileHighlight,
190
- endAngle: d.endAngle + fullParams.angleIncreaseWhileHighlight
191
- })(1)
192
- })
193
- // .on('interrupt', () => {
194
- // // this.pathSelection!.select('path').attr('d', (d) => {
195
- // // return this.arc!(d as any)
196
- // // })
197
- // this.initHighlight()
198
- // })
199
- } else {
200
- // 取消放大
201
- segment
202
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
203
- .transition('highlight')
204
- .attr('d', (d: PieDatum) => {
205
- return tweenArc(d)(1)
206
- })
207
- }
208
- })
209
- }
210
-
211
- // 各別的pie
212
- function createEachRose (pluginName: string, context: {
213
- containerSelection: d3.Selection<SVGGElement, any, any, unknown>
214
- computedData$: Observable<ComputedDatumSeries[][]>
215
- visibleComputedData$: Observable<ComputedDatumSeries[][]>
216
- visibleComputedLayoutData$: Observable<ComputedDatumSeries[][]>
217
- containerVisibleComputedLayoutData$: Observable<ComputedDatumSeries[]>
218
- SeriesDataMap$: Observable<Map<string, ComputedDatumSeries[]>>
219
- fullParams$: Observable<RoseParams>
220
- fullChartParams$: Observable<ChartParams>
221
- seriesHighlight$: Observable<ComputedDatumSeries[]>
222
- seriesContainerPosition$: Observable<ContainerPosition>
223
- event$: Subject<EventSeries>
224
- }) {
225
- const destroy$ = new Subject()
226
-
227
- const pathClassName = getClassName(pluginName, 'path')
228
-
229
- let lastPieData: PieDatum[] = [] // 紀錄補間動畫前次的資料
230
-
231
- const shorterSideWith$ = context.seriesContainerPosition$.pipe(
232
- takeUntil(destroy$),
233
- map(d => d.width < d.height ? d.width : d.height),
234
- distinctUntilChanged()
235
- )
236
-
237
- const pieData$: Observable<PieDatum[]> = combineLatest({
238
- containerVisibleComputedLayoutData: context.containerVisibleComputedLayoutData$,
239
- fullParams: context.fullParams$,
240
- }).pipe(
241
- takeUntil(destroy$),
242
- switchMap(async (d) => d),
243
- map(data => {
244
- const eachAngle = roseEndAngle / data.containerVisibleComputedLayoutData.length
245
- return data.containerVisibleComputedLayoutData.map((d, i) => {
246
- return {
247
- id: d.id,
248
- data: d,
249
- index: i,
250
- value: d.value,
251
- startAngle: eachAngle * i,
252
- endAngle: eachAngle * (i + 1),
253
- padAngle: data.fullParams.padAngle,
254
- prevValue: (lastPieData[i] && lastPieData[i].id === d.id) ? lastPieData[i].value : 0
255
- }
256
- })
257
- })
258
- )
259
-
260
- const highlightTarget$ = context.fullChartParams$.pipe(
261
- takeUntil(destroy$),
262
- map(d => d.highlightTarget),
263
- distinctUntilChanged()
264
- )
265
-
266
- const maxValue$ = context.visibleComputedLayoutData$.pipe(
267
- map(data => Math.max(...data.flat().map(d => d.value))),
268
- distinctUntilChanged()
269
- )
270
-
271
- // context.visibleComputedLayoutData$.subscribe(data => {
272
- // console.log('visibleComputedLayoutData$', data)
273
- // })
274
-
275
- const tweenArc$ = combineLatest({
276
- fullParams: context.fullParams$,
277
- axisWidth: shorterSideWith$,
278
- maxValue: maxValue$
279
- }).pipe(
280
- takeUntil(destroy$),
281
- switchMap(async d => d),
282
- map((data) => {
283
- return makeTweenArcFn({
284
- cornerRadius: data.fullParams.cornerRadius,
285
- outerRadius: data.fullParams.outerRadius,
286
- axisWidth: data.axisWidth,
287
- maxValue: data.maxValue,
288
- arcScaleType: data.fullParams.arcScaleType,
289
- fullParams: data.fullParams
290
- })
291
- })
292
- )
293
-
294
- const transitionDuration$ = context.fullChartParams$.pipe(
295
- takeUntil(destroy$),
296
- map(d => d.transitionDuration),
297
- distinctUntilChanged()
298
- )
299
-
300
- // 是否在transition中
301
- const isTransitionMoving$ = new BehaviorSubject<boolean>(false)
302
-
303
- const pathSelection$ = new Observable<d3.Selection<SVGPathElement, PieDatum, any, any>>(subscriber => {
304
- combineLatest({
305
- pieData: pieData$,
306
- tweenArc: tweenArc$,
307
- transitionDuration: transitionDuration$,
308
- fullParams: context.fullParams$,
309
- fullChartParams: context.fullChartParams$
310
- }).pipe(
311
- takeUntil(destroy$),
312
- switchMap(async d => d)
313
- ).subscribe(data => {
314
- const pieData = data.pieData.map((d, i) => {
315
- d.prevValue = (lastPieData[i] && lastPieData[i].id === d.id) ? lastPieData[i].value : 0
316
- return d
317
- })
318
-
319
- isTransitionMoving$.next(true)
320
-
321
- const pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any> = context.containerSelection
322
- .selectAll<SVGPathElement, PieDatum>('path')
323
- .data(pieData, d => d.id)
324
- .join('path')
325
- .classed(pathClassName, true)
326
- .style('cursor', 'pointer')
327
- .attr('fill', (d, i) => d.data.color)
328
- .attr('stroke', (d, i) => getDatumColor({
329
- datum: d.data,
330
- colorType: data.fullParams.strokeColorType,
331
- fullChartParams: data.fullChartParams
332
- }))
333
- .attr('stroke-width', data.fullParams.strokeWidth)
334
- pathSelection.interrupt('graphicMove')
335
- pathSelection
336
- .transition('graphicMove')
337
- .duration(data.transitionDuration)
338
- .attrTween('d', data.tweenArc)
339
- .on('end', () => {
340
- subscriber.next(pathSelection)
341
-
342
- isTransitionMoving$.next(false)
343
- // lastPieData = Object.assign([], data.pieData)
344
- // console.log('lastPieData', lastPieData)
345
- })
346
- lastPieData = Object.assign([], pieData)
347
-
348
- })
349
- }).pipe(
350
- shareReplay(1)
351
- )
352
-
353
- combineLatest({
354
- pathSelection: pathSelection$,
355
- SeriesDataMap: context.SeriesDataMap$,
356
- computedData: context.computedData$,
357
- highlightTarget: highlightTarget$
358
- }).pipe(
359
- takeUntil(destroy$),
360
- switchMap(async d => d)
361
- ).subscribe(data => {
362
- data.pathSelection
363
- .on('mouseover', (event, pieDatum) => {
364
- event.stopPropagation()
365
-
366
- context.event$.next({
367
- type: 'series',
368
- eventName: 'mouseover',
369
- pluginName,
370
- highlightTarget: data.highlightTarget,
371
- datum: pieDatum.data,
372
- series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
373
- seriesIndex: pieDatum.data.seriesIndex,
374
- seriesLabel: pieDatum.data.seriesLabel,
375
- event,
376
- data: data.computedData
377
- })
378
- })
379
- .on('mousemove', (event, pieDatum) => {
380
- event.stopPropagation()
381
-
382
- context.event$.next({
383
- type: 'series',
384
- eventName: 'mousemove',
385
- pluginName,
386
- highlightTarget: data.highlightTarget,
387
- datum: pieDatum.data,
388
- series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
389
- seriesIndex: pieDatum.data.seriesIndex,
390
- seriesLabel: pieDatum.data.seriesLabel,
391
- event,
392
- data: data.computedData,
393
- })
394
- })
395
- .on('mouseout', (event, pieDatum) => {
396
- event.stopPropagation()
397
-
398
- context.event$.next({
399
- type: 'series',
400
- eventName: 'mouseout',
401
- pluginName,
402
- highlightTarget: data.highlightTarget,
403
- datum: pieDatum.data,
404
- series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
405
- seriesIndex: pieDatum.data.seriesIndex,
406
- seriesLabel: pieDatum.data.seriesLabel,
407
- event,
408
- data: data.computedData,
409
- })
410
- })
411
- .on('click', (event, pieDatum) => {
412
- event.stopPropagation()
413
-
414
- context.event$.next({
415
- type: 'series',
416
- eventName: 'click',
417
- pluginName,
418
- highlightTarget: data.highlightTarget,
419
- datum: pieDatum.data,
420
- series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
421
- seriesIndex: pieDatum.data.seriesIndex,
422
- seriesLabel: pieDatum.data.seriesLabel,
423
- event,
424
- data: data.computedData,
425
- })
426
- })
427
- })
428
-
429
- combineLatest({
430
- pathSelection: pathSelection$,
431
- highlight: context.seriesHighlight$.pipe(
432
- map(data => data.map(d => d.id))
433
- ),
434
- fullParams: context.fullParams$,
435
- fullChartParams: context.fullChartParams$,
436
- // arc: arc$,
437
- tweenArc: tweenArc$,
438
- isTransitionMoving: isTransitionMoving$
439
- }).pipe(
440
- takeUntil(destroy$),
441
- switchMap(async d => d),
442
- filter(d => !d.isTransitionMoving) // 避免資料變更時的動畫和highlight的動畫重覆執行
443
- ).subscribe(data => {
444
- highlight({
445
- pathSelection: data.pathSelection,
446
- ids: data.highlight,
447
- fullParams: data.fullParams,
448
- fullChartParams: data.fullChartParams,
449
- tweenArc: data.tweenArc,
450
- // arcMouseover: data.arcMouseover
451
- })
452
- })
453
-
454
-
455
-
456
-
457
- return () => {
458
- destroy$.next(undefined)
459
- }
460
- }
461
-
462
- export const Rose = defineSeriesPlugin(pluginConfig)(({ selection, name, subject, observer }) => {
463
- const destroy$ = new Subject()
464
-
465
- const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
466
- selection: selection,
467
- pluginName,
468
- separateSeries$: observer.separateSeries$,
469
- seriesLabels$: observer.seriesLabels$,
470
- seriesContainerPosition$: observer.seriesContainerPosition$
471
- })
472
-
473
- const unsubscribeFnArr: (() => void)[] = []
474
-
475
- seriesCenterSelection$
476
- .pipe(
477
- takeUntil(destroy$)
478
- )
479
- .subscribe(seriesCenterSelection => {
480
- // 每次重新計算時,清除之前的訂閱
481
- unsubscribeFnArr.forEach(fn => fn())
482
-
483
- seriesCenterSelection.each((d, containerIndex, g) => {
484
- const containerSelection = d3.select(g[containerIndex])
485
-
486
- const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
487
- takeUntil(destroy$),
488
- map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
489
- )
490
-
491
- const containerPosition$ = observer.seriesContainerPosition$.pipe(
492
- takeUntil(destroy$),
493
- map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
494
- )
495
-
496
- unsubscribeFnArr[containerIndex] = createEachRose(pluginName, {
497
- containerSelection: containerSelection,
498
- computedData$: observer.computedData$,
499
- visibleComputedData$: observer.visibleComputedData$,
500
- visibleComputedLayoutData$: observer.visibleComputedLayoutData$,
501
- containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
502
- SeriesDataMap$: observer.SeriesDataMap$,
503
- fullParams$: observer.fullParams$,
504
- fullChartParams$: observer.fullChartParams$,
505
- seriesHighlight$: observer.seriesHighlight$,
506
- seriesContainerPosition$: containerPosition$,
507
- event$: subject.event$,
508
- })
509
-
510
- })
511
- })
512
-
513
- return () => {
514
- destroy$.next(undefined)
515
- unsubscribeFnArr.forEach(fn => fn())
516
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ filter,
6
+ switchMap,
7
+ takeUntil,
8
+ distinctUntilChanged,
9
+ shareReplay,
10
+ Observable,
11
+ Subject,
12
+ BehaviorSubject } from 'rxjs'
13
+ import type { DefinePluginConfig } from '../../../lib/core-types'
14
+ import type {
15
+ ComputedDataSeries,
16
+ ComputedDatumSeries,
17
+ ContainerPosition,
18
+ ChartParams,
19
+ EventSeries,
20
+ Layout } from '../../../lib/core-types'
21
+ import type { D3PieDatum } from '../seriesUtils'
22
+ import type { RoseParams } from '../../../lib/plugins-basic-types'
23
+ import {
24
+ defineSeriesPlugin } from '../../../lib/core'
25
+ import { DEFAULT_ROSE_PARAMS } from '../defaults'
26
+ // import { makePieData } from '../seriesUtils'
27
+ // import { getD3TransitionEase, makeD3Arc } from '../../utils/d3Utils'
28
+ import { getDatumColor, getClassName } from '../../utils/orbchartsUtils'
29
+ import { seriesCenterSelectionObservable } from '../seriesObservables'
30
+ import { LAYER_INDEX_OF_GRAPHIC } from '../../const'
31
+
32
+ // @Q@ 暫時先寫在這裡,之後pie一起重構後再放到seriesUtils
33
+ export interface PieDatum extends D3PieDatum {
34
+ data: ComputedDatumSeries
35
+ id: string
36
+ prevValue: number // 補間動畫用的(前次資料的value)
37
+ }
38
+
39
+ const pluginName = 'Rose'
40
+
41
+ const roseInnerRadius = 0
42
+ const roseStartAngle = 0
43
+ const roseEndAngle = Math.PI * 2
44
+
45
+ const pluginConfig: DefinePluginConfig<typeof pluginName, typeof DEFAULT_ROSE_PARAMS> = {
46
+ name: pluginName,
47
+ defaultParams: DEFAULT_ROSE_PARAMS,
48
+ layerIndex: LAYER_INDEX_OF_GRAPHIC,
49
+ validator: (params, { validateColumns }) => {
50
+ const result = validateColumns(params, {
51
+ outerRadius: {
52
+ toBeTypes: ['number'],
53
+ },
54
+ padAngle: {
55
+ toBeTypes: ['number'],
56
+ },
57
+ strokeColorType: {
58
+ toBeOption: 'ColorType'
59
+ },
60
+ strokeWidth: {
61
+ toBeTypes: ['number']
62
+ },
63
+ cornerRadius: {
64
+ toBeTypes: ['number']
65
+ },
66
+ arcScaleType: {
67
+ toBe: '"area" | "radius"',
68
+ test: (value: any) => value === 'area' || value === 'radius'
69
+ },
70
+ angleIncreaseWhileHighlight: {
71
+ toBeTypes: ['number']
72
+ }
73
+ })
74
+ return result
75
+ }
76
+ }
77
+
78
+ function makeTweenArcFn ({ cornerRadius, outerRadius, axisWidth, maxValue, arcScaleType, fullParams }: {
79
+ // interpolateRadius: (t: number) => number
80
+ outerRadius: number
81
+ cornerRadius: number
82
+ axisWidth: number
83
+ maxValue: number
84
+ arcScaleType: 'radius' | 'area'
85
+ fullParams: RoseParams
86
+ }): (d: PieDatum) => (t: number) => string {
87
+
88
+ const outerRadiusWidth = (axisWidth / 2) * outerRadius
89
+
90
+ // const arcScale = d3.scaleLinear()
91
+ // .domain([0, maxValue])
92
+ // .range([0, outerRadiusWidth])
93
+
94
+ const exponent = arcScaleType === 'area'
95
+ ? 0.5 // 比例映射面積(0.5為取平方根)
96
+ : 1 // 比例映射半徑
97
+
98
+ const arcScale = d3.scalePow()
99
+ .domain([0, maxValue])
100
+ .range([0, outerRadiusWidth])
101
+ .exponent(exponent)
102
+
103
+ return (d: PieDatum) => {
104
+ const prevEachOuterRadius = arcScale(d.prevValue)!
105
+ const eachOuterRadius = arcScale(d.value)!
106
+
107
+ const interpolateRadius = d3.interpolate(prevEachOuterRadius, eachOuterRadius)
108
+
109
+ return (t: number) => {
110
+
111
+ const outerRadius = interpolateRadius(t)
112
+
113
+ const arc = d3.arc()
114
+ .innerRadius(0)
115
+ .outerRadius(outerRadius)
116
+ .padAngle(fullParams.padAngle)
117
+ .padRadius(outerRadius)
118
+ .cornerRadius(cornerRadius)
119
+
120
+ return arc(d as any)
121
+ }
122
+ }
123
+ }
124
+
125
+ // function renderPie ({ selection, data, tweenArc, transitionDuration, pathClassName }: {
126
+ // selection: d3.Selection<SVGGElement, unknown, any, unknown>
127
+ // data: PieDatum[]
128
+ // // arc: d3.Arc<any, d3.DefaultArcObject>
129
+ // tweenArc: (d: PieDatum) => (t: number) => string
130
+ // transitionDuration: number
131
+ // pathClassName: string
132
+ // }): d3.Selection<SVGPathElement, PieDatum, any, any> {
133
+ // // console.log('data', data)
134
+ // const pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any> = selection
135
+ // .selectAll<SVGPathElement, PieDatum>('path')
136
+ // .data(data, d => d.id)
137
+ // .join('path')
138
+ // .classed(pathClassName, true)
139
+ // .style('cursor', 'pointer')
140
+ // .attr('fill', (d, i) => d.data.color)
141
+ // pathSelection
142
+ // .transition('graphicMove')
143
+ // .duration(transitionDuration)
144
+ // .attrTween('d', tweenArc)
145
+
146
+ // return pathSelection
147
+ // }
148
+
149
+ function highlight ({ pathSelection, ids, fullParams, fullChartParams, tweenArc }: {
150
+ pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any>
151
+ ids: string[]
152
+ fullParams: RoseParams
153
+ fullChartParams: ChartParams
154
+ // arc: d3.Arc<any, d3.DefaultArcObject>
155
+ tweenArc: (d: PieDatum) => (t: number) => string
156
+ }) {
157
+ pathSelection.interrupt('highlight')
158
+
159
+ if (!ids.length) {
160
+ // 取消放大
161
+ pathSelection
162
+ .transition('highlight')
163
+ .style('opacity', 1)
164
+ .attr('d', (d: PieDatum) => {
165
+ return tweenArc(d)(1)
166
+ })
167
+ return
168
+ }
169
+
170
+ pathSelection.each((d, i, n) => {
171
+ const segment = d3.select(n[i])
172
+
173
+ if (ids.includes(d.data.id)) {
174
+ segment
175
+ .style('opacity', 1)
176
+ .transition('highlight')
177
+ .ease(d3.easeElastic)
178
+ .duration(500)
179
+ // .attr('d', (d: any) => {
180
+ // return arc!({
181
+ // ...d,
182
+ // startAngle: d.startAngle - 0.5,
183
+ // endAngle: d.endAngle + 0.5
184
+ // })
185
+ // })
186
+ .attr('d', (d: PieDatum) => {
187
+ return tweenArc({
188
+ ...d,
189
+ startAngle: d.startAngle - fullParams.angleIncreaseWhileHighlight,
190
+ endAngle: d.endAngle + fullParams.angleIncreaseWhileHighlight
191
+ })(1)
192
+ })
193
+ // .on('interrupt', () => {
194
+ // // this.pathSelection!.select('path').attr('d', (d) => {
195
+ // // return this.arc!(d as any)
196
+ // // })
197
+ // this.initHighlight()
198
+ // })
199
+ } else {
200
+ // 取消放大
201
+ segment
202
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
203
+ .transition('highlight')
204
+ .attr('d', (d: PieDatum) => {
205
+ return tweenArc(d)(1)
206
+ })
207
+ }
208
+ })
209
+ }
210
+
211
+ // 各別的pie
212
+ function createEachRose (pluginName: string, context: {
213
+ containerSelection: d3.Selection<SVGGElement, any, any, unknown>
214
+ computedData$: Observable<ComputedDatumSeries[][]>
215
+ visibleComputedData$: Observable<ComputedDatumSeries[][]>
216
+ visibleComputedLayoutData$: Observable<ComputedDatumSeries[][]>
217
+ containerVisibleComputedLayoutData$: Observable<ComputedDatumSeries[]>
218
+ SeriesDataMap$: Observable<Map<string, ComputedDatumSeries[]>>
219
+ fullParams$: Observable<RoseParams>
220
+ fullChartParams$: Observable<ChartParams>
221
+ seriesHighlight$: Observable<ComputedDatumSeries[]>
222
+ seriesContainerPosition$: Observable<ContainerPosition>
223
+ event$: Subject<EventSeries>
224
+ }) {
225
+ const destroy$ = new Subject()
226
+
227
+ const pathClassName = getClassName(pluginName, 'path')
228
+
229
+ let lastPieData: PieDatum[] = [] // 紀錄補間動畫前次的資料
230
+
231
+ const shorterSideWith$ = context.seriesContainerPosition$.pipe(
232
+ takeUntil(destroy$),
233
+ map(d => d.width < d.height ? d.width : d.height),
234
+ distinctUntilChanged()
235
+ )
236
+
237
+ const pieData$: Observable<PieDatum[]> = combineLatest({
238
+ containerVisibleComputedLayoutData: context.containerVisibleComputedLayoutData$,
239
+ fullParams: context.fullParams$,
240
+ }).pipe(
241
+ takeUntil(destroy$),
242
+ switchMap(async (d) => d),
243
+ map(data => {
244
+ const eachAngle = roseEndAngle / data.containerVisibleComputedLayoutData.length
245
+ return data.containerVisibleComputedLayoutData.map((d, i) => {
246
+ return {
247
+ id: d.id,
248
+ data: d,
249
+ index: i,
250
+ value: d.value,
251
+ startAngle: eachAngle * i,
252
+ endAngle: eachAngle * (i + 1),
253
+ padAngle: data.fullParams.padAngle,
254
+ prevValue: (lastPieData[i] && lastPieData[i].id === d.id) ? lastPieData[i].value : 0
255
+ }
256
+ })
257
+ })
258
+ )
259
+
260
+ const highlightTarget$ = context.fullChartParams$.pipe(
261
+ takeUntil(destroy$),
262
+ map(d => d.highlightTarget),
263
+ distinctUntilChanged()
264
+ )
265
+
266
+ const maxValue$ = context.visibleComputedLayoutData$.pipe(
267
+ map(data => Math.max(...data.flat().map(d => d.value))),
268
+ distinctUntilChanged()
269
+ )
270
+
271
+ // context.visibleComputedLayoutData$.subscribe(data => {
272
+ // console.log('visibleComputedLayoutData$', data)
273
+ // })
274
+
275
+ const tweenArc$ = combineLatest({
276
+ fullParams: context.fullParams$,
277
+ axisWidth: shorterSideWith$,
278
+ maxValue: maxValue$
279
+ }).pipe(
280
+ takeUntil(destroy$),
281
+ switchMap(async d => d),
282
+ map((data) => {
283
+ return makeTweenArcFn({
284
+ cornerRadius: data.fullParams.cornerRadius,
285
+ outerRadius: data.fullParams.outerRadius,
286
+ axisWidth: data.axisWidth,
287
+ maxValue: data.maxValue,
288
+ arcScaleType: data.fullParams.arcScaleType,
289
+ fullParams: data.fullParams
290
+ })
291
+ })
292
+ )
293
+
294
+ const transitionDuration$ = context.fullChartParams$.pipe(
295
+ takeUntil(destroy$),
296
+ map(d => d.transitionDuration),
297
+ distinctUntilChanged()
298
+ )
299
+
300
+ // 是否在transition中
301
+ const isTransitionMoving$ = new BehaviorSubject<boolean>(false)
302
+
303
+ const pathSelection$ = new Observable<d3.Selection<SVGPathElement, PieDatum, any, any>>(subscriber => {
304
+ combineLatest({
305
+ pieData: pieData$,
306
+ tweenArc: tweenArc$,
307
+ transitionDuration: transitionDuration$,
308
+ fullParams: context.fullParams$,
309
+ fullChartParams: context.fullChartParams$
310
+ }).pipe(
311
+ takeUntil(destroy$),
312
+ switchMap(async d => d)
313
+ ).subscribe(data => {
314
+ const pieData = data.pieData.map((d, i) => {
315
+ d.prevValue = (lastPieData[i] && lastPieData[i].id === d.id) ? lastPieData[i].value : 0
316
+ return d
317
+ })
318
+
319
+ isTransitionMoving$.next(true)
320
+
321
+ const pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any> = context.containerSelection
322
+ .selectAll<SVGPathElement, PieDatum>('path')
323
+ .data(pieData, d => d.id)
324
+ .join('path')
325
+ .classed(pathClassName, true)
326
+ .style('cursor', 'pointer')
327
+ .attr('fill', (d, i) => d.data.color)
328
+ .attr('stroke', (d, i) => getDatumColor({
329
+ datum: d.data,
330
+ colorType: data.fullParams.strokeColorType,
331
+ fullChartParams: data.fullChartParams
332
+ }))
333
+ .attr('stroke-width', data.fullParams.strokeWidth)
334
+ pathSelection.interrupt('graphicMove')
335
+ pathSelection
336
+ .transition('graphicMove')
337
+ .duration(data.transitionDuration)
338
+ .attrTween('d', data.tweenArc)
339
+ .on('end', () => {
340
+ subscriber.next(pathSelection)
341
+
342
+ isTransitionMoving$.next(false)
343
+ // lastPieData = Object.assign([], data.pieData)
344
+ // console.log('lastPieData', lastPieData)
345
+ })
346
+ lastPieData = Object.assign([], pieData)
347
+
348
+ })
349
+ }).pipe(
350
+ shareReplay(1)
351
+ )
352
+
353
+ combineLatest({
354
+ pathSelection: pathSelection$,
355
+ SeriesDataMap: context.SeriesDataMap$,
356
+ computedData: context.computedData$,
357
+ highlightTarget: highlightTarget$
358
+ }).pipe(
359
+ takeUntil(destroy$),
360
+ switchMap(async d => d)
361
+ ).subscribe(data => {
362
+ data.pathSelection
363
+ .on('mouseover', (event, pieDatum) => {
364
+ event.stopPropagation()
365
+
366
+ context.event$.next({
367
+ type: 'series',
368
+ eventName: 'mouseover',
369
+ pluginName,
370
+ highlightTarget: data.highlightTarget,
371
+ datum: pieDatum.data,
372
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
373
+ seriesIndex: pieDatum.data.seriesIndex,
374
+ seriesLabel: pieDatum.data.seriesLabel,
375
+ event,
376
+ data: data.computedData
377
+ })
378
+ })
379
+ .on('mousemove', (event, pieDatum) => {
380
+ event.stopPropagation()
381
+
382
+ context.event$.next({
383
+ type: 'series',
384
+ eventName: 'mousemove',
385
+ pluginName,
386
+ highlightTarget: data.highlightTarget,
387
+ datum: pieDatum.data,
388
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
389
+ seriesIndex: pieDatum.data.seriesIndex,
390
+ seriesLabel: pieDatum.data.seriesLabel,
391
+ event,
392
+ data: data.computedData,
393
+ })
394
+ })
395
+ .on('mouseout', (event, pieDatum) => {
396
+ event.stopPropagation()
397
+
398
+ context.event$.next({
399
+ type: 'series',
400
+ eventName: 'mouseout',
401
+ pluginName,
402
+ highlightTarget: data.highlightTarget,
403
+ datum: pieDatum.data,
404
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
405
+ seriesIndex: pieDatum.data.seriesIndex,
406
+ seriesLabel: pieDatum.data.seriesLabel,
407
+ event,
408
+ data: data.computedData,
409
+ })
410
+ })
411
+ .on('click', (event, pieDatum) => {
412
+ event.stopPropagation()
413
+
414
+ context.event$.next({
415
+ type: 'series',
416
+ eventName: 'click',
417
+ pluginName,
418
+ highlightTarget: data.highlightTarget,
419
+ datum: pieDatum.data,
420
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
421
+ seriesIndex: pieDatum.data.seriesIndex,
422
+ seriesLabel: pieDatum.data.seriesLabel,
423
+ event,
424
+ data: data.computedData,
425
+ })
426
+ })
427
+ })
428
+
429
+ combineLatest({
430
+ pathSelection: pathSelection$,
431
+ highlight: context.seriesHighlight$.pipe(
432
+ map(data => data.map(d => d.id))
433
+ ),
434
+ fullParams: context.fullParams$,
435
+ fullChartParams: context.fullChartParams$,
436
+ // arc: arc$,
437
+ tweenArc: tweenArc$,
438
+ isTransitionMoving: isTransitionMoving$
439
+ }).pipe(
440
+ takeUntil(destroy$),
441
+ switchMap(async d => d),
442
+ filter(d => !d.isTransitionMoving) // 避免資料變更時的動畫和highlight的動畫重覆執行
443
+ ).subscribe(data => {
444
+ highlight({
445
+ pathSelection: data.pathSelection,
446
+ ids: data.highlight,
447
+ fullParams: data.fullParams,
448
+ fullChartParams: data.fullChartParams,
449
+ tweenArc: data.tweenArc,
450
+ // arcMouseover: data.arcMouseover
451
+ })
452
+ })
453
+
454
+
455
+
456
+
457
+ return () => {
458
+ destroy$.next(undefined)
459
+ }
460
+ }
461
+
462
+ export const Rose = defineSeriesPlugin(pluginConfig)(({ selection, name, subject, observer }) => {
463
+ const destroy$ = new Subject()
464
+
465
+ const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
466
+ selection: selection,
467
+ pluginName,
468
+ separateSeries$: observer.separateSeries$,
469
+ seriesLabels$: observer.seriesLabels$,
470
+ seriesContainerPosition$: observer.seriesContainerPosition$
471
+ })
472
+
473
+ const unsubscribeFnArr: (() => void)[] = []
474
+
475
+ seriesCenterSelection$
476
+ .pipe(
477
+ takeUntil(destroy$)
478
+ )
479
+ .subscribe(seriesCenterSelection => {
480
+ // 每次重新計算時,清除之前的訂閱
481
+ unsubscribeFnArr.forEach(fn => fn())
482
+
483
+ seriesCenterSelection.each((d, containerIndex, g) => {
484
+ const containerSelection = d3.select(g[containerIndex])
485
+
486
+ const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
487
+ takeUntil(destroy$),
488
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
489
+ )
490
+
491
+ const containerPosition$ = observer.seriesContainerPosition$.pipe(
492
+ takeUntil(destroy$),
493
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
494
+ )
495
+
496
+ unsubscribeFnArr[containerIndex] = createEachRose(pluginName, {
497
+ containerSelection: containerSelection,
498
+ computedData$: observer.computedData$,
499
+ visibleComputedData$: observer.visibleComputedData$,
500
+ visibleComputedLayoutData$: observer.visibleComputedLayoutData$,
501
+ containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
502
+ SeriesDataMap$: observer.SeriesDataMap$,
503
+ fullParams$: observer.fullParams$,
504
+ fullChartParams$: observer.fullChartParams$,
505
+ seriesHighlight$: observer.seriesHighlight$,
506
+ seriesContainerPosition$: containerPosition$,
507
+ event$: subject.event$,
508
+ })
509
+
510
+ })
511
+ })
512
+
513
+ return () => {
514
+ destroy$.next(undefined)
515
+ unsubscribeFnArr.forEach(fn => fn())
516
+ }
517
517
  })