evui 3.4.155 → 3.4.156

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.155",
3
+ "version": "3.4.156",
4
4
  "description": "A EXEM Library project",
5
5
  "author": "exem <dev_client@ex-em.com>",
6
6
  "license": "MIT",
@@ -454,10 +454,10 @@ class EvChart {
454
454
  const lastHitInfo = this.lastHitInfo;
455
455
  const defaultSelectInfo = this.defaultSelectItemInfo;
456
456
 
457
- if (lastHitInfo?.maxIndex || lastHitInfo?.maxIndex === 0) {
457
+ if (lastHitInfo?.dataIndex || lastHitInfo?.dataIndex === 0) {
458
458
  selectInfo = {
459
459
  seriesID: lastHitInfo.sId,
460
- dataIndex: lastHitInfo.maxIndex,
460
+ dataIndex: lastHitInfo.dataIndex,
461
461
  };
462
462
  } else if (defaultSelectInfo?.dataIndex || defaultSelectInfo?.dataIndex === 0) {
463
463
  selectInfo = { ...defaultSelectInfo };
@@ -301,6 +301,9 @@ class Bar {
301
301
  item.data = barData[clampedIndex];
302
302
  item.index = clampedIndex;
303
303
  item.hit = this.isPointInBar(offset, barData[clampedIndex]);
304
+ // bar 박스 내부 클릭은 "직접 박스 히트"로 표시.
305
+ // findHitItem에서 line 포인트 근접 히트보다 우선 선택되도록 하기 위함.
306
+ item.directHit = item.hit;
304
307
  }
305
308
 
306
309
  return item;
@@ -347,6 +350,8 @@ class Bar {
347
350
  item.data = barData;
348
351
  item.index = barData.index;
349
352
  item.hit = this.isPointInBar(offset, barData);
353
+ // bar 박스 내부 클릭은 "직접 박스 히트"로 표시 (findHitItem 우선순위용).
354
+ item.directHit = item.hit;
350
355
  return item;
351
356
  }
352
357
 
@@ -328,6 +328,24 @@ class Line {
328
328
  const gdata = this.data.filter(data => !Util.isNullOrUndefined(data.x));
329
329
  const isLinearInterpolation = this.useLinearInterpolation();
330
330
 
331
+ // line 포인트 "정확 히트" 판정용 반경.
332
+ // combo 차트에서 line 포인트 중심을 직격한 경우, 같은 좌표의 bar(directHit)보다
333
+ // line이 우선되도록 item.directHit = true로 표시한다. 그 외(단순 Y축 근접)는 기존처럼 hit만.
334
+ // 포인트 반지름에 기본 포인트 크기(LINE_OPTION.pointSize)만큼의 클릭 여유 마진을 더하고,
335
+ // 시각적으로 하이라이트되는 포인트 반경(highlight.maxSize)을 최소 보장값으로 사용한다.
336
+ const directHitRadius = Math.max(
337
+ (this.pointSize ?? LINE_OPTION.pointSize) + LINE_OPTION.pointSize,
338
+ LINE_OPTION.highlight.maxSize,
339
+ );
340
+ const isLinePointDirectHit = (point) => {
341
+ if (!point || point.xp === undefined || point.yp === undefined) {
342
+ return false;
343
+ }
344
+ const dx = xp - point.xp;
345
+ const dy = yp - point.yp;
346
+ return dx * dx + dy * dy <= directHitRadius * directHitRadius;
347
+ };
348
+
331
349
  if (gdata?.length) {
332
350
  if (typeof dataIndex === 'number' && this.show) {
333
351
  item.data = gdata[dataIndex];
@@ -340,6 +358,10 @@ class Line {
340
358
  if (yDist <= directHitThreshold) {
341
359
  item.hit = true;
342
360
  }
361
+ if (isLinePointDirectHit(point)) {
362
+ item.hit = true;
363
+ item.directHit = true;
364
+ }
343
365
  }
344
366
  } else if (typeof this.beforeFindItemIndex === 'number' && this.beforeFindItemIndex !== -1 && this.show && useSelectLabelOrItem) {
345
367
  item.data = gdata[this.beforeFindItemIndex];
@@ -472,6 +494,10 @@ class Line {
472
494
  if (yDist <= directHitThreshold) {
473
495
  item.hit = true;
474
496
  }
497
+ if (isLinePointDirectHit(point)) {
498
+ item.hit = true;
499
+ item.directHit = true;
500
+ }
475
501
  }
476
502
  }
477
503
  }
@@ -155,7 +155,7 @@ const modules = {
155
155
 
156
156
  if (tipType === 'sel') {
157
157
  if (hitInfo && hitInfo.label !== null) {
158
- lastTip.pos = type === 'bar' ? hitInfo.maxIndex : hitInfo.label;
158
+ lastTip.pos = type === 'bar' ? hitInfo.dataIndex : hitInfo.label;
159
159
  ldata = lastTip.pos;
160
160
  } else if (lastTip.pos !== null) {
161
161
  ldata = lastTip.pos;
@@ -809,12 +809,9 @@ const modules = {
809
809
  return null;
810
810
  }
811
811
 
812
- itemPosition = [this.getItemByPosition(
813
- [dataInfo.xp, dataInfo.yp],
814
- useApproximate,
815
- dataIndex,
816
- true,
817
- )];
812
+ itemPosition = [
813
+ this.getHitItemByPosition([dataInfo.xp, dataInfo.yp], useApproximate, dataIndex, true),
814
+ ];
818
815
  } else {
819
816
  const seriesList = Object.entries(this.seriesList);
820
817
  let firShowSeriesID;
@@ -835,7 +832,7 @@ const modules = {
835
832
  return null;
836
833
  }
837
834
 
838
- return this.getItemByPosition(
835
+ return this.getHitItemByPosition(
839
836
  [dataInfo?.xp ?? 0, dataInfo?.yp ?? 0],
840
837
  useApproximate,
841
838
  idx,
@@ -863,31 +860,48 @@ const modules = {
863
860
  },
864
861
 
865
862
  /**
866
- * Find graph item by position x and y
863
+ * Find the hit item at the given position (x, y).
864
+ *
865
+ * 선택 우선순위:
866
+ * 1. directHit (bar 박스 내부 클릭) — 가장 가까운 것
867
+ * 2. hit (line 포인트 근접 등) — 가장 가까운 것
868
+ * 3. hit이 전혀 없으면 데이터가 있는 첫 시리즈로 fallback (기존 동작 호환)
869
+ *
870
+ * 과거에는 "같은 라벨 위에서 값이 가장 큰 시리즈"를 돌려주는 max-value 덮어쓰기 방식이었으나,
871
+ * bar + line combo 차트에서 작은 bar를 클릭해도 큰 값의 line이 선택되는 버그의 원인이었다.
872
+ * 이번 수정으로 사용자가 실제로 가리킨 시리즈(hit)가 선택되도록 바뀐다.
873
+ *
867
874
  * @param {array} offset position x and y
868
875
  * @param {boolean} useApproximate if it's true. it'll look for closed item on mouse position
869
876
  * @param {number} dataIndex selected data index
870
877
  * @param {boolean} useSelectLabelOrItem used to display select label/item at tooltip location
871
878
  *
872
- * @returns {object} clicked item information
879
+ * @returns {object} hit item information
873
880
  */
874
- getItemByPosition(
875
- offset,
876
- useApproximate = false,
877
- dataIndex,
878
- useSelectLabelOrItem = false,
879
- ) {
881
+ getHitItemByPosition(offset, useApproximate = false, dataIndex, useSelectLabelOrItem = false) {
880
882
  const seriesIDs = Object.keys(this.seriesList);
881
883
  const isHorizontal = !!this.options.horizontal;
882
884
 
883
- let maxType = null;
884
- let maxLabel = null;
885
- let maxValuePos = null;
886
- let maxValue = null;
887
- let maxSeriesID = '';
885
+ // hit 기반 결과 (최우선)
886
+ let hitType = null;
887
+ let hitLabel = null;
888
+ let hitValuePos = null;
889
+ let hitValue = null;
890
+ let hitSeriesID = '';
891
+ let hitDataIndex = null;
892
+ let hitDistance = Infinity;
893
+ let hasDirectHit = false;
894
+
895
+ // fallback: hit이 전혀 없을 때 사용할 "데이터 있는 첫 시리즈" 정보
896
+ let fallbackType = null;
897
+ let fallbackLabel = null;
898
+ let fallbackValuePos = null;
899
+ let fallbackValue = null;
900
+ let fallbackSeriesID = '';
901
+ let fallbackDataIndex = null;
902
+
888
903
  let acc = 0;
889
904
  let useStack = false;
890
- let maxIndex = null;
891
905
 
892
906
  for (let ix = 0; ix < seriesIDs.length; ix++) {
893
907
  const seriesID = seriesIDs[ix];
@@ -907,12 +921,13 @@ const modules = {
907
921
 
908
922
  if (data) {
909
923
  if (Util.isPieType(item.type)) {
910
- maxLabel = seriesID;
911
- maxSeriesID = seriesID;
912
- maxValuePos = (data.ea - data.sa) / 2;
913
- maxValue = data.o;
914
- maxIndex = data.index;
915
- maxType = item.type;
924
+ // pie 차트는 hit detection 체계가 달라 기존 동작 유지 (단일 pie 시리즈가 일반적)
925
+ hitType = item.type;
926
+ hitLabel = seriesID;
927
+ hitSeriesID = seriesID;
928
+ hitValuePos = (data.ea - data.sa) / 2;
929
+ hitValue = data.o;
930
+ hitDataIndex = data.index;
916
931
  } else {
917
932
  const ldata = isHorizontal ? data.y : data.x;
918
933
  const lp = isHorizontal ? data.yp : data.xp;
@@ -927,23 +942,45 @@ const modules = {
927
942
  acc += data.y;
928
943
  }
929
944
 
945
+ // fallback 기록: 데이터가 있는 첫 시리즈를 저장
946
+ if (fallbackSeriesID === '') {
947
+ fallbackType = series.type;
948
+ fallbackLabel = ldata;
949
+ fallbackValuePos = lp;
950
+ fallbackValue = g;
951
+ fallbackSeriesID = seriesID;
952
+ fallbackDataIndex = index;
953
+ }
930
954
 
931
- if (maxType === 'bar' && useStack) {
932
- if (item.hit) {
933
- maxValue = g;
934
- maxSeriesID = seriesID;
935
- maxIndex = index;
936
- maxLabel = ldata;
937
- maxValuePos = lp;
938
- maxType = series.type;
955
+ // hit 기반 선택: item.hit이 true이고 유효한 좌표가 있을 때만 고려
956
+ if (item.hit && data.xp !== undefined && data.yp !== undefined) {
957
+ const distance = (data.xp - offset[0]) ** 2 + (data.yp - offset[1]) ** 2;
958
+
959
+ if (item.directHit) {
960
+ // 직접 박스 히트는 최우선. 여러 개이면 가장 가까운 것.
961
+ if (!hasDirectHit || distance < hitDistance) {
962
+ hitDistance = distance;
963
+ hitType = series.type;
964
+ hitLabel = ldata;
965
+ hitValuePos = lp;
966
+ hitValue = g;
967
+ hitSeriesID = seriesID;
968
+ hitDataIndex = index;
969
+ }
970
+ hasDirectHit = true;
971
+ } else if (!hasDirectHit) {
972
+ // directHit가 없을 때만 일반 hit 거리 비교 참여
973
+ // (라인 근접 히트가 박스 직접 히트를 이기지 못하도록)
974
+ if (distance < hitDistance) {
975
+ hitDistance = distance;
976
+ hitType = series.type;
977
+ hitLabel = ldata;
978
+ hitValuePos = lp;
979
+ hitValue = g;
980
+ hitSeriesID = seriesID;
981
+ hitDataIndex = index;
982
+ }
939
983
  }
940
- } else if (maxValue === null || maxValue <= g) {
941
- maxValue = g;
942
- maxSeriesID = seriesID;
943
- maxLabel = ldata;
944
- maxValuePos = lp;
945
- maxIndex = index;
946
- maxType = series.type;
947
984
  }
948
985
  }
949
986
  }
@@ -951,22 +988,24 @@ const modules = {
951
988
  }
952
989
  }
953
990
 
991
+ const hasHit = hitSeriesID !== '';
992
+
954
993
  return {
955
- type: maxType,
956
- label: maxLabel,
957
- pos: maxValuePos,
958
- value: maxValue ?? 0,
959
- sId: maxSeriesID,
994
+ type: hasHit ? hitType : fallbackType,
995
+ label: hasHit ? hitLabel : fallbackLabel,
996
+ pos: hasHit ? hitValuePos : fallbackValuePos,
997
+ value: (hasHit ? hitValue : fallbackValue) ?? 0,
998
+ sId: hasHit ? hitSeriesID : fallbackSeriesID,
960
999
  acc,
961
1000
  useStack,
962
- maxIndex,
1001
+ dataIndex: hasHit ? hitDataIndex : fallbackDataIndex,
963
1002
  };
964
1003
  },
965
1004
 
966
1005
  /**
967
1006
  * @typedef {Object} LabelInfoResult
968
1007
  * @property {number} labelIndex - 선택된 라벨의 인덱스
969
- * @property {object} hitInfo - 해당 위치에서의 히트 정보 (getItemByPosition 반환값)
1008
+ * @property {object} hitInfo - 해당 위치에서의 히트 정보 (getHitItemByPosition 반환값)
970
1009
  */
971
1010
  /**
972
1011
  * Find label info by position x and y
@@ -1040,13 +1079,13 @@ const modules = {
1040
1079
  offsetX = x;
1041
1080
  }
1042
1081
 
1043
- hitInfo = this.getItemByPosition(
1082
+ hitInfo = this.getHitItemByPosition(
1044
1083
  [offsetX, y],
1045
1084
  selectLabel?.useApproximateValue,
1046
1085
  dataIndex,
1047
1086
  true,
1048
1087
  );
1049
- labelIndex = hitInfo.maxIndex ?? -1;
1088
+ labelIndex = hitInfo.dataIndex ?? -1;
1050
1089
  }
1051
1090
 
1052
1091
  return {
@@ -294,13 +294,13 @@ const modules = {
294
294
  const useSelectSeries = selectSeriesOpt?.use && selectSeriesOpt?.useClick;
295
295
 
296
296
  const setSelectedItemInfo = () => {
297
- const hitInfo = this.getItemByPosition(offset, false);
297
+ const hitInfo = this.getHitItemByPosition(offset, false);
298
298
 
299
299
  ({
300
300
  label: args.label,
301
301
  value: args.value,
302
302
  sId: args.seriesId,
303
- maxIndex: args.dataIndex,
303
+ dataIndex: args.dataIndex,
304
304
  acc: args.acc,
305
305
  } = hitInfo);
306
306
 
@@ -308,7 +308,7 @@ const modules = {
308
308
  args.selected = {
309
309
  eventTarget: 'item',
310
310
  seriesId: this.isDeselectItem(hitInfo) ? null : hitInfo.sId,
311
- dataIndex: this.isDeselectItem(hitInfo) ? null : hitInfo.maxIndex,
311
+ dataIndex: this.isDeselectItem(hitInfo) ? null : hitInfo.dataIndex,
312
312
  };
313
313
  }
314
314
  };
@@ -917,6 +917,9 @@ const modules = {
917
917
  let maxg = null;
918
918
  let maxSID = null;
919
919
  let minDistance = Infinity;
920
+ // directHit(bar 박스 내부 클릭/hover) 시리즈가 발견되었는지 추적.
921
+ // 한 번이라도 directHit가 있으면 line의 근접 포인트 히트는 hitId 후보에서 배제된다.
922
+ let hasDirectHit = false;
920
923
 
921
924
  // 1. 먼저 공통으로 사용할 데이터 인덱스 결정
922
925
  const targetDataIndex = this.findClosestDataIndex(offset, sIds);
@@ -986,12 +989,23 @@ const modules = {
986
989
  maxSID = sId;
987
990
  }
988
991
 
989
- // 마우스 위치와의 거리 계산하여 가장 가까운 시리즈 선택
992
+ // 마우스 위치와의 거리 계산하여 가장 가까운 시리즈 선택.
993
+ // directHit(bar 박스 내부)가 하나라도 있으면 그중에서만 선택하고,
994
+ // 라인의 근접 포인트 히트(item.hit=true, directHit=false)는 hitId 후보에서 배제한다.
995
+ // bar + line combo 차트에서 작은 bar 클릭 시 큰 값의 line이 잡히던 버그 방지.
990
996
  if (item.hit && item.data.xp !== undefined && item.data.yp !== undefined) {
991
997
  const distance = (item.data.xp - offset[0]) ** 2
992
998
  + (item.data.yp - offset[1]) ** 2;
993
999
 
994
- if (distance < minDistance) {
1000
+ if (item.directHit) {
1001
+ // directHit는 최우선. 여러 directHit 중에서는 가장 가까운 것 선택.
1002
+ if (!hasDirectHit || distance < minDistance) {
1003
+ minDistance = distance;
1004
+ hitId = sId;
1005
+ }
1006
+ hasDirectHit = true;
1007
+ } else if (!hasDirectHit && distance < minDistance) {
1008
+ // directHit가 없을 때만 일반 hit 거리 비교
995
1009
  minDistance = distance;
996
1010
  hitId = sId;
997
1011
  }
@@ -1622,9 +1636,9 @@ const modules = {
1622
1636
  */
1623
1637
  isDeselectItem(hitInfo) {
1624
1638
  return this.options.selectItem.useDeselectItem
1625
- && hitInfo?.maxIndex === this.defaultSelectItemInfo?.dataIndex
1639
+ && hitInfo?.dataIndex === this.defaultSelectItemInfo?.dataIndex
1626
1640
  && hitInfo?.sId === this.defaultSelectItemInfo?.seriesID
1627
- && !isNaN(hitInfo?.maxIndex);
1641
+ && !isNaN(hitInfo?.dataIndex);
1628
1642
  },
1629
1643
 
1630
1644
  /**