@orbcharts/plugins-basic 3.0.5 → 3.0.7
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.
- package/LICENSE +200 -200
- package/dist/orbcharts-plugins-basic.es.js +3414 -3390
- package/dist/orbcharts-plugins-basic.umd.js +35 -35
- package/lib/core-types.ts +7 -7
- package/lib/core.ts +6 -6
- package/lib/gridObservables.ts +6 -6
- package/lib/plugins-basic-types.ts +6 -6
- package/package.json +48 -48
- package/src/base/BaseBars.ts +765 -765
- package/src/base/BaseBarsTriangle.ts +676 -676
- package/src/base/BaseDots.ts +464 -464
- package/src/base/BaseGroupAxis.ts +691 -691
- package/src/base/BaseLegend.ts +684 -684
- package/src/base/BaseLineAreas.ts +629 -629
- package/src/base/BaseLines.ts +706 -706
- package/src/base/BaseOrdinalBubbles.ts +729 -729
- package/src/base/BaseRacingBars.ts +582 -582
- package/src/base/BaseRacingLabels.ts +404 -404
- package/src/base/BaseRacingValueLabels.ts +403 -403
- package/src/base/BaseStackedBars.ts +782 -782
- package/src/base/BaseTooltip.ts +408 -386
- package/src/base/BaseValueAxis.ts +600 -600
- package/src/base/BaseXAxis.ts +427 -427
- package/src/base/BaseXZoom.ts +241 -241
- package/src/base/BaseYAxis.ts +389 -389
- package/src/base/types.ts +2 -2
- package/src/const.ts +30 -30
- package/src/grid/defaults.ts +213 -213
- package/src/grid/gridObservables.ts +635 -635
- package/src/grid/index.ts +16 -16
- package/src/grid/plugins/Bars.ts +69 -69
- package/src/grid/plugins/BarsPN.ts +66 -66
- package/src/grid/plugins/BarsTriangle.ts +73 -73
- package/src/grid/plugins/Dots.ts +68 -68
- package/src/grid/plugins/GridLegend.ts +107 -107
- package/src/grid/plugins/GridTooltip.ts +66 -66
- package/src/grid/plugins/GroupAux.ts +1095 -1095
- package/src/grid/plugins/GroupAxis.ts +73 -73
- package/src/grid/plugins/GroupZoom.ts +218 -218
- package/src/grid/plugins/LineAreas.ts +65 -65
- package/src/grid/plugins/Lines.ts +59 -59
- package/src/grid/plugins/StackedBars.ts +64 -64
- package/src/grid/plugins/StackedValueAxis.ts +96 -96
- package/src/grid/plugins/ValueAxis.ts +94 -94
- package/src/index.ts +6 -6
- package/src/multiGrid/defaults.ts +244 -244
- package/src/multiGrid/index.ts +14 -14
- package/src/multiGrid/multiGridObservables.ts +50 -50
- package/src/multiGrid/plugins/MultiBars.ts +108 -108
- package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
- package/src/multiGrid/plugins/MultiDots.ts +102 -102
- package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
- package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
- package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
- package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
- package/src/multiGrid/plugins/MultiLines.ts +101 -101
- package/src/multiGrid/plugins/MultiStackedBars.ts +106 -106
- package/src/multiGrid/plugins/MultiStackedValueAxis.ts +134 -134
- package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
- package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
- package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
- package/src/multiValue/defaults.ts +523 -523
- package/src/multiValue/index.ts +16 -16
- package/src/multiValue/multiValueObservables.ts +781 -781
- package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
- package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
- package/src/multiValue/plugins/OrdinalAux.ts +660 -660
- package/src/multiValue/plugins/OrdinalAxis.ts +524 -524
- package/src/multiValue/plugins/OrdinalBubbles.ts +226 -226
- package/src/multiValue/plugins/OrdinalZoom.ts +57 -57
- package/src/multiValue/plugins/RacingBars.ts +375 -375
- package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
- package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
- package/src/multiValue/plugins/Scatter.ts +486 -486
- package/src/multiValue/plugins/ScatterBubbles.ts +635 -635
- package/src/multiValue/plugins/XAxis.ts +107 -107
- package/src/multiValue/plugins/XYAux.ts +683 -683
- package/src/multiValue/plugins/XYAxes.ts +194 -194
- package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
- package/src/multiValue/plugins/XZoom.ts +40 -40
- package/src/noneData/defaults.ts +102 -102
- package/src/noneData/index.ts +3 -3
- package/src/noneData/plugins/Container.ts +27 -27
- package/src/noneData/plugins/Tooltip.ts +373 -373
- package/src/relationship/defaults.ts +221 -221
- package/src/relationship/index.ts +5 -5
- package/src/relationship/plugins/ForceDirected.ts +1056 -1056
- package/src/relationship/plugins/ForceDirectedBubbles.ts +1294 -1294
- package/src/relationship/plugins/RelationshipLegend.ts +100 -100
- package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
- package/src/relationship/relationshipObservables.ts +49 -49
- package/src/series/defaults.ts +223 -222
- package/src/series/index.ts +9 -9
- package/src/series/plugins/Bubbles.ts +784 -766
- package/src/series/plugins/Pie.ts +622 -622
- package/src/series/plugins/PieEventTexts.ts +283 -283
- package/src/series/plugins/PieLabels.ts +639 -639
- package/src/series/plugins/Rose.ts +515 -515
- package/src/series/plugins/RoseLabels.ts +599 -599
- package/src/series/plugins/SeriesLegend.ts +107 -107
- package/src/series/plugins/SeriesTooltip.ts +66 -66
- package/src/series/seriesObservables.ts +168 -168
- package/src/series/seriesUtils.ts +51 -51
- package/src/tree/defaults.ts +102 -102
- package/src/tree/index.ts +4 -4
- package/src/tree/plugins/TreeLegend.ts +100 -100
- package/src/tree/plugins/TreeMap.ts +341 -341
- package/src/tree/plugins/TreeTooltip.ts +66 -66
- package/src/utils/commonUtils.ts +31 -31
- package/src/utils/d3Graphics.ts +176 -176
- package/src/utils/d3Utils.ts +92 -92
- package/src/utils/observables.ts +14 -14
- package/src/utils/orbchartsUtils.ts +129 -129
- package/tsconfig.base.json +13 -13
- package/tsconfig.json +2 -2
- package/vite.config.js +22 -22
@@ -1,782 +1,782 @@
|
|
1
|
-
import * as d3 from 'd3'
|
2
|
-
import {
|
3
|
-
Observable,
|
4
|
-
Subject,
|
5
|
-
debounceTime,
|
6
|
-
iif,
|
7
|
-
of,
|
8
|
-
takeUntil,
|
9
|
-
filter,
|
10
|
-
map,
|
11
|
-
switchMap,
|
12
|
-
combineLatest,
|
13
|
-
merge,
|
14
|
-
shareReplay,
|
15
|
-
distinctUntilChanged
|
16
|
-
} from 'rxjs'
|
17
|
-
import type {
|
18
|
-
ChartParams,
|
19
|
-
HighlightTarget,
|
20
|
-
DataFormatterMultiValue,
|
21
|
-
ComputedDataMultiValue,
|
22
|
-
ComputedDatumMultiValue,
|
23
|
-
ComputedXYDatumMultiValue,
|
24
|
-
ComputedXYDataMultiValue,
|
25
|
-
TransformData,
|
26
|
-
ContainerSize,
|
27
|
-
ContainerPositionScaled,
|
28
|
-
Layout,
|
29
|
-
} from '../../lib/core-types'
|
30
|
-
import {
|
31
|
-
createAxisToLabelIndexScale,
|
32
|
-
createAxisToValueScale,
|
33
|
-
createLabelToAxisScale,
|
34
|
-
createValueToAxisScale,
|
35
|
-
} from '../../lib/core'
|
36
|
-
import { getClassName, getUniID } from '../utils/orbchartsUtils'
|
37
|
-
import { d3EventObservable } from '../utils/observables'
|
38
|
-
|
39
|
-
// 建立 multiValue 主要的 selection
|
40
|
-
export const multiValueSelectionsObservable = ({ selection, pluginName, clipPathID, categoryLabels$, containerPosition$, graphicTransform$ }: {
|
41
|
-
selection: d3.Selection<any, unknown, any, unknown>
|
42
|
-
pluginName: string
|
43
|
-
clipPathID: string
|
44
|
-
// computedData$: Observable<ComputedDataMultiValue>
|
45
|
-
categoryLabels$: Observable<string[]>
|
46
|
-
containerPosition$: Observable<ContainerPositionScaled[]>
|
47
|
-
// multiValueAxesTransform$: Observable<TransformData>
|
48
|
-
graphicTransform$: Observable<TransformData>
|
49
|
-
}) => {
|
50
|
-
const categoryClassName = getClassName(pluginName, 'category')
|
51
|
-
const axesClassName = getClassName(pluginName, 'axes')
|
52
|
-
const graphicClassName = getClassName(pluginName, 'graphic')
|
53
|
-
|
54
|
-
// <g> category selection(container排放位置)
|
55
|
-
// <g> axes selection(圖軸)
|
56
|
-
// <defs> clipPath selection
|
57
|
-
// <g> graphic selection(圖形 scale 範圍的變形)
|
58
|
-
const categorySelection$ = categoryLabels$.pipe(
|
59
|
-
map((categoryLabels, i) => {
|
60
|
-
return selection
|
61
|
-
.selectAll<SVGGElement, string>(`g.${categoryClassName}`)
|
62
|
-
.data(categoryLabels, d => d)
|
63
|
-
.join(
|
64
|
-
enter => {
|
65
|
-
return enter
|
66
|
-
.append('g')
|
67
|
-
.classed(categoryClassName, true)
|
68
|
-
.each((d, i, g) => {
|
69
|
-
const axesSelection = d3.select(g[i])
|
70
|
-
.selectAll<SVGGElement, ComputedDatumMultiValue[]>(`g.${axesClassName}`)
|
71
|
-
.data([i])
|
72
|
-
.join(
|
73
|
-
enter => {
|
74
|
-
return enter
|
75
|
-
.append('g')
|
76
|
-
.classed(axesClassName, true)
|
77
|
-
.attr('clip-path', `url(#${clipPathID})`)
|
78
|
-
.each((d, i, g) => {
|
79
|
-
const defsSelection = d3.select(g[i])
|
80
|
-
.selectAll<SVGDefsElement, any>('defs')
|
81
|
-
.data([i])
|
82
|
-
.join('defs')
|
83
|
-
|
84
|
-
const graphicGSelection = d3.select(g[i])
|
85
|
-
.selectAll<SVGGElement, any>('g')
|
86
|
-
.data([i])
|
87
|
-
.join('g')
|
88
|
-
.classed(graphicClassName, true)
|
89
|
-
})
|
90
|
-
},
|
91
|
-
update => update,
|
92
|
-
exit => exit.remove()
|
93
|
-
)
|
94
|
-
})
|
95
|
-
},
|
96
|
-
update => update,
|
97
|
-
exit => exit.remove()
|
98
|
-
)
|
99
|
-
}),
|
100
|
-
shareReplay(1)
|
101
|
-
)
|
102
|
-
|
103
|
-
// <g> category selection
|
104
|
-
combineLatest({
|
105
|
-
categorySelection: categorySelection$,
|
106
|
-
containerPosition: containerPosition$
|
107
|
-
}).pipe(
|
108
|
-
switchMap(async d => d)
|
109
|
-
).subscribe(data => {
|
110
|
-
data.categorySelection
|
111
|
-
.transition()
|
112
|
-
.attr('transform', (d, i) => {
|
113
|
-
const containerPosition = data.containerPosition[i] ?? data.containerPosition[0]
|
114
|
-
const translate = containerPosition.translate
|
115
|
-
const scale = containerPosition.scale
|
116
|
-
return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
|
117
|
-
})
|
118
|
-
})
|
119
|
-
|
120
|
-
// <g> axes selection
|
121
|
-
const axesSelection$ = categorySelection$.pipe(
|
122
|
-
map(categorySelection => {
|
123
|
-
return categorySelection
|
124
|
-
.select<SVGGElement>(`g.${axesClassName}`)
|
125
|
-
}),
|
126
|
-
shareReplay(1)
|
127
|
-
)
|
128
|
-
|
129
|
-
// <defs> clipPath selection
|
130
|
-
const defsSelection$ = axesSelection$.pipe(
|
131
|
-
map(axesSelection => {
|
132
|
-
return axesSelection.select<SVGDefsElement>('defs')
|
133
|
-
}),
|
134
|
-
shareReplay(1)
|
135
|
-
)
|
136
|
-
|
137
|
-
// <g> graphic selection
|
138
|
-
const graphicGSelection$ = combineLatest({
|
139
|
-
axesSelection: axesSelection$,
|
140
|
-
graphicTransform: graphicTransform$
|
141
|
-
}).pipe(
|
142
|
-
switchMap(async d => d),
|
143
|
-
map(data => {
|
144
|
-
const graphicGSelection = data.axesSelection
|
145
|
-
.select<SVGGElement>(`g.${graphicClassName}`)
|
146
|
-
graphicGSelection
|
147
|
-
.transition()
|
148
|
-
.duration(50)
|
149
|
-
.style('transform', data.graphicTransform.value)
|
150
|
-
return graphicGSelection
|
151
|
-
}),
|
152
|
-
shareReplay(1)
|
153
|
-
)
|
154
|
-
|
155
|
-
return {
|
156
|
-
categorySelection$,
|
157
|
-
axesSelection$,
|
158
|
-
defsSelection$,
|
159
|
-
graphicGSelection$
|
160
|
-
}
|
161
|
-
}
|
162
|
-
|
163
|
-
// 建立 multiValue 主要的 selection - 只取無scale的container selection
|
164
|
-
export const multiValueContainerSelectionsObservable = ({ selection, pluginName, clipPathID, computedData$, containerPosition$, isCategorySeprate$ }: {
|
165
|
-
selection: d3.Selection<any, unknown, any, unknown>
|
166
|
-
pluginName: string
|
167
|
-
clipPathID: string | null
|
168
|
-
computedData$: Observable<ComputedDataMultiValue>
|
169
|
-
containerPosition$: Observable<ContainerPositionScaled[]>
|
170
|
-
isCategorySeprate$: Observable<boolean>
|
171
|
-
}) => {
|
172
|
-
const containerClassName = getClassName(pluginName, 'container')
|
173
|
-
|
174
|
-
const containerSelection$ = combineLatest({
|
175
|
-
computedData: computedData$.pipe(
|
176
|
-
distinctUntilChanged((a, b) => {
|
177
|
-
// 只有當series的數量改變時,才重新計算
|
178
|
-
return a.length === b.length
|
179
|
-
}),
|
180
|
-
),
|
181
|
-
isCategorySeprate: isCategorySeprate$
|
182
|
-
}).pipe(
|
183
|
-
switchMap(async (d) => d),
|
184
|
-
map(data => {
|
185
|
-
return data.isCategorySeprate
|
186
|
-
// category分開的時候顯示各別axis
|
187
|
-
? data.computedData
|
188
|
-
// category合併的時候只顯示第一個axis
|
189
|
-
: [data.computedData[0]]
|
190
|
-
}),
|
191
|
-
map((computedData, i) => {
|
192
|
-
return selection
|
193
|
-
.selectAll<SVGGElement, ComputedDatumMultiValue[]>(`g.${containerClassName}`)
|
194
|
-
.data(computedData, d => (d && d[0]) ? d[0].categoryIndex : i)
|
195
|
-
.join('g')
|
196
|
-
.classed(containerClassName, true)
|
197
|
-
.attr('clip-path', _ => clipPathID ? `url(#${clipPathID})` : 'none')
|
198
|
-
}),
|
199
|
-
shareReplay(1)
|
200
|
-
)
|
201
|
-
|
202
|
-
combineLatest({
|
203
|
-
containerSelection: containerSelection$,
|
204
|
-
gridContainerPosition: containerPosition$
|
205
|
-
}).pipe(
|
206
|
-
switchMap(async d => d)
|
207
|
-
).subscribe(data => {
|
208
|
-
data.containerSelection
|
209
|
-
.attr('transform', (d, i) => {
|
210
|
-
const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
|
211
|
-
const translate = gridContainerPosition.translate
|
212
|
-
const scale = gridContainerPosition.scale
|
213
|
-
// return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
|
214
|
-
return `translate(${translate[0]}, ${translate[1]})`
|
215
|
-
})
|
216
|
-
// .attr('opacity', 0)
|
217
|
-
// .transition()
|
218
|
-
// .attr('opacity', 1)
|
219
|
-
})
|
220
|
-
|
221
|
-
return containerSelection$
|
222
|
-
}
|
223
|
-
|
224
|
-
|
225
|
-
export const multiValueXYPositionObservable = ({ rootSelection, fullDataFormatter$, filteredXYMinMaxData$, containerPosition$, containerSize$, layout$ }: {
|
226
|
-
rootSelection: d3.Selection<any, unknown, any, unknown>
|
227
|
-
fullDataFormatter$: Observable<DataFormatterMultiValue>
|
228
|
-
// computedData$: Observable<ComputedDataMultiValue>
|
229
|
-
// xyMinMax$: Observable<{ minX: number, maxX: number, minY: number, maxY: number }>
|
230
|
-
filteredXYMinMaxData$: Observable<{
|
231
|
-
minXDatum: ComputedXYDatumMultiValue
|
232
|
-
maxXDatum: ComputedXYDatumMultiValue
|
233
|
-
minYDatum: ComputedXYDatumMultiValue
|
234
|
-
maxYDatum: ComputedXYDatumMultiValue
|
235
|
-
}>
|
236
|
-
containerPosition$: Observable<ContainerPositionScaled[]>
|
237
|
-
containerSize$: Observable<ContainerSize>
|
238
|
-
layout$: Observable<Layout>
|
239
|
-
}) => {
|
240
|
-
const rootMousemove$ = d3EventObservable(rootSelection, 'mousemove').pipe(
|
241
|
-
debounceTime(2) // 避免過度頻繁觸發,實測時沒加電腦容易卡頓
|
242
|
-
)
|
243
|
-
|
244
|
-
// const columnAmount$ = containerPosition$.pipe(
|
245
|
-
// map(containerPosition => {
|
246
|
-
// const maxColumnIndex = containerPosition.reduce((acc, current) => {
|
247
|
-
// return current.columnIndex > acc ? current.columnIndex : acc
|
248
|
-
// }, 0)
|
249
|
-
// return maxColumnIndex + 1
|
250
|
-
// }),
|
251
|
-
// distinctUntilChanged(),
|
252
|
-
// shareReplay(1)
|
253
|
-
// )
|
254
|
-
|
255
|
-
// const rowAmount$ = containerPosition$.pipe(
|
256
|
-
// map(containerPosition => {
|
257
|
-
// const maxRowIndex = containerPosition.reduce((acc, current) => {
|
258
|
-
// return current.rowIndex > acc ? current.rowIndex : acc
|
259
|
-
// }, 0)
|
260
|
-
// return maxRowIndex + 1
|
261
|
-
// }),
|
262
|
-
// distinctUntilChanged(),
|
263
|
-
// shareReplay(1)
|
264
|
-
// )
|
265
|
-
|
266
|
-
// const xyScale$ = combineLatest({
|
267
|
-
// layout: layout$,
|
268
|
-
// filteredXYMinMaxData: filteredXYMinMaxData$,
|
269
|
-
// fullDataFormatter: fullDataFormatter$,
|
270
|
-
// columnAmount: columnAmount$,
|
271
|
-
// rowAmount: rowAmount$
|
272
|
-
// }).pipe(
|
273
|
-
// switchMap(async d => d),
|
274
|
-
// map(data => {
|
275
|
-
// const xScale = createAxisToValueScale({
|
276
|
-
// maxValue: data.filteredXYMinMaxData.maxXDatum.value[0],
|
277
|
-
// minValue: data.filteredXYMinMaxData.minXDatum.value[0],
|
278
|
-
// axisWidth: data.layout.width,
|
279
|
-
// scaleDomain: data.fullDataFormatter.xAxis.scaleDomain,
|
280
|
-
// scaleRange: data.fullDataFormatter.xAxis.scaleRange,
|
281
|
-
// })
|
282
|
-
// const yScale = createAxisToValueScale({
|
283
|
-
// maxValue: data.filteredXYMinMaxData.maxYDatum.value[1],
|
284
|
-
// minValue: data.filteredXYMinMaxData.minYDatum.value[1],
|
285
|
-
// axisWidth: data.layout.height,
|
286
|
-
// scaleDomain: data.fullDataFormatter.yAxis.scaleDomain,
|
287
|
-
// scaleRange: data.fullDataFormatter.yAxis.scaleRange,
|
288
|
-
// reverse: true
|
289
|
-
// })
|
290
|
-
// return { xScale, yScale }
|
291
|
-
// })
|
292
|
-
// )
|
293
|
-
|
294
|
-
const xyScale$: Observable<{
|
295
|
-
xScale: d3.ScaleLinear<number, number, never>;
|
296
|
-
yScale: d3.ScaleLinear<number, number, never>;
|
297
|
-
}> = new Observable(subscriber => {
|
298
|
-
combineLatest({
|
299
|
-
// layout: layout$,
|
300
|
-
containerSize: containerSize$,
|
301
|
-
filteredXYMinMaxData: filteredXYMinMaxData$,
|
302
|
-
fullDataFormatter: fullDataFormatter$,
|
303
|
-
// columnAmount: columnAmount$,
|
304
|
-
// rowAmount: rowAmount$
|
305
|
-
}).pipe(
|
306
|
-
switchMap(async d => d),
|
307
|
-
).subscribe(data => {
|
308
|
-
const xValueIndex = data.fullDataFormatter.xAxis.valueIndex
|
309
|
-
const yValueIndex = data.fullDataFormatter.yAxis.valueIndex
|
310
|
-
if (!data.filteredXYMinMaxData.minXDatum || !data.filteredXYMinMaxData.maxXDatum
|
311
|
-
|| data.filteredXYMinMaxData.minXDatum.value[xValueIndex] == null || data.filteredXYMinMaxData.maxXDatum.value[xValueIndex] == null
|
312
|
-
|| !data.filteredXYMinMaxData.minYDatum || !data.filteredXYMinMaxData.maxYDatum
|
313
|
-
|| data.filteredXYMinMaxData.minYDatum.value[yValueIndex] == null || data.filteredXYMinMaxData.maxYDatum.value[yValueIndex] == null
|
314
|
-
) {
|
315
|
-
return
|
316
|
-
}
|
317
|
-
const xScale = createAxisToValueScale({
|
318
|
-
maxValue: data.filteredXYMinMaxData.maxXDatum.value[xValueIndex],
|
319
|
-
minValue: data.filteredXYMinMaxData.minXDatum.value[xValueIndex],
|
320
|
-
axisWidth: data.containerSize.width,
|
321
|
-
scaleDomain: data.fullDataFormatter.xAxis.scaleDomain,
|
322
|
-
scaleRange: data.fullDataFormatter.xAxis.scaleRange,
|
323
|
-
})
|
324
|
-
const yScale = createAxisToValueScale({
|
325
|
-
maxValue: data.filteredXYMinMaxData.maxYDatum.value[yValueIndex],
|
326
|
-
minValue: data.filteredXYMinMaxData.minYDatum.value[yValueIndex],
|
327
|
-
axisWidth: data.containerSize.height,
|
328
|
-
scaleDomain: data.fullDataFormatter.yAxis.scaleDomain,
|
329
|
-
scaleRange: data.fullDataFormatter.yAxis.scaleRange,
|
330
|
-
reverse: true
|
331
|
-
})
|
332
|
-
subscriber.next({ xScale, yScale })
|
333
|
-
})
|
334
|
-
})
|
335
|
-
|
336
|
-
const axisValue$ = combineLatest({
|
337
|
-
rootMousemove: rootMousemove$,
|
338
|
-
// columnAmount: columnAmount$,
|
339
|
-
// rowAmount: rowAmount$,
|
340
|
-
layout: layout$,
|
341
|
-
containerPosition: containerPosition$
|
342
|
-
}).pipe(
|
343
|
-
switchMap(async d => d),
|
344
|
-
map(data => {
|
345
|
-
// 由於event座標是基於底層的,但是container會有多欄,所以要重新計算
|
346
|
-
// return {
|
347
|
-
// x: ((data.rootMousemove.offsetX - data.layout.left) / data.containerPosition[0].scale[0])
|
348
|
-
// % (data.layout.rootWidth / data.columnAmount / data.containerPosition[0].scale[0]),
|
349
|
-
// y: ((data.rootMousemove.offsetY - data.layout.top) / data.containerPosition[0].scale[1])
|
350
|
-
// % (data.layout.rootHeight / data.rowAmount / data.containerPosition[0].scale[1])
|
351
|
-
// }
|
352
|
-
const x = (() => {
|
353
|
-
let x = data.rootMousemove.offsetX
|
354
|
-
const rangeArr = data.containerPosition
|
355
|
-
.map((d, i) => [d.translate[0], data.containerPosition[i + 1]?.translate[0] ?? data.layout.rootWidth])
|
356
|
-
.filter(d => d[0] < d[1])
|
357
|
-
const range = rangeArr.find(d => x >= d[0] && x <= d[1])
|
358
|
-
if (range) {
|
359
|
-
x = x - range[0]
|
360
|
-
}
|
361
|
-
return x - data.layout.left
|
362
|
-
})()
|
363
|
-
|
364
|
-
const y = (() => {
|
365
|
-
let y = data.rootMousemove.offsetY
|
366
|
-
const rangeArr = data.containerPosition
|
367
|
-
.map((d, i) => [d.translate[1], data.containerPosition[i + 1]?.translate[1] ?? data.layout.rootHeight])
|
368
|
-
.filter(d => d[0] < d[1])
|
369
|
-
const range = rangeArr.find(d => y >= d[0] && y <= d[1])
|
370
|
-
if (range) {
|
371
|
-
y = y - range[0]
|
372
|
-
}
|
373
|
-
return y - data.layout.top
|
374
|
-
})()
|
375
|
-
|
376
|
-
return { x, y }
|
377
|
-
})
|
378
|
-
)
|
379
|
-
|
380
|
-
return combineLatest({
|
381
|
-
xyScale: xyScale$,
|
382
|
-
axisValue: axisValue$,
|
383
|
-
containerPosition: containerPosition$
|
384
|
-
}).pipe(
|
385
|
-
switchMap(async d => d),
|
386
|
-
map(data => {
|
387
|
-
return {
|
388
|
-
x: data.axisValue.x / data.containerPosition[0].scale[0],
|
389
|
-
y: data.axisValue.y / data.containerPosition[0].scale[1],
|
390
|
-
xValue: data.xyScale.xScale(data.axisValue.x),
|
391
|
-
yValue: data.xyScale.yScale(data.axisValue.y)
|
392
|
-
}
|
393
|
-
})
|
394
|
-
)
|
395
|
-
}
|
396
|
-
|
397
|
-
export const ordinalPositionObservable = ({ rootSelection, ordinalScaleDomain$, ordinalScale$, ordinalPadding$, containerSize$, containerPosition$, layout$ }: {
|
398
|
-
rootSelection: d3.Selection<any, unknown, any, unknown>
|
399
|
-
ordinalScaleDomain$: Observable<[number, number]>
|
400
|
-
ordinalScale$: Observable<d3.ScaleLinear<number, number>>
|
401
|
-
ordinalPadding$: Observable<number>
|
402
|
-
containerSize$: Observable<ContainerSize>
|
403
|
-
containerPosition$: Observable<ContainerPositionScaled[]>
|
404
|
-
layout$: Observable<Layout>
|
405
|
-
}) => {
|
406
|
-
const rootMousemove$ = d3EventObservable(rootSelection, 'mousemove').pipe(
|
407
|
-
debounceTime(2) // 避免過度頻繁觸發,實測時沒加電腦容易卡頓
|
408
|
-
)
|
409
|
-
|
410
|
-
// const columnAmount$ = containerPosition$.pipe(
|
411
|
-
// map(containerPosition => {
|
412
|
-
// const maxColumnIndex = containerPosition.reduce((acc, current) => {
|
413
|
-
// return current.columnIndex > acc ? current.columnIndex : acc
|
414
|
-
// }, 0)
|
415
|
-
// return maxColumnIndex + 1
|
416
|
-
// }),
|
417
|
-
// distinctUntilChanged(),
|
418
|
-
// shareReplay(1)
|
419
|
-
// )
|
420
|
-
|
421
|
-
const axisX$ = combineLatest({
|
422
|
-
rootMousemove: rootMousemove$,
|
423
|
-
// columnAmount: columnAmount$,
|
424
|
-
layout: layout$,
|
425
|
-
// containerSize: containerSize$,
|
426
|
-
containerPosition: containerPosition$,
|
427
|
-
}).pipe(
|
428
|
-
switchMap(async d => d),
|
429
|
-
map(data => {
|
430
|
-
// 由於event座標是基於底層的,但是container會有多欄,所以要重新計算
|
431
|
-
// return ((data.rootMousemove.offsetX - data.layout.left) / data.containerPosition[0].scale[0])
|
432
|
-
// % (data.layout.rootWidth / data.columnAmount / data.containerPosition[0].scale[0])
|
433
|
-
|
434
|
-
let x = data.rootMousemove.offsetX
|
435
|
-
const rangeArr = data.containerPosition
|
436
|
-
.map((d, i) => [d.translate[0], data.containerPosition[i + 1]?.translate[0] ?? data.layout.rootWidth])
|
437
|
-
.filter(d => d[0] < d[1])
|
438
|
-
const range = rangeArr.find(d => x >= d[0] && x <= d[1])
|
439
|
-
if (range) {
|
440
|
-
x = x - range[0]
|
441
|
-
}
|
442
|
-
return x - data.layout.left
|
443
|
-
})
|
444
|
-
)
|
445
|
-
|
446
|
-
const scaleRangeLabels$ = ordinalScaleDomain$.pipe(
|
447
|
-
map(data => {
|
448
|
-
const range = data[1] - data[0] + 1
|
449
|
-
return new Array(range).fill(0).map((d, i) => String(i + data[0]))
|
450
|
-
})
|
451
|
-
)
|
452
|
-
|
453
|
-
return combineLatest({
|
454
|
-
scaleRangeLabels: scaleRangeLabels$,
|
455
|
-
// layout: layout$,
|
456
|
-
containerSize: containerSize$,
|
457
|
-
axisX: axisX$,
|
458
|
-
ordinalScale: ordinalScale$,
|
459
|
-
ordinalPadding: ordinalPadding$,
|
460
|
-
ordinalScaleDomain: ordinalScaleDomain$,
|
461
|
-
containerPosition: containerPosition$
|
462
|
-
}).pipe(
|
463
|
-
switchMap(async d => d),
|
464
|
-
map(data => {
|
465
|
-
// 比例尺座標對應非連續資料索引
|
466
|
-
const xIndexScale = createAxisToLabelIndexScale({
|
467
|
-
axisLabels: data.scaleRangeLabels,
|
468
|
-
axisWidth: data.containerSize.width,
|
469
|
-
padding: 0.5,
|
470
|
-
reverse: false
|
471
|
-
})
|
472
|
-
|
473
|
-
const seq = xIndexScale(data.axisX)
|
474
|
-
const xIndex = seq + data.ordinalScaleDomain[0]
|
475
|
-
const x = (data.ordinalScale(xIndex) + data.ordinalPadding) / data.containerPosition[0].scale[0]
|
476
|
-
|
477
|
-
return {
|
478
|
-
x: x,
|
479
|
-
xValue: xIndex,
|
480
|
-
}
|
481
|
-
})
|
482
|
-
)
|
483
|
-
}
|
484
|
-
|
485
|
-
// 排名數量
|
486
|
-
export const computedRankingAmountObservable = ({ containerSize$, visibleComputedData$, textSizePx$, rankingAmount$ }: {
|
487
|
-
containerSize$: Observable<ContainerSize>
|
488
|
-
visibleComputedData$: Observable<ComputedDatumMultiValue[][]>
|
489
|
-
textSizePx$: Observable<number>
|
490
|
-
rankingAmount$: Observable<'auto' | number>
|
491
|
-
}) => {
|
492
|
-
const minLineHeightObservable = ({ textSizePx$ }: {
|
493
|
-
textSizePx$: Observable<number>
|
494
|
-
}) => {
|
495
|
-
return textSizePx$.pipe(
|
496
|
-
map(textSizePx => textSizePx * 2), // 2倍行高
|
497
|
-
shareReplay(1)
|
498
|
-
)
|
499
|
-
}
|
500
|
-
|
501
|
-
const containerHeightObservable = ({ minLineHeight$, containerSize$ }: {
|
502
|
-
minLineHeight$: Observable<number>
|
503
|
-
containerSize$: Observable<ContainerSize>
|
504
|
-
}) => {
|
505
|
-
return combineLatest({
|
506
|
-
minLineHeight: minLineHeight$,
|
507
|
-
containerSize: containerSize$
|
508
|
-
}).pipe(
|
509
|
-
switchMap(async (d) => d),
|
510
|
-
map(data => {
|
511
|
-
// 避免過小造成計算 scale 錯誤
|
512
|
-
return data.containerSize.height > data.minLineHeight
|
513
|
-
? data.containerSize.height
|
514
|
-
: data.minLineHeight
|
515
|
-
}),
|
516
|
-
distinctUntilChanged(),
|
517
|
-
shareReplay(1)
|
518
|
-
)
|
519
|
-
}
|
520
|
-
|
521
|
-
const rankingAmountLimitObservable = ({ minLineHeight$, containerHeight$ }: {
|
522
|
-
containerHeight$: Observable<number>
|
523
|
-
minLineHeight$: Observable<number>
|
524
|
-
}) => {
|
525
|
-
|
526
|
-
return combineLatest({
|
527
|
-
minLineHeight: minLineHeight$,
|
528
|
-
containerHeight: containerHeight$
|
529
|
-
}).pipe(
|
530
|
-
switchMap(async (d) => d),
|
531
|
-
map(data => {
|
532
|
-
const labelAmountLimit = Math.floor(data.containerHeight / data.minLineHeight)
|
533
|
-
return labelAmountLimit
|
534
|
-
}),
|
535
|
-
distinctUntilChanged(),
|
536
|
-
shareReplay(1)
|
537
|
-
)
|
538
|
-
}
|
539
|
-
|
540
|
-
const minLineHeight$ = minLineHeightObservable({ textSizePx$ })
|
541
|
-
|
542
|
-
const containerHeight$ = containerHeightObservable({
|
543
|
-
minLineHeight$,
|
544
|
-
containerSize$
|
545
|
-
})
|
546
|
-
|
547
|
-
const rankingAmountLimit$ = rankingAmountLimitObservable({
|
548
|
-
containerHeight$,
|
549
|
-
minLineHeight$
|
550
|
-
})
|
551
|
-
|
552
|
-
// 計算要排名的數量
|
553
|
-
return rankingAmount$.pipe(
|
554
|
-
switchMap(rankingAmount => {
|
555
|
-
return iif(
|
556
|
-
() => rankingAmount === 'auto',
|
557
|
-
// 'auto': 不超過限制
|
558
|
-
combineLatest({
|
559
|
-
visibleComputedData: visibleComputedData$,
|
560
|
-
rankingAmountLimit: rankingAmountLimit$,
|
561
|
-
}).pipe(
|
562
|
-
switchMap(async d => d),
|
563
|
-
map(data => {
|
564
|
-
const rankingAmountArr = data.visibleComputedData.map(categoryData => {
|
565
|
-
return Math.min(data.rankingAmountLimit, categoryData.length)
|
566
|
-
})
|
567
|
-
return Math.max(...rankingAmountArr) // 取所有 container 計算出來的最大值
|
568
|
-
})
|
569
|
-
),
|
570
|
-
// number: 指定數量
|
571
|
-
rankingAmount$ as Observable<number>,
|
572
|
-
)
|
573
|
-
})
|
574
|
-
)
|
575
|
-
}
|
576
|
-
|
577
|
-
export const rankingItemHeightObservable = ({ containerSize$, textSizePx$, computedRankingAmount$ }: {
|
578
|
-
containerSize$: Observable<ContainerSize>
|
579
|
-
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
580
|
-
textSizePx$: Observable<number>
|
581
|
-
// rankingAmount$: Observable<'auto' | number>
|
582
|
-
computedRankingAmount$: Observable<number>
|
583
|
-
}) => {
|
584
|
-
const minLineHeightObservable = ({ textSizePx$ }: {
|
585
|
-
textSizePx$: Observable<number>
|
586
|
-
}) => {
|
587
|
-
return textSizePx$.pipe(
|
588
|
-
map(textSizePx => textSizePx * 2), // 2倍行高
|
589
|
-
shareReplay(1)
|
590
|
-
)
|
591
|
-
}
|
592
|
-
|
593
|
-
const containerHeightObservable = ({ minLineHeight$, containerSize$ }: {
|
594
|
-
minLineHeight$: Observable<number>
|
595
|
-
containerSize$: Observable<ContainerSize>
|
596
|
-
}) => {
|
597
|
-
return combineLatest({
|
598
|
-
minLineHeight: minLineHeight$,
|
599
|
-
containerSize: containerSize$
|
600
|
-
}).pipe(
|
601
|
-
switchMap(async (d) => d),
|
602
|
-
map(data => {
|
603
|
-
// 避免過小造成計算 scale 錯誤
|
604
|
-
return data.containerSize.height > data.minLineHeight
|
605
|
-
? data.containerSize.height
|
606
|
-
: data.minLineHeight
|
607
|
-
}),
|
608
|
-
distinctUntilChanged(),
|
609
|
-
shareReplay(1)
|
610
|
-
)
|
611
|
-
}
|
612
|
-
|
613
|
-
const minLineHeight$ = minLineHeightObservable({ textSizePx$ })
|
614
|
-
|
615
|
-
const containerHeight$ = containerHeightObservable({
|
616
|
-
minLineHeight$,
|
617
|
-
containerSize$
|
618
|
-
})
|
619
|
-
|
620
|
-
return combineLatest({
|
621
|
-
containerHeight: containerHeight$,
|
622
|
-
computedRankingAmount: computedRankingAmount$
|
623
|
-
}).pipe(
|
624
|
-
switchMap(async (d) => d),
|
625
|
-
map(data => {
|
626
|
-
// // 依每個 category 計算 scale
|
627
|
-
// return data.visibleComputedRankingData.map((categoryData, i) => {
|
628
|
-
// const rankingAmount = data.computedRankingAmountList[i]
|
629
|
-
// const rankingItemHeight = data.containerHeight / rankingAmount
|
630
|
-
// return rankingItemHeight
|
631
|
-
// })
|
632
|
-
const rankingItemHeight = data.containerHeight / data.computedRankingAmount
|
633
|
-
return rankingItemHeight
|
634
|
-
})
|
635
|
-
)
|
636
|
-
}
|
637
|
-
|
638
|
-
export const rankingScaleListObservable = ({ visibleComputedRankingData$, rankingItemHeight$ }: {
|
639
|
-
visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
640
|
-
rankingItemHeight$: Observable<number>
|
641
|
-
}) => {
|
642
|
-
|
643
|
-
return combineLatest({
|
644
|
-
visibleComputedRankingData: visibleComputedRankingData$,
|
645
|
-
rankingItemHeight: rankingItemHeight$,
|
646
|
-
}).pipe(
|
647
|
-
switchMap(async (d) => d),
|
648
|
-
map(data => {
|
649
|
-
// 依每個 category 計算 scale
|
650
|
-
return data.visibleComputedRankingData.map((categoryData, i) => {
|
651
|
-
const allLabelAmount = categoryData.length
|
652
|
-
// const rankingItemHeight = data.rankingItemHeightList[i]
|
653
|
-
const totalHeight = data.rankingItemHeight * allLabelAmount // 有可能超出圖軸高度
|
654
|
-
|
655
|
-
return createLabelToAxisScale({
|
656
|
-
axisLabels: categoryData.map(d => d.label),
|
657
|
-
axisWidth: totalHeight,
|
658
|
-
padding: 0.5
|
659
|
-
})
|
660
|
-
})
|
661
|
-
})
|
662
|
-
)
|
663
|
-
}
|
664
|
-
|
665
|
-
|
666
|
-
// // Ranking資料 - 有 XY 資料 @Q@ 若沒用到要棄用
|
667
|
-
// export const computedRankingWithXYDataObservable = ({ visibleComputedRankingData$, computedRankingAmountList$, xyValueIndex$, layout$ }: {
|
668
|
-
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
669
|
-
// computedRankingAmountList$: Observable<number[]>
|
670
|
-
// xyValueIndex$: Observable<[number, number]>
|
671
|
-
// layout$: Observable<Layout>
|
672
|
-
// }): Observable<ComputedXYDataMultiValue> => {
|
673
|
-
|
674
|
-
// // // 未篩選範圍前的 scale
|
675
|
-
// // function createOriginXScale (xMinMax: { minX: number, maxX: number }, layout: Layout) {
|
676
|
-
// // let maxValue = xMinMax.maxX
|
677
|
-
// // let minValue = xMinMax.minX
|
678
|
-
// // if (minValue === maxValue && maxValue === 0) {
|
679
|
-
// // // 避免最大及最小值相同造成無法計算scale
|
680
|
-
// // maxValue = 1
|
681
|
-
// // }
|
682
|
-
// // const valueScale: d3.ScaleLinear<number, number> = createValueToAxisScale({
|
683
|
-
// // maxValue,
|
684
|
-
// // minValue,
|
685
|
-
// // axisWidth: layout.width,
|
686
|
-
// // scaleDomain: ['auto', 'auto'], // 不使用dataFormatter設定 --> 以0為基準到最大或最小值為範圍( * 如果是使用[minValue, maxValue]的話,在兩者很接近的情況下有可能造成scale倍率過高而svg變型時失真的情況)
|
687
|
-
// // scaleRange: [0, 1] // 不使用dataFormatter設定
|
688
|
-
// // })
|
689
|
-
|
690
|
-
// // return valueScale
|
691
|
-
// // }
|
692
|
-
|
693
|
-
// // 未篩選範圍及visible前的 scale
|
694
|
-
// function createOriginYScale (yMinMax: { minY: number, maxY: number }, layout: Layout) {
|
695
|
-
// let maxValue = yMinMax.maxY
|
696
|
-
// let minValue = yMinMax.minY
|
697
|
-
// if (minValue === maxValue && maxValue === 0) {
|
698
|
-
// // 避免最大及最小值相同造成無法計算scale
|
699
|
-
// maxValue = 1
|
700
|
-
// }
|
701
|
-
// const valueScale: d3.ScaleLinear<number, number> = createValueToAxisScale({
|
702
|
-
// maxValue,
|
703
|
-
// minValue,
|
704
|
-
// axisWidth: layout.height,
|
705
|
-
// scaleDomain: ['auto', 'auto'], // 不使用dataFormatter設定 --> 以0為基準到最大或最小值為範圍( * 如果是使用[minValue, maxValue]的話,在兩者很接近的情況下有可能造成scale倍率過高而svg變型時失真的情況)
|
706
|
-
// scaleRange: [0, 1], // 不使用dataFormatter設定
|
707
|
-
// // reverse: true
|
708
|
-
// })
|
709
|
-
|
710
|
-
// return valueScale
|
711
|
-
// }
|
712
|
-
|
713
|
-
// return combineLatest({
|
714
|
-
// visibleComputedRankingData: visibleComputedRankingData$,
|
715
|
-
// computedRankingAmountList: computedRankingAmountList$,
|
716
|
-
// xyValueIndex: xyValueIndex$,
|
717
|
-
// layout: layout$
|
718
|
-
// }).pipe(
|
719
|
-
// switchMap(async d => d),
|
720
|
-
// map(data => {
|
721
|
-
|
722
|
-
// // const maxX = data.visibleComputedRankingData
|
723
|
-
// // .flat()
|
724
|
-
// // .reduce((acc, current) => {
|
725
|
-
// // const maxXIndex = current.value.length - 1
|
726
|
-
// // return maxXIndex > acc ? maxXIndex : acc
|
727
|
-
// // }, 0)
|
728
|
-
// // const xMinMax = {
|
729
|
-
// // minX: 0,
|
730
|
-
// // maxX
|
731
|
-
// // }
|
732
|
-
// // const xScale = createOriginXScale(xMinMax, data.layout)
|
733
|
-
// // console.log('data.visibleComputedRankingData', data.visibleComputedRankingData)
|
734
|
-
// return data.visibleComputedRankingData
|
735
|
-
// .map((categoryData, categoryIndex) => {
|
736
|
-
// const yMinMax = {
|
737
|
-
// minY: 0,
|
738
|
-
// maxY: data.computedRankingAmountList[categoryIndex]
|
739
|
-
// }
|
740
|
-
// const yScale = createOriginYScale(yMinMax, data.layout)
|
741
|
-
|
742
|
-
// return categoryData.map((datum, datumIndex) => {
|
743
|
-
// return {
|
744
|
-
// ...datum,
|
745
|
-
// // axisX: xScale(datum.value[data.xyValueIndex[0]] ?? 0),
|
746
|
-
// axisX: 0,
|
747
|
-
// // axisY: yScale(datum.value[data.xyValueIndex[1]] ?? 0), // y軸的繪圖座標是從上到下,所以反轉
|
748
|
-
// axisY: yScale(datumIndex),
|
749
|
-
// }
|
750
|
-
// })
|
751
|
-
// })
|
752
|
-
// })
|
753
|
-
// )
|
754
|
-
// }
|
755
|
-
|
756
|
-
// // Ranking資料 - 有 XY 資料 @Q@ 若沒用到要棄用
|
757
|
-
// export const computedRankingWithXYDataObservable = ({ visibleComputedRankingData$, rankingScaleList$ }: {
|
758
|
-
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
759
|
-
// rankingScaleList$: Observable<d3.ScalePoint<string>[]>
|
760
|
-
// }): Observable<ComputedXYDataMultiValue> => {
|
761
|
-
|
762
|
-
// return combineLatest({
|
763
|
-
// visibleComputedRankingData: visibleComputedRankingData$,
|
764
|
-
// rankingScaleList: rankingScaleList$
|
765
|
-
// }).pipe(
|
766
|
-
// switchMap(async d => d),
|
767
|
-
// map(data => {
|
768
|
-
// return data.visibleComputedRankingData
|
769
|
-
// .map((categoryData, categoryIndex) => {
|
770
|
-
// const yScale = data.rankingScaleList[categoryIndex]
|
771
|
-
|
772
|
-
// return categoryData.map((datum, datumIndex) => {
|
773
|
-
// return {
|
774
|
-
// ...datum,
|
775
|
-
// axisX: 0,
|
776
|
-
// axisY: yScale(datum.label),
|
777
|
-
// }
|
778
|
-
// })
|
779
|
-
// })
|
780
|
-
// })
|
781
|
-
// )
|
1
|
+
import * as d3 from 'd3'
|
2
|
+
import {
|
3
|
+
Observable,
|
4
|
+
Subject,
|
5
|
+
debounceTime,
|
6
|
+
iif,
|
7
|
+
of,
|
8
|
+
takeUntil,
|
9
|
+
filter,
|
10
|
+
map,
|
11
|
+
switchMap,
|
12
|
+
combineLatest,
|
13
|
+
merge,
|
14
|
+
shareReplay,
|
15
|
+
distinctUntilChanged
|
16
|
+
} from 'rxjs'
|
17
|
+
import type {
|
18
|
+
ChartParams,
|
19
|
+
HighlightTarget,
|
20
|
+
DataFormatterMultiValue,
|
21
|
+
ComputedDataMultiValue,
|
22
|
+
ComputedDatumMultiValue,
|
23
|
+
ComputedXYDatumMultiValue,
|
24
|
+
ComputedXYDataMultiValue,
|
25
|
+
TransformData,
|
26
|
+
ContainerSize,
|
27
|
+
ContainerPositionScaled,
|
28
|
+
Layout,
|
29
|
+
} from '../../lib/core-types'
|
30
|
+
import {
|
31
|
+
createAxisToLabelIndexScale,
|
32
|
+
createAxisToValueScale,
|
33
|
+
createLabelToAxisScale,
|
34
|
+
createValueToAxisScale,
|
35
|
+
} from '../../lib/core'
|
36
|
+
import { getClassName, getUniID } from '../utils/orbchartsUtils'
|
37
|
+
import { d3EventObservable } from '../utils/observables'
|
38
|
+
|
39
|
+
// 建立 multiValue 主要的 selection
|
40
|
+
export const multiValueSelectionsObservable = ({ selection, pluginName, clipPathID, categoryLabels$, containerPosition$, graphicTransform$ }: {
|
41
|
+
selection: d3.Selection<any, unknown, any, unknown>
|
42
|
+
pluginName: string
|
43
|
+
clipPathID: string
|
44
|
+
// computedData$: Observable<ComputedDataMultiValue>
|
45
|
+
categoryLabels$: Observable<string[]>
|
46
|
+
containerPosition$: Observable<ContainerPositionScaled[]>
|
47
|
+
// multiValueAxesTransform$: Observable<TransformData>
|
48
|
+
graphicTransform$: Observable<TransformData>
|
49
|
+
}) => {
|
50
|
+
const categoryClassName = getClassName(pluginName, 'category')
|
51
|
+
const axesClassName = getClassName(pluginName, 'axes')
|
52
|
+
const graphicClassName = getClassName(pluginName, 'graphic')
|
53
|
+
|
54
|
+
// <g> category selection(container排放位置)
|
55
|
+
// <g> axes selection(圖軸)
|
56
|
+
// <defs> clipPath selection
|
57
|
+
// <g> graphic selection(圖形 scale 範圍的變形)
|
58
|
+
const categorySelection$ = categoryLabels$.pipe(
|
59
|
+
map((categoryLabels, i) => {
|
60
|
+
return selection
|
61
|
+
.selectAll<SVGGElement, string>(`g.${categoryClassName}`)
|
62
|
+
.data(categoryLabels, d => d)
|
63
|
+
.join(
|
64
|
+
enter => {
|
65
|
+
return enter
|
66
|
+
.append('g')
|
67
|
+
.classed(categoryClassName, true)
|
68
|
+
.each((d, i, g) => {
|
69
|
+
const axesSelection = d3.select(g[i])
|
70
|
+
.selectAll<SVGGElement, ComputedDatumMultiValue[]>(`g.${axesClassName}`)
|
71
|
+
.data([i])
|
72
|
+
.join(
|
73
|
+
enter => {
|
74
|
+
return enter
|
75
|
+
.append('g')
|
76
|
+
.classed(axesClassName, true)
|
77
|
+
.attr('clip-path', `url(#${clipPathID})`)
|
78
|
+
.each((d, i, g) => {
|
79
|
+
const defsSelection = d3.select(g[i])
|
80
|
+
.selectAll<SVGDefsElement, any>('defs')
|
81
|
+
.data([i])
|
82
|
+
.join('defs')
|
83
|
+
|
84
|
+
const graphicGSelection = d3.select(g[i])
|
85
|
+
.selectAll<SVGGElement, any>('g')
|
86
|
+
.data([i])
|
87
|
+
.join('g')
|
88
|
+
.classed(graphicClassName, true)
|
89
|
+
})
|
90
|
+
},
|
91
|
+
update => update,
|
92
|
+
exit => exit.remove()
|
93
|
+
)
|
94
|
+
})
|
95
|
+
},
|
96
|
+
update => update,
|
97
|
+
exit => exit.remove()
|
98
|
+
)
|
99
|
+
}),
|
100
|
+
shareReplay(1)
|
101
|
+
)
|
102
|
+
|
103
|
+
// <g> category selection
|
104
|
+
combineLatest({
|
105
|
+
categorySelection: categorySelection$,
|
106
|
+
containerPosition: containerPosition$
|
107
|
+
}).pipe(
|
108
|
+
switchMap(async d => d)
|
109
|
+
).subscribe(data => {
|
110
|
+
data.categorySelection
|
111
|
+
.transition()
|
112
|
+
.attr('transform', (d, i) => {
|
113
|
+
const containerPosition = data.containerPosition[i] ?? data.containerPosition[0]
|
114
|
+
const translate = containerPosition.translate
|
115
|
+
const scale = containerPosition.scale
|
116
|
+
return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
|
117
|
+
})
|
118
|
+
})
|
119
|
+
|
120
|
+
// <g> axes selection
|
121
|
+
const axesSelection$ = categorySelection$.pipe(
|
122
|
+
map(categorySelection => {
|
123
|
+
return categorySelection
|
124
|
+
.select<SVGGElement>(`g.${axesClassName}`)
|
125
|
+
}),
|
126
|
+
shareReplay(1)
|
127
|
+
)
|
128
|
+
|
129
|
+
// <defs> clipPath selection
|
130
|
+
const defsSelection$ = axesSelection$.pipe(
|
131
|
+
map(axesSelection => {
|
132
|
+
return axesSelection.select<SVGDefsElement>('defs')
|
133
|
+
}),
|
134
|
+
shareReplay(1)
|
135
|
+
)
|
136
|
+
|
137
|
+
// <g> graphic selection
|
138
|
+
const graphicGSelection$ = combineLatest({
|
139
|
+
axesSelection: axesSelection$,
|
140
|
+
graphicTransform: graphicTransform$
|
141
|
+
}).pipe(
|
142
|
+
switchMap(async d => d),
|
143
|
+
map(data => {
|
144
|
+
const graphicGSelection = data.axesSelection
|
145
|
+
.select<SVGGElement>(`g.${graphicClassName}`)
|
146
|
+
graphicGSelection
|
147
|
+
.transition()
|
148
|
+
.duration(50)
|
149
|
+
.style('transform', data.graphicTransform.value)
|
150
|
+
return graphicGSelection
|
151
|
+
}),
|
152
|
+
shareReplay(1)
|
153
|
+
)
|
154
|
+
|
155
|
+
return {
|
156
|
+
categorySelection$,
|
157
|
+
axesSelection$,
|
158
|
+
defsSelection$,
|
159
|
+
graphicGSelection$
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
// 建立 multiValue 主要的 selection - 只取無scale的container selection
|
164
|
+
export const multiValueContainerSelectionsObservable = ({ selection, pluginName, clipPathID, computedData$, containerPosition$, isCategorySeprate$ }: {
|
165
|
+
selection: d3.Selection<any, unknown, any, unknown>
|
166
|
+
pluginName: string
|
167
|
+
clipPathID: string | null
|
168
|
+
computedData$: Observable<ComputedDataMultiValue>
|
169
|
+
containerPosition$: Observable<ContainerPositionScaled[]>
|
170
|
+
isCategorySeprate$: Observable<boolean>
|
171
|
+
}) => {
|
172
|
+
const containerClassName = getClassName(pluginName, 'container')
|
173
|
+
|
174
|
+
const containerSelection$ = combineLatest({
|
175
|
+
computedData: computedData$.pipe(
|
176
|
+
distinctUntilChanged((a, b) => {
|
177
|
+
// 只有當series的數量改變時,才重新計算
|
178
|
+
return a.length === b.length
|
179
|
+
}),
|
180
|
+
),
|
181
|
+
isCategorySeprate: isCategorySeprate$
|
182
|
+
}).pipe(
|
183
|
+
switchMap(async (d) => d),
|
184
|
+
map(data => {
|
185
|
+
return data.isCategorySeprate
|
186
|
+
// category分開的時候顯示各別axis
|
187
|
+
? data.computedData
|
188
|
+
// category合併的時候只顯示第一個axis
|
189
|
+
: [data.computedData[0]]
|
190
|
+
}),
|
191
|
+
map((computedData, i) => {
|
192
|
+
return selection
|
193
|
+
.selectAll<SVGGElement, ComputedDatumMultiValue[]>(`g.${containerClassName}`)
|
194
|
+
.data(computedData, d => (d && d[0]) ? d[0].categoryIndex : i)
|
195
|
+
.join('g')
|
196
|
+
.classed(containerClassName, true)
|
197
|
+
.attr('clip-path', _ => clipPathID ? `url(#${clipPathID})` : 'none')
|
198
|
+
}),
|
199
|
+
shareReplay(1)
|
200
|
+
)
|
201
|
+
|
202
|
+
combineLatest({
|
203
|
+
containerSelection: containerSelection$,
|
204
|
+
gridContainerPosition: containerPosition$
|
205
|
+
}).pipe(
|
206
|
+
switchMap(async d => d)
|
207
|
+
).subscribe(data => {
|
208
|
+
data.containerSelection
|
209
|
+
.attr('transform', (d, i) => {
|
210
|
+
const gridContainerPosition = data.gridContainerPosition[i] ?? data.gridContainerPosition[0]
|
211
|
+
const translate = gridContainerPosition.translate
|
212
|
+
const scale = gridContainerPosition.scale
|
213
|
+
// return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})`
|
214
|
+
return `translate(${translate[0]}, ${translate[1]})`
|
215
|
+
})
|
216
|
+
// .attr('opacity', 0)
|
217
|
+
// .transition()
|
218
|
+
// .attr('opacity', 1)
|
219
|
+
})
|
220
|
+
|
221
|
+
return containerSelection$
|
222
|
+
}
|
223
|
+
|
224
|
+
|
225
|
+
export const multiValueXYPositionObservable = ({ rootSelection, fullDataFormatter$, filteredXYMinMaxData$, containerPosition$, containerSize$, layout$ }: {
|
226
|
+
rootSelection: d3.Selection<any, unknown, any, unknown>
|
227
|
+
fullDataFormatter$: Observable<DataFormatterMultiValue>
|
228
|
+
// computedData$: Observable<ComputedDataMultiValue>
|
229
|
+
// xyMinMax$: Observable<{ minX: number, maxX: number, minY: number, maxY: number }>
|
230
|
+
filteredXYMinMaxData$: Observable<{
|
231
|
+
minXDatum: ComputedXYDatumMultiValue
|
232
|
+
maxXDatum: ComputedXYDatumMultiValue
|
233
|
+
minYDatum: ComputedXYDatumMultiValue
|
234
|
+
maxYDatum: ComputedXYDatumMultiValue
|
235
|
+
}>
|
236
|
+
containerPosition$: Observable<ContainerPositionScaled[]>
|
237
|
+
containerSize$: Observable<ContainerSize>
|
238
|
+
layout$: Observable<Layout>
|
239
|
+
}) => {
|
240
|
+
const rootMousemove$ = d3EventObservable(rootSelection, 'mousemove').pipe(
|
241
|
+
debounceTime(2) // 避免過度頻繁觸發,實測時沒加電腦容易卡頓
|
242
|
+
)
|
243
|
+
|
244
|
+
// const columnAmount$ = containerPosition$.pipe(
|
245
|
+
// map(containerPosition => {
|
246
|
+
// const maxColumnIndex = containerPosition.reduce((acc, current) => {
|
247
|
+
// return current.columnIndex > acc ? current.columnIndex : acc
|
248
|
+
// }, 0)
|
249
|
+
// return maxColumnIndex + 1
|
250
|
+
// }),
|
251
|
+
// distinctUntilChanged(),
|
252
|
+
// shareReplay(1)
|
253
|
+
// )
|
254
|
+
|
255
|
+
// const rowAmount$ = containerPosition$.pipe(
|
256
|
+
// map(containerPosition => {
|
257
|
+
// const maxRowIndex = containerPosition.reduce((acc, current) => {
|
258
|
+
// return current.rowIndex > acc ? current.rowIndex : acc
|
259
|
+
// }, 0)
|
260
|
+
// return maxRowIndex + 1
|
261
|
+
// }),
|
262
|
+
// distinctUntilChanged(),
|
263
|
+
// shareReplay(1)
|
264
|
+
// )
|
265
|
+
|
266
|
+
// const xyScale$ = combineLatest({
|
267
|
+
// layout: layout$,
|
268
|
+
// filteredXYMinMaxData: filteredXYMinMaxData$,
|
269
|
+
// fullDataFormatter: fullDataFormatter$,
|
270
|
+
// columnAmount: columnAmount$,
|
271
|
+
// rowAmount: rowAmount$
|
272
|
+
// }).pipe(
|
273
|
+
// switchMap(async d => d),
|
274
|
+
// map(data => {
|
275
|
+
// const xScale = createAxisToValueScale({
|
276
|
+
// maxValue: data.filteredXYMinMaxData.maxXDatum.value[0],
|
277
|
+
// minValue: data.filteredXYMinMaxData.minXDatum.value[0],
|
278
|
+
// axisWidth: data.layout.width,
|
279
|
+
// scaleDomain: data.fullDataFormatter.xAxis.scaleDomain,
|
280
|
+
// scaleRange: data.fullDataFormatter.xAxis.scaleRange,
|
281
|
+
// })
|
282
|
+
// const yScale = createAxisToValueScale({
|
283
|
+
// maxValue: data.filteredXYMinMaxData.maxYDatum.value[1],
|
284
|
+
// minValue: data.filteredXYMinMaxData.minYDatum.value[1],
|
285
|
+
// axisWidth: data.layout.height,
|
286
|
+
// scaleDomain: data.fullDataFormatter.yAxis.scaleDomain,
|
287
|
+
// scaleRange: data.fullDataFormatter.yAxis.scaleRange,
|
288
|
+
// reverse: true
|
289
|
+
// })
|
290
|
+
// return { xScale, yScale }
|
291
|
+
// })
|
292
|
+
// )
|
293
|
+
|
294
|
+
const xyScale$: Observable<{
|
295
|
+
xScale: d3.ScaleLinear<number, number, never>;
|
296
|
+
yScale: d3.ScaleLinear<number, number, never>;
|
297
|
+
}> = new Observable(subscriber => {
|
298
|
+
combineLatest({
|
299
|
+
// layout: layout$,
|
300
|
+
containerSize: containerSize$,
|
301
|
+
filteredXYMinMaxData: filteredXYMinMaxData$,
|
302
|
+
fullDataFormatter: fullDataFormatter$,
|
303
|
+
// columnAmount: columnAmount$,
|
304
|
+
// rowAmount: rowAmount$
|
305
|
+
}).pipe(
|
306
|
+
switchMap(async d => d),
|
307
|
+
).subscribe(data => {
|
308
|
+
const xValueIndex = data.fullDataFormatter.xAxis.valueIndex
|
309
|
+
const yValueIndex = data.fullDataFormatter.yAxis.valueIndex
|
310
|
+
if (!data.filteredXYMinMaxData.minXDatum || !data.filteredXYMinMaxData.maxXDatum
|
311
|
+
|| data.filteredXYMinMaxData.minXDatum.value[xValueIndex] == null || data.filteredXYMinMaxData.maxXDatum.value[xValueIndex] == null
|
312
|
+
|| !data.filteredXYMinMaxData.minYDatum || !data.filteredXYMinMaxData.maxYDatum
|
313
|
+
|| data.filteredXYMinMaxData.minYDatum.value[yValueIndex] == null || data.filteredXYMinMaxData.maxYDatum.value[yValueIndex] == null
|
314
|
+
) {
|
315
|
+
return
|
316
|
+
}
|
317
|
+
const xScale = createAxisToValueScale({
|
318
|
+
maxValue: data.filteredXYMinMaxData.maxXDatum.value[xValueIndex],
|
319
|
+
minValue: data.filteredXYMinMaxData.minXDatum.value[xValueIndex],
|
320
|
+
axisWidth: data.containerSize.width,
|
321
|
+
scaleDomain: data.fullDataFormatter.xAxis.scaleDomain,
|
322
|
+
scaleRange: data.fullDataFormatter.xAxis.scaleRange,
|
323
|
+
})
|
324
|
+
const yScale = createAxisToValueScale({
|
325
|
+
maxValue: data.filteredXYMinMaxData.maxYDatum.value[yValueIndex],
|
326
|
+
minValue: data.filteredXYMinMaxData.minYDatum.value[yValueIndex],
|
327
|
+
axisWidth: data.containerSize.height,
|
328
|
+
scaleDomain: data.fullDataFormatter.yAxis.scaleDomain,
|
329
|
+
scaleRange: data.fullDataFormatter.yAxis.scaleRange,
|
330
|
+
reverse: true
|
331
|
+
})
|
332
|
+
subscriber.next({ xScale, yScale })
|
333
|
+
})
|
334
|
+
})
|
335
|
+
|
336
|
+
const axisValue$ = combineLatest({
|
337
|
+
rootMousemove: rootMousemove$,
|
338
|
+
// columnAmount: columnAmount$,
|
339
|
+
// rowAmount: rowAmount$,
|
340
|
+
layout: layout$,
|
341
|
+
containerPosition: containerPosition$
|
342
|
+
}).pipe(
|
343
|
+
switchMap(async d => d),
|
344
|
+
map(data => {
|
345
|
+
// 由於event座標是基於底層的,但是container會有多欄,所以要重新計算
|
346
|
+
// return {
|
347
|
+
// x: ((data.rootMousemove.offsetX - data.layout.left) / data.containerPosition[0].scale[0])
|
348
|
+
// % (data.layout.rootWidth / data.columnAmount / data.containerPosition[0].scale[0]),
|
349
|
+
// y: ((data.rootMousemove.offsetY - data.layout.top) / data.containerPosition[0].scale[1])
|
350
|
+
// % (data.layout.rootHeight / data.rowAmount / data.containerPosition[0].scale[1])
|
351
|
+
// }
|
352
|
+
const x = (() => {
|
353
|
+
let x = data.rootMousemove.offsetX
|
354
|
+
const rangeArr = data.containerPosition
|
355
|
+
.map((d, i) => [d.translate[0], data.containerPosition[i + 1]?.translate[0] ?? data.layout.rootWidth])
|
356
|
+
.filter(d => d[0] < d[1])
|
357
|
+
const range = rangeArr.find(d => x >= d[0] && x <= d[1])
|
358
|
+
if (range) {
|
359
|
+
x = x - range[0]
|
360
|
+
}
|
361
|
+
return x - data.layout.left
|
362
|
+
})()
|
363
|
+
|
364
|
+
const y = (() => {
|
365
|
+
let y = data.rootMousemove.offsetY
|
366
|
+
const rangeArr = data.containerPosition
|
367
|
+
.map((d, i) => [d.translate[1], data.containerPosition[i + 1]?.translate[1] ?? data.layout.rootHeight])
|
368
|
+
.filter(d => d[0] < d[1])
|
369
|
+
const range = rangeArr.find(d => y >= d[0] && y <= d[1])
|
370
|
+
if (range) {
|
371
|
+
y = y - range[0]
|
372
|
+
}
|
373
|
+
return y - data.layout.top
|
374
|
+
})()
|
375
|
+
|
376
|
+
return { x, y }
|
377
|
+
})
|
378
|
+
)
|
379
|
+
|
380
|
+
return combineLatest({
|
381
|
+
xyScale: xyScale$,
|
382
|
+
axisValue: axisValue$,
|
383
|
+
containerPosition: containerPosition$
|
384
|
+
}).pipe(
|
385
|
+
switchMap(async d => d),
|
386
|
+
map(data => {
|
387
|
+
return {
|
388
|
+
x: data.axisValue.x / data.containerPosition[0].scale[0],
|
389
|
+
y: data.axisValue.y / data.containerPosition[0].scale[1],
|
390
|
+
xValue: data.xyScale.xScale(data.axisValue.x),
|
391
|
+
yValue: data.xyScale.yScale(data.axisValue.y)
|
392
|
+
}
|
393
|
+
})
|
394
|
+
)
|
395
|
+
}
|
396
|
+
|
397
|
+
export const ordinalPositionObservable = ({ rootSelection, ordinalScaleDomain$, ordinalScale$, ordinalPadding$, containerSize$, containerPosition$, layout$ }: {
|
398
|
+
rootSelection: d3.Selection<any, unknown, any, unknown>
|
399
|
+
ordinalScaleDomain$: Observable<[number, number]>
|
400
|
+
ordinalScale$: Observable<d3.ScaleLinear<number, number>>
|
401
|
+
ordinalPadding$: Observable<number>
|
402
|
+
containerSize$: Observable<ContainerSize>
|
403
|
+
containerPosition$: Observable<ContainerPositionScaled[]>
|
404
|
+
layout$: Observable<Layout>
|
405
|
+
}) => {
|
406
|
+
const rootMousemove$ = d3EventObservable(rootSelection, 'mousemove').pipe(
|
407
|
+
debounceTime(2) // 避免過度頻繁觸發,實測時沒加電腦容易卡頓
|
408
|
+
)
|
409
|
+
|
410
|
+
// const columnAmount$ = containerPosition$.pipe(
|
411
|
+
// map(containerPosition => {
|
412
|
+
// const maxColumnIndex = containerPosition.reduce((acc, current) => {
|
413
|
+
// return current.columnIndex > acc ? current.columnIndex : acc
|
414
|
+
// }, 0)
|
415
|
+
// return maxColumnIndex + 1
|
416
|
+
// }),
|
417
|
+
// distinctUntilChanged(),
|
418
|
+
// shareReplay(1)
|
419
|
+
// )
|
420
|
+
|
421
|
+
const axisX$ = combineLatest({
|
422
|
+
rootMousemove: rootMousemove$,
|
423
|
+
// columnAmount: columnAmount$,
|
424
|
+
layout: layout$,
|
425
|
+
// containerSize: containerSize$,
|
426
|
+
containerPosition: containerPosition$,
|
427
|
+
}).pipe(
|
428
|
+
switchMap(async d => d),
|
429
|
+
map(data => {
|
430
|
+
// 由於event座標是基於底層的,但是container會有多欄,所以要重新計算
|
431
|
+
// return ((data.rootMousemove.offsetX - data.layout.left) / data.containerPosition[0].scale[0])
|
432
|
+
// % (data.layout.rootWidth / data.columnAmount / data.containerPosition[0].scale[0])
|
433
|
+
|
434
|
+
let x = data.rootMousemove.offsetX
|
435
|
+
const rangeArr = data.containerPosition
|
436
|
+
.map((d, i) => [d.translate[0], data.containerPosition[i + 1]?.translate[0] ?? data.layout.rootWidth])
|
437
|
+
.filter(d => d[0] < d[1])
|
438
|
+
const range = rangeArr.find(d => x >= d[0] && x <= d[1])
|
439
|
+
if (range) {
|
440
|
+
x = x - range[0]
|
441
|
+
}
|
442
|
+
return x - data.layout.left
|
443
|
+
})
|
444
|
+
)
|
445
|
+
|
446
|
+
const scaleRangeLabels$ = ordinalScaleDomain$.pipe(
|
447
|
+
map(data => {
|
448
|
+
const range = data[1] - data[0] + 1
|
449
|
+
return new Array(range).fill(0).map((d, i) => String(i + data[0]))
|
450
|
+
})
|
451
|
+
)
|
452
|
+
|
453
|
+
return combineLatest({
|
454
|
+
scaleRangeLabels: scaleRangeLabels$,
|
455
|
+
// layout: layout$,
|
456
|
+
containerSize: containerSize$,
|
457
|
+
axisX: axisX$,
|
458
|
+
ordinalScale: ordinalScale$,
|
459
|
+
ordinalPadding: ordinalPadding$,
|
460
|
+
ordinalScaleDomain: ordinalScaleDomain$,
|
461
|
+
containerPosition: containerPosition$
|
462
|
+
}).pipe(
|
463
|
+
switchMap(async d => d),
|
464
|
+
map(data => {
|
465
|
+
// 比例尺座標對應非連續資料索引
|
466
|
+
const xIndexScale = createAxisToLabelIndexScale({
|
467
|
+
axisLabels: data.scaleRangeLabels,
|
468
|
+
axisWidth: data.containerSize.width,
|
469
|
+
padding: 0.5,
|
470
|
+
reverse: false
|
471
|
+
})
|
472
|
+
|
473
|
+
const seq = xIndexScale(data.axisX)
|
474
|
+
const xIndex = seq + data.ordinalScaleDomain[0]
|
475
|
+
const x = (data.ordinalScale(xIndex) + data.ordinalPadding) / data.containerPosition[0].scale[0]
|
476
|
+
|
477
|
+
return {
|
478
|
+
x: x,
|
479
|
+
xValue: xIndex,
|
480
|
+
}
|
481
|
+
})
|
482
|
+
)
|
483
|
+
}
|
484
|
+
|
485
|
+
// 排名數量
|
486
|
+
export const computedRankingAmountObservable = ({ containerSize$, visibleComputedData$, textSizePx$, rankingAmount$ }: {
|
487
|
+
containerSize$: Observable<ContainerSize>
|
488
|
+
visibleComputedData$: Observable<ComputedDatumMultiValue[][]>
|
489
|
+
textSizePx$: Observable<number>
|
490
|
+
rankingAmount$: Observable<'auto' | number>
|
491
|
+
}) => {
|
492
|
+
const minLineHeightObservable = ({ textSizePx$ }: {
|
493
|
+
textSizePx$: Observable<number>
|
494
|
+
}) => {
|
495
|
+
return textSizePx$.pipe(
|
496
|
+
map(textSizePx => textSizePx * 2), // 2倍行高
|
497
|
+
shareReplay(1)
|
498
|
+
)
|
499
|
+
}
|
500
|
+
|
501
|
+
const containerHeightObservable = ({ minLineHeight$, containerSize$ }: {
|
502
|
+
minLineHeight$: Observable<number>
|
503
|
+
containerSize$: Observable<ContainerSize>
|
504
|
+
}) => {
|
505
|
+
return combineLatest({
|
506
|
+
minLineHeight: minLineHeight$,
|
507
|
+
containerSize: containerSize$
|
508
|
+
}).pipe(
|
509
|
+
switchMap(async (d) => d),
|
510
|
+
map(data => {
|
511
|
+
// 避免過小造成計算 scale 錯誤
|
512
|
+
return data.containerSize.height > data.minLineHeight
|
513
|
+
? data.containerSize.height
|
514
|
+
: data.minLineHeight
|
515
|
+
}),
|
516
|
+
distinctUntilChanged(),
|
517
|
+
shareReplay(1)
|
518
|
+
)
|
519
|
+
}
|
520
|
+
|
521
|
+
const rankingAmountLimitObservable = ({ minLineHeight$, containerHeight$ }: {
|
522
|
+
containerHeight$: Observable<number>
|
523
|
+
minLineHeight$: Observable<number>
|
524
|
+
}) => {
|
525
|
+
|
526
|
+
return combineLatest({
|
527
|
+
minLineHeight: minLineHeight$,
|
528
|
+
containerHeight: containerHeight$
|
529
|
+
}).pipe(
|
530
|
+
switchMap(async (d) => d),
|
531
|
+
map(data => {
|
532
|
+
const labelAmountLimit = Math.floor(data.containerHeight / data.minLineHeight)
|
533
|
+
return labelAmountLimit
|
534
|
+
}),
|
535
|
+
distinctUntilChanged(),
|
536
|
+
shareReplay(1)
|
537
|
+
)
|
538
|
+
}
|
539
|
+
|
540
|
+
const minLineHeight$ = minLineHeightObservable({ textSizePx$ })
|
541
|
+
|
542
|
+
const containerHeight$ = containerHeightObservable({
|
543
|
+
minLineHeight$,
|
544
|
+
containerSize$
|
545
|
+
})
|
546
|
+
|
547
|
+
const rankingAmountLimit$ = rankingAmountLimitObservable({
|
548
|
+
containerHeight$,
|
549
|
+
minLineHeight$
|
550
|
+
})
|
551
|
+
|
552
|
+
// 計算要排名的數量
|
553
|
+
return rankingAmount$.pipe(
|
554
|
+
switchMap(rankingAmount => {
|
555
|
+
return iif(
|
556
|
+
() => rankingAmount === 'auto',
|
557
|
+
// 'auto': 不超過限制
|
558
|
+
combineLatest({
|
559
|
+
visibleComputedData: visibleComputedData$,
|
560
|
+
rankingAmountLimit: rankingAmountLimit$,
|
561
|
+
}).pipe(
|
562
|
+
switchMap(async d => d),
|
563
|
+
map(data => {
|
564
|
+
const rankingAmountArr = data.visibleComputedData.map(categoryData => {
|
565
|
+
return Math.min(data.rankingAmountLimit, categoryData.length)
|
566
|
+
})
|
567
|
+
return Math.max(...rankingAmountArr) // 取所有 container 計算出來的最大值
|
568
|
+
})
|
569
|
+
),
|
570
|
+
// number: 指定數量
|
571
|
+
rankingAmount$ as Observable<number>,
|
572
|
+
)
|
573
|
+
})
|
574
|
+
)
|
575
|
+
}
|
576
|
+
|
577
|
+
export const rankingItemHeightObservable = ({ containerSize$, textSizePx$, computedRankingAmount$ }: {
|
578
|
+
containerSize$: Observable<ContainerSize>
|
579
|
+
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
580
|
+
textSizePx$: Observable<number>
|
581
|
+
// rankingAmount$: Observable<'auto' | number>
|
582
|
+
computedRankingAmount$: Observable<number>
|
583
|
+
}) => {
|
584
|
+
const minLineHeightObservable = ({ textSizePx$ }: {
|
585
|
+
textSizePx$: Observable<number>
|
586
|
+
}) => {
|
587
|
+
return textSizePx$.pipe(
|
588
|
+
map(textSizePx => textSizePx * 2), // 2倍行高
|
589
|
+
shareReplay(1)
|
590
|
+
)
|
591
|
+
}
|
592
|
+
|
593
|
+
const containerHeightObservable = ({ minLineHeight$, containerSize$ }: {
|
594
|
+
minLineHeight$: Observable<number>
|
595
|
+
containerSize$: Observable<ContainerSize>
|
596
|
+
}) => {
|
597
|
+
return combineLatest({
|
598
|
+
minLineHeight: minLineHeight$,
|
599
|
+
containerSize: containerSize$
|
600
|
+
}).pipe(
|
601
|
+
switchMap(async (d) => d),
|
602
|
+
map(data => {
|
603
|
+
// 避免過小造成計算 scale 錯誤
|
604
|
+
return data.containerSize.height > data.minLineHeight
|
605
|
+
? data.containerSize.height
|
606
|
+
: data.minLineHeight
|
607
|
+
}),
|
608
|
+
distinctUntilChanged(),
|
609
|
+
shareReplay(1)
|
610
|
+
)
|
611
|
+
}
|
612
|
+
|
613
|
+
const minLineHeight$ = minLineHeightObservable({ textSizePx$ })
|
614
|
+
|
615
|
+
const containerHeight$ = containerHeightObservable({
|
616
|
+
minLineHeight$,
|
617
|
+
containerSize$
|
618
|
+
})
|
619
|
+
|
620
|
+
return combineLatest({
|
621
|
+
containerHeight: containerHeight$,
|
622
|
+
computedRankingAmount: computedRankingAmount$
|
623
|
+
}).pipe(
|
624
|
+
switchMap(async (d) => d),
|
625
|
+
map(data => {
|
626
|
+
// // 依每個 category 計算 scale
|
627
|
+
// return data.visibleComputedRankingData.map((categoryData, i) => {
|
628
|
+
// const rankingAmount = data.computedRankingAmountList[i]
|
629
|
+
// const rankingItemHeight = data.containerHeight / rankingAmount
|
630
|
+
// return rankingItemHeight
|
631
|
+
// })
|
632
|
+
const rankingItemHeight = data.containerHeight / data.computedRankingAmount
|
633
|
+
return rankingItemHeight
|
634
|
+
})
|
635
|
+
)
|
636
|
+
}
|
637
|
+
|
638
|
+
export const rankingScaleListObservable = ({ visibleComputedRankingData$, rankingItemHeight$ }: {
|
639
|
+
visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
640
|
+
rankingItemHeight$: Observable<number>
|
641
|
+
}) => {
|
642
|
+
|
643
|
+
return combineLatest({
|
644
|
+
visibleComputedRankingData: visibleComputedRankingData$,
|
645
|
+
rankingItemHeight: rankingItemHeight$,
|
646
|
+
}).pipe(
|
647
|
+
switchMap(async (d) => d),
|
648
|
+
map(data => {
|
649
|
+
// 依每個 category 計算 scale
|
650
|
+
return data.visibleComputedRankingData.map((categoryData, i) => {
|
651
|
+
const allLabelAmount = categoryData.length
|
652
|
+
// const rankingItemHeight = data.rankingItemHeightList[i]
|
653
|
+
const totalHeight = data.rankingItemHeight * allLabelAmount // 有可能超出圖軸高度
|
654
|
+
|
655
|
+
return createLabelToAxisScale({
|
656
|
+
axisLabels: categoryData.map(d => d.label),
|
657
|
+
axisWidth: totalHeight,
|
658
|
+
padding: 0.5
|
659
|
+
})
|
660
|
+
})
|
661
|
+
})
|
662
|
+
)
|
663
|
+
}
|
664
|
+
|
665
|
+
|
666
|
+
// // Ranking資料 - 有 XY 資料 @Q@ 若沒用到要棄用
|
667
|
+
// export const computedRankingWithXYDataObservable = ({ visibleComputedRankingData$, computedRankingAmountList$, xyValueIndex$, layout$ }: {
|
668
|
+
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
669
|
+
// computedRankingAmountList$: Observable<number[]>
|
670
|
+
// xyValueIndex$: Observable<[number, number]>
|
671
|
+
// layout$: Observable<Layout>
|
672
|
+
// }): Observable<ComputedXYDataMultiValue> => {
|
673
|
+
|
674
|
+
// // // 未篩選範圍前的 scale
|
675
|
+
// // function createOriginXScale (xMinMax: { minX: number, maxX: number }, layout: Layout) {
|
676
|
+
// // let maxValue = xMinMax.maxX
|
677
|
+
// // let minValue = xMinMax.minX
|
678
|
+
// // if (minValue === maxValue && maxValue === 0) {
|
679
|
+
// // // 避免最大及最小值相同造成無法計算scale
|
680
|
+
// // maxValue = 1
|
681
|
+
// // }
|
682
|
+
// // const valueScale: d3.ScaleLinear<number, number> = createValueToAxisScale({
|
683
|
+
// // maxValue,
|
684
|
+
// // minValue,
|
685
|
+
// // axisWidth: layout.width,
|
686
|
+
// // scaleDomain: ['auto', 'auto'], // 不使用dataFormatter設定 --> 以0為基準到最大或最小值為範圍( * 如果是使用[minValue, maxValue]的話,在兩者很接近的情況下有可能造成scale倍率過高而svg變型時失真的情況)
|
687
|
+
// // scaleRange: [0, 1] // 不使用dataFormatter設定
|
688
|
+
// // })
|
689
|
+
|
690
|
+
// // return valueScale
|
691
|
+
// // }
|
692
|
+
|
693
|
+
// // 未篩選範圍及visible前的 scale
|
694
|
+
// function createOriginYScale (yMinMax: { minY: number, maxY: number }, layout: Layout) {
|
695
|
+
// let maxValue = yMinMax.maxY
|
696
|
+
// let minValue = yMinMax.minY
|
697
|
+
// if (minValue === maxValue && maxValue === 0) {
|
698
|
+
// // 避免最大及最小值相同造成無法計算scale
|
699
|
+
// maxValue = 1
|
700
|
+
// }
|
701
|
+
// const valueScale: d3.ScaleLinear<number, number> = createValueToAxisScale({
|
702
|
+
// maxValue,
|
703
|
+
// minValue,
|
704
|
+
// axisWidth: layout.height,
|
705
|
+
// scaleDomain: ['auto', 'auto'], // 不使用dataFormatter設定 --> 以0為基準到最大或最小值為範圍( * 如果是使用[minValue, maxValue]的話,在兩者很接近的情況下有可能造成scale倍率過高而svg變型時失真的情況)
|
706
|
+
// scaleRange: [0, 1], // 不使用dataFormatter設定
|
707
|
+
// // reverse: true
|
708
|
+
// })
|
709
|
+
|
710
|
+
// return valueScale
|
711
|
+
// }
|
712
|
+
|
713
|
+
// return combineLatest({
|
714
|
+
// visibleComputedRankingData: visibleComputedRankingData$,
|
715
|
+
// computedRankingAmountList: computedRankingAmountList$,
|
716
|
+
// xyValueIndex: xyValueIndex$,
|
717
|
+
// layout: layout$
|
718
|
+
// }).pipe(
|
719
|
+
// switchMap(async d => d),
|
720
|
+
// map(data => {
|
721
|
+
|
722
|
+
// // const maxX = data.visibleComputedRankingData
|
723
|
+
// // .flat()
|
724
|
+
// // .reduce((acc, current) => {
|
725
|
+
// // const maxXIndex = current.value.length - 1
|
726
|
+
// // return maxXIndex > acc ? maxXIndex : acc
|
727
|
+
// // }, 0)
|
728
|
+
// // const xMinMax = {
|
729
|
+
// // minX: 0,
|
730
|
+
// // maxX
|
731
|
+
// // }
|
732
|
+
// // const xScale = createOriginXScale(xMinMax, data.layout)
|
733
|
+
// // console.log('data.visibleComputedRankingData', data.visibleComputedRankingData)
|
734
|
+
// return data.visibleComputedRankingData
|
735
|
+
// .map((categoryData, categoryIndex) => {
|
736
|
+
// const yMinMax = {
|
737
|
+
// minY: 0,
|
738
|
+
// maxY: data.computedRankingAmountList[categoryIndex]
|
739
|
+
// }
|
740
|
+
// const yScale = createOriginYScale(yMinMax, data.layout)
|
741
|
+
|
742
|
+
// return categoryData.map((datum, datumIndex) => {
|
743
|
+
// return {
|
744
|
+
// ...datum,
|
745
|
+
// // axisX: xScale(datum.value[data.xyValueIndex[0]] ?? 0),
|
746
|
+
// axisX: 0,
|
747
|
+
// // axisY: yScale(datum.value[data.xyValueIndex[1]] ?? 0), // y軸的繪圖座標是從上到下,所以反轉
|
748
|
+
// axisY: yScale(datumIndex),
|
749
|
+
// }
|
750
|
+
// })
|
751
|
+
// })
|
752
|
+
// })
|
753
|
+
// )
|
754
|
+
// }
|
755
|
+
|
756
|
+
// // Ranking資料 - 有 XY 資料 @Q@ 若沒用到要棄用
|
757
|
+
// export const computedRankingWithXYDataObservable = ({ visibleComputedRankingData$, rankingScaleList$ }: {
|
758
|
+
// visibleComputedRankingData$: Observable<ComputedDatumMultiValue[][]>
|
759
|
+
// rankingScaleList$: Observable<d3.ScalePoint<string>[]>
|
760
|
+
// }): Observable<ComputedXYDataMultiValue> => {
|
761
|
+
|
762
|
+
// return combineLatest({
|
763
|
+
// visibleComputedRankingData: visibleComputedRankingData$,
|
764
|
+
// rankingScaleList: rankingScaleList$
|
765
|
+
// }).pipe(
|
766
|
+
// switchMap(async d => d),
|
767
|
+
// map(data => {
|
768
|
+
// return data.visibleComputedRankingData
|
769
|
+
// .map((categoryData, categoryIndex) => {
|
770
|
+
// const yScale = data.rankingScaleList[categoryIndex]
|
771
|
+
|
772
|
+
// return categoryData.map((datum, datumIndex) => {
|
773
|
+
// return {
|
774
|
+
// ...datum,
|
775
|
+
// axisX: 0,
|
776
|
+
// axisY: yScale(datum.label),
|
777
|
+
// }
|
778
|
+
// })
|
779
|
+
// })
|
780
|
+
// })
|
781
|
+
// )
|
782
782
|
// }
|