diff --git a/.circleci/test.sh b/.circleci/test.sh index cda97917c29..2d31d1f8e30 100755 --- a/.circleci/test.sh +++ b/.circleci/test.sh @@ -102,7 +102,7 @@ case $1 in ;; make-baselines-mathjax3) - python3 test/image/make_baseline.py mathjax3 legend_mathjax_title_and_items mathjax parcats_grid_subplots table_latex_multitrace_scatter table_plain_birds table_wrapped_birds ternary-mathjax || EXIT_STATE=$? + python3 test/image/make_baseline.py mathjax3 legend_mathjax_title_and_items mathjax parcats_grid_subplots table_latex_multitrace_scatter table_plain_birds table_wrapped_birds ternary-mathjax zz-ternary-mathjax-title-place-subtitle || EXIT_STATE=$? exit $EXIT_STATE ;; diff --git a/draftlogs/7012_add.md b/draftlogs/7012_add.md new file mode 100644 index 00000000000..e59fb52ecd3 --- /dev/null +++ b/draftlogs/7012_add.md @@ -0,0 +1 @@ + - Add `subtitle` attribute to `layout.title` to enable adding subtitles to plots [[#7012](https://github.com/plotly/plotly.js/pull/7012)] diff --git a/lib/locales/es.js b/lib/locales/es.js index ba523466fdd..5c107032429 100644 --- a/lib/locales/es.js +++ b/lib/locales/es.js @@ -11,6 +11,7 @@ module.exports = { 'Click to enter Component B title': 'Introducir el título del Componente B', // plots/ternary/ternary.js:406 'Click to enter Component C title': 'Introducir el título del Componente C', // plots/ternary/ternary.js:417 'Click to enter Plot title': 'Introducir el título de la Gráfica', // plot_api/plot_api.js:579 + 'Click to enter Plot subtitle': 'Introducir el subtítulo de la Gráfica', // plot_api/plot_api.js:579 'Click to enter X axis title': 'Introducir el título del eje X', // plots/plots.js:301 'Click to enter Y axis title': 'Introducir el título del eje Y', // plots/plots.js:302 'Click to enter radial axis title': 'Introducir el título del eje radial', diff --git a/lib/locales/fr.js b/lib/locales/fr.js index 4471e6696a3..4f30fcc3c4e 100644 --- a/lib/locales/fr.js +++ b/lib/locales/fr.js @@ -11,6 +11,7 @@ module.exports = { 'Click to enter Component B title': 'Ajouter un titre à la composante B', 'Click to enter Component C title': 'Ajouter un titre à la composante C', 'Click to enter Plot title': 'Ajouter un titre au graphique', + 'Click to enter Plot subtitle': 'Ajouter un sous-titre au graphique', 'Click to enter X axis title': 'Ajouter un titre à l\'axe des x', 'Click to enter Y axis title': 'Ajouter un titre à l\'axe des y', 'Click to enter radial axis title': 'Ajouter un titre à l\'axe radial', diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 704dec9ee43..29b562407ef 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -14,6 +14,8 @@ var interactConstants = require('../../constants/interactions'); var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE; var numStripRE = / [XY][0-9]* /; +var SUBTITLE_PADDING_MATHJAX_EM = 1.6; +var SUBTITLE_PADDING_EM = 1.6; /** * Titles - (re)draw titles on the axes and plot: @@ -48,6 +50,8 @@ var numStripRE = / [XY][0-9]* /; * @return {selection} d3 selection of title container group */ function draw(gd, titleClass, options) { + var fullLayout = gd._fullLayout; + var cont = options.propContainer; var prop = options.propName; var placeholder = options.placeholder; @@ -56,13 +60,10 @@ function draw(gd, titleClass, options) { var attributes = options.attributes; var transform = options.transform; var group = options.containerGroup; - - var fullLayout = gd._fullLayout; - var opacity = 1; - var isplaceholder = false; var title = cont.title; var txt = (title && title.text ? title.text : '').trim(); + var titleIsPlaceholder = false; var font = title && title.font ? title.font : {}; var fontFamily = font.family; @@ -75,23 +76,58 @@ function draw(gd, titleClass, options) { var fontLineposition = font.lineposition; var fontShadow = font.shadow; + // Get subtitle properties + var subtitleProp = options.subtitlePropName; + var subtitleEnabled = !!subtitleProp; + var subtitlePlaceholder = options.subtitlePlaceholder; + var subtitle = (cont.title || {}).subtitle || {text: '', font: {}}; + var subtitleTxt = subtitle.text.trim(); + var subtitleIsPlaceholder = false; + var subtitleOpacity = 1; + + var subtitleFont = subtitle.font; + var subFontFamily = subtitleFont.family; + var subFontSize = subtitleFont.size; + var subFontColor = subtitleFont.color; + var subFontWeight = subtitleFont.weight; + var subFontStyle = subtitleFont.style; + var subFontVariant = subtitleFont.variant; + var subFontTextcase = subtitleFont.textcase; + var subFontLineposition = subtitleFont.lineposition; + var subFontShadow = subtitleFont.shadow; + // only make this title editable if we positively identify its property // as one that has editing enabled. + // Subtitle is editable if and only if title is editable var editAttr; 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]; + function matchesPlaceholder(text, placeholder) { + if(text === undefined || placeholder === undefined) return false; + // look for placeholder text while stripping out numbers from eg X2, Y3 + // this is just for backward compatibility with the old version that had + // "Click to enter X2 title" and may have gotten saved in some old plots, + // we don't want this to show up when these are displayed. + return text.replace(numStripRE, ' % ') === placeholder.replace(numStripRE, ' % '); + } + if(txt === '') opacity = 0; - // look for placeholder text while stripping out numbers from eg X2, Y3 - // this is just for backward compatibility with the old version that had - // "Click to enter X2 title" and may have gotten saved in some old plots, - // we don't want this to show up when these are displayed. - else if(txt.replace(numStripRE, ' % ') === placeholder.replace(numStripRE, ' % ')) { - opacity = 0.2; - isplaceholder = true; + else if(matchesPlaceholder(txt, placeholder)) { if(!editable) txt = ''; + opacity = 0.2; + titleIsPlaceholder = true; + } + + if(subtitleEnabled) { + if(subtitleTxt === '') subtitleOpacity = 0; + else if(matchesPlaceholder(subtitleTxt, subtitlePlaceholder)) { + if(!editable) subtitleTxt = ''; + subtitleOpacity = 0.2; + subtitleIsPlaceholder = true; + } } if(options._meta) { @@ -100,7 +136,7 @@ function draw(gd, titleClass, options) { txt = Lib.templateString(txt, fullLayout._meta); } - var elShouldExist = txt || editable; + var elShouldExist = txt || subtitleTxt || editable; var hColorbarMoveTitle; if(!group) { @@ -108,7 +144,7 @@ function draw(gd, titleClass, options) { hColorbarMoveTitle = fullLayout._hColorbarMoveTitle; } - var el = group.selectAll('text') + var el = group.selectAll('text.' + titleClass) .data(elShouldExist ? [0] : []); el.enter().append('text'); el.text(txt) @@ -120,13 +156,29 @@ function draw(gd, titleClass, options) { .attr('class', titleClass); el.exit().remove(); + var subtitleEl = null; + var subtitleClass = titleClass + '-subtitle'; + var subtitleElShouldExist = subtitleTxt || editable; + + if(subtitleEnabled && subtitleElShouldExist) { + subtitleEl = group.selectAll('text.' + subtitleClass) + .data(subtitleElShouldExist ? [0] : []); + subtitleEl.enter().append('text'); + subtitleEl.text(subtitleTxt).attr('class', subtitleClass); + subtitleEl.exit().remove(); + } + + if(!elShouldExist) return group; - function titleLayout(titleEl) { - Lib.syncOrAsync([drawTitle, scootTitle], titleEl); + function titleLayout(titleEl, subtitleEl) { + Lib.syncOrAsync([drawTitle, scootTitle], { title: titleEl, subtitle: subtitleEl }); } - function drawTitle(titleEl) { + function drawTitle(titleAndSubtitleEls) { + var titleEl = titleAndSubtitleEls.title; + var subtitleEl = titleAndSubtitleEls.subtitle; + var transformVal; if(!transform && hColorbarMoveTitle) { @@ -147,6 +199,23 @@ function draw(gd, titleClass, options) { titleEl.attr('transform', transformVal); + // Callback to adjust the subtitle position after mathjax is rendered + // Mathjax is rendered asynchronously, which is why this step needs to be + // passed as a callback + function adjustSubtitlePosition(titleElMathGroup) { + if(!titleElMathGroup) return; + + var subtitleElement = d3.select(titleElMathGroup.node().parentNode).select('.' + subtitleClass); + if(!subtitleElement.empty()) { + var titleElMathBbox = titleElMathGroup.node().getBBox(); + if(titleElMathBbox.height) { + // Position subtitle based on bottom of Mathjax title + var subtitleY = titleElMathBbox.y + titleElMathBbox.height + (SUBTITLE_PADDING_MATHJAX_EM * subFontSize); + subtitleElement.attr('y', subtitleY); + } + } + } + titleEl.style('opacity', opacity * Color.opacity(fontColor)) .call(Drawing.font, { color: Color.rgb(fontColor), @@ -160,12 +229,43 @@ function draw(gd, titleClass, options) { lineposition: fontLineposition, }) .attr(attributes) - .call(svgTextUtils.convertToTspans, gd); + .call(svgTextUtils.convertToTspans, gd, adjustSubtitlePosition); + + if(subtitleEl) { + // Set subtitle y position based on bottom of title + // We need to check the Mathjax group as well, in case the Mathjax + // has already rendered + var titleElMathGroup = group.select('.' + titleClass + '-math-group'); + var titleElBbox = titleEl.node().getBBox(); + var titleElMathBbox = titleElMathGroup.node() ? titleElMathGroup.node().getBBox() : undefined; + var subtitleY = titleElMathBbox ? titleElMathBbox.y + titleElMathBbox.height + (SUBTITLE_PADDING_MATHJAX_EM * subFontSize) : titleElBbox.y + titleElBbox.height + (SUBTITLE_PADDING_EM * subFontSize); + + var subtitleAttributes = Lib.extendFlat({}, attributes, { + y: subtitleY + }); + + subtitleEl.attr('transform', transformVal); + subtitleEl.style('opacity', subtitleOpacity * Color.opacity(subFontColor)) + .call(Drawing.font, { + color: Color.rgb(subFontColor), + size: d3.round(subFontSize, 2), + family: subFontFamily, + weight: subFontWeight, + style: subFontStyle, + variant: subFontVariant, + textcase: subFontTextcase, + shadow: subFontShadow, + lineposition: subFontLineposition, + }) + .attr(subtitleAttributes) + .call(svgTextUtils.convertToTspans, gd); + } return Plots.previousPromises(gd); } - function scootTitle(titleElIn) { + function scootTitle(titleAndSubtitleEls) { + var titleElIn = titleAndSubtitleEls.title; var titleGroup = d3.select(titleElIn.node().parentNode); if(avoid && avoid.selection && avoid.side && txt) { @@ -239,12 +339,10 @@ function draw(gd, titleClass, options) { } } - el.call(titleLayout); + el.call(titleLayout, subtitleEl); - function setPlaceholder() { - opacity = 0; - isplaceholder = true; - el.text(placeholder) + function setPlaceholder(element, placeholderText) { + element.text(placeholderText) .on('mouseover.opacity', function() { d3.select(this).transition() .duration(interactConstants.SHOW_PLACEHOLDER).style('opacity', 1); @@ -256,8 +354,10 @@ function draw(gd, titleClass, options) { } if(editable) { - if(!txt) setPlaceholder(); - else el.on('.opacity', null); + if(!txt) { + setPlaceholder(el, placeholder); + titleIsPlaceholder = true; + } else el.on('.opacity', null); el.call(svgTextUtils.makeEditable, {gd: gd}) .on('edit', function(text) { @@ -275,12 +375,43 @@ function draw(gd, titleClass, options) { this.text(d || ' ') .call(svgTextUtils.positionText, attributes.x, attributes.y); }); + + if(subtitleEnabled) { + // Adjust subtitle position now that title placeholder has been added + // Only adjust if subtitle is enabled and title text was originally empty + if(subtitleEnabled && !txt) { + var titleElBbox = el.node().getBBox(); + var subtitleY = titleElBbox.y + titleElBbox.height + (SUBTITLE_PADDING_EM * subFontSize); + subtitleEl.attr('y', subtitleY); + } + + if(!subtitleTxt) { + setPlaceholder(subtitleEl, subtitlePlaceholder); + subtitleIsPlaceholder = true; + } else subtitleEl.on('.opacity', null); + subtitleEl.call(svgTextUtils.makeEditable, {gd: gd}) + .on('edit', function(text) { + Registry.call('_guiRelayout', gd, 'title.subtitle.text', text); + }) + .on('cancel', function() { + this.text(this.attr('data-unformatted')) + .call(titleLayout); + }) + .on('input', function(d) { + this.text(d || ' ') + .call(svgTextUtils.positionText, subtitleEl.attr('x'), subtitleEl.attr('y')); + }); + } } - el.classed('js-placeholder', isplaceholder); + + el.classed('js-placeholder', titleIsPlaceholder); + if(subtitleEl) subtitleEl.classed('js-placeholder', subtitleIsPlaceholder); return group; } module.exports = { - draw: draw + draw: draw, + SUBTITLE_PADDING_EM: SUBTITLE_PADDING_EM, + SUBTITLE_PADDING_MATHJAX_EM: SUBTITLE_PADDING_MATHJAX_EM, }; diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 487decf74f9..295d2b94118 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -408,18 +408,20 @@ exports.drawMainTitle = function(gd) { Titles.draw(gd, 'gtitle', { propContainer: fullLayout, propName: 'title.text', + subtitlePropName: 'title.subtitle.text', placeholder: fullLayout._dfltTitle.plot, + subtitlePlaceholder: fullLayout._dfltTitle.subtitle, attributes: ({ x: x, y: y, 'text-anchor': textAnchor, dy: dy - }) + }), }); if(title.text && title.automargin) { var titleObj = d3.selectAll('.gtitle'); - var titleHeight = Drawing.bBox(titleObj.node()).height; + var titleHeight = Drawing.bBox(d3.selectAll('.g-gtitle').node()).height; var pushMargin = needsMarginPush(gd, title, titleHeight); if(pushMargin > 0) { applyTitleAutoMargin(gd, y, pushMargin, titleHeight); @@ -443,6 +445,21 @@ exports.drawMainTitle = function(gd) { this.setAttribute('dy', newDy); }); } + + // If there is a subtitle + var subtitleObj = d3.selectAll('.gtitle-subtitle'); + if(subtitleObj.node()) { + // Get bottom edge of title bounding box + var titleBB = titleObj.node().getBBox(); + var titleBottom = titleBB.y + titleBB.height; + var subtitleY = titleBottom + Titles.SUBTITLE_PADDING_EM * title.subtitle.font.size; + subtitleObj.attr({ + x: x, + y: subtitleY, + 'text-anchor': textAnchor, + dy: getMainTitleDyAdj(title.yanchor) + }).call(svgTextUtils.positionText, x, subtitleY); + } } } }; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index a0cb4e412d3..0a6fc5543c3 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -41,6 +41,18 @@ module.exports = { 'by the now deprecated `titlefont` attribute.' ].join(' ') }), + subtitle: { + text: { + valType: 'string', + editType: 'layoutstyle', + description: 'Sets the plot\'s subtitle.' + }, + font: fontAttrs({ + editType: 'layoutstyle', + description: 'Sets the subtitle font.' + }), + editType: 'layoutstyle', + }, xref: { valType: 'enumerated', dflt: 'container', diff --git a/src/plots/plots.js b/src/plots/plots.js index e75d2066d83..c1e4c631942 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -316,6 +316,7 @@ plots.supplyDefaults = function(gd, opts) { // When editable=false the two behave the same, no title is drawn. newFullLayout._dfltTitle = { plot: _(gd, 'Click to enter Plot title'), + subtitle: _(gd, 'Click to enter Plot subtitle'), x: _(gd, 'Click to enter X axis title'), y: _(gd, 'Click to enter Y axis title'), colorbar: _(gd, 'Click to enter Colorscale title'), @@ -1494,6 +1495,13 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { coerce('title.y'); coerce('title.yanchor'); + coerce('title.subtitle.text', layoutOut._dfltTitle.subtitle); + Lib.coerceFont(coerce, 'title.subtitle.font', font, { + overrideDflt: { + size: Math.round(layoutOut.title.font.size * 0.7) + } + }); + if(titleAutomargin) { // when automargin=true // title.y is 1 or 0 if paper ref diff --git a/test/image/baselines/mathjax3___ternary-mathjax.png b/test/image/baselines/mathjax3___ternary-mathjax.png index 29267b0f394..43ab8bf8133 100644 Binary files a/test/image/baselines/mathjax3___ternary-mathjax.png and b/test/image/baselines/mathjax3___ternary-mathjax.png differ diff --git a/test/image/baselines/mathjax3___zz-ternary-mathjax-title-place-subtitle.png b/test/image/baselines/mathjax3___zz-ternary-mathjax-title-place-subtitle.png new file mode 100644 index 00000000000..eef6c4e317b Binary files /dev/null and b/test/image/baselines/mathjax3___zz-ternary-mathjax-title-place-subtitle.png differ diff --git a/test/image/baselines/ternary-mathjax.png b/test/image/baselines/ternary-mathjax.png index ad0cebb9730..b416e5dd00a 100644 Binary files a/test/image/baselines/ternary-mathjax.png and b/test/image/baselines/ternary-mathjax.png differ diff --git a/test/image/baselines/zz-subtitle-font-styling.png b/test/image/baselines/zz-subtitle-font-styling.png new file mode 100644 index 00000000000..780e187bd49 Binary files /dev/null and b/test/image/baselines/zz-subtitle-font-styling.png differ diff --git a/test/image/baselines/zz-subtitle.png b/test/image/baselines/zz-subtitle.png new file mode 100644 index 00000000000..aee496a8fe0 Binary files /dev/null and b/test/image/baselines/zz-subtitle.png differ diff --git a/test/image/baselines/zz-ternary-mathjax-title-place-subtitle.png b/test/image/baselines/zz-ternary-mathjax-title-place-subtitle.png new file mode 100644 index 00000000000..bc884352869 Binary files /dev/null and b/test/image/baselines/zz-ternary-mathjax-title-place-subtitle.png differ diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js index 8d0991752aa..714baa6259a 100644 --- a/test/image/compare_pixels_test.js +++ b/test/image/compare_pixels_test.js @@ -84,7 +84,8 @@ if(mathjax3) { 'table_latex_multitrace_scatter', 'table_plain_birds', 'table_wrapped_birds', - 'ternary-mathjax' + 'ternary-mathjax', + 'zz-ternary-mathjax-title-place-subtitle', ]; } diff --git a/test/image/make_baseline.js b/test/image/make_baseline.js index 0666465311b..b9977292d14 100644 --- a/test/image/make_baseline.js +++ b/test/image/make_baseline.js @@ -63,7 +63,8 @@ if(mathjax3) { 'table_latex_multitrace_scatter', 'table_plain_birds', 'table_wrapped_birds', - 'ternary-mathjax' + 'ternary-mathjax', + 'zz-ternary-mathjax-title-place-subtitle' ]; } diff --git a/test/image/mocks/ternary-mathjax.json b/test/image/mocks/ternary-mathjax.json index c6ca4408f5d..311f37e5c5f 100644 --- a/test/image/mocks/ternary-mathjax.json +++ b/test/image/mocks/ternary-mathjax.json @@ -48,16 +48,13 @@ } } }, - "annotations": [ - { - "showarrow": false, - "text": "Simple Ternary Plot with Markers", - "x": 0.5, - "y": 1.3, - "font": { - "size": 15 - } + "width": 600, + "height": 600, + "title": { + "text": "Simple Ternary Plot with Markers", + "subtitle": { + "text": "$y = ax^2 + bx + c$" } - ] + } } } diff --git a/test/image/mocks/zz-subtitle-font-styling.json b/test/image/mocks/zz-subtitle-font-styling.json new file mode 100644 index 00000000000..c94959dc191 --- /dev/null +++ b/test/image/mocks/zz-subtitle-font-styling.json @@ -0,0 +1,25 @@ +{ + "data": [ + { + "x": [1, 2, 3, 4], + "y": [10, 12, 11, 13], + "type": "scatter" + } + ], + "layout": { + "title": { + "text": "Main title
2nd line", + "subtitle": { + "font": { + "weight": "bold", + "style": "italic", + "variant": "small-caps", + "textcase": "word caps", + "lineposition": "under", + "shadow": "2px 2px 4px yellow" + }, + "text": "subtitle
2nd line" + } + } + } +} diff --git a/test/image/mocks/zz-subtitle.json b/test/image/mocks/zz-subtitle.json new file mode 100644 index 00000000000..07cdd9a5bf5 --- /dev/null +++ b/test/image/mocks/zz-subtitle.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "x": [1, 2, 3, 4], + "y": [10, 12, 11, 13], + "type": "scatter" + } + ], + "layout": { + "title": { + "text": "A simple plot", + "subtitle": { + "text": "With a subtitle" + } + }, + "xaxis": { + "title": { + "text": "x-axis" + } + }, + "yaxis": { + "title": { + "text": "y-axis" + } + } + } +} diff --git a/test/image/mocks/zz-ternary-mathjax-title-place-subtitle.json b/test/image/mocks/zz-ternary-mathjax-title-place-subtitle.json new file mode 100644 index 00000000000..f33d2ca2b44 --- /dev/null +++ b/test/image/mocks/zz-ternary-mathjax-title-place-subtitle.json @@ -0,0 +1,60 @@ +{ + "data": [ + { + "type": "scatterternary", + "mode": "markers", + "a": [75], + "b": [25], + "c": [0], + "text": ["point 1"], + "marker": { + "symbol": 100, + "color": "#DB7365", + "size": 14, + "line": { + "width": 2 + } + } + } + ], + "layout": { + "ternary": { + "sum": 100, + "aaxis": { + "title": { + "text": "$A^2$" + }, + "showline": true, + "showgrid": true + }, + "baxis": { + "title": { + "text": "$B^2$" + }, + "tickprefix": "$\\sqrt", + "ticksuffix": "^2$", + "showline": true, + "showgrid": true + }, + "caxis": { + "title": { + "text": "$C^2$" + }, + "showline": true, + "showgrid": true, + "labelalias": { + "20": "$(\\sqrt20)^2$", + "60": "Sixty" + } + } + }, + "width": 600, + "height": 600, + "title": { + "subtitle": { + "text": "Simple Ternary Plot with Markers" + }, + "text": "$y = ax^2 + bx + c$" + } + } +} diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 272994d2d54..4c7b45919f1 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -385,6 +385,12 @@ describe('config argument', function() { .then(done, done.fail); }); + it('should make subtitles editable', function(done) { + initPlot('titleText') + .then(checkIfEditable('gtitle-subtitle', 'Click to enter Plot subtitle')) + .then(done, done.fail); + }); + it('should make x axes labels editable', function(done) { initPlot('axisTitleText') .then(checkIfEditable('g-xtitle', 'Click to enter X axis title')) diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js index 15840446a5e..23685e64ff6 100644 --- a/test/jasmine/tests/plot_api_react_test.js +++ b/test/jasmine/tests/plot_api_react_test.js @@ -417,6 +417,7 @@ describe('@noCIdep Plotly.react', function() { .then(function() { expect(d3SelectAll('.drag').size()).toBe(11); expect(d3SelectAll('.gtitle').text()).toBe('Click to enter Plot title'); + expect(d3SelectAll('.gtitle-subtitle').text()).toBe('Click to enter Plot subtitle'); countCalls({plot: 1}); return Plotly.react(gd, data, layout, {staticPlot: true}); @@ -1988,7 +1989,7 @@ describe('Plotly.react and uirevision attributes', function() { function editEditable() { return Registry.call('_guiUpdate', gd, {'colorbar.x': 0.8, 'colorbar.y': 0.6}, - {'title.text': 'yep', 'legend.x': 1.1, 'legend.y': 0.9}, + {'title.text': 'yep', 'title.subtitle.text': 'hey', 'legend.x': 1.1, 'legend.y': 0.9}, [2] ); } @@ -1999,6 +2000,7 @@ describe('Plotly.react and uirevision attributes', function() { 'colorbar.y': original ? [undefined, 0.5] : 0.6 }], { 'title.text': original ? [undefined, 'Click to enter Plot title'] : 'yep', + 'title.subtitle.text': original ? [undefined, 'Click to enter Plot subtitle'] : 'hey', 'legend.x': original ? [undefined, 1.02] : 1.1, 'legend.y': original ? [undefined, 1] : 0.9 }); diff --git a/test/jasmine/tests/titles_test.js b/test/jasmine/tests/titles_test.js index 75ac84a30b8..1f2dcdaecd9 100644 --- a/test/jasmine/tests/titles_test.js +++ b/test/jasmine/tests/titles_test.js @@ -1312,8 +1312,8 @@ describe('Editable titles', function() { gd = createGraphDiv(); }); - function checkTitle(letter, text, opacityOut, opacityIn) { - var titleEl = d3Select('.' + letter + 'title'); + function checkTitle(className, text, opacityOut, opacityIn) { + var titleEl = d3Select('.' + className); expect(titleEl.text()).toBe(text); expect(+(titleEl.node().style.opacity || 1)).toBe(opacityOut); @@ -1328,6 +1328,7 @@ describe('Editable titles', function() { expect(+(titleEl.node().style.opacity || 1)).toBe(opacityIn); mouseEvent('mouseout', xCenter, yCenter); + console.log({ titleEl_opacity: titleEl.node().style.opacity, className: className }); setTimeout(function() { expect(+(titleEl.node().style.opacity || 1)).toBe(opacityOut); done(); @@ -1337,14 +1338,15 @@ describe('Editable titles', function() { return promise; } - function editTitle(letter, attr, text) { + function editTitle(className, attr, text) { return new Promise(function(resolve) { gd.once('plotly_relayout', function(eventData) { - expect(eventData[attr]).toEqual(text, [letter, attr, eventData]); + expect(eventData[attr]).toEqual(text, [className, attr, eventData]); + console.log(eventData[attr]); setTimeout(resolve, 10); }); - var textNode = document.querySelector('.' + letter + 'title'); + var textNode = document.querySelector('.' + className); textNode.dispatchEvent(new window.MouseEvent('click')); var editNode = document.querySelector('.plugin-editable.editable'); @@ -1359,13 +1361,14 @@ describe('Editable titles', function() { Plotly.newPlot(gd, data, {}, {editable: true}) .then(function() { return Promise.all([ - // Check all three titles in parallel. This only works because + // Check all four titles in parallel. This only works because // we're using synthetic events, not a real mouse. It's a big // win though because the test takes 1.2 seconds with the // animations... - checkTitle('x', 'Click to enter X axis title', 0.2, 0.2), - checkTitle('y', 'Click to enter Y axis title', 0.2, 0.2), - checkTitle('g', 'Click to enter Plot title', 0.2, 0.2) + checkTitle('xtitle', 'Click to enter X axis title', 0.2, 0.2), + checkTitle('ytitle', 'Click to enter Y axis title', 0.2, 0.2), + checkTitle('gtitle', 'Click to enter Plot title', 0.2, 0.2), + checkTitle('gtitle-subtitle', 'Click to enter Plot subtitle', 0.2, 0.2) ]); }) .then(done, done.fail); @@ -1375,13 +1378,14 @@ describe('Editable titles', function() { Plotly.newPlot(gd, data, { xaxis: {title: {text: ''}}, yaxis: {title: {text: ''}}, - title: {text: ''} + title: { text: '', subtitle: { text: ''}}, }, {editable: true}) .then(function() { return Promise.all([ - checkTitle('x', 'Click to enter X axis title', 0, 1), - checkTitle('y', 'Click to enter Y axis title', 0, 1), - checkTitle('g', 'Click to enter Plot title', 0, 1) + checkTitle('xtitle', 'Click to enter X axis title', 0, 1), + checkTitle('ytitle', 'Click to enter Y axis title', 0, 1), + checkTitle('gtitle', 'Click to enter Plot title', 0, 1), + checkTitle('gtitle-subtitle', 'Click to enter Plot subtitle', 0, 1) ]); }) .then(done, done.fail); @@ -1394,19 +1398,23 @@ describe('Editable titles', function() { title: {text: ''} }, {editable: true}) .then(function() { - return editTitle('x', 'xaxis.title.text', 'XXX'); + return editTitle('xtitle', 'xaxis.title.text', 'XXX'); + }) + .then(function() { + return editTitle('ytitle', 'yaxis.title.text', 'YYY'); }) .then(function() { - return editTitle('y', 'yaxis.title.text', 'YYY'); + return editTitle('gtitle', 'title.text', 'TTT'); }) .then(function() { - return editTitle('g', 'title.text', 'TTT'); + return editTitle('gtitle-subtitle', 'title.subtitle.text', 'SSS'); }) .then(function() { return Promise.all([ - checkTitle('x', 'XXX', 1, 1), - checkTitle('y', 'YYY', 1, 1), - checkTitle('g', 'TTT', 1, 1) + checkTitle('xtitle', 'XXX', 1, 1), + checkTitle('ytitle', 'YYY', 1, 1), + checkTitle('gtitle', 'TTT', 1, 1), + checkTitle('gtitle-subtitle', 'SSS', 1, 1) ]); }) .then(done, done.fail); diff --git a/test/plot-schema.json b/test/plot-schema.json index fd2b1659e37..3e5704237d6 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -13273,6 +13273,104 @@ } }, "role": "object", + "subtitle": { + "editType": "layoutstyle", + "font": { + "color": { + "editType": "layoutstyle", + "valType": "color" + }, + "description": "Sets the subtitle font.", + "editType": "layoutstyle", + "family": { + "description": "HTML font family - the typeface that will be applied by the web browser. The web browser will only be able to apply a font if it is available on the system which it operates. Provide multiple font families, separated by commas, to indicate the preference in which to apply fonts if they aren't available on the system. The Chart Studio Cloud (at https://chart-studio.plotly.com or on-premise) generates images on a server, where only a select number of fonts are installed and supported. These include *Arial*, *Balto*, *Courier New*, *Droid Sans*,, *Droid Serif*, *Droid Sans Mono*, *Gravitas One*, *Old Standard TT*, *Open Sans*, *Overpass*, *PT Sans Narrow*, *Raleway*, *Times New Roman*.", + "editType": "layoutstyle", + "noBlank": true, + "strict": true, + "valType": "string" + }, + "lineposition": { + "description": "Sets the kind of decoration line(s) with text, such as an *under*, *over* or *through* as well as combinations e.g. *under+over*, etc.", + "dflt": "none", + "editType": "layoutstyle", + "extras": [ + "none" + ], + "flags": [ + "under", + "over", + "through" + ], + "valType": "flaglist" + }, + "role": "object", + "shadow": { + "description": "Sets the shape and color of the shadow behind text. *auto* places minimal shadow and applies contrast text font color. See https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow for additional options.", + "dflt": "none", + "editType": "layoutstyle", + "valType": "string" + }, + "size": { + "editType": "layoutstyle", + "min": 1, + "valType": "number" + }, + "style": { + "description": "Sets whether a font should be styled with a normal or italic face from its family.", + "dflt": "normal", + "editType": "layoutstyle", + "valType": "enumerated", + "values": [ + "normal", + "italic" + ] + }, + "textcase": { + "description": "Sets capitalization of text. It can be used to make text appear in all-uppercase or all-lowercase, or with each word capitalized.", + "dflt": "normal", + "editType": "layoutstyle", + "valType": "enumerated", + "values": [ + "normal", + "word caps", + "upper", + "lower" + ] + }, + "variant": { + "description": "Sets the variant of the font.", + "dflt": "normal", + "editType": "layoutstyle", + "valType": "enumerated", + "values": [ + "normal", + "small-caps", + "all-small-caps", + "all-petite-caps", + "petite-caps", + "unicase" + ] + }, + "weight": { + "description": "Sets the weight (or boldness) of the font.", + "dflt": "normal", + "editType": "layoutstyle", + "extras": [ + "normal", + "bold" + ], + "max": 1000, + "min": 1, + "valType": "integer" + } + }, + "role": "object", + "text": { + "description": "Sets the plot's subtitle.", + "editType": "layoutstyle", + "valType": "string" + } + }, "text": { "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.", "editType": "layoutstyle",