@orbcharts/core 3.0.1 → 3.0.3

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 (78) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-core.es.js +2345 -2229
  3. package/dist/orbcharts-core.umd.js +5 -4
  4. package/dist/src/{utils → multiValue}/multiValueObservables.d.ts +20 -10
  5. package/dist/src/utils/errorMessage.d.ts +5 -0
  6. package/dist/src/utils/index.d.ts +0 -1
  7. package/dist/src/utils/observables.d.ts +3 -2
  8. package/dist/src/utils/orbchartsUtils.d.ts +1 -0
  9. package/lib/core-types.ts +7 -7
  10. package/package.json +46 -42
  11. package/src/AbstractChart.ts +57 -57
  12. package/src/GridChart.ts +24 -24
  13. package/src/MultiGridChart.ts +24 -24
  14. package/src/MultiValueChart.ts +24 -24
  15. package/src/RelationshipChart.ts +24 -24
  16. package/src/SeriesChart.ts +24 -24
  17. package/src/TreeChart.ts +24 -24
  18. package/src/base/createBaseChart.ts +526 -506
  19. package/src/base/createBasePlugin.ts +154 -154
  20. package/src/base/validators/chartOptionsValidator.ts +23 -23
  21. package/src/base/validators/chartParamsValidator.ts +133 -133
  22. package/src/base/validators/elementValidator.ts +13 -13
  23. package/src/base/validators/pluginsValidator.ts +14 -14
  24. package/src/defaults.ts +283 -282
  25. package/src/defineGridPlugin.ts +3 -3
  26. package/src/defineMultiGridPlugin.ts +3 -3
  27. package/src/defineMultiValuePlugin.ts +3 -3
  28. package/src/defineNoneDataPlugin.ts +4 -4
  29. package/src/defineRelationshipPlugin.ts +3 -3
  30. package/src/defineSeriesPlugin.ts +3 -3
  31. package/src/defineTreePlugin.ts +3 -3
  32. package/src/grid/computedDataFn.ts +129 -129
  33. package/src/grid/contextObserverCallback.ts +201 -198
  34. package/src/grid/dataFormatterValidator.ts +125 -120
  35. package/src/grid/dataValidator.ts +12 -12
  36. package/src/{utils → grid}/gridObservables.ts +718 -705
  37. package/src/index.ts +20 -20
  38. package/src/multiGrid/computedDataFn.ts +123 -123
  39. package/src/multiGrid/contextObserverCallback.ts +75 -72
  40. package/src/multiGrid/dataFormatterValidator.ts +120 -115
  41. package/src/multiGrid/dataValidator.ts +12 -12
  42. package/src/{utils → multiGrid}/multiGridObservables.ts +401 -401
  43. package/src/multiValue/computedDataFn.ts +113 -113
  44. package/src/multiValue/contextObserverCallback.ts +328 -276
  45. package/src/multiValue/dataFormatterValidator.ts +94 -89
  46. package/src/multiValue/dataValidator.ts +12 -12
  47. package/src/{utils → multiValue}/multiValueObservables.ts +1219 -1044
  48. package/src/relationship/computedDataFn.ts +159 -159
  49. package/src/relationship/contextObserverCallback.ts +80 -80
  50. package/src/relationship/dataFormatterValidator.ts +13 -13
  51. package/src/relationship/dataValidator.ts +13 -13
  52. package/src/{utils → relationship}/relationshipObservables.ts +84 -84
  53. package/src/series/computedDataFn.ts +88 -88
  54. package/src/series/contextObserverCallback.ts +107 -107
  55. package/src/series/dataFormatterValidator.ts +46 -41
  56. package/src/series/dataValidator.ts +12 -12
  57. package/src/{utils → series}/seriesObservables.ts +175 -175
  58. package/src/tree/computedDataFn.ts +129 -129
  59. package/src/tree/contextObserverCallback.ts +58 -58
  60. package/src/tree/dataFormatterValidator.ts +13 -13
  61. package/src/tree/dataValidator.ts +13 -13
  62. package/src/{utils → tree}/treeObservables.ts +105 -105
  63. package/src/utils/commonUtils.ts +55 -55
  64. package/src/utils/d3Scale.ts +198 -198
  65. package/src/utils/errorMessage.ts +43 -42
  66. package/src/utils/index.ts +4 -10
  67. package/src/utils/observables.ts +293 -281
  68. package/src/utils/orbchartsUtils.ts +396 -377
  69. package/src/utils/validator.ts +126 -126
  70. package/tsconfig.base.json +13 -13
  71. package/tsconfig.json +2 -2
  72. package/vite-env.d.ts +6 -6
  73. package/vite.config.js +22 -22
  74. /package/dist/src/{utils → grid}/gridObservables.d.ts +0 -0
  75. /package/dist/src/{utils → multiGrid}/multiGridObservables.d.ts +0 -0
  76. /package/dist/src/{utils → relationship}/relationshipObservables.d.ts +0 -0
  77. /package/dist/src/{utils → series}/seriesObservables.d.ts +0 -0
  78. /package/dist/src/{utils → tree}/treeObservables.d.ts +0 -0
