You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Not sure if we have an open issue for this but it would be great to have more-automatic text positioning to avoid text overlapping on e.g. scatters.
Reacted by Matt Fehskens, Kyle King, Steven Nicolaou, Zhujin (Connor) Li, Jaeyoon Song, DarkPatrick, rafael, Frederik, Pietro D'Antuono, ismirsehregal and 46 more
Reacted by Janosh Riebesell, Phuc Nguyen, Daniel Vecera and Colin Dean
As a workaround some people might find this useful. This code from ckjellson is intended for matplotlib but can be adapted to work with dash. I've done that for this dash app and I've included a demo in the readme to show it working. I'm new to dash and a recent graduate so I'm sure there are plenty of issues.
The basic idea is to get the x and y limits in a callback html.Div(id='limits', style={'display': 'none'}):
Then to calculate the x and y range per pixel dcc.Store(id='xy-per-pixel') (this and the last callback could be combined, but I need two in my app as I calculate x_pixels from the screen width):
@app.callback(
Output('xy-per-pixel', 'data'),
Input('limits', 'children'),
prevent_initial_call=True
)
def xy_per_pixel(limits):
"""Calculates the x and y range per pixel""
limits = literal_eval(limits)
x_lims, y_lims = limits[0], limits[1]
x_pixels = y_pixels = 700 # this assumes your graph is a 700x700 plot
x_per_pixel = (x_lims[1] - x_lims[0]) / x_pixels
y_per_pixel = (y_lims[1] - y_lims[0]) / y_pixels
return [x_per_pixel, y_per_pixel]
Then the allocate_text function from the linked repo can be modified to work with dash as below:
def allocate_text(
x,
y,
text_list,
fig,
x_lims,
y_lims,
x_per_pixel,
y_per_pixel,
font,
text_size,
x_scatter: Union[np.ndarray, List[float]] = None,
y_scatter: Union[np.ndarray, List[float]] = None,
margin: float = 0.00,
min_distance: float = 0.0075,
max_distance: float = 0.07,
draw_lines: bool = True,
linecolor: str = "grey",
draw_all: bool = True,
nbr_candidates: int = 100,
):
"""
Args:
x: a list of x coordinates for the labels.
y: a list of y coordinates for the labels.
text_list: a list of strings for the labels.
fig (_type_): plotly dash figure.
x_lims: the x limits of the plot.
y_lims: the y limits of the plot.
x_per_pixel: the x range per pixel.
y_per_pixel: the y range per pixel.
font (_type_): a pillow ImageFont object
text_size (int): size of text.
x_scatter (Union[np.ndarray, List[float]], optional): x-coords of all scattered points in plot 1d array/list.
y_scatter (Union[np.ndarray, List[float]], optional): y-coords of all scattered points in plot 1d array/list.
text_size (int): size of text.
margin (float, optional): parameter for margins between objects. Increase for larger margins to points and lines.
min_distance (float, optional): parameter for min distance between text and origin.
max_distance (float, optional): parameter for max distance between text and origin.
draw_lines (bool, optional): draws lines from original points to text-boxes.
linecolor (str, optional): color code of the lines between points and text-boxes.
draw_all (bool, optional): Draws all texts after allocating as many as possible despite overlap.
nbr_candidates (int, optional): Sets the number of candidates used.
"""
# Ensure good inputs
x = np.array(x)
y = np.array(y)
assert len(x) == len(y)
if x_scatter is not None:
assert y_scatter is not None
if y_scatter is not None:
assert x_scatter is not None
assert len(y_scatter) == len(x_scatter)
x_scatter = x_scatter
y_scatter = y_scatter
assert min_distance <= max_distance
assert min_distance >= margin
# Create boxes in original plot
original_boxes = []
for x_coord, y_coord, s in zip(x, y, text_list):
w, h = font.getlength(s) * x_per_pixel, text_size * y_per_pixel
original_boxes.append((x_coord, y_coord, w, h, s))
# Process extracted textboxes
if x_scatter is None:
scatterxy = None
else:
scatterxy = np.transpose(np.vstack([x_scatter, y_scatter]))
non_overlapping_boxes, overlapping_boxes_inds = get_non_overlapping_boxes(
original_boxes,
x_lims,
y_lims,
margin,
min_distance,
max_distance,
nbr_candidates,
draw_all,
scatter_xy=scatterxy,
)
if draw_lines:
for x_coord, y_coord, w, h, s, ind in non_overlapping_boxes:
x_near, y_near = find_nearest_point_on_box(
x_coord, y_coord, w, h, x[ind], y[ind]
)
if x_near is not None:
fig.add_annotation(
dict(
x=x[ind],
y=y[ind],
ax=x_near,
ay=y_near,
showarrow=True,
arrowcolor=linecolor,
text="",
axref='x',
ayref='y'
)
)
for x_coord, y_coord, w, h, s, ind in non_overlapping_boxes:
fig.add_annotation(
dict(
x=x_coord,
y=y_coord,
showarrow=False,
text=s,
font=dict(size=text_size),
xshift=w / (2 * x_per_pixel),
yshift=h / (2 * y_per_pixel),
)
)
if draw_all:
for ind in overlapping_boxes_inds:
fig.add_annotation(
dict(
x=x[ind],
y=y[ind],
showarrow=False,
text=text_list[ind],
font=dict(size=text_size)
)
)
Where the font argument is a pillow ImageFont object:
from PIL import ImageFont
text_size = 10
font = ImageFont.truetype('assets/fonts/arial.ttf', text_size)
Hi - we are trying to tidy up the stale issues and PRs in Plotly's public repositories so that we can focus on things that are still important to our community. Since this one has been sitting for a while, I'm going to close it; if it is still a concern, please add a comment letting us know what recent version of our software you've checked it with so that I can reopen it and add it to our backlog. Alternatively, if it's a request for tech support, please post in our community forum. Thank you - @gvwilson
This is obviously still an issue. Text labels overlapping. Modern libraries like Syncfusion don't have this problem. Would be nice if Plotly can do the same. Using version 2.34
Reacted by Alex Engel, Dustin Cai and Christian Ganhör
Please, go and test it (still very early release, might have some rough edges). Please, let us know if you encounter any issue and open it in the repo!
cc @cpsievert: you mentioned ggrepel back in 2021; this is exactly that, implemented as an R wrapper over plotly.js. @nicolaskruchten@jackparmer@alexcjohnson: in case this is useful or could inform a native implementation. @DustinCai@camdecoster: this directly addresses what you were asking for.
Thanks for letting us know! We've got a big backlog of work, so I'm not sure when this will get prioritized. If someone wants to take a crack at it, we can provide support and prioritize a review. Someone else just mentioned this as an issue today, so here's another example of why this is needed (taken from a Codepen):
<scriptsrc="https://cdn.plot.ly/plotly-2.35.2.min.js"></script><divid="chart" style="width:700px;height:500px;"></div><script>// Sample data — mean and median land close together so labels overlapconstdata=[];for(leti=0;i<500;i++){data.push(Math.random()*10+Math.random()*10);}constmean=data.reduce((a,b)=>a+b,0)/data.length;constsorted=[...data].sort((a,b)=>a-b);constmedian=sorted[Math.floor(sorted.length/2)];consttrace={x: data,type: 'histogram',nbinsx: 30,marker: {color: 'steelblue'}};constlayout={title: 'Histogram with Overlapping Mean & Median Annotations',xaxis: {title: 'Value'},yaxis: {title: 'Frequency'},shapes: [{type: 'line',x0: mean,x1: mean,y0: 0,y1: 1,yref: 'paper',line: {color: 'red',width: 2,dash: 'dash'}},{type: 'line',x0: median,x1: median,y0: 0,y1: 1,yref: 'paper',line: {color: 'green',width: 2,dash: 'dot'}}],annotations: [{x: mean,y: 1,yref: 'paper',text: 'Mean: '+mean.toFixed(2),showarrow: true,arrowhead: 2,ax: 0,ay: -20,font: {color: 'red',size: 14},bgcolor: 'rgba(255,255,255,0.7)'},{x: median,y: 1,yref: 'paper',text: 'Median: '+median.toFixed(2),showarrow: true,arrowhead: 2,ax: 0,ay: -20,font: {color: 'green',size: 14},bgcolor: 'rgba(255,255,255,0.7)'}]};Plotly.newPlot('chart',[trace],layout);</script>
Hello all - I have a PR up for this feature in #8065. Please take it for a spin and let me know what you think! And thanks for all of the discussion above
Activity
Karim-53 commentedon May 26, 2022
One solution is to alternate the text position, see: plotly/plotly.py#925 (comment)
tprior01 commentedon Apr 18, 2023
As a workaround some people might find this useful. This code from ckjellson is intended for matplotlib but can be adapted to work with dash. I've done that for this dash app and I've included a demo in the readme to show it working. I'm new to dash and a recent graduate so I'm sure there are plenty of issues.
The basic idea is to get the x and y limits in a callback
html.Div(id='limits', style={'display': 'none'}):Then to calculate the x and y range per pixel
dcc.Store(id='xy-per-pixel')(this and the last callback could be combined, but I need two in my app as I calculatex_pixelsfrom the screen width):Then the allocate_text function from the linked repo can be modified to work with dash as below:
Where the font argument is a pillow ImageFont object:
phisanti commentedon Aug 24, 2023
Is there any timeframe for the implementation a similar feature to
ggrepelin R?DustinCai commentedon Jun 25, 2024
Anyone updates on this for Scattergeo maps?
gvwilson commentedon Jul 8, 2024
Hi - we are trying to tidy up the stale issues and PRs in Plotly's public repositories so that we can focus on things that are still important to our community. Since this one has been sitting for a while, I'm going to close it; if it is still a concern, please add a comment letting us know what recent version of our software you've checked it with so that I can reopen it and add it to our backlog. Alternatively, if it's a request for tech support, please post in our community forum. Thank you - @gvwilson
ceds commentedon Aug 26, 2024
This is obviously still an issue. Text labels overlapping. Modern libraries like Syncfusion don't have this problem. Would be nice if Plotly can do the same. Using version 2.34
Ferreira-B commentedon Nov 7, 2024
I'd say this is an important feature that other modern libraries already handle
camdecoster commentedon Feb 13, 2025
Does anyone have a good example showing this problem? I'm interested in all charts, though I imagine that this is most apparent on scatter plots.
cameronboroumand commentedon Dec 11, 2025
Tableau handles this well. Tableau visualization with labels "intelligently" visible:
Plotly figure:
volmart commentedon Feb 25, 2026
Another example:
DustinCai commentedon Feb 28, 2026
Any updates on this?
docs: add mtcars vignette tutorial
phisanti commentedon Mar 2, 2026
Hi everyone, we had this problem in our webapp for a long time. Thus, we recently coded up a solution:
https://github.com/bigomics/plotly.repel
Please, go and test it (still very early release, might have some rough edges). Please, let us know if you encounter any issue and open it in the repo!
phisanti commentedon Mar 2, 2026
cc @cpsievert: you mentioned ggrepel back in 2021; this is exactly that, implemented as an R wrapper over
plotly.js. @nicolaskruchten @jackparmer @alexcjohnson: in case this is useful or could inform a native implementation.@DustinCai @camdecoster: this directly addresses what you were asking for.
camdecoster commentedon May 14, 2026
Thanks for letting us know! We've got a big backlog of work, so I'm not sure when this will get prioritized. If someone wants to take a crack at it, we can provide support and prioritize a review. Someone else just mentioned this as an issue today, so here's another example of why this is needed (taken from a Codepen):
chriddyp commentedon Sep 22, 2026
Hello all - I have a PR up for this feature in #8065. Please take it for a spin and let me know what you think! And thanks for all of the discussion above