Commit 07fa226874
Verified · cmc
Layout: unified · split
README.md +3 −2
| @@ -1,4 +1,4 @@ | |||
| 1 | # Omaha Incidents | 1 | # Omaha Crime Mapping & Analysis |
| 2 | 2 | ||
| 3 | Data from the Omaha police department, used to analyze and visualize statistics. | 3 | Data from the Omaha police department, used to analyze and visualize statistics. |
| 4 | 4 | ||
| @@ -22,4 +22,5 @@ sqlite2rest serve ./raw_data/ingress.db | |||
| 22 | 22 | ||
| 23 | ## Screenshots | 23 | ## Screenshots |
| 24 | 24 | ||
| 25 |  | 25 |  |
| 26 |  | ||
app.py +69 −26
| @@ -1,7 +1,9 @@ | |||
| 1 | from dash import Dash, html, dcc, callback, Output, Input, dash_table | 1 | from dash import Dash, html, dcc, callback, Output, Input, dash_table |
| 2 | import plotly.express as px | 2 | import plotly.express as px |
| 3 | import pandas as pd | 3 | import pandas as pd |
| 4 | import numpy as np | ||
| 4 | import sqlite3 | 5 | import sqlite3 |
| 6 | from datetime import datetime, date | ||
| 5 | 7 | ||
| 6 | # Connect to database and query all incidents | 8 | # Connect to database and query all incidents |
| 7 | connection = sqlite3.connect("./raw_data/ingress.db") | 9 | connection = sqlite3.connect("./raw_data/ingress.db") |
| @@ -9,44 +11,51 @@ cursor = connection.cursor() | |||
| 9 | query = "SELECT * FROM incidents;" | 11 | query = "SELECT * FROM incidents;" |
| 10 | df = pd.read_sql_query(query, connection).sort_values(by="description") | 12 | df = pd.read_sql_query(query, connection).sort_values(by="description") |
| 11 | 13 | ||
| 12 | # Create custom YEAR column to use in dropdown | 14 | # Replace empty cells with NaN |
| 13 | df['year'] = df['date'].str[-4:] | 15 | df = df.replace(r'^\s*$', np.nan, regex=True) |
| 16 | |||
| 17 | # Convert date column to datetime | ||
| 18 | # TODO: Create a combined datetime column in the db to load/convert here | ||
| 19 | df["date"] = pd.to_datetime(df["date"]) | ||
| 14 | 20 | ||
| 15 | # Configure HTML layout | 21 | # Configure HTML layout |
| 16 | app = Dash(__name__) | 22 | app = Dash(__name__) |
| 17 | app.layout = html.Div(children = [ | 23 | app.layout = html.Div(children = [ |
| 18 | html.H1(children="Omaha Police Incidents", style={"textAlign":"center"}), | 24 | html.H1(children="Omaha Crime Mapping & Analysis", style={"textAlign":"center"}), |
| 19 | html.Div([ | 25 | html.Div([ |
| 20 | html.H2(children="Totals per Category and Year", style={"textAlign":"center"}), | 26 | html.Div(className="flex", |
| 21 | dcc.Dropdown(df.sort_values("description").description.unique(), "INJURY", id="dropdown"), | 27 | children = [ |
| 22 | dcc.Dropdown(df.sort_values("year").year.unique(), "2023", id="year-dropdown"), | 28 | html.P("Crime:"), |
| 23 | html.Hr(), | 29 | dcc.Dropdown(df.sort_values("description").description.unique(), "INJURY", id="dropdown"), |
| 24 | dash_table.DataTable(data=df.to_dict("records"), page_size=5, id="table"), | 30 | ] |
| 25 | html.H2(children="Map of Incidents per Category and Year", style={"textAlign":"center"}), | 31 | ), |
| 26 | dcc.Graph(id="map-graph") | 32 | html.Div(className="flex", |
| 33 | children = [ | ||
| 34 | html.P("Date Range:"), | ||
| 35 | dcc.DatePickerRange( | ||
| 36 | id='date-picker-range', | ||
| 37 | min_date_allowed=date(2015, 1, 1), | ||
| 38 | max_date_allowed=date(2023, 12, 31), | ||
| 39 | start_date=date(2015, 1, 1), | ||
| 40 | end_date=date(2023,12,31) | ||
| 41 | ), | ||
| 42 | ] | ||
| 43 | ), | ||
| 44 | dcc.Graph(id="map-graph"), | ||
| 45 | dcc.Graph(id="line-graph"), | ||
| 46 | dash_table.DataTable(data=df.to_dict("records"), page_size=5, id="table") | ||
| 27 | ]) | 47 | ]) |
| 28 | ]) | 48 | ]) |
| 29 | 49 | ||
| 30 | # Create table | ||
| 31 | @callback( | ||
| 32 | Output("table", "data"), | ||
| 33 | Input("dropdown", "value"), | ||
| 34 | Input("year-dropdown", "value") | ||
| 35 | ) | ||
| 36 | def update_table(description, year): | ||
| 37 | dff = df[df.year == year] | ||
| 38 | dff = dff.reset_index() | ||
| 39 | dff = dff[dff.description == description] | ||
| 40 | return dff.to_dict("records") | ||
| 41 | |||
| 42 | # Create map | 50 | # Create map |
| 43 | @callback( | 51 | @callback( |
| 44 | Output("map-graph", "figure"), | 52 | Output("map-graph", "figure"), |
| 45 | Input("dropdown", "value"), | 53 | Input("dropdown", "value"), |
| 46 | Input("year-dropdown", "value") | 54 | Input('date-picker-range', 'start_date'), |
| 55 | Input('date-picker-range', 'end_date') | ||
| 47 | ) | 56 | ) |
| 48 | def update_map(description, year): | 57 | def update_map(description, start_date, end_date): |
| 49 | dff = df[df.year == year] | 58 | dff = df[(df['date'] > start_date) & (df['date'] < end_date)] |
| 50 | dff = dff.reset_index() | 59 | dff = dff.reset_index() |
| 51 | dff = dff[dff.description == description] | 60 | dff = dff[dff.description == description] |
| 52 | 61 | ||
| @@ -54,7 +63,6 @@ def update_map(description, year): | |||
| 54 | dff, | 63 | dff, |
| 55 | lat="lat", | 64 | lat="lat", |
| 56 | lon="lon", | 65 | lon="lon", |
| 57 | color="description", | ||
| 58 | hover_name="description", | 66 | hover_name="description", |
| 59 | hover_data=["date", "time"], | 67 | hover_data=["date", "time"], |
| 60 | title="Incident Count by Coordinates", | 68 | title="Incident Count by Coordinates", |
| @@ -69,5 +77,40 @@ def update_map(description, year): | |||
| 69 | 77 | ||
| 70 | return fig | 78 | return fig |
| 71 | 79 | ||
| 80 | # Create line graph | ||
| 81 | @callback( | ||
| 82 | Output("line-graph", "figure"), | ||
| 83 | Input("dropdown", "value"), | ||
| 84 | Input('date-picker-range', 'start_date'), | ||
| 85 | Input('date-picker-range', 'end_date') | ||
| 86 | ) | ||
| 87 | def update_line(description, start_date, end_date): | ||
| 88 | dff = df[(df['date'] > start_date) & (df['date'] < end_date)] | ||
| 89 | dff = dff.reset_index() | ||
| 90 | dff = dff[dff.description == description] | ||
| 91 | dff = dff.groupby(by="date").count() | ||
| 92 | dff = dff.reset_index() | ||
| 93 | |||
| 94 | fig = px.line( | ||
| 95 | dff, | ||
| 96 | x="date", | ||
| 97 | y="description", | ||
| 98 | ) | ||
| 99 | |||
| 100 | return fig | ||
| 101 | |||
| 102 | # Create table | ||
| 103 | @callback( | ||
| 104 | Output("table", "data"), | ||
| 105 | Input("dropdown", "value"), | ||
| 106 | Input('date-picker-range', 'start_date'), | ||
| 107 | Input('date-picker-range', 'end_date') | ||
| 108 | ) | ||
| 109 | def update_table(description, start_date, end_date): | ||
| 110 | dff = df[(df['date'] > start_date) & (df['date'] < end_date)] | ||
| 111 | dff = dff.reset_index() | ||
| 112 | dff = dff[dff.description == description] | ||
| 113 | return dff.to_dict("records") | ||
| 114 | |||
| 72 | if __name__ == "__main__": | 115 | if __name__ == "__main__": |
| 73 | app.run(debug=True) | 116 | app.run(debug=True) |
assets/styles.css +28 −3
| @@ -4,6 +4,31 @@ body { | |||
| 4 | margin: 2rem auto; | 4 | margin: 2rem auto; |
| 5 | } | 5 | } |
| 6 | 6 | ||
| 7 | hr { | 7 | .dash-table-container { |
| 8 | margin: 1rem auto; | 8 | overflow: scroll; |
| 9 | }\ No newline at end of file | 9 | } |
| 10 | |||
| 11 | .dash-table-container, | ||
| 12 | .dash-graph { | ||
| 13 | margin: 2rem 0; | ||
| 14 | max-width: 100%; | ||
| 15 | } | ||
| 16 | |||
| 17 | .flex { | ||
| 18 | display: flex; | ||
| 19 | flex-direction: row; | ||
| 20 | align-items: center; | ||
| 21 | } | ||
| 22 | |||
| 23 | .flex p { | ||
| 24 | margin-right: 1rem; | ||
| 25 | flex-shrink: 0; | ||
| 26 | } | ||
| 27 | |||
| 28 | .flex .dash-dropdown { | ||
| 29 | width: 100%; | ||
| 30 | } | ||
| 31 | |||
| 32 | .flex #date-picker-range { | ||
| 33 | width: 100%; | ||
| 34 | } | ||
screenshots/dashboard.png deleted
Binary file not shown.
screenshots/dashboard_01.png added
Binary file not shown.
screenshots/dashboard_02.png added
Binary file not shown.