-
Notifications
You must be signed in to change notification settings - Fork 151
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
Gravitate to Centroid #63
base: master
Are you sure you want to change the base?
Conversation
@@ -65,10 +73,10 @@ function polylabel(polygon, precision, debug) { | |||
|
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.
This bit is sort of a mystery to me if (cell.max - bestCell.d <= precision) continue;
.
I wonder if the bestCell.d
should be replaced here by fitness(bestCell)
. I don't understand all the logic behind max
, so I left this as-is. Seems to work fine like this.
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.
This test corresponds to point 4 of the algorithm.
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.
If it helps, my interpretation of that line is that cell.max
provides the best possible value for distance from the polygon edge, and if the bestCell.d
is at least within precision
of cell.max
then a better solution does not exist within the selected cell
, and we can move on through the priority queue.
this.x = x; // cell center x | ||
this.y = y; // cell center y | ||
this.h = h; // half the cell size | ||
this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon | ||
this.distanceToCentroid = centroidCell ? pointToPointDist(this, centroidCell) : 0; |
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.
Note: The only case where centroidCell
is not provided is when generating the centroid cell itself, so distance should be 0.
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.
Though of course it works in the simple case of an horizontal rectangle, I think gravitating towards the centroid is the wrong concept. The centroid is global, and what we're looking for is more breadth locally.
Here's a counter-example where gravitating towards the centroid (blue) makes things worse.
- red is the pole of inaccessibility as a "locus" (in this case a line)
- red dot is the polylabel given by the standard algorithm
- green is the polylabel gravitated towards the centroid with a weight of +0.1):
https://observablehq.com/d/de13dcd035b7ebbd
What we would want in this case is the middle of the red line.
Thank you so much @Fil for reviewing! Excellent work with the counterexample. It begs the question, how could we make it resolve to the center of the right side rectangular region? It's almost as if we should split the polygon into two (or more) separate regions, and gravitate towards the centroid of the region that the point is inside... But how to split? Maybe looking at previously visited cells could help. Or... blurring the polygon and applying Marching Squares (as in https://github.com/d3/d3-contour) would probably do that sort of splitting. I bet your counterexample would be split into two separate shapes with the right blur and contour threshold. At that point the shape could be treated as a MultiPolygon, in which case the polygon with the most optimal fitness function would be labeled (at least that's how I solved the MultiPolygon case for map labeling). Possibly related to this other issue regions of inaccessibility #50. Quite a difficult problem indeed. However, I must say that gravitating to the centroid just a little bit really improves the US map labels. Since this change is arguably an overall improvement for the case of geographic regions, and it's strictly opt-in, it may be worth adding this as a stopgap until a more ideal algorithm is discovered (that addresses your counterexample). I feel like this is a problem space that @veltman would appreciate. Wonder if there's any overlap betweent this and his work on https://github.com/veltman/flubber |
Well first of all it's not for me to decide. Currently the algorithm is "pure" in the sense that it returns "the pole of inaccessibility" (or more accurately "a" pole of inaccessibility). My preference would be to keep it that way. But this is not always the best place for a label in the general case. For example, since labels are usually much larger than tall, you might want to stretch your geometry vertically before looking for the pole, as in this block). As for a better placement algorithm I don't know, but it sure is an interesting question. I have some ideas (maybe we could be happy if, starting from that "pole" position we measured the available breadth, rescaled the geometries accordingly, and replayed the algorithm), but I haven't tried to implement them yet. |
Yes! I had been wondering how to find an "ellipse of inaccessibility", which would be better than using circles, and had not thought of stretching the geometry - amazing idea. |
Published in @datavis-tech/polylabel to use in the mean time. https://github.com/datavis-tech/polylabel/releases/tag/v1.3.0 |
Very nice implementation — thanks for taking on this! Definitely easier to merge than the previous one if we do decide to do it. I'm not sure yet — @Fil raises a valid counter-point, although it would be nice to find a balance between mathematical correctness and practicality for this library. I just tried looking into how stretching the input could help with labeling, and it's pretty interesting — it fixes some cases (depending on ratio) but introduces new unexpected ones. Play around here: https://observablehq.com/@mourner/labeling-with-stretched-polylabel |
Interesting idea, agree with @Fil that you can come up with a number of cases where the centroid makes it worse rather than better, especially since it's so dependent on what the the label being placed is. Not necessarily a dealbreaker since it's opt-in, but I would assume the criteria for inclusion is that you can throw that at a large set of arbitrary shapes and mostly get improvements and rarely make it much worse? Otherwise, if it's something you would only apply on an ad hoc basis (e.g. setting a custom centroidWeight for each country through trial and error) it doesn't seem much easier to me than setting custom x/y nudge values. |
You can check here a modified version of the Vladimir's notebook with automatic ratio calculations: |
It looks close to perfect. I've added a customizable projection and a detail (to use geoPath().bounds() rather than do it manually). I'd suggest to clamp the ratio to maybe [1/10, 10] to prevent against the occasional very thin feature, like Fiji on https://observablehq.com/d/7c984c2d23c003fe (try and change the clamping ratio A to 10000). The next step is to add occlusion and zoom, but I've got to go :) |
Now I'm looking into solving the case of label overlaps. Options include:
|
@@ -65,10 +73,10 @@ function polylabel(polygon, precision, debug) { | |||
|
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.
} | |
// do not drill down further if there's no chance of a better solution | |
if ((cell.max - cell.distanceToCentroid * centroidWeight) - fitness(bestCell) <= precision) continue; | |
Here's my suggestion to fix a potentially code-breaking issue of this line.
Given that cell.max
represents the best possible result from a new cell to be compared, it should be equally adjusted as in the fitness(cell)
function (subtracting the weighted cell.distanceToCentroid
from the result to obtain the final "fitness".
Not making this change might result in an infinite loop, typically when thecentroidWeight
is much larger than the precision
. e.g. polylabel(polygon, 0.000001, false, 0.01);
Noting down some thoughts here from renewed interest in this space. The challenge at hand: optimize position of labels on a Streamgraph visualization. Existing solution in use there: d3-area-label. Desired solution: optimize angle of the labels as well, such that the size of the text can be maximized. Related work: Experimental Label Placement. This technique suffers from many drawbacks, but has some nice qualities as well. The reason why I'm posting here is that a solution idea emerged from discussions with @Fil , who suggested the following:
This idea is very promising. The polylabel-on-ellipse idea is proved out in Labeling with stretched Polylabel (updated). The aspect ratio of the ellipses for each label can be calculated based on the bounding box of the label text. The remaining work to implement the solution involves:
polylabelStretched = (rings, ratio) => {
const polygon = [];
for (const ring of rings) {
// stretch the input
const newRing = [];
for (const [x, y] of ring.map(projection)) newRing.push([x / ratio, y]);
polygon.push(newRing);
}
const result = polylabel(polygon, 0.5);
result[0] *= ratio; // stretch the result back
result.distance *= ratio;
return result;
}
Related: |
Hi just dove into this topic and have created a fork, Not really compatible with a merge as i deleted all the dependencies and enlist stuff but the file can ow be included directly in the browser. there is a demo at https://polylabel.pages.dev Let me know what you think feel free to improve it as the code needs another pass of clean up. Im using leaflet in this project and leaflet-labeler which resolves the collisions. |
Closes #33
A fresh take on #49
Before (look at Montana, South Dakota, and Kansas):
After (with
centroidWeight
of 0.01):The screenshots are from stamen/pleth#13, if you'd like to experiment or see it in action.
Summary of changes:
centroidWeight
.fitness
function that penalizes distance from centroid, according tocentroidWeight
.