d3 / d3-interpolate

Interpolate numbers, colors, strings, arrays, objects, whatever!

Home Page:https://d3js.org/d3-interpolate

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Exclude keys from interpolation?

mcnuttandrew opened this issue · comments

In some situations interpolateObject does more work than is desired. For instance, if I have an object like {x: 1, y: 2, label: 'red'} that interpolates to {x: 3, y: 4, label: 'red'}. I want it to come out of interpolate as exactly {x: 3, y: 4, label: 'red'}, however, because interpolateObject tries to interpolate all of the values in the object the output of this interpolation tends to be {x: 3, y: 4, label: 'rgb(255, 0, 0)'}.

Is there a way to exclude specific keys from the interpolation? Or do I need to do some sort of recombination after the interpolation? Given the design goal of this function to be the fast inner part of a loop for animation, needing to modify the data after interpolation is less than ideal.

I’d probably write a custom interpolator that just interpolates the properties (e.g., x and y) that you want interpolated. You could then use Object.create (or equivalently create a shallow copy of b) to have the return value inherit any other values that you do not want interpolated.

function interpolateXY(a, b) {
  var x = interpolate(a.x, b.x), // Or interpolateNumber?
      y = interpolate(a.y, b.y), // Or interpolateNumber?
      c = Object.create(b);
  return function(t) {
    return c.x = x(t), c.y = y(t), c;
  };
}

I think that strategy is perfectly reasonable, but it doesn't feel very general. I guess I would like a programmatic way to describe values that I don't want to be interpreted, something like

function modifiableInterpolation(a, b, ignoreableKeys) {
  const interpolators = Object.keys(a)
    .filter(key => !ignoreableKeys.find(key))
    .reduce((acc, key) => {
      if (typeof a[key] === 'object') {
        acc[key] = modifiableInterpolation(a[key], b[key], ignoreableKeys);
      } else {
        acc[key] = interpolate(a[key], b[key]);
      }
      return acc;
    }, {});
  const c = Object.create(b);
  return t => {
    Object.keys(interpolators).forEach(intpKey => {
      c[intpKey] = interpolators[intpKey](t);
    });
    return c;
  };
}

This has the added benefit that if I am interpolating against a deeply nested structure then I can ignore the interpolations on say, the labels on the leaves.

You could do something by copying the implementation of d3.interpolateObject and d3.interpolate:

function interpolateIgnoreKeys(ignore) {

  function interpolateObject(a, b) {
    var i = {},
        c = {},
        k;

    if (a === null || typeof a !== "object") a = {};
    if (b === null || typeof b !== "object") b = {};

    for (k in b) {
      if ((k in a) && !(k in ignore)) {
        i[k] = interpolate(a[k], b[k]);
      } else {
        c[k] = b[k];
      }
    }

    return function(t) {
      for (k in i) c[k] = i[k](t);
      return c;
    };
  }

  function interpolate(a, b) {
    var t = typeof b, c;
    return b == null || t === "boolean" ? constant(b)
        : (t === "number" ? interpolateNumber
        : t === "string" ? ((c = interpolateColor(b)) ? (b = c, rgb) : interpolateString)
        : b instanceof color ? interpolateRgb
        : b instanceof Date ? interpolateDate
        : Array.isArray(b) ? interpolateArray
        : typeof b.valueOf !== "function" && typeof b.toString !== "function" || isNaN(b) ? interpolateObject
        : interpolateNumber)(a, b);
  }

  return interpolate;
}

That seems like a pretty reasonable strategy! Do you think it would be appropriate to add this into d3-interpolate? I know i've run into this a few times, and I bet other folks might have as well. If so, i'd be delighted to file a PR.

It feels a little too specific to me, as-is; I haven’t seen other anecdotal evidence to suggest it’s a common use case. That said, I could imagine a more general function which is like, give me a d3.interpolate, but instead of using d3.interpolateObject for objects, use my function instead. And similarly, you might be able to override the interpolator for other types (like numbers or dates or whatever). So that could eliminate the need to copy the d3.interpolate implementation as above, but you’d still be responsible for implementing the substitute for d3.interpolateObject.