@orbcharts/plugins-basic 3.0.0-alpha.24 → 3.0.0-alpha.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. package/dist/orbcharts-plugins-basic.es.js +19826 -0
  2. package/dist/orbcharts-plugins-basic.umd.js +12 -0
  3. package/dist/src/grid/defaults.d.ts +12 -0
  4. package/dist/src/grid/gridObservables.d.ts +15 -0
  5. package/dist/src/grid/index.d.ts +12 -0
  6. package/dist/src/grid/plugins/BarStack.d.ts +3 -0
  7. package/dist/src/grid/plugins/Bars.d.ts +3 -0
  8. package/dist/src/grid/plugins/BarsTriangle.d.ts +3 -0
  9. package/dist/src/grid/plugins/Dots.d.ts +3 -0
  10. package/dist/src/grid/plugins/GroupArea.d.ts +3 -0
  11. package/dist/src/grid/plugins/GroupAxis.d.ts +3 -0
  12. package/dist/src/grid/plugins/Lines.d.ts +3 -0
  13. package/dist/src/grid/plugins/Ranking.d.ts +0 -0
  14. package/dist/src/grid/plugins/RankingAxis.d.ts +0 -0
  15. package/dist/src/grid/plugins/ScalingArea.d.ts +1 -0
  16. package/dist/src/grid/plugins/ValueAxis.d.ts +3 -0
  17. package/dist/src/grid/plugins/ValueStackAxis.d.ts +3 -0
  18. package/dist/src/grid/types.d.ts +73 -0
  19. package/dist/src/index.d.ts +3 -0
  20. package/dist/src/multiGrid/index.d.ts +0 -0
  21. package/dist/src/multiGrid/plugins/Diverging.d.ts +0 -0
  22. package/dist/src/multiGrid/plugins/DivergingAxes.d.ts +0 -0
  23. package/dist/src/multiGrid/plugins/TwoScaleAxes.d.ts +0 -0
  24. package/dist/src/multiGrid/plugins/TwoScales.d.ts +0 -0
  25. package/dist/src/multiValue/index.d.ts +0 -0
  26. package/dist/src/multiValue/plugins/Scatter.d.ts +0 -0
  27. package/dist/src/multiValue/plugins/ScatterAxes.d.ts +0 -0
  28. package/dist/src/noneData/defaults.d.ts +4 -0
  29. package/dist/src/noneData/index.d.ts +4 -0
  30. package/dist/src/noneData/plugins/Container.d.ts +1 -0
  31. package/dist/src/noneData/plugins/Tooltip.d.ts +3 -0
  32. package/dist/src/noneData/types.d.ts +24 -0
  33. package/dist/src/relationship/index.d.ts +0 -0
  34. package/dist/src/relationship/plugins/Relationship.d.ts +0 -0
  35. package/dist/src/series/defaults.d.ts +7 -0
  36. package/dist/src/series/index.d.ts +7 -0
  37. package/dist/src/series/plugins/Bubbles.d.ts +3 -0
  38. package/dist/src/series/plugins/Pie.d.ts +3 -0
  39. package/dist/src/series/plugins/PieEventTexts.d.ts +1 -0
  40. package/dist/src/series/plugins/PieLabels.d.ts +3 -0
  41. package/dist/src/series/plugins/SeriesLegend.d.ts +3 -0
  42. package/dist/src/series/plugins/Waffle.d.ts +0 -0
  43. package/dist/src/series/seriesUtils.d.ts +19 -0
  44. package/dist/src/series/types.d.ts +52 -0
  45. package/dist/src/tree/index.d.ts +0 -0
  46. package/dist/src/tree/plugins/TreeMap.d.ts +0 -0
  47. package/dist/src/utils/commonUtils.d.ts +2 -0
  48. package/dist/src/utils/d3Graphics.d.ts +13 -0
  49. package/dist/src/utils/d3Utils.d.ts +13 -0
  50. package/dist/src/utils/observables.d.ts +3 -0
  51. package/dist/src/utils/orbchartsUtils.d.ts +21 -0
  52. package/dist/vite.config.d.ts +2 -0
  53. package/package.json +3 -2
  54. package/src/series/defaults.ts +13 -2
  55. package/src/series/index.ts +2 -1
  56. package/src/series/plugins/Bubbles.ts +0 -3
  57. package/src/series/plugins/Pie.ts +0 -4
  58. package/src/series/plugins/PieLabels.ts +0 -4
  59. package/src/series/plugins/SeriesLegend.ts +457 -0
  60. package/src/series/types.ts +10 -0
  61. package/src/utils/orbchartsUtils.ts +10 -1
