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`