-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Axis constraints #1522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Axis constraints #1522
Changes from 1 commit
e9a89c2
d9a8ab7
6c60a32
98900a2
84d95a5
f521227
13f848e
dee136d
ef3ca48
25c2cfb
86e0d5a
745b953
7fc9cbd
ffd65fd
b4d11e6
23859e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
|
||
var mouseChange = require('mouse-change'); | ||
var mouseWheel = require('mouse-wheel'); | ||
var cartesianConstants = require('../cartesian/constants'); | ||
|
||
module.exports = createCamera; | ||
|
||
|
@@ -22,8 +23,10 @@ function Camera2D(element, plot) { | |
this.lastInputTime = Date.now(); | ||
this.lastPos = [0, 0]; | ||
this.boxEnabled = false; | ||
this.boxInited = false; | ||
this.boxStart = [0, 0]; | ||
this.boxEnd = [0, 0]; | ||
this.dragStart = [0, 0]; | ||
} | ||
|
||
|
||
|
@@ -37,13 +40,33 @@ function createCamera(scene) { | |
scene.yaxis.autorange = false; | ||
} | ||
|
||
function getSubplotConstraint() { | ||
// note: this assumes we only have one x and one y axis on this subplot | ||
// when this constraint is lifted this block won't make sense | ||
var constraints = scene.graphDiv._fullLayout._axisConstraintGroups; | ||
var xaId = scene.xaxis._id; | ||
var yaId = scene.yaxis._id; | ||
for(var i = 0; i < constraints.length; i++) { | ||
if(constraints[i][xaId] !== -1) { | ||
if(constraints[i][yaId] !== -1) return true; | ||
break; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
result.mouseListener = mouseChange(element, function(buttons, x, y) { | ||
var dataBox = scene.calcDataBox(), | ||
viewBox = plot.viewBox; | ||
|
||
var lastX = result.lastPos[0], | ||
lastY = result.lastPos[1]; | ||
|
||
var MINDRAG = cartesianConstants.MINDRAG * plot.pixelRatio; | ||
var MINZOOM = cartesianConstants.MINZOOM * plot.pixelRatio; | ||
|
||
var dx, dy; | ||
|
||
x *= plot.pixelRatio; | ||
y *= plot.pixelRatio; | ||
|
||
|
@@ -76,32 +99,114 @@ function createCamera(scene) { | |
(viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + | ||
dataBox[1]; | ||
|
||
if(!result.boxEnabled) { | ||
if(!result.boxInited) { | ||
result.boxStart[0] = dataX; | ||
result.boxStart[1] = dataY; | ||
result.dragStart[0] = x; | ||
result.dragStart[1] = y; | ||
} | ||
|
||
result.boxEnd[0] = dataX; | ||
result.boxEnd[1] = dataY; | ||
|
||
result.boxEnabled = true; | ||
// we need to mark the box as initialized right away | ||
// so that we can tell the start and end pionts apart | ||
result.boxInited = true; | ||
|
||
// but don't actually enable the box until the cursor moves | ||
if(!result.boxEnabled && ( | ||
result.boxStart[0] !== result.boxEnd[0] || | ||
result.boxStart[1] !== result.boxEnd[1]) | ||
) { | ||
result.boxEnabled = true; | ||
} | ||
|
||
// constrain aspect ratio if the axes require it | ||
var smallDx = Math.abs(result.dragStart[0] - x) < MINZOOM; | ||
var smallDy = Math.abs(result.dragStart[1] - y) < MINZOOM; | ||
if(getSubplotConstraint() && !(smallDx && smallDy)) { | ||
dx = result.boxEnd[0] - result.boxStart[0]; | ||
dy = result.boxEnd[1] - result.boxStart[1]; | ||
var dydx = (dataBox[3] - dataBox[1]) / (dataBox[2] - dataBox[0]); | ||
|
||
if(Math.abs(dx * dydx) > Math.abs(dy)) { | ||
result.boxEnd[1] = result.boxStart[1] + | ||
Math.abs(dx) * dydx * (Math.sign(dy) || 1); | ||
|
||
// gl-select-box clips to the plot area bounds, | ||
// which breaks the axis constraint, so don't allow | ||
// this box to go out of bounds | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in contrast, svg lets you make these constrained boxes extend outside the plot area. Didn't seem worth diving into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. No need to do this for now 👍 |
||
if(result.boxEnd[1] < dataBox[1]) { | ||
result.boxEnd[1] = dataBox[1]; | ||
result.boxEnd[0] = result.boxStart[0] + | ||
(dataBox[1] - result.boxStart[1]) / Math.abs(dydx); | ||
} | ||
else if(result.boxEnd[1] > dataBox[3]) { | ||
result.boxEnd[1] = dataBox[3]; | ||
result.boxEnd[0] = result.boxStart[0] + | ||
(dataBox[3] - result.boxStart[1]) / Math.abs(dydx); | ||
} | ||
} | ||
else { | ||
result.boxEnd[0] = result.boxStart[0] + | ||
Math.abs(dy) / dydx * (Math.sign(dx) || 1); | ||
|
||
if(result.boxEnd[0] < dataBox[0]) { | ||
result.boxEnd[0] = dataBox[0]; | ||
result.boxEnd[1] = result.boxStart[1] + | ||
(dataBox[0] - result.boxStart[0]) * Math.abs(dydx); | ||
} | ||
else if(result.boxEnd[0] > dataBox[2]) { | ||
result.boxEnd[0] = dataBox[2]; | ||
result.boxEnd[1] = result.boxStart[1] + | ||
(dataBox[2] - result.boxStart[0]) * Math.abs(dydx); | ||
} | ||
} | ||
} | ||
// otherwise clamp small changes to the origin so we get 1D zoom | ||
else { | ||
if(smallDx) result.boxEnd[0] = result.boxStart[0]; | ||
if(smallDy) result.boxEnd[1] = result.boxStart[1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard while I was at it I added in a few of the bits of behavior used in svg, such as 1D zoom, minimum zoom size, and clamping panning to pure x (y) if dy (dx) is small enough. Do you want to take it for a spin and see what you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loving it. Thanks! |
||
} | ||
} | ||
else if(result.boxEnabled) { | ||
updateRange(0, result.boxStart[0], result.boxEnd[0]); | ||
updateRange(1, result.boxStart[1], result.boxEnd[1]); | ||
unSetAutoRange(); | ||
dx = result.boxStart[0] !== result.boxEnd[0]; | ||
dy = result.boxStart[1] !== result.boxEnd[1]; | ||
if(dx || dy) { | ||
if(dx) { | ||
updateRange(0, result.boxStart[0], result.boxEnd[0]); | ||
scene.xaxis.autorange = false; | ||
} | ||
if(dy) { | ||
updateRange(1, result.boxStart[1], result.boxEnd[1]); | ||
scene.yaxis.autorange = false; | ||
} | ||
scene.relayoutCallback(); | ||
} | ||
else { | ||
scene.glplot.setDirty(); | ||
} | ||
result.boxEnabled = false; | ||
scene.relayoutCallback(); | ||
result.boxInited = false; | ||
} | ||
break; | ||
|
||
case 'pan': | ||
result.boxEnabled = false; | ||
result.boxInited = false; | ||
|
||
if(buttons) { | ||
var dx = (lastX - x) * (dataBox[2] - dataBox[0]) / | ||
if(!result.panning) { | ||
result.dragStart[0] = x; | ||
result.dragStart[1] = y; | ||
} | ||
|
||
if(Math.abs(result.dragStart[0] - x) < MINDRAG) x = result.dragStart[0]; | ||
if(Math.abs(result.dragStart[1] - y) < MINDRAG) y = result.dragStart[1]; | ||
|
||
dx = (lastX - x) * (dataBox[2] - dataBox[0]) / | ||
(plot.viewBox[2] - plot.viewBox[0]); | ||
var dy = (lastY - y) * (dataBox[3] - dataBox[1]) / | ||
dy = (lastY - y) * (dataBox[3] - dataBox[1]) / | ||
(plot.viewBox[3] - plot.viewBox[1]); | ||
|
||
dataBox[0] += dx; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ var createOptions = require('./convert'); | |
var createCamera = require('./camera'); | ||
var convertHTMLToUnicode = require('../../lib/html2unicode'); | ||
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); | ||
var enforceAxisConstraints = require('../../plots/cartesian/constraints'); | ||
|
||
var AXES = ['xaxis', 'yaxis']; | ||
var STATIC_CANVAS, STATIC_CONTEXT; | ||
|
@@ -426,6 +427,13 @@ proto.plot = function(fullData, calcData, fullLayout) { | |
ax.setScale(); | ||
} | ||
|
||
var mockLayout = { | ||
_axisConstraintGroups: this.graphDiv._fullLayout._axisConstraintGroups, | ||
xaxis: this.xaxis, | ||
yaxis: this.yaxis | ||
}; | ||
enforceAxisConstraints({_fullLayout: mockLayout}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kind of hacky, but A bunch of things here would break if/when we support overlaid axes... |
||
|
||
options.ticks = this.computeTickMarks(); | ||
|
||
options.dataBox = this.calcDataBox(); | ||
|
@@ -544,26 +552,36 @@ proto.draw = function() { | |
var x = mouseListener.x * glplot.pixelRatio; | ||
var y = this.canvas.height - glplot.pixelRatio * mouseListener.y; | ||
|
||
var result; | ||
|
||
if(camera.boxEnabled && fullLayout.dragmode === 'zoom') { | ||
this.selectBox.enabled = true; | ||
|
||
this.selectBox.selectBox = [ | ||
var selectBox = this.selectBox.selectBox = [ | ||
Math.min(camera.boxStart[0], camera.boxEnd[0]), | ||
Math.min(camera.boxStart[1], camera.boxEnd[1]), | ||
Math.max(camera.boxStart[0], camera.boxEnd[0]), | ||
Math.max(camera.boxStart[1], camera.boxEnd[1]) | ||
]; | ||
|
||
// 1D zoom | ||
for(var i = 0; i < 2; i++) { | ||
if(camera.boxStart[i] === camera.boxEnd[i]) { | ||
selectBox[i] = glplot.dataBox[i]; | ||
selectBox[i + 2] = glplot.dataBox[i + 2]; | ||
} | ||
} | ||
|
||
glplot.setDirty(); | ||
} | ||
else { | ||
else if(!camera.panning) { | ||
this.selectBox.enabled = false; | ||
|
||
var size = fullLayout._size, | ||
domainX = this.xaxis.domain, | ||
domainY = this.yaxis.domain; | ||
|
||
var result = glplot.pick( | ||
result = glplot.pick( | ||
(x / glplot.pixelRatio) + size.l + domainX[0] * size.w, | ||
(y / glplot.pixelRatio) - (size.t + (1 - domainY[1]) * size.h) | ||
); | ||
|
@@ -629,12 +647,15 @@ proto.draw = function() { | |
}); | ||
} | ||
} | ||
else if(!result && this.lastPickResult) { | ||
this.spikes.update({}); | ||
this.lastPickResult = null; | ||
this.graphDiv.emit('plotly_unhover'); | ||
Fx.loneUnhover(this.svgContainer); | ||
} | ||
} | ||
|
||
// Remove hover effects if we're not over a point OR | ||
// if we're zooming or panning (in which case result is not set) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also to match svg There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice. Thanks 👍 |
||
if(!result && this.lastPickResult) { | ||
this.spikes.update({}); | ||
this.lastPickResult = null; | ||
this.graphDiv.emit('plotly_unhover'); | ||
Fx.loneUnhover(this.svgContainer); | ||
} | ||
|
||
glplot.draw(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to move this block above the following
_has('gl2d')
block, soscaleanchor/scaleratio
get the necessary recalc. Seems like as a general rule, we might want to reorder these from biggest change to smallest, ie all the recalcs, then all the replots, then the ticks & styles... if we don't find an altogether better way to manage these flags (via the schema?).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the idea of adding a
recalc
true flag to the attributes that require a recalc on restyle / relayout. But that can wait. We should do this in one PR crossing off #648.