Mapbox-gl-js: Variable text placement for point labels

Created on 25 Jul 2017  Â·  8Comments  Â·  Source: mapbox/mapbox-gl-js

The purpose of this is ticket is to discuss and document our experimentation with variable text placement for point labels, and to decide if we want to implement this in Mapbox GL as an alternative option to fixed label placements.

Generally, variable label placement is the ability for the renderer to assign different placements for a label in order to improve the likelihood that the label will be visible.

Implementing viewport label placement will unblock this ability. (Caveat: we can't implement this for api-gl, as cross-tile placements would not be predictable.)

Example placement workflow:

For each label:

  1. Try Placement Option 1.
  2. If using Option 1 will cause the label to not be placed (because it collides with an already placed label), try Placement Option 2.
  3. And so on through all specified placement options.
  4. If last placement option does not allow label to be placed, do not place label and move on to next label.

Benefits

Allowing variable label placements should result in label visibility that more closely matches a map designer's intent, as defined by the zoom level visibility and placement order of features in the data source + stylesheet. Specifically:

1. Improved label hierarchy

With a fixed label placement, it's common for a label that was attempted to be placed later (and therefore generally less important) to be visible while a label that was attempted to be placed earlier (and therefore generally more important) to be hidden. This is due to a collision between the label with earlier placement and another label with even earlier placement.

For example:

better-hierarchy

In this example, Label 1 is placed first. Label 2 is attempted to be placed second, but with the fixed placement, it collides with Label 1 and does not get placed. With a variable placement, using the second placement option (the right side) gives Label 2 space to be placed. Label 3 is attempted to be placed third, and with the fixed placement there is space for it to be placed since Label 2 was not placed. With the variable placement, Label 3 collides with Label 2 and does not get placed. Even though the number of labels visible is the same with fixed and variable placement in this example, variable placement improves the visibility of more important labels.

2. Improved label density

In addition, variable label placement can allow more labels overall to be visible, to more closely match the density of the data.

For example:

better-density

In this example, a fixed placement allows 2 out of 3 labels to be visible, while a variable placement allows all 3 labels to be visible.

Spec implementation

Text "placement" here refers specifically to a combination of 3 style spec layout properties:

  • text-anchor
  • text-justify
  • text-offset

Typical combinations of this are:
placement-options

Assuming that we want to allow users to control the variable placement options (which I think is fairly necessary for this to be useful), here's a few possible spec changes to consider:

Allow array values (stacks) for the 3 relevant properties

For these 3 properties, we could allow users to specify an array of values that are treated as a stack.

These 3 properties would need to be evaluated together, i.e. each array would be required to have the same length, and the corresponding position in each array would need to be applied together.

e.g. If these

  • text-anchor: [left, right, top, bottom]
  • text-justify: [left, right, center, center]
  • text-offset: [[0.5, 0], [-0.5, 0], [0, 0.5], [0, -0.5]]

Then the first placement option would be:

  • text-anchor: left
  • text-justify: left
  • text-offset: [0.5, 0]

Add new properties for text offsets

Our current implementation of the text-offset property uses x/y coordinates to define the offset, which means that offset distance and direction are defined together. We may want to explore the possibility of adding new properties that separate these two variables, e.g. text-offset-distance and text-offset-direction.

Benefits compared to using current text-offset property:

  • Easier to define variable placements
  • Easier to use a data field to define placement direction

Implementation considerations:

  • These require each other?
  • Use of these properties disables text-offset?
  • options for values:

    • text-offset-distance:



      • em (same as existing text-offset unit)


      • px



    • text-offset-direction:



      • not a property; offset direction automatically follow text-anchor


      • enum: same as the 9 text-anchor values + auto (follows text-anchor))


      • degrees (angle of rotation)



e.g.

  • text-justify: [left, right, center, center]
  • text-anchor: [left, right, top, bottom]
  • text-offset-distance: 20
  • text-offset-direction auto

