evui 3.4.154 → 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 +267 -132
- package/dist/evui.common.js.map +1 -1
- package/dist/evui.umd.js +267 -132
- 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 +19 -10
- package/src/components/chart/element/element.bar.js +23 -17
- package/src/components/chart/element/element.line.js +26 -0
- package/src/components/chart/element/element.tip.js +2 -2
- package/src/components/chart/model/model.store.js +90 -51
- package/src/components/chart/plugins/plugins.interaction.js +21 -7
- package/src/components/chart/plugins/plugins.scrollbar.js +57 -26
package/package.json
CHANGED
|
@@ -268,7 +268,13 @@ class EvChart {
|
|
|
268
268
|
|
|
269
269
|
this.axesRange = this.getAxesRange();
|
|
270
270
|
this.labelOffset = this.getLabelOffset();
|
|
271
|
+
|
|
271
272
|
this.labelRange = this.getAxesLabelRange();
|
|
273
|
+
|
|
274
|
+
if (this.scrollbar?.x?.use || this.scrollbar?.y?.use) {
|
|
275
|
+
this.updateScrollbarPosition();
|
|
276
|
+
}
|
|
277
|
+
|
|
272
278
|
this.axesSteps = this.calculateSteps();
|
|
273
279
|
|
|
274
280
|
this.adjustXAndYAxisWidth();
|
|
@@ -276,10 +282,6 @@ class EvChart {
|
|
|
276
282
|
this.drawAxis(hitInfo);
|
|
277
283
|
this.drawSeries(hitInfo);
|
|
278
284
|
|
|
279
|
-
if (this.scrollbar?.x?.use || this.scrollbar?.y?.use) {
|
|
280
|
-
this.updateScrollbarPosition();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
285
|
this.drawTip();
|
|
284
286
|
|
|
285
287
|
if (
|
|
@@ -452,10 +454,10 @@ class EvChart {
|
|
|
452
454
|
const lastHitInfo = this.lastHitInfo;
|
|
453
455
|
const defaultSelectInfo = this.defaultSelectItemInfo;
|
|
454
456
|
|
|
455
|
-
if (lastHitInfo?.
|
|
457
|
+
if (lastHitInfo?.dataIndex || lastHitInfo?.dataIndex === 0) {
|
|
456
458
|
selectInfo = {
|
|
457
459
|
seriesID: lastHitInfo.sId,
|
|
458
|
-
dataIndex: lastHitInfo.
|
|
460
|
+
dataIndex: lastHitInfo.dataIndex,
|
|
459
461
|
};
|
|
460
462
|
} else if (defaultSelectInfo?.dataIndex || defaultSelectInfo?.dataIndex === 0) {
|
|
461
463
|
selectInfo = { ...defaultSelectInfo };
|
|
@@ -856,12 +858,18 @@ class EvChart {
|
|
|
856
858
|
* @param {boolean} updateData is update data
|
|
857
859
|
* @returns {undefined}
|
|
858
860
|
*/
|
|
859
|
-
updateScrollbar(updateData) {
|
|
860
|
-
|
|
861
|
+
updateScrollbar(updateData, updateByScrollbar) {
|
|
862
|
+
const isForceUpdate = updateByScrollbar || updateData;
|
|
863
|
+
const xUse = this.options.axesX?.[0]?.scrollbar?.use ?? false;
|
|
864
|
+
const yUse = this.options.axesY?.[0]?.scrollbar?.use ?? false;
|
|
865
|
+
const prevXUse = this.scrollbar?.x?.use ?? false;
|
|
866
|
+
const prevYUse = this.scrollbar?.y?.use ?? false;
|
|
867
|
+
|
|
868
|
+
if (xUse !== prevXUse || xUse || (isForceUpdate && xUse)) {
|
|
861
869
|
this.updateScrollbarInfo('x', updateData);
|
|
862
870
|
}
|
|
863
871
|
|
|
864
|
-
if (
|
|
872
|
+
if (yUse !== prevYUse || yUse || (isForceUpdate && yUse)) {
|
|
865
873
|
this.updateScrollbarInfo('y', updateData);
|
|
866
874
|
}
|
|
867
875
|
}
|
|
@@ -886,13 +894,14 @@ class EvChart {
|
|
|
886
894
|
updateData,
|
|
887
895
|
updateTooltip,
|
|
888
896
|
lightUpdate,
|
|
897
|
+
updateByScrollbar,
|
|
889
898
|
} = updateInfo;
|
|
890
899
|
|
|
891
900
|
if (!this.isInit) {
|
|
892
901
|
return;
|
|
893
902
|
}
|
|
894
903
|
|
|
895
|
-
this.updateScrollbar(updateData);
|
|
904
|
+
this.updateScrollbar(updateData, updateByScrollbar);
|
|
896
905
|
|
|
897
906
|
this.resetProps();
|
|
898
907
|
|
|
@@ -115,13 +115,12 @@ class Bar {
|
|
|
115
115
|
const startIndex = truthyNumber(minIndex) ? minIndex : 0;
|
|
116
116
|
const endIndex = truthyNumber(maxIndex) ? maxIndex : this.data.length - 1;
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
this.visibleStartIndex = startIndex;
|
|
119
|
+
|
|
119
120
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
120
|
-
const screenIndex = i - startIndex;
|
|
121
|
-
const item = this.data[i];
|
|
121
|
+
const screenIndex = i - startIndex;
|
|
122
|
+
const item = this.data[i];
|
|
122
123
|
if (item) {
|
|
123
|
-
// 스크롤 offset(minIndex)만큼 보정해서 그리기
|
|
124
|
-
|
|
125
124
|
const categoryPoint = isHorizontal
|
|
126
125
|
? ysp - (cArea * screenIndex) - cPad
|
|
127
126
|
: xsp + (cArea * screenIndex) + cPad;
|
|
@@ -214,11 +213,7 @@ class Bar {
|
|
|
214
213
|
item.yp = y; // eslint-disable-line
|
|
215
214
|
item.w = w; // eslint-disable-line
|
|
216
215
|
item.h = isHorizontal ? -h : h; // eslint-disable-line
|
|
217
|
-
item.index = i;
|
|
218
|
-
|
|
219
|
-
// 검색(hitInfo) 로직은 this.data[0..filteredCount-1] 범위만 검사하므로,
|
|
220
|
-
// 현재 화면에 그린 항목을 배열 앞쪽으로 매핑해준다.
|
|
221
|
-
this.data[screenIndex] = item;
|
|
216
|
+
item.index = i;
|
|
222
217
|
}
|
|
223
218
|
}
|
|
224
219
|
}
|
|
@@ -294,13 +289,21 @@ class Bar {
|
|
|
294
289
|
*/
|
|
295
290
|
findGraphData(offset, isHorizontal, dataIndex, useIndicatorOnLabel) {
|
|
296
291
|
if (typeof dataIndex === 'number' && this.show && useIndicatorOnLabel) {
|
|
297
|
-
const
|
|
292
|
+
const barData = this.data;
|
|
298
293
|
const item = { data: null, hit: false, color: this.color };
|
|
299
294
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
295
|
+
// dataIndex를 현재 화면에 보이는 범위로 clamp하여 stale xp/yp 참조 방지
|
|
296
|
+
const visStart = this.visibleStartIndex ?? 0;
|
|
297
|
+
const visEnd = visStart + (this.filteredCount ?? barData.length) - 1;
|
|
298
|
+
const clampedIndex = Math.max(visStart, Math.min(dataIndex, visEnd));
|
|
299
|
+
|
|
300
|
+
if (barData[clampedIndex]) {
|
|
301
|
+
item.data = barData[clampedIndex];
|
|
302
|
+
item.index = clampedIndex;
|
|
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;
|
|
@@ -326,10 +329,11 @@ class Bar {
|
|
|
326
329
|
const [xp, yp] = offset;
|
|
327
330
|
const item = { data: null, hit: false, color: this.color };
|
|
328
331
|
const gdata = this.data;
|
|
332
|
+
const startIdx = this.visibleStartIndex ?? 0;
|
|
329
333
|
const totalCount = this.filteredCount ?? gdata.length;
|
|
330
334
|
|
|
331
|
-
let s =
|
|
332
|
-
let e = totalCount - 1;
|
|
335
|
+
let s = startIdx;
|
|
336
|
+
let e = startIdx + totalCount - 1;
|
|
333
337
|
|
|
334
338
|
while (s <= e) {
|
|
335
339
|
const m = Math.floor((s + e) / 2);
|
|
@@ -346,6 +350,8 @@ class Bar {
|
|
|
346
350
|
item.data = barData;
|
|
347
351
|
item.index = barData.index;
|
|
348
352
|
item.hit = this.isPointInBar(offset, barData);
|
|
353
|
+
// bar 박스 내부 클릭은 "직접 박스 히트"로 표시 (findHitItem 우선순위용).
|
|
354
|
+
item.directHit = item.hit;
|
|
349
355
|
return item;
|
|
350
356
|
}
|
|
351
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;
|
|
@@ -339,7 +339,7 @@ const modules = {
|
|
|
339
339
|
let labelCount = labelAxes.labels.length;
|
|
340
340
|
if (scrollbarOpt?.use) {
|
|
341
341
|
const { range, interval, type } = scrollbarOpt;
|
|
342
|
-
const [min, max] = range;
|
|
342
|
+
const [min, max] = range ?? [];
|
|
343
343
|
if (truthyNumber(min) && truthyNumber(max)) {
|
|
344
344
|
labelCount = Math.floor((+max - +min) / interval) + 1;
|
|
345
345
|
startIndex = type === 'step' ? min : labelAxes.labels.findIndex(v => v === +min);
|
|
@@ -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
|
/**
|
|
@@ -29,11 +29,15 @@ const module = {
|
|
|
29
29
|
scrollbarOpt[key] = merged[key];
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
if (scrollbarOpt.resetPosition) {
|
|
33
|
+
scrollbarOpt.range = axisOpt?.[0]?.range?.length ? [...axisOpt?.[0]?.range] : null;
|
|
34
|
+
this.resetScrollbarSavedPositions(dir);
|
|
35
|
+
}
|
|
33
36
|
|
|
34
37
|
if (!scrollbarOpt.isInit) {
|
|
35
38
|
scrollbarOpt.type = axisOpt?.[0]?.type;
|
|
36
39
|
scrollbarOpt.range = axisOpt?.[0]?.range?.length ? [...axisOpt?.[0]?.range] : null;
|
|
40
|
+
this.resetScrollbarSavedPositions(dir);
|
|
37
41
|
|
|
38
42
|
this.initScrollbarRange(dir);
|
|
39
43
|
this.createScrollbarLayout(dir);
|
|
@@ -104,16 +108,18 @@ const module = {
|
|
|
104
108
|
const axisOpt = dir === 'x' ? this.axesX : this.axesY;
|
|
105
109
|
const isUpdateAxesRange = !isEqual(newOpt?.[0]?.range, axisOpt?.[0]?.range);
|
|
106
110
|
if (isUpdateAxesRange || updateData) {
|
|
107
|
-
const isResetPosition = dir === 'x'
|
|
108
|
-
|
|
111
|
+
const isResetPosition = dir === 'x'
|
|
112
|
+
? this.options.axesX?.[0]?.scrollbar?.resetPosition
|
|
113
|
+
: this.options.axesY?.[0]?.scrollbar?.resetPosition;
|
|
114
|
+
|
|
115
|
+
if (isUpdateAxesRange) {
|
|
109
116
|
this.scrollbar[dir].range = newOpt?.[0]?.range?.length ? [...newOpt?.[0]?.range] : null;
|
|
110
|
-
// range가 업데이트되면 저장된 스크롤 위치를 초기화
|
|
111
|
-
delete this.scrollbar[dir].savedPosition;
|
|
112
|
-
} else if (updateData) {
|
|
113
|
-
// 데이터가 업데이트되면 저장된 픽셀 위치는 더 이상 유효하지 않으므로 삭제하여
|
|
114
|
-
// 논리적 범위에 따라 다시 계산하도록 합니다.
|
|
115
|
-
delete this.scrollbar[dir].savedPosition;
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
if (isResetPosition || updateData) {
|
|
120
|
+
this.resetScrollbarSavedPositions(dir);
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
this.initScrollbarRange(dir);
|
|
118
124
|
}
|
|
119
125
|
this.scrollbar[dir].use = !!newOpt?.[0].scrollbar?.use;
|
|
@@ -244,16 +250,29 @@ const module = {
|
|
|
244
250
|
const buttonSize = scrollbarOpt.showButton ? scrollHeight : 0;
|
|
245
251
|
const trackSize = fullSize - (buttonSize * 2);
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
let savedThumbPosition = null;
|
|
249
|
-
if (preservePosition && scrollbarOpt.savedPosition !== undefined) {
|
|
250
|
-
savedThumbPosition = scrollbarOpt.savedPosition;
|
|
251
|
-
}
|
|
253
|
+
const thumbSize = this.getScrollbarThumbSize(dir, trackSize);
|
|
252
254
|
|
|
253
|
-
|
|
255
|
+
// 비율로 저장된 위치가 있으면 새 track 크기에 맞게 복원
|
|
256
|
+
if (preservePosition && scrollbarOpt.savedPositionRatio !== undefined) {
|
|
257
|
+
const maxPosition = Math.max(0, trackSize - thumbSize.size);
|
|
258
|
+
if (scrollbarOpt.savedAtStart) {
|
|
259
|
+
thumbSize.position = 0;
|
|
260
|
+
} else if (scrollbarOpt.savedAtEnd) {
|
|
261
|
+
thumbSize.position = maxPosition;
|
|
262
|
+
} else {
|
|
263
|
+
thumbSize.position = Math.min(scrollbarOpt.savedPositionRatio * trackSize, maxPosition);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
254
266
|
|
|
255
|
-
//
|
|
256
|
-
|
|
267
|
+
// 위치를 비율 및 처음/끝 고정 여부로 저장
|
|
268
|
+
// currentMaxPosition === 0 (thumbSize >= trackSize) 인 경우 저장하지 않음
|
|
269
|
+
// → trackSize가 다시 커졌을 때 이전 savedAtEnd/savedAtStart/savedPositionRatio 상태를 유지
|
|
270
|
+
const currentMaxPosition = Math.max(0, trackSize - thumbSize.size);
|
|
271
|
+
if (currentMaxPosition > 0) {
|
|
272
|
+
scrollbarOpt.savedPositionRatio = thumbSize.position / trackSize;
|
|
273
|
+
scrollbarOpt.savedAtStart = thumbSize.position <= 0;
|
|
274
|
+
scrollbarOpt.savedAtEnd = thumbSize.position >= currentMaxPosition;
|
|
275
|
+
}
|
|
257
276
|
|
|
258
277
|
let scrollbarStyle = 'display: block;';
|
|
259
278
|
let scrollbarTrackStyle;
|
|
@@ -328,9 +347,8 @@ const module = {
|
|
|
328
347
|
* get scrollbar thumb size
|
|
329
348
|
* @param dir axis direction ('x' | 'y')
|
|
330
349
|
* @param trackSize scrollbar track size
|
|
331
|
-
* @param savedThumbPosition 기존 위치를 보존해야 하는 경우 저장된 위치
|
|
332
350
|
*/
|
|
333
|
-
getScrollbarThumbSize(dir, trackSize
|
|
351
|
+
getScrollbarThumbSize(dir, trackSize) {
|
|
334
352
|
const scrollbarOpt = this.scrollbar[dir];
|
|
335
353
|
const [min, max] = scrollbarOpt.range;
|
|
336
354
|
const axesType = scrollbarOpt.type;
|
|
@@ -374,11 +392,6 @@ const module = {
|
|
|
374
392
|
scrollbarOpt.steps = steps;
|
|
375
393
|
scrollbarOpt.interval = interval;
|
|
376
394
|
|
|
377
|
-
// 기존 위치를 보존해야 하는 경우 저장된 위치를 사용
|
|
378
|
-
if (savedThumbPosition !== null) {
|
|
379
|
-
thumbPosition = savedThumbPosition;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
395
|
return {
|
|
383
396
|
size: thumbSize,
|
|
384
397
|
position: thumbPosition,
|
|
@@ -438,16 +451,32 @@ const module = {
|
|
|
438
451
|
scrollbarOpt.range = [minValue, maxValue];
|
|
439
452
|
|
|
440
453
|
// 사용자가 스크롤할 때는 저장된 위치를 초기화
|
|
441
|
-
|
|
454
|
+
this.resetScrollbarSavedPositions(dir);
|
|
442
455
|
|
|
443
456
|
this.update({
|
|
444
457
|
updateSeries: false,
|
|
445
458
|
updateSelTip: { update: false, keepDomain: false },
|
|
446
459
|
lightUpdate: minValue > 1,
|
|
460
|
+
updateByScrollbar: true,
|
|
447
461
|
});
|
|
448
462
|
}
|
|
449
463
|
},
|
|
450
464
|
|
|
465
|
+
/**
|
|
466
|
+
* reset scrollbar saved positions
|
|
467
|
+
* @param dir axis direction ('x' | 'y')
|
|
468
|
+
*/
|
|
469
|
+
resetScrollbarSavedPositions(dir) {
|
|
470
|
+
const scrollbarOpt = this.scrollbar[dir];
|
|
471
|
+
if (!scrollbarOpt) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
delete scrollbarOpt.savedPositionRatio;
|
|
476
|
+
delete scrollbarOpt.savedAtStart;
|
|
477
|
+
delete scrollbarOpt.savedAtEnd;
|
|
478
|
+
},
|
|
479
|
+
|
|
451
480
|
/**
|
|
452
481
|
* create scroll event
|
|
453
482
|
*/
|
|
@@ -662,11 +691,13 @@ const module = {
|
|
|
662
691
|
this.scrollbar[dir].range = [movedMin, movedMax];
|
|
663
692
|
|
|
664
693
|
// 사용자가 드래그로 스크롤할 때는 저장된 위치를 초기화
|
|
665
|
-
|
|
694
|
+
this.resetScrollbarSavedPositions(dir);
|
|
666
695
|
|
|
667
696
|
this.update({
|
|
668
697
|
updateSeries: false,
|
|
669
698
|
updateSelTip: { update: false, keepDomain: false },
|
|
699
|
+
lightUpdate: movedMin > 1,
|
|
700
|
+
updateByScrollbar: true,
|
|
670
701
|
});
|
|
671
702
|
},
|
|
672
703
|
|