@orbcharts/core 3.0.4 → 3.0.5

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 (69) hide show
  1. package/LICENSE +200 -200
  2. package/dist/orbcharts-core.es.js +1685 -1636
  3. package/dist/orbcharts-core.umd.js +4 -4
  4. package/dist/src/series/seriesObservables.d.ts +18 -7
  5. package/lib/core-types.ts +7 -7
  6. package/package.json +46 -46
  7. package/src/AbstractChart.ts +57 -57
  8. package/src/GridChart.ts +24 -24
  9. package/src/MultiGridChart.ts +24 -24
  10. package/src/MultiValueChart.ts +24 -24
  11. package/src/RelationshipChart.ts +24 -24
  12. package/src/SeriesChart.ts +24 -24
  13. package/src/TreeChart.ts +24 -24
  14. package/src/base/createBaseChart.ts +524 -524
  15. package/src/base/createBasePlugin.ts +154 -154
  16. package/src/base/validators/chartOptionsValidator.ts +23 -23
  17. package/src/base/validators/chartParamsValidator.ts +133 -133
  18. package/src/base/validators/elementValidator.ts +13 -13
  19. package/src/base/validators/pluginsValidator.ts +14 -14
  20. package/src/defaults.ts +284 -283
  21. package/src/defineGridPlugin.ts +3 -3
  22. package/src/defineMultiGridPlugin.ts +3 -3
  23. package/src/defineMultiValuePlugin.ts +3 -3
  24. package/src/defineNoneDataPlugin.ts +4 -4
  25. package/src/defineRelationshipPlugin.ts +3 -3
  26. package/src/defineSeriesPlugin.ts +3 -3
  27. package/src/defineTreePlugin.ts +3 -3
  28. package/src/grid/computedDataFn.ts +129 -129
  29. package/src/grid/contextObserverCallback.ts +201 -201
  30. package/src/grid/dataFormatterValidator.ts +125 -125
  31. package/src/grid/dataValidator.ts +12 -12
  32. package/src/grid/gridObservables.ts +694 -718
  33. package/src/index.ts +20 -20
  34. package/src/multiGrid/computedDataFn.ts +123 -123
  35. package/src/multiGrid/contextObserverCallback.ts +75 -75
  36. package/src/multiGrid/dataFormatterValidator.ts +120 -120
  37. package/src/multiGrid/dataValidator.ts +12 -12
  38. package/src/multiGrid/multiGridObservables.ts +357 -401
  39. package/src/multiValue/computedDataFn.ts +113 -113
  40. package/src/multiValue/contextObserverCallback.ts +328 -328
  41. package/src/multiValue/dataFormatterValidator.ts +94 -94
  42. package/src/multiValue/dataValidator.ts +12 -12
  43. package/src/multiValue/multiValueObservables.ts +865 -1219
  44. package/src/relationship/computedDataFn.ts +159 -159
  45. package/src/relationship/contextObserverCallback.ts +80 -80
  46. package/src/relationship/dataFormatterValidator.ts +13 -13
  47. package/src/relationship/dataValidator.ts +13 -13
  48. package/src/relationship/relationshipObservables.ts +84 -84
  49. package/src/series/computedDataFn.ts +88 -88
  50. package/src/series/contextObserverCallback.ts +132 -107
  51. package/src/series/dataFormatterValidator.ts +46 -46
  52. package/src/series/dataValidator.ts +12 -12
  53. package/src/series/seriesObservables.ts +209 -175
  54. package/src/tree/computedDataFn.ts +129 -129
  55. package/src/tree/contextObserverCallback.ts +58 -58
  56. package/src/tree/dataFormatterValidator.ts +13 -13
  57. package/src/tree/dataValidator.ts +13 -13
  58. package/src/tree/treeObservables.ts +105 -105
  59. package/src/utils/commonUtils.ts +55 -55
  60. package/src/utils/d3Scale.ts +198 -198
  61. package/src/utils/errorMessage.ts +40 -40
  62. package/src/utils/index.ts +3 -3
  63. package/src/utils/observables.ts +308 -293
  64. package/src/utils/orbchartsUtils.ts +396 -396
  65. package/src/utils/validator.ts +126 -126
  66. package/tsconfig.base.json +13 -13
  67. package/tsconfig.json +2 -2
  68. package/vite-env.d.ts +6 -6
  69. package/vite.config.js +22 -22
