Skip to content

Commit d1e3448

Browse files
committed
contour line label basic functionality
no optimization at all yet
1 parent d534fb2 commit d1e3448

File tree

6 files changed

+264
-13
lines changed

6 files changed

+264
-13
lines changed

src/plot_api/plot_api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1374,7 +1374,8 @@ function _restyle(gd, aobj, _traces) {
13741374
'line.cmin', 'line.cmax',
13751375
'marker.line.cmin', 'marker.line.cmax',
13761376
'contours.start', 'contours.end', 'contours.size',
1377-
'contours.showlines',
1377+
'contours.showlines', 'contours.showlabels', 'contours.labelformat',
1378+
'contours.font', 'contours.font.family', 'contours.font.size',
13781379
'line', 'line.smoothing', 'line.shape',
13791380
'error_y.width', 'error_x.width', 'error_x.copy_ystyle',
13801381
'marker.maxdisplayed'

src/traces/contour/attributes.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var scatterAttrs = require('../scatter/attributes');
1313
var colorscaleAttrs = require('../../components/colorscale/attributes');
1414
var colorbarAttrs = require('../../components/colorbar/attributes');
1515
var dash = require('../../components/drawing/attributes').dash;
16+
var fontAttrs = require('../../plots/font_attributes');
1617
var extendFlat = require('../../lib/extend').extendFlat;
1718

1819
var scatterLineAttrs = scatterAttrs.line;
@@ -108,6 +109,32 @@ module.exports = extendFlat({}, {
108109
'Determines whether or not the contour lines are drawn.',
109110
'Has only an effect if `contours.coloring` is set to *fill*.'
110111
].join(' ')
112+
},
113+
showlabels: {
114+
valType: 'boolean',
115+
dflt: false,
116+
role: 'style',
117+
description: [
118+
'Determines whether to label the contour lines with their values.'
119+
].join(' ')
120+
},
121+
font: extendFlat({}, fontAttrs, {
122+
description: [
123+
'Sets the font used for labeling the contour levels.',
124+
'The default color comes from the lines, if shown.',
125+
// TODO: same size as layout.font, or smaller? 80%?
126+
'The default family and size come from `layout.font`.'
127+
].join(' ')
128+
}),
129+
labelformat: {
130+
valType: 'string',
131+
dflt: '',
132+
role: 'style',
133+
description: [
134+
'Sets the contour label formatting rule using d3 formatting',
135+
'mini-language which is very similar to Python, see:',
136+
'https://github.com/d3/d3-format/blob/master/README.md#locale_format.'
137+
].join(' ')
111138
}
112139
},
113140

src/traces/contour/constants.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,17 @@ module.exports = {
3535

3636
// after one index has been used for a saddle, which do we
3737
// substitute to be used up later?
38-
SADDLEREMAINDER: {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}
38+
SADDLEREMAINDER: {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11},
39+
40+
// number of contour levels after which we start increasing the number of
41+
// labels we draw. Many contours means they will generally be close
42+
// together, so it will be harder to follow a long way to find a label
43+
LABELINCREASE: 10,
44+
45+
// minimum length of a contour line, as a multiple of the label length,
46+
// at which we draw *any* labels
47+
LABELMIN: 3,
48+
49+
// max number of labels to draw on a single contour path, no matter how long
50+
LABELMAX: 10
3951
};

src/traces/contour/plot.js

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ var d3 = require('d3');
1313

1414
var Lib = require('../../lib');
1515
var Drawing = require('../../components/drawing');
16+
var svgTextUtils = require('../../lib/svg_text_utils');
1617

1718
var heatmapPlot = require('../heatmap/plot');
1819
var makeCrossings = require('./make_crossings');
1920
var findAllPaths = require('./find_all_paths');
2021
var endPlus = require('./end_plus');
22+
var constants = require('./constants');
2123

2224

2325
module.exports = function plot(gd, plotinfo, cdcontours) {
@@ -80,7 +82,7 @@ function plotOne(gd, plotinfo, cd) {
8082
var plotGroup = makeContourGroup(plotinfo, cd, id);
8183
makeBackground(plotGroup, perimeter, contours);
8284
makeFills(plotGroup, pathinfo, perimeter, contours);
83-
makeLines(plotGroup, pathinfo, contours);
85+
makeLines(plotGroup, pathinfo, gd, cd[0], contours, perimeter);
8486
clipGaps(plotGroup, plotinfo, fullLayout._defs, cd[0], perimeter);
8587
}
8688

@@ -259,7 +261,11 @@ function joinAllPaths(pi, perimeter) {
259261
return fullpath;
260262
}
261263

262-
function makeLines(plotgroup, pathinfo, contours) {
264+
var TRAILING_ZEROS = /\.?0+$/;
265+
266+
function makeLines(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
267+
var defs = gd._fullLayout._defs;
268+
263269
var smoothing = pathinfo[0].smoothing;
264270

265271
var lineContainer = plotgroup.selectAll('g.contourlines').data([0]);
@@ -296,6 +302,190 @@ function makeLines(plotgroup, pathinfo, contours) {
296302
})
297303
.style('stroke-miterlimit', 1)
298304
.style('vector-effect', 'non-scaling-stroke');
305+
306+
var showLabels = contours.showlabels;
307+
var clipId = showLabels ? 'clipline' + cd0.trace.uid : null;
308+
309+
var lineClip = defs.select('.clips').selectAll('#' + clipId)
310+
.data(showLabels ? [0] : []);
311+
lineClip.exit().remove();
312+
313+
lineClip.enter().append('clipPath')
314+
.classed('contourlineclip', true)
315+
.attr('id', clipId);
316+
317+
Drawing.setClipUrl(lineContainer, clipId);
318+
319+
var labelGroup = plotgroup.selectAll('g.contourlabels')
320+
.data(showLabels ? [0] : []);
321+
322+
labelGroup.exit().remove();
323+
324+
labelGroup.enter().append('g')
325+
.classed('contourlabels', true);
326+
327+
if(showLabels) {
328+
var labelClipPathData = straightClosedPath(perimeter);
329+
330+
var labelData = [];
331+
332+
var contourFormat;
333+
if(contours.labelformat) {
334+
contourFormat = d3.format(contours.labelformat);
335+
}
336+
else {
337+
// round to 2 digits past magnitude of contours.size,
338+
// then remove trailing zeroes
339+
var valRound = 2 - Math.floor(Math.log(contours.size) / Math.LN10 + 0.01);
340+
if(valRound <= 0) {
341+
contourFormat = function(v) { return v.toFixed(); };
342+
}
343+
else {
344+
contourFormat = function(v) {
345+
var valStr = v.toFixed(valRound);
346+
return valStr.replace(TRAILING_ZEROS, '');
347+
};
348+
}
349+
}
350+
351+
var dummyText = defs.append('text')
352+
.attr('data-notex', 1)
353+
.call(Drawing.font, contours.font);
354+
355+
var plotDiagonal = Math.sqrt(Math.pow(pathinfo[0].xaxis._length, 2) +
356+
Math.pow(pathinfo[0].yaxis._length, 2));
357+
358+
// the path length to use to scale the number of labels to draw:
359+
var normLength = plotDiagonal /
360+
Math.max(1, pathinfo.length / constants.LABELINCREASE);
361+
362+
linegroup.each(function(d) {
363+
// - make a dummy label for this level and calc its bbox
364+
var text = contourFormat(d.level);
365+
dummyText.text(text)
366+
.call(svgTextUtils.convertToTspans, gd);
367+
var bBox = Drawing.bBox(dummyText.node());
368+
var textWidth = bBox.width;
369+
var textHeight = bBox.height;
370+
var dy = (bBox.top + bBox.bottom) / 2;
371+
var textOpts = {
372+
text: text,
373+
width: textWidth,
374+
height: textHeight,
375+
level: d.level,
376+
dy: dy
377+
};
378+
379+
d3.select(this).selectAll('path').each(function() {
380+
var path = this;
381+
var pathLen = path.getTotalLength();
382+
383+
if(pathLen < textWidth * constants.LABELMIN) return;
384+
385+
var labelCount = Math.ceil(pathLen / normLength);
386+
for(var i = 0.5; i < labelCount; i++) {
387+
var positionOnPath = i * pathLen / labelCount;
388+
var loc = getLocation(path, pathLen, positionOnPath, textOpts);
389+
// TODO: no optimization yet: just get display mechanics working
390+
labelClipPathData += addLabel(loc, textOpts, labelData);
391+
}
392+
393+
});
394+
// - iterate over paths for this level, finding the best position(s)
395+
// for label(s) on that path, given all the other labels we've
396+
// already placed
397+
});
398+
399+
dummyText.remove();
400+
401+
var labels = labelGroup.selectAll('text')
402+
.data(labelData, function(d) {
403+
return d.text + ',' + d.x + ',' + d.y + ',' + d.theta;
404+
});
405+
406+
labels.exit().remove();
407+
408+
labels.enter().append('text')
409+
.attr({
410+
'data-notex': 1,
411+
'text-anchor': 'middle'
412+
})
413+
.each(function(d) {
414+
var x = d.x + Math.sin(d.theta) * d.dy;
415+
var y = d.y - Math.cos(d.theta) * d.dy;
416+
d3.select(this)
417+
.text(d.text)
418+
.attr({
419+
x: x,
420+
y: y,
421+
transform: 'rotate(' + (180 * d.theta / Math.PI) + ' ' + x + ' ' + y + ')'
422+
})
423+
.call(svgTextUtils.convertToTspans, gd)
424+
.call(Drawing.font, contours.font.family, contours.font.size);
425+
});
426+
427+
var lineClipPath = lineClip.selectAll('path').data([0]);
428+
lineClipPath.enter().append('path');
429+
lineClipPath.attr('d', labelClipPathData);
430+
}
431+
432+
}
433+
434+
function straightClosedPath(pts) {
435+
return 'M' + pts.join('L') + 'Z';
436+
}
437+
438+
function addLabel(loc, textOpts, labelData) {
439+
var halfWidth = textOpts.width / 2;
440+
var halfHeight = textOpts.height / 2;
441+
442+
var x = loc.x;
443+
var y = loc.y;
444+
var theta = loc.theta;
445+
446+
var sin = Math.sin(theta);
447+
var cos = Math.cos(theta);
448+
var dxw = halfWidth * cos;
449+
var dxh = halfHeight * sin;
450+
var dyw = halfWidth * sin;
451+
var dyh = -halfHeight * cos;
452+
var bBoxPts = [
453+
[x - dxw - dxh, y - dyw - dyh],
454+
[x + dxw - dxh, y + dyw - dyh],
455+
[x + dxw + dxh, y + dyw + dyh],
456+
[x - dxw + dxh, y - dyw + dyh],
457+
];
458+
459+
labelData.push({
460+
text: textOpts.text,
461+
x: x,
462+
y: y,
463+
dy: textOpts.dy,
464+
theta: theta,
465+
level: textOpts.level,
466+
width: textOpts.width,
467+
height: textOpts.height
468+
});
469+
470+
return straightClosedPath(bBoxPts);
471+
}
472+
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};
299489
}
300490

