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'
+ );
+ });
+});