@orbcharts/plugins-basic 3.0.0-alpha.75 → 3.0.0-alpha.76

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 (81) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +3461 -3468
  3. package/dist/orbcharts-plugins-basic.umd.js +17 -17
  4. package/lib/core-types.ts +7 -7
  5. package/lib/core.ts +6 -6
  6. package/package.json +43 -43
  7. package/src/base/BaseBarStack.ts +779 -779
  8. package/src/base/BaseBars.ts +764 -764
  9. package/src/base/BaseBarsTriangle.ts +672 -672
  10. package/src/base/BaseDots.ts +513 -513
  11. package/src/base/BaseGroupAxis.ts +678 -678
  12. package/src/base/BaseLegend.ts +642 -642
  13. package/src/base/BaseLineAreas.ts +627 -627
  14. package/src/base/BaseLines.ts +704 -704
  15. package/src/base/BaseValueAxis.ts +578 -578
  16. package/src/base/types.ts +2 -2
  17. package/src/const.ts +30 -30
  18. package/src/grid/defaults.ts +128 -128
  19. package/src/grid/gridObservables.ts +545 -545
  20. package/src/grid/index.ts +15 -15
  21. package/src/grid/plugins/BarStack.ts +64 -64
  22. package/src/grid/plugins/Bars.ts +69 -69
  23. package/src/grid/plugins/BarsPN.ts +66 -66
  24. package/src/grid/plugins/BarsTriangle.ts +73 -73
  25. package/src/grid/plugins/Dots.ts +68 -68
  26. package/src/grid/plugins/GridLegend.ts +96 -96
  27. package/src/grid/plugins/GroupAux.ts +1098 -1098
  28. package/src/grid/plugins/GroupAxis.ts +97 -97
  29. package/src/grid/plugins/LineAreas.ts +65 -65
  30. package/src/grid/plugins/Lines.ts +59 -59
  31. package/src/grid/plugins/ScalingArea.ts +218 -218
  32. package/src/grid/plugins/ValueAxis.ts +93 -93
  33. package/src/grid/plugins/ValueStackAxis.ts +95 -95
  34. package/src/grid/types.ts +123 -123
  35. package/src/index.ts +9 -9
  36. package/src/multiGrid/defaults.ts +158 -158
  37. package/src/multiGrid/index.ts +13 -13
  38. package/src/multiGrid/multiGridObservables.ts +49 -49
  39. package/src/multiGrid/plugins/MultiBarStack.ts +106 -106
  40. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  41. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  42. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  43. package/src/multiGrid/plugins/MultiGridLegend.ts +148 -148
  44. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  45. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  46. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  47. package/src/multiGrid/plugins/MultiValueAxis.ts +133 -133
  48. package/src/multiGrid/plugins/MultiValueStackAxis.ts +133 -133
  49. package/src/multiGrid/plugins/OverlappingValueAxes.ts +299 -299
  50. package/src/multiGrid/plugins/OverlappingValueStackAxes.ts +298 -298
  51. package/src/multiGrid/types.ts +72 -72
  52. package/src/noneData/defaults.ts +102 -102
  53. package/src/noneData/index.ts +3 -3
  54. package/src/noneData/plugins/Container.ts +27 -27
  55. package/src/noneData/plugins/Tooltip.ts +373 -373
  56. package/src/noneData/types.ts +26 -26
  57. package/src/series/defaults.ts +149 -149
  58. package/src/series/index.ts +9 -9
  59. package/src/series/plugins/Bubbles.ts +603 -603
  60. package/src/series/plugins/Pie.ts +623 -623
  61. package/src/series/plugins/PieEventTexts.ts +283 -283
  62. package/src/series/plugins/PieLabels.ts +640 -640
  63. package/src/series/plugins/Rose.ts +516 -516
  64. package/src/series/plugins/RoseLabels.ts +600 -600
  65. package/src/series/plugins/SeriesLegend.ts +96 -96
  66. package/src/series/seriesObservables.ts +145 -145
  67. package/src/series/seriesUtils.ts +51 -51
  68. package/src/series/types.ts +87 -87
  69. package/src/tree/defaults.ts +23 -23
  70. package/src/tree/index.ts +3 -3
  71. package/src/tree/plugins/TreeLegend.ts +96 -96
  72. package/src/tree/plugins/TreeMap.ts +333 -333
  73. package/src/tree/types.ts +23 -23
  74. package/src/utils/commonUtils.ts +21 -21
  75. package/src/utils/d3Graphics.ts +174 -174
  76. package/src/utils/d3Utils.ts +73 -73
  77. package/src/utils/observables.ts +14 -14
  78. package/src/utils/orbchartsUtils.ts +100 -100
  79. package/tsconfig.base.json +13 -13
  80. package/tsconfig.json +2 -2
  81. 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
- SeriesContainerPosition,
18
- ChartParams,
19
- EventSeries,
20
- Layout } from '../../../lib/core-types'
21
- import type { D3PieDatum } from '../seriesUtils'
22
- import type { RoseParams } from '../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<SeriesContainerPosition>
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
+ SeriesContainerPosition,
18
+ ChartParams,
19
+ EventSeries,
20
+ Layout } from '../../../lib/core-types'
21
+ import type { D3PieDatum } from '../seriesUtils'
22
+ import type { RoseParams } from '../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<SeriesContainerPosition>
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
  })