@orbcharts/plugins-basic 3.0.0-beta.1 → 3.0.0-beta.3

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