diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..527aeefc9ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +Thanks for your interest in plotly.js! + +Before opening an issue, please search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/plotly/plotly.js/issues/new). + +Bug reports must be accompanied with a reproducible example. We recommend using [codepen](http://codepen.io/), [jsfiddle](https://jsfiddle.net/) or [jsbin](https://jsbin.com) to share your example. + +Note that GitHub issues are reserved for bug reports and feature requests only. Implementation questions should be asked on community.plot.ly (tagged [`plotly-js`](http://community.plot.ly/c/plotly-js)) or on Stack Overflow (tagged [`plotly`](https://stackoverflow.com/questions/tagged/plotly)). + +Comments on GitHub issues or pull requests should add content to the discussions. Approbation comments such as *+1* or *I would like this feature to be implemented as well* will be deleted by the maintainers. Please use [GitHub reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) instead. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..5d27f6d4caa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +Thanks for your interest in plotly.js! + +Developers are strongly encouraged to first make a PR to their own plotly.js fork and ask one of the maintainers to review the modifications there. Once the pull request is deemed satisfactory, the developer will be asked to make a pull request to the main plotly.js repo and may be asked to squash some commits before doing so. + +Developers should `git rebase` their local branch off the latest `master` before opening a pull request. + +Note that it is forbidden to force push (i.e. `git push -f`) to remote branches associated with opened pull requests. Force pushes make it hard for maintainers to keep track of updates. Therefore, if required, please `git merge master` into your PR branch instead of `git rebase master`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e857af653ce..e3841feb578 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,40 +2,11 @@ ## Opening issues -Search for existing and closed issues. If your problem or idea is not addressed -yet, [please open a new issue](https://github.com/plotly/plotly.js/issues/new). - -Bug reports must be accompanied with a reproducible example. We recommend using -[codepen](http://codepen.io/), [jsfiddle](https://jsfiddle.net/) or -[jsbin](https://jsbin.com) to share your example. - -Note that GitHub issues are reserved for bug reports and feature requests only. -Implementation questions should be asked on -community.plot.ly (tagged [`plotly-js`](http://community.plot.ly/c/plotly-js)) or on Stack Overflow (tagged -[`plotly`](https://stackoverflow.com/questions/tagged/plotly)). - -Comments on GitHub issues or pull requests should add content to the discussions. -Approbation comments such as *+1* or *I would like this feature to be implemented as well* -will be deleted by the maintainers. Please use -[GitHub reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) -instead. +Please read the [issue guidelines](./.github/ISSUE_TEMPLATE.md). ## Making pull requests -Developers are strongly encouraged to first make a PR to their own plotly.js -fork and ask one of the maintainers to review the modifications there. Once the -pull request is deemed satisfactory, the developer will be asked to make a pull -request to the main plotly.js repo and may be asked to squash some commits -before doing so. - -Developers should `git rebase` their local branch off the latest `master` before -opening a pull request. - -Note that it is forbidden to force push (i.e. `git push -f`) to remote branches -associated with opened pull requests. Force pushes make it hard for maintainers -to keep track of updates. Therefore, if required, please -`git merge master` into your PR branch instead of `git rebase master`. - +Please read the [pull request guidelines](./.github/PULL_REQUEST_TEMPLATE.md). ## GitHub labels diff --git a/build/plotcss.js b/build/plotcss.js index 169edfce295..556adf30e3b 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -1,6 +1,5 @@ 'use strict'; -var Plotly = require('../src/plotly'); var rules = { "X,X div": "font-family:'Open Sans', verdana, arial, sans-serif;margin:0;padding:0;", "X input,X button": "font-family:'Open Sans', verdana, arial, sans-serif;", @@ -54,9 +53,4 @@ var rules = { "Y .notifier-close:hover": "color:#444;text-decoration:none;cursor:pointer;" }; -for(var selector in rules) { - var fullSelector = selector.replace(/^,/,' ,') - .replace(/X/g, '.js-plotly-plot .plotly') - .replace(/Y/g, '.plotly-notifier'); - Plotly.Lib.addStyleRule(fullSelector, rules[selector]); -} +module.exports = rules; diff --git a/src/components/colorscale/is_valid_scale_array.js b/src/components/colorscale/is_valid_scale_array.js index 5c7bed6584e..95b0a625ef7 100644 --- a/src/components/colorscale/is_valid_scale_array.js +++ b/src/components/colorscale/is_valid_scale_array.js @@ -13,21 +13,23 @@ var tinycolor = require('tinycolor2'); module.exports = function isValidScaleArray(scl) { - var isValid = true, - highestVal = 0, - si; - - if(!Array.isArray(scl)) return false; - else { - if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false; - for(var i = 0; i < scl.length; i++) { - si = scl[i]; - if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) { - isValid = false; - break; - } - highestVal = +si[0]; + var highestVal = 0; + + if(!Array.isArray(scl) || scl.length < 2) return false; + + if(!scl[0] || !scl[scl.length - 1]) return false; + + if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false; + + for(var i = 0; i < scl.length; i++) { + var si = scl[i]; + + if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) { + return false; } - return isValid; + + highestVal = +si[0]; } + + return true; }; diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index a57a0038248..44a7687fa62 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -86,7 +86,7 @@ dragElement.init = function init(options) { if(options.prepFn) options.prepFn(e, startX, startY); - dragCover = coverSlip(); + dragCover = coverSlip(gd); dragCover.onmousemove = onMove; dragCover.onmouseup = onDone; @@ -139,7 +139,7 @@ dragElement.init = function init(options) { if(options.doneFn) options.doneFn(gd._dragged, numClicks); if(!gd._dragged) { - var e2 = document.createEvent('MouseEvents'); + var e2 = gd._document.createEvent('MouseEvents'); e2.initEvent('click', true, true); initialTarget.dispatchEvent(e2); } @@ -159,8 +159,8 @@ dragElement.init = function init(options) { options.element.style.pointerEvents = 'all'; }; -function coverSlip() { - var cover = document.createElement('div'); +function coverSlip(gd) { + var cover = gd._document.createElement('div'); cover.className = 'dragcover'; var cStyle = cover.style; @@ -172,7 +172,7 @@ function coverSlip() { cStyle.zIndex = 999999999; cStyle.background = 'none'; - document.body.appendChild(cover); + gd._document.body.appendChild(cover); return cover; } diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index b6c4b999fe3..2c9a6a6261c 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -462,7 +462,8 @@ drawing.makeTester = function(gd) { left: '-10000px', top: '-10000px', width: '9000px', - height: '9000px' + height: '9000px', + 'z-index': '1' }); // browsers differ on how they describe the bounding rect of diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 23b6ae8a72c..a43279afdef 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -50,19 +50,19 @@ modeBarButtons.toImage = { click: function(gd) { var format = 'png'; - Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); + Lib.notifier(gd, 'Taking snapshot - this may take a few seconds', 'long'); if(Lib.isIE()) { - Lib.notifier('IE only supports svg. Changing format to svg.', 'long'); + Lib.notifier(gd, 'IE only supports svg. Changing format to svg.', 'long'); format = 'svg'; } downloadImage(gd, {'format': format}) .then(function(filename) { - Lib.notifier('Snapshot succeeded - ' + filename, 'long'); + Lib.notifier(gd, 'Snapshot succeeded - ' + filename, 'long'); }) .catch(function() { - Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long'); + Lib.notifier(gd, 'Sorry there was a problem downloading your snapshot!', 'long'); }); } }; diff --git a/src/components/rangeslider/create_slider.js b/src/components/rangeslider/create_slider.js index 83caa2ad7eb..00a9b3d06b1 100644 --- a/src/components/rangeslider/create_slider.js +++ b/src/components/rangeslider/create_slider.js @@ -33,7 +33,7 @@ module.exports = function createSlider(gd) { var minStart = 0, maxStart = width; - var slider = document.createElementNS(svgNS, 'g'); + var slider = gd._document.createElementNS(svgNS, 'g'); helpers.setAttributes(slider, { 'class': 'range-slider', 'data-min': minStart, @@ -43,7 +43,7 @@ module.exports = function createSlider(gd) { }); - var sliderBg = document.createElementNS(svgNS, 'rect'), + var sliderBg = gd._document.createElementNS(svgNS, 'rect'), borderCorrect = options.borderwidth % 2 === 0 ? options.borderwidth : options.borderwidth - 1; helpers.setAttributes(sliderBg, { 'fill': options.bgcolor, @@ -56,7 +56,7 @@ module.exports = function createSlider(gd) { }); - var maskMin = document.createElementNS(svgNS, 'rect'); + var maskMin = gd._document.createElementNS(svgNS, 'rect'); helpers.setAttributes(maskMin, { 'x': 0, 'width': minStart, @@ -65,7 +65,7 @@ module.exports = function createSlider(gd) { }); - var maskMax = document.createElementNS(svgNS, 'rect'); + var maskMax = gd._document.createElementNS(svgNS, 'rect'); helpers.setAttributes(maskMax, { 'x': maxStart, 'width': width - maxStart, @@ -74,9 +74,9 @@ module.exports = function createSlider(gd) { }); - var grabberMin = document.createElementNS(svgNS, 'g'), - grabAreaMin = document.createElementNS(svgNS, 'rect'), - handleMin = document.createElementNS(svgNS, 'rect'); + var grabberMin = gd._document.createElementNS(svgNS, 'g'), + grabAreaMin = gd._document.createElementNS(svgNS, 'rect'), + handleMin = gd._document.createElementNS(svgNS, 'rect'); helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (minStart - handleWidth - 1) + ')' }); helpers.setAttributes(grabAreaMin, { 'width': 10, @@ -97,9 +97,9 @@ module.exports = function createSlider(gd) { helpers.appendChildren(grabberMin, [handleMin, grabAreaMin]); - var grabberMax = document.createElementNS(svgNS, 'g'), - grabAreaMax = document.createElementNS(svgNS, 'rect'), - handleMax = document.createElementNS(svgNS, 'rect'); + var grabberMax = gd._document.createElementNS(svgNS, 'g'), + grabAreaMax = gd._document.createElementNS(svgNS, 'rect'), + handleMax = gd._document.createElementNS(svgNS, 'rect'); helpers.setAttributes(grabberMax, { 'transform': 'translate(' + maxStart + ')' }); helpers.setAttributes(grabAreaMax, { 'width': 10, @@ -120,7 +120,7 @@ module.exports = function createSlider(gd) { helpers.appendChildren(grabberMax, [handleMax, grabAreaMax]); - var slideBox = document.createElementNS(svgNS, 'rect'); + var slideBox = gd._document.createElementNS(svgNS, 'rect'); helpers.setAttributes(slideBox, { 'x': minStart, 'width': maxStart - minStart, @@ -137,8 +137,8 @@ module.exports = function createSlider(gd) { minVal = slider.getAttribute('data-min'), maxVal = slider.getAttribute('data-max'); - window.addEventListener('mousemove', mouseMove); - window.addEventListener('mouseup', mouseUp); + gd._document.defaultView.addEventListener('mousemove', mouseMove); + gd._document.defaultView.addEventListener('mouseup', mouseUp); function mouseMove(e) { var delta = +e.clientX - startX, @@ -189,8 +189,8 @@ module.exports = function createSlider(gd) { } function mouseUp() { - window.removeEventListener('mousemove', mouseMove); - window.removeEventListener('mouseup', mouseUp); + gd._document.defaultView.removeEventListener('mousemove', mouseMove); + gd._document.defaultView.removeEventListener('mouseup', mouseUp); slider.style.cursor = 'auto'; } }); @@ -222,8 +222,8 @@ module.exports = function createSlider(gd) { function setDataRange(dataMin, dataMax) { - if(window.requestAnimationFrame) { - window.requestAnimationFrame(function() { + if(gd._document.defaultView.requestAnimationFrame) { + gd._document.defaultView.requestAnimationFrame(function() { Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]); }); } else { diff --git a/src/core.js b/src/core.js index 2e3baa4e6bc..3a2cd8faac0 100644 --- a/src/core.js +++ b/src/core.js @@ -33,6 +33,7 @@ exports.setPlotConfig = require('./plot_api/set_plot_config'); exports.register = Plotly.register; exports.toImage = require('./plot_api/to_image'); exports.downloadImage = require('./snapshot/download'); +exports.validate = require('./plot_api/validate'); // plot icons exports.Icons = require('../build/ploticon'); diff --git a/src/css/helpers.js b/src/css/helpers.js new file mode 100644 index 00000000000..ebf4c6470a0 --- /dev/null +++ b/src/css/helpers.js @@ -0,0 +1,38 @@ +/** +* Copyright 2012-2016, 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'; + +// expands a plotcss selector +exports.buildFullSelector = function buildFullSelector(selector) { + var fullSelector = selector.replace(/,/, ', ') + .replace(/:after/g, '::after') + .replace(/:before/g, '::before') + .replace(/X/g, '.js-plotly-plot .plotly') + .replace(/Y/g, '.plotly-notifier'); + + return fullSelector; +}; + +// Gets all the rules currently attached to the document +exports.getAllRuleSelectors = function getAllRuleSelectors(sourceDocument) { + var allSelectors = []; + + for(var i = 0; i < sourceDocument.styleSheets.length; i++) { + var styleSheet = sourceDocument.styleSheets[i]; + + for(var j = 0; j < styleSheet.cssRules.length; j++) { + var cssRule = styleSheet.cssRules[j]; + + allSelectors.push(cssRule.selectorText); + } + } + + return allSelectors; +}; diff --git a/src/css/plotcss_injector.js b/src/css/plotcss_injector.js new file mode 100644 index 00000000000..4f4e54051ec --- /dev/null +++ b/src/css/plotcss_injector.js @@ -0,0 +1,52 @@ +/** +* Copyright 2012-2016, 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 helpers = require('./helpers'); +var lib = require('../lib'); +var plotcss = require('../../build/plotcss'); + +// Inject styling information into the document containing the graph div +module.exports = function injectStyles(gd) { + // If the graph div has already been styled, bail + if(gd._plotCSSLoaded) return; + + var targetSelectors = helpers.getAllRuleSelectors(gd._document); + var targetStyleSheet = null; + + if(gd._document.getElementsByTagName('style').length === 0) { + var style = gd._document.createElement('style'); + // WebKit hack :( + style.appendChild(gd._document.createTextNode('')); + gd._document.head.appendChild(style); + targetStyleSheet = style.sheet; + } + else { + // Just grab the first style element to append to + targetStyleSheet = gd._document.getElementsByTagName('style')[0].sheet; + } + + for(var selector in plotcss) { + var fullSelector = helpers.buildFullSelector(selector); + + // Don't duplicate selectors + if(targetSelectors.indexOf(fullSelector) === -1) { + if(targetStyleSheet.insertRule) { + targetStyleSheet.insertRule(fullSelector + '{' + plotcss[selector] + '}', 0); + } + else if(targetStyleSheet.addRule) { + targetStyleSheet.addRule(fullSelector, plotcss[selector], 0); + } + else lib.warn('injectStyles failed'); + } + } + + gd._plotCSSLoaded = true; +}; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 9c67cb32014..7f7e65a5900 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -99,16 +99,14 @@ exports.valObjects = { // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'], coerceFunction: function(v, propOut, dflt, opts) { - if(opts.strict === true && typeof v !== 'string') { - propOut.set(dflt); - return; - } + if(typeof v !== 'string') { + var okToCoerce = (typeof v === 'number'); - var s = String(v); - if(v === undefined || (opts.noBlank === true && !s)) { - propOut.set(dflt); + if(opts.strict === true || !okToCoerce) propOut.set(dflt); + else propOut.set(String(v)); } - else propOut.set(s); + else if(opts.noBlank && !v) propOut.set(dflt); + else propOut.set(v); } }, color: { @@ -162,11 +160,11 @@ exports.valObjects = { subplotid: { description: [ 'An id string of a subplot type (given by dflt), optionally', - 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', + 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', '\'geo\', \'geo2\', \'geo3\', ...' ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], + requiredOpts: ['dflt'], + otherOpts: [], coerceFunction: function(v, propOut, dflt) { var dlen = dflt.length; if(typeof v === 'string' && v.substr(0, dlen) === dflt && @@ -175,6 +173,18 @@ exports.valObjects = { return; } propOut.set(dflt); + }, + validateFunction: function(v, opts) { + var dflt = opts.dflt, + dlen = dflt.length; + + if(v === dflt) return true; + if(typeof v !== 'string') return false; + if(v.substr(0, dlen) === dflt && idRegex.test(v.substr(dlen))) { + return true; + } + + return false; } }, flaglist: { @@ -239,6 +249,22 @@ exports.valObjects = { } propOut.set(vOut); + }, + validateFunction: function(v, opts) { + if(!Array.isArray(v)) return false; + + var items = opts.items; + + if(v.length !== items.length) return false; + + // valid when all items are valid + for(var i = 0; i < items.length; i++) { + var isItemValid = exports.validate(v[i], opts.items[i]); + + if(!isItemValid) return false; + } + + return true; } } }; @@ -309,3 +335,22 @@ exports.coerceFont = function(coerce, attr, dfltObj) { return out; }; + +exports.validate = function(value, opts) { + var valObject = exports.valObjects[opts.valType]; + + if(opts.arrayOk && Array.isArray(value)) return true; + + if(valObject.validateFunction) { + return valObject.validateFunction(value, opts); + } + + var failed = {}, + out = failed, + propMock = { set: function(v) { out = v; } }; + + // 'failed' just something mutable that won't be === anything else + + valObject.coerceFunction(value, propMock, failed, opts); + return out !== failed; +}; diff --git a/src/lib/index.js b/src/lib/index.js index 32f3f811a67..7a46be4e59c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -21,6 +21,7 @@ lib.valObjects = coerceModule.valObjects; lib.coerce = coerceModule.coerce; lib.coerce2 = coerceModule.coerce2; lib.coerceFont = coerceModule.coerceFont; +lib.validate = coerceModule.validate; var datesModule = require('./dates'); lib.dateTime2ms = datesModule.dateTime2ms; @@ -387,30 +388,6 @@ lib.removeElement = function(el) { if(elParent) elParent.removeChild(el); }; -/** - * for dynamically adding style rules - * makes one stylesheet that contains all rules added - * by all calls to this function - */ -lib.addStyleRule = function(selector, styleString) { - if(!lib.styleSheet) { - var style = document.createElement('style'); - // WebKit hack :( - style.appendChild(document.createTextNode('')); - document.head.appendChild(style); - lib.styleSheet = style.sheet; - } - var styleSheet = lib.styleSheet; - - if(styleSheet.insertRule) { - styleSheet.insertRule(selector + '{' + styleString + '}', 0); - } - else if(styleSheet.addRule) { - styleSheet.addRule(selector, styleString, 0); - } - else lib.warn('addStyleRule failed'); -}; - lib.getTranslate = function(element) { var re = /.*\btranslate\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/, diff --git a/src/lib/notifier.js b/src/lib/notifier.js index a1bfbfcc14f..ae6a741783f 100644 --- a/src/lib/notifier.js +++ b/src/lib/notifier.js @@ -16,12 +16,13 @@ var NOTEDATA = []; /** * notifier + * @param {object} gd figure Object * @param {String} text The person's user name * @param {Number} [delay=1000] The delay time in milliseconds * or 'long' which provides 2000 ms delay time. * @return {undefined} this function does not return a value */ -module.exports = function(text, displayLength) { +module.exports = function(gd, text, displayLength) { if(NOTEDATA.indexOf(text) !== -1) return; NOTEDATA.push(text); @@ -30,7 +31,7 @@ module.exports = function(text, displayLength) { if(isNumeric(displayLength)) ts = displayLength; else if(displayLength === 'long') ts = 3000; - var notifierContainer = d3.select('body') + var notifierContainer = d3.select(gd._document.body) .selectAll('.plotly-notifier') .data([0]); notifierContainer.enter() diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3cf9366e167..24f981a5abf 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -18,6 +18,8 @@ var Lib = require('../lib'); var Events = require('../lib/events'); var Queue = require('../lib/queue'); +var injectStyles = require('../css/plotcss_injector'); + var Plots = require('../plots/plots'); var Fx = require('../plots/cartesian/graph_interact'); @@ -54,6 +56,14 @@ Plotly.plot = function(gd, data, layout, config) { gd = getGraphDiv(gd); + // Get the document the graph div lives in, so we can make sure things like + // drag covers are attached to the correct document + gd._document = gd.ownerDocument || window.document; + + // Inject the plot styles into the document where we're plotting, bails if + // already styled + injectStyles(gd); + // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); @@ -2541,12 +2551,12 @@ function plotAutoSize(gd, aobj) { // embedded in an iframe - just take the full iframe size // if we get to this point, with no aspect ratio restrictions if(gd._context.fillFrame) { - newWidth = window.innerWidth; - newHeight = window.innerHeight; + newWidth = gd._document.defaultView.innerWidth; + newHeight = gd._document.defaultView.innerHeight; // somehow we get a few extra px height sometimes... // just hide it - document.body.style.overflow = 'hidden'; + gd._document.body.style.overflow = 'hidden'; } else if(isNumeric(context.frameMargins) && context.frameMargins > 0) { var reservedMargins = calculateReservedMargins(gd._boundingBoxMargins), @@ -2563,7 +2573,7 @@ function plotAutoSize(gd, aobj) { // provide height and width for the container div, // specify size in layout, or take the defaults, // but don't enforce any ratio restrictions - computedStyle = window.getComputedStyle(gd); + computedStyle = gd._document.defaultView.getComputedStyle(gd); newHeight = parseFloat(computedStyle.height) || fullLayout.height; newWidth = parseFloat(computedStyle.width) || fullLayout.width; } diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js new file mode 100644 index 00000000000..58dc5829934 --- /dev/null +++ b/src/plot_api/validate.js @@ -0,0 +1,309 @@ +/** +* Copyright 2012-2016, 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 Lib = require('../lib'); +var Plots = require('../plots/plots'); +var PlotSchema = require('./plot_schema'); + +var isPlainObject = Lib.isPlainObject; +var isArray = Array.isArray; + + +/** + * Validate a data array and layout object. + * + * @param {array} data + * @param {object} layout + * + * @return {array} array of error objects each containing: + * - {string} code + * error code ('object', 'array', 'schema', 'unused', 'invisible' or 'value') + * - {string} container + * container where the error occurs ('data' or 'layout') + * - {number} trace + * trace index of the 'data' container where the error occurs + * - {array} path + * nested path to the key that causes the error + * - {string} astr + * attribute string variant of 'path' compatible with Plotly.restyle and + * Plotly.relayout. + * - {string} msg + * error message (shown in console in logger config argument is enable) + */ +module.exports = function valiate(data, layout) { + var schema = PlotSchema.get(), + errorList = [], + gd = {}; + + var dataIn, layoutIn; + + if(isArray(data)) { + gd.data = Lib.extendDeep([], data); + dataIn = data; + } + else { + gd.data = []; + dataIn = []; + errorList.push(format('array', 'data')); + } + + if(isPlainObject(layout)) { + gd.layout = Lib.extendDeep({}, layout); + layoutIn = layout; + } + else { + gd.layout = {}; + layoutIn = {}; + if(arguments.length > 1) { + errorList.push(format('object', 'layout')); + } + } + + // N.B. dataIn and layoutIn are in general not the same as + // gd.data and gd.layout after supplyDefaults as some attributes + // in gd.data and gd.layout (still) get mutated during this step. + + Plots.supplyDefaults(gd); + + var dataOut = gd._fullData, + len = dataIn.length; + + for(var i = 0; i < len; i++) { + var traceIn = dataIn[i], + base = ['data', i]; + + if(!isPlainObject(traceIn)) { + errorList.push(format('object', base)); + continue; + } + + var traceOut = dataOut[i], + traceType = traceOut.type, + traceSchema = schema.traces[traceType].attributes; + + // PlotSchema does something fancy with trace 'type', reset it here + // to make the trace schema compatible with Lib.validate. + traceSchema.type = { + valType: 'enumerated', + values: [traceType] + }; + + if(traceOut.visible === false && traceIn.visible !== false) { + errorList.push(format('invisible', base)); + } + + crawl(traceIn, traceOut, traceSchema, errorList, base); + } + + var layoutOut = gd._fullLayout, + layoutSchema = fillLayoutSchema(schema, dataOut); + + crawl(layoutIn, layoutOut, layoutSchema, errorList, 'layout'); + + // return undefined if no validation errors were found + return (errorList.length === 0) ? void(0) : errorList; +}; + +function crawl(objIn, objOut, schema, list, base, path) { + path = path || []; + + var keys = Object.keys(objIn); + + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + + var p = path.slice(); + p.push(k); + + var valIn = objIn[k], + valOut = objOut[k]; + + var nestedSchema = getNestedSchema(schema, k); + + if(!isInSchema(schema, k)) { + list.push(format('schema', base, p)); + } + else if(isPlainObject(valIn) && isPlainObject(valOut)) { + crawl(valIn, valOut, nestedSchema, list, base, p); + } + else if(nestedSchema.items && isArray(valIn)) { + var itemName = k.substr(0, k.length - 1); + + for(var j = 0; j < valIn.length; j++) { + var _nestedSchema = nestedSchema.items[itemName], + _p = p.slice(); + + _p.push(j); + + crawl(valIn[j], valOut[j], _nestedSchema, list, base, _p); + } + } + else if(!isPlainObject(valIn) && isPlainObject(valOut)) { + list.push(format('object', base, p, valIn)); + } + else if(!isArray(valIn) && isArray(valOut) && nestedSchema.valType !== 'info_array') { + list.push(format('array', base, p, valIn)); + } + else if(!(k in objOut)) { + list.push(format('unused', base, p, valIn)); + } + else if(!Lib.validate(valIn, nestedSchema)) { + list.push(format('value', base, p, valIn)); + } + } + + return list; +} + +// the 'full' layout schema depends on the traces types presents +function fillLayoutSchema(schema, dataOut) { + for(var i = 0; i < dataOut.length; i++) { + var traceType = dataOut[i].type, + traceLayoutAttr = schema.traces[traceType].layoutAttributes; + + if(traceLayoutAttr) { + Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr); + } + } + + return schema.layout.layoutAttributes; +} + +// validation error codes +var code2msgFunc = { + object: function(base, astr) { + var prefix; + + if(base === 'layout' && astr === '') prefix = 'The layout argument'; + else if(base[0] === 'data') { + prefix = 'Trace ' + base[1] + ' in the data argument'; + } + else prefix = inBase(base) + 'key ' + astr; + + return prefix + ' must be linked to an object container'; + }, + array: function(base, astr) { + var prefix; + + if(base === 'data') prefix = 'The data argument'; + else prefix = inBase(base) + 'key ' + astr; + + return prefix + ' must be linked to an array container'; + }, + schema: function(base, astr) { + return inBase(base) + 'key ' + astr + ' is not part of the schema'; + }, + unused: function(base, astr, valIn) { + var target = isPlainObject(valIn) ? 'container' : 'key'; + + return inBase(base) + target + ' ' + astr + ' did not get coerced'; + }, + invisible: function(base) { + return 'Trace ' + base[1] + ' got defaulted to be not visible'; + }, + value: function(base, astr, valIn) { + return [ + inBase(base) + 'key ' + astr, + 'is set to an invalid value (' + valIn + ')' + ].join(' '); + } +}; + +function inBase(base) { + if(isArray(base)) return 'In data trace ' + base[1] + ', '; + + return 'In ' + base + ', '; +} + +function format(code, base, path, valIn) { + path = path || ''; + + var container, trace; + + // container is either 'data' or 'layout + // trace is the trace index if 'data', null otherwise + + if(isArray(base)) { + container = base[0]; + trace = base[1]; + } + else { + container = base; + trace = null; + } + + var astr = convertPathToAttributeString(path), + msg = code2msgFunc[code](base, astr, valIn); + + // log to console if logger config option is enabled + Lib.log(msg); + + return { + code: code, + container: container, + trace: trace, + path: path, + astr: astr, + msg: msg + }; +} + +function isInSchema(schema, key) { + var parts = splitKey(key), + keyMinusId = parts.keyMinusId, + id = parts.id; + + if((keyMinusId in schema) && schema[keyMinusId]._isSubplotObj && id) { + return true; + } + + return (key in schema); +} + +function getNestedSchema(schema, key) { + var parts = splitKey(key); + + return schema[parts.keyMinusId]; +} + +function splitKey(key) { + var idRegex = /([2-9]|[1-9][0-9]+)$/; + + var keyMinusId = key.split(idRegex)[0], + id = key.substr(keyMinusId.length, key.length); + + return { + keyMinusId: keyMinusId, + id: id + }; +} + +function convertPathToAttributeString(path) { + if(!isArray(path)) return String(path); + + var astr = ''; + + for(var i = 0; i < path.length; i++) { + var p = path[i]; + + if(typeof p === 'number') { + astr = astr.substr(0, astr.length - 1) + '[' + p + ']'; + } + else { + astr += p; + } + + if(i < path.length - 1) astr += '.'; + } + + return astr; +} diff --git a/src/plotly.js b/src/plotly.js index 5ceb2019839..491c0ed5a77 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -26,9 +26,6 @@ exports.Lib = require('./lib'); exports.util = require('./lib/svg_text_utils'); exports.Queue = require('./lib/queue'); -// plot css -require('../build/plotcss'); - // configuration exports.MathJaxConfig = require('./fonts/mathjax_config'); exports.defaultConfig = require('./plot_api/plot_config'); diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index ff6f7bc52bf..8e99adb1ab0 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -305,7 +305,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { dragTail(zoomMode); if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { - Lib.notifier('Double-click to
zoom back out', 'long'); + Lib.notifier(gd, 'Double-click to
zoom back out', 'long'); SHOWZOOMOUTTIP = false; } } diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 565c4ce53b3..5f34cde15e9 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -114,6 +114,7 @@ module.exports = function setConvert(ax) { if(!isFinite(ax._m) || !isFinite(ax._b)) { Lib.notifier( + ax._gd, 'Something went wrong with axis scaling', 'long'); ax._gd._replotting = false; diff --git a/src/plots/plots.js b/src/plots/plots.js index 4b814bfd85c..307c4d38547 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -809,6 +809,10 @@ plots.purge = function(gd) { // remove modebar if(fullLayout._modeBar) fullLayout._modeBar.destroy(); + // styling + delete gd._document; + delete gd._plotCSSLoaded; + // data and layout delete gd.data; delete gd.layout; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 1fd6d7094f7..9a93f376205 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -570,7 +570,7 @@ proto.initInteractions = function() { Plotly.relayout(gd, attrs); if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { - Lib.notifier('Double-click to
zoom back out', 'long'); + Lib.notifier(gd, 'Double-click to
zoom back out', 'long'); SHOWZOOMOUTTIP = false; } } diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index e8611ccc82e..828c3288db9 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -73,7 +73,7 @@ module.exports = function calc(gd, trace) { function noZsmooth(msg) { zsmooth = trace._input.zsmooth = trace.zsmooth = false; - Lib.notifier('cannot fast-zsmooth: ' + msg); + Lib.notifier(gd, 'cannot fast-zsmooth: ' + msg); } // check whether we really can smooth (ie all boxes are about the same size) diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index f8d6d96c9e4..f6248d7eb63 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -184,7 +184,7 @@ function makeCircleGeoJSON(calcTrace, hash) { // Translate vals in trace arrayOk containers // into a val-to-index hash object function translate(props, key, val, index) { - if(!hash[key][val]) hash[key][val] = index; + if(hash[key][val] === undefined) hash[key][val] = index; props[key] = hash[key][val]; } diff --git a/tasks/util/pull_css.js b/tasks/util/pull_css.js index 1f3cb6def53..ff5fefea671 100644 --- a/tasks/util/pull_css.js +++ b/tasks/util/pull_css.js @@ -38,15 +38,9 @@ module.exports = function pullCSS(data, pathOut) { var outStr = [ '\'use strict\';', '', - 'var Plotly = require(\'../src/plotly\');', 'var rules = ' + rulesStr + ';', '', - 'for(var selector in rules) {', - ' var fullSelector = selector.replace(/^,/,\' ,\')', - ' .replace(/X/g, \'.js-plotly-plot .plotly\')', - ' .replace(/Y/g, \'.plotly-notifier\');', - ' Plotly.Lib.addStyleRule(fullSelector, rules[selector]);', - '}', + 'module.exports = rules;', '' ].join('\n'); diff --git a/test/jasmine/tests/colorscale_test.js b/test/jasmine/tests/colorscale_test.js index dba9936b7a1..3610c17cdec 100644 --- a/test/jasmine/tests/colorscale_test.js +++ b/test/jasmine/tests/colorscale_test.js @@ -20,6 +20,13 @@ describe('Test colorscale:', function() { it('should accept only array of 2-item arrays', function() { expect(isValidScale('a')).toBe(false); + expect(isValidScale([])).toBe(false); + expect(isValidScale([null, undefined])).toBe(false); + expect(isValidScale([{}, [1, 'rgb(0, 0, 200']])).toBe(false); + expect(isValidScale([[0, 'rgb(200, 0, 0)'], {}])).toBe(false); + expect(isValidScale([[0, 'rgb(0, 0, 200)'], undefined])).toBe(false); + expect(isValidScale([null, [1, 'rgb(0, 0, 200)']])).toBe(false); + expect(isValidScale(['a', 'b'])).toBe(false); expect(isValidScale(['a'])).toBe(false); expect(isValidScale([['a'], ['b']])).toBe(false); diff --git a/test/jasmine/tests/dragelement_test.js b/test/jasmine/tests/dragelement_test.js index 924f7f3bcaf..ad6abd29eb1 100644 --- a/test/jasmine/tests/dragelement_test.js +++ b/test/jasmine/tests/dragelement_test.js @@ -15,6 +15,7 @@ describe('dragElement', function() { this.element = document.createElement('div'); this.gd.className = 'js-plotly-plot'; + this.gd._document = document; this.gd._fullLayout = { _hoverlayer: d3.select(this.hoverlayer) }; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index aede6b72b30..02c0595b094 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -527,13 +527,13 @@ describe('Test lib.js:', function() { .toEqual('42'); expect(coerce({s: [1, 2, 3]}, {}, stringAttrs, 's')) - .toEqual('1,2,3'); + .toEqual(dflt); expect(coerce({s: true}, {}, stringAttrs, 's')) - .toEqual('true'); + .toEqual(dflt); expect(coerce({s: {1: 2}}, {}, stringAttrs, 's')) - .toEqual('[object Object]'); // useless, but that's what it does!! + .toEqual(dflt); }); }); @@ -627,7 +627,22 @@ describe('Test lib.js:', function() { .toEqual([0.5, 1]); }); + it('should coerce unexpected input as best as it can', function() { + expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range')) + .toEqual([12]); + expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range', [-1, 20])) + .toEqual([12, 20]); + + expect(coerce({domain: [0.5]}, {}, infoArrayAttrs, 'domain')) + .toEqual([0.5, 1]); + + expect(coerce({range: ['-10', 100, 12]}, {}, infoArrayAttrs, 'range')) + .toEqual([-10, 100]); + + expect(coerce({domain: [0, 0.5, 1]}, {}, infoArrayAttrs, 'domain')) + .toEqual([0, 0.5]); + }); }); describe('subplotid valtype', function() { @@ -756,6 +771,222 @@ describe('Test lib.js:', function() { }); }); + describe('validate', function() { + + function assert(shouldPass, shouldFail, valObject) { + shouldPass.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(true, JSON.stringify(v) + ' should pass'); + }); + + shouldFail.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(false, JSON.stringify(v) + ' should fail'); + }); + } + + it('should work for valType \'data_array\' where', function() { + var shouldPass = [[20], []], + shouldFail = ['a', {}, 20, undefined, null]; + + assert(shouldPass, shouldFail, { + valType: 'data_array' + }); + + assert(shouldPass, shouldFail, { + valType: 'data_array', + dflt: [1, 2] + }); + }); + + it('should work for valType \'enumerated\' where', function() { + assert(['a', 'b'], ['c', 1, null, undefined, ''], { + valType: 'enumerated', + values: ['a', 'b'], + dflt: 'a' + }); + + assert([1, '1', 2, '2'], ['c', 3, null, undefined, ''], { + valType: 'enumerated', + values: [1, 2], + coerceNumber: true, + dflt: 1 + }); + + assert(['a', 'b', [1, 2]], ['c', 1, null, undefined, ''], { + valType: 'enumerated', + values: ['a', 'b'], + arrayOk: true, + dflt: 'a' + }); + }); + + it('should work for valType \'boolean\' where', function() { + var shouldPass = [true, false], + shouldFail = ['a', 1, {}, [], null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'boolean', + dflt: true + }); + + assert(shouldPass, shouldFail, { + valType: 'boolean', + dflt: false + }); + }); + + it('should work for valType \'number\' where', function() { + var shouldPass = [20, '20', 1e6], + shouldFail = ['a', [], {}, null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'number' + }); + + assert(shouldPass, shouldFail, { + valType: 'number', + dflt: null + }); + + assert([20, '20'], [-10, '-10', 25, '25'], { + valType: 'number', + dflt: 20, + min: 0, + max: 21 + }); + + assert([20, '20', [1, 2]], ['a', {}], { + valType: 'number', + dflt: 20, + arrayOk: true + }); + }); + + it('should work for valType \'integer\' where', function() { + assert([1, 2, '3', '4'], ['a', 1.321321, {}, [], null, 2 / 3, undefined, null], { + valType: 'integer', + dflt: 1 + }); + + assert([1, 2, '3', '4'], [-1, '-2', 2.121, null, undefined, [], {}], { + valType: 'integer', + min: 0, + dflt: 1 + }); + }); + + it('should work for valType \'string\' where', function() { + var date = new Date(2016, 1, 1); + + assert(['3', '4', 'a', 3, 1.2113, ''], [undefined, {}, [], null, date, false], { + valType: 'string', + dflt: 'a' + }); + + assert(['3', '4', 'a', 3, 1.2113], ['', undefined, {}, [], null, date, true], { + valType: 'string', + dflt: 'a', + noBlank: true + }); + + assert(['3', '4', ''], [undefined, 1, {}, [], null, date, true, false], { + valType: 'string', + dflt: 'a', + strict: true + }); + + assert(['3', '4'], [undefined, 1, {}, [], null, date, '', true, false], { + valType: 'string', + dflt: 'a', + strict: true, + noBlank: true + }); + }); + + it('should work for valType \'color\' where', function() { + var shouldPass = ['red', '#d3d3d3', 'rgba(0,255,255,0.1)'], + shouldFail = [1, {}, [], 'rgq(233,122,332,1)', null, undefined]; + + assert(shouldPass, shouldFail, { + valType: 'color' + }); + }); + + it('should work for valType \'colorscale\' where', function() { + var good = [ [0, 'red'], [1, 'blue'] ], + bad = [ [0.1, 'red'], [1, 'blue'] ], + bad2 = [ [0], [1] ], + bad3 = [ ['red'], ['blue']], + bad4 = ['red', 'blue']; + + var shouldPass = ['Viridis', 'Greens', good], + shouldFail = ['red', 1, undefined, null, {}, [], bad, bad2, bad3, bad4]; + + assert(shouldPass, shouldFail, { + valType: 'colorscale' + }); + }); + + it('should work for valType \'angle\' where', function() { + var shouldPass = ['auto', '120', 270], + shouldFail = [{}, [], 'red', null, undefined, '']; + + assert(shouldPass, shouldFail, { + valType: 'angle', + dflt: 0 + }); + }); + + it('should work for valType \'subplotid\' where', function() { + var shouldPass = ['sp', 'sp4', 'sp10'], + shouldFail = [{}, [], 'sp1', 'sp0', 'spee1', null, undefined, true]; + + assert(shouldPass, shouldFail, { + valType: 'subplotid', + dflt: 'sp' + }); + }); + + it('should work for valType \'flaglist\' where', function() { + var shouldPass = ['a', 'b', 'a+b', 'b+a', 'c'], + shouldFail = [{}, [], 'red', null, undefined, '', 'a + b']; + + assert(shouldPass, shouldFail, { + valType: 'flaglist', + flags: ['a', 'b'], + extras: ['c'] + }); + }); + + it('should work for valType \'any\' where', function() { + var shouldPass = ['', '120', null, false, {}, []], + shouldFail = [undefined]; + + assert(shouldPass, shouldFail, { + valType: 'any' + }); + }); + + it('should work for valType \'info_array\' where', function() { + var shouldPass = [[1, 2], [-20, '20']], + shouldFail = [ + {}, [], [10], [null, 10], ['aads', null], + 'red', null, undefined, '', + [1, 10, null] + ]; + + assert(shouldPass, shouldFail, { + valType: 'info_array', + items: [{ + valType: 'number', dflt: -20 + }, { + valType: 'number', dflt: 20 + }] + }); + }); + }); + describe('setCursor', function() { beforeEach(function() { diff --git a/test/jasmine/tests/plot_css_test.js b/test/jasmine/tests/plot_css_test.js new file mode 100644 index 00000000000..204d420f100 --- /dev/null +++ b/test/jasmine/tests/plot_css_test.js @@ -0,0 +1,152 @@ +var Plotly = require('@lib/index'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('css injection', function() { + var helpers = require('@src/css/helpers'); + var plotcss = require('@build/plotcss'); + + // create a graph div in a child window + function createGraphDivInChildWindow() { + var childWindow = window.open('about:blank', 'popoutWindow', ''); + + var gd = childWindow.document.createElement('div'); + gd.id = 'graph'; + childWindow.document.body.appendChild(gd); + + // force the graph to be at position 0,0 no matter what + gd.style.position = 'fixed'; + gd.style.left = 0; + gd.style.top = 0; + + return gd; + } + + // the most basic of basic plots + function plot(target) { + Plotly.plot(target, [{ + x: [1, 2, 3, 4, 5], + y: [1, 2, 4, 8, 16] + }], { + margin: { + t: 0 + } + }); + } + + // deletes all rules defined in plotcss + function deletePlotCSSRules(sourceDocument) { + for(var selector in plotcss) { + var fullSelector = helpers.buildFullSelector(selector); + + for(var i = 0; i < sourceDocument.styleSheets.length; i++) { + var styleSheet = sourceDocument.styleSheets[i]; + var selectors = []; + + for(var j = 0; j < styleSheet.cssRules.length; j++) { + var cssRule = styleSheet.cssRules[j]; + + selectors.push(cssRule.selectorText); + } + + var selectorIndex = selectors.indexOf(fullSelector); + + if(selectorIndex !== -1) { + styleSheet.deleteRule(selectorIndex); + break; + } + } + } + } + + it('inserts styles on initial plot', function() { + deletePlotCSSRules(document); // clear the rules + + // fix scope errors + var selector = null; + var fullSelector = null; + + // make sure the rules are cleared + var allSelectors = helpers.getAllRuleSelectors(document); + + for(selector in plotcss) { + fullSelector = helpers.buildFullSelector(selector); + + expect(allSelectors.indexOf(fullSelector)).toEqual(-1); + } + + // plot + var gd = createGraphDiv(); + plot(gd); + + // check for styles + allSelectors = helpers.getAllRuleSelectors(document); + + for(selector in plotcss) { + fullSelector = helpers.buildFullSelector(selector); + + expect(allSelectors.indexOf(fullSelector)).not.toEqual(-1); + } + + // clean up + destroyGraphDiv(); + }); + + it('inserts styles in a child window document', function() { + var gd = createGraphDivInChildWindow(); + var childWindow = gd.ownerDocument.defaultView; + + // plot + plot(gd); + + // check for styles + var allSelectors = helpers.getAllRuleSelectors(gd.ownerDocument); + + for(var selector in plotcss) { + var fullSelector = helpers.buildFullSelector(selector); + + expect(allSelectors.indexOf(fullSelector)).not.toEqual(-1); + } + + // clean up + childWindow.close(); + }); + + it('does not insert duplicate styles', function() { + deletePlotCSSRules(document); // clear the rules + + // fix scope errors + var selector = null; + var fullSelector = null; + + // make sure the rules are cleared + var allSelectors = helpers.getAllRuleSelectors(document); + + for(selector in plotcss) { + fullSelector = helpers.buildFullSelector(selector); + + expect(allSelectors.indexOf(fullSelector)).toEqual(-1); + } + + // plot + var gd = createGraphDiv(); + plot(gd); + plot(gd); // plot again so injectStyles gets called again + + // check for styles + allSelectors = helpers.getAllRuleSelectors(document); + + for(selector in plotcss) { + fullSelector = helpers.buildFullSelector(selector); + + var firstIndex = allSelectors.indexOf(fullSelector); + + // there should be no occurences after the initial one + expect(allSelectors.indexOf(fullSelector, firstIndex + 1)).toEqual(-1); + } + + // clean up + destroyGraphDiv(); + }); +}); diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 7607d8f1739..22bf6f4a2dd 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -243,6 +243,10 @@ describe('scattermapbox calc', function() { describe('scattermapbox convert', function() { 'use strict'; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + function _convert(trace) { var gd = { data: [trace] }; @@ -397,6 +401,23 @@ describe('scattermapbox convert', function() { }); }); + it('for markers + circle bubbles traces with repeated values, should', function() { + var opts = _convert(Lib.extendFlat({}, base, { + lon: ['-96.796988', '-81.379236', '-85.311819', ''], + lat: ['32.776664', '28.538335', '35.047157', '' ], + marker: { size: ['5', '49', '5', ''] } + })); + + expect(opts.circle.paint['circle-radius'].stops) + .toBeCloseTo2DArray([[0, 2.5], [1, 24.5]], 'not replicate stops'); + + var radii = opts.circle.geojson.features.map(function(f) { + return f.properties['circle-radius']; + }); + + expect(radii).toBeCloseToArray([0, 1, 0], 'link features to correct stops'); + }); + function assertVisibility(opts, expectations) { var actual = ['fill', 'line', 'circle', 'symbol'].map(function(l) { return opts[l].layout.visibility; diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js new file mode 100644 index 00000000000..4273b84a355 --- /dev/null +++ b/test/jasmine/tests/validate_test.js @@ -0,0 +1,251 @@ +var Plotly = require('@lib/index'); + + +describe('Plotly.validate', function() { + + function assertErrorContent(obj, code, cont, trace, path, astr, msg) { + expect(obj.code).toEqual(code); + expect(obj.container).toEqual(cont); + expect(obj.trace).toEqual(trace); + expect(obj.path).toEqual(path); + expect(obj.astr).toEqual(astr); + expect(obj.msg).toEqual(msg); + } + + it('should return undefined when no errors are found', function() { + var out = Plotly.validate([{ + type: 'scatter', + x: [1, 2, 3] + }], { + title: 'my simple graph' + }); + + expect(out).toBeUndefined(); + }); + + it('should report when data is not an array', function() { + var out = Plotly.validate({ + type: 'scatter', + x: [1, 2, 3] + }); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'array', 'data', null, '', '', + 'The data argument must be linked to an array container' + ); + }); + + it('should report when a data trace is not an object', function() { + var out = Plotly.validate([{ + type: 'scatter', + x: [1, 2, 3] + }, [1, 2, 3]]); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'object', 'data', 1, '', '', + 'Trace 1 in the data argument must be linked to an object container' + ); + }); + + it('should report when layout is not an object', function() { + var out = Plotly.validate([], [1, 2, 3]); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'object', 'layout', null, '', '', + 'The layout argument must be linked to an object container' + ); + }); + + it('should report when trace is defaulted to not be visible', function() { + var out = Plotly.validate([{ + type: 'scatter' + // missing 'x' and 'y + }], {}); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'invisible', 'data', 0, '', '', + 'Trace 0 got defaulted to be not visible' + ); + }); + + it('should report when trace contains keys not part of the schema', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + markerColor: 'blue' + }], {}); + + expect(out.length).toEqual(1); + assertErrorContent( + out[0], 'schema', 'data', 0, ['markerColor'], 'markerColor', + 'In data trace 0, key markerColor is not part of the schema' + ); + }); + + it('should report when trace contains keys that are not coerced', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + mode: 'lines', + marker: { color: 'blue' } + }, { + x: [1, 2, 3], + mode: 'markers', + marker: { + color: 'blue', + cmin: 10 + } + }], {}); + + expect(out.length).toEqual(2); + assertErrorContent( + out[0], 'unused', 'data', 0, ['marker'], 'marker', + 'In data trace 0, container marker did not get coerced' + ); + assertErrorContent( + out[1], 'unused', 'data', 1, ['marker', 'cmin'], 'marker.cmin', + 'In data trace 1, key marker.cmin did not get coerced' + ); + }); + + it('should report when trace contains keys set to invalid values', function() { + var out = Plotly.validate([{ + x: [1, 2, 3], + mode: 'lines', + line: { width: 'a big number' } + }, { + x: [1, 2, 3], + mode: 'markers', + marker: { color: 10 } + }], {}); + + expect(out.length).toEqual(2); + assertErrorContent( + out[0], 'value', 'data', 0, ['line', 'width'], 'line.width', + 'In data trace 0, key line.width is set to an invalid value (a big number)' + ); + assertErrorContent( + out[1], 'value', 'data', 1, ['marker', 'color'], 'marker.color', + 'In data trace 1, key marker.color is set to an invalid value (10)' + ); + }); + + it('should work with isLinkedToArray attributes', function() { + var out = Plotly.validate([], { + annotations: [{ + text: 'some text' + }, { + arrowSymbol: 'cat' + }, { + font: { color: 'wont-work' } + }], + xaxis: { + type: 'date', + rangeselector: { + buttons: [{ + label: '1 month', + step: 'all', + count: 10 + }, { + title: '1 month' + }] + } + }, + xaxis2: { + type: 'date', + rangeselector: { + buttons: [{ + title: '1 month' + }] + } + }, + xaxis3: { + type: 'date', + rangeselector: { + buttons: 'wont-work' + } + }, + shapes: [{ + opacity: 'none' + }] + }); + + expect(out.length).toEqual(7); + assertErrorContent( + out[0], 'schema', 'layout', null, + ['annotations', 1, 'arrowSymbol'], 'annotations[1].arrowSymbol', + 'In layout, key annotations[1].arrowSymbol is not part of the schema' + ); + assertErrorContent( + out[1], 'value', 'layout', null, + ['annotations', 2, 'font', 'color'], 'annotations[2].font.color', + 'In layout, key annotations[2].font.color is set to an invalid value (wont-work)' + ); + assertErrorContent( + out[2], 'unused', 'layout', null, + ['xaxis', 'rangeselector', 'buttons', 0, 'count'], + 'xaxis.rangeselector.buttons[0].count', + 'In layout, key xaxis.rangeselector.buttons[0].count did not get coerced' + ); + assertErrorContent( + out[3], 'schema', 'layout', null, + ['xaxis', 'rangeselector', 'buttons', 1, 'title'], + 'xaxis.rangeselector.buttons[1].title', + 'In layout, key xaxis.rangeselector.buttons[1].title is not part of the schema' + ); + assertErrorContent( + out[4], 'schema', 'layout', null, + ['xaxis2', 'rangeselector', 'buttons', 0, 'title'], + 'xaxis2.rangeselector.buttons[0].title', + 'In layout, key xaxis2.rangeselector.buttons[0].title is not part of the schema' + ); + assertErrorContent( + out[5], 'array', 'layout', null, + ['xaxis3', 'rangeselector', 'buttons'], + 'xaxis3.rangeselector.buttons', + 'In layout, key xaxis3.rangeselector.buttons must be linked to an array container' + ); + assertErrorContent( + out[6], 'value', 'layout', null, + ['shapes', 0, 'opacity'], 'shapes[0].opacity', + 'In layout, key shapes[0].opacity is set to an invalid value (none)' + ); + }); + + it('should work with isSubplotObj attributes', function() { + var out = Plotly.validate([], { + xaxis2: { + range: 30 + }, + scene10: { + bgcolor: 'red' + }, + geo0: {}, + yaxis5: 'sup' + }); + + expect(out.length).toEqual(4); + assertErrorContent( + out[0], 'value', 'layout', null, + ['xaxis2', 'range'], 'xaxis2.range', + 'In layout, key xaxis2.range is set to an invalid value (30)' + ); + assertErrorContent( + out[1], 'unused', 'layout', null, + ['scene10'], 'scene10', + 'In layout, container scene10 did not get coerced' + ); + assertErrorContent( + out[2], 'schema', 'layout', null, + ['geo0'], 'geo0', + 'In layout, key geo0 is not part of the schema' + ); + assertErrorContent( + out[3], 'object', 'layout', null, + ['yaxis5'], 'yaxis5', + 'In layout, key yaxis5 must be linked to an object container' + ); + }); +});