@orbcharts/plugins-basic 3.0.0-alpha.61 → 3.0.0-alpha.62

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +1 -1
  3. package/dist/orbcharts-plugins-basic.umd.js +1 -1
  4. package/package.json +42 -42
  5. package/src/base/BaseBarStack.ts +778 -778
  6. package/src/base/BaseBars.ts +764 -764
  7. package/src/base/BaseBarsTriangle.ts +672 -672
  8. package/src/base/BaseDots.ts +513 -513
  9. package/src/base/BaseGroupAxis.ts +558 -558
  10. package/src/base/BaseLegend.ts +641 -641
  11. package/src/base/BaseLineAreas.ts +628 -628
  12. package/src/base/BaseLines.ts +704 -704
  13. package/src/base/BaseValueAxis.ts +480 -478
  14. package/src/base/types.ts +2 -2
  15. package/src/grid/defaults.ts +128 -128
  16. package/src/grid/gridObservables.ts +541 -541
  17. package/src/grid/index.ts +15 -15
  18. package/src/grid/plugins/BarStack.ts +43 -43
  19. package/src/grid/plugins/Bars.ts +44 -44
  20. package/src/grid/plugins/BarsPN.ts +41 -41
  21. package/src/grid/plugins/BarsTriangle.ts +42 -42
  22. package/src/grid/plugins/Dots.ts +37 -37
  23. package/src/grid/plugins/GridLegend.ts +59 -59
  24. package/src/grid/plugins/GroupAux.ts +976 -976
  25. package/src/grid/plugins/GroupAxis.ts +35 -35
  26. package/src/grid/plugins/LineAreas.ts +40 -40
  27. package/src/grid/plugins/Lines.ts +40 -40
  28. package/src/grid/plugins/ScalingArea.ts +173 -173
  29. package/src/grid/plugins/ValueAxis.ts +36 -36
  30. package/src/grid/plugins/ValueStackAxis.ts +38 -38
  31. package/src/grid/types.ts +123 -123
  32. package/src/index.ts +9 -9
  33. package/src/multiGrid/defaults.ts +158 -158
  34. package/src/multiGrid/index.ts +13 -13
  35. package/src/multiGrid/multiGridObservables.ts +49 -49
  36. package/src/multiGrid/plugins/MultiBarStack.ts +78 -78
  37. package/src/multiGrid/plugins/MultiBars.ts +77 -77
  38. package/src/multiGrid/plugins/MultiBarsTriangle.ts +77 -77
  39. package/src/multiGrid/plugins/MultiDots.ts +65 -65
  40. package/src/multiGrid/plugins/MultiGridLegend.ts +89 -89
  41. package/src/multiGrid/plugins/MultiGroupAxis.ts +69 -69
  42. package/src/multiGrid/plugins/MultiLineAreas.ts +77 -77
  43. package/src/multiGrid/plugins/MultiLines.ts +77 -77
  44. package/src/multiGrid/plugins/MultiValueAxis.ts +69 -69
  45. package/src/multiGrid/plugins/MultiValueStackAxis.ts +69 -69
  46. package/src/multiGrid/plugins/OverlappingValueAxes.ts +167 -167
  47. package/src/multiGrid/plugins/OverlappingValueStackAxes.ts +168 -168
  48. package/src/multiGrid/types.ts +72 -72
  49. package/src/noneData/defaults.ts +102 -102
  50. package/src/noneData/index.ts +3 -3
  51. package/src/noneData/plugins/Container.ts +10 -10
  52. package/src/noneData/plugins/Tooltip.ts +310 -310
  53. package/src/noneData/types.ts +26 -26
  54. package/src/series/defaults.ts +144 -144
  55. package/src/series/index.ts +9 -9
  56. package/src/series/plugins/Bubbles.ts +545 -545
  57. package/src/series/plugins/Pie.ts +576 -576
  58. package/src/series/plugins/PieEventTexts.ts +262 -262
  59. package/src/series/plugins/PieLabels.ts +304 -304
  60. package/src/series/plugins/Rose.ts +472 -472
  61. package/src/series/plugins/RoseLabels.ts +362 -362
  62. package/src/series/plugins/SeriesLegend.ts +59 -59
  63. package/src/series/seriesObservables.ts +145 -145
  64. package/src/series/seriesUtils.ts +51 -51
  65. package/src/series/types.ts +83 -83
  66. package/src/tree/defaults.ts +23 -23
  67. package/src/tree/index.ts +3 -3
  68. package/src/tree/plugins/TreeLegend.ts +59 -59
  69. package/src/tree/plugins/TreeMap.ts +305 -305
  70. package/src/tree/types.ts +23 -23
  71. package/src/utils/commonUtils.ts +21 -21
  72. package/src/utils/d3Graphics.ts +124 -124
  73. package/src/utils/d3Utils.ts +73 -73
  74. package/src/utils/observables.ts +14 -14
  75. package/src/utils/orbchartsUtils.ts +100 -100
  76. package/tsconfig.base.json +13 -13
  77. package/tsconfig.json +2 -2
  78. package/vite.config.js +22 -22
