Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subtitle to plots #7012

Merged
merged 51 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
24ed9c9
add subtitle property and simple prototype
emilykl May 22, 2024
7469d8e
add simple subtitle mock
emilykl May 22, 2024
3afc2e7
coerce step for subtitle
emilykl May 22, 2024
13f8b2a
fully-featured subtitles with plot options
emilykl Jun 4, 2024
cf5eb51
add subtitle mock
emilykl Jun 4, 2024
4a55ff2
remove old mock
emilykl Jun 5, 2024
625ea53
fix subtitle placement when title is mathjax
emilykl Jun 6, 2024
972836e
small cleanup of mathjax logic
emilykl Jun 7, 2024
f6e9781
add mock testing font options with subtitle
emilykl Jun 7, 2024
35a6599
streamline placeholder logic
emilykl Jun 17, 2024
60a6287
add subtitle to titles tests
emilykl Jun 17, 2024
7eb9e52
fix small bug in subtitle logic
emilykl Jun 17, 2024
7e9fcf9
syntax
emilykl Jun 17, 2024
3190382
re-add removed comment
emilykl Jun 17, 2024
608740f
fix title positioning
emilykl Jun 17, 2024
183518c
french and spanish
emilykl Jun 17, 2024
2452a48
add config test for editable subtitle
emilykl Jun 17, 2024
8bce81b
add plot api react test for subtitle
emilykl Jun 17, 2024
2aca950
add image baselines for subtitle
emilykl Jun 17, 2024
ade53d3
update plot-schema
emilykl Jun 17, 2024
13af030
mark restyle radial axis title test as flaky
emilykl Jun 18, 2024
b2a26e0
Merge branch 'master' into add-subtitle
emilykl Jun 18, 2024
2790ec7
add draftlog
emilykl Jun 18, 2024
03638ee
add mathjax subtitle mock; add addl padding bw sutbtitle and title wh…
emilykl Jun 18, 2024
8210b58
fix bug with colorbar title
emilykl Jun 18, 2024
496ccc7
fix placeholder positioning bug
emilykl Jun 19, 2024
15754fa
remove zz-subtitle-mathjax mock
archmoj Jun 26, 2024
f2cbc23
add mathjax subtitle and title to ternary-mathjax
archmoj Jun 26, 2024
f3842e0
test positioning of subtitle when title is mathjax
archmoj Jun 26, 2024
e3813f2
update draftlog
emilykl Jun 27, 2024
e70e521
fix subtitle position after editing when title is MathJax
emilykl Jun 28, 2024
e9e8adb
syntax
emilykl Jun 28, 2024
0d154bb
fix error when no subtitle
emilykl Jun 28, 2024
2aa2b8f
syntax
emilykl Jul 8, 2024
527ff8d
change Object.assign to Lib.extendFlat
emilykl Jul 8, 2024
28d30ee
use `title` and `subtitle` keys instead of `t` and `st`
emilykl Jul 8, 2024
aa7b0a6
remove extra newline
emilykl Jul 8, 2024
db5de4d
set subtitle font size based on title font size
emilykl Jul 8, 2024
154f481
Merge branch 'add-subtitle' of https://github.com/plotly/plotly.js in…
emilykl Jul 8, 2024
6701eee
change subtitle font size multiplier to 0.7
emilykl Jul 8, 2024
313301c
change plotsubtitle key to subtitle
emilykl Jul 8, 2024
423dfd3
streamline logic
emilykl Jul 9, 2024
cef06c8
Update src/plot_api/subroutines.js
emilykl Jul 9, 2024
7cc019d
adjust mathjax padding multiplier
emilykl Jul 9, 2024
b7cda0b
render subtitle even if title is not present
emilykl Jul 9, 2024
ec166a3
better method of determining subtitle position
emilykl Jul 11, 2024
8e44892
update mathjax baselines
emilykl Jul 11, 2024
e189245
bbox bug
emilykl Jul 11, 2024
de628de
update baselines
emilykl Jul 11, 2024
47a5b82
take full title+subtitle height into account for automargin
emilykl Jul 17, 2024
b8942ee
improved subtitle automargin positioning
emilykl Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;

