from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd
import numpy as np
df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-25/Building_Permits_Issued_Past_180_Days.csv"
)
df = df.dropna(subset=["fee", "estprojectcost"])
# Create bins for estprojectcost
bins = [0, 10000, 50000, 200000, 1000000, np.inf]
labels = [
"0 - 10k",
"10k - 50k",
"50k - 200k",
"200k - 1M",
"1M+"
]
df["estprojectcost_bin"] = pd.cut(
df["estprojectcost"], bins=bins, labels=labels, include_lowest=True
)
# Human-friendly filter labels
filter_labels = {
"permitclassmapped": "Permit Class",
"statuscurrentmapped": "Permit Status",
"estprojectcost_bin": "Estimated Project Cost"
}
def get_options(col):
if col == "estprojectcost_bin":
# Keep bin order, do not sort
unique_vals = [label for label in labels if label in df[col].dropna().unique()]
return [{"label": "All", "value": "all"}] + [
{"label": str(val), "value": val} for val in unique_vals
]
else:
return [{"label": "All", "value": "all"}] + [
{"label": str(val), "value": val}
for val in sorted(df[col].dropna().unique())
]
def kpi_card(title, value, icon, dark=True):
bg = "#000"
color = "#fff"
return dbc.Card(
dbc.CardBody(
[
html.Div([
html.I(
className=f"bi {icon} text-info",
style={
"fontSize": "2.1rem",
"position": "absolute",
"top": "12px",
"right": "18px",
},
),
], style={"height": "0px", "position": "relative"}),
html.Div(
title,
className="fs-6",
style={
"marginLeft": "0px",
"textAlign": "center",
"fontWeight": "bold",
"marginBottom": "0.2em"
},
),
html.H4(
value,
className="fw-bold mb-1",
style={
"fontSize": "1.5rem",
"marginLeft": "0px",
"marginTop": "10px",
"textAlign": "center"
},
),
]
),
className="shadow kpi-card", # <-- HOVER CLASS!
style={
"background": bg,
"color": color,
"minHeight": "80px",
"maxHeight": "100px",
"margin": "0 10px",
"width": "100%",
"maxWidth": "100%",
"display": "inline-block",
"position": "relative",
},
)
def empty_figure(message="No data to display", height=700, dark=True):
bg = "#000"
font_color = "white"
return {
"data": [],
"layout": {
"xaxis": {"visible": False},
"yaxis": {"visible": False},
"annotations": [
{
"text": message,
"xref": "paper",
"yref": "paper",
"showarrow": False,
"font": {"size": 24, "color": font_color},
"x": 0.5,
"y": 0.5,
}
],
"paper_bgcolor": bg,
"plot_bgcolor": bg,
"height": height,
},
}
extra_filters = [
"permitclassmapped",
"statuscurrentmapped",
"estprojectcost_bin",
]
SIDEBAR_STYLE = {
"position": "fixed",
"top": 0,
"left": 0,
"bottom": 0,
"width": "270px",
"padding": "24px 18px 18px 18px",
"background": "#23272b", # Bootstrap dark
"zIndex": 1000,
"overflowY": "auto",
"transition": "background 0.3s",
"color": "#fff",
}
CONTENT_STYLE_DARK = {
"marginLeft": "270px",
"padding": "24px 24px 24px 24px",
"minHeight": "100vh",
"background": "#000",
"color": "#fff",
"transition": "background 0.3s, color 0.3s",
"position": "relative",
}
COLOR_SCALES = [
{"label": "Sunsetdark", "value": "Sunsetdark"},
{"label": "Viridis", "value": "Viridis"},
{"label": "Cividis", "value": "Cividis"},
{"label": "Plasma", "value": "Plasma"},
{"label": "Turbo", "value": "Turbo"},
{"label": "Magma", "value": "Magma"},
{"label": "Inferno", "value": "Inferno"},
{"label": "IceFire", "value": "IceFire"},
{"label": "Magenta", "value": "Magenta"},
{"label": "Bluered", "value": "Bluered"},
{"label": "Blugrn", "value": "Blugrn"},
]
app = Dash(
__name__,
external_stylesheets=[
dbc.themes.DARKLY,
dbc.themes.BOOTSTRAP,
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css",
],
)
app.title = "Building Permits Dashboard"
app.layout = html.Div([
html.Div(
html.H1(
"Raleigh, NC - Building Permits In The Last 180 Days",
style={
"fontWeight": "bold",
"fontSize": "2.7rem",
"color": "#d7c08a",
"fontFamily": "Segoe UI, Arial, sans-serif",
"letterSpacing": "0.5px",
"textAlign": "center",
"margin": 0,
"padding": "18px 0 18px 0",
"background": "#000",
}
),
style={
"marginLeft": "270px",
"marginRight": "0px",
"background": "#000",
"padding": 0,
},
),
html.Div(
[
*[html.Br() for _ in range(5)],
*[
html.Div([
dbc.Label(
filter_labels.get(col, col.replace("_", " ").title()),
className="text-light",
style={"fontSize": "1.05rem", "fontWeight": "bold"}
),
dbc.Select(
id=f"extra-filter-{i}",
options=get_options(col),
value="all",
className="mb-6 bg-dark text-light form-select-sm",
style={
"fontSize": "1.05rem",
"background": "#23272b",
"color": "#fff",
"marginBottom": "70px",
},
),
])
for i, col in enumerate(extra_filters)
],
# Color scale dropdown at the bottom
html.Div([
dbc.Label(
"Color Scale",
className="text-light",
style={"fontSize": "1.05rem", "fontWeight": "bold"}
),
dbc.Select(
id="color-scale-dropdown",
options=COLOR_SCALES,
value="Sunsetdark",
className="mb-6 bg-dark text-light form-select-sm",
style={
"fontSize": "1.05rem",
"background": "#202224",
"color": "#fff",
"marginBottom": "30px",
},
),
]),
],
id="sidebar",
style=SIDEBAR_STYLE,
),
html.Div(
[
html.Div(
[
dbc.Switch(
id="map-style-switch",
value=True,
className="form-switch",
style={"fontSize": "1.1rem", "color": "#fff", "display": "inline-block"},
),
html.Span(
id="map-style-label",
children="Dark map",
style={
"marginLeft": "12px",
"fontSize": "1.1rem",
"color": "#fff",
"verticalAlign": "middle",
"fontWeight": "bold",
},
),
],
style={
"position": "absolute",
"top": "4px",
"right": "36px",
"zIndex": 2000,
"background": "#000",
"padding": "8px 16px",
"borderRadius": "12px",
}
),
dbc.Row(
[
dbc.Col(
kpi_card("Number of Permits", "-", "bi-clipboard-data"),
width=3,
id="kpi-card-1"
),
dbc.Col(
kpi_card("Total Fee", "-", "bi-cash-stack"),
width=3,
id="kpi-card-2"
),
dbc.Col(
kpi_card("Average Fee", "-", "bi-bar-chart-line"),
width=3,
id="kpi-card-3"
),
dbc.Col(
kpi_card("Total Housing Units", "-", "bi-house-door"),
width=3,
id="kpi-card-4"
),
],
className="mb-3 g-4",
style={"margin": "0", "width": "100%"},
),
dcc.Graph(
id="map-chart",
style={"height": "700px", "background": "#000"},
config={
"scrollZoom": True,
"displayModeBar": True,
"displaylogo": False,
"modeBarButtonsToAdd": ["zoomInMapbox", "zoomOutMapbox"],
},
),
],
id="main-content",
style=CONTENT_STYLE_DARK,
),
], style={
"background": "#000",
"minHeight": "100vh",
"position": "relative",
"padding": "0px"
})
@app.callback(
Output("map-chart", "figure"),
Output("kpi-card-1", "children"),
Output("kpi-card-2", "children"),
Output("kpi-card-3", "children"),
Output("kpi-card-4", "children"),
Output("main-content", "style"),
Output("sidebar", "style"),
Output("map-style-label", "children"),
Input("map-style-switch", "value"),
Input("extra-filter-0", "value"),
Input("extra-filter-1", "value"),
Input("extra-filter-2", "value"),
Input("color-scale-dropdown", "value"),
)
def update_dashboard(switch_value, f0, f1, f2, color_scale):
dark = True
filtered = df.copy()
# Map style based on switch
map_style = "carto-darkmatter" if switch_value else "open-street-map"
map_label = "Dark map" if switch_value else "Light map"
extra_values = [f0, f1, f2]
for val, col in zip(extra_values, extra_filters):
if val != "all":
filtered = filtered[filtered[col] == val]
# KPIs
total_permits = len(filtered)
total_fee = filtered["fee"].sum()
avg_fee = filtered["fee"].mean()
total_housing = (
filtered["housingunitstotal"].sum()
if "housingunitstotal" in filtered.columns
else 0
)
kpi1 = kpi_card("Number of Permits", f"{total_permits:,}", "bi-clipboard-data", dark)
kpi2 = kpi_card("Total Fee", f"${total_fee:,.0f}", "bi-cash-stack", dark)
kpi3 = kpi_card("Average Fee", f"${avg_fee:,.0f}", "bi-bar-chart-line", dark)
kpi4 = kpi_card("Total Housing Units", f"{total_housing:,}", "bi-house-door", dark)
# Map
required_map_cols = {"latitude_perm", "longitude_perm", "fee"}
if filtered.empty or not required_map_cols.issubset(filtered.columns):
fig = empty_figure(dark=dark)
else:
center_lat = filtered["latitude_perm"].mean()
center_lon = filtered["longitude_perm"].mean()
hover_cols = [
"fee",
"permitclassmapped",
"statuscurrentmapped",
"estprojectcost_bin",
"estprojectcost",
"contractorcompanyname",
"permitnumber",
"issuedate",
"address",
"description",
"applicantname",
"workclass",
]
hover_cols = [col for col in hover_cols if col in filtered.columns]
fig = px.scatter_mapbox(
filtered,
lat="latitude_perm",
lon="longitude_perm",
size="fee",
color="fee",
color_continuous_scale=color_scale,
mapbox_style=map_style,
zoom=10,
center={"lat": center_lat, "lon": center_lon},
height=700,
size_max=40,
hover_data=hover_cols,
)
fig.update_layout(
paper_bgcolor="#000",
plot_bgcolor="#000",
font_color="#fff",
margin=dict(l=0, r=0, t=0, b=0),
)
main_style = CONTENT_STYLE_DARK
sidebar_style = SIDEBAR_STYLE.copy()
return fig, kpi1, kpi2, kpi3, kpi4, main_style, sidebar_style, map_label
if __name__ == "__main__":
app.run()