@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,553 @@
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ switchMap,
6
+ first,
7
+ takeUntil,
8
+ Subject,
9
+ Observable,
10
+ distinctUntilChanged} from 'rxjs'
11
+ import type {
12
+ ChartParams,
13
+ DatumValue,
14
+ DataSeries,
15
+ EventName,
16
+ ComputedDataSeries,
17
+ ComputedDatumSeries } from '@orbcharts/core'
18
+ import {
19
+ defineSeriesPlugin } from '@orbcharts/core'
20
+ import type { BubblesPluginParams, ScaleType } from '../types'
21
+ import { DEFAULT_BUBBLES_PLUGIN_PARAMS } from '../defaults'
22
+ import { renderCircleText } from '../../utils/d3Graphics'
23
+
24
+ interface BubblesDatum extends ComputedDatumSeries {
25
+ x: number
26
+ y: number
27
+ r: number
28
+ _originR: number // 紀錄變化前的r
29
+ }
30
+
31
+ let force: d3.Simulation<d3.SimulationNodeDatum, undefined> | undefined
32
+
33
+ function makeForce (bubblesSelection: d3.Selection<SVGGElement, any, any, any>, fullParams: BubblesPluginParams) {
34
+ return d3.forceSimulation()
35
+ .velocityDecay(fullParams.force!.velocityDecay!)
36
+ // .alphaDecay(0.2)
37
+ .force(
38
+ "collision",
39
+ d3.forceCollide()
40
+ .radius(d => {
41
+ // @ts-ignore
42
+ return d.r + fullParams.force!.collisionSpacing
43
+ })
44
+ // .strength(0.01)
45
+ )
46
+ .force("charge", d3.forceManyBody().strength((d) => {
47
+ // @ts-ignore
48
+ return - Math.pow(d.r, 2.0) * fullParams.force!.strength
49
+ }))
50
+ // .force("x", d3.forceX().strength(forceStrength).x(this.graphicWidth / 2))
51
+ // .force("y", d3.forceY().strength(forceStrength).y(this.graphicHeight / 2))
52
+ .on("tick", () => {
53
+ // if (!bubblesSelection) {
54
+ // return
55
+ // }
56
+ bubblesSelection
57
+ .attr("transform", (d) => {
58
+ return `translate(${d.x},${d.y})`
59
+ })
60
+ // .attr("cx", (d) => d.x)
61
+ // .attr("cy", (d) => d.y)
62
+ })
63
+ }
64
+
65
+
66
+ // 計算最大泡泡的半徑
67
+ function getMaxR ({ data, bubbleGroupR, maxValue, avgValue }: {
68
+ data: DatumValue[]
69
+ bubbleGroupR: number
70
+ maxValue: number
71
+ avgValue: number
72
+ }) {
73
+ // 平均r(假想是正方型來計算的,比如說大正方型裡有4個正方型,則 r = width/Math.sqrt(4)/2)
74
+ const avgR = bubbleGroupR / Math.sqrt(data.length)
75
+ const avgSize = avgR * avgR * Math.PI
76
+ const sizeRate = avgSize / avgValue
77
+ const maxSize = maxValue * sizeRate
78
+ const maxR = Math.pow(maxSize / Math.PI, 0.5)
79
+
80
+ const modifier = 0.75 // @Q@ 因為以下公式是假設泡泡是正方型來計算,所以畫出來的圖會偏大一些,這個數值是用來修正用的
81
+ return maxR * modifier
82
+ }
83
+
84
+ function createBubblesData ({ data, LastBubbleDataMap, graphicWidth, graphicHeight, scaleType }: {
85
+ data: ComputedDataSeries
86
+ LastBubbleDataMap: Map<string, BubblesDatum>
87
+ graphicWidth: number
88
+ graphicHeight: number
89
+ scaleType: ScaleType
90
+ // highlightIds: string[]
91
+ }) {
92
+ const bubbleGroupR = Math.min(...[graphicWidth, graphicHeight]) / 2
93
+
94
+ const filteredData = data
95
+ .flat()
96
+ .filter(_d => _d.value != null && _d.visible != false)
97
+
98
+ const maxValue = Math.max(
99
+ ...filteredData.map(_d => _d.value!)
100
+ )
101
+
102
+ const avgValue = (
103
+ filteredData.reduce((prev, current) => prev + (current.value ?? 0), 0)
104
+ ) / filteredData.length
105
+
106
+ const maxR = getMaxR({ data: filteredData, bubbleGroupR, maxValue, avgValue })
107
+
108
+ const exponent = scaleType === 'area'
109
+ ? 0.5 // 比例映射面積(0.5為取平方根)
110
+ : 1 // 比例映射半徑
111
+
112
+ const scaleBubbleR = d3.scalePow()
113
+ .domain([0, maxValue])
114
+ .range([0, maxR])
115
+ .exponent(exponent)
116
+
117
+ const bubbleData: BubblesDatum[] = filteredData.map((_d) => {
118
+ const d: BubblesDatum = _d as BubblesDatum
119
+
120
+ const existDatum = LastBubbleDataMap.get(_d.id)
121
+
122
+ if (existDatum) {
123
+ // 使用現有的座標
124
+ d.x = existDatum.x
125
+ d.y = existDatum.y
126
+ } else {
127
+ d.x = Math.random() * graphicWidth
128
+ d.y = Math.random() * graphicHeight
129
+ }
130
+ const r = scaleBubbleR!(d.value ?? 0)!
131
+ d.r = r
132
+ d._originR = r
133
+
134
+ return d
135
+ })
136
+
137
+ return bubbleData
138
+ }
139
+
140
+ function renderBubbles ({ graphicSelection, bubblesData, fullParams }: {
141
+ graphicSelection: d3.Selection<SVGGElement, any, any, any>
142
+ bubblesData: BubblesDatum[]
143
+ fullParams: BubblesPluginParams
144
+ }) {
145
+ let update = graphicSelection.selectAll<SVGGElement, BubblesDatum>("g")
146
+ .data(bubblesData, (d) => d.id)
147
+ let enter = update.enter()
148
+ .append<SVGGElement>("g")
149
+ .attr('cursor', 'pointer')
150
+ enter
151
+ .style('font-size', 12)
152
+ .style('fill', '#ffffff')
153
+ .attr("text-anchor", "middle")
154
+ .attr("transform", (d) => {
155
+ return `translate(${d.x},${d.y})`
156
+ })
157
+ // .attr("cx", (d) => d.x)
158
+ // .attr("cy", (d) => d.y)
159
+
160
+ enter
161
+ .append("circle")
162
+ .attr("class", "node")
163
+ // update.merge(enter)
164
+ .attr("cx", 0)
165
+ .attr("cy", 0)
166
+ // .attr("r", 1e-6)
167
+ .attr('fill', (d) => d.color)
168
+ // .transition()
169
+ // .duration(500)
170
+
171
+ enter
172
+ .append('text')
173
+ .style('opacity', 0.8)
174
+ .attr('pointer-events', 'none')
175
+
176
+ update.exit().remove()
177
+
178
+ const bubblesSelection = update.merge(enter)
179
+
180
+ bubblesSelection.select('circle')
181
+ .transition()
182
+ .duration(200)
183
+ .attr("r", (d) => d.r)
184
+ .attr('fill', (d) => d.color)
185
+ bubblesSelection
186
+ .each((d,i,g) => {
187
+ const gSelection = d3.select(g[i])
188
+ let breakAll = true
189
+ if (d.label.length <= fullParams.bubbleText.lineLengthMin) {
190
+ breakAll = false
191
+ }
192
+ gSelection.call(renderCircleText, {
193
+ text: d.label,
194
+ radius: d.r * fullParams.bubbleText.fillRate,
195
+ lineHeight: fullParams.bubbleText.lineHeight,
196
+ isBreakAll: breakAll
197
+ })
198
+
199
+ })
200
+
201
+ return bubblesSelection
202
+ }
203
+
204
+ function setHighlightData ({ data, highlightRIncrease, highlightIds }: {
205
+ data: BubblesDatum[]
206
+ // fullParams: BubblesPluginParams
207
+ highlightRIncrease: number
208
+ highlightIds: string[]
209
+ }) {
210
+ if (highlightRIncrease == 0) {
211
+ return
212
+ }
213
+ if (!highlightIds.length) {
214
+ data.forEach(d => d.r = d._originR)
215
+ return
216
+ }
217
+ data.forEach(d => {
218
+ if (highlightIds.includes(d.id)) {
219
+ d.r = d._originR + highlightRIncrease
220
+ } else {
221
+ d.r = d._originR
222
+ }
223
+ })
224
+ }
225
+
226
+ function drag (): d3.DragBehavior<Element, unknown, unknown> {
227
+ return d3.drag()
228
+ .on("start", (event, d: any) => {
229
+ if (!event.active) {
230
+ force!.alpha(1).restart();
231
+ }
232
+ d.fx = d.x
233
+ d.fy = d.y
234
+ })
235
+ .on("drag", (event, d: any) => {
236
+ if (!event.active) {
237
+ force!.alphaTarget(0)
238
+ }
239
+ d.fx = event.x
240
+ d.fy = event.y
241
+ })
242
+ .on("end", (event, d: any) => {
243
+ d.fx = null
244
+ d.fy = null
245
+ })
246
+ }
247
+
248
+
249
+ // private nodeTypePos (d: any) {
250
+ // console.log(d)
251
+ // console.log(this.TypeCenters.get(d.type)!)
252
+ // const typeCenter = this.TypeCenters.get(d.type)!
253
+ // return typeCenter ? typeCenter.x : 0
254
+ // }
255
+
256
+ function groupBubbles ({ fullParams, graphicWidth, graphicHeight }: {
257
+ fullParams: BubblesPluginParams
258
+ graphicWidth: number
259
+ graphicHeight: number
260
+ }) {
261
+ force!
262
+ // .force('x', d3.forceX().strength(fullParams.force.strength).x(graphicWidth / 2))
263
+ // .force('y', d3.forceY().strength(fullParams.force.strength).y(graphicHeight / 2))
264
+ .force('x', d3.forceX().strength(fullParams.force.strength).x(0))
265
+ .force('y', d3.forceY().strength(fullParams.force.strength).y(0))
266
+
267
+ force!.alpha(1).restart();
268
+ }
269
+
270
+ function highlight ({ bubblesSelection, highlightIds, fullChartParams }: {
271
+ bubblesSelection: d3.Selection<SVGGElement, BubblesDatum, any, any>
272
+ fullChartParams: ChartParams
273
+ highlightIds: string[]
274
+ }) {
275
+ bubblesSelection.interrupt('highlight')
276
+
277
+ if (!highlightIds.length) {
278
+ bubblesSelection
279
+ .transition('highlight')
280
+ .style('opacity', 1)
281
+ return
282
+ }
283
+
284
+ bubblesSelection.each((d, i, n) => {
285
+ const segment = d3.select(n[i])
286
+
287
+ if (highlightIds.includes(d.id)) {
288
+ segment
289
+ .style('opacity', 1)
290
+ .transition('highlight')
291
+ .ease(d3.easeElastic)
292
+ .duration(500)
293
+ } else {
294
+ // 取消放大
295
+ segment
296
+ .style('opacity', fullChartParams.styles.unhighlightedOpacity)
297
+ }
298
+ })
299
+ }
300
+
301
+ export const Bubbles = defineSeriesPlugin('Bubbles', DEFAULT_BUBBLES_PLUGIN_PARAMS)(({ selection, name, observer, subject }) => {
302
+
303
+ const destroy$ = new Subject()
304
+
305
+ const graphicSelection: d3.Selection<SVGGElement, any, any, any> = selection.append('g')
306
+ const bubblesSelection$: Subject<d3.Selection<SVGGElement, BubblesDatum, any, any>> = new Subject()
307
+ // 紀錄前一次bubble data
308
+ let LastBubbleDataMap: Map<string, BubblesDatum> = new Map()
309
+
310
+
311
+ // fullParams$.subscribe(d => {
312
+ // force = makeForce(bubblesSelection, d)
313
+ // })
314
+
315
+ observer.layout$
316
+ .pipe(
317
+ first()
318
+ )
319
+ .subscribe(size => {
320
+ selection
321
+ .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
322
+ observer.layout$
323
+ .pipe(
324
+ takeUntil(destroy$)
325
+ )
326
+ .subscribe(size => {
327
+ selection
328
+ .transition()
329
+ .attr('transform', `translate(${size.width / 2}, ${size.height / 2})`)
330
+ })
331
+ })
332
+
333
+ // const bubbleGroupR$ = layout$.pipe(
334
+ // map(d => {
335
+ // const minWidth = Math.min(...[d.width, d.height])
336
+ // return minWidth / 2
337
+ // })
338
+ // )
339
+
340
+ // const maxValue$ = computedData$.pipe(
341
+ // map(d => Math.max(
342
+ // ...d
343
+ // .flat()
344
+ // .filter(_d => _d.value != null)
345
+ // .map(_d => _d.value!)
346
+ // )
347
+ // )
348
+ // )
349
+
350
+ // const avgValue$ = computedData$.pipe(
351
+ // map(d => {
352
+ // const total = d
353
+ // .flat()
354
+ // .reduce((prev, current) => prev + (current.value ?? 0), 0)
355
+ // return total / d.length
356
+ // })
357
+ // )
358
+
359
+ // const SeriesDataMap$ = observer.computedData$.pipe(
360
+ // takeUntil(destroy$),
361
+ // map(d => makeSeriesDataMap(d))
362
+ // )
363
+
364
+ const scaleType$ = observer.fullParams$.pipe(
365
+ takeUntil(destroy$),
366
+ map(d => d.scaleType),
367
+ distinctUntilChanged()
368
+ )
369
+
370
+ const bubblesData$ = new Observable<BubblesDatum[]>(subscriber => {
371
+ combineLatest({
372
+ layout: observer.layout$,
373
+ computedData: observer.computedData$,
374
+ scaleType: scaleType$
375
+ }).pipe(
376
+ takeUntil(destroy$),
377
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
378
+ switchMap(async (d) => d),
379
+ ).subscribe(data => {
380
+ const bubblesData = createBubblesData({
381
+ data: data.computedData,
382
+ LastBubbleDataMap,
383
+ graphicWidth: data.layout.width,
384
+ graphicHeight: data.layout.height,
385
+ scaleType: data.scaleType
386
+ })
387
+ subscriber.next(bubblesData)
388
+ })
389
+ })
390
+
391
+ // 紀錄前一次bubble data
392
+ bubblesData$.subscribe(d => {
393
+ LastBubbleDataMap = new Map(d.map(_d => [_d.id, _d])) // key: id, value: datum
394
+ })
395
+
396
+ const highlightTarget$ = observer.fullChartParams$.pipe(
397
+ takeUntil(destroy$),
398
+ map(d => d.highlightTarget),
399
+ distinctUntilChanged()
400
+ )
401
+
402
+ combineLatest({
403
+ layout: observer.layout$,
404
+ computedData: observer.computedData$,
405
+ bubblesData: bubblesData$,
406
+ SeriesDataMap: observer.SeriesDataMap$,
407
+ fullParams: observer.fullParams$,
408
+ highlightTarget: highlightTarget$
409
+ // fullChartParams: fullChartParams$
410
+ // highlight: highlight$
411
+ }).pipe(
412
+ takeUntil(destroy$),
413
+ // 轉換後會退訂前一個未完成的訂閱事件,因此可以取到「同時間」最後一次的訂閱事件
414
+ switchMap(async (d) => d),
415
+ ).subscribe(data => {
416
+
417
+ const bubblesSelection = renderBubbles({
418
+ graphicSelection,
419
+ bubblesData: data.bubblesData,
420
+ fullParams: data.fullParams
421
+ })
422
+
423
+ force = makeForce(bubblesSelection, data.fullParams)
424
+
425
+ bubblesSelection
426
+ .on('mouseover', (event, datum) => {
427
+ // this.tooltip!.setDatum({
428
+ // data: d,
429
+ // x: d3.event.clientX,
430
+ // y: d3.event.clientY
431
+ // })
432
+
433
+ subject.event$.next({
434
+ type: 'series',
435
+ eventName: 'mouseover',
436
+ pluginName: name,
437
+ highlightTarget: data.highlightTarget,
438
+ datum,
439
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
440
+ seriesIndex: datum.seriesIndex,
441
+ seriesLabel: datum.seriesLabel,
442
+ event,
443
+ data: data.computedData
444
+ })
445
+ })
446
+ .on('mousemove', (event, datum) => {
447
+ // this.tooltip!.setDatum({
448
+ // x: d3.event.clientX,
449
+ // y: d3.event.clientY
450
+ // })
451
+
452
+ subject.event$.next({
453
+ type: 'series',
454
+ eventName: 'mousemove',
455
+ pluginName: name,
456
+ highlightTarget: data.highlightTarget,
457
+ datum,
458
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
459
+ seriesIndex: datum.seriesIndex,
460
+ seriesLabel: datum.seriesLabel,
461
+ event,
462
+ data: data.computedData
463
+ })
464
+ })
465
+ .on('mouseout', (event, datum) => {
466
+ // this.tooltip!.remove()
467
+
468
+ subject.event$.next({
469
+ type: 'series',
470
+ eventName: 'mouseout',
471
+ pluginName: name,
472
+ highlightTarget: data.highlightTarget,
473
+ datum,
474
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
475
+ seriesIndex: datum.seriesIndex,
476
+ seriesLabel: datum.seriesLabel,
477
+ event,
478
+ data: data.computedData
479
+ })
480
+ })
481
+ .on('click', (event, datum) => {
482
+
483
+ subject.event$.next({
484
+ type: 'series',
485
+ eventName: 'click',
486
+ pluginName: name,
487
+ highlightTarget: data.highlightTarget,
488
+ datum,
489
+ series: data.SeriesDataMap.get(datum.seriesLabel)!,
490
+ seriesIndex: datum.seriesIndex,
491
+ seriesLabel: datum.seriesLabel,
492
+ event,
493
+ data: data.computedData
494
+ })
495
+ })
496
+ .call(drag() as any)
497
+
498
+ force!.nodes(data.bubblesData);
499
+
500
+ groupBubbles({
501
+ fullParams: data.fullParams,
502
+ graphicWidth: data.layout.width,
503
+ graphicHeight: data.layout.height
504
+ })
505
+
506
+ bubblesSelection$.next(bubblesSelection)
507
+ })
508
+
509
+ // const highlight$ = highlightObservable({ datumList$: computedData$, fullChartParams$, event$: store.event$ })
510
+ const highlightSubscription = observer.seriesHighlight$.subscribe()
511
+ combineLatest({
512
+ bubblesSelection: bubblesSelection$,
513
+ bubblesData: bubblesData$,
514
+ highlight: observer.seriesHighlight$,
515
+ fullChartParams: observer.fullChartParams$,
516
+ fullParams: observer.fullParams$,
517
+ layout: observer.layout$
518
+ }).pipe(
519
+ takeUntil(destroy$),
520
+ switchMap(async d => d)
521
+ ).subscribe(data => {
522
+ highlight({
523
+ bubblesSelection: data.bubblesSelection,
524
+ highlightIds: data.highlight,
525
+ fullChartParams: data.fullChartParams
526
+ })
527
+
528
+ if (data.fullParams.highlightRIncrease) {
529
+ setHighlightData ({
530
+ data: data.bubblesData,
531
+ highlightRIncrease: data.fullParams.highlightRIncrease,
532
+ highlightIds: data.highlight
533
+ })
534
+ renderBubbles({
535
+ graphicSelection,
536
+ bubblesData: data.bubblesData,
537
+ fullParams: data.fullParams
538
+ })
539
+ }
540
+
541
+ groupBubbles({
542
+ fullParams: data.fullParams,
543
+ graphicWidth: data.layout.width,
544
+ graphicHeight: data.layout.height
545
+ })
546
+ force!.nodes(data.bubblesData);
547
+ })
548
+
549
+ return () => {
550
+ destroy$.next(undefined)
551
+ highlightSubscription.unsubscribe()
552
+ }
553
+ })