@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,653 +1,676 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- switchMap,
5
- distinctUntilChanged,
6
- of,
7
- first,
8
- map,
9
- takeUntil,
10
- shareReplay,
11
- Observable,
12
- Subject } from 'rxjs'
13
- import type { ColorType, ComputedDataGrid, GridContainerPosition } from '@orbcharts/core'
14
- import type { BasePluginFn } from './types'
15
- import type {
16
- ComputedDatumGrid,
17
- DataFormatterGrid,
18
- ChartParams,
19
- TransformData } from '@orbcharts/core'
20
- import { parseTickFormatValue } from '../utils/d3Utils'
21
- import { getColor, getClassName } from '../utils/orbchartsUtils'
22
-
23
- export interface BaseGroupAxisParams {
24
- // xLabel: string
25
- // labelAnchor: 'start' | 'end'
26
- labelOffset: [number, number]
27
- labelColorType: ColorType
28
- axisLineVisible: boolean
29
- axisLineColorType: ColorType
30
- ticks: number | null | 'all'
31
- tickFormat: string | ((text: any) => string)
32
- tickLineVisible: boolean
33
- tickPadding: number
34
- tickFullLine: boolean
35
- tickFullLineDasharray: string
36
- tickColorType: ColorType
37
- // axisLineColor: string
38
- // axisLabelColor: string
39
- tickTextRotate: number
40
- tickTextColorType: ColorType
41
- }
42
-
43
- interface BaseGroupAxisContext {
44
- selection: d3.Selection<any, unknown, any, unknown>
45
- computedData$: Observable<ComputedDataGrid>
46
- fullParams$: Observable<BaseGroupAxisParams>
47
- fullDataFormatter$: Observable<DataFormatterGrid>
48
- fullChartParams$: Observable<ChartParams>
49
- gridAxesTransform$: Observable<TransformData>
50
- gridAxesReverseTransform$: Observable<TransformData>
51
- gridAxesSize$: Observable<{
52
- width: number;
53
- height: number;
54
- }>
55
- gridContainerPosition$: Observable<GridContainerPosition[]>
56
- isSeriesSeprate$: Observable<boolean>
57
- textSizePx$: Observable<number>
58
- }
59
-
60
- interface TextAlign {
61
- textAnchor: "start" | "middle" | "end"
62
- dominantBaseline: "middle" | "auto" | "hanging"
63
- }
64
-
65
- // const pluginName = 'GroupAxis'
66
- // const containerClassName = getClassName(pluginName, 'container')
67
- // const xAxisGClassName = getClassName(pluginName, 'xAxisG')
68
- // const xAxisClassName = getClassName(pluginName, 'xAxis')
69
- // const groupingLabelClassName = getClassName(pluginName, 'groupingLabel')
70
- const defaultTickSize = 6
71
-
72
- function renderAxisLabel ({ selection, groupingLabelClassName, fullParams, axisLabelAlign, gridAxesSize, fullDataFormatter, chartParams, textReverseTransform }: {
73
- selection: d3.Selection<SVGGElement, any, any, any>,
74
- groupingLabelClassName: string
75
- fullParams: BaseGroupAxisParams
76
- axisLabelAlign: TextAlign
77
- gridAxesSize: { width: number, height: number }
78
- fullDataFormatter: DataFormatterGrid,
79
- chartParams: ChartParams
80
- textReverseTransform: string
81
- }) {
82
-
83
- const offsetX = fullParams.tickPadding + fullParams.labelOffset[0]
84
- const offsetY = fullParams.tickPadding + fullParams.labelOffset[1]
85
- let labelX = 0
86
- let labelY = 0
87
- if (fullDataFormatter.grid.groupAxis.position === 'bottom') {
88
- labelY = offsetY
89
- if (fullDataFormatter.grid.valueAxis.position === 'left') {
90
- labelX = offsetX
91
- } else if (fullDataFormatter.grid.valueAxis.position === 'right') {
92
- labelX = - offsetX
93
- }
94
- } else if (fullDataFormatter.grid.groupAxis.position === 'top') {
95
- labelY = - offsetY
96
- if (fullDataFormatter.grid.valueAxis.position === 'left') {
97
- labelX = offsetX
98
- } else if (fullDataFormatter.grid.valueAxis.position === 'right') {
99
- labelX = - offsetX
100
- }
101
- } else if (fullDataFormatter.grid.groupAxis.position === 'left') {
102
- labelX = - offsetX
103
- if (fullDataFormatter.grid.valueAxis.position === 'bottom') {
104
- labelY = - offsetY
105
- } else if (fullDataFormatter.grid.valueAxis.position === 'top') {
106
- labelY = offsetY
107
- }
108
- } else if (fullDataFormatter.grid.groupAxis.position === 'right') {
109
- labelX = offsetX
110
- if (fullDataFormatter.grid.valueAxis.position === 'bottom') {
111
- labelY = - offsetY
112
- } else if (fullDataFormatter.grid.valueAxis.position === 'top') {
113
- labelY = offsetY
114
- }
115
- }
116
-
117
- const axisLabelSelection = selection
118
- .selectAll<SVGGElement, BaseGroupAxisParams>(`g.${groupingLabelClassName}`)
119
- .data([fullParams])
120
- .join('g')
121
- .classed(groupingLabelClassName, true)
122
- .each((d, i, g) => {
123
- const text = d3.select(g[i])
124
- .selectAll<SVGTextElement, BaseGroupAxisParams>('text')
125
- .data([d])
126
- .join(
127
- enter => {
128
- return enter
129
- .append('text')
130
- .style('font-weight', 'bold')
131
- },
132
- update => update,
133
- exit => exit.remove()
134
- )
135
- .attr('text-anchor', axisLabelAlign.textAnchor)
136
- .attr('dominant-baseline', axisLabelAlign.dominantBaseline)
137
- .attr('font-size', chartParams.styles.textSize)
138
- .style('fill', getColor(fullParams.labelColorType, chartParams))
139
- .style('transform', textReverseTransform)
140
- // 偏移使用 x, y 而非 transform 才不會受到外層 scale 變形影響
141
- .attr('x', labelX)
142
- .attr('y', labelY)
143
- .text(d => fullDataFormatter.grid.groupAxis.label)
144
- })
145
- .attr('transform', d => `translate(${gridAxesSize.width}, 0)`)
146
- // .attr('transform', d => `translate(${gridAxesSize.width + d.tickPadding + fullParams.labelOffset[0]}, ${- d.tickPadding - fullParams.labelOffset[1]})`)
147
-
148
- }
149
-
150
- function renderAxis ({ selection, xAxisClassName, fullParams, tickTextAlign, gridAxesSize, fullDataFormatter, chartParams, groupScale, groupScaleDomain, groupLabels, textReverseTransformWithRotate, textSizePx }: {
151
- selection: d3.Selection<SVGGElement, any, any, any>,
152
- xAxisClassName: string
153
- fullParams: BaseGroupAxisParams
154
- tickTextAlign: TextAlign
155
- gridAxesSize: { width: number, height: number }
156
- fullDataFormatter: DataFormatterGrid,
157
- chartParams: ChartParams
158
- groupScale: d3.ScaleLinear<number, number>
159
- groupScaleDomain: number[]
160
- groupLabels: string[]
161
- textReverseTransformWithRotate: string
162
- textSizePx: number
163
- }) {
164
-
165
- const xAxisSelection = selection
166
- .selectAll<SVGGElement, BaseGroupAxisParams>(`g.${xAxisClassName}`)
167
- .data([fullParams])
168
- .join('g')
169
- .classed(xAxisClassName, true)
170
-
171
- // 計算所有範圍內groupLabels數量(顯示所有刻度)
172
- const allTicksAmount = Math.floor(groupScaleDomain[1]) - Math.ceil(groupScaleDomain[0]) + 1
173
-
174
- // 刻度文字偏移
175
- let tickPadding = 0
176
- let textX = 0
177
- if (fullDataFormatter.grid.groupAxis.position === 'left') {
178
- tickPadding = 0
179
- textX = - fullParams.tickPadding
180
- } else if (fullDataFormatter.grid.groupAxis.position === 'right') {
181
- tickPadding = 0
182
- textX = fullParams.tickPadding
183
- } else if (fullDataFormatter.grid.groupAxis.position === 'bottom') {
184
- tickPadding = fullParams.tickPadding
185
- textX = 0
186
- } else if (fullDataFormatter.grid.groupAxis.position === 'top') {
187
- tickPadding = - fullParams.tickPadding
188
- textX = - 0
189
- }
190
-
191
- // 設定X軸刻度
192
- const xAxis = d3.axisBottom(groupScale)
193
- .scale(groupScale)
194
- .ticks(fullParams.ticks === 'all'
195
- ? allTicksAmount
196
- : fullParams.ticks > allTicksAmount
197
- ? allTicksAmount // 不顯示超過groupLabels數量的刻度
198
- : fullParams.ticks)
199
- .tickSize(fullParams.tickFullLine == true
200
- ? gridAxesSize.height
201
- : - defaultTickSize)
202
- .tickSizeOuter(0)
203
- .tickFormat((groupIndex: number) => {
204
- // 用index對應到groupLabel
205
- const groupLabel = groupLabels[groupIndex] ?? '' // 非整數index不顯示
206
- return parseTickFormatValue(groupLabel, fullParams.tickFormat)
207
- })
208
- .tickPadding(tickPadding)
209
-
210
- const xAxisEl = xAxisSelection
211
- .transition()
212
- .duration(100)
213
- .call(xAxis)
214
- .on('end', (self, t) => {
215
- // 先等transition結束再處理文字,否則會被原本的文字覆蓋
216
- xAxisEl
217
- .selectAll('.tick text')
218
- .each((groupIndex: number, i, n) => {
219
- // -- 將原本單行文字改為多行文字 --
220
- const groupLabel = groupLabels[groupIndex] ?? '' // 非整數index不顯示
221
- const groupLabelTspans = groupLabel.split('\n')
222
- const textSelection = d3.select(n[i])
223
- .text(null) // 先清空原本的 text
224
-
225
- const textX = Number(textSelection.attr('x'))
226
- let textY = Number(textSelection.attr('y'))
227
- if (fullDataFormatter.grid.groupAxis.position === 'top') {
228
- // 當文字在上方時,要往上偏移第一行的高度
229
- textY -= (groupLabelTspans.length - 1) * textSizePx
230
- }
231
-
232
- textSelection
233
- .selectAll('tspan')
234
- .data(groupLabelTspans)
235
- .join('tspan')
236
- .attr('x', textX)
237
- .attr('y', (_d, _i) => textY + _i * textSizePx)
238
- .text(d => d)
239
- })
240
- })
241
-
242
- xAxisEl.selectAll('line')
243
- .style('fill', 'none')
244
- .style('stroke', fullParams.tickLineVisible == true ? getColor(fullParams.tickColorType, chartParams) : 'none')
245
- .style('stroke-dasharray', fullParams.tickFullLineDasharray)
246
- .attr('pointer-events', 'none')
247
-
248
- xAxisEl.selectAll('path')
249
- .style('fill', 'none')
250
- .style('stroke', fullParams.axisLineVisible == true ? getColor(fullParams.axisLineColorType, chartParams) : 'none')
251
- .style('shape-rendering', 'crispEdges')
252
-
253
- const xText = xAxisSelection.selectAll<SVGTextElement, BaseGroupAxisParams>('text')
254
- // .style('font-family', 'sans-serif')
255
- .attr('font-size', chartParams.styles.textSize)
256
- // .style('font-weight', 'bold')
257
- .style('color', getColor(fullParams.tickTextColorType, chartParams))
258
- .attr('text-anchor', tickTextAlign.textAnchor)
259
- .attr('dominant-baseline', tickTextAlign.dominantBaseline)
260
- .attr('x', textX)
261
- .style('transform', textReverseTransformWithRotate)
262
-
263
- // 抵消掉預設的偏移
264
- // if (fullDataFormatter.grid.groupAxis.position === 'left' || fullDataFormatter.grid.groupAxis.position === 'right') {
265
- xText.attr('dy', 0)
266
- // }
267
-
268
- return xAxisSelection
269
- }
270
-
271
-
272
- export const createBaseGroupAxis: BasePluginFn<BaseGroupAxisContext> = ((pluginName: string, {
273
- selection,
274
- computedData$,
275
- fullParams$,
276
- fullDataFormatter$,
277
- fullChartParams$,
278
- gridAxesTransform$,
279
- gridAxesReverseTransform$,
280
- gridAxesSize$,
281
- gridContainerPosition$,
282
- isSeriesSeprate$,
283
- textSizePx$,
284
- }) => {
285
-
286
- const destroy$ = new Subject()
287
-
288
- const containerClassName = getClassName(pluginName, 'container')
289
- const xAxisGClassName = getClassName(pluginName, 'xAxisG')
290
- const xAxisClassName = getClassName(pluginName, 'xAxis')
291
- const groupingLabelClassName = getClassName(pluginName, 'groupingLabel')
292
-
293
- const containerSelection$ = combineLatest({
294
- computedData: computedData$.pipe(
295
- distinctUntilChanged((a, b) => {
296
- // 只有當series的數量改變時,才重新計算
297
- return a.length === b.length
298
- }),
299
- ),
300
- isSeriesSeprate: isSeriesSeprate$
301
- }).pipe(
302
- takeUntil(destroy$),
303
- switchMap(async (d) => d),
304
- map(data => {
305
- return data.isSeriesSeprate
306
- // series分開的時候顯示各別axis
307
- ? data.computedData
308
- // series合併的時候只顯示第一個axis
309
- : [data.computedData[0]]
310
- }),
311
- map((computedData, i) => {
312
- return selection
313
- .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${containerClassName}`)
314
- .data(computedData, d => d[0] ? d[0].seriesIndex : i)
315
- .join('g')
316
- .classed(containerClassName, true)
317
- })
318
- )
319
-
320
- const axisSelection$ = containerSelection$.pipe(
321
- takeUntil(destroy$),
322
- map((containerSelection, i) => {
323
- return containerSelection
324
- .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${xAxisGClassName}`)
325
- .data([xAxisGClassName])
326
- .join('g')
327
- .classed(xAxisGClassName, true)
328
- })
329
- )
330
-
331
- combineLatest({
332
- containerSelection: containerSelection$,
333
- gridContainerPosition: gridContainerPosition$
334
- }).pipe(
335
- takeUntil(destroy$),
336
- switchMap(async d => d)
337
- ).subscribe(data => {
338
- data.containerSelection
339
- .attr('transform', (d, i) => {
340
- const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
341
- const translate = gridContainerPosition.translate
342
- const scale = gridContainerPosition.scale
343
- return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
344
- })
345
- // .attr('opacity', 0)
346
- // .transition()
347
- // .attr('opacity', 1)
348
- })
349
-
350
- combineLatest({
351
- axisSelection: axisSelection$,
352
- gridAxesTransform: gridAxesTransform$,
353
- }).pipe(
354
- takeUntil(destroy$),
355
- switchMap(async d => d)
356
- ).subscribe(data => {
357
- data.axisSelection
358
- .style('transform', data.gridAxesTransform.value)
359
- // .attr('opacity', 0)
360
- // .transition()
361
- // .attr('opacity', 1)
362
-
363
- })
364
-
365
-
366
- // const gridAxesSize$ = gridAxisSizeObservable({
367
- // fullDataFormatter$,
368
- // layout$
369
- // })
370
-
371
-
372
- // const tickTextFormatter$ = fullDataFormatter$
373
- // .pipe(
374
- // map(d => {
375
- // return d.grid.seriesDirection === 'row' ? d.grid.columnLabelFormat : d.grid.rowLabelFormat
376
- // })
377
- // )
378
-
379
- // const textReverseTransform$: Observable<string> = new Observable(subscriber => {
380
- // combineLatest({
381
- // params: fullParams$,
382
- // gridAxesTransform: gridAxesTransform$
383
- // }).pipe(
384
- // takeUntil(destroy$),
385
- // // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
386
- // switchMap(async (d) => d),
387
- // ).subscribe(data => {
388
-
389
- // const transformData = Object.assign({}, data.gridAxesTransform)
390
-
391
- // const value = getAxesTransformValue({
392
- // translate: [0, 0],
393
- // scale: [transformData.scale[0] * -1, transformData.scale[1] * -1],
394
- // rotate: transformData.rotate * -1 + data.params.tickTextRotate,
395
- // rotateX: transformData.rotateX * -1,
396
- // rotateY: transformData.rotateY * -1
397
- // })
398
-
399
- // subscriber.next(value)
400
- // })
401
- // })
402
- // const reverseTransform$: Observable<TransformData> = gridAxesTransform$.pipe(
403
- // takeUntil(destroy$),
404
- // map(d => {
405
- // const translate: [number, number] = [d.translate[0] * -1, d.translate[1] * -1]
406
- // const scale: [number, number] = [d.scale[0] * -1, d.scale[1] * -1]
407
- // const rotate = d.rotate * -1
408
- // const rotateX = d.rotateX * -1
409
- // const rotateY = d.rotateY * -1
410
- // return {
411
- // translate,
412
- // scale,
413
- // rotate,
414
- // rotateX,
415
- // rotateY,
416
- // value: ''
417
- // }
418
- // }),
419
- // )
420
- const textReverseTransform$ = combineLatest({
421
- gridAxesReverseTransform: gridAxesReverseTransform$,
422
- gridContainerPosition: gridContainerPosition$
423
- }).pipe(
424
- takeUntil(destroy$),
425
- switchMap(async (d) => d),
426
- map(data => {
427
- // const axisReverseTranslateValue = `translate(${data.gridAxesReverseTransform.translate[0]}px, ${data.gridAxesReverseTransform.translate[1]}px)`
428
- const axesRotateXYReverseValue = `rotateX(${data.gridAxesReverseTransform.rotateX}deg) rotateY(${data.gridAxesReverseTransform.rotateY}deg)`
429
- const axesRotateReverseValue = `rotate(${data.gridAxesReverseTransform.rotate}deg)`
430
- const containerScaleReverseValue = `scale(${1 / data.gridContainerPosition[0].scale[0]}, ${1 / data.gridContainerPosition[0].scale[1]})`
431
- // 必須按照順序(先抵消外層rotate,再抵消最外層scale)
432
- return `${axesRotateXYReverseValue} ${axesRotateReverseValue} ${containerScaleReverseValue}`
433
- }),
434
- distinctUntilChanged()
435
- )
436
-
437
- const textReverseTransformWithRotate$ = combineLatest({
438
- textReverseTransform: textReverseTransform$,
439
- fullParams: fullParams$,
440
- }).pipe(
441
- takeUntil(destroy$),
442
- switchMap(async (d) => d),
443
- map(data => {
444
- // 必須按照順序(先抵消外層rotate,再抵消最外層scale,最後再做本身的rotate)
445
- return `${data.textReverseTransform} rotate(${data.fullParams.tickTextRotate}deg)`
446
- })
447
- )
448
-
449
- // 使用pointScale計算非連續性比例尺
450
- // const groupScale$: Observable<d3.ScalePoint<string>> = new Observable(subscriber => {
451
- // combineLatest({
452
- // fullDataFormatter: fullDataFormatter$,
453
- // gridAxesSize: gridAxesSize$,
454
- // computedData: computedData$
455
- // }).pipe(
456
- // takeUntil(destroy$),
457
- // switchMap(async (d) => d),
458
- // ).subscribe(data => {
459
- // const groupMin = 0
460
- // const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
461
- // const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
462
- // ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
463
- // : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
464
- // const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'auto'
465
- // ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
466
- // : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
467
-
468
- // const groupingLength = data.computedData[0]
469
- // ? data.computedData[0].length
470
- // : 0
471
-
472
- // let _labels = (data.computedData[0] ?? []).map(d => d.groupLabel)
473
-
474
- // const axisLabels = new Array(groupingLength).fill(0)
475
- // .map((d, i) => {
476
- // return _labels[i] != null
477
- // ? _labels[i]
478
- // : String(i) // 沒有label則用序列號填充
479
- // })
480
- // .filter((d, i) => {
481
- // return i >= groupScaleDomainMin && i <= groupScaleDomainMax
482
- // })
483
-
484
-
485
- // const padding = data.fullDataFormatter.grid.groupAxis.scalePadding
486
-
487
- // const groupScale = createAxisPointScale({
488
- // axisLabels,
489
- // axisWidth: data.gridAxesSize.width,
490
- // padding
491
- // })
492
-
493
- // subscriber.next(groupScale)
494
- // })
495
- // })
496
-
497
- const groupScaleDomain$ = combineLatest({
498
- fullDataFormatter: fullDataFormatter$,
499
- gridAxesSize: gridAxesSize$,
500
- computedData: computedData$
501
- }).pipe(
502
- takeUntil(destroy$),
503
- switchMap(async (d) => d),
504
- map(data => {
505
- const groupMin = 0
506
- const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
507
- // const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
508
- // ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
509
- // : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
510
- const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] - data.fullDataFormatter.grid.groupAxis.scalePadding
511
- const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'max'
512
- ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
513
- : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
514
-
515
- return [groupScaleDomainMin, groupScaleDomainMax]
516
- }),
517
- shareReplay(1)
518
- )
519
-
520
- const groupScale$ = combineLatest({
521
- groupScaleDomain: groupScaleDomain$,
522
- gridAxesSize: gridAxesSize$
523
- }).pipe(
524
- takeUntil(destroy$),
525
- switchMap(async (d) => d),
526
- map(data => {
527
- const groupScale: d3.ScaleLinear<number, number> = d3.scaleLinear()
528
- .domain(data.groupScaleDomain)
529
- .range([0, data.gridAxesSize.width])
530
- return groupScale
531
- })
532
- )
533
-
534
- const groupLabels$ = computedData$.pipe(
535
- map(computedData => (computedData[0] ?? []).map(d => d.groupLabel))
536
- )
537
-
538
- const tickTextAlign$: Observable<TextAlign> = combineLatest({
539
- fullDataFormatter: fullDataFormatter$,
540
- fullParams: fullParams$
541
- }).pipe(
542
- takeUntil(destroy$),
543
- switchMap(async (d) => d),
544
- map(data => {
545
- let textAnchor: 'start' | 'middle' | 'end' = 'middle'
546
- let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
547
-
548
- if (data.fullDataFormatter.grid.groupAxis.position === 'bottom') {
549
- textAnchor = data.fullParams.tickTextRotate
550
- ? 'end'
551
- : 'middle'
552
- dominantBaseline = 'hanging'
553
- } else if (data.fullDataFormatter.grid.groupAxis.position === 'top') {
554
- textAnchor = data.fullParams.tickTextRotate
555
- ? 'start'
556
- : 'middle'
557
- dominantBaseline = 'auto'
558
- } else if (data.fullDataFormatter.grid.groupAxis.position === 'left') {
559
- textAnchor = 'end'
560
- dominantBaseline = 'middle'
561
- } else if (data.fullDataFormatter.grid.groupAxis.position === 'right') {
562
- textAnchor = 'start'
563
- dominantBaseline = 'middle'
564
- }
565
- return {
566
- textAnchor,
567
- dominantBaseline
568
- }
569
- })
570
- )
571
-
572
- const axisLabelAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
573
- takeUntil(destroy$),
574
- map(d => {
575
- let textAnchor: 'start' | 'middle' | 'end' = 'start'
576
- let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
577
-
578
- if (d.grid.groupAxis.position === 'bottom') {
579
- dominantBaseline = 'hanging'
580
- } else if (d.grid.groupAxis.position === 'top') {
581
- dominantBaseline = 'auto'
582
- } else if (d.grid.groupAxis.position === 'left') {
583
- textAnchor = 'end'
584
- } else if (d.grid.groupAxis.position === 'right') {
585
- textAnchor = 'start'
586
- }
587
- if (d.grid.valueAxis.position === 'left') {
588
- textAnchor = 'start'
589
- } else if (d.grid.valueAxis.position === 'right') {
590
- textAnchor = 'end'
591
- } else if (d.grid.valueAxis.position === 'bottom') {
592
- dominantBaseline = 'auto'
593
- } else if (d.grid.valueAxis.position === 'top') {
594
- dominantBaseline = 'hanging'
595
- }
596
- return {
597
- textAnchor,
598
- dominantBaseline
599
- }
600
- })
601
- )
602
-
603
- combineLatest({
604
- axisSelection: axisSelection$,
605
- fullParams: fullParams$,
606
- tickTextAlign: tickTextAlign$,
607
- axisLabelAlign: axisLabelAlign$,
608
- gridAxesSize: gridAxesSize$,
609
- fullDataFormatter: fullDataFormatter$,
610
- chartParams: fullChartParams$,
611
- groupScale: groupScale$,
612
- groupScaleDomain: groupScaleDomain$,
613
- groupLabels: groupLabels$,
614
- textReverseTransform: textReverseTransform$,
615
- textReverseTransformWithRotate: textReverseTransformWithRotate$,
616
- textSizePx: textSizePx$
617
- // tickTextFormatter: tickTextFormatter$
618
- }).pipe(
619
- takeUntil(destroy$),
620
- switchMap(async (d) => d),
621
- ).subscribe(data => {
622
-
623
- renderAxis({
624
- selection: data.axisSelection,
625
- xAxisClassName,
626
- fullParams: data.fullParams,
627
- tickTextAlign: data.tickTextAlign,
628
- gridAxesSize: data.gridAxesSize,
629
- fullDataFormatter: data.fullDataFormatter,
630
- chartParams: data.chartParams,
631
- groupScale: data.groupScale,
632
- groupScaleDomain: data.groupScaleDomain,
633
- groupLabels: data.groupLabels,
634
- textReverseTransformWithRotate: data.textReverseTransformWithRotate,
635
- textSizePx: data.textSizePx
636
- })
637
-
638
- renderAxisLabel({
639
- selection: data.axisSelection,
640
- groupingLabelClassName,
641
- fullParams: data.fullParams,
642
- axisLabelAlign: data.axisLabelAlign,
643
- gridAxesSize: data.gridAxesSize,
644
- fullDataFormatter: data.fullDataFormatter,
645
- chartParams: data.chartParams,
646
- textReverseTransform: data.textReverseTransform,
647
- })
648
- })
649
-
650
- return () => {
651
- destroy$.next(undefined)
652
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ switchMap,
5
+ distinctUntilChanged,
6
+ of,
7
+ first,
8
+ map,
9
+ takeUntil,
10
+ shareReplay,
11
+ Observable,
12
+ Subject } from 'rxjs'
13
+ import type { ColorType, ComputedDataGrid, GridContainerPosition } from '@orbcharts/core'
14
+ import type { BasePluginFn } from './types'
15
+ import type {
16
+ ComputedDatumGrid,
17
+ DataFormatterGrid,
18
+ ChartParams,
19
+ TransformData } from '@orbcharts/core'
20
+ import { parseTickFormatValue } from '../utils/d3Utils'
21
+ import { getColor, getClassName } from '../utils/orbchartsUtils'
22
+ import { renderTspansOnAxis } from '../utils/d3Graphics'
23
+
24
+ export interface BaseGroupAxisParams {
25
+ // xLabel: string
26
+ // labelAnchor: 'start' | 'end'
27
+ labelOffset: [number, number]
28
+ labelColorType: ColorType
29
+ axisLineVisible: boolean
30
+ axisLineColorType: ColorType
31
+ ticks: number | null | 'all'
32
+ tickFormat: string | ((text: any) => string)
33
+ tickLineVisible: boolean
34
+ tickPadding: number
35
+ tickFullLine: boolean
36
+ tickFullLineDasharray: string
37
+ tickColorType: ColorType
38
+ // axisLineColor: string
39
+ // axisLabelColor: string
40
+ tickTextRotate: number
41
+ tickTextColorType: ColorType
42
+ }
43
+
44
+ interface BaseGroupAxisContext {
45
+ selection: d3.Selection<any, unknown, any, unknown>
46
+ computedData$: Observable<ComputedDataGrid>
47
+ fullParams$: Observable<BaseGroupAxisParams>
48
+ fullDataFormatter$: Observable<DataFormatterGrid>
49
+ fullChartParams$: Observable<ChartParams>
50
+ gridAxesTransform$: Observable<TransformData>
51
+ gridAxesReverseTransform$: Observable<TransformData>
52
+ gridAxesSize$: Observable<{
53
+ width: number;
54
+ height: number;
55
+ }>
56
+ gridContainerPosition$: Observable<GridContainerPosition[]>
57
+ isSeriesSeprate$: Observable<boolean>
58
+ textSizePx$: Observable<number>
59
+ }
60
+
61
+ interface TextAlign {
62
+ textAnchor: "start" | "middle" | "end"
63
+ dominantBaseline: "middle" | "auto" | "hanging"
64
+ }
65
+
66
+ interface GroupLabelData {
67
+ text: string
68
+ textArr: string[]
69
+ }
70
+
71
+ // const pluginName = 'GroupAxis'
72
+ // const containerClassName = getClassName(pluginName, 'container')
73
+ // const xAxisGClassName = getClassName(pluginName, 'xAxisG')
74
+ // const xAxisClassName = getClassName(pluginName, 'xAxis')
75
+ // const groupingLabelClassName = getClassName(pluginName, 'groupingLabel')
76
+ const defaultTickSize = 6
77
+
78
+ function createGroupLabelData (groupLabels: string[], tickFormat: string | ((text: any) => string)): GroupLabelData[] {
79
+ return groupLabels.map((_text, i) => {
80
+ const text = parseTickFormatValue(_text, tickFormat)
81
+ const textArr = text.split('\n')
82
+
83
+ return {
84
+ text,
85
+ textArr
86
+ }
87
+ })
88
+ }
89
+
90
+ function renderAxisLabel ({ selection, groupingLabelClassName, fullParams, axisLabelAlign, gridAxesSize, fullDataFormatter, chartParams, textReverseTransform }: {
91
+ selection: d3.Selection<SVGGElement, any, any, any>,
92
+ groupingLabelClassName: string
93
+ fullParams: BaseGroupAxisParams
94
+ axisLabelAlign: TextAlign
95
+ gridAxesSize: { width: number, height: number }
96
+ fullDataFormatter: DataFormatterGrid,
97
+ chartParams: ChartParams
98
+ textReverseTransform: string
99
+ }) {
100
+
101
+ const offsetX = fullParams.tickPadding + fullParams.labelOffset[0]
102
+ const offsetY = fullParams.tickPadding + fullParams.labelOffset[1]
103
+ let labelX = 0
104
+ let labelY = 0
105
+ if (fullDataFormatter.grid.groupAxis.position === 'bottom') {
106
+ labelY = offsetY
107
+ if (fullDataFormatter.grid.valueAxis.position === 'left') {
108
+ labelX = offsetX
109
+ } else if (fullDataFormatter.grid.valueAxis.position === 'right') {
110
+ labelX = - offsetX
111
+ }
112
+ } else if (fullDataFormatter.grid.groupAxis.position === 'top') {
113
+ labelY = - offsetY
114
+ if (fullDataFormatter.grid.valueAxis.position === 'left') {
115
+ labelX = offsetX
116
+ } else if (fullDataFormatter.grid.valueAxis.position === 'right') {
117
+ labelX = - offsetX
118
+ }
119
+ } else if (fullDataFormatter.grid.groupAxis.position === 'left') {
120
+ labelX = - offsetX
121
+ if (fullDataFormatter.grid.valueAxis.position === 'bottom') {
122
+ labelY = - offsetY
123
+ } else if (fullDataFormatter.grid.valueAxis.position === 'top') {
124
+ labelY = offsetY
125
+ }
126
+ } else if (fullDataFormatter.grid.groupAxis.position === 'right') {
127
+ labelX = offsetX
128
+ if (fullDataFormatter.grid.valueAxis.position === 'bottom') {
129
+ labelY = - offsetY
130
+ } else if (fullDataFormatter.grid.valueAxis.position === 'top') {
131
+ labelY = offsetY
132
+ }
133
+ }
134
+
135
+ const axisLabelSelection = selection
136
+ .selectAll<SVGGElement, BaseGroupAxisParams>(`g.${groupingLabelClassName}`)
137
+ .data([fullParams])
138
+ .join('g')
139
+ .classed(groupingLabelClassName, true)
140
+ .each((d, i, g) => {
141
+ const text = d3.select(g[i])
142
+ .selectAll<SVGTextElement, BaseGroupAxisParams>('text')
143
+ .data([d])
144
+ .join(
145
+ enter => {
146
+ return enter
147
+ .append('text')
148
+ .style('font-weight', 'bold')
149
+ },
150
+ update => update,
151
+ exit => exit.remove()
152
+ )
153
+ .attr('text-anchor', axisLabelAlign.textAnchor)
154
+ .attr('dominant-baseline', axisLabelAlign.dominantBaseline)
155
+ .attr('font-size', chartParams.styles.textSize)
156
+ .style('fill', getColor(fullParams.labelColorType, chartParams))
157
+ .style('transform', textReverseTransform)
158
+ // 偏移使用 x, y 而非 transform 才不會受到外層 scale 變形影響
159
+ .attr('x', labelX)
160
+ .attr('y', labelY)
161
+ .text(d => fullDataFormatter.grid.groupAxis.label)
162
+ })
163
+ .attr('transform', d => `translate(${gridAxesSize.width}, 0)`)
164
+ // .attr('transform', d => `translate(${gridAxesSize.width + d.tickPadding + fullParams.labelOffset[0]}, ${- d.tickPadding - fullParams.labelOffset[1]})`)
165
+
166
+ }
167
+
168
+ function renderAxis ({ selection, xAxisClassName, fullParams, tickTextAlign, gridAxesSize, fullDataFormatter, chartParams, groupScale, groupScaleDomain, groupLabelData, textReverseTransformWithRotate, textSizePx }: {
169
+ selection: d3.Selection<SVGGElement, any, any, any>,
170
+ xAxisClassName: string
171
+ fullParams: BaseGroupAxisParams
172
+ tickTextAlign: TextAlign
173
+ gridAxesSize: { width: number, height: number }
174
+ fullDataFormatter: DataFormatterGrid,
175
+ chartParams: ChartParams
176
+ groupScale: d3.ScaleLinear<number, number>
177
+ groupScaleDomain: number[]
178
+ // groupLabels: string[]
179
+ groupLabelData: GroupLabelData[]
180
+ textReverseTransformWithRotate: string
181
+ textSizePx: number
182
+ }) {
183
+
184
+ const xAxisSelection = selection
185
+ .selectAll<SVGGElement, BaseGroupAxisParams>(`g.${xAxisClassName}`)
186
+ .data([fullParams])
187
+ .join('g')
188
+ .classed(xAxisClassName, true)
189
+
190
+ // 計算所有範圍內groupLabels數量(顯示所有刻度)
191
+ const allTicksAmount = Math.floor(groupScaleDomain[1]) - Math.ceil(groupScaleDomain[0]) + 1
192
+
193
+ // 刻度文字偏移
194
+ let tickPadding = 0
195
+ let textX = 0
196
+ if (fullDataFormatter.grid.groupAxis.position === 'left') {
197
+ tickPadding = 0
198
+ textX = - fullParams.tickPadding
199
+ } else if (fullDataFormatter.grid.groupAxis.position === 'right') {
200
+ tickPadding = 0
201
+ textX = fullParams.tickPadding
202
+ } else if (fullDataFormatter.grid.groupAxis.position === 'bottom') {
203
+ tickPadding = fullParams.tickPadding
204
+ textX = 0
205
+ } else if (fullDataFormatter.grid.groupAxis.position === 'top') {
206
+ tickPadding = - fullParams.tickPadding
207
+ textX = - 0
208
+ }
209
+
210
+ // 設定X軸刻度
211
+ const xAxis = d3.axisBottom(groupScale)
212
+ .scale(groupScale)
213
+ .ticks(fullParams.ticks === 'all'
214
+ ? allTicksAmount
215
+ : fullParams.ticks > allTicksAmount
216
+ ? allTicksAmount // 不顯示超過groupLabels數量的刻度
217
+ : fullParams.ticks)
218
+ .tickSize(fullParams.tickFullLine == true
219
+ ? gridAxesSize.height
220
+ : - defaultTickSize)
221
+ .tickSizeOuter(0)
222
+ .tickFormat((groupIndex: number) => {
223
+ // 用index對應到groupLabel
224
+ // const groupLabel = groupLabels[groupIndex] ?? '' // 非整數index不顯示
225
+ // return parseTickFormatValue(groupLabel, fullParams.tickFormat)
226
+ return groupLabelData[groupIndex]?.text ?? ''
227
+ })
228
+ .tickPadding(tickPadding)
229
+
230
+ const xAxisEl = xAxisSelection
231
+ .transition()
232
+ .duration(100)
233
+ .call(xAxis)
234
+ .on('end', (self, t) => {
235
+ // 先等transition結束再處理文字,否則會被原本的文字覆蓋
236
+ xAxisEl
237
+ .selectAll('.tick text')
238
+ .each((groupIndex: number, i, n) => {
239
+ // const groupLabel = groupLabels[groupIndex] ?? '' // 非整數index不顯示
240
+ // const groupLabelText = parseTickFormatValue(groupLabel, fullParams.tickFormat)
241
+ const textArr = groupLabelData[groupIndex]?.textArr ?? []
242
+
243
+ // 將原本單行文字改為多行文字
244
+ renderTspansOnAxis(d3.select(n[i]), {
245
+ textArr,
246
+ textSizePx,
247
+ groupAxisPosition: fullDataFormatter.grid.groupAxis.position
248
+ })
249
+ })
250
+ })
251
+
252
+ xAxisEl.selectAll('line')
253
+ .style('fill', 'none')
254
+ .style('stroke', fullParams.tickLineVisible == true ? getColor(fullParams.tickColorType, chartParams) : 'none')
255
+ .style('stroke-dasharray', fullParams.tickFullLineDasharray)
256
+ .attr('pointer-events', 'none')
257
+
258
+ xAxisEl.selectAll('path')
259
+ .style('fill', 'none')
260
+ .style('stroke', fullParams.axisLineVisible == true ? getColor(fullParams.axisLineColorType, chartParams) : 'none')
261
+ .style('shape-rendering', 'crispEdges')
262
+
263
+ const xText = xAxisSelection.selectAll<SVGTextElement, BaseGroupAxisParams>('text')
264
+ // .style('font-family', 'sans-serif')
265
+ .attr('font-size', chartParams.styles.textSize)
266
+ // .style('font-weight', 'bold')
267
+ .attr('fill', getColor(fullParams.tickTextColorType, chartParams))
268
+ .attr('text-anchor', tickTextAlign.textAnchor)
269
+ .attr('dominant-baseline', tickTextAlign.dominantBaseline)
270
+ .attr('x', textX)
271
+ .style('transform', textReverseTransformWithRotate)
272
+
273
+ // 抵消掉預設的偏移
274
+ // if (fullDataFormatter.grid.groupAxis.position === 'left' || fullDataFormatter.grid.groupAxis.position === 'right') {
275
+ xText.attr('dy', 0)
276
+ // }
277
+
278
+ return xAxisSelection
279
+ }
280
+
281
+
282
+ export const createBaseGroupAxis: BasePluginFn<BaseGroupAxisContext> = ((pluginName: string, {
283
+ selection,
284
+ computedData$,
285
+ fullParams$,
286
+ fullDataFormatter$,
287
+ fullChartParams$,
288
+ gridAxesTransform$,
289
+ gridAxesReverseTransform$,
290
+ gridAxesSize$,
291
+ gridContainerPosition$,
292
+ isSeriesSeprate$,
293
+ textSizePx$,
294
+ }) => {
295
+
296
+ const destroy$ = new Subject()
297
+
298
+ const containerClassName = getClassName(pluginName, 'container')
299
+ const xAxisGClassName = getClassName(pluginName, 'xAxisG')
300
+ const xAxisClassName = getClassName(pluginName, 'xAxis')
301
+ const groupingLabelClassName = getClassName(pluginName, 'groupingLabel')
302
+
303
+ const containerSelection$ = combineLatest({
304
+ computedData: computedData$.pipe(
305
+ distinctUntilChanged((a, b) => {
306
+ // 只有當series的數量改變時,才重新計算
307
+ return a.length === b.length
308
+ }),
309
+ ),
310
+ isSeriesSeprate: isSeriesSeprate$
311
+ }).pipe(
312
+ takeUntil(destroy$),
313
+ switchMap(async (d) => d),
314
+ map(data => {
315
+ return data.isSeriesSeprate
316
+ // series分開的時候顯示各別axis
317
+ ? data.computedData
318
+ // series合併的時候只顯示第一個axis
319
+ : [data.computedData[0]]
320
+ }),
321
+ map((computedData, i) => {
322
+ return selection
323
+ .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${containerClassName}`)
324
+ .data(computedData, d => d[0] ? d[0].seriesIndex : i)
325
+ .join('g')
326
+ .classed(containerClassName, true)
327
+ })
328
+ )
329
+
330
+ const axisSelection$ = containerSelection$.pipe(
331
+ takeUntil(destroy$),
332
+ map((containerSelection, i) => {
333
+ return containerSelection
334
+ .selectAll<SVGGElement, ComputedDatumGrid[]>(`g.${xAxisGClassName}`)
335
+ .data([xAxisGClassName])
336
+ .join('g')
337
+ .classed(xAxisGClassName, true)
338
+ })
339
+ )
340
+
341
+ combineLatest({
342
+ containerSelection: containerSelection$,
343
+ gridContainerPosition: gridContainerPosition$
344
+ }).pipe(
345
+ takeUntil(destroy$),
346
+ switchMap(async d => d)
347
+ ).subscribe(data => {
348
+ data.containerSelection
349
+ .attr('transform', (d, i) => {
350
+ const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
351
+ const translate = gridContainerPosition.translate
352
+ const scale = gridContainerPosition.scale
353
+ return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
354
+ })
355
+ // .attr('opacity', 0)
356
+ // .transition()
357
+ // .attr('opacity', 1)
358
+ })
359
+
360
+ combineLatest({
361
+ axisSelection: axisSelection$,
362
+ gridAxesTransform: gridAxesTransform$,
363
+ }).pipe(
364
+ takeUntil(destroy$),
365
+ switchMap(async d => d)
366
+ ).subscribe(data => {
367
+ data.axisSelection
368
+ .style('transform', data.gridAxesTransform.value)
369
+ // .attr('opacity', 0)
370
+ // .transition()
371
+ // .attr('opacity', 1)
372
+
373
+ })
374
+
375
+
376
+ // const gridAxesSize$ = gridAxisSizeObservable({
377
+ // fullDataFormatter$,
378
+ // layout$
379
+ // })
380
+
381
+
382
+ // const tickTextFormatter$ = fullDataFormatter$
383
+ // .pipe(
384
+ // map(d => {
385
+ // return d.grid.seriesDirection === 'row' ? d.grid.columnLabelFormat : d.grid.rowLabelFormat
386
+ // })
387
+ // )
388
+
389
+ // const textReverseTransform$: Observable<string> = new Observable(subscriber => {
390
+ // combineLatest({
391
+ // params: fullParams$,
392
+ // gridAxesTransform: gridAxesTransform$
393
+ // }).pipe(
394
+ // takeUntil(destroy$),
395
+ // // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
396
+ // switchMap(async (d) => d),
397
+ // ).subscribe(data => {
398
+
399
+ // const transformData = Object.assign({}, data.gridAxesTransform)
400
+
401
+ // const value = getAxesTransformValue({
402
+ // translate: [0, 0],
403
+ // scale: [transformData.scale[0] * -1, transformData.scale[1] * -1],
404
+ // rotate: transformData.rotate * -1 + data.params.tickTextRotate,
405
+ // rotateX: transformData.rotateX * -1,
406
+ // rotateY: transformData.rotateY * -1
407
+ // })
408
+
409
+ // subscriber.next(value)
410
+ // })
411
+ // })
412
+ // const reverseTransform$: Observable<TransformData> = gridAxesTransform$.pipe(
413
+ // takeUntil(destroy$),
414
+ // map(d => {
415
+ // const translate: [number, number] = [d.translate[0] * -1, d.translate[1] * -1]
416
+ // const scale: [number, number] = [d.scale[0] * -1, d.scale[1] * -1]
417
+ // const rotate = d.rotate * -1
418
+ // const rotateX = d.rotateX * -1
419
+ // const rotateY = d.rotateY * -1
420
+ // return {
421
+ // translate,
422
+ // scale,
423
+ // rotate,
424
+ // rotateX,
425
+ // rotateY,
426
+ // value: ''
427
+ // }
428
+ // }),
429
+ // )
430
+ const textReverseTransform$ = combineLatest({
431
+ gridAxesReverseTransform: gridAxesReverseTransform$,
432
+ gridContainerPosition: gridContainerPosition$
433
+ }).pipe(
434
+ takeUntil(destroy$),
435
+ switchMap(async (d) => d),
436
+ map(data => {
437
+ // const axisReverseTranslateValue = `translate(${data.gridAxesReverseTransform.translate[0]}px, ${data.gridAxesReverseTransform.translate[1]}px)`
438
+ const axesRotateXYReverseValue = `rotateX(${data.gridAxesReverseTransform.rotateX}deg) rotateY(${data.gridAxesReverseTransform.rotateY}deg)`
439
+ const axesRotateReverseValue = `rotate(${data.gridAxesReverseTransform.rotate}deg)`
440
+ const containerScaleReverseValue = `scale(${1 / data.gridContainerPosition[0].scale[0]}, ${1 / data.gridContainerPosition[0].scale[1]})`
441
+ // 必須按照順序(先抵消外層rotate,再抵消最外層scale)
442
+ return `${axesRotateXYReverseValue} ${axesRotateReverseValue} ${containerScaleReverseValue}`
443
+ }),
444
+ distinctUntilChanged()
445
+ )
446
+
447
+ const textReverseTransformWithRotate$ = combineLatest({
448
+ textReverseTransform: textReverseTransform$,
449
+ fullParams: fullParams$,
450
+ }).pipe(
451
+ takeUntil(destroy$),
452
+ switchMap(async (d) => d),
453
+ map(data => {
454
+ // 必須按照順序(先抵消外層rotate,再抵消最外層scale,最後再做本身的rotate)
455
+ return `${data.textReverseTransform} rotate(${data.fullParams.tickTextRotate}deg)`
456
+ })
457
+ )
458
+
459
+ // 使用pointScale計算非連續性比例尺
460
+ // const groupScale$: Observable<d3.ScalePoint<string>> = new Observable(subscriber => {
461
+ // combineLatest({
462
+ // fullDataFormatter: fullDataFormatter$,
463
+ // gridAxesSize: gridAxesSize$,
464
+ // computedData: computedData$
465
+ // }).pipe(
466
+ // takeUntil(destroy$),
467
+ // switchMap(async (d) => d),
468
+ // ).subscribe(data => {
469
+ // const groupMin = 0
470
+ // const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
471
+ // const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
472
+ // ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
473
+ // : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
474
+ // const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'auto'
475
+ // ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
476
+ // : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
477
+
478
+ // const groupingLength = data.computedData[0]
479
+ // ? data.computedData[0].length
480
+ // : 0
481
+
482
+ // let _labels = (data.computedData[0] ?? []).map(d => d.groupLabel)
483
+
484
+ // const axisLabels = new Array(groupingLength).fill(0)
485
+ // .map((d, i) => {
486
+ // return _labels[i] != null
487
+ // ? _labels[i]
488
+ // : String(i) // 沒有label則用序列號填充
489
+ // })
490
+ // .filter((d, i) => {
491
+ // return i >= groupScaleDomainMin && i <= groupScaleDomainMax
492
+ // })
493
+
494
+
495
+ // const padding = data.fullDataFormatter.grid.groupAxis.scalePadding
496
+
497
+ // const groupScale = createAxisPointScale({
498
+ // axisLabels,
499
+ // axisWidth: data.gridAxesSize.width,
500
+ // padding
501
+ // })
502
+
503
+ // subscriber.next(groupScale)
504
+ // })
505
+ // })
506
+
507
+ const groupScaleDomain$ = combineLatest({
508
+ fullDataFormatter: fullDataFormatter$,
509
+ gridAxesSize: gridAxesSize$,
510
+ computedData: computedData$
511
+ }).pipe(
512
+ takeUntil(destroy$),
513
+ switchMap(async (d) => d),
514
+ map(data => {
515
+ const groupMin = 0
516
+ const groupMax = data.computedData[0] ? data.computedData[0].length - 1 : 0
517
+ // const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] === 'auto'
518
+ // ? groupMin - data.fullDataFormatter.grid.groupAxis.scalePadding
519
+ // : data.fullDataFormatter.grid.groupAxis.scaleDomain[0] as number - data.fullDataFormatter.grid.groupAxis.scalePadding
520
+ const groupScaleDomainMin = data.fullDataFormatter.grid.groupAxis.scaleDomain[0] - data.fullDataFormatter.grid.groupAxis.scalePadding
521
+ const groupScaleDomainMax = data.fullDataFormatter.grid.groupAxis.scaleDomain[1] === 'max'
522
+ ? groupMax + data.fullDataFormatter.grid.groupAxis.scalePadding
523
+ : data.fullDataFormatter.grid.groupAxis.scaleDomain[1] as number + data.fullDataFormatter.grid.groupAxis.scalePadding
524
+
525
+ return [groupScaleDomainMin, groupScaleDomainMax]
526
+ }),
527
+ shareReplay(1)
528
+ )
529
+
530
+ const groupScale$ = combineLatest({
531
+ groupScaleDomain: groupScaleDomain$,
532
+ gridAxesSize: gridAxesSize$
533
+ }).pipe(
534
+ takeUntil(destroy$),
535
+ switchMap(async (d) => d),
536
+ map(data => {
537
+ const groupScale: d3.ScaleLinear<number, number> = d3.scaleLinear()
538
+ .domain(data.groupScaleDomain)
539
+ .range([0, data.gridAxesSize.width])
540
+ return groupScale
541
+ })
542
+ )
543
+
544
+ const groupLabels$ = computedData$.pipe(
545
+ map(computedData => (computedData[0] ?? []).map(d => d.groupLabel))
546
+ )
547
+
548
+ const tickTextAlign$: Observable<TextAlign> = combineLatest({
549
+ fullDataFormatter: fullDataFormatter$,
550
+ fullParams: fullParams$
551
+ }).pipe(
552
+ takeUntil(destroy$),
553
+ switchMap(async (d) => d),
554
+ map(data => {
555
+ let textAnchor: 'start' | 'middle' | 'end' = 'middle'
556
+ let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
557
+
558
+ if (data.fullDataFormatter.grid.groupAxis.position === 'bottom') {
559
+ textAnchor = data.fullParams.tickTextRotate
560
+ ? 'end'
561
+ : 'middle'
562
+ dominantBaseline = 'hanging'
563
+ } else if (data.fullDataFormatter.grid.groupAxis.position === 'top') {
564
+ textAnchor = data.fullParams.tickTextRotate
565
+ ? 'start'
566
+ : 'middle'
567
+ dominantBaseline = 'auto'
568
+ } else if (data.fullDataFormatter.grid.groupAxis.position === 'left') {
569
+ textAnchor = 'end'
570
+ dominantBaseline = 'middle'
571
+ } else if (data.fullDataFormatter.grid.groupAxis.position === 'right') {
572
+ textAnchor = 'start'
573
+ dominantBaseline = 'middle'
574
+ }
575
+ return {
576
+ textAnchor,
577
+ dominantBaseline
578
+ }
579
+ })
580
+ )
581
+
582
+ const axisLabelAlign$: Observable<TextAlign> = fullDataFormatter$.pipe(
583
+ takeUntil(destroy$),
584
+ map(d => {
585
+ let textAnchor: 'start' | 'middle' | 'end' = 'start'
586
+ let dominantBaseline: 'auto' | 'middle' | 'hanging' = 'hanging'
587
+
588
+ if (d.grid.groupAxis.position === 'bottom') {
589
+ dominantBaseline = 'hanging'
590
+ } else if (d.grid.groupAxis.position === 'top') {
591
+ dominantBaseline = 'auto'
592
+ } else if (d.grid.groupAxis.position === 'left') {
593
+ textAnchor = 'end'
594
+ } else if (d.grid.groupAxis.position === 'right') {
595
+ textAnchor = 'start'
596
+ }
597
+ if (d.grid.valueAxis.position === 'left') {
598
+ textAnchor = 'start'
599
+ } else if (d.grid.valueAxis.position === 'right') {
600
+ textAnchor = 'end'
601
+ } else if (d.grid.valueAxis.position === 'bottom') {
602
+ dominantBaseline = 'auto'
603
+ } else if (d.grid.valueAxis.position === 'top') {
604
+ dominantBaseline = 'hanging'
605
+ }
606
+ return {
607
+ textAnchor,
608
+ dominantBaseline
609
+ }
610
+ })
611
+ )
612
+
613
+ const groupLabelData$ = combineLatest({
614
+ groupLabels: groupLabels$,
615
+ fullParams: fullParams$
616
+ }).pipe(
617
+ takeUntil(destroy$),
618
+ switchMap(async (d) => d),
619
+ map(data => {
620
+ return createGroupLabelData(data.groupLabels, data.fullParams.tickFormat)
621
+ })
622
+ )
623
+
624
+ combineLatest({
625
+ axisSelection: axisSelection$,
626
+ fullParams: fullParams$,
627
+ tickTextAlign: tickTextAlign$,
628
+ axisLabelAlign: axisLabelAlign$,
629
+ gridAxesSize: gridAxesSize$,
630
+ fullDataFormatter: fullDataFormatter$,
631
+ chartParams: fullChartParams$,
632
+ groupScale: groupScale$,
633
+ groupScaleDomain: groupScaleDomain$,
634
+ // groupLabels: groupLabels$,
635
+ groupLabelData: groupLabelData$,
636
+ textReverseTransform: textReverseTransform$,
637
+ textReverseTransformWithRotate: textReverseTransformWithRotate$,
638
+ textSizePx: textSizePx$
639
+ // tickTextFormatter: tickTextFormatter$
640
+ }).pipe(
641
+ takeUntil(destroy$),
642
+ switchMap(async (d) => d),
643
+ ).subscribe(data => {
644
+
645
+ renderAxis({
646
+ selection: data.axisSelection,
647
+ xAxisClassName,
648
+ fullParams: data.fullParams,
649
+ tickTextAlign: data.tickTextAlign,
650
+ gridAxesSize: data.gridAxesSize,
651
+ fullDataFormatter: data.fullDataFormatter,
652
+ chartParams: data.chartParams,
653
+ groupScale: data.groupScale,
654
+ groupScaleDomain: data.groupScaleDomain,
655
+ // groupLabels: data.groupLabels,
656
+ groupLabelData: data.groupLabelData,
657
+ textReverseTransformWithRotate: data.textReverseTransformWithRotate,
658
+ textSizePx: data.textSizePx
659
+ })
660
+
661
+ renderAxisLabel({
662
+ selection: data.axisSelection,
663
+ groupingLabelClassName,
664
+ fullParams: data.fullParams,
665
+ axisLabelAlign: data.axisLabelAlign,
666
+ gridAxesSize: data.gridAxesSize,
667
+ fullDataFormatter: data.fullDataFormatter,
668
+ chartParams: data.chartParams,
669
+ textReverseTransform: data.textReverseTransform,
670
+ })
671
+ })
672
+
673
+ return () => {
674
+ destroy$.next(undefined)
675
+ }
653
676
  })