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 | 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 | 23 | ## Screenshots |
| 24 | 24 | |
| 25 |  | |
| 25 |  | |
| 26 |  | |
app.py +69 −26
| @@ -1,7 +1,9 @@ | ||
| 1 | 1 | from dash import Dash, html, dcc, callback, Output, Input, dash_table |
| 2 | 2 | import plotly.express as px |
| 3 | 3 | import pandas as pd |
| 4 | import numpy as np | |
| 4 | 5 | import sqlite3 |
| 6 | from datetime import datetime, date | |
| 5 | 7 | |
| 6 | 8 | # Connect to database and query all incidents |
| 7 | 9 | connection = sqlite3.connect("./raw_data/ingress.db") |
| @@ -9,44 +11,51 @@ cursor = connection.cursor() | ||
| 9 | 11 | query = "SELECT * FROM incidents;" |
| 10 | 12 | df = pd.read_sql_query(query, connection).sort_values(by="description") |
| 11 | 13 | |
| 12 | # Create custom YEAR column to use in dropdown | |
| 13 | df['year'] = df['date'].str[-4:] | |
| 14 | # Replace empty cells with NaN | |
| 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 | 21 | # Configure HTML layout |
| 16 | 22 | app = Dash(__name__) |
| 17 | 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 | 25 | html.Div([ |
| 20 | html.H2(children="Totals per Category and Year", style={"textAlign":"center"}), | |
| 21 | dcc.Dropdown(df.sort_values("description").description.unique(), "INJURY", id="dropdown"), | |
| 22 | dcc.Dropdown(df.sort_values("year").year.unique(), "2023", id="year-dropdown"), | |
| 23 | html.Hr(), | |
| 24 | dash_table.DataTable(data=df.to_dict("records"), page_size=5, id="table"), | |
| 25 | html.H2(children="Map of Incidents per Category and Year", style={"textAlign":"center"}), | |
| 26 | dcc.Graph(id="map-graph") | |
| 26 | html.Div(className="flex", | |
| 27 | children = [ | |
| 28 | html.P("Crime:"), | |
| 29 | dcc.Dropdown(df.sort_values("description").description.unique(), "INJURY", id="dropdown"), | |
| 30 | ] | |
| 31 | ), | |
| 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 | 50 | # Create map |
| 43 | 51 | @callback( |
| 44 | 52 | Output("map-graph", "figure"), |
| 45 | 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): | |
| 49 | dff = df[df.year == year] | |
| 57 | def update_map(description, start_date, end_date): | |
| 58 | dff = df[(df['date'] > start_date) & (df['date'] < end_date)] | |
| 50 | 59 | dff = dff.reset_index() |
| 51 | 60 | dff = dff[dff.description == description] |
| 52 | 61 | |
| @@ -54,7 +63,6 @@ def update_map(description, year): | ||
| 54 | 63 | dff, |
| 55 | 64 | lat="lat", |
| 56 | 65 | lon="lon", |
| 57 | color="description", | |
| 58 | 66 | hover_name="description", |
| 59 | 67 | hover_data=["date", "time"], |
| 60 | 68 | title="Incident Count by Coordinates", |
| @@ -69,5 +77,40 @@ def update_map(description, year): | ||
| 69 | 77 | |
| 70 | 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 | 115 | if __name__ == "__main__": |
| 73 | 116 | app.run(debug=True) |
assets/styles.css +28 −3
| @@ -4,6 +4,31 @@ body { | ||
| 4 | 4 | margin: 2rem auto; |
| 5 | 5 | } |
| 6 | 6 | |
| 7 | hr { | |
| 8 | margin: 1rem auto; | |
| 9 | } | |
| \ No newline at end of file | ||
| 7 | .dash-table-container { | |
| 8 | overflow: scroll; | |
| 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.