Creating an interactive dashboard with Dash Plotly using crime data | by Xiao Wang | Towards Data Science

My previous article briefly introduced how to develop a simple dashboard with Dash. As a big fan of visualisation, a dashboard like that wouldn’t make me happy. I explored a bit further this week and would like to make an interactive dashboard like what we can usually do with PowerBI or Tableau.
Acknowledgement: This work is based on/includes Stats NZ’s data which are licensed by Stats NZ for reuse under the Creative Commons Attribution 4.0 International licence.
Here’s how the dashboard looks like. I will then explain the way to achieve this:
You may want to check this article if you would like to start with an easy dashboard.
towardsdatascience.com
Data:
Data used in the current article is from Stats NZ as mentioned in the acknowledgement at the start. I downloaded the data from Stats NZ and then transformed to the dataframe for the visualisation.
The dataframe included six columns, [Offence] is about different types offences; [Location] is about the which district the offence happened; [Year] is when the offence happened; [Measure] is whether the offence has been resolved or not; [Scene] is where the offence happened; and [Total] is the number of offences.
Packages:
Pandas, numpy, plotly.express, and plotly.graph_objects were used for data cleaning and visualisation; dash, dash_core_components, and dash_html_components were used for creating the dashboard.
import pandas as pdimport numpy as npimport plotly.express as pximport plotly.graph_objects as goimport dashimport dash_core_components as dccimport dash_html_components as htmlfrom dash.dependencies import Input, Output
For this dashboard, I planned to have four graphs:
fig1 = px.bar(    offence_district, #dataframe    x=offence_district.groupby("Offence")["Total"].agg(sum), #x    y=offence_district["Offence"].unique(),#y    labels={"x": "Total Recorded", "y": "Offence"}, #define lable    color=offence_district.groupby("Offence")["Total"].agg(sum),    color_continuous_scale=px.colors.sequential.RdBu,#color    text=offence_district.groupby("Offence")["Total"].agg(sum),#text    title="Recorded Crime by Offence", #title    orientation="h"  #horizonal bar chart)fig1.update_layout(    title=dict(x=0.5), #set title in the center    margin=dict(l=550, r=20, t=60, b=20) #set margin of the chart    paper_bgcolor="#D6EAF8", #set the background color of the chart)fig1.update_traces(texttemplate="%{text:.2s}") #formart the text
*Plotly has a big range of color settings, such as discrete colors and continuous colors, which I found really helpful in visualisations to highlight some information or make comparisons. Check the instruction from ploty to get more information.
fig2 = px.scatter(    offence_district, #dataframe    x="Location", #x    y="Offence", #y    size="Total", #bubble size    color="Total",#bubble color    color_continuous_scale=px.colors.sequential.Plotly3, #color theme    title="Offences by Location", #chart title)fig2.update_layout(    xaxis_tickangle=30,#angle of the tick on x-axis    title=dict(x=0.5), #set the title in center    xaxis_tickfont=dict(size=9), #set the font for x-axis    yaxis_tickfont=dict(size=9), #set the font for y-axis    margin=dict(l=500, r=20, t=50, b=20), #set the margin    paper_bgcolor="LightSteelblue", #set the background color for chart)
In the third chart, I would like to compare the amount of “resolved” records and those haven’t been resolved. So first I split the original dataframe to two.
s1 = offence_district[offence_district['Measure']=='Resolved']s2 = offence_district[offence_district['Measure']=='Unresolved']
Then I used the plotly.graph_objects to build the graph:
trace1 = go.Bar(    #setup the chart for Resolved records    x=s1["Location"].unique(), #x for Resolved records    y=s1.groupby("Location")["Total"].agg(sum),#y for Resolved records    marker_color=px.colors.qualitative.Dark24[0],  #color    text=s1.groupby("Location")["Total"].agg(sum), #label/text    textposition="outside", #text position    name="Resolved", #legend name)trace2 = go.Bar(   #setup the chart for Unresolved records    x=s2["Location"].unique(),    y=s2.groupby("Location")["Total"].agg(sum),    text=s2.groupby("Location")["Total"].agg(sum),    marker_color=px.colors.qualitative.Dark24[1],    textposition="outside",    name="Unresolved",)data = [trace1, trace2] #combine two charts/columnslayout = go.Layout(barmode="group", title="Resolved vs Unresolved") #define how to display the columnsfig3 = go.Figure(data=data, layout=layout)fig3.update_layout(    title=dict(x=0.5), #center the title    xaxis_title="District",#setup the x-axis title    yaxis_title="Total", #setup the x-axis title    margin=dict(l=20, r=20, t=60, b=20),#setup the margin    paper_bgcolor="aliceblue", #setup the background color)fig3.update_traces(texttemplate="%{text:.2s}") #text formart
fig4= px.bar(offence_district,              x = offence_district.groupby('Scene')['Total'].agg(sum),              y = offence_district['Scene'].unique(),              labels={                     "x": "Total Recorded",                     "y": "Scene"                 },              color = offence_district.groupby('Scene')['Total'].agg(sum),              color_continuous_scale=px.colors.sequential.Sunset,              #color_discrete_sequence=['rgb(253,180,98)','rgb(190,186,218)'],              text = offence_district.groupby('Scene')['Total'].agg(sum),             title = 'Recorded Crime by Scene',              #,barmode = 'group'              orientation = 'h'             )fig4.update_layout( title = dict(x=0.5), paper_bgcolor="#BDBDBD")fig4.update_traces(texttemplate = '%{text:.2s}')
Once we got all graphs ready, the next step is to setup the layout of the dashboard. In other words, how do you want your dashboard looks like.
For example, in my dashboard I would like to have a header at the top center, then followed up with four graphs, two in one row. Additionally, I would like to have all four graphs be able to filter by “Year”.
To achieve what I mentioned above, in my layout setup, the header, four graphs, and a dropdown (filter) need to be coded.
app = dash.Dash(__name__)app.layout = html.Div(    children=[        html.Div(            children=[                html.P(children="🚓", style={'fontSize': "30px",'textAlign': 'center'}, className="header-emoji"), #emoji                html.H1(                    children="Crime Analytics",style={'textAlign': 'center'}, className="header-title"                 ), #Header title                html.H2(                    children="Analyze the crime records"                    " by district in New Zealand"                    " between 1994 and 2014",                    className="header-description", style={'textAlign': 'center'},                ),            ],            className="header",style={'backgroundColor':'#F5F5F5'},        ), #Description below the header                        html.Div(            children=[                html.Div(children = 'Year', style={'fontSize': "24px"},className = 'menu-title'),                dcc.Dropdown(                    id = 'year-filter',                    options = [                        {'label': Year, 'value':Year}                        for Year in offence_district.Year.unique()                    ], #'Year' is the filter                    value ='2010',                    clearable = False,                    searchable = False,                    className = 'dropdown', style={'fontSize': "24px",'textAlign': 'center'},                ),            ],            className = 'menu',        ), #the dropdown function                html.Div(            children=[                html.Div(                children = dcc.Graph(                    id = 'scatter',                    figure = fig2,                  #  config={"displayModeBar": False},                ),                style={'width': '50%', 'display': 'inline-block'},            ),                html.Div(                children = dcc.Graph(                    id = 'bar',                    figure = fig1,                    #config={"displayModeBar": False},                ),                style={'width': '50%', 'display': 'inline-block'},            ),                html.Div(                children = dcc.Graph(                    id = 'bibar',                    figure = fig3,                    #config={"displayModeBar": False},                ),                style={'width': '50%', 'display': 'inline-block'},            ),                html.Div(                children = dcc.Graph(                    id = 'barscene',                    figure = fig4,                    #config={"displayModeBar": False},                ),                style={'width': '50%', 'display': 'inline-block'},            ),        ],        className = 'double-graph',        ),     ]) #Four graphs
To be able to pass the dropdown (filter) value to the graphs in the dashboard, we need to setup the “callback” so the graphs can be interactive. Below I setup callback for each graph separately.
@app.callback(    Output("scatter", "figure"), #the output is the scatterchart    [Input("year-filter", "value")], #the input is the year-filter)def update_charts(Year):    filtered_data = offence_district[offence_district["Year"] == Year] #the graph/dataframe will be filterd by "Year"scatter = px.scatter(        filtered_data,        x="Location",        y="Offence",        size="Total",        color="Total",        color_continuous_scale=px.colors.sequential.Plotly3,        title="Offences by Location",    )    scatter.update_layout(        xaxis_tickangle=30,        title=dict(x=0.5),        xaxis_tickfont=dict(size=9),        yaxis_tickfont=dict(size=9),        margin=dict(l=500, r=20, t=50, b=20),        paper_bgcolor="LightSteelblue",    )    return scatter #return the scatterchart according to the filter@app.callback(    Output("bar", "figure"),    [Input("year-filter", "value")],)def update_charts(Year):    filtered_data = offence_district[offence_district["Year"] == Year]    bar = px.bar(        filtered_data,        x=filtered_data.groupby("Offence")["Total"].agg(sum),        y=filtered_data["Offence"].unique(),        color=filtered_data.groupby("Offence")["Total"].agg(sum),        color_continuous_scale=px.colors.sequential.RdBu,        text=filtered_data.groupby("Offence")["Total"].agg(sum),        title="Recorded Crime by Offence",        orientation="h",    )    bar.update_layout(        title=dict(x=0.5), margin=dict(l=550, r=20, t=60, b=20), paper_bgcolor="#D6EAF8"    )    bar.update_traces(texttemplate="%{text:.2s}")    return bar@app.callback(    Output("bibar", "figure"),    [Input("year-filter", "value")],)def update_charts(Year):    filtered_s1 = s1[s1["Year"] == Year]    filtered_s2 = s2[s2["Year"] == Year]    trace1 = go.Bar(        x=filtered_s1["Location"].unique(),        y=filtered_s1.groupby("Location")["Total"].agg(sum),        text=filtered_s1.groupby("Location")["Total"].agg(sum),        textposition="outside",        marker_color=px.colors.qualitative.Dark24[0],        name="Resolved",    )    trace2 = go.Bar(        x=filtered_s2["Location"].unique(),        y=filtered_s2.groupby("Location")["Total"].agg(sum),        text=filtered_s2.groupby("Location")["Total"].agg(sum),        textposition="outside",        marker_color=px.colors.qualitative.Dark24[1],        name="Unresolved",    )    data = [trace1, trace2]    layout = go.Layout(barmode="group", title="Resolved vs Unresolved")    bibar = go.Figure(data=data, layout=layout)    bibar.update_layout(        title=dict(x=0.5),        xaxis_title="District",        yaxis_title="Total",        paper_bgcolor="aliceblue",        margin=dict(l=20, r=20, t=60, b=20),    )    bibar.update_traces(texttemplate="%{text:.2s}")    return bibar@app.callback(    Output("barscene", "figure"),    [Input("year-filter", "value")],)def update_charts(Year):    filtered_data = offence_district[offence_district["Year"] == Year]    barscene = px.bar(        filtered_data,        x=filtered_data.groupby("Scene")["Total"].agg(sum),        y=filtered_data["Scene"].unique(),        labels={"x": "Total Recorded", "y": "Scene"},        color=filtered_data.groupby("Scene")["Total"].agg(sum),        color_continuous_scale=px.colors.sequential.Sunset,        # color_discrete_sequence=['rgb(253,180,98)','rgb(190,186,218)'],        text=filtered_data.groupby("Scene")["Total"].agg(sum),        title="Recorded Crime by Scene",        # ,barmode = 'group'        orientation="h",    )    barscene.update_layout(title=dict(x=0.5), paper_bgcolor="#BDBDBD")    barscene.update_traces(texttemplate="%{text:.2s}")    return barscene
Once we got dash layout and callback, we can run the dashboard and play with it.
Although I don’t think it is an easy start to develop dashboard with Dash, Dash provides users flexibilities to customize or personalize their dashboard with a lot of fun. To start building your own dashboard, remember the three components for Dash: 1) graphs; 2) layout; and 3) callback.
The layout part to me is not that straightforward as I have zero knowledge about html or css language. This is the link to css intro if you have similar problems with me.
##if not using virtual environmentif __name__ == '__main__':    app.run_server()##if using virtual environmentif __name__ == '__main__':    app.run_server(debug = True)
That’s all you need to create an interactive dashboard. Let’s get started!
Ping me on Linkedin if there’s any questions or just say a big Hi 🤪!