@@ -1,506 +1,526 @@
1
- import * as d3 from 'd3'
2
- import {
3
- combineLatest,
4
- iif,
5
- of,
6
- EMPTY,
7
- Subject,
8
- BehaviorSubject,
9
- Observable,
10
- first,
11
- takeUntil,
12
- catchError,
13
- throwError } from 'rxjs'
14
- import {
15
- map,
16
- mergeWith,
17
- concatMap,
18
- switchMap,
19
- switchAll,
20
- throttleTime,
21
- debounceTime,
22
- distinctUntilChanged,
23
- share,
24
- shareReplay,
25
- filter,
26
- take,
27
- startWith,
28
- scan,
29
- } from 'rxjs/operators'
30
- import type {
31
- CreateBaseChart,
32
- CreateChart,
33
- ComputedDataFn,
34
- ChartEntity,
35
- ChartType,
36
- ChartParams,
37
- ChartParamsPartial,
38
- ContextSubject,
39
- ComputedDataTypeMap,
40
- ContextObserverCallback,
41
- ChartOptionsPartial,
42
- DataTypeMap,
43
- DataFormatterTypeMap,
44
- DataFormatterPartialTypeMap,
45
- DataFormatterBase,
46
- DataFormatterContext,
47
- DataFormatterValidator,
48
- DataValidator,
49
- Layout,
50
- PluginEntity,
51
- PluginContext,
52
- Preset,
53
- PresetPartial,
54
- ContextObserverTypeMap,
55
- ValidatorResult,
56
- } from '../../lib/core-types'
57
- import { mergeOptionsWithDefault, resizeObservable } from '../utils'
58
- import { createValidatorErrorMessage, createValidatorWarningMessage, createOrbChartsErrorMessage } from '../utils/errorMessage'
59
- import { chartOptionsValidator } from './validators/chartOptionsValidator'
60
- import { elementValidator } from './validators/elementValidator'
61
- import { chartParamsValidator } from './validators/chartParamsValidator'
62
- import { pluginsValidator } from './validators/pluginsValidator'
63
- import {
64
- DEFAULT_CHART_OPTIONS,
65
- DEFAULT_PADDING,
66
- DEFAULT_CHART_PARAMS,
67
- DEFAULT_CHART_WIDTH,
68
- DEFAULT_CHART_HEIGHT } from '../defaults'
69
-
70
- // 判斷dataFormatter是否需要size參數
71
- // const isAxesTypeMap: {[key in ChartType]: Boolean} = {
72
- // series: false,
73
- // grid: true,
74
- // multiGrid: true,
75
- // multiValue: true,
76
- // tree: false,
77
- // relationship: false
78
- // }
79
-
80
-
81
- function mergeDataFormatter <T>(dataFormatter: any, defaultDataFormatter: T, chartType: ChartType): T {
82
- const mergedData = mergeOptionsWithDefault(dataFormatter, defaultDataFormatter)
83
-
84
- if (chartType === 'multiGrid' && (dataFormatter as DataFormatterPartialTypeMap<'multiGrid'>).gridList != null) {
85
- // multiGrid欄位為陣列,需要各別來merge預設值
86
- (mergedData as DataFormatterTypeMap<'multiGrid'>).gridList = (dataFormatter as DataFormatterPartialTypeMap<'multiGrid'>).gridList.map((d, i) => {
87
- const defaultGrid = (defaultDataFormatter as DataFormatterTypeMap<'multiGrid'>).gridList[i] || (defaultDataFormatter as DataFormatterTypeMap<'multiGrid'>).gridList[0]
88
- return mergeOptionsWithDefault(d, defaultGrid)
89
- })
90
- }
91
- return mergedData
92
- }
93
-
94
- export const createBaseChart: CreateBaseChart = <T extends ChartType>({
95
- defaultDataFormatter,
96
- dataFormatterValidator,
97
- computedDataFn,
98
- dataValidator,
99
- contextObserverCallback
100
- }: {
101
- defaultDataFormatter: DataFormatterTypeMap<T>
102
- dataFormatterValidator: DataFormatterValidator<T>
103
- computedDataFn: ComputedDataFn<T>
104
- dataValidator: DataValidator<T>
105
- contextObserverCallback: ContextObserverCallback<T>
106
- }): CreateChart<T> => {
107
- const destroy$ = new Subject()
108
-
109
- const chartType: ChartType = (defaultDataFormatter as unknown as DataFormatterBase<any>).type
110
-
111
- // 建立chart實例
112
- return function createChart (element: HTMLElement | Element, options?: ChartOptionsPartial<T>): ChartEntity<T> {
113
- try {
114
- const { status, columnName, expectToBe } = chartOptionsValidator(options)
115
- if (status === 'error') {
116
- throw new Error(createValidatorErrorMessage({
117
- columnName,
118
- expectToBe,
119
- from: 'Chart.constructor'
120
- }))
121
- } else if (status === 'warning') {
122
- console.warn(createValidatorWarningMessage({
123
- columnName,
124
- expectToBe,
125
- from: 'Chart.constructor'
126
- }))
127
- } else {
128
- const { status, columnName, expectToBe } = elementValidator(element)
129
- if (status === 'error') {
130
- throw new Error(createValidatorErrorMessage({
131
- columnName,
132
- expectToBe,
133
- from: 'Chart.constructor'
134
- }))
135
- } else if (status === 'warning') {
136
- console.warn(createValidatorWarningMessage({
137
- columnName,
138
- expectToBe,
139
- from: 'Chart.constructor'
140
- }))
141
- }
142
- }
143
- } catch (e) {
144
- throw new Error(e)
145
- }
146
-
147
-
148
- // -- selections --
149
- // svg selection
150
- d3.select(element).selectAll('svg').remove()
151
- const svgSelection = d3.select(element).append('svg')
152
- svgSelection
153
- .attr('xmlns:xlink', 'http://www.w3.org/1999/xlink')
154
- .attr('xmls', 'http://www.w3.org/2000/svg')
155
- .attr('version', '1.1')
156
- .style('position', 'absolute')
157
- // .style('width', '100%')
158
- // .style('height', '100%')
159
- .classed('orbcharts__root', true)
160
- // 傳入操作的 selection
161
- const selectionLayout = svgSelection.append('g')
162
- selectionLayout.classed('orbcharts__layout', true)
163
- const selectionPlugins = selectionLayout.append('g')
164
- selectionPlugins.classed('orbcharts__plugins', true)
165
-
166
- // chartSubject
167
- const chartSubject: ContextSubject<T> = {
168
- data$: new Subject(),
169
- dataFormatter$: new Subject(),
170
- plugins$: new Subject(),
171
- chartParams$: new Subject(),
172
- event$: new Subject()
173
- }
174
-
175
- // options
176
- const mergedPresetWithDefault: Preset<T, any> = ((options) => {
177
- const _options = options ? options : DEFAULT_CHART_OPTIONS as ChartOptionsPartial<T>
178
- const preset = _options.preset ? _options.preset : {} as PresetPartial<T, any>
179
-
180
- return {
181
- name: preset.name ?? '',
182
- description: preset.description ?? '',
183
- descriptionZh: preset.descriptionZh ?? '',
184
- chartParams: preset.chartParams
185
- ? mergeOptionsWithDefault(preset.chartParams, DEFAULT_CHART_PARAMS)
186
- : DEFAULT_CHART_PARAMS,
187
- dataFormatter: preset.dataFormatter
188
- // ? mergeOptionsWithDefault(preset.dataFormatter, defaultDataFormatter)
189
- ? mergeDataFormatter(preset.dataFormatter, defaultDataFormatter, chartType)
190
- : defaultDataFormatter,
191
- pluginParams: preset.pluginParams
192
- ? preset.pluginParams
193
- : {}
194
- }
195
- })(options)
196
-
197
- const sharedData$ = chartSubject.data$.pipe(shareReplay(1))
198
- const shareAndMergedDataFormatter$ = chartSubject.dataFormatter$
199
- .pipe(
200
- takeUntil(destroy$),
201
- startWith({} as DataFormatterPartialTypeMap<T>),
202
- map((dataFormatter) => {
203
- try {
204
- // 檢查 dataFormatter$ 資料格式是否正確
205
- const { status, columnName, expectToBe } = dataFormatterValidator(dataFormatter)
206
- if (status === 'error') {
207
- throw new Error(createValidatorErrorMessage({
208
- columnName,
209
- expectToBe,
210
- from: 'Chart.dataFormatter$'
211
- }))
212
- } else if (status === 'warning') {
213
- console.warn(createValidatorWarningMessage({
214
- columnName,
215
- expectToBe,
216
- from: 'Chart.dataFormatter$'
217
- }))
218
- }
219
- } catch (e) {
220
- // throw new Error(e)
221
- // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
222
- console.error(createOrbChartsErrorMessage(e))
223
- }
224
- return mergeDataFormatter(dataFormatter, mergedPresetWithDefault.dataFormatter, chartType)
225
- }),
226
- // catchError((e) => {
227
- // console.error(createOrbChartsErrorMessage(e))
228
- // return EMPTY
229
- // }),
230
- shareReplay(1)
231
- )
232
- const shareAndMergedChartParams$ = chartSubject.chartParams$
233
- .pipe(
234
- takeUntil(destroy$),
235
- startWith({}),
236
- map((d) => {
237
- try {
238
- // 檢查 chartParams$ 資料格式是否正確
239
- const { status, columnName, expectToBe } = chartParamsValidator(chartType, d)
240
- if (status === 'error') {
241
- throw new Error(createValidatorErrorMessage({
242
- columnName,
243
- expectToBe,
244
- from: 'Chart.chartParams$'
245
- }))
246
- } else if (status === 'warning') {
247
- console.warn(createValidatorWarningMessage({
248
- columnName,
249
- expectToBe,
250
- from: 'Chart.chartParams$'
251
- }))
252
- }
253
- } catch (e) {
254
- // throw new Error(e)
255
- // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
256
- console.error(createOrbChartsErrorMessage(e))
257
- }
258
- return mergeOptionsWithDefault(d, mergedPresetWithDefault.chartParams)
259
- }),
260
- // catchError((e) => {
261
- // console.error(createOrbChartsErrorMessage(e))
262
- // return EMPTY
263
- // }),
264
- shareReplay(1)
265
- )
266
-
267
- // -- size --
268
- // padding
269
- const mergedPadding$ = shareAndMergedChartParams$
270
- .pipe(
271
- takeUntil(destroy$),
272
- startWith({}),
273
- map((d: any) => {
274
- return mergeOptionsWithDefault(d.padding ?? {}, DEFAULT_PADDING)
275
- })
276
- )
277
- mergedPadding$
278
- .pipe(
279
- takeUntil(destroy$),
280
- first()
281
- )
282
- .subscribe(d => {
283
- selectionLayout
284
- .attr('transform', `translate(${d.left}, ${d.top})`)
285
- })
286
- mergedPadding$.subscribe(size => {
287
- selectionLayout
288
- .transition()
289
- .attr('transform', `translate(${size.left}, ${size.top})`)
290
- })
291
-
292
- // 監聽外層的element尺寸
293
- const rootSize$: Observable<{ width: number; height: number }> = of({
294
- width: options?.width ?? DEFAULT_CHART_OPTIONS.width,
295
- height: options?.height ?? DEFAULT_CHART_OPTIONS.height
296
- }).pipe(
297
- switchMap(size => {
298
- return iif(
299
- () => size.width === 'auto' || size.height === 'auto',
300
- // 'auto' 的話就監聽element的尺寸
301
- resizeObservable(element).pipe(
302
- map((d) => {
303
- return {
304
- width: size.width === 'auto' ? d.width : size.width,
305
- height: size.height === 'auto' ? d.height : size.height
306
- }
307
- })
308
- ),
309
- of(size as { width: number; height: number })
310
- )
311
- }),
312
- takeUntil(destroy$),
313
- share()
314
- )
315
- const rootSizeFiltered$ = of().pipe(
316
- mergeWith(
317
- rootSize$.pipe(
318
- debounceTime(250)
319
- ),
320
- rootSize$.pipe(
321
- throttleTime(250)
322
- )
323
- ),
324
- distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
325
- share()
326
- )
327
- const rootSizeSubscription = rootSizeFiltered$.subscribe()
328
-
329
- // layout
330
- const layout$: Observable<Layout> = combineLatest({
331
- rootSize: rootSizeFiltered$,
332
- mergedPadding: mergedPadding$
333
- }).pipe(
334
- takeUntil(destroy$),
335
- switchMap(async (d) => {
336
- const rootWidth = d.rootSize.width > 0
337
- ? d.rootSize.width
338
- : DEFAULT_CHART_WIDTH
339
- const rootHeight = d.rootSize.height > 0
340
- ? d.rootSize.height
341
- : DEFAULT_CHART_HEIGHT
342
- return {
343
- width: rootWidth - d.mergedPadding.left - d.mergedPadding.right,
344
- height: rootHeight - d.mergedPadding.top - d.mergedPadding.bottom,
345
- top: d.mergedPadding.top,
346
- right: d.mergedPadding.right,
347
- bottom: d.mergedPadding.bottom,
348
- left: d.mergedPadding.left,
349
- rootWidth,
350
- rootHeight
351
- }
352
- }),
353
- shareReplay(1)
354
- )
355
- layout$.subscribe(d => {
356
- svgSelection
357
- .attr('width', d.rootWidth)
358
- .attr('height', d.rootHeight)
359
- })
360
-
361
- // -- computedData --
362
- const computedData$: Observable<ComputedDataTypeMap<T>> = combineLatest({
363
- data: sharedData$,
364
- dataFormatter: shareAndMergedDataFormatter$,
365
- chartParams: shareAndMergedChartParams$,
366
- // layout: iif(() => isAxesTypeMap[chartType] === true, layout$, of(undefined))
367
- }).pipe(
368
- takeUntil(destroy$),
369
- switchMap(async d => d),
370
- switchMap((d) => {
371
- return of(d)
372
- .pipe(
373
- map(_d => {
374
- try {
375
- // 檢查 data$ 資料格式是否正確
376
- const { status, columnName, expectToBe } = dataValidator(_d.data)
377
- if (status === 'error') {
378
- throw new Error(createValidatorErrorMessage({
379
- columnName,
380
- expectToBe,
381
- from: 'Chart.data$'
382
- }))
383
- } else if (status === 'warning') {
384
- console.warn(createValidatorWarningMessage({
385
- columnName,
386
- expectToBe,
387
- from: 'Chart.data$'
388
- }))
389
- }
390
- } catch (e) {
391
- // throw new Error(e)
392
- // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
393
- console.error(createOrbChartsErrorMessage(e))
394
- }
395
- return computedDataFn({ data: _d.data, dataFormatter: _d.dataFormatter, chartParams: _d.chartParams })
396
- }),
397
- // catchError((e) => {
398
- // console.error(createOrbChartsErrorMessage(e))
399
- // return EMPTY
400
- // })
401
- )
402
- }),
403
- shareReplay(1)
404
- )
405
-
406
- // subscribe - computedData組合了所有的chart參數,所以訂閱computedData可以一次訂閱所有的資料流
407
- computedData$.subscribe()
408
-
409
- // -- plugins --
410
- const pluginEntityMap: any = {} // 用於destroy
411
- chartSubject.plugins$.subscribe(plugins => {
412
- try {
413
- // 檢查 plugins$ 資料格式是否正確
414
- const { status, columnName, expectToBe } = pluginsValidator(chartType, plugins)
415
- if (status === 'error') {
416
- throw new Error(createValidatorErrorMessage({
417
- columnName,
418
- expectToBe,
419
- from: 'Chart.plugins$'
420
- }))
421
- } else if (status === 'warning') {
422
- console.warn(createValidatorWarningMessage({
423
- columnName,
424
- expectToBe,
425
- from: 'Chart.plugins$'
426
- }))
427
- }
428
- } catch (e) {
429
- console.error(createOrbChartsErrorMessage(e))
430
- return
431
- // throw new Error(e)
432
- }
433
-
434
- selectionPlugins
435
- .selectAll<SVGGElement, PluginEntity<T, any, any>>('g.orbcharts__plugin')
436
- .data(plugins, d => d.name as string)
437
- .join(
438
- enter => {
439
- return enter
440
- .append('g')
441
- .attr('class', plugin => {
442
- return `orbcharts__plugin orbcharts__${plugin.name}`
443
- })
444
- .each((plugin, i, n) => {
445
- const _pluginObserverBase = {
446
- fullParams$: new Observable(),
447
- fullChartParams$: shareAndMergedChartParams$,
448
- fullDataFormatter$: shareAndMergedDataFormatter$,
449
- computedData$,
450
- layout$
451
- }
452
- const pluginObserver: ContextObserverTypeMap<T, typeof plugin.defaultParams> = contextObserverCallback({
453
- observer: _pluginObserverBase,
454
- subject: chartSubject
455
- })
456
-
457
- // -- createPlugin(plugin) --
458
- const pluginSelection = d3.select(n[i])
459
- const pluginContext: PluginContext<T, typeof plugin.name, typeof plugin.defaultParams> = {
460
- selection: pluginSelection,
461
- rootSelection: svgSelection,
462
- name: plugin.name,
463
- chartType,
464
- subject: chartSubject,
465
- observer: pluginObserver
466
- }
467
-
468
- plugin.setPresetParams(mergedPresetWithDefault.pluginParams[plugin.name] ?? {})
469
- // 傳入context
470
- plugin.setContext(pluginContext)
471
-
472
- // 紀錄起來
473
- pluginEntityMap[pluginContext.name as string] = plugin
474
-
475
- // init plugin
476
- plugin.init()
477
-
478
- })
479
- },
480
- update => update,
481
- exit => {
482
- return exit
483
- .each((plugin: PluginEntity<T, unknown, unknown>, i, n) => {
484
- if (pluginEntityMap[plugin.name as string]) {
485
- pluginEntityMap[plugin.name as string].destroy()
486
- pluginEntityMap[plugin.name as string] = undefined
487
- }
488
- })
489
- .remove()
490
- }
491
- )
492
- .sort((a, b) => a.layerIndex - b.layerIndex)
493
-
494
- })
495
-
496
- return {
497
- ...chartSubject,
498
- selection: svgSelection,
499
- destroy () {
500
- d3.select(element).selectAll('svg').remove()
501
- destroy$.next(undefined)
502
- rootSizeSubscription.unsubscribe()
503
- }
504
- }
505
- }
506
- }
1
+ import * as d3 from 'd3'
2
+ import {
3
+ combineLatest,
4
+ iif,
5
+ of,
6
+ EMPTY,
7
+ Subject,
8
+ BehaviorSubject,
9
+ Observable,
10
+ first,
11
+ takeUntil,
12
+ catchError,
13
+ throwError } from 'rxjs'
14
+ import {
15
+ map,
16
+ mergeWith,
17
+ concatMap,
18
+ switchMap,
19
+ switchAll,
20
+ throttleTime,
21
+ debounceTime,
22
+ distinctUntilChanged,
23
+ share,
24
+ shareReplay,
25
+ filter,
26
+ take,
27
+ startWith,
28
+ scan,
29
+ } from 'rxjs/operators'
30
+ import type {
31
+ CreateBaseChart,
32
+ CreateChart,
33
+ ComputedDataFn,
34
+ ChartEntity,
35
+ ChartType,
36
+ ChartParams,
37
+ ChartParamsPartial,
38
+ ContextSubject,
39
+ ComputedDataTypeMap,
40
+ ContextObserverCallback,
41
+ ChartOptionsPartial,
42
+ DataTypeMap,
43
+ DataFormatterTypeMap,
44
+ DataFormatterPartialTypeMap,
45
+ DataFormatterBase,
46
+ DataFormatterContext,
47
+ DataFormatterValidator,
48
+ DataValidator,
49
+ Layout,
50
+ PluginEntity,
51
+ PluginContext,
52
+ Preset,
53
+ PresetPartial,
54
+ ContextObserverTypeMap,
55
+ ValidatorResult,
56
+ } from '../../lib/core-types'
57
+ import { mergeOptionsWithDefault, resizeObservable } from '../utils'
58
+ import { createValidatorErrorMessage, createValidatorWarningMessage, createUnexpectedErrorMessage, createOrbChartsErrorMessage } from '../utils/errorMessage'
59
+ import { chartOptionsValidator } from './validators/chartOptionsValidator'
60
+ import { elementValidator } from './validators/elementValidator'
61
+ import { chartParamsValidator } from './validators/chartParamsValidator'
62
+ import { pluginsValidator } from './validators/pluginsValidator'
63
+ import {
64
+ DEFAULT_CHART_OPTIONS,
65
+ DEFAULT_PADDING,
66
+ DEFAULT_CHART_PARAMS,
67
+ DEFAULT_CHART_WIDTH,
68
+ DEFAULT_CHART_HEIGHT } from '../defaults'
69
+
70
+ // 判斷dataFormatter是否需要size參數
71
+ // const isAxesTypeMap: {[key in ChartType]: Boolean} = {
72
+ // series: false,
73
+ // grid: true,
74
+ // multiGrid: true,
75
+ // multiValue: true,
76
+ // tree: false,
77
+ // relationship: false
78
+ // }
79
+
80
+
81
+ function mergeDataFormatter <T>(dataFormatter: any, defaultDataFormatter: T, chartType: ChartType): T {
82
+ const mergedData = mergeOptionsWithDefault(dataFormatter, defaultDataFormatter)
83
+
84
+ if (chartType === 'multiGrid' && (dataFormatter as DataFormatterPartialTypeMap<'multiGrid'>).gridList != null) {
85
+ // multiGrid欄位為陣列,需要各別來merge預設值
86
+ (mergedData as DataFormatterTypeMap<'multiGrid'>).gridList = (dataFormatter as DataFormatterPartialTypeMap<'multiGrid'>).gridList.map((d, i) => {
87
+ const defaultGrid = (defaultDataFormatter as DataFormatterTypeMap<'multiGrid'>).gridList[i] || (defaultDataFormatter as DataFormatterTypeMap<'multiGrid'>).gridList[0]
88
+ return mergeOptionsWithDefault(d, defaultGrid)
89
+ })
90
+ }
91
+ return mergedData
92
+ }
93
+
94
+ export const createBaseChart: CreateBaseChart = <T extends ChartType>({
95
+ defaultDataFormatter,
96
+ dataFormatterValidator,
97
+ computedDataFn,
98
+ dataValidator,
99
+ contextObserverCallback
100
+ }: {
101
+ defaultDataFormatter: DataFormatterTypeMap<T>
102
+ dataFormatterValidator: DataFormatterValidator<T>
103
+ computedDataFn: ComputedDataFn<T>
104
+ dataValidator: DataValidator<T>
105
+ contextObserverCallback: ContextObserverCallback<T>
106
+ }): CreateChart<T> => {
107
+ const destroy$ = new Subject()
108
+
109
+ const chartType: ChartType = (defaultDataFormatter as unknown as DataFormatterBase<any>).type
110
+
111
+ // 建立chart實例
112
+ return function createChart (element: HTMLElement | Element, options?: ChartOptionsPartial<T>): ChartEntity<T> {
113
+ try {
114
+ const { status, columnName, expectToBe } = chartOptionsValidator(options)
115
+ if (status === 'error') {
116
+ throw new Error(createValidatorErrorMessage({
117
+ columnName,
118
+ expectToBe,
119
+ from: 'Chart.constructor'
120
+ }))
121
+ } else if (status === 'warning') {
122
+ console.warn(createValidatorWarningMessage({
123
+ columnName,
124
+ expectToBe,
125
+ from: 'Chart.constructor'
126
+ }))
127
+ } else {
128
+ const { status, columnName, expectToBe } = elementValidator(element)
129
+ if (status === 'error') {
130
+ throw new Error(createValidatorErrorMessage({
131
+ columnName,
132
+ expectToBe,
133
+ from: 'Chart.constructor'
134
+ }))
135
+ } else if (status === 'warning') {
136
+ console.warn(createValidatorWarningMessage({
137
+ columnName,
138
+ expectToBe,
139
+ from: 'Chart.constructor'
140
+ }))
141
+ }
142
+ }
143
+ } catch (e) {
144
+ throw new Error(e)
145
+ }
146
+
147
+
148
+ // -- selections --
149
+ // svg selection
150
+ d3.select(element).selectAll('svg').remove()
151
+ const svgSelection = d3.select(element).append('svg')
152
+ svgSelection
153
+ .attr('xmlns:xlink', 'http://www.w3.org/1999/xlink')
154
+ .attr('xmls', 'http://www.w3.org/2000/svg')
155
+ .attr('version', '1.1')
156
+ .style('position', 'absolute')
157
+ // .style('width', '100%')
158
+ // .style('height', '100%')
159
+ .classed('orbcharts__root', true)
160
+ // 傳入操作的 selection
161
+ const selectionLayout = svgSelection.append('g')
162
+ selectionLayout.classed('orbcharts__layout', true)
163
+ const selectionPlugins = selectionLayout.append('g')
164
+ selectionPlugins.classed('orbcharts__plugins', true)
165
+
166
+ // chartSubject
167
+ const chartSubject: ContextSubject<T> = {
168
+ data$: new Subject(),
169
+ dataFormatter$: new Subject(),
170
+ plugins$: new Subject(),
171
+ chartParams$: new Subject(),
172
+ event$: new Subject()
173
+ }
174
+
175
+ // options
176
+ const mergedPresetWithDefault: Preset<T, any> = ((options) => {
177
+ const _options = options ? options : DEFAULT_CHART_OPTIONS as ChartOptionsPartial<T>
178
+ const preset = _options.preset ? _options.preset : {} as PresetPartial<T, any>
179
+
180
+ return {
181
+ name: preset.name ?? '',
182
+ description: preset.description ?? '',
183
+ descriptionZh: preset.descriptionZh ?? '',
184
+ chartParams: preset.chartParams
185
+ ? mergeOptionsWithDefault(preset.chartParams, DEFAULT_CHART_PARAMS)
186
+ : DEFAULT_CHART_PARAMS,
187
+ dataFormatter: preset.dataFormatter
188
+ // ? mergeOptionsWithDefault(preset.dataFormatter, defaultDataFormatter)
189
+ ? mergeDataFormatter(preset.dataFormatter, defaultDataFormatter, chartType)
190
+ : defaultDataFormatter,
191
+ pluginParams: preset.pluginParams
192
+ ? preset.pluginParams
193
+ : {}
194
+ }
195
+ })(options)
196
+
197
+ const sharedData$ = chartSubject.data$.pipe(shareReplay(1))
198
+ const shareAndMergedDataFormatter$ = chartSubject.dataFormatter$
199
+ .pipe(
200
+ takeUntil(destroy$),
201
+ startWith({} as DataFormatterPartialTypeMap<T>),
202
+ map((dataFormatter) => {
203
+ try {
204
+ // 檢查 dataFormatter$ 資料格式是否正確
205
+ const { status, columnName, expectToBe } = dataFormatterValidator(dataFormatter)
206
+ if (status === 'error') {
207
+ throw new Error(createValidatorErrorMessage({
208
+ columnName,
209
+ expectToBe,
210
+ from: 'Chart.dataFormatter$'
211
+ }))
212
+ } else if (status === 'warning') {
213
+ console.warn(createValidatorWarningMessage({
214
+ columnName,
215
+ expectToBe,
216
+ from: 'Chart.dataFormatter$'
217
+ }))
218
+ }
219
+ } catch (e) {
220
+ // throw new Error(e)
221
+ // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
222
+ console.error(createOrbChartsErrorMessage(e))
223
+ }
224
+ try {
225
+ return mergeDataFormatter(dataFormatter, mergedPresetWithDefault.dataFormatter, chartType)
226
+ } catch (e) {
227
+ throw new Error(createUnexpectedErrorMessage({
228
+ from: 'Chart.dataFormatter$',
229
+ systemMessage: e
230
+ }))
231
+ }
232
+ }),
233
+ // catchError((e) => {
234
+ // console.error(createOrbChartsErrorMessage(e))
235
+ // return EMPTY
236
+ // }),
237
+ shareReplay(1)
238
+ )
239
+ const shareAndMergedChartParams$ = chartSubject.chartParams$
240
+ .pipe(
241
+ takeUntil(destroy$),
242
+ startWith({}),
243
+ map((d) => {
244
+ try {
245
+ // 檢查 chartParams$ 資料格式是否正確
246
+ const { status, columnName, expectToBe } = chartParamsValidator(chartType, d)
247
+ if (status === 'error') {
248
+ throw new Error(createValidatorErrorMessage({
249
+ columnName,
250
+ expectToBe,
251
+ from: 'Chart.chartParams$'
252
+ }))
253
+ } else if (status === 'warning') {
254
+ console.warn(createValidatorWarningMessage({
255
+ columnName,
256
+ expectToBe,
257
+ from: 'Chart.chartParams$'
258
+ }))
259
+ }
260
+ } catch (e) {
261
+ // throw new Error(e)
262
+ // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
263
+ console.error(createOrbChartsErrorMessage(e))
264
+ }
265
+ try {
266
+ return mergeOptionsWithDefault(d, mergedPresetWithDefault.chartParams)
267
+ } catch (e) {
268
+ throw new Error(createUnexpectedErrorMessage({
269
+ from: 'Chart.chartParams$',
270
+ systemMessage: e
271
+ }))
272
+ }
273
+ }),
274
+ // catchError((e) => {
275
+ // console.error(createOrbChartsErrorMessage(e))
276
+ // return EMPTY
277
+ // }),
278
+ shareReplay(1)
279
+ )
280
+
281
+ // -- size --
282
+ // padding
283
+ const mergedPadding$ = shareAndMergedChartParams$
284
+ .pipe(
285
+ takeUntil(destroy$),
286
+ startWith({}),
287
+ map((d: any) => {
288
+ return mergeOptionsWithDefault(d.padding ?? {}, DEFAULT_PADDING)
289
+ })
290
+ )
291
+ mergedPadding$
292
+ .pipe(
293
+ takeUntil(destroy$),
294
+ first()
295
+ )
296
+ .subscribe(d => {
297
+ selectionLayout
298
+ .attr('transform', `translate(${d.left}, ${d.top})`)
299
+ })
300
+ mergedPadding$.subscribe(size => {
301
+ selectionLayout
302
+ .transition()
303
+ .attr('transform', `translate(${size.left}, ${size.top})`)
304
+ })
305
+
306
+ // 監聽外層的element尺寸
307
+ const rootSize$: Observable<{ width: number; height: number }> = of({
308
+ width: options?.width ?? DEFAULT_CHART_OPTIONS.width,
309
+ height: options?.height ?? DEFAULT_CHART_OPTIONS.height
310
+ }).pipe(
311
+ switchMap(size => {
312
+ return iif(
313
+ () => size.width === 'auto' || size.height === 'auto',
314
+ // 有 'auto' 的話就監聽element的尺寸
315
+ resizeObservable(element).pipe(
316
+ map((d) => {
317
+ return {
318
+ width: size.width === 'auto' ? d.width : size.width,
319
+ height: size.height === 'auto' ? d.height : size.height
320
+ }
321
+ })
322
+ ),
323
+ of(size as { width: number; height: number })
324
+ )
325
+ }),
326
+ takeUntil(destroy$),
327
+ share()
328
+ )
329
+ const rootSizeFiltered$ = of().pipe(
330
+ mergeWith(
331
+ rootSize$.pipe(
332
+ debounceTime(250)
333
+ ),
334
+ rootSize$.pipe(
335
+ throttleTime(250)
336
+ )
337
+ ),
338
+ distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
339
+ share()
340
+ )
341
+ const rootSizeSubscription = rootSizeFiltered$.subscribe()
342
+
343
+ // layout
344
+ const layout$: Observable<Layout> = combineLatest({
345
+ rootSize: rootSizeFiltered$,
346
+ mergedPadding: mergedPadding$
347
+ }).pipe(
348
+ takeUntil(destroy$),
349
+ switchMap(async (d) => {
350
+ const rootWidth = d.rootSize.width > 0
351
+ ? d.rootSize.width
352
+ : DEFAULT_CHART_WIDTH
353
+ const rootHeight = d.rootSize.height > 0
354
+ ? d.rootSize.height
355
+ : DEFAULT_CHART_HEIGHT
356
+ return {
357
+ width: rootWidth - d.mergedPadding.left - d.mergedPadding.right,
358
+ height: rootHeight - d.mergedPadding.top - d.mergedPadding.bottom,
359
+ top: d.mergedPadding.top,
360
+ right: d.mergedPadding.right,
361
+ bottom: d.mergedPadding.bottom,
362
+ left: d.mergedPadding.left,
363
+ rootWidth,
364
+ rootHeight
365
+ }
366
+ }),
367
+ shareReplay(1)
368
+ )
369
+ layout$.subscribe(d => {
370
+ svgSelection
371
+ .attr('width', d.rootWidth)
372
+ .attr('height', d.rootHeight)
373
+ })
374
+
375
+ // -- computedData --
376
+ const computedData$: Observable<ComputedDataTypeMap<T>> = combineLatest({
377
+ data: sharedData$,
378
+ dataFormatter: shareAndMergedDataFormatter$,
379
+ chartParams: shareAndMergedChartParams$,
380
+ // layout: iif(() => isAxesTypeMap[chartType] === true, layout$, of(undefined))
381
+ }).pipe(
382
+ takeUntil(destroy$),
383
+ switchMap(async d => d),
384
+ switchMap((d) => {
385
+ return of(d)
386
+ .pipe(
387
+ map(_d => {
388
+ try {
389
+ // 檢查 data$ 資料格式是否正確
390
+ const { status, columnName, expectToBe } = dataValidator(_d.data)
391
+ if (status === 'error') {
392
+ throw new Error(createValidatorErrorMessage({
393
+ columnName,
394
+ expectToBe,
395
+ from: 'Chart.data$'
396
+ }))
397
+ } else if (status === 'warning') {
398
+ console.warn(createValidatorWarningMessage({
399
+ columnName,
400
+ expectToBe,
401
+ from: 'Chart.data$'
402
+ }))
403
+ }
404
+ } catch (e) {
405
+ // throw new Error(e)
406
+ // 驗證失敗仍繼續執行,才不會把 Observable 資料流給中斷掉
407
+ console.error(createOrbChartsErrorMessage(e))
408
+ }
409
+ try {
410
+ return computedDataFn({ data: _d.data, dataFormatter: _d.dataFormatter, chartParams: _d.chartParams })
411
+ } catch (e) {
412
+ throw new Error(createUnexpectedErrorMessage({
413
+ from: 'Chart.data$',
414
+ systemMessage: e
415
+ }))
416
+ }
417
+ }),
418
+ // catchError((e) => {
419
+ // console.error(createOrbChartsErrorMessage(e))
420
+ // return EMPTY
421
+ // })
422
+ )
423
+ }),
424
+ shareReplay(1)
425
+ )
426
+
427
+ // subscribe - computedData組合了所有的chart參數,所以訂閱computedData可以一次訂閱所有的資料流
428
+ computedData$.subscribe()
429
+
430
+ // -- plugins --
431
+ const pluginEntityMap: any = {} // 用於destroy
432
+ chartSubject.plugins$.subscribe(plugins => {
433
+ try {
434
+ // 檢查 plugins$ 資料格式是否正確
435
+ const { status, columnName, expectToBe } = pluginsValidator(chartType, plugins)
436
+ if (status === 'error') {
437
+ throw new Error(createValidatorErrorMessage({
438
+ columnName,
439
+ expectToBe,
440
+ from: 'Chart.plugins$'
441
+ }))
442
+ } else if (status === 'warning') {
443
+ console.warn(createValidatorWarningMessage({
444
+ columnName,
445
+ expectToBe,
446
+ from: 'Chart.plugins$'
447
+ }))
448
+ }
449
+ } catch (e) {
450
+ // plugin驗證失敗就不執行
451
+ throw new Error(e)
452
+ }
453
+
454
+ selectionPlugins
455
+ .selectAll<SVGGElement, PluginEntity<T, any, any>>('g.orbcharts__plugin')
456
+ .data(plugins, d => d.name as string)
457
+ .join(
458
+ enter => {
459
+ return enter
460
+ .append('g')
461
+ .attr('class', plugin => {
462
+ return `orbcharts__plugin orbcharts__${plugin.name}`
463
+ })
464
+ .each((plugin, i, n) => {
465
+ const _pluginObserverBase = {
466
+ fullParams$: new Observable(),
467
+ fullChartParams$: shareAndMergedChartParams$,
468
+ fullDataFormatter$: shareAndMergedDataFormatter$,
469
+ computedData$,
470
+ layout$
471
+ }
472
+ const pluginObserver: ContextObserverTypeMap<T, typeof plugin.defaultParams> = contextObserverCallback({
473
+ observer: _pluginObserverBase,
474
+ subject: chartSubject
475
+ })
476
+
477
+ // -- createPlugin(plugin) --
478
+ const pluginSelection = d3.select(n[i])
479
+ const pluginContext: PluginContext<T, typeof plugin.name, typeof plugin.defaultParams> = {
480
+ selection: pluginSelection,
481
+ rootSelection: svgSelection,
482
+ name: plugin.name,
483
+ chartType,
484
+ subject: chartSubject,
485
+ observer: pluginObserver
486
+ }
487
+
488
+ plugin.setPresetParams(mergedPresetWithDefault.pluginParams[plugin.name] ?? {})
489
+ // 傳入context
490
+ plugin.setContext(pluginContext)
491
+
492
+ // 紀錄起來
493
+ pluginEntityMap[pluginContext.name as string] = plugin
494
+
495
+ // init plugin
496
+ plugin.init()
497
+
498
+ })
499
+ },
500
+ update => update,
501
+ exit => {
502
+ return exit
503
+ .each((plugin: PluginEntity<T, unknown, unknown>, i, n) => {
504
+ if (pluginEntityMap[plugin.name as string]) {
505
+ pluginEntityMap[plugin.name as string].destroy()
506
+ pluginEntityMap[plugin.name as string] = undefined
507
+ }
508
+ })
509
+ .remove()
510
+ }
511
+ )
512
+ .sort((a, b) => a.layerIndex - b.layerIndex)
513
+
514
+ })
515
+
516
+ return {
517
+ ...chartSubject,
518
+ selection: svgSelection,
519
+ destroy () {
520
+ d3.select(element).selectAll('svg').remove()
521
+ destroy$.next(undefined)
522
+ rootSizeSubscription.unsubscribe()
523
+ }
524
+ }
525
+ }
526
+ }