How to fill disconnected areas


I’d like to fill the area between 2 lines, but with different colors according to the position of a line vis-a-vis the other. Here is an example of what it would look like:

I tried with varea but it cannot fill disconnected areas. How would you achieve that?


Here is one way, using patches:

Note this case is constructed to be simple for demonstration. Specifically, there are only two regions. The general problem is slightly more involved, you would need to split the mask arrays in to sub-arrays, based on where there are contiguous “runs”.

It’s also possible to do something similar with mutli_polygons, and that would afford the possibility of having all the red regions be “a single logical (disjoint) region” e.g. for purposes of hit-testing. It’s unclear if that is needed for your requirements.

I confess I don’t understand your code, especially why you reverse the order of data points…

Anyway, I tried to apply your snippet on a simple dataframe:

df = pd.DataFrame({"x":[1,2,3,4,5], "y0":[10,20,15,30,25], "y1":[14,22,11,32,20]})

source = ColumnDataSource(df)
p = figure(plot_width=400, plot_height=300)

red = np.where(df.y0 > df.y1)
xr = np.append(df.x.iloc[red], df.x.iloc[red][::-1])
yr = np.append(df.y0.iloc[red], df.y1.iloc[red][::-1])

green = np.where(df.y0 < df.y1)
xg = np.append(df.x.iloc[green], df.x.iloc[green][::-1])
yg = np.append(df.y0.iloc[green], df.y1.iloc[green][::-1])

p.patches(xs=[xr, xg], ys=[yr, yg], color=["red", "green"])

but it didn’t produce the right result:

Think of it as tracing the “bottom” border of the area in one direction, then tracing the “top” border of the area in the opposite direction, in order to end up where you started and make a closed polygon.

There are more than two regions in the data in your example above, so you will need to take care of the second part I mentioned above, i.e splitting into however many regions there actually are. It looks like there are two red regions and two green regions. So the final call to patches will reflect that, and xs, ys and color will all be length 4.

I am afraid I am not a numpy expert so I don’t anything for that at my fingertips. But as a benchmark for you, the above data should end up structured like this (I’ve used plain lists for simplicity):

# two red regions
xs_red   = [ [1,   2,  2,  1], [ 4,  4] ]
ys_red   = [ [10, 20, 22, 14], [30, 32] ]

# two green regions
xs_green = [ [ 3,  3], [ 5,  5] ]
ys_green = [ [11, 15], [20, 25] ]

# four total regions
xs    = xs_red + xs_green
ys    = ys_red + ys_green
color = ["red", "red", "green", "green"]

p = figure()
p.patches(xs, ys, color=color, line_width=3)

Though as I mentioned on GitHub, “areas” with only two points are not really sensible, and the results certainly don’t look useful IMO, even with increased line width

1 Like

One idea could be to define y values for all x for both red and green patches. However for low x-resolution the patches will not intersect at proper values (I have added some data points to your data in the example below).

df_org = pd.DataFrame({'x':[1,2,3,4,5], 'y0':[10,20,15,30,25], 'y1':[14,22,11,32,20]})
df = pd.DataFrame({'x':[1,2,2.3,3,3.65,4,4.3,5], 'y0':[10,20,19,15,25,30,28.5,25], 'y1':[14,22,19,11,25,32,28.5,20]})

df['y0>y1']=df.apply(lambda x:x['y1'] if x['y0']>x['y1'] else x['y0'], axis = 1)
x_patch = np.append(df['x'], df['x'][::-1])
y_red = np.append(df['y0'], df['y0>y1'][::-1])
y_green = np.append(df['y1'], df['y0>y1'][::-1])

p = figure(plot_width = 800, plot_height = 500)
p.line(x=df_org['x'], y=df_org['y0'], line_color = 'red')
p.scatter(x=df_org['x'], y=df_org['y0'], line_color = 'red', size = 6, marker = 'circle')
p.line(x=df_org['x'], y=df_org['y1'], line_color = 'green')
p.scatter(x=df_org['x'], y=df_org['y1'], line_color = 'green', size = 6, marker = 'square')

p.patches(xs=[x_patch, x_patch], ys = [y_red, y_green], color=['red', 'green'])

First, I’d just like to acknowledge I don’t this this is trivial to do. The data preparation is fairly tedious and there are various potential corner cases and the “right” answer might depend on details and specifics. It’d be helpful to know more about the real use-case. E.g. the picture you show is of a dense time series where one can probably (maybe) get away with not stitching intersections together. But the example code you provide is obviously very sparse data, where that approach looks bad. Which case is actually your need?

I’d also add, now that I’ve had a chance to think about his more: I still don’t think it’s reasonable to try to use views/filters for this[1], but it might be reasonable to build dual-shading into the varea directly as an option. E.g. adding an optional flipped_fill property that applies a different fill wherever the “upper” line is less than the “lower” line. That operation has well-defined scope, would be simple to document and for users to use, could be narrowly implemented in varea without much or any impact anywhere else, and most importantly could actually do the correct desired thing, e.g. taking intersections properly into account. I would encourage you to file a new GitHub Issue about this different approach.

  1. Apart from the complications, views can’t help with handling intersections truly properly, because the intersections may be in between two data points. They can also easily lead to “isolated” 2-point areas that look bad. Views just don’t fit the actual need here. ↩︎

Thanks for your help. What I expect is close to Jonas’ proposal, in principle it can fit to any kind of data. But yes, I admit that the data is provided was purely artificial.

Well, I didn’t know this was so hard to achieve with the current bokeh methods. Creating a “flipped_fill” option would clearly address my need, but it would look very artificial I guess.

Anyway, that’s not very serious, I can deal without it. What I suggest is wait whether other users have similar requests, and in that case a feature request could be added in github.

Can you elaborate on this statement? According to my understanding of your situation, it would be visually exactly what you are asking for (i.e. just like the candlestick chart at the top)?

Sure, but what I meant by “artificial” is that very few people would be probably interested in such an option. So, I wouldn’t dare to ask bokeh developers to code it just for me.

FWIW although I don’t recall many questions about this specifically to date, I think that kind of of indicator is fairly common especially on financial charts. I think it’s a reasonable ask tho I can’t speculate when it might be prioritized, an issue is a good first step to at least make sure it is not forgotten about.

Ok, I’ll create an issue then.

1 Like