@orbcharts/plugins-basic 3.0.0-alpha.68 → 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,598 +1,604 @@
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 { PieLabelsParams } from '../types'
20
- import type { PieDatum } from '../seriesUtils'
21
- import { DEFAULT_PIE_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
- arcLabels: string[]
31
- lineStartX: number
32
- lineStartY: number
33
- lineStartMouseoverX: number
34
- lineStartMouseoverY: number
35
- x: number
36
- y: number
37
- mouseoverX: number
38
- mouseoverY: number
39
- textWidth: number, // 文字寬度
40
- collisionShiftX: number // 避免碰撞的位移
41
- collisionShiftY: number
42
- quadrant: number // 第幾象限
43
- }
44
-
45
- const pluginName = 'PieLabels'
46
- const labelGClassName = getClassName(pluginName, 'label-g')
47
- const lineGClassName = getClassName(pluginName, 'line-g')
48
- const textClassName = getClassName(pluginName, 'text')
49
-
50
- const pieOuterCentroid = 2
51
-
52
- function makeRenderData ({ pieData, arc, arcMouseover, labelCentroid, lineStartCentroid, fullParams }: {
53
- pieData: PieDatum[]
54
- arc: d3.Arc<any, d3.DefaultArcObject>
55
- arcMouseover: d3.Arc<any, d3.DefaultArcObject>
56
- labelCentroid: number
57
- lineStartCentroid: number
58
- fullParams: PieLabelsParams
59
- }): RenderDatum[] {
60
- return pieData
61
- .map((d, i) => {
62
- const [_x, _y] = arc!.centroid(d as any)
63
- const [_mouseoverX, _mouseoverY] = arcMouseover!.centroid(d as any)
64
- const arcLabel = fullParams.labelFn(d.data)
65
- return {
66
- pieDatum: d,
67
- arcIndex: i,
68
- arcLabels: arcLabel.split('\n'),
69
- lineStartX: _x * lineStartCentroid,
70
- lineStartY: _y * lineStartCentroid,
71
- lineStartMouseoverX: _mouseoverX * lineStartCentroid,
72
- lineStartMouseoverY: _mouseoverY * lineStartCentroid,
73
- x: _x * labelCentroid!,
74
- y: _y * labelCentroid!,
75
- mouseoverX: _mouseoverX * labelCentroid!,
76
- mouseoverY: _mouseoverY * labelCentroid!,
77
- textWidth: 0, // 後面再做計算
78
- collisionShiftX: 0, // 後面再做計算
79
- collisionShiftY: 0, // 後面再做計算
80
- quadrant: _x >= 0 && _y <= 0
81
- ? 1
82
- : _x < 0 && _y <= 0
83
- ? 2
84
- : _x < 0 && _y > 0
85
- ? 3
86
- : 4
87
- }
88
- })
89
- .filter(d => d.pieDatum.data.visible)
90
- }
91
-
92
- // 繪製圓餅圖
93
- function renderLabel ({ labelGSelection, data, fullParams, fullChartParams, textSizePx }: {
94
- labelGSelection: d3.Selection<SVGGElement, undefined, any, any>
95
- data: RenderDatum[]
96
- fullParams: PieLabelsParams
97
- fullChartParams: ChartParams
98
- textSizePx: number
99
- }) {
100
- // console.log(data)
101
- // let update = this.gSelection.selectAll('g').data(pieData)
102
- const textSelection = labelGSelection
103
- .selectAll<SVGTextElement, RenderDatum>('text')
104
- .data(data, d => d.pieDatum.id)
105
- .join('text')
106
- .classed(textClassName, true)
107
- .attr('font-weight', 'bold')
108
- .attr('text-anchor', d => d.quadrant == 1 || d.quadrant == 4 ? 'start' : 'end')
109
- .style('dominant-baseline', d => d.quadrant == 1 || d.quadrant == 2 ? 'auto' : 'hanging')
110
- // .style('pointer-events', 'none')
111
- .style('cursor', d => fullChartParams.highlightTarget && fullChartParams.highlightTarget != 'none'
112
- ? 'pointer'
113
- : 'none')
114
- // .text(d => fullParams.labelFn(d.pieDatum.data))
115
- .attr('font-size', fullChartParams.styles.textSize)
116
- .attr('x', 0)
117
- .attr('y', 0)
118
- .attr('fill', (d, i) => getDatumColor({ datum: d.pieDatum.data, colorType: fullParams.labelColorType, fullChartParams }))
119
- .each((d, i, n) => {
120
- const textNode = d3.select<SVGTextElement, RenderDatum>(n[i])
121
- .selectAll('tspan')
122
- .data(d.arcLabels)
123
- .join('tspan')
124
- .attr('x', 0)
125
- .attr('y', (_d, _i) => d.quadrant == 1 || d.quadrant == 2
126
- ? - (d.arcLabels.length - 1 - _i) * textSizePx
127
- : _i * textSizePx)
128
- .text(d => d)
129
- })
130
- textSelection
131
- .transition()
132
- .attr('transform', (d) => {
133
- // console.log('transform', d)
134
- return 'translate(' + d.x + ',' + d.y + ')'
135
- })
136
- // .on('end', () => initHighlight({ labelSelection, data, fullChartParams }))
137
-
138
- // 如無新增資料則不用等動畫
139
- // if (enter.size() == 0) {
140
- // this.initHighlight()
141
- // }
142
-
143
- return textSelection
144
- }
145
-
146
- // // 獲取每個文字元素的邊界框並檢查是否重疊
147
- // function resolveCollisions(labelSelection: d3.Selection<SVGPathElement, RenderDatum, any, any>, data: RenderDatum[]) {
148
- // const textArray = labelSelection.nodes();
149
- // const padding = 10; // 調整文字間的間距
150
-
151
- // // 存儲每個標籤的當前位置
152
- // const positions = textArray.map((textNode, i) => {
153
- // const bbox = textNode.getBBox();
154
- // // const arcCentroid = arc.centroid(data[i]);
155
- // const arcCentroid = [data[i].x, data[i].y];
156
- // return {
157
- // node: textNode,
158
- // x: arcCentroid[0],
159
- // y: arcCentroid[1],
160
- // width: bbox.width,
161
- // height: bbox.height
162
- // };
163
- // });
164
- // // console.log('positions', positions)
165
-
166
- // for (let i = 0; i < positions.length; i++) {
167
- // const a = positions[i];
168
-
169
- // for (let j = i + 1; j < positions.length; j++) {
170
- // const b = positions[j];
171
-
172
- // // 檢查是否重疊
173
- // if (!(a.x + a.width / 2 < b.x - b.width / 2 ||
174
- // a.x - a.width / 2 > b.x + b.width / 2 ||
175
- // a.y + a.height / 2 < b.y - b.height / 2 ||
176
- // a.y - a.height / 2 > b.y + b.height / 2)) {
177
-
178
- // // 如果有重疊,則位移其中一個文字,這裡我們進行上下位移
179
- // const moveDown = (b.y > a.y) ? padding : -padding; // 決定方向
180
- // b.y += moveDown; // 更新 b y 座標
181
-
182
- // // 更新 b x 座標,根據與 a 的位置差異進行微調
183
- // const moveRight = (b.x > a.x) ? padding : -padding;
184
- // b.x += moveRight;
185
-
186
- // // // 重新設置 b 的 transform 來移動
187
- // d3.select(b.node)
188
- // .transition()
189
- // .attr("transform", `translate(${b.x},${b.y})`);
190
-
191
- // data[j].collisionShiftX = moveRight
192
- // data[j].collisionShiftY = moveDown
193
- // }
194
- // }
195
- // }
196
- // }
197
-
198
- // 獲取每個文字元素的邊界框並檢查是否重疊
199
- function resolveCollisions(textSelection: d3.Selection<SVGTextElement, RenderDatum, any, any>, data: RenderDatum[], textSizePx: number) {
200
- const textArray = textSelection.nodes();
201
- const padding = textSizePx // 調整文字間的間距
202
-
203
- // 存儲每個標籤的當前位置
204
- const positions = textArray.map((textNode, i) => {
205
- const bbox = textNode.getBBox();
206
- // const arcCentroid = arc.centroid(data[i]);
207
- const arcCentroid = [data[i].x, data[i].y];
208
- return {
209
- node: textNode,
210
- x: arcCentroid[0],
211
- y: arcCentroid[1],
212
- width: bbox.width,
213
- height: bbox.height
214
- }
215
- })
216
-
217
- // 順時針碰撞檢測(只處理 2、4 象限,將較後面的文字碰撞時往外偏移)
218
- for (let i = 0; i < positions.length; i++) {
219
- const a = positions[i]
220
-
221
- for (let j = i + 1; j < positions.length; j++) {
222
- const b = positions[j]
223
-
224
- // 記錄文字寬度
225
- data[i].textWidth = a.width
226
-
227
- const ax = a.x + data[i].collisionShiftX
228
- const ay = a.y + data[i].collisionShiftY
229
- const bx = b.x + data[j].collisionShiftX
230
- const by = b.y + data[j].collisionShiftY
231
-
232
- // 檢查是否重疊
233
- if (!(ax + a.width / 2 < bx - b.width / 2 ||
234
- ax - a.width / 2 > bx + b.width / 2 ||
235
- ay + a.height / 2 < by - b.height / 2 ||
236
- ay - a.height / 2 > by + b.height / 2)) {
237
-
238
- if (data[j].quadrant == 2) {
239
- const moveDown = (by > ay)
240
- ? -padding * 2
241
- : -padding
242
- data[j].collisionShiftY += moveDown // 由後一個累加高度
243
- } else if (data[j].quadrant == 4) {
244
- const moveDown = (by > ay)
245
- ? padding
246
- : padding * 2
247
- data[j].collisionShiftY += moveDown // 由後一個累加高度
248
- }
249
- }
250
- }
251
- }
252
-
253
- // 逆時針碰撞檢測(只處理 1、3 象限,將較前面的文字碰撞時往外偏移)
254
- for (let i = positions.length - 1; i >= 0; i--) {
255
- const a = positions[i]
256
-
257
- for (let j = i - 1; j >= 0; j--) {
258
- const b = positions[j]
259
-
260
- // 記錄文字寬度
261
- data[i].textWidth = a.width
262
-
263
- const ax = a.x + data[i].collisionShiftX
264
- const ay = a.y + data[i].collisionShiftY
265
- const bx = b.x + data[j].collisionShiftX
266
- const by = b.y + data[j].collisionShiftY
267
-
268
- // 檢查是否重疊
269
- if (!(ax + a.width / 2 < bx - b.width / 2 ||
270
- ax - a.width / 2 > bx + b.width / 2 ||
271
- ay + a.height / 2 < by - b.height / 2 ||
272
- ay - a.height / 2 > by + b.height / 2)) {
273
-
274
- if (data[j].quadrant == 1) {
275
- const moveDown = (by > ay)
276
- ? -padding * 2
277
- : -padding
278
- data[j].collisionShiftY += moveDown // 由前一個累加高度
279
- } else if (data[j].quadrant == 3) {
280
- const moveDown = (by > ay)
281
- ? padding
282
- : padding * 2
283
- data[j].collisionShiftY += moveDown // 由前一個累加高度
284
- }
285
- }
286
- }
287
- }
288
-
289
- // 全部算完再來 render
290
- textSelection
291
- .data(data)
292
- .transition()
293
- .attr('transform', (d) => {
294
- return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
295
- })
296
- }
297
-
298
- function renderLine ({ lineGSelection, data, fullParams, fullChartParams }: {
299
- lineGSelection: d3.Selection<SVGGElement, undefined, any, any>
300
- data: RenderDatum[]
301
- fullParams: PieLabelsParams
302
- fullChartParams: ChartParams
303
- }) {
304
-
305
- // 只顯示在有偏移的標籤
306
- const filteredData = data.filter(d => d.collisionShiftX || d.collisionShiftY)
307
-
308
- // 添加標籤的連接線
309
- const lines = lineGSelection.selectAll<SVGPolylineElement, RenderDatum>("polyline")
310
- .data(filteredData, d => d.pieDatum.id)
311
- .join("polyline")
312
- .attr("stroke", d => getDatumColor({ datum: d.pieDatum.data, colorType: fullParams.labelColorType, fullChartParams }))
313
- .attr("stroke-width", 1)
314
- .attr("fill", "none")
315
- .attr("points", (d) => {
316
- return [[d.lineStartX, d.lineStartY], [d.lineStartX, d.lineStartY]] as any // 畫出從弧線中心到延伸點的線
317
- })
318
- lines
319
- .transition()
320
- .attr("points", (d) => {
321
- // const pos = arc.centroid(d) // 起點:弧線的中心點
322
- // const outerPos = [pos[0] * 2.5, pos[1] * 2.5] // 外部延伸的點(乘以倍率來延長線段)
323
-
324
- let lineEndX = d.x + d.collisionShiftX
325
- let lineEndY = d.y + d.collisionShiftY
326
- // if (d.lineStartX >= Math.abs(d.lineStartY)) {
327
- // lineEndX -= d.textWidth / 2
328
- // } else if (d.lineStartX <= - Math.abs(d.lineStartY)) {
329
- // lineEndX += d.textWidth / 2
330
- // }
331
-
332
- return [[lineEndX, lineEndY], [d.lineStartX, d.lineStartY]] as any // 畫出從弧線中心到延伸點的線
333
- })
334
-
335
- return lines
336
- }
337
-
338
- function highlight ({ textSelection, lineSelection, ids, fullChartParams }: {
339
- textSelection: d3.Selection<SVGTextElement, RenderDatum, any, any>
340
- lineSelection: d3.Selection<SVGPolylineElement, RenderDatum, any, any>
341
- ids: string[]
342
- fullChartParams: ChartParams
343
- }) {
344
- textSelection.interrupt('highlight')
345
- lineSelection.interrupt('highlight')
346
-
347
- if (!ids.length) {
348
- textSelection
349
- .transition('highlight')
350
- .duration(200)
351
- .attr('transform', (d) => {
352
- return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
353
- })
354
- .style('opacity', 1)
355
- lineSelection
356
- .transition('highlight')
357
- .duration(200)
358
- .style('opacity', 1)
359
- return
360
- }
361
-
362
- textSelection.each((d, i, n) => {
363
- const segment = d3.select<SVGTextElement, RenderDatum>(n[i])
364
-
365
- if (ids.includes(d.pieDatum.id)) {
366
- segment
367
- .style('opacity', 1)
368
- .transition('highlight')
369
- .duration(200)
370
- .attr('transform', (d) => {
371
- // 如果已經有偏移過,則使用偏移後的位置(如果再改變的話很容易造成文字重疊)
372
- if (d.collisionShiftX || d.collisionShiftY) {
373
- return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
374
- }
375
- return `translate(${d.mouseoverX + d.collisionShiftX},${d.mouseoverY + d.collisionShiftY})`
376
- })
377
- } else {
378
- segment
379
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
380
- .transition('highlight')
381
- .duration(200)
382
- .attr('transform', (d) => {
383
- return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
384
- })
385
- }
386
- })
387
- lineSelection.each((d, i, n) => {
388
- const segment = d3.select<SVGPolylineElement, RenderDatum>(n[i])
389
-
390
- if (ids.includes(d.pieDatum.data.id)) {
391
- segment
392
- .style('opacity', 1)
393
- .transition('highlight')
394
- .duration(200)
395
- } else {
396
- segment
397
- .style('opacity', fullChartParams.styles.unhighlightedOpacity)
398
- .transition('highlight')
399
- .duration(200)
400
- }
401
- })
402
- }
403
-
404
-
405
- function createEachPieLabel (pluginName: string, context: {
406
- containerSelection: d3.Selection<SVGGElement, any, any, unknown>
407
- // computedData$: Observable<ComputedDatumSeries[][]>
408
- containerVisibleComputedLayoutData$: Observable<ComputedDatumSeries[]>
409
- // SeriesDataMap$: Observable<Map<string, ComputedDatumSeries[]>>
410
- fullParams$: Observable<PieLabelsParams>
411
- fullChartParams$: Observable<ChartParams>
412
- textSizePx$: Observable<number>
413
- seriesHighlight$: Observable<ComputedDatumSeries[]>
414
- seriesContainerPosition$: Observable<SeriesContainerPosition>
415
- event$: Subject<EventSeries>
416
- }) {
417
- const destroy$ = new Subject()
418
-
419
- context.containerSelection.selectAll('g').remove()
420
-
421
- const lineGSelection: d3.Selection<SVGGElement, any, any, unknown> = context.containerSelection.append('g')
422
- lineGSelection.classed(lineGClassName, true)
423
- const labelGSelection: d3.Selection<SVGGElement, any, any, unknown> = context.containerSelection.append('g')
424
- labelGSelection.classed(labelGClassName, true)
425
-
426
- // const graphicSelection: d3.Selection<SVGGElement, any, any, any> = selection.append('g')
427
- const textSelection$: Subject<d3.Selection<SVGTextElement, RenderDatum, any, any>> = new Subject()
428
- const lineSelection$: Subject<d3.Selection<SVGPolylineElement, RenderDatum, any, any>> = new Subject()
429
- let renderData: RenderDatum[] = []
430
-
431
- const shorterSideWith$ = context.seriesContainerPosition$.pipe(
432
- takeUntil(destroy$),
433
- map(d => d.width < d.height ? d.width : d.height),
434
- distinctUntilChanged()
435
- )
436
-
437
- const lineStartCentroid$ = context.fullParams$.pipe(
438
- takeUntil(destroy$),
439
- map(d => {
440
- return d.labelCentroid >= pieOuterCentroid
441
- ? pieOuterCentroid // 當 label在 pie的外側時,線條從 pie的邊緣開始
442
- : d.labelCentroid // 當 label在 pie的內側時,線條從 label未偏移前的位置開始
443
-
444
- })
445
- )
446
-
447
- combineLatest({
448
- shorterSideWith: shorterSideWith$,
449
- containerVisibleComputedLayoutData: context.containerVisibleComputedLayoutData$,
450
- fullParams: context.fullParams$,
451
- fullChartParams: context.fullChartParams$,
452
- textSizePx: context.textSizePx$,
453
- lineStartCentroid: lineStartCentroid$
454
- }).pipe(
455
- takeUntil(destroy$),
456
- switchMap(async (d) => d),
457
- ).subscribe(data => {
458
-
459
- // 弧產生器 (d3.arc())
460
- const arc = makeD3Arc({
461
- axisWidth: data.shorterSideWith,
462
- innerRadius: 0,
463
- outerRadius: data.fullParams.outerRadius,
464
- padAngle: 0,
465
- cornerRadius: 0
466
- })
467
-
468
- const arcMouseover = makeD3Arc({
469
- axisWidth: data.shorterSideWith,
470
- innerRadius: 0,
471
- outerRadius: data.fullParams.outerRadiusWhileHighlight, // 外半徑變化
472
- padAngle: 0,
473
- cornerRadius: 0
474
- })
475
-
476
- const pieData = makePieData({
477
- data: data.containerVisibleComputedLayoutData,
478
- startAngle: data.fullParams.startAngle,
479
- endAngle: data.fullParams.endAngle
480
- })
481
-
482
- renderData = makeRenderData({
483
- pieData,
484
- arc,
485
- arcMouseover,
486
- labelCentroid: data.fullParams.labelCentroid,
487
- lineStartCentroid: data.lineStartCentroid,
488
- fullParams: data.fullParams
489
- })
490
-
491
- // 先移除線條,等偏移後再重新繪製
492
- lineGSelection.selectAll('polyline').remove()
493
-
494
- const textSelection = renderLabel({
495
- labelGSelection,
496
- data: renderData,
497
- fullParams: data.fullParams,
498
- fullChartParams: data.fullChartParams,
499
- textSizePx: data.textSizePx
500
- })
501
-
502
- // 等 label 本身的 transition 結束後再進行碰撞檢測
503
- setTimeout(() => {
504
- // resolveCollisions(labelSelection, renderData)
505
- // 偏移 label
506
- resolveCollisions(textSelection, renderData, data.textSizePx)
507
-
508
- const lineSelection = renderLine({ lineGSelection, data: renderData, fullParams: data.fullParams, fullChartParams: data.fullChartParams })
509
-
510
- lineSelection$.next(lineSelection)
511
-
512
- }, 1000)
513
-
514
- textSelection$.next(textSelection)
515
-
516
- })
517
-
518
- combineLatest({
519
- textSelection: textSelection$,
520
- lineSelection: lineSelection$,
521
- highlight: context.seriesHighlight$.pipe(
522
- map(data => data.map(d => d.id))
523
- ),
524
- fullChartParams: context.fullChartParams$,
525
- }).pipe(
526
- takeUntil(destroy$),
527
- switchMap(async d => d)
528
- ).subscribe(data => {
529
- highlight({
530
- textSelection: data.textSelection,
531
- lineSelection: data.lineSelection,
532
- ids: data.highlight,
533
- fullChartParams: data.fullChartParams,
534
- })
535
- })
536
-
537
- return () => {
538
- destroy$.next(undefined)
539
- }
540
- }
541
-
542
-
543
- export const PieLabels = defineSeriesPlugin(pluginName, DEFAULT_PIE_LABELS_PARAMS)(({ selection, observer, subject }) => {
544
-
545
- const destroy$ = new Subject()
546
-
547
- const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
548
- selection: selection,
549
- pluginName,
550
- separateSeries$: observer.separateSeries$,
551
- seriesLabels$: observer.seriesLabels$,
552
- seriesContainerPosition$: observer.seriesContainerPosition$
553
- })
554
-
555
- const unsubscribeFnArr: (() => void)[] = []
556
-
557
- seriesCenterSelection$
558
- .pipe(
559
- takeUntil(destroy$)
560
- )
561
- .subscribe(seriesCenterSelection => {
562
- // 每次重新計算時,清除之前的訂閱
563
- unsubscribeFnArr.forEach(fn => fn())
564
-
565
- seriesCenterSelection.each((d, containerIndex, g) => {
566
-
567
- const containerSelection = d3.select(g[containerIndex])
568
-
569
- const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
570
- takeUntil(destroy$),
571
- map(data => data[containerIndex] ?? data[0])
572
- )
573
-
574
- const containerPosition$ = observer.seriesContainerPosition$.pipe(
575
- takeUntil(destroy$),
576
- map(data => data[containerIndex] ?? data[0])
577
- )
578
-
579
- unsubscribeFnArr[containerIndex] = createEachPieLabel(pluginName, {
580
- containerSelection: containerSelection,
581
- // computedData$: observer.computedData$,
582
- containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
583
- // SeriesDataMap$: observer.SeriesDataMap$,
584
- fullParams$: observer.fullParams$,
585
- fullChartParams$: observer.fullChartParams$,
586
- textSizePx$: observer.textSizePx$,
587
- seriesHighlight$: observer.seriesHighlight$,
588
- seriesContainerPosition$: containerPosition$,
589
- event$: subject.event$,
590
- })
591
-
592
- })
593
- })
594
-
595
- return () => {
596
- destroy$.next(undefined)
597
- }
598
- })
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 { PieLabelsParams } from '../types'
20
+ import type { PieDatum } from '../seriesUtils'
21
+ import { DEFAULT_PIE_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
+ import { renderTspansOnQuadrant } from '../../utils/d3Graphics'
27
+
28
+ interface RenderDatum {
29
+ pieDatum: PieDatum
30
+ arcIndex: number
31
+ arcLabels: string[]
32
+ lineStartX: number
33
+ lineStartY: number
34
+ lineStartMouseoverX: number
35
+ lineStartMouseoverY: number
36
+ x: number
37
+ y: number
38
+ mouseoverX: number
39
+ mouseoverY: number
40
+ textWidth: number, // 文字寬度
41
+ collisionShiftX: number // 避免碰撞的位移
42
+ collisionShiftY: number
43
+ quadrant: number // 第幾象限
44
+ }
45
+
46
+ const pluginName = 'PieLabels'
47
+ const labelGClassName = getClassName(pluginName, 'label-g')
48
+ const lineGClassName = getClassName(pluginName, 'line-g')
49
+ const textClassName = getClassName(pluginName, 'text')
50
+
51
+ const pieOuterCentroid = 2
52
+
53
+ function makeRenderData ({ pieData, arc, arcMouseover, labelCentroid, lineStartCentroid, fullParams }: {
54
+ pieData: PieDatum[]
55
+ arc: d3.Arc<any, d3.DefaultArcObject>
56
+ arcMouseover: d3.Arc<any, d3.DefaultArcObject>
57
+ labelCentroid: number
58
+ lineStartCentroid: number
59
+ fullParams: PieLabelsParams
60
+ }): RenderDatum[] {
61
+ return pieData
62
+ .map((d, i) => {
63
+ const [_x, _y] = arc!.centroid(d as any)
64
+ const [_mouseoverX, _mouseoverY] = arcMouseover!.centroid(d as any)
65
+ const arcLabel = fullParams.labelFn(d.data)
66
+ return {
67
+ pieDatum: d,
68
+ arcIndex: i,
69
+ arcLabels: arcLabel.split('\n'),
70
+ lineStartX: _x * lineStartCentroid,
71
+ lineStartY: _y * lineStartCentroid,
72
+ lineStartMouseoverX: _mouseoverX * lineStartCentroid,
73
+ lineStartMouseoverY: _mouseoverY * lineStartCentroid,
74
+ x: _x * labelCentroid!,
75
+ y: _y * labelCentroid!,
76
+ mouseoverX: _mouseoverX * labelCentroid!,
77
+ mouseoverY: _mouseoverY * labelCentroid!,
78
+ textWidth: 0, // 後面再做計算
79
+ collisionShiftX: 0, // 後面再做計算
80
+ collisionShiftY: 0, // 後面再做計算
81
+ quadrant: _x >= 0 && _y <= 0
82
+ ? 1
83
+ : _x < 0 && _y <= 0
84
+ ? 2
85
+ : _x < 0 && _y > 0
86
+ ? 3
87
+ : 4
88
+ }
89
+ })
90
+ .filter(d => d.pieDatum.data.visible)
91
+ }
92
+
93
+ // 繪製圓餅圖
94
+ function renderLabel ({ labelGSelection, data, fullParams, fullChartParams, textSizePx }: {
95
+ labelGSelection: d3.Selection<SVGGElement, undefined, any, any>
96
+ data: RenderDatum[]
97
+ fullParams: PieLabelsParams
98
+ fullChartParams: ChartParams
99
+ textSizePx: number
100
+ }) {
101
+ // console.log(data)
102
+ // let update = this.gSelection.selectAll('g').data(pieData)
103
+ const textSelection = labelGSelection
104
+ .selectAll<SVGTextElement, RenderDatum>('text')
105
+ .data(data, d => d.pieDatum.id)
106
+ .join('text')
107
+ .classed(textClassName, true)
108
+ .attr('font-weight', 'bold')
109
+ .attr('text-anchor', d => d.quadrant == 1 || d.quadrant == 4 ? 'start' : 'end')
110
+ .style('dominant-baseline', d => d.quadrant == 1 || d.quadrant == 2 ? 'auto' : 'hanging')
111
+ // .style('pointer-events', 'none')
112
+ .style('cursor', d => fullChartParams.highlightTarget && fullChartParams.highlightTarget != 'none'
113
+ ? 'pointer'
114
+ : 'none')
115
+ // .text(d => fullParams.labelFn(d.pieDatum.data))
116
+ .attr('font-size', fullChartParams.styles.textSize)
117
+ .attr('x', 0)
118
+ .attr('y', 0)
119
+ .attr('fill', (d, i) => getDatumColor({ datum: d.pieDatum.data, colorType: fullParams.labelColorType, fullChartParams }))
120
+ .each((d, i, n) => {
121
+ // const textNode = d3.select<SVGTextElement, RenderDatum>(n[i])
122
+ // .selectAll('tspan')
123
+ // .data(d.arcLabels)
124
+ // .join('tspan')
125
+ // .attr('x', 0)
126
+ // .attr('y', (_d, _i) => d.quadrant == 1 || d.quadrant == 2
127
+ // ? - (d.arcLabels.length - 1 - _i) * textSizePx
128
+ // : _i * textSizePx)
129
+ // .text(d => d)
130
+ renderTspansOnQuadrant(d3.select<SVGTextElement, RenderDatum>(n[i]), {
131
+ textArr: d.arcLabels,
132
+ textSizePx,
133
+ quadrant: d.quadrant
134
+ })
135
+ })
136
+ textSelection
137
+ .transition()
138
+ .attr('transform', (d) => {
139
+ // console.log('transform', d)
140
+ return 'translate(' + d.x + ',' + d.y + ')'
141
+ })
142
+ // .on('end', () => initHighlight({ labelSelection, data, fullChartParams }))
143
+
144
+ // 如無新增資料則不用等動畫
145
+ // if (enter.size() == 0) {
146
+ // this.initHighlight()
147
+ // }
148
+
149
+ return textSelection
150
+ }
151
+
152
+ // // 獲取每個文字元素的邊界框並檢查是否重疊
153
+ // function resolveCollisions(labelSelection: d3.Selection<SVGPathElement, RenderDatum, any, any>, data: RenderDatum[]) {
154
+ // const textArray = labelSelection.nodes();
155
+ // const padding = 10; // 調整文字間的間距
156
+
157
+ // // 存儲每個標籤的當前位置
158
+ // const positions = textArray.map((textNode, i) => {
159
+ // const bbox = textNode.getBBox();
160
+ // // const arcCentroid = arc.centroid(data[i]);
161
+ // const arcCentroid = [data[i].x, data[i].y];
162
+ // return {
163
+ // node: textNode,
164
+ // x: arcCentroid[0],
165
+ // y: arcCentroid[1],
166
+ // width: bbox.width,
167
+ // height: bbox.height
168
+ // };
169
+ // });
170
+ // // console.log('positions', positions)
171
+
172
+ // for (let i = 0; i < positions.length; i++) {
173
+ // const a = positions[i];
174
+
175
+ // for (let j = i + 1; j < positions.length; j++) {
176
+ // const b = positions[j];
177
+
178
+ // // 檢查是否重疊
179
+ // if (!(a.x + a.width / 2 < b.x - b.width / 2 ||
180
+ // a.x - a.width / 2 > b.x + b.width / 2 ||
181
+ // a.y + a.height / 2 < b.y - b.height / 2 ||
182
+ // a.y - a.height / 2 > b.y + b.height / 2)) {
183
+
184
+ // // 如果有重疊,則位移其中一個文字,這裡我們進行上下位移
185
+ // const moveDown = (b.y > a.y) ? padding : -padding; // 決定方向
186
+ // b.y += moveDown; // 更新 b 的 y 座標
187
+
188
+ // // 更新 b 的 x 座標,根據與 a 的位置差異進行微調
189
+ // const moveRight = (b.x > a.x) ? padding : -padding;
190
+ // b.x += moveRight;
191
+
192
+ // // // 重新設置 b 的 transform 來移動
193
+ // d3.select(b.node)
194
+ // .transition()
195
+ // .attr("transform", `translate(${b.x},${b.y})`);
196
+
197
+ // data[j].collisionShiftX = moveRight
198
+ // data[j].collisionShiftY = moveDown
199
+ // }
200
+ // }
201
+ // }
202
+ // }
203
+
204
+ // 獲取每個文字元素的邊界框並檢查是否重疊
205
+ function resolveCollisions(textSelection: d3.Selection<SVGTextElement, RenderDatum, any, any>, data: RenderDatum[], textSizePx: number) {
206
+ const textArray = textSelection.nodes();
207
+ const padding = textSizePx // 調整文字間的間距
208
+
209
+ // 存儲每個標籤的當前位置
210
+ const positions = textArray.map((textNode, i) => {
211
+ const bbox = textNode.getBBox();
212
+ // const arcCentroid = arc.centroid(data[i]);
213
+ const arcCentroid = [data[i].x, data[i].y];
214
+ return {
215
+ node: textNode,
216
+ x: arcCentroid[0],
217
+ y: arcCentroid[1],
218
+ width: bbox.width,
219
+ height: bbox.height
220
+ }
221
+ })
222
+
223
+ // 順時針碰撞檢測(只處理 2、4 象限,將較後面的文字碰撞時往外偏移)
224
+ for (let i = 0; i < positions.length; i++) {
225
+ const a = positions[i]
226
+
227
+ for (let j = i + 1; j < positions.length; j++) {
228
+ const b = positions[j]
229
+
230
+ // 記錄文字寬度
231
+ data[i].textWidth = a.width
232
+
233
+ const ax = a.x + data[i].collisionShiftX
234
+ const ay = a.y + data[i].collisionShiftY
235
+ const bx = b.x + data[j].collisionShiftX
236
+ const by = b.y + data[j].collisionShiftY
237
+
238
+ // 檢查是否重疊
239
+ if (!(ax + a.width / 2 < bx - b.width / 2 ||
240
+ ax - a.width / 2 > bx + b.width / 2 ||
241
+ ay + a.height / 2 < by - b.height / 2 ||
242
+ ay - a.height / 2 > by + b.height / 2)) {
243
+
244
+ if (data[j].quadrant == 2) {
245
+ const moveDown = (by > ay)
246
+ ? -padding * 2
247
+ : -padding
248
+ data[j].collisionShiftY += moveDown // 由後一個累加高度
249
+ } else if (data[j].quadrant == 4) {
250
+ const moveDown = (by > ay)
251
+ ? padding
252
+ : padding * 2
253
+ data[j].collisionShiftY += moveDown // 由後一個累加高度
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ // 逆時針碰撞檢測(只處理 1、3 象限,將較前面的文字碰撞時往外偏移)
260
+ for (let i = positions.length - 1; i >= 0; i--) {
261
+ const a = positions[i]
262
+
263
+ for (let j = i - 1; j >= 0; j--) {
264
+ const b = positions[j]
265
+
266
+ // 記錄文字寬度
267
+ data[i].textWidth = a.width
268
+
269
+ const ax = a.x + data[i].collisionShiftX
270
+ const ay = a.y + data[i].collisionShiftY
271
+ const bx = b.x + data[j].collisionShiftX
272
+ const by = b.y + data[j].collisionShiftY
273
+
274
+ // 檢查是否重疊
275
+ if (!(ax + a.width / 2 < bx - b.width / 2 ||
276
+ ax - a.width / 2 > bx + b.width / 2 ||
277
+ ay + a.height / 2 < by - b.height / 2 ||
278
+ ay - a.height / 2 > by + b.height / 2)) {
279
+
280
+ if (data[j].quadrant == 1) {
281
+ const moveDown = (by > ay)
282
+ ? -padding * 2
283
+ : -padding
284
+ data[j].collisionShiftY += moveDown // 由前一個累加高度
285
+ } else if (data[j].quadrant == 3) {
286
+ const moveDown = (by > ay)
287
+ ? padding
288
+ : padding * 2
289
+ data[j].collisionShiftY += moveDown // 由前一個累加高度
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ // 全部算完再來 render
296
+ textSelection
297
+ .data(data)
298
+ .transition()
299
+ .attr('transform', (d) => {
300
+ return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
301
+ })
302
+ }
303
+
304
+ function renderLine ({ lineGSelection, data, fullParams, fullChartParams }: {
305
+ lineGSelection: d3.Selection<SVGGElement, undefined, any, any>
306
+ data: RenderDatum[]
307
+ fullParams: PieLabelsParams
308
+ fullChartParams: ChartParams
309
+ }) {
310
+
311
+ // 只顯示在有偏移的標籤
312
+ const filteredData = data.filter(d => d.collisionShiftX || d.collisionShiftY)
313
+
314
+ // 添加標籤的連接線
315
+ const lines = lineGSelection.selectAll<SVGPolylineElement, RenderDatum>("polyline")
316
+ .data(filteredData, d => d.pieDatum.id)
317
+ .join("polyline")
318
+ .attr("stroke", d => getDatumColor({ datum: d.pieDatum.data, colorType: fullParams.labelColorType, fullChartParams }))
319
+ .attr("stroke-width", 1)
320
+ .attr("fill", "none")
321
+ .attr("points", (d) => {
322
+ return [[d.lineStartX, d.lineStartY], [d.lineStartX, d.lineStartY]] as any // 畫出從弧線中心到延伸點的線
323
+ })
324
+ lines
325
+ .transition()
326
+ .attr("points", (d) => {
327
+ // const pos = arc.centroid(d) // 起點:弧線的中心點
328
+ // const outerPos = [pos[0] * 2.5, pos[1] * 2.5] // 外部延伸的點(乘以倍率來延長線段)
329
+
330
+ let lineEndX = d.x + d.collisionShiftX
331
+ let lineEndY = d.y + d.collisionShiftY
332
+ // if (d.lineStartX >= Math.abs(d.lineStartY)) {
333
+ // lineEndX -= d.textWidth / 2
334
+ // } else if (d.lineStartX <= - Math.abs(d.lineStartY)) {
335
+ // lineEndX += d.textWidth / 2
336
+ // }
337
+
338
+ return [[lineEndX, lineEndY], [d.lineStartX, d.lineStartY]] as any // 畫出從弧線中心到延伸點的線
339
+ })
340
+
341
+ return lines
342
+ }
343
+
344
+ function highlight ({ textSelection, lineSelection, ids, fullChartParams }: {
345
+ textSelection: d3.Selection<SVGTextElement, RenderDatum, any, any>
346
+ lineSelection: d3.Selection<SVGPolylineElement, RenderDatum, any, any>
347
+ ids: string[]
348
+ fullChartParams: ChartParams
349
+ }) {
350
+ textSelection.interrupt('highlight')
351
+ lineSelection.interrupt('highlight')
352
+
353
+ if (!ids.length) {
354
+ textSelection
355
+ .transition('highlight')
356
+ .duration(200)
357
+ .attr('transform', (d) => {
358
+ return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
359
+ })
360
+ .style('opacity', 1)
361
+ lineSelection
362
+ .transition('highlight')
363
+ .duration(200)
364
+ .style('opacity', 1)
365
+ return
366
+ }
367
+
368
+ textSelection.each((d, i, n) => {
369
+ const segment = d3.select<SVGTextElement, RenderDatum>(n[i])
370
+
371
+ if (ids.includes(d.pieDatum.id)) {
372
+ segment
373
+ .style('opacity', 1)
374
+ .transition('highlight')
375
+ .duration(200)
376
+ .attr('transform', (d) => {
377
+ // 如果已經有偏移過,則使用偏移後的位置(如果再改變的話很容易造成文字重疊)
378
+ if (d.collisionShiftX || d.collisionShiftY) {
379
+ return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
380
+ }
381
+ return `translate(${d.mouseoverX + d.collisionShiftX},${d.mouseoverY + d.collisionShiftY})`
382
+ })
383
+ } else {
384
+ segment
385
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
386
+ .transition('highlight')
387
+ .duration(200)
388
+ .attr('transform', (d) => {
389
+ return `translate(${d.x + d.collisionShiftX},${d.y + d.collisionShiftY})`
390
+ })
391
+ }
392
+ })
393
+ lineSelection.each((d, i, n) => {
394
+ const segment = d3.select<SVGPolylineElement, RenderDatum>(n[i])
395
+
396
+ if (ids.includes(d.pieDatum.data.id)) {
397
+ segment
398
+ .style('opacity', 1)
399
+ .transition('highlight')
400
+ .duration(200)
401
+ } else {
402
+ segment
403
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
404
+ .transition('highlight')
405
+ .duration(200)
406
+ }
407
+ })
408
+ }
409
+
410
+
411
+ function createEachPieLabel (pluginName: string, context: {
412
+ containerSelection: d3.Selection<SVGGElement, any, any, unknown>
413
+ // computedData$: Observable<ComputedDatumSeries[][]>
414
+ containerVisibleComputedLayoutData$: Observable<ComputedDatumSeries[]>
415
+ // SeriesDataMap$: Observable<Map<string, ComputedDatumSeries[]>>
416
+ fullParams$: Observable<PieLabelsParams>
417
+ fullChartParams$: Observable<ChartParams>
418
+ textSizePx$: Observable<number>
419
+ seriesHighlight$: Observable<ComputedDatumSeries[]>
420
+ seriesContainerPosition$: Observable<SeriesContainerPosition>
421
+ event$: Subject<EventSeries>
422
+ }) {
423
+ const destroy$ = new Subject()
424
+
425
+ context.containerSelection.selectAll('g').remove()
426
+
427
+ const lineGSelection: d3.Selection<SVGGElement, any, any, unknown> = context.containerSelection.append('g')
428
+ lineGSelection.classed(lineGClassName, true)
429
+ const labelGSelection: d3.Selection<SVGGElement, any, any, unknown> = context.containerSelection.append('g')
430
+ labelGSelection.classed(labelGClassName, true)
431
+
432
+ // const graphicSelection: d3.Selection<SVGGElement, any, any, any> = selection.append('g')
433
+ const textSelection$: Subject<d3.Selection<SVGTextElement, RenderDatum, any, any>> = new Subject()
434
+ const lineSelection$: Subject<d3.Selection<SVGPolylineElement, RenderDatum, any, any>> = new Subject()
435
+ let renderData: RenderDatum[] = []
436
+
437
+ const shorterSideWith$ = context.seriesContainerPosition$.pipe(
438
+ takeUntil(destroy$),
439
+ map(d => d.width < d.height ? d.width : d.height),
440
+ distinctUntilChanged()
441
+ )
442
+
443
+ const lineStartCentroid$ = context.fullParams$.pipe(
444
+ takeUntil(destroy$),
445
+ map(d => {
446
+ return d.labelCentroid >= pieOuterCentroid
447
+ ? pieOuterCentroid // 當 label在 pie的外側時,線條從 pie的邊緣開始
448
+ : d.labelCentroid // 當 label在 pie的內側時,線條從 label未偏移前的位置開始
449
+
450
+ })
451
+ )
452
+
453
+ combineLatest({
454
+ shorterSideWith: shorterSideWith$,
455
+ containerVisibleComputedLayoutData: context.containerVisibleComputedLayoutData$,
456
+ fullParams: context.fullParams$,
457
+ fullChartParams: context.fullChartParams$,
458
+ textSizePx: context.textSizePx$,
459
+ lineStartCentroid: lineStartCentroid$
460
+ }).pipe(
461
+ takeUntil(destroy$),
462
+ switchMap(async (d) => d),
463
+ ).subscribe(data => {
464
+
465
+ // 弧產生器 (d3.arc())
466
+ const arc = makeD3Arc({
467
+ axisWidth: data.shorterSideWith,
468
+ innerRadius: 0,
469
+ outerRadius: data.fullParams.outerRadius,
470
+ padAngle: 0,
471
+ cornerRadius: 0
472
+ })
473
+
474
+ const arcMouseover = makeD3Arc({
475
+ axisWidth: data.shorterSideWith,
476
+ innerRadius: 0,
477
+ outerRadius: data.fullParams.outerRadiusWhileHighlight, // 外半徑變化
478
+ padAngle: 0,
479
+ cornerRadius: 0
480
+ })
481
+
482
+ const pieData = makePieData({
483
+ data: data.containerVisibleComputedLayoutData,
484
+ startAngle: data.fullParams.startAngle,
485
+ endAngle: data.fullParams.endAngle
486
+ })
487
+
488
+ renderData = makeRenderData({
489
+ pieData,
490
+ arc,
491
+ arcMouseover,
492
+ labelCentroid: data.fullParams.labelCentroid,
493
+ lineStartCentroid: data.lineStartCentroid,
494
+ fullParams: data.fullParams
495
+ })
496
+
497
+ // 先移除線條,等偏移後再重新繪製
498
+ lineGSelection.selectAll('polyline').remove()
499
+
500
+ const textSelection = renderLabel({
501
+ labelGSelection,
502
+ data: renderData,
503
+ fullParams: data.fullParams,
504
+ fullChartParams: data.fullChartParams,
505
+ textSizePx: data.textSizePx
506
+ })
507
+
508
+ // label 本身的 transition 結束後再進行碰撞檢測
509
+ setTimeout(() => {
510
+ // resolveCollisions(labelSelection, renderData)
511
+ // 偏移 label
512
+ resolveCollisions(textSelection, renderData, data.textSizePx)
513
+
514
+ const lineSelection = renderLine({ lineGSelection, data: renderData, fullParams: data.fullParams, fullChartParams: data.fullChartParams })
515
+
516
+ lineSelection$.next(lineSelection)
517
+
518
+ }, 1000)
519
+
520
+ textSelection$.next(textSelection)
521
+
522
+ })
523
+
524
+ combineLatest({
525
+ textSelection: textSelection$,
526
+ lineSelection: lineSelection$,
527
+ highlight: context.seriesHighlight$.pipe(
528
+ map(data => data.map(d => d.id))
529
+ ),
530
+ fullChartParams: context.fullChartParams$,
531
+ }).pipe(
532
+ takeUntil(destroy$),
533
+ switchMap(async d => d)
534
+ ).subscribe(data => {
535
+ highlight({
536
+ textSelection: data.textSelection,
537
+ lineSelection: data.lineSelection,
538
+ ids: data.highlight,
539
+ fullChartParams: data.fullChartParams,
540
+ })
541
+ })
542
+
543
+ return () => {
544
+ destroy$.next(undefined)
545
+ }
546
+ }
547
+
548
+
549
+ export const PieLabels = defineSeriesPlugin(pluginName, DEFAULT_PIE_LABELS_PARAMS)(({ selection, observer, subject }) => {
550
+
551
+ const destroy$ = new Subject()
552
+
553
+ const { seriesCenterSelection$ } = seriesCenterSelectionObservable({
554
+ selection: selection,
555
+ pluginName,
556
+ separateSeries$: observer.separateSeries$,
557
+ seriesLabels$: observer.seriesLabels$,
558
+ seriesContainerPosition$: observer.seriesContainerPosition$
559
+ })
560
+
561
+ const unsubscribeFnArr: (() => void)[] = []
562
+
563
+ seriesCenterSelection$
564
+ .pipe(
565
+ takeUntil(destroy$)
566
+ )
567
+ .subscribe(seriesCenterSelection => {
568
+ // 每次重新計算時,清除之前的訂閱
569
+ unsubscribeFnArr.forEach(fn => fn())
570
+
571
+ seriesCenterSelection.each((d, containerIndex, g) => {
572
+
573
+ const containerSelection = d3.select(g[containerIndex])
574
+
575
+ const containerVisibleComputedLayoutData$ = observer.visibleComputedLayoutData$.pipe(
576
+ takeUntil(destroy$),
577
+ map(data => data[containerIndex] ?? data[0])
578
+ )
579
+
580
+ const containerPosition$ = observer.seriesContainerPosition$.pipe(
581
+ takeUntil(destroy$),
582
+ map(data => data[containerIndex] ?? data[0])
583
+ )
584
+
585
+ unsubscribeFnArr[containerIndex] = createEachPieLabel(pluginName, {
586
+ containerSelection: containerSelection,
587
+ // computedData$: observer.computedData$,
588
+ containerVisibleComputedLayoutData$: containerVisibleComputedLayoutData$,
589
+ // SeriesDataMap$: observer.SeriesDataMap$,
590
+ fullParams$: observer.fullParams$,
591
+ fullChartParams$: observer.fullChartParams$,
592
+ textSizePx$: observer.textSizePx$,
593
+ seriesHighlight$: observer.seriesHighlight$,
594
+ seriesContainerPosition$: containerPosition$,
595
+ event$: subject.event$,
596
+ })
597
+
598
+ })
599
+ })
600
+
601
+ return () => {
602
+ destroy$.next(undefined)
603
+ }
604
+ })