UNPKG

victory-core

Version:
466 lines (420 loc) 15.2 kB
import React from "react"; import isDate from "lodash/isDate"; import isPlainObject from "lodash/isPlainObject"; import sortedUniq from "lodash/sortedUniq"; import * as Data from "./data"; import * as Scale from "./scale"; import * as Helpers from "./helpers"; import * as Collection from "./collection"; // Private Methods function cleanDomain(domain, props, axis) { const scaleType = Scale.getScaleType(props, axis); if (scaleType !== "log") { return domain; } const rules = (dom) => { const almostZero = dom[0] < 0 || dom[1] < 0 ? -1 / Number.MAX_SAFE_INTEGER : 1 / Number.MAX_SAFE_INTEGER; const domainOne = dom[0] === 0 ? almostZero : dom[0]; const domainTwo = dom[1] === 0 ? almostZero : dom[1]; return [domainOne, domainTwo]; }; return rules(domain); } function getDomainPadding(props, axis) { const formatPadding = (padding) => { return Array.isArray(padding) ? { left: padding[0], right: padding[1] } : { left: padding, right: padding }; }; return isPlainObject(props.domainPadding) ? formatPadding(props.domainPadding[axis]) : formatPadding(props.domainPadding); } function getFlatData(dataset, axis: "x" | "y") { const axisKey = `_${axis}`; return dataset.flat().map((datum: any) => { return datum[axisKey] && datum[axisKey][1] !== undefined ? datum[axisKey][1] : datum[axisKey]; }); } function getExtremeFromData(dataset, axis, type = "min") { const getExtreme = (arr) => type === "max" ? Math.max(...arr) : Math.min(...arr); const initialValue = type === "max" ? -Infinity : Infinity; let containsDate = false; const result = dataset.flat().reduce((memo: number, datum: any) => { const current0 = datum[`_${axis}0`] !== undefined ? datum[`_${axis}0`] : datum[`_${axis}`]; const current1 = datum[`_${axis}1`] !== undefined ? datum[`_${axis}1`] : datum[`_${axis}`]; const current = getExtreme([current0, current1]); containsDate = containsDate || current0 instanceof Date || current1 instanceof Date; return getExtreme([memo, current]); }, initialValue); return containsDate ? new Date(result) : result; } function padDomain(domain, props, axis) { if (!props.domainPadding) { return domain; } const minDomain = getMinFromProps(props, axis); const maxDomain = getMaxFromProps(props, axis); const padding = getDomainPadding(props, axis); if (!padding.left && !padding.right) { return domain; } const min = Collection.getMinValue(domain); const max = Collection.getMaxValue(domain); const currentAxis = Helpers.getCurrentAxis(axis, props.horizontal); const range = Helpers.getRange(props, currentAxis); const rangeExtent = Math.abs(range[0] - range[1]); const paddedRangeExtent = Math.max( rangeExtent - padding.left - padding.right, 1, ); const paddedDomainExtent = (Math.abs(max.valueOf() - min.valueOf()) / paddedRangeExtent) * rangeExtent; const simplePadding = { left: (paddedDomainExtent * padding.left) / rangeExtent, right: (paddedDomainExtent * padding.right) / rangeExtent, }; let paddedDomain = { min: min.valueOf() - simplePadding.left, max: max.valueOf() + simplePadding.right, }; const singleQuadrantDomainPadding = isPlainObject( props.singleQuadrantDomainPadding, ) ? props.singleQuadrantDomainPadding[axis] : props.singleQuadrantDomainPadding; const addsQuadrants = (min.valueOf() >= 0 && paddedDomain.min <= 0) || (max.valueOf() <= 0 && paddedDomain.max >= 0); const adjust = (val, type) => { const coerce = (type === "min" && min.valueOf() >= 0 && val <= 0) || (type === "max" && max.valueOf() <= 0 && val >= 0); return coerce ? 0 : val; }; if (addsQuadrants && singleQuadrantDomainPadding !== false) { // Naive initial padding calculation const initialPadding = { // @ts-expect-error `max/min` might be dates left: (Math.abs(max - min) * padding.left) / rangeExtent, // @ts-expect-error `max/min` might be dates right: (Math.abs(max - min) * padding.right) / rangeExtent, }; // Adjust the domain by the initial padding const adjustedDomain = { min: adjust(min.valueOf() - initialPadding.left, "min"), max: adjust(max.valueOf() + initialPadding.right, "max"), }; // re-calculate padding, taking the adjusted domain into account const finalPadding = { left: (Math.abs(adjustedDomain.max - adjustedDomain.min) * padding.left) / rangeExtent, right: (Math.abs(adjustedDomain.max - adjustedDomain.min) * padding.right) / rangeExtent, }; // Adjust the domain by the final padding paddedDomain = { min: adjust(min.valueOf() - finalPadding.left, "min"), max: adjust(max.valueOf() + finalPadding.right, "max"), }; } // default to minDomain / maxDomain if they exist const finalDomain = { min: minDomain !== undefined ? minDomain : paddedDomain.min, max: maxDomain !== undefined ? maxDomain : paddedDomain.max, }; return min instanceof Date || max instanceof Date ? getDomainFromMinMax(new Date(finalDomain.min), new Date(finalDomain.max)) : getDomainFromMinMax(finalDomain.min, finalDomain.max); } // Public Methods /** * Returns a getDomain function * @param {Function} getDomainFromDataFunction: a function that takes props and axis and * returns a domain based on data * @param {Function} formatDomainFunction: a function that takes domain, props, and axis and * returns a formatted domain * @returns {Function} a function that takes props and axis and returns a formatted domain */ export function createDomainFunction( getDomainFromDataFunction?, formatDomainFunction?, ) { const getDomainFromDataFn = Helpers.isFunction(getDomainFromDataFunction) ? getDomainFromDataFunction : getDomainFromData; const formatDomainFn = Helpers.isFunction(formatDomainFunction) ? formatDomainFunction : formatDomain; return (props, axis) => { const propsDomain = getDomainFromProps(props, axis); if (propsDomain) { return formatDomainFn(propsDomain, props, axis); } const categories = Data.getCategories(props, axis); const domain = categories ? getDomainFromCategories(props, axis, categories) : getDomainFromDataFn(props, axis); return domain ? formatDomainFn(domain, props, axis) : undefined; }; } /** * Returns a formatted domain. * @param {Array} domain: a domain in the form of a two element array * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} a domain in the form of a two element array */ export function formatDomain(domain, props, axis) { return cleanDomain(padDomain(domain, props, axis), props, axis); } /** * Returns a domain for a given axis based on props, category, or data * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} the domain for the given axis */ export function getDomain(props, axis) { return createDomainFunction()(props, axis); } /** * Returns a domain based on categories if they exist * @param {Object} props: the props object * @param {String} axis: the current axis * @param {Array} categories: an array of categories corresponding to a given axis * @returns {Array|undefined} returns a domain from categories or undefined */ export function getDomainFromCategories(props, axis, categories?) { const categoriesArray = categories || Data.getCategories(props, axis); const { polar, startAngle = 0, endAngle = 360 } = props; if (!categoriesArray) { return undefined; } const minDomain = getMinFromProps(props, axis); const maxDomain = getMaxFromProps(props, axis); const stringArray = Collection.containsStrings(categoriesArray) ? Data.getStringsFromCategories(props, axis) : []; const stringMap = stringArray.length === 0 ? null : stringArray.reduce((memo, string, index) => { memo[string] = index + 1; return memo; }, {}); const categoryValues = stringMap ? categoriesArray.map((value) => stringMap[value]) : categoriesArray; const min = minDomain !== undefined ? minDomain : Collection.getMinValue(categoryValues); const max = maxDomain !== undefined ? maxDomain : Collection.getMaxValue(categoryValues); const categoryDomain = getDomainFromMinMax(min, max); return polar && axis === "x" && Math.abs(startAngle - endAngle) === 360 ? getSymmetricDomain(categoryDomain, categoryValues) : categoryDomain; } /** * Returns a domain from a dataset for a given axis * @param {Object} props: the props object * @param {String} axis: the current axis * @param {Array} dataset: an array of data * @returns {Array} the domain based on data */ export function getDomainFromData(props, axis, dataset) { const datasetArray = dataset || Data.getData(props); const { polar, startAngle = 0, endAngle = 360 } = props; const minDomain = getMinFromProps(props, axis); const maxDomain = getMaxFromProps(props, axis); if (datasetArray.length < 1) { return minDomain !== undefined && maxDomain !== undefined ? getDomainFromMinMax(minDomain, maxDomain) : undefined; } const min = minDomain !== undefined ? minDomain : getExtremeFromData(datasetArray, axis, "min"); const max = maxDomain !== undefined ? maxDomain : getExtremeFromData(datasetArray, axis, "max"); const domain = getDomainFromMinMax(min, max); return polar && axis === "x" && Math.abs(startAngle - endAngle) === 360 ? getSymmetricDomain(domain, getFlatData(datasetArray, axis)) : domain; } /** * Returns a domain in the form of a two element array given a min and max value. * @param {Number|Date} min: the props object * @param {Number|Date} max: the current axis * @returns {Array} the minDomain based on props */ export function getDomainFromMinMax(min, max) { const getSinglePointDomain = (val) => { // d3-scale does not properly resolve very small differences. const verySmallNumber = // eslint-disable-next-line no-magic-numbers val === 0 ? 2 * Math.pow(10, -10) : Math.pow(10, -10); const verySmallDate = 1; const minVal = val instanceof Date ? new Date(Number(val) - verySmallDate) : Number(val) - verySmallNumber; const maxVal = val instanceof Date ? new Date(Number(val) + verySmallDate) : Number(val) + verySmallNumber; return val === 0 ? [0, maxVal] : [minVal, maxVal]; }; return Number(min) === Number(max) ? getSinglePointDomain(max) : [min, max]; } /** * Returns a the domain for a given axis if domain is given in props * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array|undefined} the domain based on props */ export function getDomainFromProps(props, axis) { const minDomain = getMinFromProps(props, axis); const maxDomain = getMaxFromProps(props, axis); if (isPlainObject(props.domain) && props.domain[axis]) { return props.domain[axis]; } else if (Array.isArray(props.domain)) { return props.domain; } else if (minDomain !== undefined && maxDomain !== undefined) { return getDomainFromMinMax(minDomain, maxDomain); } return undefined; } /** * Returns a domain for a given axis. This method forces the domain to include * zero unless the domain is explicitly specified in props. * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Array} the domain for the given axis */ export function getDomainWithZero(props, axis) { const propsDomain = getDomainFromProps(props, axis); if (propsDomain) { return propsDomain; } const dataset = Data.getData(props); const y0Min = dataset.reduce( (min, datum) => (datum._y0 < min ? datum._y0 : min), Infinity, ); const ensureZero = (domain) => { if (axis === "x") { return domain; } const defaultMin = y0Min !== Infinity ? y0Min : 0; const maxDomainProp = getMaxFromProps(props, axis); const minDomainProp = getMinFromProps(props, axis); const max = maxDomainProp !== undefined ? maxDomainProp : Collection.getMaxValue(domain, defaultMin); const min = minDomainProp !== undefined ? minDomainProp : Collection.getMinValue(domain, defaultMin); return getDomainFromMinMax(min, max); }; const getDomainFunction = () => { return getDomainFromData(props, axis, dataset); }; const formatDomainFunction = (domain) => { return formatDomain(ensureZero(domain), props, axis); }; return createDomainFunction(getDomainFunction, formatDomainFunction)( props, axis, ); } /** * Returns the maxDomain from props if it exists * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Number|Date|undefined} the maxDomain based on props */ export function getMaxFromProps(props, axis) { if (isPlainObject(props.maxDomain) && props.maxDomain[axis] !== undefined) { return props.maxDomain[axis]; } return typeof props.maxDomain === "number" || isDate(props.maxDomain) ? props.maxDomain : undefined; } /** * Returns the minDomain from props if it exists * @param {Object} props: the props object * @param {String} axis: the current axis * @returns {Number|Date|undefined} the minDomain based on props */ export function getMinFromProps(props, axis) { if (isPlainObject(props.minDomain) && props.minDomain[axis] !== undefined) { return props.minDomain[axis]; } return typeof props.minDomain === "number" || isDate(props.minDomain) ? props.minDomain : undefined; } /** * Returns a symmetrically padded domain for polar charts * @param {Array} domain: the original domain * @param {Array} values: a flat array of values corresponding to either tickValues, or data values * for a given dimension i.e. only x values. * @returns {Array} the symmetric domain */ export function getSymmetricDomain(domain, values: number[]) { const processedData = sortedUniq(values.sort((a, b) => a - b)); const step = processedData[1] - processedData[0]; return [domain[0], domain[1] + step]; } /** * Checks whether a given component can be used to calculate domain * @param {Component} component: a React component instance * @returns {Boolean} Returns true if the given component has a role included in the whitelist */ export function isDomainComponent(component) { const getRole = (child) => { return child && child.type ? child.type.role : ""; }; let role = getRole(component); if (role === "portal") { const children = React.Children.toArray(component.props.children); role = children.length ? getRole(children[0]) : ""; } const whitelist = [ "area", "axis", "bar", "boxplot", "candlestick", "errorbar", "group", "histogram", "line", "pie", "scatter", "stack", "voronoi", ]; return whitelist.includes(role); }