301491
function clipGaps(plotGroup, plotinfo, defs, cd0, perimeter) {

src/traces/contour/style.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,31 @@ module.exports = function style(gd) {
2525
});
2626

2727
contours.each(function(d) {
28-
var c = d3.select(this),
29-
trace = d.trace,
30-
contours = trace.contours,
31-
line = trace.line,
32-
cs = contours.size || 1,
33-
start = contours.start;
28+
var c = d3.select(this);
29+
var trace = d.trace;
30+
var contours = trace.contours;
31+
var line = trace.line;
32+
var cs = contours.size || 1;
33+
var start = contours.start;
34+
var colorLines = contours.coloring === 'lines';
3435

3536
var colorMap = makeColorMap(trace);
3637

3738
c.selectAll('g.contourlevel').each(function(d) {
3839
d3.select(this).selectAll('path')
3940
.call(Drawing.lineGroupStyle,
4041
line.width,
41-
contours.coloring === 'lines' ? colorMap(d.level) : line.color,
42+
colorLines ? colorMap(d.level) : line.color,
4243
line.dash);
4344
});
4445

46+
var labelFontColor = (contours.font || {}).color;
47+
c.selectAll('g.contourlabels text').each(function(d) {
48+
Drawing.font(d3.select(this), {
49+
color: labelFontColor || (colorLines ? colorMap(d.level) : line.color)
50+
});
51+
});
52+
4553
var firstFill;
4654

4755
c.selectAll('g.contourfill path')

src/traces/contour/style_defaults.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,38 @@
1010
'use strict';
1111

1212
var colorscaleDefaults = require('../../components/colorscale/defaults');
13+
var Lib = require('../../lib');
1314

1415

1516
module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout, defaultColor, defaultWidth) {
1617
var coloring = coerce('contours.coloring');
1718

1819
var showLines;
20+
var lineColor = '';
1921
if(coloring === 'fill') showLines = coerce('contours.showlines');
2022

2123
if(showLines !== false) {
22-
if(coloring !== 'lines') coerce('line.color', defaultColor || '#000');
24+
if(coloring !== 'lines') lineColor = coerce('line.color', defaultColor || '#000');
2325
coerce('line.width', defaultWidth === undefined ? 0.5 : defaultWidth);
2426
coerce('line.dash');
2527
}
2628

2729
coerce('line.smoothing');
2830

29-
if((traceOut.contours || {}).coloring !== 'none') {
31+
if(coloring !== 'none') {
3032
colorscaleDefaults(
3133
traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}
3234
);
3335
}
36+
37+
var showLabels = coerce('contours.showlabels');
38+
if(showLabels) {
39+
var globalFont = layout.font;
40+
Lib.coerceFont(coerce, 'contours.font', {
41+
family: globalFont.family,
42+
size: globalFont.size,
43+
color: lineColor
44+
});
45+
coerce('contours.labelformat');
46+
}
3447
};

0 commit comments

Comments
 (0)