Vehicle Data App Example - Data Visualization GUIs with Dash and Python p.5




Welcome to part five of the data visualization apps in Python with Dash tutorial series. In this part, we're going to cover how to make the vehicle sensor reading app that I showed in the beginning of this series.

Since it might be tedious to follow along if you didn't have an OBD II reader and a car turned on, we're just going to use random data.

We want this app to have a drop-down with options for things we might want to visualize. When we choose something, we want to add this thing to the list of things we're graphing. We'd also like the ability to remove currently-graphed topics and we want all of this data to be graphed live. Finally, we'd like to have the charts intuitively-sized and laid out on the page, depending on how many charts we're trying to visualize.

To begin, let's make our imports that we'll need:

import dash
import dash_core_components as dcc
import dash_html_components as html
from pandas_datareader.data import DataReader
import time
from collections import deque
import plotly.graph_objs as go
import random

Next, let's define the app, and then we'll specify a dictionary for the datapoints we intend to cover:

app = dash.Dash('vehicle-data')

max_length = 50
times = deque(maxlen=max_length)
oil_temps = deque(maxlen=max_length)
intake_temps = deque(maxlen=max_length)
coolant_temps = deque(maxlen=max_length)
rpms = deque(maxlen=max_length)
speeds = deque(maxlen=max_length)
throttle_pos = deque(maxlen=max_length)

data_dict = {"Oil Temperature":oil_temps,
"Intake Temperature": intake_temps,
"Coolant Temperature": coolant_temps,
"RPM":rpms,
"Speed":speeds,
"Throttle Position":throttle_pos}

For this app, the options for things we can graph are static, there's no reason they'd change on us. That said, you could use events to even update this list and your drop-down menu, depending on some actual changing event. We need some sample data to work with, so here's that:


def update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos):

    times.append(time.time())
    if len(times) == 1:
        #starting relevant values
        oil_temps.append(random.randrange(180,230))
        intake_temps.append(random.randrange(95,115))
        coolant_temps.append(random.randrange(170,220))
        rpms.append(random.randrange(1000,9500))
        speeds.append(random.randrange(30,140))
        throttle_pos.append(random.randrange(10,90))
    else:
        for data_of_interest in [oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos]:
            data_of_interest.append(data_of_interest[-1]+data_of_interest[-1]*random.uniform(-0.0001,0.0001))

    return times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos

times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos = update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos)

I don't want to spend too much time on this code, basically, it just makes somewhat reasonable sample data for these values that we're measuring. If you have a real database or a real sensor to read from, I highly encourage you to replace my code with something that reads meaningful data that you're interested in!

Now we can get to the structure of this app, the layout:

app.layout = html.Div([
    html.Div([
        html.H2('Vehicle Data',
                style={'float': 'left',
                       }),
        ]),
    dcc.Dropdown(id='vehicle-data-name',
                 options=[{'label': s, 'value': s}
                          for s in data_dict.keys()],
                 value=['Coolant Temperature','Oil Temperature','Intake Temperature'],
                 multi=True
                 ),
    html.Div(children=html.Div(id='graphs'), className='row'),
    dcc.Interval(
        id='graph-update',
        interval=100),
    ], className="container",style={'width':'98%','margin-left':10,'margin-right':10,'max-width':50000})

Some of this should be no surprise, but some of this might require an explanation. The dcc.Dropdown options are coming from our dictionary values. Again, this dropdown *could* be dynamically updated by events, and doesn't actually need to be static! The Multi=True means we can have multiple options selected. Besides this drop-down menu, we need the graph itself, so then we plot the graph, but, notice that we've got a graphs-IDed div *inside* another dive with a className of "row." The className parameter is what replaces the class parameter from within HTML. We can't use class since that keyword is occupied in Python. Then, even though it's at the end, it's part of the main div, we see some styles being applied. It looked to me like the default Dash CSS has a default max-width for divs or something, so I just put a new max-width, and then set the width of this div to be 98% of the page. I noticed there was a lot of wasted space that I just simply didn't want. You can feel free to make the styles whatever you want!

It's going to come later, but just note that the row className is not coming from the Dash CSS. Instead, it's coming from an external CSS framework (Materialize CSS). I am using them, because I am familiar with that framework, and it's super useful for making response front-end layouts. I didn't find anything for doing custom/dynamic-sized divs based on content in the default Dash stylings, so that's why I am bringing in Materialize too. That said, make sure you check out all of what Dash has to offer out of the box. For example, check out: the core components. Things like uploading data, tables, sliders, dropdowns (obviously) and more are there.

