4

I have the following piece of code

import plotly.express as px
import pandas as pd
import numpy as np

x = [1,2,3,4,5,6]

df = pd.DataFrame(
    {
        'x': x*3,
        'y': list(np.array(x)) + list(np.array(x)**2) + list(np.array(x)**.5),
        'color': list(np.array(x)*0) + list(np.array(x)*0+1) + list(np.array(x)*0+2),
    }
)

for plotting_function in [px.scatter, px.line]:
    fig = plotting_function(
        df,
        x = 'x',
        y = 'y',
        color = 'color',
        title = f'Using {plotting_function.__name__}',
    )
    fig.show()

which produces the following two plots:

enter image description here

enter image description here

For some reason px.line is not producing the continuous color scale that I want, and in the documentation for px.scatter I cannot find how to join the points with lines. How can I produce a plot with a continuous color scale and lines joining the points for each trace?

This is the plot I want to produce: enter image description here

user171780
  • 2,243
  • 1
  • 20
  • 43
  • There is a way to set your own line color for line graphs. What does it mean to set a continuous color scale for a line chart? Normally, a changing line graph is represented by a single color. And do I need a color bar? – r-beginners Oct 25 '21 at 03:39
  • 3
    The easiest way to do this is as follows. `fig = px.line(df, x='x', y='y', color='color', markers=True, color_discrete_sequence=['#0d0887','#d8576b','#f0f921'])` – r-beginners Oct 25 '21 at 03:43
  • A continuous color for a line chart is uncommon but I think it should still be supported. What about drawing trendlines through a very small cluster? – Derek O Oct 25 '21 at 05:00
  • 1
    I don't think it is uncommon. Any "slicing" of a 3D plot results in this, where one of the variables is turned into color. I am surprised that the `plotly.express.line` function does not support this by default. – user171780 Oct 25 '21 at 07:34
  • Due to the slightly different semantics (if you think about it) of a line plot, I think that using `color_discrete_sequence` (as pointed out by @r-beginners) is the most natural solution and indeed supported for a plotly express line chart! – matanster Jun 27 '22 at 13:33

3 Answers3

4

I am not sure this is possible using only plotly.express. If you use px.line, then you can pass the argument markers=True as described in this answer, but from the px.line documentation it doesn't look like continuous color scales are supported.

UPDATED ANSWER: in order to have both a legend that groups both the lines and markers together, it's probably simpest to use go.Scatter with the argument mode='lines+markers'. You'll need to add the traces one at a time (by plotting each unique color portion of the data one at a time) in order to be able to control each line+marker group from the legend.

When plotting these traces, you will need some functions to retrieve the colors of the lines from the continuous color scale because go.Scatter won't know what color your lines are supposed to be unless you specify them - thankfully that has been answered here.

Also you won't be able to generate a colorbar adding the markers one color at a time, so to add a colorbar, you can plot all of the markers at once using go.Scatter, but use the argument marker=dict(size=0, color="rgba(0,0,0,0)", colorscale='Plasma', colorbar=dict(thickness=20)) to display a colorbar, but ensure that these duplicate markers are not visible.

Putting all of this together:

# import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

x = [1,2,3,4,5,6]

df = pd.DataFrame(
    {
        'x': x*3,
        'y': list(np.array(x)) + list(np.array(x)**2) + list(np.array(x)**.5),
        'color': list(np.array(x)*0) + list(np.array(x)*0+1) + list(np.array(x)*0+2),
    }
)

# 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",
    )

fig = go.Figure()

## add the lines+markers
for color_val in df.color.unique():
    color_val_normalized = (color_val - min(df.color)) / (max(df.color) - min(df.color))
    # print(f"color_val={color_val}, color_val_normalized={color_val_normalized}")
    df_subset = df[df['color'] == color_val]
    fig.add_trace(go.Scatter(
        x=df_subset['x'],
        y=df_subset['y'],
        mode='lines+markers',
        marker=dict(color=get_color('Plasma', color_val_normalized)),
        name=f"line+marker {color_val}",
        legendgroup=f"line+marker {color_val}"
    ))

