From f247698417da5716f1507aa211397e61bbe66890 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 19 Nov 2015 11:51:10 -0500 Subject: [PATCH 01/36] put modebar configuration into seperate modebar_config file: - 'click' keys are now linked to strings corresponding to the name of the Modebar prototype method instead of being linked to the prototype methods themselves. - 'val' can be a function of graphInfo (e.g. for 'hoverCompare2d') --- src/components/modebar/index.js | 199 ++------------------- src/components/modebar/modebar_config.js | 209 +++++++++++++++++++++++ 2 files changed, 223 insertions(+), 185 deletions(-) create mode 100644 src/components/modebar/modebar_config.js diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index a5987cf12b0..396f5a35ed7 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -12,6 +12,7 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); +var modebarConfig = require('./modebar_config'); /** * UI controller for interactive plots * @Class @@ -44,7 +45,7 @@ function ModeBar (config) { var group = _this.createGroup(); buttonGroup.forEach( function (buttonName) { - var buttonConfig = _this.config()[buttonName]; + var buttonConfig = modebarConfig[buttonName]; if (!buttonConfig) { throw new Error(buttonName + ' not specfied in modebar configuration'); @@ -83,11 +84,7 @@ proto.createGroup = function () { /** * Create a new button div and set constant and configurable attributes - * @Param {object} config - * @Param {string} config.attr - * @Param {string} config.val - * @Param {string} config.title - * @Param {function} config.click + * @Param {object} config (see ./modebar_config,js for more info) * @Return {HTMLelement} */ proto.createButton = function (config) { @@ -97,13 +94,20 @@ proto.createButton = function (config) { button.setAttribute('rel', 'tooltip'); button.className = 'modebar-btn'; - if (config.attr !== undefined) button.setAttribute('data-attr', config.attr); - if (config.val !== undefined) button.setAttribute('data-val', config.val); button.setAttribute('data-title', config.title); button.setAttribute('data-gravity', config.gravity || 'n'); + + if(config.attr !== undefined) button.setAttribute('data-attr', config.attr); + + var val = config.val; + if(val !== undefined) { + if(typeof val === 'function') val = val(this.graphInfo); + button.setAttribute('data-val', val); + } + button.addEventListener('click', function () { - config.click.apply(_this, arguments); - }); + _this[config.click].apply(_this, arguments); + }); button.setAttribute('data-toggle', config.toggle); if(config.toggle) button.classList.add('active'); @@ -492,181 +496,6 @@ proto.sendDataToCloud = function() { Plotly.Plots.sendDataToCloud(gd) }; -/** - * - * @Property config specification hash of button parameters - */ -proto.config = function config() { - return { - zoom2d: { - title: 'Zoom', - attr: 'dragmode', - val: 'zoom', - icon: 'zoombox', - click: this.handleCartesian - }, - pan2d: { - title: 'Pan', - attr: 'dragmode', - val: 'pan', - icon: 'pan', - click: this.handleCartesian - }, - zoomIn2d: { - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: 'zoom_plus', - click: this.handleCartesian - }, - zoomOut2d: { - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: 'zoom_minus', - click: this.handleCartesian - }, - autoScale2d: { - title: 'Autoscale', - attr: 'zoom', - val: 'auto', - icon: 'autoscale', - click: this.handleCartesian - }, - resetScale2d: { - title: 'Reset axes', - attr: 'zoom', - val: 'reset', - icon: 'home', - click: this.handleCartesian - }, - hoverClosest2d: { - title: 'Show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: 'tooltip_basic', - gravity: 'ne', - click: this.handleCartesian - }, - hoverCompare2d: { - title: 'Compare data on hover', - attr: 'hovermode', - val: this.graphInfo._fullLayout._isHoriz ? 'y' : 'x', - icon: 'tooltip_compare', - gravity: 'ne', - click: this.handleCartesian - }, - toImage: { - title: 'download plot as a png', - icon: 'camera', - click: this.toImage - }, - sendDataToCloud: { - title: 'save and edit plot in cloud', - icon: 'disk', - click: this.sendDataToCloud - }, - // gl3d - zoom3d: { - title: 'Zoom', - attr: 'dragmode', - val: 'zoom', - icon: 'zoombox', - click: this.handleDrag3d - }, - pan3d: { - title: 'Pan', - attr: 'dragmode', - val: 'pan', - icon: 'pan', - click: this.handleDrag3d - }, - orbitRotation: { - title: 'orbital rotation', - attr: 'dragmode', - val: 'orbit', - icon: '3d_rotate', - click: this.handleDrag3d - }, - tableRotation: { - title: 'turntable rotation', - attr: 'dragmode', - val: 'turntable', - icon: 'z-axis', - click: this.handleDrag3d - }, - resetCameraDefault3d: { - title: 'Reset camera to default', - attr: 'resetDefault', - icon: 'home', - click: this.handleCamera3d - }, - resetCameraLastSave3d: { - title: 'Reset camera to last save', - attr: 'resetLastSave', - icon: 'movie', - click: this.handleCamera3d - }, - hoverClosest3d: { - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: this.handleHover3d - }, - // geo - zoomInGeo: { - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: 'zoom_plus', - click: this.handleGeo - }, - zoomOutGeo: { - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: 'zoom_minus', - click: this.handleGeo - }, - resetGeo: { - title: 'Reset', - attr: 'reset', - val: null, - icon: 'autoscale', - click: this.handleGeo - }, - hoverClosestGeo: { - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: this.handleGeo - }, - // pie - hoverClosestPie: { - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: 'tooltip_basic', - gravity: 'ne', - click: this.handleHoverPie - }, - // gl2d - hoverClosestGl2d: { - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: this.handleHoverGl2d - } - }; }; module.exports = ModeBar; diff --git a/src/components/modebar/modebar_config.js b/src/components/modebar/modebar_config.js new file mode 100644 index 00000000000..aa4a836c861 --- /dev/null +++ b/src/components/modebar/modebar_config.js @@ -0,0 +1,209 @@ +/** +* Copyright 2012-2015, 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'; + +/** + * Modebar buttons configuration + * + * @param {string} title text that appears while hovering over the button + * @param {string} icon name of the svg icon associated with the button + * @param {string} [gravity] icon positioning + * @param {string} click name of the modebar click handler associated with the button + * @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 graphInfo + * @param {boolean} [toggle] is the button a toggle button? + * + */ + +module.exports = { + + // for all plot types + toImage: { + title: 'download plot as a png', + icon: 'camera', + click: 'toImage' + }, + sendDataToCloud: { + title: 'save and edit plot in cloud', + icon: 'disk', + click: 'sendDataToCloud' + }, + + // cartesian and gl2d + zoom2d: { + title: 'Zoom', + attr: 'dragmode', + val: 'zoom', + icon: 'zoombox', + click: 'handleCartesian' + }, + pan2d: { + title: 'Pan', + attr: 'dragmode', + val: 'pan', + icon: 'pan', + click: 'handleCartesian' + }, + zoomIn2d: { + title: 'Zoom in', + attr: 'zoom', + val: 'in', + icon: 'zoom_plus', + click: 'handleCartesian' + }, + zoomOut2d: { + title: 'Zoom out', + attr: 'zoom', + val: 'out', + icon: 'zoom_minus', + click: 'handleCartesian' + }, + autoScale2d: { + title: 'Autoscale', + attr: 'zoom', + val: 'auto', + icon: 'autoscale', + click: 'handleCartesian' + }, + resetScale2d: { + title: 'Reset axes', + attr: 'zoom', + val: 'reset', + icon: 'home', + click: 'handleCartesian' + }, + + // cartesian only + hoverClosest2d: { + title: 'Show closest data on hover', + attr: 'hovermode', + val: 'closest', + icon: 'tooltip_basic', + gravity: 'ne', + click: 'handleCartesian' + }, + hoverCompare2d: { + title: 'Compare data on hover', + attr: 'hovermode', + val: function(graphInfo) { + return graphInfo._fullLayout._isHoriz ? 'y' : 'x'; + }, + icon: 'tooltip_compare', + gravity: 'ne', + click: 'handleCartesian' + }, + + // gl3d + zoom3d: { + title: 'Zoom', + attr: 'dragmode', + val: 'zoom', + icon: 'zoombox', + click: 'handleDrag3d' + }, + pan3d: { + title: 'Pan', + attr: 'dragmode', + val: 'pan', + icon: 'pan', + click: 'handleDrag3d' + }, + orbitRotation: { + title: 'orbital rotation', + attr: 'dragmode', + val: 'orbit', + icon: '3d_rotate', + click: 'handleDrag3d' + }, + tableRotation: { + title: 'turntable rotation', + attr: 'dragmode', + val: 'turntable', + icon: 'z-axis', + click: 'handleDrag3d' + }, + resetCameraDefault3d: { + title: 'Reset camera to default', + attr: 'resetDefault', + icon: 'home', + click: 'handleCamera3d' + }, + resetCameraLastSave3d: { + title: 'Reset camera to last save', + attr: 'resetLastSave', + icon: 'movie', + click: 'handleCamera3d' + }, + hoverClosest3d: { + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: 'handleHover3d' + }, + + // geo + zoomInGeo: { + title: 'Zoom in', + attr: 'zoom', + val: 'in', + icon: 'zoom_plus', + click: 'handleGeo' + }, + zoomOutGeo: { + title: 'Zoom out', + attr: 'zoom', + val: 'out', + icon: 'zoom_minus', + click: 'handleGeo' + }, + resetGeo: { + title: 'Reset', + attr: 'reset', + val: null, + icon: 'autoscale', + click: 'handleGeo' + }, + hoverClosestGeo: { + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: 'handleGeo' + }, + + // gl2d only + hoverClosestGl2d: { + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: 'handleHoverGl2d' + }, + + // pie traces only + hoverClosestPie: { + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: 'closest', + icon: 'tooltip_basic', + gravity: 'ne', + click: 'handleHoverPie' + } + +}; From 84a7c065840d48a1413e9c7e0cfc2686143390b0 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 19 Nov 2015 11:53:06 -0500 Subject: [PATCH 02/36] clean up ModeBar API: - rm !(this instanceof ModeBar) code path - rename 'config' arg --> 'opts' to not be mistaken with the buttons config --- src/components/modebar/index.js | 24 +++++++++--------------- src/plots/cartesian/graph_interact.js | 1 - 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 396f5a35ed7..7e293efb0ed 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -16,20 +16,16 @@ var modebarConfig = require('./modebar_config'); /** * UI controller for interactive plots * @Class - * @Param {object} config - * @Param {object} config.buttons nested arrays of grouped buttons to initialize - * @Param {object} config.container container div to append modebar - * @Param {object} config.Plotly main plotly namespace module - * @Param {object} config.graphInfo primary plot object containing data and layout + * @Param {object} opts + * @Param {object} opts.buttons nested arrays of grouped buttons to initialize + * @Param {object} opts.container container div to append modebar + * @Param {object} opts.graphInfo primary plot object containing data and layout */ -function ModeBar (config) { - - if (!(this instanceof ModeBar)) return new ModeBar(); - +function ModeBar(opts) { var _this = this; this._snapshotInProgress = false; - this.graphInfo = config.graphInfo; + this.graphInfo = opts.graphInfo; this.element = document.createElement('div'); if(this.graphInfo._context.displayModeBar === 'hover') { @@ -38,7 +34,7 @@ function ModeBar (config) { this.element.className = 'modebar'; } - this.buttons = config.buttons; + this.buttons = opts.buttons; this.buttonElements = []; this.buttons.forEach( function (buttonGroup) { @@ -64,7 +60,7 @@ function ModeBar (config) { this.element.appendChild(this.getLogo()); } - config.container.appendChild(this.element); + opts.container.appendChild(this.element); this.updateActiveButton(); } @@ -493,9 +489,7 @@ proto.toImage = function() { proto.sendDataToCloud = function() { var gd = this.graphInfo; - Plotly.Plots.sendDataToCloud(gd) -}; - + Plotly.Plots.sendDataToCloud(gd); }; module.exports = ModeBar; diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 628d9f6ef5a..f9b5dbc5a73 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -1287,7 +1287,6 @@ fx.modeBar = function(gd){ var modebar = new Plotly.ModeBar({ buttons: buttons, container: fullLayout._paperdiv.node(), - Plotly: Plotly, graphInfo: gd }); From 387e7dafa970c54069c1cfeb9e8abd9c6819a3e4 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 19 Nov 2015 11:54:28 -0500 Subject: [PATCH 03/36] require Icons in modebar index directly, instead of relying on internal Plotly. --- src/components/modebar/index.js | 12 +++++++----- src/plotly.js | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 7e293efb0ed..6561656aa4f 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -12,7 +12,10 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); +var Icons = require('../../../build/ploticon'); var modebarConfig = require('./modebar_config'); + + /** * UI controller for interactive plots * @Class @@ -108,7 +111,7 @@ proto.createButton = function (config) { button.setAttribute('data-toggle', config.toggle); if(config.toggle) button.classList.add('active'); - button.appendChild(this.createIcon(Plotly.Icons[config.icon])); + button.appendChild(this.createIcon(Icons[config.icon])); return button; }; @@ -121,8 +124,7 @@ proto.createButton = function (config) { * @Return {HTMLelement} */ proto.createIcon = function (thisIcon) { - var iconDef = Plotly.Icons, - iconHeight = iconDef.ascent - iconDef.descent, + var iconHeight = Icons.ascent - Icons.descent, svgNS = 'http://www.w3.org/2000/svg', icon = document.createElementNS(svgNS, 'svg'), path = document.createElementNS(svgNS, 'path'); @@ -132,7 +134,7 @@ proto.createIcon = function (thisIcon) { icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); path.setAttribute('d', thisIcon.path); - path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + iconDef.ascent + ')'); + path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + Icons.ascent + ')'); icon.appendChild(path); return icon; @@ -202,7 +204,7 @@ proto.getLogo = function(){ a.setAttribute('data-title', 'Produced with Plotly'); a.className = 'modebar-btn plotlyjsicon modebar-btn--logo'; - a.appendChild(this.createIcon(Plotly.Icons.plotlylogo)); + a.appendChild(this.createIcon(Icons.plotlylogo)); group.appendChild(a); return group; diff --git a/src/plotly.js b/src/plotly.js index ffff4245a39..368af2d31a9 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -25,8 +25,7 @@ exports.Lib = require('./lib'); exports.util = require('./lib/svg_text_utils'); exports.Queue = require('./lib/queue'); -// plot icons svg and plot css -exports.Icons = require('../build/ploticon'); +// plot css require('../build/plotcss'); // configuration From 079f747f81dd0650828520058308012b8e99a066 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 10:28:34 -0500 Subject: [PATCH 04/36] rename modebar_config -> buttons_config --- .../{modebar_config.js => buttons_config.js} | 48 +++++++++++++++++++ src/components/modebar/index.js | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) rename src/components/modebar/{modebar_config.js => buttons_config.js} (79%) diff --git a/src/components/modebar/modebar_config.js b/src/components/modebar/buttons_config.js similarity index 79% rename from src/components/modebar/modebar_config.js rename to src/components/modebar/buttons_config.js index aa4a836c861..05cad8e6ee9 100644 --- a/src/components/modebar/modebar_config.js +++ b/src/components/modebar/buttons_config.js @@ -12,6 +12,8 @@ /** * Modebar buttons configuration * + * @param {string} category button category depending on e.g. plot type + * @param {string} group button group ('ext', 'drag', 'zoom', 'hover') * @param {string} title text that appears while hovering over the button * @param {string} icon name of the svg icon associated with the button * @param {string} [gravity] icon positioning @@ -28,11 +30,15 @@ module.exports = { // for all plot types toImage: { + category: 'all', + group: 'ext', title: 'download plot as a png', icon: 'camera', click: 'toImage' }, sendDataToCloud: { + category: 'all', + group: 'ext', title: 'save and edit plot in cloud', icon: 'disk', click: 'sendDataToCloud' @@ -40,6 +46,8 @@ module.exports = { // cartesian and gl2d zoom2d: { + category: '2d', + group: 'drag', title: 'Zoom', attr: 'dragmode', val: 'zoom', @@ -47,6 +55,8 @@ module.exports = { click: 'handleCartesian' }, pan2d: { + category: '2d', + group: 'drag', title: 'Pan', attr: 'dragmode', val: 'pan', @@ -54,6 +64,8 @@ module.exports = { click: 'handleCartesian' }, zoomIn2d: { + category: '2d', + group: 'zoom', title: 'Zoom in', attr: 'zoom', val: 'in', @@ -61,6 +73,8 @@ module.exports = { click: 'handleCartesian' }, zoomOut2d: { + category: '2d', + group: 'zoom', title: 'Zoom out', attr: 'zoom', val: 'out', @@ -68,6 +82,8 @@ module.exports = { click: 'handleCartesian' }, autoScale2d: { + category: '2d', + group: 'zoom', title: 'Autoscale', attr: 'zoom', val: 'auto', @@ -75,6 +91,8 @@ module.exports = { click: 'handleCartesian' }, resetScale2d: { + category: '2d', + group: 'zoom', title: 'Reset axes', attr: 'zoom', val: 'reset', @@ -84,6 +102,8 @@ module.exports = { // cartesian only hoverClosest2d: { + category: 'cartesian', + group: 'hover', title: 'Show closest data on hover', attr: 'hovermode', val: 'closest', @@ -92,6 +112,8 @@ module.exports = { click: 'handleCartesian' }, hoverCompare2d: { + category: 'cartesian', + group: 'hover', title: 'Compare data on hover', attr: 'hovermode', val: function(graphInfo) { @@ -104,6 +126,8 @@ module.exports = { // gl3d zoom3d: { + category: 'gl3d', + group: 'drag', title: 'Zoom', attr: 'dragmode', val: 'zoom', @@ -111,6 +135,8 @@ module.exports = { click: 'handleDrag3d' }, pan3d: { + category: 'gl3d', + group: 'drag', title: 'Pan', attr: 'dragmode', val: 'pan', @@ -118,6 +144,8 @@ module.exports = { click: 'handleDrag3d' }, orbitRotation: { + category: 'gl3d', + group: 'drag', title: 'orbital rotation', attr: 'dragmode', val: 'orbit', @@ -125,6 +153,8 @@ module.exports = { click: 'handleDrag3d' }, tableRotation: { + category: 'gl3d', + group: 'drag', title: 'turntable rotation', attr: 'dragmode', val: 'turntable', @@ -132,18 +162,24 @@ module.exports = { click: 'handleDrag3d' }, resetCameraDefault3d: { + category: 'gl3d', + group: 'zoom', title: 'Reset camera to default', attr: 'resetDefault', icon: 'home', click: 'handleCamera3d' }, resetCameraLastSave3d: { + category: 'gl3d', + group: 'zoom', title: 'Reset camera to last save', attr: 'resetLastSave', icon: 'movie', click: 'handleCamera3d' }, hoverClosest3d: { + category: 'gl3d', + group: 'hover', title: 'Toggle show closest data on hover', attr: 'hovermode', val: null, @@ -155,6 +191,8 @@ module.exports = { // geo zoomInGeo: { + category: 'geo', + group: 'zoom', title: 'Zoom in', attr: 'zoom', val: 'in', @@ -162,6 +200,8 @@ module.exports = { click: 'handleGeo' }, zoomOutGeo: { + category: 'geo', + group: 'zoom', title: 'Zoom out', attr: 'zoom', val: 'out', @@ -169,6 +209,8 @@ module.exports = { click: 'handleGeo' }, resetGeo: { + category: 'geo', + group: 'zoom', title: 'Reset', attr: 'reset', val: null, @@ -176,6 +218,8 @@ module.exports = { click: 'handleGeo' }, hoverClosestGeo: { + category: 'geo', + group: 'hover', title: 'Toggle show closest data on hover', attr: 'hovermode', val: null, @@ -187,6 +231,8 @@ module.exports = { // gl2d only hoverClosestGl2d: { + category: 'gl2d', + group: 'hover', title: 'Toggle show closest data on hover', attr: 'hovermode', val: null, @@ -198,6 +244,8 @@ module.exports = { // pie traces only hoverClosestPie: { + category: 'pie', + group: 'hover', title: 'Toggle show closest data on hover', attr: 'hovermode', val: 'closest', diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 6561656aa4f..f5b75b12e9f 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -13,7 +13,7 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); var Icons = require('../../../build/ploticon'); -var modebarConfig = require('./modebar_config'); +var buttonsConfig = require('./buttons_config'); /** From 189e313f8bb0f5c61d557cc70eab2b511083bca3 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 10:33:22 -0500 Subject: [PATCH 05/36] mv create/update Modebar wrapper in modebar/ : - manageModebar handles logic around which button should appear on the modebar and how to modebar buttons are grouped. --- src/components/modebar/manage.js | 112 ++++++++++++++++++++++++++ src/plot_api/plot_api.js | 11 +-- src/plots/cartesian/graph_interact.js | 93 --------------------- 3 files changed, 116 insertions(+), 100 deletions(-) create mode 100644 src/components/modebar/manage.js diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js new file mode 100644 index 00000000000..976fce07c65 --- /dev/null +++ b/src/components/modebar/manage.js @@ -0,0 +1,112 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); + +var createModebar = require('./'); +var buttonsConfig = require('./buttons_config'); + +/** + * Modebar wrapper around 'create' and 'update', + * chooses buttons to pass to Modebar constructor based on + * plot type and plot config. + * + * @param {object} gd main plot object + * + */ +module.exports = function manageModebar(gd) { + var fullLayout = gd._fullLayout, + context = gd._context, + modebar = fullLayout._modebar; + + if(!context.displayModeBar && modebar) { + modebar.destroy(); + delete fullLayout._modebar; + return; + } + + var buttons = chooseButtons(fullLayout, context.modebarButtons); + + if(modebar) modebar.update(gd, buttons); + else fullLayout._modebar = createModebar(gd, buttons); +}; + +function chooseButtons(fullLayout) { + var buttons = findButtons('all'); + + if(fullLayout._hasGL3D) buttons = buttons.concat(findButtons('gl3d')); + + if(fullLayout._hasGeo) buttons = buttons.concat(findButtons('geo')); + + if(fullLayout._hasCartesian && !areAllAxesFixed(fullLayout)) { + buttons = buttons.concat(findButtons('2d')); + buttons = buttons.concat(findButtons('cartesian')); + } + + if(fullLayout._hasGL2D) { + buttons = buttons.concat(findButtons('2d')); + buttons = buttons.concat(findButtons('gl2d')); + } + + if(fullLayout._hasPie) buttons = buttons.concat(findButtons('pie')); + + buttons = groupButtons(buttons); + + return buttons; +} + +function findButtons(category, list) { + var buttonNames = Object.keys(buttonsConfig); + var out = []; + + for(var i = 0; i < buttonNames.length; i++) { + var buttonName = buttonNames[i]; + if(buttonsConfig[buttonName].category === category) out.push(buttonName); + } + + return out; +} + +function areAllAxesFixed(fullLayout) { + var axList = Plotly.Axes.list({_fullLayout: fullLayout}, null, true); + var allFixed = true; + + for(var i = 0; i < axList.length; i++) { + if(!axList[i].fixedrange) { + allFixed = false; + break; + } + } + + return allFixed; +} + +function groupButtons(buttons) { + var hashObj = {}; + var i; + + for(i = 0; i < buttons.length; i++) { + var button = buttons[i], + group = buttonsConfig[button].group; + + if(hashObj[group] === undefined) hashObj[group] = [button]; + else hashObj[group].push(button); + } + + var groups = Object.keys(hashObj); + var out = []; + + for(i = 0; i < groups.length; i++) { + out.push(hashObj[groups[i]]); + } + + return out; +} diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f95878fcb42..d1cf3064209 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -11,6 +11,7 @@ var Plotly = require('../plotly'); var Events = require('../lib/events'); +var manageModebar = require('../components/modebar/manage'); var d3 = require('d3'); var m4FromQuat = require('gl-mat4/fromQuat'); @@ -399,11 +400,6 @@ function setPlotContext(gd, config) { } }); - // cause a remake of the modebar any time we change context - if(gd._fullLayout && gd._fullLayout._modebar) { - delete gd._fullLayout._modebar; - } - // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility if(config.plot3dPixelRatio && !context.plotGlPixelRatio) { context.plotGlPixelRatio = context.plot3dPixelRatio; @@ -2468,8 +2464,9 @@ Plotly.relayout = function relayout(gd, astr, val) { return plots.previousPromises(gd); }); } + // this is decoupled enough it doesn't need async regardless - if(domodebar) Plotly.Fx.modeBar(gd); + if(domodebar) manageModebar(gd); var subplotIds; if(doSceneDragmode || domodebar) { @@ -3006,7 +3003,7 @@ function lsInner(gd) { Plotly.Titles.draw(gd, 'gtitle'); - Plotly.Fx.modeBar(gd); + manageModebar(gd); return gd._promises.length && Promise.all(gd._promises); } diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index f9b5dbc5a73..867c6069acd 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -1279,99 +1279,6 @@ fx.click = function(gd,evt){ } }; -// dragmode and hovermode toolbars -fx.modeBar = function(gd){ - - function initModebar(){ - - var modebar = new Plotly.ModeBar({ - buttons: buttons, - container: fullLayout._paperdiv.node(), - graphInfo: gd - }); - - if(fullLayout._privateplot) { - d3.select(modebar.element).append('span') - .classed('badge-private float--left', true) - .text('PRIVATE'); - } - - return modebar; - } - - function deleteModebar() { - Plotly.Lib.removeElement(gd.querySelector('.modebar')); - } - - var modebar, - fullLayout = gd._fullLayout || {}; - - if (!gd._context.displayModeBar) return deleteModebar(); - - var buttons = chooseModebarButtons(fullLayout); - - if (!fullLayout._modebar){ - deleteModebar(); - fullLayout._modebar = initModebar(); - } - - modebar = fullLayout._modebar; - - //if the buttons are different, clean old and init new modebar - if (!modebar.hasButtons(buttons)) { - fullLayout._modebar.cleanup(); - fullLayout._modebar = initModebar(); - } -}; - -function chooseModebarButtons(fullLayout) { - if(fullLayout._hasGL3D) { - return [ - ['toImage', 'sendDataToCloud'], - ['orbitRotation', 'tableRotation', 'zoom3d', 'pan3d'], - ['resetCameraDefault3d', 'resetCameraLastSave3d'], - ['hoverClosest3d'] - ]; - } - else if(fullLayout._hasGeo) { - return [ - ['toImage', 'sendDataToCloud'], - ['zoomInGeo', 'zoomOutGeo', 'resetGeo'], - ['hoverClosestGeo'] - ]; - } - - var axList = Plotly.Axes.list({_fullLayout: fullLayout}, null, true), - allFixed = true, - i, - buttons; - - for(i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) { - allFixed = false; - break; - } - } - - if(allFixed) buttons = [['toImage', 'sendDataToCloud']]; - else buttons = [ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'], - ['zoomIn2d', 'zoomOut2d', 'resetScale2d', 'autoScale2d'] - ]; - - if(fullLayout._hasCartesian) { - buttons.push(['hoverClosest2d', 'hoverCompare2d']); - } - else if(fullLayout._hasPie) { - buttons.push(['hoverClosestPie']); - } - else if(fullLayout._hasGL2D) { - buttons.push(['hoverClosestGl2d']); - } - - return buttons; -} // ---------------------------------------------------- // Axis dragging functions From a320f29b3fa3c85f478d2ced0a78aea6956854f2 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 10:34:24 -0500 Subject: [PATCH 06/36] make main modebar module export a creator function --- src/components/modebar/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index f5b75b12e9f..6a980f731ed 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -495,3 +495,22 @@ proto.sendDataToCloud = function() { }; module.exports = ModeBar; +function createModebar(gd, buttons) { + var fullLayout = gd._fullLayout; + + var modebar = new ModeBar({ + graphInfo: gd, + container: fullLayout._paperdiv.node(), + buttons: buttons + }); + + if(fullLayout._privateplot) { + d3.select(modebar.element).append('span') + .classed('badge-private float--left', true) + .text('PRIVATE'); + } + + return modebar; +} + +module.exports = createModebar; From 22a6f3bc5c95dd2ff08cff41e3d13e1fe50392c0 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:09:21 -0500 Subject: [PATCH 07/36] add update method to Modebar: - use it in manageModebar to update buttons based on plot type and config arguments --- src/components/modebar/index.js | 70 ++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 6a980f731ed..d1b43b45d83 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -25,29 +25,65 @@ var buttonsConfig = require('./buttons_config'); * @Param {object} opts.graphInfo primary plot object containing data and layout */ function ModeBar(opts) { - var _this = this; - this._snapshotInProgress = false; - this.graphInfo = opts.graphInfo; + this.container = opts.container; this.element = document.createElement('div'); - if(this.graphInfo._context.displayModeBar === 'hover') { + this.update(opts.graphInfo, opts.buttons); + + this.container.appendChild(this.element); +} + +var proto = ModeBar.prototype; + +/** + * Update modebar (buttons and logo) + * + * @param {object} graphInfo primary plot object containing data and layout + * @param {array of arrays} buttons nested arrays of grouped buttons to initialize + * + */ +proto.update = function(graphInfo, buttons) { + this.graphInfo = graphInfo; + + var context = this.graphInfo._context; + + if(context.displayModeBar === 'hover') { this.element.className = 'modebar modebar--hover'; - } else { - this.element.className = 'modebar'; } + else this.element.className = 'modebar'; + + var needsNewButtons = !this.hasButtons(buttons), + needsNewLogo = (this.hasLogo !== context.displaylogo); + + if(needsNewButtons || needsNewLogo) { + this.removeAllButtons(); + + this.updateButtons(buttons); + + if(context.displaylogo) { + this.element.appendChild(this.getLogo()); + this.hasLogo = true; + } + } + + this.updateActiveButton(); +}; + +proto.updateButtons = function(buttons) { + var _this = this; - this.buttons = opts.buttons; + this.buttons = buttons; this.buttonElements = []; - this.buttons.forEach( function (buttonGroup) { + this.buttons.forEach(function(buttonGroup) { var group = _this.createGroup(); - buttonGroup.forEach( function (buttonName) { - var buttonConfig = modebarConfig[buttonName]; + buttonGroup.forEach(function(buttonName) { + var buttonConfig = buttonsConfig[buttonName]; if (!buttonConfig) { - throw new Error(buttonName + ' not specfied in modebar configuration'); + throw new Error(buttonName + 'not specfied in modebar configuration'); } var button = _this.createButton(buttonConfig); @@ -58,17 +94,7 @@ function ModeBar(opts) { _this.element.appendChild(group); }); - - if (this.graphInfo._context.displaylogo) { - this.element.appendChild(this.getLogo()); - } - - opts.container.appendChild(this.element); - - this.updateActiveButton(); -} - -var proto = ModeBar.prototype; +}; /** * Empty div for containing a group of buttons From 18e09dc5f345723a07426abd49fe266beadcd734 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:11:06 -0500 Subject: [PATCH 08/36] replace Modebar.cleanup with removeAllButtons: - no need to remove entire modebar element when buttons need to be changed. - remove only child elements --- src/components/modebar/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index d1b43b45d83..23f2346926b 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -460,12 +460,6 @@ proto.handleHoverGl2d = function(ev) { }); }; -proto.cleanup = function(){ - this.element.innerHTML = ''; - var modebarParent = this.element.parentNode; - if (modebarParent) modebarParent.removeChild(this.element); -}; - proto.toImage = function() { var format = 'png'; @@ -520,7 +514,13 @@ proto.sendDataToCloud = function() { Plotly.Plots.sendDataToCloud(gd); }; -module.exports = ModeBar; +proto.removeAllButtons = function() { + while(this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + + this.hasLogo = false; +}; function createModebar(gd, buttons) { var fullLayout = gd._fullLayout; From 9a3e45b4018a2492da5019f6fbcc28f072c8a64f Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:11:52 -0500 Subject: [PATCH 09/36] update Modebar.hasButtons for cases where buttons haven't been drawn yet --- src/components/modebar/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 23f2346926b..d9b4e0c80cc 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -206,6 +206,8 @@ proto.updateActiveButton = function(buttonClicked) { proto.hasButtons = function (buttons) { var currentButtons = this.buttons; + if(!currentButtons) return false; + if (buttons.length !== currentButtons.length) return false; for (var i = 0; i < buttons.length; ++i) { From 10d591b5d58f909cc6c5f596553d8fb9e53af0a3 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:12:39 -0500 Subject: [PATCH 10/36] add Modebar.destroy method --- src/components/modebar/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index d9b4e0c80cc..478d9005769 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -223,7 +223,7 @@ proto.hasButtons = function (buttons) { /** * @return {HTMLDivElement} The logo image wrapped in a group */ -proto.getLogo = function(){ +proto.getLogo = function() { var group = this.createGroup(), a = document.createElement('a'); @@ -523,6 +523,11 @@ proto.removeAllButtons = function() { this.hasLogo = false; }; + +proto.destroy = function() { + Plotly.Lib.removeElement(this.container.querySelector('.modebar')); +}; + function createModebar(gd, buttons) { var fullLayout = gd._fullLayout; From 40e3021e59b38fe80db37ac47394a6adb06a15a6 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:16:18 -0500 Subject: [PATCH 11/36] add 'modebarButtonsToRemove' config argument: - adds ability to remove specific modebar buttons by listing their names upon plot configuration - throw error if 'modebarButtonsToRemove' isn't an Array - add filter step to choose button routine, removing buttons that are in 'modebarButtonsToRemove' list. --- src/components/modebar/manage.js | 27 +++++++++++++++++++++++++-- src/plot_api/plot_config.js | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 976fce07c65..52e9fb01f70 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -33,13 +33,20 @@ module.exports = function manageModebar(gd) { return; } - var buttons = chooseButtons(fullLayout, context.modebarButtons); + if(!Array.isArray(context.modebarButtonsToRemove)) { + throw new Error([ + '*modebarButtonsToRemove* configuration options', + 'must be an array.' + ].join(' ')); + } + + var buttons = chooseButtons(fullLayout, context.modebarButtonsToRemove); if(modebar) modebar.update(gd, buttons); else fullLayout._modebar = createModebar(gd, buttons); }; -function chooseButtons(fullLayout) { +function chooseButtons(fullLayout, buttonsToRemove) { var buttons = findButtons('all'); if(fullLayout._hasGL3D) buttons = buttons.concat(findButtons('gl3d')); @@ -58,6 +65,7 @@ function chooseButtons(fullLayout) { if(fullLayout._hasPie) buttons = buttons.concat(findButtons('pie')); + buttons = filterButtons(buttons, buttonsToRemove); buttons = groupButtons(buttons); return buttons; @@ -89,6 +97,21 @@ function areAllAxesFixed(fullLayout) { return allFixed; } +// Remove buttons according to modebarButtonsToRemove plot config options +function filterButtons(buttons, buttonsToRemove) { + var out = []; + + for(var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + + if(buttonsToRemove.indexOf(button) !== -1) continue; + + out.push(button); + } + + return out; +} + function groupButtons(buttons) { var hashObj = {}; var i; diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 0a5a321f828..959688b4257 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -58,6 +58,9 @@ module.exports = { // display the modebar (true, false, or 'hover') displayModeBar: 'hover', + // remove modebar button by name + modebarButtonsToRemove: [], + // add the plotly logo on the end of the modebar displaylogo: true, From a79aba3628c05bfa78e2cfb15887f020b1c1b755 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:17:01 -0500 Subject: [PATCH 12/36] generalize findButtons helper to look for buttons by category and group --- src/components/modebar/manage.js | 49 ++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 52e9fb01f70..7f56ad1ef54 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -47,23 +47,40 @@ module.exports = function manageModebar(gd) { }; function chooseButtons(fullLayout, buttonsToRemove) { - var buttons = findButtons('all'); + var buttons = findButtons({category: 'all'}), + buttons2d = findButtons({category: '2d'}); - if(fullLayout._hasGL3D) buttons = buttons.concat(findButtons('gl3d')); + // TODO how to plots of multiple types? - if(fullLayout._hasGeo) buttons = buttons.concat(findButtons('geo')); + if(fullLayout._hasGL3D) { + buttons = buttons.concat(findButtons({category: 'gl3d'})); + } + + if(fullLayout._hasGeo) { + buttons = buttons.concat(findButtons({category: 'geo'})); + } - if(fullLayout._hasCartesian && !areAllAxesFixed(fullLayout)) { - buttons = buttons.concat(findButtons('2d')); - buttons = buttons.concat(findButtons('cartesian')); + if(fullLayout._hasCartesian) { + if(areAllAxesFixed(fullLayout)) { + buttons = buttons.concat(findButtons({ + category: 'cartesian', + group: 'hover' + })); + } + else { + buttons = buttons.concat(buttons2d); + buttons = buttons.concat(findButtons({category: 'cartesian'})); + } } if(fullLayout._hasGL2D) { - buttons = buttons.concat(findButtons('2d')); - buttons = buttons.concat(findButtons('gl2d')); + buttons = buttons.concat(buttons2d); + buttons = buttons.concat(findButtons({category: 'gl2d'})); } - if(fullLayout._hasPie) buttons = buttons.concat(findButtons('pie')); + if(fullLayout._hasPie) { + buttons = buttons.concat(findButtons({category: 'pie'})); + } buttons = filterButtons(buttons, buttonsToRemove); buttons = groupButtons(buttons); @@ -71,13 +88,21 @@ function chooseButtons(fullLayout, buttonsToRemove) { return buttons; } -function findButtons(category, list) { - var buttonNames = Object.keys(buttonsConfig); +// Find buttons in buttonsConfig by category or group +function findButtons(opts) { + var buttonNames = Object.keys(buttonsConfig), + category = opts.category, + group = opts.group; + var out = []; for(var i = 0; i < buttonNames.length; i++) { var buttonName = buttonNames[i]; - if(buttonsConfig[buttonName].category === category) out.push(buttonName); + + if(category && buttonsConfig[buttonName].category !== category) continue; + if(group && buttonsConfig[buttonName].group !== group) continue; + + out.push(buttonName); } return out; From aac197e507cff69e9b463a5cc2396287ff66b5b4 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:17:17 -0500 Subject: [PATCH 13/36] lint --- src/components/modebar/index.js | 1 + src/plot_api/plot_api.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 478d9005769..f3a2edfa08c 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -200,6 +200,7 @@ proto.updateActiveButton = function(buttonClicked) { /** * Check if modebar is configured as button configuration argument + * * @Param {object} buttons 2d array of grouped button names * @Return {boolean} */ diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d1cf3064209..ef38b16800b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -32,7 +32,7 @@ var plots = Plotly.Plots; * object describing the overall display of the plot, * all the stuff that doesn't pertain to any individual trace * @param {object} config - * configuration options + * configuration options (see ./plot_config.js for more info) * */ Plotly.plot = function(gd, data, layout, config) { From b826ddf9821387f571d70a235a7709bc440fe327 Mon Sep 17 00:00:00 2001 From: etpinard Date: Fri, 20 Nov 2015 17:17:38 -0500 Subject: [PATCH 14/36] update / add modebar jasmine tests --- test/jasmine/tests/modebar_test.js | 266 ++++++++++++++++++++++------- 1 file changed, 209 insertions(+), 57 deletions(-) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index b1b70c7319e..a03f370bcf9 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -1,97 +1,249 @@ -var Plotly = require('@src/plotly'); +var d3 = require('d3'); -describe('Test Modebar', function() { +var createModebar = require('@src/components/modebar'); +var manageModebar = require('@src/components/modebar/manage'); + + +describe('Modebar', function() { 'use strict'; - var getMockGraphInfo = function() { - var graphInfo = { + function getMockContainerTree() { + var root = document.createElement('div'); + root.className = 'plot-container'; + var parent = document.createElement('div'); + parent.className = 'svg-container'; + root.appendChild(parent); + + return parent; + } + + function getMockGraphInfo() { + return { _fullLayout: { - dragmode: 'zoom' + dragmode: 'zoom', + _paperdiv: d3.select(getMockContainerTree()) }, _context: { + displaylogo: true, displayModeBar: true, - displaylogo: true + modebarButtonsToRemove: [] } }; - return graphInfo; - }; + } - var getMockContainerTree = function() { - var root = document.createElement('div'); - root.className = 'plot-container'; - var parent = document.createElement('div'); - parent.className = 'svg-container'; - root.appendChild(parent); - return parent; - }; + function countGroups(modebar) { + return d3.select(modebar.element).selectAll('div.modebar-group')[0].length; + } + + function countButtons(modebar) { + return d3.select(modebar.element).selectAll('a.modebar-btn')[0].length; + } + + function countLogo(modebar) { + return d3.select(modebar.element).selectAll('a.plotlyjsicon')[0].length; + } - var createModebar = function(buttons) { - var container = getMockContainerTree(), - graphInfo = getMockGraphInfo(); + var buttons = [['toImage', 'sendDataToCloud']]; + var modebar = createModebar(getMockGraphInfo(), buttons); - var modebar = new Plotly.ModeBar({ - buttons: buttons, - container: container, - Plotly: Plotly, - graphInfo: graphInfo + describe('createModebar', function() { + it('creates a modebar', function() { + expect(countGroups(modebar)).toEqual(2); + expect(countButtons(modebar)).toEqual(3); + expect(countLogo(modebar)).toEqual(1); }); - return modebar; - }; + }); - describe('Test modebarCleanup:', function() { + describe('modebar.removeAllButtons', function() { + it('removes all modebar buttons', function() { + modebar.removeAllButtons(); - it('should make a cleanup.', function() { - var buttons = [['zoom2d']]; - var modebar = createModebar(buttons); - var modebarParent = modebar.element.parentNode; - modebar.cleanup(); expect(modebar.element.innerHTML).toEqual(''); - expect(modebarParent.querySelector('.modebar')) - .toBeNull(); + expect(modebar.hasLogo).toBe(false); }); }); - describe('Test modebarHasButtons:', function() { + describe('modebar.destroy', function() { + it('removes the modebar entirely', function() { + var modebarParent = modebar.element.parentNode; - var modeButtons2d, - modeButtons3d; + modebar.destroy(); - // Same as in ../graph_interact.js - beforeEach( function() { - modeButtons2d = [ - ['toImage'], + expect(modebarParent.querySelector('.modebar')).toBeNull(); + }); + }); + + describe('manageModebar', function() { + + it('creates modebar (cartesian version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], - ['zoomIn2d', 'zoomOut2d', 'resetScale2d', 'autoScale2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], ['hoverClosest2d', 'hoverCompare2d'] ]; - modeButtons3d = [ - ['toImage'], - ['orbitRotation', 'tableRotation', 'zoom3d', 'pan3d'], + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullLayout.xaxis = {fixedrange: false}; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(5); + expect(countButtons(modebar)).toEqual(11); + expect(countLogo(modebar)).toEqual(1); + }); + + it('creates modebar (cartesian fixed-axes version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], + ['hoverClosest2d', 'hoverCompare2d'] + ]; + + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(3); + expect(countButtons(modebar)).toEqual(5); + expect(countLogo(modebar)).toEqual(1); + }); + + it('creates modebar (gl3d version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], + ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], ['resetCameraDefault3d', 'resetCameraLastSave3d'], ['hoverClosest3d'] ]; + + var gd = getMockGraphInfo(); + gd._fullLayout._hasGL3D = true; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(5); + expect(countButtons(modebar)).toEqual(10); + expect(countLogo(modebar)).toEqual(1); + }); + + it('creates modebar (geo version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], + ['zoomInGeo', 'zoomOutGeo', 'resetGeo'], + ['hoverClosestGeo'] + ]; + + var gd = getMockGraphInfo(); + gd._fullLayout._hasGeo = true; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(4); + expect(countButtons(modebar)).toEqual(7); + expect(countLogo(modebar)).toEqual(1); + }); + + it('creates modebar (gl2d version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], + ['hoverClosestGl2d'] + ]; + + var gd = getMockGraphInfo(); + gd._fullLayout._hasGL2D = true; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(5); + expect(countButtons(modebar)).toEqual(10); + expect(countLogo(modebar)).toEqual(1); + }); + + it('creates modebar (pie version)', function() { + var buttons = [ + ['toImage', 'sendDataToCloud'], + ['hoverClosestPie'] + ]; + + var gd = getMockGraphInfo(); + gd._fullLayout._hasPie = true; + + manageModebar(gd); + var modebar = gd._fullLayout._modebar; + + expect(modebar.hasButtons(buttons)).toBe(true); + expect(countGroups(modebar)).toEqual(3); + expect(countButtons(modebar)).toEqual(4); + expect(countLogo(modebar)).toEqual(1); }); - it('should return true going from 3D -> 3D buttons.', function() { - var modebar = createModebar(modeButtons3d); - expect(modebar.hasButtons(modeButtons3d)).toBe(true); + it('throws an error if modebarButtonsToRemove isn\'t an array', function() { + var gd = getMockGraphInfo(); + gd._context.modebarButtonsToRemove = 'not gonna work'; + + expect(function() { manageModebar(gd); }).toThrowError(); }); - it('should return true going from 2D -> 2D buttons.', function() { - var modebar = createModebar(modeButtons2d); - expect(modebar.hasButtons(modeButtons2d)).toBe(true); + it('displays or not modebar according to displayModeBar config arg', function() { + var gd = getMockGraphInfo(); + manageModebar(gd); + expect(gd._fullLayout._modebar).toBeDefined(); + + gd._context.displayModeBar = false; + manageModebar(gd); + expect(gd._fullLayout._modebar).not.toBeDefined(); }); - it('should return false going from 2D -> 3D buttons.', function() { - var modebar = createModebar(modeButtons2d); - expect(modebar.hasButtons(modeButtons3d)).toBe(false); + it('displays or not logo according to displaylogo config arg', function() { + var gd = getMockGraphInfo(); + manageModebar(gd); + expect(countLogo(gd._fullLayout._modebar)).toEqual(1); + + gd._context.displaylogo = false; + manageModebar(gd); + expect(countLogo(gd._fullLayout._modebar)).toEqual(0); }); - it('should return false going from 3D -> 2D buttons.', function() { - var modebar = createModebar(modeButtons3d); - expect(modebar.hasButtons(modeButtons2d)).toBe(false); + it('updates modebar buttons if plot type changes', function() { + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullLayout.xaxis = {fixedrange: false}; + + manageModebar(gd); // gives 11 buttons + gd._fullLayout._hasCartesian = false; + gd._fullLayout._hasGL3D = true; + manageModebar(gd); + + expect(countButtons(gd._fullLayout._modebar)).toEqual(10); + }); + + it('updates modebar buttons if plot type changes', function() { + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullLayout.xaxis = {fixedrange: false}; + + manageModebar(gd); // gives 11 buttons + gd._context.modebarButtonsToRemove = ['toImage', 'sendDataToCloud']; + manageModebar(gd); + + expect(countButtons(gd._fullLayout._modebar)).toEqual(9); }); + }); + }); From b3fdb5dffb82366ce9de129b6a4a23627cc86afa Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:36:55 -0500 Subject: [PATCH 15/36] add click handler definition in modebar button config: - click handler are function of (modabar, eventObject) --- src/components/modebar/buttons.js | 495 +++++++++++++++++++++++ src/components/modebar/buttons_config.js | 257 ------------ src/components/modebar/index.js | 279 ------------- src/components/modebar/manage.js | 2 +- 4 files changed, 496 insertions(+), 537 deletions(-) create mode 100644 src/components/modebar/buttons.js delete mode 100644 src/components/modebar/buttons_config.js diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js new file mode 100644 index 00000000000..13866ac96da --- /dev/null +++ b/src/components/modebar/buttons.js @@ -0,0 +1,495 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); +var Lib = require('../../lib'); +var Snapshot = require('../../snapshot'); + + +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 + * @param {string} icon + * name of the svg icon associated with the button + * @param {string} [gravity] + * icon positioning + * @param {function} click + * click handler associated with the button + * @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 graphInfo + * @param {boolean} [toggle] + * is the button a toggle button? + */ + +modebarButtons.toImage = { + name: 'toImage', + title: 'Download plot as a png', + icon: 'camera', + click: function(modebar) { + var format = 'png'; + + if (Lib.isIE()) { + Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' + + 'Consider exporting your images using the Plotly Cloud', 'long'); + return; + } + + if (modebar._snapshotInProgress) { + Lib.notifier('Snapshotting is still in progress - please hold', 'long'); + return; + } + + modebar._snapshotInProgress = true; + Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); + + var ev = Snapshot.toImage(modebar.graphInfo, {format: format}); + + var filename = modebar.graphInfo.fn || 'newplot'; + filename += '.' + format; + + ev.once('success', function(result) { + + modebar._snapshotInProgress = false; + + var downloadLink = document.createElement('a'); + downloadLink.href = result; + downloadLink.download = filename; // only supported by FF and Chrome + + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + ev.clean(); + }); + + ev.once('error', function (err) { + modebar._snapshotInProgress = false; + + Lib.notifier('Sorry there was a problem downloading your ' + format, 'long'); + console.error(err); + + ev.clean(); + }); + } +}; + +modebarButtons.sendDataToCloud = { + name: 'sendDataToCloud', + title: 'Save and edit plot in cloud', + icon: 'disk', + click: function(modebar) { + var gd = modebar.graphInfo; + Plotly.Plots.sendDataToCloud(gd); + } +}; + +modebarButtons.zoom2d = { + name: 'zoom2d', + title: 'Zoom', + attr: 'dragmode', + val: 'zoom', + icon: 'zoombox', + click: handleCartesian +}; + +modebarButtons.pan2d = { + name: 'pan2d', + title: 'Pan', + attr: 'dragmode', + val: 'pan', + icon: 'pan', + click: handleCartesian +}; + +modebarButtons.zoomIn2d = { + name: 'zoomIn2d', + title: 'Zoom in', + attr: 'zoom', + val: 'in', + icon: 'zoom_plus', + click: handleCartesian +}; + +modebarButtons.zoomOut2d = { + name: 'zoomOut2d', + title: 'Zoom out', + attr: 'zoom', + val: 'out', + icon: 'zoom_minus', + click: handleCartesian +}; + +modebarButtons.autoScale2d = { + name: 'autoScale2d', + title: 'Autoscale', + attr: 'zoom', + val: 'auto', + icon: 'autoscale', + click: handleCartesian +}; + +modebarButtons.resetScale2d = { + name: 'resetScale2d', + title: 'Reset axes', + attr: 'zoom', + val: 'reset', + icon: 'home', + click: handleCartesian +}; + +modebarButtons.hoverClosestCartesian = { + name: 'hoverClosestCartesian', + title: 'Show closest data on hover', + attr: 'hovermode', + val: 'closest', + icon: 'tooltip_basic', + gravity: 'ne', + click: handleCartesian +}; + +modebarButtons.hoverCompareCartesian = { + name: 'hoverCompareCartesian', + title: 'Compare data on hover', + attr: 'hovermode', + val: function(graphInfo) { + return graphInfo._fullLayout._isHoriz ? 'y' : 'x'; + }, + icon: 'tooltip_compare', + gravity: 'ne', + click: handleCartesian +}; + +function handleCartesian(modebar, ev) { + var button = ev.currentTarget, + astr = button.getAttribute('data-attr'), + val = button.getAttribute('data-val') || true, + graphInfo = modebar.graphInfo, + fullLayout = graphInfo._fullLayout, + aobj = {}; + + if(astr === 'zoom') { + var mag = (val === 'in') ? 0.5 : 2, + r0 = (1 + mag) / 2, + r1 = (1 - mag) / 2, + axList = Plotly.Axes.list(graphInfo, null, true); + + var ax, axName, initialRange; + + for(var i = 0; i < axList.length; i++) { + ax = axList[i]; + if(!ax.fixedrange) { + axName = ax._name; + if(val === 'auto') aobj[axName + '.autorange'] = true; + else if(val === 'reset') { + if(ax._rangeInitial === undefined) { + aobj[axName + '.autorange'] = true; + } + else aobj[axName + '.range'] = ax._rangeInitial.slice(); + } + else { + initialRange = ax.range; + aobj[axName + '.range'] = [ + r0 * initialRange[0] + r1 * initialRange[1], + r0 * initialRange[1] + r1 * initialRange[0] + ]; + } + } + } + } 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; + } + + Plotly.relayout(graphInfo, aobj).then( function() { + modebar.updateActiveButton(); + if(astr === 'dragmode') { + if(fullLayout._hasCartesian) { + Plotly.Fx.setCursor( + fullLayout._paper.select('.nsewdrag'), + {pan:'move', zoom:'crosshair'}[val] + ); + } + Plotly.Fx.supplyLayoutDefaults(graphInfo.layout, fullLayout, + graphInfo._fullData); + } + }); +} + +modebarButtons.zoom3d = { + name: 'zoom3d', + title: 'Zoom', + attr: 'dragmode', + val: 'zoom', + icon: 'zoombox', + click: handleDrag3d +}; + +modebarButtons.pan3d = { + name: 'pan3d', + title: 'Pan', + attr: 'dragmode', + val: 'pan', + icon: 'pan', + click: handleDrag3d +}; + +modebarButtons.orbitRotation = { + name: 'orbitRotation', + title: 'orbital rotation', + attr: 'dragmode', + val: 'orbit', + icon: '3d_rotate', + click: handleDrag3d +}; + +modebarButtons.tableRotation = { + name: 'tableRotation', + title: 'turntable rotation', + attr: 'dragmode', + val: 'turntable', + icon: 'z-axis', + click: handleDrag3d +}; + +function handleDrag3d(modebar, ev) { + var button = ev.currentTarget, + attr = button.getAttribute('data-attr'), + val = button.getAttribute('data-val') || true, + graphInfo = modebar.graphInfo, + layoutUpdate = {}; + + layoutUpdate[attr] = val; + + /* + * Dragmode will go through the relayout -> doplot -> scene.plot() + * routine where the dragmode will be set in scene.plot() + */ + Plotly.relayout(graphInfo, layoutUpdate).then( function() { + modebar.updateActiveButton(); + }); +} + +modebarButtons.resetCameraDefault3d = { + name: 'resetCameraDefault3d', + title: 'Reset camera to default', + attr: 'resetDefault', + icon: 'home', + click: handleCamera3d +}; + +modebarButtons.resetCameraLastSave3d = { + name: 'resetCameraLastSave3d', + title: 'Reset camera to last save', + attr: 'resetLastSave', + icon: 'movie', + click: handleCamera3d +}; + +function handleCamera3d(modebar, ev) { + var button = ev.currentTarget, + attr = button.getAttribute('data-attr'), + layout = modebar.graphInfo.layout, + fullLayout = modebar.graphInfo._fullLayout, + sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); + + for(var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + sceneLayout = layout[sceneId], + fullSceneLayout = fullLayout[sceneId], + scene = fullSceneLayout._scene; + + if(!sceneLayout || attr==='resetDefault') scene.setCameraToDefault(); + else if(attr === 'resetLastSave') { + + var cameraPos = sceneLayout.camera; + if(cameraPos) scene.setCamera(cameraPos); + else scene.setCameraToDefault(); + } + } + + /* + * TODO have a sceneLastTouched in _fullLayout to only + * update the camera of the scene last touched by the user + */ +} + +modebarButtons.hoverClosest3d = { + name: 'hoverClosest3d', + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: function(modebar, ev) { + var button = ev.currentTarget, + val = JSON.parse(button.getAttribute('data-val')) || false, + graphInfo = modebar.graphInfo, + fullLayout = graphInfo._fullLayout, + sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); + + var axes = ['xaxis', 'yaxis', 'zaxis'], + spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; + + // initialize 'current spike' object to be stored in the DOM + var currentSpikes = {}, + axisSpikes = {}, + layoutUpdate = {}; + + if(val) { + layoutUpdate = val; + button.setAttribute('data-val', JSON.stringify(null)); + } + else { + layoutUpdate = {'allaxes.showspikes': false}; + + for(var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + sceneLayout = fullLayout[sceneId], + sceneSpikes = currentSpikes[sceneId] = {}; + + // copy all the current spike attrs + for(var j = 0; j < 3; j++) { + var axis = axes[j]; + axisSpikes = sceneSpikes[axis] = {}; + + for(var k = 0; k < spikeAttrs.length; k++) { + var spikeAttr = spikeAttrs[k]; + axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; + } + } + } + + button.setAttribute('data-val', JSON.stringify(currentSpikes)); + } + + Plotly.relayout(graphInfo, layoutUpdate).then(function() { + modebar.updateActiveButton(button); + }); + } +}; + +modebarButtons.zoomInGeo = { + name: 'zoomInGeo', + title: 'Zoom in', + attr: 'zoom', + val: 'in', + icon: 'zoom_plus', + click: handleGeo +}; + +modebarButtons.zoomOutGeo = { + name: 'zoomOutGeo', + title: 'Zoom in', + attr: 'zoom', + val: 'out', + icon: 'zoom_minus', + click: handleGeo +}; + +modebarButtons.resetGeo = { + name: 'resetGeo', + title: 'Reset', + attr: 'reset', + val: null, + icon: 'autoscale', + click: handleGeo +}; + +modebarButtons.hoverClosestGeo = { + name: 'hoverClosestGeo', + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: handleGeo +}; + +function handleGeo(modebar, ev) { + var button = ev.currentTarget, + attr = button.getAttribute('data-attr'), + val = button.getAttribute('data-val') || true, + fullLayout = modebar.graphInfo._fullLayout, + geoIds = Plotly.Plots.getSubplotIds(fullLayout, 'geo'); + + for(var i = 0; i < geoIds.length; i++) { + var geo = fullLayout[geoIds[i]]._geo; + + if(attr === 'zoom') { + var scale = geo.projection.scale(); + var newScale = (val === 'in') ? 2 * scale : 0.5 * scale; + geo.projection.scale(newScale); + geo.zoom.scale(newScale); + geo.render(); + } + else if(attr === 'reset') geo.zoomReset(); + else if(attr === 'hovermode') geo.showHover = !geo.showHover; + } + + modebar.updateActiveButton(button); +} + +modebarButtons.hoverClosestGl2d = { + name: 'hoverClosestGl2d', + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: 'tooltip_basic', + gravity: 'ne', + click: function(modebar, ev) { + var button = ev.currentTarget, + graphInfo = modebar.graphInfo, + newHover = graphInfo._fullLayout.hovermode ? + false : + 'closest'; + + Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { + modebar.updateActiveButton(button); + }); + } +}; + +modebarButtons.hoverClosestPie = { + name: 'hoverClosestPie', + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: 'closest', + icon: 'tooltip_basic', + gravity: 'ne', + click: function(modebar) { + var graphInfo = modebar.graphInfo, + newHover = graphInfo._fullLayout.hovermode ? + false : + 'closest'; + + Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { + modebar.updateActiveButton(); + }); + + } +}; diff --git a/src/components/modebar/buttons_config.js b/src/components/modebar/buttons_config.js deleted file mode 100644 index 05cad8e6ee9..00000000000 --- a/src/components/modebar/buttons_config.js +++ /dev/null @@ -1,257 +0,0 @@ -/** -* Copyright 2012-2015, 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'; - -/** - * Modebar buttons configuration - * - * @param {string} category button category depending on e.g. plot type - * @param {string} group button group ('ext', 'drag', 'zoom', 'hover') - * @param {string} title text that appears while hovering over the button - * @param {string} icon name of the svg icon associated with the button - * @param {string} [gravity] icon positioning - * @param {string} click name of the modebar click handler associated with the button - * @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 graphInfo - * @param {boolean} [toggle] is the button a toggle button? - * - */ - -module.exports = { - - // for all plot types - toImage: { - category: 'all', - group: 'ext', - title: 'download plot as a png', - icon: 'camera', - click: 'toImage' - }, - sendDataToCloud: { - category: 'all', - group: 'ext', - title: 'save and edit plot in cloud', - icon: 'disk', - click: 'sendDataToCloud' - }, - - // cartesian and gl2d - zoom2d: { - category: '2d', - group: 'drag', - title: 'Zoom', - attr: 'dragmode', - val: 'zoom', - icon: 'zoombox', - click: 'handleCartesian' - }, - pan2d: { - category: '2d', - group: 'drag', - title: 'Pan', - attr: 'dragmode', - val: 'pan', - icon: 'pan', - click: 'handleCartesian' - }, - zoomIn2d: { - category: '2d', - group: 'zoom', - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: 'zoom_plus', - click: 'handleCartesian' - }, - zoomOut2d: { - category: '2d', - group: 'zoom', - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: 'zoom_minus', - click: 'handleCartesian' - }, - autoScale2d: { - category: '2d', - group: 'zoom', - title: 'Autoscale', - attr: 'zoom', - val: 'auto', - icon: 'autoscale', - click: 'handleCartesian' - }, - resetScale2d: { - category: '2d', - group: 'zoom', - title: 'Reset axes', - attr: 'zoom', - val: 'reset', - icon: 'home', - click: 'handleCartesian' - }, - - // cartesian only - hoverClosest2d: { - category: 'cartesian', - group: 'hover', - title: 'Show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: 'tooltip_basic', - gravity: 'ne', - click: 'handleCartesian' - }, - hoverCompare2d: { - category: 'cartesian', - group: 'hover', - title: 'Compare data on hover', - attr: 'hovermode', - val: function(graphInfo) { - return graphInfo._fullLayout._isHoriz ? 'y' : 'x'; - }, - icon: 'tooltip_compare', - gravity: 'ne', - click: 'handleCartesian' - }, - - // gl3d - zoom3d: { - category: 'gl3d', - group: 'drag', - title: 'Zoom', - attr: 'dragmode', - val: 'zoom', - icon: 'zoombox', - click: 'handleDrag3d' - }, - pan3d: { - category: 'gl3d', - group: 'drag', - title: 'Pan', - attr: 'dragmode', - val: 'pan', - icon: 'pan', - click: 'handleDrag3d' - }, - orbitRotation: { - category: 'gl3d', - group: 'drag', - title: 'orbital rotation', - attr: 'dragmode', - val: 'orbit', - icon: '3d_rotate', - click: 'handleDrag3d' - }, - tableRotation: { - category: 'gl3d', - group: 'drag', - title: 'turntable rotation', - attr: 'dragmode', - val: 'turntable', - icon: 'z-axis', - click: 'handleDrag3d' - }, - resetCameraDefault3d: { - category: 'gl3d', - group: 'zoom', - title: 'Reset camera to default', - attr: 'resetDefault', - icon: 'home', - click: 'handleCamera3d' - }, - resetCameraLastSave3d: { - category: 'gl3d', - group: 'zoom', - title: 'Reset camera to last save', - attr: 'resetLastSave', - icon: 'movie', - click: 'handleCamera3d' - }, - hoverClosest3d: { - category: 'gl3d', - group: 'hover', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: 'handleHover3d' - }, - - // geo - zoomInGeo: { - category: 'geo', - group: 'zoom', - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: 'zoom_plus', - click: 'handleGeo' - }, - zoomOutGeo: { - category: 'geo', - group: 'zoom', - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: 'zoom_minus', - click: 'handleGeo' - }, - resetGeo: { - category: 'geo', - group: 'zoom', - title: 'Reset', - attr: 'reset', - val: null, - icon: 'autoscale', - click: 'handleGeo' - }, - hoverClosestGeo: { - category: 'geo', - group: 'hover', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: 'handleGeo' - }, - - // gl2d only - hoverClosestGl2d: { - category: 'gl2d', - group: 'hover', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: 'tooltip_basic', - gravity: 'ne', - click: 'handleHoverGl2d' - }, - - // pie traces only - hoverClosestPie: { - category: 'pie', - group: 'hover', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: 'tooltip_basic', - gravity: 'ne', - click: 'handleHoverPie' - } - -}; diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index f3a2edfa08c..3c15fb7b0ff 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -13,7 +13,6 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); var Icons = require('../../../build/ploticon'); -var buttonsConfig = require('./buttons_config'); /** @@ -239,284 +238,6 @@ proto.getLogo = function() { return group; }; -/** - * Apply D3 cartesian mode attributes to layout to update hover functionality - * @Param {object} ev event object - */ -proto.handleCartesian = function(ev) { - var button = ev.currentTarget, - astr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - _this = this, - graphInfo = this.graphInfo, - fullLayout = this.graphInfo._fullLayout, - aobj = {}; - - if(astr === 'zoom') { - var mag = (val === 'in') ? 0.5 : 2, - r0 = (1 + mag) / 2, - r1 = (1 - mag) / 2, - axList = Plotly.Axes.list(graphInfo, null, true); - - var ax, axName, initialRange; - - for(var i = 0; i < axList.length; i++) { - ax = axList[i]; - if(!ax.fixedrange) { - axName = ax._name; - if(val === 'auto') aobj[axName + '.autorange'] = true; - else if(val === 'reset') { - if(ax._rangeInitial === undefined) { - aobj[axName + '.autorange'] = true; - } - else aobj[axName + '.range'] = ax._rangeInitial.slice(); - } - else { - initialRange = ax.range; - aobj[axName + '.range'] = [ - r0 * initialRange[0] + r1 * initialRange[1], - r0 * initialRange[1] + r1 * initialRange[0] - ]; - } - } - } - } 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; - } - - Plotly.relayout(graphInfo, aobj).then( function() { - _this.updateActiveButton(); - if(astr === 'dragmode') { - if(fullLayout._hasCartesian) { - Plotly.Fx.setCursor( - fullLayout._paper.select('.nsewdrag'), - {pan:'move', zoom:'crosshair'}[val] - ); - } - Plotly.Fx.supplyLayoutDefaults(graphInfo.layout, fullLayout, - graphInfo._fullData); - } - }); -}; - -/** - * Toggle the data hover mode - * @Param {object} ev event object - */ -proto.handleHover3d = function(ev) { - var button = ev.currentTarget, - val = JSON.parse(button.getAttribute('data-val')) || false, - _this = this, - graphInfo = this.graphInfo, - fullLayout = graphInfo._fullLayout, - sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'), - layoutUpdate = {}, - - // initialize 'current spike' object to be stored in the DOM - currentSpikes = {}, - axes = ['xaxis', 'yaxis', 'zaxis'], - spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; - - var i, sceneId, sceneLayout, sceneSpikes; - var j, axis, axisSpikes; - var k, spikeAttr; - - if (val) { - layoutUpdate = val; - button.setAttribute('data-val', JSON.stringify(null)); - } - else { - layoutUpdate = {'allaxes.showspikes': false}; - - for (i = 0; i < sceneIds.length; i++) { - sceneId = sceneIds[i]; - sceneLayout = fullLayout[sceneId]; - sceneSpikes = currentSpikes[sceneId] = {}; - - // copy all the current spike attrs - for (j = 0; j < 3; j++) { - axis = axes[j]; - axisSpikes = sceneSpikes[axis] = {}; - for (k = 0; k < spikeAttrs.length; k++) { - spikeAttr = spikeAttrs[k]; - axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; - } - } - } - - button.setAttribute('data-val', JSON.stringify(currentSpikes)); - } - - Plotly.relayout(graphInfo, layoutUpdate).then( function() { - _this.updateActiveButton(button); - }); - -}; - -/** - * Reconfigure keyboard bindings for webgl3D camera control on drag - * @Param {object} ev event object - */ -proto.handleDrag3d = function(ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - _this = this, - graphInfo = this.graphInfo, - layoutUpdate = {}; - - layoutUpdate[attr] = val; - - // Dragmode will go through the relayout->doplot->scene.plot() - // routine where the dragmode will be set in scene.plot() - Plotly.relayout(graphInfo, layoutUpdate).then( function() { - _this.updateActiveButton(); - }); -}; - - -/** - * Reset the position of the webgl3D camera - * @Param {object} ev event object - */ -proto.handleCamera3d = function(ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - layout = this.graphInfo.layout, - fullLayout = this.graphInfo._fullLayout, - sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); - - var i, sceneId, sceneLayout, fullSceneLayout, scene, cameraPos; - - for (i = 0; i < sceneIds.length; i++) { - sceneId = sceneIds[i]; - sceneLayout = layout[sceneId]; - fullSceneLayout = fullLayout[sceneId]; - scene = fullSceneLayout._scene; - - if (!sceneLayout || attr==='resetDefault') scene.setCameraToDefault(); - else if (attr === 'resetLastSave') { - - cameraPos = sceneLayout.camera; - if (cameraPos) scene.setCamera(cameraPos); - else scene.setCameraToDefault(); - } - } - - /* TODO have a sceneLastTouched in _fullLayout to only - * update the camera of the scene last touched by the user - */ -}; - -proto.handleGeo = function(ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = this.graphInfo._fullLayout, - geoIds = Plotly.Plots.getSubplotIds(fullLayout, 'geo'); - - var geo, scale, newScale; - - for(var i = 0; i < geoIds.length; i++) { - geo = fullLayout[geoIds[i]]._geo; - - if(attr === 'zoom') { - scale = geo.projection.scale(); - newScale = val==='in' ? 2 * scale : 0.5 * scale; - geo.projection.scale(newScale); - geo.zoom.scale(newScale); - geo.render(); - } - else if(attr === 'reset') geo.zoomReset(); - else if(attr === 'hovermode') geo.showHover = !geo.showHover; - } - - this.updateActiveButton(button); -}; - -proto.handleHoverPie = function() { - var _this = this, - graphInfo = _this.graphInfo, - newHover = graphInfo._fullLayout.hovermode ? - false : - 'closest'; - - Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { - _this.updateActiveButton(); - }); -}; - -proto.handleHoverGl2d = function(ev) { - var _this = this, - button = ev.currentTarget, - graphInfo = _this.graphInfo, - newHover = graphInfo._fullLayout.hovermode ? false : 'closest'; - - Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { - _this.updateActiveButton(button); - }); -}; - -proto.toImage = function() { - - var format = 'png'; - var _this = this; - - if ( Plotly.Lib.isIE() ) { - Plotly.Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' + - 'Consider exporting your images using the Plotly Cloud', 'long'); - return; - } - - if (this._snapshotInProgress) { - Plotly.Lib.notifier('Snapshotting is still in progress - please hold', 'long'); - return; - } - - this._snapshotInProgress = true; - Plotly.Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); - - var ev = Plotly.Snapshot.toImage(this.graphInfo, {format: format}); - - var filename = this.graphInfo.fn || 'newplot'; - filename += '.' + format; - - ev.once('success', function(result) { - - _this._snapshotInProgress = false; - - var downloadLink = document.createElement('a'); - downloadLink.href = result; - downloadLink.download = filename; // only supported by FF and Chrome - - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - - ev.clean(); - }); - - ev.once('error', function (err) { - _this._snapshotInProgress = false; - - Plotly.Lib.notifier('Sorry there was a problem downloading your ' + format, 'long'); - console.error(err); - - ev.clean(); - }); -}; - -proto.sendDataToCloud = function() { - var gd = this.graphInfo; - Plotly.Plots.sendDataToCloud(gd); -}; - proto.removeAllButtons = function() { while(this.element.firstChild) { this.element.removeChild(this.element.firstChild); diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 7f56ad1ef54..b8512a70514 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -12,7 +12,7 @@ var Plotly = require('../../plotly'); var createModebar = require('./'); -var buttonsConfig = require('./buttons_config'); +var modebarButtons = require('./buttons'); /** * Modebar wrapper around 'create' and 'update', From 74350d661927932d463cd9ff356743de674c5d0c Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:40:05 -0500 Subject: [PATCH 16/36] make Modebar constructor not rely on button config in scope: - each button's config is passed to the constructor on init. --- src/components/modebar/index.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 3c15fb7b0ff..978cc91bfca 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -19,7 +19,7 @@ var Icons = require('../../../build/ploticon'); * UI controller for interactive plots * @Class * @Param {object} opts - * @Param {object} opts.buttons nested arrays of grouped buttons to initialize + * @Param {object} opts.buttons nested arrays of grouped buttons config objects * @Param {object} opts.container container div to append modebar * @Param {object} opts.graphInfo primary plot object containing data and layout */ @@ -52,6 +52,7 @@ proto.update = function(graphInfo, buttons) { } else this.element.className = 'modebar'; + // if buttons or logo have changed, redraw modebar interior var needsNewButtons = !this.hasButtons(buttons), needsNewLogo = (this.hasLogo !== context.displaylogo); @@ -78,15 +79,10 @@ proto.updateButtons = function(buttons) { this.buttons.forEach(function(buttonGroup) { var group = _this.createGroup(); - buttonGroup.forEach(function(buttonName) { - var buttonConfig = buttonsConfig[buttonName]; - - if (!buttonConfig) { - throw new Error(buttonName + 'not specfied in modebar configuration'); + buttonGroup.forEach(function(buttonConfig) { } var button = _this.createButton(buttonConfig); - _this.buttonElements.push(button); group.appendChild(button); }); @@ -108,7 +104,7 @@ proto.createGroup = function () { /** * Create a new button div and set constant and configurable attributes - * @Param {object} config (see ./modebar_config,js for more info) + * @Param {object} config (see ./buttons.js for more info) * @Return {HTMLelement} */ proto.createButton = function (config) { From da787d7d5aaf03ce35fe5773cc2307d1688190b2 Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:41:15 -0500 Subject: [PATCH 17/36] update modebar manager: - the manager main purpose now is to pass the appropriate button groups and configs to the modebar constructor depending on the plot type. --- src/components/modebar/manage.js | 125 ++++++++++--------------------- 1 file changed, 39 insertions(+), 86 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index b8512a70514..0d45d5c386c 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -40,72 +40,62 @@ module.exports = function manageModebar(gd) { ].join(' ')); } - var buttons = chooseButtons(fullLayout, context.modebarButtonsToRemove); + var buttonGroups = getButtonGroups(fullLayout, context.modebarButtonsToRemove); - if(modebar) modebar.update(gd, buttons); - else fullLayout._modebar = createModebar(gd, buttons); + if(modebar) modebar.update(gd, buttonGroups); + else fullLayout._modebar = createModebar(gd, buttonGroups); }; -function chooseButtons(fullLayout, buttonsToRemove) { - var buttons = findButtons({category: 'all'}), - buttons2d = findButtons({category: '2d'}); +// logic behind which buttons are displayed by default +function getButtonGroups(fullLayout, buttonsToRemove) { + var groups = []; - // TODO how to plots of multiple types? + function addGroup(newGroup) { + var out = []; - if(fullLayout._hasGL3D) { - buttons = buttons.concat(findButtons({category: 'gl3d'})); - } + for(var i = 0; i < newGroup.length; i++) { + var button = newGroup[i]; + if(buttonsToRemove.indexOf(button) !== -1) continue; + out.push(modebarButtons[button]); + } - if(fullLayout._hasGeo) { - buttons = buttons.concat(findButtons({category: 'geo'})); + groups.push(out); } - if(fullLayout._hasCartesian) { - if(areAllAxesFixed(fullLayout)) { - buttons = buttons.concat(findButtons({ - category: 'cartesian', - group: 'hover' - })); - } - else { - buttons = buttons.concat(buttons2d); - buttons = buttons.concat(findButtons({category: 'cartesian'})); - } - } + // buttons common to all plot types + addGroup(['toImage', 'sendDataToCloud']); - if(fullLayout._hasGL2D) { - buttons = buttons.concat(buttons2d); - buttons = buttons.concat(findButtons({category: 'gl2d'})); + if(fullLayout._hasGL3D) { + addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']); + addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']); + addGroup(['hoverClosest3d']); } - if(fullLayout._hasPie) { - buttons = buttons.concat(findButtons({category: 'pie'})); + if(fullLayout._hasGeo) { + addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']); + addGroup(['hoverClosestGeo']); } - buttons = filterButtons(buttons, buttonsToRemove); - buttons = groupButtons(buttons); - - return buttons; -} - -// Find buttons in buttonsConfig by category or group -function findButtons(opts) { - var buttonNames = Object.keys(buttonsConfig), - category = opts.category, - group = opts.group; - - var out = []; + var hasCartesian = fullLayout._hasCartesian, + hasGL2D = fullLayout._hasGL2D, + allAxesFixed = areAllAxesFixed(fullLayout); - for(var i = 0; i < buttonNames.length; i++) { - var buttonName = buttonNames[i]; - - if(category && buttonsConfig[buttonName].category !== category) continue; - if(group && buttonsConfig[buttonName].group !== group) continue; + if((hasCartesian || hasGL2D) && !allAxesFixed) { + addGroup(['zoom2d', 'pan2d']); + addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); + } - out.push(buttonName); + if(hasCartesian) { + addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); + } + if(hasGL2D) { + addGroup(['hoverClosestGl2d']); + } + if(fullLayout._hasPie) { + addGroup(['hoverClosestPie']); } - return out; + return groups; } function areAllAxesFixed(fullLayout) { @@ -121,40 +111,3 @@ function areAllAxesFixed(fullLayout) { return allFixed; } - -// Remove buttons according to modebarButtonsToRemove plot config options -function filterButtons(buttons, buttonsToRemove) { - var out = []; - - for(var i = 0; i < buttons.length; i++) { - var button = buttons[i]; - - if(buttonsToRemove.indexOf(button) !== -1) continue; - - out.push(button); - } - - return out; -} - -function groupButtons(buttons) { - var hashObj = {}; - var i; - - for(i = 0; i < buttons.length; i++) { - var button = buttons[i], - group = buttonsConfig[button].group; - - if(hashObj[group] === undefined) hashObj[group] = [button]; - else hashObj[group].push(button); - } - - var groups = Object.keys(hashObj); - var out = []; - - for(i = 0; i < groups.length; i++) { - out.push(hashObj[groups[i]]); - } - - return out; -} From bb3fb10c75ea726313e99b2f5a4085b852b9b4f2 Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:43:43 -0500 Subject: [PATCH 18/36] add check for button 'name': - throw error when name is not passed in - throw error when name is not unique - check for button 'name' is Modebar.hasButton method --- src/components/modebar/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 978cc91bfca..5dc79ae2323 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -75,12 +75,20 @@ proto.updateButtons = function(buttons) { this.buttons = buttons; this.buttonElements = []; + this.buttonsNames = []; this.buttons.forEach(function(buttonGroup) { var group = _this.createGroup(); buttonGroup.forEach(function(buttonConfig) { + var buttonName = buttonConfig.name; + if(!buttonName) { + throw new Error('must provide button \'name\' in button config'); } + if(_this.buttonsNames.indexOf(buttonName) !== -1) { + throw new Error('button name \'', + buttonName + '\' is taken'); + } + _this.buttonsNames.push(buttonName); var button = _this.createButton(buttonConfig); _this.buttonElements.push(button); @@ -192,11 +200,10 @@ proto.updateActiveButton = function(buttonClicked) { }); }; - /** * Check if modebar is configured as button configuration argument * - * @Param {object} buttons 2d array of grouped button names + * @Param {object} buttons 2d array of grouped button config objects * @Return {boolean} */ proto.hasButtons = function (buttons) { @@ -209,7 +216,7 @@ proto.hasButtons = function (buttons) { for (var i = 0; i < buttons.length; ++i) { if (buttons[i].length !== currentButtons[i].length) return false; for (var j = 0; j < buttons[i].length; j++) { - if (buttons[i][j] !== currentButtons[i][j]) return false; + if (buttons[i][j].name !== currentButtons[i][j].name) return false; } } From 933603c92d9460bb85e68e02b34430811a53bf38 Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:45:14 -0500 Subject: [PATCH 19/36] robustify button config API: - add fallbacks to 'title', and 'toggle' and 'icon' - throw error when 'click' handler isn't given --- src/components/modebar/index.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 5dc79ae2323..0fc34344d09 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -122,7 +122,7 @@ proto.createButton = function (config) { button.setAttribute('rel', 'tooltip'); button.className = 'modebar-btn'; - button.setAttribute('data-title', config.title); + button.setAttribute('data-title', config.title || ''); button.setAttribute('data-gravity', config.gravity || 'n'); if(config.attr !== undefined) button.setAttribute('data-attr', config.attr); @@ -133,14 +133,20 @@ proto.createButton = function (config) { button.setAttribute('data-val', val); } - button.addEventListener('click', function () { - _this[config.click].apply(_this, arguments); - }); + var click = config.click; + if(typeof click !== 'function') { + throw new Error('must provide button \'click\' function in button config'); + } + else { + button.addEventListener('click', function(ev) { + config.click(_this, ev); + }); + } - button.setAttribute('data-toggle', config.toggle); + button.setAttribute('data-toggle', config.toggle || false); if(config.toggle) button.classList.add('active'); - button.appendChild(this.createIcon(Icons[config.icon])); + button.appendChild(this.createIcon(Icons[config.icon || 'tooltip_basic'])); return button; }; @@ -183,7 +189,7 @@ proto.updateActiveButton = function(buttonClicked) { this.buttonElements.forEach(function(button) { var thisval = button.getAttribute('data-val') || true, dataAttr = button.getAttribute('data-attr'), - isToggleButton = button.getAttribute('data-toggle')==='true', + isToggleButton = (button.getAttribute('data-toggle') === 'true'), button3 = d3.select(button); // Use 'data-toggle' and 'buttonClicked' to toggle buttons From 543fe149d373413bb971980616aefc3f70863507 Mon Sep 17 00:00:00 2001 From: etpinard Date: Mon, 23 Nov 2015 16:45:43 -0500 Subject: [PATCH 20/36] update modebar tests --- test/jasmine/tests/modebar_test.js | 78 ++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index a03f370bcf9..c78c73bd962 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -7,6 +7,8 @@ var manageModebar = require('@src/components/modebar/manage'); describe('Modebar', function() { 'use strict'; + function noop() {}; + function getMockContainerTree() { var root = document.createElement('div'); root.className = 'plot-container'; @@ -44,7 +46,14 @@ describe('Modebar', function() { } - var buttons = [['toImage', 'sendDataToCloud']]; + var buttons = [[{ + name: 'button 1', + click: noop + }, { + name: 'button 2', + click: noop + }]]; + var modebar = createModebar(getMockGraphInfo(), buttons); describe('createModebar', function() { @@ -53,6 +62,32 @@ describe('Modebar', function() { expect(countButtons(modebar)).toEqual(3); expect(countLogo(modebar)).toEqual(1); }); + + it('throws when button config does not have name', function() { + expect(function() { + createModebar(getMockGraphInfo(), [[ + { click: function() { console.log('not gonna work'); } } + ]]); + }).toThrowError(); + }); + + it('throws when button name is not unique', function() { + expect(function() { + createModebar(getMockGraphInfo(), [[ + { name: 'A', click: function() { console.log('not gonna'); } }, + { name: 'A', click: function() { console.log('... work'); } } + ]]); + }).toThrowError(); + }); + + it('throws when button config does not have a click handler', function() { + expect(function() { + createModebar(getMockGraphInfo(), [[ + { name: 'not gonna work' } + ]]); + }).toThrowError(); + }); + }); describe('modebar.removeAllButtons', function() { @@ -76,13 +111,25 @@ describe('Modebar', function() { describe('manageModebar', function() { + function getButtons(list) { + for(var i = 0; i < list.length; i++) { + for(var j = 0; j < list[i].length; j++) { + list[i][j] = { + name: list[i][j], + click: noop + }; + } + } + return list; + } + it('creates modebar (cartesian version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosest2d', 'hoverCompare2d'] - ]; + ['hoverClosestCartesian', 'hoverCompareCartesian'] + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; @@ -98,10 +145,10 @@ describe('Modebar', function() { }); it('creates modebar (cartesian fixed-axes version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['hoverClosest2d', 'hoverCompare2d'] - ]; + ['hoverClosestCartesian', 'hoverCompareCartesian'] + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; @@ -116,12 +163,12 @@ describe('Modebar', function() { }); it('creates modebar (gl3d version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], ['resetCameraDefault3d', 'resetCameraLastSave3d'], ['hoverClosest3d'] - ]; + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasGL3D = true; @@ -136,11 +183,11 @@ describe('Modebar', function() { }); it('creates modebar (geo version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoomInGeo', 'zoomOutGeo', 'resetGeo'], ['hoverClosestGeo'] - ]; + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasGeo = true; @@ -155,15 +202,16 @@ describe('Modebar', function() { }); it('creates modebar (gl2d version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], ['hoverClosestGl2d'] - ]; + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasGL2D = true; + gd._fullLayout.xaxis = {fixedrange: false}; manageModebar(gd); var modebar = gd._fullLayout._modebar; @@ -175,10 +223,10 @@ describe('Modebar', function() { }); it('creates modebar (pie version)', function() { - var buttons = [ + var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['hoverClosestPie'] - ]; + ]); var gd = getMockGraphInfo(); gd._fullLayout._hasPie = true; From 166da3b9b22032550572d88f409ccd6e0258b75a Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 12:07:33 -0500 Subject: [PATCH 21/36] modif modebar click handler call signature: - modebar click handler are now functions of (gd, ev) the graph div and the event object only. - remove all traces of modebar methods inside click handler - call updateActiveButtons after click handler - N.B. updateActiveButtons is also called in Plotly.relayout --- src/components/modebar/buttons.js | 95 ++++++++++++------------------- src/components/modebar/index.js | 4 +- 2 files changed, 38 insertions(+), 61 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 13866ac96da..63fba3054e6 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -28,7 +28,9 @@ var modebarButtons = module.exports = {}; * @param {string} [gravity] * icon positioning * @param {function} click - * click handler associated with the button + * 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 @@ -42,7 +44,7 @@ modebarButtons.toImage = { name: 'toImage', title: 'Download plot as a png', icon: 'camera', - click: function(modebar) { + click: function(gd) { var format = 'png'; if (Lib.isIE()) { @@ -51,22 +53,22 @@ modebarButtons.toImage = { return; } - if (modebar._snapshotInProgress) { + if (gd._snapshotInProgress) { Lib.notifier('Snapshotting is still in progress - please hold', 'long'); return; } - modebar._snapshotInProgress = true; + gd._snapshotInProgress = true; Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); - var ev = Snapshot.toImage(modebar.graphInfo, {format: format}); + var ev = Snapshot.toImage(gd, {format: format}); - var filename = modebar.graphInfo.fn || 'newplot'; + var filename = gd.fn || 'newplot'; filename += '.' + format; ev.once('success', function(result) { - modebar._snapshotInProgress = false; + gd._snapshotInProgress = false; var downloadLink = document.createElement('a'); downloadLink.href = result; @@ -80,7 +82,7 @@ modebarButtons.toImage = { }); ev.once('error', function (err) { - modebar._snapshotInProgress = false; + gd._snapshotInProgress = false; Lib.notifier('Sorry there was a problem downloading your ' + format, 'long'); console.error(err); @@ -94,8 +96,7 @@ modebarButtons.sendDataToCloud = { name: 'sendDataToCloud', title: 'Save and edit plot in cloud', icon: 'disk', - click: function(modebar) { - var gd = modebar.graphInfo; + click: function(gd) { Plotly.Plots.sendDataToCloud(gd); } }; @@ -176,19 +177,18 @@ modebarButtons.hoverCompareCartesian = { click: handleCartesian }; -function handleCartesian(modebar, ev) { +function handleCartesian(gd, ev) { var button = ev.currentTarget, astr = button.getAttribute('data-attr'), val = button.getAttribute('data-val') || true, - graphInfo = modebar.graphInfo, - fullLayout = graphInfo._fullLayout, + fullLayout = gd._fullLayout, aobj = {}; if(astr === 'zoom') { var mag = (val === 'in') ? 0.5 : 2, r0 = (1 + mag) / 2, r1 = (1 - mag) / 2, - axList = Plotly.Axes.list(graphInfo, null, true); + axList = Plotly.Axes.list(gd, null, true); var ax, axName, initialRange; @@ -212,7 +212,8 @@ function handleCartesian(modebar, ev) { } } } - } else { + } + else { // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y' if (astr==='hovermode' && (val==='x' || val==='y')) { val = fullLayout._isHoriz ? 'y' : 'x'; @@ -222,8 +223,7 @@ function handleCartesian(modebar, ev) { aobj[astr] = val; } - Plotly.relayout(graphInfo, aobj).then( function() { - modebar.updateActiveButton(); + Plotly.relayout(gd, aobj).then( function() { if(astr === 'dragmode') { if(fullLayout._hasCartesian) { Plotly.Fx.setCursor( @@ -231,8 +231,7 @@ function handleCartesian(modebar, ev) { {pan:'move', zoom:'crosshair'}[val] ); } - Plotly.Fx.supplyLayoutDefaults(graphInfo.layout, fullLayout, - graphInfo._fullData); + Plotly.Fx.supplyLayoutDefaults(gd.layout, fullLayout, gd._fullData); } }); } @@ -273,11 +272,10 @@ modebarButtons.tableRotation = { click: handleDrag3d }; -function handleDrag3d(modebar, ev) { +function handleDrag3d(gd, ev) { var button = ev.currentTarget, attr = button.getAttribute('data-attr'), val = button.getAttribute('data-val') || true, - graphInfo = modebar.graphInfo, layoutUpdate = {}; layoutUpdate[attr] = val; @@ -286,9 +284,7 @@ function handleDrag3d(modebar, ev) { * Dragmode will go through the relayout -> doplot -> scene.plot() * routine where the dragmode will be set in scene.plot() */ - Plotly.relayout(graphInfo, layoutUpdate).then( function() { - modebar.updateActiveButton(); - }); + Plotly.relayout(gd, layoutUpdate); } modebarButtons.resetCameraDefault3d = { @@ -307,11 +303,11 @@ modebarButtons.resetCameraLastSave3d = { click: handleCamera3d }; -function handleCamera3d(modebar, ev) { +function handleCamera3d(gd, ev) { var button = ev.currentTarget, attr = button.getAttribute('data-attr'), - layout = modebar.graphInfo.layout, - fullLayout = modebar.graphInfo._fullLayout, + layout = gd.layout, + fullLayout = gd._fullLayout, sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); for(var i = 0; i < sceneIds.length; i++) { @@ -343,11 +339,10 @@ modebarButtons.hoverClosest3d = { toggle: true, icon: 'tooltip_basic', gravity: 'ne', - click: function(modebar, ev) { + click: function(gd, ev) { var button = ev.currentTarget, val = JSON.parse(button.getAttribute('data-val')) || false, - graphInfo = modebar.graphInfo, - fullLayout = graphInfo._fullLayout, + fullLayout = gd._fullLayout, sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); var axes = ['xaxis', 'yaxis', 'zaxis'], @@ -385,9 +380,7 @@ modebarButtons.hoverClosest3d = { button.setAttribute('data-val', JSON.stringify(currentSpikes)); } - Plotly.relayout(graphInfo, layoutUpdate).then(function() { - modebar.updateActiveButton(button); - }); + Plotly.relayout(gd, layoutUpdate); } }; @@ -429,11 +422,11 @@ modebarButtons.hoverClosestGeo = { click: handleGeo }; -function handleGeo(modebar, ev) { +function handleGeo(gd, ev) { var button = ev.currentTarget, attr = button.getAttribute('data-attr'), val = button.getAttribute('data-val') || true, - fullLayout = modebar.graphInfo._fullLayout, + fullLayout = gd._fullLayout, geoIds = Plotly.Plots.getSubplotIds(fullLayout, 'geo'); for(var i = 0; i < geoIds.length; i++) { @@ -449,8 +442,6 @@ function handleGeo(modebar, ev) { else if(attr === 'reset') geo.zoomReset(); else if(attr === 'hovermode') geo.showHover = !geo.showHover; } - - modebar.updateActiveButton(button); } modebarButtons.hoverClosestGl2d = { @@ -461,17 +452,7 @@ modebarButtons.hoverClosestGl2d = { toggle: true, icon: 'tooltip_basic', gravity: 'ne', - click: function(modebar, ev) { - var button = ev.currentTarget, - graphInfo = modebar.graphInfo, - newHover = graphInfo._fullLayout.hovermode ? - false : - 'closest'; - - Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { - modebar.updateActiveButton(button); - }); - } + click: toggleHover }; modebarButtons.hoverClosestPie = { @@ -481,15 +462,11 @@ modebarButtons.hoverClosestPie = { val: 'closest', icon: 'tooltip_basic', gravity: 'ne', - click: function(modebar) { - var graphInfo = modebar.graphInfo, - newHover = graphInfo._fullLayout.hovermode ? - false : - 'closest'; - - Plotly.relayout(graphInfo, 'hovermode', newHover).then(function() { - modebar.updateActiveButton(); - }); - - } + click: toggleHover }; + +function toggleHover(gd) { + var newHover = gd._fullLayout.hovermode ? false : 'closest'; + + Plotly.relayout(gd, 'hovermode', newHover); +} diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 0fc34344d09..f2952259640 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -24,7 +24,6 @@ var Icons = require('../../../build/ploticon'); * @Param {object} opts.graphInfo primary plot object containing data and layout */ function ModeBar(opts) { - this._snapshotInProgress = false; this.container = opts.container; this.element = document.createElement('div'); @@ -139,7 +138,8 @@ proto.createButton = function (config) { } else { button.addEventListener('click', function(ev) { - config.click(_this, ev); + config.click(_this.graphInfo, ev); + _this.updateActiveButton(ev.currentTarget); }); } From 5c7461021ae1ad0defbff1e510e700dc1aaaffa1 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 12:12:48 -0500 Subject: [PATCH 22/36] merge doSceneDragmode and domodebar relayout checks, so that 'dragmode' update also go through the modebar manager --- src/plot_api/plot_api.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index ef38b16800b..d4da85c363d 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2172,7 +2172,6 @@ Plotly.relayout = function relayout(gd, astr, val) { doplot = false, docalc = false, domodebar = false, - doSceneDragmode = false, newkey, axes, keys, xyref, scene, axisAttr; if(typeof astr === 'string') aobj[astr] = val; @@ -2406,8 +2405,7 @@ Plotly.relayout = function relayout(gd, astr, val) { * height, width, autosize get dealt with below. Except for the case of * of subplots - scenes - which require scene.handleDragmode to be called. */ - else if(ai==='hovermode') domodebar = true; - else if (ai === 'dragmode') doSceneDragmode = true; + else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) domodebar = true; else if(['hovermode','dragmode','height', 'width','autosize'].indexOf(ai)===-1) { doplot = true; @@ -2466,10 +2464,10 @@ Plotly.relayout = function relayout(gd, astr, val) { } // this is decoupled enough it doesn't need async regardless - if(domodebar) manageModebar(gd); + if(domodebar) { + manageModebar(gd); - var subplotIds; - if(doSceneDragmode || domodebar) { + var subplotIds; subplotIds = plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { scene = fullLayout[subplotIds[i]]._scene; From ef3c734a3713d6d6b7dc9acc121f0659c7fab4bc Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 12:14:24 -0500 Subject: [PATCH 23/36] add full svg Icons to modebar button config --- src/components/modebar/buttons.js | 49 ++++++++++++++++--------------- src/components/modebar/index.js | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 63fba3054e6..3dde9b52635 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -12,6 +12,7 @@ var Plotly = require('../../plotly'); var Lib = require('../../lib'); var Snapshot = require('../../snapshot'); +var Icons = require('../../../build/ploticon'); var modebarButtons = module.exports = {}; @@ -24,7 +25,7 @@ var modebarButtons = module.exports = {}; * @param {string} title * text that appears while hovering over the button * @param {string} icon - * name of the svg icon associated with the button + * svg icon associated with the button * @param {string} [gravity] * icon positioning * @param {function} click @@ -43,7 +44,7 @@ var modebarButtons = module.exports = {}; modebarButtons.toImage = { name: 'toImage', title: 'Download plot as a png', - icon: 'camera', + icon: Icons.camera, click: function(gd) { var format = 'png'; @@ -95,7 +96,7 @@ modebarButtons.toImage = { modebarButtons.sendDataToCloud = { name: 'sendDataToCloud', title: 'Save and edit plot in cloud', - icon: 'disk', + icon: Icons.disk, click: function(gd) { Plotly.Plots.sendDataToCloud(gd); } @@ -106,7 +107,7 @@ modebarButtons.zoom2d = { title: 'Zoom', attr: 'dragmode', val: 'zoom', - icon: 'zoombox', + icon: Icons.zoombox, click: handleCartesian }; @@ -115,7 +116,7 @@ modebarButtons.pan2d = { title: 'Pan', attr: 'dragmode', val: 'pan', - icon: 'pan', + icon: Icons.pan, click: handleCartesian }; @@ -124,7 +125,7 @@ modebarButtons.zoomIn2d = { title: 'Zoom in', attr: 'zoom', val: 'in', - icon: 'zoom_plus', + icon: Icons.zoom_plus, click: handleCartesian }; @@ -133,7 +134,7 @@ modebarButtons.zoomOut2d = { title: 'Zoom out', attr: 'zoom', val: 'out', - icon: 'zoom_minus', + icon: Icons.zoom_minus, click: handleCartesian }; @@ -142,7 +143,7 @@ modebarButtons.autoScale2d = { title: 'Autoscale', attr: 'zoom', val: 'auto', - icon: 'autoscale', + icon: Icons.autoscale, click: handleCartesian }; @@ -151,7 +152,7 @@ modebarButtons.resetScale2d = { title: 'Reset axes', attr: 'zoom', val: 'reset', - icon: 'home', + icon: Icons.home, click: handleCartesian }; @@ -160,7 +161,7 @@ modebarButtons.hoverClosestCartesian = { title: 'Show closest data on hover', attr: 'hovermode', val: 'closest', - icon: 'tooltip_basic', + icon: Icons.tooltip_basic, gravity: 'ne', click: handleCartesian }; @@ -172,7 +173,7 @@ modebarButtons.hoverCompareCartesian = { val: function(graphInfo) { return graphInfo._fullLayout._isHoriz ? 'y' : 'x'; }, - icon: 'tooltip_compare', + icon: Icons.tooltip_compare, gravity: 'ne', click: handleCartesian }; @@ -241,7 +242,7 @@ modebarButtons.zoom3d = { title: 'Zoom', attr: 'dragmode', val: 'zoom', - icon: 'zoombox', + icon: Icons.zoombox, click: handleDrag3d }; @@ -250,7 +251,7 @@ modebarButtons.pan3d = { title: 'Pan', attr: 'dragmode', val: 'pan', - icon: 'pan', + icon: Icons.pan, click: handleDrag3d }; @@ -259,7 +260,7 @@ modebarButtons.orbitRotation = { title: 'orbital rotation', attr: 'dragmode', val: 'orbit', - icon: '3d_rotate', + icon: Icons['3d_rotate'], click: handleDrag3d }; @@ -268,7 +269,7 @@ modebarButtons.tableRotation = { title: 'turntable rotation', attr: 'dragmode', val: 'turntable', - icon: 'z-axis', + icon: Icons['z-axis'], click: handleDrag3d }; @@ -291,7 +292,7 @@ modebarButtons.resetCameraDefault3d = { name: 'resetCameraDefault3d', title: 'Reset camera to default', attr: 'resetDefault', - icon: 'home', + icon: Icons.home, click: handleCamera3d }; @@ -299,7 +300,7 @@ modebarButtons.resetCameraLastSave3d = { name: 'resetCameraLastSave3d', title: 'Reset camera to last save', attr: 'resetLastSave', - icon: 'movie', + icon: Icons.movie, click: handleCamera3d }; @@ -337,7 +338,7 @@ modebarButtons.hoverClosest3d = { attr: 'hovermode', val: null, toggle: true, - icon: 'tooltip_basic', + icon: Icons.tooltip_basic, gravity: 'ne', click: function(gd, ev) { var button = ev.currentTarget, @@ -389,7 +390,7 @@ modebarButtons.zoomInGeo = { title: 'Zoom in', attr: 'zoom', val: 'in', - icon: 'zoom_plus', + icon: Icons.zoom_plus, click: handleGeo }; @@ -398,7 +399,7 @@ modebarButtons.zoomOutGeo = { title: 'Zoom in', attr: 'zoom', val: 'out', - icon: 'zoom_minus', + icon: Icons.zoom_minus, click: handleGeo }; @@ -407,7 +408,7 @@ modebarButtons.resetGeo = { title: 'Reset', attr: 'reset', val: null, - icon: 'autoscale', + icon: Icons.autoscale, click: handleGeo }; @@ -417,7 +418,7 @@ modebarButtons.hoverClosestGeo = { attr: 'hovermode', val: null, toggle: true, - icon: 'tooltip_basic', + icon: Icons.tooltip_basic, gravity: 'ne', click: handleGeo }; @@ -450,7 +451,7 @@ modebarButtons.hoverClosestGl2d = { attr: 'hovermode', val: null, toggle: true, - icon: 'tooltip_basic', + icon: Icons.tooltip_basic, gravity: 'ne', click: toggleHover }; @@ -460,7 +461,7 @@ modebarButtons.hoverClosestPie = { title: 'Toggle show closest data on hover', attr: 'hovermode', val: 'closest', - icon: 'tooltip_basic', + icon: Icons.tooltip_basic, gravity: 'ne', click: toggleHover }; diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index f2952259640..02b9e5acbc7 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -146,7 +146,7 @@ proto.createButton = function (config) { button.setAttribute('data-toggle', config.toggle || false); if(config.toggle) button.classList.add('active'); - button.appendChild(this.createIcon(Icons[config.icon || 'tooltip_basic'])); + button.appendChild(this.createIcon(config.icon || Icons.tooltip_basic)); return button; }; From 7cd5b3090a675b750ff31b60fa9a1f7e56a474ff Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 12:15:01 -0500 Subject: [PATCH 24/36] make svg 'ascent' and 'descent' props part of every plotly icons --- src/components/modebar/index.js | 6 +++--- tasks/util/pull_font_svg.js | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 02b9e5acbc7..522d47b64a7 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -159,17 +159,17 @@ proto.createButton = function (config) { * @Return {HTMLelement} */ proto.createIcon = function (thisIcon) { - var iconHeight = Icons.ascent - Icons.descent, + var iconHeight = thisIcon.ascent - thisIcon.descent, svgNS = 'http://www.w3.org/2000/svg', icon = document.createElementNS(svgNS, 'svg'), path = document.createElementNS(svgNS, 'path'); icon.setAttribute('height', '1em'); - icon.setAttribute('width', (thisIcon.width / iconHeight)+'em'); + icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em'); icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); path.setAttribute('d', thisIcon.path); - path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + Icons.ascent + ')'); + path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'); icon.appendChild(path); return icon; diff --git a/tasks/util/pull_font_svg.js b/tasks/util/pull_font_svg.js index 51002ec9b2a..c67a049e4da 100644 --- a/tasks/util/pull_font_svg.js +++ b/tasks/util/pull_font_svg.js @@ -10,15 +10,16 @@ module.exports = function pullFontSVG(data, pathOut) { var font_obj = result.svg.defs[0].font[0], default_width = Number(font_obj.$['horiz-adv-x']), - chars = { - ascent: Number(font_obj['font-face'][0].$.ascent), - descent: Number(font_obj['font-face'][0].$.descent) - }; + ascent = Number(font_obj['font-face'][0].$.ascent), + descent = Number(font_obj['font-face'][0].$.descent), + chars = {}; font_obj.glyph.forEach(function(glyph) { chars[glyph.$['glyph-name']] = { width: Number(glyph.$['horiz-adv-x']) || default_width, - path: glyph.$.d + path: glyph.$.d, + ascent: ascent, + descent: descent }; }); From 2934f1131834e000f3453dd29196e74f790cf18e Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 12:15:31 -0500 Subject: [PATCH 25/36] rename 'graphInfo' --> 'gd' for consistency in modebar buttons defs --- src/components/modebar/buttons.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3dde9b52635..5a4485cafab 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -36,7 +36,7 @@ var modebarButtons = module.exports = {}; * attribute associated with button, * use this with 'val' to keep track of the state * @param {*} [val] - * initial 'attr' value, can be a function of graphInfo + * initial 'attr' value, can be a function of gd * @param {boolean} [toggle] * is the button a toggle button? */ @@ -170,8 +170,8 @@ modebarButtons.hoverCompareCartesian = { name: 'hoverCompareCartesian', title: 'Compare data on hover', attr: 'hovermode', - val: function(graphInfo) { - return graphInfo._fullLayout._isHoriz ? 'y' : 'x'; + val: function(gd) { + return gd._fullLayout._isHoriz ? 'y' : 'x'; }, icon: Icons.tooltip_compare, gravity: 'ne', From eff56fcb944a139ca8168f3a81c755ff43abf4d6 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 13:05:58 -0500 Subject: [PATCH 26/36] improve fallback for button 'title': - don't show hover text when 'title' is set to null or false or '' - if undefined, fall back to 'name' --- src/components/modebar/buttons.js | 4 ++-- src/components/modebar/index.js | 7 +++++-- test/jasmine/tests/modebar_test.js | 32 +++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 5a4485cafab..9aa8d11c858 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -23,7 +23,8 @@ var modebarButtons = module.exports = {}; * @param {string} name * name / id of the buttons (for tracking) * @param {string} title - * text that appears while hovering over the button + * text that appears while hovering over the button, + * enter null, false or '' for no hover text * @param {string} icon * svg icon associated with the button * @param {string} [gravity] @@ -68,7 +69,6 @@ modebarButtons.toImage = { filename += '.' + format; ev.once('success', function(result) { - gd._snapshotInProgress = false; var downloadLink = document.createElement('a'); diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 522d47b64a7..4685183982a 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -121,8 +121,10 @@ proto.createButton = function (config) { button.setAttribute('rel', 'tooltip'); button.className = 'modebar-btn'; - button.setAttribute('data-title', config.title || ''); - button.setAttribute('data-gravity', config.gravity || 'n'); + var title = config.title; + if(title !== null && title !== false && title !== '') { + button.setAttribute('data-title', title || config.name); + } if(config.attr !== undefined) button.setAttribute('data-attr', config.attr); @@ -147,6 +149,7 @@ proto.createButton = function (config) { if(config.toggle) button.classList.add('active'); button.appendChild(this.createIcon(config.icon || Icons.tooltip_basic)); + button.setAttribute('data-gravity', config.gravity || 'n'); return button; }; diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index c78c73bd962..015aff39ab9 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -7,7 +7,7 @@ var manageModebar = require('@src/components/modebar/manage'); describe('Modebar', function() { 'use strict'; - function noop() {}; + function noop() {} function getMockContainerTree() { var root = document.createElement('div'); @@ -45,6 +45,10 @@ describe('Modebar', function() { return d3.select(modebar.element).selectAll('a.plotlyjsicon')[0].length; } + function checkBtnAttr(modebar, index, attr) { + var buttons = d3.select(modebar.element).selectAll('a.modebar-btn'); + return d3.select(buttons[0][index]).attr(attr); + } var buttons = [[{ name: 'button 1', @@ -88,6 +92,32 @@ describe('Modebar', function() { }).toThrowError(); }); + it('defaults title to name when missing', function() { + var modebar = createModebar(getMockGraphInfo(), [[ + { name: 'the title too', click: noop } + ]]); + + expect(checkBtnAttr(modebar, 0, 'data-title')).toEqual('the title too'); + }); + + it('hides title to when title is set to null or \'\' or false', function() { + var modebar; + + modebar = createModebar(getMockGraphInfo(), [[ + { name: 'button', title: null, click: noop } + ]]); + expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + + modebar = createModebar(getMockGraphInfo(), [[ + { name: 'button', title: '', click: noop } + ]]); + expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + + modebar = createModebar(getMockGraphInfo(), [[ + { name: 'button', title: false, click: noop } + ]]); + expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + }); }); describe('modebar.removeAllButtons', function() { From ed90f81bba7d8ac45e2b8911bbc0e63a2141bfea Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 13:35:23 -0500 Subject: [PATCH 27/36] sub 'modebar' --> 'modeBar' --- src/components/modebar/buttons.js | 53 +++---- src/components/modebar/index.js | 16 +- src/components/modebar/manage.js | 32 ++-- src/plot_api/plot_api.js | 6 +- src/plot_api/plot_config.js | 7 +- src/plots/cartesian/axes.js | 2 +- src/plots/cartesian/graph_interact.js | 2 +- src/plots/gl3d/scene.js | 2 +- test/jasmine/tests/modebar_test.js | 206 +++++++++++++------------- 9 files changed, 165 insertions(+), 161 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 9aa8d11c858..f3a34ca6939 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -15,10 +15,10 @@ var Snapshot = require('../../snapshot'); var Icons = require('../../../build/ploticon'); -var modebarButtons = module.exports = {}; +var modeBarButtons = module.exports = {}; /** - * Modebar buttons configuration + * ModeBar buttons configuration * * @param {string} name * name / id of the buttons (for tracking) @@ -26,7 +26,8 @@ var modebarButtons = module.exports = {}; * text that appears while hovering over the button, * enter null, false or '' for no hover text * @param {string} icon - * svg icon associated with the button + * 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 @@ -42,7 +43,7 @@ var modebarButtons = module.exports = {}; * is the button a toggle button? */ -modebarButtons.toImage = { +modeBarButtons.toImage = { name: 'toImage', title: 'Download plot as a png', icon: Icons.camera, @@ -93,7 +94,7 @@ modebarButtons.toImage = { } }; -modebarButtons.sendDataToCloud = { +modeBarButtons.sendDataToCloud = { name: 'sendDataToCloud', title: 'Save and edit plot in cloud', icon: Icons.disk, @@ -102,7 +103,7 @@ modebarButtons.sendDataToCloud = { } }; -modebarButtons.zoom2d = { +modeBarButtons.zoom2d = { name: 'zoom2d', title: 'Zoom', attr: 'dragmode', @@ -111,7 +112,7 @@ modebarButtons.zoom2d = { click: handleCartesian }; -modebarButtons.pan2d = { +modeBarButtons.pan2d = { name: 'pan2d', title: 'Pan', attr: 'dragmode', @@ -120,7 +121,7 @@ modebarButtons.pan2d = { click: handleCartesian }; -modebarButtons.zoomIn2d = { +modeBarButtons.zoomIn2d = { name: 'zoomIn2d', title: 'Zoom in', attr: 'zoom', @@ -129,7 +130,7 @@ modebarButtons.zoomIn2d = { click: handleCartesian }; -modebarButtons.zoomOut2d = { +modeBarButtons.zoomOut2d = { name: 'zoomOut2d', title: 'Zoom out', attr: 'zoom', @@ -138,7 +139,7 @@ modebarButtons.zoomOut2d = { click: handleCartesian }; -modebarButtons.autoScale2d = { +modeBarButtons.autoScale2d = { name: 'autoScale2d', title: 'Autoscale', attr: 'zoom', @@ -147,7 +148,7 @@ modebarButtons.autoScale2d = { click: handleCartesian }; -modebarButtons.resetScale2d = { +modeBarButtons.resetScale2d = { name: 'resetScale2d', title: 'Reset axes', attr: 'zoom', @@ -156,7 +157,7 @@ modebarButtons.resetScale2d = { click: handleCartesian }; -modebarButtons.hoverClosestCartesian = { +modeBarButtons.hoverClosestCartesian = { name: 'hoverClosestCartesian', title: 'Show closest data on hover', attr: 'hovermode', @@ -166,7 +167,7 @@ modebarButtons.hoverClosestCartesian = { click: handleCartesian }; -modebarButtons.hoverCompareCartesian = { +modeBarButtons.hoverCompareCartesian = { name: 'hoverCompareCartesian', title: 'Compare data on hover', attr: 'hovermode', @@ -237,7 +238,7 @@ function handleCartesian(gd, ev) { }); } -modebarButtons.zoom3d = { +modeBarButtons.zoom3d = { name: 'zoom3d', title: 'Zoom', attr: 'dragmode', @@ -246,7 +247,7 @@ modebarButtons.zoom3d = { click: handleDrag3d }; -modebarButtons.pan3d = { +modeBarButtons.pan3d = { name: 'pan3d', title: 'Pan', attr: 'dragmode', @@ -255,7 +256,7 @@ modebarButtons.pan3d = { click: handleDrag3d }; -modebarButtons.orbitRotation = { +modeBarButtons.orbitRotation = { name: 'orbitRotation', title: 'orbital rotation', attr: 'dragmode', @@ -264,7 +265,7 @@ modebarButtons.orbitRotation = { click: handleDrag3d }; -modebarButtons.tableRotation = { +modeBarButtons.tableRotation = { name: 'tableRotation', title: 'turntable rotation', attr: 'dragmode', @@ -288,7 +289,7 @@ function handleDrag3d(gd, ev) { Plotly.relayout(gd, layoutUpdate); } -modebarButtons.resetCameraDefault3d = { +modeBarButtons.resetCameraDefault3d = { name: 'resetCameraDefault3d', title: 'Reset camera to default', attr: 'resetDefault', @@ -296,7 +297,7 @@ modebarButtons.resetCameraDefault3d = { click: handleCamera3d }; -modebarButtons.resetCameraLastSave3d = { +modeBarButtons.resetCameraLastSave3d = { name: 'resetCameraLastSave3d', title: 'Reset camera to last save', attr: 'resetLastSave', @@ -332,7 +333,7 @@ function handleCamera3d(gd, ev) { */ } -modebarButtons.hoverClosest3d = { +modeBarButtons.hoverClosest3d = { name: 'hoverClosest3d', title: 'Toggle show closest data on hover', attr: 'hovermode', @@ -385,7 +386,7 @@ modebarButtons.hoverClosest3d = { } }; -modebarButtons.zoomInGeo = { +modeBarButtons.zoomInGeo = { name: 'zoomInGeo', title: 'Zoom in', attr: 'zoom', @@ -394,7 +395,7 @@ modebarButtons.zoomInGeo = { click: handleGeo }; -modebarButtons.zoomOutGeo = { +modeBarButtons.zoomOutGeo = { name: 'zoomOutGeo', title: 'Zoom in', attr: 'zoom', @@ -403,7 +404,7 @@ modebarButtons.zoomOutGeo = { click: handleGeo }; -modebarButtons.resetGeo = { +modeBarButtons.resetGeo = { name: 'resetGeo', title: 'Reset', attr: 'reset', @@ -412,7 +413,7 @@ modebarButtons.resetGeo = { click: handleGeo }; -modebarButtons.hoverClosestGeo = { +modeBarButtons.hoverClosestGeo = { name: 'hoverClosestGeo', title: 'Toggle show closest data on hover', attr: 'hovermode', @@ -445,7 +446,7 @@ function handleGeo(gd, ev) { } } -modebarButtons.hoverClosestGl2d = { +modeBarButtons.hoverClosestGl2d = { name: 'hoverClosestGl2d', title: 'Toggle show closest data on hover', attr: 'hovermode', @@ -456,7 +457,7 @@ modebarButtons.hoverClosestGl2d = { click: toggleHover }; -modebarButtons.hoverClosestPie = { +modeBarButtons.hoverClosestPie = { name: 'hoverClosestPie', title: 'Toggle show closest data on hover', attr: 'hovermode', diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 4685183982a..f6794d53d80 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -20,7 +20,7 @@ var Icons = require('../../../build/ploticon'); * @Class * @Param {object} opts * @Param {object} opts.buttons nested arrays of grouped buttons config objects - * @Param {object} opts.container container div to append modebar + * @Param {object} opts.container container div to append modeBar * @Param {object} opts.graphInfo primary plot object containing data and layout */ function ModeBar(opts) { @@ -35,7 +35,7 @@ function ModeBar(opts) { var proto = ModeBar.prototype; /** - * Update modebar (buttons and logo) + * Update modeBar (buttons and logo) * * @param {object} graphInfo primary plot object containing data and layout * @param {array of arrays} buttons nested arrays of grouped buttons to initialize @@ -210,7 +210,7 @@ proto.updateActiveButton = function(buttonClicked) { }; /** - * Check if modebar is configured as button configuration argument + * Check if modeBar is configured as button configuration argument * * @Param {object} buttons 2d array of grouped button config objects * @Return {boolean} @@ -262,22 +262,22 @@ proto.destroy = function() { Plotly.Lib.removeElement(this.container.querySelector('.modebar')); }; -function createModebar(gd, buttons) { +function createModeBar(gd, buttons) { var fullLayout = gd._fullLayout; - var modebar = new ModeBar({ + var modeBar = new ModeBar({ graphInfo: gd, container: fullLayout._paperdiv.node(), buttons: buttons }); if(fullLayout._privateplot) { - d3.select(modebar.element).append('span') + d3.select(modeBar.element).append('span') .classed('badge-private float--left', true) .text('PRIVATE'); } - return modebar; + return modeBar; } -module.exports = createModebar; +module.exports = createModeBar; diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 0d45d5c386c..0352c4e9034 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -11,39 +11,41 @@ var Plotly = require('../../plotly'); -var createModebar = require('./'); -var modebarButtons = require('./buttons'); +var createModeBar = require('./'); +var modeBarButtons = require('./buttons'); /** - * Modebar wrapper around 'create' and 'update', - * chooses buttons to pass to Modebar constructor based on + * ModeBar wrapper around 'create' and 'update', + * chooses buttons to pass to ModeBar constructor based on * plot type and plot config. * * @param {object} gd main plot object * */ -module.exports = function manageModebar(gd) { +module.exports = function manageModeBar(gd) { var fullLayout = gd._fullLayout, context = gd._context, - modebar = fullLayout._modebar; + modeBar = fullLayout._modeBar; - if(!context.displayModeBar && modebar) { - modebar.destroy(); - delete fullLayout._modebar; + if(!context.displayModeBar && modeBar) { + modeBar.destroy(); + delete fullLayout._modeBar; return; } - if(!Array.isArray(context.modebarButtonsToRemove)) { + if(!Array.isArray(context.modeBarButtonsToRemove)) { throw new Error([ - '*modebarButtonsToRemove* configuration options', + '*modeBarButtonsToRemove* configuration options', 'must be an array.' ].join(' ')); } - var buttonGroups = getButtonGroups(fullLayout, context.modebarButtonsToRemove); +// if(!Array.isArray(context.m)) - if(modebar) modebar.update(gd, buttonGroups); - else fullLayout._modebar = createModebar(gd, buttonGroups); + var buttonGroups = getButtonGroups(fullLayout, context.modeBarButtonsToRemove); + + if(modeBar) modeBar.update(gd, buttonGroups); + else fullLayout._modeBar = createModeBar(gd, buttonGroups); }; // logic behind which buttons are displayed by default @@ -56,7 +58,7 @@ function getButtonGroups(fullLayout, buttonsToRemove) { for(var i = 0; i < newGroup.length; i++) { var button = newGroup[i]; if(buttonsToRemove.indexOf(button) !== -1) continue; - out.push(modebarButtons[button]); + out.push(modeBarButtons[button]); } groups.push(out); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d4da85c363d..c59f677619d 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -11,7 +11,7 @@ var Plotly = require('../plotly'); var Events = require('../lib/events'); -var manageModebar = require('../components/modebar/manage'); +var manageModeBar = require('../components/modebar/manage'); var d3 = require('d3'); var m4FromQuat = require('gl-mat4/fromQuat'); @@ -2465,7 +2465,7 @@ Plotly.relayout = function relayout(gd, astr, val) { // this is decoupled enough it doesn't need async regardless if(domodebar) { - manageModebar(gd); + manageModeBar(gd); var subplotIds; subplotIds = plots.getSubplotIds(fullLayout, 'gl3d'); @@ -3001,7 +3001,7 @@ function lsInner(gd) { Plotly.Titles.draw(gd, 'gtitle'); - manageModebar(gd); + manageModeBar(gd); return gd._promises.length && Promise.all(gd._promises); } diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 959688b4257..f17ad413500 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -55,13 +55,14 @@ module.exports = { // false or function adding source(s) to linkText showSources: false, - // display the modebar (true, false, or 'hover') + // display the mode bar (true, false, or 'hover') displayModeBar: 'hover', // remove modebar button by name - modebarButtonsToRemove: [], + // (see ./components/modebar/buttons.js for the list of names) + modeBarButtonsToRemove: [], - // add the plotly logo on the end of the modebar + // add the plotly logo on the end of the mode bar displaylogo: true, // increase the pixel ratio for Gl plot images diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index f35d298dc6c..c4a3e8148bd 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -935,7 +935,7 @@ axes.doAutoRange = function(ax) { }; // save a copy of the initial axis ranges in fullLayout -// use them in modebar and dblclick events +// use them in mode bar and dblclick events axes.saveRangeInitial = function(gd, overwrite) { var axList = axes.list(gd, '', true), hasOneAxisChanged = false; diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 867c6069acd..3af14739642 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -45,7 +45,7 @@ fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { if(layoutOut._hasCartesian) { // flag for 'horizontal' plots: - // determines the state of the modebar 'compare' hovermode button + // determines the state of the mode bar 'compare' hovermode button isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); hovermodeDflt = isHoriz ? 'y' : 'x'; } diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index bf4de0c23e7..e8e276a30f8 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -539,7 +539,7 @@ proto.destroy = function() { }; -// for reset camera button in modebar +// for reset camera button in mode bar proto.setCameraToDefault = function setCameraToDefault () { // as in Gl3dLayout.layoutAttributes this.glplot.camera.lookAt( diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 015aff39ab9..3b684ec7589 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -1,10 +1,10 @@ var d3 = require('d3'); -var createModebar = require('@src/components/modebar'); -var manageModebar = require('@src/components/modebar/manage'); +var createModeBar = require('@src/components/modebar'); +var manageModeBar = require('@src/components/modebar/manage'); -describe('Modebar', function() { +describe('ModeBar', function() { 'use strict'; function noop() {} @@ -28,25 +28,25 @@ describe('Modebar', function() { _context: { displaylogo: true, displayModeBar: true, - modebarButtonsToRemove: [] + modeBarButtonsToRemove: [] } }; } - function countGroups(modebar) { - return d3.select(modebar.element).selectAll('div.modebar-group')[0].length; + function countGroups(modeBar) { + return d3.select(modeBar.element).selectAll('div.modebar-group')[0].length; } - function countButtons(modebar) { - return d3.select(modebar.element).selectAll('a.modebar-btn')[0].length; + function countButtons(modeBar) { + return d3.select(modeBar.element).selectAll('a.modebar-btn')[0].length; } - function countLogo(modebar) { - return d3.select(modebar.element).selectAll('a.plotlyjsicon')[0].length; + function countLogo(modeBar) { + return d3.select(modeBar.element).selectAll('a.plotlyjsicon')[0].length; } - function checkBtnAttr(modebar, index, attr) { - var buttons = d3.select(modebar.element).selectAll('a.modebar-btn'); + function checkBtnAttr(modeBar, index, attr) { + var buttons = d3.select(modeBar.element).selectAll('a.modebar-btn'); return d3.select(buttons[0][index]).attr(attr); } @@ -58,18 +58,18 @@ describe('Modebar', function() { click: noop }]]; - var modebar = createModebar(getMockGraphInfo(), buttons); + var modeBar = createModeBar(getMockGraphInfo(), buttons); describe('createModebar', function() { - it('creates a modebar', function() { - expect(countGroups(modebar)).toEqual(2); - expect(countButtons(modebar)).toEqual(3); - expect(countLogo(modebar)).toEqual(1); + it('creates a mode bar', function() { + expect(countGroups(modeBar)).toEqual(2); + expect(countButtons(modeBar)).toEqual(3); + expect(countLogo(modeBar)).toEqual(1); }); it('throws when button config does not have name', function() { expect(function() { - createModebar(getMockGraphInfo(), [[ + createModeBar(getMockGraphInfo(), [[ { click: function() { console.log('not gonna work'); } } ]]); }).toThrowError(); @@ -77,7 +77,7 @@ describe('Modebar', function() { it('throws when button name is not unique', function() { expect(function() { - createModebar(getMockGraphInfo(), [[ + createModeBar(getMockGraphInfo(), [[ { name: 'A', click: function() { console.log('not gonna'); } }, { name: 'A', click: function() { console.log('... work'); } } ]]); @@ -86,60 +86,60 @@ describe('Modebar', function() { it('throws when button config does not have a click handler', function() { expect(function() { - createModebar(getMockGraphInfo(), [[ + createModeBar(getMockGraphInfo(), [[ { name: 'not gonna work' } ]]); }).toThrowError(); }); it('defaults title to name when missing', function() { - var modebar = createModebar(getMockGraphInfo(), [[ + var modeBar = createModeBar(getMockGraphInfo(), [[ { name: 'the title too', click: noop } ]]); - expect(checkBtnAttr(modebar, 0, 'data-title')).toEqual('the title too'); + expect(checkBtnAttr(modeBar, 0, 'data-title')).toEqual('the title too'); }); it('hides title to when title is set to null or \'\' or false', function() { - var modebar; + var modeBar; - modebar = createModebar(getMockGraphInfo(), [[ + modeBar = createModeBar(getMockGraphInfo(), [[ { name: 'button', title: null, click: noop } ]]); - expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); - modebar = createModebar(getMockGraphInfo(), [[ + modeBar = createModeBar(getMockGraphInfo(), [[ { name: 'button', title: '', click: noop } ]]); - expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); - modebar = createModebar(getMockGraphInfo(), [[ + modeBar = createModeBar(getMockGraphInfo(), [[ { name: 'button', title: false, click: noop } ]]); - expect(checkBtnAttr(modebar, 0, 'data-title')).toBe(null); + expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); }); }); - describe('modebar.removeAllButtons', function() { - it('removes all modebar buttons', function() { - modebar.removeAllButtons(); + describe('modeBar.removeAllButtons', function() { + it('removes all mode bar buttons', function() { + modeBar.removeAllButtons(); - expect(modebar.element.innerHTML).toEqual(''); - expect(modebar.hasLogo).toBe(false); + expect(modeBar.element.innerHTML).toEqual(''); + expect(modeBar.hasLogo).toBe(false); }); }); - describe('modebar.destroy', function() { - it('removes the modebar entirely', function() { - var modebarParent = modebar.element.parentNode; + describe('modeBar.destroy', function() { + it('removes the mode bar entirely', function() { + var modeBarParent = modeBar.element.parentNode; - modebar.destroy(); + modeBar.destroy(); - expect(modebarParent.querySelector('.modebar')).toBeNull(); + expect(modeBarParent.querySelector('.modebar')).toBeNull(); }); }); - describe('manageModebar', function() { + describe('manageModeBar', function() { function getButtons(list) { for(var i = 0; i < list.length; i++) { @@ -153,7 +153,7 @@ describe('Modebar', function() { return list; } - it('creates modebar (cartesian version)', function() { + it('creates mode bar (cartesian version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], @@ -165,16 +165,16 @@ describe('Modebar', function() { gd._fullLayout._hasCartesian = true; gd._fullLayout.xaxis = {fixedrange: false}; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(5); - expect(countButtons(modebar)).toEqual(11); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(5); + expect(countButtons(modeBar)).toEqual(11); + expect(countLogo(modeBar)).toEqual(1); }); - it('creates modebar (cartesian fixed-axes version)', function() { + it('creates mode bar (cartesian fixed-axes version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['hoverClosestCartesian', 'hoverCompareCartesian'] @@ -183,16 +183,16 @@ describe('Modebar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(3); - expect(countButtons(modebar)).toEqual(5); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(5); + expect(countLogo(modeBar)).toEqual(1); }); - it('creates modebar (gl3d version)', function() { + it('creates mode bar (gl3d version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], @@ -203,16 +203,16 @@ describe('Modebar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasGL3D = true; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(5); - expect(countButtons(modebar)).toEqual(10); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(5); + expect(countButtons(modeBar)).toEqual(10); + expect(countLogo(modeBar)).toEqual(1); }); - it('creates modebar (geo version)', function() { + it('creates mode bar (geo version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoomInGeo', 'zoomOutGeo', 'resetGeo'], @@ -222,16 +222,16 @@ describe('Modebar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasGeo = true; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(4); - expect(countButtons(modebar)).toEqual(7); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(4); + expect(countButtons(modeBar)).toEqual(7); + expect(countLogo(modeBar)).toEqual(1); }); - it('creates modebar (gl2d version)', function() { + it('creates mode bar (gl2d version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], @@ -243,16 +243,16 @@ describe('Modebar', function() { gd._fullLayout._hasGL2D = true; gd._fullLayout.xaxis = {fixedrange: false}; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(5); - expect(countButtons(modebar)).toEqual(10); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(5); + expect(countButtons(modeBar)).toEqual(10); + expect(countLogo(modeBar)).toEqual(1); }); - it('creates modebar (pie version)', function() { + it('creates mode bar (pie version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['hoverClosestPie'] @@ -261,65 +261,65 @@ describe('Modebar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasPie = true; - manageModebar(gd); - var modebar = gd._fullLayout._modebar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - expect(modebar.hasButtons(buttons)).toBe(true); - expect(countGroups(modebar)).toEqual(3); - expect(countButtons(modebar)).toEqual(4); - expect(countLogo(modebar)).toEqual(1); + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(4); + expect(countLogo(modeBar)).toEqual(1); }); - it('throws an error if modebarButtonsToRemove isn\'t an array', function() { + it('throws an error if modeBarButtonsToRemove isn\'t an array', function() { var gd = getMockGraphInfo(); - gd._context.modebarButtonsToRemove = 'not gonna work'; + gd._context.modeBarButtonsToRemove = 'not gonna work'; - expect(function() { manageModebar(gd); }).toThrowError(); + expect(function() { manageModeBar(gd); }).toThrowError(); }); - it('displays or not modebar according to displayModeBar config arg', function() { + it('displays or not mode bar according to displayModeBar config arg', function() { var gd = getMockGraphInfo(); - manageModebar(gd); - expect(gd._fullLayout._modebar).toBeDefined(); + manageModeBar(gd); + expect(gd._fullLayout._modeBar).toBeDefined(); gd._context.displayModeBar = false; - manageModebar(gd); - expect(gd._fullLayout._modebar).not.toBeDefined(); + manageModeBar(gd); + expect(gd._fullLayout._modeBar).not.toBeDefined(); }); it('displays or not logo according to displaylogo config arg', function() { var gd = getMockGraphInfo(); - manageModebar(gd); - expect(countLogo(gd._fullLayout._modebar)).toEqual(1); + manageModeBar(gd); + expect(countLogo(gd._fullLayout._modeBar)).toEqual(1); gd._context.displaylogo = false; - manageModebar(gd); - expect(countLogo(gd._fullLayout._modebar)).toEqual(0); + manageModeBar(gd); + expect(countLogo(gd._fullLayout._modeBar)).toEqual(0); }); - it('updates modebar buttons if plot type changes', function() { + it('updates mode bar buttons if plot type changes', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; gd._fullLayout.xaxis = {fixedrange: false}; - manageModebar(gd); // gives 11 buttons + manageModeBar(gd); // gives 11 buttons gd._fullLayout._hasCartesian = false; gd._fullLayout._hasGL3D = true; - manageModebar(gd); + manageModeBar(gd); - expect(countButtons(gd._fullLayout._modebar)).toEqual(10); + expect(countButtons(gd._fullLayout._modeBar)).toEqual(10); }); - it('updates modebar buttons if plot type changes', function() { + it('updates mode bar buttons if plot type changes', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; gd._fullLayout.xaxis = {fixedrange: false}; - manageModebar(gd); // gives 11 buttons - gd._context.modebarButtonsToRemove = ['toImage', 'sendDataToCloud']; - manageModebar(gd); + manageModeBar(gd); // gives 11 buttons + gd._context.modeBarButtonsToRemove = ['toImage', 'sendDataToCloud']; + manageModeBar(gd); - expect(countButtons(gd._fullLayout._modebar)).toEqual(9); + expect(countButtons(gd._fullLayout._modeBar)).toEqual(9); }); }); From 99723410257ceb32a5f8fa98d7b794b0824840e1 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 14:55:55 -0500 Subject: [PATCH 28/36] export Icons, Plotly.Icons has the full set of icons, ready to be used by users in their custom modebar buttons --- src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.js b/src/index.js index 0b41010c308..234311ae494 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,9 @@ exports.deleteTraces = Plotly.deleteTraces; exports.moveTraces = Plotly.moveTraces; exports.setPlotConfig = require('./plot_api/set_plot_config'); +// plot icons +exports.Icons = require('../build/ploticon'); + // unofficial 'beta' plot methods, use at your own risk exports.Plots = Plotly.Plots; exports.Fx = Plotly.Fx; From 313da5c4664d13a9ee50b1cd1a0a981106770983 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 14:58:18 -0500 Subject: [PATCH 29/36] add 'modeBarButtonsToAdd' config arg: - takes an array of buttons config objects (as the defined for the default buttons in modebar/buttons.js) - these 'buttons to add' get appended an additional group of buttons passed to the ModeBar constructor --- src/components/modebar/manage.js | 18 +++++++++++++++--- src/plot_api/plot_config.js | 6 +++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 0352c4e9034..026550836c3 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -40,16 +40,26 @@ module.exports = function manageModeBar(gd) { ].join(' ')); } -// if(!Array.isArray(context.m)) + if(!Array.isArray(context.modeBarButtonsToAdd)) { + throw new Error([ + '*modeBarButtonsToAdd* configuration options', + 'must be an array.' + ].join(' ')); + } + - var buttonGroups = getButtonGroups(fullLayout, context.modeBarButtonsToRemove); + buttonGroups = getButtonGroups( + fullLayout, + context.modeBarButtonsToRemove, + context.modeBarButtonsToAdd + ); if(modeBar) modeBar.update(gd, buttonGroups); else fullLayout._modeBar = createModeBar(gd, buttonGroups); }; // logic behind which buttons are displayed by default -function getButtonGroups(fullLayout, buttonsToRemove) { +function getButtonGroups(fullLayout, buttonsToRemove, buttonsToAdd) { var groups = []; function addGroup(newGroup) { @@ -97,6 +107,8 @@ function getButtonGroups(fullLayout, buttonsToRemove) { addGroup(['hoverClosestPie']); } + if(buttonsToAdd.length) groups.push(buttonsToAdd); + return groups; } diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index f17ad413500..181436d643e 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -58,10 +58,14 @@ module.exports = { // display the mode bar (true, false, or 'hover') displayModeBar: 'hover', - // remove modebar button by name + // remove mode bar button by name // (see ./components/modebar/buttons.js for the list of names) modeBarButtonsToRemove: [], + // add mode bar button using config objects + // (see ./components/modebar/buttons.js for list of arguments) + modeBarButtonsToAdd: [], + // add the plotly logo on the end of the mode bar displaylogo: true, From de960ab90833bc63af51e61a1d9f9fb92cd186e2 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 15:00:11 -0500 Subject: [PATCH 30/36] add 'modeBarButtons' config arg: - this allows for fully custom modebar buttons - modeBarButtons is a nested array of button config objects - support 'string' entries to reference default buttons --- src/components/modebar/manage.js | 31 +++++++++++++++++++++++++++++++ src/plot_api/plot_config.js | 6 ++++++ 2 files changed, 37 insertions(+) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 026550836c3..defdd839f67 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -47,12 +47,19 @@ module.exports = function manageModeBar(gd) { ].join(' ')); } + var customButtons = context.modeBarButtons; + var buttonGroups; + if(Array.isArray(customButtons) && customButtons.length > 1) { + buttonGroups = fillCustomButton(customButtons); + } + else { buttonGroups = getButtonGroups( fullLayout, context.modeBarButtonsToRemove, context.modeBarButtonsToAdd ); + } if(modeBar) modeBar.update(gd, buttonGroups); else fullLayout._modeBar = createModeBar(gd, buttonGroups); @@ -125,3 +132,27 @@ function areAllAxesFixed(fullLayout) { return allFixed; } + +// fill in custom buttons referring to default mode bar buttons +function fillCustomButton(customButtons) { + for(var i = 0; i < customButtons.length; i++) { + var buttonGroup = customButtons[i]; + + for(var j = 0; j < buttonGroup.length; j++) { + var button = buttonGroup[j]; + + if(typeof button === 'string') + if(modeBarButtons[button] !== undefined) { + customButtons[i][j] = modeBarButtons[button]; + } + else { + throw new Error([ + '*modeBarButtons* configuration options', + 'invalid button name' + ].join(' ')); + } + } + } + + return customButtons; +} diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 181436d643e..d19c7ca828b 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -66,6 +66,12 @@ module.exports = { // (see ./components/modebar/buttons.js for list of arguments) modeBarButtonsToAdd: [], + // fully custom mode bar buttons as nested array, + // where the outer arrays represents button groups, and + // the inner arrays have buttons config objects or names of default buttons + // (see ./components/modebar/buttons.js for more info) + modeBarButtons: false, + // add the plotly logo on the end of the mode bar displaylogo: true, From 08843efe8ba613b434709205ef83ef8f8d58449b Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 15:05:21 -0500 Subject: [PATCH 31/36] make Icons.question the fall back icon --- src/components/modebar/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index f6794d53d80..5fbfaaa3dcf 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -148,7 +148,7 @@ proto.createButton = function (config) { button.setAttribute('data-toggle', config.toggle || false); if(config.toggle) button.classList.add('active'); - button.appendChild(this.createIcon(config.icon || Icons.tooltip_basic)); + button.appendChild(this.createIcon(config.icon || Icons.question)); button.setAttribute('data-gravity', config.gravity || 'n'); return button; From 4e4afc66e63d6c4d0e5a2f30ce827eba30bee07e Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 15:30:02 -0500 Subject: [PATCH 32/36] fix condition on custom buttons --- src/components/modebar/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index defdd839f67..6c40c29f7e1 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -50,7 +50,7 @@ module.exports = function manageModeBar(gd) { var customButtons = context.modeBarButtons; var buttonGroups; - if(Array.isArray(customButtons) && customButtons.length > 1) { + if(Array.isArray(customButtons) && customButtons.length) { buttonGroups = fillCustomButton(customButtons); } else { From 3dd0c1583e637d5afd66be82a487fcb9e950baea Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 15:30:51 -0500 Subject: [PATCH 33/36] add test for modeBar 'add' and 'fully custom' modebar buttons --- test/jasmine/tests/modebar_test.js | 108 +++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 12 deletions(-) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 3b684ec7589..fa47dfaa8a9 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -28,7 +28,8 @@ describe('ModeBar', function() { _context: { displaylogo: true, displayModeBar: true, - modeBarButtonsToRemove: [] + modeBarButtonsToRemove: [], + modeBarButtonsToAdd: [] } }; } @@ -144,10 +145,9 @@ describe('ModeBar', function() { function getButtons(list) { for(var i = 0; i < list.length; i++) { for(var j = 0; j < list[i].length; j++) { - list[i][j] = { - name: list[i][j], - click: noop - }; + + // minimal button config object + list[i][j] = { name: list[i][j], click: noop }; } } return list; @@ -277,6 +277,13 @@ describe('ModeBar', function() { expect(function() { manageModeBar(gd); }).toThrowError(); }); + it('throws an error if modeBarButtonsToAdd isn\'t an array', function() { + var gd = getMockGraphInfo(); + gd._context.modeBarButtonsToAdd = 'not gonna work'; + + expect(function() { manageModeBar(gd); }).toThrowError(); + }); + it('displays or not mode bar according to displayModeBar config arg', function() { var gd = getMockGraphInfo(); manageModeBar(gd); @@ -297,12 +304,18 @@ describe('ModeBar', function() { expect(countLogo(gd._fullLayout._modeBar)).toEqual(0); }); - it('updates mode bar buttons if plot type changes', function() { + // gives 11 buttons in 5 groups by default + function setupGraphInfo() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; gd._fullLayout.xaxis = {fixedrange: false}; + return gd; + } + + it('updates mode bar buttons if plot type changes', function() { + var gd = setupGraphInfo(); + manageModeBar(gd); - manageModeBar(gd); // gives 11 buttons gd._fullLayout._hasCartesian = false; gd._fullLayout._hasGL3D = true; manageModeBar(gd); @@ -310,18 +323,89 @@ describe('ModeBar', function() { expect(countButtons(gd._fullLayout._modeBar)).toEqual(10); }); - it('updates mode bar buttons if plot type changes', function() { - var gd = getMockGraphInfo(); - gd._fullLayout._hasCartesian = true; - gd._fullLayout.xaxis = {fixedrange: false}; + it('updates mode bar buttons if modeBarButtonsToRemove changes', function() { + var gd = setupGraphInfo(); + manageModeBar(gd); - manageModeBar(gd); // gives 11 buttons gd._context.modeBarButtonsToRemove = ['toImage', 'sendDataToCloud']; manageModeBar(gd); expect(countButtons(gd._fullLayout._modeBar)).toEqual(9); }); + it('updates mode bar buttons if modeBarButtonsToAdd changes', function() { + var gd = setupGraphInfo(); + manageModeBar(gd); + + gd._context.modeBarButtonsToAdd = [{ + name: 'some button', + click: noop + }]; + manageModeBar(gd); + + expect(countGroups(gd._fullLayout._modeBar)).toEqual(6); + expect(countButtons(gd._fullLayout._modeBar)).toEqual(12); + }); + + it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove', function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtonsToRemove = [ + 'toImage', 'pan2d', 'hoverCompareCartesian' + ]; + gd._context.modeBarButtonsToAdd = [ + { name: 'some button', click: noop }, + { name: 'some other button', click: noop } + ]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(6); + expect(countButtons(modeBar)).toEqual(10); + }); + + it('sets up buttons with fully custom modeBarButtons', function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [[ + { name: 'some button', click: noop }, + { name: 'some other button', click: noop } + ], [ + { name: 'some button in another group', click: noop }, + { name: 'some other button in another group', click: noop } + ]]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(5); + }); + + it('sets up buttons with custom modeBarButtons + default name', function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [[ + { name: 'some button', click: noop }, + { name: 'some other button', click: noop } + ], [ + 'toImage', 'pan2d', 'hoverCompareCartesian' + ]]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(6); + }); + + it('throw error when modeBarButtons contains invalid name', function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [[ + 'toImage', 'pan2d', 'no gonna work' + ]]; + + expect(function() { manageModeBar(gd); }).toThrowError(); + }); + }); }); From 71033b9805095d71a5cc9308bd2f9f7a220c2720 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 24 Nov 2015 16:07:44 -0500 Subject: [PATCH 34/36] improve logic around button title and name --- src/components/modebar/index.js | 5 ++--- test/jasmine/tests/modebar_test.js | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 5fbfaaa3dcf..c97e763b1b7 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -122,9 +122,8 @@ proto.createButton = function (config) { button.className = 'modebar-btn'; var title = config.title; - if(title !== null && title !== false && title !== '') { - button.setAttribute('data-title', title || config.name); - } + if(title === undefined) title = config.name; + if(title || title === 0) button.setAttribute('data-title', title); if(config.attr !== undefined) button.setAttribute('data-attr', config.attr); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index fa47dfaa8a9..64ed6008729 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -101,7 +101,7 @@ describe('ModeBar', function() { expect(checkBtnAttr(modeBar, 0, 'data-title')).toEqual('the title too'); }); - it('hides title to when title is set to null or \'\' or false', function() { + it('hides title to when title is falsy but not 0', function() { var modeBar; modeBar = createModeBar(getMockGraphInfo(), [[ @@ -118,6 +118,11 @@ describe('ModeBar', function() { { name: 'button', title: false, click: noop } ]]); expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); + + modeBar = createModeBar(getMockGraphInfo(), [[ + { name: 'button', title: 0, click: noop } + ]]); + expect(checkBtnAttr(modeBar, 0, 'data-title')).toEqual('0'); }); }); From 26936aa0e776d787a7a163b736d1ca7a00c1528d Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 25 Nov 2015 09:38:18 -0500 Subject: [PATCH 35/36] add ability to multiple mode bar button groups via 'modeBarButtonToAdd' --- src/components/modebar/manage.js | 10 +++++++++- test/jasmine/tests/modebar_test.js | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 6c40c29f7e1..2a64c5efd53 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -114,7 +114,15 @@ function getButtonGroups(fullLayout, buttonsToRemove, buttonsToAdd) { addGroup(['hoverClosestPie']); } - if(buttonsToAdd.length) groups.push(buttonsToAdd); + // append buttonsToAdd to the groups + if(buttonsToAdd.length) { + if(Array.isArray(buttonsToAdd[0])) { + for(var i = 0; i < buttonsToAdd.length; i++) { + groups.push(buttonsToAdd[i]); + } + } + else groups.push(buttonsToAdd); + } return groups; } diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 64ed6008729..86d65b3780c 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -369,6 +369,26 @@ describe('ModeBar', function() { expect(countButtons(modeBar)).toEqual(10); }); + it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove (2)', function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtonsToRemove = [ + 'toImage', 'pan2d', 'hoverCompareCartesian' + ]; + gd._context.modeBarButtonsToAdd = [[ + { name: 'some button', click: noop }, + { name: 'some other button', click: noop } + ], [ + { name: 'some button 2', click: noop }, + { name: 'some other button 2', click: noop } + ]]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(7); + expect(countButtons(modeBar)).toEqual(12); + }); + it('sets up buttons with fully custom modeBarButtons', function() { var gd = setupGraphInfo(); gd._context.modeBarButtons = [[ From eadd7fb97e8e09c3125c7dc660751fba4166c339 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 25 Nov 2015 09:38:29 -0500 Subject: [PATCH 36/36] add comments --- src/components/modebar/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index c97e763b1b7..e0bdd3269de 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -85,7 +85,7 @@ proto.updateButtons = function(buttons) { throw new Error('must provide button \'name\' in button config'); } if(_this.buttonsNames.indexOf(buttonName) !== -1) { - throw new Error('button name \'', + buttonName + '\' is taken'); + throw new Error('button name \'' + buttonName + '\' is taken'); } _this.buttonsNames.push(buttonName); @@ -140,6 +140,8 @@ proto.createButton = function (config) { else { button.addEventListener('click', function(ev) { config.click(_this.graphInfo, ev); + + // only needed for 'hoverClosestGeo' which does not call relayout _this.updateActiveButton(ev.currentTarget); }); }