Fill Area Between Multiple Lines in Plot

Matplotlib fill between multiple lines

If you start the plot in point (0, 0), and therefore do not need to consider the area of the polygon not in the first quadrant, then this should do the trick in this particular situation:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(0,10,0.1)

# The lines to plot
y1 = 4 - 2*x
y2 = 3 - 0.5*x
y3 = 1 -x

# The upper edge of polygon (min of lines y1 & y2)
y4 = np.minimum(y1, y2)

# Set y-limit, making neg y-values not show in plot
plt.ylim(0, 5)

# Plotting of lines
plt.plot(x, y1,
x, y2,
x, y3)

# Filling between line y3 and line y4
plt.fill_between(x, y3, y4, color='grey', alpha='0.5')
plt.show()

Sample Image

Fill area between multiple lines in plot

Here is an approach:

a = data.frame(time = c(1:100), x = rnorm(100))
b = data.frame(time = c(1:100), y = rnorm(100))
c = data.frame(time = c(1:100), z = rnorm(100))

calculate the pmin and pmax:

min_a <- pmin(a, b, c)
max_a <- pmax(a, b, c)

construct the polygon as usual:

polygon(c(c$time, rev(c$time)), c(max_a$x ,rev(min_a$x)), col = rgb(1, 0, 0,0.5) )

Sample Image

or using ggplot:

library(tidyverse)
data.frame(a, b, c) %>% #combine the three data frames
group_by(time) %>% # group by time for next step
mutate(max = max(x, y, z), # calculate max of x, y, z in each time
min = min(x, y, z)) %>% #same as above
select(-time.1, - time.2) %>% #discard redundant columns
gather(key, value, 2:4) %>% #convert to long format so you can color by key in the geom line call
ggplot()+
geom_ribbon(aes(x = time, ymin= min, ymax = max), fill= "red", alpha = 0.3)+
geom_line(aes(x = time, y = value, color = key))

Sample Image

Filling In the Area Between Two Lines with a Custom Color Gradient

You could draw a colored rectangle covering the curves. And use the polygon created by fill_between to clip that rectangle:

import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import numpy as np

x = np.linspace(0, 10, 200)
y1 = np.random.normal(0.02, 1, 200).cumsum() + 20
y2 = np.random.normal(0.05, 1, 200).cumsum() + 50

cm1 = LinearSegmentedColormap.from_list('Temperature Map', ['blue', 'red'])

polygon = plt.fill_between(x, y1, y2, lw=0, color='none')
xlim = (x.min(), x.max())
ylim = plt.ylim()
verts = np.vstack([p.vertices for p in polygon.get_paths()])
gradient = plt.imshow(np.linspace(0, 1, 256).reshape(-1, 1), cmap=cm1, aspect='auto', origin='lower',
extent=[verts[:, 0].min(), verts[:, 0].max(), verts[:, 1].min(), verts[:, 1].max()])
gradient.set_clip_path(polygon.get_paths()[0], transform=plt.gca().transData)
plt.xlim(xlim)
plt.ylim(ylim)
plt.show()

clipping imshow by fill_between polygon

A more complicated alternative, would color such that the upper curve corresponds to red and the lower curve to blue:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 200)
y1 = np.random.normal(0.02, 1, 200).cumsum() + 20
y2 = np.random.normal(0.05, 1, 200).cumsum() + 50

polygon = plt.fill_between(x, y1, y2, lw=0, color='none')
ylim = plt.ylim()
verts = np.vstack([p.vertices for p in polygon.get_paths()])
ymin, ymax = verts[:, 1].min(), verts[:, 1].max()
gradient = plt.imshow(np.array([np.interp(np.linspace(ymin, ymax, 200), [y1i, y2i], np.arange(2))
for y1i, y2i in zip(y1, y2)]).T,
cmap='turbo', aspect='auto', origin='lower', extent=[x.min(), x.max(), ymin, ymax])
gradient.set_clip_path(polygon.get_paths()[0], transform=plt.gca().transData)
plt.ylim(ylim)
plt.show()

