From 21acf6c8f9125a5b638856f4ee872e8ba1f10230 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Fri, 27 May 2022 03:24:53 +0100 Subject: [PATCH 1/2] refactor v1 --- apps/dash-uber-rides-demo/.gitignore | 194 ++++++- apps/dash-uber-rides-demo/README.md | 2 +- apps/dash-uber-rides-demo/app.py | 486 ++---------------- apps/dash-uber-rides-demo/assets/css/app.py | 0 .../assets/{ => css}/base.css | 0 .../assets/{ => css}/style.css | 0 .../assets/dash-logo-new.png | Bin 18588 -> 0 bytes .../{ => assets/github}/demo.png | Bin .../assets/images/plotly-logo-dark-theme.png | Bin 0 -> 23021 bytes apps/dash-uber-rides-demo/constants.py | 52 ++ apps/dash-uber-rides-demo/requirements.txt | 4 +- apps/dash-uber-rides-demo/runtime.txt | 1 + apps/dash-uber-rides-demo/utils/components.py | 78 +++ apps/dash-uber-rides-demo/utils/figures.py | 185 +++++++ .../utils/helper_functions.py | 105 ++++ apps/dash-uber-rides-demo/utils/model.py | 0 16 files changed, 657 insertions(+), 450 deletions(-) create mode 100644 apps/dash-uber-rides-demo/assets/css/app.py rename apps/dash-uber-rides-demo/assets/{ => css}/base.css (100%) rename apps/dash-uber-rides-demo/assets/{ => css}/style.css (100%) delete mode 100644 apps/dash-uber-rides-demo/assets/dash-logo-new.png rename apps/dash-uber-rides-demo/{ => assets/github}/demo.png (100%) create mode 100644 apps/dash-uber-rides-demo/assets/images/plotly-logo-dark-theme.png create mode 100644 apps/dash-uber-rides-demo/constants.py create mode 100644 apps/dash-uber-rides-demo/runtime.txt create mode 100644 apps/dash-uber-rides-demo/utils/components.py create mode 100644 apps/dash-uber-rides-demo/utils/figures.py create mode 100644 apps/dash-uber-rides-demo/utils/helper_functions.py create mode 100644 apps/dash-uber-rides-demo/utils/model.py diff --git a/apps/dash-uber-rides-demo/.gitignore b/apps/dash-uber-rides-demo/.gitignore index 048af9b18..8e1af00b7 100644 --- a/apps/dash-uber-rides-demo/.gitignore +++ b/apps/dash-uber-rides-demo/.gitignore @@ -1,6 +1,190 @@ -venv -.vscode -*.pyc -.DS_Store +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments .env -output.csv \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-uber-rides-demo/README.md b/apps/dash-uber-rides-demo/README.md index b5d43456e..24209287b 100644 --- a/apps/dash-uber-rides-demo/README.md +++ b/apps/dash-uber-rides-demo/README.md @@ -47,7 +47,7 @@ You can run the app on your browser at http://127.0.0.1:8050 ## Screenshots -![demo.png](demo.png) +![demo.png](assets/github/demo.png) ## Resources diff --git a/apps/dash-uber-rides-demo/app.py b/apps/dash-uber-rides-demo/app.py index 00a8ec05e..08adb483f 100644 --- a/apps/dash-uber-rides-demo/app.py +++ b/apps/dash-uber-rides-demo/app.py @@ -1,63 +1,14 @@ -import dash -import dash_core_components as dcc -import dash_html_components as html -import pandas as pd -import numpy as np +from dash import Dash, dcc, html, Input, Output, callback -from dash.dependencies import Input, Output -from plotly import graph_objs as go -from plotly.graph_objs import * -from datetime import datetime as dt +import utils.figures as figs +from constants import totalList +from utils.helper_functions import total_rides_calculation +from utils.components import controls - -app = dash.Dash( - __name__, meta_tags=[{"name": "viewport", "content": "width=device-width"}], -) -app.title = "New York Uber Rides" +app = Dash(__name__, title = "New York Uber Rides") server = app.server -# Plotly mapbox public token -mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" - -# Dictionary of important locations in New York -list_of_locations = { - "Madison Square Garden": {"lat": 40.7505, "lon": -73.9934}, - "Yankee Stadium": {"lat": 40.8296, "lon": -73.9262}, - "Empire State Building": {"lat": 40.7484, "lon": -73.9857}, - "New York Stock Exchange": {"lat": 40.7069, "lon": -74.0113}, - "JFK Airport": {"lat": 40.644987, "lon": -73.785607}, - "Grand Central Station": {"lat": 40.7527, "lon": -73.9772}, - "Times Square": {"lat": 40.7589, "lon": -73.9851}, - "Columbia University": {"lat": 40.8075, "lon": -73.9626}, - "United Nations HQ": {"lat": 40.7489, "lon": -73.9680}, -} - -# Initialize data frame -df1 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data1.csv", - dtype=object, -) -df2 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data2.csv", - dtype=object, -) -df3 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data3.csv", - dtype=object, -) -df = pd.concat([df1, df2, df3], axis=0) -df["Date/Time"] = pd.to_datetime(df["Date/Time"], format="%Y-%m-%d %H:%M") -df.index = df["Date/Time"] -df.drop("Date/Time", 1, inplace=True) -totalList = [] -for month in df.groupby(df.index.month): - dailyList = [] - for day in month[1].groupby(month[1].index.day): - dailyList.append(day[1]) - totalList.append(dailyList) -totalList = np.array(totalList) - # Layout of Dash App app.layout = html.Div( children=[ @@ -65,85 +16,7 @@ className="row", children=[ # Column for user controls - html.Div( - className="four columns div-user-controls", - children=[ - html.A( - html.Img( - className="logo", - src=app.get_asset_url("dash-logo-new.png"), - ), - href="https://plotly.com/dash/", - ), - html.H2("DASH - UBER DATA APP"), - html.P( - """Select different days using the date picker or by selecting - different time frames on the histogram.""" - ), - html.Div( - className="div-for-dropdown", - children=[ - dcc.DatePickerSingle( - id="date-picker", - min_date_allowed=dt(2014, 4, 1), - max_date_allowed=dt(2014, 9, 30), - initial_visible_month=dt(2014, 4, 1), - date=dt(2014, 4, 1).date(), - display_format="MMMM D, YYYY", - style={"border": "0px solid black"}, - ) - ], - ), - # Change to side-by-side for mobile layout - html.Div( - className="row", - children=[ - html.Div( - className="div-for-dropdown", - children=[ - # Dropdown for locations on map - dcc.Dropdown( - id="location-dropdown", - options=[ - {"label": i, "value": i} - for i in list_of_locations - ], - placeholder="Select a location", - ) - ], - ), - html.Div( - className="div-for-dropdown", - children=[ - # Dropdown to select times - dcc.Dropdown( - id="bar-selector", - options=[ - { - "label": str(n) + ":00", - "value": str(n), - } - for n in range(24) - ], - multi=True, - placeholder="Select certain hours", - ) - ], - ), - ], - ), - html.P(id="total-rides"), - html.P(id="total-rides-selection"), - html.P(id="date-value"), - dcc.Markdown( - """ - Source: [FiveThirtyEight](https://github.com/fivethirtyeight/uber-tlc-foil-response/tree/master/uber-trip-data) - - Links: [Source Code](https://github.com/plotly/dash-sample-apps/tree/main/apps/dash-uber-rides-demo) | [Enterprise Demo](https://plotly.com/get-demo/) - """ - ), - ], - ), + controls(app), # Column for app graphs and plots html.Div( className="eight columns div-for-charts bg-grey", @@ -163,66 +36,14 @@ ] ) -# Gets the amount of days in the specified month -# Index represents month (0 is April, 1 is May, ... etc.) -daysInMonth = [30, 31, 30, 31, 31, 30] - -# Get index for the specified month in the dataframe -monthIndex = pd.Index(["Apr", "May", "June", "July", "Aug", "Sept"]) - -# Get the amount of rides per hour based on the time selected -# This also higlights the color of the histogram bars based on -# if the hours are selected -def get_selection(month, day, selection): - xVal = [] - yVal = [] - xSelected = [] - colorVal = [ - "#F4EC15", - "#DAF017", - "#BBEC19", - "#9DE81B", - "#80E41D", - "#66E01F", - "#4CDC20", - "#34D822", - "#24D249", - "#25D042", - "#26CC58", - "#28C86D", - "#29C481", - "#2AC093", - "#2BBCA4", - "#2BB5B8", - "#2C99B4", - "#2D7EB0", - "#2D65AC", - "#2E4EA4", - "#2E38A4", - "#3B2FA0", - "#4E2F9C", - "#603099", - ] - - # Put selected times into a list of numbers xSelected - xSelected.extend([int(x) for x in selection]) - - for i in range(24): - # If bar is selected then color it white - if i in xSelected and len(xSelected) < 24: - colorVal[i] = "#FFFFFF" - xVal.append(i) - # Get the number of rides at a particular time - yVal.append(len(totalList[month][day][totalList[month][day].index.hour == i])) - return [np.array(xVal), np.array(yVal), np.array(colorVal)] - -# Selected Data in the Histogram updates the Values in the Hours selection dropdown menu -@app.callback( +@callback( Output("bar-selector", "value"), - [Input("histogram", "selectedData"), Input("histogram", "clickData")], + Input("histogram", "selectedData"), + Input("histogram", "clickData"), ) def update_bar_selector(value, clickData): + " Selected Data in the Histogram updates the Values in the Hours selection dropdown menu " holder = [] if clickData: holder.append(str(int(clickData["points"][0]["x"]))) @@ -232,276 +53,57 @@ def update_bar_selector(value, clickData): return list(set(holder)) -# Clear Selected Data if Click Data is used -@app.callback(Output("histogram", "selectedData"), [Input("histogram", "clickData")]) +@callback( + Output("histogram", "selectedData"), + Input("histogram", "clickData"), +) def update_selected_data(clickData): + " Clear Selected Data if Click Data is used " if clickData: return {"points": []} -# Update the total number of rides Tag -@app.callback(Output("total-rides", "children"), [Input("date-picker", "date")]) +@callback( + Output("total-rides", "children"), + Input("date-picker", "date"), +) def update_total_rides(datePicked): + " Update the total number of rides Tag " date_picked = dt.strptime(datePicked, "%Y-%m-%d") - return "Total Number of rides: {:,d}".format( - len(totalList[date_picked.month - 4][date_picked.day - 1]) - ) + total_rides = len(totalList[date_picked.month - 4][date_picked.day - 1]) + return f"Total Number of rides: {total_rides:,d}" -# Update the total number of rides in selected times -@app.callback( - [Output("total-rides-selection", "children"), Output("date-value", "children")], - [Input("date-picker", "date"), Input("bar-selector", "value")], +@callback( + Output("total-rides-selection", "children"), + Output("date-value", "children"), + Input("date-picker", "date"), + Input("bar-selector", "value"), ) -def update_total_rides_selection(datePicked, selection): - firstOutput = "" - - if selection is not None or len(selection) is not 0: - date_picked = dt.strptime(datePicked, "%Y-%m-%d") - totalInSelection = 0 - for x in selection: - totalInSelection += len( - totalList[date_picked.month - 4][date_picked.day - 1][ - totalList[date_picked.month - 4][date_picked.day - 1].index.hour - == int(x) - ] - ) - firstOutput = "Total rides in selection: {:,d}".format(totalInSelection) +def update_total_rides_selection(date_picked, bars_selected): + " Update the total number of rides in selected times " + return total_rides_calculation(date_picked, bars_selected) - if ( - datePicked is None - or selection is None - or len(selection) is 24 - or len(selection) is 0 - ): - return firstOutput, (datePicked, " - showing hour(s): All") - holder = sorted([int(x) for x in selection]) - - if holder == list(range(min(holder), max(holder) + 1)): - return ( - firstOutput, - ( - datePicked, - " - showing hour(s): ", - holder[0], - "-", - holder[len(holder) - 1], - ), - ) - - holder_to_string = ", ".join(str(x) for x in holder) - return firstOutput, (datePicked, " - showing hour(s): ", holder_to_string) - - -# Update Histogram Figure based on Month, Day and Times Chosen -@app.callback( +@callback( Output("histogram", "figure"), - [Input("date-picker", "date"), Input("bar-selector", "value")], + Input("date-picker", "date"), + Input("bar-selector", "value"), ) -def update_histogram(datePicked, selection): - date_picked = dt.strptime(datePicked, "%Y-%m-%d") - monthPicked = date_picked.month - 4 - dayPicked = date_picked.day - 1 +def update_histogram(date_picked, bars_selected): + " Update Histogram Figure based on Month, Day and Times Chosen " + return figs.histogram(date_picked, bars_selected) - [xVal, yVal, colorVal] = get_selection(monthPicked, dayPicked, selection) - layout = go.Layout( - bargap=0.01, - bargroupgap=0, - barmode="group", - margin=go.layout.Margin(l=10, r=0, t=0, b=50), - showlegend=False, - plot_bgcolor="#323130", - paper_bgcolor="#323130", - dragmode="select", - font=dict(color="white"), - xaxis=dict( - range=[-0.5, 23.5], - showgrid=False, - nticks=25, - fixedrange=True, - ticksuffix=":00", - ), - yaxis=dict( - range=[0, max(yVal) + max(yVal) / 4], - showticklabels=False, - showgrid=False, - fixedrange=True, - rangemode="nonnegative", - zeroline=False, - ), - annotations=[ - dict( - x=xi, - y=yi, - text=str(yi), - xanchor="center", - yanchor="bottom", - showarrow=False, - font=dict(color="white"), - ) - for xi, yi in zip(xVal, yVal) - ], - ) - - return go.Figure( - data=[ - go.Bar(x=xVal, y=yVal, marker=dict(color=colorVal), hoverinfo="x"), - go.Scatter( - opacity=0, - x=xVal, - y=yVal / 2, - hoverinfo="none", - mode="markers", - marker=dict(color="rgb(66, 134, 244, 0)", symbol="square", size=40), - visible=True, - ), - ], - layout=layout, - ) - - -# Get the Coordinates of the chosen months, dates and times -def getLatLonColor(selectedData, month, day): - listCoords = totalList[month][day] - - # No times selected, output all times for chosen month and date - if selectedData is None or len(selectedData) is 0: - return listCoords - listStr = "listCoords[" - for time in selectedData: - if selectedData.index(time) is not len(selectedData) - 1: - listStr += "(totalList[month][day].index.hour==" + str(int(time)) + ") | " - else: - listStr += "(totalList[month][day].index.hour==" + str(int(time)) + ")]" - return eval(listStr) - - -# Update Map Graph based on date-picker, selected data on histogram and location dropdown -@app.callback( +@callback( Output("map-graph", "figure"), - [ - Input("date-picker", "date"), - Input("bar-selector", "value"), - Input("location-dropdown", "value"), - ], + Input("date-picker", "date"), + Input("bar-selector", "value"), + Input("location-dropdown", "value"), ) -def update_graph(datePicked, selectedData, selectedLocation): - zoom = 12.0 - latInitial = 40.7272 - lonInitial = -73.991251 - bearing = 0 - - if selectedLocation: - zoom = 15.0 - latInitial = list_of_locations[selectedLocation]["lat"] - lonInitial = list_of_locations[selectedLocation]["lon"] - - date_picked = dt.strptime(datePicked, "%Y-%m-%d") - monthPicked = date_picked.month - 4 - dayPicked = date_picked.day - 1 - listCoords = getLatLonColor(selectedData, monthPicked, dayPicked) - - return go.Figure( - data=[ - # Data for all rides based on date and time - Scattermapbox( - lat=listCoords["Lat"], - lon=listCoords["Lon"], - mode="markers", - hoverinfo="lat+lon+text", - text=listCoords.index.hour, - marker=dict( - showscale=True, - color=np.append(np.insert(listCoords.index.hour, 0, 0), 23), - opacity=0.5, - size=5, - colorscale=[ - [0, "#F4EC15"], - [0.04167, "#DAF017"], - [0.0833, "#BBEC19"], - [0.125, "#9DE81B"], - [0.1667, "#80E41D"], - [0.2083, "#66E01F"], - [0.25, "#4CDC20"], - [0.292, "#34D822"], - [0.333, "#24D249"], - [0.375, "#25D042"], - [0.4167, "#26CC58"], - [0.4583, "#28C86D"], - [0.50, "#29C481"], - [0.54167, "#2AC093"], - [0.5833, "#2BBCA4"], - [1.0, "#613099"], - ], - colorbar=dict( - title="Time of
Day", - x=0.93, - xpad=0, - nticks=24, - tickfont=dict(color="#d8d8d8"), - titlefont=dict(color="#d8d8d8"), - thicknessmode="pixels", - ), - ), - ), - # Plot of important locations on the map - Scattermapbox( - lat=[list_of_locations[i]["lat"] for i in list_of_locations], - lon=[list_of_locations[i]["lon"] for i in list_of_locations], - mode="markers", - hoverinfo="text", - text=[i for i in list_of_locations], - marker=dict(size=8, color="#ffa0a0"), - ), - ], - layout=Layout( - autosize=True, - margin=go.layout.Margin(l=0, r=35, t=0, b=0), - showlegend=False, - mapbox=dict( - accesstoken=mapbox_access_token, - center=dict(lat=latInitial, lon=lonInitial), # 40.7272 # -73.991251 - style="dark", - bearing=bearing, - zoom=zoom, - ), - updatemenus=[ - dict( - buttons=( - [ - dict( - args=[ - { - "mapbox.zoom": 12, - "mapbox.center.lon": "-73.991251", - "mapbox.center.lat": "40.7272", - "mapbox.bearing": 0, - "mapbox.style": "dark", - } - ], - label="Reset Zoom", - method="relayout", - ) - ] - ), - direction="left", - pad={"r": 0, "t": 0, "b": 0, "l": 0}, - showactive=False, - type="buttons", - x=0.45, - y=0.02, - xanchor="left", - yanchor="bottom", - bgcolor="#323130", - borderwidth=1, - bordercolor="#6d6d6d", - font=dict(color="#FFFFFF"), - ) - ], - ), - ) +def update_graph(date_picked, bars_selected, location): + " Update Map Graph based on date-picker, selected data on histogram and location dropdown " + return figs.map(date_picked, bars_selected, location) if __name__ == "__main__": diff --git a/apps/dash-uber-rides-demo/assets/css/app.py b/apps/dash-uber-rides-demo/assets/css/app.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/dash-uber-rides-demo/assets/base.css b/apps/dash-uber-rides-demo/assets/css/base.css similarity index 100% rename from apps/dash-uber-rides-demo/assets/base.css rename to apps/dash-uber-rides-demo/assets/css/base.css diff --git a/apps/dash-uber-rides-demo/assets/style.css b/apps/dash-uber-rides-demo/assets/css/style.css similarity index 100% rename from apps/dash-uber-rides-demo/assets/style.css rename to apps/dash-uber-rides-demo/assets/css/style.css diff --git a/apps/dash-uber-rides-demo/assets/dash-logo-new.png b/apps/dash-uber-rides-demo/assets/dash-logo-new.png deleted file mode 100644 index 040cde17424f9bd58cec6360eb2d1ace958f0634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18588 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 Date: Thu, 1 Dec 2022 18:40:41 +0000 Subject: [PATCH 2/2] final --- apps/dash-uber-rides-demo/app.py | 73 ++-- apps/dash-uber-rides-demo/assets/css/app.py | 0 apps/dash-uber-rides-demo/assets/css/base.css | 392 ------------------ .../dash-uber-rides-demo/assets/css/style.css | 54 ++- apps/dash-uber-rides-demo/constants.py | 22 +- apps/dash-uber-rides-demo/requirements.txt | 5 +- apps/dash-uber-rides-demo/utils/components.py | 89 ++-- apps/dash-uber-rides-demo/utils/figures.py | 12 +- .../utils/helper_functions.py | 6 +- 9 files changed, 155 insertions(+), 498 deletions(-) delete mode 100644 apps/dash-uber-rides-demo/assets/css/app.py delete mode 100644 apps/dash-uber-rides-demo/assets/css/base.css diff --git a/apps/dash-uber-rides-demo/app.py b/apps/dash-uber-rides-demo/app.py index 08adb483f..2082567bf 100644 --- a/apps/dash-uber-rides-demo/app.py +++ b/apps/dash-uber-rides-demo/app.py @@ -1,40 +1,61 @@ from dash import Dash, dcc, html, Input, Output, callback +import dash_bootstrap_components as dbc import utils.figures as figs from constants import totalList from utils.helper_functions import total_rides_calculation from utils.components import controls +from datetime import datetime as dt -app = Dash(__name__, title = "New York Uber Rides") +app = Dash(__name__, title = "New York Uber Rides", external_stylesheets=[dbc.themes.BOOTSTRAP]) server = app.server # Layout of Dash App -app.layout = html.Div( - children=[ - html.Div( - className="row", - children=[ - # Column for user controls - controls(app), - # Column for app graphs and plots - html.Div( - className="eight columns div-for-charts bg-grey", - children=[ - dcc.Graph(id="map-graph"), - html.Div( - className="text-padding", - children=[ - "Select any of the bars on the histogram to section data by time." - ], - ), - dcc.Graph(id="histogram"), - ], - ), - ], - ) - ] -) +# app.layout = html.Div( +# children=[ +# html.Div( +# className="row", +# children=[ +# # Column for user controls +# controls(app), +# # Column for app graphs and plots +# html.Div( +# className="eight columns div-for-charts bg-grey", +# children=[ +# dcc.Graph(id="map-graph", config={ 'displayModeBar': False }), +# html.Div( +# className="text-padding", +# children="Select any of the bars on the histogram to section data by time." +# ), +# dcc.Graph(id="histogram", config={ 'displayModeBar': False }), +# ], +# ), +# ], +# ) +# ] +# ) + + +# Layout of Dash App +app.layout = dbc.Row([ + # Column for user controls + controls(app), + + # Column for app graphs and plots + dbc.Col( + children=[ + dcc.Graph(id="map-graph", config={ 'displayModeBar': False }), + html.Div( + className="text-padding", + children="Select any of the bars on the histogram to section data by time." + ), + dcc.Graph(id="histogram", config={ 'displayModeBar': False }), + ], + className="div-for-charts bg-grey", + md=8, + ), +]) @callback( diff --git a/apps/dash-uber-rides-demo/assets/css/app.py b/apps/dash-uber-rides-demo/assets/css/app.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/dash-uber-rides-demo/assets/css/base.css b/apps/dash-uber-rides-demo/assets/css/base.css deleted file mode 100644 index 98d7d1891..000000000 --- a/apps/dash-uber-rides-demo/assets/css/base.css +++ /dev/null @@ -1,392 +0,0 @@ -/* Table of contents –––––––––––––––––––––––––––––––––––––––––––––––––– -Taken from https://codepen.io/chriddyp/pen/bWLwgP.css -- Grid -- Base Styles - - Typography - - Links - - Buttons - - Forms - - Lists - - Code - - Tables - - Spacing - - Utilities - - Clearing - - Media Queries - - Custom App CSS */ - - -/* Grid –––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; - } - .column, .columns { - width: 100%; - float: left; - box-sizing: border-box; - } - /* For devices larger than 400px */ - @media (min-width: 400px) { - .container { - width: 85%; - padding: 0; - } - } - /* For devices larger than 550px */ - @media (min-width: 550px) { - .container { - width: 80%; - } - .column, .columns { - margin-left: 4%; - } - .column:first-child, .columns:first-child { - margin-left: 0; - } - .one.column, .one.columns { - width: 4.66666666667%; - } - .two.columns { - width: 13.3333333333%; - } - .three.columns { - width: 22%; - } - .four.columns { - width: 30.6666666667%; - } - .five.columns { - width: 39.3333333333%; - } - .six.columns { - width: 48%; - } - .seven.columns { - width: 56.6666666667%; - } - .eight.columns { - width: 65.3333333333%; - } - .nine.columns { - width: 74.0%; - } - .ten.columns { - width: 82.6666666667%; - } - .eleven.columns { - width: 91.3333333333%; - } - .twelve.columns { - width: 100%; - margin-left: 0; - } - .one-third.column { - width: 30.6666666667%; - } - .two-thirds.column { - width: 65.3333333333%; - } - .one-half.column { - width: 48%; - } - /* Offsets */ - .offset-by-one.column, .offset-by-one.columns { - margin-left: 8.66666666667%; - } - .offset-by-two.column, .offset-by-two.columns { - margin-left: 17.3333333333%; - } - .offset-by-three.column, .offset-by-three.columns { - margin-left: 26%; - } - .offset-by-four.column, .offset-by-four.columns { - margin-left: 34.6666666667%; - } - .offset-by-five.column, .offset-by-five.columns { - margin-left: 43.3333333333%; - } - .offset-by-six.column, .offset-by-six.columns { - margin-left: 52%; - } - .offset-by-seven.column, .offset-by-seven.columns { - margin-left: 60.6666666667%; - } - .offset-by-eight.column, .offset-by-eight.columns { - margin-left: 69.3333333333%; - } - .offset-by-nine.column, .offset-by-nine.columns { - margin-left: 78.0%; - } - .offset-by-ten.column, .offset-by-ten.columns { - margin-left: 86.6666666667%; - } - .offset-by-eleven.column, .offset-by-eleven.columns { - margin-left: 95.3333333333%; - } - .offset-by-one-third.column, .offset-by-one-third.columns { - margin-left: 34.6666666667%; - } - .offset-by-two-thirds.column, .offset-by-two-thirds.columns { - margin-left: 69.3333333333%; - } - .offset-by-one-half.column, .offset-by-one-half.columns { - margin-left: 52%; - } - } - /* Base Styles –––––––––––––––––––––––––––––––––––––––––––––––––– */ - /* NOTE html is set to 62.5% so that all the REM measurements throughout Skeleton are based on 10px sizing. So basically 1.5rem = 15px :) */ - html { - font-size: 62.5%; - } - body { - font-size: 1.5em; - /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: rgb(50, 50, 50); - } - /* Typography –––––––––––––––––––––––––––––––––––––––––––––––––– */ - h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 0; - font-weight: 300; - } - h1 { - font-size: 4.5rem; - line-height: 1.2; - letter-spacing: -.1rem; - margin-bottom: 2rem; - } - h2 { - font-size: 3.6rem; - line-height: 1.25; - letter-spacing: -.1rem; - margin-bottom: 1.8rem; - margin-top: 1.8rem; - } - h3 { - font-size: 3.0rem; - line-height: 1.3; - letter-spacing: -.1rem; - margin-bottom: 1.5rem; - margin-top: 1.5rem; - } - h4 { - font-size: 2.6rem; - line-height: 1.35; - letter-spacing: -.08rem; - margin-bottom: 1.2rem; - margin-top: 1.2rem; - } - h5 { - font-size: 2.2rem; - line-height: 1.5; - letter-spacing: -.05rem; - margin-bottom: 0.6rem; - margin-top: 0.6rem; - } - h6 { - font-size: 2.0rem; - line-height: 1.6; - letter-spacing: 0; - margin-bottom: 0.75rem; - margin-top: 0.75rem; - } - p { - margin-top: 0; - } - /* Blockquotes –––––––––––––––––––––––––––––––––––––––––––––––––– */ - blockquote { - border-left: 4px lightgrey solid; - padding-left: 1rem; - margin-top: 2rem; - margin-bottom: 2rem; - margin-left: 0rem; - } - /* Links –––––––––––––––––––––––––––––––––––––––––––––––––– */ - a { - color: #1EAEDB; - text-decoration: underline; - cursor: pointer; - } - a:hover { - color: #0FA0CE; - } - /* Buttons –––––––––––––––––––––––––––––––––––––––––––––––––– */ - .button, button, input[type="submit"], input[type="reset"], input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; - } - .button:hover, button:hover, input[type="submit"]:hover, input[type="reset"]:hover, input[type="button"]:hover, .button:focus, button:focus, input[type="submit"]:focus, input[type="reset"]:focus, input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; - } - .button.button-primary, button.button-primary, input[type="submit"].button-primary, input[type="reset"].button-primary, input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; - } - .button.button-primary:hover, button.button-primary:hover, input[type="submit"].button-primary:hover, input[type="reset"].button-primary:hover, input[type="button"].button-primary:hover, .button.button-primary:focus, button.button-primary:focus, input[type="submit"].button-primary:focus, input[type="reset"].button-primary:focus, input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; - } - /* Forms –––––––––––––––––––––––––––––––––––––––––––––––––– */ - input[type="email"], input[type="number"], input[type="search"], input[type="text"], input[type="tel"], input[type="url"], input[type="password"], textarea, select { - height: 38px; - padding: 6px 10px; - /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; - font-family: inherit; - font-size: inherit; - /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/ - } - /* Removes awkward default styles on some inputs for iOS */ - input[type="email"], input[type="number"], input[type="search"], input[type="text"], input[type="tel"], input[type="url"], input[type="password"], textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - } - textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; - } - input[type="email"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="text"]:focus, input[type="tel"]:focus, input[type="url"]:focus, input[type="password"]:focus, textarea:focus, select:focus { - border: 1px solid #33C3F0; - outline: 0; - } - label, legend { - display: block; - margin-bottom: 0px; - } - fieldset { - padding: 0; - border-width: 0; - } - input[type="checkbox"], input[type="radio"] { - display: inline; - } - label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; - } - /* Lists –––––––––––––––––––––––––––––––––––––––––––––––––– */ - ul { - list-style: circle inside; - } - ol { - list-style: decimal inside; - } - ol, ul { - padding-left: 0; - margin-top: 0; - } - ul ul, ul ol, ol ol, ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; - } - li { - margin-bottom: 1rem; - } - /* Tables –––––––––––––––––––––––––––––––––––––––––––––––––– */ - table { - border-collapse: collapse; - } - th, td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; - } - th:first-child, td:first-child { - padding-left: 0; - } - th:last-child, td:last-child { - padding-right: 0; - } - /* Spacing –––––––––––––––––––––––––––––––––––––––––––––––––– */ - button, .button { - margin-bottom: 0rem; - } - input, textarea, select, fieldset { - margin-bottom: 0rem; - } - pre, dl, figure, table, form { - margin-bottom: 0rem; - } - p, ul, ol { - margin-bottom: 0.75rem; - } - /* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */ - .u-full-width { - width: 100%; - box-sizing: border-box; - } - .u-max-full-width { - max-width: 100%; - box-sizing: border-box; - } - .u-pull-right { - float: right; - } - .u-pull-left { - float: left; - } - /* Misc –––––––––––––––––––––––––––––––––––––––––––––––––– */ - hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; - } - /* Clearing –––––––––––––––––––––––––––––––––––––––––––––––––– */ - /* Self Clearing Goodness */ - .container:after, .row:after, .u-cf { - content: ""; - display: table; - clear: both; - } - /* Media Queries –––––––––––––––––––––––––––––––––––––––––––––––––– */ - /* Note: The best way to structure the use of media queries is to create the queries near the relevant code. For example, if you wanted to change the styles for buttons on small devices, paste the mobile query code up in the buttons section and style it there. */ - /* Larger than mobile */ - @media (min-width: 400px) { - } - /* Larger than phablet (also point when grid becomes active) */ - @media (min-width: 550px) { - } - /* Larger than tablet */ - @media (min-width: 750px) { - } - /* Larger than desktop */ - @media (min-width: 1000px) { - } - /* Larger than Desktop HD */ - @media (min-width: 1200px) { - } \ No newline at end of file diff --git a/apps/dash-uber-rides-demo/assets/css/style.css b/apps/dash-uber-rides-demo/assets/css/style.css index 9df9a2de6..46c48ad97 100644 --- a/apps/dash-uber-rides-demo/assets/css/style.css +++ b/apps/dash-uber-rides-demo/assets/css/style.css @@ -6,8 +6,6 @@ body { background-color: #1E1E1E; color: #d8d8d8; - margin: 0; - padding: 0; } h1, h2, h3, h4, h5 { font-family: "Open Sans"; @@ -22,9 +20,6 @@ p { font-size: 14px; padding-left: 12px; } -a { - text-decoration: none; -} .bg-grey{ background-color: #31302F; } @@ -36,7 +31,7 @@ a { display: flex; flex-direction: column; height: 100vh; - width: 100%; + /* width: 100%; */ } #histogram { flex-grow: 1 @@ -60,11 +55,17 @@ a { width: 97%; text-align: center; } -.logo { + +.logos { + display: flex; + justify-content: space-between; +} +.logos img { height: 35px; padding-bottom: 12px; margin-left: 8px; } + .Select-control, .Select-menu-outer, .Select-multi-value-wrapper, .select-up, .is-open .Select-control { background-color: #1E1E1E; color: white; @@ -157,7 +158,6 @@ a { } .div-for-charts { padding: 0px; - width: 100%; text-align: center; } @@ -207,3 +207,41 @@ a { .CalendarMonth_table td { padding: unset; } + + + +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans, sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; + vertical-align: super; +} + +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-uber-rides-demo/constants.py b/apps/dash-uber-rides-demo/constants.py index 46c20c47b..d58ceef45 100644 --- a/apps/dash-uber-rides-demo/constants.py +++ b/apps/dash-uber-rides-demo/constants.py @@ -5,7 +5,7 @@ mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" # Dictionary of important locations in New York -list_of_locations = { +dict_of_locations = { "Madison Square Garden": {"lat": 40.7505, "lon": -73.9934}, "Yankee Stadium": {"lat": 40.8296, "lon": -73.9262}, "Empire State Building": {"lat": 40.7484, "lon": -73.9857}, @@ -18,29 +18,21 @@ } # Initialize data frame -df1 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data1.csv", - dtype=object, -) -df2 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data2.csv", - dtype=object, -) -df3 = pd.read_csv( - "https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data3.csv", - dtype=object, -) +df1 = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data1.csv", dtype=object ) +df2 = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data2.csv", dtype=object ) +df3 = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/uber-rides-data3.csv", dtype=object ) df = pd.concat([df1, df2, df3], axis=0) + df["Date/Time"] = pd.to_datetime(df["Date/Time"], format="%Y-%m-%d %H:%M") df.index = df["Date/Time"] -df.drop("Date/Time", 1, inplace=True) +df.drop("Date/Time", axis=1, inplace=True) totalList = [] for month in df.groupby(df.index.month): dailyList = [] for day in month[1].groupby(month[1].index.day): dailyList.append(day[1]) totalList.append(dailyList) -totalList = np.array(totalList) +# totalList = np.array(totalList) diff --git a/apps/dash-uber-rides-demo/requirements.txt b/apps/dash-uber-rides-demo/requirements.txt index 0059cf942..ffe41dfb0 100644 --- a/apps/dash-uber-rides-demo/requirements.txt +++ b/apps/dash-uber-rides-demo/requirements.txt @@ -1,3 +1,4 @@ -dash==1.12.0 +dash==2.4.1 pandas==1.4.2 -gunicorn==20.1.0 \ No newline at end of file +gunicorn==20.1.0 +dash-bootstrap-components==1.2.1 \ No newline at end of file diff --git a/apps/dash-uber-rides-demo/utils/components.py b/apps/dash-uber-rides-demo/utils/components.py index 738e4360c..441ad22b7 100644 --- a/apps/dash-uber-rides-demo/utils/components.py +++ b/apps/dash-uber-rides-demo/utils/components.py @@ -1,11 +1,13 @@ from datetime import datetime as dt from dash import html, dcc +import dash_bootstrap_components as dbc -from constants import list_of_locations +from constants import dict_of_locations def controls(app): - return html.Div( - className="four columns div-user-controls", + return dbc.Col( + className="div-user-controls", + md=4, children=[ html.Div([ html.A( @@ -17,60 +19,55 @@ def controls(app): ), html.A("LEARN MORE", href="https://plotly.com/dash/", target="_blank", className="demo-button") ], className="logos"), + html.H2("DASH - UBER DATA APP"), html.P( """Select different days using the date picker or by selecting different time frames on the histogram.""" ), - html.Div( + + dcc.DatePickerSingle( + id="date-picker", + min_date_allowed=dt(2014, 4, 1), + max_date_allowed=dt(2014, 9, 30), + initial_visible_month=dt(2014, 4, 1), + date=dt(2014, 4, 1).date(), + display_format="MMMM D, YYYY", + style={"border": "0px solid black"}, className="div-for-dropdown", - children=[ - dcc.DatePickerSingle( - id="date-picker", - min_date_allowed=dt(2014, 4, 1), - max_date_allowed=dt(2014, 9, 30), - initial_visible_month=dt(2014, 4, 1), - date=dt(2014, 4, 1).date(), - display_format="MMMM D, YYYY", - style={"border": "0px solid black"}, - ) - ], ), - # Change to side-by-side for mobile layout - html.Div( - className="row", - children=[ - html.Div( + + dbc.Row([ + # Dropdown for locations on map + dbc.Col( + dcc.Dropdown( + id="location-dropdown", + options=[{'label': k, 'value': k} for k, v in dict_of_locations.items()], + placeholder="Select a location", className="div-for-dropdown", - children=[ - # Dropdown for locations on map - dcc.Dropdown( - id="location-dropdown", - options=list_of_locations, - placeholder="Select a location", - ) - ], ), - html.Div( - className="div-for-dropdown", - children=[ - # Dropdown to select times - dcc.Dropdown( - id="bar-selector", - options=[ - { - "label": str(n) + ":00", - "value": str(n), - } - for n in range(24) - ], - multi=True, - placeholder="Select certain hours", - ) + sm=6, + ), + + # Dropdown to select times + dbc.Col( + dcc.Dropdown( + id="bar-selector", + options=[ + { + "label": str(n) + ":00", + "value": str(n), + } + for n in range(24) ], + multi=True, + placeholder="Select certain hours", + className="div-for-dropdown", ), - ], - ), + sm=6, + ), + ]), + html.P(id="total-rides"), html.P(id="total-rides-selection"), html.P(id="date-value"), diff --git a/apps/dash-uber-rides-demo/utils/figures.py b/apps/dash-uber-rides-demo/utils/figures.py index ebcdca8b6..ada9f9cb7 100644 --- a/apps/dash-uber-rides-demo/utils/figures.py +++ b/apps/dash-uber-rides-demo/utils/figures.py @@ -3,7 +3,7 @@ import numpy as np from utils.helper_functions import get_selection, getLatLonColor -from constants import list_of_locations, mapbox_access_token +from constants import dict_of_locations, mapbox_access_token def map(date_picked, bars_selected, location): zoom = 12.0 @@ -13,8 +13,8 @@ def map(date_picked, bars_selected, location): if location: zoom = 15.0 - latInitial = list_of_locations[location]["lat"] - lonInitial = list_of_locations[location]["lon"] + latInitial = dict_of_locations[location]["lat"] + lonInitial = dict_of_locations[location]["lon"] date_picked = dt.strptime(date_picked, "%Y-%m-%d") monthPicked = date_picked.month - 4 @@ -66,11 +66,11 @@ def map(date_picked, bars_selected, location): ), # Plot of important locations on the map go.Scattermapbox( - lat=[list_of_locations[i]["lat"] for i in list_of_locations], - lon=[list_of_locations[i]["lon"] for i in list_of_locations], + lat=[dict_of_locations[i]["lat"] for i in dict_of_locations], + lon=[dict_of_locations[i]["lon"] for i in dict_of_locations], mode="markers", hoverinfo="text", - text=[i for i in list_of_locations], + text=[i for i in dict_of_locations], marker=dict(size=8, color="#ffa0a0"), ), ], diff --git a/apps/dash-uber-rides-demo/utils/helper_functions.py b/apps/dash-uber-rides-demo/utils/helper_functions.py index 912539657..53aa3ca04 100644 --- a/apps/dash-uber-rides-demo/utils/helper_functions.py +++ b/apps/dash-uber-rides-demo/utils/helper_functions.py @@ -55,11 +55,11 @@ def getLatLonColor(selectedData, month, day): listCoords = totalList[month][day] # No times selected, output all times for chosen month and date - if selectedData is None or len(selectedData) is 0: + if selectedData is None or len(selectedData) == 0: return listCoords listStr = "listCoords[" for time in selectedData: - if selectedData.index(time) is not len(selectedData) - 1: + if selectedData.index(time) != len(selectedData) - 1: listStr += "(totalList[month][day].index.hour==" + str(int(time)) + ") | " else: listStr += "(totalList[month][day].index.hour==" + str(int(time)) + ")]" @@ -80,7 +80,7 @@ def total_rides_calculation(date_picked, bars_selected): == int(x) ] ) - firstOutput = "Total rides in selection: {:,d}".format(totalInSelection) + firstOutput = f"Total rides in selection: {totalInSelection:,d}" if (bars_selected is None or bars_selected is None