Skip to content

Commit 8b35a8e

Browse files
authored
Merge pull request #2046 from plotly/annotations-offline
Annotations offline
2 parents d69d7ba + 97cea60 commit 8b35a8e

33 files changed

+289
-207
lines changed

src/components/annotations/arrow_paths.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
* All paths are tuned for maximum scalability of the arrowhead,
1313
* ie throughout arrowwidth=0.3..3 the head is joined smoothly
1414
* to the line, with the line coming from the left and ending at (0, 0).
15+
*
1516
* `backoff` is the distance to move the arrowhead and the end of the line,
1617
* in order that the arrowhead points to the desired place, either at
1718
* the tip of the arrow or (in the case of circle or square)
1819
* the center of the symbol.
20+
*
21+
* `noRotate`, if truthy, says that this arrowhead should not rotate with the
22+
* arrow. That's the case for squares, which should always be straight, and
23+
* circles, for which it's irrelevant.
1924
*/
2025

2126
module.exports = [
@@ -52,11 +57,13 @@ module.exports = [
5257
// circle
5358
{
5459
path: 'M2,0A2,2 0 1,1 0,-2A2,2 0 0,1 2,0Z',
55-
backoff: 0
60+
backoff: 0,
61+
noRotate: true
5662
},
5763
// square
5864
{
5965
path: 'M2,2V-2H-2V2Z',
60-
backoff: 0
66+
backoff: 0,
67+
noRotate: true
6168
}
6269
];

src/components/annotations/draw.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
520520
.style('stroke-width', strokewidth + 'px')
521521
.call(Color.stroke, Color.rgb(arrowColor));
522522

523-
drawArrowHead(arrow, options.arrowhead, 'end', options.arrowsize, options.standoff);
523+
drawArrowHead(arrow, 'end', options);
524524

525525
// the arrow dragger is a small square right at the head, then a line to the tail,
526526
// all expanded by a stroke width of 6px plus the arrow line width

src/components/annotations/draw_arrow_head.js

+39-34
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,44 @@
1010
'use strict';
1111

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

1514
var Color = require('../color');
16-
var Drawing = require('../drawing');
1715

1816
var ARROWPATHS = require('./arrow_paths');
1917

20-
// add arrowhead(s) to a path or line d3 element el3
21-
// style: 1-6, first 5 are pointers, 6 is circle, 7 is square, 8 is none
22-
// ends is 'start', 'end' (default), 'start+end'
23-
// mag is magnification vs. default (default 1)
24-
25-
module.exports = function drawArrowHead(el3, style, ends, mag, standoff) {
26-
if(!isNumeric(mag)) mag = 1;
27-
var el = el3.node(),
28-
headStyle = ARROWPATHS[style||0];
29-
30-
if(typeof ends !== 'string' || !ends) ends = 'end';
31-
32-
var scale = (Drawing.getPx(el3, 'stroke-width') || 1) * mag,
33-
stroke = el3.style('stroke') || Color.defaultLine,
34-
opacity = el3.style('stroke-opacity') || 1,
35-
doStart = ends.indexOf('start') >= 0,
36-
doEnd = ends.indexOf('end') >= 0,
37-
backOff = headStyle.backoff * scale + standoff,
38-
start,
39-
end,
40-
startRot,
41-
endRot;
18+
/**
19+
* Add arrowhead(s) to a path or line element
20+
*
21+
* @param {d3.selection} el3: a d3-selected line or path element
22+
*
23+
* @param {string} ends: 'start', 'end', or 'start+end' for which ends get arrowheads
24+
*
25+
* @param {object} options: style information. Must have all the following:
26+
* @param {number} options.arrowhead: head style - see ./arrow_paths
27+
* @param {number} options.arrowsize: relative size of the head vs line width
28+
* @param {number} options.standoff: distance in px to move the arrow point from its target
29+
* @param {number} options.arrowwidth: width of the arrow line
30+
* @param {string} options.arrowcolor: color of the arrow line, for the head to match
31+
* Note that the opacity of this color is ignored, as it's assumed the container
32+
* of both the line and head has opacity applied to it so there isn't greater opacity
33+
* where they overlap.
34+
*/
35+
module.exports = function drawArrowHead(el3, ends, options) {
36+
var el = el3.node();
37+
var headStyle = ARROWPATHS[options.arrowhead || 0];
38+
var scale = (options.arrowwidth || 1) * options.arrowsize;
39+
var doStart = ends.indexOf('start') >= 0;
40+
var doEnd = ends.indexOf('end') >= 0;
41+
var backOff = headStyle.backoff * scale + options.standoff;
42+
43+
var start, end, startRot, endRot;
4244

4345
if(el.nodeName === 'line') {
4446
start = {x: +el3.attr('x1'), y: +el3.attr('y1')};
4547
end = {x: +el3.attr('x2'), y: +el3.attr('y2')};
4648

47-
var dx = start.x - end.x,
48-
dy = start.y - end.y;
49+
var dx = start.x - end.x;
50+
var dy = start.y - end.y;
4951

5052
startRot = Math.atan2(dy, dx);
5153
endRot = startRot + Math.PI;
@@ -83,16 +85,19 @@ module.exports = function drawArrowHead(el3, style, ends, mag, standoff) {
8385
}
8486

8587
if(doStart) {
86-
var start0 = el.getPointAtLength(0),
87-
dstart = el.getPointAtLength(0.1);
88+
var start0 = el.getPointAtLength(0);
89+
var dstart = el.getPointAtLength(0.1);
90+
8891
startRot = Math.atan2(start0.y - dstart.y, start0.x - dstart.x);
8992
start = el.getPointAtLength(Math.min(backOff, pathlen));
93+
9094
if(backOff) dashArray = '0px,' + backOff + 'px,';
9195
}
9296

9397
if(doEnd) {
94-
var end0 = el.getPointAtLength(pathlen),
95-
dend = el.getPointAtLength(pathlen - 0.1);
98+
var end0 = el.getPointAtLength(pathlen);
99+
var dend = el.getPointAtLength(pathlen - 0.1);
100+
96101
endRot = Math.atan2(end0.y - dend.y, end0.x - dend.x);
97102
end = el.getPointAtLength(Math.max(0, pathlen - backOff));
98103

@@ -110,19 +115,19 @@ module.exports = function drawArrowHead(el3, style, ends, mag, standoff) {
110115

111116
function drawhead(p, rot) {
112117
if(!headStyle.path) return;
113-
if(style > 5) rot = 0; // don't rotate square or circle
118+
if(headStyle.noRotate) rot = 0;
119+
114120
d3.select(el.parentNode).append('path')
115121
.attr({
116122
'class': el3.attr('class'),
117123
d: headStyle.path,
118124
transform:
119125
'translate(' + p.x + ',' + p.y + ')' +
120-
'rotate(' + (rot * 180 / Math.PI) + ')' +
126+
(rot ? 'rotate(' + (rot * 180 / Math.PI) + ')' : '') +
121127
'scale(' + scale + ')'
122128
})
123129
.style({
124-
fill: stroke,
125-
opacity: opacity,
130+
fill: Color.rgb(options.arrowcolor),
126131
'stroke-width': 0
127132
});
128133
}

src/components/colorbar/draw.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ module.exports = function draw(gd, id) {
304304
lineSize = 15.6;
305305
if(titleText.node()) {
306306
lineSize =
307-
parseInt(titleText.style('font-size'), 10) * LINE_SPACING;
307+
parseInt(titleText.node().style.fontSize, 10) * LINE_SPACING;
308308
}
309309
if(mathJaxNode) {
310310
titleHeight = Drawing.bBox(mathJaxNode).height;

src/components/drawing/index.js

-6
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,6 @@ drawing.hideOutsideRangePoints = function(points, subplot) {
110110
});
111111
};
112112

113-
drawing.getPx = function(s, styleAttr) {
114-
// helper to pull out a px value from a style that may contain px units
115-
// s is a d3 selection (will pull from the first one)
116-
return Number(s.style(styleAttr).replace(/px$/, ''));
117-
};
118-
119113
drawing.crispRound = function(gd, lineWidth, dflt) {
120114
// for lines that disable antialiasing we want to
121115
// make sure the width is an integer, and at least 1 if it's nonzero

src/lib/svg_text_utils.js

+14-11
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ exports.convertToTspans = function(_context, gd, _callback) {
7777
if(tex) {
7878
((gd && gd._promises) || []).push(new Promise(function(resolve) {
7979
_context.style('display', 'none');
80-
var config = {fontSize: parseInt(_context.style('font-size'), 10)};
80+
var fontSize = parseInt(_context.node().style.fontSize, 10);
81+
var config = {fontSize: fontSize};
8182

8283
texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) {
8384
parent.selectAll('svg.' + svgClass).remove();
@@ -113,16 +114,15 @@ exports.convertToTspans = function(_context, gd, _callback) {
113114
})
114115
.style({overflow: 'visible', 'pointer-events': 'none'});
115116

116-
var fill = _context.style('fill') || 'black';
117+
var fill = _context.node().style.fill || 'black';
117118
newSvg.select('g').attr({fill: fill, stroke: fill});
118119

119120
var newSvgW = getSize(newSvg, 'width'),
120121
newSvgH = getSize(newSvg, 'height'),
121122
newX = +_context.attr('x') - newSvgW *
122123
{start: 0, middle: 0.5, end: 1}[_context.attr('text-anchor') || 'start'],
123124
// font baseline is about 1/4 fontSize below centerline
124-
textHeight = parseInt(_context.style('font-size'), 10) ||
125-
getSize(_context, 'height'),
125+
textHeight = fontSize || getSize(_context, 'height'),
126126
dy = -textHeight / 4;
127127

128128
if(svgClass[0] === 'y') {
@@ -598,19 +598,22 @@ exports.makeEditable = function(context, options) {
598598
}
599599

600600
function appendEditable() {
601-
var plotDiv = d3.select(gd),
602-
container = plotDiv.select('.svg-container'),
603-
div = container.append('div');
601+
var plotDiv = d3.select(gd);
602+
var container = plotDiv.select('.svg-container');
603+
var div = container.append('div');
604+
var cStyle = context.node().style;
605+
var fontSize = parseFloat(cStyle.fontSize || 12);
606+
604607
div.classed('plugin-editable editable', true)
605608
.style({
606609
position: 'absolute',
607-
'font-family': context.style('font-family') || 'Arial',
608-
'font-size': context.style('font-size') || 12,
609-
color: options.fill || context.style('fill') || 'black',
610+
'font-family': cStyle.fontFamily || 'Arial',
611+
'font-size': fontSize,
612+
color: options.fill || cStyle.fill || 'black',
610613
opacity: 1,
611614
'background-color': options.background || 'transparent',
612615
outline: '#ffffff33 1px solid',
613-
margin: [-parseFloat(context.style('font-size')) / 8 + 1, 0, 0, -1].join('px ') + 'px',
616+
margin: [-fontSize / 8 + 1, 0, 0, -1].join('px ') + 'px',
614617
padding: '0',
615618
'box-sizing': 'border-box'
616619
})

src/plots/plots.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,13 @@ plots.redrawText = function(gd) {
212212
plots.resize = function(gd) {
213213
return new Promise(function(resolve, reject) {
214214

215-
if(!gd || d3.select(gd).style('display') === 'none') {
216-
reject(new Error('Resize must be passed a plot div element.'));
215+
function isHidden(gd) {
216+
var display = window.getComputedStyle(gd).display;
217+
return !display || display === 'none';
218+
}
219+
220+
if(!gd || isHidden(gd)) {
221+
reject(new Error('Resize must be passed a displayed plot div element.'));
217222
}
218223

219224
if(gd._redrawTimer) clearTimeout(gd._redrawTimer);

src/plots/polar/micropolar.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -432,13 +432,13 @@ var µ = module.exports = { version: '0.2.2' };
432432
});
433433
svg.selectAll('.geometry-group .mark').on('mouseover.tooltip', function(d, i) {
434434
var el = d3.select(this);
435-
var color = el.style('fill');
435+
var color = this.style.fill;
436436
var newColor = 'black';
437-
var opacity = el.style('opacity') || 1;
437+
var opacity = this.style.opacity || 1;
438438
el.attr({
439439
'data-opacity': opacity
440440
});
441-
if (color != 'none') {
441+
if (color && color !== 'none') {
442442
el.attr({
443443
'data-fill': color
444444
});
@@ -461,7 +461,7 @@ var µ = module.exports = { version: '0.2.2' };
461461
}).text(text);
462462
geometryTooltip.move(pos);
463463
} else {
464-
color = el.style('stroke');
464+
color = this.style.stroke || 'black';
465465
el.attr({
466466
'data-stroke': color
467467
});

src/snapshot/tosvg.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ module.exports = function toSVG(gd, format, scale) {
9797
// but in a static plot it's useless and it can confuse batik
9898
// we've tried to standardize on display:none but make sure we still
9999
// catch visibility:hidden if it ever arises
100-
if(txt.style('visibility') === 'hidden' || txt.style('display') === 'none') {
100+
if(this.style.visibility === 'hidden' || this.style.display === 'none') {
101101
txt.remove();
102102
return;
103103
}
@@ -110,15 +110,15 @@ module.exports = function toSVG(gd, format, scale) {
110110
// Font family styles break things because of quotation marks,
111111
// so we must remove them *after* the SVG DOM has been serialized
112112
// to a string (browsers convert singles back)
113-
var ff = txt.style('font-family');
113+
var ff = this.style.fontFamily;
114114
if(ff && ff.indexOf('"') !== -1) {
115115
txt.style('font-family', ff.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
116116
}
117117
});
118118

119119
svg.selectAll('.point,.scatterpts').each(function() {
120120
var pt = d3.select(this);
121-
var fill = pt.style('fill');
121+
var fill = this.style.fill;
122122

123123
// similar to font family styles above,
124124
// we must remove " after the SVG DOM has been serialized

tasks/test_syntax.js

+40-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ function assertJasmineSuites() {
5353
/*
5454
* tests about the contents of source (and lib) files:
5555
* - check for header comment
56-
* - check that we don't have .classList
56+
* - check that we don't have any features that break in IE
57+
* - check that we don't use getComputedStyle unexpectedly
5758
*/
5859
function assertSrcContents() {
5960
var licenseSrc = constants.licenseSrc;
@@ -69,6 +70,8 @@ function assertSrcContents() {
6970
// Forbidden in IE in any context
7071
var IE_BLACK_LIST = ['classList'];
7172

73+
var getComputedStyleCnt = 0;
74+
7275
glob(combineGlobs([srcGlob, libGlob]), function(err, files) {
7376
files.forEach(function(file) {
7477
var code = fs.readFileSync(file, 'utf-8');
@@ -85,13 +88,21 @@ function assertSrcContents() {
8588
if(source === 'Math.sign') {
8689
logs.push(file + ' : contains Math.sign (IE failure)');
8790
}
91+
else if(source === 'window.getComputedStyle') {
92+
getComputedStyleCnt++;
93+
}
8894
else if(IE_BLACK_LIST.indexOf(lastPart) !== -1) {
8995
logs.push(file + ' : contains .' + lastPart + ' (IE failure)');
9096
}
9197
else if(IE_SVG_BLACK_LIST.indexOf(lastPart) !== -1) {
9298
logs.push(file + ' : contains .' + lastPart + ' (IE failure in SVG)');
9399
}
94100
}
101+
else if(node.type === 'Identifier' && node.source() === 'getComputedStyle') {
102+
if(node.parent.source() !== 'window.getComputedStyle') {
103+
logs.push(file + ': getComputedStyle must be called as a `window` property.');
104+
}
105+
}
95106
});
96107

97108
var header = comments[0];
@@ -106,6 +117,34 @@ function assertSrcContents() {
106117
}
107118
});
108119

120+
/*
121+
* window.getComputedStyle calls are restricted, so we want to be
122+
* explicit about it whenever we add or remove these calls. This is
123+
* the reason d3.selection.style is forbidden as a getter.
124+
*
125+
* The rule is:
126+
* - You MAY NOT call getComputedStyle during rendering a plot, EXCEPT
127+
* in calculating autosize for the plot (which only makes sense if
128+
* the plot is displayed). Other uses of getComputedStyle while
129+
* rendering will fail, at least in Chrome, if the plot div is not
130+
* attached to the DOM.
131+
*
132+
* - You MAY call getComputedStyle during interactions (hover etc)
133+
* because at that point it's known that the plot is displayed.
134+
*
135+
* - You must use the explicit `window.getComputedStyle` rather than
136+
* the implicit global scope `getComputedStyle` for jsdom compat.
137+
*
138+
* - If you use conforms to these rules, you may update
139+
* KNOWN_GET_COMPUTED_STYLE_CALLS to count the new use.
140+
*/
141+
var KNOWN_GET_COMPUTED_STYLE_CALLS = 5;
142+
if(getComputedStyleCnt !== KNOWN_GET_COMPUTED_STYLE_CALLS) {
143+
logs.push('Expected ' + KNOWN_GET_COMPUTED_STYLE_CALLS +
144+
' window.getComputedStyle calls, found ' + getComputedStyleCnt +
145+
'. See ' + __filename + ' for details how to proceed.');
146+
}
147+
109148
log('correct headers and contents in lib/ and src/', logs);
110149
});
111150
}

test/image/strict-d3.js

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ selProto.style = function() {
3232

3333
if(sel.size()) {
3434
if(typeof obj === 'string') {
35+
if(arguments.length === 1) {
36+
throw new Error('d3 selection.style called as getter: ' +
37+
'disallowed as it can fail for unattached elements. ' +
38+
'Use node.style.attribute instead.');
39+
}
3540
checkStyleVal(sel, obj, arguments[1]);
3641
} else {
3742
Object.keys(obj).forEach(function(key) { checkStyleVal(sel, key, obj[key]); });

0 commit comments

Comments
 (0)