evui 3.4.203 → 3.4.204
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 +741 -225
- package/dist/evui.common.js.map +1 -1
- package/dist/evui.umd.js +741 -225
- 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/element/element.bar.js +93 -80
- package/src/components/chart/element/element.line.js +184 -61
- package/src/components/chart/plugins/plugins.interaction.js +227 -6
- package/src/components/chart/plugins/plugins.tooltip.js +73 -3
package/package.json
CHANGED
|
@@ -61,13 +61,9 @@ class Bar {
|
|
|
61
61
|
const minmaxY = axesSteps.y[this.yAxisIndex];
|
|
62
62
|
|
|
63
63
|
let totalCount = this.data.length;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[minIndex, maxIndex] = [minmaxY.minIndex, minmaxY.maxIndex];
|
|
68
|
-
} else {
|
|
69
|
-
[minIndex, maxIndex] = [minmaxX.minIndex, minmaxX.maxIndex];
|
|
70
|
-
}
|
|
64
|
+
const [minIndex, maxIndex] = isHorizontal
|
|
65
|
+
? [minmaxY.minIndex, minmaxY.maxIndex]
|
|
66
|
+
: [minmaxX.minIndex, minmaxX.maxIndex];
|
|
71
67
|
|
|
72
68
|
// minIndex, maxIndex가 유효하면 실제 그릴 데이터 개수로 보정
|
|
73
69
|
if (truthyNumber(minIndex) && truthyNumber(maxIndex)) {
|
|
@@ -103,17 +99,7 @@ class Bar {
|
|
|
103
99
|
bArea = cArea > (cPad * 2) ? (cArea - (cPad * 2)) : cArea;
|
|
104
100
|
bArea = this.isExistGrp ? bArea : bArea / showSeriesCount;
|
|
105
101
|
|
|
106
|
-
const
|
|
107
|
-
if (typeof thickness === 'string' && /[0-9]+px/.test(thickness)) {
|
|
108
|
-
return Math.min(bArea, Number(thickness.replace('px', '')));
|
|
109
|
-
}
|
|
110
|
-
if (typeof thickness === 'number' && thickness <= 1 && thickness >= 0) {
|
|
111
|
-
return Math.ceil(bArea * thickness);
|
|
112
|
-
}
|
|
113
|
-
return bArea;
|
|
114
|
-
};
|
|
115
|
-
const size = getSize();
|
|
116
|
-
|
|
102
|
+
const size = this.calculateBarSize(thickness, bArea);
|
|
117
103
|
w = isHorizontal ? null : size;
|
|
118
104
|
h = isHorizontal ? size : null;
|
|
119
105
|
|
|
@@ -141,12 +127,10 @@ class Bar {
|
|
|
141
127
|
const item = this.data[i]; // 실제 데이터 인덱스에 해당하는 항목
|
|
142
128
|
if (item) {
|
|
143
129
|
// 스크롤 offset(minIndex)만큼 보정해서 그리기
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
categoryPoint = xsp + (cArea * (screenIndex)) + cPad;
|
|
149
|
-
}
|
|
130
|
+
|
|
131
|
+
const categoryPoint = isHorizontal
|
|
132
|
+
? ysp - (cArea * screenIndex) - cPad
|
|
133
|
+
: xsp + (cArea * screenIndex) + cPad;
|
|
150
134
|
|
|
151
135
|
// 기본 위치 설정
|
|
152
136
|
if (isHorizontal) {
|
|
@@ -328,10 +312,25 @@ class Bar {
|
|
|
328
312
|
* Find graph item
|
|
329
313
|
* @param {array} offset mouse position
|
|
330
314
|
* @param {boolean} isHorizontal determines if a horizontal option's value
|
|
315
|
+
* @param {number} dataIndex selected label data index
|
|
316
|
+
* @param {boolean} useIndicatorOnLabel
|
|
331
317
|
*
|
|
332
318
|
* @returns {object} graph item
|
|
333
319
|
*/
|
|
334
|
-
findGraphData(offset, isHorizontal) {
|
|
320
|
+
findGraphData(offset, isHorizontal, dataIndex, useIndicatorOnLabel) {
|
|
321
|
+
if (typeof dataIndex === 'number' && this.show && useIndicatorOnLabel) {
|
|
322
|
+
const gdata = this.data;
|
|
323
|
+
const item = { data: null, hit: false, color: this.color };
|
|
324
|
+
|
|
325
|
+
if (gdata[dataIndex]) {
|
|
326
|
+
item.data = gdata[dataIndex];
|
|
327
|
+
item.index = dataIndex;
|
|
328
|
+
item.hit = this.isPointInBar(offset, gdata[dataIndex]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return item;
|
|
332
|
+
}
|
|
333
|
+
|
|
335
334
|
return isHorizontal ? this.findGraphRangeCount(offset) : this.findGraphRange(offset);
|
|
336
335
|
}
|
|
337
336
|
|
|
@@ -341,12 +340,17 @@ class Bar {
|
|
|
341
340
|
*
|
|
342
341
|
* @returns {object} graph item
|
|
343
342
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Binary search for finding graph item
|
|
345
|
+
* @private
|
|
346
|
+
* @param {array} offset - mouse position
|
|
347
|
+
* @param {boolean} isHorizontal - search orientation
|
|
348
|
+
* @returns {object} graph item
|
|
349
|
+
*/
|
|
350
|
+
binarySearchBar(offset, isHorizontal) {
|
|
351
|
+
const [xp, yp] = offset;
|
|
347
352
|
const item = { data: null, hit: false, color: this.color };
|
|
348
353
|
const gdata = this.data;
|
|
349
|
-
|
|
350
354
|
const totalCount = this.filteredCount ?? gdata.length;
|
|
351
355
|
|
|
352
356
|
let s = 0;
|
|
@@ -354,20 +358,27 @@ class Bar {
|
|
|
354
358
|
|
|
355
359
|
while (s <= e) {
|
|
356
360
|
const m = Math.floor((s + e) / 2);
|
|
357
|
-
const
|
|
358
|
-
const sy =
|
|
359
|
-
const ex = sx +
|
|
360
|
-
const ey = sy +
|
|
361
|
+
const barData = gdata[m];
|
|
362
|
+
const { xp: sx, yp: sy, w, h } = barData;
|
|
363
|
+
const ex = sx + w;
|
|
364
|
+
const ey = sy + h;
|
|
365
|
+
|
|
366
|
+
const inRange = isHorizontal
|
|
367
|
+
? ((ey <= yp) && (yp <= sy))
|
|
368
|
+
: ((sx <= xp) && (xp <= ex));
|
|
369
|
+
|
|
370
|
+
if (inRange) {
|
|
371
|
+
item.data = barData;
|
|
372
|
+
item.index = barData.index;
|
|
373
|
+
item.hit = this.isPointInBar(offset, barData);
|
|
374
|
+
return item;
|
|
375
|
+
}
|
|
361
376
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
const shouldGoRight = isHorizontal
|
|
378
|
+
? (!(ey < yp))
|
|
379
|
+
: (sx + 4 < xp);
|
|
365
380
|
|
|
366
|
-
|
|
367
|
-
item.hit = true;
|
|
368
|
-
}
|
|
369
|
-
return item;
|
|
370
|
-
} else if (sx + 4 < xp) {
|
|
381
|
+
if (shouldGoRight) {
|
|
371
382
|
s = m + 1;
|
|
372
383
|
} else {
|
|
373
384
|
e = m - 1;
|
|
@@ -377,6 +388,10 @@ class Bar {
|
|
|
377
388
|
return item;
|
|
378
389
|
}
|
|
379
390
|
|
|
391
|
+
findGraphRange(offset) {
|
|
392
|
+
return this.binarySearchBar(offset, false);
|
|
393
|
+
}
|
|
394
|
+
|
|
380
395
|
/**
|
|
381
396
|
* Find graph item (horizontal)
|
|
382
397
|
* @param {array} offset mouse position
|
|
@@ -384,39 +399,7 @@ class Bar {
|
|
|
384
399
|
* @returns {object} graph item
|
|
385
400
|
*/
|
|
386
401
|
findGraphRangeCount(offset) {
|
|
387
|
-
|
|
388
|
-
const yp = offset[1];
|
|
389
|
-
const item = { data: null, hit: false, color: this.color };
|
|
390
|
-
const gdata = this.data;
|
|
391
|
-
|
|
392
|
-
const totalCount = this.filteredCount ?? gdata.length;
|
|
393
|
-
|
|
394
|
-
let s = 0;
|
|
395
|
-
let e = totalCount - 1;
|
|
396
|
-
|
|
397
|
-
while (s <= e) {
|
|
398
|
-
const m = Math.floor((s + e) / 2);
|
|
399
|
-
const sx = gdata[m].xp;
|
|
400
|
-
const sy = gdata[m].yp;
|
|
401
|
-
const ex = sx + gdata[m].w;
|
|
402
|
-
const ey = sy + gdata[m].h;
|
|
403
|
-
|
|
404
|
-
if ((ey <= yp) && (yp <= sy)) {
|
|
405
|
-
item.data = gdata[m];
|
|
406
|
-
item.index = gdata[m].index; // 원본 데이터 인덱스 사용
|
|
407
|
-
|
|
408
|
-
if ((sx <= xp) && (xp <= ex)) {
|
|
409
|
-
item.hit = true;
|
|
410
|
-
}
|
|
411
|
-
return item;
|
|
412
|
-
} else if (ey < yp) {
|
|
413
|
-
e = m - 1;
|
|
414
|
-
} else {
|
|
415
|
-
s = m + 1;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return item;
|
|
402
|
+
return this.binarySearchBar(offset, true);
|
|
420
403
|
}
|
|
421
404
|
|
|
422
405
|
/**
|
|
@@ -560,10 +543,27 @@ class Bar {
|
|
|
560
543
|
ctx.restore();
|
|
561
544
|
}
|
|
562
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Calculate bar size based on thickness
|
|
548
|
+
* @private
|
|
549
|
+
* @param {string|number} thickness - thickness value
|
|
550
|
+
* @param {number} bArea - available bar area
|
|
551
|
+
* @returns {number} calculated size
|
|
552
|
+
*/
|
|
553
|
+
calculateBarSize(thickness, bArea) {
|
|
554
|
+
if (typeof thickness === 'string' && /[0-9]+px/.test(thickness)) {
|
|
555
|
+
return Math.min(bArea, Number(thickness.replace('px', '')));
|
|
556
|
+
}
|
|
557
|
+
if (typeof thickness === 'number' && thickness <= 1 && thickness >= 0) {
|
|
558
|
+
return Math.ceil(bArea * thickness);
|
|
559
|
+
}
|
|
560
|
+
return bArea;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
563
|
drawBar({ ctx, positions }) {
|
|
564
|
-
const isHorizontal = this
|
|
564
|
+
const { isHorizontal, borderRadius } = this;
|
|
565
565
|
const isStackBar = 'stackIndex' in this;
|
|
566
|
-
const isBorderRadius =
|
|
566
|
+
const isBorderRadius = borderRadius && borderRadius > 0;
|
|
567
567
|
const { x, y, w } = positions;
|
|
568
568
|
const h = isHorizontal ? -positions.h : positions.h;
|
|
569
569
|
|
|
@@ -587,13 +587,26 @@ class Bar {
|
|
|
587
587
|
ctx.restore();
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Check if point is within bar boundaries
|
|
592
|
+
* @param {array} offset - [x, y] mouse position
|
|
593
|
+
* @param {object} barData - bar data object with xp, yp, w, h properties
|
|
594
|
+
* @returns {boolean} true if point is within bar
|
|
595
|
+
*/
|
|
596
|
+
isPointInBar(offset, barData) {
|
|
597
|
+
const [xp, yp] = offset;
|
|
598
|
+
const { xp: sx, yp: sy, w, h } = barData;
|
|
599
|
+
const ex = sx + w;
|
|
600
|
+
const ey = sy + h;
|
|
601
|
+
|
|
602
|
+
return (sx <= xp) && (xp <= ex) && (ey <= yp) && (yp <= sy);
|
|
603
|
+
}
|
|
604
|
+
|
|
590
605
|
drawRoundedRect(ctx, positions) {
|
|
591
|
-
const chartRect = this
|
|
592
|
-
const labelOffset = this.labelOffset;
|
|
593
|
-
const isHorizontal = this.isHorizontal;
|
|
606
|
+
const { chartRect, labelOffset, isHorizontal, borderRadius } = this;
|
|
594
607
|
const { x, y } = positions;
|
|
595
608
|
let { w, h } = positions;
|
|
596
|
-
let r =
|
|
609
|
+
let r = borderRadius;
|
|
597
610
|
|
|
598
611
|
const squarePath = new Path2D();
|
|
599
612
|
squarePath.rect(
|
|
@@ -327,76 +327,149 @@ class Line {
|
|
|
327
327
|
const yp = offset[1];
|
|
328
328
|
const item = { data: null, hit: false, color: this.color };
|
|
329
329
|
const gdata = this.data.filter(data => !Util.isNullOrUndefined(data.x));
|
|
330
|
-
const SPARE_XP = 0.5;
|
|
331
330
|
const isLinearInterpolation = this.useLinearInterpolation();
|
|
332
331
|
|
|
333
332
|
if (gdata?.length) {
|
|
334
333
|
if (typeof dataIndex === 'number' && this.show) {
|
|
335
334
|
item.data = gdata[dataIndex];
|
|
336
335
|
item.index = dataIndex;
|
|
336
|
+
if (item.data) {
|
|
337
|
+
const point = gdata[dataIndex];
|
|
338
|
+
const yDist = Math.abs(yp - point.yp);
|
|
339
|
+
const directHitThreshold = 15; // 직접 히트 임계값
|
|
340
|
+
|
|
341
|
+
if (yDist <= directHitThreshold) {
|
|
342
|
+
item.hit = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
337
345
|
} else if (typeof this.beforeFindItemIndex === 'number' && this.show && useSelectLabelOrItem) {
|
|
338
346
|
item.data = gdata[this.beforeFindItemIndex];
|
|
339
347
|
item.index = this.beforeFindItemIndex;
|
|
340
348
|
} else {
|
|
341
|
-
|
|
342
|
-
let
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
item.data = gdata[m + 1];
|
|
380
|
-
item.index = m + 1;
|
|
381
|
-
} else {
|
|
382
|
-
item.data = gdata[m];
|
|
383
|
-
item.index = m;
|
|
384
|
-
}
|
|
349
|
+
// Axis 트리거 방식: X축 위치에서 가장 가까운 데이터 포인트 찾기
|
|
350
|
+
let closestXDistance = Infinity;
|
|
351
|
+
let closestIndex = -1;
|
|
352
|
+
|
|
353
|
+
// null이 아닌 유효한 데이터만 필터링
|
|
354
|
+
const validData = [];
|
|
355
|
+
gdata.forEach((point, idx) => {
|
|
356
|
+
if (point.xp !== null && point.yp !== null && point.o !== null) {
|
|
357
|
+
validData.push({ ...point, originalIndex: idx });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (validData.length === 0) {
|
|
362
|
+
return item;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 이진 탐색으로 가장 가까운 포인트 찾기
|
|
366
|
+
let left = 0;
|
|
367
|
+
let right = validData.length - 1;
|
|
368
|
+
|
|
369
|
+
while (left <= right) {
|
|
370
|
+
const mid = Math.floor((left + right) / 2);
|
|
371
|
+
const point = validData[mid];
|
|
372
|
+
const xDistance = Math.abs(xp - point.xp);
|
|
373
|
+
|
|
374
|
+
if (xDistance < closestXDistance) {
|
|
375
|
+
closestXDistance = xDistance;
|
|
376
|
+
closestIndex = point.originalIndex;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (point.xp < xp) {
|
|
380
|
+
left = mid + 1;
|
|
381
|
+
// 다음 포인트도 확인
|
|
382
|
+
if (left < validData.length) {
|
|
383
|
+
const nextDistance = Math.abs(xp - validData[left].xp);
|
|
384
|
+
if (nextDistance < closestXDistance) {
|
|
385
|
+
closestXDistance = nextDistance;
|
|
386
|
+
closestIndex = validData[left].originalIndex;
|
|
385
387
|
}
|
|
386
|
-
} else {
|
|
387
|
-
item.data = gdata[m];
|
|
388
|
-
item.index = m;
|
|
389
388
|
}
|
|
389
|
+
} else if (point.xp > xp) {
|
|
390
|
+
right = mid - 1;
|
|
391
|
+
// 이전 포인트도 확인
|
|
392
|
+
if (right >= 0) {
|
|
393
|
+
const prevDistance = Math.abs(xp - validData[right].xp);
|
|
394
|
+
if (prevDistance < closestXDistance) {
|
|
395
|
+
closestXDistance = prevDistance;
|
|
396
|
+
closestIndex = validData[right].originalIndex;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
// 정확히 일치하는 경우
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
390
404
|
|
|
391
|
-
|
|
392
|
-
|
|
405
|
+
// 이진 탐색 후 주변 포인트 추가 확인 (정확도 향상)
|
|
406
|
+
const foundIdx = validData.findIndex(p => p.originalIndex === closestIndex);
|
|
407
|
+
if (foundIdx !== -1) {
|
|
408
|
+
// 앞뒤 2개씩 추가 확인
|
|
409
|
+
for (let i = Math.max(0, foundIdx - 2);
|
|
410
|
+
i <= Math.min(validData.length - 1, foundIdx + 2);
|
|
411
|
+
i++) {
|
|
412
|
+
const point = validData[i];
|
|
413
|
+
const xDistance = Math.abs(xp - point.xp);
|
|
414
|
+
if (xDistance < closestXDistance) {
|
|
415
|
+
closestXDistance = xDistance;
|
|
416
|
+
closestIndex = point.originalIndex;
|
|
393
417
|
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
394
420
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
421
|
+
// 가장 가까운 포인트 설정
|
|
422
|
+
if (closestIndex !== -1) {
|
|
423
|
+
// 데이터 간격 계산 - 모든 데이터(null 포함)의 평균 간격 사용
|
|
424
|
+
let avgInterval = 50;
|
|
425
|
+
if (gdata.length > 1) {
|
|
426
|
+
const intervals = [];
|
|
427
|
+
for (let i = 1; i < gdata.length; i++) {
|
|
428
|
+
if (gdata[i].xp !== null && gdata[i - 1].xp !== null) {
|
|
429
|
+
intervals.push(Math.abs(gdata[i].xp - gdata[i - 1].xp));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (intervals.length > 0) {
|
|
433
|
+
avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 두 가지 임계값 설정
|
|
438
|
+
const strictThreshold = avgInterval * 0.3; // 엄격한 임계값: 데이터 간격의 30%
|
|
439
|
+
const relaxedThreshold = avgInterval; // 느슨한 임계값: 데이터 간격 전체
|
|
440
|
+
|
|
441
|
+
// 1. 먼저 엄격한 임계값으로 정확한 매치 확인
|
|
442
|
+
if (closestXDistance <= strictThreshold) {
|
|
443
|
+
// 정확히 일치하거나 매우 가까운 데이터가 있음
|
|
444
|
+
item.data = gdata[closestIndex];
|
|
445
|
+
item.index = closestIndex;
|
|
398
446
|
} else {
|
|
399
|
-
|
|
447
|
+
// 2. 정확한 매치가 없을 때, 현재 X 위치 근처에 다른 유효 데이터가 있는지 확인
|
|
448
|
+
let hasNearbyValidData = false;
|
|
449
|
+
for (let i = 0; i < validData.length; i++) {
|
|
450
|
+
const xDist = Math.abs(xp - validData[i].xp);
|
|
451
|
+
if (xDist <= strictThreshold) {
|
|
452
|
+
hasNearbyValidData = true;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 3. 근처에 다른 유효 데이터가 없을 때만 느슨한 임계값 적용
|
|
458
|
+
if (!hasNearbyValidData && closestXDistance <= relaxedThreshold) {
|
|
459
|
+
item.data = gdata[closestIndex];
|
|
460
|
+
item.index = closestIndex;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Y축 거리를 확인하여 직접 히트 판정
|
|
465
|
+
if (item.data) {
|
|
466
|
+
const point = gdata[closestIndex];
|
|
467
|
+
const yDist = Math.abs(yp - point.yp);
|
|
468
|
+
const directHitThreshold = 15; // 직접 히트 임계값
|
|
469
|
+
|
|
470
|
+
if (yDist <= directHitThreshold) {
|
|
471
|
+
item.hit = true;
|
|
472
|
+
}
|
|
400
473
|
}
|
|
401
474
|
}
|
|
402
475
|
}
|
|
@@ -430,40 +503,90 @@ class Line {
|
|
|
430
503
|
const item = { data: null, hit: false, color: this.color };
|
|
431
504
|
const gdata = this.data.filter(data => !Util.isNullOrUndefined(data.x));
|
|
432
505
|
|
|
506
|
+
if (!gdata.length) {
|
|
507
|
+
return item;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 동적 감지 범위 계산
|
|
511
|
+
const gap = gdata.length > 1 ? Math.abs(gdata[1]?.xp - gdata[0]?.xp) : 50;
|
|
512
|
+
const xpInterval = Math.max(gap * 0.4, 10); // 데이터 간격의 40% 또는 최소 10px
|
|
513
|
+
|
|
433
514
|
let s = 0;
|
|
434
515
|
let e = gdata.length - 1;
|
|
516
|
+
let closestIndex = -1;
|
|
517
|
+
let closestDistance = Infinity;
|
|
435
518
|
|
|
519
|
+
// 이진 탐색으로 근처 데이터 찾기
|
|
436
520
|
while (s <= e) {
|
|
437
521
|
const m = Math.floor((s + e) / 2);
|
|
438
522
|
const x = gdata[m].xp;
|
|
439
|
-
const y = gdata[m].yp;
|
|
440
523
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
524
|
+
// X 좌표가 감지 범위 내에 있는 경우
|
|
525
|
+
if ((x - xpInterval <= xp) && (xp <= x + xpInterval)) {
|
|
526
|
+
// 중간점 주변 데이터들과 거리 비교
|
|
527
|
+
const checkStart = Math.max(0, m - 2);
|
|
528
|
+
const checkEnd = Math.min(gdata.length - 1, m + 2);
|
|
529
|
+
|
|
530
|
+
for (let i = checkStart; i <= checkEnd; i++) {
|
|
531
|
+
if (gdata[i].xp !== null && gdata[i].yp !== null) {
|
|
532
|
+
const distance = Math.sqrt(
|
|
533
|
+
((xp - gdata[i].xp) ** 2)
|
|
534
|
+
+ ((yp - gdata[i].yp) ** 2),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (distance < closestDistance) {
|
|
538
|
+
closestDistance = distance;
|
|
539
|
+
closestIndex = i;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (closestIndex !== -1) {
|
|
545
|
+
item.data = gdata[closestIndex];
|
|
546
|
+
item.index = closestIndex;
|
|
444
547
|
|
|
445
|
-
|
|
446
|
-
|
|
548
|
+
// 매우 가까운 경우 hit으로 표시
|
|
549
|
+
if (closestDistance < 5) {
|
|
550
|
+
item.hit = true;
|
|
551
|
+
}
|
|
447
552
|
}
|
|
448
553
|
|
|
449
554
|
return item;
|
|
450
|
-
} else if (x +
|
|
555
|
+
} else if (x + xpInterval < xp) {
|
|
556
|
+
// 마우스가 오른쪽에 있는 경우
|
|
451
557
|
if (m < e && xp < gdata[m + 1].xp) {
|
|
452
558
|
const curr = Math.abs(gdata[m].xp - xp);
|
|
453
559
|
const next = Math.abs(gdata[m + 1].xp - xp);
|
|
454
560
|
|
|
455
561
|
item.data = curr > next ? gdata[m + 1] : gdata[m];
|
|
456
562
|
item.index = curr > next ? m + 1 : m;
|
|
563
|
+
|
|
564
|
+
// Y 거리도 확인하여 hit 판정
|
|
565
|
+
const selectedPoint = item.data;
|
|
566
|
+
const yDist = Math.abs(yp - selectedPoint.yp);
|
|
567
|
+
if (yDist < 10) {
|
|
568
|
+
item.hit = true;
|
|
569
|
+
}
|
|
570
|
+
|
|
457
571
|
return item;
|
|
458
572
|
}
|
|
459
573
|
s = m + 1;
|
|
460
574
|
} else {
|
|
575
|
+
// 마우스가 왼쪽에 있는 경우
|
|
461
576
|
if (m > 0 && xp > gdata[m - 1].xp) {
|
|
462
577
|
const prev = Math.abs(gdata[m - 1].xp - xp);
|
|
463
578
|
const curr = Math.abs(gdata[m].xp - xp);
|
|
464
579
|
|
|
465
580
|
item.data = prev > curr ? gdata[m] : gdata[m - 1];
|
|
466
581
|
item.index = prev > curr ? m : m - 1;
|
|
582
|
+
|
|
583
|
+
// Y 거리도 확인하여 hit 판정
|
|
584
|
+
const selectedPoint = item.data;
|
|
585
|
+
const yDist = Math.abs(yp - selectedPoint.yp);
|
|
586
|
+
if (yDist < 10) {
|
|
587
|
+
item.hit = true;
|
|
588
|
+
}
|
|
589
|
+
|
|
467
590
|
return item;
|
|
468
591
|
}
|
|
469
592
|
e = m - 1;
|