Skip to content

Commit

Permalink
Merge pull request #6535 from plotly/multiple-legends
Browse files Browse the repository at this point in the history
Draw multiple legends on a graph
  • Loading branch information
archmoj committed Apr 24, 2023
2 parents 45a4d98 + d7f278a commit 3f533b2
Show file tree
Hide file tree
Showing 13 changed files with 547 additions and 45 deletions.
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
Expand Up @@ -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',
Expand Down
43 changes: 36 additions & 7 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Up @@ -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);
Expand All @@ -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');
});

Expand 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) {
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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');
}
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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');
Expand All @@ -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)
Expand Down Expand Up @@ -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');
}
Expand 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);
Expand All @@ -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) {
Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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]),
Expand All @@ -860,3 +903,7 @@ function getYanchor(legendObj) {
Lib.isMiddleAnchor(legendObj) ? 'middle' :
'top';
}

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

0 comments on commit 3f533b2

Please sign in to comment.