@orbcharts/plugins-basic 3.0.0-beta.19 → 3.0.0-beta.20

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