Skip to content

Commit

Permalink
Merge pull request #5193 from Displayr/888-style-transforms
Browse files Browse the repository at this point in the history
Align interactions when plot created inside scaled and/or translated elements using CSS transform
  • Loading branch information
archmoj authored Nov 18, 2020
2 parents a382098 + 95a81fc commit 4afc441
Show file tree
Hide file tree
Showing 24 changed files with 2,187 additions and 1,230 deletions.
12 changes: 7 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 35 additions & 25 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ exports.loneHover = function loneHover(hoverItems, opts) {
d.offset -= anchor;
});

alignHoverText(hoverLabel, fullOpts.rotateLabels);
var scaleX = opts.gd._fullLayout._inverseScaleX;
var scaleY = opts.gd._fullLayout._inverseScaleY;
alignHoverText(hoverLabel, fullOpts.rotateLabels, scaleX, scaleY);

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

var transformedCoords = Lib.apply3DTransform(fullLayout._inverseTransform)(xpx, ypx);

xpx = transformedCoords[0];
ypx = transformedCoords[1];

// in case hover was called from mouseout into hovertext,
// it's possible you're not actually over the plot anymore
if(xpx < 0 || xpx > xaArray[0]._length || ypx < 0 || ypx > yaArray[0]._length) {
Expand Down Expand Up @@ -718,10 +725,8 @@ function _hover(gd, evt, subplot, noHoverEvent) {

if(!helpers.isUnifiedHover(hovermode)) {
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
alignHoverText(hoverLabels, rotateLabels);
}

// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
alignHoverText(hoverLabels, rotateLabels, fullLayout._inverseScaleX, fullLayout._inverseScaleY);
} // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
// we should improve the "fx" API so other plots can use it without these hack.
if(evt.target && evt.target.tagName) {
var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
Expand Down Expand Up @@ -1479,7 +1484,10 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
}
}

function alignHoverText(hoverLabels, rotateLabels) {
function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
var pX = function(x) { return x * scaleX; };
var pY = function(y) { return y * scaleY; };

// finally set the text positioning relative to the data and draw the
// box around it
hoverLabels.each(function(d) {
Expand All @@ -1495,7 +1503,8 @@ function alignHoverText(hoverLabels, rotateLabels) {
var offsetX = 0;
var offsetY = d.offset;

if(anchor === 'middle') {
var isMiddle = anchor === 'middle';
if(isMiddle) {
txx -= d.tx2width / 2;
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
}
Expand All @@ -1504,49 +1513,50 @@ function alignHoverText(hoverLabels, rotateLabels) {
offsetX = d.offset * YSHIFTX;
}

g.select('path').attr('d', anchor === 'middle' ?
g.select('path')
.attr('d', isMiddle ?
// middle aligned: rect centered on data
('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) +
'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') :
('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) +
'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') :
// left or right aligned: side rect with arrow to data
('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) +
'v' + (d.by / 2 - HOVERARROWSIZE) +
'h' + (horzSign * d.bx) +
'v-' + d.by +
'H' + (horzSign * HOVERARROWSIZE + offsetX) +
'V' + (offsetY - HOVERARROWSIZE) +
('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) +
'v' + pY(d.by / 2 - HOVERARROWSIZE) +
'h' + pX(horzSign * d.bx) +
'v-' + pY(d.by) +
'H' + pX(horzSign * HOVERARROWSIZE + offsetX) +
'V' + pY(offsetY - HOVERARROWSIZE) +
'Z'));

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

if(textAlign !== 'auto') {
if(textAlign === 'left' && anchor !== 'start') {
tx.attr('text-anchor', 'start');
posX = anchor === 'middle' ?
posX = isMiddle ?
-d.bx / 2 - d.tx2width / 2 + HOVERTEXTPAD :
-d.bx - HOVERTEXTPAD;
} else if(textAlign === 'right' && anchor !== 'end') {
tx.attr('text-anchor', 'end');
posX = anchor === 'middle' ?
posX = isMiddle ?
d.bx / 2 - d.tx2width / 2 - HOVERTEXTPAD :
d.bx + HOVERTEXTPAD;
}
}

tx.call(svgTextUtils.positionText, posX, posY);
tx.call(svgTextUtils.positionText, pX(posX), pY(posY));

if(d.tx2width) {
g.select('text.name')
.call(svgTextUtils.positionText,
tx2x + alignShift * HOVERTEXTPAD + offsetX,
offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD);
pX(tx2x + alignShift * HOVERTEXTPAD + offsetX),
pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD));
g.select('rect')
.call(Drawing.setRect,
tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX,
offsetY - d.by / 2 - 1,
d.tx2width, d.by + 2);
pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX),
pY(offsetY - d.by / 2 - 1),
pX(d.tx2width), pY(d.by + 2));
}
});
}
Expand Down
64 changes: 63 additions & 1 deletion src/lib/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

