Skip to content

Commit 2689600

Browse files
committed
Start refactoring heatmap/contour into reusable parts
1 parent 2dbdf07 commit 2689600

File tree

9 files changed

+759
-677
lines changed

9 files changed

+759
-677
lines changed

src/traces/contour/constants.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
// some constants to help with marching squares algorithm
12+
// where does the path start for each index?
13+
module.exports.BOTTOMSTART = [1, 9, 13, 104, 713];
14+
module.exports.TOPSTART = [4, 6, 7, 104, 713];
15+
module.exports.LEFTSTART = [8, 12, 14, 208, 1114];
16+
module.exports.RIGHTSTART = [2, 3, 11, 208, 1114];
17+
18+
// which way [dx,dy] do we leave a given index?
19+
// saddles are already disambiguated
20+
module.exports.NEWDELTA = [
21+
null, [-1, 0], [0, -1], [-1, 0],
22+
[1, 0], null, [0, -1], [-1, 0],
23+
[0, 1], [0, 1], null, [0, 1],
24+
[1, 0], [1, 0], [0, -1]
25+
];
26+
27+
// for each saddle, the first index here is used
28+
// for dx||dy<0, the second for dx||dy>0
29+
module.exports.CHOOSESADDLE = {
30+
104: [4, 1],
31+
208: [2, 8],
32+
713: [7, 13],
33+
1114: [11, 14]
34+
};
35+
36+
// after one index has been used for a saddle, which do we
37+
// substitute to be used up later?
38+
module.exports.SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11};
39+

