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

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 (91) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +6325 -6272
  3. package/dist/orbcharts-plugins-basic.umd.js +8 -8
  4. package/dist/src/base/BaseGroupAxis.d.ts +1 -0
  5. package/dist/src/grid/index.d.ts +1 -1
  6. package/dist/src/grid/plugins/BarsPN.d.ts +1 -0
  7. package/dist/src/grid/types.d.ts +3 -2
  8. package/dist/src/multiGrid/defaults.d.ts +2 -1
  9. package/dist/src/multiGrid/index.d.ts +2 -0
  10. package/dist/src/multiGrid/multiGridObservables.d.ts +1 -1
  11. package/dist/src/multiGrid/plugins/MultiValueStackAxis.d.ts +1 -0
  12. package/dist/src/multiGrid/plugins/OverlappingValueStackAxes.d.ts +1 -0
  13. package/dist/src/multiGrid/types.d.ts +3 -0
  14. package/dist/src/series/types.d.ts +2 -3
  15. package/package.json +42 -42
  16. package/src/base/BaseBarStack.ts +778 -778
  17. package/src/base/BaseBars.ts +764 -764
  18. package/src/base/BaseBarsTriangle.ts +672 -672
  19. package/src/base/BaseDots.ts +513 -502
  20. package/src/base/BaseGroupAxis.ts +562 -496
  21. package/src/base/BaseLegend.ts +641 -641
  22. package/src/base/BaseLineAreas.ts +625 -625
  23. package/src/base/BaseLines.ts +699 -699
  24. package/src/base/BaseValueAxis.ts +478 -478
  25. package/src/base/types.ts +2 -2
  26. package/src/grid/defaults.ts +125 -121
  27. package/src/grid/gridObservables.ts +248 -247
  28. package/src/grid/index.ts +15 -15
  29. package/src/grid/plugins/BarStack.ts +43 -50
  30. package/src/grid/plugins/Bars.ts +44 -51
  31. package/src/grid/plugins/{BarsDiverging.ts → BarsPN.ts} +41 -41
  32. package/src/grid/plugins/BarsTriangle.ts +42 -50
  33. package/src/grid/plugins/Dots.ts +37 -37
  34. package/src/grid/plugins/GridLegend.ts +59 -59
  35. package/src/grid/plugins/GroupAux.ts +645 -645
  36. package/src/grid/plugins/GroupAxis.ts +35 -42
  37. package/src/grid/plugins/LineAreas.ts +39 -39
  38. package/src/grid/plugins/Lines.ts +38 -38
  39. package/src/grid/plugins/ScalingArea.ts +173 -173
  40. package/src/grid/plugins/ValueAxis.ts +36 -43
  41. package/src/grid/plugins/ValueStackAxis.ts +38 -79
  42. package/src/grid/types.ts +122 -120
  43. package/src/index.ts +9 -9
  44. package/src/multiGrid/defaults.ts +152 -147
  45. package/src/multiGrid/index.ts +14 -12
  46. package/src/multiGrid/multiGridObservables.ts +44 -43
  47. package/src/multiGrid/plugins/MultiBarStack.ts +78 -78
  48. package/src/multiGrid/plugins/MultiBars.ts +77 -77
  49. package/src/multiGrid/plugins/MultiBarsTriangle.ts +77 -77
  50. package/src/multiGrid/plugins/MultiDots.ts +65 -65
  51. package/src/multiGrid/plugins/MultiGridLegend.ts +89 -89
  52. package/src/multiGrid/plugins/MultiGroupAxis.ts +69 -69
  53. package/src/multiGrid/plugins/MultiLineAreas.ts +67 -67
  54. package/src/multiGrid/plugins/MultiLines.ts +66 -66
  55. package/src/multiGrid/plugins/MultiValueAxis.ts +69 -69
  56. package/src/multiGrid/plugins/MultiValueStackAxis.ts +69 -0
  57. package/src/multiGrid/plugins/OverlappingValueAxes.ts +166 -173
  58. package/src/multiGrid/plugins/OverlappingValueStackAxes.ts +167 -0
  59. package/src/multiGrid/types.ts +71 -67
  60. package/src/noneData/defaults.ts +64 -64
  61. package/src/noneData/index.ts +3 -3
  62. package/src/noneData/plugins/Container.ts +10 -10
  63. package/src/noneData/plugins/Tooltip.ts +310 -310
  64. package/src/noneData/types.ts +26 -26
  65. package/src/series/defaults.ts +126 -126
  66. package/src/series/index.ts +9 -9
  67. package/src/series/plugins/Bubbles.ts +545 -602
  68. package/src/series/plugins/Pie.ts +576 -576
  69. package/src/series/plugins/PieEventTexts.ts +262 -262
  70. package/src/series/plugins/PieLabels.ts +304 -304
  71. package/src/series/plugins/Rose.ts +472 -472
  72. package/src/series/plugins/RoseLabels.ts +362 -362
  73. package/src/series/plugins/SeriesLegend.ts +59 -59
  74. package/src/series/seriesObservables.ts +145 -145
  75. package/src/series/seriesUtils.ts +51 -51
  76. package/src/series/types.ts +83 -83
  77. package/src/tree/defaults.ts +22 -22
  78. package/src/tree/index.ts +3 -3
  79. package/src/tree/plugins/TreeLegend.ts +59 -59
  80. package/src/tree/plugins/TreeMap.ts +305 -305
  81. package/src/tree/types.ts +23 -23
  82. package/src/utils/commonUtils.ts +21 -21
  83. package/src/utils/d3Graphics.ts +124 -124
  84. package/src/utils/d3Utils.ts +73 -73
  85. package/src/utils/observables.ts +14 -14
  86. package/src/utils/orbchartsUtils.ts +100 -100
  87. package/tsconfig.dev.json +16 -16
  88. package/tsconfig.json +13 -13
  89. package/tsconfig.prod.json +13 -13
  90. package/vite.config.js +49 -49
  91. package/dist/src/grid/plugins/BarsDiverging.d.ts +0 -1
@@ -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
  }