@orbcharts/plugins-basic 3.0.0-alpha.47 → 3.0.0-alpha.49

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,473 @@
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 { 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
+ const rosePadAngle = 0
43
+
44
+ function makeTweenArcFn ({ cornerRadius, outerRadius, axisWidth, maxValue, arcScaleType }: {
45
+ // interpolateRadius: (t: number) => number
46
+ outerRadius: number
47
+ cornerRadius: number
48
+ axisWidth: number
49
+ maxValue: number
50
+ arcScaleType: 'radius' | 'area'
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(rosePadAngle)
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.mouseoverAngleIncrease,
155
+ endAngle: d.endAngle + fullParams.mouseoverAngleIncrease
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: rosePadAngle,
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
+ })
255
+ })
256
+ )
257
+
258
+ const transitionDuration$ = context.fullChartParams$.pipe(
259
+ takeUntil(destroy$),
260
+ map(d => d.transitionDuration),
261
+ distinctUntilChanged()
262
+ )
263
+
264
+ // 是否在transition中
265
+ const isTransitionMoving$ = new BehaviorSubject<boolean>(false)
266
+
267
+ const pathSelection$ = new Observable<d3.Selection<SVGPathElement, PieDatum, any, any>>(subscriber => {
268
+ combineLatest({
269
+ pieData: pieData$,
270
+ tweenArc: tweenArc$,
271
+ transitionDuration: transitionDuration$,
272
+ }).pipe(
273
+ takeUntil(destroy$),
274
+ switchMap(async d => d)
275
+ ).subscribe(data => {
276
+ const pieData = data.pieData.map((d, i) => {
277
+ d.prevValue = (lastPieData[i] && lastPieData[i].id === d.id) ? lastPieData[i].value : 0
278
+ return d
279
+ })
280
+
281
+ isTransitionMoving$.next(true)
282
+
283
+ const pathSelection: d3.Selection<SVGPathElement, PieDatum, any, any> = context.containerSelection
284
+ .selectAll<SVGPathElement, PieDatum>('path')
285
+ .data(pieData, d => d.id)
286
+ .join('path')
287
+ .classed(pathClassName, true)
288
+ .style('cursor', 'pointer')
289
+ .attr('fill', (d, i) => d.data.color)
290
+ pathSelection.interrupt('graphicMove')
291
+ pathSelection
292
+ .transition('graphicMove')
293
+ .duration(data.transitionDuration)
294
+ .attrTween('d', data.tweenArc)
295
+ .on('end', () => {
296
+ subscriber.next(pathSelection)
297
+
298
+ isTransitionMoving$.next(false)
299
+ // lastPieData = Object.assign([], data.pieData)
300
+ // console.log('lastPieData', lastPieData)
301
+ })
302
+ lastPieData = Object.assign([], pieData)
303
+
304
+ })
305
+ }).pipe(
306
+ shareReplay(1)
307
+ )
308
+
309
+ combineLatest({
310
+ pathSelection: pathSelection$,
311
+ SeriesDataMap: context.SeriesDataMap$,
312
+ computedData: context.computedData$,
313
+ highlightTarget: highlightTarget$
314
+ }).pipe(
315
+ takeUntil(destroy$),
316
+ switchMap(async d => d)
317
+ ).subscribe(data => {
318
+ data.pathSelection
319
+ .on('mouseover', (event, pieDatum) => {
320
+ event.stopPropagation()
321
+
322
+ context.event$.next({
323
+ type: 'series',
324
+ eventName: 'mouseover',
325
+ pluginName,
326
+ highlightTarget: data.highlightTarget,
327
+ datum: pieDatum.data,
328
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
329
+ seriesIndex: pieDatum.data.seriesIndex,
330
+ seriesLabel: pieDatum.data.seriesLabel,
331
+ event,
332
+ data: data.computedData
333
+ })
334
+ })
335
+ .on('mousemove', (event, pieDatum) => {
336
+ event.stopPropagation()
337
+
338
+ context.event$.next({
339
+ type: 'series',
340
+ eventName: 'mousemove',
341
+ pluginName,
342
+ highlightTarget: data.highlightTarget,
343
+ datum: pieDatum.data,
344
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
345
+ seriesIndex: pieDatum.data.seriesIndex,
346
+ seriesLabel: pieDatum.data.seriesLabel,
347
+ event,
348
+ data: data.computedData,
349
+ })
350
+ })
351
+ .on('mouseout', (event, pieDatum) => {
352
+ event.stopPropagation()
353
+
354
+ context.event$.next({
355
+ type: 'series',
356
+ eventName: 'mouseout',
357
+ pluginName,
358
+ highlightTarget: data.highlightTarget,
359
+ datum: pieDatum.data,
360
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
361
+ seriesIndex: pieDatum.data.seriesIndex,
362
+ seriesLabel: pieDatum.data.seriesLabel,
363
+ event,
364
+ data: data.computedData,
365
+ })
366
+ })
367
+ .on('click', (event, pieDatum) => {
368
+ event.stopPropagation()
369
+
370
+ context.event$.next({
371
+ type: 'series',
372
+ eventName: 'click',
373
+ pluginName,
374
+ highlightTarget: data.highlightTarget,
375
+ datum: pieDatum.data,
376
+ series: data.SeriesDataMap.get(pieDatum.data.seriesLabel)!,
377
+ seriesIndex: pieDatum.data.seriesIndex,
378
+ seriesLabel: pieDatum.data.seriesLabel,
379
+ event,
380
+ data: data.computedData,
381
+ })
382
+ })
383
+ })
384
+
385
+ combineLatest({
386
+ pathSelection: pathSelection$,
387
+ highlight: context.seriesHighlight$.pipe(
388
+ map(data => data.map(d => d.id))
389
+ ),
390
+ fullParams: context.fullParams$,
391
+ fullChartParams: context.fullChartParams$,
392
+ // arc: arc$,
393
+ tweenArc: tweenArc$,
394
+ isTransitionMoving: isTransitionMoving$
395
+ }).pipe(
396
+ takeUntil(destroy$),
397
+ switchMap(async d => d),
398
+ filter(d => !d.isTransitionMoving) // 避免資料變更時的動畫和highlight的動畫重覆執行
399
+ ).subscribe(data => {
400
+ highlight({
401
+ pathSelection: data.pathSelection,
402
+ ids: data.highlight,
403
+ fullParams: data.fullParams,
404
+ fullChartParams: data.fullChartParams,
405
+ tweenArc: data.tweenArc,
406
+ // arcMouseover: data.arcMouseover
407
+ })
408
+ })
409
+
410
+
411
+
412
+
413
+ return () => {
414
+ destroy$.next(undefined)
415
+ }
416
+ }
417
+
418
+ export const Rose = defineSeriesPlugin(pluginName, DEFAULT_ROSE_PARAMS)(({ selection, name, subject, observer }) => {
419
+ const destroy$ = new Subject()
420
+
421
+ const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
422
+ selection: selection,
423
+ pluginName,
424
+ separateSeries$: observer.separateSeries$,
425
+ seriesLabels$: observer.seriesLabels$,
426
+ seriesContainerPosition$: observer.seriesContainerPosition$
427
+ })
428
+
429
+ const unsubscribeFnArr: (() => void)[] = []
430
+
431
+ seriesCenterSelection$
432
+ .pipe(
433
+ takeUntil(destroy$)
434
+ )
435
+ .subscribe(seriesCenterSelection => {
436
+ // 每次重新計算時,清除之前的訂閱
437
+ unsubscribeFnArr.forEach(fn => fn())
438
+
439
+ seriesCenterSelection.each((d, containerIndex, g) => {
440
+ const containerSelection = d3.select(g[containerIndex])
441
+
442
+ const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
443
+ takeUntil(destroy$),
444
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
445
+ )
446
+
447
+ const containerPosition$ = observer.seriesContainerPosition$.pipe(
448
+ takeUntil(destroy$),
449
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
450
+ )
451
+
452
+ unsubscribeFnArr[containerIndex] = createEachRose(pluginName, {
453
+ containerSelection: containerSelection,
454
+ computedData$: observer.computedData$,
455
+ visibleComputedData$: observer.visibleComputedData$,
456
+ visibleComputedLayoutData$: observer.visibleComputedLayoutData$,
457
+ containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
458
+ SeriesDataMap$: observer.SeriesDataMap$,
459
+ fullParams$: observer.fullParams$,
460
+ fullChartParams$: observer.fullChartParams$,
461
+ seriesHighlight$: observer.seriesHighlight$,
462
+ seriesContainerPosition$: containerPosition$,
463
+ event$: subject.event$,
464
+ })
465
+
466
+ })
467
+ })
468
+
469
+ return () => {
470
+ destroy$.next(undefined)
471
+ unsubscribeFnArr.forEach(fn => fn())
472
+ }
473
+ })