evui 3.4.203 → 3.4.205
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
|
@@ -66,6 +66,25 @@ const modules = {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
// tooltip이 표시될 때 indicator를 해당 라벨 위치로 이동 (line 차트이거나 line series가 포함된 경우)
|
|
71
|
+
const hasLineSeries = Object.values(this.seriesList || {}).some(series => series.type === 'line');
|
|
72
|
+
if (tooltip.use && (type === 'line' || hasLineSeries)) {
|
|
73
|
+
// indicator를 그리고 실제 위치한 라벨 정보를 받음
|
|
74
|
+
const indicatorInfo = this.drawIndicatorForTooltip(hitInfo, indicator.color);
|
|
75
|
+
|
|
76
|
+
// 실제 indicator가 위치한 라벨 값을 동기화에 사용
|
|
77
|
+
const actualLabelValue = indicatorInfo?.labelValue;
|
|
78
|
+
const label = this.getTimeLabel(offset);
|
|
79
|
+
|
|
80
|
+
args.hoveredLabel = {
|
|
81
|
+
horizontal: this.options.horizontal,
|
|
82
|
+
label,
|
|
83
|
+
mousePosition: [e.clientX, e.clientY],
|
|
84
|
+
dataLabel: actualLabelValue,
|
|
85
|
+
isTooltipBased: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
69
88
|
} else if (tooltip.use && this.isInitTooltip) {
|
|
70
89
|
if (typeof tooltip?.returnValue === 'function') {
|
|
71
90
|
tooltip.returnValue([], e);
|
|
@@ -78,15 +97,21 @@ const modules = {
|
|
|
78
97
|
this.drawSelectionArea(this.dragInfoBackup);
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
// tooltip 기반 indicator가 아직 설정되지 않은 경우에만 일반 indicator 처리
|
|
101
|
+
if (!args.hoveredLabel && type !== 'pie' && type !== 'scatter' && type !== 'heatMap') {
|
|
102
|
+
// line 차트가 아니고 line series가 없거나, tooltip이 없을 때는 일반 indicator 표시
|
|
103
|
+
const hasLineSeries = Object.values(this.seriesList || {}).some(series => series.type === 'line');
|
|
104
|
+
if ((type !== 'line' && !hasLineSeries) || !tooltip.use || !Object.keys(hitInfo.items).length) {
|
|
105
|
+
this.drawIndicator(offset, indicator.color);
|
|
106
|
+
}
|
|
83
107
|
const label = this.getTimeLabel(offset);
|
|
84
108
|
args.hoveredLabel = {
|
|
85
109
|
horizontal: this.options.horizontal,
|
|
86
110
|
label,
|
|
87
111
|
mousePosition: [e.clientX, e.clientY],
|
|
112
|
+
isTooltipBased: false,
|
|
88
113
|
};
|
|
89
|
-
} else {
|
|
114
|
+
} else if (!args.hoveredLabel) {
|
|
90
115
|
args.hoveredLabel = {
|
|
91
116
|
label: '',
|
|
92
117
|
};
|
|
@@ -881,12 +906,82 @@ const modules = {
|
|
|
881
906
|
let maxg = null;
|
|
882
907
|
let maxSID = null;
|
|
883
908
|
|
|
909
|
+
// 파이 차트는 특별한 처리가 필요
|
|
910
|
+
if (this.options.type === 'pie') {
|
|
911
|
+
for (let ix = 0; ix < sIds.length; ix++) {
|
|
912
|
+
const sId = sIds[ix];
|
|
913
|
+
const series = this.seriesList[sId];
|
|
914
|
+
|
|
915
|
+
if (series.findGraphData && series.show) {
|
|
916
|
+
const item = series.findGraphData(offset);
|
|
917
|
+
|
|
918
|
+
if (item?.data && item.hit) {
|
|
919
|
+
const gdata = item.data.o;
|
|
920
|
+
|
|
921
|
+
if (gdata !== null && gdata !== undefined) {
|
|
922
|
+
const formattedSeriesName = this.getFormattedTooltipLabel({
|
|
923
|
+
dataId: series.id,
|
|
924
|
+
seriesId: sId,
|
|
925
|
+
seriesName: series.name,
|
|
926
|
+
itemData: item.data,
|
|
927
|
+
});
|
|
928
|
+
const sw = ctx ? ctx.measureText(formattedSeriesName).width : 1;
|
|
929
|
+
|
|
930
|
+
item.id = series.id;
|
|
931
|
+
item.name = formattedSeriesName;
|
|
932
|
+
item.axis = { x: 0, y: 0 };
|
|
933
|
+
items[sId] = item;
|
|
934
|
+
|
|
935
|
+
const formattedTxt = this.getFormattedTooltipValue({
|
|
936
|
+
dataId: series.id,
|
|
937
|
+
seriesId: sId,
|
|
938
|
+
seriesName: formattedSeriesName,
|
|
939
|
+
value: gdata,
|
|
940
|
+
itemData: item.data,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
item.data.formatted = formattedTxt;
|
|
944
|
+
|
|
945
|
+
if (maxsw < sw) {
|
|
946
|
+
maxs = formattedSeriesName;
|
|
947
|
+
maxsw = sw;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (maxv.length <= `${formattedTxt}`.length) {
|
|
951
|
+
maxv = `${formattedTxt}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (maxg === null || maxg <= gdata) {
|
|
955
|
+
maxg = gdata;
|
|
956
|
+
maxSID = sId;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
hitId = sId;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const maxHighlight = maxg !== null ? [maxSID, maxg] : null;
|
|
966
|
+
return { items, hitId, maxTip: [maxs, maxv], maxHighlight };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// 1. 먼저 공통으로 사용할 데이터 인덱스 결정
|
|
970
|
+
const targetDataIndex = this.findClosestDataIndex(offset, sIds);
|
|
971
|
+
|
|
972
|
+
if (targetDataIndex === -1) {
|
|
973
|
+
return { items, hitId, maxTip: [maxs, maxv], maxHighlight: null };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// 2. 모든 시리즈가 동일한 데이터 인덱스 사용
|
|
977
|
+
const allSeriesIsBar = sIds.every(sId => this.seriesList[sId].type === 'bar');
|
|
884
978
|
for (let ix = 0; ix < sIds.length; ix++) {
|
|
885
979
|
const sId = sIds[ix];
|
|
886
980
|
const series = this.seriesList[sId];
|
|
887
981
|
|
|
888
|
-
if (series.findGraphData) {
|
|
889
|
-
|
|
982
|
+
if (series.findGraphData && series.show) {
|
|
983
|
+
// 특정 데이터 인덱스로 데이터 요청
|
|
984
|
+
const item = series.findGraphData(offset, isHorizontal, targetDataIndex, !allSeriesIsBar);
|
|
890
985
|
|
|
891
986
|
if (item?.data) {
|
|
892
987
|
let gdata;
|
|
@@ -951,6 +1046,60 @@ const modules = {
|
|
|
951
1046
|
return { items, hitId, maxTip: [maxs, maxv], maxHighlight };
|
|
952
1047
|
},
|
|
953
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* Find the closest data index (label) based on mouse position
|
|
1051
|
+
* @param {array} offset mouse position
|
|
1052
|
+
* @param {array} sIds series IDs
|
|
1053
|
+
* @returns {number} closest data index
|
|
1054
|
+
*/
|
|
1055
|
+
findClosestDataIndex(offset, sIds) {
|
|
1056
|
+
const [xp, yp] = offset;
|
|
1057
|
+
const isHorizontal = !!this.options.horizontal;
|
|
1058
|
+
const mousePos = isHorizontal ? yp : xp;
|
|
1059
|
+
let closestDistance = Infinity;
|
|
1060
|
+
let closestIndex = -1;
|
|
1061
|
+
|
|
1062
|
+
// 첫 번째 표시 중인 시리즈를 기준으로 라벨 위치 확인
|
|
1063
|
+
const referenceSeries = sIds.find(sId => this.seriesList[sId]?.show);
|
|
1064
|
+
if (!referenceSeries || !this.seriesList[referenceSeries]?.data) {
|
|
1065
|
+
return -1;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const referenceData = this.seriesList[referenceSeries].data;
|
|
1069
|
+
|
|
1070
|
+
// 각 라벨에서 가장 가까운 것 찾기
|
|
1071
|
+
for (let i = 0; i < referenceData.length; i++) {
|
|
1072
|
+
// 이 라벨에 유효한 데이터가 있는 시리즈가 하나 이상 있는지 확인
|
|
1073
|
+
const hasValidData = sIds.some((sId) => {
|
|
1074
|
+
const series = this.seriesList[sId];
|
|
1075
|
+
return series?.show && series.data?.[i]?.o !== null && series.data?.[i]?.o !== undefined;
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
if (hasValidData) {
|
|
1079
|
+
const point = referenceData[i];
|
|
1080
|
+
if (point) {
|
|
1081
|
+
// 라벨 위치 계산
|
|
1082
|
+
let labelPos;
|
|
1083
|
+
if (isHorizontal) {
|
|
1084
|
+
labelPos = point.h ? point.yp + (point.h / 2) : point.yp;
|
|
1085
|
+
} else {
|
|
1086
|
+
labelPos = point.w ? point.xp + (point.w / 2) : point.xp;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (labelPos !== null) {
|
|
1090
|
+
const distance = Math.abs(mousePos - labelPos);
|
|
1091
|
+
if (distance < closestDistance) {
|
|
1092
|
+
closestDistance = distance;
|
|
1093
|
+
closestIndex = i;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return closestIndex;
|
|
1101
|
+
},
|
|
1102
|
+
|
|
954
1103
|
/**
|
|
955
1104
|
* get formatted label for tooltip
|
|
956
1105
|
* @param dataId
|
|
@@ -1075,7 +1224,8 @@ const modules = {
|
|
|
1075
1224
|
itemData: hasData,
|
|
1076
1225
|
});
|
|
1077
1226
|
|
|
1078
|
-
if
|
|
1227
|
+
// Only add data if there's a valid value for this exact label
|
|
1228
|
+
if (hasData && hasData.o !== null && hasData.o !== undefined && !hitInfo.items[sId]) {
|
|
1079
1229
|
const item = {};
|
|
1080
1230
|
item.color = series.color;
|
|
1081
1231
|
item.hit = false;
|
|
@@ -1270,6 +1420,77 @@ const modules = {
|
|
|
1270
1420
|
return after;
|
|
1271
1421
|
},
|
|
1272
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Draw indicator at the label position when tooltip is displayed
|
|
1425
|
+
* @param {object} hitInfo hit item information from findHitItem
|
|
1426
|
+
* @param {string} color indicator color
|
|
1427
|
+
* @returns {object|null} indicator position info with actual label value
|
|
1428
|
+
*/
|
|
1429
|
+
drawIndicatorForTooltip(hitInfo, color) {
|
|
1430
|
+
if (!hitInfo?.items || !Object.keys(hitInfo.items).length) {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const ctx = this.overlayCtx;
|
|
1435
|
+
const { horizontal } = this.options;
|
|
1436
|
+
const graphPos = {
|
|
1437
|
+
x1: this.chartRect.x1 + this.labelOffset.left,
|
|
1438
|
+
x2: this.chartRect.x2 - this.labelOffset.right,
|
|
1439
|
+
y1: this.chartRect.y1 + this.labelOffset.top,
|
|
1440
|
+
y2: this.chartRect.y2 - this.labelOffset.bottom,
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// 첫 번째 시리즈의 데이터를 기준으로 라벨 위치 계산
|
|
1444
|
+
const firstSeriesId = Object.keys(hitInfo.items)[0];
|
|
1445
|
+
const firstItem = hitInfo.items[firstSeriesId];
|
|
1446
|
+
|
|
1447
|
+
if (!firstItem?.data) {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// 실제 indicator가 위치하는 라벨 값 추출
|
|
1452
|
+
const actualLabelValue = horizontal ? firstItem.data.y : firstItem.data.x;
|
|
1453
|
+
|
|
1454
|
+
let indicatorPosition;
|
|
1455
|
+
|
|
1456
|
+
if (horizontal) {
|
|
1457
|
+
// 수평 차트에서는 Y축 라벨 위치에 수평선
|
|
1458
|
+
const yPosition = firstItem.data.yp + (firstItem.data.h ? firstItem.data.h / 2 : 0);
|
|
1459
|
+
indicatorPosition = [graphPos.x1, yPosition];
|
|
1460
|
+
} else {
|
|
1461
|
+
// 수직 차트에서는 X축 라벨 위치에 수직선
|
|
1462
|
+
const xPosition = firstItem.data.xp + (firstItem.data.w ? firstItem.data.w / 2 : 0);
|
|
1463
|
+
indicatorPosition = [xPosition, graphPos.y1];
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
ctx.beginPath();
|
|
1467
|
+
ctx.save();
|
|
1468
|
+
ctx.strokeStyle = color;
|
|
1469
|
+
ctx.lineWidth = 1;
|
|
1470
|
+
|
|
1471
|
+
if (this.options.indicator?.segments) {
|
|
1472
|
+
ctx.setLineDash(this.options.indicator.segments);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (horizontal) {
|
|
1476
|
+
ctx.moveTo(graphPos.x1, indicatorPosition[1] + 0.5);
|
|
1477
|
+
ctx.lineTo(graphPos.x2, indicatorPosition[1] + 0.5);
|
|
1478
|
+
} else {
|
|
1479
|
+
ctx.moveTo(indicatorPosition[0] + 0.5, graphPos.y1);
|
|
1480
|
+
ctx.lineTo(indicatorPosition[0] + 0.5, graphPos.y2);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
ctx.stroke();
|
|
1484
|
+
ctx.restore();
|
|
1485
|
+
ctx.closePath();
|
|
1486
|
+
|
|
1487
|
+
// 실제 indicator가 위치한 라벨 정보 반환
|
|
1488
|
+
return {
|
|
1489
|
+
labelValue: actualLabelValue,
|
|
1490
|
+
position: indicatorPosition,
|
|
1491
|
+
};
|
|
1492
|
+
},
|
|
1493
|
+
|
|
1273
1494
|
/**
|
|
1274
1495
|
* Find items by series within a range
|
|
1275
1496
|
* @param {object} range object for find series items
|
|
@@ -783,8 +783,8 @@ const modules = {
|
|
|
783
783
|
y1: this.chartRect.y1 + this.labelOffset.top,
|
|
784
784
|
y2: this.chartRect.y2 - this.labelOffset.bottom,
|
|
785
785
|
};
|
|
786
|
-
const mouseXIp =
|
|
787
|
-
const mouseYIp =
|
|
786
|
+
const mouseXIp = 15; // mouseInterpolation - 더 넓은 범위에서 감지
|
|
787
|
+
const mouseYIp = 15; // Y축도 동일하게 증가
|
|
788
788
|
const options = this.options;
|
|
789
789
|
|
|
790
790
|
if (offsetX >= (graphPos.x1 - mouseXIp) && offsetX <= (graphPos.x2 + mouseXIp)
|
|
@@ -858,10 +858,18 @@ const modules = {
|
|
|
858
858
|
*
|
|
859
859
|
* @returns {undefined}
|
|
860
860
|
*/
|
|
861
|
-
drawSyncedIndicator({ horizontal, label, mousePosition }) {
|
|
861
|
+
drawSyncedIndicator({ horizontal, label, mousePosition, dataLabel, isTooltipBased }) {
|
|
862
862
|
if (!mousePosition || !!horizontal !== !!this.options.horizontal) {
|
|
863
863
|
return;
|
|
864
864
|
}
|
|
865
|
+
|
|
866
|
+
// tooltip 기반 동기화인 경우
|
|
867
|
+
if (isTooltipBased) {
|
|
868
|
+
this.drawSyncedIndicatorForTooltip({ dataLabel, mousePosition });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 기존 시간 기반 동기화
|
|
865
873
|
if (
|
|
866
874
|
this.options.syncHover === false
|
|
867
875
|
|| (!horizontal && !this.options.axesX.every(({ type }) => type === 'time'))
|
|
@@ -900,6 +908,68 @@ const modules = {
|
|
|
900
908
|
}
|
|
901
909
|
},
|
|
902
910
|
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* 제공된 dataLabel과 일치하는 Label이 있다면 indicator를 그림
|
|
914
|
+
* @param {object} dataLabel data label
|
|
915
|
+
* @param {object} mousePosition mouse position
|
|
916
|
+
*
|
|
917
|
+
* @returns {undefined}
|
|
918
|
+
*/
|
|
919
|
+
drawSyncedIndicatorForTooltip({ dataLabel, mousePosition }) {
|
|
920
|
+
if (!this.data?.labels || !dataLabel) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const matchingLabelIndex = this.data.labels.findIndex(
|
|
925
|
+
label => label?.valueOf() === dataLabel?.valueOf(),
|
|
926
|
+
);
|
|
927
|
+
if (matchingLabelIndex === -1) {
|
|
928
|
+
this.overlayClear();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const { horizontal } = this.options;
|
|
933
|
+
const { top, bottom, left, right } = this.chartDOM.getBoundingClientRect();
|
|
934
|
+
const isHoveredChart = inRange(mousePosition[0], left, right)
|
|
935
|
+
&& inRange(mousePosition[1], bottom, top);
|
|
936
|
+
if (isHoveredChart) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
this.overlayClear();
|
|
941
|
+
|
|
942
|
+
const graphPos = {
|
|
943
|
+
x1: this.chartRect.x1 + this.labelOffset.left,
|
|
944
|
+
x2: this.chartRect.x2 - this.labelOffset.right,
|
|
945
|
+
y1: this.chartRect.y1 + this.labelOffset.top,
|
|
946
|
+
y2: this.chartRect.y2 - this.labelOffset.bottom,
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const labelsCount = this.data.labels.length;
|
|
950
|
+
let indicatorPosition;
|
|
951
|
+
|
|
952
|
+
if (horizontal) {
|
|
953
|
+
const chartHeight = graphPos.y2 - graphPos.y1;
|
|
954
|
+
// CategoryMode인 경우 라벨들이 균등 간격으로 배치됨
|
|
955
|
+
const isCategoryMode = this.options.axesY?.some(axis => axis.categoryMode);
|
|
956
|
+
const positionY = isCategoryMode
|
|
957
|
+
? graphPos.y1 + (chartHeight * (matchingLabelIndex + 0.5)) / labelsCount
|
|
958
|
+
: graphPos.y1 + (chartHeight * matchingLabelIndex) / (labelsCount - 1);
|
|
959
|
+
indicatorPosition = [graphPos.x2, positionY];
|
|
960
|
+
} else {
|
|
961
|
+
const chartWidth = graphPos.x2 - graphPos.x1;
|
|
962
|
+
// CategoryMode인 경우 라벨들이 균등 간격으로 배치됨
|
|
963
|
+
const isCategoryMode = this.options.axesX?.some(axis => axis.categoryMode);
|
|
964
|
+
const positionX = isCategoryMode
|
|
965
|
+
? graphPos.x1 + (chartWidth * (matchingLabelIndex + 0.5)) / labelsCount
|
|
966
|
+
: graphPos.x1 + (chartWidth * matchingLabelIndex) / (labelsCount - 1);
|
|
967
|
+
indicatorPosition = [positionX, graphPos.y2];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
this.drawIndicator(indicatorPosition, this.options.indicator.color);
|
|
971
|
+
},
|
|
972
|
+
|
|
903
973
|
/**
|
|
904
974
|
* Clear tooltip canvas
|
|
905
975
|
*
|