evui 3.4.151 → 3.4.153

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evui",
3
- "version": "3.4.151",
3
+ "version": "3.4.153",
4
4
  "description": "A EXEM Library project",
5
5
  "author": "exem <dev_client@ex-em.com>",
6
6
  "license": "MIT",
@@ -34,70 +34,71 @@
34
34
  import EvChartToolbar from './ChartToolbar';
35
35
  import { useModel, useWrapper, useZoomModel } from './uses';
36
36
 
37
- export default {
38
- name: 'EvChart',
39
- components: {
40
- EvChartToolbar,
37
+ export default {
38
+ name: 'EvChart',
39
+ components: {
40
+ EvChartToolbar,
41
+ },
42
+ props: {
43
+ selectedItem: {
44
+ type: Object,
45
+ default: null,
41
46
  },
42
- props: {
43
- selectedItem: {
44
- type: Object,
45
- default: null,
46
- },
47
- selectedLabel: {
48
- type: Object,
49
- default: null,
50
- },
51
- selectedSeries: {
52
- type: Object,
53
- default: null,
54
- },
55
- options: {
56
- type: Object,
57
- default: () => ({}),
58
- },
59
- data: {
60
- type: Object,
61
- default: () => ({}),
62
- },
63
- resizeTimeout: {
64
- type: Number,
65
- default: 0,
66
- },
67
- zoomStartIdx: {
68
- type: Number,
69
- default: 0,
70
- },
71
- zoomEndIdx: {
72
- type: Number,
73
- default: 0,
74
- },
75
- realTimeScatterReset: {
76
- type: Boolean,
77
- default: false,
78
- },
47
+ selectedLabel: {
48
+ type: Object,
49
+ default: null,
79
50
  },
80
- emits: [
81
- 'click',
82
- 'dbl-click',
83
- 'drag-select',
84
- 'mouse-move',
85
- 'update:selectedItem',
86
- 'update:selectedLabel',
87
- 'update:selectedSeries',
88
- 'update:zoomStartIdx',
89
- 'update:zoomEndIdx',
90
- 'update:realTimeScatterReset',
91
- ],
92
- setup(props, { emit }) {
93
- let evChart = null;
94
- const isMounted = ref(false);
95
- const injectIsChartGroup = inject('isChartGroup', false);
96
- const injectBrushSeries = inject('brushSeries', { list: [], chartIdx: null });
97
- const injectGroupSelectedLabel = inject('groupSelectedLabel', null);
98
- const injectGroupHoveredLabel = inject('groupHoveredLabel', null);
99
- const injectBrushIdx = inject('brushIdx', { start: 0, end: -1 });
100
- const injectEvChartPropsInGroup = inject('evChartPropsInGroup', []);
51
+ selectedSeries: {
52
+ type: Object,
53
+ default: null,
54
+ },
55
+ options: {
56
+ type: Object,
57
+ default: () => ({}),
58
+ },
59
+ data: {
60
+ type: Object,
61
+ default: () => ({}),
62
+ },
63
+ resizeTimeout: {
64
+ type: Number,
65
+ default: 0,
66
+ },
67
+ zoomStartIdx: {
68
+ type: Number,
69
+ default: 0,
70
+ },
71
+ zoomEndIdx: {
72
+ type: Number,
73
+ default: 0,
74
+ },
75
+ realTimeScatterReset: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+ },
80
+ emits: [
81
+ 'click',
82
+ 'dbl-click',
83
+ 'drag-select',
84
+ 'mouse-move',
85
+ 'update:selectedItem',
86
+ 'update:selectedLabel',
87
+ 'update:selectedSeries',
88
+ 'update:zoomStartIdx',
89
+ 'update:zoomEndIdx',
90
+ 'update:realTimeScatterReset',
91
+ 'click-legend',
92
+ ],
93
+ setup(props, { emit }) {
94
+ let evChart = null;
95
+ const isMounted = ref(false);
96
+ const injectIsChartGroup = inject('isChartGroup', false);
97
+ const injectBrushSeries = inject('brushSeries', { list: [], chartIdx: null });
98
+ const injectGroupSelectedLabel = inject('groupSelectedLabel', null);
99
+ const injectGroupHoveredLabel = inject('groupHoveredLabel', null);
100
+ const injectBrushIdx = inject('brushIdx', { start: 0, end: -1 });
101
+ const injectEvChartPropsInGroup = inject('evChartPropsInGroup', []);
101
102
 
102
103
  const {
103
104
  eventListeners,
@@ -269,9 +270,12 @@
269
270
 
270
271
  emit('update:realTimeScatterReset', false);
271
272
  }
272
- });
273
+ },
274
+ );
273
275
 
274
- watch(() => props.options.realTimeScatter?.use, (use) => {
276
+ watch(
277
+ () => props.options.realTimeScatter?.use,
278
+ (use) => {
275
279
  evChart.options.realTimeScatter.use = use ?? false;
276
280
 
277
281
  evChart.update({
@@ -127,6 +127,10 @@ class EvChart {
127
127
  this.axesX = this.createAxes('x', axesX);
128
128
  this.axesY = this.createAxes('y', axesY);
129
129
 
130
+ if (axesX?.[0]?.scrollbar?.use || axesY?.[0]?.scrollbar?.use) {
131
+ this.initScrollbar();
132
+ }
133
+
130
134
  this.initDefaultSelectInfo();
131
135
 
132
136
  this.drawChart();
@@ -162,10 +166,6 @@ class EvChart {
162
166
  this.setLegendPosition();
163
167
  }
164
168
 
165
- if (opt.axesX?.[0]?.scrollbar?.use || opt.axesY?.[0]?.scrollbar?.use) {
166
- this.initScrollbar();
167
- }
168
-
169
169
  this.chartRect = this.getChartRect();
170
170
  }
171
171
 
@@ -277,7 +277,6 @@ class EvChart {
277
277
  this.drawSeries(hitInfo);
278
278
 
279
279
  if (this.scrollbar?.x?.use || this.scrollbar?.y?.use) {
280
- this.initScrollbar();
281
280
  this.updateScrollbarPosition();
282
281
  }
283
282
 
@@ -852,6 +851,21 @@ class EvChart {
852
851
  return labelOffset;
853
852
  }
854
853
 
854
+ /**
855
+ * Update scrollbar information
856
+ * @param {boolean} updateData is update data
857
+ * @returns {undefined}
858
+ */
859
+ updateScrollbar(updateData) {
860
+ if (this.scrollbar?.x?.isInit || this.options.axesX?.[0]?.scrollbar?.use) {
861
+ this.updateScrollbarInfo('x', updateData);
862
+ }
863
+
864
+ if (this.scrollbar?.y?.isInit || this.options.axesY?.[0]?.scrollbar?.use) {
865
+ this.updateScrollbarInfo('y', updateData);
866
+ }
867
+ }
868
+
855
869
  /**
856
870
  * To re-render chart, reset properties, canvas and then render chart.
857
871
  * @param {object} updateInfo information for each components are needed to update
@@ -871,7 +885,6 @@ class EvChart {
871
885
  updateLegend,
872
886
  updateData,
873
887
  updateTooltip,
874
- updateByScrollbar,
875
888
  lightUpdate,
876
889
  } = updateInfo;
877
890
 
@@ -879,9 +892,7 @@ class EvChart {
879
892
  return;
880
893
  }
881
894
 
882
- if (updateByScrollbar) {
883
- this.updateScrollbar?.(updateData);
884
- }
895
+ this.updateScrollbar(updateData);
885
896
 
886
897
  this.resetProps();
887
898
 
@@ -992,7 +1003,6 @@ class EvChart {
992
1003
 
993
1004
  this.initDefaultSelectInfo();
994
1005
 
995
-
996
1006
  let renderHitInfo = updateInfo?.hitInfo;
997
1007
  if (!renderHitInfo?.legend && this.legendHover?.sId) {
998
1008
  renderHitInfo = { ...(renderHitInfo || {}), legend: this.legendHover };
@@ -1065,20 +1075,16 @@ class EvChart {
1065
1075
  * @returns {undefined}
1066
1076
  */
1067
1077
  resize(promiseRes) {
1068
- // 차트 크기가 변경될 때 저장된 스크롤 픽셀 위치를 초기화하여
1069
- // 새로운 크기에 맞춰 스크롤바 크기/위치를 재계산하도록 함
1070
- if (this.scrollbar?.x) {
1071
- delete this.scrollbar.x.savedPosition;
1072
- }
1073
- if (this.scrollbar?.y) {
1074
- delete this.scrollbar.y.savedPosition;
1075
- }
1076
-
1077
1078
  this.clear();
1078
1079
  this.bufferCtx.restore();
1079
1080
  this.bufferCtx.save();
1080
1081
 
1081
1082
  this.initRect();
1083
+
1084
+ if (this.options.axesX?.[0]?.scrollbar?.use || this.options.axesY?.[0]?.scrollbar?.use) {
1085
+ this.initScrollbar();
1086
+ }
1087
+
1082
1088
  this.initScale();
1083
1089
  this.chartRect = this.getChartRect();
1084
1090
  this.drawChart();
@@ -1018,8 +1018,11 @@ const modules = {
1018
1018
  const isHorizontal = !!this.options.horizontal;
1019
1019
  const mousePos = isHorizontal ? yp : xp;
1020
1020
 
1021
- // 번째 표시 중인 시리즈를 기준으로 라벨 위치 확인
1022
- const referenceSeries = sIds.find(sId => this.seriesList[sId]?.show);
1021
+ // 데이터 있는 시리즈를 기준으로 라벨 위치 확인
1022
+ const referenceSeries = sIds.find((sId) => {
1023
+ const series = this.seriesList[sId];
1024
+ return series?.show && series?.data?.length > 0;
1025
+ });
1023
1026
  if (!referenceSeries || !this.seriesList[referenceSeries]?.data) {
1024
1027
  return -1;
1025
1028
  }
@@ -542,10 +542,28 @@ const modules = {
542
542
  this.brushSeries.chartIdx = chartIdx;
543
543
  }
544
544
 
545
- this.update({
546
- updateSeries: false,
547
- updateSelTip: { update: true, keepDomain: true },
548
- });
545
+ if (this.options.eventBehavior?.legendClick !== 'emitOnly') {
546
+ this.update({
547
+ updateSeries: false,
548
+ updateSelTip: { update: true, keepDomain: true },
549
+ });
550
+ }
551
+
552
+ // click-legend event 발생
553
+ const activeSeries = Object.values(this.seriesList).filter(series => series.show);
554
+ const activeSeriesIds = activeSeries.map(series => series.sId);
555
+ const isActiveAll = activeSeriesIds.length === Object.values(this.seriesList).length;
556
+ const args = {
557
+ e,
558
+ data: {
559
+ seriesIds: isActiveAll ? [] : activeSeriesIds,
560
+ isActiveAll,
561
+ },
562
+ };
563
+
564
+ if (typeof this.listeners['click-legend'] === 'function') {
565
+ this.listeners['click-legend'](args);
566
+ }
549
567
  };
550
568
 
551
569
  /**
@@ -750,10 +768,28 @@ const modules = {
750
768
  }
751
769
  }
752
770
 
753
- this.update({
754
- updateSeries: false,
755
- updateSelTip: { update: true, keepDomain: true },
756
- });
771
+ if (this.options.eventBehavior?.legendClick !== 'emitOnly') {
772
+ this.update({
773
+ updateSeries: false,
774
+ updateSelTip: { update: true, keepDomain: true },
775
+ });
776
+ }
777
+
778
+ // click-legend event 발생
779
+ const activeSeries = series.colorState.filter(colorItem => colorItem.show);
780
+ const activeSerieIndices = activeSeries.map(colorItem => +colorItem.id.split('#')[1]);
781
+ const isActiveAll = series.colorState.length === activeSeries.length;
782
+ const args = {
783
+ e,
784
+ data: {
785
+ seriesIndices: isActiveAll ? [] : activeSerieIndices,
786
+ isActiveAll,
787
+ },
788
+ };
789
+
790
+ if (typeof this.listeners['click-legend'] === 'function') {
791
+ this.listeners['click-legend'](args);
792
+ }
757
793
  };
758
794
 
759
795
  /**
@@ -29,6 +29,8 @@ const module = {
29
29
  scrollbarOpt[key] = merged[key];
30
30
  });
31
31
 
32
+ delete scrollbarOpt.savedPosition;
33
+
32
34
  if (!scrollbarOpt.isInit) {
33
35
  scrollbarOpt.type = axisOpt?.[0]?.type;
34
36
  scrollbarOpt.range = axisOpt?.[0]?.range?.length ? [...axisOpt?.[0]?.range] : null;
@@ -83,14 +85,6 @@ const module = {
83
85
  }
84
86
  },
85
87
 
86
- /**
87
- * update scrollbar information
88
- */
89
- updateScrollbar(updateData) {
90
- this.updateScrollbarInfo('x', updateData);
91
- this.updateScrollbarInfo('y', updateData);
92
- },
93
-
94
88
  /**
95
89
  * Updated scrollbar information with updated axis information
96
90
  * @param dir axis direction (x | y)
@@ -449,7 +443,6 @@ const module = {
449
443
  this.update({
450
444
  updateSeries: false,
451
445
  updateSelTip: { update: false, keepDomain: false },
452
- updateByScrollbar: true,
453
446
  lightUpdate: minValue > 1,
454
447
  });
455
448
  }
@@ -11,7 +11,8 @@ class LinearScale extends Scale {
11
11
  * @returns {string} formatted label
12
12
  */
13
13
  getTruthyValue(value) {
14
- return truthyNumber(value) ? Number(value.toFixed(this.decimalPoint)) : value;
14
+ const decimalPoint = this.adjustedDecimalPoint ?? this.decimalPoint;
15
+ return truthyNumber(value) ? Number(value.toFixed(decimalPoint)) : value;
15
16
  }
16
17
 
17
18
  getLabelFormat(value, data = {}) {
@@ -31,8 +32,8 @@ class LinearScale extends Scale {
31
32
  }
32
33
  }
33
34
 
34
-
35
- return Util.labelSignFormat(value, this.decimalPoint);
35
+ const decimalPoint = this.adjustedDecimalPoint ?? this.decimalPoint;
36
+ return Util.labelSignFormat(value, decimalPoint);
36
37
  }
37
38
 
38
39
 
@@ -58,41 +59,171 @@ class LinearScale extends Scale {
58
59
  return Math.ceil((max - min) / step);
59
60
  }
60
61
 
61
- /**
62
- * Get decimal point from range
63
- * @param {object} {
64
- * graphRange: number,
65
- * numberOfSteps: number,
66
- * interval: number,
67
- * }
62
+
63
+ /**
64
+ * Get auto decimal point from interval
65
+ * interval을 표현할 수 있는 최소 decimal 반환
66
+ * 너무 긴 decimal은 제한
67
+ * @param {number} interval
68
68
  * @returns {number} decimal point
69
69
  */
70
- getDecimalPointFromRange({
71
- graphRange,
72
- numberOfSteps,
73
- }) {
74
- if (numberOfSteps <= 0 || graphRange === 0) {
70
+ getAutoDecimalPointFromInterval(interval) {
71
+ if (!isFinite(interval) || interval === 0) {
75
72
  return 0;
76
73
  }
77
74
 
78
- const interval = graphRange / numberOfSteps;
79
- if (interval === 0) {
75
+ const absInterval = Math.abs(interval);
76
+
77
+ // 1 미만 값 처리 (소수점 최대 10자리 제한)
78
+ if (absInterval < 1) {
79
+ let decimals = 0;
80
+ let temp = absInterval;
81
+
82
+ while (temp < 1) {
83
+ temp *= 10;
84
+ decimals++;
85
+
86
+ if (decimals > 10) {
87
+ break;
88
+ }
89
+ }
90
+
91
+ return decimals;
92
+ }
93
+
94
+ // 1 이상 값 처리 (소수점 최대 2자리 제한)
95
+ for (let decimal = 0; decimal <= 6; decimal++) {
96
+ const rounded = Number(absInterval.toFixed(decimal));
97
+
98
+ if (Math.abs(rounded - absInterval) < 1e-10) {
99
+ return Math.min(decimal, 2);
100
+ }
101
+ }
102
+
103
+ return 2;
104
+ }
105
+
106
+ /**
107
+ * axis interval을 nice number로 변환
108
+ * (1, 2, 5 × 10^n)
109
+ *
110
+ * @param {Object} params
111
+ * @param {number} params.range
112
+ * @param {boolean} params.round
113
+ * @returns {number}
114
+ */
115
+ getNiceNumber({ range, round = false }) {
116
+ if (!isFinite(range) || range <= 0) {
80
117
  return 0;
81
118
  }
82
119
 
83
- let decimals = 0;
84
- let temp = interval;
120
+ const exponent = Math.floor(Math.log10(range));
121
+ const fraction = range / (10 ** exponent);
122
+
123
+ let niceFraction;
85
124
 
86
- while (temp < 1) {
87
- temp *= 10;
88
- decimals++;
125
+ if (round) {
126
+ if (fraction < 1.5) {
127
+ niceFraction = 1;
128
+ } else if (fraction < 3) {
129
+ niceFraction = 2;
130
+ } else if (fraction < 7) {
131
+ niceFraction = 5;
132
+ } else {
133
+ niceFraction = 10;
134
+ }
135
+ } else if (fraction <= 1) {
136
+ niceFraction = 1;
137
+ } else if (fraction <= 2) {
138
+ niceFraction = 2;
139
+ } else if (fraction <= 5) {
140
+ niceFraction = 5;
141
+ } else {
142
+ niceFraction = 10;
143
+ }
144
+
145
+ return niceFraction * (10 ** exponent);
146
+ }
89
147
 
90
- if (decimals > 10) {
91
- break;
148
+ /**
149
+ * With range information, calculate how many labels in axis
150
+ * @param {object} range min/max information
151
+ *
152
+ * @returns {object} steps, interval, min/max graph value
153
+ */
154
+ calculateSteps(range) {
155
+ const { minValue, maxValue } = range;
156
+ const maxSteps = Math.max(1, range.maxSteps);
157
+
158
+ const hasUserRange = Array.isArray(this.range) && this.range.length === 2;
159
+ const hasUserInterval = (
160
+ typeof this.interval === 'number'
161
+ || (typeof this.interval === 'object' && this.interval !== null)
162
+ );
163
+
164
+ const resolvedInterval = hasUserInterval ? this.getInterval(range) : null;
165
+ const isValidInterval = (
166
+ resolvedInterval != null
167
+ && resolvedInterval > 0
168
+ && isFinite(resolvedInterval)
169
+ );
170
+
171
+ const graphMin = +minValue;
172
+ let graphMax = +maxValue;
173
+ const graphRange = graphMax - graphMin;
174
+
175
+ let interval;
176
+ let steps;
177
+
178
+ if (hasUserRange && isValidInterval) {
179
+ // 1) user range + interval
180
+ const candidateSteps = graphRange / resolvedInterval;
181
+ const isExactlyDividable = Math.abs(candidateSteps - Math.round(candidateSteps)) < 1e-10;
182
+
183
+ if (isExactlyDividable && candidateSteps <= maxSteps) {
184
+ interval = resolvedInterval;
185
+ steps = Math.round(candidateSteps);
186
+ } else {
187
+ // interval 호환되지 않음 -> 사용자 interval을 사용하지 않음
188
+ steps = maxSteps;
189
+ interval = graphRange / steps;
190
+ }
191
+ } else if (hasUserRange) {
192
+ // 2) user range only
193
+ steps = maxSteps;
194
+ interval = graphRange / steps;
195
+ } else if (isValidInterval) {
196
+ // 3) user interval only
197
+ interval = resolvedInterval;
198
+ steps = Math.ceil(graphRange / interval);
199
+
200
+ while (steps > maxSteps) {
201
+ interval *= 2;
202
+ steps = Math.ceil(graphRange / interval);
92
203
  }
204
+
205
+ graphMax = graphMin + (interval * steps);
206
+ } else {
207
+ // 4) auto
208
+ interval = this.getNiceNumber({
209
+ range: graphRange / maxSteps,
210
+ round: true,
211
+ });
212
+
213
+ steps = Math.ceil(graphRange / interval);
214
+ graphMax = graphMin + (interval * steps);
93
215
  }
94
216
 
95
- return decimals;
217
+ this.adjustedDecimalPoint = this.decimalPoint === 'auto'
218
+ ? this.getAutoDecimalPointFromInterval(interval)
219
+ : this.decimalPoint;
220
+
221
+ return {
222
+ steps,
223
+ interval,
224
+ graphMin,
225
+ graphMax,
226
+ };
96
227
  }
97
228
  }
98
229
 
@@ -44,6 +44,100 @@ class TimeScale extends Scale {
44
44
  }
45
45
  return Math.ceil((max - min) / step);
46
46
  }
47
+
48
+ /**
49
+ * With range information, calculate how many labels in axis
50
+ * @param {object} range min/max information
51
+ *
52
+ * @returns {object} steps, interval, min/max graph value
53
+ */
54
+ calculateSteps(range) {
55
+ const { maxValue, minValue, maxSteps } = range;
56
+
57
+ // 사용자 interval로 인식하는 경우: 숫자 또는 객체({ time, unit }) 형태만
58
+ // 문자열('hour', 'second' 등)은 기존 로직(분기 D)으로 처리
59
+ const hasUserRange = Array.isArray(this.range) && this.range.length === 2;
60
+ const hasUserInterval = (
61
+ typeof this.interval === 'number'
62
+ || (typeof this.interval === 'object' && this.interval !== null)
63
+ );
64
+
65
+ const resolvedInterval = hasUserInterval ? this.getInterval(range) : null;
66
+ const isValidInterval = (
67
+ resolvedInterval != null
68
+ && resolvedInterval > 0
69
+ && isFinite(resolvedInterval)
70
+ );
71
+
72
+ const graphMin = +minValue;
73
+ let graphMax = +maxValue;
74
+ const graphRange = graphMax - graphMin;
75
+
76
+ let interval;
77
+ let steps;
78
+
79
+ if (hasUserRange && isValidInterval) {
80
+ // 1) user range + interval
81
+ const candidateSteps = graphRange / resolvedInterval;
82
+ const isExactlyDividable = Math.abs(candidateSteps - Math.round(candidateSteps)) < 1e-10;
83
+ if (isExactlyDividable && candidateSteps <= maxSteps) {
84
+ // 1-1) interval 호환되는 경우
85
+ interval = resolvedInterval;
86
+ steps = Math.round(candidateSteps);
87
+ } else {
88
+ // 1-2) interval 호환되지 않음 -> 사용자 interval을 사용하지 않음
89
+ steps = maxSteps;
90
+ interval = graphRange / steps;
91
+ }
92
+ } else if (hasUserRange) {
93
+ // 2) user range only
94
+ steps = maxSteps;
95
+ interval = graphRange / steps;
96
+ } else if (isValidInterval) {
97
+ // 3) user interval only
98
+ interval = resolvedInterval;
99
+ steps = Math.ceil(graphRange / interval);
100
+ while (steps > maxSteps) {
101
+ interval *= 2;
102
+ steps = Math.ceil(graphRange / interval);
103
+ }
104
+ graphMax = graphMin + (interval * steps);
105
+ } else {
106
+ // 4) 기존 로직
107
+ interval = this.getInterval(range);
108
+ let increase = minValue;
109
+ let numberOfSteps;
110
+
111
+ while (increase < maxValue) {
112
+ increase += interval;
113
+ }
114
+
115
+ graphMax = increase;
116
+
117
+ numberOfSteps = Math.round(graphRange / interval);
118
+
119
+ while (numberOfSteps > maxSteps) {
120
+ interval *= 2;
121
+ numberOfSteps = Math.round(graphRange / interval);
122
+ const tempInterval = graphRange / numberOfSteps;
123
+ interval = this.decimalPoint ? tempInterval : Math.ceil(tempInterval);
124
+ }
125
+
126
+ if (graphMax - graphMin > (numberOfSteps * interval)) {
127
+ const tempInterval = (graphMax - graphMin) / numberOfSteps;
128
+ interval = this.decimalPoint ? tempInterval : Math.ceil(tempInterval);
129
+ }
130
+
131
+ steps = numberOfSteps;
132
+ }
133
+
134
+ return {
135
+ steps,
136
+ interval,
137
+ graphMin,
138
+ graphMax,
139
+ };
140
+ }
47
141
  }
48
142
 
49
143
  export default TimeScale;