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/dist/evui.common.js +158 -66
- package/dist/evui.common.js.map +1 -1
- package/dist/evui.umd.js +158 -66
- package/dist/evui.umd.js.map +1 -1
- package/dist/evui.umd.min.js +1 -1
- package/dist/evui.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chart/chart.core.js +2 -2
- package/src/components/chart/element/element.bar.js +5 -0
- package/src/components/chart/element/element.line.js +26 -0
- package/src/components/chart/element/element.tip.js +1 -1
- package/src/components/chart/model/model.store.js +90 -51
- package/src/components/chart/plugins/plugins.interaction.js +21 -7
package/package.json
CHANGED
|
@@ -454,10 +454,10 @@ class EvChart {
|
|
|
454
454
|
const lastHitInfo = this.lastHitInfo;
|
|
455
455
|
const defaultSelectInfo = this.defaultSelectItemInfo;
|
|
456
456
|
|
|
457
|
-
if (lastHitInfo?.
|
|
457
|
+
if (lastHitInfo?.dataIndex || lastHitInfo?.dataIndex === 0) {
|
|
458
458
|
selectInfo = {
|
|
459
459
|
seriesID: lastHitInfo.sId,
|
|
460
|
-
dataIndex: lastHitInfo.
|
|
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.
|
|
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 = [
|
|
813
|
-
[dataInfo.xp, dataInfo.yp],
|
|
814
|
-
|
|
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.
|
|
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
|
|
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}
|
|
879
|
+
* @returns {object} hit item information
|
|
873
880
|
*/
|
|
874
|
-
|
|
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
|
-
|
|
884
|
-
let
|
|
885
|
-
let
|
|
886
|
-
let
|
|
887
|
-
let
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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:
|
|
956
|
-
label:
|
|
957
|
-
pos:
|
|
958
|
-
value:
|
|
959
|
-
sId:
|
|
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
|
-
|
|
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 - 해당 위치에서의 히트 정보 (
|
|
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.
|
|
1082
|
+
hitInfo = this.getHitItemByPosition(
|
|
1044
1083
|
[offsetX, y],
|
|
1045
1084
|
selectLabel?.useApproximateValue,
|
|
1046
1085
|
dataIndex,
|
|
1047
1086
|
true,
|
|
1048
1087
|
);
|
|
1049
|
-
labelIndex = hitInfo.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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?.
|
|
1639
|
+
&& hitInfo?.dataIndex === this.defaultSelectItemInfo?.dataIndex
|
|
1626
1640
|
&& hitInfo?.sId === this.defaultSelectItemInfo?.seriesID
|
|
1627
|
-
&& !isNaN(hitInfo?.
|
|
1641
|
+
&& !isNaN(hitInfo?.dataIndex);
|
|
1628
1642
|
},
|
|
1629
1643
|
|
|
1630
1644
|
/**
|