var d3 = require('d3');
var loggers = require('./loggers');
var matrix = require('./matrix');
var mat4X4 = require('gl-mat4');

/**
* Allow referencing a graph DOM element either directly
Expand Down Expand Up @@ -91,11 +93,71 @@ function deleteRelatedStyleRule(uid) {
if(style) removeElement(style);
}

function getFullTransformMatrix(element) {
var allElements = getElementAndAncestors(element);
// the identity matrix
var out = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
allElements.forEach(function(e) {
var t = getElementTransformMatrix(e);
if(t) {
var m = matrix.convertCssMatrix(t);
out = mat4X4.multiply(out, out, m);
}
});
return out;
}

/**
* extracts and parses the 2d css style transform matrix from some element
*/
function getElementTransformMatrix(element) {
var style = window.getComputedStyle(element, null);
var transform = (
style.getPropertyValue('-webkit-transform') ||
style.getPropertyValue('-moz-transform') ||
style.getPropertyValue('-ms-transform') ||
style.getPropertyValue('-o-transform') ||
style.getPropertyValue('transform')
);

if(transform === 'none') return null;
// the transform is a string in the form of matrix(a, b, ...) or matrix3d(...)
return transform
.replace('matrix', '')
.replace('3d', '')
.slice(1, -1)
.split(',')
.map(function(n) { return +n; });
}
/**
* retrieve all DOM elements that are ancestors of the specified one (including itself)
*/
function getElementAndAncestors(element) {
var allElements = [];
while(isTransformableElement(element)) {
allElements.push(element);
element = element.parentNode;
}
return allElements;
}

function isTransformableElement(element) {
return element && (element instanceof Element || element instanceof HTMLElement);
}

module.exports = {
getGraphDiv: getGraphDiv,
isPlotDiv: isPlotDiv,
removeElement: removeElement,
addStyleRule: addStyleRule,
addRelatedStyleRule: addRelatedStyleRule,
deleteRelatedStyleRule: deleteRelatedStyleRule
deleteRelatedStyleRule: deleteRelatedStyleRule,
getFullTransformMatrix: getFullTransformMatrix,
getElementTransformMatrix: getElementTransformMatrix,
getElementAndAncestors: getElementAndAncestors,
};
6 changes: 6 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ lib.dot = matrixModule.dot;
lib.translationMatrix = matrixModule.translationMatrix;
lib.rotationMatrix = matrixModule.rotationMatrix;
lib.rotationXYMatrix = matrixModule.rotationXYMatrix;
lib.apply3DTransform = matrixModule.apply3DTransform;
lib.apply2DTransform = matrixModule.apply2DTransform;
lib.apply2DTransform2 = matrixModule.apply2DTransform2;
lib.convertCssMatrix = matrixModule.convertCssMatrix;
lib.inverseTransformMatrix = matrixModule.inverseTransformMatrix;

