@orbcharts/plugins-basic 3.0.0-alpha.59 → 3.0.0-alpha.60

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. package/LICENSE +200 -200
  2. package/package.json +42 -43
  3. package/src/base/BaseBarStack.ts +778 -778
  4. package/src/base/BaseBars.ts +764 -764
  5. package/src/base/BaseBarsTriangle.ts +672 -672
  6. package/src/base/BaseDots.ts +513 -513
  7. package/src/base/BaseGroupAxis.ts +558 -558
  8. package/src/base/BaseLegend.ts +641 -641
  9. package/src/base/BaseLineAreas.ts +628 -628
  10. package/src/base/BaseLines.ts +704 -704
  11. package/src/base/BaseValueAxis.ts +478 -478
  12. package/src/base/types.ts +2 -2
  13. package/src/grid/defaults.ts +128 -128
  14. package/src/grid/gridObservables.ts +541 -541
  15. package/src/grid/index.ts +15 -15
  16. package/src/grid/plugins/BarStack.ts +43 -43
  17. package/src/grid/plugins/Bars.ts +44 -44
  18. package/src/grid/plugins/BarsPN.ts +41 -41
  19. package/src/grid/plugins/BarsTriangle.ts +42 -42
  20. package/src/grid/plugins/Dots.ts +37 -37
  21. package/src/grid/plugins/GridLegend.ts +59 -59
  22. package/src/grid/plugins/GroupAux.ts +976 -976
  23. package/src/grid/plugins/GroupAxis.ts +35 -35
  24. package/src/grid/plugins/LineAreas.ts +40 -40
  25. package/src/grid/plugins/Lines.ts +40 -40
  26. package/src/grid/plugins/ScalingArea.ts +173 -173
  27. package/src/grid/plugins/ValueAxis.ts +36 -36
  28. package/src/grid/plugins/ValueStackAxis.ts +38 -38
  29. package/src/grid/types.ts +123 -123
  30. package/src/index.ts +9 -9
  31. package/src/multiGrid/defaults.ts +158 -158
  32. package/src/multiGrid/index.ts +13 -13
  33. package/src/multiGrid/multiGridObservables.ts +49 -49
  34. package/src/multiGrid/plugins/MultiBarStack.ts +78 -78
  35. package/src/multiGrid/plugins/MultiBars.ts +77 -77
  36. package/src/multiGrid/plugins/MultiBarsTriangle.ts +77 -77
  37. package/src/multiGrid/plugins/MultiDots.ts +65 -65
  38. package/src/multiGrid/plugins/MultiGridLegend.ts +89 -89
  39. package/src/multiGrid/plugins/MultiGroupAxis.ts +69 -69
  40. package/src/multiGrid/plugins/MultiLineAreas.ts +77 -77
  41. package/src/multiGrid/plugins/MultiLines.ts +77 -77
  42. package/src/multiGrid/plugins/MultiValueAxis.ts +69 -69
  43. package/src/multiGrid/plugins/MultiValueStackAxis.ts +69 -69
  44. package/src/multiGrid/plugins/OverlappingValueAxes.ts +167 -167
  45. package/src/multiGrid/plugins/OverlappingValueStackAxes.ts +168 -168
  46. package/src/multiGrid/types.ts +72 -72
  47. package/src/noneData/defaults.ts +102 -102
  48. package/src/noneData/index.ts +3 -3
  49. package/src/noneData/plugins/Container.ts +10 -10
  50. package/src/noneData/plugins/Tooltip.ts +310 -310
  51. package/src/noneData/types.ts +26 -26
  52. package/src/series/defaults.ts +144 -144
  53. package/src/series/index.ts +9 -9
  54. package/src/series/plugins/Bubbles.ts +545 -545
  55. package/src/series/plugins/Pie.ts +576 -576
  56. package/src/series/plugins/PieEventTexts.ts +262 -262
  57. package/src/series/plugins/PieLabels.ts +304 -304
  58. package/src/series/plugins/Rose.ts +472 -472
  59. package/src/series/plugins/RoseLabels.ts +362 -362
  60. package/src/series/plugins/SeriesLegend.ts +59 -59
  61. package/src/series/seriesObservables.ts +145 -145
  62. package/src/series/seriesUtils.ts +51 -51
  63. package/src/series/types.ts +83 -83
  64. package/src/tree/defaults.ts +23 -23
  65. package/src/tree/index.ts +3 -3
  66. package/src/tree/plugins/TreeLegend.ts +59 -59
  67. package/src/tree/plugins/TreeMap.ts +305 -305
  68. package/src/tree/types.ts +23 -23
  69. package/src/utils/commonUtils.ts +21 -21
  70. package/src/utils/d3Graphics.ts +124 -124
  71. package/src/utils/d3Utils.ts +73 -73
  72. package/src/utils/observables.ts +14 -14
  73. package/src/utils/orbchartsUtils.ts +100 -100
  74. package/tsconfig.base.json +13 -13
  75. package/tsconfig.json +2 -7
  76. package/{vite.config.mjs → vite.config.js} +22 -40
  77. package/tsconfig.prod.json +0 -3
  78. /package/dist/{vite.config.d.mts → vite.config.d.ts} +0 -0
