diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js
index eb1efe65552..8a5b68018ea 100644
--- a/src/components/colorbar/attributes.js
+++ b/src/components/colorbar/attributes.js
@@ -175,21 +175,56 @@ module.exports = overrideAll({
exponentformat: axesAttrs.exponentformat,
showexponent: axesAttrs.showexponent,
title: {
- valType: 'string',
- role: 'info',
- description: 'Sets the title of the color bar.'
+ text: {
+ valType: 'string',
+ role: 'info',
+ description: [
+ 'Sets the title of the color bar.',
+ 'Note that before the existence of `title.text`, the title\'s',
+ 'contents used to be defined as the `title` attribute itself.',
+ 'This behavior has been deprecated.'
+ ].join(' ')
+ },
+ font: fontAttrs({
+ description: [
+ 'Sets this color bar\'s title font.',
+ 'Note that the title\'s font used to be set',
+ 'by the now deprecated `titlefont` attribute.'
+ ].join(' ')
+ }),
+ side: {
+ valType: 'enumerated',
+ values: ['right', 'top', 'bottom'],
+ role: 'style',
+ dflt: 'top',
+ description: [
+ 'Determines the location of color bar\'s title',
+ 'with respect to the color bar.',
+ 'Note that the title\'s location used to be set',
+ 'by the now deprecated `titleside` attribute.'
+ ].join(' ')
+ }
},
- titlefont: fontAttrs({
- description: 'Sets this color bar\'s title font.'
- }),
- titleside: {
- valType: 'enumerated',
- values: ['right', 'top', 'bottom'],
- role: 'style',
- dflt: 'top',
- description: [
- 'Determines the location of the colorbar title',
- 'with respect to the color bar.'
- ].join(' ')
+
+ _deprecated: {
+ title: {
+ valType: 'string',
+ role: 'info',
+ description: [
+ 'Deprecated in favor of color bar\'s `title.text`.',
+ 'Note that value of color bar\'s `title` is no longer a simple',
+ '*string* but a set of sub-attributes.'
+ ].join(' ')
+ },
+ titlefont: fontAttrs({
+ description: 'Deprecated in favor of color bar\'s `title.font`.'
+ }),
+ titleside: {
+ valType: 'enumerated',
+ values: ['right', 'top', 'bottom'],
+ role: 'style',
+ dflt: 'top',
+ description: 'Deprecated in favor of color bar\'s `title.side`.'
+ }
}
}, 'colorbars', 'from-root');
diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js
index 6ad1e3d5cbb..8225f2d0801 100644
--- a/src/components/colorbar/defaults.js
+++ b/src/components/colorbar/defaults.js
@@ -59,7 +59,7 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) {
handleTickLabelDefaults(colorbarIn, colorbarOut, coerce, 'linear', opts);
handleTickMarkDefaults(colorbarIn, colorbarOut, coerce, 'linear', opts);
- coerce('title', layout._dfltTitle.colorbar);
- Lib.coerceFont(coerce, 'titlefont', layout.font);
- coerce('titleside');
+ coerce('title.text', layout._dfltTitle.colorbar);
+ Lib.coerceFont(coerce, 'title.font', layout.font);
+ coerce('title.side');
};
diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js
index a87982e3628..c6ccf28d21f 100644
--- a/src/components/colorbar/draw.js
+++ b/src/components/colorbar/draw.js
@@ -184,7 +184,6 @@ module.exports = function draw(gd, id) {
showticksuffix: opts.showticksuffix,
ticksuffix: opts.ticksuffix,
title: opts.title,
- titlefont: opts.titlefont,
showline: true,
anchor: 'free',
side: 'right',
@@ -218,11 +217,11 @@ module.exports = function draw(gd, id) {
// save for other callers to access this axis
component.axis = cbAxisOut;
- if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
- cbAxisOut.titleside = opts.titleside;
+ if(['top', 'bottom'].indexOf(opts.title.side) !== -1) {
+ cbAxisOut.title.side = opts.title.side;
cbAxisOut.titlex = opts.x + xpadFrac;
cbAxisOut.titley = yBottomFrac +
- (opts.titleside === 'top' ? lenFrac - ypadFrac : ypadFrac);
+ (opts.title.side === 'top' ? lenFrac - ypadFrac : ypadFrac);
}
if(opts.line.color && opts.tickmode === 'auto') {
@@ -285,15 +284,15 @@ module.exports = function draw(gd, id) {
var axisLayer = container.select('.cbaxis');
var titleHeight = 0;
- if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
+ if(['top', 'bottom'].indexOf(opts.title.side) !== -1) {
// draw the title so we know how much room it needs
// when we squish the axis. This one only applies to
// top or bottom titles, not right side.
var x = gs.l + (opts.x + xpadFrac) * gs.w,
- fontSize = cbAxisOut.titlefont.size,
+ fontSize = cbAxisOut.title.font.size,
y;
- if(opts.titleside === 'top') {
+ if(opts.title.side === 'top') {
y = (1 - (yBottomFrac + lenFrac - ypadFrac)) * gs.h +
gs.t + 3 + fontSize * 0.75;
}
@@ -307,7 +306,7 @@ module.exports = function draw(gd, id) {
}
function drawAxis() {
- if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
+ if(['top', 'bottom'].indexOf(opts.title.side) !== -1) {
// squish the axis top to make room for the title
var titleGroup = container.select('.cbtitle'),
titleText = titleGroup.select('text'),
@@ -338,7 +337,7 @@ module.exports = function draw(gd, id) {
// TODO: configurable
titleHeight += 5;
- if(opts.titleside === 'top') {
+ if(opts.title.side === 'top') {
cbAxisOut.domain[1] -= titleHeight / gs.h;
titleTrans[1] *= -1;
}
@@ -459,8 +458,8 @@ module.exports = function draw(gd, id) {
});
},
function() {
- if(['top', 'bottom'].indexOf(opts.titleside) === -1) {
- var fontSize = cbAxisOut.titlefont.size,
+ if(['top', 'bottom'].indexOf(opts.title.side) === -1) {
+ var fontSize = cbAxisOut.title.font.size,
y = cbAxisOut._offset + cbAxisOut._length / 2,
x = gs.l + (cbAxisOut.position || 0) * gs.w + ((cbAxisOut.side === 'right') ?
10 + fontSize * ((cbAxisOut.showticklabels ? 1 : 0.5)) :
@@ -472,7 +471,7 @@ module.exports = function draw(gd, id) {
drawTitle('h' + cbAxisOut._id + 'title', {
avoid: {
selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'),
- side: opts.titleside,
+ side: opts.title.side,
offsetLeft: gs.l,
offsetTop: 0,
maxShift: fullLayout.width
@@ -525,11 +524,11 @@ module.exports = function draw(gd, id) {
.node(),
titleWidth;
if(mathJaxNode &&
- ['top', 'bottom'].indexOf(opts.titleside) !== -1) {
+ ['top', 'bottom'].indexOf(opts.title.side) !== -1) {
titleWidth = Drawing.bBox(mathJaxNode).width;
}
else {
- // note: the formula below works for all titlesides,
+ // note: the formula below works for all title sides,
// (except for top/bottom mathjax, above)
// but the weird gs.l is because the titleunshift
// transform gets removed by Drawing.bBox
@@ -558,7 +557,7 @@ module.exports = function draw(gd, id) {
container.selectAll('.cboutline').attr({
x: xLeft,
y: yTopPx + opts.ypad +
- (opts.titleside === 'top' ? titleHeight : 0),
+ (opts.title.side === 'top' ? titleHeight : 0),
width: Math.max(thickPx, 2),
height: Math.max(outerheight - 2 * opts.ypad - titleHeight, 2)
})
diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js
index b9ab966ea10..e9b69b620fe 100644
--- a/src/components/legend/draw.js
+++ b/src/components/legend/draw.js
@@ -30,7 +30,6 @@ var FROM_BR = alignmentConstants.FROM_BR;
var getLegendData = require('./get_legend_data');
var style = require('./style');
var helpers = require('./helpers');
-var anchorUtils = require('./anchor_utils');
var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY;
@@ -154,17 +153,17 @@ module.exports = function draw(gd) {
lx = gs.l + gs.w * opts.x,
ly = gs.t + gs.h * (1 - opts.y);
- if(anchorUtils.isRightAnchor(opts)) {
+ if(Lib.isRightAnchor(opts)) {
lx -= opts._width;
}
- else if(anchorUtils.isCenterAnchor(opts)) {
+ else if(Lib.isCenterAnchor(opts)) {
lx -= opts._width / 2;
}
- if(anchorUtils.isBottomAnchor(opts)) {
+ if(Lib.isBottomAnchor(opts)) {
ly -= opts._height;
}
- else if(anchorUtils.isMiddleAnchor(opts)) {
+ else if(Lib.isMiddleAnchor(opts)) {
ly -= opts._height / 2;
}
@@ -700,18 +699,18 @@ function expandMargin(gd) {
opts = fullLayout.legend;
var xanchor = 'left';
- if(anchorUtils.isRightAnchor(opts)) {
+ if(Lib.isRightAnchor(opts)) {
xanchor = 'right';
}
- else if(anchorUtils.isCenterAnchor(opts)) {
+ else if(Lib.isCenterAnchor(opts)) {
xanchor = 'center';
}
var yanchor = 'top';
- if(anchorUtils.isBottomAnchor(opts)) {
+ if(Lib.isBottomAnchor(opts)) {
yanchor = 'bottom';
}
- else if(anchorUtils.isMiddleAnchor(opts)) {
+ else if(Lib.isMiddleAnchor(opts)) {
yanchor = 'middle';
}
@@ -731,10 +730,10 @@ function expandHorizontalMargin(gd) {
opts = fullLayout.legend;
var xanchor = 'left';
- if(anchorUtils.isRightAnchor(opts)) {
+ if(Lib.isRightAnchor(opts)) {
xanchor = 'right';
}
- else if(anchorUtils.isCenterAnchor(opts)) {
+ else if(Lib.isCenterAnchor(opts)) {
xanchor = 'center';
}
diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js
index aa717d663b5..65209412e70 100644
--- a/src/components/rangeselector/draw.js
+++ b/src/components/rangeselector/draw.js
@@ -17,7 +17,6 @@ var Drawing = require('../drawing');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
var axisIds = require('../../plots/cartesian/axis_ids');
-var anchorUtils = require('../legend/anchor_utils');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
@@ -218,21 +217,21 @@ function reposition(gd, buttons, opts, axName, selector) {
var ly = graphSize.t + graphSize.h * (1 - opts.y);
var xanchor = 'left';
- if(anchorUtils.isRightAnchor(opts)) {
+ if(Lib.isRightAnchor(opts)) {
lx -= width;
xanchor = 'right';
}
- if(anchorUtils.isCenterAnchor(opts)) {
+ if(Lib.isCenterAnchor(opts)) {
lx -= width / 2;
xanchor = 'center';
}
var yanchor = 'top';
- if(anchorUtils.isBottomAnchor(opts)) {
+ if(Lib.isBottomAnchor(opts)) {
ly -= height;
yanchor = 'bottom';
}
- if(anchorUtils.isMiddleAnchor(opts)) {
+ if(Lib.isMiddleAnchor(opts)) {
ly -= height / 2;
yanchor = 'middle';
}
diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js
index 25e1cee95b5..c885e1e9c65 100644
--- a/src/components/rangeslider/draw.js
+++ b/src/components/rangeslider/draw.js
@@ -172,7 +172,7 @@ module.exports = function(gd) {
placeholder: fullLayout._dfltTitle.x,
attributes: {
x: axisOpts._offset + axisOpts._length / 2,
- y: y + opts._height + opts._offsetShift + 10 + 1.5 * axisOpts.titlefont.size,
+ y: y + opts._height + opts._offsetShift + 10 + 1.5 * axisOpts.title.font.size,
'text-anchor': 'middle'
}
});
diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js
index 574dee98e9d..e0188bb0db2 100644
--- a/src/components/sliders/attributes.js
+++ b/src/components/sliders/attributes.js
@@ -133,7 +133,7 @@ module.exports = overrideAll(templatedArray('slider', {
role: 'style',
description: 'Sets the x position (in normalized coordinates) of the slider.'
},
- pad: extendDeepAll({}, padAttrs, {
+ pad: extendDeepAll(padAttrs({editType: 'arraydraw'}), {
description: 'Set the padding of the slider component along each side.'
}, {t: {dflt: 20}}),
xanchor: {
diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js
index 62557f92ded..b224d940176 100644
--- a/src/components/sliders/draw.js
+++ b/src/components/sliders/draw.js
@@ -15,7 +15,6 @@ var Color = require('../color');
var Drawing = require('../drawing');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
-var anchorUtils = require('../legend/anchor_utils');
var arrayEditor = require('../../plot_api/plot_template').arrayEditor;
var constants = require('./constants');
@@ -207,21 +206,21 @@ function findDimensions(gd, sliderOpts) {
dims.height = dims.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + dims.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b;
var xanchor = 'left';
- if(anchorUtils.isRightAnchor(sliderOpts)) {
+ if(Lib.isRightAnchor(sliderOpts)) {
dims.lx -= dims.outerLength;
xanchor = 'right';
}
- if(anchorUtils.isCenterAnchor(sliderOpts)) {
+ if(Lib.isCenterAnchor(sliderOpts)) {
dims.lx -= dims.outerLength / 2;
xanchor = 'center';
}
var yanchor = 'top';
- if(anchorUtils.isBottomAnchor(sliderOpts)) {
+ if(Lib.isBottomAnchor(sliderOpts)) {
dims.ly -= dims.height;
yanchor = 'bottom';
}
- if(anchorUtils.isMiddleAnchor(sliderOpts)) {
+ if(Lib.isMiddleAnchor(sliderOpts)) {
dims.ly -= dims.height / 2;
yanchor = 'middle';
}
diff --git a/src/components/titles/index.js b/src/components/titles/index.js
index e2211dd88df..8b3f9c81fa3 100644
--- a/src/components/titles/index.js
+++ b/src/components/titles/index.js
@@ -67,19 +67,21 @@ function draw(gd, titleClass, options) {
var group = options.containerGroup;
var fullLayout = gd._fullLayout;
- var titlefont = cont.titlefont || {};
- var font = titlefont.family;
- var fontSize = titlefont.size;
- var fontColor = titlefont.color;
var opacity = 1;
var isplaceholder = false;
- var txt = (cont.title || '').trim();
+ var title = cont.title;
+ var txt = (title && title.text ? title.text : '').trim();
+
+ var font = title && title.font ? title.font : {};
+ var fontFamily = font.family;
+ var fontSize = font.size;
+ var fontColor = font.color;
// only make this title editable if we positively identify its property
// as one that has editing enabled.
var editAttr;
- if(prop === 'title') editAttr = 'titleText';
+ if(prop === 'title.text') editAttr = 'titleText';
else if(prop.indexOf('axis') !== -1) editAttr = 'axisTitleText';
else if(prop.indexOf('colorbar' !== -1)) editAttr = 'colorbarTitleText';
var editable = gd._context.edits[editAttr];
@@ -137,7 +139,7 @@ function draw(gd, titleClass, options) {
titleEl.attr('transform', transformVal);
titleEl.style({
- 'font-family': font,
+ 'font-family': fontFamily,
'font-size': d3.round(fontSize, 2) + 'px',
fill: Color.rgb(fontColor),
opacity: opacity * Color.opacity(fontColor),
diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js
index 6224392ed02..e6cf24e8d5b 100644
--- a/src/components/updatemenus/attributes.js
+++ b/src/components/updatemenus/attributes.js
@@ -162,7 +162,7 @@ module.exports = overrideAll(templatedArray('updatemenu', {
].join(' ')
},
- pad: extendFlat({}, padAttrs, {
+ pad: extendFlat(padAttrs({editType: 'arraydraw'}), {
description: 'Sets the padding around the buttons or dropdown menu.'
}),
diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js
index f1e5a9adaad..778e01b685e 100644
--- a/src/components/updatemenus/draw.js
+++ b/src/components/updatemenus/draw.js
@@ -16,7 +16,6 @@ var Color = require('../color');
var Drawing = require('../drawing');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
-var anchorUtils = require('../legend/anchor_utils');
var arrayEditor = require('../../plot_api/plot_template').arrayEditor;
var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
@@ -566,21 +565,21 @@ function findDimensions(gd, menuOpts) {
dims.ly = graphSize.t + graphSize.h * (1 - menuOpts.y);
var xanchor = 'left';
- if(anchorUtils.isRightAnchor(menuOpts)) {
+ if(Lib.isRightAnchor(menuOpts)) {
dims.lx -= paddedWidth;
xanchor = 'right';
}
- if(anchorUtils.isCenterAnchor(menuOpts)) {
+ if(Lib.isCenterAnchor(menuOpts)) {
dims.lx -= paddedWidth / 2;
xanchor = 'center';
}
var yanchor = 'top';
- if(anchorUtils.isBottomAnchor(menuOpts)) {
+ if(Lib.isBottomAnchor(menuOpts)) {
dims.ly -= paddedHeight;
yanchor = 'bottom';
}
- if(anchorUtils.isMiddleAnchor(menuOpts)) {
+ if(Lib.isMiddleAnchor(menuOpts)) {
dims.ly -= paddedHeight / 2;
yanchor = 'middle';
}
diff --git a/src/constants/alignment.js b/src/constants/alignment.js
index a63cc18266d..120e9dc2361 100644
--- a/src/constants/alignment.js
+++ b/src/constants/alignment.js
@@ -41,12 +41,17 @@ module.exports = {
// multiple of fontSize to get the vertical offset between lines
LINE_SPACING: 1.3,
- // multiple of fontSize to shift from the baseline to the midline
+ // multiple of fontSize to shift from the baseline
+ // to the cap (captical letter) line
// (to use when we don't calculate this shift from Drawing.bBox)
- // To be precise this should be half the cap height (capital letter)
- // of the font, and according to wikipedia:
+ // This is an approximation since in reality cap height can differ
+ // from font to font. However, according to Wikipedia
// an "average" font might have a cap height of 70% of the em
// https://en.wikipedia.org/wiki/Em_(typography)#History
+ CAP_SHIFT: 0.70,
+
+ // half the cap height (distance between baseline and cap line)
+ // of an "average" font (for more info see above).
MID_SHIFT: 0.35,
OPPOSITE_SIDE: {
diff --git a/src/lib/anchor_utils.js b/src/lib/anchor_utils.js
new file mode 100644
index 00000000000..0c61fc36efc
--- /dev/null
+++ b/src/lib/anchor_utils.js
@@ -0,0 +1,62 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+
+/**
+ * Determine the position anchor property of x/y xanchor/yanchor components.
+ *
+ * - values < 1/3 align the low side at that fraction,
+ * - values [1/3, 2/3] align the center at that fraction,
+ * - values > 2/3 align the right at that fraction.
+ */
+
+
+exports.isLeftAnchor = function isLeftAnchor(opts) {
+ return (
+ opts.xanchor === 'left' ||
+ (opts.xanchor === 'auto' && opts.x <= 1 / 3)
+ );
+};
+
+exports.isCenterAnchor = function isCenterAnchor(opts) {
+ return (
+ opts.xanchor === 'center' ||
+ (opts.xanchor === 'auto' && opts.x > 1 / 3 && opts.x < 2 / 3)
+ );
+};
+
+exports.isRightAnchor = function isRightAnchor(opts) {
+ return (
+ opts.xanchor === 'right' ||
+ (opts.xanchor === 'auto' && opts.x >= 2 / 3)
+ );
+};
+
+exports.isTopAnchor = function isTopAnchor(opts) {
+ return (
+ opts.yanchor === 'top' ||
+ (opts.yanchor === 'auto' && opts.y >= 2 / 3)
+ );
+};
+
+exports.isMiddleAnchor = function isMiddleAnchor(opts) {
+ return (
+ opts.yanchor === 'middle' ||
+ (opts.yanchor === 'auto' && opts.y > 1 / 3 && opts.y < 2 / 3)
+ );
+};
+
+exports.isBottomAnchor = function isBottomAnchor(opts) {
+ return (
+ opts.yanchor === 'bottom' ||
+ (opts.yanchor === 'auto' && opts.y <= 1 / 3)
+ );
+};
diff --git a/src/lib/index.js b/src/lib/index.js
index 5b329d5c223..2d9de4b1ac1 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -100,6 +100,14 @@ lib.pathArc = anglesModule.pathArc;
lib.pathSector = anglesModule.pathSector;
lib.pathAnnulus = anglesModule.pathAnnulus;
+var anchorUtils = require('./anchor_utils');
+lib.isLeftAnchor = anchorUtils.isLeftAnchor;
+lib.isCenterAnchor = anchorUtils.isCenterAnchor;
+lib.isRightAnchor = anchorUtils.isRightAnchor;
+lib.isTopAnchor = anchorUtils.isTopAnchor;
+lib.isMiddleAnchor = anchorUtils.isMiddleAnchor;
+lib.isBottomAnchor = anchorUtils.isBottomAnchor;
+
var geom2dModule = require('./geometry2d');
lib.segmentsIntersect = geom2dModule.segmentsIntersect;
lib.segmentDistance = geom2dModule.segmentDistance;
diff --git a/src/lib/regex.js b/src/lib/regex.js
index acfaff9d41b..ded7c4dfe40 100644
--- a/src/lib/regex.js
+++ b/src/lib/regex.js
@@ -16,11 +16,13 @@
* @param {Optional(string)} tail: a fixed piece after the id
* eg counterRegex('scene', '.annotations') for scene2.annotations etc.
* @param {boolean} openEnded: if true, the string may continue past the match.
+ * @param {boolean} matchBeginning: if false, the string may start before the match.
*/
-exports.counter = function(head, tail, openEnded) {
+exports.counter = function(head, tail, openEnded, matchBeginning) {
var fullTail = (tail || '') + (openEnded ? '' : '$');
+ var startWithPrefix = matchBeginning === false ? '' : '^';
if(head === 'xy') {
- return new RegExp('^x([2-9]|[1-9][0-9]+)?y([2-9]|[1-9][0-9]+)?' + fullTail);
+ return new RegExp(startWithPrefix + 'x([2-9]|[1-9][0-9]+)?y([2-9]|[1-9][0-9]+)?' + fullTail);
}
- return new RegExp('^' + head + '([2-9]|[1-9][0-9]+)?' + fullTail);
+ return new RegExp(startWithPrefix + head + '([2-9]|[1-9][0-9]+)?' + fullTail);
};
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index cdb8177c60c..e2c244cbfd7 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -53,6 +53,8 @@ exports.cleanLayout = function(layout) {
}
var axisAttrRegex = (Plots.subplotsRegistry.cartesian || {}).attrRegex;
+ var polarAttrRegex = (Plots.subplotsRegistry.polar || {}).attrRegex;
+ var ternaryAttrRegex = (Plots.subplotsRegistry.ternary || {}).attrRegex;
var sceneAttrRegex = (Plots.subplotsRegistry.gl3d || {}).attrRegex;
var keys = Object.keys(layout);
@@ -91,6 +93,24 @@ exports.cleanLayout = function(layout) {
}
delete ax.autotick;
}
+
+ cleanTitle(ax);
+ }
+
+ // modifications for polar
+ else if(polarAttrRegex && polarAttrRegex.test(key)) {
+ var polar = layout[key];
+
+ cleanTitle(polar.radialaxis);
+ }
+
+ // modifications for ternary
+ else if(ternaryAttrRegex && ternaryAttrRegex.test(key)) {
+ var ternary = layout[key];
+
+ cleanTitle(ternary.aaxis);
+ cleanTitle(ternary.baxis);
+ cleanTitle(ternary.caxis);
}
// modifications for 3D scenes
@@ -119,6 +139,11 @@ exports.cleanLayout = function(layout) {
delete scene.cameraposition;
}
+
+ // clean axis titles
+ cleanTitle(scene.xaxis);
+ cleanTitle(scene.yaxis);
+ cleanTitle(scene.zaxis);
}
}
@@ -176,6 +201,9 @@ exports.cleanLayout = function(layout) {
}
}
+ // clean plot title
+ cleanTitle(layout);
+
/*
* Moved from rotate -> orbit for dragmode
*/
@@ -185,6 +213,11 @@ exports.cleanLayout = function(layout) {
// supported, but new tinycolor does not because they're not valid css
Color.clean(layout);
+ // clean the layout container in layout.template
+ if(layout.template && layout.template.layout) {
+ exports.cleanLayout(layout.template.layout);
+ }
+
return layout;
};
@@ -196,6 +229,46 @@ function cleanAxRef(container, attr) {
}
}
+/**
+ * Cleans up old title attribute structure (flat) in favor of the new one (nested).
+ *
+ * @param {Object} titleContainer - an object potentially including deprecated title attributes
+ */
+function cleanTitle(titleContainer) {
+ if(titleContainer) {
+
+ // title -> title.text
+ // (although title used to be a string attribute,
+ // numbers are accepted as well)
+ if(typeof titleContainer.title === 'string' || typeof titleContainer.title === 'number') {
+ titleContainer.title = {
+ text: titleContainer.title
+ };
+ }
+
+ rewireAttr('titlefont', 'font');
+ rewireAttr('titleposition', 'position');
+ rewireAttr('titleside', 'side');
+ rewireAttr('titleoffset', 'offset');
+ }
+
+ function rewireAttr(oldAttrName, newAttrName) {
+ var oldAttrSet = titleContainer[oldAttrName];
+ var newAttrSet = titleContainer.title && titleContainer.title[newAttrName];
+
+ if(oldAttrSet && !newAttrSet) {
+
+ // Ensure title object exists
+ if(!titleContainer.title) {
+ titleContainer.title = {};
+ }
+
+ titleContainer.title[newAttrName] = titleContainer[oldAttrName];
+ delete titleContainer[oldAttrName];
+ }
+ }
+}
+
/*
* cleanData: Make a few changes to the data for backward compatibility
* before it gets used for anything. Modifies the data traces users provide.
@@ -399,6 +472,13 @@ exports.cleanData = function(data) {
delete trace.autobiny;
delete trace.ybins;
}
+
+ cleanTitle(trace);
+ if(trace.colorbar) cleanTitle(trace.colorbar);
+ if(trace.marker && trace.marker.colorbar) cleanTitle(trace.marker.colorbar);
+ if(trace.line && trace.line.colorbar) cleanTitle(trace.line.colorbar);
+ if(trace.aaxis) cleanTitle(trace.aaxis);
+ if(trace.baxis) cleanTitle(trace.baxis);
}
};
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index b9f955a324c..15c4c4d0b0d 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -154,7 +154,7 @@ exports.plot = function(gd, data, layout, config) {
// Legacy polar plots
if(!fullLayout._has('polar') && data && data[0] && data[0].r) {
Lib.log('Legacy polar charts are deprecated!');
- return plotPolar(gd, data, layout);
+ return plotLegacyPolar(gd, data, layout);
}
// so we don't try to re-call Plotly.plot from inside
@@ -503,7 +503,7 @@ function setPlotContext(gd, config) {
context._hasZeroWidth = context._hasZeroWidth || gd.clientWidth === 0;
}
-function plotPolar(gd, data, layout) {
+function plotLegacyPolar(gd, data, layout) {
// build or reuse the container skeleton
var plotContainer = d3.select(gd).selectAll('.plot-container')
.data([0]);
@@ -544,7 +544,7 @@ function plotPolar(gd, data, layout) {
// editable title
var opacity = 1;
- var txt = gd._fullLayout.title;
+ var txt = gd._fullLayout.title ? gd._fullLayout.title.text : '';
if(txt === '' || !txt) opacity = 0;
var titleLayout = function() {
@@ -578,7 +578,7 @@ function plotPolar(gd, data, layout) {
var setContenteditable = function() {
this.call(svgTextUtils.makeEditable, {gd: gd})
.on('edit', function(text) {
- gd.framework({layout: {title: text}});
+ gd.framework({layout: {title: {text: text}}});
this.text(text)
.call(titleLayout);
this.call(setContenteditable);
@@ -1405,8 +1405,11 @@ function _restyle(gd, aobj, traces) {
var fullLayout = gd._fullLayout,
fullData = gd._fullData,
data = gd.data,
+ eventData = Lib.extendDeepAll({}, aobj),
i;
+ cleanDeprecatedAttributeKeys(aobj);
+
// initialize flags
var flags = editTypes.traceFlags();
@@ -1691,10 +1694,53 @@ function _restyle(gd, aobj, traces) {
undoit: undoit,
redoit: redoit,
traces: traces,
- eventData: Lib.extendDeepNoArrays([], [redoit, traces])
+ eventData: Lib.extendDeepNoArrays([], [eventData, traces])
};
}
+/**
+ * Converts deprecated attribute keys to
+ * the current API to ensure backwards compatibility.
+ *
+ * This is needed for the update mechanism to determine which
+ * subroutines to run based on the actual attribute
+ * definitions (that don't include the deprecated ones).
+ *
+ * E.g. Maps {'xaxis.title': 'A chart'} to {'xaxis.title.text': 'A chart'}
+ * and {titlefont: {...}} to {'title.font': {...}}.
+ *
+ * @param aobj
+ */
+function cleanDeprecatedAttributeKeys(aobj) {
+ var oldAxisTitleRegex = Lib.counterRegex('axis', '\.title', false, false);
+ var colorbarRegex = /colorbar\.title$/;
+ var keys = Object.keys(aobj);
+ var i, key, value;
+
+ for(i = 0; i < keys.length; i++) {
+ key = keys[i];
+ value = aobj[key];
+
+ if((key === 'title' || oldAxisTitleRegex.test(key) || colorbarRegex.test(key)) &&
+ (typeof value === 'string' || typeof value === 'number')) {
+ replace(key, key.replace('title', 'title.text'));
+ } else if(key.indexOf('titlefont') > -1) {
+ replace(key, key.replace('titlefont', 'title.font'));
+ } else if(key.indexOf('titleposition') > -1) {
+ replace(key, key.replace('titleposition', 'title.position'));
+ } else if(key.indexOf('titleside') > -1) {
+ replace(key, key.replace('titleside', 'title.side'));
+ } else if(key.indexOf('titleoffset') > -1) {
+ replace(key, key.replace('titleoffset', 'title.offset'));
+ }
+ }
+
+ function replace(oldAttrStr, newAttrStr) {
+ aobj[newAttrStr] = aobj[oldAttrStr];
+ delete aobj[oldAttrStr];
+ }
+}
+
/**
* relayout: update layout attributes of an existing plot
*
@@ -1825,13 +1871,17 @@ var AX_DOMAIN_RE = /^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/;
function _relayout(gd, aobj) {
var layout = gd.layout,
fullLayout = gd._fullLayout,
- keys = Object.keys(aobj),
axes = Axes.list(gd),
+ eventData = Lib.extendDeepAll({}, aobj),
arrayEdits = {},
+ keys,
arrayStr,
i,
j;
+ cleanDeprecatedAttributeKeys(aobj);
+ keys = Object.keys(aobj);
+
// look for 'allaxes', split out into all axes
// in case of 3D the axis are nested within a scene which is held in _id
for(i = 0; i < keys.length; i++) {
@@ -2158,7 +2208,7 @@ function _relayout(gd, aobj) {
rangesAltered: rangesAltered,
undoit: undoit,
redoit: redoit,
- eventData: Lib.extendDeep({}, redoit)
+ eventData: eventData
};
}
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 100b5c1aec9..68874a4eae7 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -27,6 +27,10 @@ var enforceAxisConstraints = axisConstraints.enforce;
var cleanAxisConstraints = axisConstraints.clean;
var doAutoRange = require('../plots/cartesian/autorange').doAutoRange;
+var SVG_TEXT_ANCHOR_START = 'start';
+var SVG_TEXT_ANCHOR_MIDDLE = 'middle';
+var SVG_TEXT_ANCHOR_END = 'end';
+
exports.layoutStyles = function(gd) {
return Lib.syncOrAsync([Plots.doAutoMargin, lsInner], gd);
};
@@ -450,18 +454,92 @@ function findCounterAxisLineWidth(ax, side, counterAx, axList) {
exports.drawMainTitle = function(gd) {
var fullLayout = gd._fullLayout;
+ var textAnchor = getMainTitleTextAnchor(fullLayout);
+ var dy = getMainTitleDy(fullLayout);
+
Titles.draw(gd, 'gtitle', {
propContainer: fullLayout,
- propName: 'title',
+ propName: 'title.text',
placeholder: fullLayout._dfltTitle.plot,
attributes: {
- x: fullLayout.width / 2,
- y: fullLayout._size.t / 2,
- 'text-anchor': 'middle'
+ x: getMainTitleX(fullLayout, textAnchor),
+ y: getMainTitleY(fullLayout, dy),
+ 'text-anchor': textAnchor,
+ dy: dy
}
});
};
+function getMainTitleX(fullLayout, textAnchor) {
+ var title = fullLayout.title;
+ var gs = fullLayout._size;
+ var hPadShift = 0;
+
+ if(textAnchor === SVG_TEXT_ANCHOR_START) {
+ hPadShift = title.pad.l;
+ } else if(textAnchor === SVG_TEXT_ANCHOR_END) {
+ hPadShift = -title.pad.r;
+ }
+
+ switch(title.xref) {
+ case 'paper':
+ return gs.l + gs.w * title.x + hPadShift;
+ case 'container':
+ default:
+ return fullLayout.width * title.x + hPadShift;
+ }
+}
+
+function getMainTitleY(fullLayout, dy) {
+ var title = fullLayout.title;
+ var gs = fullLayout._size;
+ var vPadShift = 0;
+
+ if(dy === '0em' || !dy) {
+ vPadShift = -title.pad.b;
+ } else if(dy === alignmentConstants.CAP_SHIFT + 'em') {
+ vPadShift = title.pad.t;
+ }
+
+ if(title.y === 'auto') {
+ return gs.t / 2;
+ } else {
+ switch(title.yref) {
+ case 'paper':
+ return gs.t + gs.h - gs.h * title.y + vPadShift;
+ case 'container':
+ default:
+ return fullLayout.height - fullLayout.height * title.y + vPadShift;
+ }
+ }
+}
+
+function getMainTitleTextAnchor(fullLayout) {
+ var title = fullLayout.title;
+
+ var textAnchor = SVG_TEXT_ANCHOR_MIDDLE;
+ if(Lib.isRightAnchor(title)) {
+ textAnchor = SVG_TEXT_ANCHOR_END;
+ } else if(Lib.isLeftAnchor(title)) {
+ textAnchor = SVG_TEXT_ANCHOR_START;
+ }
+
+ return textAnchor;
+}
+
+function getMainTitleDy(fullLayout) {
+ var title = fullLayout.title;
+
+ var dy = '0em';
+ if(Lib.isTopAnchor(title)) {
+ dy = alignmentConstants.CAP_SHIFT + 'em';
+ } else if(Lib.isMiddleAnchor(title)) {
+ dy = alignmentConstants.MID_SHIFT + 'em';
+ }
+
+ return dy;
+}
+
exports.doTraceStyle = function(gd) {
var calcdata = gd.calcdata;
var editStyleCalls = [];
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 3f1464aa9fb..a69b715caf4 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -1825,8 +1825,8 @@ axes.drawOne = function(gd, ax, opts) {
push[s] += ax._boundingBox.width;
}
- if(ax.title !== fullLayout._dfltTitle[axLetter]) {
- push[s] += ax.titlefont.size;
+ if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
+ push[s] += ax.title.font.size;
}
Plots.autoMargin(gd, pushKey, push);
@@ -2337,7 +2337,7 @@ axes.drawTitle = function(gd, ax) {
var axLetter = axId.charAt(0);
var offsetBase = 1.5;
var gs = fullLayout._size;
- var fontSize = ax.titlefont.size;
+ var fontSize = ax.title.font.size;
var transform, counterAxis, x, y;
@@ -2387,7 +2387,7 @@ axes.drawTitle = function(gd, ax) {
Titles.draw(gd, axId + 'title', {
propContainer: ax,
- propName: ax._name + '.title',
+ propName: ax._name + '.title.text',
placeholder: fullLayout._dfltTitle[axLetter],
avoid: avoid,
transform: transform,
@@ -2640,11 +2640,11 @@ function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) {
i;
if(key === 'title') {
// special handling of placeholder titles
- if(xVal === dfltTitle.x) {
- xVal = dfltTitle.y;
+ if(xVal && xVal.text === dfltTitle.x) {
+ xVal.text = dfltTitle.y;
}
- if(yVal === dfltTitle.y) {
- yVal = dfltTitle.x;
+ if(yVal && yVal.text === dfltTitle.y) {
+ yVal.text = dfltTitle.x;
}
}
diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js
index f3691bf8003..dacf3dff14d 100644
--- a/src/plots/cartesian/axis_defaults.js
+++ b/src/plots/cartesian/axis_defaults.js
@@ -68,8 +68,8 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
// try to get default title from splom trace, fallback to graph-wide value
var dfltTitle = splomStash.label || layoutOut._dfltTitle[letter];
- coerce('title', dfltTitle);
- Lib.coerceFont(coerce, 'titlefont', {
+ coerce('title.text', dfltTitle);
+ Lib.coerceFont(coerce, 'title.font', {
family: font.family,
size: Math.round(font.size * 1.2),
color: dfltFontColor
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index 404f023eb71..1f9b064ab7e 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -41,17 +41,27 @@ module.exports = {
].join(' ')
},
title: {
- valType: 'string',
- role: 'info',
- editType: 'ticks',
- description: 'Sets the title of this axis.'
+ text: {
+ valType: 'string',
+ role: 'info',
+ editType: 'ticks',
+ description: [
+ 'Sets the title of this axis.',
+ 'Note that before the existence of `title.text`, the title\'s',
+ 'contents used to be defined as the `title` attribute itself.',
+ 'This behavior has been deprecated.'
+ ].join(' ')
+ },
+ font: fontAttrs({
+ editType: 'ticks',
+ description: [
+ 'Sets this axis\' title font.',
+ 'Note that the title\'s font used to be customized',
+ 'by the now deprecated `titlefont` attribute.'
+ ].join(' ')
+ }),
+ editType: 'ticks'
},
- titlefont: fontAttrs({
- editType: 'ticks',
- description: [
- 'Sets this axis\' title font.'
- ].join(' ')
- }),
type: {
valType: 'enumerated',
// '-' means we haven't yet run autotype or couldn't find any data
@@ -778,6 +788,22 @@ module.exports = {
'Set `tickmode` to *auto* for old `autotick` *true* behavior.',
'Set `tickmode` to *linear* for `autotick` *false*.'
].join(' ')
- }
+ },
+ title: {
+ valType: 'string',
+ role: 'info',
+ editType: 'ticks',
+ description: [
+ 'Value of `title` is no longer a simple *string* but a set of sub-attributes.',
+ 'To set the axis\' title, please use `title.text` now.'
+ ].join(' ')
+ },
+ titlefont: fontAttrs({
+ editType: 'ticks',
+ description: [
+ 'Former `titlefont` is now the sub-attribute `font` of `title`.',
+ 'To customize title font properties, please use `title.font` now.'
+ ].join(' ')
+ })
}
};
diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js
index 43ab172a845..e8ba2054732 100644
--- a/src/plots/gl2d/convert.js
+++ b/src/plots/gl2d/convert.js
@@ -113,14 +113,14 @@ proto.merge = function(options) {
// '_name' is e.g. xaxis, xaxis2, yaxis, yaxis4 ...
ax = options[this.scene[axisName]._name];
- axTitle = ax.title === this.scene.fullLayout._dfltTitle[axisLetter] ? '' : ax.title;
+ axTitle = ax.title.text === this.scene.fullLayout._dfltTitle[axisLetter] ? '' : ax.title.text;
for(j = 0; j <= 2; j += 2) {
this.labelEnable[i + j] = false;
this.labels[i + j] = axTitle;
- this.labelColor[i + j] = str2RGBArray(ax.titlefont.color);
- this.labelFont[i + j] = ax.titlefont.family;
- this.labelSize[i + j] = ax.titlefont.size;
+ this.labelColor[i + j] = str2RGBArray(ax.title.font.color);
+ this.labelFont[i + j] = ax.title.font.family;
+ this.labelSize[i + j] = ax.title.font.size;
this.labelPad[i + j] = this.getLabelPad(axisName, ax);
this.tickEnable[i + j] = false;
@@ -208,7 +208,7 @@ proto.hasAxisInAltrPos = function(axisName, ax) {
proto.getLabelPad = function(axisName, ax) {
var offsetBase = 1.5,
- fontSize = ax.titlefont.size,
+ fontSize = ax.title.font.size,
showticklabels = ax.showticklabels;
if(axisName === 'xaxis') {
diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js
index 78309a1deb5..e2c2c9482bc 100644
--- a/src/plots/gl3d/layout/axis_attributes.js
+++ b/src/plots/gl3d/layout/axis_attributes.js
@@ -73,7 +73,6 @@ module.exports = overrideAll({
categoryorder: axesAttrs.categoryorder,
categoryarray: axesAttrs.categoryarray,
title: axesAttrs.title,
- titlefont: axesAttrs.titlefont,
type: axesAttrs.type,
autorange: axesAttrs.autorange,
rangemode: axesAttrs.rangemode,
@@ -113,5 +112,9 @@ module.exports = overrideAll({
gridwidth: axesAttrs.gridwidth,
zeroline: axesAttrs.zeroline,
zerolinecolor: axesAttrs.zerolinecolor,
- zerolinewidth: axesAttrs.zerolinewidth
+ zerolinewidth: axesAttrs.zerolinewidth,
+ _deprecated: {
+ title: axesAttrs._deprecated.title,
+ titlefont: axesAttrs._deprecated.titlefont
+ }
}, 'plot', 'from-root');
diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js
index e28b1a74de4..3c18a06ab69 100644
--- a/src/plots/gl3d/layout/axis_defaults.js
+++ b/src/plots/gl3d/layout/axis_defaults.js
@@ -56,7 +56,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) {
options.fullLayout);
coerce('gridcolor', colorMix(containerOut.color, options.bgColor, gridLightness).toRgbString());
- coerce('title', axName[0]); // shouldn't this be on-par with 2D?
+ coerce('title.text', axName[0]); // shouldn't this be on-par with 2D?
containerOut.setScale = Lib.noop;
diff --git a/src/plots/gl3d/layout/convert.js b/src/plots/gl3d/layout/convert.js
index 8aded799e13..d5b7b0fecab 100644
--- a/src/plots/gl3d/layout/convert.js
+++ b/src/plots/gl3d/layout/convert.js
@@ -83,11 +83,11 @@ proto.merge = function(sceneLayout) {
}
// Axes labels
- opts.labels[i] = axes.title;
- if('titlefont' in axes) {
- if(axes.titlefont.color) opts.labelColor[i] = str2RgbaArray(axes.titlefont.color);
- if(axes.titlefont.family) opts.labelFont[i] = axes.titlefont.family;
- if(axes.titlefont.size) opts.labelSize[i] = axes.titlefont.size;
+ opts.labels[i] = axes.title.text;
+ if('font' in axes.title) {
+ if(axes.title.font.color) opts.labelColor[i] = str2RgbaArray(axes.title.font.color);
+ if(axes.title.font.family) opts.labelFont[i] = axes.title.font.family;
+ if(axes.title.font.size) opts.labelSize[i] = axes.title.font.size;
}
// Lines
diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js
index d8428800dba..356ac5f8161 100644
--- a/src/plots/layout_attributes.js
+++ b/src/plots/layout_attributes.js
@@ -11,6 +11,8 @@
var fontAttrs = require('./font_attributes');
var colorAttrs = require('../components/color/attributes');
var colorscaleAttrs = require('../components/colorscale/layout_attributes');
+var padAttrs = require('./pad_attributes');
+var extendFlat = require('../lib/extend').extendFlat;
var globalFont = fontAttrs({
editType: 'calc',
@@ -27,17 +29,117 @@ globalFont.color.dflt = colorAttrs.defaultLine;
module.exports = {
font: globalFont,
title: {
- valType: 'string',
- role: 'info',
- editType: 'layoutstyle',
- description: [
- 'Sets the plot\'s title.'
- ].join(' ')
+ text: {
+ valType: 'string',
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the plot\'s title.',
+ 'Note that before the existence of `title.text`, the title\'s',
+ 'contents used to be defined as the `title` attribute itself.',
+ 'This behavior has been deprecated.'
+ ].join(' ')
+ },
+ font: fontAttrs({
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the title font.',
+ 'Note that the title\'s font used to be customized',
+ 'by the now deprecated `titlefont` attribute.'
+ ].join(' ')
+ }),
+ xref: {
+ valType: 'enumerated',
+ dflt: 'container',
+ values: ['container', 'paper'],
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the container `x` refers to.',
+ '*container* spans the entire `width` of the plot.',
+ '*paper* refers to the width of the plotting area only.'
+ ].join(' ')
+ },
+ yref: {
+ valType: 'enumerated',
+ dflt: 'container',
+ values: ['container', 'paper'],
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the container `y` refers to.',
+ '*container* spans the entire `height` of the plot.',
+ '*paper* refers to the height of the plotting area only.'
+ ].join(' ')
+ },
+ x: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ dflt: 0.5,
+ role: 'style',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the x position with respect to `xref` in normalized',
+ 'coordinates from *0* (left) to *1* (right).'
+ ].join(' ')
+ },
+ y: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ dflt: 'auto',
+ role: 'style',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the y position with respect to `yref` in normalized',
+ 'coordinates from *0* (bottom) to *1* (top).',
+ '*auto* places the baseline of the title onto the',
+ 'vertical center of the top margin.'
+ ].join(' ')
+ },
+ xanchor: {
+ valType: 'enumerated',
+ dflt: 'auto',
+ values: ['auto', 'left', 'center', 'right'],
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the title\'s horizontal alignment with respect to its x position.',
+ '*left* means that the title starts at x,',
+ '*right* means that the title ends at x',
+ 'and *center* means that the title\'s center is at x.',
+ '*auto* divides `xref` by three and calculates the `xanchor`',
+ 'value automatically based on the value of `x`.'
+ ].join(' ')
+ },
+ yanchor: {
+ valType: 'enumerated',
+ dflt: 'auto',
+ values: ['auto', 'top', 'middle', 'bottom'],
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Sets the title\'s vertical alignment with respect to its y position.',
+ '*top* means that the title\'s cap line is at y,',
+ '*bottom* means that the title\'s baseline is at y',
+ 'and *middle* means that the title\'s midline is at y.',
+ '*auto* divides `yref` by three and calculates the `yanchor`',
+ 'value automatically based on the value of `y`.'
+ ].join(' ')
+ },
+ pad: extendFlat(padAttrs({editType: 'layoutstyle'}), {
+ description: [
+ 'Sets the padding of the title.',
+ 'Each padding value only applies when the corresponding',
+ '`xanchor`/`yanchor` value is set accordingly. E.g. for left',
+ 'padding to take effect, `xanchor` must be set to *left*.',
+ 'The same rule applies if `xanchor`/`yanchor` is determined automatically.',
+ 'Padding is muted if the respective anchor value is *middle*/*center*.'
+ ].join(' ')
+ }),
+ editType: 'layoutstyle'
},
- titlefont: fontAttrs({
- editType: 'layoutstyle',
- description: 'Sets the title font.'
- }),
autosize: {
valType: 'boolean',
role: 'info',
@@ -255,5 +357,23 @@ module.exports = {
description: 'Sets the color of the active or hovered on icons in the modebar.'
},
editType: 'modebar'
+ },
+ _deprecated: {
+ title: {
+ valType: 'string',
+ role: 'info',
+ editType: 'layoutstyle',
+ description: [
+ 'Value of `title` is no longer a simple *string* but a set of sub-attributes.',
+ 'To set the contents of the title, please use `title.text` now.'
+ ].join(' ')
+ },
+ titlefont: fontAttrs({
+ editType: 'layoutstyle',
+ description: [
+ 'Former `titlefont` is now the sub-attribute `font` of `title`.',
+ 'To customize title font properties, please use `title.font` now.'
+ ].join(' ')
+ })
}
};
diff --git a/src/plots/pad_attributes.js b/src/plots/pad_attributes.js
index 539f2daee6e..47264860c50 100644
--- a/src/plots/pad_attributes.js
+++ b/src/plots/pad_attributes.js
@@ -8,37 +8,46 @@
'use strict';
-// This is used exclusively by components inside component arrays,
-// hence the 'arraydraw' editType. If this ever gets used elsewhere
-// we could generalize it as a function ala font_attributes
-module.exports = {
- t: {
- valType: 'number',
- dflt: 0,
- role: 'style',
- editType: 'arraydraw',
- description: 'The amount of padding (in px) along the top of the component.'
- },
- r: {
- valType: 'number',
- dflt: 0,
- role: 'style',
- editType: 'arraydraw',
- description: 'The amount of padding (in px) on the right side of the component.'
- },
- b: {
- valType: 'number',
- dflt: 0,
- role: 'style',
- editType: 'arraydraw',
- description: 'The amount of padding (in px) along the bottom of the component.'
- },
- l: {
- valType: 'number',
- dflt: 0,
- role: 'style',
- editType: 'arraydraw',
- description: 'The amount of padding (in px) on the left side of the component.'
- },
- editType: 'arraydraw'
+/**
+ * Creates a set of padding attributes.
+ *
+ * @param {object} opts
+ * @param {string} editType:
+ * the editType for all pieces of this padding definition
+ *
+ * @return {object} attributes object containing {t, r, b, l} as specified
+ */
+module.exports = function(opts) {
+ var editType = opts.editType;
+ return {
+ t: {
+ valType: 'number',
+ dflt: 0,
+ role: 'style',
+ editType: editType,
+ description: 'The amount of padding (in px) along the top of the component.'
+ },
+ r: {
+ valType: 'number',
+ dflt: 0,
+ role: 'style',
+ editType: editType,
+ description: 'The amount of padding (in px) on the right side of the component.'
+ },
+ b: {
+ valType: 'number',
+ dflt: 0,
+ role: 'style',
+ editType: editType,
+ description: 'The amount of padding (in px) along the bottom of the component.'
+ },
+ l: {
+ valType: 'number',
+ dflt: 0,
+ role: 'style',
+ editType: editType,
+ description: 'The amount of padding (in px) on the left side of the component.'
+ },
+ editType: editType
+ };
};
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 7d8947ff6b9..0fd72fd7341 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1324,14 +1324,25 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {
var globalFont = Lib.coerceFont(coerce, 'font');
- coerce('title', layoutOut._dfltTitle.plot);
+ coerce('title.text', layoutOut._dfltTitle.plot);
- Lib.coerceFont(coerce, 'titlefont', {
+ Lib.coerceFont(coerce, 'title.font', {
family: globalFont.family,
size: Math.round(globalFont.size * 1.4),
color: globalFont.color
});
+ coerce('title.xref');
+ coerce('title.yref');
+ coerce('title.x');
+ coerce('title.y');
+ coerce('title.xanchor');
+ coerce('title.yanchor');
+ coerce('title.pad.t');
+ coerce('title.pad.r');
+ coerce('title.pad.b');
+ coerce('title.pad.l');
+
// Make sure that autosize is defaulted to *true*
// on layouts with no set width and height for backward compatibly,
// in particular https://plot.ly/javascript/responsive-fluid-layout/
diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js
index 8334b0c3d96..ec630027526 100644
--- a/src/plots/polar/layout_attributes.js
+++ b/src/plots/polar/layout_attributes.js
@@ -112,13 +112,17 @@ var radialAxisAttrs = {
},
- title: extendFlat({}, axesAttrs.title, {editType: 'plot', dflt: ''}),
- titlefont: overrideAll(axesAttrs.titlefont, 'plot', 'from-root'),
+ title: overrideAll(axesAttrs.title, 'plot', 'from-root'),
// might need a 'titleside' and even 'titledirection' down the road
hoverformat: axesAttrs.hoverformat,
- editType: 'calc'
+ editType: 'calc',
+
+ _deprecated: {
+ title: axesAttrs._deprecated.title,
+ titlefont: axesAttrs._deprecated.titlefont
+ }
};
extendFlat(
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index 32bf5afff4b..69c031acef3 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -100,8 +100,8 @@ function handleDefaults(contIn, contOut, coerce, opts) {
coerceAxis('side');
coerceAxis('angle', sector[0]);
- coerceAxis('title');
- Lib.coerceFont(coerceAxis, 'titlefont', {
+ coerceAxis('title.text');
+ Lib.coerceFont(coerceAxis, 'title.font', {
family: opts.font.family,
size: Math.round(opts.font.size * 1.2),
color: dfltFontColor
diff --git a/src/plots/polar/legacy/micropolar.js b/src/plots/polar/legacy/micropolar.js
index eba2a693341..fc94cc39170 100644
--- a/src/plots/polar/legacy/micropolar.js
+++ b/src/plots/polar/legacy/micropolar.js
@@ -215,8 +215,8 @@ var µ = module.exports = { version: '0.2.2' };
centeringOffset[0] = Math.max(0, centeringOffset[0]);
centeringOffset[1] = Math.max(0, centeringOffset[1]);
svg.select('.outer-group').attr('transform', 'translate(' + centeringOffset + ')');
- if (axisConfig.title) {
- var title = svg.select('g.title-group text').style(fontStyle).text(axisConfig.title);
+ if (axisConfig.title && axisConfig.title.text) {
+ var title = svg.select('g.title-group text').style(fontStyle).text(axisConfig.title.text);
var titleBBox = title.node().getBBox();
title.attr({
x: chartCenter[0] - titleBBox.width / 2,
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index 2794ea1acab..9d4c8d50611 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -483,9 +483,13 @@ proto.updateRadialAxisTitle = function(fullLayout, polarLayout, _angle) {
var sina = Math.sin(angleRad);
var pad = 0;
+
+ // Hint: no need to check if there is in fact a title.text set
+ // because if plot is editable, pad needs to be calculated anyways
+ // to properly show placeholder text when title is empty.
if(radialLayout.title) {
var h = Drawing.bBox(_this.layers['radial-axis'].node()).height;
- var ts = radialLayout.titlefont.size;
+ var ts = radialLayout.title.font.size;
pad = radialLayout.side === 'counterclockwise' ?
-h - ts * 0.4 :
h + ts * 0.8;
diff --git a/src/plots/ternary/layout_attributes.js b/src/plots/ternary/layout_attributes.js
index 72397f0f876..86ae1f9d910 100644
--- a/src/plots/ternary/layout_attributes.js
+++ b/src/plots/ternary/layout_attributes.js
@@ -17,7 +17,6 @@ var extendFlat = require('../../lib/extend').extendFlat;
var ternaryAxesAttrs = {
title: axesAttrs.title,
- titlefont: axesAttrs.titlefont,
color: axesAttrs.color,
// ticks
tickmode: axesAttrs.tickmode,
@@ -63,6 +62,10 @@ var ternaryAxesAttrs = {
'values of the other two axes. The full view corresponds to',
'all the minima set to zero.'
].join(' ')
+ },
+ _deprecated: {
+ title: axesAttrs._deprecated.title,
+ titlefont: axesAttrs._deprecated.titlefont
}
};
diff --git a/src/plots/ternary/layout_defaults.js b/src/plots/ternary/layout_defaults.js
index 7952a4559a5..0ccbbd2dfd8 100644
--- a/src/plots/ternary/layout_defaults.js
+++ b/src/plots/ternary/layout_defaults.js
@@ -83,10 +83,10 @@ function handleAxisDefaults(containerIn, containerOut, options) {
letterUpper = axName.charAt(0).toUpperCase(),
dfltTitle = 'Component ' + letterUpper;
- var title = coerce('title', dfltTitle);
+ var title = coerce('title.text', dfltTitle);
containerOut._hovertitle = title === dfltTitle ? title : letterUpper;
- Lib.coerceFont(coerce, 'titlefont', {
+ Lib.coerceFont(coerce, 'title.font', {
family: options.font.family,
size: Math.round(options.font.size * 1.2),
color: dfltFontColor
diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js
index 672015c4ef7..f13ebfa9199 100644
--- a/src/plots/ternary/ternary.js
+++ b/src/plots/ternary/ternary.js
@@ -377,7 +377,7 @@ proto.drawAxes = function(doTitles) {
placeholder: _(gd, 'Click to enter Component A title'),
attributes: {
x: _this.x0 + _this.w / 2,
- y: _this.y0 - aaxis.titlefont.size / 3 - apad,
+ y: _this.y0 - aaxis.title.font.size / 3 - apad,
'text-anchor': 'middle'
}
});
@@ -387,7 +387,7 @@ proto.drawAxes = function(doTitles) {
placeholder: _(gd, 'Click to enter Component B title'),
attributes: {
x: _this.x0 - bpad,
- y: _this.y0 + _this.h + baxis.titlefont.size * 0.83 + bpad,
+ y: _this.y0 + _this.h + baxis.title.font.size * 0.83 + bpad,
'text-anchor': 'middle'
}
});
@@ -397,7 +397,7 @@ proto.drawAxes = function(doTitles) {
placeholder: _(gd, 'Click to enter Component C title'),
attributes: {
x: _this.x0 + _this.w + bpad,
- y: _this.y0 + _this.h + caxis.titlefont.size * 0.83 + bpad,
+ y: _this.y0 + _this.h + caxis.title.font.size * 0.83 + bpad,
'text-anchor': 'middle'
}
});
diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js
index 5a9a49b359a..ebda88a12ad 100644
--- a/src/snapshot/cloneplot.js
+++ b/src/snapshot/cloneplot.js
@@ -24,7 +24,7 @@ function cloneLayoutOverride(tileClass) {
autosize: true,
width: 150,
height: 150,
- title: '',
+ title: {text: ''},
showlegend: false,
margin: {l: 5, r: 5, t: 5, b: 5, pad: 0},
annotations: []
@@ -33,7 +33,7 @@ function cloneLayoutOverride(tileClass) {
case 'thumbnail':
override = {
- title: '',
+ title: {text: ''},
hidesources: true,
showlegend: false,
borderwidth: 0,
@@ -81,7 +81,7 @@ module.exports = function clonePlot(graphObj, options) {
for(i = 0; i < keys.length; i++) {
if(keyIsAxis(keys[i])) {
- newLayout[keys[i]].title = '';
+ newLayout[keys[i]].title = {text: ''};
}
}
@@ -109,7 +109,7 @@ module.exports = function clonePlot(graphObj, options) {
var axesImageOverride = {};
if(options.tileClass === 'thumbnail') {
axesImageOverride = {
- title: '',
+ title: {text: ''},
showaxeslabels: false,
showticklabels: false,
linetickenable: false
diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js
index 42e6dcc7f13..1bb49be9f42 100644
--- a/src/traces/carpet/axis_attributes.js
+++ b/src/traces/carpet/axis_attributes.js
@@ -34,26 +34,39 @@ module.exports = {
editType: 'calc'
},
title: {
- valType: 'string',
- role: 'info',
+ text: {
+ valType: 'string',
+ dflt: '',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the title of this axis.',
+ 'Note that before the existence of `title.text`, the title\'s',
+ 'contents used to be defined as the `title` attribute itself.',
+ 'This behavior has been deprecated.'
+ ].join(' ')
+ },
+ font: fontAttrs({
+ editType: 'calc',
+ description: [
+ 'Sets this axis\' title font.',
+ 'Note that the title\'s font used to be set',
+ 'by the now deprecated `titlefont` attribute.'
+ ].join(' ')
+ }),
+ offset: {
+ valType: 'number',
+ role: 'info',
+ dflt: 10,
+ editType: 'calc',
+ description: [
+ 'An additional amount by which to offset the title from the tick',
+ 'labels, given in pixels.',
+ 'Note that this used to be set',
+ 'by the now deprecated `titleoffset` attribute.'
+ ].join(' '),
+ },
editType: 'calc',
- description: 'Sets the title of this axis.'
- },
- titlefont: fontAttrs({
- editType: 'calc',
- description: [
- 'Sets this axis\' title font.'
- ].join(' ')
- }),
- titleoffset: {
- valType: 'number',
- role: 'info',
- dflt: 10,
- editType: 'calc',
- description: [
- 'An additional amount by which to offset the title from the tick',
- 'labels, given in pixels'
- ].join(' '),
},
type: {
valType: 'enumerated',
@@ -494,5 +507,30 @@ module.exports = {
editType: 'calc',
description: 'The stride between grid lines along the axis'
},
+
+ _deprecated: {
+ title: {
+ valType: 'string',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Deprecated in favor of `title.text`.',
+ 'Note that value of `title` is no longer a simple',
+ '*string* but a set of sub-attributes.'
+ ].join(' ')
+ },
+ titlefont: fontAttrs({
+ editType: 'calc',
+ description: 'Deprecated in favor of `title.font`.'
+ }),
+ titleoffset: {
+ valType: 'number',
+ role: 'info',
+ dflt: 10,
+ editType: 'calc',
+ description: 'Deprecated in favor of `title.offset`.'
+ }
+ },
+
editType: 'calc'
};
diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js
index 511428dc1ba..59cd9c19948 100644
--- a/src/traces/carpet/axis_defaults.js
+++ b/src/traces/carpet/axis_defaults.js
@@ -113,14 +113,15 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options)
// inherit from global font color in case that was provided.
var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color;
- coerce('title');
- Lib.coerceFont(coerce, 'titlefont', {
- family: font.family,
- size: Math.round(font.size * 1.2),
- color: dfltFontColor
- });
-
- coerce('titleoffset');
+ var title = coerce('title.text');
+ if(title) {
+ Lib.coerceFont(coerce, 'title.font', {
+ family: font.family,
+ size: Math.round(font.size * 1.2),
+ color: dfltFontColor
+ });
+ coerce('title.offset');
+ }
coerce('tickangle');
@@ -203,11 +204,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options)
// but no, we *actually* want to coerce this.
coerce('tickmode');
- if(!containerOut.title || (containerOut.title && containerOut.title.length === 0)) {
- delete containerOut.titlefont;
- delete containerOut.titleoffset;
- }
-
return containerOut;
};
diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js
index c7ad270a55c..2d978d31e0f 100644
--- a/src/traces/carpet/plot.js
+++ b/src/traces/carpet/plot.js
@@ -197,7 +197,7 @@ var midShift = ((1 - alignmentConstants.MID_SHIFT) / lineSpacing) + 1;
function drawAxisTitle(gd, layer, trace, t, xy, dxy, axis, xa, ya, labelOrientation, labelClass) {
var data = [];
- if(axis.title) data.push(axis.title);
+ if(axis.title.text) data.push(axis.title.text);
var titleJoin = layer.selectAll('text.' + labelClass).data(data);
var offset = labelOrientation.maxExtent;
@@ -213,8 +213,8 @@ function drawAxisTitle(gd, layer, trace, t, xy, dxy, axis, xa, ya, labelOrientat
}
// In addition to the size of the labels, add on some extra padding:
- var titleSize = axis.titlefont.size;
- offset += titleSize + axis.titleoffset;
+ var titleSize = axis.title.font.size;
+ offset += titleSize + axis.title.offset;
var labelNorm = labelOrientation.angle + (labelOrientation.flip < 0 ? 180 : 0);
var angleDiff = (labelNorm - orientation.angle + 450) % 360;
@@ -222,7 +222,7 @@ function drawAxisTitle(gd, layer, trace, t, xy, dxy, axis, xa, ya, labelOrientat
var el = d3.select(this);
- el.text(axis.title || '')
+ el.text(axis.title.text)
.call(svgTextUtils.convertToTspans, gd);
if(reverseTitle) {
@@ -236,7 +236,7 @@ function drawAxisTitle(gd, layer, trace, t, xy, dxy, axis, xa, ya, labelOrientat
)
.classed('user-select-none', true)
.attr('text-anchor', 'middle')
- .call(Drawing.font, axis.titlefont);
+ .call(Drawing.font, axis.title.font);
});
titleJoin.exit().remove();
diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js
index 1b42870a83d..a01385a23bd 100644
--- a/src/traces/pie/attributes.js
+++ b/src/traces/pie/attributes.js
@@ -185,31 +185,44 @@ module.exports = {
}),
title: {
- valType: 'string',
- dflt: '',
- role: 'info',
- editType: 'calc',
- description: [
- 'Sets the title of the pie chart.',
- 'If it is empty, no title is displayed.'
- ].join(' ')
- },
- titleposition: {
- valType: 'enumerated',
- values: [
- 'top left', 'top center', 'top right',
- 'middle center',
- 'bottom left', 'bottom center', 'bottom right'
- ],
- role: 'info',
- editType: 'calc',
- description: [
- 'Specifies the location of the `title`.',
- ].join(' ')
+ text: {
+ valType: 'string',
+ dflt: '',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the title of the pie chart.',
+ 'If it is empty, no title is displayed.',
+ 'Note that before the existence of `title.text`, the title\'s',
+ 'contents used to be defined as the `title` attribute itself.',
+ 'This behavior has been deprecated.'
+ ].join(' ')
+ },
+ font: extendFlat({}, textFontAttrs, {
+ description: [
+ 'Sets the font used for `title`.',
+ 'Note that the title\'s font used to be set',
+ 'by the now deprecated `titlefont` attribute.'
+ ].join(' ')
+ }),
+ position: {
+ valType: 'enumerated',
+ values: [
+ 'top left', 'top center', 'top right',
+ 'middle center',
+ 'bottom left', 'bottom center', 'bottom right'
+ ],
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Specifies the location of the `title`.',
+ 'Note that the title\'s position used to be set',
+ 'by the now deprecated `titleposition` attribute.'
+ ].join(' ')
+ },
+
+ editType: 'calc'
},
- titlefont: extendFlat({}, textFontAttrs, {
- description: 'Sets the font used for `title`.'
- }),
// position and shape
domain: domainAttrs({name: 'pie', trace: true, editType: 'calc'}),
@@ -283,5 +296,33 @@ module.exports = {
'to pull all slices apart from each other equally',
'or an array to highlight one or more slices.'
].join(' ')
+ },
+
+ _deprecated: {
+ title: {
+ valType: 'string',
+ dflt: '',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Deprecated in favor of `title.text`.',
+ 'Note that value of `title` is no longer a simple',
+ '*string* but a set of sub-attributes.'
+ ].join(' ')
+ },
+ titlefont: extendFlat({}, textFontAttrs, {
+ description: 'Deprecated in favor of `title.font`.'
+ }),
+ titleposition: {
+ valType: 'enumerated',
+ values: [
+ 'top left', 'top center', 'top right',
+ 'middle center',
+ 'bottom left', 'bottom center', 'bottom right'
+ ],
+ role: 'info',
+ editType: 'calc',
+ description: 'Deprecated in favor of `title.position`.'
+ }
}
};
diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js
index 4b3c156140a..2110884688c 100644
--- a/src/traces/pie/defaults.js
+++ b/src/traces/pie/defaults.js
@@ -77,11 +77,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleDomainDefaults(traceOut, layout, coerce);
var hole = coerce('hole');
- var title = coerce('title');
+ var title = coerce('title.text');
if(title) {
- var titlePosition = coerce('titleposition', hole ? 'middle center' : 'top center');
- if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
- coerceFont(coerce, 'titlefont', layout.font);
+ var titlePosition = coerce('title.position', hole ? 'middle center' : 'top center');
+ if(!hole && titlePosition === 'middle center') traceOut.title.position = 'top center';
+ coerceFont(coerce, 'title.font', layout.font);
}
coerce('sort');
diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js
index eb41487af80..5eff4d6fc07 100644
--- a/src/traces/pie/plot.js
+++ b/src/traces/pie/plot.js
@@ -322,7 +322,7 @@ module.exports = function plot(gd, cdpie) {
// add the title
var titleTextGroup = d3.select(this).selectAll('g.titletext')
- .data(trace.title ? [0] : []);
+ .data(trace.title.text ? [0] : []);
titleTextGroup.enter().append('g')
.classed('titletext', true);
@@ -334,18 +334,18 @@ module.exports = function plot(gd, cdpie) {
s.attr('data-notex', 1);
});
- titleText.text(trace.title)
+ titleText.text(trace.title.text)
.attr({
'class': 'titletext',
transform: '',
'text-anchor': 'middle',
})
- .call(Drawing.font, trace.titlefont)
+ .call(Drawing.font, trace.title.font)
.call(svgTextUtils.convertToTspans, gd);
var transform;
- if(trace.titleposition === 'middle center') {
+ if(trace.title.position === 'middle center') {
transform = positionTitleInside(cd0);
} else {
transform = positionTitleOutside(cd0, fullLayout._size);
@@ -473,11 +473,11 @@ function prerenderTitles(cdpie, gd) {
cd0 = cdpie[i][0];
trace = cd0.trace;
- if(trace.title) {
+ if(trace.title.text) {
var dummyTitle = Drawing.tester.append('text')
.attr('data-notex', 1)
- .text(trace.title)
- .call(Drawing.font, trace.titlefont)
+ .text(trace.title.text)
+ .call(Drawing.font, trace.title.font)
.call(svgTextUtils.convertToTspans, gd);
var bBox = Drawing.bBox(dummyTitle.node(), true);
cd0.titleBox = {
@@ -579,7 +579,7 @@ function positionTitleInside(cd0) {
y: cd0.cy,
scale: cd0.trace.hole * cd0.r * 2 / textDiameter,
tx: 0,
- ty: - cd0.titleBox.height / 2 + cd0.trace.titlefont.size
+ ty: - cd0.titleBox.height / 2 + cd0.trace.title.font.size
};
}
@@ -602,25 +602,25 @@ function positionTitleOutside(cd0, plotSize) {
// we reason below as if the baseline is the top middle point of the text box.
// so we must add the font size to approximate the y-coord. of the top.
// note that this correction must happen after scaling.
- translate.ty += trace.titlefont.size;
+ translate.ty += trace.title.font.size;
maxPull = getMaxPull(trace);
- if(trace.titleposition.indexOf('top') !== -1) {
+ if(trace.title.position.indexOf('top') !== -1) {
topMiddle.y -= (1 + maxPull) * cd0.r;
translate.ty -= cd0.titleBox.height;
}
- else if(trace.titleposition.indexOf('bottom') !== -1) {
+ else if(trace.title.position.indexOf('bottom') !== -1) {
topMiddle.y += (1 + maxPull) * cd0.r;
}
- if(trace.titleposition.indexOf('left') !== -1) {
+ if(trace.title.position.indexOf('left') !== -1) {
// we start the text at the left edge of the pie
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
topMiddle.x -= (1 + maxPull) * cd0.r;
translate.tx += cd0.titleBox.width / 2;
- } else if(trace.titleposition.indexOf('center') !== -1) {
+ } else if(trace.title.position.indexOf('center') !== -1) {
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
- } else if(trace.titleposition.indexOf('right') !== -1) {
+ } else if(trace.title.position.indexOf('right') !== -1) {
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
topMiddle.x += (1 + maxPull) * cd0.r;
translate.tx -= cd0.titleBox.width / 2;
@@ -774,7 +774,7 @@ function scalePies(cdpie, plotSize) {
pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
// leave some space for the title, if it will be displayed outside
- if(trace.title && trace.titleposition !== 'middle center') {
+ if(trace.title.text && trace.title.position !== 'middle center') {
pieBoxHeight -= getTitleSpace(cd0, plotSize);
}
@@ -784,7 +784,7 @@ function scalePies(cdpie, plotSize) {
cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2;
cd0.cy = plotSize.t + plotSize.h * (1 - trace.domain.y[0]) - pieBoxHeight / 2;
- if(trace.title && trace.titleposition.indexOf('bottom') !== -1) {
+ if(trace.title.text && trace.title.position.indexOf('bottom') !== -1) {
cd0.cy -= getTitleSpace(cd0, plotSize);
}
diff --git a/test/image/mocks/shapes.json b/test/image/mocks/shapes.json
index 2aadf3f0020..f98fcc6b843 100644
--- a/test/image/mocks/shapes.json
+++ b/test/image/mocks/shapes.json
@@ -12,10 +12,10 @@
"yaxis":"y2"
}],
"layout": {
- "xaxis":{"title":"linear","range":[0,1],"domain":[0.35,0.65],"type":"linear","showgrid":false,"zeroline":false,"showticklabels":false},
- "yaxis":{"title":"log","range":[0,1],"domain":[0.6,1],"type":"log","showgrid":false,"zeroline":false,"showticklabels":false},
- "xaxis2":{"title":"date","range":["2000-01-01","2000-01-02"],"domain":[0.7,1],"anchor":"y2","type":"date","showgrid":false,"zeroline":false,"showticklabels":false},
- "yaxis2":{"title":"category","range":[0,1],"domain":[0.6,1],"anchor":"x2","type":"category","showgrid":false,"zeroline":false,"showticklabels":false},
+ "xaxis":{"title":{"text":"linear"},"range":[0,1],"domain":[0.35,0.65],"type":"linear","showgrid":false,"zeroline":false,"showticklabels":false},
+ "yaxis":{"title":{"text":"log"},"range":[0,1],"domain":[0.6,1],"type":"log","showgrid":false,"zeroline":false,"showticklabels":false},
+ "xaxis2":{"title":{"text":"date"},"range":["2000-01-01","2000-01-02"],"domain":[0.7,1],"anchor":"y2","type":"date","showgrid":false,"zeroline":false,"showticklabels":false},
+ "yaxis2":{"title":{"text":"category"},"range":[0,1],"domain":[0.6,1],"anchor":"x2","type":"category","showgrid":false,"zeroline":false,"showticklabels":false},
"height":400,
"width":800,
"margin": {"l":20,"r":20,"pad":0},
diff --git a/test/jasmine/bundle_tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js
index 825c537ecc3..2e85825bb65 100644
--- a/test/jasmine/bundle_tests/plotschema_test.js
+++ b/test/jasmine/bundle_tests/plotschema_test.js
@@ -228,7 +228,7 @@ describe('plot schema', function() {
assertPlotSchema(
function(attr, attrName, attrs, level, attrString) {
- if(attr && isPlainObject(attr[DEPRECATED])) {
+ if(attr && isPlainObject(attr[DEPRECATED]) && isValObject(attr[DEPRECATED])) {
Object.keys(attr[DEPRECATED]).forEach(function(dAttrName) {
var dAttr = attr[DEPRECATED][dAttrName];
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index 595b4012fb3..5835d6ad832 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -29,7 +29,9 @@ describe('Test axes', function() {
data: [{x: [1, 2, 3], y: [1, 2, 3]}],
layout: {
xaxis: {
- title: 'A Title!!!',
+ title: {
+ text: 'A Title!!!'
+ },
type: 'log',
autorange: 'reversed',
rangemode: 'tozero',
@@ -42,14 +44,18 @@ describe('Test axes', function() {
tickcolor: '#f00'
},
yaxis: {
- title: 'Click to enter Y axis title',
+ title: {
+ text: 'Click to enter Y axis title'
+ },
type: 'date'
}
}
};
var expectedYaxis = Lib.extendDeep({}, gd.layout.xaxis),
expectedXaxis = {
- title: 'Click to enter X axis title',
+ title: {
+ text: 'Click to enter X axis title'
+ },
type: 'date'
};
@@ -345,11 +351,11 @@ describe('Test axes', function() {
expect(layoutOut.xaxis.zerolinecolor).toBe(undefined);
});
- it('should use \'axis.color\' as default for \'axis.titlefont.color\'', function() {
+ it('should use \'axis.color\' as default for \'axis.title.font.color\'', function() {
layoutIn = {
xaxis: { color: 'red' },
yaxis: {},
- yaxis2: { titlefont: { color: 'yellow' } }
+ yaxis2: { title: { font: { color: 'yellow' } } }
};
layoutOut.font = { color: 'blue' };
@@ -357,9 +363,9 @@ describe('Test axes', function() {
layoutOut._subplots.yaxis.push('y2');
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
- expect(layoutOut.xaxis.titlefont.color).toEqual('red');
- expect(layoutOut.yaxis.titlefont.color).toEqual('blue');
- expect(layoutOut.yaxis2.titlefont.color).toEqual('yellow');
+ expect(layoutOut.xaxis.title.font.color).toEqual('red');
+ expect(layoutOut.yaxis.title.font.color).toEqual('blue');
+ expect(layoutOut.yaxis2.title.font.color).toEqual('yellow');
});
it('should use \'axis.color\' as default for \'axis.linecolor\'', function() {
@@ -2882,14 +2888,14 @@ describe('Test axes', function() {
expect(size.t).toBe(previousSize.t);
previousSize = Lib.extendDeep({}, size);
- return Plotly.relayout(gd, {'yaxis.titlefont.size': 30});
+ return Plotly.relayout(gd, {'yaxis.title.font.size': 30});
})
.then(function() {
var size = gd._fullLayout._size;
expect(size).toEqual(previousSize);
previousSize = Lib.extendDeep({}, size);
- return Plotly.relayout(gd, {'yaxis.title': 'hello'});
+ return Plotly.relayout(gd, {'yaxis.title.text': 'hello'});
})
.then(function() {
var size = gd._fullLayout._size;
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index dbb12c7754b..b8b3bc16dad 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -1655,7 +1655,8 @@ describe('hover info', function() {
it('should contain the axis names', function(done) {
var gd = document.getElementById('graph');
- Plotly.restyle(gd, 'hovertemplate', '%{yaxis.title}:%{y:$.2f}
%{xaxis.title}:%{x:0.4f}')
+ Plotly.restyle(gd, 'hovertemplate',
+ '%{yaxis.title.text}:%{y:$.2f}
%{xaxis.title.text}:%{x:0.4f}')
.then(function() {
Fx.hover('graph', evt, 'xy');
diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js
index 62aacfa360c..3f474fcc026 100644
--- a/test/jasmine/tests/legend_test.js
+++ b/test/jasmine/tests/legend_test.js
@@ -6,7 +6,6 @@ var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY;
var Legend = require('@src/components/legend');
var getLegendData = require('@src/components/legend/get_legend_data');
var helpers = require('@src/components/legend/helpers');
-var anchorUtils = require('@src/components/legend/anchor_utils');
var d3 = require('d3');
var failTest = require('../assets/fail_test');
@@ -503,7 +502,7 @@ describe('legend anchor utils:', function() {
'use strict';
describe('isRightAnchor', function() {
- var isRightAnchor = anchorUtils.isRightAnchor;
+ var isRightAnchor = Lib.isRightAnchor;
var threshold = 2 / 3;
it('should return true when \'xanchor\' is set to \'right\'', function() {
@@ -524,7 +523,7 @@ describe('legend anchor utils:', function() {
});
describe('isCenterAnchor', function() {
- var isCenterAnchor = anchorUtils.isCenterAnchor;
+ var isCenterAnchor = Lib.isCenterAnchor;
var threshold0 = 1 / 3;
var threshold1 = 2 / 3;
@@ -546,7 +545,7 @@ describe('legend anchor utils:', function() {
});
describe('isBottomAnchor', function() {
- var isBottomAnchor = anchorUtils.isBottomAnchor;
+ var isBottomAnchor = Lib.isBottomAnchor;
var threshold = 1 / 3;
it('should return true when \'yanchor\' is set to \'right\'', function() {
@@ -567,7 +566,7 @@ describe('legend anchor utils:', function() {
});
describe('isMiddleAnchor', function() {
- var isMiddleAnchor = anchorUtils.isMiddleAnchor;
+ var isMiddleAnchor = Lib.isMiddleAnchor;
var threshold0 = 1 / 3;
var threshold1 = 2 / 3;
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index ad06311c263..65c28b9fe1d 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -2658,12 +2658,12 @@ describe('Queue', function() {
expect(gd.undoQueue.queue[0].undo.args[0][1]['marker.color']).toEqual([null]);
expect(gd.undoQueue.queue[0].redo.args[0][1]['marker.color']).toEqual('red');
- return Plotly.relayout(gd, 'title', 'A title');
+ return Plotly.relayout(gd, 'title.text', 'A title');
})
.then(function() {
expect(gd.undoQueue.index).toEqual(2);
- expect(gd.undoQueue.queue[1].undo.args[0][1].title).toEqual(null);
- expect(gd.undoQueue.queue[1].redo.args[0][1].title).toEqual('A title');
+ expect(gd.undoQueue.queue[1].undo.args[0][1]['title.text']).toEqual(null);
+ expect(gd.undoQueue.queue[1].redo.args[0][1]['title.text']).toEqual('A title');
return Plotly.restyle(gd, 'mode', 'markers');
})
@@ -2674,8 +2674,8 @@ describe('Queue', function() {
expect(gd.undoQueue.queue[1].undo.args[0][1].mode).toEqual([null]);
expect(gd.undoQueue.queue[1].redo.args[0][1].mode).toEqual('markers');
- expect(gd.undoQueue.queue[0].undo.args[0][1].title).toEqual(null);
- expect(gd.undoQueue.queue[0].redo.args[0][1].title).toEqual('A title');
+ expect(gd.undoQueue.queue[0].undo.args[0][1]['title.text']).toEqual(null);
+ expect(gd.undoQueue.queue[0].redo.args[0][1]['title.text']).toEqual('A title');
return Plotly.restyle(gd, 'transforms[0]', { type: 'filter' });
})
diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js
index 0a9cfd04866..e131248ce48 100644
--- a/test/jasmine/tests/pie_test.js
+++ b/test/jasmine/tests/pie_test.js
@@ -692,29 +692,100 @@ describe('Pie traces', function() {
});
});
- it('should be able to restyle title color', function(done) {
- function _assert(msg, exp) {
- var title = d3.select('.titletext > text').node();
- expect(title.style.fill).toBe(exp.color, msg);
- }
+ function _assertTitle(msg, expText, expColor) {
+ var title = d3.select('.titletext > text');
+ expect(title.text()).toBe(expText, msg + ' text');
+ expect(title.node().style.fill).toBe(expColor, msg + ' color');
+ }
+ it('show a user-defined title with a custom position and font', function(done) {
+ Plotly.plot(gd, [{
+ type: 'pie',
+ values: [1, 2, 3],
+ title: {
+ text: 'yo',
+ font: {color: 'blue'},
+ position: 'top left'
+ }
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('still support the deprecated `title` structure (backwards-compatibility)', function(done) {
Plotly.plot(gd, [{
type: 'pie',
values: [1, 2, 3],
title: 'yo',
- titlefont: {color: 'blue'}
+ titlefont: {color: 'blue'},
+ titleposition: 'top left'
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should be able to restyle title', function(done) {
+ Plotly.plot(gd, [{
+ type: 'pie',
+ values: [1, 2, 3],
+ title: {
+ text: 'yo',
+ font: {color: 'blue'},
+ position: 'top left'
+ }
}])
.then(function() {
- _assert('base', {color: 'rgb(0, 0, 255)'});
- return Plotly.restyle(gd, 'titlefont.color', 'red');
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+
+ return Plotly.restyle(gd, {
+ 'title.text': 'oy',
+ 'title.font.color': 'red',
+ 'title.position': 'bottom right'
+ });
})
.then(function() {
- _assert('base', {color: 'rgb(255, 0, 0)'});
+ _assertTitle('base', 'oy', 'rgb(255, 0, 0)');
+ _verifyTitle(false, true, false, true, false);
})
.catch(failTest)
.then(done);
});
+ it('should be able to restyle title despite using the deprecated attributes', function(done) {
+ Plotly.plot(gd, [{
+ type: 'pie',
+ values: [1, 2, 3],
+ title: 'yo',
+ titlefont: {color: 'blue'},
+ titleposition: 'top left'
+ }])
+ .then(function() {
+ _assertTitle('base', 'yo', 'rgb(0, 0, 255)');
+ _verifyTitle(true, false, true, false, false);
+
+ return Plotly.restyle(gd, {
+ 'title': 'oy',
+ 'titlefont.color': 'red',
+ 'titleposition': 'bottom right'
+ });
+ })
+ .then(function() {
+ _assertTitle('base', 'oy', 'rgb(255, 0, 0)');
+ _verifyTitle(false, true, false, true, false);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('should be able to react with new text colors', function(done) {
Plotly.plot(gd, [{
type: 'pie',
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 4898c620c41..4d8141cf2e6 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -547,6 +547,30 @@ describe('Test plot api', function() {
.catch(failTest)
.then(done);
});
+
+ it('passes update data back to plotly_relayout unmodified ' +
+ 'even if deprecated attributes have been used', function(done) {
+ Plotly.newPlot(gd, [{y: [1, 3, 2]}]);
+
+ gd.on('plotly_relayout', function(eventData) {
+ expect(eventData).toEqual({
+ 'title': 'Plotly chart',
+ 'xaxis.title': 'X',
+ 'xaxis.titlefont': {color: 'green'},
+ 'yaxis.title': 'Y',
+ 'polar.radialaxis.title': 'Radial'
+ });
+ done();
+ });
+
+ Plotly.relayout(gd, {
+ 'title': 'Plotly chart',
+ 'xaxis.title': 'X',
+ 'xaxis.titlefont': {color: 'green'},
+ 'yaxis.title': 'Y',
+ 'polar.radialaxis.title': 'Radial'
+ });
+ });
});
describe('Plotly.relayout subroutines switchboard', function() {
diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js
index be0325be62b..2d48c4afd1f 100644
--- a/test/jasmine/tests/polar_test.js
+++ b/test/jasmine/tests/polar_test.js
@@ -134,7 +134,7 @@ describe('Test polar plots defaults:', function() {
expect(layoutOut.polar.angularaxis.linecolor).toBe('red');
expect(layoutOut.polar.angularaxis.gridcolor).toBe('rgb(255, 153, 153)', 'blend by 60% with bgcolor');
- expect(layoutOut.polar.radialaxis.titlefont.color).toBe('blue');
+ expect(layoutOut.polar.radialaxis.title.font.color).toBe('blue');
expect(layoutOut.polar.radialaxis.linecolor).toBe('blue');
expect(layoutOut.polar.radialaxis.gridcolor).toBe('rgb(153, 153, 255)', 'blend by 60% with bgcolor');
});
diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js
index 90c875e535d..d7da2aa9d52 100644
--- a/test/jasmine/tests/snapshot_test.js
+++ b/test/jasmine/tests/snapshot_test.js
@@ -36,19 +36,19 @@ describe('Plotly.Snapshot', function() {
data = [dummyTrace1, dummyTrace2];
layout = {
- title: 'Chart Title',
+ title: {text: 'Chart Title'},
showlegend: true,
autosize: true,
width: 688,
height: 460,
xaxis: {
- title: 'xaxis title',
+ title: {text: 'xaxis title'},
range: [-0.323374917925, 5.32337491793],
type: 'linear',
autorange: true
},
yaxis: {
- title: 'yaxis title',
+ title: {text: 'yaxis title'},
range: [1.41922290389, 10.5807770961],
type: 'linear',
autorange: true
@@ -70,7 +70,7 @@ describe('Plotly.Snapshot', function() {
autosize: true,
width: 150,
height: 150,
- title: '',
+ title: {text: ''},
showlegend: false,
margin: {'l': 5, 'r': 5, 't': 5, 'b': 5, 'pad': 0},
annotations: []
@@ -100,7 +100,7 @@ describe('Plotly.Snapshot', function() {
};
var THUMBNAIL_DEFAULT_LAYOUT = {
- 'title': '',
+ 'title': {text: ''},
'hidesources': true,
'showlegend': false,
'hovermode': false,
@@ -117,6 +117,7 @@ describe('Plotly.Snapshot', function() {
expect(thumbTile.layout.showlegend).toEqual(THUMBNAIL_DEFAULT_LAYOUT.showlegend);
expect(thumbTile.layout.borderwidth).toEqual(THUMBNAIL_DEFAULT_LAYOUT.borderwidth);
expect(thumbTile.layout.annotations).toEqual(THUMBNAIL_DEFAULT_LAYOUT.annotations);
+ expect(thumbTile.layout.title).toEqual(THUMBNAIL_DEFAULT_LAYOUT.title);
});
it('should create a 3D thumbnail with limited attributes', function() {
@@ -142,7 +143,7 @@ describe('Plotly.Snapshot', function() {
};
var AXIS_OVERRIDE = {
- title: '',
+ title: {text: ''},
showaxeslabels: false,
showticklabels: false,
linetickenable: false
diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js
index f1dfb13523b..3c4d81911b3 100644
--- a/test/jasmine/tests/splom_test.js
+++ b/test/jasmine/tests/splom_test.js
@@ -325,10 +325,10 @@ describe('Test splom trace defaults:', function() {
});
var fullLayout = gd._fullLayout;
- expect(fullLayout.xaxis.title).toBe('A');
- expect(fullLayout.yaxis.title).toBe('A');
- expect(fullLayout.xaxis2.title).toBe('B');
- expect(fullLayout.yaxis2.title).toBe('B');
+ expect(fullLayout.xaxis.title.text).toBe('A');
+ expect(fullLayout.yaxis.title.text).toBe('A');
+ expect(fullLayout.xaxis2.title.text).toBe('B');
+ expect(fullLayout.yaxis2.title.text).toBe('B');
});
it('should set axis title default using dimensions *label* (even visible false dimensions)', function() {
@@ -346,12 +346,12 @@ describe('Test splom trace defaults:', function() {
});
var fullLayout = gd._fullLayout;
- expect(fullLayout.xaxis.title).toBe('A');
- expect(fullLayout.yaxis.title).toBe('A');
- expect(fullLayout.xaxis2.title).toBe('B');
- expect(fullLayout.yaxis2.title).toBe('B');
- expect(fullLayout.xaxis3.title).toBe('C');
- expect(fullLayout.yaxis3.title).toBe('C');
+ expect(fullLayout.xaxis.title.text).toBe('A');
+ expect(fullLayout.yaxis.title.text).toBe('A');
+ expect(fullLayout.xaxis2.title.text).toBe('B');
+ expect(fullLayout.yaxis2.title.text).toBe('B');
+ expect(fullLayout.xaxis3.title.text).toBe('C');
+ expect(fullLayout.yaxis3.title.text).toBe('C');
});
it('should ignore (x|y)axes values beyond dimensions length', function() {
@@ -382,12 +382,12 @@ describe('Test splom trace defaults:', function() {
'x2y', 'x2y2', 'x2y3',
'x3y', 'x3y2', 'x3y3'
]);
- expect(fullLayout.xaxis.title).toBe('A');
- expect(fullLayout.yaxis.title).toBe('A');
- expect(fullLayout.xaxis2.title).toBe('B');
- expect(fullLayout.yaxis2.title).toBe('B');
- expect(fullLayout.xaxis3.title).toBe('C');
- expect(fullLayout.yaxis3.title).toBe('C');
+ expect(fullLayout.xaxis.title.text).toBe('A');
+ expect(fullLayout.yaxis.title.text).toBe('A');
+ expect(fullLayout.xaxis2.title.text).toBe('B');
+ expect(fullLayout.yaxis2.title.text).toBe('B');
+ expect(fullLayout.xaxis3.title.text).toBe('C');
+ expect(fullLayout.yaxis3.title.text).toBe('C');
expect(fullLayout.xaxis4).toBe(undefined);
expect(fullLayout.yaxis4).toBe(undefined);
});
@@ -422,12 +422,12 @@ describe('Test splom trace defaults:', function() {
]);
expect(fullLayout.xaxis).toBe(undefined);
expect(fullLayout.yaxis).toBe(undefined);
- expect(fullLayout.xaxis2.title).toBe('A');
- expect(fullLayout.yaxis2.title).toBe('A');
- expect(fullLayout.xaxis3.title).toBe('B');
- expect(fullLayout.yaxis3.title).toBe('B');
- expect(fullLayout.xaxis4.title).toBe('C');
- expect(fullLayout.yaxis4.title).toBe('C');
+ expect(fullLayout.xaxis2.title.text).toBe('A');
+ expect(fullLayout.yaxis2.title.text).toBe('A');
+ expect(fullLayout.xaxis3.title.text).toBe('B');
+ expect(fullLayout.yaxis3.title.text).toBe('B');
+ expect(fullLayout.xaxis4.title.text).toBe('C');
+ expect(fullLayout.yaxis4.title.text).toBe('C');
expect(fullLayout.xaxis5).toBe(undefined);
expect(fullLayout.yaxis5).toBe(undefined);
});
diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js
index 0e4097c6b59..9ebff733c6a 100644
--- a/test/jasmine/tests/template_test.js
+++ b/test/jasmine/tests/template_test.js
@@ -158,7 +158,9 @@ describe('makeTemplate', function() {
{fill: 'toself'}
] },
layout: {
- title: 'Fill toself and tonext',
+ title: {
+ text: 'Fill toself and tonext'
+ },
width: 400,
height: 400
}
diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js
index f202749ed3b..7d209b41f8d 100644
--- a/test/jasmine/tests/ternary_test.js
+++ b/test/jasmine/tests/ternary_test.js
@@ -1,5 +1,6 @@
var Plotly = require('@lib');
var Lib = require('@src/lib');
+var rgb = require('@src/components/color').rgb;
var supplyLayoutDefaults = require('@src/plots/ternary/layout_defaults');
@@ -382,6 +383,55 @@ describe('ternary plots', function() {
.then(done);
});
+ it('should be able to relayout axis title attributes', function(done) {
+ var gd = createGraphDiv();
+ var fig = Lib.extendDeep({}, require('@mocks/ternary_simple.json'));
+
+ function _assert(axisPrefix, title, family, color, size) {
+ var titleSel = d3.select('.' + axisPrefix + 'title');
+ var titleNode = titleSel.node();
+
+ var msg = 'for ' + axisPrefix + 'axis title';
+ expect(titleSel.text()).toBe(title, 'title ' + msg);
+ expect(titleNode.style['font-family']).toBe(family, 'font family ' + msg);
+ expect(parseFloat(titleNode.style['font-size'])).toBe(size, 'font size ' + msg);
+ expect(titleNode.style.fill).toBe(color, 'font color ' + msg);
+ }
+
+ Plotly.plot(gd, fig).then(function() {
+ _assert('a', 'Component A', '"Open Sans", verdana, arial, sans-serif', rgb('#ccc'), 14);
+ _assert('b', 'chocolate', '"Open Sans", verdana, arial, sans-serif', rgb('#0f0'), 14);
+ _assert('c', 'Component C', '"Open Sans", verdana, arial, sans-serif', rgb('#444'), 14);
+
+ // Note: Different update notations to also test legacy title structures
+ return Plotly.relayout(gd, {
+ 'ternary.aaxis.title.text': 'chips',
+ 'ternary.aaxis.title.font.color': 'yellow',
+ 'ternary.aaxis.titlefont.family': 'monospace',
+ 'ternary.aaxis.titlefont.size': 16,
+ 'ternary.baxis.title': 'white chocolate',
+ 'ternary.baxis.title.font.color': 'blue',
+ 'ternary.baxis.titlefont.family': 'sans-serif',
+ 'ternary.baxis.titlefont.size': 10,
+ 'ternary.caxis.title': {
+ text: 'candy',
+ font: {
+ color: 'pink',
+ family: 'serif',
+ size: 30
+ }
+ }
+ });
+ })
+ .then(function() {
+ _assert('a', 'chips', 'monospace', rgb('yellow'), 16);
+ _assert('b', 'white chocolate', 'sans-serif', rgb('blue'), 10);
+ _assert('c', 'candy', 'serif', rgb('pink'), 30);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('should be able to hide/show ticks and tick labels', function(done) {
var gd = createGraphDiv();
var fig = Lib.extendDeep({}, require('@mocks/ternary_simple.json'));
@@ -546,9 +596,9 @@ describe('ternary defaults', function() {
layoutIn = {};
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
- expect(layoutOut.ternary.aaxis.title).toEqual('Component A');
- expect(layoutOut.ternary.baxis.title).toEqual('Component B');
- expect(layoutOut.ternary.caxis.title).toEqual('Component C');
+ expect(layoutOut.ternary.aaxis.title.text).toEqual('Component A');
+ expect(layoutOut.ternary.baxis.title.text).toEqual('Component B');
+ expect(layoutOut.ternary.caxis.title.text).toEqual('Component C');
});
it('should default \'gricolor\' to 60% dark', function() {
diff --git a/test/jasmine/tests/titles_test.js b/test/jasmine/tests/titles_test.js
index 0e81784bb29..2dd4ba2230f 100644
--- a/test/jasmine/tests/titles_test.js
+++ b/test/jasmine/tests/titles_test.js
@@ -1,13 +1,1060 @@
var d3 = require('d3');
var Plotly = require('@lib/index');
+var alignmentConstants = require('@src/constants/alignment');
var interactConstants = require('@src/constants/interactions');
+var Lib = require('@src/lib');
+var rgb = require('@src/components/color').rgb;
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var mouseEvent = require('../assets/mouse_event');
-describe('editable titles', function() {
+describe('Plot title', function() {
+ 'use strict';
+
+ var data = [{x: [1, 2, 3], y: [1, 2, 3]}];
+ var titlePad = {t: 10, r: 10, b: 10, l: 10};
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ var containerElemSelector = {
+ desc: 'container',
+ select: function() {
+ return d3.selectAll('.svg-container').node();
+ },
+ ref: 'container'
+ };
+
+ var paperElemSelector = {
+ desc: 'plot area',
+ select: function() {
+ var bgLayer = d3.selectAll('.bglayer .bg');
+ expect(bgLayer.empty()).toBe(false,
+ 'No background layer found representing the size of the plot area');
+ return bgLayer.node();
+ },
+ ref: 'paper'
+ };
+
+ it('is centered horizontally and vertically above the plot by default', function() {
+ Plotly.plot(gd, data, {title: {text: 'Plotly line chart'}});
+
+ expectDefaultCenteredPosition(gd);
+ });
+
+ it('can still be defined as `layout.title` to ensure backwards-compatibility', function() {
+ Plotly.plot(gd, data, {title: 'Plotly line chart'});
+
+ expectTitle('Plotly line chart');
+ expectDefaultCenteredPosition(gd);
+ });
+
+ it('can be updated via `relayout`', function(done) {
+ Plotly.plot(gd, data, {title: 'Plotly line chart'})
+ .then(expectTitleFn('Plotly line chart'))
+ .then(function() {
+ return Plotly.relayout(gd, {title: {text: 'Some other title'}});
+ })
+ .then(expectTitleFn('Some other title'))
+ .catch(fail)
+ .then(done);
+ });
+
+ // Horizontal alignment
+ [
+ {
+ selector: containerElemSelector,
+ xref: 'container'
+ },
+ {
+ selector: paperElemSelector,
+ xref: 'paper'
+ }
+ ].forEach(function(testCase) {
+ it('can be placed at the left edge of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: testCase.xref,
+ x: 0,
+ xanchor: 'left'
+ }));
+
+ expectLeftEdgeAlignedTo(testCase.selector);
+ });
+
+ it('can be placed at the right edge of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: testCase.xref,
+ x: 1,
+ xanchor: 'right'
+ }));
+
+ expectRightEdgeAlignedTo(testCase.selector);
+ });
+
+ it('can be placed at the center of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: testCase.xref,
+ x: 0.5,
+ xanchor: 'center'
+ }));
+
+ expectCenteredWithin(testCase.selector);
+ });
+ });
+
+ // Vertical alignment
+ [
+ {
+ selector: containerElemSelector,
+ yref: 'container'
+ },
+ {
+ selector: paperElemSelector,
+ yref: 'paper'
+ }
+ ].forEach(function(testCase) {
+ it('can be placed at the top edge of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: testCase.yref,
+ y: 1,
+ yanchor: 'top'
+ }));
+
+ expectCapLineAlignsWithTopEdgeOf(testCase.selector);
+ });
+
+ it('can be placed at the bottom edge of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: testCase.yref,
+ y: 0,
+ yanchor: 'bottom'
+ }));
+
+ expectBaselineAlignsWithBottomEdgeOf(testCase.selector);
+ });
+
+ it('can be placed in the vertical center of the ' + testCase.selector.desc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: testCase.yref,
+ y: 0.5,
+ yanchor: 'middle'
+ }));
+
+ expectCenteredVerticallyWithin(testCase.selector);
+ });
+ });
+
+ // y 'auto' value
+ it('provides a y \'auto\' value putting title baseline in middle ' +
+ 'of top margin irrespective of `yref`', function() {
+
+ // yref: 'container'
+ Plotly.plot(gd, data, extendLayout({
+ yref: 'container',
+ y: 'auto'
+ }));
+
+ expectBaselineInMiddleOfTopMargin(gd);
+
+ // yref: 'paper'
+ Plotly.relayout(gd, {'title.yref': 'paper'});
+
+ expectBaselineInMiddleOfTopMargin(gd);
+ });
+
+ // xanchor 'auto' test
+ [
+ {x: 0, expAlignment: 'start'},
+ {x: 0.3, expAlignment: 'start'},
+ {x: 0.4, expAlignment: 'middle'},
+ {x: 0.5, expAlignment: 'middle'},
+ {x: 0.6, expAlignment: 'middle'},
+ {x: 0.7, expAlignment: 'end'},
+ {x: 1, expAlignment: 'end'}
+ ].forEach(function(testCase) {
+ runXAnchorAutoTest(testCase, 'container');
+ runXAnchorAutoTest(testCase, 'paper');
+ });
+
+ function runXAnchorAutoTest(testCase, xref) {
+ var testDesc = 'with {xanchor: \'auto\', x: ' + testCase.x + ', xref: \'' + xref +
+ '\'} expected to be aligned ' + testCase.expAlignment;
+ it(testDesc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: xref,
+ x: testCase.x,
+ xanchor: 'auto'
+ }));
+
+ var textAnchor = titleSel().attr('text-anchor');
+ expect(textAnchor).toBe(testCase.expAlignment, testDesc);
+ });
+ }
+
+ // yanchor 'auto' test
+ //
+ // Note: in contrast to xanchor, there's no SVG attribute like
+ // text-anchor we can safely assume to work in all browsers. Thus the
+ // dy attribute has to be used and as a consequence it's much harder to test
+ // arbitrary vertical alignment options. Because of that only the
+ // most common use cases are tested in this regard.
+ [
+ {y: 0, expAlignment: 'bottom', expFn: expectBaselineAlignsWithBottomEdgeOf},
+ {y: 0.5, expAlignment: 'middle', expFn: expectCenteredVerticallyWithin},
+ {y: 1, expAlignment: 'top', expFn: expectCapLineAlignsWithTopEdgeOf},
+ ].forEach(function(testCase) {
+ runYAnchorAutoTest(testCase, 'container', containerElemSelector);
+ runYAnchorAutoTest(testCase, 'paper', paperElemSelector);
+ });
+
+ function runYAnchorAutoTest(testCase, yref, elemSelector) {
+ var testDesc = 'with {yanchor: \'auto\', y: ' + testCase.y + ', yref: \'' + yref +
+ '\'} expected to be aligned ' + testCase.expAlignment;
+ it(testDesc, function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: yref,
+ y: testCase.y,
+ yanchor: 'auto'
+ }));
+
+ testCase.expFn(elemSelector);
+ });
+ }
+
+ it('{y: \'auto\'} overrules {yanchor: \'auto\'} to support behavior ' +
+ 'before chart title alignment was introduced', function() {
+ Plotly.plot(gd, data, extendLayout({
+ y: 'auto',
+ yanchor: 'auto'
+ }));
+
+ expectDefaultCenteredPosition(gd);
+ });
+
+ // Horizontal padding
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('can be placed x pixels away from left ' + refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: refSelector.ref,
+ xanchor: 'left',
+ x: 0,
+ pad: titlePad
+ }));
+
+ expect(titleSel().attr('text-anchor')).toBe('start');
+ expect(titleX() - 10).toBe(leftOf(refSelector));
+ });
+ });
+
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('can be placed x pixels away from right ' + refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: refSelector.ref,
+ xanchor: 'right',
+ x: 1,
+ pad: titlePad
+ }));
+
+ expect(titleSel().attr('text-anchor')).toBe('end');
+ expect(titleX() + 10).toBe(rightOf(refSelector));
+ });
+ });
+
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('figures out for itself which horizontal padding applies when {xanchor: \'auto\'}' +
+ refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: refSelector.ref,
+ xanchor: 'auto',
+ x: 1,
+ pad: titlePad
+ }));
+
+ expect(titleSel().attr('text-anchor')).toBe('end');
+ expect(titleX() + 10).toBe(rightOf(refSelector));
+
+ Plotly.relayout(gd, 'title.x', 0);
+
+ expect(titleSel().attr('text-anchor')).toBe('start');
+ expect(titleX() - 10).toBe(leftOf(refSelector));
+
+ Plotly.relayout(gd, 'title.x', 0.5);
+ expectCenteredWithin(refSelector);
+ });
+ });
+
+ // Cases when horizontal padding is ignored
+ // (just testing with paper is sufficient)
+ [
+ {pad: {l: 20}, dir: 'left'},
+ {pad: {r: 20}, dir: 'right'}
+ ].forEach(function(testCase) {
+ it('mutes ' + testCase.dir + ' padding for {xanchor: \'center\'}', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: 'paper',
+ xanchor: 'middle',
+ x: 0.5,
+ pad: testCase.pad
+ }));
+
+ expectCenteredWithin(paperElemSelector);
+ });
+ });
+
+ it('mutes left padding when xanchor is right', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: 'paper',
+ x: 1,
+ xanchor: 'right',
+ pad: {
+ l: 1000
+ }
+ }));
+
+ expectRightEdgeAlignedTo(paperElemSelector);
+ });
+
+ it('mutes right padding when xanchor is left', function() {
+ Plotly.plot(gd, data, extendLayout({
+ xref: 'paper',
+ x: 0,
+ xanchor: 'left',
+ pad: {
+ r: 1000
+ }
+ }));
+
+ expectLeftEdgeAlignedTo(paperElemSelector);
+ });
+
+ // Vertical padding
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('can be placed x pixels below top ' + refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: refSelector.ref,
+ yanchor: 'top',
+ y: 1,
+ pad: titlePad
+ }));
+
+ var capLineY = calcTextCapLineY(titleSel());
+ expect(capLineY).toBe(topOf(refSelector) + 10);
+ });
+ });
+
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('can be placed x pixels above bottom ' + refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: refSelector.ref,
+ yanchor: 'bottom',
+ y: 0,
+ pad: titlePad
+ }));
+
+ var baselineY = calcTextBaselineY(titleSel());
+ expect(baselineY).toBe(bottomOf(refSelector) - 10);
+ });
+ });
+
+ [containerElemSelector, paperElemSelector].forEach(function(refSelector) {
+ it('figures out for itself which vertical padding applies when {yanchor: \'auto\'}' +
+ refSelector.desc + ' edge', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: refSelector.ref,
+ yanchor: 'auto',
+ y: 1,
+ pad: titlePad
+ }));
+
+ var capLineY = calcTextCapLineY(titleSel());
+ expect(capLineY).toBe(topOf(refSelector) + 10);
+
+ Plotly.relayout(gd, 'title.y', 0);
+
+ var baselineY = calcTextBaselineY(titleSel());
+ expect(baselineY).toBe(bottomOf(refSelector) - 10);
+
+ Plotly.relayout(gd, 'title.y', 0.5);
+ expectCenteredVerticallyWithin(refSelector);
+ });
+ });
+
+ // Cases when vertical padding is ignored
+ // (just testing with paper is sufficient)
+ [
+ {pad: {t: 20}, dir: 'top'},
+ {pad: {b: 20}, dir: 'bottom'}
+ ].forEach(function(testCase) {
+ it('mutes ' + testCase.dir + ' padding for {yanchor: \'middle\'}', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: 'paper',
+ yanchor: 'middle',
+ y: 0.5,
+ pad: testCase.pad
+ }));
+
+ expectCenteredVerticallyWithin(paperElemSelector);
+ });
+ });
+
+ it('mutes top padding when yanchor is bottom', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: 'paper',
+ y: 0,
+ yanchor: 'bottom',
+ pad: {
+ t: 1000
+ }
+ }));
+
+ expectBaselineAlignsWithBottomEdgeOf(paperElemSelector);
+ });
+
+ it('mutes bottom padding when yanchor is top', function() {
+ Plotly.plot(gd, data, extendLayout({
+ yref: 'paper',
+ y: 1,
+ yanchor: 'top',
+ pad: {
+ b: 1000
+ }
+ }));
+
+ expectCapLineAlignsWithTopEdgeOf(paperElemSelector);
+ });
+
+ function extendLayout(titleAttrs) {
+ return {
+ title: Lib.extendFlat({text: 'A Chart Title'}, titleAttrs),
+
+ // This needs to be set to have a DOM element that represents the
+ // exact size of the plot area.
+ plot_bgcolor: '#f9f9f9',
+ };
+ }
+
+ function topOf(elemSelector) {
+ return elemSelector.select().getBoundingClientRect().top;
+ }
+
+ function rightOf(elemSelector) {
+ return elemSelector.select().getBoundingClientRect().right;
+ }
+
+ function bottomOf(elemSelector) {
+ return elemSelector.select().getBoundingClientRect().bottom;
+ }
+
+ function leftOf(elemSelector) {
+ return elemSelector.select().getBoundingClientRect().left;
+ }
+
+ function expectLeftEdgeAlignedTo(elemSelector) {
+ expectHorizontalEdgeAligned(elemSelector, 'left');
+ }
+
+ function expectRightEdgeAlignedTo(elemSelector) {
+ expectHorizontalEdgeAligned(elemSelector, 'right');
+ }
+
+ function expectHorizontalEdgeAligned(elemSelector, edgeKey) {
+ var refElem = elemSelector.select();
+ var titleElem = titleSel().node();
+ var refElemBB = refElem.getBoundingClientRect();
+ var titleBB = titleElem.getBoundingClientRect();
+
+ var tolerance = 1.1;
+ var msg = 'Title ' + edgeKey + ' of ' + elemSelector.desc;
+ expect(titleBB[edgeKey] - refElemBB[edgeKey]).toBeWithin(0, tolerance, msg);
+ }
+
+ function expectCapLineAlignsWithTopEdgeOf(elemSelector) {
+ var refElem = elemSelector.select();
+ var refElemBB = refElem.getBoundingClientRect();
+
+ // Note: getBoundingClientRect of an SVG element
+ // doesn't return the tightest bounding box of the current text
+ // but a rectangle that is wide enough to contain any
+ // possible character even though something like 'Ö' isn't
+ // in the current title string. Moreover getBoundingClientRect
+ // (with respect to SVG elements) differs from browser to
+ // browser and thus is unreliable for testing vertical alignment
+ // of SVG text. Because of that the cap line is calculated based on the
+ // element properties.
+ var capLineY = calcTextCapLineY(titleSel());
+
+ var msg = 'Title\'s cap line y is same as the top of ' + elemSelector.desc;
+ expect(capLineY).toBeWithin(refElemBB.top, 1.1, msg);
+ }
+
+ function expectBaselineAlignsWithBottomEdgeOf(elemSelector) {
+ var refElem = elemSelector.select();
+ var refElemBB = refElem.getBoundingClientRect();
+
+ // Note: using getBoundingClientRect is not reliable, see
+ // comment in `expectCapLineAlignsWithTopEdgeOf` for more info.
+ var baselineY = calcTextBaselineY(titleSel());
+
+ var msg = 'Title baseline sits on the bottom of ' + elemSelector.desc;
+ expect(baselineY).toBeWithin(refElemBB.bottom, 1.1, msg);
+ }
+
+ function expectCenteredWithin(elemSelector) {
+ var refElem = elemSelector.select();
+ var titleElem = titleSel().node();
+ var refElemBB = refElem.getBoundingClientRect();
+ var titleBB = titleElem.getBoundingClientRect();
+
+ var leftDistance = titleBB.left - refElemBB.left;
+ var rightDistance = refElemBB.right - titleBB.right;
+
+ var tolerance = 1.1;
+ var msg = 'Title in center of ' + elemSelector.desc;
+ expect(leftDistance).toBeWithin(rightDistance, tolerance, msg);
+ }
+
+ function expectCenteredVerticallyWithin(elemSelector) {
+ var refElem = elemSelector.select();
+ var titleElem = titleSel().node();
+ var refElemBB = refElem.getBoundingClientRect();
+ var titleBB = titleElem.getBoundingClientRect();
+
+ var topDistance = titleBB.top - refElemBB.top;
+ var bottomDistance = refElemBB.bottom - titleBB.bottom;
+
+ var tolerance = 1.1;
+ var msg = 'Title centered vertically within ' + elemSelector.desc;
+ expect(topDistance).toBeWithin(bottomDistance, tolerance, msg);
+ }
+});
+
+describe('Titles can be updated', function() {
+ 'use strict';
+
+ var data = [{x: [1, 2, 3], y: [1, 2, 3]}];
+ var NEW_TITLE = 'Weight over years';
+ var NEW_XTITLE = 'Age in years';
+ var NEW_YTITLE = 'Average weight';
+ var gd;
+
+ beforeEach(function() {
+ var layout = {
+ title: {text: 'Plotly line chart'},
+ xaxis: {title: {text: 'Age'}},
+ yaxis: {title: {text: 'Weight'}}
+ };
+ gd = createGraphDiv();
+ Plotly.plot(gd, data, Lib.extendDeep({}, layout));
+
+ expectTitles('Plotly line chart', 'Age', 'Weight');
+ });
+
+ afterEach(destroyGraphDiv);
+
+ [
+ {
+ desc: 'by replacing the entire title objects',
+ update: {
+ title: {text: NEW_TITLE},
+ xaxis: {title: {text: NEW_XTITLE}},
+ yaxis: {title: {text: NEW_YTITLE}}
+ }
+ },
+ {
+ desc: 'by using attribute strings',
+ update: {
+ 'title.text': NEW_TITLE,
+ 'xaxis.title.text': NEW_XTITLE,
+ 'yaxis.title.text': NEW_YTITLE
+ }
+ },
+ {
+ desc: 'despite passing title only as a string (backwards-compatibility)',
+ update: {
+ title: NEW_TITLE,
+ xaxis: {title: NEW_XTITLE},
+ yaxis: {title: NEW_YTITLE}
+ }
+ },
+ {
+ desc: 'despite passing title only as a string using string attributes ' +
+ '(backwards-compatibility)',
+ update: {
+ 'title': NEW_TITLE,
+ 'xaxis.title': NEW_XTITLE,
+ 'yaxis.title': NEW_YTITLE
+ }
+ }
+ ].forEach(function(testCase) {
+ it('via `Plotly.relayout` ' + testCase.desc, function() {
+ Plotly.relayout(gd, testCase.update);
+
+ expectChangedTitles();
+ });
+
+ it('via `Plotly.update` ' + testCase.desc, function() {
+ Plotly.update(gd, {}, testCase.update);
+
+ expectChangedTitles();
+ });
+ });
+
+ function expectChangedTitles() {
+ expectTitles(NEW_TITLE, NEW_XTITLE, NEW_YTITLE);
+ }
+
+ function expectTitles(expTitle, expXTitle, expYTitle) {
+ expectTitle(expTitle);
+
+ var xSel = xTitleSel();
+ expect(xSel.empty()).toBe(false, 'X-axis title element missing');
+ expect(xSel.text()).toBe(expXTitle);
+
+ var ySel = yTitleSel();
+ expect(ySel.empty()).toBe(false, 'Y-axis title element missing');
+ expect(ySel.text()).toBe(expYTitle);
+ }
+});
+
+describe('Titles support setting custom font properties', function() {
+ 'use strict';
+
+ var data = [{x: [1, 2, 3], y: [1, 2, 3]}];
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('through defining a `font` property in the respective title attribute', function() {
+ var layout = {
+ title: {
+ text: 'Plotly line chart',
+ font: {
+ color: 'blue',
+ family: 'serif',
+ size: 24
+ }
+ },
+ xaxis: {
+ title: {
+ text: 'X-Axis',
+ font: {
+ color: '#333',
+ family: 'sans-serif',
+ size: 20
+ }
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Y-Axis',
+ font: {
+ color: '#666',
+ family: 'Arial',
+ size: 16
+ }
+ }
+ }
+ };
+ Plotly.plot(gd, data, layout);
+
+ expectTitleFont('blue', 'serif', 24);
+ expectXAxisTitleFont('#333', 'sans-serif', 20);
+ expectYAxisTitleFont('#666', 'Arial', 16);
+ });
+
+ it('through using the deprecated `titlefont` properties (backwards-compatibility)', function() {
+ var layout = {
+ title: {
+ text: 'Plotly line chart',
+ },
+ titlefont: {
+ color: 'blue',
+ family: 'serif',
+ size: 24
+ },
+ xaxis: {
+ title: {
+ text: 'X-Axis',
+ },
+ titlefont: {
+ color: '#333',
+ family: 'sans-serif',
+ size: 20
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Y-Axis',
+ },
+ titlefont: {
+ color: '#666',
+ family: 'Arial',
+ size: 16
+ }
+ }
+ };
+ Plotly.plot(gd, data, layout);
+
+ expectTitleFont('blue', 'serif', 24);
+ expectXAxisTitleFont('#333', 'sans-serif', 20);
+ expectYAxisTitleFont('#666', 'Arial', 16);
+ });
+});
+
+describe('Title fonts can be updated', function() {
+ 'use strict';
+
+ var data = [{x: [1, 2, 3], y: [1, 2, 3]}];
+ var NEW_TITLE = 'Weight over years';
+ var NEW_XTITLE = 'Age in years';
+ var NEW_YTITLE = 'Average weight';
+ var NEW_TITLE_FONT = {color: '#333', family: 'serif', size: 28};
+ var NEW_XTITLE_FONT = {color: '#666', family: 'sans-serif', size: 18};
+ var NEW_YTITLE_FONT = {color: '#999', family: 'serif', size: 12};
+ var gd;
+
+ beforeEach(function() {
+ var layout = {
+ title: {
+ text: 'Plotly line chart',
+ font: {color: 'black', family: 'sans-serif', size: 24}
+ },
+ xaxis: {
+ title: {
+ text: 'Age',
+ font: {color: 'red', family: 'serif', size: 20}
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Weight',
+ font: {color: 'green', family: 'monospace', size: 16}
+ }
+ }
+ };
+ gd = createGraphDiv();
+ Plotly.plot(gd, data, Lib.extendDeep({}, layout));
+
+ expectTitleFont('black', 'sans-serif', 24);
+ expectXAxisTitleFont('red', 'serif', 20);
+ expectYAxisTitleFont('green', 'monospace', 16);
+ });
+
+ afterEach(destroyGraphDiv);
+
+ [
+ {
+ desc: 'by replacing the entire title objects',
+ update: {
+ title: {
+ text: NEW_TITLE,
+ font: NEW_TITLE_FONT
+ },
+ xaxis: {
+ title: {
+ text: NEW_XTITLE,
+ font: NEW_XTITLE_FONT
+ }
+ },
+ yaxis: {
+ title: {
+ text: NEW_YTITLE,
+ font: NEW_YTITLE_FONT
+ }
+ }
+ }
+ },
+ {
+ desc: 'by using attribute strings',
+ update: {
+ 'title.font.color': NEW_TITLE_FONT.color,
+ 'title.font.family': NEW_TITLE_FONT.family,
+ 'title.font.size': NEW_TITLE_FONT.size,
+ 'xaxis.title.font.color': NEW_XTITLE_FONT.color,
+ 'xaxis.title.font.family': NEW_XTITLE_FONT.family,
+ 'xaxis.title.font.size': NEW_XTITLE_FONT.size,
+ 'yaxis.title.font.color': NEW_YTITLE_FONT.color,
+ 'yaxis.title.font.family': NEW_YTITLE_FONT.family,
+ 'yaxis.title.font.size': NEW_YTITLE_FONT.size
+ }
+ },
+ {
+ desc: 'despite passing deprecated `titlefont` properties (backwards-compatibility)',
+ update: {
+ titlefont: NEW_TITLE_FONT,
+ xaxis: {
+ title: NEW_XTITLE,
+ titlefont: NEW_XTITLE_FONT
+ },
+ yaxis: {
+ title: NEW_YTITLE,
+ titlefont: NEW_YTITLE_FONT
+ }
+ }
+ },
+ {
+ desc: 'despite using string attributes representing the deprecated structure ' +
+ '(backwards-compatibility)',
+ update: {
+ 'titlefont.color': NEW_TITLE_FONT.color,
+ 'titlefont.family': NEW_TITLE_FONT.family,
+ 'titlefont.size': NEW_TITLE_FONT.size,
+ 'xaxis.titlefont.color': NEW_XTITLE_FONT.color,
+ 'xaxis.titlefont.family': NEW_XTITLE_FONT.family,
+ 'xaxis.titlefont.size': NEW_XTITLE_FONT.size,
+ 'yaxis.titlefont.color': NEW_YTITLE_FONT.color,
+ 'yaxis.titlefont.family': NEW_YTITLE_FONT.family,
+ 'yaxis.titlefont.size': NEW_YTITLE_FONT.size
+ }
+ },
+ {
+ desc: 'despite using string attributes replacing deprecated `titlefont` attributes ' +
+ '(backwards-compatibility)',
+ update: {
+ 'titlefont': NEW_TITLE_FONT,
+ 'xaxis.titlefont': NEW_XTITLE_FONT,
+ 'yaxis.titlefont': NEW_YTITLE_FONT
+ }
+ }
+ ].forEach(function(testCase) {
+ it('via `Plotly.relayout` ' + testCase.desc, function() {
+ Plotly.relayout(gd, testCase.update);
+
+ expectChangedTitleFonts();
+ });
+
+ it('via `Plotly.update` ' + testCase.desc, function() {
+ Plotly.update(gd, {}, testCase.update);
+
+ expectChangedTitleFonts();
+ });
+ });
+
+ function expectChangedTitleFonts() {
+ expectTitleFont(NEW_TITLE_FONT.color, NEW_TITLE_FONT.family, NEW_TITLE_FONT.size);
+ expectXAxisTitleFont(NEW_XTITLE_FONT.color, NEW_XTITLE_FONT.family, NEW_XTITLE_FONT.size);
+ expectYAxisTitleFont(NEW_YTITLE_FONT.color, NEW_YTITLE_FONT.family, NEW_YTITLE_FONT.size);
+ }
+});
+
+describe('Titles for multiple axes', function() {
+ 'use strict';
+
+ var data = [
+ {x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x', yaxis: 'y'},
+ {x: [1, 2, 3], y: [3, 2, 1], xaxis: 'x2', yaxis: 'y2'}
+ ];
+ var multiAxesLayout = {
+ xaxis: {
+ title: 'X-Axis 1',
+ titlefont: {
+ size: 30
+ }
+ },
+ xaxis2: {
+ title: 'X-Axis 2',
+ titlefont: {
+ family: 'serif'
+ },
+ side: 'top'
+ },
+ yaxis: {
+ title: 'Y-Axis 1',
+ titlefont: {
+ family: 'Roboto'
+ },
+ },
+ yaxis2: {
+ title: 'Y-Axis 2',
+ titlefont: {
+ color: 'blue'
+ },
+ side: 'right'
+ }
+ };
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('still support deprecated `title` and `titlefont` syntax (backwards-compatibility)', function() {
+ Plotly.plot(gd, data, multiAxesLayout);
+
+ expect(xTitleSel(1).text()).toBe('X-Axis 1');
+ expect(xTitleSel(1).node().style.fontSize).toBe('30px');
+
+ expect(xTitleSel(2).text()).toBe('X-Axis 2');
+ expect(xTitleSel(2).node().style.fontFamily).toBe('serif');
+
+ expect(yTitleSel(1).text()).toBe('Y-Axis 1');
+ expect(yTitleSel(1).node().style.fontFamily).toBe('Roboto');
+
+ expect(yTitleSel(2).text()).toBe('Y-Axis 2');
+ expect(yTitleSel(2).node().style.fill).toBe(rgb('blue'));
+ });
+
+ it('can be updated using deprecated `title` and `titlefont` syntax (backwards-compatibility)', function() {
+ Plotly.plot(gd, data, multiAxesLayout);
+
+ Plotly.relayout(gd, {
+ 'xaxis2.title': '2nd X-Axis',
+ 'xaxis2.titlefont.color': 'pink',
+ 'xaxis2.titlefont.family': 'sans-serif',
+ 'xaxis2.titlefont.size': '14',
+ 'yaxis2.title': '2nd Y-Axis',
+ 'yaxis2.titlefont.color': 'yellow',
+ 'yaxis2.titlefont.family': 'monospace',
+ 'yaxis2.titlefont.size': '5'
+ });
+
+ var x2Style = xTitleSel(2).node().style;
+ expect(xTitleSel(2).text()).toBe('2nd X-Axis');
+ expect(x2Style.fill).toBe(rgb('pink'));
+ expect(x2Style.fontFamily).toBe('sans-serif');
+ expect(x2Style.fontSize).toBe('14px');
+
+ var y2Style = yTitleSel(2).node().style;
+ expect(yTitleSel(2).text()).toBe('2nd Y-Axis');
+ expect(y2Style.fill).toBe(rgb('yellow'));
+ expect(y2Style.fontFamily).toBe('monospace');
+ expect(y2Style.fontSize).toBe('5px');
+ });
+});
+
+function expectTitle(expTitle) {
+ expectTitleFn(expTitle)();
+}
+
+function expectTitleFn(expTitle) {
+ return function() {
+ expect(titleSel().text()).toBe(expTitle);
+ };
+}
+
+function expectTitleFont(color, family, size) {
+ expectFont(titleSel(), color, family, size);
+}
+
+function expectXAxisTitleFont(color, family, size) {
+ expectFont(xTitleSel(), color, family, size);
+}
+
+function expectYAxisTitleFont(color, family, size) {
+ expectFont(yTitleSel(), color, family, size);
+}
+
+function expectFont(sel, color, family, size) {
+ var node = sel.node();
+ expect(node.style.fill).toBe(rgb(color));
+ expect(node.style.fontFamily).toBe(family);
+ expect(node.style.fontSize).toBe(size + 'px');
+}
+
+function expectDefaultCenteredPosition(gd) {
+ var containerBB = gd.getBoundingClientRect();
+
+ expect(titleX()).toBe(containerBB.width / 2);
+ expectBaselineInMiddleOfTopMargin(gd);
+}
+
+function expectBaselineInMiddleOfTopMargin(gd) {
+ var baselineY = calcTextBaselineY(titleSel());
+ var topMarginHeight = gd._fullLayout.margin.t;
+
+ expect(baselineY).toBe(topMarginHeight / 2);
+}
+
+function titleX() {
+ return Number.parseFloat(titleSel().attr('x'));
+}
+
+function calcTextBaselineY(textSel) {
+ var y = Number.parseFloat(textSel.attr('y'));
+ var yShift = 0;
+ var dy = textSel.attr('dy');
+ var parsedDy, dyNumValue, dyUnit;
+ var fontSize, parsedFontSize;
+
+ if(dy) {
+ parsedDy = /^([0-9.]*)(\w*)$/.exec(dy);
+ if(parsedDy) {
+ dyUnit = parsedDy[2];
+ dyNumValue = Number.parseFloat(parsedDy[1]);
+
+ if(dyUnit === 'em') {
+ fontSize = textSel.node().style.fontSize;
+ parsedFontSize = parseFontSizeAttr(fontSize);
+
+ yShift = dyNumValue * Number.parseFloat(parsedFontSize.val);
+ } else if(dyUnit === '') {
+ yShift = dyNumValue;
+ } else {
+ fail('Calculating y-shift for unit ' + dyUnit + ' not implemented in test');
+ }
+ } else {
+ fail('dy value \'' + dy + '\' could not be parsed by test');
+ }
+ }
+
+ return y + yShift;
+}
+
+function calcTextCapLineY(textSel) {
+ var baselineY = calcTextBaselineY(textSel);
+ var fontSize = textSel.node().style.fontSize;
+ var fontSizeVal = parseFontSizeAttr(fontSize).val;
+
+ // CAP_SHIFT is assuming a cap height of an average font.
+ // One would have to analyze the font metrics of the
+ // used font to determine an accurate cap shift factor.
+ return baselineY - fontSizeVal * alignmentConstants.CAP_SHIFT;
+}
+
+function parseFontSizeAttr(fontSizeAttr) {
+ var parsedFontSize = /^([0-9.]*)px$/.exec(fontSizeAttr);
+ var isFontSizeInPx = !!parsedFontSize;
+ expect(isFontSizeInPx).toBe(true, 'Tests assumes font-size is set in pixel');
+
+ return {
+ val: parsedFontSize[1],
+ unit: parsedFontSize[2]
+ };
+}
+
+function titleSel() {
+ var titleSel = d3.select('.infolayer .g-gtitle .gtitle');
+ expect(titleSel.empty()).toBe(false, 'Plot title element missing');
+ return titleSel;
+}
+
+function xTitleSel(num) {
+ var axIdx = num === 1 ? '' : (num || '');
+ var xTitleSel = d3.select('.x' + axIdx + 'title');
+ expect(xTitleSel.empty()).toBe(false, 'X-axis ' + axIdx + ' title element missing');
+ return xTitleSel;
+}
+
+function yTitleSel(num) {
+ var axIdx = num === 1 ? '' : (num || '');
+ var yTitleSel = d3.select('.y' + axIdx + 'title');
+ expect(yTitleSel.empty()).toBe(false, 'Y-axis ' + axIdx + ' title element missing');
+ return yTitleSel;
+}
+
+describe('Editable titles', function() {
'use strict';
var data = [{x: [1, 2, 3], y: [1, 2, 3]}];
@@ -81,9 +1128,9 @@ describe('editable titles', function() {
it('has hover effects for blank titles', function(done) {
Plotly.plot(gd, data, {
- xaxis: {title: ''},
- yaxis: {title: ''},
- title: ''
+ xaxis: {title: {text: ''}},
+ yaxis: {title: {text: ''}},
+ title: {text: ''}
}, {editable: true})
.then(function() {
return Promise.all([
@@ -97,18 +1144,18 @@ describe('editable titles', function() {
it('has no hover effects for titles that used to be blank', function(done) {
Plotly.plot(gd, data, {
- xaxis: {title: ''},
- yaxis: {title: ''},
- title: ''
+ xaxis: {title: {text: ''}},
+ yaxis: {title: {text: ''}},
+ title: {text: ''}
}, {editable: true})
.then(function() {
- return editTitle('x', 'xaxis.title', 'XXX');
+ return editTitle('x', 'xaxis.title.text', 'XXX');
})
.then(function() {
- return editTitle('y', 'yaxis.title', 'YYY');
+ return editTitle('y', 'yaxis.title.text', 'YYY');
})
.then(function() {
- return editTitle('g', 'title', 'TTT');
+ return editTitle('g', 'title.text', 'TTT');
})
.then(function() {
return Promise.all([
diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js
index 0a455f2b4e5..aed5c603233 100644
--- a/test/jasmine/tests/validate_test.js
+++ b/test/jasmine/tests/validate_test.js
@@ -18,7 +18,9 @@ describe('Plotly.validate', function() {
type: 'scatter',
x: [1, 2, 3]
}], {
- title: 'my simple graph'
+ title: {
+ text: 'my simple graph'
+ }
});
expect(out).toBeUndefined();
@@ -371,7 +373,9 @@ describe('Plotly.validate', function() {
}]
}),
], {
- title: 'my transformed graph'
+ title: {
+ text: 'my transformed graph'
+ }
});
expect(out.length).toEqual(5);