@orbcharts/plugins-basic 3.0.8 → 3.0.9

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