Skip to content

Commit 61f250c

Browse files
committed
editContainerArray: consistent handling of container array edits
supported in all relayout edits, not yet in restyle edits start on editComponentArray more baby steps more partial... finish up refactor - but still need to test edge cases of linear -> log delete shapes from all layers in case index changed support editContainerArray for all layout container arrays
1 parent 13a87ce commit 61f250c

21 files changed

+1164
-540
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var isNumeric = require('fast-isnumeric');
13+
var toLogRange = require('../../lib/to_log_range');
14+
15+
/*
16+
* convertCoords: when converting an axis between log and linear
17+
* you need to alter any annotations on that axis to keep them
18+
* pointing at the same data point.
19+
* In v2.0 this will become obsolete
20+
*
21+
* gd: the plot div
22+
* ax: the axis being changed
23+
* newType: the type it's getting
24+
* doExtra: function(attr, val) from inside relayout that sets the attribute.
25+
* Use this to make the changes as it's aware if any other changes in the
26+
* same relayout call should override this conversion.
27+
*/
28+
module.exports = function convertCoords(gd, ax, newType, doExtra) {
29+
var toLog = (newType === 'log') && (ax.type === 'linear'),
30+
fromLog = (newType === 'linear') && (ax.type === 'log');
31+
32+
if(!(toLog || fromLog)) return;
33+
34+
var annotations = gd._fullLayout.annotations,
35+
axLetter = ax._id.charAt(0),
36+
ann,
37+
attrPrefix;
38+
39+
function convert(attr) {
40+
var currentVal = ann[attr],
41+
newVal = null;
42+
43+
if(toLog) newVal = toLogRange(currentVal, ax.range);
44+
else newVal = Math.pow(10, currentVal);
45+
46+
// if conversion failed, delete the value so it gets a default value
47+
if(!isNumeric(newVal)) newVal = null;
48+
49+
doExtra(attrPrefix + attr, newVal);
50+
}
51+
52+
for(var i = 0; i < annotations.length; i++) {
53+
ann = annotations[i];
54+
attrPrefix = 'annotations[' + i + '].';
55+
56+
if(ann[axLetter + 'ref'] === ax._id) convert(axLetter);
57+
if(ann['a' + axLetter + 'ref'] === ax._id) convert('a' + axLetter);
58+
}
59+
};

src/components/annotations/draw.js

Lines changed: 14 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
'use strict';
1111

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

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

25-
var handleAnnotationDefaults = require('./annotation_defaults');
26-
var supplyLayoutDefaults = require('./defaults');
2724
var drawArrowHead = require('./draw_arrow_head');
2825

2926

@@ -41,6 +38,9 @@ module.exports = {
4138
drawOne: drawOne
4239
};
4340

