@orbcharts/plugins-basic 3.0.0-alpha.68 → 3.0.0-alpha.70

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 (79) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +4838 -4760
  3. package/dist/orbcharts-plugins-basic.umd.js +15 -14
  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 +176 -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
+ })