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

Array edits #1403

Merged
merged 18 commits into from
Feb 24, 2017
Merged
Show file tree
Hide file tree
Changes from 8 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
59 changes: 59 additions & 0 deletions src/components/annotations/convert_coords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* 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 isNumeric = require('fast-isnumeric');
var toLogRange = require('../../lib/to_log_range');

/*
* convertCoords: when converting an axis between log and linear
* you need to alter any annotations on that axis to keep them
* pointing at the same data point.
* In v2.0 this will become obsolete
*
* gd: the plot div
* ax: the axis being changed
* newType: the type it's getting
* doExtra: function(attr, val) from inside relayout that sets the attribute.
* Use this to make the changes as it's aware if any other changes in the
* same relayout call should override this conversion.
*/
module.exports = function convertCoords(gd, ax, newType, doExtra) {
var toLog = (newType === 'log') && (ax.type === 'linear'),
fromLog = (newType === 'linear') && (ax.type === 'log');

if(!(toLog || fromLog)) return;

var annotations = gd._fullLayout.annotations,
axLetter = ax._id.charAt(0),
ann,
attrPrefix;

function convert(attr) {
var currentVal = ann[attr],
newVal = null;

if(toLog) newVal = toLogRange(currentVal, ax.range);
else newVal = Math.pow(10, currentVal);

// if conversion failed, delete the value so it gets a default value
if(!isNumeric(newVal)) newVal = null;

doExtra(attrPrefix + attr, newVal);
}

for(var i = 0; i < annotations.length; i++) {
ann = annotations[i];
attrPrefix = 'annotations[' + i + '].';

if(ann[axLetter + 'ref'] === ax._id) convert(axLetter);
if(ann['a' + axLetter + 'ref'] === ax._id) convert('a' + axLetter);
}
};
201 changes: 14 additions & 187 deletions src/components/annotations/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
'use strict';

var d3 = require('d3');
var isNumeric = require('fast-isnumeric');

var Plotly = require('../../plotly');
var Plots = require('../../plots/plots');
Expand All @@ -22,8 +21,6 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var setCursor = require('../../lib/setcursor');
var dragElement = require('../dragelement');

var handleAnnotationDefaults = require('./annotation_defaults');
var supplyLayoutDefaults = require('./defaults');
var drawArrowHead = require('./draw_arrow_head');


Expand All @@ -41,6 +38,9 @@ module.exports = {
drawOne: drawOne
};

