From 10bbf378d39a2a0030af081d94634ceedc192d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 26 Dec 2018 18:14:55 -0500 Subject: [PATCH 1/8] add configAttributes to plot_config.js module --- src/lib/loggers.js | 8 +- src/lib/queue.js | 6 +- src/plot_api/plot_api.js | 6 +- src/plot_api/plot_config.js | 619 ++++++++++++++++++++++----------- src/plot_api/template_api.js | 2 +- src/plot_api/validate.js | 2 +- test/jasmine/tests/lib_test.js | 2 +- 7 files changed, 428 insertions(+), 217 deletions(-) diff --git a/src/lib/loggers.js b/src/lib/loggers.js index 02be37218d2..f53e9b02ab7 100644 --- a/src/lib/loggers.js +++ b/src/lib/loggers.js @@ -10,7 +10,7 @@ /* eslint-disable no-console */ -var config = require('../plot_api/plot_config'); +var dfltConfig = require('../plot_api/plot_config').dfltConfig; var loggers = module.exports = {}; @@ -21,7 +21,7 @@ var loggers = module.exports = {}; */ loggers.log = function() { - if(config.logging > 1) { + if(dfltConfig.logging > 1) { var messages = ['LOG:']; for(var i = 0; i < arguments.length; i++) { @@ -33,7 +33,7 @@ loggers.log = function() { }; loggers.warn = function() { - if(config.logging > 0) { + if(dfltConfig.logging > 0) { var messages = ['WARN:']; for(var i = 0; i < arguments.length; i++) { @@ -45,7 +45,7 @@ loggers.warn = function() { }; loggers.error = function() { - if(config.logging > 0) { + if(dfltConfig.logging > 0) { var messages = ['ERROR:']; for(var i = 0; i < arguments.length; i++) { diff --git a/src/lib/queue.js b/src/lib/queue.js index 732fb013cd6..9ac3f1edd5d 100644 --- a/src/lib/queue.js +++ b/src/lib/queue.js @@ -6,12 +6,10 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var Lib = require('../lib'); -var config = require('../plot_api/plot_config'); - +var dfltConfig = require('../plot_api/plot_config').dfltConfig; /** * Copy arg array *without* removing `undefined` values from objects. @@ -91,7 +89,7 @@ queue.add = function(gd, undoFunc, undoArgs, redoFunc, redoArgs) { queueObj.redo.args.push(redoArgs); } - if(gd.undoQueue.queue.length > config.queueLength) { + if(gd.undoQueue.queue.length > dfltConfig.queueLength) { gd.undoQueue.queue.shift(); gd.undoQueue.index--; } diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 5400519d0ed..83e725a5292 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -33,7 +33,7 @@ var initInteractions = require('../plots/cartesian/graph_interact').initInteract var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); -var defaultConfig = require('./plot_config'); +var dfltConfig = require('./plot_config').dfltConfig; var manageArrays = require('./manage_arrays'); var helpers = require('./helpers'); var subroutines = require('./subroutines'); @@ -405,7 +405,7 @@ function emitAfterPlot(gd) { } exports.setPlotConfig = function setPlotConfig(obj) { - return Lib.extendFlat(defaultConfig, obj); + return Lib.extendFlat(dfltConfig, obj); }; function setBackground(gd, bgColor) { @@ -423,7 +423,7 @@ function opaqueSetBackground(gd, bgColor) { function setPlotContext(gd, config) { if(!gd._context) { - gd._context = Lib.extendDeep({}, defaultConfig); + gd._context = Lib.extendDeep({}, dfltConfig); // stash href, used to make robust clipPath URLs var base = d3.select('base'); diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index fe92027831a..6e351ceb8e6 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -14,212 +14,425 @@ * * The defaults are the appropriate settings for plotly.js, * so we get the right experience without any config argument. + * + * N.B. the config options are not coerced using Lib.coerce so keys + * like `valType` and `values` are only set for documentation purposes + * at the moment. */ -module.exports = { - - // no interactivity, for export or image generation - staticPlot: false, +var configAttributes = { + staticPlot: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether the graphs are interactive or not.', + 'If *false*, no interactivity, for export or image generation.' + ].join(' ') + }, - // base URL for the 'Edit in Chart Studio' (aka sendDataToCloud) mode bar button - // and the showLink/sendData on-graph link - plotlyServerURL: 'https://plot.ly', + plotlyServerURL: { + valType: 'string', + dflt: 'https://plot.ly', + description: [ + 'Sets base URL for the \'Edit in Chart Studio\' (aka sendDataToCloud) mode bar button', + 'and the showLink/sendData on-graph link' + ].join(' ') + }, - /* - * we can edit titles, move annotations, etc - sets all pieces of `edits` - * unless a separate `edits` config item overrides individual parts - */ - editable: false, + editable: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether the graph is editable or not.', + 'Sets all pieces of `edits`', + 'unless a separate `edits` config item overrides individual parts.' + ].join(' ') + }, edits: { - /* - * annotationPosition: the main anchor of the annotation, which is the - * text (if no arrow) or the arrow (which drags the whole thing leaving - * the arrow length & direction unchanged) - */ - annotationPosition: false, - // just for annotations with arrows, change the length and direction of the arrow - annotationTail: false, - annotationText: false, - axisTitleText: false, - colorbarPosition: false, - colorbarTitleText: false, - legendPosition: false, - // edit the trace name fields from the legend - legendText: false, - shapePosition: false, - // the global `layout.title` - titleText: false - }, - - /* - * DO autosize once regardless of layout.autosize - * (use default width or height values otherwise) - */ - autosizable: false, - - /* - * responsive: determines whether to change the layout size when window is resized. - * In v2, this option will be removed and will always be true. - */ - responsive: false, - - // set the length of the undo/redo queue - queueLength: 0, - - // if we DO autosize, do we fill the container or the screen? - fillFrame: false, - - // if we DO autosize, set the frame margins in percents of plot size - frameMargins: 0, - - // mousewheel or two-finger scroll zooms the plot - scrollZoom: false, - - // double click interaction (false, 'reset', 'autosize' or 'reset+autosize') - doubleClick: 'reset+autosize', - - // new users see some hints about interactivity - showTips: true, - - // enable axis pan/zoom drag handles - showAxisDragHandles: true, - - /* - * enable direct range entry at the pan/zoom drag points - * (drag handles must be enabled above) - */ - showAxisRangeEntryBoxes: true, - - /* - * Add a text link to open this plot in plotly? - * This link shows up in the bottom right corner of the plot, and works - * identically to the newer ModeBar button controlled by `showSendToCloud` - * unless `sendData: false` is used. - */ - showLink: false, - - /* - * If we show a text link (`showLink: true`), does it contain data or just - * a reference to a plotly cloud file? This option should only be used on - * plot.ly or another plotly server, and is not supported by the newer - * ModeBar button `showSendToCloud`. - */ - sendData: true, - - /* - * Should we include a ModeBar button, labeled "Edit in Chart Studio", - * that sends this chart to plot.ly or another plotly server as specified - * by `plotlyServerURL` for editing, export, etc? Prior to version 1.43.0 - * this button was included by default, now it is opt-in using this flag. - * - * Note that this button can (depending on `plotlyServerURL`) send your data - * to an external server. However that server doesn't persist your data - * until you arrive at the Chart Studio and explicitly click "Save". - */ - showSendToCloud: false, - - // text appearing in the sendData link - linkText: 'Edit chart', - - // false or function adding source(s) to linkText - showSources: false, - - // display the mode bar (true, false, or 'hover') - displayModeBar: 'hover', - - /* - * 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: [], - - /* - * 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, - - // statically override options for toImage modebar button - // allowed keys are format, filename, width, height, scale - // see ../components/modebar/buttons.js - toImageButtonOptions: {}, - - // add the plotly logo on the end of the mode bar - displaylogo: true, - - // watermark the images with the company's logo - watermark: false, - - // increase the pixel ratio for Gl plot images - plotGlPixelRatio: 2, - - /* - * background setting function - * 'transparent' sets the background `layout.paper_color` - * 'opaque' blends bg color with white ensuring an opaque background - * or any other custom function of gd - */ - setBackground: 'transparent', - - // URL to topojson files used in geo charts - topojsonURL: 'https://cdn.plot.ly/', - - /* - * Mapbox access token (required to plot mapbox trace types) - * If using an Mapbox Atlas server, set this option to '', - * so that plotly.js won't attempt to authenticate to the public Mapbox server. - */ - mapboxAccessToken: null, - - /* - * Turn all console logging on or off (errors will be thrown) - * This should ONLY be set via Plotly.setPlotConfig - * 0: no logs - * 1: warnings and errors, but not informational messages - * 2: verbose logs - */ - logging: 1, - - /* - * Set global transform to be applied to all traces with no - * specification needed - */ - globalTransforms: [], - - /* - * Which localization should we use? - * Should be a string like 'en' or 'en-US'. - */ - locale: 'en-US', - - /* - * Localization definitions - * Locales can be provided either here (specific to one chart) or globally - * by registering them as modules. - * Should be an object of objects {locale: {dictionary: {...}, format: {...}}} - * { - * da: { - * dictionary: {'Reset axes': 'Nulstil aksler', ...}, - * format: {months: [...], shortMonths: [...]} - * }, - * ... - * } - * All parts are optional. When looking for translation or format fields, we - * look first for an exact match in a config locale, then in a registered - * module. If those fail, we strip off any regionalization ('en-US' -> 'en') - * and try each (config, registry) again. The final fallback for translation - * is untranslated (which is US English) and for formats is the base English - * (the only consequence being the last fallback date format %x is DD/MM/YYYY - * instead of MM/DD/YYYY). Currently `grouping` and `currency` are ignored - * for our automatic number formatting, but can be used in custom formats. - */ - locales: {} + annotationPosition: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines if the main anchor of the annotation is editable.', + 'The main anchor corresponds to the', + 'text (if no arrow) or the arrow (which drags the whole thing leaving', + 'the arrow length & direction unchanged).' + ].join(' ') + }, + annotationTail: { + valType: 'boolean', + dflt: false, + description: [ + 'Has only an effect for annotations with arrows.', + 'Enables changing the length and direction of the arrow.' + ].join(' ') + }, + annotationText: { + valType: 'boolean', + dflt: false, + description: 'Enables editing annotation text.' + }, + axisTitleText: { + valType: 'boolean', + dflt: false, + description: 'Enables editing axis title text.' + }, + colorbarPosition: { + valType: 'boolean', + dflt: false, + description: 'Enables moving colorbars.' + }, + colorbarTitleText: { + valType: 'boolean', + dflt: false, + description: 'Enables editing colorbar title text.' + }, + legendPosition: { + valType: 'boolean', + dflt: false, + description: 'Enables moving the legend.' + }, + legendText: { + valType: 'boolean', + dflt: false, + description: 'Enables editing the trace name fields from the legend' + }, + shapePosition: { + valType: 'boolean', + dflt: false, + description: 'Enables moving shapes.' + }, + titleText: { + valType: 'boolean', + dflt: false, + description: 'Enables editing the global layout title.' + } + }, + + autosizable: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether the graphs are plotted with respect to', + 'layout.autosize:true and infer its container size.' + ].join(' ') + }, + responsive: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether to change the layout size when window is resized.', + 'In v2, this option will be removed and will always be true.' + ].join(' ') + }, + fillFrame: { + valType: 'boolean', + dflt: false, + description: [ + 'When `layout.autosize` is turned on, determines whether the graph', + 'fills the container (the default) or the screen (if set to *true*).' + ].join(' ') + }, + frameMargins: { + valType: 'number', + dflt: 0, + min: 0, + max: 0.5, + description: [ + 'When `layout.autosize` is turned on, set the frame margins', + 'in fraction of the graph size.' + ].join(' ') + }, + + scrollZoom: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether mouse wheel or two-finger scroll zooms is', + 'enable. Has an effect only on cartesian subplots.' + ].join(' ') + }, + doubleClick: { + valType: 'enumerated', + values: [false, 'reset', 'autosize', 'reset+autosize'], + dflt: 'reset+autosize', + description: [ + 'Sets the double click interaction mode.', + 'Has an effect only in cartesian plots.', + 'If *false*, double click is disable.', + 'If *reset*, double click resets the axis ranges to their initial values.', + 'If *autosize*, double click set the axis ranges to their autorange values.', + 'If *reset+autosize, the odd double clicks resets the axis ranges', + 'to their initial values and even double clicks set the axis ranges', + 'to their autorange values.' + ].join(' ') + }, + + showAxisDragHandles: { + valType: 'boolean', + dflt: true, + description: [ + 'Set to *false* to omit cartesian axis pan/zoom drag handles.' + ].join(' ') + }, + showAxisRangeEntryBoxes: { + valType: 'boolean', + dflt: true, + description: [ + 'Set to *false* to omit direct range entry at the pan/zoom drag points,', + 'note that `showAxisDragHandles` must be enabled to have an effect.' + ].join(' ') + }, + + showTips: { + valType: 'boolean', + dflt: true, + description: [ + 'Determines whether or not tips are shown while interacting', + 'with the resulting graphs.' + ].join(' ') + }, + + showLink: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether a link to plot.ly is displayed', + 'at the bottom right corner of resulting graphs.', + 'Use with `sendData` and `linkText`.' + ].join(' ') + }, + linkText: { + valType: 'string', + dflt: 'Edit chart', + noBlank: true, + description: [ + 'Sets the text appearing in the `showLink` link.' + ].join(' ') + }, + sendData: { + valType: 'boolean', + dflt: true, + description: [ + 'If *showLink* is true, does it contain data', + 'just link to a plot.ly file?' + ].join(' ') + }, + showSources: { + valType: 'any', + dflt: false, + description: [ + 'Adds a source-displaying function to show sources on', + 'the resulting graphs.' + ].join(' ') + }, + + displayModeBar: { + valType: 'enumerated', + values: ['hover', true, false], + dflt: 'hover', + description: [ + 'Determines the mode bar display mode.', + 'If *true*, the mode bar is always visible.', + 'If *false*, the mode bar is always hidden.', + 'If *hover*, the mode bar is visible while the mouse cursor', + 'is on the graph container.' + ].join(' ') + }, + showSendToCloud: { + valType: 'boolean', + dflt: false, + description: [ + 'Should we include a ModeBar button, labeled "Edit in Chart Studio",', + 'that sends this chart to plot.ly or another plotly server as specified', + 'by `plotlyServerURL` for editing, export, etc? Prior to version 1.43.0', + 'this button was included by default, now it is opt-in using this flag.', + 'Note that this button can (depending on `plotlyServerURL`) send your data', + 'to an external server. However that server does not persist your data', + 'until you arrive at the Chart Studio and explicitly click "Save".' + ].join(' ') + }, + modeBarButtonsToRemove: { + valType: 'any', + dflt: [], + description: [ + 'Remove mode bar buttons by name.', + 'See ./components/modebar/buttons.js for the list of names.' + ].join(' ') + }, + modeBarButtonsToAdd: { + valType: 'any', + dflt: [], + description: [ + 'Add mode bar button using config objects', + 'See ./components/modebar/buttons.js for list of arguments.' + ].join(' ') + }, + modeBarButtons: { + valType: 'any', + dflt: false, + description: [ + 'Define 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.' + ].join(' ') + }, + toImageButtonOptions: { + valType: 'any', + dflt: {}, + description: [ + 'Statically override options for toImage modebar button', + 'allowed keys are format, filename, width, height, scale', + 'see ../components/modebar/buttons.js' + ].join(' ') + }, + displaylogo: { + valType: 'boolean', + dflt: true, + description: [ + 'Determines whether or not the plotly logo is displayed', + 'on the end of the mode bar.' + ].join(' ') + }, + watermark: { + valType: 'boolean', + dflt: false, + description: 'watermark the images with the company\'s logo' + }, + + plotGlPixelRatio: { + valType: 'number', + dflt: 2, + min: 0, + max: 4, + description: [ + 'Set the pixel ratio during WebGL image export.', + 'This config option was formerly named `plot3dPixelRatio`', + 'which is now deprecated.' + ].join(' ') + }, + + setBackground: { + valType: 'any', + dflt: 'transparent', + description: [ + 'Set function to add the background color (i.e. `layout.paper_color`)', + 'to a different container.', + 'This function take the graph div as first argument and the current background', + 'color as second argument.', + 'Alternatively, set to string *opaque* to ensure there is white behind it.' + ].join(' ') + }, + + topojsonURL: { + valType: 'string', + noBlank: true, + dflt: 'https://cdn.plot.ly/', + description: [ + 'Set the URL to topojson used in geo charts.', + 'By default, the topojson files are fetched from cdn.plot.ly.', + 'For example, set this option to:', + '/dist/topojson/', + 'to render geographical feature using the topojson files', + 'that ship with the plotly.js module.' + ].join(' ') + }, + + mapboxAccessToken: { + valType: 'string', + dflt: null, + description: [ + 'Mapbox access token (required to plot mapbox trace types)', + 'If using an Mapbox Atlas server, set this option to \'\'', + 'so that plotly.js won\'t attempt to authenticate to the public Mapbox server.' + ].join(' ') + }, + + logging: { + valType: 'boolean', + dflt: 1, + description: [ + 'Turn all console logging on or off (errors will be thrown)', + 'This should ONLY be set via Plotly.setPlotConfig', + 'Available levels:', + '0: no logs', + '1: warnings and errors, but not informational messages', + '2: verbose logs' + ].join(' ') + }, + + queueLength: { + valType: 'integer', + min: 0, + dflt: 0, + description: 'Sets the length of the undo/redo queue.' + }, + + globalTransforms: { + valType: 'any', + dflt: [], + description: [ + 'Set global transform to be applied to all traces with no', + 'specification needed' + ].join(' ') + }, + + locale: { + valType: 'string', + dflt: 'en-US', + description: [ + 'Which localization should we use?', + 'Should be a string like \'en\' or \'en-US\'.' + ].join(' ') + }, + + locales: { + valType: 'any', + dflt: {}, + description: [ + 'Localization definitions', + 'Locales can be provided either here (specific to one chart) or globally', + 'by registering them as modules.', + 'Should be an object of objects {locale: {dictionary: {...}, format: {...}}}', + '{', + ' da: {', + ' dictionary: {\'Reset axes\': \'Nulstil aksler\', ...},', + ' format: {months: [...], shortMonths: [...]}', + ' },', + ' ...', + '}', + 'All parts are optional. When looking for translation or format fields, we', + 'look first for an exact match in a config locale, then in a registered', + 'module. If those fail, we strip off any regionalization (\'en-US\' -> \'en\')', + 'and try each (config, registry) again. The final fallback for translation', + 'is untranslated (which is US English) and for formats is the base English', + '(the only consequence being the last fallback date format %x is DD/MM/YYYY', + 'instead of MM/DD/YYYY). Currently `grouping` and `currency` are ignored', + 'for our automatic number formatting, but can be used in custom formats.' + ].join(' ') + } +}; + +var dfltConfig = {}; + +function crawl(src, target) { + for(var k in src) { + var obj = src[k]; + if(obj.valType) { + target[k] = obj.dflt; + } else { + if(!target[k]) { + target[k] = {}; + } + crawl(obj, target[k]); + } + } +} + +crawl(configAttributes, dfltConfig); + +module.exports = { + configAttributes: configAttributes, + dfltConfig: dfltConfig }; diff --git a/src/plot_api/template_api.js b/src/plot_api/template_api.js index 98565fbb4ea..94128c0b9e2 100644 --- a/src/plot_api/template_api.js +++ b/src/plot_api/template_api.js @@ -15,7 +15,7 @@ var PlotSchema = require('./plot_schema'); var Plots = require('../plots/plots'); var plotAttributes = require('../plots/attributes'); var Template = require('./plot_template'); -var dfltConfig = require('./plot_config'); +var dfltConfig = require('./plot_config').dfltConfig; /** * Plotly.makeTemplate: create a template off an existing figure to reuse diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index 7c0f3dcac23..ce7b5e4dd36 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -11,7 +11,7 @@ var Lib = require('../lib'); var Plots = require('../plots/plots'); var PlotSchema = require('./plot_schema'); -var dfltConfig = require('./plot_config'); +var dfltConfig = require('./plot_config').dfltConfig; var isPlainObject = Lib.isPlainObject; var isArray = Array.isArray; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 0b9d0604e35..00351710619 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1,7 +1,7 @@ var Lib = require('@src/lib'); var setCursor = require('@src/lib/setcursor'); var overrideCursor = require('@src/lib/override_cursor'); -var config = require('@src/plot_api/plot_config'); +var config = require('@src/plot_api/plot_config').dfltConfig; var d3 = require('d3'); var Plotly = require('@lib'); From 7698e7690e63fbd10398ece4976bd4a1403b1470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 26 Dec 2018 18:15:06 -0500 Subject: [PATCH 2/8] add config attributes to plot-schema --- src/plot_api/plot_schema.js | 7 +++++-- test/jasmine/bundle_tests/plotschema_test.js | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 265e8bf5d03..db6ec1e5779 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -16,6 +16,7 @@ var baseAttributes = require('../plots/attributes'); var baseLayoutAttributes = require('../plots/layout_attributes'); var frameAttributes = require('../plots/frame_attributes'); var animationAttributes = require('../plots/animation_attributes'); +var configAttributes = require('./plot_config').configAttributes; // polar attributes are not part of the Registry yet var polarAreaAttrs = require('../plots/polar/legacy/area_attributes'); @@ -47,7 +48,7 @@ exports.UNDERSCORE_ATTRS = UNDERSCORE_ATTRS; * - transforms * - frames * - animations - * - config (coming soon ...) + * - config */ exports.get = function() { var traces = {}; @@ -96,7 +97,9 @@ exports.get = function() { transforms: transforms, frames: getFramesAttributes(), - animation: formatAttributes(animationAttributes) + animation: formatAttributes(animationAttributes), + + config: formatAttributes(configAttributes) }; }; diff --git a/test/jasmine/bundle_tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js index 40b99b1a86a..8fb836176e9 100644 --- a/test/jasmine/bundle_tests/plotschema_test.js +++ b/test/jasmine/bundle_tests/plotschema_test.js @@ -328,6 +328,11 @@ describe('plot schema', function() { expect(plotSchema.frames.items.frames_entry.role).toEqual('object'); }); + it('should list config attributes', function() { + expect(plotSchema.config).toBeDefined(); + expect(plotSchema.config.scrollZoom).toBeDefined(); + }); + it('should list trace-dependent & direction-dependent error bar attributes', function() { var scatterSchema = plotSchema.traces.scatter.attributes; expect(scatterSchema.error_x.copy_ystyle).toBeDefined(); From c83538a4ae0c6b16f0341d5953422aeeb3ab7bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 8 Jan 2019 15:56:10 -0500 Subject: [PATCH 3/8] make scrollZoom a flaglist w/ dflt 'gl3d+geo+mapbox' - which match the current "default", but will make scrollZoom:false disable zoom for all subplots! --- src/plot_api/plot_api.js | 19 +++++++++++ src/plot_api/plot_config.js | 13 ++++--- src/plots/cartesian/dragbox.js | 2 +- src/plots/gl2d/scene2d.js | 2 +- test/jasmine/tests/config_test.js | 57 +++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 7c4e21b69d0..d8948fcb50e 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -507,6 +507,25 @@ function setPlotContext(gd, config) { // Check if gd has a specified widht/height to begin with context._hasZeroHeight = context._hasZeroHeight || gd.clientHeight === 0; context._hasZeroWidth = context._hasZeroWidth || gd.clientWidth === 0; + + // fill context._scrollZoom helper to help manage scrollZoom flaglist + var szIn = context.scrollZoom; + var szOut = context._scrollZoom = {}; + if(szIn === true) { + szOut.cartesian = 1; + szOut.gl3d = 1; + szOut.geo = 1; + szOut.mapbox = 1; + } else if(typeof szIn === 'string') { + var parts = szIn.split('+'); + for(i = 0; i < parts.length; i++) { + szOut[parts[i]] = 1; + } + } else if(szIn !== false) { + szOut.gl3d = 1; + szOut.geo = 1; + szOut.mapbox = 1; + } } function plotLegacyPolar(gd, data, layout) { diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 13f88a4ed48..bac84f84ecc 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -145,11 +145,16 @@ var configAttributes = { }, scrollZoom: { - valType: 'boolean', - dflt: false, + valType: 'flaglist', + flags: ['cartesian', 'gl3d', 'geo', 'mapbox'], + extras: [true, false], + dflt: 'gl3d+geo+mapbox', description: [ - 'Determines whether mouse wheel or two-finger scroll zooms is', - 'enable. Has an effect only on cartesian subplots.' + 'Determines whether mouse wheel or two-finger scroll zooms is enable.', + 'Turned on by default for gl3d, geo and mapbox subplots', + '(as these subplot types do not have zoombox via pan),', + 'but turned off by default for cartesian subplots.', + 'Set `scrollZoom` to *false* to disable scrolling for all subplots.' ].join(' ') }, doubleClick: { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index e8110756b0c..10d2cb97e60 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -417,7 +417,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // deactivate mousewheel scrolling on embedded graphs // devs can override this with layout._enablescrollzoom, // but _ ensures this setting won't leave their page - if(!gd._context.scrollZoom && !gd._fullLayout._enablescrollzoom) { + if(!gd._context._scrollZoom.cartesian && !gd._fullLayout._enablescrollzoom) { return; } diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 0b8ea837ea8..789efdfe096 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -38,7 +38,7 @@ function Scene2D(options, fullLayout) { this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio; this.id = options.id; this.staticPlot = !!options.staticPlot; - this.scrollZoom = this.graphDiv._context.scrollZoom; + this.scrollZoom = this.graphDiv._context._scrollZoom.cartesian; this.fullData = null; this.updateRefs(fullLayout); diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 2c8b10d8c39..1dbb20a0f34 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -760,4 +760,61 @@ describe('config argument', function() { .then(done); }); }); + + describe('scrollZoom:', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + + afterEach(destroyGraphDiv); + + function plot(config) { + return Plotly.plot(gd, [], {}, config); + } + + it('should fill in scrollZoom default', function(done) { + plot(undefined).then(function() { + expect(gd._context.scrollZoom).toBe('gl3d+geo+mapbox'); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + }) + .catch(failTest) + .then(done); + }); + + it('should fill in blank scrollZoom value', function(done) { + plot({scrollZoom: null}).then(function() { + expect(gd._context.scrollZoom).toBe(null); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom:true', function(done) { + plot({scrollZoom: true}).then(function() { + expect(gd._context.scrollZoom).toBe(true); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, cartesian: 1, mapbox: 1}); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom:false', function(done) { + plot({scrollZoom: false}).then(function() { + expect(gd._context.scrollZoom).toBe(false); + expect(gd._context._scrollZoom).toEqual({}); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom flaglist', function(done) { + plot({scrollZoom: 'mapbox+cartesian'}).then(function() { + expect(gd._context.scrollZoom).toBe('mapbox+cartesian'); + expect(gd._context._scrollZoom).toEqual({mapbox: 1, cartesian: 1}); + }) + .catch(failTest) + .then(done); + }); + }); }); From b56591d0ad6c5101f14aae3876641431907adde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 8 Jan 2019 16:49:34 -0500 Subject: [PATCH 4/8] resolves #143 - implement scrollZoom:false for geo subplots --- src/plots/geo/geo.js | 3 +++ test/jasmine/tests/geo_test.js | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index ce4fb293994..9a0961f1661 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -420,6 +420,9 @@ proto.updateFx = function(fullLayout, geoLayout) { bgRect.node().onmousedown = null; bgRect.call(createGeoZoom(_this, geoLayout)); bgRect.on('dblclick.zoom', zoomReset); + if(!gd._context._scrollZoom.geo) { + bgRect.on('wheel.zoom', null); + } } else if(dragMode === 'select' || dragMode === 'lasso') { bgRect.on('.zoom', null); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 0c57fc6e444..be42d2abc08 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -1961,4 +1961,52 @@ describe('Test geo zoom/pan/drag interactions:', function() { .catch(failTest) .then(done); }); + + it('should respect scrollZoom config option', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/geo_winkel-tripel')); + fig.layout.width = 700; + fig.layout.height = 500; + fig.layout.dragmode = 'pan'; + + function _assert(step, attr, proj, eventKeys) { + var msg = '[' + step + '] '; + + var geoLayout = gd._fullLayout.geo; + var scale = geoLayout.projection.scale; + expect(scale).toBeCloseTo(attr[0], 1, msg + 'zoom'); + + var geo = geoLayout._subplot; + var _scale = geo.projection.scale(); + expect(_scale).toBeCloseTo(proj[0], 0, msg + 'scale'); + + assertEventData(msg, eventKeys); + } + + plot(fig) + .then(function() { + _assert('base', [1], [101.9], undefined); + }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scroll enable (by default)', + [1.3], [134.4], + ['geo.projection.rotation.lon', 'geo.center.lon', 'geo.center.lat', 'geo.projection.scale'] + ); + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scrollZoom:false', [1.3], [134.4], undefined); + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: 'geo'}); }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scrollZoom:geo', + [1.74], [177.34], + ['geo.projection.rotation.lon', 'geo.center.lon', 'geo.center.lat', 'geo.projection.scale'] + ); + }) + .catch(failTest) + .then(done); + }); }); From 26618dd822aa0362cad2684dd17c8753a0ae9cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 8 Jan 2019 17:03:03 -0500 Subject: [PATCH 5/8] resolves #2998 - implement scrollZoom:false for gl3d subplots --- src/plots/gl3d/camera.js | 3 + src/plots/gl3d/scene.js | 14 +++-- test/jasmine/tests/gl3d_plot_interact_test.js | 59 +++++++++++-------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/plots/gl3d/camera.js b/src/plots/gl3d/camera.js index 0331cd3f0b3..32e796a8760 100644 --- a/src/plots/gl3d/camera.js +++ b/src/plots/gl3d/camera.js @@ -48,6 +48,7 @@ function createCamera(element, options) { var camera = { keyBindingMode: 'rotate', + enableWheel: true, view: view, element: element, delay: options.delay || 16, @@ -257,7 +258,9 @@ function createCamera(element, options) { } camera.wheelListener = mouseWheel(element, function(dx, dy) { + // TODO remove now that we can disable scroll via scrollZoom? if(camera.keyBindingMode === false) return; + if(!camera.enableWheel) return; var flipX = camera.flipX ? 1 : -1; var flipY = camera.flipY ? 1 : -1; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index bb3dea88c8c..a6bc7c91a56 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -227,8 +227,15 @@ function initializeGLPlot(scene, canvas, gl) { scene.graphDiv.emit('plotly_relayout', update); }; - scene.glplot.canvas.addEventListener('mouseup', relayoutCallback.bind(null, scene)); - scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene), passiveSupported ? {passive: false} : false); + scene.glplot.canvas.addEventListener('mouseup', function() { + relayoutCallback(scene); + }); + + scene.glplot.canvas.addEventListener('wheel', function() { + if(gd._context._scrollZoom.gl3d) { + relayoutCallback(scene); + } + }, passiveSupported ? {passive: false} : false); if(!scene.staticMode) { scene.glplot.canvas.addEventListener('webglcontextlost', function(event) { @@ -385,7 +392,6 @@ function computeTraceBounds(scene, trace, bounds) { } proto.plot = function(sceneData, fullLayout, layout) { - // Save parameters this.plotArgs = [sceneData, fullLayout, layout]; @@ -412,6 +418,7 @@ proto.plot = function(sceneData, fullLayout, layout) { // Update camera and camera mode this.setCamera(fullSceneLayout.camera); this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); + this.camera.enableWheel = this.graphDiv._context._scrollZoom.gl3d; // Update scene this.glplot.update({}); @@ -776,7 +783,6 @@ proto.updateFx = function(dragmode, hovermode) { fullCamera.up = zUp; Lib.nestedProperty(layout, attr).set(zUp); } else { - // none rotation modes [pan or zoom] camera.keyBindingMode = dragmode; } diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 65de62cce8f..7578762f3a2 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -1097,6 +1097,11 @@ describe('Test gl3d drag and wheel interactions', function() { } }; + function _assertAndReset(cnt) { + expect(relayoutCallback).toHaveBeenCalledTimes(cnt); + relayoutCallback.calls.reset(); + } + Plotly.plot(gd, mock) .then(function() { relayoutCallback = jasmine.createSpy('relayoutCallback'); @@ -1115,48 +1120,32 @@ describe('Test gl3d drag and wheel interactions', function() { return scroll(sceneTarget); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return scroll(sceneTarget2); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return Plotly.relayout(gd, { - 'scene.dragmode': false, - 'scene2.dragmode': false - }); + _assertAndReset(1); + return Plotly.relayout(gd, {'scene.dragmode': false, 'scene2.dragmode': false}); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget, [0, 0], [100, 100]); }) .then(function() { return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(0); + _assertAndReset(0); - return Plotly.relayout(gd, { - 'scene.dragmode': 'orbit', - 'scene2.dragmode': 'turntable' - }); + return Plotly.relayout(gd, {'scene.dragmode': 'orbit', 'scene2.dragmode': 'turntable'}); }) .then(function() { expect(relayoutCallback).toHaveBeenCalledTimes(1); @@ -1168,7 +1157,27 @@ describe('Test gl3d drag and wheel interactions', function() { return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(2); + _assertAndReset(2); + return Plotly.plot(gd, [], {}, {scrollZoom: false}); + }) + .then(function() { + return scroll(sceneTarget); + }) + .then(function() { + return scroll(sceneTarget2); + }) + .then(function() { + _assertAndReset(0); + return Plotly.plot(gd, [], {}, {scrollZoom: 'gl3d'}); + }) + .then(function() { + return scroll(sceneTarget); + }) + .then(function() { + return scroll(sceneTarget2); + }) + .then(function() { + _assertAndReset(2); }) .catch(failTest) .then(done); From 16a01e7d4fd388f68b3bce61af0a362e6770d300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 8 Jan 2019 17:29:31 -0500 Subject: [PATCH 6/8] implement scrollZoom:false for mapbox subplots --- src/plots/mapbox/mapbox.js | 6 +++++ test/jasmine/tests/mapbox_test.js | 45 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 78509140a2c..51e8ea9372d 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -258,6 +258,12 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) { self.updateLayout(fullLayout); self.resolveOnRender(resolve); } + + if(this.gd._context._scrollZoom.mapbox) { + map.scrollZoom.enable(); + } else { + map.scrollZoom.disable(); + } }; proto.updateData = function(calcData) { diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 9d6f6f8e53a..a0881b6c17b 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -976,6 +976,51 @@ describe('@noCI, mapbox plots', function() { .then(done); }, LONG_TIMEOUT_INTERVAL); + it('should respect scrollZoom config option', function(done) { + var relayoutCnt = 0; + gd.on('plotly_relayout', function() { relayoutCnt++; }); + + function _scroll() { + relayoutCnt = 0; + return new Promise(function(resolve) { + mouseEvent('mousemove', pointPos[0], pointPos[1]); + mouseEvent('scroll', pointPos[0], pointPos[1], {deltaY: -400}); + setTimeout(resolve, 500); + }); + } + + var zoom = getMapInfo(gd).zoom; + expect(zoom).toBeCloseTo(1.234); + + _scroll().then(function() { + expect(relayoutCnt).toBe(1, 'scroll relayout cnt'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBeGreaterThan(zoom); + zoom = zoomNew; + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) + .then(_scroll) + .then(function() { + expect(relayoutCnt).toBe(0, 'no additional relayout call'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBe(zoom); + zoom = zoomNew; + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: true}); }) + .then(_scroll) + .then(function() { + expect(relayoutCnt).toBe(1, 'scroll relayout cnt'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBeGreaterThan(zoom); + zoom = zoomNew; + }) + .catch(failTest) + .then(done); + }, LONG_TIMEOUT_INTERVAL); + function getMapInfo(gd) { var subplot = gd._fullLayout.mapbox._subplot; var map = subplot.map; From 09f1af5c3bcf97a079286a4983c27a1088ba3f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Jan 2019 10:35:18 -0500 Subject: [PATCH 7/8] explicitly assert that _scrollZoom.cartesian isn't set --- test/jasmine/tests/config_test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 1dbb20a0f34..0d6715531a3 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -776,6 +776,7 @@ describe('config argument', function() { plot(undefined).then(function() { expect(gd._context.scrollZoom).toBe('gl3d+geo+mapbox'); expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + expect(gd._context._scrollZoom.cartesian).toBe(undefined, 'no cartesian!'); }) .catch(failTest) .then(done); @@ -785,6 +786,7 @@ describe('config argument', function() { plot({scrollZoom: null}).then(function() { expect(gd._context.scrollZoom).toBe(null); expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + expect(gd._context._scrollZoom.cartesian).toBe(undefined, 'no cartesian!'); }) .catch(failTest) .then(done); From bed30cd0b242a8552d6d29f57a6a6bc206f16b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Jan 2019 11:38:02 -0500 Subject: [PATCH 8/8] config opts declaration fixes --- src/plot_api/plot_config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index bac84f84ecc..c78a955280f 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -167,7 +167,7 @@ var configAttributes = { 'If *false*, double click is disable.', 'If *reset*, double click resets the axis ranges to their initial values.', 'If *autosize*, double click set the axis ranges to their autorange values.', - 'If *reset+autosize, the odd double clicks resets the axis ranges', + 'If *reset+autosize*, the odd double clicks resets the axis ranges', 'to their initial values and even double clicks set the axis ranges', 'to their autorange values.' ].join(' ') @@ -309,7 +309,7 @@ var configAttributes = { plotGlPixelRatio: { valType: 'number', dflt: 2, - min: 0, + min: 1, max: 4, description: [ 'Set the pixel ratio during WebGL image export.',