Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
Add a readme for your dashboard here. Include content overview, data citations, and any relevant technical details.
# ADU Query Tool

Enter a Philadelphia address to see its zoning and whether an Accessory Dwelling Unit (ADU) is permitted.

## Purpose

* **Users:** Homeowners and developers
* **Decision:** Can I build an ADU on this property?
* **Data:** Zoning base districts, zoning overlays, historic register, and the L&I ADU requirements

## How it works

1. Search an address; the map drops a pin.
2. The popup checks base district, overlays and historic status automatically.
3. It follows the L&I flowchart, asking for structure type and lot size.
4. If permitted, confirm the Additional Code Requirements checklist.

## Sources

All data accessed October 16, 2026.

* **ADU rules:** City of Philadelphia, Department of Licenses and Inspections. (2022). *Accessory Dwelling Unit Checklist* (PZ_004_INF, rev. 3/8/2022). https://www.phila.gov/media/20220308130807/PZ_004_INF_Accessory-Dwelling-Unit-Checklist-rev-3.8.2022.pdf
* **Zoning base districts** (`Zoning_BaseDistricts`): Philadelphia City Planning Commission, via OpenDataPhilly. https://www.opendataphilly.org
* **Zoning overlays** (`Zoning_Overlays`): Philadelphia City Planning Commission, via OpenDataPhilly. https://www.opendataphilly.org
* **Historic districts** (`historicdistricts_local`): Philadelphia Historical Commission / City Planning Commission, via OpenDataPhilly. https://www.opendataphilly.org/datasets/philadelphia-registered-historic-districts (last updated 2017)
* **Historic sites** (`historic_sites_philreg`): Philadelphia Historical Commission / City Planning Commission, via OpenDataPhilly. https://www.opendataphilly.org/datasets/philadelphia-registered-historic-sites (last updated 2017)
* **Basemap and geocoding:** Mapbox
* **Colors** Color Brewer 2.0

## Tech

Mapbox GL JS, vanilla JavaScript, HTML, CSS.

## Run locally

1. Add your Mapbox access token in `js/main.js`.
2. Open `index.html` with a local server (e.g. VS Code Live Server).

## Limits

* Historic status, structure type and lot size need user confirmation.
* Informational only, not legal advice. L&I makes the final determination.

## Credits

Built by Chloe Alimurong. Code written with assistance from Claude (Anthropic).
Binary file added adu-query-tool/.DS_Store
Binary file not shown.
101 changes: 101 additions & 0 deletions adu-query-tool/css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
html {
font-size: 22px;
height: 100%;
}

body {
height: 100%;
padding: 0;
margin: 0;
font-family: Didot, serif;
}

main {
height: 100%;
}

.map-section {
position: relative; /* lets the search card and legend sit on top of the map */
height: 100%;
}

.map-container {
position: absolute;
inset: 0; /* fills .map-section */
}

.map {
width: 100%;
height: 100%;
}

.mapboxgl-popup {
z-index: 20;
}

/* Search card */

.map-search-controls {
position: absolute;
top: 16px;
left: 16px;
z-index: 10; /* above the map */
width: min(560px, calc(100% - 32px));
box-sizing: border-box;
margin: 0;
padding: 16px 18px;
background: #fff;
border: 0;
border-radius: 8px;
box-shadow: 0 1px 6px rgb(0 0 0);
}

.search-title {
margin: 0 0 4px;
font-size: 1.15rem;
line-height: 1.2;
}

.search-help {
margin: 0 0 8px;
font-size: 0.85rem;
color: #555;
}

.map-search-controls input {
width: 100%;
box-sizing: border-box;
padding: 12px 14px; /* taller box */
font-size: 1.1rem; /* bigger typed text */
}

.map-search-controls,
.map-search-controls input {
font-family: Didot, "Bodoni MT", serif;
}

.mapboxgl-popup button,
.mapboxgl-popup input {
font-family: inherit;
}

/* keeps the legend for screen readers without showing it */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}

.mapboxgl-popup-content {
padding: 16px 18px;
border-radius: 10px;
box-shadow: 0 4px 16px rgb(0 0 0 / 35%);
}

.mapboxgl-popup-close-button {
font-size: 22px;
padding: 4px 10px;
}
File renamed without changes
File renamed without changes
44 changes: 44 additions & 0 deletions adu-query-tool/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!doctype html>

<html lang='en'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />

<title>ADU Query</title>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.7.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.7.0/mapbox-gl.css' rel='stylesheet' />
<link rel='stylesheet' href='css/styles.css' />
</head>

<body>
<!-- <header class='article-title'>
<h1>Accessory Dwelling Unit</h1>
<h1>Query Tool</h1>
</header> -->

<main>
<section class='map-section'>
<fieldset class='map-search-controls'>
<legend class='visually-hidden'>Address search</legend>
<h1 class='search-title'>Accessory Dwelling Unit Query Tool</h1>
<p class='search-help'>
Enter a Philadelphia address to see its zoning and whether an ADU is permitted.
</p>
<input
type='text'
name='address-search'
placeholder='Type address here...'
aria-label='Search around an address or place'
/>
</fieldset>

<div class='map-container'>
<div class='map' id='map'></div>
</div>
</section>
</main>

