@orbcharts/plugins-basic 3.0.0-alpha.41 → 3.0.0-alpha.43

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