plotly.js
Version:
The open source javascript graphing library that powers plotly
853 lines (749 loc) • 24.1 kB
JavaScript
;
var Registry = require('../../registry');
var Plots = require('../../plots/plots');
var axisIds = require('../../plots/cartesian/axis_ids');
var Icons = require('../../fonts/ploticon');
var eraseActiveShape = require('../shapes/draw').eraseActiveShape;
var confirmCloudDialog = require('./share_chart/dialog');
var Lib = require('../../lib');
var _ = Lib._;
var modeBarButtons = (module.exports = {});
/**
* ModeBar buttons configuration
*
* @param {string} name
* name / id of the buttons (for tracking)
* @param {string} title
* text that appears while hovering over the button,
* enter null, false or '' for no hover text
* @param {string} icon
* svg icon object associated with the button
* can be linked to Plotly.Icons to use the default plotly icons
* @param {string} [gravity]
* icon positioning
* @param {function} click
* click handler associated with the button, a function of
* 'gd' (the main graph object) and
* 'ev' (the event object)
* @param {string} [attr]
* attribute associated with button,
* use this with 'val' to keep track of the state
* @param {*} [val]
* initial 'attr' value, can be a function of gd
* @param {boolean} [toggle]
* is the button a toggle button?
*/
modeBarButtons.toImage = {
name: 'toImage',
title: function (gd) {
var opts = gd._context.toImageButtonOptions || {};
var format = opts.format || 'png';
return format === 'png'
? _(gd, 'Download plot as a PNG')
: // legacy text
_(gd, 'Download plot'); // generic non-PNG text
},
icon: Icons.camera,
click: function (gd) {
var toImageButtonOptions = gd._context.toImageButtonOptions;
var opts = { format: toImageButtonOptions.format || 'png' };
Lib.notifier(_(gd, 'Preparing image - this may take a few seconds'), 'long', gd);
['filename', 'width', 'height', 'scale'].forEach(function (key) {
if (key in toImageButtonOptions) {
opts[key] = toImageButtonOptions[key];
}
});
Registry.call('downloadImage', gd, opts)
.then(function (filename) {
Lib.notifier(_(gd, 'Image download succeeded') + ' - ' + filename, 'long', gd);
})
.catch(function () {
Lib.notifier(_(gd, 'Sorry, there was a problem downloading your image!'), 'long', gd);
});
}
};
modeBarButtons.sendChartToCloud = {
name: 'sendChartToCloud',
title: function (gd) {
return _(gd, 'Share chart...');
},
icon: Icons.cloudupload,
click: function (gd) {
var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL;
if (!baseUrl) {
console.error('No destination URL provided (plotlyServerURL is empty)');
return;
}
// Validate that the provided plotlyServerURL is a valid URL
// with an http or https protocol
var baseUrlObj;
try {
baseUrlObj = new URL(baseUrl);
} catch (e) {
console.error('Invalid plotlyServerURL: ' + baseUrl);
return;
}
const supportedProtocols = ['http:', 'https:'];
if (!supportedProtocols.includes(baseUrlObj.protocol)) {
console.error(`Invalid protocol '${baseUrlObj.protocol}' in plotlyServerURL '${baseUrl}'. Must be one of: ${supportedProtocols.join(', ')}`);
return;
}
confirmCloudDialog(gd, baseUrl, function () {
Plots.sendDataToCloud(gd, baseUrl);
});
}
};
modeBarButtons.zoom2d = {
name: 'zoom2d',
_cat: 'zoom',
title: function (gd) {
return _(gd, 'Zoom');
},
attr: 'dragmode',
val: 'zoom',
icon: Icons.zoombox,
click: handleCartesian
};
modeBarButtons.pan2d = {
name: 'pan2d',
_cat: 'pan',
title: function (gd) {
return _(gd, 'Pan');
},
attr: 'dragmode',
val: 'pan',
icon: Icons.pan,
click: handleCartesian
};
modeBarButtons.select2d = {
name: 'select2d',
_cat: 'select',
title: function (gd) {
return _(gd, 'Box Select');
},
attr: 'dragmode',
val: 'select',
icon: Icons.selectbox,
click: handleCartesian
};
modeBarButtons.lasso2d = {
name: 'lasso2d',
_cat: 'lasso',
title: function (gd) {
return _(gd, 'Lasso Select');
},
attr: 'dragmode',
val: 'lasso',
icon: Icons.lasso,
click: handleCartesian
};
modeBarButtons.drawclosedpath = {
name: 'drawclosedpath',
title: function (gd) {
return _(gd, 'Draw closed freeform');
},
attr: 'dragmode',
val: 'drawclosedpath',
icon: Icons.drawclosedpath,
click: handleCartesian
};
modeBarButtons.drawopenpath = {
name: 'drawopenpath',
title: function (gd) {
return _(gd, 'Draw open freeform');
},
attr: 'dragmode',
val: 'drawopenpath',
icon: Icons.drawopenpath,
click: handleCartesian
};
modeBarButtons.drawline = {
name: 'drawline',
title: function (gd) {
return _(gd, 'Draw line');
},
attr: 'dragmode',
val: 'drawline',
icon: Icons.drawline,
click: handleCartesian
};
modeBarButtons.drawrect = {
name: 'drawrect',
title: function (gd) {
return _(gd, 'Draw rectangle');
},
attr: 'dragmode',
val: 'drawrect',
icon: Icons.drawrect,
click: handleCartesian
};
modeBarButtons.drawcircle = {
name: 'drawcircle',
title: function (gd) {
return _(gd, 'Draw circle');
},
attr: 'dragmode',
val: 'drawcircle',
icon: Icons.drawcircle,
click: handleCartesian
};
modeBarButtons.eraseshape = {
name: 'eraseshape',
title: function (gd) {
return _(gd, 'Erase active shape');
},
icon: Icons.eraseshape,
click: eraseActiveShape
};
modeBarButtons.zoomIn2d = {
name: 'zoomIn2d',
_cat: 'zoomin',
title: function (gd) {
return _(gd, 'Zoom in');
},
attr: 'zoom',
val: 'in',
icon: Icons.zoom_plus,
click: handleCartesian
};
modeBarButtons.zoomOut2d = {
name: 'zoomOut2d',
_cat: 'zoomout',
title: function (gd) {
return _(gd, 'Zoom out');
},
attr: 'zoom',
val: 'out',
icon: Icons.zoom_minus,
click: handleCartesian
};
modeBarButtons.autoScale2d = {
name: 'autoScale2d',
_cat: 'autoscale',
title: function (gd) {
return _(gd, 'Autoscale');
},
attr: 'zoom',
val: 'auto',
icon: Icons.autoscale,
click: handleCartesian
};
modeBarButtons.resetScale2d = {
name: 'resetScale2d',
_cat: 'resetscale',
title: function (gd) {
return _(gd, 'Reset axes');
},
attr: 'zoom',
val: 'reset',
icon: Icons.home,
click: handleCartesian
};
modeBarButtons.hoverClosestCartesian = {
name: 'hoverClosestCartesian',
_cat: 'hoverclosest',
title: function (gd) {
return _(gd, 'Show closest data on hover');
},
attr: 'hovermode',
val: 'closest',
icon: Icons.tooltip_basic,
gravity: 'ne',
click: handleCartesian
};
modeBarButtons.hoverCompareCartesian = {
name: 'hoverCompareCartesian',
_cat: 'hoverCompare',
title: function (gd) {
return _(gd, 'Compare data on hover');
},
attr: 'hovermode',
val: function (gd) {
return gd._fullLayout._isHoriz ? 'y' : 'x';
},
icon: Icons.tooltip_compare,
gravity: 'ne',
click: handleCartesian
};
function handleCartesian(gd, ev) {
var button = ev.currentTarget;
var astr = button.getAttribute('data-attr');
var val = button.getAttribute('data-val') || true;
var fullLayout = gd._fullLayout;
var aobj = {};
var axList = axisIds.list(gd, null, true);
var allSpikesEnabled = fullLayout._cartesianSpikesEnabled;
var ax, i;
if (astr === 'zoom') {
var mag = val === 'in' ? 0.5 : 2;
var r0 = (1 + mag) / 2;
var r1 = (1 - mag) / 2;
var axName, allowed;
for (i = 0; i < axList.length; i++) {
ax = axList[i];
allowed =
ax.modebardisable === 'none' ||
ax.modebardisable.indexOf(val === 'auto' || val === 'reset' ? 'autoscale' : 'zoominout') === -1;
if (allowed && !ax.fixedrange) {
axName = ax._name;
if (val === 'auto') {
aobj[axName + '.autorange'] = true;
} else if (val === 'reset') {
if (ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined) {
aobj[axName + '.autorange'] = true;
} else if (ax._rangeInitial0 === undefined) {
aobj[axName + '.autorange'] = ax._autorangeInitial;
aobj[axName + '.range'] = [null, ax._rangeInitial1];
} else if (ax._rangeInitial1 === undefined) {
aobj[axName + '.range'] = [ax._rangeInitial0, null];
aobj[axName + '.autorange'] = ax._autorangeInitial;
} else {
aobj[axName + '.range'] = [ax._rangeInitial0, ax._rangeInitial1];
}
// N.B. "reset" also resets showspikes
if (ax._showSpikeInitial !== undefined) {
aobj[axName + '.showspikes'] = ax._showSpikeInitial;
if (allSpikesEnabled === 'on' && !ax._showSpikeInitial) {
allSpikesEnabled = 'off';
}
}
} else {
var rangeNow = [ax.r2l(ax.range[0]), ax.r2l(ax.range[1])];
var rangeNew = [r0 * rangeNow[0] + r1 * rangeNow[1], r0 * rangeNow[1] + r1 * rangeNow[0]];
aobj[axName + '.range[0]'] = ax.l2r(rangeNew[0]);
aobj[axName + '.range[1]'] = ax.l2r(rangeNew[1]);
}
}
}
} else {
// if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
if (astr === 'hovermode' && (val === 'x' || val === 'y')) {
val = fullLayout._isHoriz ? 'y' : 'x';
button.setAttribute('data-val', val);
}
aobj[astr] = val;
}
fullLayout._cartesianSpikesEnabled = allSpikesEnabled;
Registry.call('_guiRelayout', gd, aobj);
}
modeBarButtons.zoom3d = {
name: 'zoom3d',
_cat: 'zoom',
title: function (gd) {
return _(gd, 'Zoom');
},
attr: 'scene.dragmode',
val: 'zoom',
icon: Icons.zoombox,
click: handleDrag3d
};
modeBarButtons.pan3d = {
name: 'pan3d',
_cat: 'pan',
title: function (gd) {
return _(gd, 'Pan');
},
attr: 'scene.dragmode',
val: 'pan',
icon: Icons.pan,
click: handleDrag3d
};
modeBarButtons.orbitRotation = {
name: 'orbitRotation',
title: function (gd) {
return _(gd, 'Orbital rotation');
},
attr: 'scene.dragmode',
val: 'orbit',
icon: Icons['3d_rotate'],
click: handleDrag3d
};
modeBarButtons.tableRotation = {
name: 'tableRotation',
title: function (gd) {
return _(gd, 'Turntable rotation');
},
attr: 'scene.dragmode',
val: 'turntable',
icon: Icons['z-axis'],
click: handleDrag3d
};
function handleDrag3d(gd, ev) {
var button = ev.currentTarget;
var attr = button.getAttribute('data-attr');
var val = button.getAttribute('data-val') || true;
var sceneIds = gd._fullLayout._subplots.gl3d || [];
var layoutUpdate = {};
var parts = attr.split('.');
for (var i = 0; i < sceneIds.length; i++) {
layoutUpdate[sceneIds[i] + '.' + parts[1]] = val;
}
// for multi-type subplots
var val2d = val === 'pan' ? val : 'zoom';
layoutUpdate.dragmode = val2d;
Registry.call('_guiRelayout', gd, layoutUpdate);
}
modeBarButtons.resetCameraDefault3d = {
name: 'resetCameraDefault3d',
_cat: 'resetCameraDefault',
title: function (gd) {
return _(gd, 'Reset camera to default');
},
attr: 'resetDefault',
icon: Icons.home,
click: handleCamera3d
};
modeBarButtons.resetCameraLastSave3d = {
name: 'resetCameraLastSave3d',
_cat: 'resetCameraLastSave',
title: function (gd) {
return _(gd, 'Reset camera to last save');
},
attr: 'resetLastSave',
icon: Icons.movie,
click: handleCamera3d
};
function handleCamera3d(gd, ev) {
var button = ev.currentTarget;
var attr = button.getAttribute('data-attr');
var resetLastSave = attr === 'resetLastSave';
var resetDefault = attr === 'resetDefault';
var fullLayout = gd._fullLayout;
var sceneIds = fullLayout._subplots.gl3d || [];
var aobj = {};
for (var i = 0; i < sceneIds.length; i++) {
var sceneId = sceneIds[i];
var camera = sceneId + '.camera';
var aspectratio = sceneId + '.aspectratio';
var aspectmode = sceneId + '.aspectmode';
var scene = fullLayout[sceneId]._scene;
var didUpdate;
if (resetLastSave) {
aobj[camera + '.up'] = scene.viewInitial.up;
aobj[camera + '.eye'] = scene.viewInitial.eye;
aobj[camera + '.center'] = scene.viewInitial.center;
didUpdate = true;
} else if (resetDefault) {
aobj[camera + '.up'] = null;
aobj[camera + '.eye'] = null;
aobj[camera + '.center'] = null;
didUpdate = true;
}
if (didUpdate) {
aobj[aspectratio + '.x'] = scene.viewInitial.aspectratio.x;
aobj[aspectratio + '.y'] = scene.viewInitial.aspectratio.y;
aobj[aspectratio + '.z'] = scene.viewInitial.aspectratio.z;
aobj[aspectmode] = scene.viewInitial.aspectmode;
}
}
Registry.call('_guiRelayout', gd, aobj);
}
modeBarButtons.hoverClosest3d = {
name: 'hoverClosest3d',
_cat: 'hoverclosest',
title: function (gd) {
return _(gd, 'Toggle show closest data on hover');
},
attr: 'hovermode',
val: null,
toggle: true,
icon: Icons.tooltip_basic,
gravity: 'ne',
click: handleHover3d
};
function getNextHover3d(gd, ev) {
var button = ev.currentTarget;
var val = button._previousVal;
var fullLayout = gd._fullLayout;
var sceneIds = fullLayout._subplots.gl3d || [];
var axes = ['xaxis', 'yaxis', 'zaxis'];
// initialize 'current spike' object to be stored in the DOM
var currentSpikes = {};
var layoutUpdate = {};
if (val) {
layoutUpdate = val;
button._previousVal = null;
} else {
for (var i = 0; i < sceneIds.length; i++) {
var sceneId = sceneIds[i];
var sceneLayout = fullLayout[sceneId];
var hovermodeAStr = sceneId + '.hovermode';
currentSpikes[hovermodeAStr] = sceneLayout.hovermode;
layoutUpdate[hovermodeAStr] = false;
// copy all the current spike attrs
for (var j = 0; j < 3; j++) {
var axis = axes[j];
var spikeAStr = sceneId + '.' + axis + '.showspikes';
layoutUpdate[spikeAStr] = false;
currentSpikes[spikeAStr] = sceneLayout[axis].showspikes;
}
}
button._previousVal = currentSpikes;
}
return layoutUpdate;
}
function handleHover3d(gd, ev) {
var layoutUpdate = getNextHover3d(gd, ev);
Registry.call('_guiRelayout', gd, layoutUpdate);
}
modeBarButtons.zoomInGeo = {
name: 'zoomInGeo',
_cat: 'zoomin',
title: function (gd) {
return _(gd, 'Zoom in');
},
attr: 'zoom',
val: 'in',
icon: Icons.zoom_plus,
click: handleGeo
};
modeBarButtons.zoomOutGeo = {
name: 'zoomOutGeo',
_cat: 'zoomout',
title: function (gd) {
return _(gd, 'Zoom out');
},
attr: 'zoom',
val: 'out',
icon: Icons.zoom_minus,
click: handleGeo
};
modeBarButtons.resetGeo = {
name: 'resetGeo',
_cat: 'reset',
title: function (gd) {
return _(gd, 'Reset');
},
attr: 'reset',
val: null,
icon: Icons.autoscale,
click: handleGeo
};
modeBarButtons.hoverClosestGeo = {
name: 'hoverClosestGeo',
_cat: 'hoverclosest',
title: function (gd) {
return _(gd, 'Toggle show closest data on hover');
},
attr: 'hovermode',
val: null,
toggle: true,
icon: Icons.tooltip_basic,
gravity: 'ne',
click: toggleHover
};
const ZOOM_STEP_GEO = 2;
function handleGeo(gd, ev) {
const button = ev.currentTarget;
const attr = button.getAttribute('data-attr');
const val = button.getAttribute('data-val') || true;
const fullLayout = gd._fullLayout;
const geoIds = fullLayout._subplots.geo || [];
for (const id of geoIds) {
const geoLayout = fullLayout[id];
const geoSubplot = geoLayout._subplot;
if (attr === 'zoom') {
// Under fitbounds, geoLayout.projection.scale is undefined; read the
// effective view from the D3 projection state instead
const projection = geoSubplot.projection;
const effectiveScale = projection.scale() / geoSubplot.fitScale; // Convert to schema format (multiples of original scale)
const [rotationLon, rotationLat] = projection.rotate().map((d) => -d); // Flip sign because D3 rotation is opposite of ours
const [centerLon, centerLat] = projection.invert(geoSubplot.midPt);
const { minscale } = geoLayout.projection;
const maxscale = geoLayout.projection.maxscale ?? Infinity;
// swap if user supplied min > max so clamping is well-defined
const min = Math.min(minscale, maxscale);
const max = Math.max(minscale, maxscale);
let newScale = val === 'in' ? ZOOM_STEP_GEO * effectiveScale : (1 / ZOOM_STEP_GEO) * effectiveScale;
// clamp to [min, max]
if (newScale > max) newScale = max;
else if (newScale < min) newScale = min;
if (newScale !== effectiveScale) {
// Persist the currently-effective view attrs with the new scale; turn
// off fitbounds so auto-fit doesn't overwrite them on the ensuing replot
Registry.call('_guiRelayout', gd, {
[id + '.projection.scale']: newScale,
[id + '.projection.rotation.lon']: rotationLon,
[id + '.projection.rotation.lat']: rotationLat,
[id + '.center.lon']: centerLon,
[id + '.center.lat']: centerLat,
[id + '.fitbounds']: false
});
}
}
}
if (attr === 'reset') resetView(gd, 'geo');
}
modeBarButtons.hoverClosestPie = {
name: 'hoverClosestPie',
_cat: 'hoverclosest',
title: function (gd) {
return _(gd, 'Toggle show closest data on hover');
},
attr: 'hovermode',
val: 'closest',
icon: Icons.tooltip_basic,
gravity: 'ne',
click: toggleHover
};
function getNextHover(gd) {
var fullLayout = gd._fullLayout;
if (fullLayout.hovermode) return false;
if (fullLayout._has('cartesian')) {
return fullLayout._isHoriz ? 'y' : 'x';
}
return 'closest';
}
function toggleHover(gd) {
var newHover = getNextHover(gd);
Registry.call('_guiRelayout', gd, 'hovermode', newHover);
}
modeBarButtons.resetViewSankey = {
name: 'resetSankeyGroup',
title: function (gd) {
return _(gd, 'Reset view');
},
icon: Icons.home,
click: function (gd) {
var aObj = {
'node.groups': [],
'node.x': [],
'node.y': []
};
for (var i = 0; i < gd._fullData.length; i++) {
var viewInitial = gd._fullData[i]._viewInitial;
aObj['node.groups'].push(viewInitial.node.groups.slice());
aObj['node.x'].push(viewInitial.node.x.slice());
aObj['node.y'].push(viewInitial.node.y.slice());
}
Registry.call('restyle', gd, aObj);
}
};
// buttons when more then one plot types are present
modeBarButtons.toggleHover = {
name: 'toggleHover',
title: function (gd) {
return _(gd, 'Toggle show closest data on hover');
},
attr: 'hovermode',
val: null,
toggle: true,
icon: Icons.tooltip_basic,
gravity: 'ne',
click: function (gd, ev) {
var layoutUpdate = getNextHover3d(gd, ev);
layoutUpdate.hovermode = getNextHover(gd);
Registry.call('_guiRelayout', gd, layoutUpdate);
}
};
modeBarButtons.resetViews = {
name: 'resetViews',
title: function (gd) {
return _(gd, 'Reset views');
},
icon: Icons.home,
click: function (gd, ev) {
var button = ev.currentTarget;
button.setAttribute('data-attr', 'zoom');
button.setAttribute('data-val', 'reset');
handleCartesian(gd, ev);
button.setAttribute('data-attr', 'resetLastSave');
handleCamera3d(gd, ev);
resetView(gd, 'geo');
resetView(gd, 'map');
}
};
modeBarButtons.toggleSpikelines = {
name: 'toggleSpikelines',
title: function (gd) {
return _(gd, 'Toggle Spike Lines');
},
icon: Icons.spikeline,
attr: '_cartesianSpikesEnabled',
val: 'on',
click: function (gd) {
var fullLayout = gd._fullLayout;
var allSpikesEnabled = fullLayout._cartesianSpikesEnabled;
fullLayout._cartesianSpikesEnabled = allSpikesEnabled === 'on' ? 'off' : 'on';
Registry.call('_guiRelayout', gd, setSpikelineVisibility(gd));
}
};
function setSpikelineVisibility(gd) {
var fullLayout = gd._fullLayout;
var areSpikesOn = fullLayout._cartesianSpikesEnabled === 'on';
var axList = axisIds.list(gd, null, true);
var aobj = {};
for (var i = 0; i < axList.length; i++) {
var ax = axList[i];
aobj[ax._name + '.showspikes'] = areSpikesOn ? true : ax._showSpikeInitial;
}
return aobj;
}
modeBarButtons.resetViewMap = {
name: 'resetViewMap',
_cat: 'resetView',
title: function (gd) {
return _(gd, 'Reset view');
},
attr: 'reset',
icon: Icons.home,
click: function (gd) {
resetView(gd, 'map');
}
};
modeBarButtons.zoomInMap = {
name: 'zoomInMap',
_cat: 'zoomin',
title: function (gd) {
return _(gd, 'Zoom in');
},
attr: 'zoom',
val: 'in',
icon: Icons.zoom_plus,
click: handleMapZoom
};
modeBarButtons.zoomOutMap = {
name: 'zoomOutMap',
_cat: 'zoomout',
title: function (gd) {
return _(gd, 'Zoom out');
},
attr: 'zoom',
val: 'out',
icon: Icons.zoom_minus,
click: handleMapZoom
};
function handleMapZoom(gd, ev) {
_handleMapZoom(gd, ev, 'map');
}
function _handleMapZoom(gd, ev, mapType) {
var button = ev.currentTarget;
var val = button.getAttribute('data-val');
var fullLayout = gd._fullLayout;
var subplotIds = fullLayout._subplots[mapType] || [];
var scalar = 1.05;
var aObj = {};
for (var i = 0; i < subplotIds.length; i++) {
var id = subplotIds[i];
var current = fullLayout[id].zoom;
var next = val === 'in' ? scalar * current : current / scalar;
aObj[id + '.zoom'] = next;
}
Registry.call('_guiRelayout', gd, aObj);
}
function resetView(gd, subplotType) {
var fullLayout = gd._fullLayout;
var subplotIds = fullLayout._subplots[subplotType] || [];
var aObj = {};
for (var i = 0; i < subplotIds.length; i++) {
var id = subplotIds[i];
var subplotObj = fullLayout[id]._subplot;
var viewInitial = subplotObj.viewInitial;
var viewKeys = Object.keys(viewInitial);
for (var j = 0; j < viewKeys.length; j++) {
var key = viewKeys[j];
aObj[id + '.' + key] = viewInitial[key];
}
}
Registry.call('_guiRelayout', gd, aObj);
}