41+
/*
42+
* draw: draw all annotations without any new modifications
43+
*/
4444
function draw(gd) {
4545
var fullLayout = gd._fullLayout;
4646

@@ -55,196 +55,26 @@ function draw(gd) {
5555
return Plots.previousPromises(gd);
5656
}
5757

58-
function drawOne(gd, index, opt, value) {
58+
/*
59+
* drawOne: draw a single annotation, potentially with modifications
60+
*
61+
* index (int): the annotation to draw
62+
*/
63+
function drawOne(gd, index) {
5964
var layout = gd.layout,
6065
fullLayout = gd._fullLayout,
61-
i;
62-
63-
if(!isNumeric(index) || index === -1) {
64-
65-
// no index provided - we're operating on ALL annotations
66-
if(!index && Array.isArray(value)) {
67-
// a whole annotation array is passed in
68-
// (as in, redo of delete all)
69-
layout.annotations = value;
70-
supplyLayoutDefaults(layout, fullLayout);
71-
draw(gd);
72-
return;
73-
}
74-
else if(value === 'remove') {
75-
// delete all
76-
delete layout.annotations;
77-
fullLayout.annotations = [];
78-
draw(gd);
79-
return;
80-
}
81-
else if(opt && value !== 'add') {
82-
// make the same change to all annotations
83-
for(i = 0; i < fullLayout.annotations.length; i++) {
84-
drawOne(gd, i, opt, value);
85-
}
86-
return;
87-
}
88-
else {
89-
// add a new empty annotation
90-
index = fullLayout.annotations.length;
91-
fullLayout.annotations.push({});
92-
}
93-
}
94-
95-
if(!opt && value) {
96-
if(value === 'remove') {
97-
fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]')
98-
.remove();
99-
fullLayout.annotations.splice(index, 1);
100-
layout.annotations.splice(index, 1);
101-
for(i = index; i < fullLayout.annotations.length; i++) {
102-
fullLayout._infolayer
103-
.selectAll('.annotation[data-index="' + (i + 1) + '"]')
104-
.attr('data-index', String(i));
105-
106-
// redraw all annotations past the removed one,
107-
// so they bind to the right events
108-
drawOne(gd, i);
109-
}
110-
return;
111-
}
112-
else if(value === 'add' || Lib.isPlainObject(value)) {
113-
fullLayout.annotations.splice(index, 0, {});
114-
115-
var rule = Lib.isPlainObject(value) ?
116-
Lib.extendFlat({}, value) :
117-
{text: 'New text'};
118-
119-
if(layout.annotations) {
120-
layout.annotations.splice(index, 0, rule);
121-
} else {
122-
layout.annotations = [rule];
123-
}
124-
125-
for(i = fullLayout.annotations.length - 1; i > index; i--) {
126-
fullLayout._infolayer
127-
.selectAll('.annotation[data-index="' + (i - 1) + '"]')
128-
.attr('data-index', String(i));
129-
drawOne(gd, i);
130-
}
131-
}
132-
}
66+
gs = gd._fullLayout._size;
13367

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

13771
// remember a few things about what was already there,
13872
var optionsIn = layout.annotations[index],
139-
oldPrivate = fullLayout.annotations[index];
140-
141-
// not sure how we're getting here... but C12 is seeing a bug
142-
// where we fail here when they add/remove annotations
143-
if(!optionsIn) return;
73+
options = fullLayout.annotations[index];
14474

145-
// alter the input annotation as requested
146-
var optionsEdit = {};
147-
if(typeof opt === 'string' && opt) optionsEdit[opt] = value;
148-
else if(Lib.isPlainObject(opt)) optionsEdit = opt;
149-
150-
var optionKeys = Object.keys(optionsEdit);
151-
for(i = 0; i < optionKeys.length; i++) {
152-
var k = optionKeys[i];
153-
Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
154-
}
155-
156-
// return early in visible: false updates
157-
if(optionsIn.visible === false) return;
158-
159-
var gs = fullLayout._size;
160-
var oldRef = {xref: oldPrivate.xref, yref: oldPrivate.yref};
161-
162-
var axLetters = ['x', 'y'];
163-
for(i = 0; i < 2; i++) {
164-
var axLetter = axLetters[i];
165-
// if we don't have an explicit position already,
166-
// don't set one just because we're changing references
167-
// or axis type.
168-
// the defaults will be consistent most of the time anyway,
169-
// except in log/linear changes
170-
if(optionsEdit[axLetter] !== undefined ||
171-
optionsIn[axLetter] === undefined) {
172-
continue;
173-
}
174-
175-
var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')),
176-
axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')),
177-
position = optionsIn[axLetter],
178-
axTypeOld = oldPrivate['_' + axLetter + 'type'];
179-
180-
if(optionsEdit[axLetter + 'ref'] !== undefined) {
181-
182-
// TODO: include ax / ay / axref / ayref here if not 'pixel'
183-
// or even better, move all of this machinery out of here and into
184-
// streambed as extra attributes to a regular relayout call
185-
// we should do this after v2.0 when it can work equivalently for
186-
// annotations, shapes, and images.
187-
188-
var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto',
189-
plotSize = (axLetter === 'x' ? gs.w : gs.h),
190-
halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) /
191-
(2 * plotSize);
192-
if(axOld && axNew) { // data -> different data
193-
// go to the same fraction of the axis length
194-
// whether or not these axes share a domain
195-
196-
position = axNew.fraction2r(axOld.r2fraction(position));
197-
}
198-
else if(axOld) { // data -> paper
199-
// first convert to fraction of the axis
200-
position = axOld.r2fraction(position);
201-
202-
// next scale the axis to the whole plot
203-
position = axOld.domain[0] +
204-
position * (axOld.domain[1] - axOld.domain[0]);
205-
206-
// finally see if we need to adjust auto alignment
207-
// because auto always means middle / center alignment for data,
208-
// but it changes for page alignment based on the closest side
209-
if(autoAnchor) {
210-
var posPlus = position + halfSizeFrac,
211-
posMinus = position - halfSizeFrac;
212-
if(position + posMinus < 2 / 3) position = posMinus;
213-
else if(position + posPlus > 4 / 3) position = posPlus;
214-
}
215-
}
216-
else if(axNew) { // paper -> data
217-
// first see if we need to adjust auto alignment
218-
if(autoAnchor) {
219-
if(position < 1 / 3) position += halfSizeFrac;
220-
else if(position > 2 / 3) position -= halfSizeFrac;
221-
}
222-
223-
// next convert to fraction of the axis
224-
position = (position - axNew.domain[0]) /
225-
(axNew.domain[1] - axNew.domain[0]);
226-
227-
// finally convert to data coordinates
228-
position = axNew.fraction2r(position);
229-
}
230-
}
231-
232-
if(axNew && axNew === axOld && axTypeOld) {
233-
if(axTypeOld === 'log' && axNew.type !== 'log') {
234-
position = Math.pow(10, position);
235-
}
236-
else if(axTypeOld !== 'log' && axNew.type === 'log') {
237-
position = (position > 0) ?
238-
Math.log(position) / Math.LN10 : undefined;
239-
}
240-
}
241-
242-
optionsIn[axLetter] = position;
243-
}
244-
245-
var options = {};
246-
handleAnnotationDefaults(optionsIn, options, fullLayout);
247-
fullLayout.annotations[index] = options;
75+
// this annotation is gone - quit now after deleting it
76+
// TODO: use d3 idioms instead of deleting and redrawing every time
77+
if(!optionsIn || options.visible === false) return;
24878

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

458288
options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
459289
options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;
460-
461-
// save the current axis type for later log/linear changes
462-
options['_' + axLetter + 'type'] = ax && ax.type;
463290
});
464291

