diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 7db7e18c14d..92066a95f71 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -203,7 +203,6 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var annText = annTextGroupInner.append('text') .classed('annotation-text', true) - .attr('data-unformatted', options.text) .text(options.text); function textLayout(s) { @@ -232,11 +231,6 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { wholeLink.node().appendChild(annTextBG.node()); } - - // make sure lines are aligned the way they will be - // at the end, even if their position changes - annText.selectAll('tspan.line').attr({y: 0, x: 0}); - var mathjaxGroup = annTextGroupInner.select('.annotation-text-math-group'); var hasMathjax = !mathjaxGroup.empty(); var anntextBB = Drawing.bBox( @@ -423,14 +417,11 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { .call(Drawing.setClipUrl, isSizeConstrained ? annClipID : null); } else { - var texty = borderfull + yShift - anntextBB.top, - textx = borderfull + xShift - anntextBB.left; - annText.attr({ - x: textx, - y: texty - }) - .call(Drawing.setClipUrl, isSizeConstrained ? annClipID : null); - annText.selectAll('tspan.line').attr({y: texty, x: textx}); + var texty = borderfull + yShift - anntextBB.top; + var textx = borderfull + xShift - anntextBB.left; + + annText.call(svgTextUtils.positionText, textx, texty) + .call(Drawing.setClipUrl, isSizeConstrained ? annClipID : null); } annTextClip.select('rect').call(Drawing.setRect, borderfull, borderfull, @@ -693,7 +684,6 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { .call(textLayout) .on('edit', function(_text) { options.text = _text; - this.attr({'data-unformatted': options.text}); this.call(textLayout); var update = {}; diff --git a/src/components/annotations/draw_arrow_head.js b/src/components/annotations/draw_arrow_head.js index 69e5181914c..51cc2364774 100644 --- a/src/components/annotations/draw_arrow_head.js +++ b/src/components/annotations/draw_arrow_head.js @@ -111,7 +111,7 @@ module.exports = function drawArrowHead(el3, style, ends, mag, standoff) { function drawhead(p, rot) { if(!headStyle.path) return; if(style > 5) rot = 0; // don't rotate square or circle - d3.select(el.parentElement).append('path') + d3.select(el.parentNode).append('path') .attr({ 'class': el3.attr('class'), d: headStyle.path, diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index c9ece0344cc..7053a215d2b 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -23,6 +23,8 @@ var setCursor = require('../../lib/setcursor'); var Drawing = require('../drawing'); var Color = require('../color'); var Titles = require('../titles'); +var svgTextUtils = require('../../lib/svg_text_utils'); +var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; var handleAxisDefaults = require('../../plots/cartesian/axis_defaults'); var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults'); @@ -296,7 +298,7 @@ module.exports = function draw(gd, id) { lineSize = 15.6; if(titleText.node()) { lineSize = - parseInt(titleText.style('font-size'), 10) * 1.3; + parseInt(titleText.style('font-size'), 10) * LINE_SPACING; } if(mathJaxNode) { titleHeight = Drawing.bBox(mathJaxNode).height; @@ -308,8 +310,7 @@ module.exports = function draw(gd, id) { } else if(titleText.node() && !titleText.classed('js-placeholder')) { - titleHeight = Drawing.bBox( - titleGroup.node()).height; + titleHeight = Drawing.bBox(titleText.node()).height; } if(titleHeight) { // buffer btwn colorbar and title @@ -322,8 +323,7 @@ module.exports = function draw(gd, id) { } else { cbAxisOut.domain[0] += titleHeight / gs.h; - var nlines = Math.max(1, - titleText.selectAll('tspan.line').size()); + var nlines = svgTextUtils.lineCount(titleText); titleTrans[1] += (1 - nlines) * lineSize; } diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index bfb083cf6d3..3ce00f296a7 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -20,6 +20,9 @@ var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); +var alignment = require('../../constants/alignment'); +var LINE_SPACING = alignment.LINE_SPACING; + var subTypes = require('../../traces/scatter/subtypes'); var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); @@ -41,6 +44,12 @@ drawing.font = function(s, family, size, color) { if(color) s.call(Color.fill, color); }; +/* + * Positioning helpers + * Note: do not use `setPosition` with nodes modified by + * `svgTextUtils.convertToTspans`. Use `svgTextUtils.positionText` + * instead, so that elements get updated to match. + */ drawing.setPosition = function(s, x, y) { s.attr('x', x).attr('y', y); }; drawing.setSize = function(s, w, h) { s.attr('width', w).attr('height', h); }; drawing.setRect = function(s, x, y, w, h) { @@ -420,8 +429,7 @@ drawing.tryColorscale = function(marker, prefix) { }; // draw text at points -var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1}, - LINEEXPAND = 1.3; +var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1}; drawing.textPointStyle = function(s, trace, gd) { s.each(function(d) { var p = d3.select(this), @@ -454,20 +462,15 @@ drawing.textPointStyle = function(s, trace, gd) { .attr('text-anchor', h) .text(text) .call(svgTextUtils.convertToTspans, gd); - var pgroup = d3.select(this.parentNode), - tspans = p.selectAll('tspan.line'), - numLines = ((tspans[0].length || 1) - 1) * LINEEXPAND + 1, - dx = TEXTOFFSETSIGN[h] * r, - dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + + + var pgroup = d3.select(this.parentNode); + var numLines = (svgTextUtils.lineCount(p) - 1) * LINE_SPACING + 1; + var dx = TEXTOFFSETSIGN[h] * r; + var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2; // fix the overall text group position pgroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); - - // then fix multiline text - if(numLines > 1) { - tspans.attr({ x: p.attr('x'), y: p.attr('y') }); - } }); }; @@ -606,20 +609,76 @@ drawing.makeTester = function() { drawing.testref = testref; }; -// use our offscreen tester to get a clientRect for an element, -// in a reference frame where it isn't translated and its anchor -// point is at (0,0) -// always returns a copy of the bbox, so the caller can modify it safely +/* + * use our offscreen tester to get a clientRect for an element, + * in a reference frame where it isn't translated and its anchor + * point is at (0,0) + * always returns a copy of the bbox, so the caller can modify it safely + */ drawing.savedBBoxes = {}; var savedBBoxesCount = 0; var maxSavedBBoxes = 10000; -drawing.bBox = function(node) { - // cache elements we've already measured so we don't have to - // remeasure the same thing many times - var hash = nodeHash(node); - var out = drawing.savedBBoxes[hash]; - if(out) return Lib.extendFlat({}, out); +drawing.bBox = function(node, hash) { + /* + * Cache elements we've already measured so we don't have to + * remeasure the same thing many times + * We have a few bBox callers though who pass a node larger than + * a or a MathJax , such as an axis group containing many labels. + * These will not generate a hash (unless we figure out an appropriate + * hash key for them) and thus we will not hash them. + */ + if(!hash) hash = nodeHash(node); + var out; + if(hash) { + out = drawing.savedBBoxes[hash]; + if(out) return Lib.extendFlat({}, out); + } + else if(node.children.length === 1) { + /* + * If we have only one child element, which is itself hashable, make + * a new hash from this element plus its x,y,transform + * These bounding boxes *include* x,y,transform - mostly for use by + * callers trying to avoid overlaps (ie titles) + */ + var innerNode = node.children[0]; + + hash = nodeHash(innerNode); + if(hash) { + var x = +innerNode.getAttribute('x') || 0; + var y = +innerNode.getAttribute('y') || 0; + var transform = innerNode.getAttribute('transform'); + + if(!transform) { + // in this case, just varying x and y, don't bother caching + // the final bBox because the alteration is quick. + var innerBB = drawing.bBox(innerNode, hash); + if(x) { + innerBB.left += x; + innerBB.right += x; + } + if(y) { + innerBB.top += y; + innerBB.bottom += y; + } + return innerBB; + } + /* + * else we have a transform - rather than make a complicated + * (and error-prone and probably slow) transform parser/calculator, + * just continue on calculating the boundingClientRect of the group + * and use the new composite hash to cache it. + * That said, `innerNode.transform.baseVal` is an array of + * `SVGTransform` objects, that *do* seem to have a nice matrix + * multiplication interface that we could use to avoid making + * another getBoundingClientRect call... + */ + hash += '~' + x + '~' + y + '~' + transform; + + out = drawing.savedBBoxes[hash]; + if(out) return Lib.extendFlat({}, out); + } + } var tester = drawing.tester.node(); @@ -627,12 +686,10 @@ drawing.bBox = function(node) { var testNode = node.cloneNode(true); tester.appendChild(testNode); - // standardize its position... do we really want to do this? - d3.select(testNode).attr({ - x: 0, - y: 0, - transform: '' - }); + // standardize its position (and newline tspans if any) + d3.select(testNode) + .attr('transform', null) + .call(svgTextUtils.positionText, 0, 0); var testRect = testNode.getBoundingClientRect(); var refRect = drawing.testref @@ -655,11 +712,11 @@ drawing.bBox = function(node) { // by saving boxes for long-gone elements if(savedBBoxesCount >= maxSavedBBoxes) { drawing.savedBBoxes = {}; - maxSavedBBoxes = 0; + savedBBoxesCount = 0; } // cache this bbox - drawing.savedBBoxes[hash] = bb; + if(hash) drawing.savedBBoxes[hash] = bb; savedBBoxesCount++; return Lib.extendFlat({}, bb); @@ -667,23 +724,11 @@ drawing.bBox = function(node) { // capture everything about a node (at least in our usage) that // impacts its bounding box, given that bBox clears x, y, and transform -// TODO: is this really everything? Is it worth taking only parts of style, -// so we can share across more changes (like colors)? I guess we can't strip -// colors and stuff from inside innerHTML so maybe not worth bothering outside. -// TODO # 2: this can be long, so could take a lot of memory, do we want to -// hash it? But that can be slow... -// extracting this string from a typical element takes ~3 microsec, where -// doing a simple hash ala https://stackoverflow.com/questions/7616461 -// adds ~15 microsec (nearly all of this is spent in charCodeAt) -// function hash(s) { -// var h = 0; -// for (var i = 0; i < s.length; i++) { -// h = (((h << 5) - h) + s.charCodeAt(i)) | 0; // codePointAt? -// } -// return h; -// } function nodeHash(node) { - return node.innerHTML + + var inputText = node.getAttribute('data-unformatted'); + if(inputText === null) return; + return inputText + + node.getAttribute('data-math') + node.getAttribute('text-anchor') + node.getAttribute('style'); } @@ -841,13 +886,3 @@ drawing.setTextPointsScale = function(selection, xScale, yScale) { el.attr('transform', transforms.join(' ')); }); }; - -drawing.measureText = function(tester, text, font) { - var dummyText = tester.append('text') - .text(text) - .call(drawing.font, font); - - var bbox = drawing.bBox(dummyText.node()); - dummyText.remove(); - return bbox; -}; diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 4af8366b964..40cc370e46a 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -595,23 +595,16 @@ function createHoverText(hoverData, opts, gd) { .attr('data-notex', 1); ltext.text(t0) - .call(svgTextUtils.convertToTspans, gd) - .call(Drawing.setPosition, 0, 0) - .selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); + .call(svgTextUtils.positionText, 0, 0) + .call(svgTextUtils.convertToTspans, gd); label.attr('transform', ''); var tbb = ltext.node().getBoundingClientRect(); if(hovermode === 'x') { ltext.attr('text-anchor', 'middle') - .call(Drawing.setPosition, 0, (xa.side === 'top' ? + .call(svgTextUtils.positionText, 0, (xa.side === 'top' ? (outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD) : - (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); + (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))); var topsign = xa.side === 'top' ? '-' : ''; lpath.attr('d', 'M0,0' + @@ -627,14 +620,9 @@ function createHoverText(hoverData, opts, gd) { } else { ltext.attr('text-anchor', ya.side === 'right' ? 'start' : 'end') - .call(Drawing.setPosition, + .call(svgTextUtils.positionText, (ya.side === 'right' ? 1 : -1) * (HOVERTEXTPAD + HOVERARROWSIZE), - outerTop - tbb.top - tbb.height / 2) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); + outerTop - tbb.top - tbb.height / 2); var leftsign = ya.side === 'right' ? '' : '-'; lpath.attr('d', 'M0,0' + @@ -742,12 +730,10 @@ function createHoverText(hoverData, opts, gd) { d.fontFamily || fontFamily, d.fontSize || fontSize, d.fontColor || contrastColor) - .call(Drawing.setPosition, 0, 0) .text(text) .attr('data-notex', 1) + .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); - tx.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); var tx2 = g.select('text.name'), tx2width = 0; @@ -759,11 +745,9 @@ function createHoverText(hoverData, opts, gd) { d.fontSize || fontSize, traceColor) .text(name) - .call(Drawing.setPosition, 0, 0) .attr('data-notex', 1) + .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); - tx2.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; } else { @@ -1031,17 +1015,12 @@ function alignHoverText(hoverLabels, rotateLabels) { 'V' + (offsetY - HOVERARROWSIZE) + 'Z')); - tx.call(Drawing.setPosition, - txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD) - .selectAll('tspan.line') - .attr({ - x: tx.attr('x'), - y: tx.attr('y') - }); + tx.call(svgTextUtils.positionText, + txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); if(d.tx2width) { - g.select('text.name, text.name tspan.line') - .call(Drawing.setPosition, + g.select('text.name') + .call(svgTextUtils.positionText, tx2x + alignShift * HOVERTEXTPAD + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); g.select('rect') diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 774c5e2e420..788b664ff09 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -22,6 +22,8 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var constants = require('./constants'); var interactConstants = require('../../constants/interactions'); +var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; + var getLegendData = require('./get_legend_data'); var style = require('./style'); var helpers = require('./helpers'); @@ -369,21 +371,17 @@ function drawTexts(g, gd) { var text = g.selectAll('text.legendtext') .data([0]); + text.enter().append('text').classed('legendtext', true); - text.attr({ - x: 40, - y: 0, - 'data-unformatted': name - }) - .style('text-anchor', 'start') - .classed('user-select-none', true) - .call(Drawing.font, fullLayout.legend.font) - .text(name); + + text.attr('text-anchor', 'start') + .classed('user-select-none', true) + .call(Drawing.font, fullLayout.legend.font) + .text(name); function textLayout(s) { svgTextUtils.convertToTspans(s, gd, function() { - s.selectAll('tspan.line').attr({x: s.attr('x')}); - g.call(computeTextDimensions, gd); + computeTextDimensions(g, gd); }); } @@ -391,8 +389,6 @@ function drawTexts(g, gd) { text.call(svgTextUtils.makeEditable, {gd: gd}) .call(textLayout) .on('edit', function(text) { - this.attr({'data-unformatted': text}); - this.text(text) .call(textLayout); @@ -557,20 +553,21 @@ function handleClick(g, gd, numClicks) { } function computeTextDimensions(g, gd) { - var legendItem = g.data()[0][0], - mathjaxGroup = g.select('g[class*=math-group]'), - opts = gd._fullLayout.legend, - lineHeight = opts.font.size * 1.3, - height, - width; + var legendItem = g.data()[0][0]; if(!legendItem.trace.showlegend) { g.remove(); return; } - if(mathjaxGroup.node()) { - var mathjaxBB = Drawing.bBox(mathjaxGroup.node()); + var mathjaxGroup = g.select('g[class*=math-group]'); + var mathjaxNode = mathjaxGroup.node(); + var opts = gd._fullLayout.legend; + var lineHeight = opts.font.size * LINE_SPACING; + var height, width; + + if(mathjaxNode) { + var mathjaxBB = Drawing.bBox(mathjaxNode); height = mathjaxBB.height; width = mathjaxBB.width; @@ -578,18 +575,19 @@ function computeTextDimensions(g, gd) { Drawing.setTranslate(mathjaxGroup, 0, (height / 4)); } else { - var text = g.selectAll('.legendtext'), - textSpans = g.selectAll('.legendtext>tspan'), - textLines = textSpans[0].length || 1; + var text = g.select('.legendtext'); + var textLines = svgTextUtils.lineCount(text); + var textNode = text.node(); height = lineHeight * textLines; - width = text.node() && Drawing.bBox(text.node()).width; + width = textNode ? Drawing.bBox(textNode).width : 0; // approximation to height offset to center the font // to avoid getBoundingClientRect var textY = lineHeight * (0.3 + (1 - textLines) / 2); - text.attr('y', textY); - textSpans.attr('y', textY); + // TODO: this 40 should go in a constants file (along with other + // values related to the legend symbol size) + svgTextUtils.positionText(text, 40, textY); } height = Math.max(height, 16) + 3; diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index 901a6977ed6..382d16f7697 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -19,6 +19,8 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var axisIds = require('../../plots/cartesian/axis_ids'); var anchorUtils = require('../legend/anchor_utils'); +var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; + var constants = require('./constants'); var getUpdateObject = require('./get_update_object'); @@ -149,8 +151,6 @@ function getFillColor(selectorLayout, d) { function drawButtonText(button, selectorLayout, d, gd) { function textLayout(s) { svgTextUtils.convertToTspans(s, gd); - - // TODO do we need anything else here? } var text = button.selectAll('text') @@ -182,26 +182,23 @@ function reposition(gd, buttons, opts, axName) { var borderWidth = opts.borderwidth; buttons.each(function() { - var button = d3.select(this), - text = button.select('.selector-text'), - tspans = text.selectAll('tspan'); + var button = d3.select(this); + var text = button.select('.selector-text'); - var tHeight = opts.font.size * 1.3, - tLines = tspans[0].length || 1, - hEff = Math.max(tHeight * tLines, 16) + 3; + var tHeight = opts.font.size * LINE_SPACING; + var hEff = Math.max(tHeight * svgTextUtils.lineCount(text), 16) + 3; opts.height = Math.max(opts.height, hEff); }); buttons.each(function() { - var button = d3.select(this), - rect = button.select('.selector-rect'), - text = button.select('.selector-text'), - tspans = text.selectAll('tspan'); + var button = d3.select(this); + var rect = button.select('.selector-rect'); + var text = button.select('.selector-text'); - var tWidth = text.node() && Drawing.bBox(text.node()).width, - tHeight = opts.font.size * 1.3, - tLines = tspans[0].length || 1; + var tWidth = text.node() && Drawing.bBox(text.node()).width; + var tHeight = opts.font.size * LINE_SPACING; + var tLines = svgTextUtils.lineCount(text); var wEff = Math.max(tWidth + 10, constants.minButtonWidth); @@ -220,13 +217,8 @@ function reposition(gd, buttons, opts, axName) { height: opts.height }); - var textAttrs = { - x: wEff / 2, - y: opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3 - }; - - text.attr(textAttrs); - tspans.attr(textAttrs); + svgTextUtils.positionText(text, wEff / 2, + opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3); opts.width += wEff + 5; }); diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index fedd7c088b5..7befbf6fd17 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -45,9 +45,6 @@ module.exports = { // padding around item text textPadX: 40, - // font size to height scale - fontSizeToHeight: 1.3, - // arrow offset off right edge arrowOffsetX: 4, diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index e3309ba1446..8a773043244 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -18,6 +18,7 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var anchorUtils = require('../legend/anchor_utils'); var constants = require('./constants'); +var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; module.exports = function draw(gd) { @@ -86,16 +87,9 @@ module.exports = function draw(gd) { }); drawSlider(gd, d3.select(this), sliderOpts); - - // makeInputProxy(gd, d3.select(this), sliderOpts); }); }; -/* function makeInputProxy(gd, sliderGroup, sliderOpts) { - sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) - .data([0]); -}*/ - // This really only just filters by visibility: function makeSliderData(fullLayout, gd) { var contOpts = fullLayout[constants.name], @@ -132,14 +126,12 @@ function findDimensions(gd, sliderOpts) { var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts); - var tWidth = (text.node() && Drawing.bBox(text.node()).width) || 0; - - // This just overwrites with the last. Which is fine as long as - // the bounding box (probably incorrectly) measures the text *on - // a single line*: - labelHeight = (text.node() && Drawing.bBox(text.node()).height) || 0; - - maxLabelWidth = Math.max(maxLabelWidth, tWidth); + var textNode = text.node(); + if(textNode) { + var bBox = Drawing.bBox(textNode); + labelHeight = Math.max(labelHeight, bBox.height); + maxLabelWidth = Math.max(maxLabelWidth, bBox.width); + } }); sliderLabels.remove(); @@ -149,26 +141,8 @@ function findDimensions(gd, sliderOpts) { constants.gripHeight ); - sliderOpts.currentValueMaxWidth = 0; - sliderOpts.currentValueHeight = 0; - sliderOpts.currentValueTotalHeight = 0; - - if(sliderOpts.currentvalue.visible) { - // Get the dimensions of the current value label: - var dummyGroup = Drawing.tester.append('g'); - - sliderLabels.each(function(stepOpts) { - var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); - var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0}; - sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); - sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); - }); - - sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; - - dummyGroup.remove(); - } - + // calculate some overall dimensions - some of these are needed for + // calculating the currentValue dimensions var graphSize = gd._fullLayout._size; sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); @@ -195,6 +169,31 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; + // loop over all possible values for currentValue to find the + // area we need for it + sliderOpts.currentValueMaxWidth = 0; + sliderOpts.currentValueHeight = 0; + sliderOpts.currentValueTotalHeight = 0; + sliderOpts.currentValueMaxLines = 1; + + if(sliderOpts.currentvalue.visible) { + // Get the dimensions of the current value label: + var dummyGroup = Drawing.tester.append('g'); + + sliderLabels.each(function(stepOpts) { + var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); + var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0}; + var lines = svgTextUtils.lineCount(curValPrefix); + sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); + sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); + sliderOpts.currentValueMaxLines = Math.max(sliderOpts.currentValueMaxLines, lines); + }); + + sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; + + dummyGroup.remove(); + } + sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; @@ -286,7 +285,10 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { text.enter().append('text') .classed(constants.labelClass, true) .classed('user-select-none', true) - .attr('text-anchor', textAnchor); + .attr({ + 'text-anchor': textAnchor, + 'data-notex': 1 + }); var str = sliderOpts.currentvalue.prefix ? sliderOpts.currentvalue.prefix : ''; @@ -305,7 +307,12 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { .text(str) .call(svgTextUtils.convertToTspans, sliderOpts.gd); - Drawing.setTranslate(text, x0, sliderOpts.currentValueHeight); + var lines = svgTextUtils.lineCount(text); + + var y0 = (sliderOpts.currentValueMaxLines + 1 - lines) * + sliderOpts.currentvalue.font.size * LINE_SPACING; + + svgTextUtils.positionText(text, x0, y0); return text; } @@ -337,7 +344,10 @@ function drawLabel(item, data, sliderOpts) { text.enter().append('text') .classed(constants.labelClass, true) .classed('user-select-none', true) - .attr('text-anchor', 'middle'); + .attr({ + 'text-anchor': 'middle', + 'data-notex': 1 + }); text.call(Drawing.font, sliderOpts.font) .text(data.step.label) @@ -368,7 +378,13 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Drawing.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight + constants.tickOffset + + sliderOpts.ticklen + + // position is the baseline of the top line of text only, even + // if the label spans multiple lines + sliderOpts.font.size * LINE_SPACING + + constants.labelOffset + + sliderOpts.currentValueTotalHeight ); }); diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 02287decbcc..72278a72d64 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -120,11 +120,8 @@ Titles.draw = function(gd, titleClass, options) { 'font-weight': Plots.fontWeight }) .attr(attributes) - .call(svgTextUtils.convertToTspans, gd) - .attr(attributes); + .call(svgTextUtils.convertToTspans, gd); - titleEl.selectAll('tspan.line') - .attr(attributes); return Plots.previousPromises(gd); } @@ -136,33 +133,33 @@ Titles.draw = function(gd, titleClass, options) { // move toward avoid.side (= left, right, top, bottom) if needed // can include pad (pixels, default 2) - var shift = 0, - backside = { - left: 'right', - right: 'left', - top: 'bottom', - bottom: 'top' - }[avoid.side], - shiftSign = (['left', 'top'].indexOf(avoid.side) !== -1) ? - -1 : 1, - pad = isNumeric(avoid.pad) ? avoid.pad : 2, - titlebb = Drawing.bBox(titleGroup.node()), - paperbb = { - left: 0, - top: 0, - right: fullLayout.width, - bottom: fullLayout.height - }, - maxshift = avoid.maxShift || ( - (paperbb[avoid.side] - titlebb[avoid.side]) * - ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1)); + var shift = 0; + var backside = { + left: 'right', + right: 'left', + top: 'bottom', + bottom: 'top' + }[avoid.side]; + var shiftSign = (['left', 'top'].indexOf(avoid.side) !== -1) ? + -1 : 1; + var pad = isNumeric(avoid.pad) ? avoid.pad : 2; + var titlebb = Drawing.bBox(titleGroup.node()); + var paperbb = { + left: 0, + top: 0, + right: fullLayout.width, + bottom: fullLayout.height + }; + var maxshift = avoid.maxShift || ( + (paperbb[avoid.side] - titlebb[avoid.side]) * + ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1)); // Prevent the title going off the paper if(maxshift < 0) shift = maxshift; else { // so we don't have to offset each avoided element, // give the title the opposite offset - var offsetLeft = avoid.offsetLeft || 0, - offsetTop = avoid.offsetTop || 0; + var offsetLeft = avoid.offsetLeft || 0; + var offsetTop = avoid.offsetTop || 0; titlebb.left -= offsetLeft; titlebb.right -= offsetLeft; titlebb.top -= offsetTop; @@ -193,8 +190,7 @@ Titles.draw = function(gd, titleClass, options) { } } - el.attr({'data-unformatted': txt}) - .call(titleLayout); + el.call(titleLayout); var placeholderText = 'Click to enter ' + name + ' title'; @@ -202,8 +198,7 @@ Titles.draw = function(gd, titleClass, options) { opacity = 0; isplaceholder = true; txt = placeholderText; - el.attr({'data-unformatted': txt}) - .text(txt) + el.text(txt) .on('mouseover.opacity', function() { d3.select(this).transition() .duration(interactConstants.SHOW_PLACEHOLDER).style('opacity', 1); @@ -228,9 +223,8 @@ Titles.draw = function(gd, titleClass, options) { .call(titleLayout); }) .on('input', function(d) { - this.text(d || ' ').attr(attributes) - .selectAll('tspan.line') - .attr(attributes); + this.text(d || ' ') + .call(svgTextUtils.positionText, attributes.x, attributes.y); }); } el.classed('js-placeholder', isplaceholder); diff --git a/src/components/updatemenus/constants.js b/src/components/updatemenus/constants.js index b1c7a2e3ef0..89348318a4c 100644 --- a/src/components/updatemenus/constants.js +++ b/src/components/updatemenus/constants.js @@ -44,9 +44,6 @@ module.exports = { textPadX: 24, arrowPadX: 16, - // font size to height scale - fontSizeToHeight: 1.3, - // item rect radii rx: 2, ry: 2, @@ -70,5 +67,13 @@ module.exports = { activeColor: '#F4FAFF', // color given to hovered buttons - hoverColor: '#F4FAFF' + hoverColor: '#F4FAFF', + + // symbol for menu open arrow + arrowSymbol: { + left: '◄', + right: '►', + up: '▲', + down: '▼' + } }; diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 118335fed1a..39efaf73cdd 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -17,6 +17,8 @@ var Drawing = require('../drawing'); var svgTextUtils = require('../../lib/svg_text_utils'); var anchorUtils = require('../legend/anchor_utils'); +var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; + var constants = require('./constants'); var ScrollBox = require('./scrollbox'); @@ -214,7 +216,7 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { .classed('user-select-none', true) .attr('text-anchor', 'end') .call(Drawing.font, menuOpts.font) - .text('▼'); + .text(constants.arrowSymbol[menuOpts.direction]); arrow.attr({ x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, @@ -463,7 +465,10 @@ function drawItemText(item, menuOpts, itemOpts, gd) { text.enter().append('text') .classed(constants.itemTextClassName, true) .classed('user-select-none', true) - .attr('text-anchor', 'start'); + .attr({ + 'text-anchor': 'start', + 'data-notex': 1 + }); text.call(Drawing.font, menuOpts.font) .text(itemOpts.label) @@ -520,17 +525,16 @@ function findDimensions(gd, menuOpts) { button.call(drawItem, menuOpts, buttonOpts, gd); - var text = button.select('.' + constants.itemTextClassName), - tspans = text.selectAll('tspan'); + var text = button.select('.' + constants.itemTextClassName); // width is given by max width of all buttons - var tWidth = text.node() && Drawing.bBox(text.node()).width, - wEff = Math.max(tWidth + constants.textPadX, constants.minWidth); + var tWidth = text.node() && Drawing.bBox(text.node()).width; + var wEff = Math.max(tWidth + constants.textPadX, constants.minWidth); // height is determined by item text - var tHeight = menuOpts.font.size * constants.fontSizeToHeight, - tLines = tspans[0].length || 1, - hEff = Math.max(tHeight * tLines, constants.minHeight) + constants.textOffsetY; + var tHeight = menuOpts.font.size * LINE_SPACING; + var tLines = svgTextUtils.lineCount(text); + var hEff = Math.max(tHeight * tLines, constants.minHeight) + constants.textOffsetY; hEff = Math.ceil(hEff); wEff = Math.ceil(wEff); @@ -624,34 +628,29 @@ function findDimensions(gd, menuOpts) { // set item positions (mutates posOpts) function setItemPosition(item, menuOpts, posOpts, overrideOpts) { overrideOpts = overrideOpts || {}; - var rect = item.select('.' + constants.itemRectClassName), - text = item.select('.' + constants.itemTextClassName), - tspans = text.selectAll('tspan'), - borderWidth = menuOpts.borderwidth, - index = posOpts.index; + var rect = item.select('.' + constants.itemRectClassName); + var text = item.select('.' + constants.itemTextClassName); + var borderWidth = menuOpts.borderwidth; + var index = posOpts.index; Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y); var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; + var finalHeight = overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1); rect.attr({ x: 0, y: 0, width: overrideOpts.width || (isVertical ? menuOpts.width1 : menuOpts.widths[index]), - height: overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1) + height: finalHeight }); - var tHeight = menuOpts.font.size * constants.fontSizeToHeight, - tLines = tspans[0].length || 1, - spanOffset = ((tLines - 1) * tHeight / 4); - - var textAttrs = { - x: constants.textOffsetX, - y: menuOpts.heights[index] / 2 - spanOffset + constants.textOffsetY - }; + var tHeight = menuOpts.font.size * LINE_SPACING; + var tLines = svgTextUtils.lineCount(text); + var spanOffset = ((tLines - 1) * tHeight / 2); - text.attr(textAttrs); - tspans.attr(textAttrs); + svgTextUtils.positionText(text, constants.textOffsetX, + finalHeight / 2 - spanOffset + constants.textOffsetY); if(isVertical) { posOpts.y += menuOpts.heights[index] + posOpts.yPad; @@ -667,8 +666,8 @@ function removeAllButtons(gButton) { } function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); + var pushMargins = gd._fullLayout._pushmargin || {}; + var keys = Object.keys(pushMargins); for(var i = 0; i < keys.length; i++) { var k = keys[i]; diff --git a/src/constants/alignment.js b/src/constants/alignment.js index 66ce5bb0121..789f5eee175 100644 --- a/src/constants/alignment.js +++ b/src/constants/alignment.js @@ -28,5 +28,7 @@ module.exports = { bottom: 1, middle: 0.5, top: 0 - } + }, + // multiple of fontSize to get the vertical offset between lines + LINE_SPACING: 1.3 }; diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 0d8fd8b1f56..6c9c2d53d07 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -16,6 +16,7 @@ var d3 = require('d3'); var Lib = require('../lib'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var stringMappings = require('../constants/string_mappings'); +var LINE_SPACING = require('../constants/alignment').LINE_SPACING; // text converter @@ -40,7 +41,15 @@ exports.convertToTspans = function(_context, gd, _callback) { svgClass += '-math'; parent.selectAll('svg.' + svgClass).remove(); parent.selectAll('g.' + svgClass + '-group').remove(); - _context.style({visibility: null}); + _context.style('display', null) + .attr({ + // some callers use data-unformatted *from the element* in 'cancel' + // so we need it here even if we're going to turn it into math + // these two (plus style and text-anchor attributes) form the key we're + // going to use for Drawing.bBox + 'data-unformatted': str, + 'data-math': 'N' + }); function showText() { if(!parent.empty()) { @@ -48,10 +57,7 @@ exports.convertToTspans = function(_context, gd, _callback) { parent.select('svg.' + svgClass).remove(); } _context.text('') - .style({ - visibility: 'inherit', - 'white-space': 'pre' - }); + .style('white-space', 'pre'); var hasLink = buildSVGText(_context.node(), str); @@ -63,12 +69,14 @@ exports.convertToTspans = function(_context, gd, _callback) { _context.style('pointer-events', 'all'); } + exports.positionText(_context); + if(_callback) _callback.call(_context); } if(tex) { ((gd && gd._promises) || []).push(new Promise(function(resolve) { - _context.style({visibility: 'hidden'}); + _context.style('display', 'none'); var config = {fontSize: parseInt(_context.style('font-size'), 10)}; texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) { @@ -84,7 +92,11 @@ exports.convertToTspans = function(_context, gd, _callback) { var mathjaxGroup = parent.append('g') .classed(svgClass + '-group', true) - .attr({'pointer-events': 'none'}); + .attr({ + 'pointer-events': 'none', + 'data-unformatted': str, + 'data-math': 'Y' + }); mathjaxGroup.node().appendChild(newSvg.node()); @@ -315,7 +327,7 @@ function buildSVGText(containerNode, str) { var lineNode = document.createElementNS(xmlnsNamespaces.svg, 'tspan'); d3.select(lineNode).attr({ class: 'line', - dy: (currentLine * 1.3) + 'em' + dy: (currentLine * LINE_SPACING) + 'em' }); containerNode.appendChild(lineNode); @@ -462,6 +474,35 @@ function buildSVGText(containerNode, str) { return hasLink; } +exports.lineCount = function lineCount(s) { + return s.selectAll('tspan.line').size() || 1; +}; + +exports.positionText = function positionText(s, x, y) { + return s.each(function() { + var text = d3.select(this); + + function setOrGet(attr, val) { + if(val === undefined) { + val = text.attr(attr); + if(val === null) { + text.attr(attr, 0); + val = 0; + } + } + else text.attr(attr, val); + return val; + } + + var thisX = setOrGet('x', x); + var thisY = setOrGet('y', y); + + if(this.nodeName === 'text') { + text.selectAll('tspan.line').attr({x: thisX, y: thisY}); + } + }); +}; + function alignHTMLWith(_base, container, options) { var alignH = options.horizontalAlign, alignV = options.verticalAlign || 'top', diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f993e5aaba0..c0ffed9387b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -480,9 +480,10 @@ function plotPolar(gd, data, layout) { .call(titleLayout); if(gd._context.editable) { - title.attr({'data-unformatted': txt}); if(!txt || txt === placeholderText) { opacity = 0.2; + // placeholder is not going through convertToTspans + // so needs explicit data-unformatted title.attr({'data-unformatted': placeholderText}) .text(placeholderText) .style({opacity: opacity}) @@ -500,8 +501,7 @@ function plotPolar(gd, data, layout) { this.call(svgTextUtils.makeEditable, {gd: gd}) .on('edit', function(text) { gd.framework({layout: {title: text}}); - this.attr({'data-unformatted': text}) - .text(text) + this.text(text) .call(titleLayout); this.call(setContenteditable); }) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 18bdd7693ff..4cea8757d31 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1813,7 +1813,7 @@ axes.doTicks = function(gd, axid, skipTitle) { var thisLabel = d3.select(this), newPromise = gd._promises.length; thisLabel - .call(Drawing.setPosition, labelx(d), labely(d)) + .call(svgTextUtils.positionText, labelx(d), labely(d)) .call(Drawing.font, d.font, d.fontSize, d.fontColor) .text(d.text) .call(svgTextUtils.convertToTspans, gd); @@ -1849,17 +1849,10 @@ axes.doTicks = function(gd, axid, skipTitle) { (labely(d) - d.fontSize / 2) + ')') : ''); if(mathjaxGroup.empty()) { - var txt = thisLabel.select('text').attr({ + thisLabel.select('text').attr({ transform: transform, 'text-anchor': anchor }); - - if(!txt.empty()) { - txt.selectAll('tspan.line').attr({ - x: txt.attr('x'), - y: txt.attr('y') - }); - } } else { var mjShift = diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index e50cdae6012..a043dead109 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -131,7 +131,7 @@ function createCamera(scene) { if(Math.abs(dx * dydx) > Math.abs(dy)) { result.boxEnd[1] = result.boxStart[1] + - Math.abs(dx) * dydx * (Math.sign(dy) || 1); + Math.abs(dx) * dydx * (dy >= 0 ? 1 : -1); // gl-select-box clips to the plot area bounds, // which breaks the axis constraint, so don't allow @@ -149,7 +149,7 @@ function createCamera(scene) { } else { result.boxEnd[0] = result.boxStart[0] + - Math.abs(dy) / dydx * (Math.sign(dx) || 1); + Math.abs(dy) / dydx * (dx >= 0 ? 1 : -1); if(result.boxEnd[0] < dataBox[0]) { result.boxEnd[0] = dataBox[0]; diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 386d45566cb..85b4cd280b1 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -86,20 +86,22 @@ module.exports = function toSVG(gd, format) { svg.node().style.background = ''; svg.selectAll('text') - .attr('data-unformatted', null) + .attr({'data-unformatted': null, 'data-math': null}) .each(function() { var txt = d3.select(this); - // hidden text is pre-formatting mathjax, - // the browser ignores it but it can still confuse batik - if(txt.style('visibility') === 'hidden') { + // hidden text is pre-formatting mathjax, the browser ignores it + // but in a static plot it's useless and it can confuse batik + // we've tried to standardize on display:none but make sure we still + // catch visibility:hidden if it ever arises + if(txt.style('visibility') === 'hidden' || txt.style('display') === 'none') { txt.remove(); return; } else { - // force other visibility value to export as visible + // clear other visibility/display values to default // to not potentially confuse non-browser SVG implementations - txt.style('visibility', 'visible'); + txt.style({visibility: null, display: null}); } // Font family styles break things because of quotation marks, diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 9cf578948fa..a534f2b9da7 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -139,21 +139,17 @@ module.exports = function plot(gd, plotinfo, cdbar) { function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { function appendTextNode(bar, text, textFont) { var textSelection = bar.append('text') - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1) .text(text) .attr({ 'class': 'bartext', transform: '', 'text-anchor': 'middle', - x: 0, - y: 0 + // prohibit tex interpretation until we can handle + // tex and regular text together + 'data-notex': 1 }) - .call(Drawing.font, textFont); - - textSelection.call(svgTextUtils.convertToTspans, gd); - textSelection.selectAll('tspan.line').attr({x: 0, y: 0}); + .call(Drawing.font, textFont) + .call(svgTextUtils.convertToTspans, gd); return textSelection; } diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 333f9f9c66e..914982943c3 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -14,6 +14,7 @@ var Drawing = require('../../components/drawing'); var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); var orientText = require('./orient_text'); +var svgTextUtils = require('../../lib/svg_text_utils'); module.exports = function plot(gd, plotinfo, cdcarpet) { for(var i = 0; i < cdcarpet.length; i++) { @@ -59,10 +60,10 @@ function plotOne(gd, plotinfo, cd) { drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); - var maxAExtent = drawAxisLabels(Drawing.tester, xa, ya, trace, t, labelLayer, aax._labels, 'a-label'); - var maxBExtent = drawAxisLabels(Drawing.tester, xa, ya, trace, t, labelLayer, bax._labels, 'b-label'); + var maxAExtent = drawAxisLabels(gd, xa, ya, trace, t, labelLayer, aax._labels, 'a-label'); + var maxBExtent = drawAxisLabels(gd, xa, ya, trace, t, labelLayer, bax._labels, 'b-label'); - drawAxisTitles(labelLayer, trace, t, xa, ya, maxAExtent, maxBExtent); + drawAxisTitles(gd, labelLayer, trace, t, xa, ya, maxAExtent, maxBExtent); // Swap for debugging in order to draw directly: // drawClipPath(trace, axisLayer, xa, ya); @@ -133,7 +134,7 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { gridJoin.exit().remove(); } -function drawAxisLabels(tester, xaxis, yaxis, trace, t, layer, labels, labelClass) { +function drawAxisLabels(gd, xaxis, yaxis, trace, t, layer, labels, labelClass) { var labelJoin = layer.selectAll('text.' + labelClass).data(labels); labelJoin.enter().append('text') @@ -152,20 +153,26 @@ function drawAxisLabels(tester, xaxis, yaxis, trace, t, layer, labels, labelClas orientation = orientText(trace, xaxis, yaxis, label.xy, [Math.cos(angle), Math.sin(angle)]); } var direction = (label.endAnchor ? -1 : 1) * orientation.flip; - var bbox = Drawing.measureText(tester, label.text, label.font); - d3.select(this) - .attr('text-anchor', direction > 0 ? 'start' : 'end') + var labelEl = d3.select(this) + .attr({ + 'text-anchor': direction > 0 ? 'start' : 'end', + 'data-notex': 1 + }) + .call(Drawing.font, label.font) .text(label.text) - .attr('transform', + .call(svgTextUtils.convertToTspans, gd); + + var bbox = Drawing.bBox(this); + + labelEl.attr('transform', // Translate to the correct point: 'translate(' + orientation.p[0] + ',' + orientation.p[1] + ') ' + // Rotate to line up with grid line tangent: 'rotate(' + orientation.angle + ')' + // Adjust the baseline and indentation: 'translate(' + label.axis.labelpadding * direction + ',' + bbox.height * 0.3 + ')' - ) - .call(Drawing.font, label.font.family, label.font.size, label.font.color); + ); maxExtent = Math.max(maxExtent, bbox.width + label.axis.labelpadding); }); @@ -175,23 +182,23 @@ function drawAxisLabels(tester, xaxis, yaxis, trace, t, layer, labels, labelClas return maxExtent; } -function drawAxisTitles(layer, trace, t, xa, ya, maxAExtent, maxBExtent) { +function drawAxisTitles(gd, layer, trace, t, xa, ya, maxAExtent, maxBExtent) { var a, b, xy, dxy; a = 0.5 * (trace.a[0] + trace.a[trace.a.length - 1]); b = trace.b[0]; xy = trace.ab2xy(a, b, true); dxy = trace.dxyda_rough(a, b); - drawAxisTitle(layer, trace, t, xy, dxy, trace.aaxis, xa, ya, maxAExtent, 'a-title'); + drawAxisTitle(gd, layer, trace, t, xy, dxy, trace.aaxis, xa, ya, maxAExtent, 'a-title'); a = trace.a[0]; b = 0.5 * (trace.b[0] + trace.b[trace.b.length - 1]); xy = trace.ab2xy(a, b, true); dxy = trace.dxydb_rough(a, b); - drawAxisTitle(layer, trace, t, xy, dxy, trace.baxis, xa, ya, maxBExtent, 'b-title'); + drawAxisTitle(gd, layer, trace, t, xy, dxy, trace.baxis, xa, ya, maxBExtent, 'b-title'); } -function drawAxisTitle(layer, trace, t, xy, dxy, axis, xa, ya, offset, labelClass) { +function drawAxisTitle(gd, layer, trace, t, xy, dxy, axis, xa, ya, offset, labelClass) { var data = []; if(axis.title) data.push(axis.title); var titleJoin = layer.selectAll('text.' + labelClass).data(data); @@ -214,6 +221,7 @@ function drawAxisTitle(layer, trace, t, xy, dxy, axis, xa, ya, offset, labelClas var el = d3.select(this); el.text(axis.title || '') + .call(svgTextUtils.convertToTspans, gd) .attr('transform', 'translate(' + orientation.p[0] + ',' + orientation.p[1] + ') ' + 'rotate(' + orientation.angle + ') ' + diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 52fce7b0e4a..bb6b48a2dcd 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -251,14 +251,11 @@ module.exports = function plot(gd, cdpie) { .attr({ 'class': 'slicetext', transform: '', - 'text-anchor': 'middle', - x: 0, - y: 0 + 'text-anchor': 'middle' }) .call(Drawing.font, textPosition === 'outside' ? trace.outsidetextfont : trace.insidetextfont) .call(svgTextUtils.convertToTspans, gd); - sliceText.selectAll('tspan.line').attr({x: 0, y: 0}); // position the text relative to the slice // TODO: so far this only accounts for flat diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 50a3d5870c5..ba2e5eae09d 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -220,7 +220,7 @@ function nodeModel(uniqueKeys, d, n) { // rendering snippets function crispLinesOnEnd(sankeyNode) { - d3.select(sankeyNode.node().parentElement).style('shape-rendering', 'crispEdges'); + d3.select(sankeyNode.node().parentNode).style('shape-rendering', 'crispEdges'); } function updateNodePositions(sankeyNode) { @@ -239,7 +239,7 @@ function linkPath(d) { } function updateNodeShapes(sankeyNode) { - d3.select(sankeyNode.node().parentElement).style('shape-rendering', 'optimizeSpeed'); + d3.select(sankeyNode.node().parentNode).style('shape-rendering', 'optimizeSpeed'); sankeyNode.call(updateNodePositions); } @@ -625,7 +625,7 @@ module.exports = function(svg, styledData, layout, callbacks) { nodeLabelTextPath .text(function(d) {return d.horizontal || d.node.dy > 5 ? d.node.label : '';}) - .style('text-anchor', function(d) {return d.horizontal && d.left ? 'end' : 'start';}); + .attr('text-anchor', function(d) {return d.horizontal && d.left ? 'end' : 'start';}); nodeLabelTextPath .transition() diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 577e099fb86..1c928211da3 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -70,9 +70,21 @@ function assertSrcContents() { // look for .classList if(node.type === 'MemberExpression') { var parts = node.source().split('.'); - if(parts[parts.length - 1] === 'classList') { + var lastPart = parts[parts.length - 1]; + if(lastPart === 'classList') { logs.push(file + ' : contains .classList (IE failure)'); } + else if(lastPart === 'innerHTML') { + // Note: if we do anything that's NOT in SVG, innerHTML is + // OK in IE. We can cross that bridge when we get to it... + logs.push(file + ' : contains .innerHTML (IE failure in SVG)'); + } + else if(lastPart === 'parentElement') { + logs.push(file + ' : contains .parentElement (IE failure)'); + } + else if(node.source() === 'Math.sign') { + logs.push(file + ' : contains Math.sign (IE failure)'); + } } }); diff --git a/test/image/baselines/binding.png b/test/image/baselines/binding.png index 6c69d0ea29c..29be16f1ce0 100644 Binary files a/test/image/baselines/binding.png and b/test/image/baselines/binding.png differ diff --git a/test/image/baselines/cheater.png b/test/image/baselines/cheater.png index 90e53ea91bb..c22359e7d98 100644 Binary files a/test/image/baselines/cheater.png and b/test/image/baselines/cheater.png differ diff --git a/test/image/baselines/range_selector.png b/test/image/baselines/range_selector.png index c8fbbddd96f..9f4d3fa8615 100644 Binary files a/test/image/baselines/range_selector.png and b/test/image/baselines/range_selector.png differ diff --git a/test/image/baselines/range_selector_style.png b/test/image/baselines/range_selector_style.png index def2526a93a..d4b8f889dd2 100644 Binary files a/test/image/baselines/range_selector_style.png and b/test/image/baselines/range_selector_style.png differ diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png index bb8cbabdb04..e8a9177497f 100644 Binary files a/test/image/baselines/sliders.png and b/test/image/baselines/sliders.png differ diff --git a/test/image/baselines/updatemenus.png b/test/image/baselines/updatemenus.png index 81809e1739d..248c2d61f11 100644 Binary files a/test/image/baselines/updatemenus.png and b/test/image/baselines/updatemenus.png differ diff --git a/test/image/baselines/updatemenus_positioning.png b/test/image/baselines/updatemenus_positioning.png index 27699ee4298..9f9fc426fd2 100644 Binary files a/test/image/baselines/updatemenus_positioning.png and b/test/image/baselines/updatemenus_positioning.png differ diff --git a/test/image/mocks/cheater.json b/test/image/mocks/cheater.json index 9c07f7aa555..e9f0817735b 100644 --- a/test/image/mocks/cheater.json +++ b/test/image/mocks/cheater.json @@ -20,16 +20,18 @@ ], "cheaterslope":2, "aaxis":{ - "title":"width, cm", + "title":"area, cm2
of fingernails", "tickformat":".1f", "type":"linear", - "smoothing": 0 + "smoothing": 0, + "ticksuffix": "×102" }, "baxis":{ - "title":"height, cm", + "title":"height, m
with shoes", "tickformat":".2f", "type":"linear", - "smoothing": 0 + "smoothing": 0, + "tickprefix": "*" } }, { diff --git a/test/image/mocks/range_selector_style.json b/test/image/mocks/range_selector_style.json index a802a17cd10..d8dce9a7688 100644 --- a/test/image/mocks/range_selector_style.json +++ b/test/image/mocks/range_selector_style.json @@ -1060,7 +1060,7 @@ "step": "year", "stepmode": "todate", "count": 1, - "label": "year
to
date" + "label": "year
to
date
" }, { "step": "all", @@ -1102,8 +1102,7 @@ "y": 1.02, "yanchor": "bottom" }, - "height": 450, - "width": 1000, - "autosize": true + "height": 500, + "width": 700 } } diff --git a/test/image/mocks/sliders.json b/test/image/mocks/sliders.json index c250b96208d..8afabcfa2d3 100644 --- a/test/image/mocks/sliders.json +++ b/test/image/mocks/sliders.json @@ -57,7 +57,7 @@ }, { "active": 4, "steps": [{ - "label": "red", + "label": "red
color", "method": "restyle", "args": [{"marker.color": "red"}] }, { diff --git a/test/image/mocks/updatemenus.json b/test/image/mocks/updatemenus.json index 707150551db..32cd7e55e7d 100644 --- a/test/image/mocks/updatemenus.json +++ b/test/image/mocks/updatemenus.json @@ -91,7 +91,7 @@ "line.color", "red" ], - "label": "red" + "label": "red
color" }, { "method": "restyle", @@ -99,7 +99,7 @@ "line.color", "blue" ], - "label": "blue" + "label": "blue
color" }, { "method": "restyle", @@ -107,7 +107,7 @@ "line.color", "green" ], - "label": "green" + "label": "green
trace
color" } ], "active": 1 diff --git a/test/jasmine/tests/page_test.js b/test/jasmine/tests/page_test.js new file mode 100644 index 00000000000..b0a134bc2e3 --- /dev/null +++ b/test/jasmine/tests/page_test.js @@ -0,0 +1,88 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var fail = require('../assets/fail_test'); + + +describe('page rendering', function() { + 'use strict'; + + var gd; + + afterEach(destroyGraphDiv); + + beforeEach(function() { + gd = createGraphDiv(); + }); + + it('should hide all elements if the div is hidden with visibility:hidden', function(done) { + // Note: we don't need to test the case of display: none, because that + // halts all rendering of children, regardless of their display/visibility properties + // and interestingly, unlike `visibility` which gets inherited as a computed style, + // display: none does not propagate through to children so we can't actually see + // that they're invisible - I guess the only way to tell that would be + + // make a plot that has pretty much all kinds of plot elements + // start with plot_types, because it has all the subplot types already + var mock = Lib.extendDeep({}, require('@mocks/plot_types.json')); + + mock.data.push( + {type: 'contour', z: [[1, 2], [3, 4]], coloring: 'heatmap'} + ); + + mock.layout.annotations = [ + {x: 1, y: 1, text: '$x+y$'}, + {x: 1, y: 1, text: 'not math', ax: -20, ay: -20} + ]; + + mock.layout.shapes = [{x0: 0.5, x1: 1.5, y0: 0.5, y1: 1.5}]; + + mock.layout.images = [{ + source: 'https://images.plot.ly/language-icons/api-home/python-logo.png', + xref: 'paper', + yref: 'paper', + x: 0, + y: 1, + sizex: 0.2, + sizey: 0.2, + xanchor: 'right', + yanchor: 'bottom' + }]; + + // then merge in a few more with other component types + mock.layout.updatemenus = require('@mocks/updatemenus.json').layout.updatemenus; + mock.layout.sliders = require('@mocks/sliders.json').layout.sliders; + + mock.layout.xaxis.title = 'XXX'; + mock.layout.showlegend = true; + + return Plotly.newPlot(gd, mock.data, mock.layout).then(function() { + var gd3 = d3.select(gd); + var allPresentationElements = gd3.selectAll('path,text,rect,image,canvas'); + + gd3.style('visibility', 'hidden'); + + // visibility: hidden is inherited by all children (unless overridden + // somewhere in the tree) + allPresentationElements.each(function() { + expect(d3.select(this).style('visibility')).toBe('hidden'); + }); + + gd3.style({visibility: null, display: 'none'}); + + // display: none is not inherited, but will zero-out the bounding box + // in principle we shouldn't need to do this test, as display: none + // cannot be overridden in a child, but it's included here for completeness. + allPresentationElements.each(function() { + var bBox = this.getBoundingClientRect(); + expect(bBox.width).toBe(0); + expect(bBox.height).toBe(0); + }); + }) + .catch(fail) + .then(done); + }); +}); diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js index 16caa318a70..12cc6ac3b64 100644 --- a/test/jasmine/tests/snapshot_test.js +++ b/test/jasmine/tests/snapshot_test.js @@ -224,12 +224,14 @@ describe('Plotly.Snapshot', function() { }); it('should force *visibility: visible* for text elements with *visibility: inherit*', function(done) { + // we've gotten rid of visibility almost entirely, using display instead d3.select(gd).style('visibility', 'inherit'); Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function() { d3.select(gd).selectAll('text').each(function() { expect(d3.select(this).style('visibility')).toEqual('visible'); + expect(d3.select(this).style('display')).toEqual('block'); }); return Plotly.Snapshot.toSVG(gd); @@ -239,7 +241,8 @@ describe('Plotly.Snapshot', function() { textElements = svgDOM.getElementsByTagName('text'); for(var i = 0; i < textElements.length; i++) { - expect(textElements[i].style.visibility).toEqual('visible'); + expect(textElements[i].style.visibility).toEqual(''); + expect(textElements[i].style.display).toEqual(''); } done(); diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index eadbf80e842..4a118295b80 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -288,11 +288,11 @@ describe('svg+text utils', function() { it('allows one to span
s', function() { var node = mockTextSVGElement('be Bold
and
Strong
'); expect(node.html()).toBe( - 'be ' + + 'be ' + 'Bold' + - '' + + '' + 'and' + - '' + + '' + '' + 'Strong'); }); @@ -300,10 +300,10 @@ describe('svg+text utils', function() { it('allows one to span
s', function() { var node = mockTextSVGElement('SO4
44
'); expect(node.html()).toBe( - 'SO\u200b' + + 'SO\u200b' + '4' + '\u200b' + - '\u200b' + + '\u200b' + '44' + '\u200b'); }); @@ -318,7 +318,7 @@ describe('svg+text utils', function() { textCases.forEach(function(textCase) { var node = mockTextSVGElement(textCase); function opener(dy) { - return '' + + return '' + '' + '' + '\u200b'; diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index 74ca4743ee3..ff27cf27958 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -651,7 +651,7 @@ describe('update menus interactions', function() { return Plotly.relayout(gd, 'updatemenus[1].buttons[1].label', 'a looooooooooooong
label'); }).then(function() { - assertItemDims(selectHeader(1), 179, 35); + assertItemDims(selectHeader(1), 165, 35); return click(selectHeader(1)); }).then(function() {