Skip to content

Commit

Permalink
Merge pull request #291 from perfectly-preserved-pie/multiselect
Browse files Browse the repository at this point in the history
Multiselect - subtypes
  • Loading branch information
perfectly-preserved-pie authored Nov 19, 2024
2 parents 849248b + ff45aa3 commit 113ea24
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 82 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

[![Build and Publish - Dev Build](https://github.com/perfectly-preserved-pie/larentals/actions/workflows/docker-image-dev.yml/badge.svg?branch=dev)](https://github.com/perfectly-preserved-pie/larentals/actions/workflows/docker-image-dev.yml)

This is an interactive map based on /u/WilliamMcCarty's weekly spreadsheets of new rental & for-sale listings in the /r/LArentals & /r/LosAngelesRealEstate subreddits and at https://www.freelarentals.com/. Just like the actual spreadsheets, you can filter the map based on different criteria, such as
This is an interactive map based on /u/WilliamMcCarty's and /u/TannerBeyer's weekly spreadsheets of new rental & for-sale listings in the /r/LArentals & /r/LosAngelesRealEstate subreddits. Just like the actual spreadsheets, you can filter the map based on different criteria, such as
* Monthly rent/List price
* Security deposit cost
* Number of bedrooms
Expand Down
34 changes: 21 additions & 13 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from dash import Dash
from dash import Dash, _dash_renderer
import dash
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import logging

# Set the React version to 18.2.0
# https://www.dash-mantine-components.com/getting-started#simple-usage
_dash_renderer._set_react_version("18.2.0")

logging.getLogger().setLevel(logging.INFO)

external_stylesheets = [dbc.themes.DARKLY, dbc.icons.BOOTSTRAP, dbc.icons.FONT_AWESOME]
Expand Down Expand Up @@ -49,19 +54,22 @@
</html>
"""

app.layout = dbc.Container([
dbc.Row( # Second row: the rest
[
dash.page_container
],
# Remove the whitespace/padding between the two cards (aka the gutters)
# https://stackoverflow.com/a/70495385
className="g-0",
app.layout = dmc.MantineProvider(
dbc.Container([
dbc.Row( # Second row: the rest
[
dash.page_container
],
# Remove the whitespace/padding between the two cards (aka the gutters)
# https://stackoverflow.com/a/70495385
className="g-0",
),
#html.Link(href='/assets/style.css', rel='stylesheet'),
],
fluid = True,
className = "dbc",
),
#html.Link(href='/assets/style.css', rel='stylesheet'),
],
fluid = True,
className = "dbc",
forceColorScheme="dark"
)

if __name__ == '__main__':
Expand Down
1 change: 0 additions & 1 deletion assets/datasets/crime.geojson

This file was deleted.

Binary file modified assets/datasets/lease.parquet
Binary file not shown.
Binary file removed assets/datasets/lease.parquet.bak
Binary file not shown.
Binary file not shown.
1 change: 0 additions & 1 deletion assets/datasets/oil_well_optimized.geojson

This file was deleted.

2 changes: 1 addition & 1 deletion functions/webscraping_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def check_expired_listing_theagency(listing_url: str, mls_number: str, board_cod
data = response.json()
is_sold = data.get('IsSold', False)
if is_sold:
logger.debug(f"Listing {mls_number} has been sold.")
logger.info(f"Listing {mls_number} has been sold.")
return is_sold
except requests.HTTPError as e:
logger.error(f"HTTP error occurred while checking if the listing for MLS {mls_number} has been sold: {e}")
Expand Down
5 changes: 4 additions & 1 deletion lease_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@
'list': 'list_price',
'lot': 'lot_size',
'mls': 'mls_number',
'name': 'street_name',
'other': 'other_deposit',
'pet deposit': 'pet_deposit',
'prking': 'parking_spaces',
'security': 'security_deposit',
'sqft': 'sqft',
'square': 'ppsqft',
'st #': 'street_number',
'st name': 'street_name',
'sub': 'subtype',
'terms': 'terms',
'yr': 'year_built',
Expand Down Expand Up @@ -156,6 +156,9 @@

df['zip_code'] = df['zip_code'].astype(pd.StringDtype())

# Remove the trailing .0 in the zip_code column
df['zip_code'] = df['zip_code'].str.replace(r'\.0$', '', regex=True)

# Tag each row with the date it was processed
for row in df.itertuples():
df.at[row.Index, 'date_processed'] = pd.Timestamp.today()
Expand Down
170 changes: 116 additions & 54 deletions pages/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dash_leaflet as dl
import numpy as np
import pandas as pd
import dash_mantine_components as dmc

def create_toggle_button(index, page_type, initial_label="Hide"):
"""Creates a toggle button with an initial label."""
Expand Down Expand Up @@ -56,6 +57,11 @@ def create_title_card(self, title, subtitle):
style={"margin-right": "5px", "margin-left": "15px"},
),
html.A("About This Project", href='https://automateordie.io/wheretolivedotla/', target='_blank'),
html.I(
className="fa fa-envelope",
style={"margin-right": "5px", "margin-left": "15px"},
),
html.A("[email protected]", href='mailto:[email protected]', target='_blank'),
]

title_card = dbc.Card(title_card_children, body=True)
Expand All @@ -65,40 +71,34 @@ def create_title_card(self, title, subtitle):
class LeaseComponents(BaseClass):
# Class Variables
subtype_meaning = {
'APT': 'Apartment (Unspecified)',
'APT/A': 'Apartment (Attached)',
'APT/D': 'Apartment (Detached)',
'CABIN/D': 'Cabin (Detached)',
'COMRES/A': 'Commercial/Residential (Attached)',
'COMRES/D': 'Commercial/Residential (Detached)',
'CONDO': 'Condo (Unspecified)',
'CONDO/A': 'Condo (Attached)',
'CONDO/D': 'Condo (Detached)',
'COOP/A': 'Cooperative (Attached)',
'DPLX/A': 'Duplex (Attached)',
'DPLX/D': 'Duplex (Detached)',
'LOFT/A': 'Loft (Attached)',
'MANL/D': '??? (Detached)',
'MH': 'Mobile Home',
'OYO/A': 'Own-Your-Own (Attached)',
'OYO/D': 'Own-Your-Own (Detached)',
'QUAD': 'Quadplex (Unspecified)',
'QUAD/A': 'Quadplex (Attached)',
'QUAD/D': 'Quadplex (Detached)',
'RMRT/A': 'Room For Rent (Attached)',
'RMRT/D': 'Room For Rent (Detached)',
'SFR': 'Single Family Residence (Unspecified)',
'SFR/A': 'Single Family Residence (Attached)',
'SFR/D': 'Single Family Residence (Detached)',
'STUD/A': 'Studio (Attached)',
'STUD/D': 'Studio (Detached)',
'TPLX': 'Triplex (Unspecified)',
'TPLX/A': 'Triplex (Attached)',
'TPLX/D': 'Triplex (Detached)',
'TWNHS': 'Townhouse (Unspecified)',
'TWNHS/A': 'Townhouse (Attached)',
'TWNHS/D': 'Townhouse (Detached)',
'Unknown': 'Unknown',
'Apartment (Attached)': 'Apartment (Attached)',
'Apartment (Detached)': 'Apartment (Detached)',
'Apartment': 'Apartment',
'Cabin (Detached)': 'Cabin (Detached)',
'Commercial Residential (Attached)': 'Commercial Residential (Attached)',
'Condominium (Attached)': 'Condominium (Attached)',
'Condominium (Detached)': 'Condominium (Detached)',
'Condominium': 'Condominium',
'Duplex (Attached)': 'Duplex (Attached)',
'Duplex (Detached)': 'Duplex (Detached)',
'Loft (Attached)': 'Loft (Attached)',
'Loft': 'Loft',
'Quadplex (Attached)': 'Quadplex (Attached)',
'Quadplex (Detached)': 'Quadplex (Detached)',
'Residential & Commercial': 'Residential & Commercial',
'Room For Rent (Attached)': 'Room For Rent (Attached)',
'Single Family (Attached)': 'Single Family (Attached)',
'Single Family (Detached)': 'Single Family (Detached)',
'Single Family': 'Single Family',
'Stock Cooperative': 'Stock Cooperative',
'Studio (Attached)': 'Studio (Attached)',
'Studio (Detached)': 'Studio (Detached)',
'Townhouse (Attached)': 'Townhouse (Attached)',
'Townhouse (Detached)': 'Townhouse (Detached)',
'Townhouse': 'Townhouse',
'Triplex (Attached)': 'Triplex (Attached)',
'Triplex (Detached)': 'Triplex (Detached)',
'Unknown': 'Unknown'
}

def __init__(self, df):
Expand Down Expand Up @@ -150,42 +150,104 @@ def categorize_laundry_features(self, feature):
return 'Other'

def create_subtype_checklist(self):
# Instance Variable
unique_values = self.df['subtype'].dropna().unique().tolist()
unique_values = ["Unknown" if i in ["/A", "/D"] else i for i in unique_values]
if "Unknown" not in unique_values:
unique_values.append("Unknown")
# Define groups
groups = {
"Apartments": [
{'label': 'Apartment', 'value': 'Apartment'},
{'label': 'Apartment (Attached)', 'value': 'APT/A'},
{'label': 'Apartment (Detached)', 'value': 'APT/D'}
],
"Cabins": [{'label': 'Cabin (Detached)', 'value': 'CABIN/D'}],
"Combo - Residential & Commercial": [{'label': 'Combo - Res & Com', 'value': 'Combo - Res & Com'}],
"Commercial Residential": [{'label': 'Commercial Residential (Attached)', 'value': 'COMRES/A'}],
"Condominiums": [
{'label': 'Condominium', 'value': 'Condominium'},
{'label': 'Condominium (Attached)', 'value': 'CONDO/A'},
{'label': 'Condominium (Detached)', 'value': 'CONDO/D'}
],
"Duplexes": [
{'label': 'Duplex (Attached)', 'value': 'DPLX/A'},
{'label': 'Duplex (Detached)', 'value': 'DPLX/D'}
],
"Lofts": [
{'label': 'Loft', 'value': 'Loft'},
{'label': 'Loft (Attached)', 'value': 'LOFT/A'}
],
"Quadplexes": [
{'label': 'Quadplex (Attached)', 'value': 'QUAD/A'},
{'label': 'Quadplex (Detached)', 'value': 'QUAD/D'}
],
"Rooms For Rent": [{'label': 'Room For Rent (Attached)', 'value': 'RMRT/A'}],
"Single Family Residences": [
{'label': 'Single Family Residence', 'value': 'Single Family'},
{'label': 'Single Family Residence (Attached)', 'value': 'SFR/A'},
{'label': 'Single Family Residence (Detached)', 'value': 'SFR/D'}
],
"Stock Cooperative": [{'label': 'Stock Cooperative', 'value': 'Stock Cooperative'}],
"Studios": [
{'label': 'Studio (Attached)', 'value': 'STUD/A'},
{'label': 'Studio (Detached)', 'value': 'STUD/D'}
],
"Townhouses": [
{'label': 'Townhouse', 'value': 'Townhouse'},
{'label': 'Townhouse (Attached)', 'value': 'TWNHS/A'},
{'label': 'Townhouse (Detached)', 'value': 'TWNHS/D'}
],
"Triplexes": [
{'label': 'Triplex (Attached)', 'value': 'TPLX/A'},
{'label': 'Triplex (Detached)', 'value': 'TPLX/D'}
],
"Unknown": [{'label': 'Unknown', 'value': 'Unknown'}]
}

# Prepare data for MultiSelect with sorted subtypes
data = [
{
"group": group,
"items": sorted(subtypes, key=lambda x: x["label"])
}
for group, subtypes in sorted(groups.items())
]

# Ensure all possible subtypes are included in the initial value
all_possible_subtypes = [item['value'] for sublist in groups.values() for item in sublist]
initial_values = all_possible_subtypes

# Custom styles to change option text color to white
# https://www.dash-mantine-components.com/components/multiselect#styles-api
custom_styles = {
"dropdown": {"color": "white"},
"groupLabel": {"color": "#ADD8E6", "fontWeight": "bold"},
"input": {"color": "white"},
"label": {"color": "white"},
"pill": {"color": "white"},
}

# Dash Component as Class Method
subtype_checklist = html.Div([
subtype_checklist = html.Div([
html.Div([
html.H5("Subtypes", style={'display': 'inline-block', 'margin-right': '10px'}),
create_toggle_button(index='subtype', initial_label="Hide", page_type='lease')
]),
html.Div([
html.H6([html.Em("Swipe (or scroll) down on the following options to view more subtypes.")]),
dcc.Checklist(
dmc.MultiSelect(
id='subtype_checklist',
options=sorted(
[
{'label': f"{i} - {self.subtype_meaning.get(i, 'Unknown')}", 'value': i}
for i in set(unique_values)
],
key=lambda x: x['label']
),
value=[term['value'] for term in [{'label': "Unknown" if pd.isnull(term) else term, 'value': "Unknown" if pd.isnull(term) else term} for term in self.df['subtype'].unique()]],
labelStyle={'display': 'block'},
inputStyle={"margin-right": "5px", "margin-left": "5px"},
data=data,
value=initial_values,
searchable=True,
nothingFoundMessage="No options found",
clearable=True,
style={"margin-bottom": "10px"},
styles=custom_styles
),
],
id={'type': 'dynamic_output_div_lease', 'index': 'subtype'},
style={
"overflow-y": "scroll",
"overflow-x": 'hidden',
"height": '220px'
"height": '120px'
})
])

return subtype_checklist

def create_bedrooms_slider(self):
Expand Down
58 changes: 48 additions & 10 deletions pages/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,22 +345,60 @@ def subtype_checklist_function(self, choice: List[str]) -> pd.Series:
if not choice:
# If no choices are selected, return False for all entries
return pd.Series([False] * len(self.df), index=self.df.index)

# Handle 'Unknown' option
if 'Unknown' in choice:
unknown_filter = self.df['subtype'].isnull()
unknown_filter = self.df['subtype'].isnull() | (self.df['subtype'] == 'Unknown')
# Remove 'Unknown' from choices to avoid filtering by it in 'isin'
choice = [c for c in choice if c != 'Unknown']
else:
unknown_filter = pd.Series([False] * len(self.df), index=self.df.index)

if choice:
# Filter where 'subtype' matches the choices
subtype_filter = self.df['subtype'].isin(choice)
else:
subtype_filter = pd.Series([False] * len(self.df), index=self.df.index)

# Combine filters

# Create a mapping for the subtypes
subtype_mapping = {
'Apartment': ['Apartment', 'APT'],
'APT/A': ['APT/A'],
'APT/D': ['APT/D'],
'Cabin (Detached)': ['CABIN/D'],
'Combo - Res & Com': ['Combo - Res & Com', 'Combo - Res &amp; Com'],
'Commercial Residential (Attached)': ['COMRES/A'],
'CONDO/A': ['CONDO/A'],
'CONDO/D': ['CONDO/D'],
'Condominium': ['Condominium', 'CONDO'],
'Duplex (Attached)': ['DPLX/A'],
'Duplex (Detached)': ['DPLX/D'],
'Loft': ['Loft', 'LOFT'],
'LOFT/A': ['LOFT/A'],
'Quadplex (Attached)': ['QUAD/A'],
'Quadplex (Detached)': ['QUAD/D'],
'Room For Rent (Attached)': ['RMRT/A'],
'SFR/A': ['SFR/A'],
'SFR/D': ['SFR/D'],
'Single Family': ['Single Family', 'SFR'],
'Stock Cooperative': ['Stock Cooperative'],
'Studio (Attached)': ['STUD/A'],
'Studio (Detached)': ['STUD/D'],
'Townhouse': ['Townhouse', 'TWNHS'],
'Triplex (Attached)': ['TPLX/A'],
'Triplex (Detached)': ['TPLX/D'],
'TWNHS/A': ['TWNHS/A'],
'TWNHS/D': ['TWNHS/D'],
}

# Create the filter based on the mapping
filters = []
for subtype in choice:
if subtype in subtype_mapping:
filters.append(self.df['subtype'].isin(subtype_mapping[subtype]))
else:
filters.append(self.df['subtype'] == subtype)

# Combine filters using logical OR
subtype_filter = pd.Series([False] * len(self.df), index=self.df.index)
for f in filters:
subtype_filter |= f

# Combine with unknown filter
combined_filter = subtype_filter | unknown_filter
return combined_filter

Expand Down

0 comments on commit 113ea24

Please sign in to comment.