Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Upload a CSV file and read it in Bokeh Web app

I have a Bokeh plotting app, and I need to allow the user to upload a CSV file and modify the plots according to the data in it. Is it possible to do this with the available widgets of Bokeh? Thank you very much.

like image 484
ssm Avatar asked Nov 24 '16 20:11

ssm


2 Answers

As far as I know there is no widget native to Bokeh that will allow a file upload.

It would be helpful if you could clarify your current setup a bit more. Are your plots running on a bokeh server or just through a Python script that generates the plots?

Generally though, if you need this to be exposed through a browser you'll probably want something like Flask running a page that lets the user upload a file to a directory which the bokeh script can then read and plot.

like image 118
Qichao Zhao Avatar answered Nov 16 '22 22:11

Qichao Zhao


Although there is no native Bokeh widget for file input. It is quite doable to extend the current tools provided by Bokeh. This answer will try to guide you through the steps of creating a custom widget and modifying the bokeh javascript to read, parse and output the file.

First though a lot of the credit goes to bigreddot's previous answer on creating the widget. I simply extended the coffescript in his answer to add a file handling function.

Now we begin by creating a new bokeh class on python which will link up to the javascript class and hold the information generated by the file input.

models.py

from bokeh.core.properties import List, String, Dict, Int
from bokeh.models import LayoutDOM

class FileInput(LayoutDOM):
__implementation__ = 'static/js/extensions_file_input.coffee'
__javascript__ = './input_widget/static/js/papaparse.js'

value = String(help="""
Selected input file.
""")

file_name = String(help="""
Name of the input file.
""")

accept = String(help="""
Character string of accepted file types for the input. This should be
written like normal html.
""")

data = List(Dict(keys_type=String, values_type=Int), default=[], help="""
List of dictionary containing the inputed data. This the output of the parser.
""")

Then we create the coffeescript implementation for our new python class. In this new class, there is an added file handler function which triggers on change of the file input widget. This file handler uses PapaParse to parse the csv and then saves the result in the class's data property. The javascript for PapaParse can be downloaded on their website.

You can extend and modify the parser for your desired application and data format.

extensions_file_input.coffee

import * as p from "core/properties"
import {WidgetBox, WidgetBoxView} from "models/layouts/widget_box"

export class FileInputView extends WidgetBoxView

  initialize: (options) ->
    super(options)
    input = document.createElement("input")
    input.type = "file"
    input.accept = @model.accept
    input.id = @model.id
    input.style = "width:" + @model.width + "px"
    input.onchange = () =>
      @model.value = input.value
      @model.file_name = input.files[0].name
      @file_handler(input)
    @el.appendChild(input)

  file_handler: (input) ->
    file = input.files[0]
    opts =  
      header: true,
      dynamicTyping: true,
      delimiter: ",",
      newline: "\r\n",
      complete: (results) =>
        input.data = results.data
        @.model.data = results.data
    Papa.parse(file, opts)


export class FileInput extends WidgetBox
  default_view: FileInputView
  type: "FileInput"
  @define {
    value: [ p.String ]
    file_name: [ p.String ]
    accept: [ p.String ]
    data : [ p.Array ]
  }

A Back on the python side we can then attach a bokeh on_change to our new input class to trigger when it's data property changes. This will happen after the csv parsing is done. This example showcases the desired interaction.

main.py

from bokeh.core.properties import List, String, Dict, Int
from bokeh.models import LayoutDOM

from bokeh.layouts import column
from bokeh.models import Button, ColumnDataSource
from bokeh.io import curdoc
from bokeh.plotting import Figure

import pandas as pd

from models import FileInput

# Starting data
x = [1, 2, 3, 4]
y = x

source = ColumnDataSource(data=dict(x=x, y=y))

plot = Figure(plot_width=400, plot_height=400)
plot.circle('x', 'y', source=source, color="navy", alpha=0.5, size=20)

button_input = FileInput(id="fileSelect",
                         accept=".csv")


def change_plot_data(attr, old, new):
    new_df = pd.DataFrame(new)
    source.data = source.from_df(new_df[['x', 'y']])


button_input.on_change('data', change_plot_data)

layout = column(plot, button_input)
curdoc().add_root(layout)

An example of a .csv file for this application would be. Make sure there is no extra line at the end of the csv.

x,y
0,2
2,3
6,4
7,5
10,25

To run this example properly, bokeh must be set up in it's proper application file tree format.

input_widget
   |
   +---main.py
   +---models.py
   +---static
        +---js
            +--- extensions_file_input.coffee
            +--- papaparse.js

To run this example, you need to be in the directory above the top most file and execute bokeh serve input_widget in the terminal.

like image 25
DuCorey Avatar answered Nov 16 '22 22:11

DuCorey