/*
* draw: draw all annotations without any new modifications
*/
function draw(gd) {
var fullLayout = gd._fullLayout;

Expand All @@ -55,196 +55,26 @@ function draw(gd) {
return Plots.previousPromises(gd);
}

function drawOne(gd, index, opt, value) {
/*
* drawOne: draw a single annotation, potentially with modifications
*
* index (int): the annotation to draw
*/
function drawOne(gd, index) {
var layout = gd.layout,
fullLayout = gd._fullLayout,
i;

if(!isNumeric(index) || index === -1) {

// no index provided - we're operating on ALL annotations
if(!index && Array.isArray(value)) {
// a whole annotation array is passed in
// (as in, redo of delete all)
layout.annotations = value;
supplyLayoutDefaults(layout, fullLayout);
draw(gd);
return;
}
else if(value === 'remove') {
// delete all
delete layout.annotations;
fullLayout.annotations = [];
draw(gd);
return;
}
else if(opt && value !== 'add') {
// make the same change to all annotations
for(i = 0; i < fullLayout.annotations.length; i++) {
drawOne(gd, i, opt, value);
}
return;
}
else {
// add a new empty annotation
index = fullLayout.annotations.length;
fullLayout.annotations.push({});
}
}

if(!opt && value) {
if(value === 'remove') {
fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]')
.remove();
fullLayout.annotations.splice(index, 1);
layout.annotations.splice(index, 1);
for(i = index; i < fullLayout.annotations.length; i++) {
fullLayout._infolayer
.selectAll('.annotation[data-index="' + (i + 1) + '"]')
.attr('data-index', String(i));

// redraw all annotations past the removed one,
// so they bind to the right events
drawOne(gd, i);
}
return;
}
else if(value === 'add' || Lib.isPlainObject(value)) {
fullLayout.annotations.splice(index, 0, {});

var rule = Lib.isPlainObject(value) ?
Lib.extendFlat({}, value) :
{text: 'New text'};

if(layout.annotations) {
layout.annotations.splice(index, 0, rule);
} else {
layout.annotations = [rule];
}

for(i = fullLayout.annotations.length - 1; i > index; i--) {
fullLayout._infolayer
.selectAll('.annotation[data-index="' + (i - 1) + '"]')
.attr('data-index', String(i));
drawOne(gd, i);
}
}
}
gs = gd._fullLayout._size;

// remove the existing annotation if there is one
fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]').remove();

// remember a few things about what was already there,
var optionsIn = layout.annotations[index],
oldPrivate = fullLayout.annotations[index];

// not sure how we're getting here... but C12 is seeing a bug
// where we fail here when they add/remove annotations
if(!optionsIn) return;
options = fullLayout.annotations[index];

// alter the input annotation as requested
var optionsEdit = {};
if(typeof opt === 'string' && opt) optionsEdit[opt] = value;
else if(Lib.isPlainObject(opt)) optionsEdit = opt;

var optionKeys = Object.keys(optionsEdit);
for(i = 0; i < optionKeys.length; i++) {
var k = optionKeys[i];
Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
}

// return early in visible: false updates
if(optionsIn.visible === false) return;

var gs = fullLayout._size;
var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref};

var axLetters = ['x', 'y'];
for(i = 0; i < 2; i++) {
var axLetter = axLetters[i];
// if we don't have an explicit position already,
// don't set one just because we're changing references
// or axis type.
// the defaults will be consistent most of the time anyway,
// except in log/linear changes
if(optionsEdit[axLetter] !== undefined ||
optionsIn[axLetter] === undefined) {
continue;
}

var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')),
axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')),
position = optionsIn[axLetter],
axTypeOld = oldPrivate['_' + axLetter + 'type'];

if(optionsEdit[axLetter + 'ref'] !== undefined) {

// TODO: include ax / ay / axref / ayref here if not 'pixel'
// or even better, move all of this machinery out of here and into
// streambed as extra attributes to a regular relayout call
// we should do this after v2.0 when it can work equivalently for
// annotations, shapes, and images.

var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto',
plotSize = (axLetter === 'x' ? gs.w : gs.h),
halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) /
(2 * plotSize);
if(axOld && axNew) { // data -> different data
// go to the same fraction of the axis length
// whether or not these axes share a domain

position = axNew.fraction2r(axOld.r2fraction(position));
}
else if(axOld) { // data -> paper
// first convert to fraction of the axis
position = axOld.r2fraction(position);

// next scale the axis to the whole plot
position = axOld.domain[0] +
position * (axOld.domain[1] - axOld.domain[0]);

// finally see if we need to adjust auto alignment
// because auto always means middle / center alignment for data,
// but it changes for page alignment based on the closest side
if(autoAnchor) {
var posPlus = position + halfSizeFrac,
posMinus = position - halfSizeFrac;
if(position + posMinus < 2 / 3) position = posMinus;
else if(position + posPlus > 4 / 3) position = posPlus;
}
}
else if(axNew) { // paper -> data
// first see if we need to adjust auto alignment
if(autoAnchor) {
if(position < 1 / 3) position += halfSizeFrac;
else if(position > 2 / 3) position -= halfSizeFrac;
}

// next convert to fraction of the axis
position = (position - axNew.domain[0]) /
(axNew.domain[1] - axNew.domain[0]);

// finally convert to data coordinates
position = axNew.fraction2r(position);
}
}

