Skip to content
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

Merged
merged 39 commits into from
Jun 30, 2017
Merged

Contour line labels #1815

merged 39 commits into from
Jun 30, 2017

Conversation

alexcjohnson
Copy link
Collaborator

@alexcjohnson alexcjohnson commented Jun 23, 2017

Fixes #1395

screen shot 2017-06-23 at 5 23 12 pm

screen shot 2017-06-23 at 5 24 15 pm

TODO:

  • contourcarpet (cc @rreusser): currently the attributes are there (so we don't get errors) but they don't work. I haven't looked to see how easy it will be to add these there, if it's too hard we could just leave it for later (and remove the attributes and make supplyDefaults work without them)
  • tests (basic, labels without lines, panned, with formatting) - since this work is all in the plot step it'll probably be all in image tests
  • there's no configurability to how many labels you get, or where they go (there is style configurability though, for font, color, size, number format). I spent a good while on the location optimizer - which tries to minimize angle and maximize distance from other labels (especially those on the same level) and from the edge. This looks pleasing to my eye across all our test plots (and on zoom/pan), but I'd be curious @geocosmite if you see a use for more control in this regard.

cc @etpinard

function makeLines(plotgroup, pathinfo, contours) {
var TRAILING_ZEROS = /\.?0+$/;

function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the innermost join content, it looks like mostly d3 operations so is probably okay, but this function comes out to 240 lines, which seems a bit on the high side.

@rreusser
Copy link
Contributor

Very impressed with the greedy approach!

screen shot 2017-06-23 at 15 03 34

screen shot 2017-06-23 at 15 01 05

screen shot 2017-06-23 at 15 01 52

screen shot 2017-06-23 at 15 02 24

@geocosmite
Copy link

geocosmite commented Jun 23, 2017

This looks FABULOUS! From these examples I don't think we would need more control on the positioning.


var maxLabels = Math.min(Math.ceil(pathLen / normLength),
constants.LABELMAX);
var dp, p0, pMax, minCost, location, pMin;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small note of caution that my syntax highlighter picks up location since window.location is special, though it's 100% fine here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good call, should be ok but I'll change it to avoid any confusion.

Copy link
Contributor

@rreusser rreusser Jun 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only danger (I think) is if var declaration gets removed. Linter may or may not catch it. So really not dangerous at all… almost certainly 🔫

@rreusser
Copy link
Contributor

Maybe a minimum path length cost component so that there aren't numbers on closed curves too small to display them? Otherwise I'm having trouble finding any problems with it. 🎉

screen shot 2017-06-23 at 18 56 44

@etpinard etpinard added this to the 1.29.0 milestone Jun 26, 2017
@alexcjohnson
Copy link
Collaborator Author

@rreusser what plot did you see those labels on tiny loops? There is a minimum of 3x the label length but perhaps we need to increase that.

@rreusser
Copy link
Contributor

@alexcjohnson zoomed in section on: http://rickyreusser.com/plotly-mock-viewer/#contour_transposed

Also, I've been thinking about carpet plots. Since plotting is pretty trace-specific, I think it might have to be duplicated, though it's probably possible to factor out the code since there's nothing really contour-specific. I'll try to throw together a POC (though probably not much work required beyond that).

@alexcjohnson
Copy link
Collaborator Author

@rreusser

Since plotting is pretty trace-specific, I think it might have to be duplicated, though it's probably possible to factor out the code since there's nothing really contour-specific. I'll try to throw together a POC (though probably not much work required beyond that).

awesome, that'll help with your previous comment too 🍰

using just width had some adverse effects with really short labels
@@ -423,7 +423,7 @@ function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, perimeter) {

var isOpen = d3.select(this).classed('openline');

if(pathLen < textWidth * constants.LABELMIN) return;
if(pathLen < (textWidth + textHeight) * constants.LABELMIN) return;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't increase LABELMIN, instead included both width and height of the label. That way when you have long labels you'll still get them on about the same length paths as before, but short labels will require a bit longer paths than they did previously:
screen shot 2017-06-26 at 2 34 00 pm

Note that this isn't in the cost function, because that controls label location on the path but only weakly affects whether the path gets a label at all - ie if it pushes past MAXCOST

'Has only an effect if `contours.coloring` is set to *fill*.'
].join(' ')
},
showlines: contourContourAttrs.showlines,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌴 thanks!

