Skip to content

Commit

Permalink
Merge pull request #6653 from plotly/shape-legends
Browse files Browse the repository at this point in the history
add options to include shapes and `newshape` in legends
  • Loading branch information
archmoj committed Jul 25, 2023
2 parents 9c9d5d0 + 9123425 commit 16d5d22
Show file tree
Hide file tree
Showing 25 changed files with 860 additions and 252 deletions.
1 change: 1 addition & 0 deletions draftlogs/6653_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add options to include shapes and `newshape` in legends [[#6653](https://github.com/plotly/plotly.js/pull/6653)]
1 change: 1 addition & 0 deletions src/components/colorbar/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function makeColorBarData(gd) {
for(var i = 0; i < calcdata.length; i++) {
var cd = calcdata[i];
trace = cd[0].trace;
if(!trace._module) continue;
var moduleOpts = trace._module.colorbar;

if(trace.visible === true && moduleOpts) {
Expand Down
38 changes: 31 additions & 7 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
var legendReallyHasATrace = false;
var defaultOrder = 'normal';

var allLegendItems = fullData.filter(function(d) {
var shapesWithLegend = (layoutOut.shapes || []).filter(function(d) { return d.showlegend; });

var allLegendItems = fullData.concat(shapesWithLegend).filter(function(d) {
return legendId === (d.legend || 'legend');
});

Expand All @@ -50,6 +52,8 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {

if(!trace.visible) continue;

var isShape = trace._isShape;

// Note that we explicitly count any trace that is either shown or
// *would* be shown by default, toward the two traces you need to
// ensure the legend is shown by default, because this can still help
Expand All @@ -67,7 +71,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
legendReallyHasATrace = true;
// Always show the legend by default if there's a pie,
// or if there's only one trace but it's explicitly shown
if(Registry.traceIs(trace, 'pie-like') ||
if(!isShape && Registry.traceIs(trace, 'pie-like') ||
trace._input.showlegend === true
) {
legendTraceCount++;
Expand All @@ -77,7 +81,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
Lib.coerceFont(traceCoerce, 'legendgrouptitle.font', grouptitlefont);
}

if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
if((!isShape && Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
['tonextx', 'tonexty'].indexOf(trace.fill) !== -1) {
defaultOrder = helpers.isGrouped({traceorder: defaultOrder}) ?
'grouped+reversed' : 'reversed';
Expand Down Expand Up @@ -199,17 +203,37 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {

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);
var allLegendsData = fullData.slice();

// shapes could also show up in legends
var shapes = layoutOut.shapes;
if(shapes) {
for(i = 0; i < shapes.length; i++) {
var shape = shapes[i];
if(!shape.showlegend) continue;

var mockTrace = {
_input: shape._input,
visible: shape.visible,
showlegend: shape.showlegend,
legend: shape.legend
};

allLegendsData.push(mockTrace);
}
}

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

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

groupDefaults(legendId, layoutIn, layoutOut, fullData);
groupDefaults(legendId, layoutIn, layoutOut, allLegendsData);

if(
layoutOut[legendId] &&
Expand Down
50 changes: 45 additions & 5 deletions src/components/legend/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,44 @@ function drawOne(gd, opts) {

var legendData;
if(!inHover) {
if(!gd.calcdata) return;
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj, fullLayout._legends.length > 1);
var calcdata = (gd.calcdata || []).slice();

var shapes = fullLayout.shapes;
for(var i = 0; i < shapes.length; i++) {
var shape = shapes[i];
if(!shape.showlegend) continue;

var shapeLegend = {
_isShape: true,
_fullInput: shape,
index: shape._index,
name: shape.name || shape.label.text || ('shape ' + shape._index),
legend: shape.legend,
legendgroup: shape.legendgroup,
legendgrouptitle: shape.legendgrouptitle,
legendrank: shape.legendrank,
legendwidth: shape.legendwidth,
showlegend: shape.showlegend,
visible: shape.visible,
opacity: shape.opacity,
mode: shape.type === 'line' ? 'lines' : 'markers',
line: shape.line,
marker: {
line: shape.line,
color: shape.fillcolor,
size: 12,
symbol:
shape.type === 'rect' ? 'square' :
shape.type === 'circle' ? 'circle' :
// case of path
'hexagon2'
},
};

calcdata.push([{ trace: shapeLegend }]);
}
if(!calcdata.length) return;
legendData = fullLayout.showlegend && getLegendData(calcdata, legendObj, fullLayout._legends.length > 1);
} else {
if(!legendObj.entries) return;
legendData = getLegendData(legendObj.entries, legendObj);
Expand Down Expand Up @@ -491,9 +527,9 @@ function drawTexts(g, gd, legendObj) {

if(Registry.hasTransform(fullInput, 'groupby')) {
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
var index = groupbyIndices[groupbyIndices.length - 1];
var _index = groupbyIndices[groupbyIndices.length - 1];

var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
var kcont = Lib.keyedContainer(fullInput, 'transforms[' + _index + '].styles', 'target', 'value.name');

kcont.set(legendItem.trace._group, newName);

Expand All @@ -502,7 +538,11 @@ function drawTexts(g, gd, legendObj) {
update.name = newName;
}

return Registry.call('_guiRestyle', gd, update, trace.index);
if(fullInput._isShape) {
return Registry.call('_guiRelayout', gd, 'shapes[' + trace.index + '].name', update.name);
} else {
return Registry.call('_guiRestyle', gd, update, trace.index);
}
});
} else {
textLayout(textEl, g, gd, legendObj);
Expand Down
117 changes: 77 additions & 40 deletions src/components/legend/handle_click.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,43 +39,65 @@ module.exports = function handleClick(g, gd, numClicks) {
if(legendItem.groupTitle && legendItem.noClick) return;

var fullData = gd._fullData;
var shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; });
var allLegendItems = fullData.concat(shapesWithLegend);

var fullTrace = legendItem.trace;
if(fullTrace._isShape) {
fullTrace = fullTrace._fullInput;
}

var legendgroup = fullTrace.legendgroup;

var i, j, kcont, key, keys, val;
var attrUpdate = {};
var attrIndices = [];
var dataUpdate = {};
var dataIndices = [];
var carrs = [];
var carrIdx = [];

function insertUpdate(traceIndex, key, value) {
var attrIndex = attrIndices.indexOf(traceIndex);
var valueArray = attrUpdate[key];
function insertDataUpdate(traceIndex, value) {
var attrIndex = dataIndices.indexOf(traceIndex);
var valueArray = dataUpdate.visible;
if(!valueArray) {
valueArray = attrUpdate[key] = [];
valueArray = dataUpdate.visible = [];
}

if(attrIndices.indexOf(traceIndex) === -1) {
attrIndices.push(traceIndex);
attrIndex = attrIndices.length - 1;
if(dataIndices.indexOf(traceIndex) === -1) {
dataIndices.push(traceIndex);
attrIndex = dataIndices.length - 1;
}

valueArray[attrIndex] = value;

return attrIndex;
}

var updatedShapes = (fullLayout.shapes || []).map(function(d) {
return d._input;
});

var shapesUpdated = false;

function insertShapesUpdate(shapeIndex, value) {
updatedShapes[shapeIndex].visible = value;
shapesUpdated = true;
}

function setVisibility(fullTrace, visibility) {
if(legendItem.groupTitle && !toggleGroup) return;

var fullInput = fullTrace._fullInput;
var fullInput = fullTrace._fullInput || fullTrace;
var isShape = fullInput._isShape;
var index = fullInput.index;
if(index === undefined) index = fullInput._index;

if(Registry.hasTransform(fullInput, 'groupby')) {
var kcont = carrs[fullInput.index];
var kcont = carrs[index];
if(!kcont) {
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1];
kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible');
carrs[fullInput.index] = kcont;
carrs[index] = kcont;
}

var curState = kcont.get(fullTrace._group);
Expand All @@ -93,20 +115,27 @@ module.exports = function handleClick(g, gd, numClicks) {
// true -> legendonly. All others toggle to true:
kcont.set(fullTrace._group, visibility);
}
carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true);
carrIdx[index] = insertDataUpdate(index, fullInput.visible === false ? false : true);
} else {
// false -> false (not possible since will not be visible in legend)
// true -> legendonly
// legendonly -> true
var nextVisibility = fullInput.visible === false ? false : visibility;

insertUpdate(fullInput.index, 'visible', nextVisibility);
if(isShape) {
insertShapesUpdate(index, nextVisibility);
} else {
insertDataUpdate(index, nextVisibility);
}
}
}

var thisLegend = fullTrace.legend;

if(Registry.traceIs(fullTrace, 'pie-like')) {
var fullInput = fullTrace._fullInput;
var isShape = fullInput && fullInput._isShape;

if(!isShape && Registry.traceIs(fullTrace, 'pie-like')) {
var thisLabel = legendItem.label;
var thisLabelIndex = hiddenSlices.indexOf(thisLabel);

Expand Down Expand Up @@ -149,8 +178,8 @@ module.exports = function handleClick(g, gd, numClicks) {
var traceIndicesInGroup = [];
var tracei;
if(hasLegendgroup) {
for(i = 0; i < fullData.length; i++) {
tracei = fullData[i];
for(i = 0; i < allLegendItems.length; i++) {
tracei = allLegendItems[i];
if(!tracei.visible) continue;
if(tracei.legendgroup === legendgroup) {
traceIndicesInGroup.push(i);
Expand All @@ -175,9 +204,10 @@ module.exports = function handleClick(g, gd, numClicks) {

if(hasLegendgroup) {
if(toggleGroup) {
for(i = 0; i < fullData.length; i++) {
if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) {
setVisibility(fullData[i], nextVisibility);
for(i = 0; i < allLegendItems.length; i++) {
var item = allLegendItems[i];
if(item.visible !== false && item.legendgroup === legendgroup) {
setVisibility(item, nextVisibility);
}
}
} else {
Expand All @@ -189,40 +219,43 @@ module.exports = function handleClick(g, gd, numClicks) {
} else if(mode === 'toggleothers') {
// Compute the clicked index. expandedIndex does what we want for expanded traces
// but also culls hidden traces. That means we have some work to do.
var isClicked, isInGroup, notInLegend, otherState;
var isClicked, isInGroup, notInLegend, otherState, _item;
var isIsolated = true;
for(i = 0; i < fullData.length; i++) {
isClicked = fullData[i] === fullTrace;
notInLegend = fullData[i].showlegend !== true;
for(i = 0; i < allLegendItems.length; i++) {
_item = allLegendItems[i];
isClicked = _item === fullTrace;
notInLegend = _item.showlegend !== true;
if(isClicked || notInLegend) continue;

isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup);
isInGroup = (hasLegendgroup && _item.legendgroup === legendgroup);

if(fullData[i].legend === thisLegend && !isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
if(!isInGroup && _item.legend === thisLegend && _item.visible === true && !Registry.traceIs(_item, 'notLegendIsolatable')) {
isIsolated = false;
break;
}
}

for(i = 0; i < fullData.length; i++) {
for(i = 0; i < allLegendItems.length; i++) {
_item = allLegendItems[i];

// False is sticky; we don't change it. Also ensure we don't change states of itmes in other legend
if(fullData[i].visible === false || fullData[i].legend !== thisLegend) continue;
if(_item.visible === false || _item.legend !== thisLegend) continue;

if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
if(Registry.traceIs(_item, 'notLegendIsolatable')) {
continue;
}

switch(fullTrace.visible) {
case 'legendonly':
setVisibility(fullData[i], true);
setVisibility(_item, true);
break;
case true:
otherState = isIsolated ? true : 'legendonly';
isClicked = fullData[i] === fullTrace;
isClicked = _item === fullTrace;
// N.B. consider traces that have a set legendgroup as toggleable
notInLegend = (fullData[i].showlegend !== true && !fullData[i].legendgroup);
isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup);
setVisibility(fullData[i], (isInGroup || notInLegend) ? true : otherState);
notInLegend = (_item.showlegend !== true && !_item.legendgroup);
isInGroup = isClicked || (hasLegendgroup && _item.legendgroup === legendgroup);
setVisibility(_item, (isInGroup || notInLegend) ? true : otherState);
break;
}
}
Expand All @@ -236,7 +269,7 @@ module.exports = function handleClick(g, gd, numClicks) {
var updateKeys = Object.keys(update);
for(j = 0; j < updateKeys.length; j++) {
key = updateKeys[j];
val = attrUpdate[key] = attrUpdate[key] || [];
val = dataUpdate[key] = dataUpdate[key] || [];
val[carrIdx[i]] = update[key];
}
}
Expand All @@ -245,17 +278,21 @@ module.exports = function handleClick(g, gd, numClicks) {
// values should be explicitly undefined for them to get properly culled
// as updates and not accidentally reset to the default value. This fills
// out sparse arrays with the required number of undefined values:
keys = Object.keys(attrUpdate);
keys = Object.keys(dataUpdate);
for(i = 0; i < keys.length; i++) {
key = keys[i];
for(j = 0; j < attrIndices.length; j++) {
for(j = 0; j < dataIndices.length; j++) {
// Use hasOwnProperty to protect against falsy values:
if(!attrUpdate[key].hasOwnProperty(j)) {
attrUpdate[key][j] = undefined;
if(!dataUpdate[key].hasOwnProperty(j)) {
dataUpdate[key][j] = undefined;
}
}
}

Registry.call('_guiRestyle', gd, attrUpdate, attrIndices);
if(shapesUpdated) {
Registry.call('_guiUpdate', gd, dataUpdate, {shapes: updatedShapes}, dataIndices);
} else {
Registry.call('_guiRestyle', gd, dataUpdate, dataIndices);
}
}
};

0 comments on commit 16d5d22

Please sign in to comment.