@orbcharts/plugins-basic 3.0.0-alpha.67 → 3.0.0-alpha.69

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