plotly.js
Version:
The open source javascript graphing library that powers plotly
238 lines (202 loc) • 8.89 kB
JavaScript
/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
var Lib = require('../../lib');
var Registry = require('../../registry');
var SHOWISOLATETIP = true;
module.exports = function handleClick(g, gd, numClicks) {
var fullLayout = gd._fullLayout;
if(gd._dragged || gd._editing) return;
var itemClick = fullLayout.legend.itemclick;
var itemDoubleClick = fullLayout.legend.itemdoubleclick;
if(numClicks === 1 && itemClick === 'toggle' && itemDoubleClick === 'toggleothers' &&
SHOWISOLATETIP && gd.data && gd._context.showTips
) {
Lib.notifier(Lib._(gd, 'Double-click on legend to isolate one trace'), 'long');
SHOWISOLATETIP = false;
} else {
SHOWISOLATETIP = false;
}
var mode;
if(numClicks === 1) mode = itemClick;
else if(numClicks === 2) mode = itemDoubleClick;
if(!mode) return;
var hiddenSlices = fullLayout.hiddenlabels ?
fullLayout.hiddenlabels.slice() :
[];
var legendItem = g.data()[0][0];
var fullData = gd._fullData;
var fullTrace = legendItem.trace;
var legendgroup = fullTrace.legendgroup;
var i, j, kcont, key, keys, val;
var attrUpdate = {};
var attrIndices = [];
var carrs = [];
var carrIdx = [];
function insertUpdate(traceIndex, key, value) {
var attrIndex = attrIndices.indexOf(traceIndex);
var valueArray = attrUpdate[key];
if(!valueArray) {
valueArray = attrUpdate[key] = [];
}
if(attrIndices.indexOf(traceIndex) === -1) {
attrIndices.push(traceIndex);
attrIndex = attrIndices.length - 1;
}
valueArray[attrIndex] = value;
return attrIndex;
}
function setVisibility(fullTrace, visibility) {
var fullInput = fullTrace._fullInput;
if(Registry.hasTransform(fullInput, 'groupby')) {
var kcont = carrs[fullInput.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;
}
var curState = kcont.get(fullTrace._group);
// If not specified, assume visible. This happens if there are other style
// properties set for a group but not the visibility. There are many similar
// ways to do this (e.g. why not just `curState = fullTrace.visible`??? The
// answer is: because it breaks other things like groupby trace names in
// subtle ways.)
if(curState === undefined) {
curState = true;
}
if(curState !== false) {
// true -> legendonly. All others toggle to true:
kcont.set(fullTrace._group, visibility);
}
carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', 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(Registry.traceIs(fullTrace, 'pie-like')) {
var thisLabel = legendItem.label;
var thisLabelIndex = hiddenSlices.indexOf(thisLabel);
if(mode === 'toggle') {
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
else hiddenSlices.splice(thisLabelIndex, 1);
} else if(mode === 'toggleothers') {
hiddenSlices = [];
gd.calcdata[0].forEach(function(d) {
if(thisLabel !== d.label) {
hiddenSlices.push(d.label);
}
});
if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) {
hiddenSlices = [];
}
}
Registry.call('_guiRelayout', gd, 'hiddenlabels', hiddenSlices);
} else {
var hasLegendgroup = legendgroup && legendgroup.length;
var traceIndicesInGroup = [];
var tracei;
if(hasLegendgroup) {
for(i = 0; i < fullData.length; i++) {
tracei = fullData[i];
if(!tracei.visible) continue;
if(tracei.legendgroup === legendgroup) {
traceIndicesInGroup.push(i);
}
}
}
if(mode === 'toggle') {
var nextVisibility;
switch(fullTrace.visible) {
case true:
nextVisibility = 'legendonly';
break;
case false:
nextVisibility = false;
break;
case 'legendonly':
nextVisibility = true;
break;
}
if(hasLegendgroup) {
for(i = 0; i < fullData.length; i++) {
if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) {
setVisibility(fullData[i], nextVisibility);
}
}
} else {
setVisibility(fullTrace, nextVisibility);
}
} 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 isIsolated = true;
for(i = 0; i < fullData.length; i++) {
isClicked = fullData[i] === fullTrace;
notInLegend = fullData[i].showlegend !== true;
if(isClicked || notInLegend) continue;
isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup);
if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
isIsolated = false;
break;
}
}
for(i = 0; i < fullData.length; i++) {
// False is sticky; we don't change it.
if(fullData[i].visible === false) continue;
if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
continue;
}
switch(fullTrace.visible) {
case 'legendonly':
setVisibility(fullData[i], true);
break;
case true:
otherState = isIsolated ? true : 'legendonly';
isClicked = fullData[i] === 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);
break;
}
}
}
for(i = 0; i < carrs.length; i++) {
kcont = carrs[i];
if(!kcont) continue;
var update = kcont.constructUpdate();
var updateKeys = Object.keys(update);
for(j = 0; j < updateKeys.length; j++) {
key = updateKeys[j];
val = attrUpdate[key] = attrUpdate[key] || [];
val[carrIdx[i]] = update[key];
}
}
// The length of the value arrays should be equal and any unspecified
// 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);
for(i = 0; i < keys.length; i++) {
key = keys[i];
for(j = 0; j < attrIndices.length; j++) {
// Use hasOwnPropety to protect against falsey values:
if(!attrUpdate[key].hasOwnProperty(j)) {
attrUpdate[key][j] = undefined;
}
}
}
Registry.call('_guiRestyle', gd, attrUpdate, attrIndices);
}
};