-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Contour line labels #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Contour line labels #1815
Changes from 15 commits
468ef5d
88cc33c
ee7a839
b56f4ea
d534fb2
d1e3448
b8c75c0
2f3a712
6299303
99f39f8
d518c83
27f8cfe
221b124
ca1cced
d52cfba
0f37648
62156dd
539ca22
2116a12
e1880c1
14c832e
1bbdf59
e5a2f91
cbdf795
4db549c
fb4b690
b0f9bb2
e1caa9e
0ff2dc4
f5873f1
b5a674f
1f85a6f
6c2043e
bbb9574
9495dee
7f086e0
49b8141
8934038
dbeecd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -487,15 +487,15 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
// to get the parity of the number of intersections. | ||
if(edges.reduce(function(a, x) { | ||
return a ^ | ||
!!lineIntersect(headX, headY, headX + 1e6, headY + 1e6, | ||
!!Lib.segmentsIntersect(headX, headY, headX + 1e6, headY + 1e6, | ||
x[0], x[1], x[2], x[3]); | ||
}, false)) { | ||
// no line or arrow - so quit drawArrow now | ||
return; | ||
} | ||
|
||
edges.forEach(function(x) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||
var p = lineIntersect(tailX, tailY, headX, headY, | ||
var p = Lib.segmentsIntersect(tailX, tailY, headX, headY, | ||
x[0], x[1], x[2], x[3]); | ||
if(p) { | ||
tailX = p.x; | ||
|
@@ -701,24 +701,3 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
} | ||
else annText.call(textLayout); | ||
} | ||
|
||
// look for intersection of two line segments | ||
// (1->2 and 3->4) - returns array [x,y] if they do, null if not | ||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { | ||
var a = x2 - x1, | ||
b = x3 - x1, | ||
c = x4 - x3, | ||
d = y2 - y1, | ||
e = y3 - y1, | ||
f = y4 - y3, | ||
det = a * f - c * d; | ||
// parallel lines? intersection is undefined | ||
// ignore the case where they are colinear | ||
if(det === 0) return null; | ||
var t = (b * f - c * e) / det, | ||
u = (b * d - a * e) / det; | ||
// segments do not intersect? | ||
if(u < 0 || u > 1 || t < 0 || t > 1) return null; | ||
|
||
return {x: x1 + a * t, y: y1 + d * t}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/** | ||
* Copyright 2012-2017, 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 mod = require('./mod'); | ||
|
||
/* | ||
* look for intersection of two line segments | ||
* (1->2 and 3->4) - returns array [x,y] if they do, null if not | ||
*/ | ||
exports.segmentsIntersect = segmentsIntersect; | ||
function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { | ||
var a = x2 - x1, | ||
b = x3 - x1, | ||
c = x4 - x3, | ||
d = y2 - y1, | ||
e = y3 - y1, | ||
f = y4 - y3, | ||
det = a * f - c * d; | ||
// parallel lines? intersection is undefined | ||
// ignore the case where they are colinear | ||
if(det === 0) return null; | ||
var t = (b * f - c * e) / det, | ||
u = (b * d - a * e) / det; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I were really going to nitpick, I'd suggest comparing bounding boxes first and here multiplying through by |
||
// segments do not intersect? | ||
if(u < 0 || u > 1 || t < 0 || t > 1) return null; | ||
|
||
return {x: x1 + a * t, y: y1 + d * t}; | ||
} | ||
|
||
/* | ||
* find the minimum distance between two line segments (1->2 and 3->4) | ||
*/ | ||
exports.segmentDistance = function segmentDistance(x1, y1, x2, y2, x3, y3, x4, y4) { | ||
if(segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return 0; | ||
|
||
// the two segments and their lengths squared | ||
var x12 = x2 - x1; | ||
var y12 = y2 - y1; | ||
var x34 = x4 - x3; | ||
var y34 = y4 - y3; | ||
var l2_12 = x12 * x12 + y12 * y12; | ||
var l2_34 = x34 * x34 + y34 * y34; | ||
|
||
// calculate distance squared, then take the sqrt at the very end | ||
var dist2 = Math.min( | ||
perpDistance2(x12, y12, l2_12, x3 - x1, y3 - y1), | ||
perpDistance2(x12, y12, l2_12, x4 - x1, y4 - y1), | ||
perpDistance2(x34, y34, l2_34, x1 - x3, y1 - y3), | ||
perpDistance2(x34, y34, l2_34, x2 - x3, y2 - y3) | ||
); | ||
|
||
return Math.sqrt(dist2); | ||
}; | ||
|
||
/* | ||
* distance squared from segment ab to point c | ||
* [xab, yab] is the vector b-a | ||
* [xac, yac] is the vector c-a | ||
* l2_ab is the length squared of (b-a), just to simplify calculation | ||
*/ | ||
function perpDistance2(xab, yab, l2_ab, xac, yac) { | ||
var fc_ab = (xac * xab + yac * yab); | ||
if(fc_ab < 0) { | ||
// point c is closer to point a | ||
return xac * xac + yac * yac; | ||
} | ||
else if(fc_ab > l2_ab) { | ||
// point c is closer to point b | ||
var xbc = xac - xab; | ||
var ybc = yac - yab; | ||
return xbc * xbc + ybc * ybc; | ||
} | ||
else { | ||
// perpendicular distance is the shortest | ||
var crossProduct = xac * yab - yac * xab; | ||
return crossProduct * crossProduct / l2_ab; | ||
} | ||
} | ||
|
||
// a very short-term cache for getTextLocation, just because | ||
// we're often looping over the same locations multiple times | ||
// invalidated as soon as we look at a different path | ||
var locationCache, workingPath, workingTextWidth; | ||
|
||
// turn a path and position along it into x, y, and angle for the given text | ||
exports.getTextLocation = function getTextLocation(path, totalPathLen, positionOnPath, textWidth) { | ||
if(path !== workingPath || textWidth !== workingTextWidth) { | ||
locationCache = {}; | ||
workingPath = path; | ||
workingTextWidth = textWidth; | ||
} | ||
if(locationCache[positionOnPath]) { | ||
return locationCache[positionOnPath]; | ||
} | ||
|
||
// for the angle, use points on the path separated by the text width | ||
// even though due to curvature, the text will cover a bit more than that | ||
var p0 = path.getPointAtLength(mod(positionOnPath - textWidth / 2, totalPathLen)); | ||
var p1 = path.getPointAtLength(mod(positionOnPath + textWidth / 2, totalPathLen)); | ||
// note: atan handles 1/0 nicely | ||
var theta = Math.atan((p1.y - p0.y) / (p1.x - p0.x)); | ||
// center the text at 2/3 of the center position plus 1/3 the p0/p1 midpoint | ||
// that's the average position of this segment, assuming it's roughly quadratic | ||
var pCenter = path.getPointAtLength(mod(positionOnPath, totalPathLen)); | ||
var x = (pCenter.x * 4 + p0.x + p1.x) / 6; | ||
var y = (pCenter.y * 4 + p0.y + p1.y) / 6; | ||
|
||
var out = {x: x, y: y, theta: theta}; | ||
locationCache[positionOnPath] = out; | ||
return out; | ||
}; | ||
|
||
exports.clearLocationCache = function() { | ||
workingPath = null; | ||
}; | ||
|
||
/* | ||
* Find the segment of `path` that's within the visible area | ||
* given by `bounds` {left, right, top, bottom}, to within a | ||
* precision of `buffer` px | ||
* | ||
* returns { | ||
* min: position where the path first enters bounds, or 0 if it | ||
* starts within bounds | ||
* max: position where the path last exits bounds, or the path length | ||
* if it finishes within bounds | ||
* total: the total path length - just included so the caller doesn't | ||
* need to call path.getTotalLength() again | ||
* } | ||
* | ||
* Works by starting from either end and repeatedly finding the distance from | ||
* that point to the plot area, and if it's outside the plot, moving along the | ||
* path by that distance (because the plot must be at least that far away on | ||
* the path). Note that if a path enters, exits, and re-enters the plot, we | ||
* will not capture this behavior. | ||
*/ | ||
exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh man, I've implemented something similar to this before. It's such a pain! |
||
var left = bounds.left; | ||
var right = bounds.right; | ||
var top = bounds.top; | ||
var bottom = bounds.bottom; | ||
|
||
function getDistToPlot(len) { | ||
var pt = path.getPointAtLength(len); | ||
var dx = (pt.x < left) ? left - pt.x : (pt.x > right ? pt.x - right : 0); | ||
var dy = (pt.y < top) ? top - pt.y : (pt.y > bottom ? pt.y - bottom : 0); | ||
return Math.sqrt(dx * dx + dy * dy); | ||
} | ||
|
||
var pMin = 0; | ||
var pTotal = path.getTotalLength(); | ||
var pMax = pTotal; | ||
|
||
var distToPlot = getDistToPlot(pMin); | ||
while(distToPlot) { | ||
pMin += distToPlot + buffer; | ||
if(pMin > pMax) return; | ||
distToPlot = getDistToPlot(pMin); | ||
} | ||
|
||
distToPlot = getDistToPlot(pMax); | ||
while(distToPlot) { | ||
pMax -= distToPlot + buffer; | ||
if(pMin > pMax) return; | ||
distToPlot = getDistToPlot(pMax); | ||
} | ||
|
||
return {min: pMin, max: pMax, total: pTotal}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1374,7 +1374,8 @@ function _restyle(gd, aobj, _traces) { | |
'line.cmin', 'line.cmax', | ||
'marker.line.cmin', 'marker.line.cmax', | ||
'contours.start', 'contours.end', 'contours.size', | ||
'contours.showlines', | ||
'contours.showlines', 'contours.showlabels', 'contours.labelformat', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wanna try adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
'contours.font', 'contours.font.family', 'contours.font.size', | ||
'line', 'line.smoothing', 'line.shape', | ||
'error_y.width', 'error_x.width', 'error_x.copy_ystyle', | ||
'marker.maxdisplayed' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ var scatterAttrs = require('../scatter/attributes'); | |
var colorscaleAttrs = require('../../components/colorscale/attributes'); | ||
var colorbarAttrs = require('../../components/colorbar/attributes'); | ||
var dash = require('../../components/drawing/attributes').dash; | ||
var fontAttrs = require('../../plots/font_attributes'); | ||
var extendFlat = require('../../lib/extend').extendFlat; | ||
|
||
var scatterLineAttrs = scatterAttrs.line; | ||
|
@@ -106,7 +107,33 @@ module.exports = extendFlat({}, { | |
role: 'style', | ||
description: [ | ||
'Determines whether or not the contour lines are drawn.', | ||
'Has only an effect if `contours.coloring` is set to *fill*.' | ||
'Has an effect only if `contours.coloring` is set to *fill*.' | ||
].join(' ') | ||
}, | ||
showlabels: { | ||
valType: 'boolean', | ||
dflt: false, | ||
role: 'style', | ||
description: [ | ||
'Determines whether to label the contour lines with their values.' | ||
].join(' ') | ||
}, | ||
font: extendFlat({}, fontAttrs, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd vote for cc @cldougl There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -> |
||
description: [ | ||
'Sets the font used for labeling the contour levels.', | ||
'The default color comes from the lines, if shown.', | ||
// TODO: same size as layout.font, or smaller? 80%? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Let's keep it as |
||
'The default family and size come from `layout.font`.' | ||
].join(' ') | ||
}), | ||
labelformat: { | ||
valType: 'string', | ||
dflt: '', | ||
role: 'style', | ||
description: [ | ||
'Sets the contour label formatting rule using d3 formatting', | ||
'mini-language which is very similar to Python, see:', | ||
'https://github.com/d3/d3-format/blob/master/README.md#locale_format.' | ||
].join(' ') | ||
} | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not part of this PR and I'm sure no meaningful performance hit, but is
reduce
necessary here? Seems likewould be more concise and readable and would bail out early when it can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless that equates to an
all
condition where you actually need to check all. Not 100% sure. The^
is a bit confusing.