Skip to content

Commit 6299303

Browse files
committed
pull path measurement routines out into Lib
1 parent 2f3a712 commit 6299303

File tree

3 files changed

+116
-22
lines changed

3 files changed

+116
-22
lines changed

src/lib/geometry2d.js

+93
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
'use strict';
1010

11+
var mod = require('./mod');
12+
1113
/*
1214
* look for intersection of two line segments
1315
* (1->2 and 3->4) - returns array [x,y] if they do, null if not
@@ -81,3 +83,94 @@ function perpDistance2(xab, yab, l2_ab, xac, yac) {
8183
return crossProduct * crossProduct / l2_ab;
8284
}
8385
}
86+
87+
// a very short-term cache for getTextLocation, just because
88+
// we're often looping over the same locations multiple times
89+
// invalidated as soon as we look at a different path
90+
var locationCache, workingPath, workingTextWidth;
91+
92+
// turn a path and position along it into x, y, and angle for the given text
93+
exports.getTextLocation = function getTextLocation(path, totalPathLen, positionOnPath, textWidth) {
94+
if(path !== workingPath || textWidth !== workingTextWidth) {
95+
locationCache = {};
96+
workingPath = path;
97+
workingTextWidth = textWidth;
98+
}
99+
if(locationCache[positionOnPath]) {
100+
return locationCache[positionOnPath];
101+
}
102+
103+
// for the angle, use points on the path separated by the text width
104+
// even though due to curvature, the text will cover a bit more than that
105+
var p0 = path.getPointAtLength(mod(positionOnPath - textWidth / 2, totalPathLen));
106+
var p1 = path.getPointAtLength(mod(positionOnPath + textWidth / 2, totalPathLen));
107+
// note: atan handles 1/0 nicely
108+
var theta = Math.atan((p1.y - p0.y) / (p1.x - p0.x));
109+
// center the text at 2/3 of the center position plus 1/3 the p0/p1 midpoint
110+
// that's the average position of this segment, assuming it's roughly quadratic
111+
var pCenter = path.getPointAtLength(mod(positionOnPath, totalPathLen));
112+
var x = (pCenter.x * 4 + p0.x + p1.x) / 6;
113+
var y = (pCenter.y * 4 + p0.y + p1.y) / 6;
114+
115+
var out = {x: x, y: y, theta: theta};
116+
locationCache[positionOnPath] = out;
117+
return out;
118+
};
119+
120+
exports.clearLocationCache = function() {
121+
workingPath = null;
122+
};
123+
124+
/*
125+
* Find the segment of `path` that's within the visible area
126+
* given by `bounds` {left, right, top, bottom}, to within a
127+
* precision of `buffer` px
128+
*
129+
* returns {
130+
* min: position where the path first enters bounds, or 0 if it
131+
* starts within bounds
132+
* max: position where the path last exits bounds, or the path length
133+
* if it finishes within bounds
134+
* total: the total path length - just included so the caller doesn't
135+
* need to call path.getTotalLength() again
136+
* }
137+
*
138+
* Works by starting from either end and repeatedly finding the distance from
139+
* that point to the plot area, and if it's outside the plot, moving along the
140+
* path by that distance (because the plot must be at least that far away on
141+
* the path). Note that if a path enters, exits, and re-enters the plot, we
142+
* will not capture this behavior.
143+
*/
144+
exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) {
145+
var left = bounds.left;
146+
var right = bounds.right;
147+
var top = bounds.top;
148+
var bottom = bounds.bottom;
149+
150+
function getDistToPlot(len) {
151+
var pt = path.getPointAtLength(len);
152+
var dx = (pt.x < left) ? left - pt.x : (pt.x > right ? pt.x - right : 0);
153+
var dy = (pt.y < top) ? top - pt.y : (pt.y > bottom ? pt.y - bottom : 0);
154+
return Math.sqrt(dx * dx + dy * dy);
155+
}
156+
157+
var pMin = 0;
158+
var pTotal = path.getTotalLength();
159+
var pMax = pTotal;
160+
161+
var distToPlot = getDistToPlot(pMin);
162+
while(distToPlot) {
163+
pMin += distToPlot + buffer;
164+
if(pMin > pMax) return;
165+
distToPlot = getDistToPlot(pMin);
166+
}
167+
168+
distToPlot = getDistToPlot(pMax);
169+
while(distToPlot) {
170+
pMax -= distToPlot + buffer;
171+
if(pMin > pMax) return;
172+
distToPlot = getDistToPlot(pMax);
173+
}
174+
175+
return {min: pMin, max: pMax, total: pTotal};
176+
};

