We’ve improved text layout on our maps, with a special focus on right-to-left and ideographic scripts, like Arabic and Chinese. With readability as the first priority, we established a new algorithm for balancing lines of text and found the best workflow for line breaking complex lines of mixed languages.
The result draws from computer science, linguistics, and traditional cartography. If you’re making web maps with right-to-left text and want to try it out now, get the RTL plugin for Mapbox GL JS - but if you want to dive in and learn a few things about the subtle rules of polyglot typesetting, keep reading.
Greedy line breaking (the status quo)
Line breaking consists of finding all of the potential break points in a string of text, then deciding where to turn those potential break points into actual line breaks, with the goal of laying out an aesthetically pleasing label. Spaces, hyphens, newlines, and other natural gaps between words or parts of words are “potential break points,” or places where we can potentially put a linebreak. The status quo was a simple “greedy” algorithm: as we laid out each character in a label, we kept track of how far we had come since the start of the line, and if we had written past the max-width
defined in the map’s style, we would break at the next potential break point, reset our width counter, and keep going until we got to the end of the label.
Line breaking for bidirectional text
Let’s add two more complications to the problem of line breaking: bidirectional text and text shaping.
- Bidirectional text: English is written left to right, but other languages like Arabic and Hebrew are typeset mainly in the other direction, right to left, with exceptions for numbers and interjections of other languages. That’s why we call this bidirectional text rather than simply “right to left”: the direction of characters and the order of phrases can run in both directions. We use an algorithm called “BiDi” to reorder bidirectional text correctly for layout.
- Text shaping: is a procedure where software imitates the natural drawing of complex scripts, which have letters that depend on their context. Arabic is a great example of a language that requires shaping: a given character could be narrow or wide, and generally look quite different, depending on where it fits next to other letters.
Line breaking bidirectional text removes some of the assumptions we made before with simple greedy line breaking. Left-to-right order no longer matches its order top-to-bottom: if Arabic text comes before some English or romanized words, it should be to the right of them on a single line, but should be above them when split across multiple lines.
Before we can calculate how wide our lines are, we have to apply Arabic shaping rules to the input text, because the shaping can change the width of each character.
Here’s how you get it wrong by applying both text shaping and BiDi before determining line breaks. Arabic is moved to the right of English letters, which is correct when text is on one line, but it’s moved below English letters in line breaking, which is wrong.
The right way is to apply text shaping first, without applying BiDi, choose line break points, and then apply BiDi with the chosen break points. This lets us correctly calculate the width of text by applying text shaping, but only start re-ordering right-to-left text after breaking the text into logically-correct lines.
Line breaking ideographic text
So far we’ve been using spaces, hyphens, and other natural divisions in text as break points. What about a system that doesn’t have spaces, like Chinese?
Line-breaking Chinese is too easy: you don’t even need to split words, since you can get away with splitting the text between any two characters (ideally you’d avoid splitting “compound” words made out of two characters, but we don’t do that yet). Unfortunately, this makes really unbalanced lines possible - a map label might have a first line of 19 characters, and then the next line is just one lonely character. That makes labels ugly and hard to read.
To solve this problem, Xiaowei, Lucas, and Minh introduced a “balanced ideographic line breaking” mode, which would make every line in a Chinese label (almost) exactly the same width. This was the modification:
- Figure out how many lines will be necessary based on the length of the text and the
max-width
setting - Divide the total length of the text by the number of lines to get a new and shorter
max-width
(think of this as trimming a few characters off the top lines in order to fill in the bottom line) - Apply the greedy algorithm
Balancing also tends to open up more space on the map, allowing us to add more labels
This approach worked great for purely ideographic text, but it relied on every character being the same width, and on being able to put line breaks anywhere – so we couldn’t use it with labels that mixed in any non-ideographic text, even if that non-ideographic text was just a single piece of punctuation.
Generalizing “balanced ideographic line breaking”
Raggedness and Knuth-Plass
We wanted to get the balanced look for as many labels as possible, but we kept running into challenges introducing special cases one language at a time. Our talented designer Nicki Dlugash helped get us unstuck by focusing on typographic convention. She explained that, to a typographer, one of our primary goals was to avoid “raggedness,” or the appearance of unevenness between lines. That comment got some gears moving and awakened a dusty undergrad memory: Donald Knuth and Michael Plass “solved” line breaking way back in 1979.
In their paper, Knuth and Plass describe a clever algorithm for breaking paragraphs into fully justified lines of text while minimizing the amount of aesthetically unpleasant tweaking (altered spacing, hyphenation, etc.) necessary to make the words exactly fit the bounds of the page. The algorithm they describe is still widely used today as part of the LATEX typesetting software. The problem they solve is slightly different from ours: they’re fully justifying lines of text that tend to be page-width, while we’re trying to find better looking, but not fully justified, lines for relatively short labels. However, the ideas from Knuth-Plass translate well to our context.
We define a bunch of quantifiable “penalties” for doing ugly things, and then choose the set of line break points that leads to the lowest penalty score (least ugly = most beautiful). There are a bunch of types of penalties, but the one we focus on most is “raggedness,” which we define as “the square of the distance between the end of this line and the average line width for this whole label”. We square the distance because large deviations are uglier than small deviations: two lines 20 points away from the average width is much better than one line 40 points away from average and one line exactly at the average width.
Algorithmic complexity
A naive solution to this problem would be “try all possible line breaks, and choose the set with the lowest penalty.” Unfortunately that gets expensive quickly: for a text with n possible line breaks, you’ll evaluate 2n combinations. Finding ideal breaking for 16 characters of Chinese text would take 216 = 65,536 calculations. Computers are fast, but we can’t afford to do that for every label we show.
One key insight of the Knuth-Plass was to constrain the penalty calculation to look at each line in isolation from the lines before and after it. By preventing later lines from affecting the scores of earlier lines, we can calculate a score for a potential break point, and then store that score for use in future calculations.
For each potential line break point X, we:
- Consider every potential break point Y that came before X:
- Calculate the penalty score for the line that would be formed from Y to X.
- Add the penalty that we previously calculated for Y
- Store the result of the best choice of the potential break points we just calculated (e.g “From the space after ‘of’, the best prior break point is the space after ‘National’, and the total penalty score for that choice is 185”)
Because so much of the work is memoized, this algorithm only requires n2/2 calculations. For our 16-character Chinese label, that comes out to 128 calculations – and even our phones are fast enough for that.
What’s changed across the map
With this new algorithm, we can mix Chinese text with any other kind of text and end up with an overall balanced block. It also splits multiline labels in other scripts more evenly and obeys the max-width
limit more consistently. Previously, a long word that started just before max-width
could blow past the limit, but with the improved algorithm, the penalty of placing breaking the max-width
rule will cause it to move down. Labels with several words will tend to look more compact.
But these are all just heuristics, so the best way to understand the change is with an example:
How do we evaluate the aesthetic impact of an algorithm?
Computerizing maps made them much more powerful, but at the same time took a lot of control away from cartographers. Cartographers and typographers used to have complete control over line-breaking decisions, and they had their own body of knowledge about how to do it beautifully. Now we’re collapsing all of that into one little algorithm – that makes for a fun challenge, but it also means we need a healthy dose of humility.
Making nearly infinite maps forces us to implement an algorithm for what used to be an art, so our goals should be to:
- Incorporate as much cartography expertise as possible into the algorithm
- Make the algorithm intelligible to cartographers so that they can learn how to work with it skillfully
- Where practical, find ways to continue to pass more control to the cartographer
Having an excellent in-house design team here at Mapbox allows us to quickly develop solutions that try to satisfy these goals, but there will always be room for improvement. Ultimately, we depend on engagement from the mapping community to keep driving us forward.
These changes are in:
- GL JS
v0.31.0
and above (publicly available today) - iOS SDK
v3.5.0
(coming soon) - Android SDK
v5.0.0
(coming soon)
Interested in working on this kind of problem? Check out our jobs page.