From 5a6a3e2690e68e2281ba96aac12142e49e7aa721 Mon Sep 17 00:00:00 2001 From: EmilyLien Date: Fri, 23 Jan 2026 13:05:47 -0500 Subject: [PATCH] adding the joint map to the homepage - first pass for review --- database/joint_map.py | 181 ++++++++++++++++++++++++++ database/templates/database/home.html | 61 +-------- database/urls.py | 1 + 3 files changed, 184 insertions(+), 59 deletions(-) create mode 100644 database/joint_map.py diff --git a/database/joint_map.py b/database/joint_map.py new file mode 100644 index 0000000..435013e --- /dev/null +++ b/database/joint_map.py @@ -0,0 +1,181 @@ +from .models import SolarProjectData, CountyData, StorageProjectData +import pandas as pd +import numpy as np +import plotly.express as px +from plotly.offline import plot +import requests +from django_plotly_dash import DjangoDash +from dash import dcc +from dash import html +from dash.dependencies import Input, Output +import dash_bootstrap_components as dbc +import os + +solarData = SolarProjectData.objects.values('data_id', + 'locality', + 'unit_of_government', + 'project_name', + 'project_mw', + 'local_permit_status', + 'latitude', + 'longitude', + 'energy_storage_onsite', + 'energy_storage_mw') +storageData = StorageProjectData.objects.values('data_id', + 'locality', + 'unit_of_government', + 'project_name', + 'project_bess_mw', + 'project_bess_capacity', + 'local_permit_status', + 'latitude', + 'longitude', + 'project_type') +solar_df = pd.DataFrame.from_records(solarData) +bess_df = pd.DataFrame.from_records(storageData) + +#join the datasets on their common columns +joint_df = pd.merge(solar_df,bess_df,how='outer',on=['data_id',"locality",'unit_of_government','project_name','local_permit_status','latitude','longitude']) + +#locate the Solar + Storage duplicate IDs +duplicate_ids = joint_df[joint_df['data_id'].duplicated()==True].data_id.to_list() + +column_names = joint_df.columns.to_list() +for i in range(0,len(duplicate_ids)): + #get the indices where a data_id value is duplicated because of mismatched names between Solar and BESS for Solar+Storage projects + project_rows = joint_df[joint_df.data_id==duplicate_ids[i]].index + #it will always be two project rows, project_row[0] and project_row[1] + #pick ONE of the names, don't care which right now + joint_df.loc[project_rows[0],'project_name'] = joint_df.loc[project_rows[1],'project_name'] + #go through the rest of the values and fill in the blanks to make duplicate rows + for name in column_names: + if pd.isna(joint_df.iloc[project_rows[0],][name]) == True and pd.isna(joint_df.iloc[project_rows[1],][name]) == False: + joint_df.loc[project_rows[0],name] = joint_df.loc[project_rows[1],name] + if pd.isna(joint_df.iloc[project_rows[0],][name]) == False and pd.isna(joint_df.iloc[project_rows[1],][name]) == True: + joint_df.loc[project_rows[1],name] = joint_df.loc[project_rows[0],name] +#now that the rows have been made identical, drop the duplicated IDs +joint_df.drop_duplicates(inplace=True) + +#filling in the missing values for project_type with 'Solar' +joint_df.project_type.replace(np.nan,'Solar',inplace=True) +#give a text description for where the bess MW is none +joint_df.project_bess_mw.replace(np.nan,'Not Applicable',inplace=True) +#give a text description for where the storage MW is none +joint_df.project_mw.replace(np.nan,'Not Applicable',inplace=True) + +#set up the plotly Dash container +jointapp = DjangoDash(name='JointDash',add_bootstrap_links=True, external_stylesheets=[dbc.themes.BOOTSTRAP,dbc.themes.FLATLY,'/static/css/styles.css']) + +jointapp.layout = dbc.Container([ + #buid the state overview page + html.Div([ + html.Br(), + html.Br(), + html.H2("Virginia Solar and Battery Energy Storage Status Map"), + html.P("View the current status of Solar and Battery Energy Storage projects across Virginia"), + html.Div( + #dashboard + [ + #developing reactive graph display based on Solar or Storage view selection + # html.H2("Select a Project Type to Graph",style={"margin-left":8}), + #html.Div(dbc.RadioItems( + # id='sourceButtons', + # className='btn-group', + # inputClassName='btn-check', + # labelClassName="btn btn--blue-d", + # options=[ + #], + #value="solarMaps", + #),style={'margin-left':4,}), + html.Br(), + html.Div(dbc.RadioItems( + id='mapButtons', + className='btn-group', + inputClassName='btn-check', + labelClassName="btn--dash", + options=[ + {"label": "Approved", "value": 'Approved'}, + {"label": "Approved/Amended","value":"Approved/Amended"}, + {"label": "By-right", "value": 'By-right'}, + {"label": "Denied", "value": 'Denied'}, + {"label": "Withdrawn","value":'Withdrawn'}, + {"label": "Pending","value":'Pending'} + ], + value='Approved', + ),style={'margin-left':4,}), + #switchable maps + html.Div(dcc.Graph(id='jointStatusMap',config={'responsive':False, + 'displayModeBar':True}), + style={ + 'margin-left':9, + 'width':1184,}), #close the map div + ]), #close the content div + ] + )], #close the container contents list + fluid=True, + style={'display': 'flex', + 'background-color':'#F2F4F8'}, + className='dashboard-container') #close the container div + + + +#this will go in a callback function +@jointapp.callback( + Output("jointStatusMap", "figure"), + Input("mapButtons","value")) + +def update_joint_amap(permit_status): + jointMap = px.scatter_map( + #mapDataClean[mapDataClean['final_action_year'] <= slide_year], + joint_df[joint_df.local_permit_status==permit_status], + color='project_type', + custom_data=['project_name','project_type','locality','local_permit_status','project_mw','project_bess_mw'], + color_discrete_sequence=['rgb(40, 67, 118)','rgb(98, 187, 70)','rgb(229, 114, 0)', 'rgb(253, 218, 36)'], + category_orders={"project_type": ["Solar", "Solar + Storage", "Colocated with Solar", "Non-Solar BESS"]}, + labels={'locality':'Locality', + 'local_permit_status':'Local Permit Status', + 'project_type':"Project Type", + 'project_mw':'Project Megawatts', + 'project_bess_mw':"BESS Project Megawatts", + 'project_name':'Project Name', + 'locality': 'Locality'}, + hover_name='project_name', + hover_data={ + 'project_mw': True, + 'locality': True, + 'latitude':False, + 'longitude':False}, + lat='latitude', + lon='longitude', + zoom=6.57, + center = {"lat": 38.00692, "lon": -79.40695}, + ) + jointMap.update_layout( + font=dict(color='#242e4c'), + legend=dict(font=dict(size=12, + color='#242e4c'), + orientation='h', + yanchor='bottom',y=1), + legend2=dict(font=dict(size=12, + color='#242e4c'), + title='Project Size'), + margin={"r":0,"t":10,"l":0,"b":0}, + width=1155, + height=600, + map_style='carto-positron-nolabels', + paper_bgcolor='#F2F4F8', + xaxis=dict(showgrid=False, + zeroline=False, + visible=False), + yaxis=dict(showgrid=False, + zeroline=False, + visible=False), + ) + jointMap.update_traces(marker=dict(size=10), + hovertemplate="%{customdata[0]}
Project Type : %{customdata[1]}
Locality: %{customdata[2]}
Local Permit Status: %{customdata[3]}
Solar Project Megawatts: %{customdata[4]}
BESS Project Megawatts: %{customdata[5]}") + jointMap.add_annotation(text='Source: Weldon Cooper Center Virginia Solar and Storage Database',x=0,y=0,xref="paper", yref="paper",font=dict(size=8),showarrow=False) + + return jointMap + +if __name__ == '__main__': + jointapp.run_server(host='0.0.0.0', port=int(os.environ.get('PORT', 8050))) \ No newline at end of file diff --git a/database/templates/database/home.html b/database/templates/database/home.html index 332b571..d4e9f7a 100644 --- a/database/templates/database/home.html +++ b/database/templates/database/home.html @@ -1,5 +1,6 @@ {% extends "database/base.html" %} {% load static %} +{% load plotly_dash %} {% block content %}

@@ -70,69 +71,11 @@

Virginia Solar and Storage Database

+
{% plotly_app_bootstrap name='JointDash' aspect='1by1' %}
-
- Solar screenshot -
- - - - - - -
diff --git a/database/urls.py b/database/urls.py index 0a5c65b..7efc817 100644 --- a/database/urls.py +++ b/database/urls.py @@ -4,6 +4,7 @@ from django.urls import path, include from .plotly_dash import dashapp from .plotly_dash_bat import dashappbat +from .joint_map import jointapp urlpatterns = [ path('', views.home, name='database-home'),