Labels above horizontal bars - better way than subplots?

Ive been using the following function from dkane.net/2020/better-horizontal-bar-charts-with-plotly to generate a horizontal bar graph with labels above each bar. This is needed because the labels in my case can get very long.

However, this takes many more seconds to render and the page almost completely freezes (heap from 20mb to 300mb) when I have more than about 80 rows.

Is there a better way?

from plotly.subplots import make_subplots

def horizontal_bar_labels(categories):
    subplots = make_subplots(
        rows=len(categories),
        cols=1,
        subplot_titles=[x["name"] for x in categories],
        shared_xaxes=True,
        print_grid=False,
        vertical_spacing=(0.45 / len(categories)),
    )

    # add bars for the categories
    for k, x in enumerate(categories):
        subplots.add_trace(dict(
            type='bar',
            orientation='h',
            y=[x["name"]],
            x=[x["value"]],
            text=["{:,.0f}".format(x["value"])],
            hoverinfo='text',
            textposition='auto',
            marker=dict(
                color="#7030a0",
            ),
        ), k+1, 1)

    # update the layout
    subplots['layout'].update(
        showlegend=False,
    )
    for x in subplots["layout"]['annotations']:
        x['x'] = 0
        x['xanchor'] = 'left'
        x['align'] = 'left'
        x['font'] = dict(
            size=12,
        )

    height_calc = 45 * len(categories)
    height_calc = max([height_calc, 350])

    return subplots

I have executed the code using the data from the site you are referring to. The graph was created immediately with no problems. My environment is plotly:5.11.0. Are you using the latest version?

Hi @r-beginners I have no problems creating the graphs, the issue is with performance when there are many rows

By the way, how many rows are executed?

Hi @lex1 welcome to the forums. You could separate the bars from each other and use annotations. I admit that it can get a bit cumbersome adjusting bargroupgap and figure height as well as the y- position of the annotations.

import plotly.graph_objects as go

# data
categories = [
    {"name": "Musée du Louvre, Paris", "value": 10200000},
    {"name": "National Museum of China, Beijing", "value": 8610092},
    {"name": "Metropolitan Museum of Art, New York City", "value": 6953927},
    {"name": "Vatican Museums, Vatican City", "value": 6756186},
    {"name": "Tate Modern, London", "value": 5868562},
    {"name": "British Museum, London", "value": 5820000},
    {"name": "National Gallery, London", "value": 5735831},
    {"name": "National Gallery of Art, Washington D.C.", "value": 4404212},
    {"name": "State Hermitage Museum, Saint Petersburg", "value": 4220000},
    {"name": "Victoria and Albert Museum, London", "value": 3967566},
]

fig = go.Figure(
    data=go.Bar(
            x=[d.get('value') for d in categories], 
            text=["{:,.0f}".format(d.get("value")) for d in categories],  
        ),
    layout={
        'bargroupgap':0.4, 
        'height': 600, 
        'yaxis':{'range':[-1, 10], 'visible': False},
    }
)

for idx, name in enumerate([d.get('name') for d in categories]): 
    fig.add_annotation(
        x=0,
        y=idx + 0.45,
        text=name,
        xanchor='left',
        showarrow=False,
        yshift=0
    )
fig.show()

creates:

2 Likes

@AIMPED Thanks! That worked perfectly, much appreciated.

1 Like

Interesting, thanks for posting @lex1! Would be interesting to try and build this as a standard mode, but two more options occur to me, that avoid the overhead of annotations:

  • Use explicit width and offset in the bar trace (these are the same attributes plotly.js uses under the hood when you have multiple grouped bar traces) and draw the tick labels over top of the plot. This is very flexible, but we don’t get the autorange right so you need to set the range manually:
import plotly.graph_objects as go