gradient between two curves

A variant could be to smooth out the color values in the horizontal direction (but still clip using the original curves):


from scipy.ndimage import gaussian_filter

gradient = plt.imshow(np.array([np.interp(np.linspace(ymin, ymax, 200), [y1i, y2i], np.arange(2))
for y1i, y2i in zip(gaussian_filter(y1, 4, mode='nearest'),
gaussian_filter(y2, 4, mode='nearest'))]).T,
cmap='turbo', aspect='auto', origin='lower', extent=[x.min(), x.max(), ymin, ymax])

gradient between two curves, smoothed

Fill area between two lines with Python

Here it is:

import matplotlib.pyplot as plt
import numpy as np
a = [[12, 17, 26, 32, 34], [235.910888671875, 245.84429931640625, 211.8711395263672, 226.2964630126953, 222.0032501220703]]
b = [[12, 26, 34], [235.910888671875, 211.8711395263672, 222.0032501220703]]

plt.plot(a[0], a[1], c='r')
plt.plot(b[0], b[1], c='r')
plt.fill_between(a[0], a[1],np.min(a[1]))
plt.fill_between(b[0], b[1],np.min(a[1]),color='white')
plt.show()

Pic

How to fill area between two sets of data connected by line?

You have to use the ydata as arguments for your fill_between, not the curves.

Either use ydata directly, or get them from your curve1/2 objects like ydata=curve1.get_ydata().

Here is an example adapted from the docs:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-5, 5, 0.01)
y1 = -5*x*x + x + 10
y2 = 5*x*x + x

c1, = plt.plot(x, y1, color='black')
c2, = plt.plot(x, y2, color='black')

# If you want/have to get the data form the plots
# x = c1.get_xdata()
# y1 = c1.get_ydata()
# y2 = c2.get_ydata()

plt.fill_between(x, y1, y2, where=y2 >y1, facecolor='yellow', alpha=0.5)
plt.fill_between(x, y1, y2, where=y2 <=y1, facecolor='red', alpha=0.5)
plt.title('Fill Between')

plt.show()

In the end you get:

Sample Image

R - Fill area between lines based on top value

The reason you're getting the wrong shading is probably because the data is a bit on the coarse side. My advice would be to interpolate the data first. Assuming dat1 is from your example.

library(ggplot2)

# From long data to wide data
dat2 <- tidyr::pivot_wider(dat1, values_from = value, names_from = var)

# Setup interpolated data (tibble because we can then reference column x)
dat3 <- tibble::tibble(
x = seq(min(dat2$month), max(dat2$month), length.out = 1000),
rainfall = with(dat2, approx(month, rainfall, xout = x)$y),
evaporation = with(dat2, approx(month, evaporation, xout = x)$y)
)

Then, we need to find a way to identify groups, and here is a helper function for that. Group IDs are based on the runs in run length encoding.

# Make function to identify groups
rle_id <- function(x) {
x <- rle(x)
rep.int(seq_along(x$lengths), x$lengths)
}

And now we can plot it.

ggplot(dat3, aes(x)) +
geom_ribbon(aes(ymin = pmin(evaporation, rainfall),
ymax = pmax(evaporation, rainfall),
group = rle_id(sign(rainfall - evaporation)),
fill = as.factor(sign(rainfall - evaporation))))

Sample Image

Created on 2021-02-14 by the reprex package (v1.0.0)

Fill area between two lines drawn with plot()

how about ggplot geom_ribbon?

library(ggplot2)

set.seed(1)
df <- data.frame(
x = seq(1,100),
ymin = rnorm(100,10,3),
ymax = rnorm(100,22,2)
)

ggplot(df,aes(x=x))+
geom_line(aes(x,ymin),color="red")+
geom_ribbon(aes(ymin=ymin,ymax=ymax),fill="lightblue")+
geom_line(aes(x=x,y=ymax),color="black")

