-
-
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 1 commit
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
no optimization at all yet
- Loading branch information
There are no files selected for viewing
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; | ||
|
@@ -108,6 +109,32 @@ module.exports = extendFlat({}, { | |
'Determines whether or not the contour lines are drawn.', | ||
'Has only an effect 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(' ') | ||
} | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,11 +13,13 @@ var d3 = require('d3'); | |
|
||
var Lib = require('../../lib'); | ||
var Drawing = require('../../components/drawing'); | ||
var svgTextUtils = require('../../lib/svg_text_utils'); | ||
|
||
var heatmapPlot = require('../heatmap/plot'); | ||
var makeCrossings = require('./make_crossings'); | ||
var findAllPaths = require('./find_all_paths'); | ||
var endPlus = require('./end_plus'); | ||
var constants = require('./constants'); | ||
|
||
|
||
module.exports = function plot(gd, plotinfo, cdcontours) { | ||
|
@@ -80,7 +82,7 @@ function plotOne(gd, plotinfo, cd) { | |
var plotGroup = makeContourGroup(plotinfo, cd, id); | ||
makeBackground(plotGroup, perimeter, contours); | ||
makeFills(plotGroup, pathinfo, perimeter, contours); | ||
makeLines(plotGroup, pathinfo, contours); | ||
makeLines(plotGroup, pathinfo, gd, cd[0], contours, perimeter); | ||
clipGaps(plotGroup, plotinfo, fullLayout._defs, cd[0], perimeter); | ||
} | ||
|
||
|
@@ -259,7 +261,11 @@ function joinAllPaths(pi, perimeter) { | |
return fullpath; | ||
} | ||
|
||
function makeLines(plotgroup, pathinfo, contours) { | ||
var TRAILING_ZEROS = /\.?0+$/; | ||
|
||
function makeLines(plotgroup, pathinfo, gd, cd0, contours, perimeter) { | ||
var defs = gd._fullLayout._defs; | ||
|
||
var smoothing = pathinfo[0].smoothing; | ||
|
||
var lineContainer = plotgroup.selectAll('g.contourlines').data([0]); | ||
|
@@ -296,6 +302,190 @@ function makeLines(plotgroup, pathinfo, contours) { | |
}) | ||
.style('stroke-miterlimit', 1) | ||
.style('vector-effect', 'non-scaling-stroke'); | ||
|
||
var showLabels = contours.showlabels; | ||
var clipId = showLabels ? 'clipline' + cd0.trace.uid : null; | ||
|
||
var lineClip = defs.select('.clips').selectAll('#' + clipId) | ||
.data(showLabels ? [0] : []); | ||
lineClip.exit().remove(); | ||
|
||
lineClip.enter().append('clipPath') | ||
.classed('contourlineclip', true) | ||
.attr('id', clipId); | ||
|
||
Drawing.setClipUrl(lineContainer, clipId); | ||
|
||
var labelGroup = plotgroup.selectAll('g.contourlabels') | ||
.data(showLabels ? [0] : []); | ||
|
||
labelGroup.exit().remove(); | ||
|
||
labelGroup.enter().append('g') | ||
.classed('contourlabels', true); | ||
|
||
if(showLabels) { | ||
var labelClipPathData = straightClosedPath(perimeter); | ||
|
||
var labelData = []; | ||
|
||
var contourFormat; | ||
if(contours.labelformat) { | ||
contourFormat = d3.format(contours.labelformat); | ||
} | ||
else { | ||
// round to 2 digits past magnitude of contours.size, | ||
// then remove trailing zeroes | ||
var valRound = 2 - Math.floor(Math.log(contours.size) / Math.LN10 + 0.01); | ||
if(valRound <= 0) { | ||
contourFormat = function(v) { return v.toFixed(); }; | ||
} | ||
else { | ||
contourFormat = function(v) { | ||
var valStr = v.toFixed(valRound); | ||
return valStr.replace(TRAILING_ZEROS, ''); | ||
}; | ||
} | ||
} | ||
|
||
var dummyText = defs.append('text') | ||
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. Why did you append this dummy text node to 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. good question! Actually, I bet if it's already in 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. 🐎 539ca22 |
||
.attr('data-notex', 1) | ||
.call(Drawing.font, contours.font); | ||
|
||
var plotDiagonal = Math.sqrt(Math.pow(pathinfo[0].xaxis._length, 2) + | ||
Math.pow(pathinfo[0].yaxis._length, 2)); | ||
|
||
// the path length to use to scale the number of labels to draw: | ||
var normLength = plotDiagonal / | ||
Math.max(1, pathinfo.length / constants.LABELINCREASE); | ||
|
||
linegroup.each(function(d) { | ||
// - make a dummy label for this level and calc its bbox | ||
var text = contourFormat(d.level); | ||
dummyText.text(text) | ||
.call(svgTextUtils.convertToTspans, gd); | ||
var bBox = Drawing.bBox(dummyText.node()); | ||
var textWidth = bBox.width; | ||
var textHeight = bBox.height; | ||
var dy = (bBox.top + bBox.bottom) / 2; | ||
var textOpts = { | ||
text: text, | ||
width: textWidth, | ||
height: textHeight, | ||
level: d.level, | ||
dy: dy | ||
}; | ||
|
||
d3.select(this).selectAll('path').each(function() { | ||
var path = this; | ||
var pathLen = path.getTotalLength(); | ||
|
||
if(pathLen < textWidth * constants.LABELMIN) return; | ||
|
||
var labelCount = Math.ceil(pathLen / normLength); | ||
for(var i = 0.5; i < labelCount; i++) { | ||
var positionOnPath = i * pathLen / labelCount; | ||
var loc = getLocation(path, pathLen, positionOnPath, textOpts); | ||
// TODO: no optimization yet: just get display mechanics working | ||
labelClipPathData += addLabel(loc, textOpts, labelData); | ||
} | ||
|
||
}); | ||
// - iterate over paths for this level, finding the best position(s) | ||
// for label(s) on that path, given all the other labels we've | ||
// already placed | ||
}); | ||
|
||
dummyText.remove(); | ||
|
||
var labels = labelGroup.selectAll('text') | ||
.data(labelData, function(d) { | ||
return d.text + ',' + d.x + ',' + d.y + ',' + d.theta; | ||
}); | ||
|
||
labels.exit().remove(); | ||
|
||
labels.enter().append('text') | ||
.attr({ | ||
'data-notex': 1, | ||
'text-anchor': 'middle' | ||
}) | ||
.each(function(d) { | ||
var x = d.x + Math.sin(d.theta) * d.dy; | ||
var y = d.y - Math.cos(d.theta) * d.dy; | ||
d3.select(this) | ||
.text(d.text) | ||
.attr({ | ||
x: x, | ||
y: y, | ||
transform: 'rotate(' + (180 * d.theta / Math.PI) + ' ' + x + ' ' + y + ')' | ||
}) | ||
.call(svgTextUtils.convertToTspans, gd) | ||
.call(Drawing.font, contours.font.family, contours.font.size); | ||
}); | ||
|
||
var lineClipPath = lineClip.selectAll('path').data([0]); | ||
lineClipPath.enter().append('path'); | ||
lineClipPath.attr('d', labelClipPathData); | ||
} | ||
|
||
} | ||
|
||
function straightClosedPath(pts) { | ||
return 'M' + pts.join('L') + 'Z'; | ||
} | ||
|
||
function addLabel(loc, textOpts, labelData) { | ||
var halfWidth = textOpts.width / 2; | ||
var halfHeight = textOpts.height / 2; | ||
|
||
var x = loc.x; | ||
var y = loc.y; | ||
var theta = loc.theta; | ||
|
||
var sin = Math.sin(theta); | ||
var cos = Math.cos(theta); | ||
var dxw = halfWidth * cos; | ||
var dxh = halfHeight * sin; | ||
var dyw = halfWidth * sin; | ||
var dyh = -halfHeight * cos; | ||
var bBoxPts = [ | ||
[x - dxw - dxh, y - dyw - dyh], | ||
[x + dxw - dxh, y + dyw - dyh], | ||
[x + dxw + dxh, y + dyw + dyh], | ||
[x - dxw + dxh, y - dyw + dyh], | ||
]; | ||
|
||
labelData.push({ | ||
text: textOpts.text, | ||
x: x, | ||
y: y,< 10669 /td> | ||
dy: textOpts.dy, | ||
theta: theta, | ||
level: textOpts.level, | ||
width: textOpts.width, | ||
height: textOpts.height | ||
}); | ||
|
||
return straightClosedPath(bBoxPts); | ||
} | ||
|
||
function getLocation(path, pathLen, positionOnPath, textOpts) { | ||
var halfWidth = textOpts.width / 2; | ||
|
||
// 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(Lib.mod(positionOnPath - halfWidth, pathLen)); | ||
var p1 = path.getPointAtLength(Lib.mod(positionOnPath + halfWidth, pathLen)); | ||
// 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(positionOnPath); | ||
var x = (pCenter.x * 4 + p0.x + p1.x) / 6; | ||
var y = (pCenter.y * 4 + p0.y + p1.y) / 6; | ||
|
||
return {x: x, y: y, theta: theta}; | ||
} | ||
|
||
function clipGaps(plotGroup, plotinfo, defs, cd0, perimeter) { | ||
|
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.
Wanna try adding
editType
flags their attribute declarations instead as in #1653 ?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.
@etpinard I had a misstep in e5a2f91 when I thought we could hunt for
editType
inside containers, but your awesome plot_api tests showed that wasn't going to work - so removed that piece in cbdf795 - look reasonable? Another baby step to cleaning uprestyle
andrelayout
...