Skip to content

Commit cba32e9

Browse files
committed
Work out a lot of details of spline evaluation
1 parent d8b5659 commit cba32e9

14 files changed

+845
-45
lines changed

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Plotly.register([
3636
require('./scattermapbox'),
3737

3838
require('./carpet'),
39+
require('./scattercarpet'),
3940

4041
require('./ohlc'),
4142
require('./candlestick')

src/components/drawing/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func');
2424

2525
var drawing = module.exports = {};
2626

27+
drawing.splineEvaluator = require('./spline_evaluator');
28+
2729
// -----------------------------------------------------
2830
// styling functions for plot elements
2931
// -----------------------------------------------------

src/traces/carpet/axis_attributes.js

+64-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ var fontAttrs = require('../../plots/font_attributes');
1313
var colorAttrs = require('../../components/color/attributes');
1414

1515
module.exports = {
16+
smoothing: {
17+
valType: 'number',
18+
dflt: 1.0,
19+
role: 'info'
20+
},
21+
cheatertype: {
22+
valType: 'enumerated',
23+
values: ['index', 'value'],
24+
dflt: 'index',
25+
role: 'info'
26+
},
27+
tickmode: {
28+
valType: 'enumerated',
29+
values: ['linear', 'array'],
30+
dflt: 'array',
31+
role: 'info',
32+
},
1633
showlabels: {
1734
valType: 'enumerated',
1835
values: ['start', 'end', 'both', 'none'],
@@ -70,14 +87,28 @@ module.exports = {
7087
labelfont: extendFlat({}, fontAttrs, {
7188
description: 'Sets the label font.'
7289
}),
73-
gridoffset: {
90+
tick0: {
91+
valType: 'any',
92+
min: 0,
93+
dflt: 0,
94+
role: 'info',
95+
description: 'The starting index of grid lines along the axis'
96+
},
97+
dtick: {
98+
valType: 'any',
99+
min: 1,
100+
dflt: 1,
101+
role: 'info',
102+
description: 'The stride between grid lines along the axis'
103+
},
104+
arraytick0: {
74105
valType: 'integer',
75106
min: 0,
76107
dflt: 0,
77108
role: 'info',
78109
description: 'The starting index of grid lines along the axis'
79110
},
80-
gridstep: {
111+
arraydtick: {
81112
valType: 'integer',
82113
min: 1,
83114
dflt: 1,
@@ -97,19 +128,41 @@ module.exports = {
97128
role: 'style',
98129
description: 'Sets the color of the grid lines.'
99130
},
100-
minorgridoffset: {
101-
valType: 'integer',
131+
startlinewidth: {
132+
valType: 'number',
102133
min: 0,
103-
dflt: 0,
134+
dflt: 1,
135+
role: 'style',
136+
description: 'Sets the width (in px) of the grid lines.'
137+
},
138+
startline: {
139+
valType: 'boolean',
104140
role: 'info',
105-
description: 'The starting index of grid lines along the axis'
141+
dflt: true
106142
},
107-
minorgridstep: {
108-
valType: 'integer',
109-
min: 1,
110-
dflt: 1,
143+
endline: {
144+
valType: 'boolean',
111145
role: 'info',
112-
description: 'The stride between grid lines along the axis'
146+
dflt: true
147+
},
148+
startlinecolor: {
149+
valType: 'color',
150+
dflt: colorAttrs.lightLine,
151+
role: 'style',
152+
description: 'Sets the color of the grid lines.'
153+
},
154+
endlinewidth: {
155+
valType: 'number',
156+
min: 0,
157+
dflt: 1,
158+
role: 'style',
159+
description: 'Sets the width (in px) of the grid lines.'
160+
},
161+
endlinecolor: {
162+
valType: 'color',
163+
dflt: colorAttrs.lightLine,
164+
role: 'style',
165+
description: 'Sets the color of the grid lines.'
113166
},
114167
minorgridwidth: {
115168
valType: 'number',

src/traces/carpet/calc.js

+178-7
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,26 @@
1111
var Axes = require('../../plots/cartesian/axes');
1212
var cheaterBasis = require('./cheater_basis');
1313
var arrayMinmax = require('./array_minmax');
14-
14+
var search = require('../../lib/search').findBin;
15+
var computeControlPoints = require('./compute_control_points');
16+
var map2dArray = require('./map_2d_array');
17+
var evaluateControlPoints = require('./evaluate_control_points');
1518

1619
module.exports = function calc(gd, trace) {
1720
var xa = Axes.getFromId(gd, trace.xaxis || 'x'),
18-
ya = Axes.getFromId(gd, trace.yaxis || 'y');
21+
ya = Axes.getFromId(gd, trace.yaxis || 'y'),
22+
aax = trace.aaxis,
23+
bax = trace.baxis,
24+
a = trace.a,
25+
b = trace.b;
1926

2027
var xdata;
2128
var ydata = trace.y;
2229

2330
if(trace._cheater) {
24-
xdata = cheaterBasis(trace.a.length, trace.b.length, trace.cheaterslope);
31+
var avals = aax.cheatertype === 'index' ? a.length : a
32+
var bvals = bax.cheatertype === 'index' ? b.length : b;
33+
xdata = cheaterBasis(avals, bvals, trace.cheaterslope);
2534
} else {
2635
xdata = trace.x;
2736
}
@@ -44,12 +53,174 @@ module.exports = function calc(gd, trace) {
4453
Axes.expand(xa, xrange, {padded: true});
4554
Axes.expand(ya, yrange, {padded: true});
4655

56+
// Precompute the cartesian-space coordinates of the grid lines:
57+
var x = xdata;
58+
var y = trace.y;
59+
60+
// Convert cartesian-space x/y coordinates to screen space pixel coordinates:
61+
trace.xp = map2dArray(trace.xp, x, xa.c2p);
62+
trace.yp = map2dArray(trace.yp, y, ya.c2p);
63+
64+
// Next compute the control points in whichever directions are necessary:
65+
var result = computeControlPoints(trace.xctrl, trace.yctrl, x, y, aax.smoothing, bax.smoothing);
66+
trace.xctrl = result[0];
67+
trace.yctrl = result[1];
68+
69+
70+
console.log('evaluateControlPoints(:', evaluateControlPoints([trace.xctrl, trace.yctrl], 0.5, 0.5));
71+
72+
73+
//trace.ab2c = getConvert(x, y, a, b, aax.smoothing, bax.smoothing);
74+
75+
// This is just a transposed function that will be useful in making
76+
// the computations a/b-agnostic:
77+
//trace.ba2c = function (b, a) { return trace.ab2c(a, b); }
78+
79+
//var agrid = makeGridLines(a, aax);
80+
//var bgrid = makeGridLines(b, bax);
81+
82+
//trace.aaxis._tickinfo = agrid;
83+
//trace.baxis._tickinfo = bgrid;
84+
4785
var cd0 = {
48-
x: xdata,
49-
y: trace.y,
50-
a: trace.a,
51-
b: trace.b
86+
x: x,
87+
y: y,
88+
a: a,
89+
b: b
5290
};
5391

5492
return [cd0];
5593
};
94+
95+
96+
97+
/*
98+
* Interpolate in the b-direction along constant-a lines
99+
* x, y: 2D data arrays to be interpolated
100+
* aidx: index of the a gridline along which to evaluate
101+
* bidx0: lower-index of the b cell in which to interpolate
102+
* tb: an interpolant in [0, 1]
103+
*/
104+
/*function interpolateConstA (x, y, aidx, bidx0, tb, bsmoothing) {
105+
if (bsmoothing) {
106+
return [
107+
x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb,
108+
y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb
109+
];
110+
} else {
111+
return [
112+
x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb,
113+
y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb
114+
];
115+
}
116+
}*/
117+
118+
/*
119+
* Interpolate in the a and b directions
120+
* x, y: 2D data arrays to be interpolated
121+
* aidx0: lower index of the a cell in which to interpolatel
122+
* bidx0: lower index of the b cell in which to interpolate
123+
* ta: an interpolant for the a direction in [0, 1]
124+
* tb: an interpolant for the b direction in [0, 1]
125+
*/
126+
/*function interpolateAB (x, y, aidx0, ta, bidx0, tb, asmoothing, bsmoothing) {
127+
if (asmoothing) {
128+
var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing);
129+
var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing);
130+
131+
return [
132+
xy0[0] * (1 - ta) + xy1[0] * ta,
133+
xy0[1] * (1 - ta) + xy1[1] * ta
134+
];
135+
} else {
136+
var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing);
137+
var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing);
138+
139+
return [
140+
xy0[0] * (1 - ta) + xy1[0] * ta,
141+
xy0[1] * (1 - ta) + xy1[1] * ta
142+
];
143+
}
144+
}*/
145+
146+
/*
147+
* Return a function that converts a/b coordinates to x/y values
148+
*/
149+
/*function getConvert (x, y, a, b, asmoothing, bsmoothing) {
150+
return function (aval, bval) {
151+
// Get the lower-indices of the box in which the point falls:
152+
var aidx = Math.min(search(aval, a), a.length - 2);
153+
var bidx = Math.min(search(bval, b), b.length - 2);
154+
155+
var ta = (aval - a[aidx]) / (a[aidx + 1] - a[aidx]);
156+
var tb = (bval - b[bidx]) / (b[bidx + 1] - b[bidx]);
157+
158+
return interpolateAB(x, y, aidx, ta, bidx, tb, asmoothing, bsmoothing);
159+
};
160+
}*/
161+
162+
/*
163+
* Interpret the tick properties to return indices, interpolants,
164+
* and values of grid lines
165+
*
166+
* Output is:
167+
* ilower: lower index of the cell in which the gridline lies
168+
* interpolant: fractional distance of this gridline to the next cell (in [0, 1])
169+
* values: value of the axis coordinate at each respective gridline
170+
*/
171+
/*function makeGridLines (data, axis) {
172+
var gridlines = [];
173+
var t = [];
174+
var i0 = [];
175+
var i1 = [];
176+
var values = [];
177+
switch(axis.tickmode) {
178+
case 'array':
179+
var start = axis.arraytick0 % axis.arraydtick;
180+
var step = axis.arraydtick;
181+
var end = data.length;
182+
// This could be optimized, but we'll convert this to a/b space and then do
183+
// the interpolation in
184+
for (var i = start; i < end; i += step) {
185+
// If it's the end point, we'll use the end of the previous range to
186+
// avoid going out of bounds:
187+
var endshift = i >= end - 1 ? 1 : 0;
188+
189+
gridlines.push({
190+
param: i,
191+
idx: i - endshift,
192+
tInterp: endshift,
193+
value: data[i]
194+
});
195+
}
196+
break;
197+
case 'linear':
198+
var v1 = getLinspaceStartingPoint(data[0], axis.tick0, axis.dtick);
199+
var n = Math.ceil((data[data.length - 1] - v1) / axis.dtick);
200+
201+
// This could be optimized, but we'll convert this to a/b space and then do
202+
// the interpolation in
203+
for (var i = 0; i < n; i++) {
204+
var val = v1 + axis.dtick * i;
205+
var idx = Math.min(search(val, data), data.length - 2);
206+
gridlines.push({
207+
param: (val - data[0]) / (data[data.length - 1] - data[0]) * data.length,
208+
idx: idx,
209+
tInterp: (val - data[idx]) / (data[idx + 1] - data[idx]),
210+
value: val
211+
});
212+
}
213+
}
214+
215+
console.log('gridlines:', gridlines);
216+
217+
return gridlines;
218+
}*/
219+
220+
/*
221+
* Given a data range from starting at x1, this function computes the first
222+
* point distributed along x0 + n * dx that lies within the range.
223+
*/
224+
function getLinspaceStartingPoint (xlow, x0, dx) {
225+
return x0 + dx * Math.ceil((xlow - x0) / dx);
226+
}

src/traces/carpet/catmull_rom.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
/*
12+
* Compute the tangent vector according to catmull-rom cubic splines (centripetal,
13+
* I think). That differs from the control point in two ways:
14+
* 1. It is a vector, not a position relative to the point
15+
* 2. the vector is longer than the position relative to p1 by a factor of 3
16+
*
17+
* Close to the boundaries, we'll use these as *quadratic control points, so that
18+
* to make a nice grid, we'll need to divide the tangent by 2 instead of 3. (The
19+
* math works out this way if you work through the bezier derivatives)
20+
*/
21+
var CatmullRomExp = 0.5;
22+
module.exports = function makeControlPoints(p0, p1, p2, smoothness) {
23+
var d1x = p0[0] - p1[0],
24+
d1y = p0[1] - p1[1],
25+
d2x = p2[0] - p1[0],
26+
d2y = p2[1] - p1[1],
27+
d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2),
28+
d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2),
29+
numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness,
30+
numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness,
31+
denom1 = d2a * (d1a + d2a) * 3,
32+
denom2 = d1a * (d1a + d2a) * 3;
33+
return [[
34+
p1[0] + (denom1 && numx / denom1),
35+
p1[1] + (denom1 && numy / denom1)
36+
], [
37+
p1[0] - (denom2 && numx / denom2),
38+
p1[1] - (denom2 && numy / denom2)
39+
]];
40+
41+
return [[dxL, dyL], [dxU, dyU]];
42+
}
43+

0 commit comments

Comments
 (0)