var anglesModule = require('./angles');
lib.deg2rad = anglesModule.deg2rad;
Expand Down Expand Up @@ -145,6 +148,9 @@ lib.removeElement = domModule.removeElement;
lib.addStyleRule = domModule.addStyleRule;
lib.addRelatedStyleRule = domModule.addRelatedStyleRule;
lib.deleteRelatedStyleRule = domModule.deleteRelatedStyleRule;
lib.getFullTransformMatrix = domModule.getFullTransformMatrix;
lib.getElementTransformMatrix = domModule.getElementTransformMatrix;
lib.getElementAndAncestors = domModule.getElementAndAncestors;

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

Expand Down
47 changes: 46 additions & 1 deletion src/lib/matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

'use strict';

var mat4X4 = require('gl-mat4');

exports.init2dArray = function(rowLength, colLength) {
var array = new Array(rowLength);
Expand Down Expand Up @@ -84,13 +85,23 @@ exports.rotationXYMatrix = function(a, x, y) {
exports.translationMatrix(-x, -y));
};

// applies a 3D transformation matrix to either x, y and z params
// Note: z is optional
exports.apply3DTransform = function(transform) {
return function() {
var args = arguments;
var xyz = arguments.length === 1 ? args[0] : [args[0], args[1], args[2] || 0];
return exports.dot(transform, [xyz[0], xyz[1], xyz[2], 1]).slice(0, 3);
};
};

// applies a 2D transformation matrix to either x and y params or an [x,y] array
exports.apply2DTransform = function(transform) {
return function() {
var args = arguments;
if(args.length === 3) {
args = args[0];
}// from map
} // from map
var xy = arguments.length === 1 ? args[0] : [args[0], args[1]];
return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2);
};
Expand All @@ -103,3 +114,37 @@ exports.apply2DTransform2 = function(transform) {
return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4)));
};
};

exports.convertCssMatrix = function(m) {
if(m) {
var len = m.length;
if(len === 16) return m;
if(len === 6) {
// converts a 2x3 css transform matrix to a 4x4 matrix see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
return [
m[0], m[1], 0, 0,
m[2], m[3], 0, 0,
0, 0, 1, 0,
m[4], m[5], 0, 1
];
}
}
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
};

// find the inverse for a 4x4 affine transform matrix
exports.inverseTransformMatrix = function(m) {
var out = [];
mat4X4.invert(out, m);
return [
[out[0], out[1], out[2], out[3]],
[out[4], out[5], out[6], out[7]],
[out[8], out[9], out[10], out[11]],
[out[12], out[13], out[14], out[15]]
];
};
12 changes: 10 additions & 2 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,9 +744,17 @@ function alignHTMLWith(_base, container, options) {

return function() {
thisRect = this.node().getBoundingClientRect();

var x0 = getLeft() - cRect.left;
var y0 = getTop() - cRect.top;
var gd = options.gd || {};
var transformedCoords = Lib.apply3DTransform(gd._fullLayout._inverseTransform)(x0, y0);
x0 = transformedCoords[0];
y0 = transformedCoords[1];

this.style({
top: (getTop() - cRect.top) + 'px',
left: (getLeft() - cRect.left) + 'px',
top: y0 + 'px',
left: x0 + 'px',
'z-index': 1000
});
return this;
Expand Down
5 changes: 5 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3714,6 +3714,11 @@ function purge(gd) {
function makePlotFramework(gd) {
var gd3 = d3.select(gd);
var fullLayout = gd._fullLayout;
if(fullLayout._inverseTransform === undefined) {
var m = fullLayout._inverseTransform = Lib.inverseTransformMatrix(Lib.getFullTransformMatrix(gd));
fullLayout._inverseScaleX = Math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]);
fullLayout._inverseScaleY = Math.sqrt(m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]);
}

// Plot container
fullLayout._container = gd3.selectAll('.plot-container').data([0]);
Expand Down
Loading

0 comments on commit 4afc441

Please sign in to comment.