While I am on the topic of docs, I also strongly suggest you peak into the Plotly Documentation, mainly with things like layouts, connecting to databases, and report generation. Plotly offers you a LOT of things that you might otherwise expect that you might have to figure out or build yourself...and you don't!

Okay, back to our app, lets write the decorator first. This one is interesting, since, actually, we're going to have Input, Output, AND events!

@app.callback(
    dash.dependencies.Output('graphs','children'),
    [dash.dependencies.Input('vehicle-data-name', 'value')],
    events=[dash.dependencies.Event('graph-update', 'interval')]
    )

All of the output in this case is going to the graphs-IDed div. Then, the input will be the value from our dropdown with the id of vehicle-data-name. Finally, we have our interval event, which we learned about from our previous tutorial. So, here, we've got both an event that is updating things and user-input.

Now for the function:

@app.callback(
    dash.dependencies.Output('graphs','children'),
    [dash.dependencies.Input('vehicle-data-name', 'value')],
    events=[dash.dependencies.Event('graph-update', 'interval')]
    )
def update_graph(data_names):
    graphs = []
    for data_name in data_names:

        data = go.Scatter(
            x=list(times),
            y=list(data_dict[data_name]),
            name='Scatter',
            fill="tozeroy",
            fillcolor="#6897bb"
            )

        graphs.append(html.Div(dcc.Graph(
            id=data_name,
            animate=True,
            figure={'data': [data],'layout' : go.Layout(xaxis=dict(range=[min(times),max(times)]),
                                                        yaxis=dict(range=[min(data_dict[data_name]),max(data_dict[data_name])]),
                                                        margin={'l':50,'r':1,'t':45,'b':1},
                                                        title='{}'.format(data_name))}
            ), className=class_choice))

    return graphs

This function isn't quite yet done, but I wanted to keep it simple and separate out our data and dash code. Basically, this function just builds the graphs list, by creating graphs for each of the elements from within data_names, which gets passed into this function via our dropdown menu values. Now we actually need the data, and you'll notice we need something to choose the class_choice.

@app.callback(
    dash.dependencies.Output('graphs','children'),
    [dash.dependencies.Input('vehicle-data-name', 'value')],
    events=[dash.dependencies.Event('graph-update', 'interval')]
    )
def update_graph(data_names):
    graphs = []
    update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos)


    if len(data_names)>2:
        class_choice = 'col s12 m6 l4'
    elif len(data_names) == 2:
        class_choice = 'col s12 m6 l6'
    else:
        class_choice = 'col s12'


    for data_name in data_names:

        data = go.Scatter(
            x=list(times),
            y=list(data_dict[data_name]),
            name='Scatter',
            fill="tozeroy",
            fillcolor="#6897bb"
            )

        graphs.append(html.Div(dcc.Graph(
            id=data_name,
            animate=True,
            figure={'data': [data],'layout' : go.Layout(xaxis=dict(range=[min(times),max(times)]),
                                                        yaxis=dict(range=[min(data_dict[data_name]),max(data_dict[data_name])]),
                                                        margin={'l':50,'r':1,'t':45,'b':1},
                                                        title='{}'.format(data_name))}
            ), className=class_choice))

    return graphs

The if-statements:

    if len(data_names)>2:
        class_choice = 'col s12 m6 l4'
    elif len(data_names) == 2:
        class_choice = 'col s12 m6 l6'
    else:
        class_choice = 'col s12'

Determine the styling from the Materialize CSS framework (we still have to bring that in), but this is what dictates how many charts per "row" we'll allow, depending on the screen's size. The values dictate how many "columns," out of 12, an element will take up. So, when there's just 1 graph, we might as well take up all columns, regardless of window size. If there are exactly 2 elements, then, on a small screen, we want each graph to be fully-wide (so they aren't super compressed/skinny), but then take up only 6 columns on medium and large screens, so there'd be 2 charts per row in this case. Finally, if there are many graphs to graph, then, again, on a small screen, one graph takes up all of the columns, on a medium sized screen we take up 6, and, on large screen, each graph takes up 4 columns, so 3 graphs could be on each row.

Now, in order for things to work with the Materialize CSS, we need to bring it in. Also, to use materialize to the full extent, we need to bring in the Materialize JS too. While you may not want to use Materialize specifically, you may often find that you want to bring in external CSS or other scripts. To do this:

external_css = ["https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css"]
for css in external_css:
    app.css.append_css({"external_url": css})

The above for CSS, and then for javascript:

external_js = ['https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js']
for js in external_js:
    app.scripts.append_script({'external_url': js})

Finally we need to run the app:

if __name__ == '__main__':
    app.run_server(debug=True)

