|
| 1 | +""" Interactive Wine Visualization Tool |
| 2 | +
|
| 3 | +See pbpython.com for the associated blog post explaining the process and |
| 4 | +goals of this script |
| 5 | +""" |
| 6 | + |
| 7 | +import pandas as pd |
| 8 | +from bokeh.plotting import figure |
| 9 | +from bokeh.layouts import layout, widgetbox |
| 10 | +from bokeh.models import ColumnDataSource, HoverTool, BoxZoomTool, ResetTool, PanTool |
| 11 | +from bokeh.models.widgets import Slider, Select, TextInput, Div |
| 12 | +from bokeh.models import WheelZoomTool, SaveTool, LassoSelectTool |
| 13 | +from bokeh.io import curdoc |
| 14 | +from functools import lru_cache |
| 15 | + |
| 16 | + |
| 17 | +# Define a cached function to read in the CSV file and return a dataframe |
| 18 | +@lru_cache() |
| 19 | +def load_data(): |
| 20 | + df = pd.read_csv("Aussie_Wines_Plotting.csv", index_col=0) |
| 21 | + return df |
| 22 | + |
| 23 | +# Column order for displaying the details of a specific review |
| 24 | +col_order = ['price', 'points', 'variety', 'province', 'description'] |
| 25 | + |
| 26 | +all_provinces = [ |
| 27 | + "All", "South Australia", "Victoria", "Western Australia", |
| 28 | + "Australia Other", "New South Wales", "Tasmania" |
| 29 | +] |
| 30 | + |
| 31 | +# Setup the display portions including widgets as well as text and HTML |
| 32 | +desc = Div(text="All Provinces", width=800) |
| 33 | +province = Select(title="Province", options=all_provinces, value="All") |
| 34 | +price_max = Slider(start=0, end=900, step=5, value=200, title="Maximum Price") |
| 35 | +title = TextInput(title="Title Contains") |
| 36 | +details = Div(text='Selection Details:', width=800) |
| 37 | + |
| 38 | +# Populate the data with the dataframe |
| 39 | +source = ColumnDataSource(data=load_data()) |
| 40 | + |
| 41 | +# Build out the hover tools |
| 42 | +hover = HoverTool(tooltips=[ |
| 43 | + ("title", "@title"), |
| 44 | + ("variety", "@variety"), |
| 45 | +]) |
| 46 | + |
| 47 | +# Define the tool list as a list of the objects so it is easier to customize |
| 48 | +# each object |
| 49 | +TOOLS = [ |
| 50 | + hover, BoxZoomTool(), LassoSelectTool(), WheelZoomTool(), PanTool(), |
| 51 | + ResetTool(), SaveTool() |
| 52 | +] |
| 53 | + |
| 54 | +# Build out the figure with the individual circle plots |
| 55 | +p = figure( |
| 56 | + plot_height=600, |
| 57 | + plot_width=700, |
| 58 | + title="Australian Wine Analysis", |
| 59 | + tools=TOOLS, |
| 60 | + x_axis_label="points", |
| 61 | + y_axis_label="price (USD)", |
| 62 | + toolbar_location='above') |
| 63 | + |
| 64 | +p.circle( |
| 65 | + y="price", |
| 66 | + x='points', |
| 67 | + source=source, |
| 68 | + color='variety_color', |
| 69 | + size=7, |
| 70 | + alpha=0.4) |
| 71 | + |
| 72 | + |
| 73 | +# Define the functions to update the data based on a selection or change |
| 74 | + |
| 75 | +def select_reviews(): |
| 76 | + """ Use the current selections to determine which filters to apply to the |
| 77 | + data. Return a dataframe of the selected data |
| 78 | + """ |
| 79 | + df = load_data() |
| 80 | + |
| 81 | + # Determine what has been selected for each widgetd |
| 82 | + max_price = price_max.value |
| 83 | + province_val = province.value |
| 84 | + title_val = title.value |
| 85 | + |
| 86 | + # Filter by price and province |
| 87 | + if province_val == "All": |
| 88 | + selected = df[df.price <= max_price] |
| 89 | + else: |
| 90 | + selected = df[(df.province == province_val) & (df.price <= max_price)] |
| 91 | + |
| 92 | + # Further filter by string in title if it is provided |
| 93 | + if title_val != "": |
| 94 | + selected = selected[selected.title.str.contains(title_val) == True] |
| 95 | + |
| 96 | + # Example showing how to update the description |
| 97 | + desc.text = "Province: {} and Price < {}".format(province_val, max_price) |
| 98 | + return selected |
| 99 | + |
| 100 | + |
| 101 | +def update(): |
| 102 | + """ Get the selected data and update the data in the source |
| 103 | + """ |
| 104 | + df_active = select_reviews() |
| 105 | + source.data = ColumnDataSource(data=df_active).data |
| 106 | + |
| 107 | + |
| 108 | +def selection_change(attrname, old, new): |
| 109 | + """ Function will be called when the poly select (or other selection tool) |
| 110 | + is used. Determine which items are selected and show the details below |
| 111 | + the graph |
| 112 | + """ |
| 113 | + selected = source.selected['1d']['indices'] |
| 114 | + |
| 115 | + # Need to get a list of the active reviews so the indices will match up |
| 116 | + df_active = select_reviews() |
| 117 | + |
| 118 | + # If something is selected, then get those details and format the results |
| 119 | + # as an HTML table |
| 120 | + if selected: |
| 121 | + data = df_active.iloc[selected, :] |
| 122 | + temp = data.set_index('title').T.reindex(index=col_order) |
| 123 | + details.text = temp.style.render() |
| 124 | + else: |
| 125 | + details.text = "Selection Details" |
| 126 | + |
| 127 | + |
| 128 | +# Setup functions for each control so that changes will be captured and data |
| 129 | +# updated as required |
| 130 | +controls = [province, price_max, title] |
| 131 | + |
| 132 | +for control in controls: |
| 133 | + control.on_change('value', lambda attr, old, new: update()) |
| 134 | + |
| 135 | +# If the source is changed to a selection, execute that selection process |
| 136 | +source.on_change('selected', selection_change) |
| 137 | + |
| 138 | +# The final portion is to layout the parts and get the server going |
| 139 | + |
| 140 | +# Build a box for all the controls |
| 141 | +inputs = widgetbox(*controls, sizing_mode='fixed') |
| 142 | + |
| 143 | +# Define a simple layout |
| 144 | +l = layout([[desc], [inputs, p], [details]], sizing_mode='fixed') |
| 145 | + |
| 146 | +# Update the data and instantiate the service |
| 147 | +update() |
| 148 | +curdoc().add_root(l) |
| 149 | + |
| 150 | +# Show the title in the browser bar |
| 151 | +curdoc().title = "Australian Wine Analysis" |
0 commit comments