<script src="./js/main.js" type="module"></script>
</body>
</html>
Binary file added adu-query-tool/js/.RData
Binary file not shown.
111 changes: 111 additions & 0 deletions adu-query-tool/js/.Rhistory
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#| label: setup
#| echo: false
#| message: false
library(tidyverse)
library(tidycensus)
library(sf)
library(caret)
library(spdep)
library(nngeo)
options(scipen = 999)
#| label: helpers
check_na <- function(formula, df) {
vars <- all.vars(formula)
bad <- vars[sapply(vars, function(v) any(is.na(df[[v]])))]
if (length(bad) > 0) {
stop("These variables have NAs: ", paste(bad, collapse = ", "),
". Fill them or choose another variable (Rule 1).")
}
}
# 10-fold CV RMSE, same folds every time
cv_rmse <- function(formula, data) {
df <- if (inherits(data, "sf")) st_drop_geometry(data) else data
check_na(formula, df)
set.seed(5080)
fit <- train(formula, data = df, method = "lm",
trControl = trainControl(method = "cv", number = 10))
fit$results$RMSE
}
# Each house's prediction from the fold where it was held out
cv_pred <- function(formula, data) {
df <- if (inherits(data, "sf")) st_drop_geometry(data) else data
check_na(formula, df)
set.seed(5080)
fit <- train(formula, data = df, method = "lm",
trControl = trainControl(method = "cv", number = 10,
savePredictions = "final"))
fit$pred %>% arrange(rowIndex) %>% pull(pred)
}
# Chicago Data Portal (Socrata) -> sf. `where` is a SoQL filter.
socrata_get <- function(dataset, where, select = NULL, limit = 200000) {
where <- str_squish(where)
q <- paste0("$where=", URLencode(where, reserved = TRUE), "&$limit=", limit)
if (!is.null(select)) q <- paste0(q, "&$select=", URLencode(str_squish(select), reserved = TRUE))
url <- paste0("https://data.cityofchicago.org/resource/", dataset, ".geojson?", q)
tmp <- tempfile(fileext = ".geojson")
resp <- tryCatch(download.file(url, tmp, quiet = TRUE), error = function(e) e)
if (inherits(resp, "error")) stop("Portal request failed. Open this URL in a browser to see why:\n", url)
st_read(tmp, quiet = TRUE)
}
#| label: load
train <- read.csv("~/GitHub/MUSA-5080-portfolio/data/chicago_train.csv")
test <- read.csv("~/GitHub/MUSA-5080-portfolio/data/chicago_test.csv")
all_sales <- bind_rows(
read_csv("~/GitHub/MUSA-5080-portfolio/data/chicago_train.csv", show_col_types = FALSE) %>% mutate(split = "train"),
read_csv("~/GitHub/MUSA-5080-portfolio/data/chicago_test.csv", show_col_types = FALSE) %>% mutate(split = "test")
) %>%
st_as_sf(coords = c("lon", "lat"), crs = 4326) %>%
st_transform(3435) # Illinois East State Plane, feet
all_sales %>% st_drop_geometry() %>% count(split)
#| label: part-1
train_new <- train
# sale price < $1
train_sp <- train %>%
filter(sale_price<=1)
# num_fireplace > 4 -> mansion
train_fp <- train %>%
filter(num_fireplaces > 4)
# lot size > 3 acres
train_ls <- train %>%
filter(building_000sf > 3)
# sale price > $ 1 million dollars
train_spm <- train %>%
filter(sale_price>1000000)
# Plot 1: sale price <= $1
ggplot(train, aes(x = building_000sf, y = sale_price)) +
geom_point(alpha = 0.3, color = "grey60") +
geom_point(data = train_sp, color = "red", size = 2) +
scale_y_log10(labels = scales::dollar) +
labs(x = "Living area (sq ft)", y = "Sale price (log)",
title = "Sales of $1 or less") +
theme_minimal()
# Plot 2: fireplaces > 4
ggplot(train, aes(x = num_fireplaces, y = sale_price)) +
geom_point(alpha = 0.3, color = "grey60") +
geom_point(data = train_fp, color = "red", size = 2) +
scale_y_continuous(labels = scales::dollar) +
labs(x = "Number of fireplaces", y = "Sale price",
title = "Homes with more than 4 fireplaces") +
theme_minimal()
# Plot 3: lot size > 3 acres
ggplot(train, aes(x = building_000sf, y = sale_price)) +
geom_point(alpha = 0.3, color = "grey60") +
geom_point(data = train_ls, color = "red", size = 2) +
scale_y_continuous(labels = scales::dollar) +
labs(x = "Living area (sq ft)", y = "Sale price",
title = "Lots larger than 3 acres") +
theme_minimal()
# Plot 4: sale price > $1M
ggplot(train, aes(x = building_000sf, y = sale_price)) +
geom_point(alpha = 0.3, color = "grey60") +
geom_point(data = train_spm, color = "red", size = 2) +
scale_y_continuous(labels = scales::dollar) +
labs(x = "Living area (sq ft)", y = "Sale price",
title = "Sales over $1 million") +
theme_minimal()
View(all_sales)
#| label: part-2
m1 <- lm(sale_price ~ building_000sf + building_age + lotsize_acre)
#| label: part-2
m1 <- lm(sale_price ~ building_000sf + building_age + lotsize_acre, data = all_sales)
summary(m1)
Loading