evui 3.3.13 → 3.3.14

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.3.13",
3
+ "version": "3.3.14",
4
4
  "description": "A EXEM Library project",
5
5
  "author": "exem <dev_client@ex-em.com>",
6
6
  "license": "MIT",
@@ -24,6 +24,10 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
24
24
  type: Object,
25
25
  default: null,
26
26
  },
27
+ selectedSeries: {
28
+ type: Object,
29
+ default: null,
30
+ },
27
31
  options: {
28
32
  type: Object,
29
33
  default: () => ({}),
@@ -43,6 +47,7 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
43
47
  'drag-select',
44
48
  'update:selectedItem',
45
49
  'update:selectedLabel',
50
+ 'update:selectedSeries',
46
51
  ],
47
52
  setup(props) {
48
53
  let evChart = {};
@@ -52,6 +57,7 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
52
57
  eventListeners,
53
58
  selectItemInfo,
54
59
  selectLabelInfo,
60
+ selectSeriesInfo,
55
61
  getNormalizedData,
56
62
  getNormalizedOptions,
57
63
  } = useModel();
@@ -67,13 +73,20 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
67
73
  );
68
74
 
69
75
  const createChart = () => {
76
+ let selected;
77
+ if (normalizedOptions.selectLabel.use) {
78
+ selected = selectLabelInfo;
79
+ } else if (normalizedOptions.selectSeries.use) {
80
+ selected = selectSeriesInfo;
81
+ }
82
+
70
83
  evChart = new EvChart(
71
84
  wrapper.value,
72
85
  normalizedData,
73
86
  normalizedOptions,
74
87
  eventListeners,
75
88
  selectItemInfo,
76
- selectLabelInfo,
89
+ selected,
77
90
  );
78
91
  };
79
92
 
@@ -115,7 +128,13 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
115
128
 
116
129
  await watch(() => props.selectedLabel, (newValue) => {
117
130
  if (newValue.dataIndex) {
118
- evChart.renderWithSelectLabel(newValue.dataIndex);
131
+ evChart.renderWithSelected(newValue.dataIndex);
132
+ }
133
+ }, { deep: true });
134
+
135
+ await watch(() => props.selectedSeries, (newValue) => {
136
+ if (newValue.seriesId) {
137
+ evChart.renderWithSelected(newValue.seriesId);
119
138
  }
120
139
  }, { deep: true });
121
140
  });
@@ -14,7 +14,7 @@ import Pie from './plugins/plugins.pie';
14
14
  import Tip from './element/element.tip';
15
15
 
16
16
  class EvChart {
17
- constructor(target, data, options, listeners, defaultSelectItemInfo, defaultSelectLabelInfo) {
17
+ constructor(target, data, options, listeners, defaultSelectItemInfo, defaultSelectInfo) {
18
18
  Object.keys(Model).forEach(key => Object.assign(this, Model[key]));
19
19
  Object.assign(this, Title);
20
20
  Object.assign(this, Legend);
@@ -66,7 +66,7 @@ class EvChart {
66
66
  };
67
67
 
68
68
  this.defaultSelectItemInfo = defaultSelectItemInfo;
69
- this.defaultSelectLabelInfo = defaultSelectLabelInfo;
69
+ this.defaultSelectInfo = defaultSelectInfo;
70
70
  }
71
71
 
72
72
  /**
@@ -93,7 +93,7 @@ class EvChart {
93
93
 
94
94
  this.axesRange = this.getAxesRange();
95
95
  this.labelOffset = this.getLabelOffset();
96
- this.initSelectedLabelInfo();
96
+ this.initSelectedInfo();
97
97
 
98
98
  this.drawChart();
99
99
 
@@ -153,7 +153,7 @@ class EvChart {
153
153
  * @returns {undefined}
154
154
  */
155
155
  drawSeries(hitInfo) {
156
- const { maxTip, selectLabel, selectItem } = this.options;
156
+ const { maxTip, selectLabel, selectItem, selectSeries } = this.options;
157
157
 
158
158
  const opt = {
159
159
  ctx: this.bufferCtx,
@@ -161,7 +161,8 @@ class EvChart {
161
161
  labelOffset: this.labelOffset,
162
162
  axesSteps: this.axesSteps,
163
163
  maxTipOpt: { background: maxTip.background, color: maxTip.color },
164
- selectLabel: { option: selectLabel, selected: this.defaultSelectLabelInfo },
164
+ selectLabel: { option: selectLabel, selected: this.defaultSelectInfo },
165
+ selectSeries: { option: selectSeries, selected: this.defaultSelectInfo },
165
166
  };
166
167
 
167
168
  let showIndex = 0;
@@ -331,7 +332,7 @@ class EvChart {
331
332
  this.labelOffset,
332
333
  this.axesSteps.x[index],
333
334
  hitInfo,
334
- this.defaultSelectLabelInfo);
335
+ this.defaultSelectInfo);
335
336
  });
336
337
 
337
338
  this.axesY.forEach((axis, index) => {
@@ -340,7 +341,7 @@ class EvChart {
340
341
  this.labelOffset,
341
342
  this.axesSteps.y[index],
342
343
  hitInfo,
343
- this.defaultSelectLabelInfo);
344
+ this.defaultSelectInfo);
344
345
  });
