From 2689600a9c51e4530d5333a21f288ab7b2443f85 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 5 Dec 2016 15:35:27 -0500 Subject: [PATCH 1/3] Start refactoring heatmap/contour into reusable parts --- src/traces/contour/constants.js | 39 +++ src/traces/contour/find_all_paths.js | 268 ++++++++++++++++++ src/traces/contour/make_crossings.js | 90 ++++++ src/traces/contour/plot.js | 365 +------------------------ src/traces/heatmap/calc.js | 319 +-------------------- src/traces/heatmap/clean_data.js | 44 +++ src/traces/heatmap/find_empties.js | 104 +++++++ src/traces/heatmap/interp2d.js | 128 +++++++++ src/traces/heatmap/make_bound_array.js | 79 ++++++ 9 files changed, 759 insertions(+), 677 deletions(-) create mode 100644 src/traces/contour/constants.js create mode 100644 src/traces/contour/find_all_paths.js create mode 100644 src/traces/contour/make_crossings.js create mode 100644 src/traces/heatmap/clean_data.js create mode 100644 src/traces/heatmap/find_empties.js create mode 100644 src/traces/heatmap/interp2d.js create mode 100644 src/traces/heatmap/make_bound_array.js diff --git a/src/traces/contour/constants.js b/src/traces/contour/constants.js new file mode 100644 index 00000000000..9a73be77740 --- /dev/null +++ b/src/traces/contour/constants.js @@ -0,0 +1,39 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +// some constants to help with marching squares algorithm +// where does the path start for each index? +module.exports.BOTTOMSTART = [1, 9, 13, 104, 713]; +module.exports.TOPSTART = [4, 6, 7, 104, 713]; +module.exports.LEFTSTART = [8, 12, 14, 208, 1114]; +module.exports.RIGHTSTART = [2, 3, 11, 208, 1114]; + +// which way [dx,dy] do we leave a given index? +// saddles are already disambiguated +module.exports.NEWDELTA = [ + null, [-1, 0], [0, -1], [-1, 0], + [1, 0], null, [0, -1], [-1, 0], + [0, 1], [0, 1], null, [0, 1], + [1, 0], [1, 0], [0, -1] +]; + +// for each saddle, the first index here is used +// for dx||dy<0, the second for dx||dy>0 +module.exports.CHOOSESADDLE = { + 104: [4, 1], + 208: [2, 8], + 713: [7, 13], + 1114: [11, 14] +}; + +// after one index has been used for a saddle, which do we +// substitute to be used up later? +module.exports.SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; + diff --git a/src/traces/contour/find_all_paths.js b/src/traces/contour/find_all_paths.js new file mode 100644 index 00000000000..e09a0ad0d08 --- /dev/null +++ b/src/traces/contour/find_all_paths.js @@ -0,0 +1,268 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var constants = require('./constants'); + +module.exports = function findAllPaths(pathinfo) { + var cnt, + startLoc, + i, + pi, + j; + + for(i = 0; i < pathinfo.length; i++) { + pi = pathinfo[i]; + + for(j = 0; j < pi.starts.length; j++) { + startLoc = pi.starts[j]; + makePath(pi, startLoc, 'edge'); + } + + cnt = 0; + while(Object.keys(pi.crossings).length && cnt < 10000) { + cnt++; + startLoc = Object.keys(pi.crossings)[0].split(',').map(Number); + makePath(pi, startLoc); + } + if(cnt === 10000) Lib.log('Infinite loop in contour?'); + } +} + +function equalPts(pt1, pt2) { + return Math.abs(pt1[0] - pt2[0]) < 0.01 && + Math.abs(pt1[1] - pt2[1]) < 0.01; +} + +function ptDist(pt1, pt2) { + var dx = pt1[0] - pt2[0], + dy = pt1[1] - pt2[1]; + return Math.sqrt(dx * dx + dy * dy); +} + +function makePath(pi, loc, edgeflag) { + var startLocStr = loc.join(','), + locStr = startLocStr, + mi = pi.crossings[locStr], + marchStep = startStep(mi, edgeflag, loc), + // start by going backward a half step and finding the crossing point + pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], + startStepStr = marchStep.join(','), + m = pi.z.length, + n = pi.z[0].length, + cnt; + + // now follow the path + for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops + if(mi > 20) { + mi = constants.CHOOSESADDLE[mi][(marchStep[0] || marchStep[1]) < 0 ? 0 : 1]; + pi.crossings[locStr] = constants.SADDLEREMAINDER[mi]; + } + else { + delete pi.crossings[locStr]; + } + + marchStep = constants.NEWDELTA[mi]; + if(!marchStep) { + Lib.log('Found bad marching index:', mi, loc, pi.level); + break; + } + + // find the crossing a half step forward, and then take the full step + pts.push(getInterpPx(pi, loc, marchStep)); + loc[0] += marchStep[0]; + loc[1] += marchStep[1]; + + // don't include the same point multiple times + if(equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop(); + locStr = loc.join(','); + + // have we completed a loop, or reached an edge? + if((locStr === startLocStr && marchStep.join(',') === startStepStr) || + (edgeflag && ( + (marchStep[0] && (loc[0] < 0 || loc[0] > n - 2)) || + (marchStep[1] && (loc[1] < 0 || loc[1] > m - 2))))) { + break; + } + mi = pi.crossings[locStr]; + } + + if(cnt === 10000) { + Lib.log('Infinite loop in contour?'); + } + var closedpath = equalPts(pts[0], pts[pts.length - 1]), + totaldist = 0, + distThresholdFactor = 0.2 * pi.smoothing, + alldists = [], + cropstart = 0, + distgroup, + cnt2, + cnt3, + newpt, + ptcnt, + ptavg, + thisdist; + + // check for points that are too close together (<1/5 the average dist, + // less if less smoothed) and just take the center (or avg of center 2) + // this cuts down on funny behavior when a point is very close to a contour level + for(cnt = 1; cnt < pts.length; cnt++) { + thisdist = ptDist(pts[cnt], pts[cnt - 1]); + totaldist += thisdist; + alldists.push(thisdist); + } + + var distThreshold = totaldist / alldists.length * distThresholdFactor; + + function getpt(i) { return pts[i % pts.length]; } + + for(cnt = pts.length - 2; cnt >= cropstart; cnt--) { + distgroup = alldists[cnt]; + if(distgroup < distThreshold) { + cnt3 = 0; + for(cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) { + if(distgroup + alldists[cnt2] < distThreshold) { + distgroup += alldists[cnt2]; + } + else break; + } + + // closed path with close points wrapping around the boundary? + if(closedpath && cnt === pts.length - 2) { + for(cnt3 = 0; cnt3 < cnt2; cnt3++) { + if(distgroup + alldists[cnt3] < distThreshold) { + distgroup += alldists[cnt3]; + } + else break; + } + } + ptcnt = cnt - cnt2 + cnt3 + 1; + ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2); + + // either endpoint included: keep the endpoint + if(!closedpath && cnt === pts.length - 2) newpt = pts[pts.length - 1]; + else if(!closedpath && cnt2 === -1) newpt = pts[0]; + + // odd # of points - just take the central one + else if(ptcnt % 2) newpt = getpt(ptavg); + + // even # of pts - average central two + else { + newpt = [(getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2, + (getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2]; + } + + pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt); + cnt = cnt2 + 1; + if(cnt3) cropstart = cnt3; + if(closedpath) { + if(cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1]; + else if(cnt === 0) pts[pts.length - 1] = pts[0]; + } + } + } + pts.splice(0, cropstart); + + // don't return single-point paths (ie all points were the same + // so they got deleted?) + if(pts.length < 2) return; + else if(closedpath) { + pts.pop(); + pi.paths.push(pts); + } + else { + if(!edgeflag) { + Lib.log('Unclosed interior contour?', + pi.level, startLocStr, pts.join('L')); + } + + // edge path - does it start where an existing edge path ends, or vice versa? + var merged = false; + pi.edgepaths.forEach(function(edgepath, edgei) { + if(!merged && equalPts(edgepath[0], pts[pts.length - 1])) { + pts.pop(); + merged = true; + + // now does it ALSO meet the end of another (or the same) path? + var doublemerged = false; + pi.edgepaths.forEach(function(edgepath2, edgei2) { + if(!doublemerged && equalPts( + edgepath2[edgepath2.length - 1], pts[0])) { + doublemerged = true; + pts.splice(0, 1); + pi.edgepaths.splice(edgei, 1); + if(edgei2 === edgei) { + // the path is now closed + pi.paths.push(pts.concat(edgepath2)); + } + else { + pi.edgepaths[edgei2] = + pi.edgepaths[edgei2].concat(pts, edgepath2); + } + } + }); + if(!doublemerged) { + pi.edgepaths[edgei] = pts.concat(edgepath); + } + } + }); + pi.edgepaths.forEach(function(edgepath, edgei) { + if(!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) { + pts.splice(0, 1); + pi.edgepaths[edgei] = edgepath.concat(pts); + merged = true; + } + }); + + if(!merged) pi.edgepaths.push(pts); + } +} + +// special function to get the marching step of the +// first point in the path (leading to loc) +function startStep(mi, edgeflag, loc) { + var dx = 0, + dy = 0; + if(mi > 20 && edgeflag) { + // these saddles start at +/- x + if(mi === 208 || mi === 1114) { + // if we're starting at the left side, we must be going right + dx = loc[0] === 0 ? 1 : -1; + } + else { + // if we're starting at the bottom, we must be going up + dy = loc[1] === 0 ? 1 : -1; + } + } + else if(constants.BOTTOMSTART.indexOf(mi) !== -1) dy = 1; + else if(constants.LEFTSTART.indexOf(mi) !== -1) dx = 1; + else if(constants.TOPSTART.indexOf(mi) !== -1) dy = -1; + else dx = -1; + return [dx, dy]; +} + +function getInterpPx(pi, loc, step) { + var locx = loc[0] + Math.max(step[0], 0), + locy = loc[1] + Math.max(step[1], 0), + zxy = pi.z[locy][locx], + xa = pi.xaxis, + ya = pi.yaxis; + + if(step[1]) { + var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); + return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), + ya.c2p(pi.y[locy], true)]; + } + else { + var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy); + return [xa.c2p(pi.x[locx], true), + ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)]; + } +} + diff --git a/src/traces/contour/make_crossings.js b/src/traces/contour/make_crossings.js new file mode 100644 index 00000000000..35705c015e9 --- /dev/null +++ b/src/traces/contour/make_crossings.js @@ -0,0 +1,90 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var constants = require('./constants'); + +// Calculate all the marching indices, for ALL levels at once. +// since we want to be exhaustive we'll check for contour crossings +// at every intersection, rather than just following a path +// TODO: shorten the inner loop to only the relevant levels +module.exports = function makeCrossings(pathinfo) { + var z = pathinfo[0].z, + m = z.length, + n = z[0].length, // we already made sure z isn't ragged in interp2d + twoWide = m === 2 || n === 2, + xi, + yi, + startIndices, + ystartIndices, + label, + corners, + mi, + pi, + i; + + for(yi = 0; yi < m - 1; yi++) { + ystartIndices = []; + if(yi === 0) ystartIndices = ystartIndices.concat(constants.BOTTOMSTART); + if(yi === m - 2) ystartIndices = ystartIndices.concat(constants.TOPSTART); + + for(xi = 0; xi < n - 1; xi++) { + startIndices = ystartIndices.slice(); + if(xi === 0) startIndices = startIndices.concat(constants.LEFTSTART); + if(xi === n - 2) startIndices = startIndices.concat(constants.RIGHTSTART); + + label = xi + ',' + yi; + corners = [[z[yi][xi], z[yi][xi + 1]], + [z[yi + 1][xi], z[yi + 1][xi + 1]]]; + for(i = 0; i < pathinfo.length; i++) { + pi = pathinfo[i]; + mi = getMarchingIndex(pi.level, corners); + if(!mi) continue; + + pi.crossings[label] = mi; + if(startIndices.indexOf(mi) !== -1) { + pi.starts.push([xi, yi]); + if(twoWide && startIndices.indexOf(mi, + startIndices.indexOf(mi) + 1) !== -1) { + // the same square has starts from opposite sides + // it's not possible to have starts on opposite edges + // of a corner, only a start and an end... + // but if the array is only two points wide (either way) + // you can have starts on opposite sides. + pi.starts.push([xi, yi]); + } + } + } + } + } +} + +// modified marching squares algorithm, +// so we disambiguate the saddle points from the start +// and we ignore the cases with no crossings +// the index I'm using is based on: +// http://en.wikipedia.org/wiki/Marching_squares +// except that the saddles bifurcate and I represent them +// as the decimal combination of the two appropriate +// non-saddle indices +function getMarchingIndex(val, corners) { + var mi = (corners[0][0] > val ? 0 : 1) + + (corners[0][1] > val ? 0 : 2) + + (corners[1][1] > val ? 0 : 4) + + (corners[1][0] > val ? 0 : 8); + if(mi === 5 || mi === 10) { + var avg = (corners[0][0] + corners[0][1] + + corners[1][0] + corners[1][1]) / 4; + // two peaks with a big valley + if(val > avg) return (mi === 5) ? 713 : 1114; + // two valleys with a big ridge + return (mi === 5) ? 104 : 208; + } + return (mi === 15) ? 0 : mi; +} diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index 15471d82eba..2e3d54faecd 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -15,6 +15,8 @@ var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); var heatmapPlot = require('../heatmap/plot'); +var makeCrossings = require('./make_crossings'); +var findAllPaths = require('./find_all_paths'); module.exports = function plot(gd, plotinfo, cdcontours) { @@ -23,33 +25,6 @@ module.exports = function plot(gd, plotinfo, cdcontours) { } }; -// some constants to help with marching squares algorithm - // where does the path start for each index? -var BOTTOMSTART = [1, 9, 13, 104, 713], - TOPSTART = [4, 6, 7, 104, 713], - LEFTSTART = [8, 12, 14, 208, 1114], - RIGHTSTART = [2, 3, 11, 208, 1114], - - // which way [dx,dy] do we leave a given index? - // saddles are already disambiguated - NEWDELTA = [ - null, [-1, 0], [0, -1], [-1, 0], - [1, 0], null, [0, -1], [-1, 0], - [0, 1], [0, 1], null, [0, 1], - [1, 0], [1, 0], [0, -1] - ], - // for each saddle, the first index here is used - // for dx||dy<0, the second for dx||dy>0 - CHOOSESADDLE = { - 104: [4, 1], - 208: [2, 8], - 713: [7, 13], - 1114: [11, 14] - }, - // after one index has been used for a saddle, which do we - // substitute to be used up later? - SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; - function plotOne(gd, plotinfo, cd) { var trace = cd[0].trace, x = cd[0].x, @@ -131,342 +106,6 @@ function emptyPathinfo(contours, plotinfo, cd0) { } return pathinfo; } - -// modified marching squares algorithm, -// so we disambiguate the saddle points from the start -// and we ignore the cases with no crossings -// the index I'm using is based on: -// http://en.wikipedia.org/wiki/Marching_squares -// except that the saddles bifurcate and I represent them -// as the decimal combination of the two appropriate -// non-saddle indices -function getMarchingIndex(val, corners) { - var mi = (corners[0][0] > val ? 0 : 1) + - (corners[0][1] > val ? 0 : 2) + - (corners[1][1] > val ? 0 : 4) + - (corners[1][0] > val ? 0 : 8); - if(mi === 5 || mi === 10) { - var avg = (corners[0][0] + corners[0][1] + - corners[1][0] + corners[1][1]) / 4; - // two peaks with a big valley - if(val > avg) return (mi === 5) ? 713 : 1114; - // two valleys with a big ridge - return (mi === 5) ? 104 : 208; - } - return (mi === 15) ? 0 : mi; -} - -// Calculate all the marching indices, for ALL levels at once. -// since we want to be exhaustive we'll check for contour crossings -// at every intersection, rather than just following a path -// TODO: shorten the inner loop to only the relevant levels -function makeCrossings(pathinfo) { - var z = pathinfo[0].z, - m = z.length, - n = z[0].length, // we already made sure z isn't ragged in interp2d - twoWide = m === 2 || n === 2, - xi, - yi, - startIndices, - ystartIndices, - label, - corners, - mi, - pi, - i; - - for(yi = 0; yi < m - 1; yi++) { - ystartIndices = []; - if(yi === 0) ystartIndices = ystartIndices.concat(BOTTOMSTART); - if(yi === m - 2) ystartIndices = ystartIndices.concat(TOPSTART); - - for(xi = 0; xi < n - 1; xi++) { - startIndices = ystartIndices.slice(); - if(xi === 0) startIndices = startIndices.concat(LEFTSTART); - if(xi === n - 2) startIndices = startIndices.concat(RIGHTSTART); - - label = xi + ',' + yi; - corners = [[z[yi][xi], z[yi][xi + 1]], - [z[yi + 1][xi], z[yi + 1][xi + 1]]]; - for(i = 0; i < pathinfo.length; i++) { - pi = pathinfo[i]; - mi = getMarchingIndex(pi.level, corners); - if(!mi) continue; - - pi.crossings[label] = mi; - if(startIndices.indexOf(mi) !== -1) { - pi.starts.push([xi, yi]); - if(twoWide && startIndices.indexOf(mi, - startIndices.indexOf(mi) + 1) !== -1) { - // the same square has starts from opposite sides - // it's not possible to have starts on opposite edges - // of a corner, only a start and an end... - // but if the array is only two points wide (either way) - // you can have starts on opposite sides. - pi.starts.push([xi, yi]); - } - } - } - } - } -} - -function makePath(pi, loc, edgeflag) { - var startLocStr = loc.join(','), - locStr = startLocStr, - mi = pi.crossings[locStr], - marchStep = startStep(mi, edgeflag, loc), - // start by going backward a half step and finding the crossing point - pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], - startStepStr = marchStep.join(','), - m = pi.z.length, - n = pi.z[0].length, - cnt; - - // now follow the path - for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops - if(mi > 20) { - mi = CHOOSESADDLE[mi][(marchStep[0] || marchStep[1]) < 0 ? 0 : 1]; - pi.crossings[locStr] = SADDLEREMAINDER[mi]; - } - else { - delete pi.crossings[locStr]; - } - - marchStep = NEWDELTA[mi]; - if(!marchStep) { - Lib.log('Found bad marching index:', mi, loc, pi.level); - break; - } - - // find the crossing a half step forward, and then take the full step - pts.push(getInterpPx(pi, loc, marchStep)); - loc[0] += marchStep[0]; - loc[1] += marchStep[1]; - - // don't include the same point multiple times - if(equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop(); - locStr = loc.join(','); - - // have we completed a loop, or reached an edge? - if((locStr === startLocStr && marchStep.join(',') === startStepStr) || - (edgeflag && ( - (marchStep[0] && (loc[0] < 0 || loc[0] > n - 2)) || - (marchStep[1] && (loc[1] < 0 || loc[1] > m - 2))))) { - break; - } - mi = pi.crossings[locStr]; - } - - if(cnt === 10000) { - Lib.log('Infinite loop in contour?'); - } - var closedpath = equalPts(pts[0], pts[pts.length - 1]), - totaldist = 0, - distThresholdFactor = 0.2 * pi.smoothing, - alldists = [], - cropstart = 0, - distgroup, - cnt2, - cnt3, - newpt, - ptcnt, - ptavg, - thisdist; - - // check for points that are too close together (<1/5 the average dist, - // less if less smoothed) and just take the center (or avg of center 2) - // this cuts down on funny behavior when a point is very close to a contour level - for(cnt = 1; cnt < pts.length; cnt++) { - thisdist = ptDist(pts[cnt], pts[cnt - 1]); - totaldist += thisdist; - alldists.push(thisdist); - } - - var distThreshold = totaldist / alldists.length * distThresholdFactor; - - function getpt(i) { return pts[i % pts.length]; } - - for(cnt = pts.length - 2; cnt >= cropstart; cnt--) { - distgroup = alldists[cnt]; - if(distgroup < distThreshold) { - cnt3 = 0; - for(cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) { - if(distgroup + alldists[cnt2] < distThreshold) { - distgroup += alldists[cnt2]; - } - else break; - } - - // closed path with close points wrapping around the boundary? - if(closedpath && cnt === pts.length - 2) { - for(cnt3 = 0; cnt3 < cnt2; cnt3++) { - if(distgroup + alldists[cnt3] < distThreshold) { - distgroup += alldists[cnt3]; - } - else break; - } - } - ptcnt = cnt - cnt2 + cnt3 + 1; - ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2); - - // either endpoint included: keep the endpoint - if(!closedpath && cnt === pts.length - 2) newpt = pts[pts.length - 1]; - else if(!closedpath && cnt2 === -1) newpt = pts[0]; - - // odd # of points - just take the central one - else if(ptcnt % 2) newpt = getpt(ptavg); - - // even # of pts - average central two - else { - newpt = [(getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2, - (getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2]; - } - - pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt); - cnt = cnt2 + 1; - if(cnt3) cropstart = cnt3; - if(closedpath) { - if(cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1]; - else if(cnt === 0) pts[pts.length - 1] = pts[0]; - } - } - } - pts.splice(0, cropstart); - - // don't return single-point paths (ie all points were the same - // so they got deleted?) - if(pts.length < 2) return; - else if(closedpath) { - pts.pop(); - pi.paths.push(pts); - } - else { - if(!edgeflag) { - Lib.log('Unclosed interior contour?', - pi.level, startLocStr, pts.join('L')); - } - - // edge path - does it start where an existing edge path ends, or vice versa? - var merged = false; - pi.edgepaths.forEach(function(edgepath, edgei) { - if(!merged && equalPts(edgepath[0], pts[pts.length - 1])) { - pts.pop(); - merged = true; - - // now does it ALSO meet the end of another (or the same) path? - var doublemerged = false; - pi.edgepaths.forEach(function(edgepath2, edgei2) { - if(!doublemerged && equalPts( - edgepath2[edgepath2.length - 1], pts[0])) { - doublemerged = true; - pts.splice(0, 1); - pi.edgepaths.splice(edgei, 1); - if(edgei2 === edgei) { - // the path is now closed - pi.paths.push(pts.concat(edgepath2)); - } - else { - pi.edgepaths[edgei2] = - pi.edgepaths[edgei2].concat(pts, edgepath2); - } - } - }); - if(!doublemerged) { - pi.edgepaths[edgei] = pts.concat(edgepath); - } - } - }); - pi.edgepaths.forEach(function(edgepath, edgei) { - if(!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) { - pts.splice(0, 1); - pi.edgepaths[edgei] = edgepath.concat(pts); - merged = true; - } - }); - - if(!merged) pi.edgepaths.push(pts); - } -} - -function findAllPaths(pathinfo) { - var cnt, - startLoc, - i, - pi, - j; - - for(i = 0; i < pathinfo.length; i++) { - pi = pathinfo[i]; - - for(j = 0; j < pi.starts.length; j++) { - startLoc = pi.starts[j]; - makePath(pi, startLoc, 'edge'); - } - - cnt = 0; - while(Object.keys(pi.crossings).length && cnt < 10000) { - cnt++; - startLoc = Object.keys(pi.crossings)[0].split(',').map(Number); - makePath(pi, startLoc); - } - if(cnt === 10000) Lib.log('Infinite loop in contour?'); - } -} - -// special function to get the marching step of the -// first point in the path (leading to loc) -function startStep(mi, edgeflag, loc) { - var dx = 0, - dy = 0; - if(mi > 20 && edgeflag) { - // these saddles start at +/- x - if(mi === 208 || mi === 1114) { - // if we're starting at the left side, we must be going right - dx = loc[0] === 0 ? 1 : -1; - } - else { - // if we're starting at the bottom, we must be going up - dy = loc[1] === 0 ? 1 : -1; - } - } - else if(BOTTOMSTART.indexOf(mi) !== -1) dy = 1; - else if(LEFTSTART.indexOf(mi) !== -1) dx = 1; - else if(TOPSTART.indexOf(mi) !== -1) dy = -1; - else dx = -1; - return [dx, dy]; -} - -function equalPts(pt1, pt2) { - return Math.abs(pt1[0] - pt2[0]) < 0.01 && - Math.abs(pt1[1] - pt2[1]) < 0.01; -} - -function ptDist(pt1, pt2) { - var dx = pt1[0] - pt2[0], - dy = pt1[1] - pt2[1]; - return Math.sqrt(dx * dx + dy * dy); -} - -function getInterpPx(pi, loc, step) { - var locx = loc[0] + Math.max(step[0], 0), - locy = loc[1] + Math.max(step[1], 0), - zxy = pi.z[locy][locx], - xa = pi.xaxis, - ya = pi.yaxis; - - if(step[1]) { - var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); - return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), - ya.c2p(pi.y[locy], true)]; - } - else { - var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy); - return [xa.c2p(pi.x[locx], true), - ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)]; - } -} - function makeContourGroup(plotinfo, cd, id) { var plotgroup = plotinfo.plot.select('.maplayer') .selectAll('g.contour.' + id) diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 29d878f5aee..632c1332e0d 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -9,8 +9,6 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); @@ -20,6 +18,10 @@ var colorscaleCalc = require('../../components/colorscale/calc'); var hasColumns = require('./has_columns'); var convertColumnXYZ = require('./convert_column_xyz'); var maxRowLength = require('./max_row_length'); +var cleanData = require('./clean_data'); +var interp2d = require('./interp2d'); +var findEmpties = require('./find_empties'); +var makeBoundArray = require('./make_bound_array'); module.exports = function calc(gd, trace) { @@ -64,7 +66,7 @@ module.exports = function calc(gd, trace) { y0 = trace.y0 || 0; dy = trace.dy || 1; - z = cleanZ(trace); + z = cleanData(trace.z, trace.transpose); if(isContour || trace.connectgaps) { trace._emptypoints = findEmpties(z); @@ -134,314 +136,3 @@ module.exports = function calc(gd, trace) { }; -function cleanZ(trace) { - var zOld = trace.z; - - var rowlen, collen, getCollen, old2new, i, j; - - function cleanZvalue(v) { - if(!isNumeric(v)) return undefined; - return +v; - } - - if(trace.transpose) { - rowlen = 0; - for(i = 0; i < zOld.length; i++) rowlen = Math.max(rowlen, zOld[i].length); - if(rowlen === 0) return false; - getCollen = function(zOld) { return zOld.length; }; - old2new = function(zOld, i, j) { return zOld[j][i]; }; - } - else { - rowlen = zOld.length; - getCollen = function(zOld, i) { return zOld[i].length; }; - old2new = function(zOld, i, j) { return zOld[i][j]; }; - } - - var zNew = new Array(rowlen); - - for(i = 0; i < rowlen; i++) { - collen = getCollen(zOld, i); - zNew[i] = new Array(collen); - for(j = 0; j < collen; j++) zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); - } - - return zNew; -} - -function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { - var arrayOut = [], - isContour = Registry.traceIs(trace, 'contour'), - isHist = Registry.traceIs(trace, 'histogram'), - isGL2D = Registry.traceIs(trace, 'gl2d'), - v0, - dv, - i; - - var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1; - - if(isArrayOfTwoItemsOrMore && !isHist && (ax.type !== 'category')) { - var len = arrayIn.length; - - // given vals are brick centers - // hopefully length === numbricks, but use this method even if too few are supplied - // and extend it linearly based on the last two points - if(len <= numbricks) { - // contour plots only want the centers - if(isContour || isGL2D) arrayOut = arrayIn.slice(0, numbricks); - else if(numbricks === 1) { - arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; - } - else { - arrayOut = [1.5 * arrayIn[0] - 0.5 * arrayIn[1]]; - - for(i = 1; i < len; i++) { - arrayOut.push((arrayIn[i - 1] + arrayIn[i]) * 0.5); - } - - arrayOut.push(1.5 * arrayIn[len - 1] - 0.5 * arrayIn[len - 2]); - } - - if(len < numbricks) { - var lastPt = arrayOut[arrayOut.length - 1], - delta = lastPt - arrayOut[arrayOut.length - 2]; - - for(i = len; i < numbricks; i++) { - lastPt += delta; - arrayOut.push(lastPt); - } - } - } - else { - // hopefully length === numbricks+1, but do something regardless: - // given vals are brick boundaries - return isContour ? - arrayIn.slice(0, numbricks) : // we must be strict for contours - arrayIn.slice(0, numbricks + 1); - } - } - else { - dv = dvIn || 1; - - if(isHist || ax.type === 'category') v0 = ax.r2c(v0In) || 0; - else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; - else if(v0In === undefined) v0 = 0; - else v0 = ax.d2c(v0In); - - for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { - arrayOut.push(v0 + dv * i); - } - } - - return arrayOut; -} - -var INTERPTHRESHOLD = 1e-2, - NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; - -function correctionOvershoot(maxFractionalChange) { - // start with less overshoot, until we know it's converging, - // then ramp up the overshoot for faster convergence - return 0.5 - 0.25 * Math.min(1, maxFractionalChange * 0.5); -} - -function interp2d(z, emptyPoints, savedInterpZ) { - // fill in any missing data in 2D array z using an iterative - // poisson equation solver with zero-derivative BC at edges - // amazingly, this just amounts to repeatedly averaging all the existing - // nearest neighbors (at least if we don't take x/y scaling into account) - var maxFractionalChange = 1, - i, - thisPt; - - if(Array.isArray(savedInterpZ)) { - for(i = 0; i < emptyPoints.length; i++) { - thisPt = emptyPoints[i]; - z[thisPt[0]][thisPt[1]] = savedInterpZ[thisPt[0]][thisPt[1]]; - } - } - else { - // one pass to fill in a starting value for all the empties - iterateInterp2d(z, emptyPoints); - } - - // we're don't need to iterate lone empties - remove them - for(i = 0; i < emptyPoints.length; i++) { - if(emptyPoints[i][2] < 4) break; - } - // but don't remove these points from the original array, - // we'll use them for masking, so make a copy. - emptyPoints = emptyPoints.slice(i); - - for(i = 0; i < 100 && maxFractionalChange > INTERPTHRESHOLD; i++) { - maxFractionalChange = iterateInterp2d(z, emptyPoints, - correctionOvershoot(maxFractionalChange)); - } - if(maxFractionalChange > INTERPTHRESHOLD) { - Lib.log('interp2d didn\'t converge quickly', maxFractionalChange); - } - - return z; -} - -function findEmpties(z) { - // return a list of empty points in 2D array z - // each empty point z[i][j] gives an array [i, j, neighborCount] - // neighborCount is the count of 4 nearest neighbors that DO exist - // this is to give us an order of points to evaluate for interpolation. - // if no neighbors exist, we iteratively look for neighbors that HAVE - // neighbors, and add a fractional neighborCount - var empties = [], - neighborHash = {}, - noNeighborList = [], - nextRow = z[0], - row = [], - blank = [0, 0, 0], - rowLength = maxRowLength(z), - prevRow, - i, - j, - thisPt, - p, - neighborCount, - newNeighborHash, - foundNewNeighbors; - - for(i = 0; i < z.length; i++) { - prevRow = row; - row = nextRow; - nextRow = z[i + 1] || []; - for(j = 0; j < rowLength; j++) { - if(row[j] === undefined) { - neighborCount = (row[j - 1] !== undefined ? 1 : 0) + - (row[j + 1] !== undefined ? 1 : 0) + - (prevRow[j] !== undefined ? 1 : 0) + - (nextRow[j] !== undefined ? 1 : 0); - - if(neighborCount) { - // for this purpose, don't count off-the-edge points - // as undefined neighbors - if(i === 0) neighborCount++; - if(j === 0) neighborCount++; - if(i === z.length - 1) neighborCount++; - if(j === row.length - 1) neighborCount++; - - // if all neighbors that could exist do, we don't - // need this for finding farther neighbors - if(neighborCount < 4) { - neighborHash[[i, j]] = [i, j, neighborCount]; - } - - empties.push([i, j, neighborCount]); - } - else noNeighborList.push([i, j]); - } - } - } - - while(noNeighborList.length) { - newNeighborHash = {}; - foundNewNeighbors = false; - - // look for cells that now have neighbors but didn't before - for(p = noNeighborList.length - 1; p >= 0; p--) { - thisPt = noNeighborList[p]; - i = thisPt[0]; - j = thisPt[1]; - - neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + - (neighborHash[[i + 1, j]] || blank)[2] + - (neighborHash[[i, j - 1]] || blank)[2] + - (neighborHash[[i, j + 1]] || blank)[2]) / 20; - - if(neighborCount) { - newNeighborHash[thisPt] = [i, j, neighborCount]; - noNeighborList.splice(p, 1); - foundNewNeighbors = true; - } - } - - if(!foundNewNeighbors) { - throw 'findEmpties iterated with no new neighbors'; - } - - // put these new cells into the main neighbor list - for(thisPt in newNeighborHash) { - neighborHash[thisPt] = newNeighborHash[thisPt]; - empties.push(newNeighborHash[thisPt]); - } - } - - // sort the full list in descending order of neighbor count - return empties.sort(function(a, b) { return b[2] - a[2]; }); -} - -function iterateInterp2d(z, emptyPoints, overshoot) { - var maxFractionalChange = 0, - thisPt, - i, - j, - p, - q, - neighborShift, - neighborRow, - neighborVal, - neighborCount, - neighborSum, - initialVal, - minNeighbor, - maxNeighbor; - - for(p = 0; p < emptyPoints.length; p++) { - thisPt = emptyPoints[p]; - i = thisPt[0]; - j = thisPt[1]; - initialVal = z[i][j]; - neighborSum = 0; - neighborCount = 0; - - for(q = 0; q < 4; q++) { - neighborShift = NEIGHBORSHIFTS[q]; - neighborRow = z[i + neighborShift[0]]; - if(!neighborRow) continue; - neighborVal = neighborRow[j + neighborShift[1]]; - if(neighborVal !== undefined) { - if(neighborSum === 0) { - minNeighbor = maxNeighbor = neighborVal; - } - else { - minNeighbor = Math.min(minNeighbor, neighborVal); - maxNeighbor = Math.max(maxNeighbor, neighborVal); - } - neighborCount++; - neighborSum += neighborVal; - } - } - - if(neighborCount === 0) { - throw 'iterateInterp2d order is wrong: no defined neighbors'; - } - - // this is the laplace equation interpolation: - // each point is just the average of its neighbors - // note that this ignores differential x/y scaling - // which I think is the right approach, since we - // don't know what that scaling means - z[i][j] = neighborSum / neighborCount; - - if(initialVal === undefined) { - if(neighborCount < 4) maxFractionalChange = 1; - } - else { - // we can make large empty regions converge faster - // if we overshoot the change vs the previous value - z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; - - if(maxNeighbor > minNeighbor) { - maxFractionalChange = Math.max(maxFractionalChange, - Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor)); - } - } - } - - return maxFractionalChange; -} diff --git a/src/traces/heatmap/clean_data.js b/src/traces/heatmap/clean_data.js new file mode 100644 index 00000000000..98174402f64 --- /dev/null +++ b/src/traces/heatmap/clean_data.js @@ -0,0 +1,44 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +module.exports = function cleanData(zOld, transpose) { + var rowlen, collen, getCollen, old2new, i, j; + + function cleanZvalue(v) { + if(!isNumeric(v)) return undefined; + return +v; + } + + if(transpose) { + rowlen = 0; + for(i = 0; i < zOld.length; i++) rowlen = Math.max(rowlen, zOld[i].length); + if(rowlen === 0) return false; + getCollen = function(zOld) { return zOld.length; }; + old2new = function(zOld, i, j) { return zOld[j][i]; }; + } + else { + rowlen = zOld.length; + getCollen = function(zOld, i) { return zOld[i].length; }; + old2new = function(zOld, i, j) { return zOld[i][j]; }; + } + + var zNew = new Array(rowlen); + + for(i = 0; i < rowlen; i++) { + collen = getCollen(zOld, i); + zNew[i] = new Array(collen); + for(j = 0; j < collen; j++) zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); + } + + return zNew; +} + diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js new file mode 100644 index 00000000000..caba5350538 --- /dev/null +++ b/src/traces/heatmap/find_empties.js @@ -0,0 +1,104 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var maxRowLength = require('./max_row_length'); + +module.exports = function findEmpties(z) { + // return a list of empty points in 2D array z + // each empty point z[i][j] gives an array [i, j, neighborCount] + // neighborCount is the count of 4 nearest neighbors that DO exist + // this is to give us an order of points to evaluate for interpolation. + // if no neighbors exist, we iteratively look for neighbors that HAVE + // neighbors, and add a fractional neighborCount + var empties = [], + neighborHash = {}, + noNeighborList = [], + nextRow = z[0], + row = [], + blank = [0, 0, 0], + rowLength = maxRowLength(z), + prevRow, + i, + j, + thisPt, + p, + neighborCount, + newNeighborHash, + foundNewNeighbors; + + for(i = 0; i < z.length; i++) { + prevRow = row; + row = nextRow; + nextRow = z[i + 1] || []; + for(j = 0; j < rowLength; j++) { + if(row[j] === undefined) { + neighborCount = (row[j - 1] !== undefined ? 1 : 0) + + (row[j + 1] !== undefined ? 1 : 0) + + (prevRow[j] !== undefined ? 1 : 0) + + (nextRow[j] !== undefined ? 1 : 0); + + if(neighborCount) { + // for this purpose, don't count off-the-edge points + // as undefined neighbors + if(i === 0) neighborCount++; + if(j === 0) neighborCount++; + if(i === z.length - 1) neighborCount++; + if(j === row.length - 1) neighborCount++; + + // if all neighbors that could exist do, we don't + // need this for finding farther neighbors + if(neighborCount < 4) { + neighborHash[[i, j]] = [i, j, neighborCount]; + } + + empties.push([i, j, neighborCount]); + } + else noNeighborList.push([i, j]); + } + } + } + + while(noNeighborList.length) { + newNeighborHash = {}; + foundNewNeighbors = false; + + // look for cells that now have neighbors but didn't before + for(p = noNeighborList.length - 1; p >= 0; p--) { + thisPt = noNeighborList[p]; + i = thisPt[0]; + j = thisPt[1]; + + neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + + (neighborHash[[i + 1, j]] || blank)[2] + + (neighborHash[[i, j - 1]] || blank)[2] + + (neighborHash[[i, j + 1]] || blank)[2]) / 20; + + if(neighborCount) { + newNeighborHash[thisPt] = [i, j, neighborCount]; + noNeighborList.splice(p, 1); + foundNewNeighbors = true; + } + } + + if(!foundNewNeighbors) { + throw 'findEmpties iterated with no new neighbors'; + } + + // put these new cells into the main neighbor list + for(thisPt in newNeighborHash) { + neighborHash[thisPt] = newNeighborHash[thisPt]; + empties.push(newNeighborHash[thisPt]); + } + } + + // sort the full list in descending order of neighbor count + return empties.sort(function(a, b) { return b[2] - a[2]; }); +} + diff --git a/src/traces/heatmap/interp2d.js b/src/traces/heatmap/interp2d.js new file mode 100644 index 00000000000..1fbe693d07b --- /dev/null +++ b/src/traces/heatmap/interp2d.js @@ -0,0 +1,128 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var INTERPTHRESHOLD = 1e-2, + NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + +function correctionOvershoot(maxFractionalChange) { + // start with less overshoot, until we know it's converging, + // then ramp up the overshoot for faster convergence + return 0.5 - 0.25 * Math.min(1, maxFractionalChange * 0.5); +} + +module.exports = function interp2d(z, emptyPoints, savedInterpZ) { + // fill in any missing data in 2D array z using an iterative + // poisson equation solver with zero-derivative BC at edges + // amazingly, this just amounts to repeatedly averaging all the existing + // nearest neighbors (at least if we don't take x/y scaling into account) + var maxFractionalChange = 1, + i, + thisPt; + + if(Array.isArray(savedInterpZ)) { + for(i = 0; i < emptyPoints.length; i++) { + thisPt = emptyPoints[i]; + z[thisPt[0]][thisPt[1]] = savedInterpZ[thisPt[0]][thisPt[1]]; + } + } + else { + // one pass to fill in a starting value for all the empties + iterateInterp2d(z, emptyPoints); + } + + // we're don't need to iterate lone empties - remove them + for(i = 0; i < emptyPoints.length; i++) { + if(emptyPoints[i][2] < 4) break; + } + // but don't remove these points from the original array, + // we'll use them for masking, so make a copy. + emptyPoints = emptyPoints.slice(i); + + for(i = 0; i < 100 && maxFractionalChange > INTERPTHRESHOLD; i++) { + maxFractionalChange = iterateInterp2d(z, emptyPoints, + correctionOvershoot(maxFractionalChange)); + } + if(maxFractionalChange > INTERPTHRESHOLD) { + Lib.log('interp2d didn\'t converge quickly', maxFractionalChange); + } + + return z; +} + +function iterateInterp2d(z, emptyPoints, overshoot) { + var maxFractionalChange = 0, + thisPt, + i, + j, + p, + q, + neighborShift, + neighborRow, + neighborVal, + neighborCount, + neighborSum, + initialVal, + minNeighbor, + maxNeighbor; + + for(p = 0; p < emptyPoints.length; p++) { + thisPt = emptyPoints[p]; + i = thisPt[0]; + j = thisPt[1]; + initialVal = z[i][j]; + neighborSum = 0; + neighborCount = 0; + + for(q = 0; q < 4; q++) { + neighborShift = NEIGHBORSHIFTS[q]; + neighborRow = z[i + neighborShift[0]]; + if(!neighborRow) continue; + neighborVal = neighborRow[j + neighborShift[1]]; + if(neighborVal !== undefined) { + if(neighborSum === 0) { + minNeighbor = maxNeighbor = neighborVal; + } + else { + minNeighbor = Math.min(minNeighbor, neighborVal); + maxNeighbor = Math.max(maxNeighbor, neighborVal); + } + neighborCount++; + neighborSum += neighborVal; + } + } + + if(neighborCount === 0) { + throw 'iterateInterp2d order is wrong: no defined neighbors'; + } + + // this is the laplace equation interpolation: + // each point is just the average of its neighbors + // note that this ignores differential x/y scaling + // which I think is the right approach, since we + // don't know what that scaling means + z[i][j] = neighborSum / neighborCount; + + if(initialVal === undefined) { + if(neighborCount < 4) maxFractionalChange = 1; + } + else { + // we can make large empty regions converge faster + // if we overshoot the change vs the previous value + z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; + + if(maxNeighbor > minNeighbor) { + maxFractionalChange = Math.max(maxFractionalChange, + Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor)); + } + } + } + + return maxFractionalChange; +} diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js new file mode 100644 index 00000000000..b180faddec5 --- /dev/null +++ b/src/traces/heatmap/make_bound_array.js @@ -0,0 +1,79 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Registry = require('../../registry'); + +module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { + var arrayOut = [], + isContour = Registry.traceIs(trace, 'contour'), + isHist = Registry.traceIs(trace, 'histogram'), + isGL2D = Registry.traceIs(trace, 'gl2d'), + v0, + dv, + i; + + var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1; + + if(isArrayOfTwoItemsOrMore && !isHist && (ax.type !== 'category')) { + var len = arrayIn.length; + + // given vals are brick centers + // hopefully length === numbricks, but use this method even if too few are supplied + // and extend it linearly based on the last two points + if(len <= numbricks) { + // contour plots only want the centers + if(isContour || isGL2D) arrayOut = arrayIn.slice(0, numbricks); + else if(numbricks === 1) { + arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; + } + else { + arrayOut = [1.5 * arrayIn[0] - 0.5 * arrayIn[1]]; + + for(i = 1; i < len; i++) { + arrayOut.push((arrayIn[i - 1] + arrayIn[i]) * 0.5); + } + + arrayOut.push(1.5 * arrayIn[len - 1] - 0.5 * arrayIn[len - 2]); + } + + if(len < numbricks) { + var lastPt = arrayOut[arrayOut.length - 1], + delta = lastPt - arrayOut[arrayOut.length - 2]; + + for(i = len; i < numbricks; i++) { + lastPt += delta; + arrayOut.push(lastPt); + } + } + } + else { + // hopefully length === numbricks+1, but do something regardless: + // given vals are brick boundaries + return isContour ? + arrayIn.slice(0, numbricks) : // we must be strict for contours + arrayIn.slice(0, numbricks + 1); + } + } + else { + dv = dvIn || 1; + + if(isHist || ax.type === 'category') v0 = ax.r2c(v0In) || 0; + else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; + else if(v0In === undefined) v0 = 0; + else v0 = ax.d2c(v0In); + + for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { + arrayOut.push(v0 + dv * i); + } + } + + return arrayOut; +} + From af9c98339762dd9ae914a4a1874f6dc2c8691067 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 5 Dec 2016 15:37:18 -0500 Subject: [PATCH 2/3] Fix lint errors and missing Lib requires --- src/traces/contour/constants.js | 1 - src/traces/contour/find_all_paths.js | 4 ++-- src/traces/contour/make_crossings.js | 2 +- src/traces/heatmap/calc.js | 2 -- src/traces/heatmap/clean_data.js | 3 +-- src/traces/heatmap/find_empties.js | 3 +-- src/traces/heatmap/interp2d.js | 4 +++- src/traces/heatmap/make_bound_array.js | 3 +-- 8 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/traces/contour/constants.js b/src/traces/contour/constants.js index 9a73be77740..60e711e2bf9 100644 --- a/src/traces/contour/constants.js +++ b/src/traces/contour/constants.js @@ -36,4 +36,3 @@ module.exports.CHOOSESADDLE = { // after one index has been used for a saddle, which do we // substitute to be used up later? module.exports.SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; - diff --git a/src/traces/contour/find_all_paths.js b/src/traces/contour/find_all_paths.js index e09a0ad0d08..6d42912fc46 100644 --- a/src/traces/contour/find_all_paths.js +++ b/src/traces/contour/find_all_paths.js @@ -8,6 +8,7 @@ 'use strict'; +var Lib = require('../../lib'); var constants = require('./constants'); module.exports = function findAllPaths(pathinfo) { @@ -33,7 +34,7 @@ module.exports = function findAllPaths(pathinfo) { } if(cnt === 10000) Lib.log('Infinite loop in contour?'); } -} +}; function equalPts(pt1, pt2) { return Math.abs(pt1[0] - pt2[0]) < 0.01 && @@ -265,4 +266,3 @@ function getInterpPx(pi, loc, step) { ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)]; } } - diff --git a/src/traces/contour/make_crossings.js b/src/traces/contour/make_crossings.js index 35705c015e9..ec87b20d499 100644 --- a/src/traces/contour/make_crossings.js +++ b/src/traces/contour/make_crossings.js @@ -63,7 +63,7 @@ module.exports = function makeCrossings(pathinfo) { } } } -} +}; // modified marching squares algorithm, // so we disambiguate the saddle points from the start diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 632c1332e0d..31809ac4424 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -134,5 +134,3 @@ module.exports = function calc(gd, trace) { return [cd0]; }; - - diff --git a/src/traces/heatmap/clean_data.js b/src/traces/heatmap/clean_data.js index 98174402f64..d80f297a9f7 100644 --- a/src/traces/heatmap/clean_data.js +++ b/src/traces/heatmap/clean_data.js @@ -40,5 +40,4 @@ module.exports = function cleanData(zOld, transpose) { } return zNew; -} - +}; diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js index caba5350538..5d8ec269ed8 100644 --- a/src/traces/heatmap/find_empties.js +++ b/src/traces/heatmap/find_empties.js @@ -100,5 +100,4 @@ module.exports = function findEmpties(z) { // sort the full list in descending order of neighbor count return empties.sort(function(a, b) { return b[2] - a[2]; }); -} - +}; diff --git a/src/traces/heatmap/interp2d.js b/src/traces/heatmap/interp2d.js index 1fbe693d07b..817e0294eae 100644 --- a/src/traces/heatmap/interp2d.js +++ b/src/traces/heatmap/interp2d.js @@ -8,6 +8,8 @@ 'use strict'; +var Lib = require('../../lib'); + var INTERPTHRESHOLD = 1e-2, NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; @@ -54,7 +56,7 @@ module.exports = function interp2d(z, emptyPoints, savedInterpZ) { } return z; -} +}; function iterateInterp2d(z, emptyPoints, overshoot) { var maxFractionalChange = 0, diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index b180faddec5..42f09489a24 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -75,5 +75,4 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, } return arrayOut; -} - +}; From 016090a38271f993859ffaa8c6566e03c8c40f00 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 7 Dec 2016 17:32:53 -0500 Subject: [PATCH 3/3] Small modifications and cleanup to heatmap reorg --- src/traces/heatmap/calc.js | 4 ++-- .../heatmap/{clean_data.js => clean_2d_array.js} | 2 +- src/traces/heatmap/find_empties.js | 13 +++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) rename src/traces/heatmap/{clean_data.js => clean_2d_array.js} (95%) diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 31809ac4424..1428406404b 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -18,7 +18,7 @@ var colorscaleCalc = require('../../components/colorscale/calc'); var hasColumns = require('./has_columns'); var convertColumnXYZ = require('./convert_column_xyz'); var maxRowLength = require('./max_row_length'); -var cleanData = require('./clean_data'); +var clean2dArray = require('./clean_2d_array'); var interp2d = require('./interp2d'); var findEmpties = require('./find_empties'); var makeBoundArray = require('./make_bound_array'); @@ -66,7 +66,7 @@ module.exports = function calc(gd, trace) { y0 = trace.y0 || 0; dy = trace.dy || 1; - z = cleanData(trace.z, trace.transpose); + z = clean2dArray(trace.z, trace.transpose); if(isContour || trace.connectgaps) { trace._emptypoints = findEmpties(z); diff --git a/src/traces/heatmap/clean_data.js b/src/traces/heatmap/clean_2d_array.js similarity index 95% rename from src/traces/heatmap/clean_data.js rename to src/traces/heatmap/clean_2d_array.js index d80f297a9f7..ab12737a581 100644 --- a/src/traces/heatmap/clean_data.js +++ b/src/traces/heatmap/clean_2d_array.js @@ -10,7 +10,7 @@ var isNumeric = require('fast-isnumeric'); -module.exports = function cleanData(zOld, transpose) { +module.exports = function clean2dArray(zOld, transpose) { var rowlen, collen, getCollen, old2new, i, j; function cleanZvalue(v) { diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js index 5d8ec269ed8..65b148dc652 100644 --- a/src/traces/heatmap/find_empties.js +++ b/src/traces/heatmap/find_empties.js @@ -10,13 +10,14 @@ var maxRowLength = require('./max_row_length'); +/* Return a list of empty points in 2D array z + * each empty point z[i][j] gives an array [i, j, neighborCount] + * neighborCount is the count of 4 nearest neighbors that DO exist + * this is to give us an order of points to evaluate for interpolation. + * if no neighbors exist, we iteratively look for neighbors that HAVE + * neighbors, and add a fractional neighborCount + */ module.exports = function findEmpties(z) { - // return a list of empty points in 2D array z - // each empty point z[i][j] gives an array [i, j, neighborCount] - // neighborCount is the count of 4 nearest neighbors that DO exist - // this is to give us an order of points to evaluate for interpolation. - // if no neighbors exist, we iteratively look for neighbors that HAVE - // neighbors, and add a fractional neighborCount var empties = [], neighborHash = {}, noNeighborList = [],