@orbcharts/plugins-basic 3.0.0-alpha.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/package.json +41 -0
  3. package/src/grid/defaults.ts +95 -0
  4. package/src/grid/gridObservables.ts +114 -0
  5. package/src/grid/index.ts +12 -0
  6. package/src/grid/plugins/BarStack.ts +661 -0
  7. package/src/grid/plugins/Bars.ts +604 -0
  8. package/src/grid/plugins/BarsTriangle.ts +594 -0
  9. package/src/grid/plugins/Dots.ts +427 -0
  10. package/src/grid/plugins/GroupArea.ts +636 -0
  11. package/src/grid/plugins/GroupAxis.ts +363 -0
  12. package/src/grid/plugins/Lines.ts +528 -0
  13. package/src/grid/plugins/Ranking.ts +0 -0
  14. package/src/grid/plugins/RankingAxis.ts +0 -0
  15. package/src/grid/plugins/ScalingArea.ts +168 -0
  16. package/src/grid/plugins/ValueAxis.ts +356 -0
  17. package/src/grid/plugins/ValueStackAxis.ts +372 -0
  18. package/src/grid/types.ts +102 -0
  19. package/src/index.ts +7 -0
  20. package/src/multiGrid/index.ts +0 -0
  21. package/src/multiGrid/plugins/Diverging.ts +0 -0
  22. package/src/multiGrid/plugins/DivergingAxes.ts +0 -0
  23. package/src/multiGrid/plugins/TwoScaleAxes.ts +0 -0
  24. package/src/multiGrid/plugins/TwoScales.ts +0 -0
  25. package/src/multiValue/index.ts +0 -0
  26. package/src/multiValue/plugins/Scatter.ts +0 -0
  27. package/src/multiValue/plugins/ScatterAxes.ts +0 -0
  28. package/src/noneData/defaults.ts +47 -0
  29. package/src/noneData/index.ts +4 -0
  30. package/src/noneData/plugins/Container.ts +11 -0
  31. package/src/noneData/plugins/Tooltip.ts +305 -0
  32. package/src/noneData/types.ts +26 -0
  33. package/src/relationship/index.ts +0 -0
  34. package/src/relationship/plugins/Relationship.ts +0 -0
  35. package/src/series/defaults.ts +82 -0
  36. package/src/series/index.ts +6 -0
  37. package/src/series/plugins/Bubbles.ts +553 -0
  38. package/src/series/plugins/Pie.ts +603 -0
  39. package/src/series/plugins/PieEventTexts.ts +194 -0
  40. package/src/series/plugins/PieLabels.ts +289 -0
  41. package/src/series/plugins/Waffle.ts +0 -0
  42. package/src/series/seriesUtils.ts +51 -0
  43. package/src/series/types.ts +53 -0
  44. package/src/tree/index.ts +0 -0
  45. package/src/tree/plugins/TreeMap.ts +0 -0
  46. package/src/utils/commonUtils.ts +22 -0
  47. package/src/utils/d3Graphics.ts +125 -0
  48. package/src/utils/d3Utils.ts +73 -0
  49. package/src/utils/observables.ts +14 -0
  50. package/src/utils/orbchartsUtils.ts +70 -0
  51. package/tsconfig.json +14 -0
  52. package/vite.config.js +45 -0
