@operato/scene-scichart 7.3.13 → 7.3.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +0,0 @@
1
- import { AxisBase2D, EventHandler, NumberRange, VisibleRangeChangedArgs } from 'scichart'
2
-
3
- export class AxisSynchroniser {
4
- public visibleRange: NumberRange
5
- private axes: AxisBase2D[] = []
6
- public visibleRangeChanged: EventHandler<VisibleRangeChangedArgs> = new EventHandler<VisibleRangeChangedArgs>()
7
-
8
- public constructor(initialRange: NumberRange, axes?: AxisBase2D[]) {
9
- this.visibleRange = initialRange
10
- this.publishChange = this.publishChange.bind(this)
11
- if (axes) {
12
- axes.forEach(a => this.addAxis(a))
13
- }
14
- }
15
-
16
- public publishChange(data?: VisibleRangeChangedArgs) {
17
- this.visibleRange = data!.visibleRange
18
- this.axes.forEach(a => (a.visibleRange = this.visibleRange))
19
- this.visibleRangeChanged.raiseEvent(data)
20
- }
21
-
22
- public addAxis(axis: AxisBase2D) {
23
- if (!this.axes.includes(axis)) {
24
- this.axes.push(axis)
25
- axis.visibleRange = this.visibleRange
26
- axis.visibleRangeChanged.subscribe(this.publishChange)
27
- }
28
- }
29
-
30
- public removeAxis(axis: AxisBase2D) {
31
- const index = this.axes.findIndex(a => a === axis)
32
- if (index >= 0) {
33
- this.axes.splice(index, 1)
34
- axis.visibleRangeChanged.unsubscribe(this.publishChange)
35
- }
36
- }
37
- }
@@ -1,579 +0,0 @@
1
- import { LitElement, html, css } from 'lit'
2
- import { property, query, customElement } from 'lit/decorators.js'
3
- import { keyed } from 'lit/directives/keyed.js'
4
-
5
- import {
6
- calculatePrecision,
7
- buildSciChart,
8
- buildSciChartOverview,
9
- convertColor,
10
- DEFAULT_COLOR
11
- } from './scichart-builder'
12
- import { AxisSynchroniser } from './axis-synchronizer'
13
- import {
14
- EAutoRange,
15
- FastLineRenderableSeries,
16
- NumberRange,
17
- NumericAxis,
18
- scaleAxes,
19
- SciChartVerticalGroup,
20
- XyDataSeries
21
- } from 'scichart'
22
-
23
- import { ScrollbarStyles } from '@operato/styles'
24
-
25
- @customElement('ox-scichart-multiple')
26
- export class OxSciChartMultiple extends LitElement {
27
- @property({ type: Object }) config: OperatoChart.ChartConfig | null = null
28
- @property({ type: Array }) data: { [attr: string]: any }[] = []
29
- @property({ type: Array }) visibleSeries: string[] = []
30
- @property({ type: Boolean, attribute: 'show-overview' }) showOverview: boolean = true
31
-
32
- private synchronizer: AxisSynchroniser = new AxisSynchroniser(new NumberRange(200, 500))
33
- private verticalGroup: SciChartVerticalGroup = new SciChartVerticalGroup()
34
-
35
- private overviewChart: any = null
36
- private overviewDataSeries: any[] = []
37
- private groupCharts: {
38
- dataKey: string
39
- sciChartSurface: any
40
- dataSeries: any[]
41
- }[] = []
42
-
43
- private initializationQueue: Promise<void> = Promise.resolve()
44
-
45
- /*
46
- [주의]
47
- ox-scichart container의 id를 글로벌 유니크하게 해야한다.
48
- SciChart가 특별히 container의 id를 기반으로 하위 컴포넌트를 구성하고 있기 때문이다.
49
- shadowDom 안에 있는 container 이더라도, 글로벌 유니크한 id를 제공해야 한다.
50
- 그렇지 않으면, 단 하나의 차트만 제대로 렌더링된다.
51
- */
52
- private containerId: string = 'ox-scichart-multiple' + ++OxSciChartMultiple.idx
53
-
54
- @query('.overview') overviewContainer!: HTMLDivElement
55
-
56
- static idx: number = 0
57
-
58
- static styles = [
59
- ScrollbarStyles,
60
- css`
61
- :host {
62
- display: flex;
63
- flex-direction: column;
64
-
65
- width: 100%;
66
- height: 100%;
67
- }
68
-
69
- .overview {
70
- height: 80px;
71
- }
72
-
73
- #chart-group {
74
- flex: 1;
75
-
76
- display: flex;
77
- flex-direction: column;
78
- overflow-y: auto;
79
- }
80
-
81
- .grouped-chart {
82
- flex: 1;
83
-
84
- min-height: 25%;
85
- }
86
-
87
- [hidden] {
88
- display: none;
89
- }
90
- `
91
- ]
92
-
93
- async initializeSciChart() {
94
- this.cleanup()
95
- await this.createOverviewChart()
96
- }
97
-
98
- dispose() {
99
- // 그룹 차트 자원 해제
100
- this.groupCharts.forEach(chart => {
101
- if (chart.sciChartSurface) {
102
- this.synchronizer.removeAxis(chart.sciChartSurface.xAxes.get(0))
103
- this.verticalGroup.removeSurface(chart.sciChartSurface)
104
-
105
- // 각 데이터 시리즈 명시적으로 해제
106
- chart.dataSeries.forEach(ds => ds.delete())
107
-
108
- // 차트 삭제
109
- chart.sciChartSurface.delete()
110
- }
111
- })
112
-
113
- // 오버뷰 차트 자원 해제
114
- if (this.overviewChart) {
115
- this.overviewChart.sciChartSurface.renderableSeries.clear()
116
- this.overviewDataSeries.forEach(ds => ds.delete())
117
- this.overviewChart.sciChartSurface.delete()
118
- this.overviewDataSeries.length = 0
119
- }
120
-
121
- // 사용된 그룹, 오버뷰 데이터 초기화
122
- this.groupCharts.length = 0
123
- this.overviewChart = null
124
- }
125
-
126
- async createOverviewChart() {
127
- const { chart, dataSeries } =
128
- (await buildSciChartOverview(
129
- {
130
- ...this.config,
131
- data: { datasets: [] }
132
- },
133
- this.overviewContainer,
134
- {},
135
- this.synchronizer
136
- )) || {}
137
-
138
- this.verticalGroup.addSurfaceToGroup(chart.sciChartSurface)
139
-
140
- this.overviewChart = chart
141
- this.overviewDataSeries = dataSeries!
142
- }
143
-
144
- async updated(changedProperties: Map<string | number | symbol, unknown>) {
145
- this.initializationQueue = this.initializationQueue
146
- .then(async () => {
147
- let needUpdateDataSeries = false
148
- let needBuildChartGroup = false
149
-
150
- if (changedProperties.has('config') && this.config) {
151
- await this.initializeSciChart()
152
- needBuildChartGroup = true
153
- needUpdateDataSeries = true
154
- }
155
-
156
- if (changedProperties.has('visibleSeries')) {
157
- await this.updateSeries(this.visibleSeries, changedProperties.get('visibleSeries') as string[])
158
- /* [중요] buildChartGroup 을 visibleSeries 수정때마다 하는 이유는, render() 의 캐시 컨트롤이 안되기 때문이다. 가급적 시도하지 말라. */
159
- needBuildChartGroup = true
160
- needUpdateDataSeries = true
161
- }
162
-
163
- if (changedProperties.has('data')) {
164
- needUpdateDataSeries = true
165
- }
166
-
167
- if (needBuildChartGroup) {
168
- await this.buildChartGroup()
169
- }
170
-
171
- if (needUpdateDataSeries) {
172
- await this.updateDataSeries()
173
- }
174
- })
175
- .catch((error: any) => {
176
- console.error('Error in updated queue:', error)
177
- })
178
- }
179
-
180
- cleanup() {
181
- this.cleanupGroup()
182
- this.cleanupOverview()
183
- }
184
-
185
- cleanupGroup() {
186
- this.groupCharts.forEach(chart => {
187
- if (chart.sciChartSurface) {
188
- this.synchronizer.removeAxis(chart.sciChartSurface.xAxes.get(0))
189
- this.verticalGroup.removeSurface(chart.sciChartSurface)
190
- chart.sciChartSurface.delete()
191
- }
192
- })
193
-
194
- this.groupCharts.length = 0
195
- }
196
-
197
- cleanupOverview() {
198
- if (this.overviewChart) {
199
- this.overviewChart.sciChartSurface.renderableSeries.clear()
200
- this.overviewDataSeries.forEach(ds => ds.delete())
201
- this.overviewDataSeries.length = 0
202
- }
203
-
204
- this.overviewChart = null
205
- }
206
-
207
- async updateDataSeries() {
208
- const { config, data } = this
209
- const { datasets = [], labelDataKey: attrX } = config?.data || {}
210
-
211
- if (!(data instanceof Array) || !attrX) {
212
- return []
213
- }
214
-
215
- const newData = this.dataSet
216
-
217
- this.groupCharts.forEach(({ dataKey, sciChartSurface, dataSeries }) => {
218
- try {
219
- // dataKey로 시작하는 모든 시리즈를 업데이트
220
- const relatedDatasets = datasets.filter(dataset => dataset.dataKey?.startsWith(dataKey))
221
-
222
- // 각 relatedDatasets에 대해 해당하는 dataSeries를 업데이트
223
- dataSeries.forEach((ds, index) => {
224
- ds.clear()
225
-
226
- const relatedDataset = relatedDatasets[index]
227
- if (relatedDataset) {
228
- const filteredData = newData[datasets.findIndex(ds => ds.dataKey === relatedDataset.dataKey)].filter(
229
- d => typeof d.yValue === 'number'
230
- )
231
-
232
- if (filteredData.length > 0) {
233
- ds.appendRange(
234
- filteredData.map(d => d.xValue),
235
- filteredData.map(d => d.yValue)
236
- )
237
- }
238
- }
239
- })
240
-
241
- sciChartSurface.zoomExtents()
242
- sciChartSurface.invalidateElement()
243
- } catch (error) {
244
- console.error('Error updating data series:', error)
245
- }
246
- })
247
-
248
- try {
249
- // Overview 차트 데이터 업데이트
250
- this.overviewDataSeries.forEach((ds, index) => {
251
- const visibleKey = this.visibleSeries[index]
252
- const dataset = datasets.find(dataset => dataset.dataKey === visibleKey)
253
- if (!dataset) {
254
- return
255
- }
256
-
257
- const dataIndex = datasets.findIndex(ds => ds.dataKey === dataset.dataKey)
258
- const filteredData = newData[dataIndex]?.filter(d => typeof d.yValue === 'number') || []
259
-
260
- ds.clear()
261
- if (filteredData.length > 0) {
262
- ds.appendRange(
263
- filteredData.map(d => d.xValue),
264
- filteredData.map(d => d.yValue)
265
- )
266
- }
267
- })
268
-
269
- // this.overviewDataSeries.forEach(ds => ds.clear())
270
-
271
- // newData.forEach((data, index) => {
272
- // if (this.visibleSeries.includes(datasets[index].dataKey!)) {
273
- // const filteredData = data.filter(d => typeof d.yValue === 'number')
274
- // if (filteredData.length > 0) {
275
- // this.overviewDataSeries[index].appendRange(
276
- // filteredData.map(d => d.xValue),
277
- // filteredData.map(d => d.yValue)
278
- // )
279
- // }
280
- // }
281
- // })
282
- } catch (error) {
283
- console.error('Error updating overview data series:', error)
284
- }
285
- }
286
-
287
- get dataSet(): { xValue: number; yValue: number }[][] {
288
- const { config, data } = this
289
- const { datasets = [], labelDataKey: attrX } = config?.data || {}
290
-
291
- if (!(data instanceof Array) || !attrX) {
292
- return []
293
- }
294
-
295
- return datasets.map(dataset => {
296
- return data
297
- .map(item => {
298
- if (!item || typeof item !== 'object') {
299
- return
300
- }
301
-
302
- const xValue = new Date(item[attrX])
303
- if (isNaN(xValue.getTime())) {
304
- console.error('Invalid date:', item[attrX])
305
- return
306
- }
307
-
308
- return {
309
- xValue: xValue.getTime() / 1000,
310
- yValue: item[dataset.dataKey!]
311
- }
312
- })
313
- .filter(Boolean) as { xValue: number; yValue: number }[]
314
- })
315
- }
316
-
317
- render() {
318
- const { datasets = [] } = this.config?.data || {}
319
-
320
- return html`
321
- <div id=${this.containerId + '-overview'} class="overview" ?hidden=${!this.showOverview}></div>
322
- <div id="chart-group">
323
- ${this.visibleSeries.map(dataKey =>
324
- keyed(
325
- dataKey,
326
- html`
327
- <div
328
- id=${this.containerId + '-' + dataKey}
329
- class="grouped-chart"
330
- ?hidden=${!this.visibleSeries.includes(dataKey!)}
331
- ></div>
332
- `
333
- )
334
- )}
335
- </div>
336
- `
337
- }
338
-
339
- async buildChartGroup() {
340
- this.cleanupGroup()
341
-
342
- const { config } = this
343
- const { datasets = [] } = config?.data || {}
344
-
345
- await Promise.all(
346
- datasets
347
- .filter(dataset => this.visibleSeries.includes(dataset.dataKey!))
348
- .map(async dataset => {
349
- await this.addChart(dataset.dataKey!)
350
- })
351
- )
352
- }
353
-
354
- async updateSeries(after: string[], before: string[]) {
355
- const addSeries = after?.filter(series => !before?.includes(series)) || []
356
- const removeSeries = before?.filter(series => !after?.includes(series)) || []
357
-
358
- for (const series of removeSeries) {
359
- await this.removeChart(series)
360
- }
361
-
362
- for (const series of addSeries) {
363
- await this.addChart(series)
364
- }
365
- }
366
-
367
- async addChart(dataKey: string) {
368
- const chartData = await this.createChart(dataKey)
369
-
370
- if (chartData) {
371
- const { chart, dataSeries, dataKey } = chartData
372
- this.verticalGroup.addSurfaceToGroup(chart.sciChartSurface)
373
-
374
- this.groupCharts = this.groupSorter([
375
- ...this.groupCharts,
376
- { dataKey, sciChartSurface: chart.sciChartSurface, dataSeries }
377
- ])
378
-
379
- await this.addSeriesToOverviewChart(dataKey)
380
- }
381
- }
382
-
383
- async removeChart(dataKey: string) {
384
- const index = this.groupCharts.findIndex(chart => chart.dataKey == dataKey)
385
- if (index === -1) return
386
-
387
- const [groupedChart] = this.groupCharts.splice(index, 1)
388
- this.destroyChart(groupedChart)
389
-
390
- this.groupCharts = this.groupSorter(this.groupCharts)
391
-
392
- await this.removeSeriesFromOverviewChart(dataKey)
393
- }
394
-
395
- groupSorter(group: any[]) {
396
- return group.sort(
397
- (a, b) =>
398
- this.visibleSeries.findIndex((s: any) => s.dataKey == a.dataKey) -
399
- this.visibleSeries.findIndex((s: any) => s.dataKey == b.dataKey)
400
- )
401
- }
402
-
403
- async appendData(appendum: { [attr: string]: any }[]) {}
404
-
405
- private async createChart(dataKey: string) {
406
- const { data = {}, options = {} } = this.config || {}
407
- const { datasets = [] } = data as OperatoChart.ChartData
408
- const primaryDataset = datasets.find(dataset => dataset.dataKey == dataKey)
409
-
410
- if (!primaryDataset) {
411
- return null
412
- }
413
-
414
- const relatedDatasets = datasets.filter(dataset => dataset.dataKey?.startsWith(dataKey))
415
-
416
- const yAxis = {
417
- ...options.scales?.yAxes?.[0],
418
- axisTitle: primaryDataset?.label
419
- }
420
-
421
- const config = {
422
- ...this.config,
423
- data: {
424
- datasets: relatedDatasets
425
- },
426
- options: {
427
- ...options,
428
- scales: {
429
- ...options.scales,
430
- yAxes: [yAxis]
431
- }
432
- }
433
- }
434
-
435
- const container = this.renderRoot.querySelector(`#${this.containerId + '-' + dataKey}`)
436
- const { chart, dataSeries } = (await buildSciChart(
437
- config,
438
- container,
439
- { fontSize: 14, fontFamily: 'Roboto', fontColor: undefined },
440
- {
441
- precision: primaryDataset.valueFormat ? calculatePrecision(primaryDataset.valueFormat) : undefined,
442
- grouped: this.containerId
443
- }
444
- ))!
445
-
446
- this.synchronizer.addAxis(chart.sciChartSurface.xAxes.get(0))
447
-
448
- // 각 시리즈에 대해 올바른 데이터를 추가
449
- const newData = this.dataSet
450
- dataSeries.forEach((ds, seriesIndex) => {
451
- const dataset = relatedDatasets[seriesIndex]
452
- const filteredData = newData[seriesIndex]?.filter(d => typeof d.yValue === 'number') || []
453
- if (filteredData.length > 0) {
454
- ds.appendRange(
455
- filteredData.map(d => d.xValue),
456
- filteredData.map(d => d.yValue)
457
- )
458
- }
459
- })
460
-
461
- return { chart, dataSeries, dataKey }
462
- }
463
-
464
- private destroyChart(groupedChart: { dataKey: string; sciChartSurface: any; dataSeries: any[] }) {
465
- this.verticalGroup.removeSurface(groupedChart.sciChartSurface)
466
- this.synchronizer.removeAxis(groupedChart.sciChartSurface.xAxes.get(0))
467
- groupedChart.sciChartSurface.delete()
468
- }
469
-
470
- private async addSeriesToOverviewChart(dataKey: string) {
471
- if (!this.overviewChart || !this.overviewDataSeries) {
472
- console.error('Overview chart is not initialized.')
473
- return
474
- }
475
-
476
- // Check if the series already exists in the overview chart
477
- const existingSeries = this.overviewChart.sciChartSurface.renderableSeries.asArray().find((series: any) => {
478
- return series.dataSeries.dataSeriesName === dataKey
479
- })
480
-
481
- if (existingSeries) {
482
- console.warn(`Series for dataKey ${dataKey} already exists in the overview chart.`)
483
- return // Exit the function without adding the series again
484
- }
485
-
486
- const dataset = this.config?.data.datasets.find(dataset => dataset.dataKey === dataKey)
487
- if (!dataset) {
488
- console.error('Dataset not found for dataKey:', dataKey)
489
- return
490
- }
491
-
492
- const newSeries = await this.createSeriesForOverview(dataset)
493
-
494
- if (newSeries) {
495
- this.overviewChart.sciChartSurface.renderableSeries.add(newSeries.series)
496
- this.overviewDataSeries.push(newSeries.dataSeries)
497
- }
498
- }
499
-
500
- private async removeSeriesFromOverviewChart(dataKey: string) {
501
- if (!this.overviewChart || !this.overviewDataSeries) {
502
- console.error('Overview chart is not initialized.')
503
- return
504
- }
505
-
506
- const { sciChartSurface } = this.overviewChart
507
-
508
- // 오버뷰 차트의 renderableSeries에서 해당 시리즈 제거
509
- const seriesIndex = sciChartSurface.renderableSeries.asArray().findIndex((series: any) => {
510
- return series.dataSeries.dataSeriesName === dataKey
511
- })
512
-
513
- if (seriesIndex !== -1) {
514
- const series = sciChartSurface.renderableSeries.get(seriesIndex)
515
- const yAxisId = series.yAxisId
516
-
517
- // 시리즈 제거
518
- sciChartSurface.renderableSeries.removeAt(seriesIndex)
519
- this.overviewDataSeries.splice(seriesIndex, 1) // 데이터 시리즈도 제거
520
-
521
- // 고유 Y축 제거
522
- if (yAxisId) {
523
- const yAxisIndex = sciChartSurface.yAxes.asArray().findIndex((axis: any) => axis.id === yAxisId)
524
- if (yAxisIndex !== -1) {
525
- sciChartSurface.yAxes.removeAt(yAxisIndex)
526
- }
527
- }
528
- } else {
529
- console.error('Series not found in overview chart for dataKey:', dataKey)
530
- }
531
- }
532
-
533
- private async createSeriesForOverview(dataset: any) {
534
- if (!this.overviewChart) return null
535
-
536
- const { sciChartSurface, wasmContext } = this.overviewChart
537
- const dataSeries = new XyDataSeries(wasmContext, {
538
- dataSeriesName: dataset.dataKey,
539
- containsNaN: false
540
- })
541
-
542
- // 새로운 Y축을 추가하여 노멀라이즈 효과를 제공
543
- const yAxisId = `yAxis_${dataset.dataKey}`
544
- const yAxis = new NumericAxis(wasmContext, {
545
- id: yAxisId,
546
- autoRange: EAutoRange.Always,
547
- drawLabels: false,
548
- drawMajorTickLines: false,
549
- drawMinorTickLines: false,
550
- drawMajorGridLines: false,
551
- drawMinorGridLines: false
552
- })
553
-
554
- sciChartSurface.yAxes.add(yAxis)
555
-
556
- const series = new FastLineRenderableSeries(wasmContext, {
557
- dataSeries,
558
- strokeThickness: 1,
559
- stroke: convertColor(dataset.color, DEFAULT_COLOR),
560
- yAxisId
561
- })
562
-
563
- // 초기 데이터 추가
564
- const newData = this.dataSet.find((data, index) => this.config?.data.datasets[index].dataKey === dataset.dataKey)
565
- if (newData && newData.length > 0) {
566
- const filteredData = newData.filter(d => typeof d.yValue === 'number')
567
- if (filteredData.length > 0) {
568
- dataSeries.appendRange(
569
- filteredData.map(d => d.xValue),
570
- filteredData.map(d => d.yValue)
571
- )
572
- } else {
573
- console.warn('No valid yValues found for dataset:', dataset.dataKey)
574
- }
575
- }
576
-
577
- return { series, dataSeries }
578
- }
579
- }