Sample Image

Fill area between 2 lines (when one is below another)

If you look at the comprehension you are using to calculate where to fill, you'll notice it only checks at the points listed in your y and z lists. However, there are regions in between those points that need to be filled as well.

This behavior is mentioned in the documentation:

Semantically, where is often used for y1 > y2 or similar. By default, the nodes of the polygon defining the filled region will only be placed at the positions in the x array. Such a polygon cannot describe the above semantics close to the intersection. The x-sections containing the intersecion are simply clipped


You need interpolate=True:

Setting interpolate to True will calculate the actual intersection point and extend the filled region up to this point

plt.fill_between(
x,y,z,
where=[(y[i]<z[i]) for i in range(len(x))],
facecolor='r',
interpolate=True
)

Sample Image

Since you also asked for a way to avoid having a list of 5, you may use axhline instead, as well as switching your lists to numpy arrays for easy comparison:

import matplotlib.pyplot as plt
import numpy as np

x = np.array([1,2,3,4,5,6,7,8,9,10])
y = np.array([4,9,1,3,6,2,4,7,6,3])
z = 5

plt.plot(x,y)
plt.axhline(y=z, color='orange')
plt.fill_between(x,y,z,where=y<z, facecolor='r', interpolate=True)

plt.show()

Plotly - Fill area between three lines - How do I remove this dark shadows

  • have simulated data to be able to re-produce
  • there were two issues with you code
    • two traces were defined as mode="text" have changed to mode="lines"
    • only want fill="tonexty" on percentile traces
import plotly.graph_objects as go
import pandas as pd
import numpy as np

df = pd.DataFrame({"date": pd.date_range("30-mar-2022", "15-may-2022")}).assign(
val=lambda d: np.random.randint(1, 10, len(d))
)

df = df.join(
df["val"]
.rolling(5)
.agg(["median", lambda w: np.percentile(w, 25), lambda w: np.percentile(w, 75)])
).dropna()
df.columns = ["date", "val", "Median", "25 Percentile", "75 Percentile"]

fig = go.Figure(layout_yaxis_range=[0, 10])

fig.add_traces(
go.Scatter(
x=df["date"],
y=df["25 Percentile"],
name="25th Percentile",
mode="lines",
line=dict(color="blue"),
# fill='tonext',
# fillcolor='grey',
connectgaps=False,
)
)

fig.add_traces(
go.Scatter(
x=df["date"],
y=df["Median"],
name="Median",
mode="lines",
line=dict(color="green"),
fill="tonexty",
fillcolor="#eaecee",
connectgaps=False,
)
)

fig.add_traces(
go.Scatter(
x=df["date"],
y=df["75 Percentile"],
name="75th Percentile",
mode="lines",
line=dict(color="blue"),
fill="tonexty",
fillcolor="#eaecee",
connectgaps=False,
)
)

fig.update_layout(template="plotly_white")
fig.show()

Sample Image

Graph with a shaded the area occupied by multiple lines

You just need to group per individual time unit and calculate the minimum / maximum values. This allows you to plot a geom_ribbon:

example %>% 
group_by(values) %>%
summarize(min = min(value), max = max(value)) %>%
ggplot() +
geom_ribbon(aes(x = values, ymin = min, ymax = max), size = 2,
fill = "#29c8e5", color = "black") +
theme_classic()

Sample Image

If you would rather have the ribbon overlying your original plot, you could do:

ribbon <- example %>% 
group_by(values) %>%
summarize(min = min(value), max = max(value))

graph1 +
geom_ribbon(aes(x = values, ymin = min, ymax = max),
data = ribbon, size = 0, fill = "#29c8e5",
color = NA, alpha = 0.3, inherit.aes = FALSE)

Sample Image

For what it's worth, I think the first option is more visually striking.



Related Topics



Leave a reply



Submit