Two proportional Y-axes without duplicating the data?

Thanks for the beautiful library!

Iโ€™d like to make a plot with two proportional y-axes: on the left one I want the original values, and on the right one I want the fraction of the total.

I could not find a way to do this without replicating the values to a second trace. I would have expected to be able to do that with the scaleratio parameters, does anyone know how to achieve that?

Below is what I currently use:

import pandas as pd
import plotly.graph_objs as go

obs = pd.Series({'A': 5, 'B':3})
# first trace = original values
data = [go.Bar(x=obs.index, y=obs, name='value')]

# The only way I could get the y-axis to scale properly is to create (and show!) another trace with the relative vales. Could we use scaleanchor/scaleratio instead?
data.append(go.Bar(x=obs.index, y=obs/obs.sum(), yaxis='y2', name='relative'))

layout = go.Layout(
    yaxis=dict(title='Value'),
    yaxis2=dict(title='Fraction of total', tickformat='%', overlaying="y1", side='right')
)

go.Figure(data=data, layout=layout)

and the result is this:

1 Like

Hi Marc, great to see you here :-). I gave it a try and here are some partial conclusions.

First, it seems that you need to have data matched to an axis for the axis to be displayed, even when setting the range of the axis or setting visible=True for the axis. However, you donโ€™t need to duplicate the whole axis, you can just add a dummy transparent scatter point as I did below.

As for axes, scaleanchor and scaleratio allow indeed to fix the scale ratio between two axis, but not the origin, so this is not what youโ€™re looking after here. Another solution can be to set explicitely the range of the two axes if you can live with this. A better (in my opinion) solution is to use the matches attribute of YAxis since this is really what you want to do: your second axis should match the first one, just with different tick labels. With this solution you have to tweak tick labels as I did below. Neither solution is perfect of course.

With an explicit range:

import pandas as pd
import plotly.graph_objs as go

obs = pd.Series({'A': 5, 'B':3})
# first trace = original values
fig = go.Figure(go.Bar(x=obs.index, y=obs, name='value', yaxis='y'))

ratio = 1. / obs.sum()
tick_pos = np.arange(0, obs.max() + 1)
tick_text = [str(ratio * pos * 100) + ' %' for pos in tick_pos]
fig.add_trace(go.Scatter(x=['A'], y=[1], yaxis='y2', marker_opacity=0, showlegend=False))
fig.update_layout(
    yaxis=dict(title='Value', range=(0, 5)),
    yaxis2=dict(title='Fraction of total', overlaying="y",
                side='right', 
                tickformat='%',
                range=(0, ratio*5),
                gridcolor='rgba(0, 0, 0, 0)', # transparent grid lines
               )
)

fig.show()

With matches attribute

import pandas as pd
import plotly.graph_objs as go

obs = pd.Series({'A': 5, 'B':3})
# first trace = original values
fig = go.Figure(go.Bar(x=obs.index, y=obs, name='value', yaxis='y'))


ratio = 1. / obs.sum()
tick_pos = np.arange(0, obs.max() + 1)
tick_text = [str(ratio * pos * 100) + ' %' for pos in tick_pos]
fig.add_trace(go.Scatter(x=['A'], y=[1], yaxis='y2', marker_opacity=0, showlegend=False))
fig.update_layout(
    yaxis=dict(title='Value'),
    yaxis2=dict(title='Fraction of total', overlaying="y",
                tickvals=tick_pos,
                ticktext=tick_text, 
                side='right', 
                tickformat='%',
                matches='y',
               )
)

fig.show()
1 Like

Hello @Emmanuelle, thank you for your quick and helpful answer!

For now Iโ€™ve tried the first approach, and it works well, and the resulting plot is nicer than what I had previously:

I will think about the second approach (but Iโ€™m not used to set the ticks manually, and I am also afraid of losing the auto ticks when I zoom inโ€ฆ)

As a follow-up, do you think we could/should extract one or two github issues from this? I mean:
a) yaxis2 is not shown when there is no data for that axis
b) scaleratio works but Iโ€™d like a way to match the zero as well

Let me know what you think. And many thanks again!

Hi again, point a) is related to https://github.com/plotly/plotly.js/issues/3487 and https://github.com/plotly/plotly.js/issues/4137 and you might want to cite these two issues if you open a new one. By the way reading #3487 helped to improve a bit the example

import pandas as pd
import plotly.graph_objs as go
import numpy as np

obs = pd.Series({'A': 5, 'B':3})
# first trace = original values
fig = go.Figure(go.Bar(x=obs.index, y=obs, name='value', yaxis='y'))

ratio = 1. / obs.sum()
tick_pos = np.arange(0, obs.max() + 1)
tick_text = [str(ratio * pos * 100) + ' %' for pos in tick_pos]
fig.add_trace(go.Scatter(x=[], y=[], yaxis='y2'))
fig.update_layout(
    yaxis=dict(title='Value', range=(0, 5)),
    yaxis2=dict(title='Fraction of total', overlaying="y",
                side='right', 
                tickformat='%',
                range=(0, ratio*5),
                gridcolor='rgba(0, 0, 0, 0)', # transparent grid lines
               )
)

fig.show()

b) is related to https://github.com/plotly/plotly.js/issues/3539 and https://github.com/plotly/plotly.js/issues/4187 so probably on the roadmap. b) - matching axes with scale ratio - could use some sponsoring if someone in this forum is interested.

Anyway I concur that it would be interesting to report about these issues @mwouts either in existing issues or in new ones (citing the old ones). Thank you very much!

1 Like