@@ -0,0 +1,528 @@
1
+ import * as d3 from 'd3'
2
+ import {
3
+ Observable,
4
+ combineLatest,
5
+ switchMap,
6
+ distinctUntilChanged,
7
+ filter,
8
+ first,
9
+ share,
10
+ shareReplay,
11
+ map,
12
+ takeUntil,
13
+ Subject } from 'rxjs'
14
+ import {
15
+ defineGridPlugin } from '@orbcharts/core'
16
+ import type {
17
+ ComputedDatumGrid,
18
+ ChartParams,
19
+ Layout } from '@orbcharts/core'
20
+ import type { LinesPluginParams } from '../types'
21
+ import { DEFAULT_LINES_PLUGIN_PARAMS } from '../defaults'
22
+ import { getD3TransitionEase } from '../../utils/d3Utils'
23
+ import { getClassName, getUniID } from '../../utils/orbchartsUtils'
24
+ import { gridGroupPositionFnObservable } from '../gridObservables'
25
+
26
+ type ClipPathDatum = {
27
+ id: string;
28
+ // x: number;
29
+ // y: number;
30
+ width: number;
31
+ height: number;
32
+ }
33
+
34
+ const pluginName = 'Lines'
35
+ const gClassName = getClassName(pluginName, 'g')
36
+ const pathClassName = getClassName(pluginName, 'path')
37
+
38
+ function createLinePath (lineCurve: string = 'curveLinear'): d3.Line<ComputedDatumGrid> {
39
+ return d3.line<ComputedDatumGrid>()
40
+ .x((d) => d.axisX)
41
+ .y((d) => d.axisY)
42
+ .curve((d3 as any)[lineCurve])
43
+ }
44
+
45
+ // function renderGraphicG ({ selection }) {
46
+
47
+ // }
48
+
49
+ // 依無值的資料分段
50
+ function makeSegmentData (data: ComputedDatumGrid[]): ComputedDatumGrid[][] {
51
+ let segmentData: ComputedDatumGrid[][] = [[]]
52
+
53
+ let currentIndex = 0
54
+ for (let i in data) {
55
+ if (data[i].visible == false || data[i].value === undefined || data[i].value === null) {
56
+ // 換下一段的 index
57
+ if (segmentData[currentIndex].length) {
58
+ currentIndex ++
59
+ segmentData[currentIndex] = []
60
+ }
61
+ continue
62
+ }
63
+ segmentData[currentIndex].push(data[i])
64
+ }
65
+
66
+ return segmentData
67
+ }
68
+
69
+
70
+ function renderLine ({ selection, segmentData, linePath, params }: {
71
+ selection: d3.Selection<SVGGElement, unknown, any, unknown>
72
+ segmentData: ComputedDatumGrid[][]
73
+ linePath: d3.Line<ComputedDatumGrid>
74
+ params: LinesPluginParams
75
+ }): d3.Selection<SVGPathElement, ComputedDatumGrid[], any, any> {
76
+ // if (!data[0]) {
77
+ // return undefined
78
+ // }
79
+
80
+ const lines = selection
81
+ .selectAll<SVGPathElement, ComputedDatumGrid[]>('path')
82
+ .data(segmentData, (d, i) => d.length ? `${d[0].id}_${d[d.length - 1].id}` : i) // 以線段起迄id結合為線段id
83
+ .join(
84
+ enter => {
85
+ return enter
86
+ .append<SVGPathElement>('path')
87
+ .classed(pathClassName, true)
88
+ .attr("fill","none")
89
+ .attr('pointer-events', 'visibleStroke') // 只對線條產生事件
90
+ .style('vector-effect', 'non-scaling-stroke')
91
+ .style('cursor', 'pointer')
92
+ },
93
+ update => update,
94
+ exit => exit.remove()
95
+ )
96
+ .attr("stroke-width", params.lineWidth)
97
+ .attr("stroke", (d, i) => d[0] && d[0].color)
98
+ .attr("d", (d) => {
99
+ return linePath(d)
100
+ })
101
+
102
+ return lines
103
+ }
104
+
105
+ function highlightLine ({ selection, seriesLabel, fullChartParams }: {
106
+ selection: d3.Selection<any, string, any, any>
107
+ seriesLabel: string | null
108
+ fullChartParams: ChartParams
109
+ }) {
110
+ selection.interrupt('highlight')
111
+ if (!seriesLabel) {
112
+ // remove highlight
113
+ selection
114
+ .transition('highlight')
115
+ .duration(200)
116
+ .style('opacity', 1)
117
+ return
118
+ }
119
+
120
+ selection
121
+ .each((d, i, n) => {
122
+ if (d === seriesLabel) {
123
+ d3.select(n[i])
124
+ .style('opacity', 1)
125
+ } else {
126
+ d3.select(n[i])
127
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
128
+ }
129
+ })
130
+ }
131
+
132
+ function renderClipPath ({ defsSelection, clipPathData, transitionDuration, transitionEase }: {
133
+ defsSelection: d3.Selection<SVGDefsElement, any, any, any>
134
+ clipPathData: ClipPathDatum[]
135
+ transitionDuration: number
136
+ transitionEase: string
137
+ }) {
138
+ const clipPath = defsSelection
139
+ .selectAll<SVGClipPathElement, Layout>('clipPath')
140
+ .data(clipPathData)
141
+ .join(
142
+ enter => {
143
+ return enter
144
+ .append('clipPath')
145
+ },
146
+ update => update,
147
+ exit => exit.remove()
148
+ )
149
+ .attr('id', d => d.id)
150
+ .each((d, i, g) => {
151
+ const rect = d3.select(g[i])
152
+ .selectAll<SVGRectElement, typeof d>('rect')
153
+ .data([d])
154
+ .join(
155
+ enter => {
156
+ const enterSelection = enter
157
+ .append('rect')
158
+ enterSelection
159
+ .transition()
160
+ .duration(transitionDuration)
161
+ .ease(getD3TransitionEase(transitionEase))
162
+ // .delay(100) // @Q@ 不知為何如果沒加 delay位置會有點跑掉
163
+ .tween('tween', (_d, _i, _g) => {
164
+ return (t) => {
165
+ const transitionWidth = _d.width * t
166
+
167
+ enterSelection
168
+ .attr('x', 0)
169
+ .attr('y', 0)
170
+ .attr('width', _d => transitionWidth)
171
+ .attr('height', _d => _d.height)
172
+ }
173
+ })
174
+ return enterSelection
175
+ },
176
+ update => {
177
+ return update
178
+ .attr('x', 0)
179
+ .attr('y', 0)
180
+ .attr('width', _d => _d.width)
181
+ .attr('height', _d => _d.height)
182
+ },
183
+ exit => exit.remove()
184
+ )
185
+ })
186
+
187
+ }
188
+
189
+
190
+ export const Lines = defineGridPlugin(pluginName, DEFAULT_LINES_PLUGIN_PARAMS)(({ selection, name, observer, subject }) => {
191
+ // const axisGUpdate = selection
192
+ // .selectAll('g')
193
+ // .data()
194
+ const destroy$ = new Subject()
195
+
196
+ const clipPathID = getUniID(pluginName, 'clipPath-box')
197
+
198
+ const axisSelection: d3.Selection<SVGGElement, any, any, any> = selection
199
+ .append('g')
200
+ .attr('clip-path', `url(#${clipPathID})`)
201
+ const defsSelection: d3.Selection<SVGDefsElement, any, any, any> = axisSelection.append('defs')
202
+ const graphicGSelection: d3.Selection<SVGGElement, any, any, any> = axisSelection.append('g')
203
+ const graphicSelection$: Subject<d3.Selection<SVGGElement, string, any, any>> = new Subject()
204
+ // let pathSelection: d3.Selection<SVGPathElement, ComputedDatumGrid[], any, any> | undefined
205
+ // .style('transform', 'translate(0px, 0px) scale(1)')
206
+
207
+ observer.gridAxesTransform$
208
+ .pipe(
209
+ takeUntil(destroy$),
210
+ map(d => d.value),
211
+ distinctUntilChanged()
212
+ ).subscribe(d => {
213
+ axisSelection
214
+ .style('transform', d)
215
+ })
216
+
217
+ observer.gridGraphicTransform$
218
+ .pipe(
219
+ takeUntil(destroy$),
220
+ map(d => d.value),
221
+ distinctUntilChanged()
222
+ ).subscribe(d => {
223
+ graphicGSelection
224
+ .transition()
225
+ .duration(50)
226
+ .style('transform', d)
227
+ })
228
+
229
+ const linePath$: Observable<d3.Line<ComputedDatumGrid>> = new Observable(subscriber => {
230
+ const paramsSubscription = observer.fullParams$
231
+ .pipe(
232
+ takeUntil(destroy$)
233
+ )
234
+ .subscribe(d => {
235
+ if (!d) return
236
+ const linePath = createLinePath(d.lineCurve)
237
+ subscriber.next(linePath)
238
+ })
239
+ return () => {
240
+ paramsSubscription.unsubscribe()
241
+ }
242
+ })
243
+
244
+ // 顯示範圍內的series labels
245
+ const seriesLabels$: Observable<string[]> = new Observable(subscriber => {
246
+ observer.computedData$.pipe(
247
+ takeUntil(destroy$),
248
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
249
+ switchMap(async (d) => d),
250
+ ).subscribe(data => {
251
+ const labels = data[0] && data[0][0]
252
+ ? data.map(d => d[0].seriesLabel)
253
+ : []
254
+ subscriber.next(labels)
255
+ })
256
+ })
257
+
258
+ // const axisSize$ = gridAxisSizeObservable({
259
+ // fullDataFormatter$,
260
+ // computedLayout$
261
+ // })
262
+
263
+ const transitionDuration$ = observer.fullChartParams$
264
+ .pipe(
265
+ map(d => d.transitionDuration),
266
+ distinctUntilChanged()
267
+ )
268
+
269
+ const transitionEase$ = observer.fullChartParams$
270
+ .pipe(
271
+ map(d => d.transitionEase),
272
+ distinctUntilChanged()
273
+ )
274
+
275
+ const clipPathSubscription = combineLatest({
276
+ seriesLabels: seriesLabels$,
277
+ axisSize: observer.gridAxesSize$,
278
+ transitionDuration: transitionDuration$,
279
+ transitionEase: transitionEase$
280
+ }).pipe(
281
+ takeUntil(destroy$),
282
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
283
+ switchMap(async (d) => d),
284
+ ).subscribe(data => {
285
+ // 外層的遮罩
286
+ const clipPathBox = [{
287
+ id: clipPathID,
288
+ width: data.axisSize.width,
289
+ height: data.axisSize.height
290
+ }]
291
+ // 各別線條的遮罩(各別動畫)
292
+ const clipPathData = clipPathBox.concat(
293
+ data.seriesLabels.map(d => {
294
+ return {
295
+ id: `orbcharts__clipPath_${d}`,
296
+ width: data.axisSize.width,
297
+ height: data.axisSize.height
298
+ }
299
+ })
300
+ )
301
+ renderClipPath({
302
+ defsSelection,
303
+ clipPathData,
304
+ transitionDuration: data.transitionDuration,
305
+ transitionEase: data.transitionEase
306
+ })
307
+ })
308
+
309
+ // const SeriesDataMap$ = observer.computedData$.pipe(
310
+ // map(d => makeGridSeriesDataMap(d))
311
+ // )
312
+
313
+ // const GroupDataMap$ = observer.computedData$.pipe(
314
+ // map(d => makeGridGroupDataMap(d))
315
+ // )
316
+
317
+ const DataMap$ = observer.computedData$.pipe(
318
+ map(d => {
319
+ const DataMap: Map<string, ComputedDatumGrid> = new Map()
320
+ d.flat().forEach(_d => DataMap.set(_d.id, _d))
321
+ return DataMap
322
+ })
323
+ )
324
+
325
+ // 取得事件座標的group資料
326
+ const gridGroupPositionFn$ = gridGroupPositionFnObservable({
327
+ fullDataFormatter$: observer.fullDataFormatter$,
328
+ gridAxesSize$: observer.gridAxesSize$,
329
+ computedData$: observer.computedData$,
330
+ fullChartParams$: observer.fullChartParams$
331
+ })
332
+
333
+ const highlightTarget$ = observer.fullChartParams$.pipe(
334
+ takeUntil(destroy$),
335
+ map(d => d.highlightTarget),
336
+ distinctUntilChanged()
337
+ )
338
+
339
+ const graphSubscription = combineLatest({
340
+ seriesLabels: seriesLabels$,
341
+ computedData: observer.computedData$,
342
+ SeriesDataMap: observer.SeriesDataMap$,
343
+ GroupDataMap: observer.GroupDataMap$,
344
+ linePath: linePath$,
345
+ params: observer.fullParams$,
346
+ highlightTarget: highlightTarget$,
347
+ gridGroupPositionFn: gridGroupPositionFn$,
348
+ }).pipe(
349
+ takeUntil(destroy$),
350
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
351
+ switchMap(async (d) => d),
352
+ ).subscribe(data => {
353
+
354
+ const updateGraphic = graphicGSelection
355
+ .selectAll<SVGGElement, number>('g')
356
+ .data(data.seriesLabels, (d, i) => d)
357
+ const enterGraphic = updateGraphic.enter()
358
+ .append('g')
359
+ .classed(gClassName, true)
360
+ updateGraphic.exit().remove()
361
+ const graphicSelection = updateGraphic.merge(enterGraphic)
362
+ .attr('clip-path', (d, i) => `url(#orbcharts__clipPath_${d})`)
363
+
364
+ // 繪圖
365
+ graphicSelection.each((d, i, all) => {
366
+ // 將資料分段
367
+ const segmentData = makeSegmentData(data.computedData[i] ?? [])
368
+
369
+ const pathSelection = renderLine({
370
+ selection: d3.select(all[i]),
371
+ linePath: data.linePath,
372
+ segmentData: segmentData,
373
+ params: data.params
374
+ })
375
+
376
+ pathSelection
377
+ .on('mouseover', (event, datum) => {
378
+ event.stopPropagation()
379
+
380
+ const seriesLabel = datum[0] ? datum[0].seriesLabel : ''
381
+ const { groupIndex, groupLabel } = data.gridGroupPositionFn(event)
382
+ const groupData = data.GroupDataMap.get(groupLabel)!
383
+ const targetDatum = groupData.find(d => d.seriesLabel === seriesLabel)
384
+ const _datum = targetDatum ?? datum[0]
385
+
386
+ subject.event$.next({
387
+ type: 'grid',
388
+ eventName: 'mouseover',
389
+ pluginName: name,
390
+ highlightTarget: data.highlightTarget,
391
+ datum: _datum,
392
+ series: data.SeriesDataMap.get(_datum.seriesLabel)!,
393
+ seriesIndex: _datum.seriesIndex,
394
+ seriesLabel: _datum.seriesLabel,
395
+ groups: data.GroupDataMap.get(_datum.groupLabel)!,
396
+ groupIndex: _datum.groupIndex,
397
+ groupLabel: _datum.groupLabel,
398
+ event,
399
+ data: data.computedData
400
+ })
401
+ })
402
+ .on('mousemove', (event, datum) => {
403
+ event.stopPropagation()
404
+
405
+ const seriesLabel = datum[0] ? datum[0].seriesLabel : ''
406
+ const { groupIndex, groupLabel } = data.gridGroupPositionFn(event)
407
+ const groupData = data.GroupDataMap.get(groupLabel)!
408
+ const targetDatum = groupData.find(d => d.seriesLabel === seriesLabel)
409
+ const _datum = targetDatum ?? datum[0]
410
+
411
+ subject.event$.next({
412
+ type: 'grid',
413
+ eventName: 'mousemove',
414
+ pluginName: name,
415
+ highlightTarget: data.highlightTarget,
416
+ datum: _datum,
417
+ series: data.SeriesDataMap.get(_datum.seriesLabel)!,
418
+ seriesIndex: _datum.seriesIndex,
419
+ seriesLabel: _datum.seriesLabel,
420
+ groups: data.GroupDataMap.get(_datum.groupLabel)!,
421
+ groupIndex: _datum.groupIndex,
422
+ groupLabel: _datum.groupLabel,
423
+ event,
424
+ data: data.computedData
425
+ })
426
+ })
427
+ .on('mouseout', (event, datum) => {
428
+ event.stopPropagation()
429
+
430
+ const seriesLabel = datum[0] ? datum[0].seriesLabel : ''
431
+ const { groupIndex, groupLabel } = data.gridGroupPositionFn(event)
432
+ const groupData = data.GroupDataMap.get(groupLabel)!
433
+ const targetDatum = groupData.find(d => d.seriesLabel === seriesLabel)
434
+ const _datum = targetDatum ?? datum[0]
435
+
436
+ subject.event$.next({
437
+ type: 'grid',
438
+ eventName: 'mouseout',
439
+ pluginName: name,
440
+ highlightTarget: data.highlightTarget,
441
+ datum: _datum,
442
+ series: data.SeriesDataMap.get(_datum.seriesLabel)!,
443
+ seriesIndex: _datum.seriesIndex,
444
+ seriesLabel: _datum.seriesLabel,
445
+ groups: data.GroupDataMap.get(_datum.groupLabel)!,
446
+ groupIndex: _datum.groupIndex,
447
+ groupLabel: _datum.groupLabel,
448
+ event,
449
+ data: data.computedData
450
+ })
451
+ })
452
+ .on('click', (event, datum) => {
453
+ event.stopPropagation()
454
+
455
+ const seriesLabel = datum[0] ? datum[0].seriesLabel : ''
456
+ const { groupIndex, groupLabel } = data.gridGroupPositionFn(event)
457
+ const groupData = data.GroupDataMap.get(groupLabel)!
458
+ const targetDatum = groupData.find(d => d.seriesLabel === seriesLabel)
459
+ const _datum = targetDatum ?? datum[0]
460
+
461
+ subject.event$.next({
462
+ type: 'grid',
463
+ eventName: 'click',
464
+ pluginName: name,
465
+ highlightTarget: data.highlightTarget,
466
+ datum: _datum,
467
+ series: data.SeriesDataMap.get(_datum.seriesLabel)!,
468
+ seriesIndex: _datum.seriesIndex,
469
+ seriesLabel: _datum.seriesLabel,
470
+ groups: data.GroupDataMap.get(_datum.groupLabel)!,
471
+ groupIndex: _datum.groupIndex,
472
+ groupLabel: _datum.groupLabel,
473
+ event,
474
+ data: data.computedData
475
+ })
476
+ })
477
+
478
+ })
479
+
480
+
481
+
482
+ graphicSelection$.next(graphicSelection)
483
+
484
+
485
+ // pathSelection = renderLine({
486
+ // selection: graphicSelection,
487
+ // linePath: d.linePath,
488
+ // data: d.computedData
489
+ // })
490
+ })
491
+
492
+ // const datumList$ = observer.computedData$.pipe(
493
+ // takeUntil(destroy$),
494
+ // map(d => d.flat())
495
+ // )
496
+ // const highlight$ = highlightObservable({ datumList$, fullChartParams$, event$: store.event$ })
497
+ const highlightSubscription = observer.gridHighlight$.subscribe()
498
+
499
+ observer.fullChartParams$.pipe(
500
+ takeUntil(destroy$),
501
+ filter(d => d.highlightTarget === 'series'),
502
+ switchMap(d => combineLatest({
503
+ graphicSelection: graphicSelection$,
504
+ highlight: observer.gridHighlight$,
505
+ DataMap: DataMap$,
506
+ fullChartParams: observer.fullChartParams$
507
+ }).pipe(
508
+ takeUntil(destroy$),
509
+ switchMap(async d => d)
510
+ ))
511
+ ).subscribe(data => {
512
+ const datum = data.DataMap.get(data.highlight[0])
513
+ // if (!datum) {
514
+ // return
515
+ // }
516
+ highlightLine({
517
+ selection: data.graphicSelection,
518
+ seriesLabel: (datum && datum.seriesLabel) ? datum.seriesLabel : null,
519
+ fullChartParams: data.fullChartParams
520
+ })
521
+ })
522
+
523
+
524
+ return () => {
525
+ highlightSubscription.unsubscribe()
526
+ destroy$.next(undefined)
527
+ }
528
+ })
File without changes
File without changes
@@ -0,0 +1,168 @@
1
+ import * as d3 from 'd3'
2
+ import {
3
+ Observable,
4
+ combineLatest,
5
+ switchMap,
6
+ distinctUntilChanged,
7
+ first,
8
+ map,
9
+ takeUntil,
10
+ debounceTime,
11
+ Subject } from 'rxjs'
12
+ import {
13
+ defineGridPlugin } from '@orbcharts/core'
14
+ import { DEFAULT_SCALING_AREA_PLUGIN_PARAMS } from '../defaults'
15
+ import { getClassName, getUniID } from '../../utils/orbchartsUtils'
16
+ import { createAxisPointScale, createAxisLinearScale } from '@orbcharts/core'
17
+
18
+ const pluginName = 'ScalingArea'
19
+ const rectClassName = getClassName(pluginName, 'rect')
20
+
21
+ export const ScalingArea = defineGridPlugin(pluginName, DEFAULT_SCALING_AREA_PLUGIN_PARAMS)(({ selection, rootSelection, name, observer, subject }) => {
22
+
23
+ const destroy$ = new Subject()
24
+
25
+ const rootRectSelection: d3.Selection<SVGRectElement, any, any, any> = rootSelection
26
+ .insert('rect', 'g')
27
+ .classed(rectClassName, true)
28
+ .attr('opacity', 0)
29
+ // .attr('pointer-events', 'none')
30
+ // .attr('clip-path', 'url(#bpcharts__clipPath-box)')
31
+ // const dataAreaSelection: d3.Selection<SVGGElement, any, any, any> = axisSelection.append('g')
32
+
33
+ // 紀錄zoom最後一次的transform
34
+ let lastTransform = {
35
+ k: 1,
36
+ x: 0,
37
+ y: 0
38
+ }
39
+
40
+ observer.layout$.pipe(
41
+ takeUntil(destroy$),
42
+ ).subscribe(d => {
43
+ rootRectSelection
44
+ .attr('width', d.width)
45
+ .attr('height', d.height)
46
+ .attr('x', d.left)
47
+ .attr('y', d.top)
48
+ })
49
+
50
+ const groupMaxIndex$ = observer.computedData$.pipe(
51
+ map(d => d[0] ? d[0].length - 1 : 0),
52
+ distinctUntilChanged()
53
+ )
54
+
55
+ // const fullDataFormatterEvent$: Subject<DataFormatterGrid> = new Subject()
56
+ // fullDataFormatterEvent$
57
+ // .pipe(
58
+ // takeUntil(destroy$),
59
+ // debounceTime(50)
60
+ // )
61
+ // .subscribe(fullDataFormatter => {
62
+ // store.fullDataFormatter$.next(fullDataFormatter)
63
+ // })
64
+
65
+ combineLatest({
66
+ initGroupAxis: observer.fullDataFormatter$.pipe(
67
+ map(d => d.groupAxis),
68
+ // 只用第一次資料來計算scale才不會造成每次變動都受到影響
69
+ first()
70
+ ),
71
+ // fullDataFormatter: fullDataFormatter$.pipe(first()), // 只用第一次資料來計算scale才不會造成每次變動都受到影響
72
+ fullDataFormatter: observer.fullDataFormatter$,
73
+ groupMaxIndex: groupMaxIndex$,
74
+ layout: observer.layout$,
75
+ axisSize: observer.gridAxesSize$
76
+ }).pipe(
77
+ takeUntil(destroy$),
78
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
79
+ switchMap(async (d) => d),
80
+ ).subscribe(data => {
81
+ const groupMin = 0
82
+ const groupMax = data.groupMaxIndex
83
+ const groupScaleDomainMin = data.initGroupAxis.scaleDomain[0] === 'auto'
84
+ ? groupMin - data.initGroupAxis.scalePadding
85
+ : data.initGroupAxis.scaleDomain[0] as number - data.initGroupAxis.scalePadding
86
+ const groupScaleDomainMax = data.initGroupAxis.scaleDomain[1] === 'auto'
87
+ ? groupMax + data.initGroupAxis.scalePadding
88
+ : data.initGroupAxis.scaleDomain[1] as number + data.initGroupAxis.scalePadding
89
+
90
+ const scaleRange: [number, number] = data.fullDataFormatter.valueAxis.position === 'left' || data.fullDataFormatter.valueAxis.position === 'top'
91
+ ? [0, 1]
92
+ : [1, 0]
93
+
94
+ const groupScale: d3.ScaleLinear<number, number> = createAxisLinearScale({
95
+ maxValue: data.groupMaxIndex,
96
+ minValue: 0,
97
+ axisWidth: data.axisSize.width,
98
+ scaleDomain: [groupScaleDomainMin, groupScaleDomainMax],
99
+ // scaleDomain: [groupMin, groupMax],
100
+ scaleRange
101
+ })
102
+
103
+ const shadowScale = groupScale.copy()
104
+
105
+ const zoom = d3.zoom()
106
+ // .scaleExtent([1, data.groupMaxIndex])
107
+ // .translateExtent([[0, 0], [data.layout.rootWidth, data.layout.rootWidth]])
108
+ .on("zoom", function zoomed(event) {
109
+ // console.log('event', event)
110
+ const t = event.transform;
111
+ // console.log('t', t)
112
+ const mapGroupindex = (d: number) => {
113
+ const n = Math.round(d)
114
+ return Math.min(groupMax, Math.max(groupMin, n));
115
+ }
116
+ const zoomedDomain = data.fullDataFormatter.groupAxis.position === 'bottom' || data.fullDataFormatter.groupAxis.position === 'top'
117
+ ? t.rescaleX(shadowScale)
118
+ .domain()
119
+ .map(mapGroupindex)
120
+ : t.rescaleY(shadowScale)
121
+ .domain()
122
+ .map(mapGroupindex)
123
+
124
+ // domain超過極限值
125
+ if (zoomedDomain[0] <= groupMin && zoomedDomain[1] >= groupMax) {
126
+ // 繼續縮小
127
+ if (t.k < lastTransform.k) {
128
+ // 維持前一次的transform
129
+ t.k = lastTransform.k
130
+ t.x = lastTransform.x
131
+ t.y = lastTransform.y
132
+ }
133
+ // domain間距小於1
134
+ } else if ((zoomedDomain[1] - zoomedDomain[0]) <= 1) {
135
+ // 繼續放大
136
+ if (t.k > lastTransform.k) {
137
+ // 維持前一次的transform
138
+ t.k = lastTransform.k
139
+ t.x = lastTransform.x
140
+ t.y = lastTransform.y
141
+ }
142
+ }
143
+ // 紀錄transform
144
+ lastTransform.k = t.k
145
+ lastTransform.x = t.x
146
+ lastTransform.y = t.y
147
+ // console.log(String(data.fullDataFormatter.visibleFilter))
148
+ // console.log('zoomedDomain', zoomedDomain)
149
+ subject.dataFormatter$.next({
150
+ ...data.fullDataFormatter,
151
+ groupAxis: {
152
+ ...data.fullDataFormatter.groupAxis,
153
+ scaleDomain: zoomedDomain
154
+ }
155
+ })
156
+ })
157
+
158
+ // 傳入外層selection
159
+ // subject.selection.call(zoom as any)
160
+ rootSelection.call(zoom as any)
161
+
162
+ })
163
+
164
+ return () => {
165
+ destroy$.next(undefined)
166
+ rootRectSelection.remove()
167
+ }
168
+ })