evui 3.3.13 → 3.3.16

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.16",
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
 
@@ -100,7 +113,8 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
100
113
  await watch(() => props.data, (chartData) => {
101
114
  const newData = getNormalizedData(chartData);
102
115
  const isUpdateSeries = !isEqual(newData.series, evChart.data.series)
103
- || !isEqual(newData.groups, evChart.data.groups);
116
+ || !isEqual(newData.groups, evChart.data.groups)
117
+ || props.options.type === 'heatMap';
104
118
  evChart.data = cloneDeep(newData);
105
119
  evChart.update({
106
120
  updateSeries: isUpdateSeries,
@@ -115,7 +129,13 @@ import { onMounted, onBeforeUnmount, watch, onDeactivated } from 'vue';
115
129
 
116
130
  await watch(() => props.selectedLabel, (newValue) => {
117
131
  if (newValue.dataIndex) {
118
- evChart.renderWithSelectLabel(newValue.dataIndex);
132
+ evChart.renderWithSelected(newValue.dataIndex);
133
+ }
134
+ }, { deep: true });
135
+
136
+ await watch(() => props.selectedSeries, (newValue) => {
137
+ if (newValue.seriesId) {
138
+ evChart.renderWithSelected(newValue.seriesId);
119
139
  }
120
140
  }, { deep: true });
121
141
  });
@@ -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;
@@ -188,8 +189,8 @@ class EvChart {
188
189
  break;
189
190
  }
190
191
  case 'bar': {
191
- const { thickness, borderRadius } = this.options;
192
- series.draw({ thickness, borderRadius, showSeriesCount, showIndex, ...opt });
192
+ const { thickness, cPadRatio, borderRadius } = this.options;
193
+ series.draw({ thickness, cPadRatio, borderRadius, showSeriesCount, showIndex, ...opt });
193
194
  if (series.show) {
194
195
  showIndex++;
195
196
  }
@@ -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
 
@@ -67,7 +67,14 @@ class Bar {
67
67
 
68
68
  const dArea = isHorizontal ? yArea : xArea;
69
69
  const cArea = dArea / (this.data.length || 1);
70
- const cPad = 2;
70
+
71
+ let cPad;
72
+ const isUnableToDrawCategoryPadding = param.cPadRatio >= 1 || param.cPadRatio <= 0;
73
+ if (isUnableToDrawCategoryPadding) {
74
+ cPad = 2;
75
+ } else {
76
+ cPad = Math.max((dArea * (param.cPadRatio / 2)) / this.data.length, 2);
77
+ }
71
78
 
72
79
  let bArea;
73
80
  let w;
@@ -77,8 +77,8 @@ class HeatMap {
77
77
  drawItem(ctx, x, y, w, h) {
78
78
  ctx.beginPath();
79
79
  if (this.stroke.show) {
80
- ctx.fillRect(x, y, w, h);
81
80
  ctx.strokeRect(x, y, w, h);
81
+ ctx.fillRect(x, y, w, h);
82
82
  } else {
83
83
  const aliasPixel = Util.aliasPixel(1);
84
84
  ctx.fillRect(
@@ -148,8 +148,11 @@ class HeatMap {
148
148
  if (this.colorAxis[colorIndex].show) {
149
149
  ctx.fillStyle = Util.colorStringToRgba(item.dataColor, opacity);
150
150
  if (this.stroke.show) {
151
- const { color, lineWidth } = this.stroke;
152
- ctx.strokeStyle = Util.colorStringToRgba(color, opacity);
151
+ const { color, lineWidth, opacity: sOpacity } = this.stroke;
152
+ ctx.strokeStyle = Util.colorStringToRgba(
153
+ color,
154
+ opacity === 1 ? sOpacity : opacity,
155
+ );
153
156
  ctx.lineWidth = lineWidth;
154
157
  xp += (lineWidth * 1.5);
155
158
  yp += (lineWidth * 1.5);
@@ -216,7 +219,7 @@ class HeatMap {
216
219
  const centerX = x + (w / 2);
217
220
  const centerY = y + (h / 2);
218
221
 
219
- if (vw >= w || formattedTxt < 0) {
222
+ if (vw >= w || vh >= h || formattedTxt < 0) {
220
223
  return;
221
224
  }
222
225
 
@@ -267,13 +270,10 @@ class HeatMap {
267
270
  const y1 = yp;
268
271
  const y2 = yp + h;
269
272
 
270
- return ((x1 <= xsp && x2 >= xsp) && (y1 <= ysp && y2 >= ysp))
271
- || ((x1 <= xep && x2 >= xep) && (y1 <= ysp && y2 >= ysp))
272
- || ((x1 <= xsp && x2 >= xsp) && (y1 <= yep && y2 >= yep))
273
- || ((x1 <= xep && x2 >= xep) && (y1 <= yep && y2 >= yep))
274
- || ((x1 >= xsp && x1 <= xep) && (y1 >= ysp && y1 <= yep))
273
+ return ((x1 >= xsp && x1 <= xep) && (y1 >= ysp && y1 <= yep))
275
274
  || ((x1 >= xsp && x1 <= xep) && (y2 >= ysp && y2 <= yep))
276
- || ((x2 >= xsp && x2 <= xep) && (y1 >= ysp && y1 <= yep));
275
+ || ((x2 >= xsp && x2 <= xep) && (y1 >= ysp && y1 <= yep))
276
+ || ((x2 >= xsp && x2 <= xep) && (y2 >= ysp && y2 <= yep));
277
277
  });
278
278
  }
279
279
 
@@ -352,6 +352,104 @@ class HeatMap {
352
352
 
353
353
  return item;
354
354
  }
355
+
356
+ findBlockRange({ xcp, xep, ycp, yep, range }) {
357
+ const labels = this.labels;
358
+
359
+ const blockRange = {
360
+ xsp: Math.min(xcp, xep),
361
+ ysp: Math.min(ycp, yep),
362
+ width: Math.ceil(Math.abs(xep - xcp)),
363
+ height: Math.ceil(Math.abs(yep - ycp)),
364
+ };
365
+
366
+ if (labels.x.length && labels.y.length) {
367
+ const { x1, x2, y1, y2 } = range;
368
+ const gapX = (x2 - x1) / labels.x.length;
369
+ const gapY = (y2 - y1) / labels.y.length;
370
+
371
+ const point = {
372
+ xsp: xcp,
373
+ xep,
374
+ ysp: ycp,
375
+ yep,
376
+ };
377
+
378
+ const setPoint = (dir, target, key) => {
379
+ let itemPoint;
380
+ let gap;
381
+ let startPoint;
382
+
383
+ if (dir === 'x') {
384
+ gap = gapX;
385
+ startPoint = x1;
386
+ } else {
387
+ gap = gapY;
388
+ startPoint = y1;
389
+ }
390
+
391
+ const findItem = labels[dir].findIndex((item, index) => {
392
+ itemPoint = Math.round(startPoint + (gap * index)) + Util.aliasPixel(1);
393
+ return itemPoint <= target && target <= itemPoint + gap;
394
+ });
395
+
396
+ if (findItem > -1) {
397
+ point[key] = ['xsp', 'ysp'].includes(key) ? itemPoint : itemPoint + gap;
398
+ }
399
+ };
400
+
401
+ setPoint('x', Math.min(xcp, xep), 'xsp');
402
+ setPoint('x', Math.max(xcp, xep), 'xep');
403
+ setPoint('y', Math.min(ycp, yep), 'ysp');
404
+ setPoint('y', Math.max(ycp, yep), 'yep');
405
+
406
+ blockRange.xsp = Math.min(point.xsp, point.xep);
407
+ blockRange.ysp = Math.min(point.ysp, point.yep);
408
+ blockRange.width = Math.abs(point.xep - point.xsp);
409
+ blockRange.height = Math.abs(point.yep - point.ysp);
410
+ }
411
+
412
+ return blockRange;
413
+ }
414
+
415
+ findSelectionRange(rangeInfo) {
416
+ const { xcp, ycp, width, height, range } = rangeInfo;
417
+
418
+ let selectionRange = null;
419
+
420
+ const { x1, x2, y1, y2 } = range;
421
+ const { x: labelX, y: labelY } = this.labels;
422
+
423
+ if (labelX.length && labelY.length) {
424
+ const gapX = (x2 - x1) / labelX.length;
425
+ const gapY = (y2 - y1) / labelY.length;
426
+
427
+ const xsp = xcp;
428
+ const xep = xcp + width;
429
+ const ysp = ycp;
430
+ const yep = ycp + height;
431
+
432
+ const xIndex = {
433
+ min: Math.floor((xsp - x1) / gapX),
434
+ max: Math.floor((xep - x1 - gapX) / gapX),
435
+ };
436
+
437
+ const lastIndexY = labelY.length - 1;
438
+ const yIndex = {
439
+ min: lastIndexY - Math.floor((yep - y1 - gapY) / gapY),
440
+ max: lastIndexY - Math.floor((ysp - y1) / gapY),
441
+ };
442
+
443
+ selectionRange = {
444
+ xMin: labelX[xIndex.min],
445
+ xMax: labelX[xIndex.max],
446
+ yMin: labelY[yIndex.min],
447
+ yMax: labelY[yIndex.max],
448
+ };
449
+ }
450
+
451
+ return selectionRange;
452
+ }
355
453
  }
356
454
 
357
455
  export default HeatMap;
@@ -102,7 +102,8 @@ class Line {
102
102
  const getXPos = val => Canvas.calculateX(val, minmaxX.graphMin, minmaxX.graphMax, xArea, xsp);
103
103
  const getYPos = val => Canvas.calculateY(val, minmaxY.graphMin, minmaxY.graphMax, yArea, ysp);
104
104
 
105
- this.data.reduce((prev, curr, ix, item) => {
105
+ // draw line
106
+ this.data.reduce((prev, curr, ix) => {
106
107
  x = getXPos(curr.x);
107
108
  y = getYPos(curr.y);
108
109
 
@@ -110,22 +111,12 @@ class Line {
110
111
  x += Util.aliasPixel(x);
111
112
  }
112
113
 
113
- if (y === null || x === null) {
114
- if (ix - 1 > -1) {
115
- // draw fill(area) series not stacked
116
- if (this.fill && prev.y !== null && !this.stackIndex) {
117
- ctx.stroke();
118
- ctx.lineTo(prev.xp, endPoint);
119
- ctx.lineTo(item[startFillIndex].xp, endPoint);
120
-
121
- ctx.fill();
122
- ctx.beginPath();
123
- }
124
- }
114
+ if (ix === 0) {
115
+ ctx.moveTo(x, y);
116
+ }
125
117
 
126
- startFillIndex = ix + 1;
127
- } else if (ix === 0 || prev.y === null || curr.y === null
128
- || prev.x === null || curr.x === null) {
118
+ const isNullValue = prev.y === null || curr.y === null || prev.x === null || curr.x === null;
119
+ if (isNullValue) {
129
120
  ctx.moveTo(x, y);
130
121
  } else {
131
122
  ctx.lineTo(x, y);
@@ -139,41 +130,45 @@ class Line {
139
130
 
140
131
  ctx.stroke();
141
132
 
142
- const dataLen = this.data.length;
133
+ // draw fill
134
+ if (this.fill && this.data.length) {
135
+ ctx.beginPath();
143
136
 
144
- if (this.fill && dataLen) {
145
137
  ctx.fillStyle = Util.colorStringToRgba(mainColor, fillOpacity);
146
138
 
147
- if (this.stackIndex) {
148
- const reversedDataList = this.data.slice().reverse();
149
- reversedDataList.forEach((curr, ix) => {
150
- ctx.beginPath();
139
+ this.data.forEach((currData, ix) => {
140
+ const isEmptyPoint = data => data?.x === null || data?.y === null
141
+ || data?.x === undefined || data?.y === undefined;
142
+
143
+ const nextData = this.data[ix + 1];
144
+
145
+ if (isEmptyPoint(currData)) {
146
+ startFillIndex = ix + 1;
151
147
 
152
- x = getXPos(curr.x);
153
- y = getYPos(curr.b);
148
+ if (!isEmptyPoint(nextData)) {
149
+ ctx.moveTo(nextData.xp, nextData.yp);
150
+ }
154
151
 
155
- const prev = reversedDataList[ix - 1];
156
- if (curr.o !== null) {
157
- if (prev && prev.o == null) {
158
- ctx.moveTo(x, getYPos(curr.b + curr.o));
159
- }
152
+ return;
153
+ }
160
154
 
161
- ctx.lineTo(x, y);
155
+ ctx.lineTo(currData.xp, currData.yp);
162
156
 
163
- if (ix === reversedDataList.length - 1) {
164
- ctx.lineTo(x, getYPos(curr.b + curr.o));
165
- }
166
- } else if (prev && prev.o) {
167
- ctx.lineTo(getXPos(prev.x), getYPos(prev.b + prev.o));
157
+ if (isEmptyPoint(nextData)) {
158
+ for (let jx = ix; jx >= startFillIndex; jx--) {
159
+ const prevData = this.data[jx];
160
+ const xp = prevData.xp;
161
+ const bp = getYPos(prevData.b) ?? endPoint;
162
+ ctx.lineTo(xp, bp);
168
163
  }
169
- });
170
- } else if (startFillIndex < dataLen) {
171
- ctx.lineTo(this.data[dataLen - 1].xp, endPoint);
172
- ctx.lineTo(this.data[startFillIndex].xp, endPoint);
173
- }
164
+
165
+ ctx.closePath();
166
+ }
167
+ });
174
168
 
175
169
  ctx.fill();
176
170
  }
171
+
177
172
  if (this.point || isExistSelectedLabel) {
178
173
  ctx.strokeStyle = Util.colorStringToRgba(mainColor, mainColorOpacity);
179
174
  const focusStyle = Util.colorStringToRgba(pointFillColor, 1);
@@ -45,7 +45,6 @@ class Pie {
45
45
  const slice = new Path2D();
46
46
 
47
47
  const radius = this.isSelect ? this.radius + 5 : this.radius;
48
- const doughnutHoleRadius = this.radius * this.doughnutHoleSize;
49
48
 
50
49
  const color = this.color;
51
50
  const noneDownplayOpacity = color.includes('rgba') ? Util.getOpacity(color) : 1;
@@ -66,7 +65,7 @@ class Pie {
66
65
  }
67
66
 
68
67
  if (this.showValue?.use) {
69
- this.drawValueLabels(ctx, doughnutHoleRadius);
68
+ this.drawValueLabels(ctx);
70
69
  }
71
70
 
72
71
  ctx.closePath();
@@ -115,7 +114,6 @@ class Pie {
115
114
  itemHighlight(item, context) {
116
115
  const ctx = context;
117
116
  const radius = this.isSelect ? this.radius + 5 : this.radius;
118
- const doughnutHoleRadius = this.radius * this.doughnutHoleSize;
119
117
 
120
118
  ctx.save();
121
119
  ctx.shadowOffsetX = 0;
@@ -133,7 +131,7 @@ class Pie {
133
131
  ctx.fill();
134
132
 
135
133
  if (this.showValue?.use) {
136
- this.drawValueLabels(ctx, doughnutHoleRadius);
134
+ this.drawValueLabels(ctx);
137
135
  }
138
136
 
139
137
  ctx.closePath();
@@ -146,19 +144,9 @@ class Pie {
146
144
  * @param context canvas context
147
145
  */
148
146
  drawValueLabels(context) {
149
- const { fontSize, textColor, formatter } = this.showValue;
150
147
  const ctx = context;
151
-
152
- ctx.save();
153
- ctx.beginPath();
154
-
155
- ctx.font = `normal normal normal ${fontSize}px Roboto`;
156
- ctx.fillStyle = textColor;
157
- ctx.lineWidth = 1;
158
- ctx.textAlign = 'center';
159
- ctx.textBaseline = 'middle';
160
-
161
148
  const value = this.data.o;
149
+ const { fontSize, textColor, formatter } = this.showValue;
162
150
 
163
151
  let formattedTxt;
164
152
  if (formatter) {
@@ -180,15 +168,27 @@ class Pie {
180
168
  && radius >= valueWidth * ratio
181
169
  && radius >= valueHeight * ratio
182
170
  ) {
171
+ ctx.save();
172
+ ctx.beginPath();
173
+
174
+ const noneDownplayOpacity = textColor.includes('rgba') ? Util.getOpacity(textColor) : 1;
175
+ const opacity = this.state === 'downplay' ? 0.1 : noneDownplayOpacity;
176
+
177
+ ctx.font = `normal normal normal ${fontSize}px Roboto`;
178
+ ctx.fillStyle = Util.colorStringToRgba(textColor, opacity);
179
+ ctx.lineWidth = 1;
180
+ ctx.textAlign = 'center';
181
+ ctx.textBaseline = 'middle';
182
+
183
183
  const halfRadius = (radius / 2) + this.doughnutHoleSize;
184
184
  const centerAngle = ((this.endAngle - this.startAngle) / 2) + this.startAngle;
185
185
  const xPos = halfRadius * Math.cos(centerAngle) + this.centerX;
186
186
  const yPos = halfRadius * Math.sin(centerAngle) + this.centerY;
187
187
 
188
188
  ctx.fillText(formattedTxt, xPos, yPos);
189
- }
190
189
 
191
- ctx.restore();
190
+ ctx.restore();
191
+ }
192
192
  }
193
193
  }
194
194
 
@@ -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) {
@@ -422,14 +422,21 @@ const modules = {
422
422
  const colorOpt = this.options.heatMapColor;
423
423
  const categoryCnt = colorOpt.categoryCnt;
424
424
 
425
+ let minValue;
425
426
  let maxValue = 0;
427
+
426
428
  let isExistError = false;
427
429
  data.forEach(({ o: value }) => {
428
430
  if (maxValue < value) {
429
- maxValue = value;
431
+ maxValue = Math.max(maxValue, value);
430
432
  }
433
+
431
434
  if (value < 0) {
432
435
  isExistError = true;
436
+ } else if (minValue === undefined) {
437
+ minValue = value;
438
+ } else {
439
+ minValue = Math.min(minValue, value);
433
440
  }
434
441
  });
435
442
 
@@ -443,8 +450,9 @@ const modules = {
443
450
  }
444
451
 
445
452
  return {
453
+ min: minValue,
446
454
  max: maxValue,
447
- interval: Math.ceil(maxValue / categoryCnt),
455
+ interval: Math.ceil((maxValue - minValue) / categoryCnt),
448
456
  existError: isExistError,
449
457
  };
450
458
  },
@@ -618,6 +626,101 @@ const modules = {
618
626
  };
619
627
  },
620
628
 
629
+ /**
630
+ * Find seriesId by position x and y
631
+ * @param {array} offset position x and y
632
+ *
633
+ * @returns {object} clicked series id
634
+ */
635
+ getSeriesIdByPosition(offset) {
636
+ const [clickedX, clickedY] = offset;
637
+ const chartRect = this.chartRect;
638
+ const labelOffset = this.labelOffset;
639
+ const aPos = {
640
+ x1: chartRect.x1 + labelOffset.left,
641
+ x2: chartRect.x2 - labelOffset.right,
642
+ y1: chartRect.y1 + labelOffset.top,
643
+ y2: chartRect.y2 - labelOffset.bottom,
644
+ };
645
+ const valueAxes = this.axesY[0];
646
+ const labelAxes = this.axesX[0];
647
+ const valueStartPoint = aPos[valueAxes.units.rectStart];
648
+ const valueEndPoint = aPos[valueAxes.units.rectEnd];
649
+ const labelStartPoint = aPos[labelAxes.units.rectStart];
650
+ const labelEndPoint = aPos[labelAxes.units.rectEnd];
651
+
652
+ const result = { sId: null };
653
+
654
+ if (clickedY > valueEndPoint && clickedY < valueStartPoint
655
+ && clickedX < labelEndPoint && clickedX > labelStartPoint) {
656
+ let hitSeries;
657
+ let positionList;
658
+ const hitItem = this.findHitItem(offset);
659
+ const hitSeriesList = Object.keys(hitItem.items);
660
+
661
+ switch (this.options.type) {
662
+ case 'line': {
663
+ const orderedSeriesList = this.seriesInfo.charts.line;
664
+ const isStackChart = Object.values(this.seriesList).some(({ stackIndex }) => stackIndex);
665
+
666
+ if (hitSeriesList.length) { // 클릭한 위치에 data 가 존재하는 경우
667
+ if (isStackChart) {
668
+ positionList = orderedSeriesList.filter(sId => hitSeriesList.includes(sId))
669
+ .map(sId => ({ sId, position: hitItem.items[sId]?.data?.yp }));
670
+ hitSeries = positionList.find(({ position }) => clickedY > position)?.sId;
671
+ } else {
672
+ hitSeries = Object.entries(hitItem.items).find(([, { hit }]) => hit)?.[0];
673
+ }
674
+ } else { // 클릭한 위치에 data 가 존재하지 않는 경우
675
+ const visibleSeriesList = orderedSeriesList.filter(sId => this.seriesList[sId].show);
676
+ positionList = visibleSeriesList.map(sId => ({
677
+ sId,
678
+ position: this.seriesList[sId].data?.map(({ xp, yp }) => [xp, yp]),
679
+ }));
680
+ const dataIndex = positionList[0].position?.findIndex(([xp]) => xp >= clickedX);
681
+ const vectorList = positionList.map(({ sId, position }) => ({
682
+ sId,
683
+ vector: { start: position[dataIndex - 1], end: position[dataIndex] },
684
+ }));
685
+
686
+ // canvas 의 클릭 위치값은 제 4 사분면의 위치이므로 clickedY, y1, y2 의 값은 음수를 취한다.
687
+ if (isStackChart) {
688
+ hitSeries = vectorList.find(({ vector }) => {
689
+ const [x1, y1] = vector.start;
690
+ const [x2, y2] = vector.end;
691
+ const v1 = [x2 - x1, y1 - y2];
692
+ const v2 = [x2 - clickedX, clickedY - y2];
693
+ const xp = v1[0] * v2[1] - v1[1] * v2[0];
694
+
695
+ return vector.start.every(v => typeof v === 'number')
696
+ && vector.end.every(v => typeof v === 'number')
697
+ && xp > 0;
698
+ })?.sId;
699
+ } else {
700
+ hitSeries = vectorList.find(({ vector }) => {
701
+ const [x1, y1] = vector.start;
702
+ const [x2, y2] = vector.end;
703
+ const a = (y1 - y2) / (x2 - x1);
704
+ const b = -1;
705
+ const c = -y1 - a * x1;
706
+ const distance = Math.abs(a * clickedX - b * clickedY + c)
707
+ / Math.sqrt(a ** 2 + b ** 2);
708
+
709
+ return distance < 3;
710
+ })?.sId;
711
+ }
712
+ }
713
+ break;
714
+ }
715
+ default:
716
+ break;
717
+ }
718
+
719
+ result.sId = hitSeries;
720
+ }
721
+
722
+ return result;
723
+ },
621
724
  /**
622
725
  * Find label info by position x and y
623
726
  * @param {array} offset position x and y