@orbcharts/plugins-basic 3.0.0-beta.7 → 3.0.0-beta.8

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 (126) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic/src/base/{BaseBarStack.d.ts → BaseStackedBar.d.ts} +4 -4
  3. package/dist/orbcharts-plugins-basic/src/grid/defaults.d.ts +3 -3
  4. package/dist/orbcharts-plugins-basic/src/grid/index.d.ts +2 -2
  5. package/dist/orbcharts-plugins-basic/src/grid/plugins/StackedBar.d.ts +1 -0
  6. package/dist/orbcharts-plugins-basic/src/grid/plugins/StackedValueAxis.d.ts +1 -0
  7. package/dist/orbcharts-plugins-basic/src/index.d.ts +1 -0
  8. package/dist/orbcharts-plugins-basic/src/multiGrid/defaults.d.ts +4 -4
  9. package/dist/orbcharts-plugins-basic/src/multiGrid/index.d.ts +3 -3
  10. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/MultiStackedBar.d.ts +1 -0
  11. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/MultiStackedValueAxis.d.ts +1 -0
  12. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/OverlappingStackedValueAxes.d.ts +1 -0
  13. package/dist/orbcharts-plugins-basic/src/noneData/plugins/Container.d.ts +0 -1
  14. package/dist/orbcharts-plugins-basic/src/noneData/plugins/Tooltip.d.ts +0 -3
  15. package/dist/orbcharts-plugins-basic/src/relationship/defaults.d.ts +5 -0
  16. package/dist/orbcharts-plugins-basic/src/relationship/index.d.ts +4 -0
  17. package/dist/orbcharts-plugins-basic/src/relationship/plugins/ForceDirected.d.ts +3 -0
  18. package/dist/orbcharts-plugins-basic/src/relationship/plugins/RelationshipLegend.d.ts +1 -0
  19. package/dist/orbcharts-plugins-basic/src/relationship/plugins/RelationshipTooltip.d.ts +1 -0
  20. package/dist/orbcharts-plugins-basic/src/relationship/relationshipObservables.d.ts +1 -0
  21. package/dist/orbcharts-plugins-basic/src/utils/commonUtils.d.ts +1 -1
  22. package/dist/orbcharts-plugins-basic/src/utils/orbchartsUtils.d.ts +1 -1
  23. package/dist/orbcharts-plugins-basic.es.js +8122 -7467
  24. package/dist/orbcharts-plugins-basic.umd.js +45 -32
  25. package/lib/core-types.ts +7 -7
  26. package/lib/core.ts +6 -6
  27. package/lib/plugins-basic-types.ts +6 -6
  28. package/package.json +44 -44
  29. package/src/base/BaseBars.ts +765 -765
  30. package/src/base/BaseBarsTriangle.ts +676 -676
  31. package/src/base/BaseDots.ts +464 -464
  32. package/src/base/BaseGroupAxis.ts +679 -679
  33. package/src/base/BaseLegend.ts +684 -684
  34. package/src/base/BaseLineAreas.ts +629 -629
  35. package/src/base/BaseLines.ts +706 -706
  36. package/src/base/{BaseBarStack.ts → BaseStackedBar.ts} +782 -782
  37. package/src/base/BaseTooltip.ts +385 -385
  38. package/src/base/BaseValueAxis.ts +583 -583
  39. package/src/base/types.ts +2 -2
  40. package/src/const.ts +30 -30
  41. package/src/grid/defaults.ts +246 -246
  42. package/src/grid/gridObservables.ts +554 -554
  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/GridZoom.ts +218 -218
  51. package/src/grid/plugins/GroupAux.ts +1103 -1103
  52. package/src/grid/plugins/GroupAxis.ts +97 -97
  53. package/src/grid/plugins/LineAreas.ts +65 -65
  54. package/src/grid/plugins/Lines.ts +59 -59
  55. package/src/grid/plugins/{BarStack.ts → StackedBar.ts} +64 -64
  56. package/src/grid/plugins/{ValueStackAxis.ts → StackedValueAxis.ts} +96 -96
  57. package/src/grid/plugins/ValueAxis.ts +94 -94
  58. package/src/index.ts +6 -10
  59. package/src/multiGrid/defaults.ts +224 -224
  60. package/src/multiGrid/index.ts +15 -15
  61. package/src/multiGrid/multiGridObservables.ts +49 -49
  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 +159 -159
  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/{MultiBarStack.ts → MultiStackedBar.ts} +106 -106
  71. package/src/multiGrid/plugins/{MultiValueStackAxis.ts → MultiStackedValueAxis.ts} +134 -134
  72. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  73. package/src/multiGrid/plugins/{OverlappingValueStackAxes.ts → OverlappingStackedValueAxes.ts} +299 -299
  74. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  75. package/src/multiValue/defaults.ts +166 -166
  76. package/src/multiValue/index.ts +8 -8
  77. package/src/multiValue/multiValueObservables.ts +297 -297
  78. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  79. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  80. package/src/multiValue/plugins/Scatter.ts +426 -426
  81. package/src/multiValue/plugins/ScatterBubbles.ts +554 -554
  82. package/src/multiValue/plugins/XYAux.ts +681 -681
  83. package/src/multiValue/plugins/XYAxes.ts +684 -684
  84. package/src/multiValue/plugins/XYZoom.ts +299 -299
  85. package/src/noneData/defaults.ts +102 -102
  86. package/src/noneData/index.ts +3 -3
  87. package/src/noneData/plugins/Container.ts +28 -28
  88. package/src/noneData/plugins/Tooltip.ts +374 -374
  89. package/src/relationship/defaults.ts +113 -0
  90. package/src/relationship/index.ts +4 -0
  91. package/src/relationship/plugins/ForceDirected.ts +1148 -0
  92. package/src/relationship/plugins/RelationshipLegend.ts +100 -0
  93. package/src/relationship/plugins/RelationshipTooltip.ts +66 -0
  94. package/src/relationship/relationshipObservables.ts +50 -0
  95. package/src/series/defaults.ts +206 -206
  96. package/src/series/index.ts +9 -9
  97. package/src/series/plugins/Bubbles.ts +604 -603
  98. package/src/series/plugins/Pie.ts +623 -623
  99. package/src/series/plugins/PieEventTexts.ts +284 -283
  100. package/src/series/plugins/PieLabels.ts +640 -640
  101. package/src/series/plugins/Rose.ts +516 -516
  102. package/src/series/plugins/RoseLabels.ts +600 -600
  103. package/src/series/plugins/SeriesLegend.ts +107 -107
  104. package/src/series/plugins/SeriesTooltip.ts +66 -66
  105. package/src/series/seriesObservables.ts +145 -145
  106. package/src/series/seriesUtils.ts +51 -51
  107. package/src/tree/defaults.ts +78 -78
  108. package/src/tree/index.ts +4 -4
  109. package/src/tree/plugins/TreeLegend.ts +100 -100
  110. package/src/tree/plugins/TreeMap.ts +333 -333
  111. package/src/tree/plugins/TreeTooltip.ts +66 -66
  112. package/src/utils/commonUtils.ts +21 -21
  113. package/src/utils/d3Graphics.ts +174 -174
  114. package/src/utils/d3Utils.ts +74 -74
  115. package/src/utils/observables.ts +14 -14
  116. package/src/utils/orbchartsUtils.ts +115 -115
  117. package/tsconfig.base.json +13 -13
  118. package/tsconfig.json +2 -2
  119. package/vite.config.js +22 -22
  120. package/dist/orbcharts-plugins-basic/src/grid/plugins/BarStack.d.ts +0 -1
  121. package/dist/orbcharts-plugins-basic/src/grid/plugins/ValueStackAxis.d.ts +0 -1
  122. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/MultiBarStack.d.ts +0 -1
  123. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/MultiValueStackAxis.d.ts +0 -1
  124. package/dist/orbcharts-plugins-basic/src/multiGrid/plugins/OverlappingValueStackAxes.d.ts +0 -1
  125. /package/dist/orbcharts-plugins-basic/src/relationship/plugins/{Relationship.d.ts → ForceDirectedBubbles.d.ts} +0 -0
  126. /package/src/relationship/plugins/{Relationship.ts → ForceDirectedBubbles.ts} +0 -0
@@ -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].series.length
102
- ? seriesIndex
103
- : seriesIndex % fullChartParams.colors[fullChartParams.colorScheme].series.length
104
- return fullChartParams.colors[fullChartParams.colorScheme].series[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 === 'series'
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
  }