Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

d3.interpolateFunction? #29

Open
phs opened this issue Sep 3, 2016 · 9 comments
Open

d3.interpolateFunction? #29

phs opened this issue Sep 3, 2016 · 9 comments

Comments

@phs
Copy link

phs commented Sep 3, 2016

I have a need to linearly deform one (unary, real domain) function onto another.

As a new d3 user I'm still learning what's in the kitchen, so to speak. Before discovering d3-interpolate I was cobbling this feature together with d3.scaleLinear. However this was falling down due to needing to construct such a scale on each interpolator call.

Doing the interpolation explicitly fixed the issue and is easy enough, but it felt a little counter to the intended style. After noticing d3-interpolate I hoped I would find such a function interpolator (homotopy) constructor, but no dice.

Could it be added?

@mbostock
Copy link
Member

mbostock commented Sep 3, 2016

Are you imagining something like d3.interpolateMeta(a, b) that takes two interpolators and returns an interpolator that linearly interpolates the output of a and b?

@phs
Copy link
Author

phs commented Sep 3, 2016

Maybe. I was looking at my hand-crafted function and thinking about how I would generalize it to allow the functions themselves to be interpolators (and so, to allow things to gel with non-number output types.)

I think it works cleanly? (i.e. without excessive object creation) But if not, I would be happy with a function that only to interpolated between unary functions from reals to reals.

@phs
Copy link
Author

phs commented Sep 3, 2016

Right, so here's a general, but possibly unperformant implementation:

function interpolateMeta(a, b) {
  return (alpha) => interpolate(a(1 - alpha), b(alpha));
}

Here we do the interpolation in the domain, since we don't want to assume the codomain is numeric. This leads directly calling the (possibly expensive?) factory on each step.

If we know the codomain is numeric, we can do the interpolation there instead. Note this won't in general result in the same interpolator as above, but that's ok: I just want a continuous, linearish deformation.

function interpolateFunction(a, b) {
  return (alpha) => (d) => (1 - alpha) * a(d) + alpha * b(d);
}

@phs
Copy link
Author

phs commented Sep 3, 2016

Err, excuse me. My interpolateMeta doesn't look that bad on performance, but it does assume the domains of a and b are in the unit interval. The thing I was afraid of was this:

function interpolateAnyDomainAndCodomain(a, b) {
  return (alpha) => (d) => interpolate(a(d), b(d))(alpha);
}

Here a and b are general functions, but the interpolate call is stuck under the (d) =>.

@phs
Copy link
Author

phs commented Sep 3, 2016

Sorry, last one. Upon third reading, my interpolateMeta is simply wrong (for example, interpolate(a, b)(0) != a.) interpolateAnyDomainAndCodomain does what was intended.

@mbostock
Copy link
Member

mbostock commented Sep 3, 2016

I was thinking this:

function interpolateMeta(a, b) {
  return (t) => interpolate(a(t), b(t))(t);
}

Which, as you point out, isn’t ideal from a performance perspective because it creates as new closure for each invocation of the meta-interpolator. This is an unfortunate consequence of the design of interpolators in this library. But, since the behavior of d3.interpolate is explicitly defined, it could use private internal methods to implement the above behavior more efficiently, after restructuring the implementation of the other interpolators, e.g.,

// A private function that interpolates directly rather than returning an interpolator.
function interpolateNumber(a, b, t) {
  return (a = +a) + (b - a) * t;
}

A more extreme option would be to have separate methods, i.e., to make the above reusable implementation public:

  • d3.interpolateNumber(a, b, t) - returns the interpolated value
  • d3.interpolatorNumber(a, b) - returns an interpolator that takes a t
  • d3.interpolateString(a, b, t) -
  • d3.interpolatorString(a, b) -
  • etc.

That might be a good long-term strategy although it’s highly inconvenient in terms of backwards-compatibility.

Technically, it doesn’t require that the domains of the two interpolators a and b are the unit interval—that’s just the convention. But it does require that they have the same domain, and that this domain is also the domain that will be used with the returned “meta” interpolator.

@mbostock
Copy link
Member

mbostock commented Sep 3, 2016

Oh also, as far as the dynamically-typed nature of d3.interpolate goes, the behavior of d3.interpolateMeta could be defined such that the first time the returned interpolator is invoked, it determines the type of interpolator to use subsequently based on the return value of b(t). So if b(t) is a string, then it henceforth uses d3.interpolateString every time (even if subsequently the behavior of b changes—though in practice it’s hard to imagine a good reason for an interpolator to return inconsistent types).

@mbostock
Copy link
Member

mbostock commented Sep 3, 2016

Lastly if you could elaborate on some practical use cases for this feature it would be helpful in establishing motivation. Thanks!

@phs
Copy link
Author

phs commented Sep 3, 2016

The motivation is visually intuitive non-linear transforms of unit square patches (linear transforms are already easy directly with SVG.)

The unit square restriction may look odd, but it makes what comes next easier. Such patches are also easy to make from arbitrary rectangles using the existing d3.scale* functions.

So as an example, imagine transforming a time-series line graph (given by function d) by mapping its base and top lines onto a pair of arbitrary paths (a and b), such as those made by d3.line or d3.radialLine. Assuming I've already scaled d by sending both its domain and range to unit intervals, what should I do to transform it onto the patch defined by a and b?

I think one answer is:

function transformedD(u) {
  return interpolate(a(u), b(u))(d(u));
}

For our given u, we see where both a and b land, and choosing a point on the resulting line segment that is d(u) of the way towards the b end (which is where we sent the top line.)

Let's see if we can tease the bits apart a little.

function interpolateMeta(a, b) {
  return (u) => (t) => interpolate(a(u), b(u))(t);
}

var transform = interpolateMeta(a, b);
var transformedDPrime = (v) => transform(v)(d(v));

Make sure I'm not nuts:

transformedDPrime
(v) => transform(v)(d(v))                                    // subst transformedDPrime
(v) => interpolateMeta(a, b)(v)(d(v))                        // subst transform
(v) => ( (u) => (t) => interpolate(a(u), b(u))(t) )(v)(d(v)) // eval interpolateMeta(a, b)
(v) => ( (t) => interpolate(a(v), b(v))(t) )(d(v))           // beta
(v) => interpolate(a(v), b(v))(d(v))                         // beta
(u) => interpolate(a(u), b(u))(d(u))                         // alpha
transformedD                                                 // unsubst transformedD

I get the feeling we can play with binder order to avoid making closures, but I need to run for the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants