-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add subtitle to plots #7012
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
Changes from 45 commits
24ed9c9
7469d8e
3afc2e7
13f8b2a
cf5eb51
4a55ff2
625ea53
972836e
f6e9781
35a6599
60a6287
7eb9e52
7e9fcf9
3190382
608740f
183518c
2452a48
8bce81b
2aca950
ade53d3
13af030
b2a26e0
2790ec7
03638ee
8210b58
496ccc7
15754fa
f2cbc23
f3842e0
e3813f2
e70e521
e9e8adb
0d154bb
2aa2b8f
527ff8d
28d30ee
aa7b0a6
db5de4d
154f481
6701eee
313301c
423dfd3
cef06c8
7cc019d
b7cda0b
ec166a3
8e44892
e189245
de628de
47a5b82
b8942ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)] |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; plotly.js/src/constants/alignment.js Line 34 in 8b1f5bd
This is why: 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I prefer the more compact look. |
||||
|
||||
/** | ||||
* 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) { | ||||
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) { | ||||
|
@@ -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) | ||||
|
@@ -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,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), | ||||
|
@@ -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) { | ||||
|
@@ -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,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; | ||||
} | ||||
|
Uh oh!
There was an error while loading. Please reload this page.