@orbcharts/plugins-basic 3.0.0-alpha.43 → 3.0.0-alpha.44

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