3

I have a simple data set and want to create a bar plot with two y-axis. I use the following code:

data = {'col 1': [-6, 18.6, 106.35, 111],
        'col 2': [-787.5, 976.5, 11246, 25682]}
df_overview = pd.DataFrame(data)
df_overview.index = ['A', 'B', 'C', 'D']
colors = ['#FF0000', '#0000FF']  # red, blue
columns = ['col 1', 'col 2']
fig = make_subplots(specs=[[{"secondary_y": True}]])
for i, col in enumerate(columns):
    fig.add_trace(
        go.Bar(x=df_overview.index, y=df_overview[col], name=col, marker_color=colors[i], offsetgroup=i,),
        secondary_y=(i == 0)
    )
fig.update_layout(
    barmode='group',
    font_size=14,
    hovermode="x unified",
)
fig.show()

enter image description here

But somehow, both y-axis do not align at zero. Is this possible with plotly-python? Can someone help please?

TRK
  • 85
  • 6
  • See if this gives you what you're looking for: `fig.update_yaxes(rangemode = 'tozero')` – Kat May 19 '23 at 13:46
  • Thank you for this suggestion. Unfortunately, the plot remains the same. – TRK May 19 '23 at 13:48
  • 2
    I can't seem to reproduce your problem. It's always best to provide a reproducible question since the data you're using has a huge impact on what happens. For now, could you tell me the literal ranges of each y-axis? (Greatest value - lowest value for each side) – Kat May 19 '23 at 14:15
  • I have updated the code above indicating how the dataset is created. For col 1 the min is -6 and the max is 111, and for col 2 the min is -787.5 and the max is 25682. Does this help? – TRK May 19 '23 at 14:29
  • 1
    Definitely...adding your data to your answer is even better...you did just add it right? I'm not losing my mind and it was there all along...hmm, looking at it now. – Kat May 19 '23 at 14:51
  • No worries, I've just added the data. Happy to hear your suggestions. – TRK May 19 '23 at 14:57

2 Answers2

3

This is a bit tricky because plotly calculates the default yaxis ranges under the hood using the following: [y_min-padding, y_max+padding] where padding=(y_max-y_min)/16.

Generally this will mean the zeros for yaxis1 and yaxis2 won't necessarily align (and most likely won't). However, we can solve for the padding of the yaxis2 by figuring out the relative location of the 0 on yaxis1. For example:

y1_min, y1_max = df_overview['col 2'].min(), df_overview['col 2'].max()
y1_padding = (y1_max - y1_min)/16
y1_range = [y1_min - y1_padding, y1_max + y1_padding]
y1_relative_zero = (0 - y1_range[0]) / (y1_range[1] - y1_range[0])

y1_relative_zero = 0.08200108720519005 meaning that the 0 on the y1 axis is roughly 8.2% of the way between the min and max. Now we can solve for the necessary padding on the second yaxis to ensure the 0 is also the same percent of the way between the min and max of the y2 data.

We need the solution to the following equation:

(0 - y2_range_min) / (y2_range_max - y2_range_min) = y1_relative_zero

where y2_range_min = y2_min - y2_padding and y2_range_max = y2_max + y2_padding.

I won't go into all the details since it's rearranging variables, but this is the solution:

y2_padding = (y1_relative_zero * (y2_max - y2_min) + y2_min) / (1 - 2*y1_relative_zero)

Putting this all together:

import pandas as pd
from plotly.subplots import make_subplots
import plotly.graph_objects as go

data = {'col 1': [-6, 18.6, 106.35, 111],
        'col 2': [-787.5, 976.5, 11246, 25682]}
df_overview = pd.DataFrame(data)
df_overview.index = ['A', 'B', 'C', 'D']
colors = ['#FF0000', '#0000FF']  # red, blue
columns = ['col 1', 'col 2']
fig = make_subplots(specs=[[{"secondary_y": True}]])
for i, col in enumerate(columns):
    fig.add_trace(
        go.Bar(x=df_overview.index, y=df_overview[col], name=col, marker_color=colors[i], offsetgroup=i,),
        secondary_y=(i == 0)
    )
fig.update_layout(
    barmode='group',
    font_size=14,
    hovermode="x unified",
)

y1_min, y1_max = df_overview['col 2'].min(), df_overview['col 2'].max()
y1_padding = (y1_max - y1_min)/16
y1_range = [y1_min - y1_padding, y1_max + y1_padding]
y1_relative_zero = (0 - y1_range[0]) / (y1_range[1] - y1_range[0])

y2_min, y2_max = df_overview['col 1'].min(), df_overview['col 1'].max()

## we solve the following equation:
# (0 - y2_range_min) / (y2_range_max - y2_range_min) = y1_relative_zero
# (0 - (y2_min - y2_padding)) / ((y2_max - y2_min) + 2*y2_padding) = y1_relative_zero
# y1_relative_zero * ((y2_max - y2_min) + 2*y2_padding) = (y2_padding - y2_min)
# y1_relative_zero * (y2_max - y2_min) + (y1_relative_zero*2*y2_padding) = (y2_padding - y2_min)
# (y2_padding - y2_min) - (y1_relative_zero*2*y2_padding) = y1_relative_zero * (y2_max - y2_min)
# y2_padding(1 - 2*y1_relative_zero) - y2_min = y1_relative_zero * (y2_max - y2_min)
y2_padding = (y1_relative_zero * (y2_max - y2_min) + y2_min) / (1 - 2*y1_relative_zero)
y2_range = [y2_min - y2_padding, y2_max + y2_padding]

fig.update_yaxes(range=y1_range, secondary_y=False)
fig.update_yaxes(range=y2_range, secondary_y=True)

fig.show()

enter image description here

Derek O
  • 16,770
  • 4
  • 24
  • 43
  • 1
    Wow, that's a very thorough and , I think, a universal solution to this. Thanks! – TRK May 19 '23 at 15:31
2

You've got several answers now, but I worked it out my own way and thought I would still throw in my 2 cents.

In this modification, I added yaxis2 to your fig.update_layout, using the above zero values to set the scale ratio between the y-axes. I then set the range to align their zeros.

Check it out.

fig.update_layout(
    barmode='group',
    font_size=14,
    hovermode="x unified",
    yaxis2 = dict(scaleanchor = "y", scaleratio = 25682/111, range = [-7.25, 115])
)

enter image description here

Kat
  • 15,669
  • 3
  • 18
  • 51
  • That's a simple solution. I see that you basically added: `scaleratio = df_overview['col 2'].max()/df_overview['col 1'].max()` to the layout function, right? Where is the `range = [-7.25, 115]` coming from? – TRK May 19 '23 at 15:30
  • Comment out the range & the zero for `yaxis2` is lower than `yaxis`. We needed the ratio below zero to be the same as above zero. That means you need less padding on the bottom of `yaxis2`. While I set it to `-7.25, 115`, Plotly adjusts to maintain that `scaleratio`. So it's a bit of a guess (the math is lengthy and unnecessary). You can see the actual ranges with `ff = fig.full_figure_for_development(); print(ff.layout.yaxis.range, ff.layout.yaxis2.range)`. Do this without then with the `range`, the ratio *below 0* goes from about 207 to 233 (25682/111 is about 233). Does that help? – Kat May 20 '23 at 14:08
  • Yes, that does make sense. Thank you very much for the explanation. – TRK May 22 '23 at 06:23