src/traces/contour/find_all_paths.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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+
var constants = require('./constants');
12+
13+
module.exports = function findAllPaths(pathinfo) {
14+
var cnt,
15+
startLoc,
16+
i,
17+
pi,
18+
j;
19+
20+
for(i = 0; i < pathinfo.length; i++) {
21+
pi = pathinfo[i];
22+
23+
for(j = 0; j < pi.starts.length; j++) {
24+
startLoc = pi.starts[j];
25+
makePath(pi, startLoc, 'edge');
26+
}
27+
28+
cnt = 0;
29+
while(Object.keys(pi.crossings).length && cnt < 10000) {
30+
cnt++;
31+
startLoc = Object.keys(pi.crossings)[0].split(',').map(Number);
32+
makePath(pi, startLoc);
33+
}
34+
if(cnt === 10000) Lib.log('Infinite loop in contour?');
35+
}
36+
}
37+
38+
function equalPts(pt1, pt2) {
39+
return Math.abs(pt1[0] - pt2[0]) < 0.01 &&
40+
Math.abs(pt1[1] - pt2[1]) < 0.01;
41+
}
42+
43+
function ptDist(pt1, pt2) {
44+
var dx = pt1[0] - pt2[0],
45+
dy = pt1[1] - pt2[1];
46+
return Math.sqrt(dx * dx + dy * dy);
47+
}
48+
49+
function makePath(pi, loc, edgeflag) {
50+
var startLocStr = loc.join(','),
51+
locStr = startLocStr,
52+
mi = pi.crossings[locStr],
53+
marchStep = startStep(mi, edgeflag, loc),
54+
// start by going backward a half step and finding the crossing point
55+
pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])],
56+
startStepStr = marchStep.join(','),
57+
m = pi.z.length,
58+
n = pi.z[0].length,
59+
cnt;
60+
61+
// now follow the path
62+
for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops
63+
if(mi > 20) {
64+
mi = constants.CHOOSESADDLE[mi][(marchStep[0] || marchStep[1]) < 0 ? 0 : 1];
65+
pi.crossings[locStr] = constants.SADDLEREMAINDER[mi];
66+
}
67+
else {
68+
delete pi.crossings[locStr];
69+
}
70+
71+
marchStep = constants.NEWDELTA[mi];
72+
if(!marchStep) {
73+
Lib.log('Found bad marching index:', mi, loc, pi.level);
74+
break;
75+
}
76+
77+
// find the crossing a half step forward, and then take the full step
78+
pts.push(getInterpPx(pi, loc, marchStep));
79+
loc[0] += marchStep[0];
80+
loc[1] += marchStep[1];
81+
82+
// don't include the same point multiple times
83+
if(equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop();
84+
locStr = loc.join(',');
85+
86+
// have we completed a loop, or reached an edge?
87+
if((locStr === startLocStr && marchStep.join(',') === startStepStr) ||
88+
(edgeflag && (
89+
(marchStep[0] && (loc[0] < 0 || loc[0] > n - 2)) ||
90+
(marchStep[1] && (loc[1] < 0 || loc[1] > m - 2))))) {
91+
break;
92+
}
93+
mi = pi.crossings[locStr];
94+
}
95+
96+
if(cnt === 10000) {
97+
Lib.log('Infinite loop in contour?');
98+
}
99+
var closedpath = equalPts(pts[0], pts[pts.length - 1]),
100+
totaldist = 0,
101+
distThresholdFactor = 0.2 * pi.smoothing,
102+
alldists = [],
103+
cropstart = 0,
104+
distgroup,
105+
cnt2,
106+
cnt3,
107+
newpt,
108+
ptcnt,
109+
ptavg,
110+
thisdist;
111+
112+
// check for points that are too close together (<1/5 the average dist,
113+
// less if less smoothed) and just take the center (or avg of center 2)
114+
// this cuts down on funny behavior when a point is very close to a contour level
115+
for(cnt = 1; cnt < pts.length; cnt++) {
116+
thisdist = ptDist(pts[cnt], pts[cnt - 1]);
117+
totaldist += thisdist;
118+
alldists.push(thisdist);
119+
}
120+
121+
var distThreshold = totaldist / alldists.length * distThresholdFactor;
122+
123+
function getpt(i) { return pts[i % pts.length]; }
124+
125+
for(cnt = pts.length - 2; cnt >= cropstart; cnt--) {
126+
distgroup = alldists[cnt];
127+
if(distgroup < distThreshold) {
128+
cnt3 = 0;
129+
for(cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) {
130+
if(distgroup + alldists[cnt2] < distThreshold) {
131+
distgroup += alldists[cnt2];
132+
}
133+
else break;
134+
}
135+
136+
// closed path with close points wrapping around the boundary?
137+
if(closedpath && cnt === pts.length - 2) {
138+
for(cnt3 = 0; cnt3 < cnt2; cnt3++) {
139+
if(distgroup + alldists[cnt3] < distThreshold) {
140+
distgroup += alldists[cnt3];
141+
}
142+
else break;
143+
}
144+
}
145+
ptcnt = cnt - cnt2 + cnt3 + 1;
146+
ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2);
147+
148+
// either endpoint included: keep the endpoint
149+
if(!closedpath && cnt === pts.length - 2) newpt = pts[pts.length - 1];
150+
else if(!closedpath && cnt2 === -1) newpt = pts[0];
151+
152+
// odd # of points - just take the central one
153+
else if(ptcnt % 2) newpt = getpt(ptavg);
154+
155+
// even # of pts - average central two
156+
else {
157+
newpt = [(getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2,
158+
(getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2];
159+
}
160+
161+
pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt);
162+
cnt = cnt2 + 1;
163+
if(cnt3) cropstart = cnt3;
164+
if(closedpath) {
165+
if(cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1];
166+
else if(cnt === 0) pts[pts.length - 1] = pts[0];
167+
}
168+
}
169+
}
170+
pts.splice(0, cropstart);
171+
172+
// don't return single-point paths (ie all points were the same
173+
// so they got deleted?)
174+
if(pts.length < 2) return;
175+
else if(closedpath) {
176+
pts.pop();
177+
pi.paths.push(pts);
178+
}
179+
else {
180+
if(!edgeflag) {
181+
Lib.log('Unclosed interior contour?',
182+
pi.level, startLocStr, pts.join('L'));
183+
}
184+
185+
// edge path - does it start where an existing edge path ends, or vice versa?
186+
var merged = false;
187+
pi.edgepaths.forEach(function(edgepath, edgei) {
188+
if(!merged && equalPts(edgepath[0], pts[pts.length - 1])) {
189+
pts.pop();
190+
merged = true;
191+
192+
// now does it ALSO meet the end of another (or the same) path?
193+
var doublemerged = false;
194+
pi.edgepaths.forEach(function(edgepath2, edgei2) {
195+
if(!doublemerged && equalPts(
196+
edgepath2[edgepath2.length - 1], pts[0])) {
197+
doublemerged = true;
198+
pts.splice(0, 1);
199+
pi.edgepaths.splice(edgei, 1);
200+
if(edgei2 === edgei) {
201+
// the path is now closed
202+
pi.paths.push(pts.concat(edgepath2));
203+
}
204+
else {
205+
pi.edgepaths[edgei2] =
206+
pi.edgepaths[edgei2].concat(pts, edgepath2);
207+
}
208+
}
209+
});
210+
if(!doublemerged) {
211+
pi.edgepaths[edgei] = pts.concat(edgepath);
212+
}
213+
}
214+
});
215+
pi.edgepaths.forEach(function(edgepath, edgei) {
216+
if(!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) {
217+
pts.splice(0, 1);
218+
pi.edgepaths[edgei] = edgepath.concat(pts);
219+
merged = true;
220+
}
221+
});
222+
223+
if(!merged) pi.edgepaths.push(pts);
224+
}
225+
}
226+
227+
// special function to get the marching step of the
228+
// first point in the path (leading to loc)
229+
function startStep(mi, edgeflag, loc) {
230+
var dx = 0,
231+
dy = 0;
232+
if(mi > 20 && edgeflag) {
233+
// these saddles start at +/- x
234+
if(mi === 208 || mi === 1114) {
235+
// if we're starting at the left side, we must be going right
236+
dx = loc[0] === 0 ? 1 : -1;
237+
}
238+
else {
239+
// if we're starting at the bottom, we must be going up
240+
dy = loc[1] === 0 ? 1 : -1;
241+
}
242+
}
243+
else if(constants.BOTTOMSTART.indexOf(mi) !== -1) dy = 1;
244+
else if(constants.LEFTSTART.indexOf(mi) !== -1) dx = 1;
245+
else if(constants.TOPSTART.indexOf(mi) !== -1) dy = -1;
246+
else dx = -1;
247+
return [dx, dy];
248+
}
249+
250+
function getInterpPx(pi, loc, step) {
251+
var locx = loc[0] + Math.max(step[0], 0),
252+
locy = loc[1] + Math.max(step[1], 0),
253+
zxy = pi.z[locy][locx],
254+
xa = pi.xaxis,
255+
ya = pi.yaxis;
256+
257+
if(step[1]) {
258+
var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy);
259+
return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true),
260+
ya.c2p(pi.y[locy], true)];
261+
}
262+
else {
263+
var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy);
264+
return [xa.c2p(pi.x[locx], true),
265+
ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)];
266+
}
267+
}
268+