if(axNew && axNew === axOld && axTypeOld) {
if(axTypeOld === 'log' && axNew.type !== 'log') {
position = Math.pow(10, position);
}
else if(axTypeOld !== 'log' && axNew.type === 'log') {
position = (position > 0) ?
Math.log(position) / Math.LN10 : undefined;
}
}

optionsIn[axLetter] = position;
}

var options = {};
handleAnnotationDefaults(optionsIn, options, fullLayout);
fullLayout.annotations[index] = options;
// this annotation is gone - quit now after deleting it
// TODO: use d3 idioms instead of deleting and redrawing every time
if(!optionsIn || options.visible === false) return;

var xa = Axes.getFromId(gd, options.xref),
ya = Axes.getFromId(gd, options.yref),
Expand Down Expand Up @@ -457,9 +287,6 @@ function drawOne(gd, index, opt, value) {

options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;

// save the current axis type for later log/linear changes
options['_' + axLetter + 'type'] = ax && ax.type;
});

if(annotationIsOffscreen) {
Expand Down
4 changes: 3 additions & 1 deletion src/components/annotations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ module.exports = {
drawOne: drawModule.drawOne,

hasClickToShow: clickModule.hasClickToShow,
onClick: clickModule.onClick
onClick: clickModule.onClick,

convertCoords: require('./convert_coords')
};
79 changes: 79 additions & 0 deletions src/components/images/convert_coords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* 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 isNumeric = require('fast-isnumeric');
var toLogRange = require('../../lib/to_log_range');

/*
* convertCoords: when converting an axis between log and linear
* you need to alter any images on that axis to keep them
* pointing at the same data point.
* In v2.0 this will become obsolete (or perhaps size will still need conversion?)
* we convert size by declaring that the maximum extent *in data units* should be
* the same, assuming the image is anchored by its center (could remove that restriction
* if we think it's important) even though the actual left and right values will not be
* quite the same since the scale becomes nonlinear (and central anchor means the pixel
* center of the image, not the data units center)
*
* gd: the plot div
* ax: the axis being changed
* newType: the type it's getting
* doExtra: function(attr, val) from inside relayout that sets the attribute.
* Use this to make the changes as it's aware if any other changes in the
* same relayout call should override this conversion.
*/
module.exports = function convertCoords(gd, ax, newType, doExtra) {
var toLog = (newType === 'log') && (ax.type === 'linear'),
fromLog = (newType === 'linear') && (ax.type === 'log');

if(!(toLog || fromLog)) return;

var images = gd._fullLayout.images,
axLetter = ax._id.charAt(0),
image,
attrPrefix;

for(var i = 0; i < images.length; i++) {
image = images[i];
attrPrefix = 'images[' + i + '].';

if(image[axLetter + 'ref'] === ax._id) {
var currentPos = image[axLetter],
currentSize = image['size' + axLetter],
newPos = null,
newSize = null;

if(toLog) {
newPos = toLogRange(currentPos, ax.range);

// this is the inverse of the conversion we do in fromLog below
// so that the conversion is reversible (notice the fromLog conversion
// is like sinh, and this one looks like arcsinh)
var dx = currentSize / Math.pow(10, newPos) / 2;
newSize = 2 * Math.log(dx + Math.sqrt(1 + dx * dx)) / Math.LN10;
}
else {
newPos = Math.pow(10, currentPos);
newSize = newPos * (Math.pow(10, currentSize / 2) - Math.pow(10, -currentSize / 2));
}

// if conversion failed, delete the value so it can get a default later on
if(!isNumeric(newPos)) {
newPos = null;
newSize = null;
}
else if(!isNumeric(newSize)) newSize = null;

doExtra(attrPrefix + axLetter, newPos);
doExtra(attrPrefix + 'size' + axLetter, newSize);
}
}
};
Loading