Skip to content
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

Merged
merged 16 commits into from
Apr 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/colorbar/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ module.exports = function draw(gd, id) {
anchor: 'free',
position: 1
},
cbAxisOut = {},
cbAxisOut = {
type: 'linear',
_id: 'y' + id
},
axisOptions = {
letter: 'y',
font: fullLayout.font,
Expand All @@ -188,8 +191,6 @@ module.exports = function draw(gd, id) {
handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions, fullLayout);
handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions);

cbAxisOut._id = 'y' + id;

// position can't go in through supplyDefaults
// because that restricts it to [0,1]
cbAxisOut.position = opts.x + xpadFrac + thickFrac;
Expand Down
7 changes: 6 additions & 1 deletion src/constants/numerical.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@ module.exports = {
* For fast conversion btwn world calendars and epoch ms, the Julian Day Number
* of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
*/
EPOCHJD: 2440587.5
EPOCHJD: 2440587.5,

/*
* Are two values nearly equal? Compare to 1PPM
*/
ALMOST_EQUAL: 1 - 1e-6
};
82 changes: 61 additions & 21 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var manageArrays = require('./manage_arrays');
var helpers = require('./helpers');
var subroutines = require('./subroutines');
var cartesianConstants = require('../plots/cartesian/constants');
var enforceAxisConstraints = require('../plots/cartesian/constraints');
var axisIds = require('../plots/cartesian/axis_ids');


/**
Expand Down Expand Up @@ -151,10 +153,6 @@ Plotly.plot = function(gd, data, layout, config) {
makePlotFramework(gd);
}

// save initial axis range once per graph
if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);


// prepare the data and find the autorange

// generate calcdata, if we need to
Expand Down Expand Up @@ -256,18 +254,24 @@ Plotly.plot = function(gd, data, layout, config) {
return Lib.syncOrAsync([
Registry.getComponentMethod('shapes', 'calcAutorange'),
Registry.getComponentMethod('annotations', 'calcAutorange'),
doAutoRange,
doAutoRangeAndConstraints,
Registry.getComponentMethod('rangeslider', 'calcAutorange')
], gd);
}

function doAutoRange() {
function doAutoRangeAndConstraints() {
if(gd._transitioning) return;

var axList = Plotly.Axes.list(gd, '', true);
for(var i = 0; i < axList.length; i++) {
Plotly.Axes.doAutoRange(axList[i]);
}

enforceAxisConstraints(gd);

// store initial ranges *after* enforcing constraints, otherwise
// we will never look like we're at the initial ranges
if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);
}

// draw ticks, titles, and calculate axis scaling (._b, ._m)
Expand Down Expand Up @@ -1857,6 +1861,16 @@ function _relayout(gd, aobj) {
return (ax || {}).autorange;
}

// for constraint enforcement: keep track of all axes (as {id: name})
// we're editing the (auto)range of, so we can tell the others constrained
// to scale with them that it's OK for them to shrink
var rangesAltered = {};

function recordAlteredAxis(pleafPlus) {
var axId = axisIds.name2id(pleafPlus.split('.')[0]);
rangesAltered[axId] = 1;
}

// alter gd.layout
for(var ai in aobj) {
if(helpers.hasParent(aobj, ai)) {
Expand Down Expand Up @@ -1891,15 +1905,17 @@ function _relayout(gd, aobj) {
//
// To do so, we must manually set them back here using the _initialAutoSize cache.
if(['width', 'height'].indexOf(ai) !== -1 && vi === null) {
gd._fullLayout[ai] = gd._initialAutoSize[ai];
fullLayout[ai] = gd._initialAutoSize[ai];
}
// check autorange vs range
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
doextra(ptrunk + '.autorange', false);
recordAlteredAxis(pleafPlus);
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
undefined);
recordAlteredAxis(pleafPlus);
}
else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
doextra(proot + '.aspectmode', 'manual');
Expand Down Expand Up @@ -2063,6 +2079,18 @@ function _relayout(gd, aobj) {
else if(proot.indexOf('geo') === 0) flags.doplot = true;
else if(proot.indexOf('ternary') === 0) flags.doplot = true;
else if(ai === 'paper_bgcolor') flags.doplot = true;
else if(proot === 'margin' ||
pp1 === 'autorange' ||
pp1 === 'rangemode' ||
pp1 === 'type' ||
pp1 === 'domain' ||
pp1 === 'fixedrange' ||
pp1 === 'scaleanchor' ||
pp1 === 'scaleratio' ||
ai.indexOf('calendar') !== -1 ||
ai.match(/^(bar|box|font)/)) {
flags.docalc = true;
}
Copy link
Collaborator Author

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, so scaleanchor/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?).

Copy link
Contributor

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.

else if(fullLayout._has('gl2d') &&
(ai.indexOf('axis') !== -1 || ai === 'plot_bgcolor')
) flags.doplot = true;
Expand All @@ -2086,15 +2114,6 @@ function _relayout(gd, aobj) {
else if(ai === 'margin.pad') {
flags.doticks = flags.dolayoutstyle = true;
}
else if(proot === 'margin' ||
pp1 === 'autorange' ||
pp1 === 'rangemode' ||
pp1 === 'type' ||
pp1 === 'domain' ||
ai.indexOf('calendar') !== -1 ||
ai.match(/^(bar|box|font)/)) {
flags.docalc = true;
}
/*
* hovermode and dragmode don't need any redrawing, since they just
* affect reaction to user input, everything else, assume full replot.
Expand All @@ -2118,16 +2137,37 @@ function _relayout(gd, aobj) {
if(!finished) flags.doplot = true;
}

var oldWidth = gd._fullLayout.width,
oldHeight = gd._fullLayout.height;
// figure out if we need to recalculate axis constraints
var constraints = fullLayout._axisConstraintGroups;
for(var axId in rangesAltered) {
for(i = 0; i < constraints.length; i++) {
var group = constraints[i];
if(group[axId]) {
// Always recalc if we're changing constrained ranges.
// Otherwise it's possible to violate the constraints by
// specifying arbitrary ranges for all axes in the group.
// this way some ranges may expand beyond what's specified,
// as they do at first draw, to satisfy the constraints.
flags.docalc = true;
for(var groupAxId in group) {
if(!rangesAltered[groupAxId]) {
axisIds.getFromId(gd, groupAxId)._constraintShrinkable = true;
}
}
}
}
}

var oldWidth = fullLayout.width,
oldHeight = fullLayout.height;

// calculate autosizing
if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout);
if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, fullLayout);

// avoid unnecessary redraws
var hasSizechanged = aobj.height || aobj.width ||
(gd._fullLayout.width !== oldWidth) ||
(gd._fullLayout.height !== oldHeight);
(fullLayout.width !== oldWidth) ||
(fullLayout.height !== oldHeight);

if(hasSizechanged) flags.docalc = true;

Expand Down
112 changes: 2 additions & 110 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ var handleTickLabelDefaults = require('./tick_label_defaults');
var handleCategoryOrderDefaults = require('./category_order_defaults');
var setConvert = require('./set_convert');
var orderedCategories = require('./ordered_categories');
var axisIds = require('./axis_ids');
var autoType = require('./axis_autotype');


/**
* options: object containing:
*
* letter: 'x' or 'y'
* title: name of the axis (ie 'Colorbar') to go in default title
* name: axis object name (ie 'xaxis') if one should be stored
* font: the default font to inherit
* outerTicks: boolean, should ticks default to outside?
* showGrid: boolean, should gridlines be shown by default?
* noHover: boolean, this axis doesn't support hover effects?
* data: the plot data to use in choosing auto type
* data: the plot data, used to manage categories
* bgColor: the plot background color, to calculate default gridline colors
*/
module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options, layoutOut) {
Expand All @@ -50,28 +47,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt);
}

