Plotly Python: Align X-Axes in a grouped bar chart with multiple Y-axis

北城以北 提交于 2019-12-08 07:42:57

问题


I have a grouped bar chart with two y axis each of them on a different scale. I am trying to align x-axis(y=0) of both the groups. I found few links link1 and link2 in which setting rangemode='zero' should work, however my data consists of negative values due to which I guess setting rangemode to zero isn't working.

Here is my code:

import plotly.offline as plt
import plotly.graph_objs as go
traces = [go.Bar(x=[1,2,3,4], y=[-1,2,-3,4], name='y actual'), 
          go.Bar(x=[1], y=[0], name='y dummy', hoverinfo='none', showlegend=False), 
          go.Bar(x=[1],y=[0],yaxis='y2', name='y2 dummy', hoverinfo='none', showlegend=False),
          go.Bar(x=[1,2,3,4], y=[22, 2, 13, 25], yaxis='y2', name='y2 actual')]
layout = go.Layout(barmode='group',
                   yaxis=dict(title='y actual', rangemode="tozero", anchor='x', overlaying='y2'),
                   yaxis2=dict(title='y2 actual', side='right', rangemode = "tozero", anchor='x'))
fig = go.Figure(data=traces, layout=layout)
plt.iplot(fig)

The plot generated by the above code:

How do I fix this?

Note: You can see two dummy traces in the code. I introduced them so that the two traces 'y actual' and 'y2 actual' don't overlay over one another. For more info on why I did that checkout this link


回答1:


A possible workaround:

set the range element for both graphs to be proportional to each other then the axes will line up. Basically, your problem is one axis has to show negative numbers and the other doesn't. By telling y2 to show negative numbers we obtain our goal.

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.graph_objs as go

init_notebook_mode(connected=True)

traces = [
    go.Bar(
        x=[1, 2, 3, 4], 
        y=[-1, 2, -3, 4], 
        name='y actual'
    ), 
    go.Bar(
        x=[1], 
        y=[0], 
        name='y dummy', 
        hoverinfo='none', 
        showlegend=False
    ), 
    go.Bar(
        x=[1],
        y=[0],
        yaxis='y2', 
        name='y2 dummy', 
        hoverinfo='none', 
        showlegend=False
    ),
   go.Bar(
       x=[1, 2, 3, 4], 
       y=[22, 2, 13, 25], 
       yaxis='y2', 
       name='y2 actual'
   )
]

# layout
layout = go.Layout(
    barmode='group',
    yaxis=dict(
        title='y actual', 
        rangemode="tozero", 
        #anchor='x', 
        overlaying='y2',
        side="left",
        range = [-4, 10]
    ),
    yaxis2=dict(
        title='y2 actual', 
        side='right', 
        rangemode = "tozero",
        #anchor='x',
        range = [-12, 30]
    )
)

# make fig
fig = go.Figure(data=traces, layout=layout)
iplot(fig)

This may be annoying to have to keep them in proportion but it will ensure their alignment.

to help automate the process you can use the following function to produce two ranges that are proportional to each other.

