Skip to content

Commit 581a534

Browse files
committed
Fix - VueUiXy - Fix cursor offset in fullscreen mode
1 parent 8b5fa9d commit 581a534

File tree

2 files changed

+62
-18
lines changed

2 files changed

+62
-18
lines changed

src/components/vue-ui-xy.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe("<VueUiXy />", () => {
176176
});
177177

178178
cy.log("@selectX");
179-
cy.get('.vue-ui-xy').trigger('mouseenter').trigger('click', { x: 40, y: 200})
179+
cy.get('.vue-ui-xy').trigger('mouseenter').trigger('click', { x: 40, y: 200, force: true })
180180
.then(() => {
181181
expect(wrapper.emitted("selectX")).to.deep.equal([
182182
[

src/components/vue-ui-xy.vue

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,41 +1127,69 @@ function calcIndividualRectY(plot) {
11271127
11281128
const hoveredIndex = ref(null);
11291129
1130-
let RAF_MOUSE_MOVE = 0;
1130+
function clientToSvgCoords(evt) {
1131+
const svgEl = svgRef.value;
1132+
if (!svgEl) return null;
1133+
1134+
// Precise mapping (handles fullscreen & letterboxing)
1135+
if (svgEl.createSVGPoint && svgEl.getScreenCTM) {
1136+
const pt = svgEl.createSVGPoint();
1137+
pt.x = evt.clientX;
1138+
pt.y = evt.clientY;
1139+
const ctm = svgEl.getScreenCTM();
1140+
if (ctm) {
1141+
const p = pt.matrixTransform(ctm.inverse());
1142+
return { x: p.x, y: p.y, ok: true };
1143+
}
1144+
}
11311145
1132-
function pointInRect(x, y, r) {
1133-
return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
1146+
// Fallback (preserveAspectRatio meet)
1147+
const rect = svgEl.getBoundingClientRect();
1148+
const vb = svgEl.viewBox?.baseVal || { x: 0, y: 0, width: rect.width, height: rect.height };
1149+
const scale = Math.min(rect.width / vb.width, rect.height / vb.height);
1150+
const drawnW = vb.width * scale;
1151+
const drawnH = vb.height * scale;
1152+
const offsetX = (rect.width - drawnW) / 2;
1153+
const offsetY = (rect.height - drawnH) / 2;
1154+
const x = (evt.clientX - rect.left - offsetX) / scale + vb.x;
1155+
const y = (evt.clientY - rect.top - offsetY) / scale + vb.y;
1156+
return { x, y, ok: true };
11341157
}
11351158
1159+
let RAF_MOUSE_MOVE = 0;
1160+
11361161
function onSvgMouseMove(e) {
11371162
if (isAnnotator.value) return;
11381163
11391164
// cancel any pending raf so a stale one cannot re-open the tooltip
11401165
if (RAF_MOUSE_MOVE) cancelAnimationFrame(RAF_MOUSE_MOVE);
11411166
1142-
const rect = svgRef.value?.getBoundingClientRect();
1143-
11441167
RAF_MOUSE_MOVE = requestAnimationFrame(() => {
11451168
RAF_MOUSE_MOVE = 0;
11461169
1170+
const svgPt = clientToSvgCoords(e);
11471171
// Mouse may have already left by the time this runs :E
1148-
if (!rect || !pointInRect(e.clientX, e.clientY, rect)) {
1172+
if (!svgPt || !svgRef.value) {
1173+
onSvgMouseLeave();
1174+
return;
1175+
}
1176+
1177+
// Ignore moves outside drawing area
1178+
const { left, right, top, bottom, width: areaW } = drawingArea.value;
1179+
if ( svgPt.x < left || svgPt.x > right || svgPt.y < top || svgPt.y > bottom) {
11491180
onSvgMouseLeave();
11501181
return;
11511182
}
11521183
1153-
const viewBox = svgRef.value.viewBox.baseVal;
1154-
const scaleX = viewBox.width / rect.width;
1155-
const svgX = (e.clientX - rect.left) * scaleX;
1156-
const localX = svgX - drawingArea.value.left;
1157-
const slotW = drawingArea.value.width / maxSeries.value;
1184+
const localX = svgPt.x - left;
1185+
const slotW = areaW / maxSeries.value;
11581186
const idx = Math.floor(localX / slotW);
11591187
11601188
if (idx >= 0 && idx < maxSeries.value) {
1161-
if (hoveredIndex.value !== idx) {
1162-
hoveredIndex.value = idx;
1163-
toggleTooltipVisibility(true, idx);
1164-
}
1189+
if (hoveredIndex.value !== idx) {
1190+
hoveredIndex.value = idx;
1191+
toggleTooltipVisibility(true, idx);
1192+
}
11651193
} else {
11661194
onSvgMouseLeave();
11671195
}
@@ -1177,7 +1205,23 @@ function onSvgMouseLeave() {
11771205
toggleTooltipVisibility(false, null);
11781206
}
11791207
1180-
function onSvgClick() {
1208+
function onSvgClick(e) {
1209+
const svgPt = clientToSvgCoords(e);
1210+
if (svgPt && svgRef.value) {
1211+
const { left, right, top, bottom, width: areaW } = drawingArea.value;
1212+
1213+
// Only react if click lands inside the drawing area
1214+
if (svgPt.x >= left && svgPt.x <= right && svgPt.y >= top && svgPt.y <= bottom) {
1215+
const slotW = areaW / Math.max(1, maxSeries.value);
1216+
const idx = Math.floor((svgPt.x - left) / slotW);
1217+
1218+
if (idx >= 0 && idx < maxSeries.value) {
1219+
selectX(idx);
1220+
return;
1221+
}
1222+
}
1223+
}
1224+
11811225
if (hoveredIndex.value != null) {
11821226
selectX(hoveredIndex.value);
11831227
}
@@ -2955,7 +2999,7 @@ defineExpose({
29552999
:class="`vue-ui-xy ${isFullscreen ? 'vue-data-ui-wrapper-fullscreen' : ''} ${FINAL_CONFIG.useCssAnimation ? '' : 'vue-ui-dna'}`"
29563000
ref="chart"
29573001
:style="`background:${FINAL_CONFIG.chart.backgroundColor}; color:${FINAL_CONFIG.chart.color};width:100%;font-family:${FINAL_CONFIG.chart.fontFamily};${FINAL_CONFIG.responsive ? 'height: 100%' : ''}`"
2958-
@mouseenter="() => setUserOptionsVisibility(true)" @mouseleave="() => setUserOptionsVisibility(false)">
3002+
@mouseenter="() => setUserOptionsVisibility(true)" @mouseleave="() => setUserOptionsVisibility(false)" @click="onSvgClick">
29593003
<PenAndPaper v-if="FINAL_CONFIG.chart.userOptions.buttons.annotator && svgRef" :svgRef="svgRef"
29603004
:backgroundColor="FINAL_CONFIG.chart.backgroundColor" :color="FINAL_CONFIG.chart.color"
29613005
:active="isAnnotator" @close="toggleAnnotator" />

0 commit comments

Comments
 (0)