victory-animation
Version:
animation wrapper for victory components
133 lines (126 loc) • 5.16 kB
JavaScript
import _ from "lodash";
import d3Interpolate from "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) && _.isFinite(obj);
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 _.isDate(obj) || _.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 () {
/* eslint-disable no-invalid-this */
const aval = (typeof a === "function") ? a.apply(this, arguments) : a;
const bval = (typeof b === "function") ? b.apply(this, arguments) : b;
return d3Interpolate.value(aval, bval)(t);
};
};
};
/**
* 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 (a, b) {
// 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);
}
};
let interpolatorAdded = false;
export const addVictoryInterpolator = function () {
if (!interpolatorAdded) {
d3Interpolate.values.push(victoryInterpolator);
interpolatorAdded = true;
}
};