def make_proportional_intervals(a, b):
    """
    Given two list like objects, compute two proprotionally sized ranges.
    This function assumes the max value in both lists is positive and non-zero
    """
    min_a, min_b = min(a), min(b)
    max_a, max_b = max(a), max(b)
    if (min_a >=0) & (min_b >= 0):
        # provide a 20% cushion to the scale
        return [0, round(1.2*max_a)], [0, round(1.2*max_b)]
    else:
        if (min_a < min_b) & (max_a < max_b):
            n = -(-max_b // max_a)
            # n = math.ceil(max_b / max_a), if you cannot assume ints.
            return [min_a, max_a], [n*min_a, n*max_a]

        elif (min_b < min_a) & (max_b < max_a):
            n = -(-max_a // max_b)
            # n = math.ceil(max_b / max_a), if you cannot assume ints.
            return [n*min_b, n*max_b], [min_b, max_b]

        elif (min_b < min_a) & (max_a < max_b):
            n = max( -(-max_b // max_a), -(min_b // min_a) )
            return [min_b / n, max_b / n], [min_b, max_b]

        elif (min_a < min_b) & (max_b < max_a):
            n = max( -(-max_a // max_b), -(min_a // min_b) )
            return [min_a, max_a], [min_a / n, max_a / n]
        elif (min_a == min_b):
            m = max(max_a, max_b)
            return [min_a, m], [min_b,  m]
        elif max_a == max_b:
            m = min(min_a, min_b)
            return [m, max_a], [m, max_b]

This function assumes your values will be integers but if not you can import math and use math.ceil() instead of my integer division. I was avoiding adding any more imports. If you want to see this code in action I created an example in jupyter notebook that you can run multiple times to see how it handles different arrays.

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.graph_objs as go
import numpy as np

def make_proportional_intervals(a, b):
    """
    Given two list like objects, compute two proprotionally sized ranges.
    This function assumes the max value in both lists is positive and non-zero
    """
    min_a, min_b = min(a), min(b)
    max_a, max_b = max(a), max(b)
    if (min_a >=0) & (min_b >= 0):
        # provide a 20% cushion to the scale
        return [0, round(1.2*max_a)], [0, round(1.2*max_b)]
    else:
        if (min_a < min_b) & (max_a < max_b):
            n = -(-max_b // max_a)
            # n = math.ceil(max_b / max_a), if you cannot assume ints.
            return [min_a, max_a], [n*min_a, n*max_a]

        elif (min_b < min_a) & (max_b < max_a):
            n = -(-max_a // max_b)
            # n = math.ceil(max_b / max_a), if you cannot assume ints.
            return [n*min_b, n*max_b], [min_b, max_b]

        elif (min_b < min_a) & (max_a < max_b):
            n = max( -(-max_b // max_a), -(min_b // min_a) )
            return [min_b / n, max_b / n], [min_b, max_b]

        elif (min_a < min_b) & (max_b < max_a):
            n = max( -(-max_a // max_b), -(min_a // min_b) )
            return [min_a, max_a], [min_a / n, max_a / n]
        elif (min_a == min_b):
            m = max(max_a, max_b)
            return [min_a, m], [min_b,  m]
        elif max_a == max_b:
            m = min(min_a, min_b)
            return [m, max_a], [m, max_b]

init_notebook_mode(connected=True)

y0 = np.random.randint(-5, 35, 6)
y1 = np.random.randint(-7, 28, 6)

print(y0, y1)
range0, range1 = make_proportional_intervals(y0, y1)

traces = [
    go.Bar(
        x=[1, 2, 3, 4, 5, 6], 
        y=y0, 
        name='y actual'
    ), 
    go.Bar(
        x=[1], 
        y=[0], 
        name='y dummy', 
        hoverinfo='none', 
        showlegend=False
    ), 
    go.Bar(
        x=[1],
        y=[0],
        yaxis='y2', 
        name='y2 dummy', 
        hoverinfo='none', 
        showlegend=False
    ),
   go.Bar(
       x=[1, 2, 3, 4, 5, 6], 
       y=y1, 
       yaxis='y2', 
       name='y2 actual'
   )
]

# layout
layout = go.Layout(
    barmode='group',
    yaxis=dict(
        title='y actual', 
        rangemode="tozero", 
        #anchor='x', 
        overlaying='y2',
        side="left",
        range = range0
    ),
    yaxis2=dict(
        title='y2 actual', 
        side='right', 
        rangemode = "tozero",
        #anchor='x',
        range = range1
    )
)

fig = go.Figure(data=traces, layout=layout)
iplot(fig)

Again, this is all just a work around for the fact that you have negative numbers and cannot use rangemode = "tozero" as scene here. Perhaps the developers will add something in the future to rangemode to correct for this.



来源:https://stackoverflow.com/questions/50880980/plotly-python-align-x-axes-in-a-grouped-bar-chart-with-multiple-y-axis

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!