Skip to content

Draw multiple legends on a graph #6535

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

Merged
merged 15 commits into from
Apr 24, 2023
Merged
3 changes: 3 additions & 0 deletions draftlogs/6535_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Add `legend` references to traces and `legend2`, `legend3`, etc. to layout
to allow positioning multiple legends on a graph [[#6535](https://github.com/plotly/plotly.js/pull/6535)],
this feature was anonymously sponsored: thank you to our sponsor!
14 changes: 14 additions & 0 deletions src/components/legend/attributes.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,20 @@ var colorAttrs = require('../color/attributes');


module.exports = {
// not really a 'subplot' attribute container,
// but this is the flag we use to denote attributes that
// support yaxis, yaxis2, yaxis3, ... counters
_isSubplotObj: true,

visible: {
valType: 'boolean',
dflt: true,
editType: 'legend',
description: [
'Determines whether or not this legend is visible.'
].join(' ')
},

bgcolor: {
valType: 'color',
editType: 'legend',
43 changes: 36 additions & 7 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
@@ -9,15 +9,22 @@ var attributes = require('./attributes');
var basePlotLayoutAttributes = require('../../plots/layout_attributes');
var helpers = require('./helpers');


module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
var containerIn = layoutIn.legend || {};
var containerOut = Template.newContainer(layoutOut, 'legend');
function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
var containerIn = layoutIn[legendId] || {};
var containerOut = Template.newContainer(layoutOut, legendId);

function coerce(attr, dflt) {
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
}

// N.B. unified hover needs to inherit from font, bgcolor & bordercolor even when legend.visible is false
var itemFont = Lib.coerceFont(coerce, 'font', layoutOut.font);
coerce('bgcolor', layoutOut.paper_bgcolor);
coerce('bordercolor');

var visible = coerce('visible');
if(!visible) return;

var trace;
var traceCoerce = function(attr, dflt) {
var traceIn = trace._input;
@@ -91,10 +98,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {

if(showLegend === false) return;

coerce('bgcolor', layoutOut.paper_bgcolor);
coerce('bordercolor');
coerce('borderwidth');
var itemFont = Lib.coerceFont(coerce, 'font', layoutOut.font);

var orientation = coerce('orientation');
var isHorizontal = orientation === 'h';
@@ -147,4 +151,29 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {

Lib.coerceFont(coerce, 'title.font', dfltTitleFont);
}
}

module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
var i;
var legends = ['legend'];

for(i = 0; i < fullData.length; i++) {
Lib.pushUnique(legends, fullData[i].legend);
}

layoutOut._legends = [];
for(i = 0; i < legends.length; i++) {
var legendId = legends[i];

groupDefaults(legendId, layoutIn, layoutOut, fullData);

if(
layoutOut[legendId] &&
layoutOut[legendId].visible
) {
layoutOut[legendId]._id = legendId;
}

layoutOut._legends.push(legendId);
}
};
115 changes: 81 additions & 34 deletions src/components/legend/draw.js
Original file line number Diff line number Diff line change
@@ -24,32 +24,61 @@ var helpers = require('./helpers');

var MAIN_TITLE = 1;

var LEGEND_PATTERN = /^legend[0-9]*$/;

module.exports = function draw(gd, opts) {
if(!opts) opts = gd._fullLayout.legend || {};
return _draw(gd, opts);
if(opts) {
drawOne(gd, opts);
} else {
var fullLayout = gd._fullLayout;
var newLegends = fullLayout._legends;

// remove old legends that won't stay on the graph
var oldLegends = fullLayout._infolayer.selectAll('[class^="legend"]');

oldLegends.each(function() {
var el = d3.select(this);
var classes = el.attr('class');
var cls = classes.split(' ')[0];
if(cls.match(LEGEND_PATTERN) && newLegends.indexOf(cls) === -1) {
el.remove();
}
});

// draw/update new legends
for(var i = 0; i < newLegends.length; i++) {
var legendId = newLegends[i];
var legendObj = gd._fullLayout[legendId];
drawOne(gd, legendObj);
}
}
};

function _draw(gd, legendObj) {
function drawOne(gd, opts) {
var legendObj = opts || {};

var fullLayout = gd._fullLayout;
var clipId = 'legend' + fullLayout._uid;
var layer;
var legendId = getId(legendObj);

var clipId, layer;

var inHover = legendObj._inHover;
if(inHover) {
layer = legendObj.layer;
clipId += '-hover';
clipId = 'hover';
} else {
layer = fullLayout._infolayer;
clipId = legendId;
}

if(!layer) return;
clipId += fullLayout._uid;

if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0;

var legendData;
if(!inHover) {
if(!gd.calcdata) return;
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj);
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj, fullLayout._legends.length > 1);
} else {
if(!legendObj.entries) return;
legendData = getLegendData(legendObj.entries, legendObj);
@@ -58,12 +87,12 @@ function _draw(gd, legendObj) {
var hiddenSlices = fullLayout.hiddenlabels || [];

if(!inHover && (!fullLayout.showlegend || !legendData.length)) {
layer.selectAll('.legend').remove();
layer.selectAll('.' + legendId).remove();
fullLayout._topdefs.select('#' + clipId).remove();
return Plots.autoMargin(gd, 'legend');
return Plots.autoMargin(gd, legendId);
}

var legend = Lib.ensureSingle(layer, 'g', 'legend', function(s) {
var legend = Lib.ensureSingle(layer, 'g', legendId, function(s) {
if(!inHover) s.attr('pointer-events', 'all');
});

@@ -84,14 +113,14 @@ function _draw(gd, legendObj) {
legendObj._titleWidth = 0;
legendObj._titleHeight = 0;
if(title.text) {
var titleEl = Lib.ensureSingle(scrollBox, 'text', 'legendtitletext');
var titleEl = Lib.ensureSingle(scrollBox, 'text', legendId + 'titletext');
titleEl.attr('text-anchor', 'start')
.call(Drawing.font, title.font)
.text(title.text);

textLayout(titleEl, scrollBox, gd, legendObj, MAIN_TITLE); // handle mathjax or multi-line text and compute title height
} else {
scrollBox.selectAll('.legendtitletext').remove();
scrollBox.selectAll('.' + legendId + 'titletext').remove();
}

var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
@@ -117,7 +146,7 @@ function _draw(gd, legendObj) {
})
.each(function() { d3.select(this).call(drawTexts, gd, legendObj); })
.call(style, gd, legendObj)
.each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd); });
.each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd, legendId); });

Lib.syncOrAsync([
Plots.previousPromises,
@@ -127,7 +156,7 @@ function _draw(gd, legendObj) {
var bw = legendObj.borderwidth;

if(!inHover) {
var expMargin = expandMargin(gd);
var expMargin = expandMargin(gd, legendId);

// IF expandMargin return a Promise (which is truthy),
// we're under a doAutoMargin redraw, so we don't have to
@@ -145,10 +174,10 @@ function _draw(gd, legendObj) {
ly = Lib.constrain(ly, 0, fullLayout.height - legendObj._effHeight);

if(lx !== lx0) {
Lib.log('Constrain legend.x to make legend fit inside graph');
Lib.log('Constrain ' + legendId + '.x to make legend fit inside graph');
}
if(ly !== ly0) {
Lib.log('Constrain legend.y to make legend fit inside graph');
Lib.log('Constrain ' + legendId + '.y to make legend fit inside graph');
}
}

@@ -294,7 +323,7 @@ function _draw(gd, legendObj) {
}

function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) {
legendObj._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY;
legendObj._scrollY = gd._fullLayout[legendId]._scrollY = scrollBoxY;
Drawing.setTranslate(scrollBox, 0, -scrollBoxY);

Drawing.setRect(
@@ -330,11 +359,14 @@ function _draw(gd, legendObj) {
},
doneFn: function() {
if(xf !== undefined && yf !== undefined) {
Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf});
var obj = {};
obj[legendId + '.x'] = xf;
obj[legendId + '.y'] = yf;
Registry.call('_guiRelayout', gd, obj);
}
},
clickFn: function(numClicks, e) {
var clickedTrace = layer.selectAll('g.traces').filter(function() {
var clickedTrace = groups.selectAll('g.traces').filter(function() {
var bbox = this.getBoundingClientRect();
return (
e.clientX >= bbox.left && e.clientX <= bbox.right &&
@@ -402,6 +434,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
}

function drawTexts(g, gd, legendObj) {
var legendId = getId(legendObj);
var legendItem = g.data()[0][0];
var trace = legendItem.trace;
var isPieLike = Registry.traceIs(trace, 'pie-like');
@@ -424,7 +457,7 @@ function drawTexts(g, gd, legendObj) {
}
}

var textEl = Lib.ensureSingle(g, 'text', 'legendtext');
var textEl = Lib.ensureSingle(g, 'text', legendId + 'text');

textEl.attr('text-anchor', 'start')
.call(Drawing.font, font)
@@ -478,12 +511,12 @@ function ensureLength(str, maxLength) {
return str;
}

function setupTraceToggle(g, gd) {
function setupTraceToggle(g, gd, legendId) {
var doubleClickDelay = gd._context.doubleClickDelay;
var newMouseDownTime;
var numClicks = 1;

var traceToggle = Lib.ensureSingle(g, 'rect', 'legendtoggle', function(s) {
var traceToggle = Lib.ensureSingle(g, 'rect', legendId + 'toggle', function(s) {
if(!gd._context.staticPlot) {
s.style('cursor', 'pointer').attr('pointer-events', 'all');
}
@@ -505,7 +538,7 @@ function setupTraceToggle(g, gd) {
});
traceToggle.on('mouseup', function() {
if(gd._dragged || gd._editing) return;
var legend = gd._fullLayout.legend;
var legend = gd._fullLayout[legendId];

if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
numClicks = Math.max(numClicks - 1, 1);
@@ -531,7 +564,11 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {

var mathjaxGroup = g.select('g[class*=math-group]');
var mathjaxNode = mathjaxGroup.node();
if(!legendObj) legendObj = gd._fullLayout.legend;

var legendId = getId(legendObj);
if(!legendObj) {
legendObj = gd._fullLayout[legendId];
}
var bw = legendObj.borderwidth;
var font;
if(aTitle === MAIN_TITLE) {
@@ -556,9 +593,12 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
Drawing.setTranslate(mathjaxGroup, 0, height * 0.25);
}
} else {
var textEl = g.select(aTitle === MAIN_TITLE ?
'.legendtitletext' : '.legendtext'
);
var cls = '.' + legendId + (
aTitle === MAIN_TITLE ? 'title' : ''
) + 'text';

var textEl = g.select(cls);

var textLines = svgTextUtils.lineCount(textEl);
var textNode = textEl.node();

@@ -619,7 +659,7 @@ function getTitleSize(legendObj) {
}

/*
* Computes in fullLayout.legend:
* Computes in fullLayout[legendId]:
*
* - _height: legend height including items past scrollbox height
* - _maxHeight: maximum legend height before scrollbox is required
@@ -630,7 +670,10 @@ function getTitleSize(legendObj) {
*/
function computeLegendDimensions(gd, groups, traces, legendObj) {
var fullLayout = gd._fullLayout;
if(!legendObj) legendObj = fullLayout.legend;
var legendId = getId(legendObj);
if(!legendObj) {
legendObj = fullLayout[legendId];
}
var gs = fullLayout._size;

var isVertical = helpers.isVertical(legendObj);
@@ -818,7 +861,7 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
var edits = gd._context.edits;
var isEditable = edits.legendText || edits.legendPosition;
traces.each(function(d) {
var traceToggle = d3.select(this).select('.legendtoggle');
var traceToggle = d3.select(this).select('.' + legendId + 'toggle');
var h = d[0].height;
var legendgroup = d[0].trace.legendgroup;
var traceWidth = getTraceWidth(d, legendObj, textGap);
@@ -833,13 +876,13 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
});
}

function expandMargin(gd) {
function expandMargin(gd, legendId) {
var fullLayout = gd._fullLayout;
var legendObj = fullLayout.legend;
var legendObj = fullLayout[legendId];
var xanchor = getXanchor(legendObj);
var yanchor = getYanchor(legendObj);

return Plots.autoMargin(gd, 'legend', {
return Plots.autoMargin(gd, legendId, {
x: legendObj.x,
y: legendObj.y,
l: legendObj._width * (FROM_TL[xanchor]),
@@ -860,3 +903,7 @@ function getYanchor(legendObj) {
Lib.isMiddleAnchor(legendObj) ? 'middle' :
'top';
}

function getId(legendObj) {
return legendObj._id || 'legend';
}
Loading