@orbcharts/plugins-basic 3.0.2 → 3.0.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 (133) hide show
  1. package/LICENSE +200 -200
  2. package/dist/lib/gridObservables.d.ts +1 -0
  3. package/dist/orbcharts-plugins-basic.es.js +14800 -13543
  4. package/dist/orbcharts-plugins-basic.umd.js +129 -99
  5. package/dist/src/base/BaseOrdinalBubbles.d.ts +26 -0
  6. package/dist/src/base/BaseRacingBars.d.ts +2 -1
  7. package/dist/src/base/BaseRacingLabels.d.ts +1 -0
  8. package/dist/src/base/BaseXZoom.d.ts +18 -0
  9. package/dist/src/grid/gridObservables.d.ts +4 -7
  10. package/dist/src/multiValue/defaults.d.ts +5 -1
  11. package/dist/src/multiValue/index.d.ts +4 -0
  12. package/dist/src/multiValue/multiValueObservables.d.ts +14 -1
  13. package/dist/src/multiValue/plugins/OrdinalAux.d.ts +3 -0
  14. package/dist/src/multiValue/plugins/OrdinalAxis.d.ts +3 -0
  15. package/dist/src/multiValue/plugins/OrdinalBubbles.d.ts +3 -0
  16. package/dist/src/multiValue/plugins/OrdinalZoom.d.ts +1 -0
  17. package/lib/core-types.ts +7 -7
  18. package/lib/core.ts +6 -6
  19. package/lib/gridObservables.ts +6 -0
  20. package/lib/plugins-basic-types.ts +6 -6
  21. package/package.json +48 -48
  22. package/src/base/BaseBars.ts +765 -765
  23. package/src/base/BaseBarsTriangle.ts +676 -676
  24. package/src/base/BaseDots.ts +464 -464
  25. package/src/base/BaseGroupAxis.ts +691 -691
  26. package/src/base/BaseLegend.ts +684 -684
  27. package/src/base/BaseLineAreas.ts +629 -629
  28. package/src/base/BaseLines.ts +706 -706
  29. package/src/base/BaseOrdinalBubbles.ts +728 -0
  30. package/src/base/BaseRacingBars.ts +582 -551
  31. package/src/base/BaseRacingLabels.ts +404 -396
  32. package/src/base/BaseRacingValueLabels.ts +403 -403
  33. package/src/base/BaseStackedBars.ts +782 -782
  34. package/src/base/BaseTooltip.ts +386 -386
  35. package/src/base/BaseValueAxis.ts +600 -600
  36. package/src/base/BaseXAxis.ts +427 -427
  37. package/src/base/BaseXZoom.ts +242 -0
  38. package/src/base/BaseYAxis.ts +389 -389
  39. package/src/base/types.ts +2 -2
  40. package/src/const.ts +30 -30
  41. package/src/grid/defaults.ts +213 -213
  42. package/src/grid/gridObservables.ts +635 -612
  43. package/src/grid/index.ts +16 -16
  44. package/src/grid/plugins/Bars.ts +69 -69
  45. package/src/grid/plugins/BarsPN.ts +66 -66
  46. package/src/grid/plugins/BarsTriangle.ts +73 -73
  47. package/src/grid/plugins/Dots.ts +68 -68
  48. package/src/grid/plugins/GridLegend.ts +107 -107
  49. package/src/grid/plugins/GridTooltip.ts +66 -66
  50. package/src/grid/plugins/GroupAux.ts +1095 -1120
  51. package/src/grid/plugins/GroupAxis.ts +73 -73
  52. package/src/grid/plugins/GroupZoom.ts +218 -218
  53. package/src/grid/plugins/LineAreas.ts +65 -65
  54. package/src/grid/plugins/Lines.ts +59 -59
  55. package/src/grid/plugins/StackedBars.ts +64 -64
  56. package/src/grid/plugins/StackedValueAxis.ts +96 -96
  57. package/src/grid/plugins/ValueAxis.ts +94 -94
  58. package/src/index.ts +6 -6
  59. package/src/multiGrid/defaults.ts +244 -244
  60. package/src/multiGrid/index.ts +14 -14
  61. package/src/multiGrid/multiGridObservables.ts +50 -50
  62. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  63. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  64. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  65. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
  66. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  67. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  68. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  69. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  70. package/src/multiGrid/plugins/MultiStackedBars.ts +106 -106
  71. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +134 -134
  72. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  73. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
  74. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  75. package/src/multiValue/defaults.ts +523 -431
  76. package/src/multiValue/index.ts +16 -12
  77. package/src/multiValue/multiValueObservables.ts +781 -666
  78. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  79. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  80. package/src/multiValue/plugins/OrdinalAux.ts +661 -0
  81. package/src/multiValue/plugins/OrdinalAxis.ts +525 -0
  82. package/src/multiValue/plugins/OrdinalBubbles.ts +226 -0
  83. package/src/multiValue/plugins/OrdinalZoom.ts +57 -0
  84. package/src/multiValue/plugins/RacingBars.ts +375 -373
  85. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
  86. package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
  87. package/src/multiValue/plugins/Scatter.ts +486 -426
  88. package/src/multiValue/plugins/ScatterBubbles.ts +635 -554
  89. package/src/multiValue/plugins/XAxis.ts +107 -107
  90. package/src/multiValue/plugins/XYAux.ts +683 -682
  91. package/src/multiValue/plugins/XYAxes.ts +194 -194
  92. package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
  93. package/src/multiValue/plugins/XZoom.ts +40 -299
  94. package/src/noneData/defaults.ts +102 -102
  95. package/src/noneData/index.ts +3 -3
  96. package/src/noneData/plugins/Container.ts +27 -27
  97. package/src/noneData/plugins/Tooltip.ts +373 -373
  98. package/src/relationship/defaults.ts +221 -221
  99. package/src/relationship/index.ts +5 -5
  100. package/src/relationship/plugins/ForceDirected.ts +1173 -1173
  101. package/src/relationship/plugins/ForceDirectedBubbles.ts +1411 -1411
  102. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  103. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  104. package/src/relationship/relationshipObservables.ts +49 -49
  105. package/src/series/defaults.ts +221 -221
  106. package/src/series/index.ts +9 -9
  107. package/src/series/plugins/Bubbles.ts +636 -636
  108. package/src/series/plugins/Pie.ts +623 -623
  109. package/src/series/plugins/PieEventTexts.ts +284 -284
  110. package/src/series/plugins/PieLabels.ts +640 -640
  111. package/src/series/plugins/Rose.ts +516 -516
  112. package/src/series/plugins/RoseLabels.ts +600 -600
  113. package/src/series/plugins/SeriesLegend.ts +107 -107
  114. package/src/series/plugins/SeriesTooltip.ts +66 -66
  115. package/src/series/seriesObservables.ts +145 -145
  116. package/src/series/seriesUtils.ts +51 -51
  117. package/src/tree/defaults.ts +102 -102
  118. package/src/tree/index.ts +4 -4
  119. package/src/tree/plugins/TreeLegend.ts +100 -100
  120. package/src/tree/plugins/TreeMap.ts +341 -341
  121. package/src/tree/plugins/TreeTooltip.ts +66 -66
  122. package/src/utils/commonUtils.ts +31 -31
  123. package/src/utils/d3Graphics.ts +176 -176
  124. package/src/utils/d3Utils.ts +92 -92
  125. package/src/utils/observables.ts +14 -14
  126. package/src/utils/orbchartsUtils.ts +129 -129
  127. package/tsconfig.base.json +13 -13
  128. package/tsconfig.json +2 -2
  129. package/vite.config.js +22 -22
  130. package/dist/src/multiValue/plugins/OrdinalXAxis.d.ts +0 -0
  131. package/dist/src/multiValue/plugins/RankingAxis_legacy.d.ts +0 -0
  132. package/src/multiValue/plugins/OrdinalXAxis.ts +0 -0
  133. package/src/multiValue/plugins/RankingAxis_legacy.ts +0 -109
@@ -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
  }