Expand Down
1 change: 1 addition & 0 deletions draftlogs/7012_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `subtitle` attribute to `layout.title` to enable adding subtitles to plots [[#7012](https://github.com/plotly/plotly.js/pull/7012)]
1 change: 1 addition & 0 deletions lib/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
emilykl marked this conversation as resolved.
Show resolved Hide resolved
'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',
Expand Down
1 change: 1 addition & 0 deletions lib/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
181 changes: 155 additions & 26 deletions src/components/titles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ var interactConstants = require('../../constants/interactions');

var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE;
var numStripRE = / [XY][0-9]* /;
var MATHJAX_PADDING_MULTIPLIER = 0.85;
var EXTRA_SPACING_BETWEEN_TITLE_AND_SUBTITLE = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I kind of like the current positioning of subtitle which is compact;
I was wondering the correct value for this constant might be 1.3

LINE_SPACING: 1.3,

This is why:
If I call

Plotly.newPlot(gd, [{y: [1, 2]}], {title: {text: 'title', subtitle: {text: 'subtittle'}}});

the subtitle render closer compared to when I call

Plotly.newPlot(gd, [{y: [1, 2]}], {title: {text: 'title<br><sub>subtittle</sub>'}}); 

So perhaps you could try using (LINE_SPACING - 1) * title.font.size in your calculations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@archmoj Do you think the subtitle looks better with additional spacing?

I'd prefer not to change the spacing just to match the appearance of <sub>, since that spacing is arbitrary.

I prefer the more compact look.


/**
* Titles - (re)draw titles on the axes and plot:
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
archmoj marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand All @@ -100,15 +136,15 @@ function draw(gd, titleClass, options) {
txt = Lib.templateString(txt, fullLayout._meta);
}

var elShouldExist = txt || editable;
var elShouldExist = txt || subtitleTxt || editable;

var hColorbarMoveTitle;
if(!group) {
group = Lib.ensureSingle(fullLayout._infolayer, 'g', 'g-' + titleClass);
hColorbarMoveTitle = fullLayout._hColorbarMoveTitle;
}

var el = group.selectAll('text')
var el = group.selectAll('text.' + titleClass)
.data(elShouldExist ? [0] : []);
el.enter().append('text');
el.text(txt)
Expand All @@ -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) {
Expand All @@ -147,6 +199,24 @@ 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 titleMathHeight = titleElMathGroup.node().getBBox().height;
if(titleMathHeight) {
// Increase the y position of the subtitle by the height of the title,
// plus a bit of padding
var newSubtitleY = Number(subtitleElement.attr('y')) + titleMathHeight + MATHJAX_PADDING_MULTIPLIER * subFontSize + EXTRA_SPACING_BETWEEN_TITLE_AND_SUBTITLE;
subtitleElement.attr('y', newSubtitleY);
}
}
}

titleEl.style('opacity', opacity * Color.opacity(fontColor))
.call(Drawing.font, {
color: Color.rgb(fontColor),
Expand All @@ -160,12 +230,42 @@ function draw(gd, titleClass, options) {
lineposition: fontLineposition,
})
.attr(attributes)
.call(svgTextUtils.convertToTspans, gd);
.call(svgTextUtils.convertToTspans, gd, adjustSubtitlePosition);

if(subtitleEl) {
// Increase the subtitle y position so that it is drawn below the subtitle
// We need to check the height of the MathJax group as well, in case the MathJax
// has already rendered
var titleElHeight = titleEl.node().getBBox().height;
var titleElMathGroup = group.select('.' + titleClass + '-math-group');
var titleElMathHeight = titleElMathGroup.node() ? titleElMathGroup.node().getBBox().height : 0;
var subtitleShift = titleElMathHeight ? titleElMathHeight + (MATHJAX_PADDING_MULTIPLIER * subFontSize) : titleElHeight;
var subtitleAttributes = Lib.extendFlat({}, attributes, {
y: subtitleShift + EXTRA_SPACING_BETWEEN_TITLE_AND_SUBTITLE + attributes.y
});

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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -275,8 +375,37 @@ 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 ht = Drawing.bBox(el.node()).height;
var newSubtitleY = Number(subtitleEl.attr('y')) + ht + EXTRA_SPACING_BETWEEN_TITLE_AND_SUBTITLE;
subtitleEl.attr('y', newSubtitleY);
}

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'));
});
emilykl marked this conversation as resolved.
Show resolved Hide resolved
}
}
el.classed('js-placeholder', isplaceholder);

el.classed('js-placeholder', titleIsPlaceholder);
if(subtitleEl) subtitleEl.classed('js-placeholder', subtitleIsPlaceholder);

return group;
}
Expand Down
4 changes: 3 additions & 1 deletion src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,15 @@ 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) {
Expand Down
12 changes: 12 additions & 0 deletions src/plots/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
}),
emilykl marked this conversation as resolved.
Show resolved Hide resolved
editType: 'layoutstyle',
},
xref: {
valType: 'enumerated',
dflt: 'container',
Expand Down
8 changes: 8 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand Down
Binary file modified test/image/baselines/mathjax3___ternary-mathjax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/ternary-mathjax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/zz-subtitle-font-styling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/zz-subtitle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion test/image/compare_pixels_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
}

Expand Down
3 changes: 2 additions & 1 deletion test/image/make_baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
];
}

Expand Down
Loading