@orbcharts/plugins-basic 3.0.7 → 3.0.9

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 (121) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-plugins-basic.es.js +6837 -6656
  3. package/dist/orbcharts-plugins-basic.umd.js +45 -45
  4. package/dist/src/base/BaseStackedBars.d.ts +2 -0
  5. package/dist/src/series/defaults.d.ts +2 -1
  6. package/dist/src/series/index.d.ts +1 -0
  7. package/dist/src/series/plugins/Indicator.d.ts +3 -0
  8. package/lib/core-types.ts +7 -7
  9. package/lib/core.ts +6 -6
  10. package/lib/gridObservables.ts +6 -6
  11. package/lib/plugins-basic-types.ts +6 -6
  12. package/package.json +48 -48
  13. package/src/base/BaseBars.ts +765 -765
  14. package/src/base/BaseBarsTriangle.ts +676 -676
  15. package/src/base/BaseDots.ts +464 -464
  16. package/src/base/BaseGroupAxis.ts +691 -691
  17. package/src/base/BaseLegend.ts +684 -684
  18. package/src/base/BaseLineAreas.ts +629 -629
  19. package/src/base/BaseLines.ts +706 -706
  20. package/src/base/BaseOrdinalBubbles.ts +729 -729
  21. package/src/base/BaseRacingBars.ts +582 -582
  22. package/src/base/BaseRacingLabels.ts +404 -404
  23. package/src/base/BaseRacingValueLabels.ts +403 -403
  24. package/src/base/BaseStackedBars.ts +793 -782
  25. package/src/base/BaseTooltip.ts +408 -408
  26. package/src/base/BaseValueAxis.ts +600 -600
  27. package/src/base/BaseXAxis.ts +427 -427
  28. package/src/base/BaseXZoom.ts +241 -241
  29. package/src/base/BaseYAxis.ts +389 -389
  30. package/src/base/types.ts +2 -2
  31. package/src/const.ts +30 -30
  32. package/src/grid/defaults.ts +213 -213
  33. package/src/grid/gridObservables.ts +635 -635
  34. package/src/grid/index.ts +16 -16
  35. package/src/grid/plugins/Bars.ts +69 -69
  36. package/src/grid/plugins/BarsPN.ts +66 -66
  37. package/src/grid/plugins/BarsTriangle.ts +73 -73
  38. package/src/grid/plugins/Dots.ts +68 -68
  39. package/src/grid/plugins/GridLegend.ts +107 -107
  40. package/src/grid/plugins/GridTooltip.ts +66 -66
  41. package/src/grid/plugins/GroupAux.ts +1095 -1095
  42. package/src/grid/plugins/GroupAxis.ts +73 -73
  43. package/src/grid/plugins/GroupZoom.ts +218 -218
  44. package/src/grid/plugins/LineAreas.ts +65 -65
  45. package/src/grid/plugins/Lines.ts +59 -59
  46. package/src/grid/plugins/StackedBars.ts +66 -64
  47. package/src/grid/plugins/StackedValueAxis.ts +97 -96
  48. package/src/grid/plugins/ValueAxis.ts +94 -94
  49. package/src/index.ts +6 -6
  50. package/src/multiGrid/defaults.ts +244 -244
  51. package/src/multiGrid/index.ts +14 -14
  52. package/src/multiGrid/multiGridObservables.ts +50 -50
  53. package/src/multiGrid/plugins/MultiBars.ts +108 -108
  54. package/src/multiGrid/plugins/MultiBarsTriangle.ts +114 -114
  55. package/src/multiGrid/plugins/MultiDots.ts +102 -102
  56. package/src/multiGrid/plugins/MultiGridLegend.ts +169 -169
  57. package/src/multiGrid/plugins/MultiGridTooltip.ts +66 -66
  58. package/src/multiGrid/plugins/MultiGroupAxis.ts +137 -137
  59. package/src/multiGrid/plugins/MultiLineAreas.ts +107 -107
  60. package/src/multiGrid/plugins/MultiLines.ts +101 -101
  61. package/src/multiGrid/plugins/MultiStackedBars.ts +109 -106
  62. package/src/multiGrid/plugins/MultiStackedValueAxis.ts +135 -134
  63. package/src/multiGrid/plugins/MultiValueAxis.ts +134 -134
  64. package/src/multiGrid/plugins/OverlappingStackedValueAxes.ts +300 -300
  65. package/src/multiGrid/plugins/OverlappingValueAxes.ts +300 -300
  66. package/src/multiValue/defaults.ts +523 -523
  67. package/src/multiValue/index.ts +16 -16
  68. package/src/multiValue/multiValueObservables.ts +781 -781
  69. package/src/multiValue/plugins/MultiValueLegend.ts +107 -107
  70. package/src/multiValue/plugins/MultiValueTooltip.ts +66 -66
  71. package/src/multiValue/plugins/OrdinalAux.ts +660 -660
  72. package/src/multiValue/plugins/OrdinalAxis.ts +524 -524
  73. package/src/multiValue/plugins/OrdinalBubbles.ts +226 -226
  74. package/src/multiValue/plugins/OrdinalZoom.ts +57 -57
  75. package/src/multiValue/plugins/RacingBars.ts +375 -375
  76. package/src/multiValue/plugins/RacingCounterTexts.ts +300 -300
  77. package/src/multiValue/plugins/RacingValueAxis.ts +114 -114
  78. package/src/multiValue/plugins/Scatter.ts +486 -486
  79. package/src/multiValue/plugins/ScatterBubbles.ts +635 -635
  80. package/src/multiValue/plugins/XAxis.ts +107 -107
  81. package/src/multiValue/plugins/XYAux.ts +683 -683
  82. package/src/multiValue/plugins/XYAxes.ts +194 -194
  83. package/src/multiValue/plugins/XYAxes_legacy.ts +683 -683
  84. package/src/multiValue/plugins/XZoom.ts +40 -40
  85. package/src/noneData/defaults.ts +102 -102
  86. package/src/noneData/index.ts +3 -3
  87. package/src/noneData/plugins/Container.ts +27 -27
  88. package/src/noneData/plugins/Tooltip.ts +373 -373
  89. package/src/relationship/defaults.ts +221 -221
  90. package/src/relationship/index.ts +5 -5
  91. package/src/relationship/plugins/ForceDirected.ts +1056 -1056
  92. package/src/relationship/plugins/ForceDirectedBubbles.ts +1294 -1294
  93. package/src/relationship/plugins/RelationshipLegend.ts +100 -100
  94. package/src/relationship/plugins/RelationshipTooltip.ts +66 -66
  95. package/src/relationship/relationshipObservables.ts +49 -49
  96. package/src/series/defaults.ts +236 -224
  97. package/src/series/index.ts +10 -9
  98. package/src/series/plugins/Bubbles.ts +784 -784
  99. package/src/series/plugins/Indicator.ts +292 -0
  100. package/src/series/plugins/Pie.ts +622 -622
  101. package/src/series/plugins/PieEventTexts.ts +283 -283
  102. package/src/series/plugins/PieLabels.ts +639 -639
  103. package/src/series/plugins/Rose.ts +515 -515
  104. package/src/series/plugins/RoseLabels.ts +599 -599
  105. package/src/series/plugins/SeriesLegend.ts +107 -107
  106. package/src/series/plugins/SeriesTooltip.ts +66 -66
  107. package/src/series/seriesObservables.ts +168 -168
  108. package/src/series/seriesUtils.ts +51 -51
  109. package/src/tree/defaults.ts +102 -102
  110. package/src/tree/index.ts +4 -4
  111. package/src/tree/plugins/TreeLegend.ts +100 -100
  112. package/src/tree/plugins/TreeMap.ts +341 -341
  113. package/src/tree/plugins/TreeTooltip.ts +66 -66
  114. package/src/utils/commonUtils.ts +31 -31
  115. package/src/utils/d3Graphics.ts +176 -176
  116. package/src/utils/d3Utils.ts +92 -92
  117. package/src/utils/observables.ts +14 -14
  118. package/src/utils/orbchartsUtils.ts +129 -129
  119. package/tsconfig.base.json +13 -13
  120. package/tsconfig.json +2 -2
  121. package/vite.config.js +22 -22
