UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

853 lines (749 loc) 24.1 kB
'use strict'; 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); }