UNPKG

victory-core

Version:
413 lines (387 loc) 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createStringMap = createStringMap; exports.downsample = downsample; exports.formatData = formatData; exports.formatDataFromDomain = formatDataFromDomain; exports.generateData = generateData; exports.getCategories = getCategories; exports.getData = getData; exports.getStringsFromAxes = getStringsFromAxes; exports.getStringsFromCategories = getStringsFromCategories; exports.getStringsFromData = getStringsFromData; exports.isDataComponent = isDataComponent; var _react = _interopRequireDefault(require("react")); var _isEmpty = _interopRequireDefault(require("lodash/isEmpty")); var _isEqual = _interopRequireDefault(require("lodash/isEqual")); var _isPlainObject = _interopRequireDefault(require("lodash/isPlainObject")); var _isUndefined = _interopRequireDefault(require("lodash/isUndefined")); var _omitBy = _interopRequireDefault(require("lodash/omitBy")); var _orderBy = _interopRequireDefault(require("lodash/orderBy")); var _property = _interopRequireDefault(require("lodash/property")); var _uniq = _interopRequireDefault(require("lodash/uniq")); var Helpers = _interopRequireWildcard(require("./helpers")); var Collection = _interopRequireWildcard(require("./collection")); var Scale = _interopRequireWildcard(require("./scale")); var Immutable = _interopRequireWildcard(require("./immutable")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Private Functions function parseDatum(datum) { const immutableDatumWhitelist = { errorX: true, errorY: true }; return Immutable.isImmutable(datum) ? Immutable.shallowToJS(datum, immutableDatumWhitelist) : datum; } function getLength(data) { return Immutable.isIterable(data) ? data.size : data.length; } // Returns generated data for a given axis based on domain and sample from props function generateDataArray(props, axis) { const propsDomain = (0, _isPlainObject.default)(props.domain) ? props.domain[axis] : props.domain; const domain = propsDomain || Scale.getBaseScale(props, axis).domain(); const samples = props.samples || 1; const domainMax = Math.max(...domain); const domainMin = Math.min(...domain); const step = (domainMax - domainMin) / samples; const values = Helpers.range(domainMin, domainMax, step); return values[values.length - 1] === domainMax ? values : values.concat(domainMax); } // Returns sorted data. If no sort keys are provided, data is returned unaltered. function sortData(dataset, sortKey, sortOrder) { if (sortOrder === void 0) { sortOrder = "ascending"; } if (!sortKey) { return dataset; } // Ensures previous VictoryLine api for sortKey prop stays consistent let formattedSortKey = sortKey; if (sortKey === "x" || sortKey === "y") { formattedSortKey = `_${sortKey}`; } const order = sortOrder === "ascending" ? "asc" : "desc"; return (0, _orderBy.default)(dataset, formattedSortKey, order); } // This method will remove data points that break certain scales. (log scale only) function cleanData(dataset, props) { const smallNumber = 1 / Number.MAX_SAFE_INTEGER; const scaleType = { x: Scale.getScaleType(props, "x"), y: Scale.getScaleType(props, "y") }; if (scaleType.x !== "log" && scaleType.y !== "log") { return dataset; } const rules = (datum, axis) => { return scaleType[axis] === "log" ? datum[`_${axis}`] !== 0 : true; }; const sanitize = datum => { const _x = rules(datum, "x") ? datum._x : smallNumber; const _y = rules(datum, "y") ? datum._y : smallNumber; const _y0 = rules(datum, "y0") ? datum._y0 : smallNumber; return Object.assign({}, datum, { _x, _y, _y0 }); }; return dataset.map(datum => { if (rules(datum, "x") && rules(datum, "y") && rules(datum, "y0")) { return datum; } return sanitize(datum); }); } // Returns a data accessor given an eventKey prop function getEventKey(key) { // creates a data accessor function // given a property key, path, array index, or null for identity. if (Helpers.isFunction(key)) { return key; } else if (key === null || key === undefined) { return () => undefined; } // otherwise, assume it is an array index, property key or path (_.property handles all three) return (0, _property.default)(key); } // Returns data with an eventKey prop added to each datum function addEventKeys(props, data) { const hasEventKeyAccessor = !!props.eventKey; const eventKeyAccessor = getEventKey(props.eventKey); return data.map((datum, index) => { if (datum.eventKey !== undefined) { return datum; } else if (hasEventKeyAccessor) { const eventKey = eventKeyAccessor(datum, index); return eventKey !== undefined ? Object.assign({ eventKey }, datum) : datum; } return datum; }); } // Exported Functions // This method will remove data points that fall outside of the desired domain (non-continuous charts only) function formatDataFromDomain(dataset, domain, defaultBaseline) { const exists = val => val !== undefined; const minDomainX = Collection.getMinValue(domain.x); const maxDomainX = Collection.getMaxValue(domain.x); const minDomainY = Collection.getMinValue(domain.y); const maxDomainY = Collection.getMaxValue(domain.y); const underMin = min => val => exists(val) && val < min; const overMax = max => val => exists(val) && val > max; const isUnderMinX = underMin(minDomainX); const isUnderMinY = underMin(minDomainY); const isOverMaxX = overMax(maxDomainX); const isOverMaxY = overMax(maxDomainY); return dataset.map(datum => { let { _x, _y, _y0, _y1 } = datum; // single x point less than min domain if (isUnderMinX(_x) || isOverMaxX(_x)) _x = null; const baseline = exists(_y0) ? _y0 : defaultBaseline; const value = exists(_y1) ? _y1 : _y; if (!exists(value)) return datum; // value only and less than min domain or greater than max domain if (!exists(baseline) && (isUnderMinY(value) || isOverMaxY(value))) _y = null; // baseline and value are both less than min domain or both greater than max domain if (isUnderMinY(baseline) && isUnderMinY(value) || isOverMaxY(baseline) && isOverMaxY(value)) _y = _y0 = _y1 = null; // baseline and value with only baseline below min, set baseline to minDomainY if (isUnderMinY(baseline) && !isUnderMinY(value)) _y0 = minDomainY; // baseline and value with only baseline above max, set baseline to maxDomainY if (isOverMaxY(baseline) && !isOverMaxY(value)) _y0 = maxDomainY; return Object.assign({}, datum, (0, _omitBy.default)({ _x, _y, _y0, _y1 }, _isUndefined.default)); }); } /** * Returns an object mapping string data to numeric data * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Object} an object mapping string data to numeric data */ function createStringMap(props, axis) { const stringsFromAxes = getStringsFromAxes(props, axis); const stringsFromCategories = getStringsFromCategories(props, axis); const stringsFromData = getStringsFromData(props, axis); const allStrings = (0, _uniq.default)([...stringsFromAxes, ...stringsFromCategories, ...stringsFromData]); return allStrings.length === 0 ? null : allStrings.reduce((memo, string, index) => { memo[string] = index + 1; return memo; }, {}); } /** * Reduces the size of a data array, such that it is <= maxPoints. * @param {Array} data: an array of data; must be sorted * @param {Number} maxPoints: maximum number of data points to return * @param {Number} startingIndex: the index of the data[0] *in the entire dataset*; this function assumes `data` param is a subset of larger dataset that has been zoommed * @returns {Array} an array of data, a subset of data param */ function downsample(data, maxPoints, startingIndex) { if (startingIndex === void 0) { startingIndex = 0; } // ensures that the downampling of data while zooming looks good. const dataLength = getLength(data); if (dataLength > maxPoints) { // limit k to powers of 2, e.g. 64, 128, 256 // so that the same points will be chosen reliably, reducing flicker on zoom const k = Math.pow(2, Math.ceil(Math.log2(dataLength / maxPoints))); return data.filter( // ensure modulo is always calculated from same reference: i + startingIndex (d, i) => (i + startingIndex) % k === 0); } return data; } /** * Returns formatted data. Data accessors are applied, and string values are replaced. * @param {Array} dataset: the original domain * @param {Object} props: the props object * @param {Array} expectedKeys: an array of expected data keys * @returns {Array} the formatted data */ function formatData(dataset, props, expectedKeys) { const isArrayOrIterable = Array.isArray(dataset) || Immutable.isIterable(dataset); if (!isArrayOrIterable || getLength(dataset) < 1) { return []; } const defaultKeys = ["x", "y", "y0"]; // TODO: We shouldn’t mutate the expectedKeys param here, // but we need to figure out why changing it causes regressions in tests. // eslint-disable-next-line no-param-reassign expectedKeys = Array.isArray(expectedKeys) ? expectedKeys : defaultKeys; const createAccessor = name => { return Helpers.createAccessor(props[name] !== undefined ? props[name] : name); }; const accessor = expectedKeys.reduce((memo, type) => { memo[type] = createAccessor(type); return memo; }, {}); const preformattedData = (0, _isEqual.default)(expectedKeys, defaultKeys) && props.x === "_x" && props.y === "_y" && props.y0 === "_y0"; let stringMap; if (preformattedData === false) { // stringMap is not required if the data is preformatted stringMap = { x: expectedKeys.indexOf("x") !== -1 ? createStringMap(props, "x") : undefined, y: expectedKeys.indexOf("y") !== -1 ? createStringMap(props, "y") : undefined, y0: expectedKeys.indexOf("y0") !== -1 ? createStringMap(props, "y") : undefined }; } const data = preformattedData ? dataset : dataset.reduce((dataArr, datum, index) => { const parsedDatum = parseDatum(datum); const fallbackValues = { x: index, y: parsedDatum }; const processedValues = expectedKeys.reduce((memo, type) => { const processedValue = accessor[type](parsedDatum); const value = processedValue !== undefined ? processedValue : fallbackValues[type]; if (value !== undefined) { if (typeof value === "string" && stringMap[type]) { memo[`${type}Name`] = value; memo[`_${type}`] = stringMap[type][value]; } else { memo[`_${type}`] = value; } } return memo; }, {}); const formattedDatum = Object.assign({}, processedValues, parsedDatum); if (!(0, _isEmpty.default)(formattedDatum)) { dataArr.push(formattedDatum); } return dataArr; }, []); const sortedData = sortData(data, props.sortKey, props.sortOrder); const cleanedData = cleanData(sortedData, props); return addEventKeys(props, cleanedData); } /** * Returns generated x and y data based on domain and sample from props * @param {Object} props: the props object * @returns {Array} an array of data */ function generateData(props) { const xValues = generateDataArray(props, "x"); const yValues = generateDataArray(props, "y"); const values = xValues.map((x, i) => { return { x, y: yValues[i] }; }); return values; } /** * Returns an array of categories for a given axis * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} an array of categories */ function getCategories(props, axis) { return props.categories && !Array.isArray(props.categories) ? props.categories[axis] : props.categories; } /** * Returns an array of formatted data * @param {Object} props: the props object * @returns {Array} an array of data */ function getData(props) { return props.data ? formatData(props.data, props) : formatData(generateData(props), props); } /** * Returns an array of strings from axis tickValues for a given axis * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} an array of strings */ function getStringsFromAxes(props, axis) { const { tickValues, tickFormat } = props; let tickValueArray; if (!tickValues || !Array.isArray(tickValues) && !tickValues[axis]) { tickValueArray = tickFormat && Array.isArray(tickFormat) ? tickFormat : []; } else { tickValueArray = tickValues[axis] || tickValues; } return tickValueArray.filter(val => typeof val === "string"); } /** * Returns an array of strings from categories for a given axis * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} an array of strings */ function getStringsFromCategories(props, axis) { if (!props.categories) { return []; } const categories = getCategories(props, axis); const categoryStrings = categories && categories.filter(val => typeof val === "string"); return categoryStrings ? Collection.removeUndefined(categoryStrings) : []; } /** * Returns an array of strings from data * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} an array of strings */ function getStringsFromData(props, axis) { const isArrayOrIterable = Array.isArray(props.data) || Immutable.isIterable(props.data); if (!isArrayOrIterable) { return []; } const key = props[axis] === undefined ? axis : props[axis]; const accessor = Helpers.createAccessor(key); // support immutable data const data = props.data.reduce((memo, d) => { memo.push(parseDatum(d)); return memo; }, []); const sortedData = sortData(data, props.sortKey, props.sortOrder); const dataStrings = sortedData.reduce((dataArr, datum) => { const parsedDatum = parseDatum(datum); dataArr.push(accessor(parsedDatum)); return dataArr; }, []).filter(datum => typeof datum === "string"); // return a unique set of strings return dataStrings.reduce((prev, curr) => { if (curr !== undefined && curr !== null && prev.indexOf(curr) === -1) { prev.push(curr); } return prev; }, []); } /** * Checks whether a given component can be used to calculate data * @param {Component} component: a React component instance * @returns {Boolean} Returns true if the given component has a role included in the whitelist */ function isDataComponent(component) { const getRole = child => { return child && child.type ? child.type.role : ""; }; let role = getRole(component); if (role === "portal") { const children = _react.default.Children.toArray(component.props.children); role = children.length ? getRole(children[0]) : ""; } const whitelist = ["area", "bar", "boxplot", "candlestick", "errorbar", "group", "histogram", "line", "pie", "scatter", "stack", "voronoi"]; return whitelist.includes(role); }