Dynamic Text Positioning with Repel Geometries#
geom_text_repel()geom_label_repel()
These functions use a force-based layout algorithm to automatically reposition text labels and resolve overlaps.
Labels repel each other and their associated data points while staying within the plot boundaries.
import random
import pandas as pd
from lets_plot import *
LetsPlot.setup_html()
df = pd.read_csv("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/mtcars.csv")
print(df.shape)
df.head()
(32, 12)
| model | mpg | cyl | disp | hp | drat | wt | qsec | vs | am | gear | carb | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Mazda RX4 | 21.0 | 6 | 160.0 | 110 | 3.90 | 2.620 | 16.46 | 0 | 1 | 4 | 4 |
| 1 | Mazda RX4 Wag | 21.0 | 6 | 160.0 | 110 | 3.90 | 2.875 | 17.02 | 0 | 1 | 4 | 4 |
| 2 | Datsun 710 | 22.8 | 4 | 108.0 | 93 | 3.85 | 2.320 | 18.61 | 1 | 1 | 4 | 1 |
| 3 | Hornet 4 Drive | 21.4 | 6 | 258.0 | 110 | 3.08 | 3.215 | 19.44 | 1 | 0 | 3 | 1 |
| 4 | Hornet Sportabout | 18.7 | 8 | 360.0 | 175 | 3.15 | 3.440 | 17.02 | 0 | 0 | 3 | 2 |
df1 = df[(2.0 < df["wt"])&(df["wt"] < 3.65)]
df2 = df[(2.0 < df["wt"])&(df["wt"] < 3)]
df2 = df2.assign(shape=df2["am"].map({0: 16, 1: 21}))
plot = ggplot(df1, aes("wt", "mpg", label="model")) + geom_point(color="red")
plot2 = ggplot(df2, aes("wt", "mpg", label="model")) + geom_point(color="red")
Comparison of geom_text() and geom_text_repel()#
gggrid([
plot2 + geom_text() + ggtitle("geom_text()"),
plot2 + geom_text_repel() + ggtitle("geom_text_repel()"),
])
geom_label_repel()#
All of the parameters discussed below apply equally to both geom_text_repel() and geom_label_repel(). For simplicity, we will use geom_text_repel() in the examples.
gggrid([
plot2 + geom_label() + ggtitle("geom_label()"),
plot2 + geom_label_repel(seed=42) + ggtitle("geom_label_repel()"),
])
seed parameter#
Controls the randomization to produce the same label layout each time the plot is generated.
gggrid([
plot2 + geom_text_repel(seed=1) + ggtitle("seed=1"),
plot2 + geom_text_repel(seed=10) + ggtitle("seed=10"),
], ncol=2)
In some cases, it may be necessary to find a seed value that produces a more optimal label arrangement. A simple approach is to re-render the plot multiple times until you’re satisfied with the result, then use the corresponding seed to reproduce it.
max_iter parameter#
Controls the maximum number of iterations used by the layout algorithm, helping to reduce notebook rendering time. More iterations generally lead to better label placement, but at the cost of increased computation time. For plots with a small number of labels, 200–300 iterations are often sufficient. The default value is 2000.
seed = 530
gggrid([
plot2 + geom_text_repel(seed=seed, max_iter=2) + ggtitle("max_iter=2"),
plot2 + geom_text_repel(seed=seed, max_iter=20) + ggtitle("max_iter=20"),
plot2 + geom_text_repel(seed=seed, max_iter=200) + ggtitle("max_iter=200"),
])
max_time parameter#
Another way to limit plot rendering time is by using the max_time parameter. This primarily serves as a safeguard against excessive computation when a large number of text labels are involved. Time is specified in seconds. The default value is 5 seconds, but you can disable the time limit by setting it to -1 if needed.
gggrid([
plot2 + geom_text_repel(seed=seed, max_time=0.001) + ggtitle("max_time=0.001"),
plot2 + geom_text_repel(seed=seed, max_time=0.01) + ggtitle("max_time=0.01"),
plot2 + geom_text_repel(seed=seed, max_time=-1) + ggtitle("max_time=-1"),
])
direction parameter#
Restricts the movement of a text label relative to its anchor point to a specific direction. The default value is both.
gggrid([
plot2 + geom_text_repel(seed=seed, direction="x") + ggtitle("direction = 'x'"),
plot2 + geom_text_repel(seed=seed, direction="y") + ggtitle("direction = 'y'"),
plot2 + geom_text_repel(seed=seed, direction="both") + ggtitle("direction = 'both'"),
])
As we can see, this option is of limited use for randomly scattered points, but in certain cases it can be extremely helpful:
plotX = ggplot(df2, aes(x="wt", label="model")) + \
geom_point(y=1, color="red") + xlim(2, 3) + ylim(1, 1.3) + \
geom_text_repel(
y=1,
nudge_y=0.05,
direction="x",
angle=90,
hjust=0.0,
seed=seed
)
plotY = ggplot(df2, aes(y="mpg", label="model")) + \
geom_point(x=1, color="red") + xlim(0.9, 1.3) + ylim(19, 35) + \
geom_text_repel(
x=1,
nudge_x=0.05,
direction="y",
hjust=0.0,
seed=seed
)
gggrid([
plotX + ggtitle("direction = x"),
plotY + ggtitle("direction = y"),
])
point_padding and box_padding parameters#
These parameters control the amount of spacing around text labels.
point_paddingadds space between the label and all nearby points, but does not affect spacing between labels.box_paddingadds space between labels, but does not affect spacing between the label and the data point.
gggrid([
plot2 + geom_text_repel(seed=seed, max_time=-1, point_padding=10) + ggtitle("point_padding"),
plot2 + geom_text_repel(seed=seed, max_time=-1, box_padding=10) + ggtitle("box_padding"),
])
max_overlaps parameter#
Specifies the maximum allowed number of overlaps with other labels. Labels that exceed this threshold will be omitted from the plot. The default value is 10. You can disable overlap filtering entirely by setting this parameter to -1.
gggrid([
plot + geom_text_repel(seed=seed, max_time=-1, max_overlaps=5) + ggtitle("max_overlaps=5"),
plot + geom_text_repel(seed=seed, max_time=-1, max_overlaps=-1) + ggtitle("max_overlaps=-1"),
])
min_segment_length parameter#
Sets the minimum length for the line connecting a label to its associated point. Lines shorter than this length will not be drawn. To display all lines, use the default value of 0. To hide all lines, set the value to something very large.
min_segment_length uses the same units as point_size, so be careful when using min_segment_length together with size_unit (see below).
gggrid([
plot + geom_text_repel(seed=seed, max_time=-1, min_segment_length=0) + ggtitle("min_segment_length=0"),
plot + geom_text_repel(seed=seed, max_time=-1, min_segment_length=9999) + ggtitle("min_segment_length=9999"),
])
Point settings#
geom_text_repel() does not draw points itself, but for it to work correctly, the values of parameters and aesthetics that control point size must match those used in the associated geom_point() layer.
ggplot(df2, aes("wt", "mpg", label="model")) + theme(legend_position="none") + \
scale_size(range=[0.5,1], guide="none") + \
scale_stroke(range=[1,4], guide="none") + \
scale_shape_identity() + \
geom_point(aes(size="gear", stroke="vs", shape="shape"), size_unit="y") + \
geom_text_repel(aes(point_size="gear", point_stroke="vs", shape="shape"), size_unit="y", seed=seed)
point_size aesthetic#
Allows you to pass to geom_text_repel() the data used to determine point sizes in a geom_point() layer. This helps accurately detect overlaps between labels and points when point sizes vary.
plot31 = ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(aes(size="gear"), color="red") + \
theme(legend_position="none")
gggrid([
plot31 + geom_text_repel(seed=seed, max_time=-1) + ggtitle("without point_size"),
plot31 + geom_text_repel(aes(point_size="gear"), seed=seed, max_time=-1) + ggtitle("with point_size"),
])
You can also provide a constant value instead.
plot32 = ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(size=10, color="red") + \
theme(legend_position="none")
gggrid([
plot32 + geom_text_repel(seed=seed, max_time=-1) + ggtitle("without point_size"),
plot32 + geom_text_repel(seed=seed, max_time=-1, point_size=10) + ggtitle("with point_size"),
])
Set point_size=0 to prevent label repulsion away from data points.
Labels will still move away from each other and away from the edges of the plot.
plot33 = ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(color="red")
gggrid([
plot33 + geom_text_repel(seed=seed, max_time=-1) + ggtitle("without point_size"),
plot33 + geom_text_repel(seed=seed, max_time=-1, point_size=0) + ggtitle("with point_size=0"),
])
point_stroke and shape aesthetics#
Allow you to pass to geom_text_repel() the data used to determine point stroke width and shape in a geom_point() layer. This ensures accurate collision detection between labels and points.
plot34 = ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(aes(stroke="gear"), shape=21, size=10, color="red") + \
theme(legend_position="none")
gggrid([
plot34 + geom_text_repel(seed=seed, max_time=-1, point_size=10) + ggtitle("without point_stroke"),
plot34 + geom_text_repel(aes(point_stroke="gear"), shape=21, point_size=10, seed=seed, max_time=-1) + ggtitle("with point_stroke"),
])
size_unit parameter#
The size_unit parameter can be used in geom_point() to define the unit of measurement for the size aesthetic. In this case, it is recommended to also use size_unit in geom_text_repel() to ensure that point sizes are calculated correctly.
plot4 = ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(size=1, size_unit="y", color="red") + \
theme(legend_position="none")
gggrid([
plot4 + geom_text_repel(seed=seed, max_time=-1, point_size=1) + ggtitle("without size_unit"),
plot4 + geom_text_repel(seed=seed, max_time=-1, point_size=1, size_unit="y") + ggtitle("with size_unit"),
]) + ggtb()
segment_color aesthetic#
Allows you to specify the color of the line connecting the label to the point. By default, the line color matches the text color and follows the color aesthetic. In the example below, the color aesthetic is defined globally for all layers, so the colors of the points, text, and lines are the same.
ggplot(df2, aes("wt", "mpg", label="model", color="wt")) + geom_point() + geom_text_repel(seed=seed, max_time=-1)
By using the color and segment_color aesthetics together, you can assign different colors to the points, labels, and connecting lines.
plot6 = ggplot(df2, aes("wt", "mpg", label="model")) + theme(legend_position="none")
gggrid([
plot6 + geom_point(aes(color="wt")) + \
geom_text_repel(aes(segment_color="wt"), seed=seed, max_time=-1) + \
ggtitle("Same color for points and lines"),
plot6 + geom_point(color="red") + \
geom_text_repel(aes(color="wt"), seed=seed, max_time=-1) + \
ggtitle("Same color for text and line"),
plot6 + geom_point(color="red") + \
geom_text_repel(color="green", segment_color="blue", seed=seed, max_time=-1) + \
ggtitle("Different colors"),
])
segment_alpha aesthetic#
Specifies the transparency level of the connecting lines between labels and points. By default, the segment transparency inherits from the text and is governed by the alpha aesthetic.
ggplot(df2, aes("wt", "mpg", label="model")) + \
geom_point(color="red") + \
geom_text_repel(point_padding=20, color="red", segment_alpha=0.1, seed=seed, max_time=-1)
segment_size aesthetic#
Specifies the width of the line connecting the label to the point.
plot2 + geom_text_repel(segment_size=2, seed=seed, max_time=-1)
linetype aesthetic#
ggplot(df2, aes(y="mpg", label="model")) + geom_point(x=1, color="red") + xlim(0.9, 1.3) + ylim(19, 35) + \
geom_text_repel(
aes(linetype="disp"),
x=1,
nudge_x=0.2,
direction="y",
hjust=0.0,
seed=seed
)
arrow parameter#
ggplot(df2, aes(y="mpg", label="model")) + geom_point(x=1, color="red") + xlim(0.9, 1.3) + ylim(19, 35) + \
geom_text_repel(
x=1,
nudge_x=0.2,
direction="y",
hjust=0.0,
arrow=arrow(type="closed", angle=10, ends="both"),
seed=seed
)