Challenges

  • Performance impact of doing more placement calculations per label
  • Lack of parity between GL maps and raster maps created with api-gl (I'm currently looking into how much of an issue this might be for users)
  • Label instability

    • With the viewport label placement refactor, we'll be calculating placements much more frequently than our current once-a-zoom-level strategy – about every 50ms. Doing variable label placements so frequently may cause more instability, since placements may change every 50ms (and as always, a label placement/visibility change can cause cascading effects for labels placed after it). However, the new viewport label placement implementation also creates cross-fading for labels in all situations, which makes label changes much less jarring. Instability may therefore not actually be a big issue.

    • If it is an issue, @ansis and @chrisloer have discussed the possibility of doing label placement prioritization based on labels that are visible for the floor zoom level. Using text-optional: true for dense, low priority point labels (e.g. small point-of-interest like stores and restaurant in Mapbox Streets vector tiles) may help with stability, particularly during rotation.

  • Same offset distance for different placements will not result in the same visual offset distance. For example, corner placements (top-left, etc) would look further offset. We would need to account for this.
  • Potentially unclear relationships between text and icons (more of a map design challenge than a rendering challenge).

Next steps

1. Prototype

@mollymerp is working on an initial mapbox-gl-js prototype in this branch, with placement logic hardcoded in the renderer.

2. Compare prototypes to fixed placement styling

Using two sets of features in Mapbox Streets vector tiles as case studies, I plan to compare the initial prototypes to our current fixed placement styling (using the new viewport placement implementation):

  • City labels with data-driven placement values.

    • We have a data field that defines the optimal placement as one of 8 possible directions. For the largest cities, these values were manually chosen based on visual inspection.
  • POI labels with a single placement value

    • Our POI label data, like most of our point label data (and probably most of our customers') do not contain any information about optimal placement, so we just place all POI text in one direction (below the icon).

3. Document more use cases

I've been mostly focused on evaluating variable label placement for use with our Mapbox Streets vector tiles. If you have a real or hypothetical use case for this, please comment below!

4. Explore options for spec implementation

I included a few initial ideas here, and will continue to round up feedback, alternatives suggestions, and important considerations.

Other variable placement options for future consideration

  • Vertical orientation for point labels as a variable placement option (particularly for Japanese maps)
  • Variable placements for line labels

/cc @mollymerp @chrisloer @ansis @mapbox/gl-core @ajashton @ericwolfe @peterliu
(Please cc anyone else who might be interested in this topic.)

cross-platform feature needs discussion

Most helpful comment

Implemented in #7596. 🎉

All 8 comments

A feature like this existed in CartoCSS (implemented by TileMill), which I used quite a lot. One tweak I would like to request is the ability to tweak the workflow:

  1. Try Placement Option 1.
  2. If using Option 1 will cause the label to not be placed (because it collides with an already placed label), try Placement Option 2.
  3. And so on through all specified placement options.
  4. If last placement option does not allow label to be placed,
    4a. If "text-alway-place" is false, do not place label and move on to next label.
    4b. If "text-always-place" is true, place the label at Placement Option 1, even though it collides.

My preference was generally that I want every label to be shown (especially important for static maps), so I want the mapping engine to try to avoid collisions, but to fallback to placing with a collision if necessary. CartoCSS only gave the ability to either avoid collisions (but maybe get no label) or allow collisions (and make no attempt to avoid them), so it'd be great to have a middle option.

Thanks for the feedback, @stevage! I'm not sure how straightforward or difficult that ability would be to implement – @ansis or @ChrisLoer would have a better sense. It might be straightforward to attempt adding the feature one more time after all placement options fail, with the first placement option and as if text-allow-overlap:true was assigned. (On the spec side it could perhaps be handled just by assigning text-allow-overlap: true along with the variable placement assignments, rather than a new property).

On the spec side it could perhaps be handled just by assigning text-allow-overlap: true along with the variable placement assignments, rather than a new property

Yeah, good thought. There are definitely use cases for having that flag on or off. General purpose maps for mass audiences probably value looking nice and avoiding any label collisions. Specialist maps for niche audiences probably value getting all the information shown one way or the other.

@nickidlugash Can the unit of text-offset be specified in terms of the icon next to which text is being placed instead of ems? In all the examples you have included above assume that the icon is smaller than 1emx1em. The style will break, the moment one decides to use larger icons.

This is an issue even today if the style uses larger icons. It would be nice if one could create robust "place text below icon layers" in the style that do not depend on icon size in ems.

Can the unit of text-offset be specified in terms of the icon next to which text is being placed instead of ems?

@mb12 An ideal unit for text offset is tricky because in practice, the ideal offset often needs to be relative to _both_ the icon size and the text size (or line width and text size, for offset line labels). Currently you can use icons larger than 1emx1em since you can specify a value of larger than 1, but would require a property function value if you want to use a variety of icon sizes within a single style layer. There are definite pain points to specifying offset values right now, as you've highlighted, so if we do consider implementing a new offset property, we'll need to take this into consideration. I suggested pixels as a alternative unit with the thought that an absolute unit in conjunction with the upcoming expressions syntax might make it easier to take into account both icon/line and text size.

This would tie in nicely with #6432 to ensure labels are drawn on screen if they would otherwise be cut offscreen.

We have a PR up implementing this! Please check it out: https://github.com/mapbox/mapbox-gl-js/pull/7596

@stevage the PR doesn't include the text-always-place flag you suggested, although there's nothing that would prevent us from doing that in the future. I think it's mainly a style-spec design question, as far as implementation goes it would be relatively straightforward.

Implemented in #7596. 🎉

Was this page helpful?
0 / 5 - 0 ratings

Related issues

stevage picture stevage  Â·  3Comments

aderaaij picture aderaaij  Â·  3Comments

muesliq picture muesliq  Â·  3Comments

rigoneri picture rigoneri  Â·  3Comments

Scarysize picture Scarysize  Â·  3Comments