How to customize color based on values in two columns?

I’m using px and trying to play around with setting color based on categorical values in two columns, consider the below example

df = pd.DataFrame([
    dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28', Resource="Alex", Shift="night"),
    dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15', Resource="Alex", Shift="day"),
    dict(Task="Job D", Start='2009-04-18', Finish='2009-04-15', Resource="Alex", Shift="day"),
    dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30', Resource="Max", Shift="night"),
    dict(Task="Job B", Start='2009-06-20', Finish='2009-07-10', Resource="Max", Shift="night"),
    dict(Task="Job B", Start='2009-07-20', Finish='2009-08-30', Resource="Max", Shift="night"),

])

color_seq = {"Job A": "red", "Job B": "blue", "Job C": "green", "Job D": "magenta"}
fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Task", color_discrete_map=color_seq)
fig.show()

Currently, the colors are set based on Task only, what I want to achieve is to also take into consideration the Shift column. For example, I have a component to let the user show the “Shift” data by choosing “Day/Night”. If the user selects “Day”, then the bars with night shift will have black color, and the day shifts will still be rendered according to the color_seq, similarly, if the user selects “Night”, then the bars with day shift will have white color and the rest will have their original colors.

How can such a second condition be achieved? Or if it’s possible without needing to fiddle with the data? Any pointers would be appreciated!

Hi @Peilin ,

I think a second condition like that be achieved you could achieved by adding pattern_shape='Shift'.

And YES, using pattern_shape will not draw black/white bar, it just add some pattern.
It will not meet your requirements.

# set Shift column as pattern shape 
fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Task",pattern_shape="Shift",color_discrete_map=color_seq)

But after set pattern_shape property, you can modify the traces pattern and color data using for_each_trace .

For initial value I will set for Night shift (black) by checking out if there is no pattern applied it will set color black and the others are according to the color_seq.

# For initial value change to Night shift (black)
fig.for_each_trace(lambda trace: trace.update(marker_color="black",marker_pattern_shape="") if not trace.marker.pattern.shape else trace.update(marker_pattern_shape=""))

Create value to get list of color when option is selected , “day” or “night” shift.

def get_shift_color(shift_name):
	if shift_name == "day":
		# get color for Day shift
		return [color_seq[data.y[0]] if data.marker.color == "black" else "white"  for data in fig.data]

	elif shift_name == "night":
		# get back the color for Night shift from initial value
		return ["black" if data.marker.color == "black" else data.marker.color  for data in fig.data]

Lastly, implement dropdown menu that will modify the data of each trace (color and shape) using method="restyle".

# create dropdown button
buttons = []
for idx,shift_name in enumerate(df.Shift.unique().tolist()):
	print(shift_name)
	button = dict(label=shift_name,
                    method="restyle",
                    args=[{"marker.color": get_shift_color(shift_name),"marker.pattern.shape": ""} ],
            )
	buttons.append(button)

# set dropdown using update figure layout
fig.update_layout(
    updatemenus=[
        dict(
            active=0, # default option is index 0 (night shift)
            buttons=buttons,
        )
    ])

The result will be like this.

The complete code here.

df = pd.DataFrame([
    dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28', Resource="Alex", Shift="night"),
    dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15', Resource="Alex", Shift="day"),
    dict(Task="Job D", Start='2009-04-18', Finish='2009-04-15', Resource="Alex", Shift="day"),
    dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30', Resource="Max", Shift="night"),
    dict(Task="Job B", Start='2009-06-20', Finish='2009-07-10', Resource="Max", Shift="night"),
    dict(Task="Job B", Start='2009-07-20', Finish='2009-08-30', Resource="Max", Shift="night"),

])

color_seq = {"Job A": "red", "Job B": "blue", "Job C": "green", "Job D": "magenta"}

# set Shift column as pattern shape 
fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Task",pattern_shape="Shift",color_discrete_map=color_seq)

# For initial value change to Night shift (black)
fig.for_each_trace(lambda trace: trace.update(marker_color="black",marker_pattern_shape="") if not trace.marker.pattern.shape else trace.update(marker_pattern_shape=""))

def get_shift_color(shift_name):
	if shift_name == "day":
		# get color for Day shift
		return [color_seq[data.y[0]] if data.marker.color == "black" else "white"  for data in fig.data]

	elif shift_name == "night":
		# get back the color for Night shift from initial value
		return ["black" if data.marker.color == "black" else data.marker.color  for data in fig.data]

# create dropdown button
buttons = []
for idx,shift_name in enumerate(df.Shift.unique().tolist()):
	print(shift_name)
	button = dict(label=shift_name,
                    method="restyle",
                    args=[{"marker.color": get_shift_color(shift_name),"marker.pattern.shape": ""} ],
            )
	buttons.append(button)

# set dropdown using update figure layout
fig.update_layout(
    updatemenus=[
        dict(
            active=0, # default option is index 0 (night shift)
            buttons=buttons,
        )
    ])

fig.show()

Hope this help.

1 Like

Woah, thanks @farispriadi , this is some advanced hacks going on here! Will test it out and report back!