plotly.js
Version:
The open source javascript graphing library that powers plotly
274 lines (228 loc) • 9.53 kB
JavaScript
var constants = require('./constants');
var search = require('../../lib/search').findBin;
var computeControlPoints = require('./compute_control_points');
var createSplineEvaluator = require('./create_spline_evaluator');
var createIDerivativeEvaluator = require('./create_i_derivative_evaluator');
var createJDerivativeEvaluator = require('./create_j_derivative_evaluator');
/*
* Create conversion functions to go from one basis to another. In particular the letter
* abbreviations are:
*
* i: i/j coordinates along the grid. Integer values correspond to data points
* a: real-valued coordinates along the a/b axes
* c: cartesian x-y coordinates
* p: screen-space pixel coordinates
*/
module.exports = function setConvert(trace) {
var a = trace._a;
var b = trace._b;
var na = a.length;
var nb = b.length;
var aax = trace.aaxis;
var bax = trace.baxis;
// Grab the limits once rather than recomputing the bounds for every point
// independently:
var amin = a[0];
var amax = a[na - 1];
var bmin = b[0];
var bmax = b[nb - 1];
var arange = a[a.length - 1] - a[0];
var brange = b[b.length - 1] - b[0];
// Compute the tolerance so that points are visible slightly outside the
// defined carpet axis:
var atol = arange * constants.RELATIVE_CULL_TOLERANCE;
var btol = brange * constants.RELATIVE_CULL_TOLERANCE;
// Expand the limits to include the relative tolerance:
amin -= atol;
amax += atol;
bmin -= btol;
bmax += btol;
trace.isVisible = function(a, b) {
return a > amin && a < amax && b > bmin && b < bmax;
};
trace.isOccluded = function(a, b) {
return a < amin || a > amax || b < bmin || b > bmax;
};
trace.setScale = function() {
var x = trace._x;
var y = trace._y;
// This is potentially a very expensive step! It does the bulk of the work of constructing
// an expanded basis of control points. Note in particular that it overwrites the existing
// basis without creating a new array since that would potentially thrash the garbage
// collector.
var result = computeControlPoints(trace._xctrl, trace._yctrl, x, y, aax.smoothing, bax.smoothing);
trace._xctrl = result[0];
trace._yctrl = result[1];
// This step is the second step in the process, but it's somewhat simpler. It just unrolls
// some logic since it would be unnecessarily expensive to compute both interpolations
// nearly identically but separately and to include a bunch of linear vs. bicubic logic in
// every single call.
trace.evalxy = createSplineEvaluator([trace._xctrl, trace._yctrl], na, nb, aax.smoothing, bax.smoothing);
trace.dxydi = createIDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing);
trace.dxydj = createJDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing);
};
/*
* Convert from i/j data grid coordinates to a/b values. Note in particular that this
* is *linear* interpolation, even if the data is interpolated bicubically.
*/
trace.i2a = function(i) {
var i0 = Math.max(0, Math.floor(i[0]), na - 2);
var ti = i[0] - i0;
return (1 - ti) * a[i0] + ti * a[i0 + 1];
};
trace.j2b = function(j) {
var j0 = Math.max(0, Math.floor(j[1]), na - 2);
var tj = j[1] - j0;
return (1 - tj) * b[j0] + tj * b[j0 + 1];
};
trace.ij2ab = function(ij) {
return [trace.i2a(ij[0]), trace.j2b(ij[1])];
};
/*
* Convert from a/b coordinates to i/j grid-numbered coordinates. This requires searching
* through the a/b data arrays and assumes they are monotonic, which is presumed to have
* been enforced already.
*/
trace.a2i = function(aval) {
var i0 = Math.max(0, Math.min(search(aval, a), na - 2));
var a0 = a[i0];
var a1 = a[i0 + 1];
return Math.max(0, Math.min(na - 1, i0 + (aval - a0) / (a1 - a0)));
};
trace.b2j = function(bval) {
var j0 = Math.max(0, Math.min(search(bval, b), nb - 2));
var b0 = b[j0];
var b1 = b[j0 + 1];
return Math.max(0, Math.min(nb - 1, j0 + (bval - b0) / (b1 - b0)));
};
trace.ab2ij = function(ab) {
return [trace.a2i(ab[0]), trace.b2j(ab[1])];
};
/*
* Convert from i/j coordinates to x/y caretesian coordinates. This means either bilinear
* or bicubic spline evaluation, but the hard part is already done at this point.
*/
trace.i2c = function(i, j) {
return trace.evalxy([], i, j);
};
trace.ab2xy = function(aval, bval, extrapolate) {
if(!extrapolate && (aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1])) {
return [false, false];
}
var i = trace.a2i(aval);
var j = trace.b2j(bval);
var pt = trace.evalxy([], i, j);
if(extrapolate) {
// This section uses the boundary derivatives to extrapolate linearly outside
// the defined range. Consider a scatter line with one point inside the carpet
// axis and one point outside. If we don't extrapolate, we can't draw the line
// at all.
var iex = 0;
var jex = 0;
var der = [];
var i0, ti, j0, tj;
if(aval < a[0]) {
i0 = 0;
ti = 0;
iex = (aval - a[0]) / (a[1] - a[0]);
} else if(aval > a[na - 1]) {
i0 = na - 2;
ti = 1;
iex = (aval - a[na - 1]) / (a[na - 1] - a[na - 2]);
} else {
i0 = Math.max(0, Math.min(na - 2, Math.floor(i)));
ti = i - i0;
}
if(bval < b[0]) {
j0 = 0;
tj = 0;
jex = (bval - b[0]) / (b[1] - b[0]);
} else if(bval > b[nb - 1]) {
j0 = nb - 2;
tj = 1;
jex = (bval - b[nb - 1]) / (b[nb - 1] - b[nb - 2]);
} else {
j0 = Math.max(0, Math.min(nb - 2, Math.floor(j)));
tj = j - j0;
}
if(iex) {
trace.dxydi(der, i0, j0, ti, tj);
pt[0] += der[0] * iex;
pt[1] += der[1] * iex;
}
if(jex) {
trace.dxydj(der, i0, j0, ti, tj);
pt[0] += der[0] * jex;
pt[1] += der[1] * jex;
}
}
return pt;
};
trace.c2p = function(xy, xa, ya) {
return [xa.c2p(xy[0]), ya.c2p(xy[1])];
};
trace.p2x = function(p, xa, ya) {
return [xa.p2c(p[0]), ya.p2c(p[1])];
};
trace.dadi = function(i /* , u*/) {
// Right now only a piecewise linear a or b basis is permitted since smoother interpolation
// would cause monotonicity problems. As a retult, u is entirely disregarded in this
// computation, though we'll specify it as a parameter for the sake of completeness and
// future-proofing. It would be possible to use monotonic cubic interpolation, for example.
//
// See: https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
// u = u || 0;
var i0 = Math.max(0, Math.min(a.length - 2, i));
// The step (denominator) is implicitly 1 since that's the grid spacing.
return a[i0 + 1] - a[i0];
};
trace.dbdj = function(j /* , v*/) {
// See above caveats for dadi which also apply here
var j0 = Math.max(0, Math.min(b.length - 2, j));
// The step (denominator) is implicitly 1 since that's the grid spacing.
return b[j0 + 1] - b[j0];
};
// Takes: grid cell coordinate (i, j) and fractional grid cell coordinates (u, v)
// Returns: (dx/da, dy/db)
//
// NB: separate grid cell + fractional grid cell coordinate format is due to the discontinuous
// derivative, as described better in create_i_derivative_evaluator.js
trace.dxyda = function(i0, j0, u, v) {
var dxydi = trace.dxydi(null, i0, j0, u, v);
var dadi = trace.dadi(i0, u);
return [dxydi[0] / dadi, dxydi[1] / dadi];
};
trace.dxydb = function(i0, j0, u, v) {
var dxydj = trace.dxydj(null, i0, j0, u, v);
var dbdj = trace.dbdj(j0, v);
return [dxydj[0] / dbdj, dxydj[1] / dbdj];
};
// Sometimes we don't care about precision and all we really want is decent rough
// directions (as is the case with labels). In that case, we can do a very rough finite
// difference and spare having to worry about precise grid coordinates:
trace.dxyda_rough = function(a, b, reldiff) {
var h = arange * (reldiff || 0.1);
var plus = trace.ab2xy(a + h, b, true);
var minus = trace.ab2xy(a - h, b, true);
return [
(plus[0] - minus[0]) * 0.5 / h,
(plus[1] - minus[1]) * 0.5 / h
];
};
trace.dxydb_rough = function(a, b, reldiff) {
var h = brange * (reldiff || 0.1);
var plus = trace.ab2xy(a, b + h, true);
var minus = trace.ab2xy(a, b - h, true);
return [
(plus[0] - minus[0]) * 0.5 / h,
(plus[1] - minus[1]) * 0.5 / h
];
};
trace.dpdx = function(xa) {
return xa._m;
};
trace.dpdy = function(ya) {
return ya._m;
};
};
;