# data
categories = [
    {"name": "Musée du Louvre, Paris", "value": 10200000},
    {"name": "National Museum of China, Beijing", "value": 8610092},
    {"name": "Metropolitan Museum of Art, New York City", "value": 6953927},
    {"name": "Vatican Museums, Vatican City", "value": 6756186},
    {"name": "Tate Modern, London", "value": 5868562},
    {"name": "British Museum, London", "value": 5820000},
    {"name": "National Gallery, London", "value": 5735831},
    {"name": "National Gallery of Art, Washington D.C.", "value": 4404212},
    {"name": "State Hermitage Museum, Saint Petersburg", "value": 4220000},
    {"name": "Victoria and Albert Museum, London", "value": 3967566},
]

fig = go.Figure(
    data=go.Bar(
        x=[d.get('value') for d in categories], 
        y=[d.get('name') for d in categories],
        orientation='h',
        width=0.5,
        offset=-0.65,
        texttemplate="%{x:,.0f}"
    ),
    layout={
        'height': 600, 
        'yaxis':{'anchor': 'free', 'side': 'right', 'range': [-0.8, 9.3]},
    }
)
fig
  • Display the labels as a separate dummy trace with barmode: 'group' - this is more automatic, but doesn’t give you as much flexibility to size the labels and bars separately.
import plotly.graph_objects as go

# data
categories = [
    {"name": "Musée du Louvre, Paris", "value": 10200000},
    {"name": "National Museum of China, Beijing", "value": 8610092},
    {"name": "Metropolitan Museum of Art, New York City", "value": 6953927},
    {"name": "Vatican Museums, Vatican City", "value": 6756186},
    {"name": "Tate Modern, London", "value": 5868562},
    {"name": "British Museum, London", "value": 5820000},
    {"name": "National Gallery, London", "value": 5735831},
    {"name": "National Gallery of Art, Washington D.C.", "value": 4404212},
    {"name": "State Hermitage Museum, Saint Petersburg", "value": 4220000},
    {"name": "Victoria and Albert Museum, London", "value": 3967566},
]

fig = go.Figure(
    data=[go.Bar(
        x=[d.get('value') for d in categories], 
        y=[d.get('name') for d in categories],
        orientation='h',
        texttemplate="%{x:,.0f}"
    ),
    go.Bar(
        x=[0] * len(categories), 
        y=[d.get('name') for d in categories],
        orientation='h',
        texttemplate="%{y}",
        textposition="outside"
    )],
    layout={
        'barmode': 'group',
        'height': 600, 
        'yaxis':{'visible': False},
        'showlegend': False
    }
)
fig
3 Likes

Hi @alexcjohnson thanks for the great alternatives! I assume the loop of annotations takes more time or ties up the thread?

It would be amazing to have this natively implemented! Thanks for noticing #6389

When trying these however, I ran into a different wall for each.

For the offset method, my row numbers vary from 1 to hundreds depending on a user dropdown. So i had to have a clunky formula to guess the required yaxis range. Perhaps I’m missing something here?

Otherwise, this would have been my preferred option as my chart is a combination of stacked offset (which is also a requested feature here #4914

For the additional trace option, everything was ok except for when using logarithm axis scale with some negative values results on the label going back to right anchoring.

I’m trying to use a number of fairly dynamic charts where the user has a lot of control on the data input, scales, etc :sweat_smile:

I assume the loop of annotations takes more time or ties up the thread?

Yeah I haven’t tried to quantify it, but annotations are a bit heavier than axis labels or bars. Given that you’re only going to have at most a couple hundred maybe it’s fine. The first place I’d expect it to cause problems is lagging while you actively pan or zoom the axis.

So i had to have a clunky formula to guess the required yaxis range. Perhaps I’m missing something here?

Nope, that sounds about right!

For the additional trace option, everything was ok except for when using logarithm axis scale with some negative values results on the label going back to right anchoring.

Not quite sure what you mean by right anchoring, but zero or negative values on a log scale are always tricky to deal with. You could try giving some small positive value instead of 0 in x=[0] * len(categories) and setting that value explicitly as the minimum of the axis range.

1 Like