Plotly imshow reversing y labels reverses the image
Question:
I’d like to visualize a 20×20 matrix, where top left point is (-10, 9) and lower right point is (9, -10). So the x is increasing from left to right and y is decreasing from top to bottom. So my idea was to pass x labels as a list: [-10, -9 … 9, 9] and y labels as [9, 8 … -9, -10]. This worked as intended in seaborn (matplotlib), however doing so in plotly just reverses the image vertically. Here’s the code:
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
fig = px.imshow(img,
x=list(range(-10, 10)),
y=list(range(-10, 10)),
)
fig.show()
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
fig = px.imshow(img,
x=list(range(-10, 10)),
y=list(reversed(range(-10, 10))),
)
fig.show()
Why is this happening and how can I fix it?
EDIT: Adding seaborn code to see the difference. As you can see, reversing the range for labels only changes the labels and has no effect on the image whatsoever, this is the effect I want in plotly.
import seaborn as sns
import numpy as np
img = np.arange(20**2).reshape((20, 20))
sns.heatmap(img,
xticklabels=list(range(-10, 10)),
yticklabels=list(range(-10, 10))
)
import seaborn as sns
import numpy as np
img = np.arange(20**2).reshape((20, 20))
sns.heatmap(img,
xticklabels=list(range(-10, 10)),
yticklabels=list(reversed(range(-10, 10)))
)
Answers:
As I mentioned in my comment, the internal representation of px.imshow() is a heatmap. I coded your objective with a heatmap in a graph object. I didn’t change this for any clear reason, I just took a different approach because I couldn’t achieve it with px.imshow().
import plotly.graph_objects as go
img = np.arange(20**2).reshape((20, 20))
fig = go.Figure(data=go.Heatmap(z=img.tolist()[::-1]))
fig.update_yaxes(tickvals=np.arange(0, 20, 1), ticktext=[str(x) for x in np.arange(-10, 10, 1)])
fig.update_xaxes(tickvals=np.arange(0, 20, 1), ticktext=[str(x) for x in np.arange(-10, 10, 1)])
fig.update_layout(autosize=False, height=500, width=500)
fig.show()
You should use origin
:
origin (str, ‘upper’ or ‘lower’ (default ‘upper’)) – position of the [0, 0] pixel of the image array, in the upper left or lower left corner. The convention ‘upper’ is typically used for matrices and images.
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
x = list(range(-10, 10))
y = list(reversed(range(-10, 10)))
fig = px.imshow(img, x=x, y=y, origin='lower')
fig.show()
Why reversing the labels doesn’t work as expected ?
For single-channel arrays (2d, not rgb), px.imshow()
builds a heatmap trace with by default the parameter autorange='reversed'
on the yaxis, as "the convention ‘upper’ is typically used for matrices". This takes form with the following line :
autorange = True if origin == "lower" else "reversed"
The thing is that the autorange
feature forces the orientation of the axis : when "reversed", the y’s should be increasing from top to bottom (increasing vs decreasing inferred according to input data, not the same behavior with non-numeric strings), which means the yaxis is flipped iff the range of y’s is not already reversed, in which case the whole is flipped vertically (any point (x, y) keeps its value). This is what you started with.
Now, if you reverse the labels in y
, it actually reverses the yaxis coordinates against the data (values should decrease as y increases), but by doing this, since the y range is now already inverted, the autorange doesn’t have to flip the image, so it results in the yaxis being reversed (expected) and the image being not flipped, hence compared to what you started with, the image goes from flipped
to unflipped (unexpected).
Alternatives
In this situation, to avoid any confusion, an alternative to the solution above would be to define a specific range
:
fig = px.imshow(img, x=x, y=y)
fig.update_yaxes(autorange=False, range=[-10.5, 9.5])
Or to have the data reoriented beforehand, in which case there is no need to reverse the y’s (and even less to specify tickvals/ticktext, unless by preference) :
img = img.tolist()[::-1] # reverse data
x = list(range(-10, 10))
y = list(range(-10, 10)) # instead of y
fig = px.imshow(img, x=x, y=y, origin='lower')
Output (same for the 3 code snippets)
I’d like to visualize a 20×20 matrix, where top left point is (-10, 9) and lower right point is (9, -10). So the x is increasing from left to right and y is decreasing from top to bottom. So my idea was to pass x labels as a list: [-10, -9 … 9, 9] and y labels as [9, 8 … -9, -10]. This worked as intended in seaborn (matplotlib), however doing so in plotly just reverses the image vertically. Here’s the code:
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
fig = px.imshow(img,
x=list(range(-10, 10)),
y=list(range(-10, 10)),
)
fig.show()
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
fig = px.imshow(img,
x=list(range(-10, 10)),
y=list(reversed(range(-10, 10))),
)
fig.show()
Why is this happening and how can I fix it?
EDIT: Adding seaborn code to see the difference. As you can see, reversing the range for labels only changes the labels and has no effect on the image whatsoever, this is the effect I want in plotly.
import seaborn as sns
import numpy as np
img = np.arange(20**2).reshape((20, 20))
sns.heatmap(img,
xticklabels=list(range(-10, 10)),
yticklabels=list(range(-10, 10))
)
import seaborn as sns
import numpy as np
img = np.arange(20**2).reshape((20, 20))
sns.heatmap(img,
xticklabels=list(range(-10, 10)),
yticklabels=list(reversed(range(-10, 10)))
)
As I mentioned in my comment, the internal representation of px.imshow() is a heatmap. I coded your objective with a heatmap in a graph object. I didn’t change this for any clear reason, I just took a different approach because I couldn’t achieve it with px.imshow().
import plotly.graph_objects as go
img = np.arange(20**2).reshape((20, 20))
fig = go.Figure(data=go.Heatmap(z=img.tolist()[::-1]))
fig.update_yaxes(tickvals=np.arange(0, 20, 1), ticktext=[str(x) for x in np.arange(-10, 10, 1)])
fig.update_xaxes(tickvals=np.arange(0, 20, 1), ticktext=[str(x) for x in np.arange(-10, 10, 1)])
fig.update_layout(autosize=False, height=500, width=500)
fig.show()
You should use origin
:
origin (str, ‘upper’ or ‘lower’ (default ‘upper’)) – position of the [0, 0] pixel of the image array, in the upper left or lower left corner. The convention ‘upper’ is typically used for matrices and images.
import numpy as np
import plotly.express as px
img = np.arange(20**2).reshape((20, 20))
x = list(range(-10, 10))
y = list(reversed(range(-10, 10)))
fig = px.imshow(img, x=x, y=y, origin='lower')
fig.show()
Why reversing the labels doesn’t work as expected ?
For single-channel arrays (2d, not rgb), px.imshow()
builds a heatmap trace with by default the parameter autorange='reversed'
on the yaxis, as "the convention ‘upper’ is typically used for matrices". This takes form with the following line :
autorange = True if origin == "lower" else "reversed"
The thing is that the autorange
feature forces the orientation of the axis : when "reversed", the y’s should be increasing from top to bottom (increasing vs decreasing inferred according to input data, not the same behavior with non-numeric strings), which means the yaxis is flipped iff the range of y’s is not already reversed, in which case the whole is flipped vertically (any point (x, y) keeps its value). This is what you started with.
Now, if you reverse the labels in y
, it actually reverses the yaxis coordinates against the data (values should decrease as y increases), but by doing this, since the y range is now already inverted, the autorange doesn’t have to flip the image, so it results in the yaxis being reversed (expected) and the image being not flipped, hence compared to what you started with, the image goes from flipped
to unflipped (unexpected).
Alternatives
In this situation, to avoid any confusion, an alternative to the solution above would be to define a specific range
:
fig = px.imshow(img, x=x, y=y)
fig.update_yaxes(autorange=False, range=[-10.5, 9.5])
Or to have the data reoriented beforehand, in which case there is no need to reverse the y’s (and even less to specify tickvals/ticktext, unless by preference) :
img = img.tolist()[::-1] # reverse data
x = list(range(-10, 10))
y = list(range(-10, 10)) # instead of y
fig = px.imshow(img, x=x, y=y, origin='lower')
Output (same for the 3 code snippets)