All together now:

import dash
import dash_core_components as dcc
import dash_html_components as html
from pandas_datareader.data import DataReader
import time
from collections import deque
import plotly.graph_objs as go
import random

app = dash.Dash('vehicle-data')

data_dict = {"Oil Temperature":oil_temps,
"Intake Temperature": intake_temps,
"Coolant Temperature": coolant_temps,
"RPM":rpms,
"Speed":speeds,
"Throttle Position":throttle_pos}

max_length = 50
times = deque(maxlen=max_length)
oil_temps = deque(maxlen=max_length)
intake_temps = deque(maxlen=max_length)
coolant_temps = deque(maxlen=max_length)
rpms = deque(maxlen=max_length)
speeds = deque(maxlen=max_length)
throttle_pos = deque(maxlen=max_length)


def update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos):

    times.append(time.time())
    if len(times) == 1:
        #starting relevant values
        oil_temps.append(random.randrange(180,230))
        intake_temps.append(random.randrange(95,115))
        coolant_temps.append(random.randrange(170,220))
        rpms.append(random.randrange(1000,9500))
        speeds.append(random.randrange(30,140))
        throttle_pos.append(random.randrange(10,90))
    else:
        for data_of_interest in [oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos]:
            data_of_interest.append(data_of_interest[-1]+data_of_interest[-1]*random.uniform(-0.0001,0.0001))

    return times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos

times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos = update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos)

app.layout = html.Div([
    html.Div([
        html.H2('Vehicle Data',
                style={'float': 'left',
                       }),
        ]),
    dcc.Dropdown(id='vehicle-data-name',
                 options=[{'label': s, 'value': s}
                          for s in data_dict.keys()],
                 value=['Coolant Temperature','Oil Temperature','Intake Temperature'],
                 multi=True
                 ),
    html.Div(children=html.Div(id='graphs'), className='row'),
    dcc.Interval(
        id='graph-update',
        interval=100),
    ], className="container",style={'width':'98%','margin-left':10,'margin-right':10,'max-width':50000})


@app.callback(
    dash.dependencies.Output('graphs','children'),
    [dash.dependencies.Input('vehicle-data-name', 'value')],
    events=[dash.dependencies.Event('graph-update', 'interval')]
    )
def update_graph(data_names):
    graphs = []
    update_obd_values(times, oil_temps, intake_temps, coolant_temps, rpms, speeds, throttle_pos)
    if len(data_names)>2:
        class_choice = 'col s12 m6 l4'
    elif len(data_names) == 2:
        class_choice = 'col s12 m6 l6'
    else:
        class_choice = 'col s12'


    for data_name in data_names:

        data = go.Scatter(
            x=list(times),
            y=list(data_dict[data_name]),
            name='Scatter',
            fill="tozeroy",
            fillcolor="#6897bb"
            )

        graphs.append(html.Div(dcc.Graph(
            id=data_name,
            animate=True,
            figure={'data': [data],'layout' : go.Layout(xaxis=dict(range=[min(times),max(times)]),
                                                        yaxis=dict(range=[min(data_dict[data_name]),max(data_dict[data_name])]),
                                                        margin={'l':50,'r':1,'t':45,'b':1},
                                                        title='{}'.format(data_name))}
            ), className=class_choice))

    return graphs



external_css = ["https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css"]
for css in external_css:
    app.css.append_css({"external_url": css})

external_js = ['https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js']
for js in external_css:
    app.scripts.append_script({'external_url': js})


if __name__ == '__main__':
    app.run_server(debug=True)

The result should be something like:

The next tutorial:





  • Intro - Data Visualization Applications with Dash and Python p.1
  • Interactive User Interface - Data Visualization GUIs with Dash and Python p.2
  • Dynamic Graph based on User Input - Data Visualization GUIs with Dash and Python p.3
  • Live Graphs - Data Visualization GUIs with Dash and Python p.4
  • Vehicle Data App Example - Data Visualization GUIs with Dash and Python p.5
  • Out of the Box Sentiment Analysis options with Python using VADER Sentiment and TextBlob
  • Streaming Tweets and Sentiment from Twitter in Python - Sentiment Analysis GUI with Dash and Python p.2
  • Reading from our sentiment database - Sentiment Analysis GUI with Dash and Python p.3
  • Live Twitter Sentiment Graph - Sentiment Analysis GUI with Dash and Python p.4
  • Dynamically Graphing Terms for Sentiment - Sentiment Analysis GUI with Dash and Python p.5
  • Deploy Dash App to a VPS web server - Data Visualization Applications with Dash and Python p.11