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.
@@ -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
- if (indicator.use && type !== 'pie' && type !== 'scatter' && type !== 'heatMap') {
82
- this.drawIndicator(offset, indicator.color);
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
- const item = series.findGraphData(offset, isHorizontal);
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 (hasData && !hitInfo.items[sId]) {
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 = 1; // mouseInterpolation
787
- const mouseYIp = 10;
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
  *