@@ -0,0 +1,457 @@
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ switchMap,
6
+ takeUntil,
7
+ Observable,
8
+ Subject } from 'rxjs'
9
+ import {
10
+ defineSeriesPlugin } from '@orbcharts/core'
11
+ import type {
12
+ ChartParams } from '@orbcharts/core'
13
+ import type { SeriesLegendParams } from '../types'
14
+ import { DEFAULT_SERIES_LEGEND_PARAMS } from '../defaults'
15
+ import { getSeriesColor, getClassName } from '../../utils/orbchartsUtils'
16
+ import { measureTextWidth } from '../../utils/commonUtils'
17
+
18
+
19
+ // 第1層 - 定位的容器(絕對位置)
20
+ interface Position {
21
+ x:number
22
+ y:number
23
+ }
24
+
25
+ // 第2層 - 圖例列表
26
+ interface LegendList {
27
+ direction: 'row' | 'column'
28
+ width: number
29
+ height: number
30
+ translateX:number
31
+ translateY:number
32
+ list: LegendItem[][]
33
+ }
34
+
35
+ // 第3層 - 圖例項目
36
+ interface LegendItem {
37
+ id: string // seriesLabel
38
+ seriesLabel: string
39
+ seriesIndex: number
40
+ lineIndex: number
41
+ itemIndex: number // 行內的item
42
+ text: string
43
+ itemWidth: number
44
+ translateX: number
45
+ translateY: number
46
+ color: string
47
+ // fontSize: number
48
+ // rectRadius: number
49
+ }
50
+
51
+ const pluginName = 'SeriesLegend'
52
+ const boxClassName = getClassName(pluginName, 'box')
53
+ const legendListClassName = getClassName(pluginName, 'legend-list')
54
+ const itemClassName = getClassName(pluginName, 'item')
55
+
56
+ function renderSeriesLegend ({ itemSelection, lengendList, seriesLabel, fullParams, fullChartParams }: {
57
+ itemSelection: d3.Selection<SVGGElement, LegendItem, any, any>
58
+ lengendList: LegendList
59
+ seriesLabel: string[]
60
+ fullParams: SeriesLegendParams
61
+ fullChartParams: ChartParams
62
+ }) {
63
+ itemSelection
64
+ .each((d, i, g) => {
65
+ // 方塊
66
+ d3.select(g[i])
67
+ .selectAll('rect')
68
+ .data([d])
69
+ .join('rect')
70
+ .attr('width', fullChartParams.styles.textSize)
71
+ .attr('height', fullChartParams.styles.textSize)
72
+ .attr('fill', _d => _d.color)
73
+ .attr('rx', fullParams.rectRadius)
74
+ // 文字
75
+ d3.select(g[i])
76
+ .selectAll('text')
77
+ .data([d])
78
+ .join(
79
+ enter => {
80
+ return enter
81
+ .append('text')
82
+ .attr('dominant-baseline', 'hanging')
83
+ },
84
+ update => {
85
+ return update
86
+ },
87
+ exit => exit.remove()
88
+ )
89
+ .attr('x', fullChartParams.styles.textSize * 1.5)
90
+ .attr('font-size', fullChartParams.styles.textSize)
91
+ .text(d => d.text)
92
+ })
93
+ }
94
+
95
+
96
+ export const SeriesLegend = defineSeriesPlugin(pluginName, DEFAULT_SERIES_LEGEND_PARAMS)(({ selection, rootSelection, observer, subject }) => {
97
+
98
+ const destroy$ = new Subject()
99
+
100
+ const seriesLabels$: Observable<string[]> = observer.SeriesDataMap$.pipe(
101
+ takeUntil(destroy$),
102
+ map(data => {
103
+ return Array.from(data.keys())
104
+ })
105
+ )
106
+
107
+ const lineDirection$ = observer.fullParams$.pipe(
108
+ takeUntil(destroy$),
109
+ map(data => {
110
+ return data.position === 'bottom' || data.position === 'top'
111
+ ? 'row'
112
+ : 'column'
113
+ })
114
+ )
115
+
116
+ const lineMaxSize$ = combineLatest({
117
+ fullParams: observer.fullParams$,
118
+ layout: observer.layout$
119
+ }).pipe(
120
+ takeUntil(destroy$),
121
+ map(data => {
122
+ return data.fullParams.position === 'bottom' || data.fullParams.position === 'top'
123
+ ? data.layout.rootWidth - 2 // 減2是避免完全貼到邊線上
124
+ : data.layout.rootHeight - 2
125
+ })
126
+ )
127
+
128
+ const boxPosition$ = combineLatest({
129
+ layout: observer.layout$,
130
+ fullParams: observer.fullParams$,
131
+ }).pipe(
132
+ takeUntil(destroy$),
133
+ switchMap(async d => d),
134
+ map(data => {
135
+ let x = 0
136
+ let y = 0
137
+ if (data.fullParams.position === 'bottom') {
138
+ y = data.layout.rootHeight
139
+ if (data.fullParams.justify === 'start') {
140
+ x = 0
141
+ } else if (data.fullParams.justify === 'center') {
142
+ x = data.layout.rootWidth / 2
143
+ } else if (data.fullParams.justify === 'end') {
144
+ x = data.layout.rootWidth
145
+ }
146
+ } else if (data.fullParams.position === 'right') {
147
+ x = data.layout.rootWidth
148
+ if (data.fullParams.justify === 'start') {
149
+ y = 0
150
+ } else if (data.fullParams.justify === 'center') {
151
+ y = data.layout.rootHeight / 2
152
+ } else if (data.fullParams.justify === 'end') {
153
+ y = data.layout.rootHeight
154
+ }
155
+ } else if (data.fullParams.position === 'top') {
156
+ y = 0
157
+ if (data.fullParams.justify === 'start') {
158
+ x = 0
159
+ } else if (data.fullParams.justify === 'center') {
160
+ x = data.layout.rootWidth / 2
161
+ } else if (data.fullParams.justify === 'end') {
162
+ x = data.layout.rootWidth
163
+ }
164
+ } else if (data.fullParams.position === 'left') {
165
+ x = 0
166
+ if (data.fullParams.justify === 'start') {
167
+ y = 0
168
+ } else if (data.fullParams.justify === 'center') {
169
+ y = data.layout.rootHeight / 2
170
+ } else if (data.fullParams.justify === 'end') {
171
+ y = data.layout.rootHeight
172
+ }
173
+ }
174
+
175
+ return {
176
+ x,
177
+ y
178
+ }
179
+ })
180
+ )
181
+
182
+ const boxSelection$: Observable<d3.Selection<SVGGElement, Position, any, any>> = boxPosition$.pipe(
183
+ takeUntil(destroy$),
184
+ map(data => {
185
+
186
+ return rootSelection
187
+ .selectAll<SVGGElement, Position>(`g.${boxClassName}`)
188
+ .data([data])
189
+ .join(
190
+ enter => {
191
+ return enter
192
+ .append('g')
193
+ .classed(boxClassName, true)
194
+ .attr('transform', d => `translate(${d.x}, ${d.y})`)
195
+ },
196
+ update => {
197
+ return update
198
+ .transition()
199
+ .attr('transform', d => `translate(${d.x}, ${d.y})`)
200
+ },
201
+ exit => exit.remove()
202
+ )
203
+ })
204
+ )
205
+
206
+ const lengendList$: Observable<LegendList> = combineLatest({
207
+ layout: observer.layout$,
208
+ fullParams: observer.fullParams$,
209
+ fullChartParams: observer.fullChartParams$,
210
+ seriesLabels: seriesLabels$,
211
+ lineDirection: lineDirection$,
212
+ lineMaxSize: lineMaxSize$
213
+ }).pipe(
214
+ takeUntil(destroy$),
215
+ switchMap(async d => d),
216
+ map(data => {
217
+ const list: LegendItem[][] = data.seriesLabels.reduce((prev: LegendItem[][], current, currentIndex) => {
218
+ const textWidth = measureTextWidth(current, data.fullChartParams.styles.textSize)
219
+ const itemWidth = (data.fullChartParams.styles.textSize * 1.5) + textWidth
220
+ const color = getSeriesColor(currentIndex, data.fullChartParams)
221
+ const lastItem: LegendItem | null = prev[0] && prev[0][0]
222
+ ? prev[prev.length - 1][prev[prev.length - 1].length - 1]
223
+ : null
224
+
225
+ const { translateX, translateY, lineIndex, itemIndex } = ((_data, _prev, _lastItem) => {
226
+ let translateX = 0
227
+ let translateY = 0
228
+ let lineIndex = 0
229
+ let itemIndex = 0
230
+
231
+ if (_data.lineDirection === 'column') {
232
+ let tempTranslateY = _lastItem
233
+ ? _lastItem.translateY + _data.fullChartParams.styles.textSize + _data.fullParams.gap
234
+ : 0
235
+
236
+ if ((tempTranslateY + _data.fullChartParams.styles.textSize) > _data.lineMaxSize) {
237
+ // 換行
238
+ lineIndex = _lastItem.lineIndex + 1
239
+ itemIndex = 0
240
+ translateY = 0
241
+ // 前一行最寬寬度
242
+ const maxItemWidthInLastLine = _prev[_prev.length - 1].reduce((p, c) => {
243
+ return c.itemWidth > p ? c.itemWidth : p
244
+ }, 0)
245
+ translateX = _lastItem.translateX + maxItemWidthInLastLine + _data.fullParams.gap
246
+ } else {
247
+ lineIndex = _lastItem ? _lastItem.lineIndex : 0
248
+ itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
249
+ translateY = tempTranslateY
250
+ translateX = _lastItem ? _lastItem.translateX : 0
251
+ }
252
+ } else {
253
+ let tempTranslateX = _lastItem
254
+ ? _lastItem.translateX + _lastItem.itemWidth + _data.fullParams.gap
255
+ : 0
256
+ if ((tempTranslateX + itemWidth) > _data.lineMaxSize) {
257
+ // 換行
258
+ lineIndex = _lastItem.lineIndex + 1
259
+ itemIndex = 0
260
+ translateX = 0
261
+ } else {
262
+ lineIndex = _lastItem ? _lastItem.lineIndex : 0
263
+ itemIndex = _lastItem ? _lastItem.itemIndex + 1 : 0
264
+ translateX = tempTranslateX
265
+ }
266
+ translateY = (_data.fullChartParams.styles.textSize + _data.fullParams.gap) * lineIndex
267
+ }
268
+
269
+ return { translateX, translateY, lineIndex, itemIndex }
270
+ })(data, prev, lastItem)
271
+
272
+ if (!prev[lineIndex]) {
273
+ prev[lineIndex] = []
274
+ }
275
+
276
+ prev[lineIndex].push({
277
+ id: current,
278
+ seriesLabel: current,
279
+ seriesIndex: currentIndex,
280
+ lineIndex,
281
+ itemIndex,
282
+ text: current,
283
+ itemWidth,
284
+ translateX,
285
+ translateY,
286
+ color,
287
+ })
288
+
289
+ return prev
290
+ }, [])
291
+
292
+ // 依list計算出來的排序位置來計算整體的偏移位置
293
+ const { width, height, translateX, translateY } = ((_data, _list) => {
294
+ let width = 0
295
+ let height = 0
296
+ let translateX = 0
297
+ let translateY = 0
298
+
299
+ if (!_list.length || !_list[0].length) {
300
+ return { width, height, translateX, translateY }
301
+ }
302
+
303
+ const firstLineLastItem = _list[0][_list[0].length - 1]
304
+ if (_data.lineDirection === 'column') {
305
+ width = _list.reduce((p, c) => {
306
+ const maxWidthInLine = c.reduce((_p, _c) => {
307
+ // 找出最寬的寬度
308
+ return _c.itemWidth > _p ? _c.itemWidth : _p
309
+ }, 0)
310
+ // 每行寬度加總
311
+ return p + maxWidthInLine
312
+ }, 0)
313
+ height = firstLineLastItem.translateY + _data.fullChartParams.styles.textSize + _data.fullParams.gap
314
+ } else {
315
+ width = firstLineLastItem.translateX + firstLineLastItem.itemWidth
316
+ height = (_data.fullChartParams.styles.textSize * _list.length) + (_data.fullParams.gap * (_list.length - 1))
317
+ }
318
+
319
+ if (_data.fullParams.position === 'left') {
320
+ if (_data.fullParams.justify === 'start') {
321
+ translateX = _data.fullParams.padding
322
+ translateY = _data.fullParams.padding
323
+ } else if (_data.fullParams.justify === 'center') {
324
+ translateX = _data.fullParams.padding
325
+ translateY = - height / 2
326
+ } else if (_data.fullParams.justify === 'end') {
327
+ translateX = _data.fullParams.padding
328
+ translateY = - height - _data.fullParams.padding
329
+ }
330
+ } else if (_data.fullParams.position === 'right') {
331
+ if (_data.fullParams.justify === 'start') {
332
+ translateX = - width - _data.fullParams.padding
333
+ translateY = _data.fullParams.padding
334
+ } else if (_data.fullParams.justify === 'center') {
335
+ translateX = - width - _data.fullParams.padding
336
+ translateY = - height / 2
337
+ } else if (_data.fullParams.justify === 'end') {
338
+ translateX = - width - _data.fullParams.padding
339
+ translateY = - height - _data.fullParams.padding
340
+ }
341
+ } else if (_data.fullParams.position === 'top') {
342
+ if (_data.fullParams.justify === 'start') {
343
+ translateX = _data.fullParams.padding
344
+ translateY = _data.fullParams.padding
345
+ } else if (_data.fullParams.justify === 'center') {
346
+ translateX = - width / 2
347
+ translateY = _data.fullParams.padding
348
+ } else if (_data.fullParams.justify === 'end') {
349
+ translateX = - width - _data.fullParams.padding
350
+ translateY = _data.fullParams.padding
351
+ }
352
+ } else {
353
+ if (_data.fullParams.justify === 'start') {
354
+ translateX = _data.fullParams.padding
355
+ translateY = - height - _data.fullParams.padding
356
+ } else if (_data.fullParams.justify === 'center') {
357
+ translateX = - width / 2
358
+ translateY = - height - _data.fullParams.padding
359
+ } else if (_data.fullParams.justify === 'end') {
360
+ translateX = - width - _data.fullParams.padding
361
+ translateY = - height - _data.fullParams.padding
362
+ }
363
+ }
364
+
365
+ // translateX += _data.fullParams.offset[0]
366
+ // translateY += _data.fullParams.offset[1]
367
+
368
+ return { width, height, translateX, translateY }
369
+ })(data, list)
370
+
371
+ return {
372
+ direction: data.lineDirection,
373
+ width,
374
+ height,
375
+ translateX,
376
+ translateY,
377
+ list
378
+ }
379
+ })
380
+ )
381
+
382
+ const lengendListSelection$ = combineLatest({
383
+ boxSelection: boxSelection$,
384
+ fullParams: observer.fullParams$,
385
+ lengendList: lengendList$
386
+ }).pipe(
387
+ takeUntil(destroy$),
388
+ switchMap(async d => d),
389
+ map(data => {
390
+ return data.boxSelection
391
+ .selectAll<SVGGElement, SeriesLegendParams>('g')
392
+ .data([data.lengendList])
393
+ .join(
394
+ enter => {
395
+ return enter
396
+ .append('g')
397
+ .classed(legendListClassName, true)
398
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
399
+ },
400
+ update => {
401
+ return update
402
+ .transition()
403
+ .attr('transform', d => `translate(${d.translateX}, ${d.translateY})`)
404
+ },
405
+ exit => exit.remove()
406
+ )
407
+ })
408
+ )
409
+
410
+ const itemSelection$ = lengendListSelection$.pipe(
411
+ takeUntil(destroy$),
412
+ map(lengendListSelection => {
413
+ const legendListData = lengendListSelection.data()
414
+ const data = legendListData[0] ? legendListData[0].list.flat() : []
415
+
416
+ return lengendListSelection
417
+ .selectAll<SVGGElement, string>(`g.${itemClassName}`)
418
+ .data(data)
419
+ .join(
420
+ enter => {
421
+ return enter
422
+ .append('g')
423
+ .classed(itemClassName, true)
424
+ .attr('cursor', 'default')
425
+ },
426
+ update => update,
427
+ exit => exit.remove()
428
+ )
429
+ .attr('transform', (d, i) => {
430
+ return `translate(${d.translateX}, ${d.translateY})`
431
+ })
432
+ })
433
+ )
434
+
435
+ combineLatest({
436
+ itemSelection: itemSelection$,
437
+ lengendList: lengendList$,
438
+ seriesLabels: seriesLabels$,
439
+ fullParams: observer.fullParams$,
440
+ fullChartParams: observer.fullChartParams$
441
+ }).pipe(
442
+ takeUntil(destroy$),
443
+ switchMap(async d => d),
444
+ ).subscribe(data => {
445
+ renderSeriesLegend({
446
+ itemSelection: data.itemSelection,
447
+ lengendList: data.lengendList,
448
+ seriesLabel: data.seriesLabels,
449
+ fullParams: data.fullParams,
450
+ fullChartParams: data.fullChartParams
451
+ })
452
+ })
453
+
454
+ return () => {
455
+ destroy$.next(undefined)
456
+ }
457
+ })
@@ -51,3 +51,13 @@ export interface PieLabelsPluginParams {
51
51
  labelFn: ((d: ComputedDatumSeries) => string)
52
52
  labelColorType: ColorType
53
53
  }