@@ -1,409 +1,409 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- map,
5
- merge,
6
- filter,
7
- switchMap,
8
- first,
9
- iif,
10
- takeUntil,
11
- Subject,
12
- Observable,
13
- distinctUntilChanged } from 'rxjs'
14
- import type {
15
- EventTypeMap,
16
- PluginConstructor,
17
- ChartType,
18
- ChartParams,
19
- Layout
20
- } from '../../lib/core-types'
21
- import type { BaseTooltipParams, BaseTooltipStyle } from '../../lib/plugins-basic-types'
22
- import type { BasePluginFn } from './types'
23
- import { defineNoneDataPlugin, textSizePxObservable } from '../../lib/core'
24
- import { getSvgGElementSize, appendSvg } from '../utils/d3Utils'
25
- import { getColor, getClassName } from '../utils/orbchartsUtils'
26
- import { measureTextWidth, toCurrency } from '../utils/commonUtils'
27
-
28
- interface BaseTooltipContext {
29
- rootSelection: d3.Selection<any, unknown, any, unknown>
30
- fullParams$: Observable<BaseTooltipParams>
31
- fullChartParams$: Observable<ChartParams>
32
- layout$: Observable<Layout>
33
- event$: Subject<EventTypeMap<any>>
34
- }
35
-
36
- // export interface BaseTooltipStyle {
37
- // backgroundColor: string
38
- // backgroundOpacity: number
39
- // strokeColor: string
40
- // offset: [number, number]
41
- // padding: number
42
- // textColor: string
43
- // textSize: number | string // chartParams上的設定
44
- // textSizePx: number
45
- // }
46
-
47
- function textToSvg (_textArr: string[] | string | null | undefined, textStyle: BaseTooltipStyle) {
48
- const lineHeight = textStyle.textSizePx * 1.5
49
-
50
- const textArr = _textArr == null
51
- ? []
52
- : Array.isArray(_textArr)
53
- ? _textArr
54
- : typeof _textArr === 'string'
55
- ? _textArr.split('\n')
56
- : [_textArr]
57
-
58
- const tspan = textArr
59
- .filter(d => d != '')
60
- .map((text, i) => {
61
- const top = i * lineHeight
62
- return `<tspan x="0" y="${top}">${text}</tspan>`
63
- })
64
- .join('')
65
-
66
- if (tspan) {
67
- return `<text font-size="${textStyle.textSize}" fill="${textStyle.textColor}" x="0" y="0" style="dominant-baseline:text-before-edge">
68
- ${tspan}
69
- </text>`
70
- } else {
71
- return ''
72
- }
73
- }
74
-
75
- function renderTooltip ({ rootSelection, pluginName, gClassName, boxClassName, rootWidth, rootHeight, svgString, tooltipStyle, event }: {
76
- rootSelection: d3.Selection<any, unknown, any, unknown>
77
- pluginName: string
78
- gClassName: string
79
- boxClassName: string
80
- rootWidth: number
81
- rootHeight: number
82
- svgString: string
83
- tooltipStyle: BaseTooltipStyle
84
- event: MouseEvent
85
- }) {
86
- // if (!svgString) {
87
- // return
88
- // }
89
- // const rootSelection = d3.select('svg.bpcharts__root')
90
- // console.log('tooltip', { rootSelection, pluginName, gClassName, boxClassName, rootWidth, rootHeight, svgString, tooltipStyle, event })
91
- rootSelection.interrupt('fadeout')
92
- const radius = 5
93
-
94
- // data(svg string無值則為空陣列)
95
- const contentData = svgString ? [svgString] : []
96
- const styleData = svgString ? [tooltipStyle] : []
97
- // tooltipG <g>
98
- const tooltipG = rootSelection
99
- .selectAll<SVGGElement, string>(`g.${gClassName}`)
100
- .data(contentData)
101
- .join(
102
- enter => {
103
- return enter
104
- .append('g')
105
- .classed(gClassName, true)
106
- .attr('pointer-events', 'none')
107
- },
108
- update => {
109
- return update
110
- },
111
- exit => {
112
- return exit
113
- // .transition('fadeout')
114
- // .duration(0)
115
- // .delay(500)
116
- .style('opacity', 0)
117
- .remove()
118
- }
119
- )
120
- .attr('transform', () => `translate(${event.offsetX}, ${event.offsetY})`)
121
-
122
- // tooltipBox <g><g>
123
- const tooltipBox = tooltipG
124
- .selectAll<SVGGElement, string>(`g.${boxClassName}`)
125
- .data(styleData)
126
- .join(
127
- enter => {
128
- return enter
129
- .append('g')
130
- .classed(getClassName(pluginName, 'box'), true)
131
- },
132
- )
133
-
134
- // rect <g><g><rect>
135
- const rect = tooltipBox
136
- .selectAll<SVGRectElement, string>('rect')
137
- .data(styleData)
138
- .join(
139
- enter => {
140
- return enter
141
- .append('rect')
142
- .attr('rx', radius)
143
- .attr('ry', radius)
144
- }
145
- )
146
- .attr('fill', d => d.backgroundColor)
147
- .attr('stroke', d => d.strokeColor)
148
- .attr('opacity', d => d.backgroundOpacity)
149
-
150
- // text <g><g><g>
151
- const contentG = tooltipBox
152
- .selectAll<SVGGElement, string>('g')
153
- .data(contentData)
154
- .join(
155
- enter => {
156
- return enter
157
- .append('g')
158
- .classed(getClassName(pluginName, 'content'), true)
159
- .attr('transform', () => `translate(${tooltipStyle.padding}, ${tooltipStyle.padding})`)
160
- }
161
- )
162
- // 使用字串加入svg
163
- if (contentData.length) {
164
- appendSvg(contentG, contentData[0])
165
- }
166
- const contentSize = (contentG?.node()) ? getSvgGElementSize(contentG!) : { width: 0, height: 0 }
167
-
168
- // rect size
169
- rect
170
- .attr('width', contentSize.width + tooltipStyle.padding * 2)
171
- .attr('height', contentSize.height + tooltipStyle.padding * 2)
172
-
173
- // -- tooltipG --
174
- // 取得tooltip <g>的尺寸
175
- const tooltipSize = (tooltipBox?.node()) ? getSvgGElementSize(tooltipBox!) : { width: 0, height: 0 }
176
- // const minX = 0
177
- const maxX = rootWidth - tooltipSize.width
178
- // const minY = 0
179
- const maxY = rootHeight - tooltipSize.height
180
-
181
- // -- 相對游標位置的offset --
182
- const offsetX = (event.offsetX + tooltipStyle.offset[0]) > maxX ? maxX - event.offsetX : tooltipStyle.offset[0]
183
- const offsetY = (event.offsetY + tooltipStyle.offset[1]) > maxY ? maxY - event.offsetY : tooltipStyle.offset[1]
184
- tooltipBox.attr('transform', d => `translate(${offsetX}, ${offsetY})`)
185
- tooltipBox.attr('transform', d => `translate(${offsetX}, ${offsetY})`)
186
-
187
- // const minX = containerSize.x
188
- // const maxX = containerSize.x + containerSize.width - tooltipSize.width
189
- // const minY = containerSize.y
190
- // const maxY = containerSize.y + containerSize.height - tooltipSize.height
191
-
192
- // .style('position', 'absolute')
193
- // .style('z-index', 10000)
194
- // .style('left', (d) => {
195
- // const x = d.x + this.fullParams.offsetX! - containerSize.x
196
- // if (x < minX) {
197
- // return `${minX}px`
198
- // } else if (x > maxX) {
199
- // return `${maxX}px`
200
- // }
201
- // return `${x}px`
202
- // })
203
- // .style('top', (d) => {
204
- // const y = d.y + this.fullParams.offsetY! - containerSize.y
205
- // if (y < minY) {
206
- // return `${minY}px`
207
- // } else if (y > maxY) {
208
- // return `${maxY}px`
209
- // }
210
- // return `${y}px`
211
- // })
212
- // .html((d) => d.contentHtml)
213
-
214
- }
215
-
216
- function removeTooltip (gClassName: string) {
217
- d3.selectAll(`g.${gClassName}`)
218
- .remove()
219
-
220
- }
221
-
222
- export const createBaseTooltip: BasePluginFn<BaseTooltipContext> = (pluginName: string, {
223
- rootSelection,
224
- fullParams$,
225
- fullChartParams$,
226
- layout$,
227
- event$
228
- }) => {
229
-
230
- const destroy$ = new Subject()
231
-
232
- const gClassName = getClassName(pluginName, 'g')
233
- const boxClassName = getClassName(pluginName, 'box')
234
-
235
- // 事件觸發
236
- const eventMouseover$: Observable<EventTypeMap<any>> = event$.pipe(
237
- takeUntil(destroy$),
238
- filter(d => d.eventName === 'mouseover' || d.eventName === 'mousemove'),
239
- // distinctUntilChanged((prev, current) => prev.eventName === current.eventName)
240
- // map(d => d as EventTypeMap<typeof chartType>)
241
- )
242
- const eventMouseout$: Observable<EventTypeMap<any>> = event$.pipe(
243
- takeUntil(destroy$),
244
- filter(d => d.eventName === 'mouseout'),
245
- // distinctUntilChanged((prev, current) => prev.eventName === current.eventName)
246
- // map(d => d as EventTypeMap<typeof chartType>)
247
- )
248
-
249
- const textSizePx$ = textSizePxObservable(fullChartParams$)
250
-
251
- const tooltipStyle$: Observable<BaseTooltipStyle> = combineLatest({
252
- fullChartParams: fullChartParams$,
253
- fullParams: fullParams$,
254
- textSizePx: textSizePx$
255
- }).pipe(
256
- takeUntil(destroy$),
257
- switchMap(async d => d),
258
- map(data => {
259
- return {
260
- backgroundColor: getColor(data.fullParams.backgroundColorType, data.fullChartParams),
261
- backgroundOpacity: data.fullParams.backgroundOpacity,
262
- strokeColor: getColor(data.fullParams.strokeColorType, data.fullChartParams),
263
- offset: data.fullParams.offset,
264
- padding: data.fullParams.padding,
265
- textSize: data.fullChartParams.styles.textSize,
266
- textSizePx: data.textSizePx,
267
- textColor: getColor(data.fullParams.textColorType, data.fullChartParams),
268
- seriesColors: data.fullChartParams.colors[data.fullChartParams.colorScheme].label
269
- }
270
- })
271
- )
272
-
273
- const contentRenderFn$: Observable<((eventData: EventTypeMap<any>) => string)> = combineLatest({
274
- fullParams: fullParams$,
275
- tooltipStyle: tooltipStyle$
276
- }).pipe(
277
- takeUntil(destroy$),
278
- switchMap(async d => d),
279
- map(data => {
280
- // if (data.fullParams.svgRenderFn) {
281
- // return data.fullParams.svgRenderFn
282
- // }
283
- // // 將textRenderFn回傳的資料使用<text>包裝起來
284
- // return (eventData: EventTypeMap<any>) => {
285
- // const textArr: string | string[] | null = data.fullParams.textRenderFn
286
- // ? data.fullParams.textRenderFn(eventData as any)
287
- // : null
288
- // return textToSvg(textArr, data.tooltipStyle)
289
- // }
290
- return (eventData: EventTypeMap<any>) => {
291
- const renderText: string | string[] = data.fullParams.renderFn(
292
- // mouseover事件的資料
293
- eventData,
294
- // context
295
- {
296
- utils: {
297
- toCurrency,
298
- measureTextWidth
299
- },
300
- styles: data.tooltipStyle
301
- }
302
- )
303
- // string
304
- if (typeof renderText === 'string') {
305
- const trimText = renderText.trim()
306
- const isSvgText = trimText.slice(0, 1) === '<' && trimText.slice(trimText.length - 1, trimText.length) === '>'
307
-
308
- if (isSvgText) {
309
- return renderText // svg字串
310
- } else {
311
- const textArr = renderText.split('\n')
312
- return textToSvg(textArr, data.tooltipStyle) // 多行文字轉svg字串
313
- }
314
- }
315
- // string[]
316
- else if (Array.isArray(renderText)) {
317
- return textToSvg(renderText, data.tooltipStyle) // 多行文字轉svg字串
318
- }
319
- return ''
320
- }
321
- })
322
- )
323
-
324
- const mouseoverTooltipSvg$ = combineLatest({
325
- event: eventMouseover$,
326
- contentRenderFn: contentRenderFn$
327
- }).pipe(
328
- takeUntil(destroy$),
329
- switchMap(async d => d),
330
- map(d => d.contentRenderFn(d.event))
331
- )
332
-
333
- const mouseoutTooltipSvg$ = eventMouseout$.pipe(
334
- takeUntil(destroy$),
335
- map(d => '')
336
- )
337
-
338
- const svgString$ = merge(mouseoverTooltipSvg$, mouseoutTooltipSvg$)
339
- .pipe(
340
- takeUntil(destroy$),
341
- distinctUntilChanged((a, b) => a === b)
342
- )
343
-
344
- const eventTooltip$ = merge(eventMouseover$, eventMouseout$)
345
- .pipe(
346
- takeUntil(destroy$),
347
- // filter(d => {
348
- // return (d.eventName === 'mouseover' || d.eventName === 'mousemove' || d.eventName === 'mouseout')
349
- // && d.event != undefined
350
- // }),
351
- // map(d => d.event!),
352
- )
353
-
354
- combineLatest({
355
- svgString: svgString$,
356
- layout: layout$,
357
- tooltipStyle: tooltipStyle$,
358
- }).pipe(
359
- takeUntil(destroy$),
360
- switchMap(async d => d),
361
- switchMap(data => {
362
- // 只當有event時才產生資料流
363
- return eventTooltip$.pipe(
364
- map(eventTooltip => {
365
- return {
366
- svgString: data.svgString,
367
- layout: data.layout,
368
- tooltipStyle: data.tooltipStyle,
369
- eventTooltip: eventTooltip
370
- }
371
- })
372
- )
373
- })
374
- ).subscribe(data => {
375
- if (data.eventTooltip.eventName === 'mouseout') {
376
- removeTooltip(gClassName)
377
- return
378
- }
379
- renderTooltip({
380
- rootSelection,
381
- pluginName,
382
- gClassName,
383
- boxClassName,
384
- rootWidth: data.layout.rootWidth,
385
- rootHeight: data.layout.rootHeight,
386
- svgString: data.svgString,
387
- tooltipStyle: data.tooltipStyle,
388
- event: data.eventTooltip.event
389
- })
390
- })
391
-
392
-
393
-
394
- // const chartType$ = eventMouseover$.pipe(
395
- // takeUntil(destroy$),
396
- // map(d => d.type),
397
- // distinctUntilChanged()
398
- // )
399
-
400
-
401
- // eventMouseover$.subscribe(event => {
402
-
403
- // })
404
-
405
-
406
- return () => {
407
- destroy$.next(undefined)
408
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ map,
5
+ merge,
6
+ filter,
7
+ switchMap,
8
+ first,
9
+ iif,
10
+ takeUntil,
11
+ Subject,
12
+ Observable,
13
+ distinctUntilChanged } from 'rxjs'
14
+ import type {
15
+ EventTypeMap,
16
+ PluginConstructor,
17
+ ChartType,
18
+ ChartParams,
19
+ Layout
20
+ } from '../../lib/core-types'
21
+ import type { BaseTooltipParams, BaseTooltipStyle } from '../../lib/plugins-basic-types'
22
+ import type { BasePluginFn } from './types'
23
+ import { defineNoneDataPlugin, textSizePxObservable } from '../../lib/core'
24
+ import { getSvgGElementSize, appendSvg } from '../utils/d3Utils'
25
+ import { getColor, getClassName } from '../utils/orbchartsUtils'
26
+ import { measureTextWidth, toCurrency } from '../utils/commonUtils'
27
+
28
+ interface BaseTooltipContext {
29
+ rootSelection: d3.Selection<any, unknown, any, unknown>
30
+ fullParams$: Observable<BaseTooltipParams>
31
+ fullChartParams$: Observable<ChartParams>
32
+ layout$: Observable<Layout>
33
+ event$: Subject<EventTypeMap<any>>
34
+ }
35
+
36
+ // export interface BaseTooltipStyle {
37
+ // backgroundColor: string
38
+ // backgroundOpacity: number
39
+ // strokeColor: string
40
+ // offset: [number, number]
41
+ // padding: number
42
+ // textColor: string
43
+ // textSize: number | string // chartParams上的設定
44
+ // textSizePx: number
45
+ // }
46
+
47
+ function textToSvg (_textArr: string[] | string | null | undefined, textStyle: BaseTooltipStyle) {
48
+ const lineHeight = textStyle.textSizePx * 1.5
49
+
50
+ const textArr = _textArr == null
51
+ ? []
52
+ : Array.isArray(_textArr)
53
+ ? _textArr
54
+ : typeof _textArr === 'string'
55
+ ? _textArr.split('\n')
56
+ : [_textArr]
57
+
58
+ const tspan = textArr
59
+ .filter(d => d != '')
60
+ .map((text, i) => {
61
+ const top = i * lineHeight
62
+ return `<tspan x="0" y="${top}">${text}</tspan>`
63
+ })
64
+ .join('')
65
+
66
+ if (tspan) {
67
+ return `<text font-size="${textStyle.textSize}" fill="${textStyle.textColor}" x="0" y="0" style="dominant-baseline:text-before-edge">
68
+ ${tspan}
69
+ </text>`
70
+ } else {
71
+ return ''
72
+ }
73
+ }
74
+
75
+ function renderTooltip ({ rootSelection, pluginName, gClassName, boxClassName, rootWidth, rootHeight, svgString, tooltipStyle, event }: {
76
+ rootSelection: d3.Selection<any, unknown, any, unknown>
77
+ pluginName: string
78
+ gClassName: string
79
+ boxClassName: string
80
+ rootWidth: number
81
+ rootHeight: number
82
+ svgString: string
83
+ tooltipStyle: BaseTooltipStyle
84
+ event: MouseEvent
85
+ }) {
86
+ // if (!svgString) {
87
+ // return
88
+ // }
89
+ // const rootSelection = d3.select('svg.bpcharts__root')
90
+ // console.log('tooltip', { rootSelection, pluginName, gClassName, boxClassName, rootWidth, rootHeight, svgString, tooltipStyle, event })
91
+ rootSelection.interrupt('fadeout')
92
+ const radius = 5
93
+
94
+ // data(svg string無值則為空陣列)
95
+ const contentData = svgString ? [svgString] : []
96
+ const styleData = svgString ? [tooltipStyle] : []
97
+ // tooltipG <g>
98
+ const tooltipG = rootSelection
99
+ .selectAll<SVGGElement, string>(`g.${gClassName}`)
100
+ .data(contentData)
101
+ .join(
102
+ enter => {
103
+ return enter
104
+ .append('g')
105
+ .classed(gClassName, true)
106
+ .attr('pointer-events', 'none')
107
+ },
108
+ update => {
109
+ return update
110
+ },
111
+ exit => {
112
+ return exit
113
+ // .transition('fadeout')
114
+ // .duration(0)
115
+ // .delay(500)
116
+ .style('opacity', 0)
117
+ .remove()
118
+ }
119
+ )
120
+ .attr('transform', () => `translate(${event.offsetX}, ${event.offsetY})`)
121
+
122
+ // tooltipBox <g><g>
123
+ const tooltipBox = tooltipG
124
+ .selectAll<SVGGElement, string>(`g.${boxClassName}`)
125
+ .data(styleData)
126
+ .join(
127
+ enter => {
128
+ return enter
129
+ .append('g')
130
+ .classed(getClassName(pluginName, 'box'), true)
131
+ },
132
+ )
133
+
134
+ // rect <g><g><rect>
135
+ const rect = tooltipBox
136
+ .selectAll<SVGRectElement, string>('rect')
137
+ .data(styleData)
138
+ .join(
139
+ enter => {
140
+ return enter
141
+ .append('rect')
142
+ .attr('rx', radius)
143
+ .attr('ry', radius)
144
+ }
145
+ )
146
+ .attr('fill', d => d.backgroundColor)
147
+ .attr('stroke', d => d.strokeColor)
148
+ .attr('opacity', d => d.backgroundOpacity)
149
+
150
+ // text <g><g><g>
151
+ const contentG = tooltipBox
152
+ .selectAll<SVGGElement, string>('g')
153
+ .data(contentData)
154
+ .join(
155
+ enter => {
156
+ return enter
157
+ .append('g')
158
+ .classed(getClassName(pluginName, 'content'), true)
159
+ .attr('transform', () => `translate(${tooltipStyle.padding}, ${tooltipStyle.padding})`)
160
+ }
161
+ )
162
+ // 使用字串加入svg
163
+ if (contentData.length) {
164
+ appendSvg(contentG, contentData[0])
165
+ }
166
+ const contentSize = (contentG?.node()) ? getSvgGElementSize(contentG!) : { width: 0, height: 0 }
167
+
168
+ // rect size
169
+ rect
170
+ .attr('width', contentSize.width + tooltipStyle.padding * 2)
171
+ .attr('height', contentSize.height + tooltipStyle.padding * 2)
172
+
173
+ // -- tooltipG --
174
+ // 取得tooltip <g>的尺寸
175
+ const tooltipSize = (tooltipBox?.node()) ? getSvgGElementSize(tooltipBox!) : { width: 0, height: 0 }
176
+ // const minX = 0
177
+ const maxX = rootWidth - tooltipSize.width
178
+ // const minY = 0
179
+ const maxY = rootHeight - tooltipSize.height
180
+
181
+ // -- 相對游標位置的offset --
182
+ const offsetX = (event.offsetX + tooltipStyle.offset[0]) > maxX ? maxX - event.offsetX : tooltipStyle.offset[0]
183
+ const offsetY = (event.offsetY + tooltipStyle.offset[1]) > maxY ? maxY - event.offsetY : tooltipStyle.offset[1]
184
+ tooltipBox.attr('transform', d => `translate(${offsetX}, ${offsetY})`)
185
+ tooltipBox.attr('transform', d => `translate(${offsetX}, ${offsetY})`)
186
+
187
+ // const minX = containerSize.x
188
+ // const maxX = containerSize.x + containerSize.width - tooltipSize.width
189
+ // const minY = containerSize.y
190
+ // const maxY = containerSize.y + containerSize.height - tooltipSize.height
191
+
192
+ // .style('position', 'absolute')
193
+ // .style('z-index', 10000)
194
+ // .style('left', (d) => {
195
+ // const x = d.x + this.fullParams.offsetX! - containerSize.x
196
+ // if (x < minX) {
197
+ // return `${minX}px`
198
+ // } else if (x > maxX) {
199
+ // return `${maxX}px`
200
+ // }
201
+ // return `${x}px`
202
+ // })
203
+ // .style('top', (d) => {
204
+ // const y = d.y + this.fullParams.offsetY! - containerSize.y
205
+ // if (y < minY) {
206
+ // return `${minY}px`
207
+ // } else if (y > maxY) {
208
+ // return `${maxY}px`
209
+ // }
210
+ // return `${y}px`
211
+ // })
212
+ // .html((d) => d.contentHtml)
213
+
214
+ }
215
+
216
+ function removeTooltip (gClassName: string) {
217
+ d3.selectAll(`g.${gClassName}`)
218
+ .remove()
219
+
220
+ }
221
+
222
+ export const createBaseTooltip: BasePluginFn<BaseTooltipContext> = (pluginName: string, {
223
+ rootSelection,
224
+ fullParams$,
225
+ fullChartParams$,
226
+ layout$,
227
+ event$
228
+ }) => {
229
+
230
+ const destroy$ = new Subject()
231
+
232
+ const gClassName = getClassName(pluginName, 'g')
233
+ const boxClassName = getClassName(pluginName, 'box')
234
+
235
+ // 事件觸發
236
+ const eventMouseover$: Observable<EventTypeMap<any>> = event$.pipe(
237
+ takeUntil(destroy$),
238
+ filter(d => d.eventName === 'mouseover' || d.eventName === 'mousemove'),
239
+ // distinctUntilChanged((prev, current) => prev.eventName === current.eventName)
240
+ // map(d => d as EventTypeMap<typeof chartType>)
241
+ )
242
+ const eventMouseout$: Observable<EventTypeMap<any>> = event$.pipe(
243
+ takeUntil(destroy$),
244
+ filter(d => d.eventName === 'mouseout'),
245
+ // distinctUntilChanged((prev, current) => prev.eventName === current.eventName)
246
+ // map(d => d as EventTypeMap<typeof chartType>)
247
+ )
248
+
249
+ const textSizePx$ = textSizePxObservable(fullChartParams$)
250
+
251
+ const tooltipStyle$: Observable<BaseTooltipStyle> = combineLatest({
252
+ fullChartParams: fullChartParams$,
253
+ fullParams: fullParams$,
254
+ textSizePx: textSizePx$
255
+ }).pipe(
256
+ takeUntil(destroy$),
257
+ switchMap(async d => d),
258
+ map(data => {
259
+ return {
260
+ backgroundColor: getColor(data.fullParams.backgroundColorType, data.fullChartParams),
261
+ backgroundOpacity: data.fullParams.backgroundOpacity,
262
+ strokeColor: getColor(data.fullParams.strokeColorType, data.fullChartParams),
263
+ offset: data.fullParams.offset,
264
+ padding: data.fullParams.padding,
265
+ textSize: data.fullChartParams.styles.textSize,
266
+ textSizePx: data.textSizePx,
267
+ textColor: getColor(data.fullParams.textColorType, data.fullChartParams),
268
+ seriesColors: data.fullChartParams.colors[data.fullChartParams.colorScheme].label
269
+ }
270
+ })
271
+ )
272
+
273
+ const contentRenderFn$: Observable<((eventData: EventTypeMap<any>) => string)> = combineLatest({
274
+ fullParams: fullParams$,
275
+ tooltipStyle: tooltipStyle$
276
+ }).pipe(
277
+ takeUntil(destroy$),
278
+ switchMap(async d => d),
279
+ map(data => {
280
+ // if (data.fullParams.svgRenderFn) {
281
+ // return data.fullParams.svgRenderFn
282
+ // }
283
+ // // 將textRenderFn回傳的資料使用<text>包裝起來
284
+ // return (eventData: EventTypeMap<any>) => {
285
+ // const textArr: string | string[] | null = data.fullParams.textRenderFn
286
+ // ? data.fullParams.textRenderFn(eventData as any)
287
+ // : null
288
+ // return textToSvg(textArr, data.tooltipStyle)
289
+ // }
290
+ return (eventData: EventTypeMap<any>) => {
291
+ const renderText: string | string[] = data.fullParams.renderFn(
292
+ // mouseover事件的資料
293
+ eventData,
294
+ // context
295
+ {
296
+ utils: {
297
+ toCurrency,
298
+ measureTextWidth
299
+ },
300
+ styles: data.tooltipStyle
301
+ }
302
+ )
303
+ // string
304
+ if (typeof renderText === 'string') {
305
+ const trimText = renderText.trim()
306
+ const isSvgText = trimText.slice(0, 1) === '<' && trimText.slice(trimText.length - 1, trimText.length) === '>'
307
+
308
+ if (isSvgText) {
309
+ return renderText // svg字串
310
+ } else {
311
+ const textArr = renderText.split('\n')
312
+ return textToSvg(textArr, data.tooltipStyle) // 多行文字轉svg字串
313
+ }
314
+ }
315
+ // string[]
316
+ else if (Array.isArray(renderText)) {
317
+ return textToSvg(renderText, data.tooltipStyle) // 多行文字轉svg字串
318
+ }
319
+ return ''
320
+ }
321
+ })
322
+ )
323
+
324
+ const mouseoverTooltipSvg$ = combineLatest({
325
+ event: eventMouseover$,
326
+ contentRenderFn: contentRenderFn$
327
+ }).pipe(
328
+ takeUntil(destroy$),
329
+ switchMap(async d => d),
330
+ map(d => d.contentRenderFn(d.event))
331
+ )
332
+
333
+ const mouseoutTooltipSvg$ = eventMouseout$.pipe(
334
+ takeUntil(destroy$),
335
+ map(d => '')
336
+ )
337
+
338
+ const svgString$ = merge(mouseoverTooltipSvg$, mouseoutTooltipSvg$)
339
+ .pipe(
340
+ takeUntil(destroy$),
341
+ distinctUntilChanged((a, b) => a === b)
342
+ )
343
+
344
+ const eventTooltip$ = merge(eventMouseover$, eventMouseout$)
345
+ .pipe(
346
+ takeUntil(destroy$),
347
+ // filter(d => {
348
+ // return (d.eventName === 'mouseover' || d.eventName === 'mousemove' || d.eventName === 'mouseout')
349
+ // && d.event != undefined
350
+ // }),
351
+ // map(d => d.event!),
352
+ )
353
+
354
+ combineLatest({
355
+ svgString: svgString$,
356
+ layout: layout$,
357
+ tooltipStyle: tooltipStyle$,
358
+ }).pipe(
359
+ takeUntil(destroy$),
360
+ switchMap(async d => d),
361
+ switchMap(data => {
362
+ // 只當有event時才產生資料流
363
+ return eventTooltip$.pipe(
364
+ map(eventTooltip => {
365
+ return {
366
+ svgString: data.svgString,
367
+ layout: data.layout,
368
+ tooltipStyle: data.tooltipStyle,
369
+ eventTooltip: eventTooltip
370
+ }
371
+ })
372
+ )
373
+ })
374
+ ).subscribe(data => {
375
+ if (data.eventTooltip.eventName === 'mouseout') {
376
+ removeTooltip(gClassName)
377
+ return
378
+ }
379
+ renderTooltip({
380
+ rootSelection,
381
+ pluginName,
382
+ gClassName,
383
+ boxClassName,
384
+ rootWidth: data.layout.rootWidth,
385
+ rootHeight: data.layout.rootHeight,
386
+ svgString: data.svgString,
387
+ tooltipStyle: data.tooltipStyle,
388
+ event: data.eventTooltip.event
389
+ })
390
+ })
391
+
392
+
393
+
394
+ // const chartType$ = eventMouseover$.pipe(
395
+ // takeUntil(destroy$),
396
+ // map(d => d.type),
397
+ // distinctUntilChanged()
398
+ // )
399
+
400
+
401
+ // eventMouseover$.subscribe(event => {
402
+
403
+ // })
404
+
405
+
406
+ return () => {
407
+ destroy$.next(undefined)
408
+ }
409
409
  }