Consider the following dataframe, called data
:
Only two elements of the "teacher" column appear twice, the others appear once only.
I make a bar plot with Plotly Express:
import plotly.express as px
px.bar(data.sort_values("start_time", ascending=False), x="teacher", y="start_time", color="start_time",
color_continuous_scale="Bluered", barmode="group")
I would like to have bars next to each other, rather than stacked. I think that px
stacks them (contrary to the behaviour in their docs) because I do not have the same number of occurrences for each teacher.
- Is that correct?
- How can I fix it?
CodePudding user response:
So assuming you want to preserve the colorbar, and have stacked bars, you'll need a more complicated workaround.
You can use plotly.express to plot the first bar so that you get the colorbar, then use fig.add_trace
to add the second bar as a graph_object
. When you add the second bar, you will need to specify the color and to do that, you'll need some helper functions such as normalize_color_val
that converts the y-value of this bar to a normalized color value relative to the data on a scale of 0 to 1, and get_color
which returns the color of the bar (as an rgb string) when you pass the colorscale name and normalized value.
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
data = pd.DataFrame(
{'teacher':['Lingrand','Milanesio','Menin','Malot','Malot','Schminke','Cornelli','Milanesio','Marchello','Menin','Huet'],
'start_time':[12,12,5,0,5,0,4,8,-1,0,4]}
)
# This function allows you to retrieve colors from a continuous color scale
# by providing the name of the color scale, and the normalized location between 0 and 1
# Reference: https://stackoverflow.com/questions/62710057/access-color-from-plotly-color-scale
def get_color(colorscale_name, loc):
from _plotly_utils.basevalidators import ColorscaleValidator
# first parameter: Name of the property being validated
# second parameter: a string, doesn't really matter in our use case
cv = ColorscaleValidator("colorscale", "")
# colorscale will be a list of lists: [[loc1, "rgb1"], [loc2, "rgb2"], ...]
colorscale = cv.validate_coerce(colorscale_name)
if hasattr(loc, "__iter__"):
return [get_continuous_color(colorscale, x) for x in loc]
return get_continuous_color(colorscale, loc)
# Identical to Adam's answer
import plotly.colors
from PIL import ImageColor
def get_continuous_color(colorscale, intermed):
"""
Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
color for any value in that range.
Plotly doesn't make the colorscales directly accessible in a common format.
Some are ready to use:
colorscale = plotly.colors.PLOTLY_SCALES["Greens"]
Others are just swatches that need to be constructed into a colorscale:
viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)
:param colorscale: A plotly continuous colorscale defined with RGB string colors.
:param intermed: value in the range [0, 1]
:return: color in rgb string format
:rtype: str
"""
if len(colorscale) < 1:
raise ValueError("colorscale must have at least one color")
hex_to_rgb = lambda c: "rgb" str(ImageColor.getcolor(c, "RGB"))
if intermed <= 0 or len(colorscale) == 1:
c = colorscale[0][1]
return c if c[0] != "#" else hex_to_rgb(c)
if intermed >= 1:
c = colorscale[-1][1]
return c if c[0] != "#" else hex_to_rgb(c)
for cutoff, color in colorscale:
if intermed > cutoff:
low_cutoff, low_color = cutoff, color
else:
high_cutoff, high_color = cutoff, color
break
if (low_color[0] == "#") or (high_color[0] == "#"):
# some color scale names (such as cividis) returns:
# [[loc1, "hex1"], [loc2, "hex2"], ...]
low_color = hex_to_rgb(low_color)
high_color = hex_to_rgb(high_color)
return plotly.colors.find_intermediate_color(
lowcolor=low_color,
highcolor=high_color,
intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
colortype="rgb",
)
def normalize_color_val(color_val, data=data):
return (color_val - min(data.start_time)) / (max(data.start_time - min(data.start_time)))
## add the first bars
fig = px.bar(
data.sort_values("start_time", ascending=False).loc[~data['teacher'].duplicated()],
x="teacher", y="start_time", color="start_time",
color_continuous_scale="Bluered", barmode="group"
)
## add the other bars, these will automatically be grouped
for x,y in data.sort_values("start_time", ascending=False).loc[data['teacher'].duplicated()].itertuples(index=False):
fig.add_trace(go.Bar(
x=[x],
y=[y],
marker=dict(color=get_color('Bluered', normalize_color_val(y))),
hovertemplate="teacher=%{x}<br>start_time=%{y}<extra></extra>",
showlegend=False
))
fig.show()