UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

603 lines (537 loc) 22.5 kB
'use strict'; var isNumeric = require('fast-isnumeric'); 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.slice(1, -1)); 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', 'arrayOk'], coerceFunction: function (v, propOut, dflt, opts) { const isBoolean = (value) => value === true || value === false; if (isBoolean(v) || (opts.arrayOk && Array.isArray(v) && v.length > 0 && v.every(isBoolean))) { 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', '#d3d3d3aa')", "- rgb/rgba (e.g. 'rgb(255, 0, 0)', 'rgba(255 0 0 / 0.5)')", "- hsl/hsla (e.g. 'hsl(0, 100%, 50%)', 'hsla(0 100% 50% / 0.5)')", "- hwb (e.g. 'hwb(0 0% 0%)')", "- lab/lch/oklab/oklch (e.g. 'oklch(0.7 0.15 180)')", "- color (e.g. 'color(display-p3 1 0 0)')", '- named colors (full list: https://www.w3.org/TR/css-color-4/#named-colors)' ].join(' '), requiredOpts: [], otherOpts: ['dflt', 'arrayOk'], coerceFunction: function (v, propOut, dflt) { if (isTypedArraySpec(v)) v = decodeTypedArraySpec(v); if (Color.isValid(v)) 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) { if (!Array.isArray(v) || !v.length) propOut.set(dflt); else if (v.every((color) => Color.isValid(color))) 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', 'arrayOk'], coerceFunction: function (v, propOut, dflt, opts) { var regex = opts.regex || counterRegex(dflt); const isSubplotId = (value) => typeof value === 'string' && regex.test(value); if (isSubplotId(v) || (opts.arrayOk && isArrayOrTypedArray(v) && v.length > 0 && v.every(isSubplotId))) { propOut.set(v); } else { 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', '(flag order does not affect the output).', '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 is 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'); var path; if (!shape) { path = coerce(attr + '.path'); } if (shape || path) { 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;