evui 3.4.155 → 3.4.157
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 +640 -117
- package/dist/evui.common.js.map +1 -1
- package/dist/evui.umd.js +640 -117
- 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/helpers/helpers.util.js +23 -0
- package/src/components/chart/model/model.store.js +169 -47
- package/src/components/chart/plugins/plugins.interaction.js +74 -22
- package/src/components/chart/plugins/plugins.scrollbar.js +9 -1
- package/src/components/chart/scale/scale.step.js +26 -2
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;
|
|
@@ -424,4 +424,27 @@ export default {
|
|
|
424
424
|
|
|
425
425
|
return `${color}80`;
|
|
426
426
|
},
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 클릭 좌표(cx, cy)에서 데이터 포인트까지의 거리²를 반환한다.
|
|
430
|
+
* w/h 가 있으면 박스 외벽까지의 거리(내부면 0), 없으면 포인트까지의 유클리드 거리².
|
|
431
|
+
* @param {object} data - 데이터 포인트 (xp, yp, w?, h?)
|
|
432
|
+
* @param {number} cx - 클릭 x 좌표
|
|
433
|
+
* @param {number} cy - 클릭 y 좌표
|
|
434
|
+
* @returns {number}
|
|
435
|
+
*/
|
|
436
|
+
calcBoxDistance(data, cx, cy) {
|
|
437
|
+
if (data.w !== null && data.w !== undefined && data.h !== null && data.h !== undefined) {
|
|
438
|
+
const sx = data.xp;
|
|
439
|
+
const sy = data.yp;
|
|
440
|
+
const xMin = Math.min(sx, sx + data.w);
|
|
441
|
+
const xMax = Math.max(sx, sx + data.w);
|
|
442
|
+
const yMin = Math.min(sy, sy + data.h);
|
|
443
|
+
const yMax = Math.max(sy, sy + data.h);
|
|
444
|
+
const dx = Math.max(0, xMin - cx, cx - xMax);
|
|
445
|
+
const dy = Math.max(0, yMin - cy, cy - yMax);
|
|
446
|
+
return dx * dx + dy * dy;
|
|
447
|
+
}
|
|
448
|
+
return (data.xp - cx) ** 2 + (data.yp - cy) ** 2;
|
|
449
|
+
},
|
|
427
450
|
};
|
|
@@ -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,94 @@ 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 (distance 기반)
|
|
869
|
+
*
|
|
867
870
|
* @param {array} offset position x and y
|
|
868
871
|
* @param {boolean} useApproximate if it's true. it'll look for closed item on mouse position
|
|
869
872
|
* @param {number} dataIndex selected data index
|
|
870
873
|
* @param {boolean} useSelectLabelOrItem used to display select label/item at tooltip location
|
|
874
|
+
* @param {boolean} disableNullLabelSnap true 이면 all-null 라벨도 그대로 반환 (click/dblclick 용)
|
|
871
875
|
*
|
|
872
|
-
* @returns {object}
|
|
876
|
+
* @returns {object} hit item information
|
|
873
877
|
*/
|
|
874
|
-
|
|
878
|
+
getHitItemByPosition(
|
|
875
879
|
offset,
|
|
876
880
|
useApproximate = false,
|
|
877
881
|
dataIndex,
|
|
878
882
|
useSelectLabelOrItem = false,
|
|
883
|
+
disableNullLabelSnap = false,
|
|
879
884
|
) {
|
|
880
885
|
const seriesIDs = Object.keys(this.seriesList);
|
|
881
886
|
const isHorizontal = !!this.options.horizontal;
|
|
882
887
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
let
|
|
888
|
+
const [cx, cy] = offset;
|
|
889
|
+
|
|
890
|
+
// dataIndex 미지정 시 클릭 좌표에 가장 가까운 valid 라벨 인덱스 결정.
|
|
891
|
+
// disableNullLabelSnap=true 이면 all-null 라벨도 후보로 인정.
|
|
892
|
+
let resolvedDataIndex = dataIndex;
|
|
893
|
+
if (resolvedDataIndex === undefined && !useApproximate) {
|
|
894
|
+
const refSeriesID = seriesIDs.find((sId) => {
|
|
895
|
+
const s = this.seriesList[sId];
|
|
896
|
+
return s?.show && s?.data?.length > 0;
|
|
897
|
+
});
|
|
898
|
+
if (refSeriesID) {
|
|
899
|
+
const refData = this.seriesList[refSeriesID].data;
|
|
900
|
+
const clickPos = isHorizontal ? offset[1] : offset[0];
|
|
901
|
+
let nearestDistance = Infinity;
|
|
902
|
+
let nearestIndex = -1;
|
|
903
|
+
for (let i = 0; i < refData.length; i++) {
|
|
904
|
+
const hasValidData = disableNullLabelSnap || seriesIDs.some((sId) => {
|
|
905
|
+
const s = this.seriesList[sId];
|
|
906
|
+
return s?.show && s.data?.[i]?.o !== null && s.data?.[i]?.o !== undefined;
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const p = refData[i];
|
|
910
|
+
if (hasValidData && p) {
|
|
911
|
+
let labelPos;
|
|
912
|
+
if (isHorizontal) {
|
|
913
|
+
labelPos = p.h ? p.yp + (p.h / 2) : p.yp;
|
|
914
|
+
} else {
|
|
915
|
+
labelPos = p.w ? p.xp + (p.w / 2) : p.xp;
|
|
916
|
+
}
|
|
917
|
+
if (labelPos !== null && labelPos !== undefined) {
|
|
918
|
+
const d = Math.abs(clickPos - labelPos);
|
|
919
|
+
if (d < nearestDistance) {
|
|
920
|
+
nearestDistance = d;
|
|
921
|
+
nearestIndex = i;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (nearestIndex !== -1) resolvedDataIndex = nearestIndex;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// hit 기반 결과 (최우선)
|
|
931
|
+
let hitType = null;
|
|
932
|
+
let hitLabel = null;
|
|
933
|
+
let hitValuePos = null;
|
|
934
|
+
let hitValue = null;
|
|
935
|
+
let hitSeriesID = '';
|
|
936
|
+
let hitDataIndex = null;
|
|
937
|
+
let hitDistance = Infinity;
|
|
938
|
+
let hasDirectHit = false;
|
|
939
|
+
|
|
940
|
+
// hit 없을 때 쓸 fallback — 값이 있는 시리즈 중 클릭 좌표에 가장 가까운 것.
|
|
941
|
+
let fallbackType = null;
|
|
942
|
+
let fallbackLabel = null;
|
|
943
|
+
let fallbackValuePos = null;
|
|
944
|
+
let fallbackValue = null;
|
|
945
|
+
let fallbackSeriesID = '';
|
|
946
|
+
let fallbackDataIndex = null;
|
|
947
|
+
let fallbackDistance = Infinity;
|
|
948
|
+
|
|
888
949
|
let acc = 0;
|
|
889
950
|
let useStack = false;
|
|
890
|
-
let maxIndex = null;
|
|
891
951
|
|
|
892
952
|
for (let ix = 0; ix < seriesIDs.length; ix++) {
|
|
893
953
|
const seriesID = seriesIDs[ix];
|
|
@@ -899,7 +959,7 @@ const modules = {
|
|
|
899
959
|
series,
|
|
900
960
|
offset,
|
|
901
961
|
isHorizontal,
|
|
902
|
-
|
|
962
|
+
resolvedDataIndex,
|
|
903
963
|
useSelectLabelOrItem,
|
|
904
964
|
);
|
|
905
965
|
const data = item.data;
|
|
@@ -907,12 +967,13 @@ const modules = {
|
|
|
907
967
|
|
|
908
968
|
if (data) {
|
|
909
969
|
if (Util.isPieType(item.type)) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
970
|
+
// pie 차트는 hit detection 체계가 달라 기존 동작 유지 (단일 pie 시리즈가 일반적)
|
|
971
|
+
hitType = item.type;
|
|
972
|
+
hitLabel = seriesID;
|
|
973
|
+
hitSeriesID = seriesID;
|
|
974
|
+
hitValuePos = (data.ea - data.sa) / 2;
|
|
975
|
+
hitValue = data.o;
|
|
976
|
+
hitDataIndex = data.index;
|
|
916
977
|
} else {
|
|
917
978
|
const ldata = isHorizontal ? data.y : data.x;
|
|
918
979
|
const lp = isHorizontal ? data.yp : data.xp;
|
|
@@ -927,23 +988,61 @@ const modules = {
|
|
|
927
988
|
acc += data.y;
|
|
928
989
|
}
|
|
929
990
|
|
|
991
|
+
// fallback 후보: 값이 있는 시리즈 중 거리가 가장 가까운 쪽.
|
|
992
|
+
// 값이 null 인 시리즈는 제외.
|
|
993
|
+
const hasMeaningfulValue = g !== null && g !== undefined && !Number.isNaN(g);
|
|
994
|
+
const hasCoords = data.xp !== null && data.xp !== undefined
|
|
995
|
+
&& data.yp !== null && data.yp !== undefined;
|
|
996
|
+
if (hasMeaningfulValue && hasCoords) {
|
|
997
|
+
const distance = Util.calcBoxDistance(data, cx, cy);
|
|
998
|
+
if (fallbackSeriesID === '' || distance < fallbackDistance) {
|
|
999
|
+
fallbackDistance = distance;
|
|
1000
|
+
fallbackType = series.type;
|
|
1001
|
+
fallbackLabel = ldata;
|
|
1002
|
+
fallbackValuePos = lp;
|
|
1003
|
+
fallbackValue = g;
|
|
1004
|
+
fallbackSeriesID = seriesID;
|
|
1005
|
+
fallbackDataIndex = index;
|
|
1006
|
+
}
|
|
1007
|
+
} else if (hasMeaningfulValue && fallbackSeriesID === '') {
|
|
1008
|
+
// 좌표 없는 예외 케이스 — 첫 후보로만 등록
|
|
1009
|
+
fallbackType = series.type;
|
|
1010
|
+
fallbackLabel = ldata;
|
|
1011
|
+
fallbackValuePos = lp;
|
|
1012
|
+
fallbackValue = g;
|
|
1013
|
+
fallbackSeriesID = seriesID;
|
|
1014
|
+
fallbackDataIndex = index;
|
|
1015
|
+
}
|
|
930
1016
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1017
|
+
// hit 기반 선택: item.hit이 true이고 유효한 좌표가 있을 때만 고려
|
|
1018
|
+
if (item.hit && data.xp !== undefined && data.yp !== undefined) {
|
|
1019
|
+
const distance = (data.xp - offset[0]) ** 2 + (data.yp - offset[1]) ** 2;
|
|
1020
|
+
|
|
1021
|
+
if (item.directHit) {
|
|
1022
|
+
// 직접 박스 히트는 최우선. 여러 개이면 가장 가까운 것.
|
|
1023
|
+
if (!hasDirectHit || distance < hitDistance) {
|
|
1024
|
+
hitDistance = distance;
|
|
1025
|
+
hitType = series.type;
|
|
1026
|
+
hitLabel = ldata;
|
|
1027
|
+
hitValuePos = lp;
|
|
1028
|
+
hitValue = g;
|
|
1029
|
+
hitSeriesID = seriesID;
|
|
1030
|
+
hitDataIndex = index;
|
|
1031
|
+
}
|
|
1032
|
+
hasDirectHit = true;
|
|
1033
|
+
} else if (!hasDirectHit) {
|
|
1034
|
+
// directHit가 없을 때만 일반 hit 거리 비교 참여
|
|
1035
|
+
// (라인 근접 히트가 박스 직접 히트를 이기지 못하도록)
|
|
1036
|
+
if (distance < hitDistance) {
|
|
1037
|
+
hitDistance = distance;
|
|
1038
|
+
hitType = series.type;
|
|
1039
|
+
hitLabel = ldata;
|
|
1040
|
+
hitValuePos = lp;
|
|
1041
|
+
hitValue = g;
|
|
1042
|
+
hitSeriesID = seriesID;
|
|
1043
|
+
hitDataIndex = index;
|
|
1044
|
+
}
|
|
939
1045
|
}
|
|
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
1046
|
}
|
|
948
1047
|
}
|
|
949
1048
|
}
|
|
@@ -951,22 +1050,45 @@ const modules = {
|
|
|
951
1050
|
}
|
|
952
1051
|
}
|
|
953
1052
|
|
|
1053
|
+
// all-null 라벨인 경우 label/dataIndex 만 채워 반환 (sId='', value=0).
|
|
1054
|
+
if (
|
|
1055
|
+
disableNullLabelSnap
|
|
1056
|
+
&& hitSeriesID === ''
|
|
1057
|
+
&& fallbackSeriesID === ''
|
|
1058
|
+
&& resolvedDataIndex !== undefined
|
|
1059
|
+
&& resolvedDataIndex >= 0
|
|
1060
|
+
) {
|
|
1061
|
+
const refSeriesID = seriesIDs.find((sId) => {
|
|
1062
|
+
const s = this.seriesList[sId];
|
|
1063
|
+
return s?.show && s?.data?.length > 0;
|
|
1064
|
+
});
|
|
1065
|
+
const refPoint = refSeriesID
|
|
1066
|
+
? this.seriesList[refSeriesID].data?.[resolvedDataIndex]
|
|
1067
|
+
: null;
|
|
1068
|
+
if (refPoint) {
|
|
1069
|
+
fallbackLabel = isHorizontal ? refPoint.y : refPoint.x;
|
|
1070
|
+
fallbackDataIndex = resolvedDataIndex;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const hasHit = hitSeriesID !== '';
|
|
1075
|
+
|
|
954
1076
|
return {
|
|
955
|
-
type:
|
|
956
|
-
label:
|
|
957
|
-
pos:
|
|
958
|
-
value:
|
|
959
|
-
sId:
|
|
1077
|
+
type: hasHit ? hitType : fallbackType,
|
|
1078
|
+
label: hasHit ? hitLabel : fallbackLabel,
|
|
1079
|
+
pos: hasHit ? hitValuePos : fallbackValuePos,
|
|
1080
|
+
value: (hasHit ? hitValue : fallbackValue) ?? 0,
|
|
1081
|
+
sId: hasHit ? hitSeriesID : fallbackSeriesID,
|
|
960
1082
|
acc,
|
|
961
1083
|
useStack,
|
|
962
|
-
|
|
1084
|
+
dataIndex: hasHit ? hitDataIndex : fallbackDataIndex,
|
|
963
1085
|
};
|
|
964
1086
|
},
|
|
965
1087
|
|
|
966
1088
|
/**
|
|
967
1089
|
* @typedef {Object} LabelInfoResult
|
|
968
1090
|
* @property {number} labelIndex - 선택된 라벨의 인덱스
|
|
969
|
-
* @property {object} hitInfo - 해당 위치에서의 히트 정보 (
|
|
1091
|
+
* @property {object} hitInfo - 해당 위치에서의 히트 정보 (getHitItemByPosition 반환값)
|
|
970
1092
|
*/
|
|
971
1093
|
/**
|
|
972
1094
|
* Find label info by position x and y
|
|
@@ -1040,13 +1162,13 @@ const modules = {
|
|
|
1040
1162
|
offsetX = x;
|
|
1041
1163
|
}
|
|
1042
1164
|
|
|
1043
|
-
hitInfo = this.
|
|
1165
|
+
hitInfo = this.getHitItemByPosition(
|
|
1044
1166
|
[offsetX, y],
|
|
1045
1167
|
selectLabel?.useApproximateValue,
|
|
1046
1168
|
dataIndex,
|
|
1047
1169
|
true,
|
|
1048
1170
|
);
|
|
1049
|
-
labelIndex = hitInfo.
|
|
1171
|
+
labelIndex = hitInfo.dataIndex ?? -1;
|
|
1050
1172
|
}
|
|
1051
1173
|
|
|
1052
1174
|
return {
|
|
@@ -2,6 +2,7 @@ import { numberWithComma } from '@/common/utils';
|
|
|
2
2
|
import throttle from '@/common/utils.throttle';
|
|
3
3
|
import { cloneDeep, defaultsDeep, inRange, isEqual } from 'lodash-es';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
|
+
import Util from '../helpers/helpers.util';
|
|
5
6
|
|
|
6
7
|
const modules = {
|
|
7
8
|
/**
|
|
@@ -180,10 +181,10 @@ const modules = {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
const setSelectedItemInfo = () => {
|
|
183
|
-
const hitInfo = this.findHitItem(offset);
|
|
184
|
+
const hitInfo = this.findHitItem(offset, true);
|
|
184
185
|
|
|
185
186
|
// 실제 클릭된 아이템의 정보 추출 (hitId가 있으면 해당 아이템, 없으면 첫 번째 아이템)
|
|
186
|
-
const hitItemId = hitInfo.hitId
|
|
187
|
+
const hitItemId = hitInfo.hitId ?? Object.keys(hitInfo.items)[0];
|
|
187
188
|
const hitItem = hitInfo.items[hitItemId];
|
|
188
189
|
|
|
189
190
|
if (hitItem) {
|
|
@@ -196,8 +197,8 @@ const modules = {
|
|
|
196
197
|
};
|
|
197
198
|
|
|
198
199
|
const setSelectedLabelInfo = (targetAxis) => {
|
|
199
|
-
const hitInfo = this.findHitItem(offset);
|
|
200
|
-
const hitItemId = hitInfo.hitId
|
|
200
|
+
const hitInfo = this.findHitItem(offset, true);
|
|
201
|
+
const hitItemId = hitInfo.hitId ?? Object.keys(hitInfo.items)[0];
|
|
201
202
|
const hitItem = hitInfo.items[hitItemId];
|
|
202
203
|
|
|
203
204
|
const {
|
|
@@ -220,8 +221,8 @@ const modules = {
|
|
|
220
221
|
};
|
|
221
222
|
|
|
222
223
|
const setSelectedSeriesInfo = () => {
|
|
223
|
-
const hitInfo = this.findHitItem(offset);
|
|
224
|
-
const hitItemId = hitInfo.hitId
|
|
224
|
+
const hitInfo = this.findHitItem(offset, true);
|
|
225
|
+
const hitItemId = hitInfo.hitId ?? Object.keys(hitInfo.items)[0];
|
|
225
226
|
const hitItem = hitInfo.items[hitItemId];
|
|
226
227
|
|
|
227
228
|
if (hitItemId !== null) {
|
|
@@ -294,13 +295,13 @@ const modules = {
|
|
|
294
295
|
const useSelectSeries = selectSeriesOpt?.use && selectSeriesOpt?.useClick;
|
|
295
296
|
|
|
296
297
|
const setSelectedItemInfo = () => {
|
|
297
|
-
const hitInfo = this.
|
|
298
|
+
const hitInfo = this.getHitItemByPosition(offset, false, undefined, false, true);
|
|
298
299
|
|
|
299
300
|
({
|
|
300
301
|
label: args.label,
|
|
301
302
|
value: args.value,
|
|
302
303
|
sId: args.seriesId,
|
|
303
|
-
|
|
304
|
+
dataIndex: args.dataIndex,
|
|
304
305
|
acc: args.acc,
|
|
305
306
|
} = hitInfo);
|
|
306
307
|
|
|
@@ -308,7 +309,7 @@ const modules = {
|
|
|
308
309
|
args.selected = {
|
|
309
310
|
eventTarget: 'item',
|
|
310
311
|
seriesId: this.isDeselectItem(hitInfo) ? null : hitInfo.sId,
|
|
311
|
-
dataIndex: this.isDeselectItem(hitInfo) ? null : hitInfo.
|
|
312
|
+
dataIndex: this.isDeselectItem(hitInfo) ? null : hitInfo.dataIndex,
|
|
312
313
|
};
|
|
313
314
|
}
|
|
314
315
|
};
|
|
@@ -337,8 +338,8 @@ const modules = {
|
|
|
337
338
|
};
|
|
338
339
|
|
|
339
340
|
const setSelectedSeriesInfo = () => {
|
|
340
|
-
const hitInfo = this.findHitItem(offset);
|
|
341
|
-
const hitItemId = hitInfo.hitId
|
|
341
|
+
const hitInfo = this.findHitItem(offset, true);
|
|
342
|
+
const hitItemId = hitInfo.hitId ?? Object.keys(hitInfo.items)[0];
|
|
342
343
|
const hitItem = hitInfo.items[hitItemId];
|
|
343
344
|
|
|
344
345
|
if (hitItemId !== null) {
|
|
@@ -904,12 +905,14 @@ const modules = {
|
|
|
904
905
|
* maxHighlight: [string, number] | null,
|
|
905
906
|
* }} hit item information
|
|
906
907
|
*/
|
|
907
|
-
findHitItem(offset) {
|
|
908
|
+
findHitItem(offset, disableNullLabelSnap = false) {
|
|
908
909
|
const sIds = Object.keys(this.seriesList);
|
|
909
910
|
const items = {};
|
|
910
911
|
const isHorizontal = !!this.options.horizontal;
|
|
911
912
|
const ctx = this.tooltipCtx;
|
|
912
913
|
|
|
914
|
+
const [cx, cy] = offset;
|
|
915
|
+
|
|
913
916
|
let hitId = null;
|
|
914
917
|
let maxs = '';
|
|
915
918
|
let maxsw = 0;
|
|
@@ -917,9 +920,14 @@ const modules = {
|
|
|
917
920
|
let maxg = null;
|
|
918
921
|
let maxSID = null;
|
|
919
922
|
let minDistance = Infinity;
|
|
923
|
+
// directHit 가 하나라도 있으면 일반 hit 는 hitId 후보에서 배제.
|
|
924
|
+
let hasDirectHit = false;
|
|
925
|
+
// hit 이 없을 때 거리 기반으로 선택할 fallback (기존 "첫 시리즈 고정" 대체).
|
|
926
|
+
let fallbackId = null;
|
|
927
|
+
let fallbackDistance = Infinity;
|
|
920
928
|
|
|
921
929
|
// 1. 먼저 공통으로 사용할 데이터 인덱스 결정
|
|
922
|
-
const targetDataIndex = this.findClosestDataIndex(offset, sIds);
|
|
930
|
+
const targetDataIndex = this.findClosestDataIndex(offset, sIds, disableNullLabelSnap);
|
|
923
931
|
|
|
924
932
|
if (targetDataIndex === -1 && !this.isNotUseIndicator()) {
|
|
925
933
|
return { items, hitId, maxTip: [maxs, maxv], maxHighlight: null };
|
|
@@ -986,24 +994,69 @@ const modules = {
|
|
|
986
994
|
maxSID = sId;
|
|
987
995
|
}
|
|
988
996
|
|
|
989
|
-
//
|
|
997
|
+
// hit 기반 선택: directHit 최우선, 그 외 일반 hit 는 directHit 없을 때만.
|
|
990
998
|
if (item.hit && item.data.xp !== undefined && item.data.yp !== undefined) {
|
|
991
999
|
const distance = (item.data.xp - offset[0]) ** 2
|
|
992
1000
|
+ (item.data.yp - offset[1]) ** 2;
|
|
993
1001
|
|
|
994
|
-
if (
|
|
1002
|
+
if (item.directHit) {
|
|
1003
|
+
if (!hasDirectHit || distance < minDistance) {
|
|
1004
|
+
minDistance = distance;
|
|
1005
|
+
hitId = sId;
|
|
1006
|
+
}
|
|
1007
|
+
hasDirectHit = true;
|
|
1008
|
+
} else if (!hasDirectHit && distance < minDistance) {
|
|
995
1009
|
minDistance = distance;
|
|
996
1010
|
hitId = sId;
|
|
997
1011
|
}
|
|
998
1012
|
}
|
|
1013
|
+
|
|
1014
|
+
// fallback 후보: hit 여부와 무관하게 거리가 가장 가까운 시리즈.
|
|
1015
|
+
// 참고: 이 블록은 outer `if (gdata !== null && gdata !== undefined)` 안에 있어서
|
|
1016
|
+
// 값이 null 인 시리즈는 items 수집 단계에서 이미 걸러진 상태. 별도 null 값 가드 불필요.
|
|
1017
|
+
if (item.data.xp !== undefined && item.data.yp !== undefined
|
|
1018
|
+
&& item.data.xp !== null && item.data.yp !== null) {
|
|
1019
|
+
const fbDistance = Util.calcBoxDistance(item.data, cx, cy);
|
|
1020
|
+
if (fbDistance < fallbackDistance) {
|
|
1021
|
+
fallbackDistance = fbDistance;
|
|
1022
|
+
fallbackId = sId;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
999
1025
|
}
|
|
1000
1026
|
}
|
|
1001
1027
|
}
|
|
1002
1028
|
}
|
|
1003
1029
|
|
|
1004
|
-
|
|
1030
|
+
// hit 없으면 거리 기반 fallback, 그것도 없으면 items 첫 키(항상 비어있을 가능성 방어).
|
|
1031
|
+
if (hitId === null) {
|
|
1032
|
+
hitId = fallbackId !== null ? fallbackId : Object.keys(items)[0];
|
|
1033
|
+
}
|
|
1005
1034
|
const maxHighlight = maxg !== null ? [maxSID, maxg] : null;
|
|
1006
1035
|
|
|
1036
|
+
// all-null 라벨인 경우 synthetic items[''] 로 label/index 만 채워 전달.
|
|
1037
|
+
if (disableNullLabelSnap
|
|
1038
|
+
&& Object.keys(items).length === 0
|
|
1039
|
+
&& targetDataIndex !== -1) {
|
|
1040
|
+
const refSeriesID = sIds.find((sId) => {
|
|
1041
|
+
const s = this.seriesList[sId];
|
|
1042
|
+
return s?.show && s?.data?.length > 0;
|
|
1043
|
+
});
|
|
1044
|
+
const refPoint = refSeriesID
|
|
1045
|
+
? this.seriesList[refSeriesID].data?.[targetDataIndex]
|
|
1046
|
+
: null;
|
|
1047
|
+
if (refPoint) {
|
|
1048
|
+
items[''] = {
|
|
1049
|
+
id: '',
|
|
1050
|
+
name: '',
|
|
1051
|
+
label: isHorizontal ? refPoint.y : refPoint.x,
|
|
1052
|
+
index: targetDataIndex,
|
|
1053
|
+
axis: { x: 0, y: 0 },
|
|
1054
|
+
data: { o: undefined, x: refPoint.x, y: refPoint.y },
|
|
1055
|
+
};
|
|
1056
|
+
hitId = '';
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1007
1060
|
return { items, hitId, maxTip: [maxs, maxv], maxHighlight };
|
|
1008
1061
|
},
|
|
1009
1062
|
|
|
@@ -1013,7 +1066,7 @@ const modules = {
|
|
|
1013
1066
|
* @param {array} sIds series IDs
|
|
1014
1067
|
* @returns {number} closest data index
|
|
1015
1068
|
*/
|
|
1016
|
-
findClosestDataIndex(offset, sIds) {
|
|
1069
|
+
findClosestDataIndex(offset, sIds, disableNullLabelSnap = false) {
|
|
1017
1070
|
const [xp, yp] = offset;
|
|
1018
1071
|
const isHorizontal = !!this.options.horizontal;
|
|
1019
1072
|
const mousePos = isHorizontal ? yp : xp;
|
|
@@ -1061,10 +1114,9 @@ const modules = {
|
|
|
1061
1114
|
let closestDistance = Infinity;
|
|
1062
1115
|
let closestIndex = -1;
|
|
1063
1116
|
|
|
1064
|
-
// 각 라벨에서 가장 가까운 것 찾기
|
|
1117
|
+
// 각 라벨에서 가장 가까운 것 찾기 (disableNullLabelSnap=true 면 all-null 라벨도 후보)
|
|
1065
1118
|
for (let i = 0; i < referenceData.length; i++) {
|
|
1066
|
-
|
|
1067
|
-
const hasValidData = sIds.some((sId) => {
|
|
1119
|
+
const hasValidData = disableNullLabelSnap || sIds.some((sId) => {
|
|
1068
1120
|
const series = this.seriesList[sId];
|
|
1069
1121
|
return series?.show && series.data?.[i]?.o !== null && series.data?.[i]?.o !== undefined;
|
|
1070
1122
|
});
|
|
@@ -1622,9 +1674,9 @@ const modules = {
|
|
|
1622
1674
|
*/
|
|
1623
1675
|
isDeselectItem(hitInfo) {
|
|
1624
1676
|
return this.options.selectItem.useDeselectItem
|
|
1625
|
-
&& hitInfo?.
|
|
1677
|
+
&& hitInfo?.dataIndex === this.defaultSelectItemInfo?.dataIndex
|
|
1626
1678
|
&& hitInfo?.sId === this.defaultSelectItemInfo?.seriesID
|
|
1627
|
-
&& !isNaN(hitInfo?.
|
|
1679
|
+
&& !isNaN(hitInfo?.dataIndex);
|
|
1628
1680
|
},
|
|
1629
1681
|
|
|
1630
1682
|
/**
|
|
@@ -113,7 +113,15 @@ const module = {
|
|
|
113
113
|
: this.options.axesY?.[0]?.scrollbar?.resetPosition;
|
|
114
114
|
|
|
115
115
|
if (isUpdateAxesRange) {
|
|
116
|
-
|
|
116
|
+
const newOptRange = newOpt?.[0]?.range;
|
|
117
|
+
const currentRange = this.scrollbar[dir].range;
|
|
118
|
+
if (!isResetPosition && newOptRange?.length && currentRange?.length) {
|
|
119
|
+
// 리사이즈 등으로 range 크기만 변경된 경우, 현재 스크롤 위치(min)를 유지하고 크기만 조정
|
|
120
|
+
const newSize = newOptRange[1] - newOptRange[0];
|
|
121
|
+
this.scrollbar[dir].range = [currentRange[0], currentRange[0] + newSize];
|
|
122
|
+
} else {
|
|
123
|
+
this.scrollbar[dir].range = newOptRange?.length ? [...newOptRange] : null;
|
|
124
|
+
}
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
if (isResetPosition || updateData) {
|