465292
if(annotationIsOffscreen) {

src/components/annotations/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ module.exports = {
2424
drawOne: drawModule.drawOne,
2525

2626
hasClickToShow: clickModule.hasClickToShow,
27-
onClick: clickModule.onClick
27+
onClick: clickModule.onClick,
28+
29+
convertCoords: require('./convert_coords')
2830
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var isNumeric = require('fast-isnumeric');
13+
var toLogRange = require('../../lib/to_log_range');
14+
15+
/*
16+
* convertCoords: when converting an axis between log and linear
17+
* you need to alter any images on that axis to keep them
18+
* pointing at the same data point.
19+
* In v2.0 this will become obsolete (or perhaps size will still need conversion?)
20+
* we convert size by declaring that the maximum extent *in data units* should be
21+
* the same, assuming the image is anchored by its center (could remove that restriction
22+
* if we think it's important) even though the actual left and right values will not be
23+
* quite the same since the scale becomes nonlinear (and central anchor means the pixel
24+
* center of the image, not the data units center)
25+
*
26+
* gd: the plot div
27+
* ax: the axis being changed
28+
* newType: the type it's getting
29+
* doExtra: function(attr, val) from inside relayout that sets the attribute.
30+
* Use this to make the changes as it's aware if any other changes in the
31+
* same relayout call should override this conversion.
32+
*/
33+
module.exports = function convertCoords(gd, ax, newType, doExtra) {
34+
var toLog = (newType === 'log') && (ax.type === 'linear'),
35+
fromLog = (newType === 'linear') && (ax.type === 'log');
36+
37+
if(!(toLog || fromLog)) return;
38+
39+
var images = gd._fullLayout.images,
40+
axLetter = ax._id.charAt(0),
41+
image,
42+
attrPrefix;
43+
44+
for(var i = 0; i < images.length; i++) {
45+
image = images[i];
46+
attrPrefix = 'images[' + i + '].';
47+
48+
if(image[axLetter + 'ref'] === ax._id) {
49+
var currentPos = image[axLetter],
50+
currentSize = image['size' + axLetter],
51+
newPos = null,
52+
newSize = null;
53+
54+
if(toLog) {
55+
newPos = toLogRange(currentPos, ax.range);
56+
57+
// this is the inverse of the conversion we do in fromLog below
58+
// so that the conversion is reversible (notice the fromLog conversion
59+
// is like sinh, and this one looks like arcsinh)
60+
var dx = currentSize / Math.pow(10, newPos) / 2;
61+
newSize = 2 * Math.log(dx + Math.sqrt(1 + dx * dx)) / Math.LN10;
62+
}
63+
else {
64+
newPos = Math.pow(10, currentPos);
65+
newSize = newPos * (Math.pow(10, currentSize / 2) - Math.pow(10, -currentSize / 2));
66+
}
67+
68+
// if conversion failed, delete the value so it can get a default later on
69+
if(!isNumeric(newPos)) {
70+
newPos = null;
71+
newSize = null;
72+
}
73+
else if(!isNumeric(newSize)) newSize = null;
74+
75+
doExtra(attrPrefix + axLetter, newPos);
76+
doExtra(attrPrefix + 'size' + axLetter, newSize);
77+
}
78+
}
79+
};

0 commit comments

Comments
 (0)