Calculate area under graph

Is there a way to calculate the area under the graph for user input start and end? And also have the varea color from selected values. I tried something below, but not sure how to have the code only iterate between start and end. Also not entirely sure how to change the colored area under the graph. Anyone have a clue?

import numpy as np
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, VArea, TextInput, CustomJS
from bokeh.layouts import column, row
from bokeh.plotting import show, figure

t = np.linspace(1, 101, 100)
A, n = 1, -0.65
y1 = np.zeros(100)
y2 = A*np.power(t, n)
source = ColumnDataSource(dict(t=t, y1=y1, y2=y2))

start = TextInput(value="0", title="Start", width = 200)
end = TextInput(value="100", title="End", width = 200)

callback = CustomJS(args=dict(source=source,
                             start = start,
                             end = end), code="""
    var data = source.data
    var t1 = start.value
    var t2 = end.value
    
    const closest = data.t.reduce((a, b) => {
        return Math.abs(b - t1) < Math.abs(a - t1) ? b : a;
        });        
    console.log(closest);
        
    var area = 0;
    var height = 1;
    console.log(t1, t2)
    for (var i = 0; i < (data.t.length - 1); i++) {
        console.log(data.y2[i+1], data.y2[i], data['y2'][i], 0.5 * (data.y2[i + 1] + data.y2[i]) * height)
        area += 0.5 * (data.y2[i + 1] + data.y2[i]) * height;
    }
    console.log(area)

    source.change.emit()
""")

start.js_on_change('value', callback)
end.js_on_change('value', callback)

plot = figure(title=None, plot_width=900, plot_height=300)

plot.varea(x="t", y1="y1", y2="y2", fill_color="#f46d43", source = source)

show(row(plot, column(start, end)))

Regarding finding start and end indices. You can either interpolate to get more precise values or you can just find the closest indices, like you do in your code (didn’t check if it works):

    var {data} = source;
    
    // Can be made faster if `data.t` is sorted and you use binary search.
    const find_closest_idx = (val) => {
        return data.t.reduce(({delta, idx}, curr, curr_idx) => {
            const curr_delta = Math.abs(curr - val);
            return (delta === undefined || curr_delta < delta) ? {delta: curr_delta, idx: curr_idx} : {delta, idx};
        }, {}).idx;
    }
    
    const start_idx = find_closest_idx(start.value);        
    const end_idx = find_closest_idx(end.value);        

Now you can just iterate from start_idx to end_idx and get the relevant data points. Make sure that your code works if the indices are the same.

About computing the area - that’s just basic geometry, nothing special about it.
For each pair of consecutive indices, you have four points that define a trapezoid. Trapezoid area is easy to find. All that’s needed is to find areas of all trapezoids and combine them.

Regarding the color - I don’t see anything in your code that would change the color. And I feel like we’ve discussed it before. But maybe it was someone else. Either way, just save the result of plot.varea, pass it to the relevant CustomJS callback and then just set its .glyph.fill_color property.

1 Like

Hi p-himik,

Thanks for the help! (added the updated code below).
Regarding the color I meant that the area shown in the graph should only be colored from start to end values. Like this: (if start and end is 2, 4)

image

import numpy as np
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, VArea, TextInput, CustomJS
from bokeh.layouts import column, row
from bokeh.plotting import show, figure
from bokeh.io import output_file

output_file('areaundergraph.html')

t = np.linspace(1, 101, 1000)
A, n = 1, -0.65
y1 = np.zeros(1000)
y2 = np.zeros(1000) + 5# A*np.power(t, n)
source = ColumnDataSource(dict(t=t, y1=y1, y2=y2))

start = TextInput(value="0", title="Start", width = 200)
end = TextInput(value="100", title="End", width = 200)

plot = figure(title=None, plot_width=900, plot_height=300)
Varea = plot.varea(x="t", y1="y1", y2="y2", fill_color="#f46d43", source = source)

callback = CustomJS(args=dict(source=source,
                              Varea = Varea,
                              start = start,
                              end = end), code="""
    var data = source.data
    
    console.log(Varea)
    
    // Can be made faster if `data.t` is sorted and you use binary search.
    const find_closest_idx = (val) => {
        return data.t.reduce(({delta, idx}, curr, curr_idx) => {
            const curr_delta = Math.abs(curr - val);
            return (delta === undefined || curr_delta < delta) ? {delta: curr_delta, idx: curr_idx} : {delta, idx};
        }, {}).idx;
    }
    
    const start_idx = find_closest_idx(start.value);        
    const end_idx = find_closest_idx(end.value);    
            
    var area = 0;
    var height = 0.1;
    for (var i = start_idx; i <= (end_idx); i++) {    
        //console.log(i, i+1)
        //area += 0.5 * (data.y2[i] + data.y2[i-1]);
        area += 0.5 * (data.y2[i] + data.y2[i-1]) * height;
        
    }
    console.log(area)

    source.change.emit()
""")

start.js_on_change('value', callback)
end.js_on_change('value', callback)

show(row(plot, column(start, end)))

You will have to add another glyph that spans just from start to end for such coloring - varea's fill_color is not vectorized, so its value is applied to the whole glyph.

Hei,