## add invisible markers to display the colorbar without displaying the markers
fig.add_trace(go.Scatter(
    x=df['x'],
    y=df['y'],
    mode='markers',
    marker=dict(
        size=0, 
        color="rgba(0,0,0,0)", 
        colorscale='Plasma', 
        cmin=min(df.color),
        cmax=max(df.color),
        colorbar=dict(thickness=40)
    ),
    showlegend=False
))

fig.update_layout(
    legend=dict(
    yanchor="top",
    y=0.99,
    xanchor="left",
    x=0.01),
    yaxis_range=[min(df.y)-2,max(df.y)+2]
)

fig.show()

enter image description here

Derek O
  • 16,770
  • 4
  • 24
  • 43
  • Thank you for your answer. Is it possible to somehow link each line to whatever `legendgroup` it has the corresponding scatter plot? So I can enable/disable both line and markers as a unique entity. – user171780 Oct 25 '21 at 06:11
  • Unfortunately I think that you can't have a legend for continuous color when using px.line - according to the documentation on legends [here](https://plotly.com/python/legend/#trace-types-legends-and-color-bars). For px.line you need to choose between discrete colors (which supports a legend) or continuous colors (which supports a colorbar) – Derek O Oct 25 '21 at 13:50
  • However, if you add each line and marker as an individual trace, I think you may be able to achieve the desired chart using `plotly.graph_objects` exclusively - I can get back to you on this – Derek O Oct 25 '21 at 13:52
2

You can achieve this using only 2 more parameters in px.line:

  • markers=True
  • color_discrete_sequence=my_plotly_continuous_sequence

The complete code would look something like this (Note the list slicing [::4] so that the colors are well spaced):

import plotly.express as px
import pandas as pd
import numpy as np

x = [1, 2, 3, 4, 5, 6]

df = pd.DataFrame(
    {
        'x': x * 3,
        'y': list(np.array(x)) + list(np.array(x) ** 2) + list(np.array(x) ** .5),
        'color': list(np.array(x) * 0) + list(np.array(x) * 0 + 1) + list(np.array(x) * 0 + 2),
    }
)

fig = px.line(
    df,
    x='x',
    y='y',
    color='color',
    color_discrete_sequence=px.colors.sequential.Plasma[::4],
    markers=True,
    template='plotly'
)
fig.show()

This produces the following output.

Plot

In case you have more lines than the colors present in the colormap, you can construct a custom colorscale so that you get one complete sequence instead of a cycling sequence:

rgb = px.colors.convert_colors_to_same_type(px.colors.sequential.RdBu)[0]

colorscale = []
n_steps = 4  # Control the number of colors in the final colorscale
for i in range(len(rgb) - 1):
    for step in np.linspace(0, 1, n_steps):
        colorscale.append(px.colors.find_intermediate_color(rgb[i], rgb[i + 1], step, colortype='rgb'))

fig = px.line(df_e, x='temperature', y='probability', color='year', color_discrete_sequence=colorscale, height=900)
fig.show()

Plot 2

Manuel Montoya
  • 1,206
  • 13
  • 25
0

Building on @manuel-montoya answer, the colormap generation code may be simplified using built in plotly.express.colors.sample_colorscale function, for example:

import numpy as np
import pandas as pd
import plotly.express as px

NO_LINES = 24
X_RANGE = 64
df = pd.DataFrame(
    {
        "c": np.arange(NO_LINES).repeat(X_RANGE),
        "x": np.tile(np.arange(X_RANGE), NO_LINES),
        "y": np.random.rand(X_RANGE * NO_LINES),
    }
)
df["y"] = 0.2 * df["x"] ** 1.2 + 2 * df["c"] + df["y"]

color_variable = "c"

myscale = px.colors.sample_colorscale(
    colorscale=px.colors.sequential.RdBu,
    samplepoints=len(df[color_variable].unique()),
    low=0.0,
    high=1.0,
    colortype="rgb",
)

px.line(
    df,
    x="x",
    y="y",
    markers=True,
    color=color_variable,
    color_discrete_sequence=myscale,
).show()

Result can be seen here. It does not display colormap, hovewer, but the code is much simpler than the accepted answer.

lukedrew
  • 1
  • 1