// set up some private properties
if(options.name) {
containerOut._name = options.name;
containerOut._id = axisIds.name2id(options.name);
}

// now figure out type and do some more initialization
var axType = coerce('type');
if(axType === '-') {
setAutoType(containerOut, options.data);

if(containerOut.type === '-') {
containerOut.type = 'linear';
}
else {
// copy autoType back to input axis
// note that if this object didn't exist
// in the input layout, we have to put it in
// this happens in the main supplyDefaults function
axType = containerIn.type = containerOut.type;
}
}
var axType = containerOut.type;

if(axType === 'date') {
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults');
Expand Down Expand Up @@ -140,87 +116,3 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,

return containerOut;
};

function setAutoType(ax, data) {
// new logic: let people specify any type they want,
// only autotype if type is '-'
if(ax.type !== '-') return;

var id = ax._id,
axLetter = id.charAt(0);

// support 3d
if(id.indexOf('scene') !== -1) id = axLetter;

var d0 = getFirstNonEmptyTrace(data, id, axLetter);
if(!d0) return;

// first check for histograms, as the count direction
// should always default to a linear axis
if(d0.type === 'histogram' &&
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
ax.type = 'linear';
return;
}

var calAttr = axLetter + 'calendar',
calendar = d0[calAttr];

// check all boxes on this x axis to see
// if they're dates, numbers, or categories
if(isBoxWithoutPositionCoords(d0, axLetter)) {
var posLetter = getBoxPosLetter(d0),
boxPositions = [],
trace;

for(var i = 0; i < data.length; i++) {
trace = data[i];
if(!Registry.traceIs(trace, 'box') ||
(trace[axLetter + 'axis'] || axLetter) !== id) continue;

if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
else if(trace.name !== undefined) boxPositions.push(trace.name);
else boxPositions.push('text');

if(trace[calAttr] !== calendar) calendar = undefined;
}

ax.type = autoType(boxPositions, calendar);
}
else {
ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar);
}
}

function getBoxPosLetter(trace) {
return {v: 'x', h: 'y'}[trace.orientation || 'v'];
}

function isBoxWithoutPositionCoords(trace, axLetter) {
var posLetter = getBoxPosLetter(trace),
isBox = Registry.traceIs(trace, 'box'),
isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');

return (
isBox &&
!isCandlestick &&
axLetter === posLetter &&
trace[posLetter] === undefined &&
trace[posLetter + '0'] === undefined
);
}

function getFirstNonEmptyTrace(data, id, axLetter) {
for(var i = 0; i < data.length; i++) {
var trace = data[i];

if((trace[axLetter + 'axis'] || axLetter) === id) {
if(isBoxWithoutPositionCoords(trace, axLetter)) {
return trace;
}
else if((trace[axLetter] || []).length || trace[axLetter + '0']) {
return trace;
}
}
}
}
Loading