@orbcharts/plugins-basic 3.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
+ })