@orbcharts/plugins-basic 3.0.0-alpha.68 → 3.0.0-alpha.70

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