54
+
55
+ export interface SeriesLegendParams {
56
+ position: 'top' | 'bottom' | 'left' | 'right'
57
+ justify: 'start' | 'center' | 'end'
58
+ padding: number
59
+ // offset: [number, number]
60
+ gap: number
61
+ rectRadius: number
62
+ // highlightEvent: boolean
63
+ }
@@ -9,7 +9,7 @@ export function getMinAndMaxValue (data: ComputedDatumBase[]): [number, number]
9
9
  return getMinAndMax(arr)
10
10
  }
11
11
 
12
-
12
+ // 取得colorType顏色
13
13
  export function getColor (colorType: ColorType, fullChartParams: ChartParams) {
14
14
  const colors = fullChartParams.colors[fullChartParams.colorScheme]
15
15
  // 對應series資料中第1個顏色
@@ -22,6 +22,15 @@ export function getColor (colorType: ColorType, fullChartParams: ChartParams) {
22
22
  : colors.primary
23
23
  }
24
24
 
25
+ // 取得Series顏色
26
+ export function getSeriesColor (seriesIndex: number, fullChartParams: ChartParams) {
27
+ const colorIndex = seriesIndex < fullChartParams.colors[fullChartParams.colorScheme].series.length
28
+ ? seriesIndex
29
+ : seriesIndex % fullChartParams.colors[fullChartParams.colorScheme].series.length
30
+ return fullChartParams.colors[fullChartParams.colorScheme].series[colorIndex]
31
+ }
32
+
33
+ // 取得Datum顏色
25
34
  export function getDatumColor ({ datum, colorType, fullChartParams }: { datum: ComputedDatumBase, colorType: ColorType, fullChartParams: ChartParams }) {
26
35
  // 對應series資料中的顏色
27
36
  if (colorType === 'series') {