@@ -1,642 +1,642 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- switchMap,
6
- takeUntil,
7
- shareReplay,
8
- Observable,
9
- Subject } from 'rxjs'
10
- import type { BasePluginFn } from './types'
11
- import type {
12
- ChartParams, Layout, ColorType } from '@orbcharts/core'
13
- import { getClassName, getColor, getDatumColor } from '../utils/orbchartsUtils'
14
- import { measureTextWidth } from '../utils/commonUtils'
15
-
16
- export interface BaseLegendParams {
17
- position: 'top' | 'bottom' | 'left' | 'right'
18
- justify: 'start' | 'center' | 'end'
19
- padding: number
20
- // offset: [number, number]
21
- backgroundFill: ColorType
22
- backgroundStroke: ColorType
23
- textColorType: ColorType
24
- gap: number
25
- seriesList: Array<{
26
- listRectWidth: number
27
- listRectHeight: number
28
- listRectRadius: number
29
- }>
30
- // highlightEvent: boolean
31
- }
32
-
33
- interface BaseLegendContext {
34
- rootSelection: d3.Selection<any, unknown, any, unknown>
35
- seriesLabels$: Observable<string[]>
36
- fullParams$: Observable<BaseLegendParams>
37
- layout$: Observable<Layout>
38
- fullChartParams$: Observable<ChartParams>
39
- textSizePx$: Observable<number>
40
- }
41
-
42
- // 第1層 - 定位的容器
43
- interface RootPosition {
44
- x:number
45
- y:number
46
- }
47
-
48
- // 第2層 - 卡片
49
- interface LegendCard {
50
- width: number
51
- height: number
52
- translateX:number
53
- translateY:number
54
- }
55
-
56
- // 第3層 - 圖例列表
57
- interface LegendList {
58
- direction: 'row' | 'column'
59
- width: number
60
- height: number
61
- translateX:number
62
- translateY:number
63
- // list: LegendItem[][]
64
- }
65
-
66
- // 第4層 - 圖例項目
67
- interface LegendItem {
68
- id: string // seriesLabel
69
- seriesLabel: string
70
- seriesIndex: number
71
- lineIndex: number
72
- itemIndex: number // 行內的item
73
- text: string
74
- itemWidth: number
75
- translateX: number
76
- translateY: number
77
- color: string
78
- listRectWidth: number
79
- listRectHeight: number
80
- listRectRadius: number
81
- }
82
-
83
- interface ListStyle {
84
- listRectWidth: number
85
- listRectHeight: number
86
- listRectRadius: number
87
- }
88
-
89
- const defaultListStyle: ListStyle = {
90
- listRectWidth: 14,
91
- listRectHeight: 14,
92
- listRectRadius: 0,
93
- }
94
-
95
- function getSeriesColor (seriesIndex: number, fullChartParams: ChartParams) {
96
- const colorIndex = seriesIndex < fullChartParams.colors[fullChartParams.colorScheme].series.length
97
- ? seriesIndex
98
- : seriesIndex % fullChartParams.colors[fullChartParams.colorScheme].series.length
99
- return fullChartParams.colors[fullChartParams.colorScheme].series[colorIndex]
100
- }
101
-
102
- function getLegendColor () {
103
-
104
- }
105
-
106
- export const createBaseLegend: BasePluginFn<BaseLegendContext> = (pluginName: string, {
107
- rootSelection,
108
- seriesLabels$,
109
- fullParams$,
110
- layout$,
111
- fullChartParams$,
112
- textSizePx$
113
- }) => {
114
-
115
- const rootPositionClassName = getClassName(pluginName, 'root-position')
116
- const legendCardClassName = getClassName(pluginName, 'legend-card')
117
- const legendListClassName = getClassName(pluginName, 'legend-list')
118
- const legendItemClassName = getClassName(pluginName, 'legend-item')
119
-
120
- const destroy$ = new Subject()
121
-
122
- // const seriesLabels$: Observable<string[]> = SeriesDataMap$.pipe(
123
- // takeUntil(destroy$),
124
- // map(data => {
125
- // return Array.from(data.keys())
126
- // })
127
- // )
128
-
129
- const SeriesLabelColorMap$ = combineLatest({
130
- seriesLabels: seriesLabels$,
131
- fullChartParams: fullChartParams$
132
- }).pipe(
133
- takeUntil(destroy$),
134
- switchMap(async d => d),
135
- map(data => {
136
- const SeriesLabelColorMap: Map<string, string> = new Map()
137
- let accIndex = 0
138
- data.seriesLabels.forEach((label, i) => {
139
- if (!SeriesLabelColorMap.has(label)) {
140
- const color = getSeriesColor(accIndex, data.fullChartParams)
141
- SeriesLabelColorMap.set(label, color)
142
- accIndex ++
143
- }
144
- })
145
- return SeriesLabelColorMap
146
- })
147
- )
148
-
149
- // 對應seriesLabels是否顯示(只顯示不重覆的)
150
- const visibleList$ = seriesLabels$.pipe(
151
- takeUntil(destroy$),
152
- map(data => {
153
- const AccSeriesLabelSet = new Set()
154
- let visibleList: boolean[] = []
155
- data.forEach(d => {
156
- if (AccSeriesLabelSet.has(d)) {
157
- visibleList.push(false) // 已存在則不顯示
158
- } else {
159
- visibleList.push(true)
160
- }
161
- AccSeriesLabelSet.add(d) // 累加已存在的seriesLabel
162
- })
163
- return visibleList
164
- })
165
- )
166
-
167
- const lineDirection$ = fullParams$.pipe(
168
- takeUntil(destroy$),
169
- map(data => {
170
- return data.position === 'bottom' || data.position === 'top'
171
- ? 'row'
172
- : 'column'
173
- })
174
- )
175
-
176
- const lineMaxSize$ = combineLatest({
177
- fullParams: fullParams$,
178
- layout: layout$
179
- }).pipe(
180
- takeUntil(destroy$),
181
- switchMap(async d => d),
182
- map(data => {
183
- const ourterSize = (data.fullParams.padding) * 2 + (data.fullParams.gap * 2) // 卡片離場景的間距 & 卡片內的間距
184
-
185
- return data.fullParams.position === 'bottom' || data.fullParams.position === 'top'
186
- ? data.layout.rootWidth - ourterSize
187
- : data.layout.rootHeight - ourterSize
188
- })
189
- )
190
-
191
- const rootPosition$ = combineLatest({
192
- layout: layout$,
193
- fullParams: fullParams$,
194
- }).pipe(
195
- takeUntil(destroy$),
196
- switchMap(async d => d),
197
- map(data => {
198
- let x = 0
199
- let y = 0
200
- if (data.fullParams.position === 'bottom') {
201
- y = data.layout.rootHeight
202
- if (data.fullParams.justify === 'start') {
203
- x = 0
204
- } else if (data.fullParams.justify === 'center') {
205
- x = data.layout.rootWidth / 2
206
- } else if (data.fullParams.justify === 'end') {
207
- x = data.layout.rootWidth
208
- }
209
- } else if (data.fullParams.position === 'right') {
210
- x = data.layout.rootWidth
211
- if (data.fullParams.justify === 'start') {
212
- y = 0
213
- } else if (data.fullParams.justify === 'center') {
214
- y = data.layout.rootHeight / 2
215
- } else if (data.fullParams.justify === 'end') {
216
- y = data.layout.rootHeight
217
- }
218
- } else if (data.fullParams.position === 'top') {
219
- y = 0
220
- if (data.fullParams.justify === 'start') {
221
- x = 0
222
- } else if (data.fullParams.justify === 'center') {
223
- x = data.layout.rootWidth / 2
224
- } else if (data.fullParams.justify === 'end') {
225
- x = data.layout.rootWidth
226
- }
227
- } else if (data.fullParams.position === 'left') {
228
- x = 0
229
- if (data.fullParams.justify === 'start') {
230
- y = 0
231
- } else if (data.fullParams.justify === 'center') {
232
- y = data.layout.rootHeight / 2
233
- } else if (data.fullParams.justify === 'end') {
234
- y = data.layout.rootHeight
235
- }
236
- }
237
-
238
- return {
239
- x,
240
- y
241
- }
242
- })
243
- )
244
-
245
- const rootPositionSelection$: Observable<d3.Selection<SVGGElement, RootPosition, any, any>> = rootPosition$.pipe(
246
- takeUntil(destroy$),
247
- map(data => {
248
-
249
- return rootSelection
250
- .selectAll<SVGGElement, RootPosition>(`g.${rootPositionClassName}`)
251
- .data([data])
252
- .join(
253
- enter => {
254
- return enter
255
- .append('g')
256
- .classed(rootPositionClassName, true)
257
- .attr('transform', d => `translate(${d.x}, ${d.y})`)
258
- },
259
- update => {
260
- return update
261
- .transition()
262
- .attr('transform', d => `translate(${d.x}, ${d.y})`)
263
- },
264
- exit => exit.remove()
265
- )
266
- })
267
- )
268
-
269
- const defaultListStyle$ = fullParams$.pipe(
270
- takeUntil(destroy$),
271
- map(data => {
272
- return data.seriesList[0] ? data.seriesList[0] : defaultListStyle
273
- })
274
- )
275
-
276
- // 先計算list內每個item
277
- const lengendItems$: Observable<LegendItem[][]> = combineLatest({
278
- visibleList: visibleList$,
279
- fullParams: fullParams$,
280
- fullChartParams: fullChartParams$,
281
- seriesLabels: seriesLabels$,
282
- lineDirection: lineDirection$,
283
- lineMaxSize: lineMaxSize$,
284
- defaultListStyle: defaultListStyle$,
285
- SeriesLabelColorMap: SeriesLabelColorMap$,
286
- textSizePx: textSizePx$
287
- }).pipe(
288
- takeUntil(destroy$),
289
- switchMap(async d => d),
290
- map(data => {
291
- return data.seriesLabels.reduce((prev: LegendItem[][], current, currentIndex) => {
292
- // visible為flase則不加入
293
- if (!data.visibleList[currentIndex]) {
294
- return prev
295
- }
296
-
297
- const textWidth = measureTextWidth(current, data.textSizePx)
298
- const itemWidth = (data.textSizePx * 1.5) + textWidth
299
- // const color = getSeriesColor(currentIndex, data.fullChartParams)
300
- const color = data.SeriesLabelColorMap.get(current)
301
- const lastItem: LegendItem | null = prev[0] && prev[0][0]
302
- ? prev[prev.length - 1][prev[prev.length - 1].length - 1]
303
- : null
304
-
305
- const { translateX, translateY, lineIndex, itemIndex } = ((_data, _prev, _lastItem) => {
306
- let translateX = 0
307
- let translateY = 0
308
- let lineIndex = 0
309
- let itemIndex = 0
310
-
311
- if (_data.lineDirection === 'column') {
312
- let tempTranslateY = _lastItem
313
- ? _lastItem.translateY + _data.textSizePx + _data.fullParams.gap
314
- : 0
315
-
316
- if ((tempTranslateY + _data.textSizePx) > _data.lineMaxSize) {
317
- // 換行
318
- lineIndex = _lastItem.lineIndex + 1
319
- itemIndex = 0
320
- translateY = 0
321
- // 前一行最寬寬度
322
- const maxItemWidthInLastLine = _prev[_prev.length - 1].reduce((p, c) => {
323
- return c.itemWidth > p ? c.itemWidth : p
324
- }, 0)
325
- translateX = _lastItem.translateX + maxItemWidthInLastLine + _data.fullParams.gap
326
- } else {
327
- lineIndex = _lastItem ? _lastItem.lineIndex : 0
328
- itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
329
- translateY = tempTranslateY
330
- translateX = _lastItem ? _lastItem.translateX : 0
331
- }
332
- } else {
333
- let tempTranslateX = _lastItem
334
- ? _lastItem.translateX + _lastItem.itemWidth + _data.fullParams.gap
335
- : 0
336
- if ((tempTranslateX + itemWidth) > _data.lineMaxSize) {
337
- // 換行
338
- lineIndex = _lastItem.lineIndex + 1
339
- itemIndex = 0
340
- translateX = 0
341
- } else {
342
- lineIndex = _lastItem ? _lastItem.lineIndex : 0
343
- itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
344
- translateX = tempTranslateX
345
- }
346
- translateY = (_data.textSizePx + _data.fullParams.gap) * lineIndex
347
- }
348
-
349
- return { translateX, translateY, lineIndex, itemIndex }
350
- })(data, prev, lastItem)
351
-
352
- if (!prev[lineIndex]) {
353
- prev[lineIndex] = []
354
- }
355
-
356
- const listStyle = data.fullParams.seriesList[itemIndex] ? data.fullParams.seriesList[itemIndex] : data.defaultListStyle
357
-
358
- prev[lineIndex].push({
359
- id: current,
360
- seriesLabel: current,
361
- seriesIndex: currentIndex,
362
- lineIndex,
363
- itemIndex,
364
- text: current,
365
- itemWidth,
366
- translateX,
367
- translateY,
368
- color,
369
- listRectWidth: listStyle.listRectWidth,
370
- listRectHeight: listStyle.listRectHeight,
371
- listRectRadius: listStyle.listRectRadius
372
- })
373
-
374
- return prev
375
- }, [])
376
- }),
377
- shareReplay(1)
378
- )
379
-
380
- // 依list計算出來的排序位置來計算整體容器的尺寸
381
- const lengendList$: Observable<LegendList> = combineLatest({
382
- fullParams: fullParams$,
383
- fullChartParams: fullChartParams$,
384
- lineDirection: lineDirection$,
385
- lengendItems: lengendItems$,
386
- textSizePx: textSizePx$
387
- }).pipe(
388
- takeUntil(destroy$),
389
- switchMap(async d => d),
390
- map(data => {
391
- // 依list計算出來的排序位置來計算整體容器的偏移位置
392
- const { width, height } = ((_data, _lengendItems) => {
393
- let width = 0
394
- let height = 0
395
-
396
- if (!_lengendItems.length || !_lengendItems[0].length) {
397
- return { width, height }
398
- }
399
-
400
- const firstLineLastItem = _lengendItems[0][_lengendItems[0].length - 1]
401
- if (_data.lineDirection === 'column') {
402
- width = _lengendItems.reduce((p, c) => {
403
- const maxWidthInLine = c.reduce((_p, _c) => {
404
- // 找出最寬的寬度
405
- return _c.itemWidth > _p ? _c.itemWidth : _p
406
- }, 0)
407
- // 每行寬度加總
408
- return p + maxWidthInLine
409
- }, 0)
410
- width += _data.fullParams.gap * (_lengendItems.length - 1)
411
- height = firstLineLastItem.translateY + _data.textSizePx
412
- } else {
413
- width = firstLineLastItem.translateX + firstLineLastItem.itemWidth
414
- height = (_data.textSizePx * _lengendItems.length) + (_data.fullParams.gap * (_lengendItems.length - 1))
415
- }
416
-
417
- return { width, height }
418
- })(data, data.lengendItems)
419
-
420
- return <LegendList>{
421
- direction: data.lineDirection,
422
- width,
423
- height,
424
- translateX: data.fullParams.gap,
425
- translateY: data.fullParams.gap
426
- }
427
- }),
428
- shareReplay(1)
429
- )
430
-
431
- const legendCard$: Observable<LegendCard> = combineLatest({
432
- fullParams: fullParams$,
433
- lengendList: lengendList$
434
- }).pipe(
435
- takeUntil(destroy$),
436
- switchMap(async d => d),
437
- map(data => {
438
- const width = data.lengendList.width + (data.fullParams.gap * 2)
439
- const height = data.lengendList.height + (data.fullParams.gap * 2)
440
- let translateX = 0
441
- let translateY = 0
442
-
443
- if (data.fullParams.position === 'left') {
444
- if (data.fullParams.justify === 'start') {
445
- translateX = data.fullParams.padding
446
- translateY = data.fullParams.padding
447
- } else if (data.fullParams.justify === 'center') {
448
- translateX = data.fullParams.padding
449
- translateY = - height / 2
450
- } else if (data.fullParams.justify === 'end') {
451
- translateX = data.fullParams.padding
452
- translateY = - height - data.fullParams.padding
453
- }
454
- } else if (data.fullParams.position === 'right') {
455
- if (data.fullParams.justify === 'start') {
456
- translateX = - width - data.fullParams.padding
457
- translateY = data.fullParams.padding
458
- } else if (data.fullParams.justify === 'center') {
459
- translateX = - width - data.fullParams.padding
460
- translateY = - height / 2
461
- } else if (data.fullParams.justify === 'end') {
462
- translateX = - width - data.fullParams.padding
463
- translateY = - height - data.fullParams.padding
464
- }
465
- } else if (data.fullParams.position === 'top') {
466
- if (data.fullParams.justify === 'start') {
467
- translateX = data.fullParams.padding
468
- translateY = data.fullParams.padding
469
- } else if (data.fullParams.justify === 'center') {
470
- translateX = - width / 2
471
- translateY = data.fullParams.padding
472
- } else if (data.fullParams.justify === 'end') {
473
- translateX = - width - data.fullParams.padding
474
- translateY = data.fullParams.padding
475
- }
476
- } else {
477
- if (data.fullParams.justify === 'start') {
478
- translateX = data.fullParams.padding
479
- translateY = - height - data.fullParams.padding
480
- } else if (data.fullParams.justify === 'center') {
481
- translateX = - width / 2
482
- translateY = - height - data.fullParams.padding
483
- } else if (data.fullParams.justify === 'end') {
484
- translateX = - width - data.fullParams.padding
485
- translateY = - height - data.fullParams.padding
486
- }
487
- }
488
- // translateX += _data.fullParams.offset[0]
489
- // translateY += _data.fullParams.offset[1]
490
-
491
- return {
492
- width,
493
- height,
494
- translateX,
495
- translateY
496
- }
497
- })
498
- )
499
-
500
- const lengendCardSelection$ = combineLatest({
501
- rootPositionSelection: rootPositionSelection$,
502
- fullParams: fullParams$,
503
- fullChartParams: fullChartParams$,
504
- legendCard: legendCard$
505
- }).pipe(
506
- takeUntil(destroy$),
507
- switchMap(async d => d),
508
- map(data => {
509
- return data.rootPositionSelection
510
- .selectAll<SVGGElement, BaseLegendParams>('g')
511
- .data([data.legendCard])
512
- .join(
513
- enter => {
514
- return enter
515
- .append('g')
516
- .classed(legendCardClassName, true)
517
- .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
518
- },
519
- update => {
520
- return update
521
- .transition()
522
- .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
523
- },
524
- exit => exit.remove()
525
- )
526
- .each((d, i, g) => {
527
- const rect = d3.select(g[i])
528
- .selectAll('rect')
529
- .data([d])
530
- .join('rect')
531
- .attr('width', d => d.width)
532
- .attr('height', d => d.height)
533
- .attr('fill', getColor(data.fullParams.backgroundFill, data.fullChartParams))
534
- .attr('stroke', getColor(data.fullParams.backgroundStroke, data.fullChartParams))
535
- })
536
- })
537
- )
538
-
539
-
540
- const lengendListSelection$ = combineLatest({
541
- lengendCardSelection: lengendCardSelection$,
542
- fullParams: fullParams$,
543
- lengendList: lengendList$
544
- }).pipe(
545
- takeUntil(destroy$),
546
- switchMap(async d => d),
547
- map(data => {
548
- return data.lengendCardSelection
549
- .selectAll<SVGGElement, BaseLegendParams>('g')
550
- .data([data.lengendList])
551
- .join(
552
- enter => {
553
- return enter
554
- .append('g')
555
- .classed(legendListClassName, true)
556
- .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
557
- },
558
- update => {
559
- return update
560
- .transition()
561
- .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
562
- },
563
- exit => exit.remove()
564
- )
565
- })
566
- )
567
-
568
- const itemSelection$ = combineLatest({
569
- lengendListSelection: lengendListSelection$,
570
- fullParams: fullParams$,
571
- fullChartParams: fullChartParams$,
572
- lengendItems: lengendItems$,
573
- textSizePx: textSizePx$
574
- }).pipe(
575
- takeUntil(destroy$),
576
- switchMap(async d => d),
577
- map(data => {
578
- const items = data.lengendItems[0] ? data.lengendItems.flat() : []
579
-
580
- return data.lengendListSelection
581
- .selectAll<SVGGElement, string>(`g.${legendItemClassName}`)
582
- .data(items)
583
- .join(
584
- enter => {
585
- return enter
586
- .append('g')
587
- .classed(legendItemClassName, true)
588
- .attr('cursor', 'default')
589
- },
590
- update => update,
591
- exit => exit.remove()
592
- )
593
- .attr('transform', (d, i) => {
594
- return `translate(${d.translateX}, ${d.translateY})`
595
- })
596
- .each((d, i, g) => {
597
- const rectCenterX = data.textSizePx / 2
598
- const transformRectWidth = - d.listRectWidth / 2
599
- const transformRectHeight = - d.listRectHeight / 2
600
- // 方塊
601
- d3.select(g[i])
602
- .selectAll('rect')
603
- .data([d])
604
- .join('rect')
605
- .attr('x', rectCenterX)
606
- .attr('y', rectCenterX)
607
- .attr('width', _d => _d.listRectWidth)
608
- .attr('height', _d => _d.listRectHeight)
609
- .attr('transform', _d => `translate(${transformRectWidth}, ${transformRectHeight})`)
610
- .attr('fill', _d => _d.color)
611
- .attr('rx', _d => _d.listRectRadius)
612
- // 文字
613
- d3.select(g[i])
614
- .selectAll('text')
615
- .data([d])
616
- .join(
617
- enter => {
618
- return enter
619
- .append('text')
620
- .attr('dominant-baseline', 'hanging')
621
- },
622
- update => {
623
- return update
624
- },
625
- exit => exit.remove()
626
- )
627
- .attr('x', data.textSizePx * 1.5)
628
- .attr('font-size', data.fullChartParams.styles.textSize)
629
- .attr('fill', d => data.fullParams.textColorType === 'series'
630
- ? getSeriesColor(d.seriesIndex, data.fullChartParams)
631
- : getColor(data.fullParams.textColorType, data.fullChartParams))
632
- .text(d => d.text)
633
- })
634
- })
635
- )
636
-
637
- itemSelection$.subscribe()
638
-
639
- return () => {
640
- destroy$.next(undefined)
641
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ switchMap,
6
+ takeUntil,
7
+ shareReplay,
8
+ Observable,
9
+ Subject } from 'rxjs'
10
+ import type { BasePluginFn } from './types'
11
+ import type {
12
+ ChartParams, Layout, ColorType } from '@orbcharts/core'
13
+ import { getClassName, getColor, getDatumColor } from '../utils/orbchartsUtils'
14
+ import { measureTextWidth } from '../utils/commonUtils'
15
+
16
+ export interface BaseLegendParams {
17
+ position: 'top' | 'bottom' | 'left' | 'right'
18
+ justify: 'start' | 'center' | 'end'
19
+ padding: number
20
+ // offset: [number, number]
21
+ backgroundFill: ColorType
22
+ backgroundStroke: ColorType
23
+ textColorType: ColorType
24
+ gap: number
25
+ seriesList: Array<{
26
+ listRectWidth: number
27
+ listRectHeight: number
28
+ listRectRadius: number
29
+ }>
30
+ // highlightEvent: boolean
31
+ }
32
+
33
+ interface BaseLegendContext {
34
+ rootSelection: d3.Selection<any, unknown, any, unknown>
35
+ seriesLabels$: Observable<string[]>
36
+ fullParams$: Observable<BaseLegendParams>
37
+ layout$: Observable<Layout>
38
+ fullChartParams$: Observable<ChartParams>
39
+ textSizePx$: Observable<number>
40
+ }
41
+
42
+ // 第1層 - 定位的容器
43
+ interface RootPosition {
44
+ x:number
45
+ y:number
46
+ }
47
+
48
+ // 第2層 - 卡片
49
+ interface LegendCard {
50
+ width: number
51
+ height: number
52
+ translateX:number
53
+ translateY:number
54
+ }
55
+
56
+ // 第3層 - 圖例列表
57
+ interface LegendList {
58
+ direction: 'row' | 'column'
59
+ width: number
60
+ height: number
61
+ translateX:number
62
+ translateY:number
63
+ // list: LegendItem[][]
64
+ }
65
+
66
+ // 第4層 - 圖例項目
67
+ interface LegendItem {
68
+ id: string // seriesLabel
69
+ seriesLabel: string
70
+ seriesIndex: number
71
+ lineIndex: number
72
+ itemIndex: number // 行內的item
73
+ text: string
74
+ itemWidth: number
75
+ translateX: number
76
+ translateY: number
77
+ color: string
78
+ listRectWidth: number
79
+ listRectHeight: number
80
+ listRectRadius: number
81
+ }
82
+
83
+ interface ListStyle {
84
+ listRectWidth: number
85
+ listRectHeight: number
86
+ listRectRadius: number
87
+ }
88
+
89
+ const defaultListStyle: ListStyle = {
90
+ listRectWidth: 14,
91
+ listRectHeight: 14,
92
+ listRectRadius: 0,
93
+ }
94
+
95
+ function getSeriesColor (seriesIndex: number, fullChartParams: ChartParams) {
96
+ const colorIndex = seriesIndex < fullChartParams.colors[fullChartParams.colorScheme].series.length
97
+ ? seriesIndex
98
+ : seriesIndex % fullChartParams.colors[fullChartParams.colorScheme].series.length
99
+ return fullChartParams.colors[fullChartParams.colorScheme].series[colorIndex]
100
+ }
101
+
102
+ function getLegendColor () {
103
+
104
+ }
105
+
106
+ export const createBaseLegend: BasePluginFn<BaseLegendContext> = (pluginName: string, {
107
+ rootSelection,
108
+ seriesLabels$,
109
+ fullParams$,
110
+ layout$,
111
+ fullChartParams$,
112
+ textSizePx$
113
+ }) => {
114
+
115
+ const rootPositionClassName = getClassName(pluginName, 'root-position')
116
+ const legendCardClassName = getClassName(pluginName, 'legend-card')
117
+ const legendListClassName = getClassName(pluginName, 'legend-list')
118
+ const legendItemClassName = getClassName(pluginName, 'legend-item')
119
+
120
+ const destroy$ = new Subject()
121
+
122
+ // const seriesLabels$: Observable<string[]> = SeriesDataMap$.pipe(
123
+ // takeUntil(destroy$),
124
+ // map(data => {
125
+ // return Array.from(data.keys())
126
+ // })
127
+ // )
128
+
129
+ const SeriesLabelColorMap$ = combineLatest({
130
+ seriesLabels: seriesLabels$,
131
+ fullChartParams: fullChartParams$
132
+ }).pipe(
133
+ takeUntil(destroy$),
134
+ switchMap(async d => d),
135
+ map(data => {
136
+ const SeriesLabelColorMap: Map<string, string> = new Map()
137
+ let accIndex = 0
138
+ data.seriesLabels.forEach((label, i) => {
139
+ if (!SeriesLabelColorMap.has(label)) {
140
+ const color = getSeriesColor(accIndex, data.fullChartParams)
141
+ SeriesLabelColorMap.set(label, color)
142
+ accIndex ++
143
+ }
144
+ })
145
+ return SeriesLabelColorMap
146
+ })
147
+ )
148
+
149
+ // 對應seriesLabels是否顯示(只顯示不重覆的)
150
+ const visibleList$ = seriesLabels$.pipe(
151
+ takeUntil(destroy$),
152
+ map(data => {
153
+ const AccSeriesLabelSet = new Set()
154
+ let visibleList: boolean[] = []
155
+ data.forEach(d => {
156
+ if (AccSeriesLabelSet.has(d)) {
157
+ visibleList.push(false) // 已存在則不顯示
158
+ } else {
159
+ visibleList.push(true)
160
+ }
161
+ AccSeriesLabelSet.add(d) // 累加已存在的seriesLabel
162
+ })
163
+ return visibleList
164
+ })
165
+ )
166
+
167
+ const lineDirection$ = fullParams$.pipe(
168
+ takeUntil(destroy$),
169
+ map(data => {
170
+ return data.position === 'bottom' || data.position === 'top'
171
+ ? 'row'
172
+ : 'column'
173
+ })
174
+ )
175
+
176
+ const lineMaxSize$ = combineLatest({
177
+ fullParams: fullParams$,
178
+ layout: layout$
179
+ }).pipe(
180
+ takeUntil(destroy$),
181
+ switchMap(async d => d),
182
+ map(data => {
183
+ const ourterSize = (data.fullParams.padding) * 2 + (data.fullParams.gap * 2) // 卡片離場景的間距 & 卡片內的間距
184
+
185
+ return data.fullParams.position === 'bottom' || data.fullParams.position === 'top'
186
+ ? data.layout.rootWidth - ourterSize
187
+ : data.layout.rootHeight - ourterSize
188
+ })
189
+ )
190
+
191
+ const rootPosition$ = combineLatest({
192
+ layout: layout$,
193
+ fullParams: fullParams$,
194
+ }).pipe(
195
+ takeUntil(destroy$),
196
+ switchMap(async d => d),
197
+ map(data => {
198
+ let x = 0
199
+ let y = 0
200
+ if (data.fullParams.position === 'bottom') {
201
+ y = data.layout.rootHeight
202
+ if (data.fullParams.justify === 'start') {
203
+ x = 0
204
+ } else if (data.fullParams.justify === 'center') {
205
+ x = data.layout.rootWidth / 2
206
+ } else if (data.fullParams.justify === 'end') {
207
+ x = data.layout.rootWidth
208
+ }
209
+ } else if (data.fullParams.position === 'right') {
210
+ x = data.layout.rootWidth
211
+ if (data.fullParams.justify === 'start') {
212
+ y = 0
213
+ } else if (data.fullParams.justify === 'center') {
214
+ y = data.layout.rootHeight / 2
215
+ } else if (data.fullParams.justify === 'end') {
216
+ y = data.layout.rootHeight
217
+ }
218
+ } else if (data.fullParams.position === 'top') {
219
+ y = 0
220
+ if (data.fullParams.justify === 'start') {
221
+ x = 0
222
+ } else if (data.fullParams.justify === 'center') {
223
+ x = data.layout.rootWidth / 2
224
+ } else if (data.fullParams.justify === 'end') {
225
+ x = data.layout.rootWidth
226
+ }
227
+ } else if (data.fullParams.position === 'left') {
228
+ x = 0
229
+ if (data.fullParams.justify === 'start') {
230
+ y = 0
231
+ } else if (data.fullParams.justify === 'center') {
232
+ y = data.layout.rootHeight / 2
233
+ } else if (data.fullParams.justify === 'end') {
234
+ y = data.layout.rootHeight
235
+ }
236
+ }
237
+
238
+ return {
239
+ x,
240
+ y
241
+ }
242
+ })
243
+ )
244
+
245
+ const rootPositionSelection$: Observable<d3.Selection<SVGGElement, RootPosition, any, any>> = rootPosition$.pipe(
246
+ takeUntil(destroy$),
247
+ map(data => {
248
+
249
+ return rootSelection
250
+ .selectAll<SVGGElement, RootPosition>(`g.${rootPositionClassName}`)
251
+ .data([data])
252
+ .join(
253
+ enter => {
254
+ return enter
255
+ .append('g')
256
+ .classed(rootPositionClassName, true)
257
+ .attr('transform', d => `translate(${d.x}, ${d.y})`)
258
+ },
259
+ update => {
260
+ return update
261
+ .transition()
262
+ .attr('transform', d => `translate(${d.x}, ${d.y})`)
263
+ },
264
+ exit => exit.remove()
265
+ )
266
+ })
267
+ )
268
+
269
+ const defaultListStyle$ = fullParams$.pipe(
270
+ takeUntil(destroy$),
271
+ map(data => {
272
+ return data.seriesList[0] ? data.seriesList[0] : defaultListStyle
273
+ })
274
+ )
275
+
276
+ // 先計算list內每個item
277
+ const lengendItems$: Observable<LegendItem[][]> = combineLatest({
278
+ visibleList: visibleList$,
279
+ fullParams: fullParams$,
280
+ fullChartParams: fullChartParams$,
281
+ seriesLabels: seriesLabels$,
282
+ lineDirection: lineDirection$,
283
+ lineMaxSize: lineMaxSize$,
284
+ defaultListStyle: defaultListStyle$,
285
+ SeriesLabelColorMap: SeriesLabelColorMap$,
286
+ textSizePx: textSizePx$
287
+ }).pipe(
288
+ takeUntil(destroy$),
289
+ switchMap(async d => d),
290
+ map(data => {
291
+ return data.seriesLabels.reduce((prev: LegendItem[][], current, currentIndex) => {
292
+ // visible為flase則不加入
293
+ if (!data.visibleList[currentIndex]) {
294
+ return prev
295
+ }
296
+
297
+ const textWidth = measureTextWidth(current, data.textSizePx)
298
+ const itemWidth = (data.textSizePx * 1.5) + textWidth
299
+ // const color = getSeriesColor(currentIndex, data.fullChartParams)
300
+ const color = data.SeriesLabelColorMap.get(current)
301
+ const lastItem: LegendItem | null = prev[0] && prev[0][0]
302
+ ? prev[prev.length - 1][prev[prev.length - 1].length - 1]
303
+ : null
304
+
305
+ const { translateX, translateY, lineIndex, itemIndex } = ((_data, _prev, _lastItem) => {
306
+ let translateX = 0
307
+ let translateY = 0
308
+ let lineIndex = 0
309
+ let itemIndex = 0
310
+
311
+ if (_data.lineDirection === 'column') {
312
+ let tempTranslateY = _lastItem
313
+ ? _lastItem.translateY + _data.textSizePx + _data.fullParams.gap
314
+ : 0
315
+
316
+ if ((tempTranslateY + _data.textSizePx) > _data.lineMaxSize) {
317
+ // 換行
318
+ lineIndex = _lastItem.lineIndex + 1
319
+ itemIndex = 0
320
+ translateY = 0
321
+ // 前一行最寬寬度
322
+ const maxItemWidthInLastLine = _prev[_prev.length - 1].reduce((p, c) => {
323
+ return c.itemWidth > p ? c.itemWidth : p
324
+ }, 0)
325
+ translateX = _lastItem.translateX + maxItemWidthInLastLine + _data.fullParams.gap
326
+ } else {
327
+ lineIndex = _lastItem ? _lastItem.lineIndex : 0
328
+ itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
329
+ translateY = tempTranslateY
330
+ translateX = _lastItem ? _lastItem.translateX : 0
331
+ }
332
+ } else {
333
+ let tempTranslateX = _lastItem
334
+ ? _lastItem.translateX + _lastItem.itemWidth + _data.fullParams.gap
335
+ : 0
336
+ if ((tempTranslateX + itemWidth) > _data.lineMaxSize) {
337
+ // 換行
338
+ lineIndex = _lastItem.lineIndex + 1
339
+ itemIndex = 0
340
+ translateX = 0
341
+ } else {
342
+ lineIndex = _lastItem ? _lastItem.lineIndex : 0
343
+ itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
344
+ translateX = tempTranslateX
345
+ }
346
+ translateY = (_data.textSizePx + _data.fullParams.gap) * lineIndex
347
+ }
348
+
349
+ return { translateX, translateY, lineIndex, itemIndex }
350
+ })(data, prev, lastItem)
351
+
352
+ if (!prev[lineIndex]) {
353
+ prev[lineIndex] = []
354
+ }
355
+
356
+ const listStyle = data.fullParams.seriesList[itemIndex] ? data.fullParams.seriesList[itemIndex] : data.defaultListStyle
357
+
358
+ prev[lineIndex].push({
359
+ id: current,
360
+ seriesLabel: current,
361
+ seriesIndex: currentIndex,
362
+ lineIndex,
363
+ itemIndex,
364
+ text: current,
365
+ itemWidth,
366
+ translateX,
367
+ translateY,
368
+ color,
369
+ listRectWidth: listStyle.listRectWidth,
370
+ listRectHeight: listStyle.listRectHeight,
371
+ listRectRadius: listStyle.listRectRadius
372
+ })
373
+
374
+ return prev
375
+ }, [])
376
+ }),
377
+ shareReplay(1)
378
+ )
379
+
380
+ // 依list計算出來的排序位置來計算整體容器的尺寸
381
+ const lengendList$: Observable<LegendList> = combineLatest({
382
+ fullParams: fullParams$,
383
+ fullChartParams: fullChartParams$,
384
+ lineDirection: lineDirection$,
385
+ lengendItems: lengendItems$,
386
+ textSizePx: textSizePx$
387
+ }).pipe(
388
+ takeUntil(destroy$),
389
+ switchMap(async d => d),
390
+ map(data => {
391
+ // 依list計算出來的排序位置來計算整體容器的偏移位置
392
+ const { width, height } = ((_data, _lengendItems) => {
393
+ let width = 0
394
+ let height = 0
395
+
396
+ if (!_lengendItems.length || !_lengendItems[0].length) {
397
+ return { width, height }
398
+ }
399
+
400
+ const firstLineLastItem = _lengendItems[0][_lengendItems[0].length - 1]
401
+ if (_data.lineDirection === 'column') {
402
+ width = _lengendItems.reduce((p, c) => {
403
+ const maxWidthInLine = c.reduce((_p, _c) => {
404
+ // 找出最寬的寬度
405
+ return _c.itemWidth > _p ? _c.itemWidth : _p
406
+ }, 0)
407
+ // 每行寬度加總
408
+ return p + maxWidthInLine
409
+ }, 0)
410
+ width += _data.fullParams.gap * (_lengendItems.length - 1)
411
+ height = firstLineLastItem.translateY + _data.textSizePx
412
+ } else {
413
+ width = firstLineLastItem.translateX + firstLineLastItem.itemWidth
414
+ height = (_data.textSizePx * _lengendItems.length) + (_data.fullParams.gap * (_lengendItems.length - 1))
415
+ }
416
+
417
+ return { width, height }
418
+ })(data, data.lengendItems)
419
+
420
+ return <LegendList>{
421
+ direction: data.lineDirection,
422
+ width,
423
+ height,
424
+ translateX: data.fullParams.gap,
425
+ translateY: data.fullParams.gap
426
+ }
427
+ }),
428
+ shareReplay(1)
429
+ )
430
+
431
+ const legendCard$: Observable<LegendCard> = combineLatest({
432
+ fullParams: fullParams$,
433
+ lengendList: lengendList$
434
+ }).pipe(
435
+ takeUntil(destroy$),
436
+ switchMap(async d => d),
437
+ map(data => {
438
+ const width = data.lengendList.width + (data.fullParams.gap * 2)
439
+ const height = data.lengendList.height + (data.fullParams.gap * 2)
440
+ let translateX = 0
441
+ let translateY = 0
442
+
443
+ if (data.fullParams.position === 'left') {
444
+ if (data.fullParams.justify === 'start') {
445
+ translateX = data.fullParams.padding
446
+ translateY = data.fullParams.padding
447
+ } else if (data.fullParams.justify === 'center') {
448
+ translateX = data.fullParams.padding
449
+ translateY = - height / 2
450
+ } else if (data.fullParams.justify === 'end') {
451
+ translateX = data.fullParams.padding
452
+ translateY = - height - data.fullParams.padding
453
+ }
454
+ } else if (data.fullParams.position === 'right') {
455
+ if (data.fullParams.justify === 'start') {
456
+ translateX = - width - data.fullParams.padding
457
+ translateY = data.fullParams.padding
458
+ } else if (data.fullParams.justify === 'center') {
459
+ translateX = - width - data.fullParams.padding
460
+ translateY = - height / 2
461
+ } else if (data.fullParams.justify === 'end') {
462
+ translateX = - width - data.fullParams.padding
463
+ translateY = - height - data.fullParams.padding
464
+ }
465
+ } else if (data.fullParams.position === 'top') {
466
+ if (data.fullParams.justify === 'start') {
467
+ translateX = data.fullParams.padding
468
+ translateY = data.fullParams.padding
469
+ } else if (data.fullParams.justify === 'center') {
470
+ translateX = - width / 2
471
+ translateY = data.fullParams.padding
472
+ } else if (data.fullParams.justify === 'end') {
473
+ translateX = - width - data.fullParams.padding
474
+ translateY = data.fullParams.padding
475
+ }
476
+ } else {
477
+ if (data.fullParams.justify === 'start') {
478
+ translateX = data.fullParams.padding
479
+ translateY = - height - data.fullParams.padding
480
+ } else if (data.fullParams.justify === 'center') {
481
+ translateX = - width / 2
482
+ translateY = - height - data.fullParams.padding
483
+ } else if (data.fullParams.justify === 'end') {
484
+ translateX = - width - data.fullParams.padding
485
+ translateY = - height - data.fullParams.padding
486
+ }
487
+ }
488
+ // translateX += _data.fullParams.offset[0]
489
+ // translateY += _data.fullParams.offset[1]
490
+
491
+ return {
492
+ width,
493
+ height,
494
+ translateX,
495
+ translateY
496
+ }
497
+ })
498
+ )
499
+
500
+ const lengendCardSelection$ = combineLatest({
501
+ rootPositionSelection: rootPositionSelection$,
502
+ fullParams: fullParams$,
503
+ fullChartParams: fullChartParams$,
504
+ legendCard: legendCard$
505
+ }).pipe(
506
+ takeUntil(destroy$),
507
+ switchMap(async d => d),
508
+ map(data => {
509
+ return data.rootPositionSelection
510
+ .selectAll<SVGGElement, BaseLegendParams>('g')
511
+ .data([data.legendCard])
512
+ .join(
513
+ enter => {
514
+ return enter
515
+ .append('g')
516
+ .classed(legendCardClassName, true)
517
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
518
+ },
519
+ update => {
520
+ return update
521
+ .transition()
522
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
523
+ },
524
+ exit => exit.remove()
525
+ )
526
+ .each((d, i, g) => {
527
+ const rect = d3.select(g[i])
528
+ .selectAll('rect')
529
+ .data([d])
530
+ .join('rect')
531
+ .attr('width', d => d.width)
532
+ .attr('height', d => d.height)
533
+ .attr('fill', getColor(data.fullParams.backgroundFill, data.fullChartParams))
534
+ .attr('stroke', getColor(data.fullParams.backgroundStroke, data.fullChartParams))
535
+ })
536
+ })
537
+ )
538
+
539
+
540
+ const lengendListSelection$ = combineLatest({
541
+ lengendCardSelection: lengendCardSelection$,
542
+ fullParams: fullParams$,
543
+ lengendList: lengendList$
544
+ }).pipe(
545
+ takeUntil(destroy$),
546
+ switchMap(async d => d),
547
+ map(data => {
548
+ return data.lengendCardSelection
549
+ .selectAll<SVGGElement, BaseLegendParams>('g')
550
+ .data([data.lengendList])
551
+ .join(
552
+ enter => {
553
+ return enter
554
+ .append('g')
555
+ .classed(legendListClassName, true)
556
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
557
+ },
558
+ update => {
559
+ return update
560
+ .transition()
561
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
562
+ },
563
+ exit => exit.remove()
564
+ )
565
+ })
566
+ )
567
+
568
+ const itemSelection$ = combineLatest({
569
+ lengendListSelection: lengendListSelection$,
570
+ fullParams: fullParams$,
571
+ fullChartParams: fullChartParams$,
572
+ lengendItems: lengendItems$,
573
+ textSizePx: textSizePx$
574
+ }).pipe(
575
+ takeUntil(destroy$),
576
+ switchMap(async d => d),
577
+ map(data => {
578
+ const items = data.lengendItems[0] ? data.lengendItems.flat() : []
579
+
580
+ return data.lengendListSelection
581
+ .selectAll<SVGGElement, string>(`g.${legendItemClassName}`)
582
+ .data(items)
583
+ .join(
584
+ enter => {
585
+ return enter
586
+ .append('g')
587
+ .classed(legendItemClassName, true)
588
+ .attr('cursor', 'default')
589
+ },
590
+ update => update,
591
+ exit => exit.remove()
592
+ )
593
+ .attr('transform', (d, i) => {
594
+ return `translate(${d.translateX}, ${d.translateY})`
595
+ })
596
+ .each((d, i, g) => {
597
+ const rectCenterX = data.textSizePx / 2
598
+ const transformRectWidth = - d.listRectWidth / 2
599
+ const transformRectHeight = - d.listRectHeight / 2
600
+ // 方塊
601
+ d3.select(g[i])
602
+ .selectAll('rect')
603
+ .data([d])
604
+ .join('rect')
605
+ .attr('x', rectCenterX)
606
+ .attr('y', rectCenterX)
607
+ .attr('width', _d => _d.listRectWidth)
608
+ .attr('height', _d => _d.listRectHeight)
609
+ .attr('transform', _d => `translate(${transformRectWidth}, ${transformRectHeight})`)
610
+ .attr('fill', _d => _d.color)
611
+ .attr('rx', _d => _d.listRectRadius)
612
+ // 文字
613
+ d3.select(g[i])
614
+ .selectAll('text')
615
+ .data([d])
616
+ .join(
617
+ enter => {
618
+ return enter
619
+ .append('text')
620
+ .attr('dominant-baseline', 'hanging')
621
+ },
622
+ update => {
623
+ return update
624
+ },
625
+ exit => exit.remove()
626
+ )
627
+ .attr('x', data.textSizePx * 1.5)
628
+ .attr('font-size', data.fullChartParams.styles.textSize)
629
+ .attr('fill', d => data.fullParams.textColorType === 'series'
630
+ ? getSeriesColor(d.seriesIndex, data.fullChartParams)
631
+ : getColor(data.fullParams.textColorType, data.fullChartParams))
632
+ .text(d => d.text)
633
+ })
634
+ })
635
+ )
636
+
637
+ itemSelection$.subscribe()
638
+
639
+ return () => {
640
+ destroy$.next(undefined)
641
+ }
642
642
  }