Search code examples
pythonplotlybar-chartaxis-labels

python plotly - rotating secondary X axis labels


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?


Solution

  • 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