diff --git a/apps/dash-oil-and-gas/.gitignore b/apps/dash-oil-and-gas/.gitignore deleted file mode 100644 index 80812c698..000000000 --- a/apps/dash-oil-and-gas/.gitignore +++ /dev/null @@ -1,103 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -doc/build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject - -# Added -unused/ -vv -.DS_Store -pyvenv.cfg -.vscode/ - -data/points.pkl \ No newline at end of file diff --git a/apps/dash-oil-and-gas/README.md b/apps/dash-oil-and-gas/README.md index d29b14970..0378a7de8 100644 --- a/apps/dash-oil-and-gas/README.md +++ b/apps/dash-oil-and-gas/README.md @@ -51,11 +51,11 @@ This Dash app displays oil production in western New York. There are filters at The following are screenshots for the app in this repo: -![animated1](screenshots/animated1.gif) +![animated1](assets/github/animated1.gif) -![screenshot](screenshots/screenshot1.png) +![screenshot](assets/github/screenshot1.png) -![screenshot](screenshots/screenshot2.png) +![screenshot](assets/github/screenshot2.png) -![screenshot](screenshots/screenshot3.png) +![screenshot](assets/github/screenshot3.png) diff --git a/apps/dash-oil-and-gas/app.py b/apps/dash-oil-and-gas/app.py index 4dcce7d65..655b73c82 100644 --- a/apps/dash-oil-and-gas/app.py +++ b/apps/dash-oil-and-gas/app.py @@ -1,86 +1,58 @@ -# Import required libraries -import pickle import copy -import pathlib -import urllib.request import dash import math import datetime as dt -import pandas as pd -from dash.dependencies import Input, Output, State, ClientsideFunction -import dash_core_components as dcc -import dash_html_components as html -# Multi-dropdown options -from controls import COUNTIES, WELL_STATUSES, WELL_TYPES, WELL_COLORS - - -# get relative data folder -PATH = pathlib.Path(__file__).parent -DATA_PATH = PATH.joinpath("data").resolve() - -app = dash.Dash( - __name__, meta_tags=[{"name": "viewport", "content": "width=device-width"}], +from dash import ( + Dash, + html, + dcc, + Input, + Output, + State, + callback, + callback_context, + ClientsideFunction, ) -app.title = "Oil & Gas Wells" -server = app.server -# Create controls -county_options = [ - {"label": str(COUNTIES[county]), "value": str(county)} for county in COUNTIES -] - -well_status_options = [ - {"label": str(WELL_STATUSES[well_status]), "value": str(well_status)} - for well_status in WELL_STATUSES -] - -well_type_options = [ - {"label": str(WELL_TYPES[well_type]), "value": str(well_type)} - for well_type in WELL_TYPES -] +# Multi-dropdown options +from constants import ( + WELL_STATUSES, + WELL_TYPES, +) +from utils.data import df -# Download pickle file -urllib.request.urlretrieve( - "https://raw.githubusercontent.com/plotly/datasets/master/dash-sample-apps/dash-oil-and-gas/data/points.pkl", - DATA_PATH.joinpath("points.pkl"), +from utils.figures import ( + make_main_figure, + make_individual_figure, + make_aggregate_figure, + make_pie_figure, + make_count_figure, +) +from utils.helper_functions import ( + human_format, + filter_dataframe, + produce_individual, + produce_aggregate, ) -points = pickle.load(open(DATA_PATH.joinpath("points.pkl"), "rb")) - -# Load data -df = pd.read_csv( - "https://github.com/plotly/datasets/raw/master/dash-sample-apps/dash-oil-and-gas/data/wellspublic.csv", - low_memory=False, +from utils.components import ( + header, + controls_card, + top_data_cards, + main_graph, + individual_graph, + pie_graph, + aggregate_graph, ) -df["Date_Well_Completed"] = pd.to_datetime(df["Date_Well_Completed"]) -df = df[df["Date_Well_Completed"] > dt.datetime(1960, 1, 1)] - -trim = df[["API_WellNo", "Well_Type", "Well_Name"]] -trim.index = trim["API_WellNo"] -dataset = trim.to_dict(orient="index") - - -# Create global chart template -mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" - -layout = dict( - autosize=True, - automargin=True, - margin=dict(l=30, r=30, b=20, t=40), - hovermode="closest", - plot_bgcolor="#F9F9F9", - paper_bgcolor="#F9F9F9", - legend=dict(font=dict(size=10), orientation="h"), - title="Satellite Overview", - mapbox=dict( - accesstoken=mapbox_access_token, - style="light", - center=dict(lon=-78.05, lat=42.54), - zoom=7, - ), + +app = dash.Dash( + __name__, + meta_tags=[{"name": "viewport", "content": "width=device-width"}], ) +app.title = "Oil & Gas Wells" +server = app.server # Create app layout app.layout = html.Div( @@ -88,180 +60,27 @@ dcc.Store(id="aggregate_data"), # empty Div to trigger javascript file for graph resizing html.Div(id="output-clientside"), + # Header to be replaced + header(app, "black", "New York Oil and Gas", "Production Overview"), html.Div( [ - html.Div( - [ - html.Img( - src=app.get_asset_url("dash-logo.png"), - id="plotly-image", - style={ - "height": "60px", - "width": "auto", - "margin-bottom": "25px", - }, - ) - ], - className="one-third column", - ), - html.Div( - [ - html.Div( - [ - html.H3( - "New York Oil and Gas", - style={"margin-bottom": "0px"}, - ), - html.H5( - "Production Overview", style={"margin-top": "0px"} - ), - ] - ) - ], - className="one-half column", - id="title", - ), - html.Div( - [ - html.A( - html.Button("Learn More", id="learn-more-button"), - href="https://plot.ly/dash/pricing/", - ) - ], - className="one-third column", - id="button", - ), - ], - id="header", - className="row flex-display", - style={"margin-bottom": "25px"}, - ), - html.Div( - [ - html.Div( - [ - html.P( - "Filter by construction date (or select range in histogram):", - className="control_label", - ), - dcc.RangeSlider( - id="year_slider", - min=1960, - max=2017, - value=[1990, 2010], - className="dcc_control", - ), - html.P("Filter by well status:", className="control_label"), - dcc.RadioItems( - id="well_status_selector", - options=[ - {"label": "All ", "value": "all"}, - {"label": "Active only ", "value": "active"}, - {"label": "Customize ", "value": "custom"}, - ], - value="active", - labelStyle={"display": "inline-block"}, - className="dcc_control", - ), - dcc.Dropdown( - id="well_statuses", - options=well_status_options, - multi=True, - value=list(WELL_STATUSES.keys()), - className="dcc_control", - ), - dcc.Checklist( - id="lock_selector", - options=[{"label": "Lock camera", "value": "locked"}], - className="dcc_control", - value=[], - ), - html.P("Filter by well type:", className="control_label"), - dcc.RadioItems( - id="well_type_selector", - options=[ - {"label": "All ", "value": "all"}, - {"label": "Productive only ", "value": "productive"}, - {"label": "Customize ", "value": "custom"}, - ], - value="productive", - labelStyle={"display": "inline-block"}, - className="dcc_control", - ), - dcc.Dropdown( - id="well_types", - options=well_type_options, - multi=True, - value=list(WELL_TYPES.keys()), - className="dcc_control", - ), - ], - className="pretty_container four columns", - id="cross-filter-options", - ), - html.Div( - [ - html.Div( - [ - html.Div( - [html.H6(id="well_text"), html.P("No. of Wells")], - id="wells", - className="mini_container", - ), - html.Div( - [html.H6(id="gasText"), html.P("Gas")], - id="gas", - className="mini_container", - ), - html.Div( - [html.H6(id="oilText"), html.P("Oil")], - id="oil", - className="mini_container", - ), - html.Div( - [html.H6(id="waterText"), html.P("Water")], - id="water", - className="mini_container", - ), - ], - id="info-container", - className="row container-display", - ), - html.Div( - [dcc.Graph(id="count_graph")], - id="countGraphContainer", - className="pretty_container", - ), - ], - id="right-column", - className="eight columns", - ), + controls_card(), + # top data cards + top_data_cards(), ], className="row flex-display", ), html.Div( [ - html.Div( - [dcc.Graph(id="main_graph")], - className="pretty_container seven columns", - ), - html.Div( - [dcc.Graph(id="individual_graph")], - className="pretty_container five columns", - ), + main_graph(), + individual_graph(), ], className="row flex-display", ), html.Div( [ - html.Div( - [dcc.Graph(id="pie_graph")], - className="pretty_container seven columns", - ), - html.Div( - [dcc.Graph(id="aggregate_graph")], - className="pretty_container five columns", - ), + pie_graph(), + aggregate_graph(), ], className="row flex-display", ), @@ -270,88 +89,6 @@ style={"display": "flex", "flex-direction": "column"}, ) - -# Helper functions -def human_format(num): - if num == 0: - return "0" - - magnitude = int(math.log(num, 1000)) - mantissa = str(int(num / (1000 ** magnitude))) - return mantissa + ["", "K", "M", "G", "T", "P"][magnitude] - - -def filter_dataframe(df, well_statuses, well_types, year_slider): - dff = df[ - df["Well_Status"].isin(well_statuses) - & df["Well_Type"].isin(well_types) - & (df["Date_Well_Completed"] > dt.datetime(year_slider[0], 1, 1)) - & (df["Date_Well_Completed"] < dt.datetime(year_slider[1], 1, 1)) - ] - return dff - - -def produce_individual(api_well_num): - try: - points[api_well_num] - except: - return None, None, None, None - - index = list( - range(min(points[api_well_num].keys()), max(points[api_well_num].keys()) + 1) - ) - gas = [] - oil = [] - water = [] - - for year in index: - try: - gas.append(points[api_well_num][year]["Gas Produced, MCF"]) - except: - gas.append(0) - try: - oil.append(points[api_well_num][year]["Oil Produced, bbl"]) - except: - oil.append(0) - try: - water.append(points[api_well_num][year]["Water Produced, bbl"]) - except: - water.append(0) - - return index, gas, oil, water - - -def produce_aggregate(selected, year_slider): - - index = list(range(max(year_slider[0], 1985), 2016)) - gas = [] - oil = [] - water = [] - - for year in index: - count_gas = 0 - count_oil = 0 - count_water = 0 - for api_well_num in selected: - try: - count_gas += points[api_well_num][year]["Gas Produced, MCF"] - except: - pass - try: - count_oil += points[api_well_num][year]["Oil Produced, bbl"] - except: - pass - try: - count_water += points[api_well_num][year]["Water Produced, bbl"] - except: - pass - gas.append(count_gas) - oil.append(count_oil) - water.append(count_water) - - return index, gas, oil, water - - # Create callbacks app.clientside_callback( ClientsideFunction(namespace="clientside", function_name="resize"), @@ -360,13 +97,11 @@ def produce_aggregate(selected, year_slider): ) -@app.callback( +@callback( Output("aggregate_data", "data"), - [ - Input("well_statuses", "value"), - Input("well_types", "value"), - Input("year_slider", "value"), - ], + Input("well_statuses", "value"), + Input("well_types", "value"), + Input("year_slider", "value"), ) def update_production_text(well_statuses, well_types, year_slider): @@ -377,9 +112,7 @@ def update_production_text(well_statuses, well_types, year_slider): # Radio -> multi -@app.callback( - Output("well_statuses", "value"), [Input("well_status_selector", "value")] -) +@callback(Output("well_statuses", "value"), Input("well_status_selector", "value")) def display_status(selector): if selector == "all": return list(WELL_STATUSES.keys()) @@ -389,7 +122,7 @@ def display_status(selector): # Radio -> multi -@app.callback(Output("well_types", "value"), [Input("well_type_selector", "value")]) +@callback(Output("well_types", "value"), Input("well_type_selector", "value")) def display_type(selector): if selector == "all": return list(WELL_TYPES.keys()) @@ -399,7 +132,7 @@ def display_type(selector): # Slider -> count graph -@app.callback(Output("year_slider", "value"), [Input("count_graph", "selectedData")]) +@callback(Output("year_slider", "value"), Input("count_graph", "selectedData")) def update_year_slider(count_graph_selected): if count_graph_selected is None: @@ -410,7 +143,7 @@ def update_year_slider(count_graph_selected): # Selectors -> well text -@app.callback( +@callback( Output("well_text", "children"), [ Input("well_statuses", "value"), @@ -425,12 +158,10 @@ def update_well_text(well_statuses, well_types, year_slider): @app.callback( - [ - Output("gasText", "children"), - Output("oilText", "children"), - Output("waterText", "children"), - ], - [Input("aggregate_data", "data")], + Output("gasText", "children"), + Output("oilText", "children"), + Output("waterText", "children"), + Input("aggregate_data", "data"), ) def update_text(data): return data[0] + " mcf", data[1] + " bbl", data[2] + " bbl" @@ -439,282 +170,63 @@ def update_text(data): # Selectors -> main graph @app.callback( Output("main_graph", "figure"), - [ - Input("well_statuses", "value"), - Input("well_types", "value"), - Input("year_slider", "value"), - ], - [State("lock_selector", "value"), State("main_graph", "relayoutData")], + Input("well_statuses", "value"), + Input("well_types", "value"), + Input("year_slider", "value"), + State("lock_selector", "value"), + State("main_graph", "relayoutData"), ) -def make_main_figure( +def return_make_main_figure( well_statuses, well_types, year_slider, selector, main_graph_layout ): - dff = filter_dataframe(df, well_statuses, well_types, year_slider) - - traces = [] - for well_type, dfff in dff.groupby("Well_Type"): - trace = dict( - type="scattermapbox", - lon=dfff["Surface_Longitude"], - lat=dfff["Surface_latitude"], - text=dfff["Well_Name"], - customdata=dfff["API_WellNo"], - name=WELL_TYPES[well_type], - marker=dict(size=4, opacity=0.6), - ) - traces.append(trace) - - # relayoutData is None by default, and {'autosize': True} without relayout action - if main_graph_layout is not None and selector is not None and "locked" in selector: - if "mapbox.center" in main_graph_layout.keys(): - lon = float(main_graph_layout["mapbox.center"]["lon"]) - lat = float(main_graph_layout["mapbox.center"]["lat"]) - zoom = float(main_graph_layout["mapbox.zoom"]) - layout["mapbox"]["center"]["lon"] = lon - layout["mapbox"]["center"]["lat"] = lat - layout["mapbox"]["zoom"] = zoom - - figure = dict(data=traces, layout=layout) - return figure + return make_main_figure( + well_statuses, well_types, year_slider, selector, main_graph_layout + ) # Main graph -> individual graph -@app.callback(Output("individual_graph", "figure"), [Input("main_graph", "hoverData")]) -def make_individual_figure(main_graph_hover): - - layout_individual = copy.deepcopy(layout) - - if main_graph_hover is None: - main_graph_hover = { - "points": [ - {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000} - ] - } - - chosen = [point["customdata"] for point in main_graph_hover["points"]] - index, gas, oil, water = produce_individual(chosen[0]) - - if index is None: - annotation = dict( - text="No data available", - x=0.5, - y=0.5, - align="center", - showarrow=False, - xref="paper", - yref="paper", - ) - layout_individual["annotations"] = [annotation] - data = [] - else: - data = [ - dict( - type="scatter", - mode="lines+markers", - name="Gas Produced (mcf)", - x=index, - y=gas, - line=dict(shape="spline", smoothing=2, width=1, color="#fac1b7"), - marker=dict(symbol="diamond-open"), - ), - dict( - type="scatter", - mode="lines+markers", - name="Oil Produced (bbl)", - x=index, - y=oil, - line=dict(shape="spline", smoothing=2, width=1, color="#a9bb95"), - marker=dict(symbol="diamond-open"), - ), - dict( - type="scatter", - mode="lines+markers", - name="Water Produced (bbl)", - x=index, - y=water, - line=dict(shape="spline", smoothing=2, width=1, color="#92d8d8"), - marker=dict(symbol="diamond-open"), - ), - ] - layout_individual["title"] = dataset[chosen[0]]["Well_Name"] - - figure = dict(data=data, layout=layout_individual) - return figure +@callback(Output("individual_graph", "figure"), Input("main_graph", "hoverData")) +def return_make_individual_figure(main_graph_hover): + return make_individual_figure(main_graph_hover) # Selectors, main graph -> aggregate graph @app.callback( Output("aggregate_graph", "figure"), - [ - Input("well_statuses", "value"), - Input("well_types", "value"), - Input("year_slider", "value"), - Input("main_graph", "hoverData"), - ], + Input("well_statuses", "value"), + Input("well_types", "value"), + Input("year_slider", "value"), + Input("main_graph", "hoverData"), ) -def make_aggregate_figure(well_statuses, well_types, year_slider, main_graph_hover): - - layout_aggregate = copy.deepcopy(layout) - - if main_graph_hover is None: - main_graph_hover = { - "points": [ - {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000} - ] - } - - chosen = [point["customdata"] for point in main_graph_hover["points"]] - well_type = dataset[chosen[0]]["Well_Type"] - dff = filter_dataframe(df, well_statuses, well_types, year_slider) - - selected = dff[dff["Well_Type"] == well_type]["API_WellNo"].values - index, gas, oil, water = produce_aggregate(selected, year_slider) - - data = [ - dict( - type="scatter", - mode="lines", - name="Gas Produced (mcf)", - x=index, - y=gas, - line=dict(shape="spline", smoothing="2", color="#F9ADA0"), - ), - dict( - type="scatter", - mode="lines", - name="Oil Produced (bbl)", - x=index, - y=oil, - line=dict(shape="spline", smoothing="2", color="#849E68"), - ), - dict( - type="scatter", - mode="lines", - name="Water Produced (bbl)", - x=index, - y=water, - line=dict(shape="spline", smoothing="2", color="#59C3C3"), - ), - ] - layout_aggregate["title"] = "Aggregate: " + WELL_TYPES[well_type] - - figure = dict(data=data, layout=layout_aggregate) - return figure +def return_make_aggregate_figure( + well_statuses, well_types, year_slider, main_graph_hover +): + return make_aggregate_figure( + well_statuses, well_types, year_slider, main_graph_hover + ) # Selectors, main graph -> pie graph -@app.callback( +@callback( Output("pie_graph", "figure"), - [ - Input("well_statuses", "value"), - Input("well_types", "value"), - Input("year_slider", "value"), - ], + Input("well_statuses", "value"), + Input("well_types", "value"), + Input("year_slider", "value"), ) -def make_pie_figure(well_statuses, well_types, year_slider): - - layout_pie = copy.deepcopy(layout) - - dff = filter_dataframe(df, well_statuses, well_types, year_slider) - - selected = dff["API_WellNo"].values - index, gas, oil, water = produce_aggregate(selected, year_slider) - - aggregate = dff.groupby(["Well_Type"]).count() - - data = [ - dict( - type="pie", - labels=["Gas", "Oil", "Water"], - values=[sum(gas), sum(oil), sum(water)], - name="Production Breakdown", - text=[ - "Total Gas Produced (mcf)", - "Total Oil Produced (bbl)", - "Total Water Produced (bbl)", - ], - hoverinfo="text+value+percent", - textinfo="label+percent+name", - hole=0.5, - marker=dict(colors=["#fac1b7", "#a9bb95", "#92d8d8"]), - domain={"x": [0, 0.45], "y": [0.2, 0.8]}, - ), - dict( - type="pie", - labels=[WELL_TYPES[i] for i in aggregate.index], - values=aggregate["API_WellNo"], - name="Well Type Breakdown", - hoverinfo="label+text+value+percent", - textinfo="label+percent+name", - hole=0.5, - marker=dict(colors=[WELL_COLORS[i] for i in aggregate.index]), - domain={"x": [0.55, 1], "y": [0.2, 0.8]}, - ), - ] - layout_pie["title"] = "Production Summary: {} to {}".format( - year_slider[0], year_slider[1] - ) - layout_pie["font"] = dict(color="#777777") - layout_pie["legend"] = dict( - font=dict(color="#CCCCCC", size="10"), orientation="h", bgcolor="rgba(0,0,0,0)" - ) - - figure = dict(data=data, layout=layout_pie) - return figure +def return_make_pie_figure(well_statuses, well_types, year_slider): + return make_pie_figure(well_statuses, well_types, year_slider) # Selectors -> count graph -@app.callback( +@callback( Output("count_graph", "figure"), - [ - Input("well_statuses", "value"), - Input("well_types", "value"), - Input("year_slider", "value"), - ], + Input("well_statuses", "value"), + Input("well_types", "value"), + Input("year_slider", "value"), ) -def make_count_figure(well_statuses, well_types, year_slider): - - layout_count = copy.deepcopy(layout) - - dff = filter_dataframe(df, well_statuses, well_types, [1960, 2017]) - g = dff[["API_WellNo", "Date_Well_Completed"]] - g.index = g["Date_Well_Completed"] - g = g.resample("A").count() - - colors = [] - for i in range(1960, 2018): - if i >= int(year_slider[0]) and i < int(year_slider[1]): - colors.append("rgb(123, 199, 255)") - else: - colors.append("rgba(123, 199, 255, 0.2)") - - data = [ - dict( - type="scatter", - mode="markers", - x=g.index, - y=g["API_WellNo"] / 2, - name="All Wells", - opacity=0, - hoverinfo="skip", - ), - dict( - type="bar", - x=g.index, - y=g["API_WellNo"], - name="All Wells", - marker=dict(color=colors), - ), - ] - - layout_count["title"] = "Completed Wells/Year" - layout_count["dragmode"] = "select" - layout_count["showlegend"] = False - layout_count["autosize"] = True - - figure = dict(data=data, layout=layout_count) - return figure +def return_make_count_figure(well_statuses, well_types, year_slider): + return make_count_figure(well_statuses, well_types, year_slider) # Main diff --git a/apps/dash-oil-and-gas/assets/css/app.css b/apps/dash-oil-and-gas/assets/css/app.css new file mode 100644 index 000000000..8b3802ab0 --- /dev/null +++ b/apps/dash-oil-and-gas/assets/css/app.css @@ -0,0 +1,61 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; + font-weight: bold; +} + +.header .header-title { + font-size: 5vh; +} +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; +} +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} + + +/* 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-oil-and-gas/screenshots/animated1.gif b/apps/dash-oil-and-gas/assets/github/animated1.gif similarity index 100% rename from apps/dash-oil-and-gas/screenshots/animated1.gif rename to apps/dash-oil-and-gas/assets/github/animated1.gif diff --git a/apps/dash-oil-and-gas/screenshots/screenshot1.png b/apps/dash-oil-and-gas/assets/github/screenshot1.png similarity index 100% rename from apps/dash-oil-and-gas/screenshots/screenshot1.png rename to apps/dash-oil-and-gas/assets/github/screenshot1.png diff --git a/apps/dash-oil-and-gas/screenshots/screenshot2.png b/apps/dash-oil-and-gas/assets/github/screenshot2.png similarity index 100% rename from apps/dash-oil-and-gas/screenshots/screenshot2.png rename to apps/dash-oil-and-gas/assets/github/screenshot2.png diff --git a/apps/dash-oil-and-gas/screenshots/screenshot3.png b/apps/dash-oil-and-gas/assets/github/screenshot3.png similarity index 100% rename from apps/dash-oil-and-gas/screenshots/screenshot3.png rename to apps/dash-oil-and-gas/assets/github/screenshot3.png diff --git a/apps/dash-oil-and-gas/assets/dash-logo.png b/apps/dash-oil-and-gas/assets/images/dash-logo.png similarity index 100% rename from apps/dash-oil-and-gas/assets/dash-logo.png rename to apps/dash-oil-and-gas/assets/images/dash-logo.png diff --git a/apps/dash-oil-and-gas/assets/images/plotly-logo-light-theme.png b/apps/dash-oil-and-gas/assets/images/plotly-logo-light-theme.png new file mode 100644 index 000000000..4920c6e34 Binary files /dev/null and b/apps/dash-oil-and-gas/assets/images/plotly-logo-light-theme.png differ diff --git a/apps/dash-oil-and-gas/controls.py b/apps/dash-oil-and-gas/constants.py similarity index 74% rename from apps/dash-oil-and-gas/controls.py rename to apps/dash-oil-and-gas/constants.py index 50916c12b..531bf99d8 100644 --- a/apps/dash-oil-and-gas/controls.py +++ b/apps/dash-oil-and-gas/constants.py @@ -1,3 +1,9 @@ +import pathlib + +# get relative data folder +PATH = pathlib.Path(__file__).parent +DATA_PATH = PATH.joinpath("data").resolve() + # flake8: noqa # In[]: @@ -143,3 +149,38 @@ TH="#EAE5D9", UN="#C29A84", ) + +# Create controls +county_options = [ + {"label": str(COUNTIES[county]), "value": str(county)} for county in COUNTIES +] + +well_status_options = [ + {"label": str(WELL_STATUSES[well_status]), "value": str(well_status)} + for well_status in WELL_STATUSES +] + +well_type_options = [ + {"label": str(WELL_TYPES[well_type]), "value": str(well_type)} + for well_type in WELL_TYPES +] + +# Create global chart template +mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" + +layout = dict( + autosize=True, + automargin=True, + margin=dict(l=30, r=30, b=20, t=40), + hovermode="closest", + plot_bgcolor="#F9F9F9", + paper_bgcolor="#F9F9F9", + legend=dict(font=dict(size=10), orientation="h"), + title="Satellite Overview", + mapbox=dict( + accesstoken=mapbox_access_token, + style="light", + center=dict(lon=-78.05, lat=42.54), + zoom=7, + ), +) diff --git a/apps/dash-oil-and-gas/data/points.pkl b/apps/dash-oil-and-gas/data/points.pkl new file mode 100644 index 000000000..2318791da Binary files /dev/null and b/apps/dash-oil-and-gas/data/points.pkl differ diff --git a/apps/dash-oil-and-gas/gitignore b/apps/dash-oil-and-gas/gitignore new file mode 100644 index 000000000..d8e187da3 --- /dev/null +++ b/apps/dash-oil-and-gas/gitignore @@ -0,0 +1,191 @@ +# .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/ +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 +.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-oil-and-gas/explore.ipynb b/apps/dash-oil-and-gas/notebooks/explore.ipynb similarity index 100% rename from apps/dash-oil-and-gas/explore.ipynb rename to apps/dash-oil-and-gas/notebooks/explore.ipynb diff --git a/apps/dash-oil-and-gas/requirements.txt b/apps/dash-oil-and-gas/requirements.txt index 2f8319789..e1e58656f 100644 --- a/apps/dash-oil-and-gas/requirements.txt +++ b/apps/dash-oil-and-gas/requirements.txt @@ -1,3 +1,3 @@ -pandas==0.24.2 -dash==1.12.0 -gunicorn==19.9.0 \ No newline at end of file +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 \ No newline at end of file diff --git a/apps/dash-oil-and-gas/runtime.txt b/apps/dash-oil-and-gas/runtime.txt new file mode 100644 index 000000000..cfa660c42 --- /dev/null +++ b/apps/dash-oil-and-gas/runtime.txt @@ -0,0 +1 @@ +python-3.8.0 \ No newline at end of file diff --git a/apps/dash-oil-and-gas/utils/components.py b/apps/dash-oil-and-gas/utils/components.py new file mode 100644 index 000000000..55f707edc --- /dev/null +++ b/apps/dash-oil-and-gas/utils/components.py @@ -0,0 +1,172 @@ +from dash import html, dcc +from constants import well_status_options, well_type_options, WELL_STATUSES, WELL_TYPES + + +def header( + app, header_color, header, subheader=None, header_background_color="transparent" +): + left_headers = html.Div( + [ + html.Div(header, className="header-title"), + html.Div(subheader, className="subheader-title"), + ], + style={"color": header_color}, + ) + + logo = html.Img(src=app.get_asset_url("images/plotly-logo-light-theme.png")) + logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") + demo_link = html.A( + "LEARN MORE", + href="https://plotly.com/dash/", + target="_blank", + className="demo-button", + ) + right_logos = html.Div([demo_link, logo_link], className="header-logos") + + return html.Div( + [left_headers, right_logos], + className="header", + style={"background-color": header_background_color}, + ) + + +def controls_card(): + return html.Div( + [ + html.P( + "Filter by construction date (or select range in histogram):", + className="control_label", + ), + dcc.RangeSlider( + id="year_slider", + min=1960, + max=2017, + value=[1990, 2010], + step=1, + marks={ + 1960: "1960", + 1970: "1970", + 1980: "1980", + 1990: "1990", + 2000: "2000", + 2010: "2010", + 2017: "2017", + }, + className="dcc_control", + ), + html.P("Filter by well status:", className="control_label"), + dcc.RadioItems( + id="well_status_selector", + options=[ + {"label": "All ", "value": "all"}, + {"label": "Active only ", "value": "active"}, + {"label": "Customize ", "value": "custom"}, + ], + value="active", + labelStyle={"display": "inline-block"}, + className="dcc_control", + ), + dcc.Dropdown( + id="well_statuses", + options=well_status_options, + multi=True, + value=list(WELL_STATUSES.keys()), + className="dcc_control", + ), + dcc.Checklist( + id="lock_selector", + options=[{"label": "Lock camera", "value": "locked"}], + className="dcc_control", + value=[], + ), + html.P("Filter by well type:", className="control_label"), + dcc.RadioItems( + id="well_type_selector", + options=[ + {"label": "All ", "value": "all"}, + {"label": "Productive only ", "value": "productive"}, + {"label": "Customize ", "value": "custom"}, + ], + value="productive", + labelStyle={"display": "inline-block"}, + className="dcc_control", + ), + dcc.Dropdown( + id="well_types", + options=well_type_options, + multi=True, + value=list(WELL_TYPES.keys()), + className="dcc_control", + ), + ], + className="pretty_container four columns", + id="cross-filter-options", + ) + + +def top_data_cards(): + return html.Div( + [ + html.Div( + [ + html.Div( + [html.H6(id="well_text"), html.P("No. of Wells")], + id="wells", + className="mini_container", + ), + html.Div( + [html.H6(id="gasText"), html.P("Gas")], + id="gas", + className="mini_container", + ), + html.Div( + [html.H6(id="oilText"), html.P("Oil")], + id="oil", + className="mini_container", + ), + html.Div( + [html.H6(id="waterText"), html.P("Water")], + id="water", + className="mini_container", + ), + ], + id="info-container", + className="row container-display", + ), + html.Div( + [dcc.Graph(id="count_graph")], + id="countGraphContainer", + className="pretty_container", + ), + ], + id="right-column", + className="eight columns", + ) + + +def main_graph(): + return html.Div( + [dcc.Graph(id="main_graph")], + className="pretty_container seven columns", + ) + + +def individual_graph(): + return html.Div( + [dcc.Graph(id="individual_graph")], + className="pretty_container five columns", + ) + + +def pie_graph(): + return html.Div( + [dcc.Graph(id="pie_graph")], + className="pretty_container seven columns", + ) + + +def aggregate_graph(): + return html.Div( + [dcc.Graph(id="aggregate_graph")], + className="pretty_container five columns", + ) diff --git a/apps/dash-oil-and-gas/utils/data.py b/apps/dash-oil-and-gas/utils/data.py new file mode 100644 index 000000000..07d99f7ea --- /dev/null +++ b/apps/dash-oil-and-gas/utils/data.py @@ -0,0 +1,25 @@ +import urllib.request +import pickle +import pandas as pd +import datetime as dt +from constants import DATA_PATH + + +# Download pickle file +urllib.request.urlretrieve( + "https://raw.githubusercontent.com/plotly/datasets/master/dash-sample-apps/dash-oil-and-gas/data/points.pkl", + DATA_PATH.joinpath("points.pkl"), +) +points = pickle.load(open(DATA_PATH.joinpath("points.pkl"), "rb")) + +# Load data +df = pd.read_csv( + "https://github.com/plotly/datasets/raw/master/dash-sample-apps/dash-oil-and-gas/data/wellspublic.csv", + low_memory=False, +) +df["Date_Well_Completed"] = pd.to_datetime(df["Date_Well_Completed"]) +df = df[df["Date_Well_Completed"] > dt.datetime(1960, 1, 1)] + +trim = df[["API_WellNo", "Well_Type", "Well_Name"]] +trim.index = trim["API_WellNo"] +dataset = trim.to_dict(orient="index") diff --git a/apps/dash-oil-and-gas/utils/figures.py b/apps/dash-oil-and-gas/utils/figures.py new file mode 100644 index 000000000..5e690fec0 --- /dev/null +++ b/apps/dash-oil-and-gas/utils/figures.py @@ -0,0 +1,251 @@ +from utils.data import df, dataset +from utils.helper_functions import ( + filter_dataframe, + produce_individual, + produce_aggregate, +) +from constants import layout, WELL_TYPES, WELL_COLORS +import copy + +import datetime as dt + + +def make_main_figure( + well_statuses, well_types, year_slider, selector, main_graph_layout +): + + dff = filter_dataframe(df, well_statuses, well_types, year_slider) + + traces = [] + for well_type, dfff in dff.groupby("Well_Type"): + trace = dict( + type="scattermapbox", + lon=dfff["Surface_Longitude"], + lat=dfff["Surface_latitude"], + text=dfff["Well_Name"], + customdata=dfff["API_WellNo"], + name=WELL_TYPES[well_type], + marker=dict(size=4, opacity=0.6), + ) + traces.append(trace) + + # relayoutData is None by default, and {'autosize': True} without relayout action + if main_graph_layout is not None and selector is not None and "locked" in selector: + if "mapbox.center" in main_graph_layout.keys(): + lon = float(main_graph_layout["mapbox.center"]["lon"]) + lat = float(main_graph_layout["mapbox.center"]["lat"]) + zoom = float(main_graph_layout["mapbox.zoom"]) + layout["mapbox"]["center"]["lon"] = lon + layout["mapbox"]["center"]["lat"] = lat + layout["mapbox"]["zoom"] = zoom + + figure = dict(data=traces, layout=layout) + return figure + + +def make_individual_figure(main_graph_hover): + + layout_individual = copy.deepcopy(layout) + + if main_graph_hover is None: + main_graph_hover = { + "points": [ + {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000} + ] + } + + chosen = [point["customdata"] for point in main_graph_hover["points"]] + index, gas, oil, water = produce_individual(chosen[0]) + + if index is None: + annotation = dict( + text="No data available", + x=0.5, + y=0.5, + align="center", + showarrow=False, + xref="paper", + yref="paper", + ) + layout_individual["annotations"] = [annotation] + data = [] + else: + data = [ + dict( + type="scatter", + mode="lines+markers", + name="Gas Produced (mcf)", + x=index, + y=gas, + line=dict(shape="spline", smoothing=2, width=1, color="#fac1b7"), + marker=dict(symbol="diamond-open"), + ), + dict( + type="scatter", + mode="lines+markers", + name="Oil Produced (bbl)", + x=index, + y=oil, + line=dict(shape="spline", smoothing=2, width=1, color="#a9bb95"), + marker=dict(symbol="diamond-open"), + ), + dict( + type="scatter", + mode="lines+markers", + name="Water Produced (bbl)", + x=index, + y=water, + line=dict(shape="spline", smoothing=2, width=1, color="#92d8d8"), + marker=dict(symbol="diamond-open"), + ), + ] + layout_individual["title"] = dataset[chosen[0]]["Well_Name"] + + figure = dict(data=data, layout=layout_individual) + return figure + + +def make_aggregate_figure(well_statuses, well_types, year_slider, main_graph_hover): + + layout_aggregate = copy.deepcopy(layout) + + if main_graph_hover is None: + main_graph_hover = { + "points": [ + {"curveNumber": 4, "pointNumber": 569, "customdata": 31101173130000} + ] + } + + chosen = [point["customdata"] for point in main_graph_hover["points"]] + well_type = dataset[chosen[0]]["Well_Type"] + dff = filter_dataframe(df, well_statuses, well_types, year_slider) + + selected = dff[dff["Well_Type"] == well_type]["API_WellNo"].values + index, gas, oil, water = produce_aggregate(selected, year_slider) + + data = [ + dict( + type="scatter", + mode="lines", + name="Gas Produced (mcf)", + x=index, + y=gas, + line=dict(shape="spline", smoothing="2", color="#F9ADA0"), + ), + dict( + type="scatter", + mode="lines", + name="Oil Produced (bbl)", + x=index, + y=oil, + line=dict(shape="spline", smoothing="2", color="#849E68"), + ), + dict( + type="scatter", + mode="lines", + name="Water Produced (bbl)", + x=index, + y=water, + line=dict(shape="spline", smoothing="2", color="#59C3C3"), + ), + ] + layout_aggregate["title"] = "Aggregate: " + WELL_TYPES[well_type] + + figure = dict(data=data, layout=layout_aggregate) + return figure + + +def make_pie_figure(well_statuses, well_types, year_slider): + + layout_pie = copy.deepcopy(layout) + + dff = filter_dataframe(df, well_statuses, well_types, year_slider) + + selected = dff["API_WellNo"].values + index, gas, oil, water = produce_aggregate(selected, year_slider) + + aggregate = dff.groupby(["Well_Type"]).count() + + data = [ + dict( + type="pie", + labels=["Gas", "Oil", "Water"], + values=[sum(gas), sum(oil), sum(water)], + name="Production Breakdown", + text=[ + "Total Gas Produced (mcf)", + "Total Oil Produced (bbl)", + "Total Water Produced (bbl)", + ], + hoverinfo="text+value+percent", + textinfo="label+percent+name", + hole=0.5, + marker=dict(colors=["#fac1b7", "#a9bb95", "#92d8d8"]), + domain={"x": [0, 0.45], "y": [0.2, 0.8]}, + ), + dict( + type="pie", + labels=[WELL_TYPES[i] for i in aggregate.index], + values=aggregate["API_WellNo"], + name="Well Type Breakdown", + hoverinfo="label+text+value+percent", + textinfo="label+percent+name", + hole=0.5, + marker=dict(colors=[WELL_COLORS[i] for i in aggregate.index]), + domain={"x": [0.55, 1], "y": [0.2, 0.8]}, + ), + ] + layout_pie["title"] = "Production Summary: {} to {}".format( + year_slider[0], year_slider[1] + ) + layout_pie["font"] = dict(color="#777777") + layout_pie["legend"] = dict( + font=dict(color="#CCCCCC", size="10"), orientation="h", bgcolor="rgba(0,0,0,0)" + ) + + figure = dict(data=data, layout=layout_pie) + return figure + + +def make_count_figure(well_statuses, well_types, year_slider): + + layout_count = copy.deepcopy(layout) + + dff = filter_dataframe(df, well_statuses, well_types, [1960, 2017]) + g = dff[["API_WellNo", "Date_Well_Completed"]] + g.index = g["Date_Well_Completed"] + g = g.resample("A").count() + + colors = [] + for i in range(1960, 2018): + if i >= int(year_slider[0]) and i < int(year_slider[1]): + colors.append("rgb(123, 199, 255)") + else: + colors.append("rgba(123, 199, 255, 0.2)") + + data = [ + dict( + type="scatter", + mode="markers", + x=g.index, + y=g["API_WellNo"] / 2, + name="All Wells", + opacity=0, + hoverinfo="skip", + ), + dict( + type="bar", + x=g.index, + y=g["API_WellNo"], + name="All Wells", + marker=dict(color=colors), + ), + ] + + layout_count["title"] = "Completed Wells/Year" + layout_count["dragmode"] = "select" + layout_count["showlegend"] = False + layout_count["autosize"] = True + + figure = dict(data=data, layout=layout_count) + return figure diff --git a/apps/dash-oil-and-gas/utils/helper_functions.py b/apps/dash-oil-and-gas/utils/helper_functions.py new file mode 100644 index 000000000..6e5441842 --- /dev/null +++ b/apps/dash-oil-and-gas/utils/helper_functions.py @@ -0,0 +1,83 @@ +import datetime as dt +from utils.data import points +import math + + +def human_format(num): + if num == 0: + return "0" + + magnitude = int(math.log(num, 1000)) + mantissa = str(int(num / (1000 ** magnitude))) + return mantissa + ["", "K", "M", "G", "T", "P"][magnitude] + + +def filter_dataframe(df, well_statuses, well_types, year_slider): + dff = df[ + df["Well_Status"].isin(well_statuses) + & df["Well_Type"].isin(well_types) + & (df["Date_Well_Completed"] > dt.datetime(year_slider[0], 1, 1)) + & (df["Date_Well_Completed"] < dt.datetime(year_slider[1], 1, 1)) + ] + return dff + + +def produce_individual(api_well_num): + try: + points[api_well_num] + except: + return None, None, None, None + + index = list( + range(min(points[api_well_num].keys()), max(points[api_well_num].keys()) + 1) + ) + gas = [] + oil = [] + water = [] + + for year in index: + try: + gas.append(points[api_well_num][year]["Gas Produced, MCF"]) + except: + gas.append(0) + try: + oil.append(points[api_well_num][year]["Oil Produced, bbl"]) + except: + oil.append(0) + try: + water.append(points[api_well_num][year]["Water Produced, bbl"]) + except: + water.append(0) + + return index, gas, oil, water + + +def produce_aggregate(selected, year_slider): + + index = list(range(max(year_slider[0], 1985), 2016)) + gas = [] + oil = [] + water = [] + + for year in index: + count_gas = 0 + count_oil = 0 + count_water = 0 + for api_well_num in selected: + try: + count_gas += points[api_well_num][year]["Gas Produced, MCF"] + except: + pass + try: + count_oil += points[api_well_num][year]["Oil Produced, bbl"] + except: + pass + try: + count_water += points[api_well_num][year]["Water Produced, bbl"] + except: + pass + gas.append(count_gas) + oil.append(count_oil) + water.append(count_water) + + return index, gas, oil, water