Skip to content

Commit 4afc441

Browse files
authored
Merge pull request #5193 from Displayr/888-style-transforms
Align interactions when plot created inside scaled and/or translated elements using CSS transform
2 parents a382098 + 95a81fc commit 4afc441

24 files changed

+2187
-1230
lines changed

package-lock.json

+7-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/fx/hover.js

+35-25
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ exports.loneHover = function loneHover(hoverItems, opts) {
194194
d.offset -= anchor;
195195
});
196196

197-
alignHoverText(hoverLabel, fullOpts.rotateLabels);
197+
var scaleX = opts.gd._fullLayout._inverseScaleX;
198+
var scaleY = opts.gd._fullLayout._inverseScaleY;
199+
alignHoverText(hoverLabel, fullOpts.rotateLabels, scaleX, scaleY);
198200

199201
return multiHover ? hoverLabel : hoverLabel.node();
200202
};
@@ -338,6 +340,11 @@ function _hover(gd, evt, subplot, noHoverEvent) {
338340
xpx = evt.clientX - dbb.left;
339341
ypx = evt.clientY - dbb.top;
340342

343+
var transformedCoords = Lib.apply3DTransform(fullLayout._inverseTransform)(xpx, ypx);
344+
345+
xpx = transformedCoords[0];
346+
ypx = transformedCoords[1];
347+
341348
// in case hover was called from mouseout into hovertext,
342349
// it's possible you're not actually over the plot anymore
343350
if(xpx < 0 || xpx > xaArray[0]._length || ypx < 0 || ypx > yaArray[0]._length) {
@@ -718,10 +725,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {
718725

719726
if(!helpers.isUnifiedHover(hovermode)) {
720727
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
721-
alignHoverText(hoverLabels, rotateLabels);
722-
}
723-
724-
// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
728+
alignHoverText(hoverLabels, rotateLabels, fullLayout._inverseScaleX, fullLayout._inverseScaleY);
729+
} // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
725730
// we should improve the "fx" API so other plots can use it without these hack.
726731
if(evt.target && evt.target.tagName) {
727732
var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
@@ -1479,7 +1484,10 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
14791484
}
14801485
}
14811486

1482-
function alignHoverText(hoverLabels, rotateLabels) {
1487+
function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
1488+
var pX = function(x) { return x * scaleX; };
1489+
var pY = function(y) { return y * scaleY; };
1490+
14831491
// finally set the text positioning relative to the data and draw the
14841492
// box around it
14851493
hoverLabels.each(function(d) {
@@ -1495,7 +1503,8 @@ function alignHoverText(hoverLabels, rotateLabels) {
14951503
var offsetX = 0;
14961504
var offsetY = d.offset;
14971505

1498-
if(anchor === 'middle') {
1506+
var isMiddle = anchor === 'middle';
1507+
if(isMiddle) {
14991508
txx -= d.tx2width / 2;
15001509
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
15011510
}
@@ -1504,49 +1513,50 @@ function alignHoverText(hoverLabels, rotateLabels) {
15041513
offsetX = d.offset * YSHIFTX;
15051514
}
15061515

1507-
g.select('path').attr('d', anchor === 'middle' ?
1516+
g.select('path')
1517+
.attr('d', isMiddle ?
15081518
// middle aligned: rect centered on data
1509-
('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) +
1510-
'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') :
1519+
('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) +
1520+
'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') :
15111521
// left or right aligned: side rect with arrow to data
1512-
('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) +
1513-
'v' + (d.by / 2 - HOVERARROWSIZE) +
1514-
'h' + (horzSign * d.bx) +
1515-
'v-' + d.by +
1516-
'H' + (horzSign * HOVERARROWSIZE + offsetX) +
1517-
'V' + (offsetY - HOVERARROWSIZE) +
1522+
('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) +
1523+
'v' + pY(d.by / 2 - HOVERARROWSIZE) +
1524+
'h' + pX(horzSign * d.bx) +
1525+
'v-' + pY(d.by) +
1526+
'H' + pX(horzSign * HOVERARROWSIZE + offsetX) +
1527+
'V' + pY(offsetY - HOVERARROWSIZE) +
15181528
'Z'));
15191529

1520-
var posX = txx + offsetX;
1530+
var posX = offsetX + txx;
15211531
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;
15221532
var textAlign = d.textAlign || 'auto';
15231533

15241534
if(textAlign !== 'auto') {
15251535
if(textAlign === 'left' && anchor !== 'start') {
15261536
tx.attr('text-anchor', 'start');
1527-
posX = anchor === 'middle' ?
1537+
posX = isMiddle ?
15281538
-d.bx / 2 - d.tx2width / 2 + HOVERTEXTPAD :
15291539
-d.bx - HOVERTEXTPAD;
15301540
} else if(textAlign === 'right' && anchor !== 'end') {
15311541
tx.attr('text-anchor', 'end');
1532-
posX = anchor === 'middle' ?
1542+
posX = isMiddle ?
15331543
d.bx / 2 - d.tx2width / 2 - HOVERTEXTPAD :
15341544
d.bx + HOVERTEXTPAD;
15351545
}
15361546
}
15371547

1538-
tx.call(svgTextUtils.positionText, posX, posY);
1548+
tx.call(svgTextUtils.positionText, pX(posX), pY(posY));
15391549

15401550
if(d.tx2width) {
15411551
g.select('text.name')
15421552
.call(svgTextUtils.positionText,
1543-
tx2x + alignShift * HOVERTEXTPAD + offsetX,
1544-
offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD);
1553+
pX(tx2x + alignShift * HOVERTEXTPAD + offsetX),
1554+
pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD));
15451555
g.select('rect')
15461556
.call(Drawing.setRect,
1547-
tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX,
1548-
offsetY - d.by / 2 - 1,
1549-
d.tx2width, d.by + 2);
1557+
pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX),
1558+
pY(offsetY - d.by / 2 - 1),
1559+
pX(d.tx2width), pY(d.by + 2));
15501560
}
15511561
});
15521562
}

src/lib/dom.js

+63-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
var d3 = require('d3');
1212
var loggers = require('./loggers');
13+
var matrix = require('./matrix');
14+
var mat4X4 = require('gl-mat4');
1315

1416
/**
1517
* Allow referencing a graph DOM element either directly
@@ -91,11 +93,71 @@ function deleteRelatedStyleRule(uid) {
9193
if(style) removeElement(style);
9294
}
9395

96+
function getFullTransformMatrix(element) {
97+
var allElements = getElementAndAncestors(element);
98+
// the identity matrix
99+
var out = [
100+
1, 0, 0, 0,
101+
0, 1, 0, 0,
102+
0, 0, 1, 0,
103+
0, 0, 0, 1
104+
];
105+
allElements.forEach(function(e) {
106+
var t = getElementTransformMatrix(e);
107+
if(t) {
108+
var m = matrix.convertCssMatrix(t);
109+
out = mat4X4.multiply(out, out, m);
110+
}
111+
});
112+
return out;
113+
}
114+
115+
/**
116+
* extracts and parses the 2d css style transform matrix from some element
117+
*/
118+
function getElementTransformMatrix(element) {
119+
var style = window.getComputedStyle(element, null);
120+
var transform = (
121+
style.getPropertyValue('-webkit-transform') ||
122+
style.getPropertyValue('-moz-transform') ||
123+
style.getPropertyValue('-ms-transform') ||
124+
style.getPropertyValue('-o-transform') ||
125+
style.getPropertyValue('transform')
126+
);
127+
128+
if(transform === 'none') return null;
129+
// the transform is a string in the form of matrix(a, b, ...) or matrix3d(...)
130+
return transform
131+
.replace('matrix', '')
132+
.replace('3d', '')
133+
.slice(1, -1)
134+
.split(',')
135+
.map(function(n) { return +n; });
136+
}
137+
/**
138+
* retrieve all DOM elements that are ancestors of the specified one (including itself)
139+
*/
140+
function getElementAndAncestors(element) {
141+
var allElements = [];
142+
while(isTransformableElement(element)) {
143+
allElements.push(element);
144+
element = element.parentNode;
145+
}
146+
return allElements;
147+
}
148+
149+
function isTransformableElement(element) {
150+
return element && (element instanceof Element || element instanceof HTMLElement);
151+
}
152+
94153
module.exports = {
95154
getGraphDiv: getGraphDiv,
96155
isPlotDiv: isPlotDiv,
97156
removeElement: removeElement,
98157
addStyleRule: addStyleRule,
99158
addRelatedStyleRule: addRelatedStyleRule,
100-
deleteRelatedStyleRule: deleteRelatedStyleRule
159+
deleteRelatedStyleRule: deleteRelatedStyleRule,
160+
getFullTransformMatrix: getFullTransformMatrix,
161+
getElementTransformMatrix: getElementTransformMatrix,
162+
getElementAndAncestors: getElementAndAncestors,
101163
};

src/lib/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ lib.dot = matrixModule.dot;
8888
lib.translationMatrix = matrixModule.translationMatrix;
8989
lib.rotationMatrix = matrixModule.rotationMatrix;
9090
lib.rotationXYMatrix = matrixModule.rotationXYMatrix;
91+
lib.apply3DTransform = matrixModule.apply3DTransform;
9192
lib.apply2DTransform = matrixModule.apply2DTransform;
9293
lib.apply2DTransform2 = matrixModule.apply2DTransform2;
94+
lib.convertCssMatrix = matrixModule.convertCssMatrix;
95+
lib.inverseTransformMatrix = matrixModule.inverseTransformMatrix;
9396

9497
var anglesModule = require('./angles');
9598
lib.deg2rad = anglesModule.deg2rad;
@@ -145,6 +148,9 @@ lib.removeElement = domModule.removeElement;
145148
lib.addStyleRule = domModule.addStyleRule;
146149
lib.addRelatedStyleRule = domModule.addRelatedStyleRule;
147150
lib.deleteRelatedStyleRule = domModule.deleteRelatedStyleRule;
151+
lib.getFullTransformMatrix = domModule.getFullTransformMatrix;
152+
lib.getElementTransformMatrix = domModule.getElementTransformMatrix;
153+
lib.getElementAndAncestors = domModule.getElementAndAncestors;
148154

149155
lib.clearResponsive = require('./clear_responsive');
150156

src/lib/matrix.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
'use strict';
1111

12+
var mat4X4 = require('gl-mat4');
1213

1314
exports.init2dArray = function(rowLength, colLength) {
1415
var array = new Array(rowLength);
@@ -84,13 +85,23 @@ exports.rotationXYMatrix = function(a, x, y) {
8485
exports.translationMatrix(-x, -y));
8586
};
8687

88+
// applies a 3D transformation matrix to either x, y and z params
89+
// Note: z is optional
90+
exports.apply3DTransform = function(transform) {
91+
return function() {
92+
var args = arguments;
93+
var xyz = arguments.length === 1 ? args[0] : [args[0], args[1], args[2] || 0];
94+
return exports.dot(transform, [xyz[0], xyz[1], xyz[2], 1]).slice(0, 3);
95+
};
96+
};
97+
8798
// applies a 2D transformation matrix to either x and y params or an [x,y] array
8899
exports.apply2DTransform = function(transform) {
89100
return function() {
90101
var args = arguments;
91102
if(args.length === 3) {
92103
args = args[0];
93-
}// from map
104+
} // from map
94105
var xy = arguments.length === 1 ? args[0] : [args[0], args[1]];
95106
return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2);
96107
};
@@ -103,3 +114,37 @@ exports.apply2DTransform2 = function(transform) {
103114
return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4)));
104115
};
105116
};
117+
118+
exports.convertCssMatrix = function(m) {
119+
if(m) {
120+
var len = m.length;
121+
if(len === 16) return m;
122+
if(len === 6) {
123+
// converts a 2x3 css transform matrix to a 4x4 matrix see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
124+
return [
125+
m[0], m[1], 0, 0,
126+
m[2], m[3], 0, 0,
127+
0, 0, 1, 0,
128+
m[4], m[5], 0, 1
129+
];
130+
}
131+
}
132+
return [
133+
1, 0, 0, 0,
134+
0, 1, 0, 0,
135+
0, 0, 1, 0,
136+
0, 0, 0, 1
137+
];
138+
};
139+
140+
// find the inverse for a 4x4 affine transform matrix
141+
exports.inverseTransformMatrix = function(m) {
142+
var out = [];
143+
mat4X4.invert(out, m);
144+
return [
145+
[out[0], out[1], out[2], out[3]],
146+
[out[4], out[5], out[6], out[7]],
147+
[out[8], out[9], out[10], out[11]],
148+
[out[12], out[13], out[14], out[15]]
149+
];
150+
};