@@ -1,478 +1,480 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- switchMap,
5
- distinctUntilChanged,
6
- first,
7
- map,
8
- takeUntil,
9
- Observable,
10
- Subject } from 'rxjs'
11
- import { createAxisLinearScale } from '@orbcharts/core'
12
- import type { BasePluginFn } from './types'
13
- import type {
14
- ComputedDataGrid,
15
- DataFormatterGrid,
16
- ChartParams,
17
- ComputedDatumGrid,
18
- GridContainerPosition,
19
- TransformData,
20
- EventGrid,
21
- ColorType } from '@orbcharts/core'
22
- import { parseTickFormatValue } from '../utils/d3Utils'
23
- import { getColor, getMinAndMaxValue, getClassName, getUniID } from '../utils/orbchartsUtils'
24
-
25
- export interface BaseValueAxisParams {
26
- labelOffset: [number, number]
27
- labelColorType: ColorType
28
- axisLineVisible: boolean
29
- axisLineColorType: ColorType
30
- ticks: number
31
- tickFormat: string | ((text: d3.NumberValue) => string)
32
- tickLineVisible: boolean
33
- tickPadding: number
34
- tickFullLine: boolean
35
- tickFullLineDasharray: string
36
- tickColorType: ColorType
37
- tickTextRotate: number
38
- tickTextColorType: ColorType
39
- }
40
-
41
- interface BaseLinesContext {
42
- selection: d3.Selection<any, unknown, any, unknown>
43
- computedData$: Observable<ComputedDataGrid>
44
- fullParams$: Observable<BaseValueAxisParams>
45
- fullDataFormatter$: Observable<DataFormatterGrid>
46
- fullChartParams$: Observable<ChartParams>
47
- gridAxesTransform$: Observable<TransformData>
48
- gridAxesReverseTransform$: Observable<TransformData>
49
- gridAxesSize$: Observable<{
50
- width: number;
51
- height: number;
52
- }>
53
- gridContainerPosition$: Observable<GridContainerPosition[]>
54
- isSeriesSeprate$: Observable<boolean>
55
- }
56
-
57
- interface TextAlign {
58
- textAnchor: "start" | "middle" | "end"
59
- dominantBaseline: "middle" | "auto" | "hanging"
60
- }
61
-
62
- // const pluginName = 'ValueAxis'
63
- // const containerClassName = getClassName(pluginName, 'container')
64
- // const yAxisGClassName = getClassName(pluginName, 'yAxisG')
65
- // const yAxisClassName = getClassName(pluginName, 'yAxis')
66
- // const textClassName = getClassName(pluginName, 'text')
67
- const defaultTickSize = 6
68
-
69
- function renderAxis ({ selection, yAxisClassName, textClassName, fullParams, tickTextAlign, axisLabelAlign, gridAxesSize, fullDataFormatter, fullChartParams, valueScale, textTransform, minAndMax }: {
70
- selection: d3.Selection<SVGGElement, any, any, any>,
71
- yAxisClassName: string
72
- textClassName: string
73
- fullParams: BaseValueAxisParams
74
- tickTextAlign: TextAlign
75
- axisLabelAlign: TextAlign
76
- gridAxesSize: { width: number, height: number }
77
- fullDataFormatter: DataFormatterGrid,
78
- fullChartParams: ChartParams
79
- valueScale: d3.ScaleLinear<number, number>
80
- textTransform: string,
81
- minAndMax: [number, number]
82
- }) {
83
-
84
- const yAxisSelection = selection
85
- .selectAll<SVGGElement, BaseValueAxisParams>(`g.${yAxisClassName}`)
86
- .data([fullParams])
87
- .join('g')
88
- .classed(yAxisClassName, true)
89
-
90
- const axisLabelSelection = selection
91
- .selectAll<SVGGElement, BaseValueAxisParams>(`g.${textClassName}`)
92
- .data([fullParams])
93
- .join('g')
94
- .classed(textClassName, true)
95
- .each((d, i, g) => {
96
- const text = d3.select(g[i])
97
- .selectAll<SVGTextElement, BaseValueAxisParams>(`text`)
98
- .data([d])
99
- .join(
100
- enter => {
101
- return enter
102
- .append('text')
103
- .style('font-weight', 'bold')
104
- },
105
- update => update,
106
- exit => exit.remove()
107
- )
108
- .attr('text-anchor', axisLabelAlign.textAnchor)
109
- .attr('dominant-baseline', axisLabelAlign.dominantBaseline)
110
- .attr('font-size', fullChartParams.styles.textSize)
111
- .style('fill', getColor(fullParams.labelColorType, fullChartParams))
112
- // .style('transform', textTransform)
113
- .text(d => fullDataFormatter.grid.valueAxis.label)
114
- })
115
- .attr('transform', d => `translate(${- d.tickPadding + fullParams.labelOffset[0]}, ${gridAxesSize.height + d.tickPadding + fullParams.labelOffset[1]})`)
116
-
117
- const valueLength = minAndMax[1] - minAndMax[0]
118
-
119
- // const _valueScale = d3.scaleLinear()
120
- // .domain([0, 150])
121
- // .range([416.5, 791.349])
122
-
123
- // 設定Y軸刻度
124
- const yAxis = d3.axisLeft(valueScale)
125
- .scale(valueScale)
126
- .ticks(valueLength > fullParams.ticks
127
- ? fullParams.ticks
128
- : ((minAndMax[0] === 0 && minAndMax[1] === 0)
129
- ? 1
130
- : Math.ceil(valueLength))) // 刻度分段數量
131
- .tickFormat(d => parseTickFormatValue(d, fullParams.tickFormat))
132
- .tickSize(fullParams.tickFullLine == true
133
- ? -gridAxesSize.width
134
- : defaultTickSize)
135
- .tickPadding(fullParams.tickPadding)
136
-
137
- const yAxisEl = yAxisSelection
138
- .transition()
139
- .duration(100)
140
- .call(yAxis)
141
-
142
- yAxisEl.selectAll('line')
143
- .style('fill', 'none')
144
- .style('stroke', fullParams.tickLineVisible == true ? getColor(fullParams.tickColorType, fullChartParams) : 'none')
145
- .style('stroke-dasharray', fullParams.tickFullLineDasharray)
146
- .attr('pointer-events', 'none')
147
-
148
- yAxisEl.selectAll('path')
149
- .style('fill', 'none')
150
- // .style('stroke', this.fullParams.axisLineColor!)
151
- .style('stroke', fullParams.axisLineVisible == true ? getColor(fullParams.axisLineColorType, fullChartParams) : 'none')
152
- .style('shape-rendering', 'crispEdges')
153
-
154
- // const yText = yAxisEl.selectAll('text')
155
- const yText = yAxisSelection.selectAll('text')
156
- // .style('font-family', 'sans-serif')
157
- .attr('font-size', fullChartParams.styles.textSize)
158
- .style('color', getColor(fullParams.tickTextColorType, fullChartParams))
159
- .attr('text-anchor', tickTextAlign.textAnchor)
160
- .attr('dominant-baseline', tickTextAlign.dominantBaseline)
161
- .attr('transform-origin', `-${fullParams.tickPadding + defaultTickSize} 0`)
162
- yText.style('transform', textTransform)
163
-
164
- return yAxisSelection
165
- }
166
-
167
-
168
-
169
- export const createBaseValueAxis: BasePluginFn<BaseLinesContext> = (pluginName: string, {
170
- selection,
171
- computedData$,
172
- fullParams$,
173
- fullDataFormatter$,
174
- fullChartParams$,
175
- gridAxesTransform$,
176
- gridAxesReverseTransform$,
177
- gridAxesSize$,
178
- gridContainerPosition$,
179
- isSeriesSeprate$,
180
- }) => {
181
-
182
- const destroy$ = new Subject()
183
-
184
- const containerClassName = getClassName(pluginName, 'container')
185
- const yAxisGClassName = getClassName(pluginName, 'yAxisG')
186
- const yAxisClassName = getClassName(pluginName, 'yAxis')
187
- const textClassName = getClassName(pluginName, 'text')
188
-
189
- const containerSelection$ = combineLatest({
190
- computedData: computedData$.pipe(
191
- distinctUntilChanged((a, b) => {
192
- // 只有當series的數量改變時,才重新計算
193
- return a.length === b.length
194
- }),
195
- ),
196
- isSeriesSeprate: isSeriesSeprate$
197
- }).pipe(
198
- takeUntil(destroy$),
199
- switchMap(async (d) => d),
200
- map(data => {
201
- return data.isSeriesSeprate
202
- // series分開的時候顯示各別axis
203
- ? data.computedData
204
- // series合併的時候只顯示第一個axis
205
- : [data.computedData[0]]
206
- }),
207
- map((computedData, i) => {
208
- return selection
209
- .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${containerClassName}`)
210
- .data(computedData, d => d[0] ? d[0].seriesIndex : i)
211
- .join('g')
212
- .classed(containerClassName, true)
213
- })
214
- )
215
-
216
- const axisSelection$ = containerSelection$.pipe(
217
- takeUntil(destroy$),
218
- map((containerSelection, i) => {
219
- return containerSelection
220
- .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${yAxisGClassName}`)
221
- .data([yAxisGClassName])
222
- .join('g')
223
- .classed(yAxisGClassName, true)
224
- })
225
- )
226
-
227
- combineLatest({
228
- containerSelection: containerSelection$,
229
- gridContainerPosition: gridContainerPosition$
230
- }).pipe(
231
- takeUntil(destroy$),
232
- switchMap(async d => d)
233
- ).subscribe(data => {
234
- data.containerSelection
235
- .attr('transform', (d, i) => {
236
- const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
237
- const translate = gridContainerPosition.translate
238
- const scale = gridContainerPosition.scale
239
- return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
240
- })
241
- // .attr('opacity', 0)
242
- // .transition()
243
- // .attr('opacity', 1)
244
- })
245
-
246
- combineLatest({
247
- axisSelection: axisSelection$,
248
- gridAxesTransform: gridAxesTransform$,
249
- }).pipe(
250
- takeUntil(destroy$),
251
- switchMap(async d => d)
252
- ).subscribe(data => {
253
- data.axisSelection
254
- .style('transform', data.gridAxesTransform.value)
255
- // .attr('opacity', 0)
256
- // .transition()
257
- // .attr('opacity', 1)
258
-
259
- })
260
-
261
- // const gridAxesSize$ = gridAxisSizeObservable({
262
- // fullDataFormatter$,
263
- // layout$
264
- // })
265
-
266
- // const textTransform$: Observable<string> = new Observable(subscriber => {
267
- // combineLatest({
268
- // fullParams: fullParams$,
269
- // layout: layout$
270
- // }).pipe(
271
- // takeUntil(destroy$),
272
- // // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
273
- // switchMap(async (d) => d),
274
- // ).subscribe(data => {
275
-
276
- // const transformData = Object.assign({}, data.layout.content.axesTransformData)
277
-
278
- // const value = getAxesTransformValue({
279
- // translate: [0, 0],
280
- // scale: [transformData.scale[0] * -1, transformData.scale[1] * -1],
281
- // rotate: transformData.rotate * -1 + data.fullParams.tickTextRotate,
282
- // rotateX: transformData.rotateX * -1,
283
- // rotateY: transformData.rotateY * -1
284
- // })
285
-
286
- // subscriber.next(value)
287
- // })
288
- // })
289
- // const reverseTransform$: Observable<TransformData> = gridAxesTransform$.pipe(
290
- // takeUntil(destroy$),
291
- // map(d => {
292
- // const translate: [number, number] = [d.translate[0] * -1, d.translate[1] * -1]
293
- // const scale: [number, number] = [d.scale[0] * -1, d.scale[1] * -1]
294
- // const rotate = d.rotate * -1
295
- // const rotateX = d.rotateX * -1
296
- // const rotateY = d.rotateY * -1
297
- // return {
298
- // translate,
299
- // scale,
300
- // rotate,
301
- // rotateX,
302
- // rotateY,
303
- // value: ''
304
- // }
305
- // }),
306
- // )
307
- const textTransform$ = combineLatest({
308
- fullParams: fullParams$,
309
- fullDataFormatter: fullDataFormatter$,
310
- gridAxesReverseTransform: gridAxesReverseTransform$,
311
- gridContainerPosition: gridContainerPosition$
312
- }).pipe(
313
- takeUntil(destroy$),
314
- switchMap(async (d) => d),
315
- map(data => {
316
- const axisReverseTranslateValue = `translate(${data.gridAxesReverseTransform.translate[0]}px, ${data.gridAxesReverseTransform.translate[1]}px)`
317
- const axisReverseRotateValue = `rotate(${data.gridAxesReverseTransform.rotate}deg) rotateX(${data.gridAxesReverseTransform.rotateX}deg) rotateY(${data.gridAxesReverseTransform.rotateY}deg)`
318
- const containerScaleReverseScaleValue = `scale(${1 / data.gridContainerPosition[0].scale[0]}, ${1 / data.gridContainerPosition[0].scale[1]})`
319
- const tickTextRotateDeg = (data.fullDataFormatter.grid.groupAxis.position === 'left' && data.fullDataFormatter.grid.valueAxis.position === 'top')
320
- || (data.fullDataFormatter.grid.groupAxis.position === 'right' && data.fullDataFormatter.grid.valueAxis.position === 'bottom')
321
- ? data.fullParams.tickTextRotate + 180 // 修正文字倒轉
322
- : data.fullParams.tickTextRotate
323
-
324
- const textRotateValue = `rotate(${tickTextRotateDeg}deg)`
325
-
326
- // 必須按照順序(先抵消外層rotate,再抵消最外層scale,最後再做本身的rotate)
327
- return `${axisReverseTranslateValue} ${axisReverseRotateValue} ${containerScaleReverseScaleValue} ${textRotateValue}`
328
- }),
329
- distinctUntilChanged()
330
- )
331
-
332
- const minAndMax$: Observable<[number, number]> = new Observable(subscriber => {
333
- combineLatest({
334
- fullDataFormatter: fullDataFormatter$,
335
- gridAxesSize: gridAxesSize$,
336
- computedData: computedData$
337
- }).pipe(
338
- takeUntil(destroy$),
339
- switchMap(async (d) => d),
340
- ).subscribe(data => {
341
- const groupMin = 0
342
- const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
343
- const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
344
- ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
345
- : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
346
- const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'auto'
347
- ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
348
- : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
349
-
350
- const filteredData = data.computedData.map((d, i) => {
351
- return d.filter((_d, _i) => {
352
- return _i >= groupScaleDomainMin && _i <= groupScaleDomainMax
353
- })
354
- })
355
-
356
- const filteredMinAndMax = getMinAndMaxValue(filteredData.flat())
357
-
358
- subscriber.next(filteredMinAndMax)
359
- })
360
- })
361
-
362
- const valueScale$: Observable<d3.ScaleLinear<number, number>> = new Observable(subscriber => {
363
- combineLatest({
364
- fullDataFormatter: fullDataFormatter$,
365
- gridAxesSize: gridAxesSize$,
366
- minAndMax: minAndMax$
367
- }).pipe(
368
- takeUntil(destroy$),
369
- switchMap(async (d) => d),
370
- ).subscribe(data => {
371
-
372
- const valueScale: d3.ScaleLinear<number, number> = createAxisLinearScale({
373
- maxValue: data.minAndMax[1],
374
- minValue: data.minAndMax[0],
375
- axisWidth: data.gridAxesSize.height,
376
- scaleDomain: data.fullDataFormatter.grid.valueAxis.scaleDomain,
377
- scaleRange: data.fullDataFormatter.grid.valueAxis.scaleRange
378
- })
379
-
380
- subscriber.next(valueScale)
381
- })
382
- })
383
-
384
- const tickTextAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
385
- takeUntil(destroy$),
386
- map(d => {
387
- let textAnchor: 'start' | 'middle' | 'end' = 'start'
388
- let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
389
-
390
- if (d.grid.valueAxis.position === 'left') {
391
- textAnchor = 'end'
392
- dominantBaseline = 'middle'
393
- } else if (d.grid.valueAxis.position === 'right') {
394
- textAnchor = 'start'
395
- dominantBaseline = 'middle'
396
- } else if (d.grid.valueAxis.position === 'bottom') {
397
- textAnchor = 'middle'
398
- dominantBaseline = 'hanging'
399
- } else if (d.grid.valueAxis.position === 'top') {
400
- textAnchor = 'middle'
401
- dominantBaseline = 'auto'
402
- }
403
- return {
404
- textAnchor,
405
- dominantBaseline
406
- }
407
- })
408
- )
409
-
410
- const axisLabelAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
411
- takeUntil(destroy$),
412
- map(d => {
413
- let textAnchor: 'start' | 'middle' | 'end' = 'start'
414
- let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
415
-
416
- if (d.grid.groupAxis.position === 'bottom') {
417
- dominantBaseline = 'auto'
418
- } else if (d.grid.groupAxis.position === 'top') {
419
- dominantBaseline = 'hanging'
420
- } else if (d.grid.groupAxis.position === 'left') {
421
- textAnchor = 'start'
422
- } else if (d.grid.groupAxis.position === 'right') {
423
- textAnchor = 'end'
424
- }
425
- if (d.grid.valueAxis.position === 'left') {
426
- textAnchor = 'end'
427
- } else if (d.grid.valueAxis.position === 'right') {
428
- textAnchor = 'start'
429
- } else if (d.grid.valueAxis.position === 'bottom') {
430
- dominantBaseline = 'hanging'
431
- } else if (d.grid.valueAxis.position === 'top') {
432
- dominantBaseline = 'auto'
433
- }
434
- return {
435
- textAnchor,
436
- dominantBaseline
437
- }
438
- })
439
- )
440
-
441
-
442
- combineLatest({
443
- axisSelection: axisSelection$,
444
- fullParams: fullParams$,
445
- tickTextAlign: tickTextAlign$,
446
- axisLabelAlign: axisLabelAlign$,
447
- computedData: computedData$,
448
- gridAxesSize: gridAxesSize$,
449
- fullDataFormatter: fullDataFormatter$,
450
- fullChartParams: fullChartParams$,
451
- valueScale: valueScale$,
452
- textTransform: textTransform$,
453
- minAndMax: minAndMax$
454
- }).pipe(
455
- takeUntil(destroy$),
456
- switchMap(async (d) => d),
457
- ).subscribe(data => {
458
-
459
- renderAxis({
460
- selection: data.axisSelection,
461
- yAxisClassName,
462
- textClassName,
463
- fullParams: data.fullParams,
464
- tickTextAlign: data.tickTextAlign,
465
- axisLabelAlign: data.axisLabelAlign,
466
- gridAxesSize: data.gridAxesSize,
467
- fullDataFormatter: data.fullDataFormatter,
468
- fullChartParams: data.fullChartParams,
469
- valueScale: data.valueScale,
470
- textTransform: data.textTransform,
471
- minAndMax: data.minAndMax
472
- })
473
- })
474
-
475
- return () => {
476
- destroy$.next(undefined)
477
- }
478
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ switchMap,
5
+ distinctUntilChanged,
6
+ first,
7
+ map,
8
+ takeUntil,
9
+ Observable,
10
+ Subject } from 'rxjs'
11
+ import { createAxisLinearScale } from '@orbcharts/core'
12
+ import type { BasePluginFn } from './types'
13
+ import type {
14
+ ComputedDataGrid,
15
+ DataFormatterGrid,
16
+ ChartParams,
17
+ ComputedDatumGrid,
18
+ GridContainerPosition,
19
+ TransformData,
20
+ EventGrid,
21
+ ColorType } from '@orbcharts/core'
22
+ import { parseTickFormatValue } from '../utils/d3Utils'
23
+ import { getColor, getMinAndMaxValue, getClassName, getUniID } from '../utils/orbchartsUtils'
24
+
25
+ export interface BaseValueAxisParams {
26
+ labelOffset: [number, number]
27
+ labelColorType: ColorType
28
+ axisLineVisible: boolean
29
+ axisLineColorType: ColorType
30
+ ticks: number
31
+ tickFormat: string | ((text: d3.NumberValue) => string)
32
+ tickLineVisible: boolean
33
+ tickPadding: number
34
+ tickFullLine: boolean
35
+ tickFullLineDasharray: string
36
+ tickColorType: ColorType
37
+ tickTextRotate: number
38
+ tickTextColorType: ColorType
39
+ }
40
+
41
+ interface BaseLinesContext {
42
+ selection: d3.Selection<any, unknown, any, unknown>
43
+ computedData$: Observable<ComputedDataGrid>
44
+ fullParams$: Observable<BaseValueAxisParams>
45
+ fullDataFormatter$: Observable<DataFormatterGrid>
46
+ fullChartParams$: Observable<ChartParams>
47
+ gridAxesTransform$: Observable<TransformData>
48
+ gridAxesReverseTransform$: Observable<TransformData>
49
+ gridAxesSize$: Observable<{
50
+ width: number;
51
+ height: number;
52
+ }>
53
+ gridContainerPosition$: Observable<GridContainerPosition[]>
54
+ isSeriesSeprate$: Observable<boolean>
55
+ }
56
+
57
+ interface TextAlign {
58
+ textAnchor: "start" | "middle" | "end"
59
+ dominantBaseline: "middle" | "auto" | "hanging"
60
+ }
61
+
62
+ // const pluginName = 'ValueAxis'
63
+ // const containerClassName = getClassName(pluginName, 'container')
64
+ // const yAxisGClassName = getClassName(pluginName, 'yAxisG')
65
+ // const yAxisClassName = getClassName(pluginName, 'yAxis')
66
+ // const textClassName = getClassName(pluginName, 'text')
67
+ const defaultTickSize = 6
68
+
69
+ function renderAxis ({ selection, yAxisClassName, textClassName, fullParams, tickTextAlign, axisLabelAlign, gridAxesSize, fullDataFormatter, fullChartParams, valueScale, textTransform, minAndMax }: {
70
+ selection: d3.Selection<SVGGElement, any, any, any>,
71
+ yAxisClassName: string
72
+ textClassName: string
73
+ fullParams: BaseValueAxisParams
74
+ tickTextAlign: TextAlign
75
+ axisLabelAlign: TextAlign
76
+ gridAxesSize: { width: number, height: number }
77
+ fullDataFormatter: DataFormatterGrid,
78
+ fullChartParams: ChartParams
79
+ valueScale: d3.ScaleLinear<number, number>
80
+ textTransform: string,
81
+ minAndMax: [number, number]
82
+ }) {
83
+
84
+ const yAxisSelection = selection
85
+ .selectAll<SVGGElement, BaseValueAxisParams>(`g.${yAxisClassName}`)
86
+ .data([fullParams])
87
+ .join('g')
88
+ .classed(yAxisClassName, true)
89
+
90
+ const axisLabelSelection = selection
91
+ .selectAll<SVGGElement, BaseValueAxisParams>(`g.${textClassName}`)
92
+ .data([fullParams])
93
+ .join('g')
94
+ .classed(textClassName, true)
95
+ .each((d, i, g) => {
96
+ const text = d3.select(g[i])
97
+ .selectAll<SVGTextElement, BaseValueAxisParams>(`text`)
98
+ .data([d])
99
+ .join(
100
+ enter => {
101
+ return enter
102
+ .append('text')
103
+ .style('font-weight', 'bold')
104
+ },
105
+ update => update,
106
+ exit => exit.remove()
107
+ )
108
+ .attr('text-anchor', axisLabelAlign.textAnchor)
109
+ .attr('dominant-baseline', axisLabelAlign.dominantBaseline)
110
+ .attr('font-size', fullChartParams.styles.textSize)
111
+ .style('fill', getColor(fullParams.labelColorType, fullChartParams))
112
+ // .style('transform', textTransform)
113
+ .text(d => fullDataFormatter.grid.valueAxis.label)
114
+ })
115
+ .attr('transform', d => `translate(${- d.tickPadding + fullParams.labelOffset[0]}, ${gridAxesSize.height + d.tickPadding + fullParams.labelOffset[1]})`)
116
+
117
+ const valueLength = minAndMax[1] - minAndMax[0]
118
+
119
+ // const _valueScale = d3.scaleLinear()
120
+ // .domain([0, 150])
121
+ // .range([416.5, 791.349])
122
+
123
+ // 設定Y軸刻度
124
+ const yAxis = d3.axisLeft(valueScale)
125
+ .scale(valueScale)
126
+ .ticks(valueLength > fullParams.ticks
127
+ ? fullParams.ticks
128
+ : ((minAndMax[0] === 0 && minAndMax[1] === 0)
129
+ ? 1
130
+ : Math.ceil(valueLength))) // 刻度分段數量
131
+ .tickFormat(d => parseTickFormatValue(d, fullParams.tickFormat))
132
+ .tickSize(fullParams.tickFullLine == true
133
+ ? -gridAxesSize.width
134
+ : defaultTickSize)
135
+ .tickPadding(fullParams.tickPadding)
136
+
137
+ const yAxisEl = yAxisSelection
138
+ .transition()
139
+ .duration(100)
140
+ .call(yAxis)
141
+
142
+ yAxisEl.selectAll('line')
143
+ .style('fill', 'none')
144
+ .style('stroke', fullParams.tickLineVisible == true ? getColor(fullParams.tickColorType, fullChartParams) : 'none')
145
+ .style('stroke-dasharray', fullParams.tickFullLineDasharray)
146
+ .attr('pointer-events', 'none')
147
+
148
+ yAxisEl.selectAll('path')
149
+ .style('fill', 'none')
150
+ // .style('stroke', this.fullParams.axisLineColor!)
151
+ .style('stroke', fullParams.axisLineVisible == true ? getColor(fullParams.axisLineColorType, fullChartParams) : 'none')
152
+ .style('shape-rendering', 'crispEdges')
153
+
154
+ // const yText = yAxisEl.selectAll('text')
155
+ const yText = yAxisSelection.selectAll('text')
156
+ // .style('font-family', 'sans-serif')
157
+ .attr('font-size', fullChartParams.styles.textSize)
158
+ .style('color', getColor(fullParams.tickTextColorType, fullChartParams))
159
+ .attr('text-anchor', tickTextAlign.textAnchor)
160
+ .attr('dominant-baseline', tickTextAlign.dominantBaseline)
161
+ .attr('transform-origin', `-${fullParams.tickPadding + defaultTickSize} 0`)
162
+ yText.style('transform', textTransform)
163
+
164
+ return yAxisSelection
165
+ }
166
+
167
+
168
+
169
+ export const createBaseValueAxis: BasePluginFn<BaseLinesContext> = (pluginName: string, {
170
+ selection,
171
+ computedData$,
172
+ fullParams$,
173
+ fullDataFormatter$,
174
+ fullChartParams$,
175
+ gridAxesTransform$,
176
+ gridAxesReverseTransform$,
177
+ gridAxesSize$,
178
+ gridContainerPosition$,
179
+ isSeriesSeprate$,
180
+ }) => {
181
+
182
+ const destroy$ = new Subject()
183
+
184
+ const containerClassName = getClassName(pluginName, 'container')
185
+ const yAxisGClassName = getClassName(pluginName, 'yAxisG')
186
+ const yAxisClassName = getClassName(pluginName, 'yAxis')
187
+ const textClassName = getClassName(pluginName, 'text')
188
+
189
+ const containerSelection$ = combineLatest({
190
+ computedData: computedData$.pipe(
191
+ distinctUntilChanged((a, b) => {
192
+ // 只有當series的數量改變時,才重新計算
193
+ return a.length === b.length
194
+ }),
195
+ ),
196
+ isSeriesSeprate: isSeriesSeprate$
197
+ }).pipe(
198
+ takeUntil(destroy$),
199
+ switchMap(async (d) => d),
200
+ map(data => {
201
+ return data.isSeriesSeprate
202
+ // series分開的時候顯示各別axis
203
+ ? data.computedData
204
+ // series合併的時候只顯示第一個axis
205
+ : [data.computedData[0]]
206
+ }),
207
+ map((computedData, i) => {
208
+ return selection
209
+ .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${containerClassName}`)
210
+ .data(computedData, d => d[0] ? d[0].seriesIndex : i)
211
+ .join('g')
212
+ .classed(containerClassName, true)
213
+ })
214
+ )
215
+
216
+ const axisSelection$ = containerSelection$.pipe(
217
+ takeUntil(destroy$),
218
+ map((containerSelection, i) => {
219
+ return containerSelection
220
+ .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${yAxisGClassName}`)
221
+ .data([yAxisGClassName])
222
+ .join('g')
223
+ .classed(yAxisGClassName, true)
224
+ })
225
+ )
226
+
227
+ combineLatest({
228
+ containerSelection: containerSelection$,
229
+ gridContainerPosition: gridContainerPosition$
230
+ }).pipe(
231
+ takeUntil(destroy$),
232
+ switchMap(async d => d)
233
+ ).subscribe(data => {
234
+ data.containerSelection
235
+ .attr('transform', (d, i) => {
236
+ const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
237
+ const translate = gridContainerPosition.translate
238
+ const scale = gridContainerPosition.scale
239
+ return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
240
+ })
241
+ // .attr('opacity', 0)
242
+ // .transition()
243
+ // .attr('opacity', 1)
244
+ })
245
+
246
+ combineLatest({
247
+ axisSelection: axisSelection$,
248
+ gridAxesTransform: gridAxesTransform$,
249
+ }).pipe(
250
+ takeUntil(destroy$),
251
+ switchMap(async d => d)
252
+ ).subscribe(data => {
253
+ data.axisSelection
254
+ .style('transform', data.gridAxesTransform.value)
255
+ // .attr('opacity', 0)
256
+ // .transition()
257
+ // .attr('opacity', 1)
258
+
259
+ })
260
+
261
+ // const gridAxesSize$ = gridAxisSizeObservable({
262
+ // fullDataFormatter$,
263
+ // layout$
264
+ // })
265
+
266
+ // const textTransform$: Observable<string> = new Observable(subscriber => {
267
+ // combineLatest({
268
+ // fullParams: fullParams$,
269
+ // layout: layout$
270
+ // }).pipe(
271
+ // takeUntil(destroy$),
272
+ // // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
273
+ // switchMap(async (d) => d),
274
+ // ).subscribe(data => {
275
+
276
+ // const transformData = Object.assign({}, data.layout.content.axesTransformData)
277
+
278
+ // const value = getAxesTransformValue({
279
+ // translate: [0, 0],
280
+ // scale: [transformData.scale[0] * -1, transformData.scale[1] * -1],
281
+ // rotate: transformData.rotate * -1 + data.fullParams.tickTextRotate,
282
+ // rotateX: transformData.rotateX * -1,
283
+ // rotateY: transformData.rotateY * -1
284
+ // })
285
+
286
+ // subscriber.next(value)
287
+ // })
288
+ // })
289
+ // const reverseTransform$: Observable<TransformData> = gridAxesTransform$.pipe(
290
+ // takeUntil(destroy$),
291
+ // map(d => {
292
+ // const translate: [number, number] = [d.translate[0] * -1, d.translate[1] * -1]
293
+ // const scale: [number, number] = [d.scale[0] * -1, d.scale[1] * -1]
294
+ // const rotate = d.rotate * -1
295
+ // const rotateX = d.rotateX * -1
296
+ // const rotateY = d.rotateY * -1
297
+ // return {
298
+ // translate,
299
+ // scale,
300
+ // rotate,
301
+ // rotateX,
302
+ // rotateY,
303
+ // value: ''
304
+ // }
305
+ // }),
306
+ // )
307
+ const textTransform$ = combineLatest({
308
+ fullParams: fullParams$,
309
+ fullDataFormatter: fullDataFormatter$,
310
+ gridAxesReverseTransform: gridAxesReverseTransform$,
311
+ gridContainerPosition: gridContainerPosition$
312
+ }).pipe(
313
+ takeUntil(destroy$),
314
+ switchMap(async (d) => d),
315
+ map(data => {
316
+ const axisReverseTranslateValue = `translate(${data.gridAxesReverseTransform.translate[0]}px, ${data.gridAxesReverseTransform.translate[1]}px)`
317
+ const axisReverseRotateValue = `rotate(${data.gridAxesReverseTransform.rotate}deg) rotateX(${data.gridAxesReverseTransform.rotateX}deg) rotateY(${data.gridAxesReverseTransform.rotateY}deg)`
318
+ const containerScaleReverseScaleValue = `scale(${1 / data.gridContainerPosition[0].scale[0]}, ${1 / data.gridContainerPosition[0].scale[1]})`
319
+ const tickTextRotateDeg = (data.fullDataFormatter.grid.groupAxis.position === 'left' && data.fullDataFormatter.grid.valueAxis.position === 'top')
320
+ || (data.fullDataFormatter.grid.groupAxis.position === 'right' && data.fullDataFormatter.grid.valueAxis.position === 'bottom')
321
+ ? data.fullParams.tickTextRotate + 180 // 修正文字倒轉
322
+ : data.fullParams.tickTextRotate
323
+
324
+ const textRotateValue = `rotate(${tickTextRotateDeg}deg)`
325
+
326
+ // 必須按照順序(先抵消外層rotate,再抵消最外層scale,最後再做本身的rotate)
327
+ return `${axisReverseTranslateValue} ${axisReverseRotateValue} ${containerScaleReverseScaleValue} ${textRotateValue}`
328
+ }),
329
+ distinctUntilChanged()
330
+ )
331
+
332
+ const minAndMax$: Observable<[number, number]> = new Observable(subscriber => {
333
+ combineLatest({
334
+ fullDataFormatter: fullDataFormatter$,
335
+ gridAxesSize: gridAxesSize$,
336
+ computedData: computedData$
337
+ }).pipe(
338
+ takeUntil(destroy$),
339
+ switchMap(async (d) => d),
340
+ ).subscribe(data => {
341
+ const groupMin = 0
342
+ const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
343
+ const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
344
+ ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
345
+ : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
346
+ const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'auto'
347
+ ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
348
+ : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
349
+
350
+ const filteredData = data.computedData.map((d, i) => {
351
+ return d.filter((_d, _i) => {
352
+ return _i >= groupScaleDomainMin && _i <= groupScaleDomainMax
353
+ })
354
+ })
355
+
356
+ const filteredMinAndMax = getMinAndMaxValue(filteredData.flat())
357
+ if (filteredMinAndMax[0] === filteredMinAndMax[1]) {
358
+ filteredMinAndMax[0] = filteredMinAndMax[1] - 1 // 避免最大及最小值相同造成無法計算scale
359
+ }
360
+ subscriber.next(filteredMinAndMax)
361
+ })
362
+ })
363
+
364
+ const valueScale$: Observable<d3.ScaleLinear<number, number>> = new Observable(subscriber => {
365
+ combineLatest({
366
+ fullDataFormatter: fullDataFormatter$,
367
+ gridAxesSize: gridAxesSize$,
368
+ minAndMax: minAndMax$
369
+ }).pipe(
370
+ takeUntil(destroy$),
371
+ switchMap(async (d) => d),
372
+ ).subscribe(data => {
373
+
374
+ const valueScale: d3.ScaleLinear<number, number> = createAxisLinearScale({
375
+ maxValue: data.minAndMax[1],
376
+ minValue: data.minAndMax[0],
377
+ axisWidth: data.gridAxesSize.height,
378
+ scaleDomain: data.fullDataFormatter.grid.valueAxis.scaleDomain,
379
+ scaleRange: data.fullDataFormatter.grid.valueAxis.scaleRange
380
+ })
381
+
382
+ subscriber.next(valueScale)
383
+ })
384
+ })
385
+
386
+ const tickTextAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
387
+ takeUntil(destroy$),
388
+ map(d => {
389
+ let textAnchor: 'start' | 'middle' | 'end' = 'start'
390
+ let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
391
+
392
+ if (d.grid.valueAxis.position === 'left') {
393
+ textAnchor = 'end'
394
+ dominantBaseline = 'middle'
395
+ } else if (d.grid.valueAxis.position === 'right') {
396
+ textAnchor = 'start'
397
+ dominantBaseline = 'middle'
398
+ } else if (d.grid.valueAxis.position === 'bottom') {
399
+ textAnchor = 'middle'
400
+ dominantBaseline = 'hanging'
401
+ } else if (d.grid.valueAxis.position === 'top') {
402
+ textAnchor = 'middle'
403
+ dominantBaseline = 'auto'
404
+ }
405
+ return {
406
+ textAnchor,
407
+ dominantBaseline
408
+ }
409
+ })
410
+ )
411
+
412
+ const axisLabelAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
413
+ takeUntil(destroy$),
414
+ map(d => {
415
+ let textAnchor: 'start' | 'middle' | 'end' = 'start'
416
+ let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
417
+
418
+ if (d.grid.groupAxis.position === 'bottom') {
419
+ dominantBaseline = 'auto'
420
+ } else if (d.grid.groupAxis.position === 'top') {
421
+ dominantBaseline = 'hanging'
422
+ } else if (d.grid.groupAxis.position === 'left') {
423
+ textAnchor = 'start'
424
+ } else if (d.grid.groupAxis.position === 'right') {
425
+ textAnchor = 'end'
426
+ }
427
+ if (d.grid.valueAxis.position === 'left') {
428
+ textAnchor = 'end'
429
+ } else if (d.grid.valueAxis.position === 'right') {
430
+ textAnchor = 'start'
431
+ } else if (d.grid.valueAxis.position === 'bottom') {
432
+ dominantBaseline = 'hanging'
433
+ } else if (d.grid.valueAxis.position === 'top') {
434
+ dominantBaseline = 'auto'
435
+ }
436
+ return {
437
+ textAnchor,
438
+ dominantBaseline
439
+ }
440
+ })
441
+ )
442
+
443
+
444
+ combineLatest({
445
+ axisSelection: axisSelection$,
446
+ fullParams: fullParams$,
447
+ tickTextAlign: tickTextAlign$,
448
+ axisLabelAlign: axisLabelAlign$,
449
+ computedData: computedData$,
450
+ gridAxesSize: gridAxesSize$,
451
+ fullDataFormatter: fullDataFormatter$,
452
+ fullChartParams: fullChartParams$,
453
+ valueScale: valueScale$,
454
+ textTransform: textTransform$,
455
+ minAndMax: minAndMax$
456
+ }).pipe(
457
+ takeUntil(destroy$),
458
+ switchMap(async (d) => d),
459
+ ).subscribe(data => {
460
+
461
+ renderAxis({
462
+ selection: data.axisSelection,
463
+ yAxisClassName,
464
+ textClassName,
465
+ fullParams: data.fullParams,
466
+ tickTextAlign: data.tickTextAlign,
467
+ axisLabelAlign: data.axisLabelAlign,
468
+ gridAxesSize: data.gridAxesSize,
469
+ fullDataFormatter: data.fullDataFormatter,
470
+ fullChartParams: data.fullChartParams,
471
+ valueScale: data.valueScale,
472
+ textTransform: data.textTransform,
473
+ minAndMax: data.minAndMax
474
+ })
475
+ })
476
+
477
+ return () => {
478
+ destroy$.next(undefined)
479
+ }
480
+ }