345
346
  }
346
347
 
@@ -660,7 +661,7 @@ class EvChart {
660
661
  this.axesY = this.createAxes('y', options.axesY);
661
662
  this.axesRange = this.getAxesRange();
662
663
  this.labelOffset = this.getLabelOffset();
663
- this.initSelectedLabelInfo();
664
+ this.initSelectedInfo();
664
665
 
665
666
  this.render(updateInfo?.hitInfo);
666
667
 
@@ -147,8 +147,6 @@ class Line {
147
147
  if (this.stackIndex) {
148
148
  const reversedDataList = this.data.slice().reverse();
149
149
  reversedDataList.forEach((curr, ix) => {
150
- ctx.beginPath();
151
-
152
150
  x = getXPos(curr.x);
153
151
  y = getYPos(curr.b);
154
152
 
@@ -250,7 +250,7 @@ const modules = {
250
250
  const opt = this.options;
251
251
  const isHorizontal = !!opt.horizontal;
252
252
  const labelTipOpt = opt.selectLabel;
253
- const { dataIndex, data, label } = this.defaultSelectLabelInfo;
253
+ const { dataIndex, data, label } = this.defaultSelectInfo;
254
254
  let drawTip = false;
255
255
 
256
256
  if (dataIndex.length) {
@@ -618,6 +618,101 @@ const modules = {
618
618
  };
619
619
  },
620
620
 
621
+ /**
622
+ * Find seriesId by position x and y
623
+ * @param {array} offset position x and y
624
+ *
625
+ * @returns {object} clicked series id
626
+ */
627
+ getSeriesIdByPosition(offset) {
628
+ const [clickedX, clickedY] = offset;
629
+ const chartRect = this.chartRect;
630
+ const labelOffset = this.labelOffset;
631
+ const aPos = {
632
+ x1: chartRect.x1 + labelOffset.left,
633
+ x2: chartRect.x2 - labelOffset.right,
634
+ y1: chartRect.y1 + labelOffset.top,
635
+ y2: chartRect.y2 - labelOffset.bottom,
636
+ };
637
+ const valueAxes = this.axesY[0];
638
+ const labelAxes = this.axesX[0];
639
+ const valueStartPoint = aPos[valueAxes.units.rectStart];
640
+ const valueEndPoint = aPos[valueAxes.units.rectEnd];
641
+ const labelStartPoint = aPos[labelAxes.units.rectStart];
642
+ const labelEndPoint = aPos[labelAxes.units.rectEnd];
643
+
644
+ const result = { sId: null };
645
+
646
+ if (clickedY > valueEndPoint && clickedY < valueStartPoint
647
+ && clickedX < labelEndPoint && clickedX > labelStartPoint) {
648
+ let hitSeries;
649
+ let positionList;
650
+ const hitItem = this.findHitItem(offset);
651
+ const hitSeriesList = Object.keys(hitItem.items);
652
+
653
+ switch (this.options.type) {
654
+ case 'line': {
655
+ const orderedSeriesList = this.seriesInfo.charts.line;
656
+ const isStackChart = Object.values(this.seriesList).some(({ stackIndex }) => stackIndex);
657
+
658
+ if (hitSeriesList.length) { // 클릭한 위치에 data 가 존재하는 경우
659
+ if (isStackChart) {
660
+ positionList = orderedSeriesList.filter(sId => hitSeriesList.includes(sId))
661
+ .map(sId => ({ sId, position: hitItem.items[sId]?.data?.yp }));
662
+ hitSeries = positionList.find(({ position }) => clickedY > position)?.sId;
663
+ } else {
664
+ hitSeries = Object.entries(hitItem.items).find(([, { hit }]) => hit)?.[0];
665
+ }
666
+ } else { // 클릭한 위치에 data 가 존재하지 않는 경우
667
+ const visibleSeriesList = orderedSeriesList.filter(sId => this.seriesList[sId].show);
668
+ positionList = visibleSeriesList.map(sId => ({
669
+ sId,
670
+ position: this.seriesList[sId].data?.map(({ xp, yp }) => [xp, yp]),
671
+ }));
672
+ const dataIndex = positionList[0].position?.findIndex(([xp]) => xp >= clickedX);
673
+ const vectorList = positionList.map(({ sId, position }) => ({
674
+ sId,
675
+ vector: { start: position[dataIndex - 1], end: position[dataIndex] },
676
+ }));
677
+
678
+ // canvas 의 클릭 위치값은 제 4 사분면의 위치이므로 clickedY, y1, y2 의 값은 음수를 취한다.
679
+ if (isStackChart) {
680
+ hitSeries = vectorList.find(({ vector }) => {
681
+ const [x1, y1] = vector.start;
682
+ const [x2, y2] = vector.end;
683
+ const v1 = [x2 - x1, y1 - y2];
684
+ const v2 = [x2 - clickedX, clickedY - y2];
685
+ const xp = v1[0] * v2[1] - v1[1] * v2[0];
686
+
687
+ return vector.start.every(v => typeof v === 'number')
688
+ && vector.end.every(v => typeof v === 'number')
689
+ && xp > 0;
690
+ })?.sId;
691
+ } else {
692
+ hitSeries = vectorList.find(({ vector }) => {
693
+ const [x1, y1] = vector.start;
694
+ const [x2, y2] = vector.end;
695
+ const a = (y1 - y2) / (x2 - x1);
696
+ const b = -1;
697
+ const c = -y1 - a * x1;
698
+ const distance = Math.abs(a * clickedX - b * clickedY + c)
699
+ / Math.sqrt(a ** 2 + b ** 2);
700
+
701
+ return distance < 3;
702
+ })?.sId;
703
+ }
704
+ }
705
+ break;
706
+ }
707
+ default:
708
+ break;
709
+ }
710
+
711
+ result.sId = hitSeries;
712
+ }
713
+
714
+ return result;
715
+ },
621
716
  /**
622
717
  * Find label info by position x and y
623
718
  * @param {array} offset position x and y
@@ -144,15 +144,22 @@ const modules = {
144
144
  maxIndex: args.dataIndex,
145
145
  acc: args.acc,
146
146
  } = hitInfo);
147
- }
148
-
149
- if (this.options.selectLabel.use) {
147
+ } else if (this.options.selectLabel.use) {
150
148
  const offset = this.getMousePosition(e);
151
149
  const clickedLabelInfo = this.getLabelInfoByPosition(offset);
152
150
  const selected = this.selectLabel(clickedLabelInfo.labelIndex);
153
- this.renderWithSelectLabel(selected.dataIndex);
151
+ this.renderWithSelected(selected.dataIndex);
152
+
153
+ args.selected = cloneDeep(this.defaultSelectInfo);
154
+ } else if (this.options.selectSeries.use) {
155
+ const offset = this.getMousePosition(e);
156
+ const hitInfo = this.getSeriesIdByPosition(offset);
157
+ if (hitInfo.sId !== null) {
158
+ const selected = this.selectSeries(hitInfo.sId);
159
+ this.renderWithSelected(selected.seriesId);
160
+ }
154
161
 
155
- args.selected = cloneDeep(this.defaultSelectLabelInfo);
162
+ args.selected = cloneDeep(this.defaultSelectInfo);
156
163
  }
157
164
 
158
165
  if (typeof this.listeners.click === 'function') {
@@ -456,32 +463,51 @@ const modules = {
456
463
  },
457
464
 
458
465
  /**
459
- * render after select label by index list
466
+ * render after selected label or selected series
460
467
  * @param indexList {array} '[0, 1 ...]'
461
468
  */
462
- renderWithSelectLabel(indexList) {
463
- this.defaultSelectLabelInfo.dataIndex = indexList;
464
- this.initSelectedLabelInfo();
469
+ renderWithSelected(list) {
470
+ if (this.options.selectLabel.use) {
471
+ this.defaultSelectInfo.dataIndex = list;
472
+ } else if (this.options.selectSeries.use) {
473
+ this.defaultSelectInfo.seriesId = list;
474
+ }
475
+ this.initSelectedInfo();
465
476
  this.render();
466
477
  },
467
478
 
468
479
  /**
469
- * init defaultSelectLabelInfo object.
470
- * (set each series data and label text)
480
+ * init defaultSelectInfo object.
481
+ * - at selectLabel using: set each series data and label text
482
+ * - at selectSeries using: set series state
471
483
  */
472
- initSelectedLabelInfo() {
473
- const { use, limit } = this.options.selectLabel;
474
-
475
- if (use) {
476
- if (!this.defaultSelectLabelInfo) {
477
- this.defaultSelectLabelInfo = { dataIndex: [] };
484
+ initSelectedInfo() {
485
+ if (this.options.selectLabel.use) {
486
+ const { limit } = this.options.selectLabel;
487
+ if (!this.defaultSelectInfo) {
488
+ this.defaultSelectInfo = { dataIndex: [] };
478
489
  }
479
- const infoObj = this.defaultSelectLabelInfo;
490
+ const infoObj = this.defaultSelectInfo;
480
491
  infoObj.dataIndex.splice(limit);
481
492
  infoObj.label = infoObj.dataIndex.map(i => this.data.labels[i]);
482
493
  const dataEntries = Object.entries(this.data.data);
483
494
  infoObj.data = infoObj.dataIndex.map(labelIdx => Object.fromEntries(
484
495
  dataEntries.map(([sId, data]) => [sId, data[labelIdx]])));
496
+ } else if (this.options.selectSeries.use) {
497
+ if (!this.defaultSelectInfo) {
498
+ this.defaultSelectInfo = { seriesId: [] };
499
+ }
500
+
501
+ const selectedList = this.defaultSelectInfo.seriesId;
502
+ Object.values(this.seriesList).forEach((series) => {
503
+ if (!selectedList.length) {
504
+ series.state = 'normal';
505
+ } else if (selectedList.includes(series.sId)) {
506
+ series.state = 'highlight';
507
+ } else {
508
+ series.state = 'downplay';
509
+ }
510
+ });
485
511
  }
486
512
  },
487
513
 
@@ -492,7 +518,7 @@ const modules = {
492
518
  */
493
519
  selectLabel(labelIndex) {
494
520
  const option = this.options?.selectLabel ?? {};
495
- const before = this.defaultSelectLabelInfo ?? { dataIndex: [] };
521
+ const before = this.defaultSelectInfo ?? { dataIndex: [] };
496
522
  const after = cloneDeep(before);
497
523
 
498
524
  if (before.dataIndex.includes(labelIndex)) {
@@ -512,6 +538,28 @@ const modules = {
512
538
  return after;
513
539
  },
514
540
 
541
+ selectSeries(seriesId) {
542
+ const option = this.options?.selectSeries ?? {};
543
+ const before = this.defaultSelectInfo ?? { seriesId: [] };
544
+ const after = cloneDeep(before);
545
+
546
+ if (before.seriesId.includes(seriesId)) {
547
+ const idx = before.seriesId.indexOf(seriesId);
548
+ after.seriesId.splice(idx, 1);
549
+ } else if (seriesId) {
550
+ after.seriesId.push(seriesId);
551
+ if (option.limit > 0 && option.limit < after.seriesId.length) {
552
+ if (option.useDeselectOverflow) {
553
+ after.seriesId.splice(0, 1);
554
+ } else {
555
+ after.seriesId.pop();
556
+ }
557
+ }
558
+ }
559
+
560
+ return after;
561
+ },
562
+
515
563
  /**
516
564
  * Find items by series within a range
517
565
  * @param {object} range object for find series items
@@ -191,9 +191,13 @@ const modules = {
191
191
  }
192
192
  const nameDOM = targetDOM.getElementsByClassName('ev-chart-legend-name')[0];
193
193
  const targetId = nameDOM.series.sId;
194
+ const selectSeriesOption = this.options.selectSeries;
195
+ const selectedList = this.defaultSelectInfo?.seriesId ?? [];
194
196
 
195
197
  Object.values(this.seriesList).forEach((series) => {
196
- series.state = series.sId === targetId ? 'highlight' : 'downplay';
198
+ series.state = series.sId === targetId
199
+ || (selectSeriesOption.use && selectedList.includes(targetId))
200
+ ? 'highlight' : 'downplay';
197
201
  });
198
202
 
199
203
  this.update({
@@ -209,8 +213,15 @@ const modules = {
209
213
  * @returns {undefined}
210
214
  */
211
215
  this.onLegendBoxLeave = () => {
216
+ const selectSeriesOption = this.options.selectSeries;
217
+ const selectedList = this.defaultSelectInfo?.seriesId ?? [];
212
218
  Object.values(this.seriesList).forEach((series) => {
213
- series.state = 'normal';
219
+ if (selectSeriesOption.use && selectedList.length) {
220
+ series.state = selectedList.includes(series.sId)
221
+ ? 'highlight' : 'downplay';
222
+ } else {
223
+ series.state = 'normal';
224
+ }
214
225
  });
215
226
 
216
227
  this.update({
@@ -164,7 +164,7 @@ class Scale {
164
164
  *
165
165
  * @returns {undefined}
166
166
  */
167
- draw(chartRect, labelOffset, stepInfo, hitInfo) {
167
+ draw(chartRect, labelOffset, stepInfo, hitInfo, selectLabelInfo) {
168
168
  const ctx = this.ctx;
169
169
  const options = this.options;
170
170
  const aPos = {
@@ -239,6 +239,13 @@ class Scale {
239
239
  linePosition = labelCenter + aliasPixel;
240
240
  labelText = this.getLabelFormat(Math.min(axisMax, ticks[ix]));
241
241
 
242
+ const isBlurredLabel = this.options?.selectLabel?.use
243
+ && this.options?.selectLabel?.useLabelOpacity
244
+ && (this.options.horizontal === (this.type === 'y'))
245
+ && selectLabelInfo?.dataIndex?.length
246
+ && !selectLabelInfo?.label
247
+ .map(t => this.getLabelFormat(Math.min(axisMax, t))).includes(labelText);
248
+
242
249
  const labelColor = this.labelStyle.color;
243
250
  let defaultOpacity = 1;
244
251
 
@@ -246,14 +253,17 @@ class Scale {
246
253
  defaultOpacity = Util.getOpacity(labelColor);
247
254
  }
248
255
 
249
- ctx.fillStyle = Util.colorStringToRgba(labelColor, defaultOpacity);
256
+ ctx.fillStyle = Util.colorStringToRgba(labelColor, isBlurredLabel ? 0.1 : defaultOpacity);
250
257
 
251
258
  let labelPoint;
252
259
 
253
260
  if (this.type === 'x') {
254
261
  labelPoint = this.position === 'top' ? offsetPoint - 10 : offsetPoint + 10;
255
262
  ctx.fillText(labelText, labelCenter, labelPoint);
256
- if (options?.selectItem?.showLabelTip && hitInfo?.label && !this.options?.horizontal) {
263
+ if (!isBlurredLabel
264
+ && options?.selectItem?.showLabelTip
265
+ && hitInfo?.label
266
+ && !this.options?.horizontal) {
257
267
  const selectedLabel = this.getLabelFormat(
258
268
  Math.min(axisMax, hitInfo.label + (0 * stepValue)),
259
269
  );
@@ -94,6 +94,11 @@ const DEFAULT_OPTIONS = {
94
94
  useApproximateValue: false,
95
95
  tipBackground: '#000000',
96
96
  },
97
+ selectSeries: {
98
+ use: false,
99
+ limit: 1,
100
+ useDeselectOverflow: false,
101
+ },
97
102
  dragSelection: {
98
103
  use: false,
99
104
  keepDisplay: true,
@@ -145,6 +150,7 @@ export const useModel = () => {
145
150
 
146
151
  const selectItemInfo = cloneDeep(props.selectedItem);
147
152
  const selectLabelInfo = cloneDeep(props.selectedLabel);
153
+ const selectSeriesInfo = cloneDeep(props.selectedSeries);
148
154
 
149
155
  const eventListeners = {
150
156
  click: async (e) => {
@@ -152,9 +158,12 @@ export const useModel = () => {
152
158
  if (e.label) {
153
159
  emit('update:selectedItem', { seriesID: e.seriesId, dataIndex: e.dataIndex });
154
160
  }
155
- if (e.selected) {
161
+ if (e.selected?.dataIndex) {
156
162
  emit('update:selectedLabel', { dataIndex: e.selected.dataIndex });
157
163
  }
164
+ if (e.selected?.seriesId) {
165
+ emit('update:selectedSeries', { seriesId: e.selected.seriesId });
166
+ }
158
167
  emit('click', e);
159
168
  },
160
169
  'dbl-click': async (e) => {
@@ -171,6 +180,7 @@ export const useModel = () => {
171
180
  eventListeners,
172
181
  selectItemInfo,
173
182
  selectLabelInfo,
183
+ selectSeriesInfo,
174
184
  getNormalizedData,
175
185
  getNormalizedOptions,
176
186
  };