diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index bf1ebf7fbec..deb849cc47f 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -248,7 +248,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { }[options.align] || 'middle' }); - svgTextUtils.convertToTspans(s, gd, drawGraphicalElements); + svgTextUtils.convertToTspans(s, gd, null, drawGraphicalElements); return s; } diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6b6e783a68b..7e97c29f146 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -496,7 +496,7 @@ function setupTraceToggle(g, gd) { function textLayout(s, g, gd, legendObj, aTitle) { if(legendObj._inHover) s.attr('data-notex', true); // do not process MathJax for unified hover - svgTextUtils.convertToTspans(s, gd, function() { + svgTextUtils.convertToTspans(s, gd, null, function() { computeTextDimensions(g, gd, legendObj, aTitle); }); } diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 3a260108d67..39ed094c664 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -56,6 +56,8 @@ function draw(gd, titleClass, options) { var attributes = options.attributes; var transform = options.transform; var group = options.containerGroup; + var isAxis = options.isAxis; + var wrap = options.wrap; var fullLayout = gd._fullLayout; @@ -120,6 +122,7 @@ function draw(gd, titleClass, options) { function drawTitle(titleEl) { var transformVal; + var convertOptions; if(transform) { transformVal = ''; @@ -133,6 +136,17 @@ function draw(gd, titleClass, options) { transformVal = null; } + if(isAxis && wrap) { + var axisName = options.propContainer._name; + var axis = gd._fullLayout[axisName]; + + convertOptions = { + wrap: wrap, + axisLength: axis._length, + axisOrientation: axis._id.substr(0, 1) === 'y' ? 'v' : 'h' + }; + } + titleEl.attr('transform', transformVal); titleEl.style({ @@ -143,13 +157,13 @@ function draw(gd, titleClass, options) { 'font-weight': Plots.fontWeight }) .attr(attributes) - .call(svgTextUtils.convertToTspans, gd); + .call(svgTextUtils.convertToTspans, gd, convertOptions); return Plots.previousPromises(gd); } - function scootTitle(titleElIn) { - var titleGroup = d3.select(titleElIn.node().parentNode); + function scootTitle(titleEl) { + var titleGroup = d3.select(titleEl.node().parentNode); if(avoid && avoid.selection && avoid.side && txt) { titleGroup.attr('transform', null); diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 55a4123cb70..2c82c97fd3b 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -17,7 +17,15 @@ function getSize(_selection, _dimension) { var FIND_TEX = /([^$]*)([$]+[^$]*[$]+)([^$]*)/; -exports.convertToTspans = function(_context, gd, _callback) { +/** + * Converts to SVG element. + * @param {*} _context + * @param {*} gd `graphDiv`. + * @param {{ axisLength: number, axisOrientation: 'v' | 'h', wrap?: boolean }} options + * @param {Function} _callback + * @returns Modified `_context`. + */ +exports.convertToTspans = function(_context, gd, options, _callback) { var str = _context.text(); // Until we get tex integrated more fully (so it can be used along with non-tex) @@ -50,7 +58,7 @@ exports.convertToTspans = function(_context, gd, _callback) { _context.text('') .style('white-space', 'pre'); - var hasLink = buildSVGText(_context.node(), str); + var hasLink = buildSVGText(_context.node(), str, options); if(hasLink) { // at least in Chrome, pointer-events does not seem @@ -428,17 +436,16 @@ function fromCodePoint(code) { ); } -/* - * buildSVGText: convert our pseudo-html into SVG tspan elements, and attach these - * to containerNode +/** + * Converts our pseudo-html SVG `` into elements, and attach these to `containerNode`. * * @param {svg text element} containerNode: the node to insert this text into * @param {string} str: the pseudo-html string to convert to svg - * + * @param {{ axisLength: number, axisOrientation: 'v' | 'h', wrap?: boolean }} options * @returns {bool}: does the result contain any links? We need to handle the text element * somewhat differently if it does, so just keep track of this when it happens. */ -function buildSVGText(containerNode, str) { +function buildSVGText(containerNode, str, options) { /* * Normalize behavior between IE and others wrt newlines and whitespace:pre * this combination makes IE barf https://github.com/plotly/plotly.js/issues/746 @@ -530,7 +537,11 @@ function buildSVGText(containerNode, str) { } function addTextNode(node, text) { - node.appendChild(document.createTextNode(text)); + return node.appendChild(document.createTextNode(text)); + } + + function removeTextNode(node, child) { + node.removeChild(child); } function exitNode(type) { @@ -550,16 +561,18 @@ function buildSVGText(containerNode, str) { currentNode = nodeStack[nodeStack.length - 1].node; } - var hasLines = BR_TAG.test(str); + var hasBrLines = BR_TAG.test(str); - if(hasLines) newLine(); + if(hasBrLines) newLine(); else { currentNode = containerNode; nodeStack = [{node: containerNode}]; } var parts = str.split(SPLIT_TAGS); - for(var i = 0; i < parts.length; i++) { + + var i = 0; + for(i; i < parts.length; i++) { var parti = parts[i]; var match = parti.match(ONE_TAG); var tagType = match && match[2].toLowerCase(); @@ -568,7 +581,24 @@ function buildSVGText(containerNode, str) { if(tagType === 'br') { newLine(); } else if(tagStyle === undefined) { - addTextNode(currentNode, convertEntities(parti)); + if(!(options && options.wrap)) return void(addTextNode(currentNode, convertEntities(parti))); + + var wordId = 0; + var wordsArray = parti.split(' '); + + newLine(); + + for(wordId; wordId < wordsArray.length; wordId++) { + var word = wordsArray[wordId]; + var preSpace = wordId === 0 ? '' : ' '; + var child = addTextNode(currentNode, convertEntities(preSpace + word)); + + if(currentNode.getBBox().width > options.axisLength) { + removeTextNode(currentNode, child); + newLine(); + addTextNode(currentNode, convertEntities(word)); + } + } } else { // tag - open or close if(match[1]) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 44ebc870209..7b16ab6c298 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -3464,6 +3464,8 @@ function drawTitle(gd, ax) { var axId = ax._id; var axLetter = axId.charAt(0); var fontSize = ax.title.font.size; + var wrap = ax.title.wrap; // TODO: Update our API documentation and the TypeScript types!. + var titleStandoff; if(ax.title.hasOwnProperty('standoff')) { @@ -3536,7 +3538,9 @@ function drawTitle(gd, ax) { placeholder: fullLayout._dfltTitle[axLetter], avoid: avoid, transform: transform, - attributes: {x: x, y: y, 'text-anchor': 'middle'} + attributes: {x: x, y: y, 'text-anchor': 'middle'}, + wrap: wrap, + isAxis: true, }); } diff --git a/src/traces/table/plot.js b/src/traces/table/plot.js index 4ef177226f3..10fc0f46497 100644 --- a/src/traces/table/plot.js +++ b/src/traces/table/plot.js @@ -561,7 +561,7 @@ function populateCellText(cellText, tableControlView, allColumnBlock, gd) { var renderCallback = d.wrappingNeeded ? wrapTextMaker : updateYPositionMaker; if(d.needsConvertToTspans) { - svgUtil.convertToTspans(selection, gd, renderCallback(allColumnBlock, element, tableControlView, gd, d)); + svgUtil.convertToTspans(selection, gd, null, renderCallback(allColumnBlock, element, tableControlView, gd, d)); } else { d3.select(element.parentNode) // basic cell adjustment - compliance with `cellPad`