'Determines whether to label the contour lines with their values.'
].join(' ')
},
font: extendFlat({}, fontAttrs, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for labelfont here.

cc @cldougl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for labelfont

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> labelFont in 2116a12

@@ -62,7 +64,11 @@ exports.get = function() {
return {
defs: {
valObjects: Lib.valObjects,
metaKeys: UNDERSCORE_ATTRS.concat(['description', 'role'])
metaKeys: UNDERSCORE_ATTRS.concat(['description', 'role']),
editTypes: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. Sure, might as well list those things.

- so we might be able to reuse them for contourcarpet
- and test getVisibleSegment
minor change to the algorithm probably due to cleaner open/closed criterion
this makes contours.coloring='lines' work with carpet
also fixes some unusual bugs like if the first contour is not shown
that were fixed long ago for contour
@@ -15,7 +15,7 @@ ContourCarpet.supplyDefaults = require('./defaults');
ContourCarpet.colorbar = require('../contour/colorbar');
ContourCarpet.calc = require('./calc');
ContourCarpet.plot = require('./plot');
ContourCarpet.style = require('./style');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuing to 🌴 contour/contourcarpet... this also fixes some bugs such as:

  • contourcarpet failed with contours.coloring='lines'
  • if the first contour wasn't shown (if you give an explicit contour start value lower than what the data would be) the colors would have been wrong. For contour this was fixed in Contour bug #1309 (the key is to use d.level rather than i in coloring the contour lines/fills) and tested in contour_edge_cases.

@etpinard do you think it's sufficient to rely on the contour tests for contourcarpet too - since it's using the same code now - or would you like me to add contourcarpet tests of these cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think it's sufficient to rely

We have plenty of carpet mocks, maybe you could update one of them with contours.coloring='lines' ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, as far as I can tell only cheater_contour is not constraint-type, but perhaps I can extend that one to cover these cases. Sure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0ff2dc4
This mock on master:
screen shot 2017-06-29 at 6 39 10 pm
and now:
screen shot 2017-06-29 at 6 40 20 pm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I'd have to dig in further to refresh my memory, but I struggled a bit to wrangle the behavior under control for both constraints and contours since it felt like they had slightly different semantics. Like constraints tend to be a single color while contours have multiple colors. But if it passes the existing image tests and fixes other behaviors, then I'm 100% content.


var colorMap = makeColorMap(trace);
// for contourcarpet only - is this a constraint-type contour trace?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit funny to have contourcarpet only code in contour.style - but perhaps someday we would like to support constraint-type coloring for regular contour plots too?

@@ -15,7 +15,7 @@ ContourCarpet.supplyDefaults = require('./defaults');
ContourCarpet.colorbar = require('../contour/colorbar');
ContourCarpet.calc = require('./calc');
ContourCarpet.plot = require('./plot');
ContourCarpet.style = require('./style');
ContourCarpet.style = require('../contour/style');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing 🎉

@@ -21,6 +21,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayou
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}

traceOut._clipPathId = 'clip' + traceOut.uid + 'carpet';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rreusser FYI - it turns out that we weren't applying the clip path to mocks that have the data trace before the carpet trace because the ID was cached during the plot step. I moved _clipPathId up to supplyDefaults to get around that. It's barely visible in the test images, but that's why they all changed (all the ones that put the data before the carpet anyway).

Also two more changes in here:

  • _clipPathId rather than clipPathId - if we're attaching something to the trace (or layout) that's not an attribute, it should either be a function or it should start with an underscore.
  • use Drawing.setClipUrl rather than directly setting the clip-path attribute. This is to handle pages with a <base> tag, which is obnoxious but important if it's present - which happens most notably in angular 😑

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow… Good catch! Yeah, I can imagine that's a subtle half-border-width-effect. 👍

same as contour - it's *probably* ok without this but I'm a little
worried about some restyle call changing element order unexpectedly.
also a bugfix to findBestTextLocation - surprised that didn't break
anything before!
@alexcjohnson alexcjohnson changed the title [WIP] Contour line labels Contour line labels Jun 30, 2017
@@ -37,6 +38,6 @@ module.exports = function plot(gd, plotinfoproxy, data) {
// separately to all scattercarpet traces, but that would require
// lots of reorganization of scatter traces that is otherwise not
// necessary. That makes this a potential optimization.
node.attr('clip-path', 'url(#clip' + carpet.uid + 'carpet)');
Drawing.setClipUrl(node, carpet._clipPathId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. That means carpet traces will now be functional in Angular 1.0 see #509

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, as mentioned in #1815 (comment)

Tried to figure out if we have a way to enforce that... only allow attr('clip-path', ...) inside Drawing.setClipUrl - like something to add to strict-d3? Seemed pretty invasive and hacky though.

@etpinard
Copy link
Contributor

Amazing PR!

This will surely make the daunting contour code more-easily reusable (contourgeo anyone?).

💃

@@ -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,
Copy link
Contributor

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 like

for (i = 0; i < edges.length; i++) {
  if (!Lib.segmentsIntersect(...)) return;
}

would be more concise and readable and would bail out early when it can.

Copy link
Contributor

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.

@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is forEach avoidable here?

* for this element beforehand.
*
* @return {object}: a plain object containing the width, height, left, right,
* top, and bottom of `node`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ 📚

*/
drawing.savedBBoxes = {};
var savedBBoxesCount = 0;
var maxSavedBBoxes = 10000;

drawing.bBox = function(node, hash) {
drawing.bBox = function(node, inTester, hash) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 technically an internal API used elsewhere, but a quick grep I can't find any cases where the argument signature change is invalid.

// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 det to eliminate division, but this is probably not a bottleneck…

@@ -36,6 +37,7 @@ module.exports = extendFlat({}, {
valType: 'boolean',
dflt: true,
role: 'style',
editType: 'docalc',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😮

Now I understand. Nice.



module.exports = function plot(gd, plotinfo, cdcontours) {
exports.plot = function plot(gd, plotinfo, cdcontours) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named for testing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse in carpet. Got it.

return lineClip;
};

exports.labelFormatter = function(contours, colorbar, fullLayout) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Does this have any more general applicability? I feel like piggybacking on this is a common need, and I struggled to figure out just which parts and in what sequence it should be done.

};
};

exports.findBestTextLocation = function(path, pathBounds, textOpts, labelData, plotBounds) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ ❤️ ❤️ Reusable for carpet?

@@ -15,7 +15,7 @@ ContourCarpet.supplyDefaults = require('./defaults');
ContourCarpet.colorbar = require('../contour/colorbar');
ContourCarpet.calc = require('./calc');
ContourCarpet.plot = require('./plot');
ContourCarpet.style = require('./style');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I'd have to dig in further to refresh my memory, but I struggled a bit to wrangle the behavior under control for both constraints and contours since it felt like they had slightly different semantics. Like constraints tend to be a single color while contours have multiple colors. But if it passes the existing image tests and fixes other behaviors, then I'm 100% content.

@rreusser
Copy link
Contributor

Looks great to me! Thrilled for carpet to get a close second look. One nitpick is that -5.0 and 5.0 are sometimes hard to distinguish since the - tends to blend into the surrounding contour. But I don't really see how we can fix that, unless the label padding were configurable so that you could add a bit more spacing.

💃

@alexcjohnson
Copy link
Collaborator Author

One nitpick is that -5.0 and 5.0 are sometimes hard to distinguish since the - tends to blend into the surrounding contour.

Hmm, interesting - I suppose we could either add some extra space ahead of a leading -, or replace it with a different unicode symbol that's raised up a bit, perhaps? I'll leave it as is for the first iteration but will take a look at it later.

@etpinard
Copy link
Contributor

Hmm, interesting - I suppose we could either add some extra space ahead of a leading -, or replace it with a different unicode symbol that's raised up a bit, perhaps? I'll leave it as is for the first iteration but will take a look at it later.

... or implement #1597

@alexcjohnson alexcjohnson merged commit fbc0296 into master Jun 30, 2017
@alexcjohnson alexcjohnson deleted the contour-labels branch June 30, 2017 18:16
@rreusser
Copy link
Contributor

This is the only other variation that jumps out. Which I don't love. Or +1/-1 instead of 1/-1, which I don't like either. I think the text shadow method seems best.

overlay_9_3_lg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants