Creating an interactive dashboard with Dash Plotly using crime data
By: Xiao Wang
Tags: Dashboard, Dash, Interactive, Plotly

You may want to check this article if you would like to start with an easy dashboard.

Prerequisites:

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.

Table 1: Dataframe

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 pd

import numpy as np

import plotly.express as px

import plotly.graph_objects as go import dash

import dash_core_components as dcc

import dash_html_components as html

from dash.dependencies import Input, Output

Step 1: Visualisation Preparation

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

Figure 1: Bar chart

*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

)

Figure 2: Bubble chart — crime by location and scene

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/columns

layout = go.Layout(barmode="group", title="Resolved vs Unresolved") #define how to display the columns

fig3 = 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

Figure 3: Column chart — Resolved vs Unresolved

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}')

Figure 4: Bar chart — Crime by Scene

Step 2: Setup the Dash Layout

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

Step 3: Setup Callback

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.



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" @app .callback(Output("scatter", "figure"),[Input("year-filter", "value")],def update_charts(Year):filtered_data = offence_district[offence_district["Year"] == 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

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("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

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("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

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 @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.

Conclusion:

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 environment

if __name__ == '__main__':

app.run_server() ##if using virtual environment

if __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 🤪!

Reference