Skip to content

Horizontal legend wrapping is broken when legendgroups are used #1913

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

Closed
tripped opened this issue Jul 25, 2017 · 12 comments
Closed

Horizontal legend wrapping is broken when legendgroups are used #1913

tripped opened this issue Jul 25, 2017 · 12 comments
Assignees
Labels
bug something broken

Comments

@tripped
Copy link

tripped commented Jul 25, 2017

jsfiddle repro here: https://jsfiddle.net/6oq39dpy/3/

Normally, when a horizontal legend contains many items, they get wrapped onto multiple lines.

However, if legend groups are used at all -- even if only a single item is assigned to its own legend group -- nothing is wrapped, and legend items extending beyond the width of the plot are invisible and inaccessible.

To make wrapping appear again in the jsfiddle repro, just comment out line 10, which sets a legend group for the first trace.

While searching for an already open issue for this, I found #769, which led to the original PR that added support for wrapping horizontal legends in the first place, and mentions legend groups, but only as a workaround for the original problem. I've only skimmed the related code so far so I don't know exactly what causes the bug. :-)

@jzthree
Copy link

jzthree commented Mar 28, 2018

if(helpers.isVertical(opts)) {
if(isGrouped) {
groups.each(function(d, i) {
Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
});
}
traces.each(function(d) {
var legendItem = d[0],
textHeight = legendItem.height,
textWidth = legendItem.width;
Drawing.setTranslate(this,
borderwidth,
(5 + borderwidth + opts._height + textHeight / 2));
opts._height += textHeight;
opts._width = Math.max(opts._width, textWidth);
});
opts._width += 45 + borderwidth * 2;
opts._height += 10 + borderwidth * 2;
if(isGrouped) {
opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
}
extraWidth = 40;
}
else if(isGrouped) {
var groupXOffsets = [opts._width],
groupData = groups.data();
for(var i = 0, n = groupData.length; i < n; i++) {
var textWidths = groupData[i].map(function(legendItemArray) {
return legendItemArray[0].width;
});
var groupWidth = 40 + Math.max.apply(null, textWidths);
opts._width += opts.tracegroupgap + groupWidth;
groupXOffsets.push(opts._width);
}
groups.each(function(d, i) {
Drawing.setTranslate(this, groupXOffsets[i], 0);
});
groups.each(function() {
var group = d3.select(this),
groupTraces = group.selectAll('g.traces'),
groupHeight = 0;
groupTraces.each(function(d) {
var legendItem = d[0],
textHeight = legendItem.height;
Drawing.setTranslate(this,
0,
(5 + borderwidth + groupHeight + textHeight / 2));
groupHeight += textHeight;
});
opts._height = Math.max(opts._height, groupHeight);
});
opts._height += 10 + borderwidth * 2;
opts._width += borderwidth * 2;
}
else {
var rowHeight = 0,
maxTraceHeight = 0,
maxTraceWidth = 0,
offsetX = 0,
fullTracesWidth = 0,
traceGap = opts.tracegroupgap || 5,
oneRowLegend;
// calculate largest width for traces and use for width of all legend items
traces.each(function(d) {
maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth);
fullTracesWidth += 40 + d[0].width + traceGap;
});
// check if legend fits in one row
oneRowLegend = (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l)) > borderwidth + fullTracesWidth - traceGap;
traces.each(function(d) {
var legendItem = d[0],
traceWidth = oneRowLegend ? 40 + d[0].width : maxTraceWidth;
if((borderwidth + offsetX + traceGap + traceWidth) > (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l))) {
offsetX = 0;
rowHeight = rowHeight + maxTraceHeight;
opts._height = opts._height + maxTraceHeight;
// reset for next row
maxTraceHeight = 0;
}
Drawing.setTranslate(this,
(borderwidth + offsetX),
(5 + borderwidth + legendItem.height / 2) + rowHeight);
opts._width += traceGap + traceWidth;
opts._height = Math.max(opts._height, legendItem.height);
// keep track of tallest trace in group
offsetX += traceGap + traceWidth;
maxTraceHeight = Math.max(legendItem.height, maxTraceHeight);
});
opts._width += borderwidth * 2;
opts._height += 10 + borderwidth * 2;
}

It looks like this is the issue, in the code structure below, if isGrouped is true, it blocks the regular horizontal legend code

if(helpers.isVertical(opts)) {
...
}
else if(isGrouped){
...
}else{
...
}

@Braintelligence
Copy link

Braintelligence commented Oct 1, 2018

@jzthree Could it be as easy as refactoring this bit and horizontal legend groups would work again?

EDIT: Really in demand of this feature... I just can't use legendgroups in a productive fashion if I want to have a responsive legend =/...

@Braintelligence
Copy link

It's not perfect at all, but you can change they legend orientation on relayout depending on the window width as a workaround until this is fixed. (Only a choice between horizontal and vertical, tho. No wrapping of two items in a row possible this way.)

@Braintelligence
Copy link

Just FYI everyone: If you set legend.traceorder = normal || reversed (but not grouped or reversed+grouped) you won't experience problems with wrapping. This is perhaps interesting for you, if you're using legendgroups solely for the feature of simultaneous isolation of data by legendclicks but don't need the grouped positioning in the legend.

@slishak
Copy link

slishak commented Oct 26, 2018

Extending on this issue, with multiple legend groups you end up with one vertical column in the legend for each group. Which I guess is not a bad way of representing a grouped legend, but I'd guess not intentional!
https://jsfiddle.net/by142svc/1/

@tobiasblasberg
Copy link

Just FYI everyone: If you set legend.traceorder = normal || reversed (but not grouped or reversed+grouped) you won't experience problems with wrapping. This is perhaps interesting for you, if you're using legendgroups solely for the feature of simultaneous isolation of data by legendclicks but don't need the grouped positioning in the legend.

I am using rplotly with plotly version 4.8.0. Unfortunately there is no legend$traceorder variable for barplots: https://plot.ly/r/reference/#bar. I would highly appreciate any solution for the alignment/wrapping of horizontal legend of barplots in R.

@alexcjohnson
Copy link
Collaborator

@tobiasblasberg it's not a trace property, it's in layout https://plot.ly/r/reference/#layout-legend-traceorder

@tobiasblasberg
Copy link

Great! That works perfect - thanks a lot!

@avsdev-cw
Copy link

+1 for this

I may even make a PR if I get a chance to implement a fix using a refactored version of the mentioned draw.js file.

@etpinard
Copy link
Contributor

etpinard commented Mar 5, 2019

@antoinerg could you take a look at this one?

@antoinerg antoinerg self-assigned this Mar 5, 2019
@antoinerg
Copy link
Contributor

antoinerg commented Mar 5, 2019

Extending on this issue, with multiple legend groups you end up with one vertical column in the legend for each group. Which I guess is not a bad way of representing a grouped legend, but I'd guess not intentional!
https://jsfiddle.net/by142svc/1/

@plotly/plotly_js is the behavior mentionned above intentional and would the following change be acceptable: e84bcb9?short_path=3367b6a#diff-3367b6ae7c2fe5d6fbdfd3a6630f48a1 ? If so, I could submit a fix that only 🔪 suspcious code which is a win imho.

Draft PR: https://github.com/plotly/plotly.js/compare/pr-1913

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

No branches or pull requests

10 participants