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

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