Open phs opened 8 years ago
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?
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.
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);
}
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) =>
.
Sorry, last one. Upon third reading, my interpolateMeta
is simply wrong (for example, interpolate(a, b)(0) != a
.) interpolateAnyDomainAndCodomain
does what was intended.
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:
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.
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).
Lastly if you could elaborate on some practical use cases for this feature it would be helpful in establishing motivation. Thanks!
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.
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 withd3.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?