3

Let's say I create a plotly figure like this:

x = [['A','A','B','B'], ['X','Y','X','Y']]
y = [1,2,3,2]
fig = go.Figure()
fig.add_bar(x=x, y=y)
fig.show()

I get this: enter image description here I want to rotate the secondary X labels ('A' and 'B'). I tried:

fig.update_xaxes(tickangle=45)

but this only rotates the 'X' and 'Y' labels. How can I do it?

1 Answer 1

3

Based on this discussion by the plotly team, it doesn't appear that the capability to rotate both labels for a multicategorical axis was implemented, because it would be difficult to control overlapping labels if the secondary label was long.

In your case, the best you could probably do is add the secondary labels as annotations. I replaced the each unique label with different numbers of spaces so they don't show up, but are still interpreted as another category by Plotly (e.g. 'A' is replaced by ' ', 'B' is replaced by ' ', and so on...).

Then instead of just placing down the labels where we know they should go, it's better to make a scalable function that determines where the secondary labels should be placed based on the number of secondary x-labels you have. It also rotates the labels as it places them down.

So I wrote a function that performs this workaround (and to demonstrate, I modified the number of secondary labels to show it works for a general case of categories and subcategories):

import plotly.graph_objects as go

## use placeholders so labels don't show up
## map each unique label to a different number of spaces
## extend the labels to show that the solution can be generalized
secondary_labels = ['A','A','A','B','B','C']

label_set = sorted(set(secondary_labels), key=secondary_labels.index)
label_mapping = {label:' '*i for i, label in enumerate(label_set)}
secondary_labels_mapped = [label_mapping[label] for label in secondary_labels]

x = [secondary_labels_mapped, ['X','Y','Z','X','Y','X']]
y = [1,2,3,4,2,4]

## source: https://www.geeksforgeeks.org/python-program-to-find-cumulative-sum-of-a-list/
def cumsum(lists):
    cu_list = []
    length = len(lists)
    cu_list = [sum(lists[0:x:1]) for x in range(0, length+1)]
    return cu_list[1:]

relative_lengths = {}
start_loc = []
for label in label_set:
    relative_lengths[label] = secondary_labels.count(label) / len(secondary_labels)

## get the length of each interval
end_lens = list(relative_lengths.values())
start_lens = [0] + end_lens[:-1]

end_locs = cumsum(end_lens)
start_locs = cumsum(start_lens)

annotation_locs = [(start + end) / 2 for start, end in zip(start_locs, end_locs)]

fig = go.Figure()
fig.add_bar(x=x, y=y)

for label, loc in zip(label_set,annotation_locs):
    fig.add_annotation(
        x=loc,
        y=0,
        xref="paper",
        yref="paper",
        text=label,
        showarrow=False,
    )

## rotate both the annotation angle and the xaxes angles by 45 degrees
## shift annotations down so they display on the axes
fig.update_annotations(textangle=45, yshift=-40)
fig.update_xaxes(tickangle=45)

fig.show()

enter image description here

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks, I'd appreciate an example.
@soungalo answer updated to include an example - hope this helps!
Nice workaround. Would be nice if there was direct control over each axis level though.
@soungalo I agree that would be nice. I think the plotly team hasn't figured out a way to give users this kind of control without leading to problems with the visualizations

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.