src/lib/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ lib.apply2DTransform2 = matrixModule.apply2DTransform2;
7777
var geom2dModule = require('./geometry2d');
7878
lib.segmentsIntersect = geom2dModule.segmentsIntersect;
7979
lib.segmentDistance = geom2dModule.segmentDistance;
80+
lib.getTextLocation = geom2dModule.getTextLocation;
81+
lib.clearLocationCache = geom2dModule.clearLocationCache;
82+
lib.getVisibleSegment = geom2dModule.getVisibleSegment;
8083

8184
var extendModule = require('./extend');
8285
lib.extendFlat = extendModule.extendFlat;

src/traces/contour/plot.js

+20-22
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function plotOne(gd, plotinfo, cd) {
8282
var plotGroup = makeContourGroup(plotinfo, cd, id);
8383
makeBackground(plotGroup, perimeter, contours);
8484
makeFills(plotGroup, pathinfo, perimeter, contours);
85-
makeLines(plotGroup, pathinfo, gd, cd[0], contours, perimeter);
85+
makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, perimeter);
8686
clipGaps(plotGroup, plotinfo, fullLayout._defs, cd[0], perimeter);
8787
}
8888

@@ -263,7 +263,7 @@ function joinAllPaths(pi, perimeter) {
263263

264264
var TRAILING_ZEROS = /\.?0+$/;
265265

266-
function makeLines(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
266+
function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
267267
var defs = gd._fullLayout._defs;
268268

269269
var smoothing = pathinfo[0].smoothing;
@@ -329,6 +329,9 @@ function makeLines(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
329329

330330
var labelData = [];
331331

332+
// invalidate the getTextLocation cache in case paths changed
333+
Lib.clearLocationCache();
334+
332335
var contourFormat;
333336
if(contours.labelformat) {
334337
contourFormat = d3.format(contours.labelformat);
@@ -352,8 +355,21 @@ function makeLines(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
352355
.attr('data-notex', 1)
353356
.call(Drawing.font, contours.font);
354357

355-
var plotDiagonal = Math.sqrt(Math.pow(pathinfo[0].xaxis._length, 2) +
356-
Math.pow(pathinfo[0].yaxis._length, 2));
358+
var xLen = pathinfo[0].xaxis._length;
359+
var yLen = pathinfo[0].yaxis._length;
360+
361+
// visible bounds of the contour trace (and the midpoints, to
362+
// help with cost calculations)
363+
var bounds = {
364+
left: Math.max(perimeter[0][0], 0),
365+
right: Math.min(perimeter[2][0], xLen),
366+
top: Math.max(perimeter[0][1], 0),
367+
bottom: Math.min(perimeter[2][1], yLen)
368+
};
369+
bounds.middle = (bounds.top + bounds.bottom) / 2;
370+
bounds.center = (bounds.left + bounds.right) / 2;
371+
372+
var plotDiagonal = Math.sqrt(xLen * xLen + yLen * yLen);
357373

358374
// the path length to use to scale the number of labels to draw:
359375
var normLength = plotDiagonal /
@@ -470,24 +486,6 @@ function addLabel(loc, textOpts, labelData) {
470486
return straightClosedPath(bBoxPts);
471487
}
472488

473-
function getLocation(path, pathLen, positionOnPath, textOpts) {
474-
var halfWidth = textOpts.width / 2;
475-
476-
// for the angle, use points on the path separated by the text width
477-
// even though due to curvature, the text will cover a bit more than that
478-
var p0 = path.getPointAtLength(Lib.mod(positionOnPath - halfWidth, pathLen));
479-
var p1 = path.getPointAtLength(Lib.mod(positionOnPath + halfWidth, pathLen));
480-
// note: atan handles 1/0 nicely
481-
var theta = Math.atan((p1.y - p0.y) / (p1.x - p0.x));
482-
// center the text at 2/3 of the center position plus 1/3 the p0/p1 midpoint
483-
// that's the average position of this segment, assuming it's roughly quadratic
484-
var pCenter = path.getPointAtLength(positionOnPath);
485-
var x = (pCenter.x * 4 + p0.x + p1.x) / 6;
486-
var y = (pCenter.y * 4 + p0.y + p1.y) / 6;
487-
488-
return {x: x, y: y, theta: theta};
489-
}
490-
491489
function clipGaps(plotGroup, plotinfo, defs, cd0, perimeter) {
492490
var clipId = 'clip' + cd0.trace.uid;
493491

0 commit comments

Comments
 (0)