Thanks for the help!:slight_smile:
If anyone was curious about the solution I added it below, or if you have any input to make it more elegant and faster :smiley:
Just need to figure out how to have it plot the area as text either outside the plot below the Textinput, or maybe as a title.

import numpy as np
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, VArea, TextInput, CustomJS, Band
from bokeh.layouts import column, row
from bokeh.plotting import show, figure
from bokeh.io import output_file

output_file('areaundergraph.html')

t = np.linspace(1, 101, 1000)
A, n = 1, -0.65
y1 = np.zeros(1000)
y2 = A*np.power(t, n)
source = ColumnDataSource(dict(t=t, y1=y1, y2=y2))
source_sel = ColumnDataSource(dict(t=t, y1=y1, y2=y2))

start = TextInput(value="0", title="Start", width = 200)
end = TextInput(value="100", title="End", width = 200)

plot = figure(title=None, plot_width=900, plot_height=300)
#Varea = plot.varea(x="t", y1="y1", y2="y2", fill_color="#f46d43", source = source)
plot.line(x="t", y="y2", line_color="#f46d43", source = source)

plot.varea(x="t", y1="y1", y2="y2", fill_color="black", source = source_sel)

callback = CustomJS(args=dict(source=source,
                              source_sel = source_sel, 
                              start = start,
                              end = end), code="""
    var data = source.data    
    var d1 = source_sel.data
    d1['t'] = [];
    d1['y1'] = [];
    d1['y2'] = [];
    
    // Can be made faster if `data.t` is sorted and you use binary search.
    const find_closest_idx = (val) => {
        return data.t.reduce(({delta, idx}, curr, curr_idx) => {
            const curr_delta = Math.abs(curr - val);
            return (delta === undefined || curr_delta < delta) ? {delta: curr_delta, idx: curr_idx} : {delta, idx};
        }, {}).idx;
    }
    
    const start_idx = find_closest_idx(start.value);        
    const end_idx = find_closest_idx(end.value);   
    
    var area = 0;
    var height = 0.1;
    var data1 = [];
    for (var i = start_idx; i <= (end_idx); i++) {    
        area += 0.5 * (data.y2[i] + data.y2[i-1]) * height;
        d1['t'].push(data.t[i])
        d1['y1'].push(data.y1[i])
        d1['y2'].push(data.y2[i])
        
    }
    console.log(area, d1)

    source.change.emit()
    source_sel.change.emit()
""")

start.js_on_change('value', callback)
end.js_on_change('value', callback)

show(row(plot, column(start, end)))
1 Like

Just need to figure out how to have it plot the area as text either outside the plot below the Textinput, or maybe as a title.

You can use a plain Div, put it right under the text inputs, and set its text accordingly. in your CustomJS callback.

1 Like

Hei,

Saw your message too late, I used paragrahp:P

import numpy as np
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, VArea, TextInput, CustomJS, Band
from bokeh.layouts import column, row
from bokeh.plotting import show, figure
from bokeh.io import output_file
from bokeh.models import Paragraph

output_file('areaundergraph.html')

t = np.linspace(0, 100, 1000)
A, n = 1, -0.65
y1 = np.zeros(1000)
y2 = A*np.power(t, n)
source = ColumnDataSource(dict(t=t, y1=y1, y2=y2))
source_sel = ColumnDataSource(dict(t=[], y1=[], y2=[]))

start = TextInput(value="0", title="Start", width = 200)
end = TextInput(value="100", title="End", width = 200)

plot = figure(title=None, plot_width=900, plot_height=300)
plot.line(x="t", y="y2", line_color="#f46d43", source = source)
#plot.scatter(x="t", y="y2", line_color="#f46d43", source = source)

plot.varea(x="t", y1="y1", y2="y2", fill_color="black", source = source_sel)

par1 = Paragraph(text="""Calculate the area by adding start and end time below:""",
width=200, height=50)
areaval = Paragraph(text="""Area =""",
width=200, height=50)

callback = CustomJS(args=dict(source=source,
                              source_sel = source_sel,
                              areaval = areaval,
                              start = start,
                              end = end), code="""
    var data = source.data    
    var d1 = source_sel.data
    d1['t'] = [];
    d1['y1'] = [];
    d1['y2'] = [];
    
    // Can be made faster if `data.t` is sorted and you use binary search.
    const find_closest_idx = (val) => {
        return data.t.reduce(({delta, idx}, curr, curr_idx) => {
            const curr_delta = Math.abs(curr - val);
            return (delta === undefined || curr_delta < delta) ? {delta: curr_delta, idx: curr_idx} : {delta, idx};
        }, {}).idx;
    }
    
    const start_idx = find_closest_idx(start.value);        
    const end_idx = find_closest_idx(end.value);
    
    var area = 0;
    var height = 0.1;
    var data1 = [];
    for (var i = start_idx; i <= (end_idx); i++) {    
        area += 0.5 * (data.y2[i] + data.y2[i-1]) * height;
        d1['t'].push(data.t[i])
        d1['y1'].push(data.y1[i])
        d1['y2'].push(data.y2[i])
        
    }
    areaval.attributes.text = 'Area = ' + String(area)

    source.change.emit()
    source_sel.change.emit()
    areaval.change.emit()
    
""")

start.js_on_change('value', callback)
end.js_on_change('value', callback)

show(row(plot, column(par1, start, end, areaval)))