src/traces/contour/make_crossings.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
var constants = require('./constants');
12+
13+
// Calculate all the marching indices, for ALL levels at once.
14+
// since we want to be exhaustive we'll check for contour crossings
15+
// at every intersection, rather than just following a path
16+
// TODO: shorten the inner loop to only the relevant levels
17+
module.exports = function makeCrossings(pathinfo) {
18+
var z = pathinfo[0].z,
19+
m = z.length,
20+
n = z[0].length, // we already made sure z isn't ragged in interp2d
21+
twoWide = m === 2 || n === 2,
22+
xi,
23+
yi,
24+
startIndices,
25+
ystartIndices,
26+
label,
27+
corners,
28+
mi,
29+
pi,
30+
i;
31+
32+
for(yi = 0; yi < m - 1; yi++) {
33+
ystartIndices = [];
34+
if(yi === 0) ystartIndices = ystartIndices.concat(constants.BOTTOMSTART);
35+
if(yi === m - 2) ystartIndices = ystartIndices.concat(constants.TOPSTART);
36+
37+
for(xi = 0; xi < n - 1; xi++) {
38+
startIndices = ystartIndices.slice();
39+
if(xi === 0) startIndices = startIndices.concat(constants.LEFTSTART);
40+
if(xi === n - 2) startIndices = startIndices.concat(constants.RIGHTSTART);
41+
42+
label = xi + ',' + yi;
43+
corners = [[z[yi][xi], z[yi][xi + 1]],
44+
[z[yi + 1][xi], z[yi + 1][xi + 1]]];
45+
for(i = 0; i < pathinfo.length; i++) {
46+
pi = pathinfo[i];
47+
mi = getMarchingIndex(pi.level, corners);
48+
if(!mi) continue;
49+
50+
pi.crossings[label] = mi;
51+
if(startIndices.indexOf(mi) !== -1) {
52+
pi.starts.push([xi, yi]);
53+
if(twoWide && startIndices.indexOf(mi,
54+
startIndices.indexOf(mi) + 1) !== -1) {
55+
// the same square has starts from opposite sides
56+
// it's not possible to have starts on opposite edges
57+
// of a corner, only a start and an end...
58+
// but if the array is only two points wide (either way)
59+
// you can have starts on opposite sides.
60+
pi.starts.push([xi, yi]);
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
67+
68+
// modified marching squares algorithm,
69+
// so we disambiguate the saddle points from the start
70+
// and we ignore the cases with no crossings
71+
// the index I'm using is based on:
72+
// http://en.wikipedia.org/wiki/Marching_squares
73+
// except that the saddles bifurcate and I represent them
74+
// as the decimal combination of the two appropriate
75+
// non-saddle indices
76+
function getMarchingIndex(val, corners) {
77+
var mi = (corners[0][0] > val ? 0 : 1) +
78+
(corners[0][1] > val ? 0 : 2) +
79+
(corners[1][1] > val ? 0 : 4) +
80+
(corners[1][0] > val ? 0 : 8);
81+
if(mi === 5 || mi === 10) {
82+
var avg = (corners[0][0] + corners[0][1] +
83+
corners[1][0] + corners[1][1]) / 4;
84+
// two peaks with a big valley
85+
if(val > avg) return (mi === 5) ? 713 : 1114;
86+
// two valleys with a big ridge
87+
return (mi === 5) ? 104 : 208;
88+
}
89+
return (mi === 15) ? 0 : mi;
90+
}

0 commit comments

Comments
 (0)