plotly.js
Version:
The open source javascript graphing library that powers plotly
516 lines (461 loc) • 18.5 kB
JavaScript
/**
* Copyright 2012-2020, 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.
*/
;
var isNumeric = require('fast-isnumeric');
var tinycolor = require('tinycolor2');
var baseTraceAttrs = require('../plots/attributes');
var colorscales = require('../components/colorscale/scales');
var DESELECTDIM = require('../constants/interactions').DESELECTDIM;
var nestedProperty = require('./nested_property');
var counterRegex = require('./regex').counter;
var modHalf = require('./mod').modHalf;
var isArrayOrTypedArray = require('./array').isArrayOrTypedArray;
exports.valObjectMeta = {
data_array: {
// You can use *dflt=[] to force said array to exist though.
description: [
'An {array} of data.',
'The value MUST be an {array}, or we ignore it.',
'Note that typed arrays (e.g. Float32Array) are supported.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
// TODO maybe `v: {type: 'float32', vals: [/* ... */]}` also
if(isArrayOrTypedArray(v)) propOut.set(v);
else if(dflt !== undefined) propOut.set(dflt);
}
},
enumerated: {
description: [
'Enumerated value type. The available values are listed',
'in `values`.'
].join(' '),
requiredOpts: ['values'],
otherOpts: ['dflt', 'coerceNumber', 'arrayOk'],
coerceFunction: function(v, propOut, dflt, opts) {
if(opts.coerceNumber) v = +v;
if(opts.values.indexOf(v) === -1) propOut.set(dflt);
else propOut.set(v);
},
validateFunction: function(v, opts) {
if(opts.coerceNumber) v = +v;
var values = opts.values;
for(var i = 0; i < values.length; i++) {
var k = String(values[i]);
if((k.charAt(0) === '/' && k.charAt(k.length - 1) === '/')) {
var regex = new RegExp(k.substr(1, k.length - 2));
if(regex.test(v)) return true;
} else if(v === values[i]) return true;
}
return false;
}
},
'boolean': {
description: 'A boolean (true/false) value.',
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
if(v === true || v === false) propOut.set(v);
else propOut.set(dflt);
}
},
number: {
description: [
'A number or a numeric value',
'(e.g. a number inside a string).',
'When applicable, values greater (less) than `max` (`min`)',
'are coerced to the `dflt`.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt', 'min', 'max', 'arrayOk'],
coerceFunction: function(v, propOut, dflt, opts) {
if(!isNumeric(v) ||
(opts.min !== undefined && v < opts.min) ||
(opts.max !== undefined && v > opts.max)) {
propOut.set(dflt);
} else propOut.set(+v);
}
},
integer: {
description: [
'An integer or an integer inside a string.',
'When applicable, values greater (less) than `max` (`min`)',
'are coerced to the `dflt`.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt', 'min', 'max', 'arrayOk'],
coerceFunction: function(v, propOut, dflt, opts) {
if(v % 1 || !isNumeric(v) ||
(opts.min !== undefined && v < opts.min) ||
(opts.max !== undefined && v > opts.max)) {
propOut.set(dflt);
} else propOut.set(+v);
}
},
string: {
description: [
'A string value.',
'Numbers are converted to strings except for attributes with',
'`strict` set to true.'
].join(' '),
requiredOpts: [],
// 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(typeof v !== 'string') {
var okToCoerce = (typeof v === 'number');
if(opts.strict === true || !okToCoerce) propOut.set(dflt);
else propOut.set(String(v));
} else if(opts.noBlank && !v) propOut.set(dflt);
else propOut.set(v);
}
},
color: {
description: [
'A string describing color.',
'Supported formats:',
'- hex (e.g. \'#d3d3d3\')',
'- rgb (e.g. \'rgb(255, 0, 0)\')',
'- rgba (e.g. \'rgb(255, 0, 0, 0.5)\')',
'- hsl (e.g. \'hsl(0, 100%, 50%)\')',
'- hsv (e.g. \'hsv(0, 100%, 100%)\')',
'- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt', 'arrayOk'],
coerceFunction: function(v, propOut, dflt) {
if(tinycolor(v).isValid()) propOut.set(v);
else propOut.set(dflt);
}
},
colorlist: {
description: [
'A list of colors.',
'Must be an {array} containing valid colors.',
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
function isColor(color) {
return tinycolor(color).isValid();
}
if(!Array.isArray(v) || !v.length) propOut.set(dflt);
else if(v.every(isColor)) propOut.set(v);
else propOut.set(dflt);
}
},
colorscale: {
description: [
'A Plotly colorscale either picked by a name:',
'(any of', Object.keys(colorscales.scales).join(', '), ')',
'customized as an {array} of 2-element {arrays} where',
'the first element is the normalized color level value',
'(starting at *0* and ending at *1*),',
'and the second item is a valid color string.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
propOut.set(colorscales.get(v, dflt));
}
},
angle: {
description: [
'A number (in degree) between -180 and 180.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
if(v === 'auto') propOut.set('auto');
else if(!isNumeric(v)) propOut.set(dflt);
else propOut.set(modHalf(+v, 360));
}
},
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',
'\'geo\', \'geo2\', \'geo3\', ...'
].join(' '),
requiredOpts: ['dflt'],
otherOpts: ['regex'],
coerceFunction: function(v, propOut, dflt, opts) {
var regex = opts.regex || counterRegex(dflt);
if(typeof v === 'string' && regex.test(v)) {
propOut.set(v);
return;
}
propOut.set(dflt);
},
validateFunction: function(v, opts) {
var dflt = opts.dflt;
if(v === dflt) return true;
if(typeof v !== 'string') return false;
if(counterRegex(dflt).test(v)) return true;
return false;
}
},
flaglist: {
description: [
'A string representing a combination of flags',
'(order does not matter here).',
'Combine any of the available `flags` with *+*.',
'(e.g. (\'lines+markers\')).',
'Values in `extras` cannot be combined.'
].join(' '),
requiredOpts: ['flags'],
otherOpts: ['dflt', 'extras', 'arrayOk'],
coerceFunction: function(v, propOut, dflt, opts) {
if(typeof v !== 'string') {
propOut.set(dflt);
return;
}
if((opts.extras || []).indexOf(v) !== -1) {
propOut.set(v);
return;
}
var vParts = v.split('+');
var i = 0;
while(i < vParts.length) {
var vi = vParts[i];
if(opts.flags.indexOf(vi) === -1 || vParts.indexOf(vi) < i) {
vParts.splice(i, 1);
} else i++;
}
if(!vParts.length) propOut.set(dflt);
else propOut.set(vParts.join('+'));
}
},
any: {
description: 'Any type.',
requiredOpts: [],
otherOpts: ['dflt', 'values', 'arrayOk'],
coerceFunction: function(v, propOut, dflt) {
if(v === undefined) propOut.set(dflt);
else propOut.set(v);
}
},
info_array: {
description: [
'An {array} of plot information.'
].join(' '),
requiredOpts: ['items'],
// set `dimensions=2` for a 2D array or '1-2' for either
// `items` may be a single object instead of an array, in which case
// `freeLength` must be true.
// if `dimensions='1-2'` and items is a 1D array, then the value can
// either be a matching 1D array or an array of such matching 1D arrays
otherOpts: ['dflt', 'freeLength', 'dimensions'],
coerceFunction: function(v, propOut, dflt, opts) {
// simplified coerce function just for array items
function coercePart(v, opts, dflt) {
var out;
var propPart = {set: function(v) { out = v; }};
if(dflt === undefined) dflt = opts.dflt;
exports.valObjectMeta[opts.valType].coerceFunction(v, propPart, dflt, opts);
return out;
}
var twoD = opts.dimensions === 2 || (opts.dimensions === '1-2' && Array.isArray(v) && Array.isArray(v[0]));
if(!Array.isArray(v)) {
propOut.set(dflt);
return;
}
var items = opts.items;
var vOut = [];
var arrayItems = Array.isArray(items);
var arrayItems2D = arrayItems && twoD && Array.isArray(items[0]);
var innerItemsOnly = twoD && arrayItems && !arrayItems2D;
var len = (arrayItems && !innerItemsOnly) ? items.length : v.length;
var i, j, row, item, len2, vNew;
dflt = Array.isArray(dflt) ? dflt : [];
if(twoD) {
for(i = 0; i < len; i++) {
vOut[i] = [];
row = Array.isArray(v[i]) ? v[i] : [];
if(innerItemsOnly) len2 = items.length;
else if(arrayItems) len2 = items[i].length;
else len2 = row.length;
for(j = 0; j < len2; j++) {
if(innerItemsOnly) item = items[j];
else if(arrayItems) item = items[i][j];
else item = items;
vNew = coercePart(row[j], item, (dflt[i] || [])[j]);
if(vNew !== undefined) vOut[i][j] = vNew;
}
}
} else {
for(i = 0; i < len; i++) {
vNew = coercePart(v[i], arrayItems ? items[i] : items, dflt[i]);
if(vNew !== undefined) vOut[i] = vNew;
}
}
propOut.set(vOut);
},
validateFunction: function(v, opts) {
if(!Array.isArray(v)) return false;
var items = opts.items;
var arrayItems = Array.isArray(items);
var twoD = opts.dimensions === 2;
// when free length is off, input and declared lengths must match
if(!opts.freeLength && v.length !== items.length) return false;
// valid when all input items are valid
for(var i = 0; i < v.length; i++) {
if(twoD) {
if(!Array.isArray(v[i]) || (!opts.freeLength && v[i].length !== items[i].length)) {
return false;
}
for(var j = 0; j < v[i].length; j++) {
if(!validate(v[i][j], arrayItems ? items[i][j] : items)) {
return false;
}
}
} else if(!validate(v[i], arrayItems ? items[i] : items)) return false;
}
return true;
}
}
};
/**
* Ensures that container[attribute] has a valid value.
*
* attributes[attribute] is an object with possible keys:
* - valType: data_array, enumerated, boolean, ... as in valObjectMeta
* - values: (enumerated only) array of allowed vals
* - min, max: (number, integer only) inclusive bounds on allowed vals
* either or both may be omitted
* - dflt: if attribute is invalid or missing, use this default
* if dflt is provided as an argument to lib.coerce it takes precedence
* as a convenience, returns the value it finally set
*/
exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt) {
var opts = nestedProperty(attributes, attribute).get();
var propIn = nestedProperty(containerIn, attribute);
var propOut = nestedProperty(containerOut, attribute);
var v = propIn.get();
var template = containerOut._template;
if(v === undefined && template) {
v = nestedProperty(template, attribute).get();
// already used the template value, so short-circuit the second check
template = 0;
}
if(dflt === undefined) dflt = opts.dflt;
/**
* arrayOk: value MAY be an array, then we do no value checking
* at this point, because it can be more complicated than the
* individual form (eg. some array vals can be numbers, even if the
* single values must be color strings)
*/
if(opts.arrayOk && isArrayOrTypedArray(v)) {
propOut.set(v);
return v;
}
var coerceFunction = exports.valObjectMeta[opts.valType].coerceFunction;
coerceFunction(v, propOut, dflt, opts);
var out = propOut.get();
// in case v was provided but invalid, try the template again so it still
// overrides the regular default
if(template && out === dflt && !validate(v, opts)) {
v = nestedProperty(template, attribute).get();
coerceFunction(v, propOut, dflt, opts);
out = propOut.get();
}
return out;
};
/**
* Variation on coerce
*
* Uses coerce to get attribute value if user input is valid,
* returns attribute default if user input it not valid or
* returns false if there is no user input.
*/
exports.coerce2 = function(containerIn, containerOut, attributes, attribute, dflt) {
var propIn = nestedProperty(containerIn, attribute);
var propOut = exports.coerce(containerIn, containerOut, attributes, attribute, dflt);
var valIn = propIn.get();
return (valIn !== undefined && valIn !== null) ? propOut : false;
};
/*
* Shortcut to coerce the three font attributes
*
* 'coerce' is a lib.coerce wrapper with implied first three arguments
*/
exports.coerceFont = function(coerce, attr, dfltObj) {
var out = {};
dfltObj = dfltObj || {};
out.family = coerce(attr + '.family', dfltObj.family);
out.size = coerce(attr + '.size', dfltObj.size);
out.color = coerce(attr + '.color', dfltObj.color);
return out;
};
/** Coerce shortcut for 'hoverinfo'
* handling 1-vs-multi-trace dflt logic
*
* @param {object} traceIn : user trace object
* @param {object} traceOut : full trace object (requires _module ref)
* @param {object} layoutOut : full layout object (require _dataLength ref)
* @return {any} : the coerced value
*/
exports.coerceHoverinfo = function(traceIn, traceOut, layoutOut) {
var moduleAttrs = traceOut._module.attributes;
var attrs = moduleAttrs.hoverinfo ? moduleAttrs : baseTraceAttrs;
var valObj = attrs.hoverinfo;
var dflt;
if(layoutOut._dataLength === 1) {
var flags = valObj.dflt === 'all' ?
valObj.flags.slice() :
valObj.dflt.split('+');
flags.splice(flags.indexOf('name'), 1);
dflt = flags.join('+');
}
return exports.coerce(traceIn, traceOut, attrs, 'hoverinfo', dflt);
};
/** Coerce shortcut for [un]selected.marker.opacity,
* which has special default logic, to ensure that it corresponds to the
* default selection behavior while allowing to be overtaken by any other
* [un]selected attribute.
*
* N.B. This must be called *after* coercing all the other [un]selected attrs,
* to give the intended result.
*
* @param {object} traceOut : fullData item
* @param {function} coerce : lib.coerce wrapper with implied first three arguments
*/
exports.coerceSelectionMarkerOpacity = function(traceOut, coerce) {
if(!traceOut.marker) return;
var mo = traceOut.marker.opacity;
// you can still have a `marker` container with no markers if there's text
if(mo === undefined) return;
var smoDflt;
var usmoDflt;
// Don't give [un]selected.marker.opacity a default value if
// marker.opacity is an array: handle this during style step.
//
// Only give [un]selected.marker.opacity a default value if you don't
// set any other [un]selected attributes.
if(!isArrayOrTypedArray(mo) && !traceOut.selected && !traceOut.unselected) {
smoDflt = mo;
usmoDflt = DESELECTDIM * mo;
}
coerce('selected.marker.opacity', smoDflt);
coerce('unselected.marker.opacity', usmoDflt);
};
function validate(value, opts) {
var valObjectDef = exports.valObjectMeta[opts.valType];
if(opts.arrayOk && isArrayOrTypedArray(value)) return true;
if(valObjectDef.validateFunction) {
return valObjectDef.validateFunction(value, opts);
}
var failed = {};
var out = failed;
var propMock = { set: function(v) { out = v; } };
// 'failed' just something mutable that won't be === anything else
valObjectDef.coerceFunction(value, propMock, failed, opts);
return out !== failed;
}
exports.validate = validate;