Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 5 additions & 2 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ 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');


/**
Expand Down Expand Up @@ -256,18 +257,20 @@ 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);
}

// draw ticks, titles, and calculate axis scaling (._b, ._m)
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;
}
}
}
}
128 changes: 128 additions & 0 deletions src/plots/cartesian/constraint_defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

var Lib = require('../../lib');
var id2name = require('./axis_ids').id2name;


module.exports = function handleConstraintDefaults(containerIn, containerOut, coerce, counterAxes, layoutOut) {
var constraintGroups = layoutOut._axisConstraintGroups;

if(!containerIn.scalewith) return;

var constraintOpts = getConstraintOpts(constraintGroups, containerOut._id, counterAxes, layoutOut);

var scalewith = Lib.coerce(containerIn, containerOut, {
scalewith: {
valType: 'enumerated',
values: constraintOpts.linkableAxes
}
}, 'scalewith');

if(scalewith) {
var scaleratio = coerce('scaleratio');
// TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero,
// but that seems hacky. Better way to say "must be a positive number"?
// Of course if you use several super-tiny values you could eventually
// force a product of these to zero and all hell would break loose...
// Likewise with super-huge values.
if(!scaleratio) scaleratio = containerOut.scaleratio = 1;

updateConstraintGroups(constraintGroups, constraintOpts.thisGroup,
containerOut._id, scalewith, scaleratio);
}
else if(counterAxes.indexOf(containerIn.scalewith) !== -1) {
Lib.warn('ignored ' + containerOut._name + '.scalewith: "' +
containerIn.scalewith + '" to avoid an infinite loop ' +
'and possibly inconsistent scaleratios.');
}
};

function getConstraintOpts(constraintGroups, thisID, counterAxes, layoutOut) {
// If this axis is already part of a constraint group, we can't
// scalewith any other axis in that group, or we'd make a loop.
// Filter counterAxes to enforce this, also matching axis types.

var thisType = layoutOut[id2name(thisID)].type;

var i, j, idj;
for(i = 0; i < constraintGroups.length; i++) {
if(constraintGroups[i][thisID]) {
var thisGroup = constraintGroups[i];

var linkableAxes = [];
for(j = 0; j < counterAxes.length; j++) {
idj = counterAxes[j];
if(!thisGroup[idj] && layoutOut[id2name(idj)].type === thisType) {
linkableAxes.push(idj);
}
}
return {linkableAxes: linkableAxes, thisGroup: thisGroup};
}
}

return {linkableAxes: counterAxes, thisGroup: null};
}


/*
* Add this axis to the axis constraint groups, which is the collection
* of axes that are all constrained together on scale.
*
* constraintGroups: a list of objects. each object is
* {axis_id: scale_within_group}, where scale_within_group is
* only important relative to the rest of the group, and defines
* the relative scales between all axes in the group
*
* thisGroup: the group the current axis is already in
* thisID: the id if the current axis
* scalewith: the id of the axis to scale it with
* scaleratio: the ratio of this axis to the scalewith axis
*/
function updateConstraintGroups(constraintGroups, thisGroup, thisID, scalewith, scaleratio) {
var i, j, groupi, keyj, thisGroupIndex;

if(thisGroup === null) {
thisGroup = {};
thisGroup[thisID] = 1;
thisGroupIndex = constraintGroups.length;
constraintGroups.push(thisGroup);
}
else {
thisGroupIndex = constraintGroups.indexOf(thisGroup);
}

var thisGroupKeys = Object.keys(thisGroup);

// we know that this axis isn't in any other groups, but we don't know
// about the scalewith axis. If it is, we need to merge the groups.
for(i = 0; i < constraintGroups.length; i++) {
groupi = constraintGroups[i];
if(i !== thisGroupIndex && groupi[scalewith]) {
var baseScale = groupi[scalewith];
for(j = 0; j < thisGroupKeys.length; j++) {
keyj = thisGroupKeys[j];
groupi[keyj] = baseScale * scaleratio * thisGroup[keyj];
}
constraintGroups.splice(thisGroupIndex, 1);
return;
}
}

// otherwise, we insert the new scalewith axis as the base scale (1)
// in its group, and scale the rest of the group to it
if(scaleratio !== 1) {
for(j = 0; j < thisGroupKeys.length; j++) {
thisGroup[thisGroupKeys[j]] *= scaleratio;
}
}
thisGroup[scalewith] = 1;
}
67 changes: 67 additions & 0 deletions src/plots/cartesian/constraints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

var id2name = require('./axis_ids').id2name;

var ALMOST_EQUAL = 1 - 1e-6;


module.exports = function enforceAxisConstraints(gd) {
var fullLayout = gd._fullLayout;
var layout = gd.layout;
var constraintGroups = fullLayout._axisConstraintGroups;

var i, j, axisID, ax, normScale;

for(i = 0; i < constraintGroups.length; i++) {
var group = constraintGroups[i];
var axisIDs = Object.keys(group);

var minScale = Infinity;
var maxScale = 0;
var normScales = {};
var axes = {};

// find the (normalized) scale of each axis in the group
for(j = 0; j < axisIDs.length; j++) {
axisID = axisIDs[j];
axes[axisID] = ax = fullLayout[id2name(axisID)];

// set axis scale here so we can use _m rather than
// having to calculate it from length and range
ax.setScale();

// abs: inverted scales still satisfy the constraint
normScales[axisID] = normScale = Math.abs(ax._m) / group[axisID];
minScale = Math.min(minScale, normScale);
maxScale = Math.max(maxScale, normScale);
}

// Do we have a constraint mismatch? Give a small buffer for rounding errors
if(minScale > ALMOST_EQUAL * maxScale) continue;

// now increase any ranges we need to until all normalized scales are equal
for(j = 0; j < axisIDs.length; j++) {
axisID = axisIDs[j];
normScale = normScales[axisID];
if(normScale > minScale) {
ax = axes[axisID];
var rangeLinear = [ax.r2l(ax.range[0]), ax.r2l(ax.range[1])];
var center = (rangeLinear[0] + rangeLinear[1]) / 2;
var newHalfSpan = (center - rangeLinear[0]) * normScale / minScale;
ax.range = layout[id2name(axisID)].range = [
ax.l2r(center - newHalfSpan),
ax.l2r(center + newHalfSpan)
];
}
}
}
};
Loading