UNPKG

victory-core

Version:
209 lines (195 loc) 7.5 kB
import isPlainObject from "lodash/isPlainObject"; import orderBy from "lodash/orderBy"; import { interpolate } from "victory-vendor/d3-interpolate"; export const isInterpolatable = function (obj) { // d3 turns null into 0 and undefined into NaN, which we don't want. if (obj !== null) { switch (typeof obj) { case "undefined": return false; case "number": // The standard `isNaN` is fine in this case since we already know the // type is number. return ( !isNaN(obj) && obj !== Number.POSITIVE_INFINITY && obj !== Number.NEGATIVE_INFINITY ); case "string": // d3 might not *actually* be able to interpolate the string, but it // won't cause any issues to let it try. return true; case "boolean": // d3 turns Booleans into integers, which we don't want. Sure, we could // interpolate from 0 -> 1, but we'd be sending a non-Boolean to // something expecting a Boolean. return false; case "object": // Don't try to interpolate class instances (except Date or Array). return obj instanceof Date || Array.isArray(obj) || isPlainObject(obj); case "function": // Careful! There may be extra properties on function objects that the // component expects to access - for instance, it may be a `d3.scale()` // function, which has its own methods attached. We don't know if the // component is only going to call the function (in which case it's // safely interpolatable) or if it's going to access special properties // (in which case our function generated from `interpolateFunction` will // most likely cause an error). We could check for enumerable properties // on the function object here to see if it's a "plain" function, but // let's just require that components prevent such function props from // being animated in the first place. return true; } } return false; }; /** * Interpolate immediately to the end value at the given step `when`. * Some nicer default behavior might be to jump at the halfway point or return * `a` if `t` is 0 (instead of always returning `b`). But d3's default * interpolator does not do these things: * * d3.interpolate('aaa', 'bbb')(0) === 'bbb' * * ...and things might get wonky if we don't replicate that behavior. * * @param {any} a - Start value. * @param {any} b - End value. * @param {Number} when - Step value (0 to 1) at which to jump to `b`. * @returns {Function} An interpolation function. */ export const interpolateImmediate = function (a, b, when = 0) { return function (t) { return t < when ? a : b; }; }; /** * Interpolate to or from a function. The interpolated value will be a function * that calls `a` (if it's a function) and `b` (if it's a function) and calls * `d3.interpolate` on the resulting values. Note that our function won't * necessarily be called (that's up to the component this eventually gets * passed to) - but if it does get called, it will return an appropriately * interpolated value. * * @param {any} a - Start value. * @param {any} b - End value. * @returns {Function} An interpolation function. */ export const interpolateFunction = function (a, b) { return function (t) { if (t >= 1) { return b; } return function (this: unknown) { /* eslint-disable prefer-rest-params */ const aval = typeof a === "function" ? a.apply(this, arguments) : a; const bval = typeof b === "function" ? b.apply(this, arguments) : b; return interpolate(aval, bval)(t); }; }; }; /** * Interpolate to or from an object. This method is a modification of the object interpolator in * d3-interpolate https://github.com/d3/d3-interpolate/blob/master/src/object.js. This interpolator * differs in that it uses our custom interpolators when interpolating the value of each property in * an object. This allows the correct interpolation of nested objects, including styles * * @param {any} startValue - Start value. * @param {any} endValue - End value. * @returns {Function} An interpolation function. */ export const interpolateObject = function (startValue, endValue) { const interpolateTypes = (x, y) => { if (x === y || !isInterpolatable(x) || !isInterpolatable(y)) { return interpolateImmediate(x, y); } if (typeof x === "function" || typeof y === "function") { return interpolateFunction(x, y); } if ( (typeof x === "object" && isPlainObject(x)) || (typeof y === "object" && isPlainObject(y)) ) { return interpolateObject(x, y); } return interpolate(x, y); }; // When the value is an array, attempt to sort by "key" so that animating nodes may be identified // based on "key" instead of index const keyData = (val) => { return Array.isArray(val) ? orderBy(val, "key") : val; }; const i = {}; const c = {}; let a = startValue; let b = endValue; let k; if (a === null || typeof a !== "object") { a = {}; } if (b === null || typeof b !== "object") { b = {}; } for (k in b) { if (k in a) { i[k] = interpolateTypes(keyData(a[k]), keyData(b[k])); } else { c[k] = b[k]; } } return function (t) { for (k in i) { c[k] = i[k](t); } return c; }; }; export const interpolateString = function (a, b) { const format = (val) => { return typeof val === "string" ? val.replace(/,/g, "") : val; }; return interpolate(format(a), format(b)); }; /** * By default, the list of interpolators used by `d3.interpolate` has a few * downsides: * * - `null` values get turned into 0. * - `undefined`, `function`, and some other value types get turned into NaN. * - Boolean types get turned into numbers, which probably will be meaningless * to whatever is consuming them. * - It tries to interpolate between identical start and end values, doing * unnecessary calculations that sometimes result in floating point rounding * errors. * * If only the default interpolators are used, `VictoryAnimation` will happily * pass down NaN (and other bad) values as props to the wrapped component. * The component will then either use the incorrect values or complain that it * was passed props of the incorrect type. This custom interpolator is added * using the `d3.interpolators` API, and prevents such cases from happening * for most values. * * @param {any} a - Start value. * @param {any} b - End value. * @returns {Function|undefined} An interpolation function, if necessary. */ export const victoryInterpolator = function <T>(a: T, b: T): (t: number) => T { // If the values are strictly equal, or either value is not interpolatable, // just use either the start value `a` or end value `b` at every step, as // there is no reasonable in-between value. if (a === b || !isInterpolatable(a) || !isInterpolatable(b)) { return interpolateImmediate(a, b); } if (typeof a === "function" || typeof b === "function") { return interpolateFunction(a, b); } if (isPlainObject(a) || isPlainObject(b)) { // @ts-expect-error These generics are tough, but they work :) return interpolateObject(a, b); } if (typeof a === "string" || typeof b === "string") { return interpolateString(a, b); } // @ts-expect-error These generics are tough, but they work :) return interpolate(a, b); };