Open eugenesvk opened 8 years ago
Unfortunately, there isn't this option built into plotly.js at the moment. The text labels are created using SVG elements, or in WebGL where text-reflow is done manually and it's not quite as easy just constraining the width.
This likely won't be of high priority for us in the immediate future, but we're always open to pull requests and are happy to help on them!
Ok, understood. Would help with a PR if I had a clue how to do that :)
Pardon my ignorance, but is there no way to introduce a JavaScript function that would be applied to all labels before they're fed to SVG/WebGL, where it becomes complicated? For example, I've google-found this function (it's not great since there is an issue with long words, but just as an illustration)
function stringDivider(str, width, spaceReplacer) {
if (str.length>width) {
var p=width
for (;p>0 && str[p]!=' ';p--) {
}
if (p>0) {
var left = str.substring(0, p);
var right = str.substring(p+1);
return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
}
}
return str;
}
that I can use internally to shorten the labels
var label = "Very Long Text Label That Doesn't Fit Properly"
x: ['giraffes', 'orangutans', stringDivider( label, 20, "<br>") ],
Also, as a side question — is there a way to read the bar width in plotly so I don't have to adjust the width manually and can make it reflow properly when chart size is adjusted.
Any update on this?
@mdtusz I'm happy to try to help with this if you're able to point me in the right direction in the codebase.
https://github.com/plotly/plotly.js/pull/1834 should see some foundation work on this.
A workaround is to line break svg elements after rendering. This assumes that there are <br/>
tags in the text, and splits text elements into multiple text elements.
var insertLinebreaks = function (d) {
if (typeof d.text === 'string') {
var words = d.text.split(/<br[/]?>/);
var el = d3.select(this);
el.text('')
for (var i = 0; i < words.length; i++) {
var tspan = el.append('tspan').text(words[i]);
if (i > 0)
tspan.attr('x', 0).attr('dy', '15');
}
}
};
Then it's a matter of selecting svg text elements and calling the above function:
d3.select('#c_Q1 svg').selectAll('g text').each(insertLinebreaks);
You can get fancier with the selector so line break just axis labels for instance.
It could also be used in conjunction with above method by @eugenesvk to evaluate long strings and insert such line break markers.
Has the plotly library managed to solve this issue now. Looking for some better alternatives.
I also wanted to know if Plotly has solved for this yet - this discussion has been going on since 2016. Adding
s manually doesn't make sense - most of us are probably grabbing data from an API.
I'd also like to know whether Plotly intends to solve this. Thanks!
Wondering if using overflow
CSS could become helpful?
See https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
whether Plotly intends to solve this
No one from our team is actively working on this but we would happily work with anyone who wants to help :)
whether Plotly intends to solve this
No one from our team is actively working on this but we would happily work with anyone who wants to help :)
Which way to go: #5700 or #2053 ?
still no updates on this right? cause it would be suuuuper clutch if this there was a way to wrap labels. I'm working with some long, dynamic violin plot labels and it would be so awesome if this feature were addressed
@craragon77 SVG doesn't have line wrap / truncation as native features I think , so the code needs to explicitly break/wrap/recenter or truncate text. Below is code that selects and breaks or truncates text in a Plotly chart after rendering.
The ideal solution would be for someone to package and test this nicely for a pull request to Plotly, but for expedience we just select break the text ourselves:
breakText: function ($el, chartOptions) {
var self = this;
var svg = $("svg", $el)
if (svg && svg.length > 0) {
var xbreaks = 0;
d3.selectAll(svg).each(function () {
if (chartOptions.wrap) {
svgLineBreaks.breakElements(this, 'g.xtick text', {
maxLineLength: _.get(chartOptions, 'xaxis.maxLabelLength') || 12,
verticalAlign: ""
})
svgLineBreaks.breakElements(this, 'g.xtick2 text', {
maxLineLength: _.get(chartOptions, 'xaxis.maxLabelLength') || 12,
verticalAlign: ""
})
svgLineBreaks.breakElements(this, 'g.ytick text', {
maxLineLength: _.get(chartOptions, 'yaxis.maxLabelLength') || 50,
verticalAlign: "middle"
})
svgLineBreaks.breakElements(this, 'g.ytick2 text', {
maxLineLength: _.get(chartOptions, 'yaxis.maxLabelLength') || 50,
verticalAlign: "middle"
})
}
else { /// truncate
d3.select(this).selectAll('g.xtick text').each(svgLineBreaks.truncateElement(_.get(chartOptions, 'xaxis.maxLabelLength') || 12));
d3.select(this).selectAll('g.ytick text').each(svgLineBreaks.truncateElement(_.get(chartOptions, 'yaxis.maxLabelLength') || 50));
d3.select(this).selectAll('g.ytick2 text').each(svgLineBreaks.truncateElement(_.get(chartOptions, 'yaxis.maxLabelLength') || 50));
d3.select(this).selectAll('g.xtick2 text').each(svgLineBreaks.truncateElement(_.get(chartOptions, 'xaxis.maxLabelLength') || 50));
}
d3.select(this).selectAll('text.legendtext').each(svgLineBreaks.truncateElement(_.get(chartOptions, 'legend.maxLabelLength') || 50))
d3.select(this).selectAll('g.xtick2 text').each(function () {
var el = d3.select(this)
el.attr('dy', 20)
})
})
}
}
const _ = require('lodash')
module.exports = {
breakStr: function (text, maxLineLength, regexForceBreak, regexSuggestBreak) {
const resultArr = [];
maxLineLength = maxLineLength || 12
regexForceBreak = regexForceBreak || /<br[/]?>|\n|\&/g
regexSuggestBreak = /\s/g
function m(str) {
return !!(str && str.trim())
}
var splitStr = text.split(regexForceBreak);
splitStr.forEach(function (str) {
if (str.length <= maxLineLength) {
resultArr.push(str);
} else {
var tempStr = str;
while (tempStr) {
var suggestedBreakPoints = []
var convenientBreakPoint = Infinity
var match, fragment;
while ((match = regexSuggestBreak.exec(tempStr)) != null) {
suggestedBreakPoints.push(match.index + 1)
if (match.index <= maxLineLength) convenientBreakPoint = match.index;
}
if (tempStr.trim() && tempStr.length <= maxLineLength) {
fragment = tempStr
resultArr.push(fragment)
tempStr = ''
}
else if ((convenientBreakPoint <= maxLineLength) && (maxLineLength > maxLineLength - 5)) {
fragment = tempStr.substr(0, convenientBreakPoint)
resultArr.push(fragment)
tempStr = tempStr.substr(convenientBreakPoint + 1);
}
else {
fragment = tempStr.substr(0, maxLineLength) + "–"
resultArr.push(fragment)
tempStr = tempStr.substr(maxLineLength);
}
}
}
});
return resultArr;
},
/**
* @method svgLineBreaks
* @returns <function(d)> Returns a function that breaks an SVG text element into multiple lines
* @param maxLineLength Maximum number of lines before truncating with ellipses
* @param regexForceBreak Regex to suggest places to always, currently •|<br>|<br/>|\n
* @param regexSuggestBreak Regex to suggest preferred breaks, currently <wbr>
* @param verticalAlign e.g. 'middle' to optionally center mutiple lines at same ypos as original text
* @example svg.selectAll('g.x.axis g text').each(svgLineBreaks);
*/
breakElement: function (maxLineLength, regexForceBreak, regexSuggestBreak, verticalAlign) {
maxLineLength = maxLineLength || 12
regexForceBreak = regexForceBreak || /<br[/]?>|•|\n/ig
regexSuggestBreak = regexSuggestBreak || /•|<wbr>/ig
return function (d) {
var el = d3.select(this);
var bbox = el.node().getBBox()
var height = bbox.height
height = height ||16.5 //hack as Plotly might draw before rendered on screen, so height zero; this is just a guess
var result = {
el: this,
dy: 0,
lines: []
}
var text = el.text()
var total_dy = 0;
if (text) {
var lines = this_module.breakStr(text, maxLineLength, regexForceBreak, regexSuggestBreak)
result.lines = lines
for (var i = 0; i < lines.length; i++) {
if (i == 0) {
el.text(lines[0])
}
else {
var tspan = el.append('tspan')
tspan.text(lines[i]);
tspan.attr('x', el.attr('x') || 0).attr('dy', height);
total_dy += height
}
}
if (verticalAlign == 'middle') {
el.attr('y', el.attr('y') - (lines.length - 1) * height / 2)
}
}
result.dy = total_dy
return result
}
},
/**
* maxLineLength, regexForceBreak, regexSuggestBreak, adjust
* @param d3el
* @param selector
* @param options.maxLineLength
* @param options.regexForceBreak
* @param options.regexSuggestBreak
* @param options.position "middle"
*/
breakElements: function (d3el, selector, options) {
var self = this;
options = options || {}
var breaker = self.breakElement(options.maxLineLength || 12, options.regexForceBreak, options.regexSuggestBreak, options.verticalAlign)
var max_total_dy = 0
d3.select(d3el).selectAll(selector).each(function (d, i) {
var result = breaker.apply(this, arguments)
if (i > 0 && (result.dy > max_total_dy)) {
max_total_dy = result.dy
}
})
},
truncateElement: function (maxLineLength) {
return function (d) {
var el = d3.select(this)
var text = el.text()
if (text && text.length > maxLineLength) {
text = (text.substr(0, maxLineLength) + "…")
}
el.text(text)
}
}
}
whether Plotly intends to solve this
No one from our team is actively working on this but we would happily work with anyone who wants to help :)
Wow, I came back to plotly after a year and I'm surprised to see this.
It looks SO unprofessional to use any sort of labeling in plotly. It appears to others that there's a bug and I did something wrong, so I don't know why this isn't a bigger issue.
It's enough to have made me use Tableau Public instead - and the entire advantage of plotly is customizability, so this has been a frustrating issue to rediscover several hours into making the plot
Charting seems so simple at first but the diversity of users, use cases, and edge cases is wild. And word break / line wrapping specifically is surprisingly nuanced.
Plotly is an open source library, so theoretically we can add features we need such as this. Something that works for us is posted above, but the gap between 'works for us' and 'works for many' is non trivial. If any one has the skills and interest to create a good patch and/or get a PR accepted for this issue, we'll contribute $.
Charting seems so simple at first but the diversity of users, use cases, and edge cases is wild. And word break / line wrapping specifically is surprisingly nuanced.
I definitely understand that considering the number of tools I've looked at for what I thought was relatively simple (horizontal timeline with groups and text wrapping that my dad could edit), but of course, these things never are! I can't
This is surprising to me because I don't think it's an edge case at all. It's essential to being able to label data on the graph - it's not presentable if you have overlapping labels/no linebreaks/uneven text sizes (although that's fixed I think), etc.
And labeling data is essential for communication.
It took a very long time and a lot of manual screenshotting for a non-web dev to make this chart look presentable with Plotly in this Dash App!
I would say I spent at least 30% of my time with Plotly trying to get labels to look presentable, and it's why I no longer use the library if I can possibly help it
I no longer work at Plotly or have any affiliation with them, so this is not an "official" response, but was a core maintainer when this issue was originally opened.
The challenge is that there's not actually a reasonable solution to the problem that will be correct in all cases. Text wrapping in the charts is tricky because there are so many variables that are difficult to actually control for - fonts, font sizes, browsers, zoom level, etc. all contribute to the actual width of a given block of text. While yes it is a challenge to format text with long labels, it's par for the course with what plotly attempts to achieve - being an extensible and flexible library which doesn't really get in the way of making nearly any chart or graph visualization. There's alternatives on either side of its niche - D3 if you want to go lower level, and things like Tableau or Highcharts if you want to go higher level, but those come with their own set of compromises you'll have to adapt to.
I don't imagine this issue will ever be resolved, as text wrapping simply isn't something that plotly.js is positioned for to handle automatically, nor should it (IMO) - it would be like asking React to automatically do the layout of your webapp. Frankly, I'm surprised I still get notifications about this issue more than 6 years later and it hasn't been closed yet :laughing:
All this to say I would love to be proven wrong and see a community contribution which does implement a robust solution. PR's are always welcome :)
Workaround example for spacing and wrapping titles on subplots:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from textwrap import wrap
def wrap_titles(title_for_wrapping):
wrappedTitle = "<br>".join(wrap(title_for_wrapping, width=50))
return wrappedTitle
fig = make_subplots(
rows=2, cols=2,
specs=[[{"type": "barpolar"}, {"type": "barpolar"}],
[{"type": "barpolar"}, {"type": "barpolar"}]],
subplot_titles=[wrap_titles(plt1_tuple[1]), wrap_titles(plt2_tuple[1]), wrap_titles(plt3_tuple[1]), wrap_titles(plt4_tuple[1])]
)
...
(fig.add_trace code)
...
fig.update_layout(height=700, showlegend=False)
# adjust titles of subplots
for annotation in fig['layout']['annotations']:
annotation['height']=60
fig.show()
If you are using Python and pandas you can use wrap() to wrap long text labels. Here is some example code:
import pandas as pd
import plotly.express as px
# these sentences are much too long to use them as labels
sentences = [
"I no longer work at Plotly or have any affiliation with them, so this is not an \"official\" response, but was a core maintainer when this issue was originally opened.",
"The challenge is that there's not actually a reasonable solution to the problem that will be correct in all cases.",
"Text wrapping in the charts is tricky because there are so many variables that are difficult to actually control for - fonts, font sizes, browsers, zoom level, etc. all contribute to the actual width of a given block of text.",
"While yes it is a challenge to format text with long labels, it's par for the course with what plotly attempts to achieve - being an extensible and flexible library which doesn't really get in the way of making nearly any chart or graph visualization."
]
df = pd.DataFrame([(text, len(text)) for text in sentences], columns=["text", "length"])
# Here we wrap the text to 20 columns and a maximum of three lines
df["text_wrapped"] = df["text"].str.wrap(width=20, max_lines=3, placeholder = " ... ")
fig = px.bar(df, x="length", y="text_wrapped", orientation="h", hover_data={"text_wrapped":False, "text":True}, text="length")
fig.update_layout(margin_pad=10, template="ggplot2")
fig.update_yaxes(title = "text", categoryorder='total ascending')
fig.show()
The output looks like The untruncated text can be seen while hovering over the bar chart. It would be nicer to show the untruncated text while hovering over the axis labels. Unfortunately hovers over the axis labels don't seem to be possible in plotly.
It looks like one could make use of foreignObject
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject to implement this feature.
To my knowledge foreignObject does not work outside browsers - so this would break SVG export
To my knowledge foreignObject does not work outside browsers - so this would break SVG export
Thanks @alexcjohnson for the note.
I was also thinking about it.
If the PNG
export works, this may still be a good option. No?
@alexcjohnson Update: It looks like using foreignObject
is not a good option as it could introduce security issues.
Is there an option to reflow long text labels?
<br>
in the code that I'd like to have Plotly be able to do with a simplewrap: true
optionJS code for this chart