@orbcharts/core 3.0.0-beta.7 → 3.0.0-beta.8

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