@@ -1,524 +1,524 @@
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
- // 不中斷資料流
221
- console.error(createOrbChartsErrorMessage(e))
222
- }
223
- try {
224
- return mergeDataFormatter(dataFormatter, mergedPresetWithDefault.dataFormatter, chartType)
225
- } catch (e) {
226
- throw new Error(createUnexpectedErrorMessage({
227
- from: 'Chart.dataFormatter$',
228
- systemMessage: e
229
- }))
230
- }
231
- }),
232
- catchError((e) => {
233
- console.error(createOrbChartsErrorMessage(e))
234
- return EMPTY
235
- }),
236
- shareReplay(1)
237
- )
238
- const shareAndMergedChartParams$ = chartSubject.chartParams$
239
- .pipe(
240
- takeUntil(destroy$),
241
- startWith({}),
242
- map((d) => {
243
- try {
244
- // 檢查 chartParams$ 資料格式是否正確
245
- const { status, columnName, expectToBe } = chartParamsValidator(chartType, d)
246
- if (status === 'error') {
247
- throw new Error(createValidatorErrorMessage({
248
- columnName,
249
- expectToBe,
250
- from: 'Chart.chartParams$'
251
- }))
252
- } else if (status === 'warning') {
253
- console.warn(createValidatorWarningMessage({
254
- columnName,
255
- expectToBe,
256
- from: 'Chart.chartParams$'
257
- }))
258
- }
259
- } catch (e) {
260
- // 不中斷資料流
261
- console.error(createOrbChartsErrorMessage(e))
262
- }
263
- try {
264
- return mergeOptionsWithDefault(d, mergedPresetWithDefault.chartParams)
265
- } catch (e) {
266
- throw new Error(createUnexpectedErrorMessage({
267
- from: 'Chart.chartParams$',
268
- systemMessage: e
269
- }))
270
- }
271
- }),
272
- catchError((e) => {
273
- console.error(createOrbChartsErrorMessage(e))
274
- return EMPTY
275
- }),
276
- shareReplay(1)
277
- )
278
-
279
- // -- size --
280
- // padding
281
- const mergedPadding$ = shareAndMergedChartParams$
282
- .pipe(
283
- takeUntil(destroy$),
284
- startWith({}),
285
- map((d: any) => {
286
- return mergeOptionsWithDefault(d.padding ?? {}, DEFAULT_PADDING)
287
- })
288
- )
289
- mergedPadding$
290
- .pipe(
291
- takeUntil(destroy$),
292
- first()
293
- )
294
- .subscribe(d => {
295
- selectionLayout
296
- .attr('transform', `translate(${d.left}, ${d.top})`)
297
- })
298
- mergedPadding$.subscribe(size => {
299
- selectionLayout
300
- .transition()
301
- .attr('transform', `translate(${size.left}, ${size.top})`)
302
- })
303
-
304
- // 監聽外層的element尺寸
305
- const rootSize$: Observable<{ width: number; height: number }> = of({
306
- width: options?.width ?? DEFAULT_CHART_OPTIONS.width,
307
- height: options?.height ?? DEFAULT_CHART_OPTIONS.height
308
- }).pipe(
309
- switchMap(size => {
310
- return iif(
311
- () => size.width === 'auto' || size.height === 'auto',
312
- // 有 'auto' 的話就監聽element的尺寸
313
- resizeObservable(element).pipe(
314
- map((d) => {
315
- return {
316
- width: size.width === 'auto' ? d.width : size.width,
317
- height: size.height === 'auto' ? d.height : size.height
318
- }
319
- })
320
- ),
321
- of(size as { width: number; height: number })
322
- )
323
- }),
324
- takeUntil(destroy$),
325
- share()
326
- )
327
- const rootSizeFiltered$ = of().pipe(
328
- mergeWith(
329
- rootSize$.pipe(
330
- debounceTime(250)
331
- ),
332
- rootSize$.pipe(
333
- throttleTime(250)
334
- )
335
- ),
336
- distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
337
- share()
338
- )
339
- const rootSizeSubscription = rootSizeFiltered$.subscribe()
340
-
341
- // layout
342
- const layout$: Observable<Layout> = combineLatest({
343
- rootSize: rootSizeFiltered$,
344
- mergedPadding: mergedPadding$
345
- }).pipe(
346
- takeUntil(destroy$),
347
- switchMap(async (d) => {
348
- const rootWidth = d.rootSize.width > 0
349
- ? d.rootSize.width
350
- : DEFAULT_CHART_WIDTH
351
- const rootHeight = d.rootSize.height > 0
352
- ? d.rootSize.height
353
- : DEFAULT_CHART_HEIGHT
354
- return {
355
- width: rootWidth - d.mergedPadding.left - d.mergedPadding.right,
356
- height: rootHeight - d.mergedPadding.top - d.mergedPadding.bottom,
357
- top: d.mergedPadding.top,
358
- right: d.mergedPadding.right,
359
- bottom: d.mergedPadding.bottom,
360
- left: d.mergedPadding.left,
361
- rootWidth,
362
- rootHeight
363
- }
364
- }),
365
- shareReplay(1)
366
- )
367
- layout$.subscribe(d => {
368
- svgSelection
369
- .attr('width', d.rootWidth)
370
- .attr('height', d.rootHeight)
371
- })
372
-
373
- // -- computedData --
374
- const computedData$: Observable<ComputedDataTypeMap<T>> = combineLatest({
375
- data: sharedData$,
376
- dataFormatter: shareAndMergedDataFormatter$,
377
- chartParams: shareAndMergedChartParams$,
378
- // layout: iif(() => isAxesTypeMap[chartType] === true, layout$, of(undefined))
379
- }).pipe(
380
- takeUntil(destroy$),
381
- switchMap(async d => d),
382
- switchMap((d) => {
383
- return of(d)
384
- .pipe(
385
- map(_d => {
386
- try {
387
- // 檢查 data$ 資料格式是否正確
388
- const { status, columnName, expectToBe } = dataValidator(_d.data)
389
- if (status === 'error') {
390
- throw new Error(createValidatorErrorMessage({
391
- columnName,
392
- expectToBe,
393
- from: 'Chart.data$'
394
- }))
395
- } else if (status === 'warning') {
396
- console.warn(createValidatorWarningMessage({
397
- columnName,
398
- expectToBe,
399
- from: 'Chart.data$'
400
- }))
401
- }
402
- } catch (e) {
403
- // 不中斷資料流
404
- console.error(createOrbChartsErrorMessage(e))
405
- }
406
- try {
407
- return computedDataFn({ data: _d.data, dataFormatter: _d.dataFormatter, chartParams: _d.chartParams })
408
- } catch (e) {
409
- throw new Error(createUnexpectedErrorMessage({
410
- from: 'Chart.data$',
411
- systemMessage: e
412
- }))
413
- }
414
- }),
415
- catchError((e) => {
416
- console.error(createOrbChartsErrorMessage(e))
417
- return EMPTY
418
- })
419
- )
420
- }),
421
- shareReplay(1)
422
- )
423
-
424
- // subscribe - computedData組合了所有的chart參數,所以訂閱computedData可以一次訂閱所有的資料流
425
- computedData$.subscribe()
426
-
427
- // -- plugins --
428
- const pluginEntityMap: any = {} // 用於destroy
429
- chartSubject.plugins$.subscribe(plugins => {
430
- try {
431
- // 檢查 plugins$ 資料格式是否正確
432
- const { status, columnName, expectToBe } = pluginsValidator(chartType, plugins)
433
- if (status === 'error') {
434
- throw new Error(createValidatorErrorMessage({
435
- columnName,
436
- expectToBe,
437
- from: 'Chart.plugins$'
438
- }))
439
- } else if (status === 'warning') {
440
- console.warn(createValidatorWarningMessage({
441
- columnName,
442
- expectToBe,
443
- from: 'Chart.plugins$'
444
- }))
445
- }
446
- } catch (e) {
447
- // plugin驗證失敗就不執行
448
- console.error(createOrbChartsErrorMessage(e))
449
- return
450
- }
451
-
452
- selectionPlugins
453
- .selectAll<SVGGElement, PluginEntity<T, any, any>>('g.orbcharts__plugin')
454
- .data(plugins, d => d.name as string)
455
- .join(
456
- enter => {
457
- return enter
458
- .append('g')
459
- .attr('class', plugin => {
460
- return `orbcharts__plugin orbcharts__${plugin.name}`
461
- })
462
- .each((plugin, i, n) => {
463
- const _pluginObserverBase = {
464
- fullParams$: new Observable(),
465
- fullChartParams$: shareAndMergedChartParams$,
466
- fullDataFormatter$: shareAndMergedDataFormatter$,
467
- computedData$,
468
- layout$
469
- }
470
- const pluginObserver: ContextObserverTypeMap<T, typeof plugin.defaultParams> = contextObserverCallback({
471
- observer: _pluginObserverBase,
472
- subject: chartSubject
473
- })
474
-
475
- // -- createPlugin(plugin) --
476
- const pluginSelection = d3.select(n[i])
477
- const pluginContext: PluginContext<T, typeof plugin.name, typeof plugin.defaultParams> = {
478
- selection: pluginSelection,
479
- rootSelection: svgSelection,
480
- name: plugin.name,
481
- chartType,
482
- subject: chartSubject,
483
- observer: pluginObserver
484
- }
485
-
486
- plugin.setPresetParams(mergedPresetWithDefault.pluginParams[plugin.name] ?? {})
487
- // 傳入context
488
- plugin.setContext(pluginContext)
489
-
490
- // 紀錄起來
491
- pluginEntityMap[pluginContext.name as string] = plugin
492
-
493
- // init plugin
494
- plugin.init()
495
-
496
- })
497
- },
498
- update => update,
499
- exit => {
500
- return exit
501
- .each((plugin: PluginEntity<T, unknown, unknown>, i, n) => {
502
- if (pluginEntityMap[plugin.name as string]) {
503
- pluginEntityMap[plugin.name as string].destroy()
504
- pluginEntityMap[plugin.name as string] = undefined
505
- }
506
- })
507
- .remove()
508
- }
509
- )
510
- .sort((a, b) => a.layerIndex - b.layerIndex)
511
-
512
- })
513
-
514
- return {
515
- ...chartSubject,
516
- selection: svgSelection,
517
- destroy () {
518
- d3.select(element).selectAll('svg').remove()
519
- destroy$.next(undefined)
520
- rootSizeSubscription.unsubscribe()
521
- }
522
- }
523
- }
524
- }
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
+ // 不中斷資料流
221
+ console.error(createOrbChartsErrorMessage(e))
222
+ }
223
+ try {
224
+ return mergeDataFormatter(dataFormatter, mergedPresetWithDefault.dataFormatter, chartType)
225
+ } catch (e) {
226
+ throw new Error(createUnexpectedErrorMessage({
227
+ from: 'Chart.dataFormatter$',
228
+ systemMessage: e
229
+ }))
230
+ }
231
+ }),
232
+ catchError((e) => {
233
+ console.error(createOrbChartsErrorMessage(e))
234
+ return EMPTY
235
+ }),
236
+ shareReplay(1)
237
+ )
238
+ const shareAndMergedChartParams$ = chartSubject.chartParams$
239
+ .pipe(
240
+ takeUntil(destroy$),
241
+ startWith({}),
242
+ map((d) => {
243
+ try {
244
+ // 檢查 chartParams$ 資料格式是否正確
245
+ const { status, columnName, expectToBe } = chartParamsValidator(chartType, d)
246
+ if (status === 'error') {
247
+ throw new Error(createValidatorErrorMessage({
248
+ columnName,
249
+ expectToBe,
250
+ from: 'Chart.chartParams$'
251
+ }))
252
+ } else if (status === 'warning') {
253
+ console.warn(createValidatorWarningMessage({
254
+ columnName,
255
+ expectToBe,
256
+ from: 'Chart.chartParams$'
257
+ }))
258
+ }
259
+ } catch (e) {
260
+ // 不中斷資料流
261
+ console.error(createOrbChartsErrorMessage(e))
262
+ }
263
+ try {
264
+ return mergeOptionsWithDefault(d, mergedPresetWithDefault.chartParams)
265
+ } catch (e) {
266
+ throw new Error(createUnexpectedErrorMessage({
267
+ from: 'Chart.chartParams$',
268
+ systemMessage: e
269
+ }))
270
+ }
271
+ }),
272
+ catchError((e) => {
273
+ console.error(createOrbChartsErrorMessage(e))
274
+ return EMPTY
275
+ }),
276
+ shareReplay(1)
277
+ )
278
+
279
+ // -- size --
280
+ // padding
281
+ const mergedPadding$ = shareAndMergedChartParams$
282
+ .pipe(
283
+ takeUntil(destroy$),
284
+ startWith({}),
285
+ map((d: any) => {
286
+ return mergeOptionsWithDefault(d.padding ?? {}, DEFAULT_PADDING)
287
+ })
288
+ )
289
+ mergedPadding$
290
+ .pipe(
291
+ takeUntil(destroy$),
292
+ first()
293
+ )
294
+ .subscribe(d => {
295
+ selectionLayout
296
+ .attr('transform', `translate(${d.left}, ${d.top})`)
297
+ })
298
+ mergedPadding$.subscribe(size => {
299
+ selectionLayout
300
+ .transition()
301
+ .attr('transform', `translate(${size.left}, ${size.top})`)
302
+ })
303
+
304
+ // 監聽外層的element尺寸
305
+ const rootSize$: Observable<{ width: number; height: number }> = of({
306
+ width: options?.width ?? DEFAULT_CHART_OPTIONS.width,
307
+ height: options?.height ?? DEFAULT_CHART_OPTIONS.height
308
+ }).pipe(
309
+ switchMap(size => {
310
+ return iif(
311
+ () => size.width === 'auto' || size.height === 'auto',
312
+ // 有 'auto' 的話就監聽element的尺寸
313
+ resizeObservable(element).pipe(
314
+ map((d) => {
315
+ return {
316
+ width: size.width === 'auto' ? d.width : size.width,
317
+ height: size.height === 'auto' ? d.height : size.height
318
+ }
319
+ })
320
+ ),
321
+ of(size as { width: number; height: number })
322
+ )
323
+ }),
324
+ takeUntil(destroy$),
325
+ share()
326
+ )
327
+ const rootSizeFiltered$ = of().pipe(
328
+ mergeWith(
329
+ rootSize$.pipe(
330
+ debounceTime(250)
331
+ ),
332
+ rootSize$.pipe(
333
+ throttleTime(250)
334
+ )
335
+ ),
336
+ distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
337
+ share()
338
+ )
339
+ const rootSizeSubscription = rootSizeFiltered$.subscribe()
340
+
341
+ // layout
342
+ const layout$: Observable<Layout> = combineLatest({
343
+ rootSize: rootSizeFiltered$,
344
+ mergedPadding: mergedPadding$
345
+ }).pipe(
346
+ takeUntil(destroy$),
347
+ switchMap(async (d) => {
348
+ const rootWidth = d.rootSize.width > 0
349
+ ? d.rootSize.width
350
+ : DEFAULT_CHART_WIDTH
351
+ const rootHeight = d.rootSize.height > 0
352
+ ? d.rootSize.height
353
+ : DEFAULT_CHART_HEIGHT
354
+ return {
355
+ width: rootWidth - d.mergedPadding.left - d.mergedPadding.right,
356
+ height: rootHeight - d.mergedPadding.top - d.mergedPadding.bottom,
357
+ top: d.mergedPadding.top,
358
+ right: d.mergedPadding.right,
359
+ bottom: d.mergedPadding.bottom,
360
+ left: d.mergedPadding.left,
361
+ rootWidth,
362
+ rootHeight
363
+ }
364
+ }),
365
+ shareReplay(1)
366
+ )
367
+ layout$.subscribe(d => {
368
+ svgSelection
369
+ .attr('width', d.rootWidth)
370
+ .attr('height', d.rootHeight)
371
+ })
372
+
373
+ // -- computedData --
374
+ const computedData$: Observable<ComputedDataTypeMap<T>> = combineLatest({
375
+ data: sharedData$,
376
+ dataFormatter: shareAndMergedDataFormatter$,
377
+ chartParams: shareAndMergedChartParams$,
378
+ // layout: iif(() => isAxesTypeMap[chartType] === true, layout$, of(undefined))
379
+ }).pipe(
380
+ takeUntil(destroy$),
381
+ switchMap(async d => d),
382
+ switchMap((d) => {
383
+ return of(d)
384
+ .pipe(
385
+ map(_d => {
386
+ try {
387
+ // 檢查 data$ 資料格式是否正確
388
+ const { status, columnName, expectToBe } = dataValidator(_d.data)
389
+ if (status === 'error') {
390
+ throw new Error(createValidatorErrorMessage({
391
+ columnName,
392
+ expectToBe,
393
+ from: 'Chart.data$'
394
+ }))
395
+ } else if (status === 'warning') {
396
+ console.warn(createValidatorWarningMessage({
397
+ columnName,
398
+ expectToBe,
399
+ from: 'Chart.data$'
400
+ }))
401
+ }
402
+ } catch (e) {
403
+ // 不中斷資料流
404
+ console.error(createOrbChartsErrorMessage(e))
405
+ }
406
+ try {
407
+ return computedDataFn({ data: _d.data, dataFormatter: _d.dataFormatter, chartParams: _d.chartParams })
408
+ } catch (e) {
409
+ throw new Error(createUnexpectedErrorMessage({
410
+ from: 'Chart.data$',
411
+ systemMessage: e
412
+ }))
413
+ }
414
+ }),
415
+ catchError((e) => {
416
+ console.error(createOrbChartsErrorMessage(e))
417
+ return EMPTY
418
+ })
419
+ )
420
+ }),
421
+ shareReplay(1)
422
+ )
423
+
424
+ // subscribe - computedData組合了所有的chart參數,所以訂閱computedData可以一次訂閱所有的資料流
425
+ computedData$.subscribe()
426
+
427
+ // -- plugins --
428
+ const pluginEntityMap: any = {} // 用於destroy
429
+ chartSubject.plugins$.subscribe(plugins => {
430
+ try {
431
+ // 檢查 plugins$ 資料格式是否正確
432
+ const { status, columnName, expectToBe } = pluginsValidator(chartType, plugins)
433
+ if (status === 'error') {
434
+ throw new Error(createValidatorErrorMessage({
435
+ columnName,
436
+ expectToBe,
437
+ from: 'Chart.plugins$'
438
+ }))
439
+ } else if (status === 'warning') {
440
+ console.warn(createValidatorWarningMessage({
441
+ columnName,
442
+ expectToBe,
443
+ from: 'Chart.plugins$'
444
+ }))
445
+ }
446
+ } catch (e) {
447
+ // plugin驗證失敗就不執行
448
+ console.error(createOrbChartsErrorMessage(e))
449
+ return
450
+ }
451
+
452
+ selectionPlugins
453
+ .selectAll<SVGGElement, PluginEntity<T, any, any>>('g.orbcharts__plugin')
454
+ .data(plugins, d => d.name as string)
455
+ .join(
456
+ enter => {
457
+ return enter
458
+ .append('g')
459
+ .attr('class', plugin => {
460
+ return `orbcharts__plugin orbcharts__${plugin.name}`
461
+ })
462
+ .each((plugin, i, n) => {
463
+ const _pluginObserverBase = {
464
+ fullParams$: new Observable(),
465
+ fullChartParams$: shareAndMergedChartParams$,
466
+ fullDataFormatter$: shareAndMergedDataFormatter$,
467
+ computedData$,
468
+ layout$
469
+ }
470
+ const pluginObserver: ContextObserverTypeMap<T, typeof plugin.defaultParams> = contextObserverCallback({
471
+ observer: _pluginObserverBase,
472
+ subject: chartSubject
473
+ })
474
+
475
+ // -- createPlugin(plugin) --
476
+ const pluginSelection = d3.select(n[i])
477
+ const pluginContext: PluginContext<T, typeof plugin.name, typeof plugin.defaultParams> = {
478
+ selection: pluginSelection,
479
+ rootSelection: svgSelection,
480
+ name: plugin.name,
481
+ chartType,
482
+ subject: chartSubject,
483
+ observer: pluginObserver
484
+ }
485
+
486
+ plugin.setPresetParams(mergedPresetWithDefault.pluginParams[plugin.name] ?? {})
487
+ // 傳入context
488
+ plugin.setContext(pluginContext)
489
+
490
+ // 紀錄起來
491
+ pluginEntityMap[pluginContext.name as string] = plugin
492
+
493
+ // init plugin
494
+ plugin.init()
495
+
496
+ })
497
+ },
498
+ update => update,
499
+ exit => {
500
+ return exit
501
+ .each((plugin: PluginEntity<T, unknown, unknown>, i, n) => {
502
+ if (pluginEntityMap[plugin.name as string]) {
503
+ pluginEntityMap[plugin.name as string].destroy()
504
+ pluginEntityMap[plugin.name as string] = undefined
505
+ }
506
+ })
507
+ .remove()
508
+ }
509
+ )
510
+ .sort((a, b) => a.layerIndex - b.layerIndex)
511
+
512
+ })
513
+
514
+ return {
515
+ ...chartSubject,
516
+ selection: svgSelection,
517
+ destroy () {
518
+ d3.select(element).selectAll('svg').remove()
519
+ destroy$.next(undefined)
520
+ rootSizeSubscription.unsubscribe()
521
+ }
522
+ }
523
+ }
524
+ }