src/lib/svg_text_utils.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,17 @@ function alignHTMLWith(_base, container, options) {
744744

745745
return function() {
746746
thisRect = this.node().getBoundingClientRect();
747+
748+
var x0 = getLeft() - cRect.left;
749+
var y0 = getTop() - cRect.top;
750+
var gd = options.gd || {};
751+
var transformedCoords = Lib.apply3DTransform(gd._fullLayout._inverseTransform)(x0, y0);
752+
x0 = transformedCoords[0];
753+
y0 = transformedCoords[1];
754+
747755
this.style({
748-
top: (getTop() - cRect.top) + 'px',
749-
left: (getLeft() - cRect.left) + 'px',
756+
top: y0 + 'px',
757+
left: x0 + 'px',
750758
'z-index': 1000
751759
});
752760
return this;

src/plot_api/plot_api.js

+5
Original file line numberDiff line numberDiff line change
@@ -3714,6 +3714,11 @@ function purge(gd) {
37143714
function makePlotFramework(gd) {
37153715
var gd3 = d3.select(gd);
37163716
var fullLayout = gd._fullLayout;
3717+
if(fullLayout._inverseTransform === undefined) {
3718+
var m = fullLayout._inverseTransform = Lib.inverseTransformMatrix(Lib.getFullTransformMatrix(gd));
3719+
fullLayout._inverseScaleX = Math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]);
3720+
fullLayout._inverseScaleY = Math.sqrt(m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]);
3721+
}
37173722

37183723
// Plot container
37193724
fullLayout._container = gd3.selectAll('.plot-container').data([0]);

0 commit comments

Comments
 (0)