UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

364 lines (307 loc) 13.3 kB
'use strict'; var Registry = require('../../registry'); var Lib = require('../../lib'); var pushUnique = Lib.pushUnique; var helpers = require('./helpers'); var SHOWISOLATETIP = true; /** * Handles click actions on individual legend items. * * @param {object} g D3 selection of the legend item element * @param {object} gd graph div * @param {object} legendObj the legend object from fullLayout * @param {string} mode toggle mode for the current action: 'toggle' | 'toggleothers' * - 'toggle': Toggle visibility of this item (or group if groupclick is 'togglegroup') * - 'toggleothers': Show only this item, hide all others (isolation mode) */ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) { var fullLayout = gd._fullLayout; if (gd._dragged || gd._editing) return; var legendItem = g.data()[0][0]; if (legendItem.groupTitle && legendItem.noClick) return; var groupClick = legendObj.groupclick; // Show isolate tip on first single click when default behavior is active if ( mode === 'toggle' && legendObj.itemdoubleclick === 'toggleothers' && SHOWISOLATETIP && gd.data && gd._context.showTips ) { Lib.notifier(Lib._(gd, 'Double-click on legend to isolate one trace'), 'long', gd); SHOWISOLATETIP = false; } var toggleGroup = groupClick === 'togglegroup'; var hiddenSlices = fullLayout.hiddenlabels ? fullLayout.hiddenlabels.slice() : []; var fullData = gd._fullData; // legendgroup membership matters even when showlegend is false, so togglegroup reaches hidden group peers. const shapesInLegend = (fullLayout.shapes || []).filter((d) => d.showlegend || d.legendgroup); var allLegendItems = fullData.concat(shapesInLegend); var fullTrace = legendItem.trace; if (fullTrace._isShape) { fullTrace = fullTrace._fullInput; } var legendgroup = fullTrace.legendgroup; var i, j, kcont, key, keys, val; var dataUpdate = {}; var dataIndices = []; var carrs = []; var carrIdx = []; function insertDataUpdate(traceIndex, value) { var attrIndex = dataIndices.indexOf(traceIndex); var valueArray = dataUpdate.visible; if (!valueArray) { valueArray = dataUpdate.visible = []; } 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 || fullTrace; var isShape = fullInput._isShape; var index = fullInput.index; if (index === undefined) index = fullInput._index; // false -> false (not possible since will not be visible in legend) // true -> legendonly // legendonly -> true var nextVisibility = fullInput.visible === false ? false : visibility; if (isShape) { insertShapesUpdate(index, nextVisibility); } else { insertDataUpdate(index, nextVisibility); } } var thisLegend = fullTrace.legend; 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); if (mode === 'toggle') { if (thisLabelIndex === -1) hiddenSlices.push(thisLabel); else hiddenSlices.splice(thisLabelIndex, 1); } else if (mode === 'toggleothers') { var changed = thisLabelIndex !== -1; var unhideList = []; for (i = 0; i < gd.calcdata.length; i++) { var cdi = gd.calcdata[i]; for (j = 0; j < cdi.length; j++) { var d = cdi[j]; var dLabel = d.label; // ensure we toggle slices that are in this legend) if (thisLegend === cdi[0].trace.legend) { if (thisLabel !== dLabel) { if (hiddenSlices.indexOf(dLabel) === -1) changed = true; pushUnique(hiddenSlices, dLabel); unhideList.push(dLabel); } } } } if (!changed) { for (var q = 0; q < unhideList.length; q++) { var pos = hiddenSlices.indexOf(unhideList[q]); if (pos !== -1) { hiddenSlices.splice(pos, 1); } } } } Registry.call('_guiRelayout', gd, 'hiddenlabels', hiddenSlices); } else { var hasLegendgroup = legendgroup && legendgroup.length; var traceIndicesInGroup = []; var tracei; if (hasLegendgroup) { for (i = 0; i < allLegendItems.length; i++) { tracei = allLegendItems[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) { if (toggleGroup) { for (i = 0; i < allLegendItems.length; i++) { var item = allLegendItems[i]; if (item.visible !== false && item.legendgroup === legendgroup) { setVisibility(item, nextVisibility); } } } else { setVisibility(fullTrace, 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, _item; var isIsolated = true; for (i = 0; i < allLegendItems.length; i++) { _item = allLegendItems[i]; isClicked = _item === fullTrace; notInLegend = _item.showlegend !== true; if (isClicked || notInLegend) continue; isInGroup = hasLegendgroup && _item.legendgroup === legendgroup; if ( !isInGroup && _item.legend === thisLegend && _item.visible === true && !Registry.traceIs(_item, 'notLegendIsolatable') ) { isIsolated = false; break; } } 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 (_item.visible === false || _item.legend !== thisLegend) continue; if (Registry.traceIs(_item, 'notLegendIsolatable')) { continue; } switch (fullTrace.visible) { case 'legendonly': setVisibility(_item, true); break; case true: otherState = isIsolated ? true : 'legendonly'; isClicked = _item === fullTrace; // N.B. consider traces that have a set legendgroup as toggleable notInLegend = _item.showlegend !== true && !_item.legendgroup; isInGroup = isClicked || (hasLegendgroup && _item.legendgroup === legendgroup); setVisibility(_item, 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 = dataUpdate[key] = dataUpdate[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(dataUpdate); for (i = 0; i < keys.length; i++) { key = keys[i]; for (j = 0; j < dataIndices.length; j++) { // Use hasOwnProperty to protect against falsy values: if (!dataUpdate[key].hasOwnProperty(j)) { dataUpdate[key][j] = undefined; } } } if (shapesUpdated) { Registry.call('_guiUpdate', gd, dataUpdate, { shapes: updatedShapes }, dataIndices); } else { Registry.call('_guiRestyle', gd, dataUpdate, dataIndices); } } }; /** * Handles click actions on legend titles. * * @param {object} gd graph div (plot container) * @param {object} legendObj the legend object from fullLayout * @param {string} mode toggle mode for the current action: 'toggle' | 'toggleothers' * - 'toggle': show/hide all items in this legend * - 'toggleothers': isolate this legend (show its items, hide items in other legends) */ exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) { const fullLayout = gd._fullLayout; const fullData = gd._fullData; const legendId = helpers.getId(legendObj); const shapesInLegend = (fullLayout.shapes || []).filter((d) => d.showlegend || d.legendgroup); const allLegendItems = fullData.concat(shapesInLegend); function isInLegend(item) { return (item.legend || 'legend') === legendId; } var toggleThisLegend; var toggleOtherLegends; if (mode === 'toggle') { // If any item is visible in this legend, hide all. If all are hidden, show all const anyVisibleHere = allLegendItems.some(function (item) { return isInLegend(item) && item.visible === true; }); toggleThisLegend = !anyVisibleHere; toggleOtherLegends = false; } else { // isolate this legend or set all legends to visible const anyVisibleElsewhere = allLegendItems.some(function (item) { return !isInLegend(item) && item.visible === true && item.showlegend !== false; }); toggleThisLegend = true; toggleOtherLegends = !anyVisibleElsewhere; } const dataUpdate = { visible: [] }; const dataIndices = []; const updatedShapes = (fullLayout.shapes || []).map(function (d) { return d._input; }); var shapesUpdated = false; for (var i = 0; i < allLegendItems.length; i++) { const item = allLegendItems[i]; const inThisLegend = isInLegend(item); // If item is not in this legend, skip if in toggle mode // or if item is not displayed in the legend if (!inThisLegend) { const notDisplayed = item.showlegend !== true && !item.legendgroup; if (mode === 'toggle' || notDisplayed) continue; } const shouldShow = inThisLegend ? toggleThisLegend : toggleOtherLegends; const newVis = shouldShow ? true : 'legendonly'; // Only update if visibility would actually change if (item.visible !== false && item.visible !== newVis) { if (item._isShape) { updatedShapes[item._index].visible = newVis; shapesUpdated = true; } else { dataIndices.push(item.index); dataUpdate.visible.push(newVis); } } } if (shapesUpdated) { Registry.call('_guiUpdate', gd, dataUpdate, { shapes: updatedShapes }, dataIndices); } else if (dataIndices.length) { Registry.call('_guiRestyle', gd, dataUpdate, dataIndices); } };