Skip to content

Commit fbc0296

Browse files
authored
Merge pull request #1815 from plotly/contour-labels
Contour line labels
2 parents 70de159 + dbeecd0 commit fbc0296

File tree

62 files changed

+1517
-389
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1517
-389
lines changed

src/components/annotations/draw.js

+2-23
Original file line numberDiff line numberDiff line change
@@ -487,15 +487,15 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
487487
// to get the parity of the number of intersections.
488488
if(edges.reduce(function(a, x) {
489489
return a ^
490-
!!lineIntersect(headX, headY, headX + 1e6, headY + 1e6,
490+
!!Lib.segmentsIntersect(headX, headY, headX + 1e6, headY + 1e6,
491491
x[0], x[1], x[2], x[3]);
492492
}, false)) {
493493
// no line or arrow - so quit drawArrow now
494494
return;
495495
}
496496

497497
edges.forEach(function(x) {
498-
var p = lineIntersect(tailX, tailY, headX, headY,
498+
var p = Lib.segmentsIntersect(tailX, tailY, headX, headY,
499499
x[0], x[1], x[2], x[3]);
500500
if(p) {
501501
tailX = p.x;
@@ -701,24 +701,3 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
701701
}
702702
else annText.call(textLayout);
703703
}
704-
705-
// look for intersection of two line segments
706-
// (1->2 and 3->4) - returns array [x,y] if they do, null if not
707-
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
708-
var a = x2 - x1,
709-
b = x3 - x1,
710-
c = x4 - x3,
711-
d = y2 - y1,
712-
e = y3 - y1,
713-
f = y4 - y3,
714-
det = a * f - c * d;
715-
// parallel lines? intersection is undefined
716-
// ignore the case where they are colinear
717-
if(det === 0) return null;
718-
var t = (b * f - c * e) / det,
719-
u = (b * d - a * e) / det;
720-
// segments do not intersect?
721-
if(u < 0 || u > 1 || t < 0 || t > 1) return null;
722-
723-
return {x: x1 + a * t, y: y1 + d * t};
724-
}

src/components/drawing/index.js

+32-18
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var drawing = module.exports = {};
3434

3535
drawing.font = function(s, family, size, color) {
3636
// also allow the form font(s, {family, size, color})
37-
if(family && family.family) {
37+
if(Lib.isPlainObject(family)) {
3838
color = family.color;
3939
size = family.size;
4040
family = family.family;
@@ -569,9 +569,6 @@ drawing.steps = function(shape) {
569569

570570
// off-screen svg render testing element, shared by the whole page
571571
// uses the id 'js-plotly-tester' and stores it in drawing.tester
572-
// makes a hash of cached text items in tester.node()._cache
573-
// so we can add references to rendered text (including all info
574-
// needed to fully determine its bounding rect)
575572
drawing.makeTester = function() {
576573
var tester = d3.select('body')
577574
.selectAll('#js-plotly-tester')
@@ -601,25 +598,37 @@ drawing.makeTester = function() {
601598
fill: 'black'
602599
});
603600

604-
if(!tester.node()._cache) {
605-
tester.node()._cache = {};
606-
}
607-
608601
drawing.tester = tester;
609602
drawing.testref = testref;
610603
};
611604

612605
/*
613606
* use our offscreen tester to get a clientRect for an element,
614-
* in a reference frame where it isn't translated and its anchor
615-
* point is at (0,0)
607+
* in a reference frame where it isn't translated (or transformed) and
608+
* its anchor point is at (0,0)
616609
* always returns a copy of the bbox, so the caller can modify it safely
610+
*
611+
* @param {SVGElement} node: the element to measure. If possible this should be
612+
* a <text> or MathJax <g> element that's already passed through
613+
* `convertToTspans` because in that case we can cache the results, but it's
614+
* possible to pass in any svg element.
615+
*
616+
* @param {boolean} inTester: is this element already in `drawing.tester`?
617+
* If you are measuring a dummy element, rather than one you really intend
618+
* to use on the plot, making it in `drawing.tester` in the first place
619+
* allows us to test faster because it cuts out cloning and appending it.
620+
*
621+
* @param {string} hash: for internal use only, if we already know the cache key
622+
* for this element beforehand.
623+
*
624+
* @return {object}: a plain object containing the width, height, left, right,
625+
* top, and bottom of `node`
617626
*/
618627
drawing.savedBBoxes = {};
619628
var savedBBoxesCount = 0;
620629
var maxSavedBBoxes = 10000;
621630

622-
drawing.bBox = function(node, hash) {
631+
drawing.bBox = function(node, inTester, hash) {
623632
/*
624633
* Cache elements we've already measured so we don't have to
625634
* remeasure the same thing many times
@@ -652,7 +661,7 @@ drawing.bBox = function(node, hash) {
652661
if(!transform) {
653662
// in this case, just varying x and y, don't bother caching
654663
// the final bBox because the alteration is quick.
655-
var innerBB = drawing.bBox(innerNode, hash);
664+
var innerBB = drawing.bBox(innerNode, false, hash);
656665
if(x) {
657666
innerBB.left += x;
658667
innerBB.right += x;
@@ -679,12 +688,17 @@ drawing.bBox = function(node, hash) {
679688
if(out) return Lib.extendFlat({}, out);
680689
}
681690
}
691+
var testNode, tester;
692+
if(inTester) {
693+
testNode = node;
694+
}
695+
else {
696+
tester = drawing.tester.node();
682697

683-
var tester = drawing.tester.node();
684-
685-
// copy the node to test into the tester
686-
var testNode = node.cloneNode(true);
687-
tester.appendChild(testNode);
698+
// copy the node to test into the tester
699+
testNode = node.cloneNode(true);
700+
tester.appendChild(testNode);
701+
}
688702

689703
// standardize its position (and newline tspans if any)
690704
d3.select(testNode)
@@ -696,7 +710,7 @@ drawing.bBox = function(node, hash) {
696710
.node()
697711
.getBoundingClientRect();
698712

699-
tester.removeChild(testNode);
713+
if(!inTester) tester.removeChild(testNode);
700714

701715
var bb = {
702716
height: testRect.height,

src/lib/geometry2d.js

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* Copyright 2012-2017, 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 mod = require('./mod');
12+
13+
/*
14+
* look for intersection of two line segments
15+
* (1->2 and 3->4) - returns array [x,y] if they do, null if not
16+
*/
17+
exports.segmentsIntersect = segmentsIntersect;
18+
function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
19+
var a = x2 - x1,
20+
b = x3 - x1,
21+
c = x4 - x3,
22+
d = y2 - y1,
23+
e = y3 - y1,
24+
f = y4 - y3,
25+
det = a * f - c * d;
26+
// parallel lines? intersection is undefined
27+
// ignore the case where they are colinear
28+
if(det === 0) return null;
29+
var t = (b * f - c * e) / det,
30+
u = (b * d - a * e) / det;
31+
// segments do not intersect?
32+
if(u < 0 || u > 1 || t < 0 || t > 1) return null;
33+
34+
return {x: x1 + a * t, y: y1 + d * t};
35+
}
36+
37+
/*
38+
* find the minimum distance between two line segments (1->2 and 3->4)
39+
*/
40+
exports.segmentDistance = function segmentDistance(x1, y1, x2, y2, x3, y3, x4, y4) {
41+
if(segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return 0;
42+
43+
// the two segments and their lengths squared
44+
var x12 = x2 - x1;
45+
var y12 = y2 - y1;
46+
var x34 = x4 - x3;
47+
var y34 = y4 - y3;
48+
var l2_12 = x12 * x12 + y12 * y12;
49+
var l2_34 = x34 * x34 + y34 * y34;
50+
51+
// calculate distance squared, then take the sqrt at the very end
52+
var dist2 = Math.min(
53+
perpDistance2(x12, y12, l2_12, x3 - x1, y3 - y1),
54+
perpDistance2(x12, y12, l2_12, x4 - x1, y4 - y1),
55+
perpDistance2(x34, y34, l2_34, x1 - x3, y1 - y3),
56+
perpDistance2(x34, y34, l2_34, x2 - x3, y2 - y3)
57+
);
58+
59+
return Math.sqrt(dist2);
60+
};
61+
62+
/*
63+
* distance squared from segment ab to point c
64+
* [xab, yab] is the vector b-a
65+
* [xac, yac] is the vector c-a
66+
* l2_ab is the length squared of (b-a), just to simplify calculation
67+
*/
68+
function perpDistance2(xab, yab, l2_ab, xac, yac) {
69+
var fc_ab = (xac * xab + yac * yab);
70+
if(fc_ab < 0) {
71+
// point c is closer to point a
72+
return xac * xac + yac * yac;
73+
}
74+
else if(fc_ab > l2_ab) {
75+
// point c is closer to point b
76+
var xbc = xac - xab;
77+
var ybc = yac - yab;
78+
return xbc * xbc + ybc * ybc;
79+
}
80+
else {
81+
// perpendicular distance is the shortest
82+
var crossProduct = xac * yab - yac * xab;
83+
return crossProduct * crossProduct / l2_ab;
84+
}
85+
}
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: undefined if nothing is visible, else object:
130+
* {
131+
* min: position where the path first enters bounds, or 0 if it
132+
* starts within bounds
133+
* max: position where the path last exits bounds, or the path length
134+
* if it finishes within bounds
135+
* len: max - min, ie the length of visible path
136+
* total: the total path length - just included so the caller doesn't
137+
* need to call path.getTotalLength() again
138+
* isClosed: true iff the start and end points of the path are both visible
139+
* and are at the same point
140+
* }
141+
*
142+
* Works by starting from either end and repeatedly finding the distance from
143+
* that point to the plot area, and if it's outside the plot, moving along the
144+
* path by that distance (because the plot must be at least that far away on
145+
* the path). Note that if a path enters, exits, and re-enters the plot, we
146+
* will not capture this behavior.
147+
*/
148+
exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) {
149+
var left = bounds.left;
150+
var right = bounds.right;
151+
var top = bounds.top;
152+
var bottom = bounds.bottom;
153+
154+
var pMin = 0;
155+
var pTotal = path.getTotalLength();
156+
var pMax = pTotal;
157+
158+
var pt0, ptTotal;
159+
160+
function getDistToPlot(len) {
161+
var pt = path.getPointAtLength(len);
162+
163+
// hold on to the start and end points for `closed`
164+
if(len === 0) pt0 = pt;
165+
else if(len === pTotal) ptTotal = pt;
166+
167+
var dx = (pt.x < left) ? left - pt.x : (pt.x > right ? pt.x - right : 0);
168+
var dy = (pt.y < top) ? top - pt.y : (pt.y > bottom ? pt.y - bottom : 0);
169+
return Math.sqrt(dx * dx + dy * dy);
170+
}
171+
172+
var distToPlot = getDistToPlot(pMin);
173+
while(distToPlot) {
174+
pMin += distToPlot + buffer;
175+
if(pMin > pMax) return;
176+
distToPlot = getDistToPlot(pMin);
177+
}
178+
179+
distToPlot = getDistToPlot(pMax);
180+
while(distToPlot) {
181+
pMax -= distToPlot + buffer;
182+
if(pMin > pMax) return;
183+
distToPlot = getDistToPlot(pMax);
184+
}
185+
186+
return {
187+
min: pMin,
188+
max: pMax,
189+
len: pMax - pMin,
190+
total: pTotal,
191+
isClosed: pMin === 0 && pMax === pTotal &&
192+
Math.abs(pt0.x - ptTotal.x) < 0.1 &&
193+
Math.abs(pt0.y - ptTotal.y) < 0.1
194+
};
195+
};

src/lib/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ lib.rotationXYMatrix = matrixModule.rotationXYMatrix;
7474
lib.apply2DTransform = matrixModule.apply2DTransform;
7575
lib.apply2DTransform2 = matrixModule.apply2DTransform2;
7676

77+
var geom2dModule = require('./geometry2d');
78+
lib.segmentsIntersect = geom2dModule.segmentsIntersect;
79+
lib.segmentDistance = geom2dModule.segmentDistance;
80+
lib.getTextLocation = geom2dModule.getTextLocation;
81+
lib.clearLocationCache = geom2dModule.clearLocationCache;
82+
lib.getVisibleSegment = geom2dModule.getVisibleSegment;
83+
7784
var extendModule = require('./extend');
7885
lib.extendFlat = extendModule.extendFlat;
7986
lib.extendDeep = extendModule.extendDeep;

0 commit comments

Comments
 (0)