@orbcharts/plugins-basic 3.0.0-alpha.48 → 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,362 @@
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ switchMap,
5
+ first,
6
+ map,
7
+ takeUntil,
8
+ Observable,
9
+ distinctUntilChanged,
10
+ Subject,
11
+ BehaviorSubject } from 'rxjs'
12
+ import {
13
+ defineSeriesPlugin } from '@orbcharts/core'
14
+ import type {
15
+ ComputedDatumSeries,
16
+ SeriesContainerPosition,
17
+ EventSeries,
18
+ ChartParams } from '@orbcharts/core'
19
+ import type { RoseLabelsParams } from '../types'
20
+ import type { PieDatum } from '../seriesUtils'
21
+ import { DEFAULT_ROSE_LABELS_PARAMS } from '../defaults'
22
+ // import { makePieData } from '../seriesUtils'
23
+ import { makeD3Arc } from '../../utils/d3Utils'
24
+ import { getDatumColor, getClassName } from '../../utils/orbchartsUtils'
25
+ import { seriesCenterSelectionObservable } from '../seriesObservables'
26
+
27
+ interface RenderDatum {
28
+ pieDatum: PieDatum
29
+ arcIndex: number
30
+ arcLabel: string
31
+ x: number
32
+ y: number
33
+ mouseoverX: number
34
+ mouseoverY: number
35
+ }
36
+
37
+ const pluginName = 'RoseLabels'
38
+ const textClassName = getClassName(pluginName, 'text')
39
+
40
+ function makeRenderData ({ pieData, centroid, arcScaleType, maxValue, axisWidth, outerRadius }: {
41
+ pieData: PieDatum[]
42
+ // arc: d3.Arc<any, d3.DefaultArcObject>
43
+ centroid: number
44
+ arcScaleType: 'area' | 'radius'
45
+ maxValue: number
46
+ axisWidth: number
47
+ outerRadius: number
48
+ }): RenderDatum[] {
49
+
50
+ const outerRadiusWidth = (axisWidth / 2) * outerRadius
51
+
52
+ const exponent = arcScaleType === 'area'
53
+ ? 0.5 // 比例映射面積(0.5為取平方根)
54
+ : 1 // 比例映射半徑
55
+
56
+ const arcScale = d3.scalePow()
57
+ .domain([0, maxValue])
58
+ .range([0, outerRadiusWidth])
59
+ .exponent(exponent)
60
+
61
+ return pieData
62
+ .map((d, i) => {
63
+ const eachOuterRadius = arcScale(d.value)
64
+
65
+ const arc = d3.arc()
66
+ .innerRadius(0)
67
+ .outerRadius(eachOuterRadius)
68
+ .padAngle(0)
69
+ .padRadius(eachOuterRadius)
70
+ .cornerRadius(0)
71
+
72
+ const [_x, _y] = arc!.centroid(d as any)
73
+ const [_mouseoverX, _mouseoverY] = [_x, _y]
74
+ return {
75
+ pieDatum: d,
76
+ arcIndex: i,
77
+ arcLabel: d.data.label,
78
+ x: _x * centroid!,
79
+ y: _y * centroid!,
80
+ mouseoverX: _mouseoverX * centroid!,
81
+ mouseoverY: _mouseoverY * centroid!
82
+ }
83
+ })
84
+ .filter(d => d.pieDatum.data.visible)
85
+ }
86
+
87
+ // 繪製圓餅圖
88
+ function renderLabel (selection: d3.Selection<SVGGElement, undefined, any, any>, data: RenderDatum[], pluginParams: RoseLabelsParams, fullChartParams: ChartParams) {
89
+ // console.log(data)
90
+ // let update = this.gSelection.selectAll('g').data(pieData)
91
+ let update: d3.Selection<SVGPathElement, RenderDatum, any, any> = selection
92
+ .selectAll<SVGPathElement, RenderDatum>('text')
93
+ .data(data, d => d.pieDatum.id)
94
+ let enter = update.enter()
95
+ .append<SVGPathElement>('text')
96
+ .classed(textClassName, true)
97
+ let exit = update.exit()
98
+
99
+ enter
100
+ .append('text')
101
+
102
+ const labelSelection = update.merge(enter)
103
+ labelSelection
104
+ .attr('font-weight', 'bold')
105
+ .attr('text-anchor', 'middle')
106
+ .style('dominant-baseline', 'middle')
107
+ // .style('pointer-events', 'none')
108
+ .style('cursor', d => fullChartParams.highlightTarget && fullChartParams.highlightTarget != 'none'
109
+ ? 'pointer'
110
+ : 'none')
111
+ // .text((d, i) => d.arcLabel)
112
+ .text(d => pluginParams.labelFn(d.pieDatum.data))
113
+ .attr('font-size', fullChartParams.styles.textSize)
114
+ .attr('fill', (d, i) => getDatumColor({ datum: d.pieDatum.data, colorType: pluginParams.labelColorType, fullChartParams }))
115
+ .transition()
116
+ .attr('transform', (d) => {
117
+ return 'translate(' + d.x + ',' + d.y + ')'
118
+ })
119
+ // .on('end', () => initHighlight({ labelSelection, data, fullChartParams }))
120
+ exit.remove()
121
+
122
+ // 如無新增資料則不用等動畫
123
+ // if (enter.size() == 0) {
124
+ // this.initHighlight()
125
+ // }
126
+
127
+ return labelSelection
128
+ }
129
+
130
+ // function initHighlight ({ labelSelection, data, fullChartParams }: {
131
+ // labelSelection: (d3.Selection<SVGPathElement, RenderDatum, any, any>)
132
+ // data: RenderDatum[]
133
+ // fullChartParams: ChartParams
134
+ // }) {
135
+ // removeHighlight({ labelSelection })
136
+ // // if (fullParams.highlightSeriesId || fullParams.highlightDatumId) {
137
+ // highlight({
138
+ // labelSelection,
139
+ // data,
140
+ // id: fullChartParams.highlightDefault,
141
+ // label: fullChartParams.highlightDefault,
142
+ // fullChartParams
143
+ // })
144
+ // // }
145
+ // }
146
+
147
+ function highlight ({ labelSelection, ids, fullChartParams }: {
148
+ labelSelection: (d3.Selection<SVGPathElement, RenderDatum, any, any>)
149
+ ids: string[]
150
+ fullChartParams: ChartParams
151
+ }) {
152
+ labelSelection.interrupt('highlight')
153
+
154
+ if (!ids.length) {
155
+ labelSelection
156
+ .transition()
157
+ .duration(200)
158
+ .attr('transform', (d) => {
159
+ return 'translate(' + d.x + ',' + d.y + ')'
160
+ })
161
+ .style('opacity', 1)
162
+ return
163
+ }
164
+
165
+ labelSelection.each((d, i, n) => {
166
+ const segment = d3.select<SVGPathElement, RenderDatum>(n[i])
167
+
168
+ if (ids.includes(d.pieDatum.data.id)) {
169
+ segment
170
+ .style('opacity', 1)
171
+ .transition()
172
+ .duration(200)
173
+ .attr('transform', (d) => {
174
+ return 'translate(' + d.mouseoverX + ',' + d.mouseoverY + ')'
175
+ })
176
+ } else {
177
+ segment
178
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
179
+ .transition()
180
+ .duration(200)
181
+ .attr('transform', (d) => {
182
+ return 'translate(' + d.x + ',' + d.y + ')'
183
+ })
184
+ }
185
+ })
186
+ }
187
+
188
+
189
+ function createEachPieLabel (pluginName: string, context: {
190
+ containerSelection: d3.Selection<SVGGElement, any, any, unknown>
191
+ // computedData$: Observable<ComputedDatumSeries[][]>
192
+ visibleComputedLayoutData$: Observable<ComputedDatumSeries[][]>
193
+ containerVisibleComputedLayoutData$: Observable<ComputedDatumSeries[]>
194
+ // SeriesDataMap$: Observable<Map<string, ComputedDatumSeries[]>>
195
+ fullParams$: Observable<RoseLabelsParams>
196
+ fullChartParams$: Observable<ChartParams>
197
+ seriesHighlight$: Observable<ComputedDatumSeries[]>
198
+ seriesContainerPosition$: Observable<SeriesContainerPosition>
199
+ event$: Subject<EventSeries>
200
+ }) {
201
+ const destroy$ = new Subject()
202
+
203
+ let labelSelection$: Subject<d3.Selection<SVGPathElement, RenderDatum, any, any>> = new Subject()
204
+ let renderData: RenderDatum[] = []
205
+
206
+ const shorterSideWith$ = context.seriesContainerPosition$.pipe(
207
+ takeUntil(destroy$),
208
+ map(d => d.width < d.height ? d.width : d.height),
209
+ distinctUntilChanged()
210
+ )
211
+
212
+ const maxValue$ = context.visibleComputedLayoutData$.pipe(
213
+ map(data => Math.max(...data.flat().map(d => d.value))),
214
+ distinctUntilChanged()
215
+ )
216
+
217
+ combineLatest({
218
+ // layout: context.seriesContainerPosition$,
219
+ shorterSideWith: shorterSideWith$,
220
+ containerVisibleComputedLayoutData: context.containerVisibleComputedLayoutData$,
221
+ maxValue: maxValue$,
222
+ fullParams: context.fullParams$,
223
+ fullChartParams: context.fullChartParams$,
224
+ }).pipe(
225
+ takeUntil(destroy$),
226
+ switchMap(async (d) => d),
227
+ ).subscribe(data => {
228
+
229
+ // const shorterSideWith = data.layout.width < data.layout.height ? data.layout.width : data.layout.height
230
+
231
+ // // 弧產生器 (d3.arc())
232
+ // const arc = makeD3Arc({
233
+ // axisWidth: shorterSideWith,
234
+ // innerRadius: 0,
235
+ // outerRadius: data.fullParams.outerRadius,
236
+ // padAngle: 0,
237
+ // cornerRadius: 0
238
+ // })
239
+
240
+ // const arcMouseover = makeD3Arc({
241
+ // axisWidth: shorterSideWith,
242
+ // innerRadius: 0,
243
+ // outerRadius: data.fullParams.mouseoverOuterRadius, // 外半徑變化
244
+ // padAngle: 0,
245
+ // cornerRadius: 0
246
+ // })
247
+
248
+ // const pieData = makePieData({
249
+ // data: data.containerVisibleComputedLayoutData,
250
+ // startAngle: data.fullParams.startAngle,
251
+ // endAngle: data.fullParams.endAngle
252
+ // })
253
+
254
+ const eachAngle = Math.PI * 2 / data.containerVisibleComputedLayoutData.length
255
+
256
+ const pieData = data.containerVisibleComputedLayoutData.map((d, i) => {
257
+ return {
258
+ id: d.id,
259
+ data: d,
260
+ index: i,
261
+ value: d.value,
262
+ startAngle: eachAngle * i,
263
+ endAngle: eachAngle * (i + 1),
264
+ padAngle: 0,
265
+ // prevValue: lastPieData[i] ? lastPieData[i].value : 0
266
+ }
267
+ })
268
+
269
+ renderData = makeRenderData({
270
+ pieData,
271
+ centroid: data.fullParams.labelCentroid,
272
+ arcScaleType: data.fullParams.arcScaleType,
273
+ maxValue: data.maxValue,
274
+ axisWidth: data.shorterSideWith,
275
+ outerRadius: data.fullParams.outerRadius
276
+ })
277
+
278
+ const labelSelection = renderLabel(context.containerSelection, renderData, data.fullParams, data.fullChartParams)
279
+
280
+ labelSelection$.next(labelSelection)
281
+
282
+ })
283
+
284
+ combineLatest({
285
+ labelSelection: labelSelection$,
286
+ highlight: context.seriesHighlight$.pipe(
287
+ map(data => data.map(d => d.id))
288
+ ),
289
+ fullChartParams: context.fullChartParams$,
290
+ }).pipe(
291
+ takeUntil(destroy$),
292
+ switchMap(async d => d)
293
+ ).subscribe(data => {
294
+ highlight({
295
+ labelSelection: data.labelSelection,
296
+ ids: data.highlight,
297
+ fullChartParams: data.fullChartParams,
298
+ })
299
+ })
300
+
301
+ return () => {
302
+ destroy$.next(undefined)
303
+ }
304
+ }
305
+
306
+
307
+ export const RoseLabels = defineSeriesPlugin(pluginName, DEFAULT_ROSE_LABELS_PARAMS)(({ selection, observer, subject }) => {
308
+
309
+ const destroy$ = new Subject()
310
+
311
+ const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
312
+ selection: selection,
313
+ pluginName,
314
+ separateSeries$: observer.separateSeries$,
315
+ seriesLabels$: observer.seriesLabels$,
316
+ seriesContainerPosition$: observer.seriesContainerPosition$
317
+ })
318
+
319
+ const unsubscribeFnArr: (() => void)[] = []
320
+
321
+ seriesCenterSelection$
322
+ .pipe(
323
+ takeUntil(destroy$)
324
+ )
325
+ .subscribe(seriesCenterSelection => {
326
+ // 每次重新計算時,清除之前的訂閱
327
+ unsubscribeFnArr.forEach(fn => fn())
328
+
329
+ seriesCenterSelection.each((d, containerIndex, g) => {
330
+
331
+ const containerSelection = d3.select(g[containerIndex])
332
+
333
+ const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
334
+ takeUntil(destroy$),
335
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
336
+ )
337
+
338
+ const containerPosition$ = observer.seriesContainerPosition$.pipe(
339
+ takeUntil(destroy$),
340
+ map(data => JSON.parse(JSON.stringify(data[containerIndex] ?? data[0])))
341
+ )
342
+
343
+ unsubscribeFnArr[containerIndex] = createEachPieLabel(pluginName, {
344
+ containerSelection: containerSelection,
345
+ // computedData$: observer.computedData$,
346
+ visibleComputedLayoutData$: observer.visibleComputedLayoutData$,
347
+ containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
348
+ // SeriesDataMap$: observer.SeriesDataMap$,
349
+ fullParams$: observer.fullParams$,
350
+ fullChartParams$: observer.fullChartParams$,
351
+ seriesHighlight$: observer.seriesHighlight$,
352
+ seriesContainerPosition$: containerPosition$,
353
+ event$: subject.event$,
354
+ })
355
+
356
+ })
357
+ })
358
+
359
+ return () => {
360
+ destroy$.next(undefined)
361
+ }
362
+ })
@@ -28,7 +28,8 @@ export function makePieData ({ data, startAngle, endAngle }: {
28
28
  .startAngle(startAngle)
29
29
  // .endAngle(startAngle + (endAngle - startAngle) * t)
30
30
  .endAngle(endAngle)
31
- .value((d) => d.visible == false ? 0 : d.value)
31
+ .value(d => d.value)
32
+ // .value((d) => d.visible == false ? 0 : d.value)
32
33
  // .sort(null) // 不要排序
33
34
  .sort((a, b) => a.seq - b.seq)
34
35
  // .sort((a: any, b: any) => {
@@ -1,7 +1,7 @@
1
1
  import type { ComputedDatumSeries, EventSeries, EventName, ColorType } from '@orbcharts/core'
2
2
  // import type { BaseLegendParams } from '../base/BaseLegend'
3
3
 
4
- export type BubbleScaleType = 'area' | 'radius'
4
+ export type ArcScaleType = 'area' | 'radius'
5
5
 
6
6
  export interface BubblesParams {
7
7
  force: {
@@ -15,16 +15,16 @@ export interface BubblesParams {
15
15
  lineLengthMin: number
16
16
  }
17
17
  highlightRIncrease: number
18
- bubbleScaleType: BubbleScaleType
18
+ arcScaleType: ArcScaleType
19
19
  }
20
20
 
21
21
  export interface PieParams {
22
22
  // padding: Padding
23
23
  outerRadius: number;
24
24
  innerRadius: number;
25
- outerMouseoverRadius: number;
25
+ mouseoverOuterRadius: number;
26
26
  // label?: LabelStyle
27
- enterDuration: number
27
+ // enterDuration: number
28
28
  startAngle: number
29
29
  endAngle: number
30
30
  padAngle: number
@@ -42,7 +42,7 @@ export interface PieLabelsParams {
42
42
  // solidColor?: string;
43
43
  // colors?: string[];
44
44
  outerRadius: number
45
- outerMouseoverRadius: number
45
+ mouseoverOuterRadius: number
46
46
  // innerRadius?: number;
47
47
  // enterDuration?: number
48
48
  startAngle: number
@@ -53,6 +53,22 @@ export interface PieLabelsParams {
53
53
  labelColorType: ColorType
54
54
  }
55
55
 
56
+ export interface RoseParams {
57
+ outerRadius: number
58
+ // padAngle: number
59
+ cornerRadius: number
60
+ arcScaleType: ArcScaleType
61
+ mouseoverAngleIncrease: number
62
+ }
63
+
64
+ export interface RoseLabelsParams {
65
+ outerRadius: number
66
+ labelCentroid: number
67
+ labelFn: ((d: ComputedDatumSeries) => string)
68
+ labelColorType: ColorType
69
+ arcScaleType: ArcScaleType
70
+ }
71
+
56
72
  export interface SeriesLegendParams {
57
73
  position: 'top' | 'bottom' | 'left' | 'right'
58
74
  justify: 'start' | 'center' | 'end'