plotly.js
Version:
The open source javascript graphing library that powers plotly
611 lines (545 loc) • 22.1 kB
JavaScript
;
var isNumeric = require('fast-isnumeric');
var tinycolor = require('tinycolor2');
var extendFlat = require('./extend').extendFlat;
var baseTraceAttrs = require('../plots/attributes');
var colorscales = require('../components/colorscale/scales');
var Color = require('../components/color');
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;
var isTypedArraySpec = require('./array').isTypedArraySpec;
var decodeTypedArraySpec = require('./array').decodeTypedArraySpec;
exports.valObjectMeta = {
data_array: {
// You can use *dflt=[] to force said array to exist though.
description: [
'An {array} of data.',
'The value must represent an {array} or it will be ignored,',
'but this array can be provided in several forms:',
'(1) a regular {array} object',
'(2) a typed array (e.g. Float32Array)',
'(3) an object with keys dtype, bdata, and optionally shape.',
'In this 3rd form, dtype is one of',
'*f8*, *f4*.',
'*i4*, *u4*,',
'*i2*, *u2*,',
'*i1*, *u1* or *u1c* for Uint8ClampedArray.',
'In addition to shorthand `dtype` above one could also use the following forms:',
'*float64*, *float32*,',
'*int32*, *uint32*,',
'*int16*, *uint16*,',
'*int8*, *uint8* or *uint8c* for Uint8ClampedArray.',
'`bdata` is either a base64-encoded string or the ArrayBuffer of',
'an integer or float typed array.',
'For either multi-dimensional arrays you must also',
'provide its dimensions separated by comma via `shape`.',
'For example using `dtype`: *f4* and `shape`: *5,100* you can',
'declare a 2-D array that has 5 rows and 100 columns',
'containing float32 values i.e. 4 bits per value.',
'`shape` is optional for one dimensional arrays.'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
coerceFunction: function(v, propOut, dflt) {
propOut.set(
isArrayOrTypedArray(v) ? v :
isTypedArraySpec(v) ? decodeTypedArraySpec(v) :
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(isTypedArraySpec(v)) v = decodeTypedArraySpec(v);
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', 'extras'],
coerceFunction: function(v, propOut, dflt, opts) {
if((opts.extras || []).indexOf(v) !== -1) {
propOut.set(v);
return;
}
if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v);
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(isTypedArraySpec(v)) v = decodeTypedArraySpec(v);
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', 'arrayOk'],
coerceFunction: function(v, propOut, dflt) {
if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v);
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((opts.extras || []).indexOf(v) !== -1) {
propOut.set(v);
return;
}
if(typeof v !== 'string') {
propOut.set(dflt);
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(
isTypedArraySpec(v) ? decodeTypedArraySpec(v) :
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;
}
if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v);
if(!isArrayOrTypedArray(v)) {
propOut.set(dflt);
return;
}
var twoD = opts.dimensions === 2 || (opts.dimensions === '1-2' && Array.isArray(v) && isArrayOrTypedArray(v[0]));
var items = opts.items;
var vOut = [];
var arrayItems = Array.isArray(items);
var arrayItems2D = arrayItems && twoD && isArrayOrTypedArray(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 = isArrayOrTypedArray(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(!isArrayOrTypedArray(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(!isArrayOrTypedArray(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;
if(opts.arrayOk) {
if(isArrayOrTypedArray(v)) {
/**
* 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)
*/
propOut.set(v);
return v;
} else {
if(isTypedArraySpec(v)) {
v = decodeTypedArraySpec(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, opts) {
if(!opts) opts = {};
dfltObj = extendFlat({}, dfltObj);
dfltObj = extendFlat(dfltObj, opts.overrideDflt || {});
var out = {
family: coerce(attr + '.family', dfltObj.family),
size: coerce(attr + '.size', dfltObj.size),
color: coerce(attr + '.color', dfltObj.color),
weight: coerce(attr + '.weight', dfltObj.weight),
style: coerce(attr + '.style', dfltObj.style),
};
if(!opts.noFontVariant) out.variant = coerce(attr + '.variant', dfltObj.variant);
if(!opts.noFontLineposition) out.lineposition = coerce(attr + '.lineposition', dfltObj.lineposition);
if(!opts.noFontTextcase) out.textcase = coerce(attr + '.textcase', dfltObj.textcase);
if(!opts.noFontShadow) {
var dfltShadow = dfltObj.shadow;
if(dfltShadow === 'none' && opts.autoShadowDflt) {
dfltShadow = 'auto';
}
out.shadow = coerce(attr + '.shadow', dfltShadow);
}
return out;
};
/*
* Shortcut to coerce the pattern attributes
*/
exports.coercePattern = function(coerce, attr, markerColor, hasMarkerColorscale) {
var shape = coerce(attr + '.shape');
if(shape) {
coerce(attr + '.solidity');
coerce(attr + '.size');
var fillmode = coerce(attr + '.fillmode');
var isOverlay = fillmode === 'overlay';
if(!hasMarkerColorscale) {
var bgcolor = coerce(attr + '.bgcolor', isOverlay ?
markerColor :
undefined
);
coerce(attr + '.fgcolor', isOverlay ?
Color.contrast(bgcolor) :
markerColor
);
}
coerce(attr + '.fgopacity', isOverlay ?
0.5 :
1
);
}
};
/** 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;