'use strict'; var Drawing = require('../../components/drawing'); var numConstants = require('../../constants/numerical'); var BADNUM = numConstants.BADNUM; var LOG_CLIP = numConstants.LOG_CLIP; var LOG_CLIP_PLUS = LOG_CLIP + 0.5; var LOG_CLIP_MINUS = LOG_CLIP - 0.5; var Lib = require('../../lib'); var segmentsIntersect = Lib.segmentsIntersect; var constrain = Lib.constrain; var constants = require('./constants'); module.exports = function linePoints(d, opts) { var trace = opts.trace || {}; var xa = opts.xaxis; var ya = opts.yaxis; var xLog = xa.type === 'log'; var yLog = ya.type === 'log'; var xLen = xa._length; var yLen = ya._length; var backoff = opts.backoff; var marker = trace.marker; var connectGaps = opts.connectGaps; var baseTolerance = opts.baseTolerance; var shape = opts.shape; var linear = shape === 'linear'; var fill = trace.fill && trace.fill !== 'none'; var segments = []; var minTolerance = constants.minTolerance; var len = d.length; var pts = new Array(len); var pti = 0; var i; // pt variables are pixel coordinates [x,y] of one point // these four are the outputs of clustering on a line var clusterStartPt, clusterEndPt, clusterHighPt, clusterLowPt; // "this" is the next point we're considering adding to the cluster var thisPt; // did we encounter the high point first, then a low point, or vice versa? var clusterHighFirst; // the first two points in the cluster determine its unit vector // so the second is always in the "High" direction var clusterUnitVector; // the pixel delta from clusterStartPt var thisVector; // val variables are (signed) pixel distances along the cluster vector var clusterRefDist, clusterHighVal, clusterLowVal, thisVal; // deviation variables are (signed) pixel distances normal to the cluster vector var clusterMinDeviation, clusterMaxDeviation, thisDeviation; // turn one calcdata point into pixel coordinates function getPt(index) { var di = d[index]; if(!di) return false; var x = opts.linearized ? xa.l2p(di.x) : xa.c2p(di.x); var y = opts.linearized ? ya.l2p(di.y) : ya.c2p(di.y); // if non-positive log values, set them VERY far off-screen // so the line looks essentially straight from the previous point. if(x === BADNUM) { if(xLog) x = xa.c2p(di.x, true); if(x === BADNUM) return false; // If BOTH were bad log values, make the line follow a constant // exponent rather than a constant slope if(yLog && y === BADNUM) { x *= Math.abs(xa._m * yLen * (xa._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS) / (ya._m * xLen * (ya._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS))); } x *= 1000; } if(y === BADNUM) { if(yLog) y = ya.c2p(di.y, true); if(y === BADNUM) return false; y *= 1000; } return [x, y]; } function crossesViewport(xFrac0, yFrac0, xFrac1, yFrac1) { var dx = xFrac1 - xFrac0; var dy = yFrac1 - yFrac0; var dx0 = 0.5 - xFrac0; var dy0 = 0.5 - yFrac0; var norm2 = dx * dx + dy * dy; var dot = dx * dx0 + dy * dy0; if(dot > 0 && dot < norm2) { var cross = dx0 * dy - dy0 * dx; if(cross * cross < norm2) return true; } } var latestXFrac, latestYFrac; // if we're off-screen, increase tolerance over baseTolerance function getTolerance(pt, nextPt) { var xFrac = pt[0] / xLen; var yFrac = pt[1] / yLen; var offScreenFraction = Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1); if(offScreenFraction && (latestXFrac !== undefined) && crossesViewport(xFrac, yFrac, latestXFrac, latestYFrac) ) { offScreenFraction = 0; } if(offScreenFraction && nextPt && crossesViewport(xFrac, yFrac, nextPt[0] / xLen, nextPt[1] / yLen) ) { offScreenFraction = 0; } return (1 + constants.toleranceGrowth * offScreenFraction) * baseTolerance; } function ptDist(pt1, pt2) { var dx = pt1[0] - pt2[0]; var dy = pt1[1] - pt2[1]; return Math.sqrt(dx * dx + dy * dy); } // last bit of filtering: clip paths that are VERY far off-screen // so we don't get near the browser's hard limit (+/- 2^29 px in Chrome and FF) var maxScreensAway = constants.maxScreensAway; // find the intersections between the segment from pt1 to pt2 // and the large rectangle maxScreensAway around the viewport // if one of pt1 and pt2 is inside and the other outside, there // will be only one intersection. // if both are outside there will be 0 or 2 intersections // (or 1 if it's right at a corner - we'll treat that like 0) // returns an array of intersection pts var xEdge0 = -xLen * maxScreensAway; var xEdge1 = xLen * (1 + maxScreensAway); var yEdge0 = -yLen * maxScreensAway; var yEdge1 = yLen * (1 + maxScreensAway); var edges = [ [xEdge0, yEdge0, xEdge1, yEdge0], [xEdge1, yEdge0, xEdge1, yEdge1], [xEdge1, yEdge1, xEdge0, yEdge1], [xEdge0, yEdge1, xEdge0, yEdge0] ]; var xEdge, yEdge, lastXEdge, lastYEdge, lastFarPt, edgePt; // for linear line shape, edge intersections should be linearly interpolated // spline uses this too, which isn't precisely correct but is actually pretty // good, because Catmull-Rom weights far-away points less in creating the curvature function getLinearEdgeIntersections(pt1, pt2) { var out = []; var ptCount = 0; for(var i = 0; i < 4; i++) { var edge = edges[i]; var ptInt = segmentsIntersect( pt1[0], pt1[1], pt2[0], pt2[1], edge[0], edge[1], edge[2], edge[3] ); if(ptInt && (!ptCount || Math.abs(ptInt.x - out[0][0]) > 1 || Math.abs(ptInt.y - out[0][1]) > 1 )) { ptInt = [ptInt.x, ptInt.y]; // if we have 2 intersections, make sure the closest one to pt1 comes first if(ptCount && ptDist(ptInt, pt1) < ptDist(out[0], pt1)) out.unshift(ptInt); else out.push(ptInt); ptCount++; } } return out; } function onlyConstrainedPoint(pt) { if(pt[0] < xEdge0 || pt[0] > xEdge1 || pt[1] < yEdge0 || pt[1] > yEdge1) { return [constrain(pt[0], xEdge0, xEdge1), constrain(pt[1], yEdge0, yEdge1)]; } } function sameEdge(pt1, pt2) { if(pt1[0] === pt2[0] && (pt1[0] === xEdge0 || pt1[0] === xEdge1)) return true; if(pt1[1] === pt2[1] && (pt1[1] === yEdge0 || pt1[1] === yEdge1)) return true; } // for line shapes hv and vh, movement in the two dimensions is decoupled, // so all we need to do is constrain each dimension independently function getHVEdgeIntersections(pt1, pt2) { var out = []; var ptInt1 = onlyConstrainedPoint(pt1); var ptInt2 = onlyConstrainedPoint(pt2); if(ptInt1 && ptInt2 && sameEdge(ptInt1, ptInt2)) return out; if(ptInt1) out.push(ptInt1); if(ptInt2) out.push(ptInt2); return out; } // hvh and vhv we sometimes have to move one of the intersection points // out BEYOND the clipping rect, by a maximum of a factor of 2, so that // the midpoint line is drawn in the right place function getABAEdgeIntersections(dim, limit0, limit1) { return function(pt1, pt2) { var ptInt1 = onlyConstrainedPoint(pt1); var ptInt2 = onlyConstrainedPoint(pt2); var out = []; if(ptInt1 && ptInt2 && sameEdge(ptInt1, ptInt2)) return out; if(ptInt1) out.push(ptInt1); if(ptInt2) out.push(ptInt2); var midShift = 2 * Lib.constrain((pt1[dim] + pt2[dim]) / 2, limit0, limit1) - ((ptInt1 || pt1)[dim] + (ptInt2 || pt2)[dim]); if(midShift) { var ptToAlter; if(ptInt1 && ptInt2) { ptToAlter = (midShift > 0 === ptInt1[dim] > ptInt2[dim]) ? ptInt1 : ptInt2; } else ptToAlter = ptInt1 || ptInt2; ptToAlter[dim] += midShift; } return out; }; } var getEdgeIntersections; if(shape === 'linear' || shape === 'spline') { getEdgeIntersections = getLinearEdgeIntersections; } else if(shape === 'hv' || shape === 'vh') { getEdgeIntersections = getHVEdgeIntersections; } else if(shape === 'hvh') getEdgeIntersections = getABAEdgeIntersections(0, xEdge0, xEdge1); else if(shape === 'vhv') getEdgeIntersections = getABAEdgeIntersections(1, yEdge0, yEdge1); // a segment pt1->pt2 entirely outside the nearby region: // find the corner it gets closest to touching function getClosestCorner(pt1, pt2) { var dx = pt2[0] - pt1[0]; var m = (pt2[1] - pt1[1]) / dx; var b = (pt1[1] * pt2[0] - pt2[1] * pt1[0]) / dx; if(b > 0) return [m > 0 ? xEdge0 : xEdge1, yEdge1]; else return [m > 0 ? xEdge1 : xEdge0, yEdge0]; } function updateEdge(pt) { var x = pt[0]; var y = pt[1]; var xSame = x === pts[pti - 1][0]; var ySame = y === pts[pti - 1][1]; // duplicate point? if(xSame && ySame) return; if(pti > 1) { // backtracking along an edge? var xSame2 = x === pts[pti - 2][0]; var ySame2 = y === pts[pti - 2][1]; if(xSame && (x === xEdge0 || x === xEdge1) && xSame2) { if(ySame2) pti--; // backtracking exactly - drop prev pt and don't add else pts[pti - 1] = pt; // not exact: replace the prev pt } else if(ySame && (y === yEdge0 || y === yEdge1) && ySame2) { if(xSame2) pti--; else pts[pti - 1] = pt; } else pts[pti++] = pt; } else pts[pti++] = pt; } function updateEdgesForReentry(pt) { // if we're outside the nearby region and going back in, // we may need to loop around a corner point if(pts[pti - 1][0] !== pt[0] && pts[pti - 1][1] !== pt[1]) { updateEdge([lastXEdge, lastYEdge]); } updateEdge(pt); lastFarPt = null; lastXEdge = lastYEdge = 0; } var arrayMarker = Lib.isArrayOrTypedArray(marker); function addPt(pt) { if(pt && backoff) { pt.i = i; pt.d = d; pt.trace = trace; pt.marker = arrayMarker ? marker[pt.i] : marker; pt.backoff = backoff; } latestXFrac = pt[0] / xLen; latestYFrac = pt[1] / yLen; // Are we more than maxScreensAway off-screen any direction? // if so, clip to this box, but in such a way that on-screen // drawing is unchanged xEdge = (pt[0] < xEdge0) ? xEdge0 : (pt[0] > xEdge1) ? xEdge1 : 0; yEdge = (pt[1] < yEdge0) ? yEdge0 : (pt[1] > yEdge1) ? yEdge1 : 0; if(xEdge || yEdge) { if(!pti) { // to get fills right - if first point is far, push it toward the // screen in whichever direction(s) are far pts[pti++] = [xEdge || pt[0], yEdge || pt[1]]; } else if(lastFarPt) { // both this point and the last are outside the nearby region // check if we're crossing the nearby region var intersections = getEdgeIntersections(lastFarPt, pt); if(intersections.length > 1) { updateEdgesForReentry(intersections[0]); pts[pti++] = intersections[1]; } } else { // we're leaving the nearby region - add the point where we left it edgePt = getEdgeIntersections(pts[pti - 1], pt)[0]; pts[pti++] = edgePt; } var lastPt = pts[pti - 1]; if(xEdge && yEdge && (lastPt[0] !== xEdge || lastPt[1] !== yEdge)) { // we've gone out beyond a new corner: add the corner too // so that the next point will take the right winding if(lastFarPt) { if(lastXEdge !== xEdge && lastYEdge !== yEdge) { if(lastXEdge && lastYEdge) { // we've gone around to an opposite corner - we // need to add the correct extra corner // in order to get the right winding updateEdge(getClosestCorner(lastFarPt, pt)); } else { // we're coming from a far edge - the extra corner // we need is determined uniquely by the sectors updateEdge([lastXEdge || xEdge, lastYEdge || yEdge]); } } else if(lastXEdge && lastYEdge) { updateEdge([lastXEdge, lastYEdge]); } } updateEdge([xEdge, yEdge]); } else if((lastXEdge - xEdge) && (lastYEdge - yEdge)) { // we're coming from an edge or far corner to an edge - again the // extra corner we need is uniquely determined by the sectors updateEdge([xEdge || lastXEdge, yEdge || lastYEdge]); } lastFarPt = pt; lastXEdge = xEdge; lastYEdge = yEdge; } else { if(lastFarPt) { // this point is in range but the previous wasn't: add its entry pt first updateEdgesForReentry(getEdgeIntersections(lastFarPt, pt)[0]); } pts[pti++] = pt; } } // loop over ALL points in this trace for(i = 0; i < len; i++) { clusterStartPt = getPt(i); if(!clusterStartPt) continue; pti = 0; lastFarPt = null; addPt(clusterStartPt); // loop over one segment of the trace for(i++; i < len; i++) { clusterHighPt = getPt(i); if(!clusterHighPt) { if(connectGaps) continue; else break; } // can't decimate if nonlinear line shape // TODO: we *could* decimate [hv]{2,3} shapes if we restricted clusters to horz or vert again // but spline would be verrry awkward to decimate if(!linear || !opts.simplify) { addPt(clusterHighPt); continue; } var nextPt = getPt(i + 1); clusterRefDist = ptDist(clusterHighPt, clusterStartPt); // #3147 - always include the very first and last points for fills if(!(fill && (pti === 0 || pti === len - 1)) && clusterRefDist < getTolerance(clusterHighPt, nextPt) * minTolerance) continue; clusterUnitVector = [ (clusterHighPt[0] - clusterStartPt[0]) / clusterRefDist, (clusterHighPt[1] - clusterStartPt[1]) / clusterRefDist ]; clusterLowPt = clusterStartPt; clusterHighVal = clusterRefDist; clusterLowVal = clusterMinDeviation = clusterMaxDeviation = 0; clusterHighFirst = false; clusterEndPt = clusterHighPt; // loop over one cluster of points that collapse onto one line for(i++; i < d.length; i++) { thisPt = nextPt; nextPt = getPt(i + 1); if(!thisPt) { if(connectGaps) continue; else break; } thisVector = [ thisPt[0] - clusterStartPt[0], thisPt[1] - clusterStartPt[1] ]; // cross product (or dot with normal to the cluster vector) thisDeviation = thisVector[0] * clusterUnitVector[1] - thisVector[1] * clusterUnitVector[0]; clusterMinDeviation = Math.min(clusterMinDeviation, thisDeviation); clusterMaxDeviation = Math.max(clusterMaxDeviation, thisDeviation); if(clusterMaxDeviation - clusterMinDeviation > getTolerance(thisPt, nextPt)) break; clusterEndPt = thisPt; thisVal = thisVector[0] * clusterUnitVector[0] + thisVector[1] * clusterUnitVector[1]; if(thisVal > clusterHighVal) { clusterHighVal = thisVal; clusterHighPt = thisPt; clusterHighFirst = false; } else if(thisVal < clusterLowVal) { clusterLowVal = thisVal; clusterLowPt = thisPt; clusterHighFirst = true; } } // insert this cluster into pts // we've already inserted the start pt, now check if we have high and low pts if(clusterHighFirst) { addPt(clusterHighPt); if(clusterEndPt !== clusterLowPt) addPt(clusterLowPt); } else { if(clusterLowPt !== clusterStartPt) addPt(clusterLowPt); if(clusterEndPt !== clusterHighPt) addPt(clusterHighPt); } // and finally insert the end pt addPt(clusterEndPt); // have we reached the end of this segment? if(i >= d.length || !thisPt) break; // otherwise we have an out-of-cluster point to insert as next clusterStartPt addPt(thisPt); clusterStartPt = thisPt; } // to get fills right - repeat what we did at the start if(lastFarPt) updateEdge([lastXEdge || lastFarPt[0], lastYEdge || lastFarPt[1]]); segments.push(pts.slice(0, pti)); } var lastShapeChar = shape.slice(shape.length - 1); if(backoff && lastShapeChar !== 'h' && lastShapeChar !== 'v') { var trimmed = false; var n = -1; var newSegments = []; for(var j = 0; j < segments.length; j++) { for(var k = 0; k < segments[j].length - 1; k++) { var start = segments[j][k]; var end = segments[j][k + 1]; var xy = Drawing.applyBackoff(end, start); if( xy[0] !== end[0] || xy[1] !== end[1] ) { trimmed = true; } if(!newSegments[n + 1]) { n++; newSegments[n] = [ start, [xy[0], xy[1]] ]; } } } return trimmed ? newSegments : segments; } return segments; };