Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support convex hull shading, fix devtools errors, minor popup changes #299

Merged
merged 22 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c131833
convex hull shading kinda works
perfectly-preserved-pie Nov 27, 2024
8005f69
Remove the dumb ass circles, but still kinda weird
perfectly-preserved-pie Nov 27, 2024
fa8b0c5
Retrieve all leaves so that the polygon matches all boundaries/markers
perfectly-preserved-pie Nov 27, 2024
5ae082b
Remove the previous polygon so it doesn't clutter up the map
perfectly-preserved-pie Nov 27, 2024
b10263e
First attempt at cluster colors
perfectly-preserved-pie Nov 27, 2024
6ecc7b0
Refactor cluster color scale and marker customization for improved cl…
perfectly-preserved-pie Nov 27, 2024
1bad28a
Make the cluster icon two concentric circles & change the marker coun…
perfectly-preserved-pie Nov 27, 2024
38db2f8
Don't bold the marker count
perfectly-preserved-pie Nov 27, 2024
197e9f2
Match default Leaflet.markercluster font and styling
perfectly-preserved-pie Nov 27, 2024
14fa14b
- Fix unintended page refresh when updating the map.
perfectly-preserved-pie Nov 28, 2024
6d84cb2
Remove dead code
perfectly-preserved-pie Nov 28, 2024
f12ea0f
Match Leaflet.markercluster default color scale
perfectly-preserved-pie Nov 28, 2024
f0312a8
Adjust color scale for clusters
perfectly-preserved-pie Nov 28, 2024
16d7120
Convex hulls for buy page
perfectly-preserved-pie Nov 28, 2024
ccfdcc5
Migrate from deprecated argument
perfectly-preserved-pie Nov 28, 2024
0f18ba4
Migrate from deprecated argument
perfectly-preserved-pie Nov 28, 2024
0748b6c
Merge branch 'convex-test1' of https://github.com/perfectly-preserved…
perfectly-preserved-pie Nov 28, 2024
6f3724e
Convex hulls support
perfectly-preserved-pie Nov 28, 2024
e35c0ac
Refactor style properties to use camelCase and get rid of annoying na…
perfectly-preserved-pie Nov 28, 2024
d099714
e35c0ac8759cdd02bbf1264da93a2a7e0a3bfb42 fix missing cluster icons
perfectly-preserved-pie Nov 28, 2024
ae98279
Popup: change listing URL hyperlink from mls # to full street address…
perfectly-preserved-pie Nov 28, 2024
cef9c33
ae98279ab02b2c7d6c82fb598778a8cabaee7b3b repeat for Buy
perfectly-preserved-pie Nov 28, 2024
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
9 changes: 6 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@

# Create the app
app = Dash(
__name__,
external_stylesheets=external_stylesheets,
use_pages=True,
external_scripts = [
'https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js' # Turf.js for convex hulls
],
external_stylesheets = external_stylesheets,
name = __name__,
# Add meta tags for mobile devices
# https://community.plotly.com/t/reorder-website-for-mobile-view/33669/5?
meta_tags = [
{"name": "viewport", "content": "width=device-width, initial-scale=1"}
],
use_pages = True,
)

# Set the page title
Expand Down
101 changes: 101 additions & 0 deletions assets/dashExtensions_default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
window.dashExtensions = Object.assign({}, window.dashExtensions, {
default: {
function0: function(feature, latlng, index, context) {
// A global reference to the currently displayed polygon
if (!context.currentPolygon) {
context.currentPolygon = null;
}

// Access all the leaves of the cluster
const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children
const clusterSize = leaves.length;

// Define a color scale (mimicking Leaflet.markercluster behavior)
// Default colors are at https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css
// Because I'm coloring the polygons (instead of using the default blue) most of the default colors look way too faint on the map
// So I'm adjusting them here
const getColor = function(size) {
if (size < 50) return 'rgba(110, 204, 57, 0.6)'; // Small clusters
if (size < 200) return 'yellow'; // Medium clusters
if (size < 500) return 'orange'; // Larger clusters
return 'red'; // Very large clusters
};

// Get the appropriate color for the cluster size
const color = getColor(clusterSize);

// Collect coordinates for the cluster's children
let features = [];
for (let i = 0; i < leaves.length; ++i) {
const coords = leaves[i].geometry.coordinates;
const lng = coords[0];
const lat = coords[1];
features.push(turf.point([lng, lat]));
}

// Create a Turf.js feature collection
const fc = turf.featureCollection(features);

// Compute the convex hull
const convexHull = turf.convex(fc);

// If the convex hull exists, create it as a polygon
let polygonLayer = null;
if (convexHull) {
polygonLayer = L.geoJSON(convexHull, {
style: {
color: color, // Use the same color as the cluster icon
weight: 2, // Border thickness
fillOpacity: 0.2, // Polygon fill transparency
fillColor: color // Polygon fill color
}
});
}

// Create a custom marker for the cluster with Leaflet.markercluster font styling
const clusterMarker = L.marker(latlng, {
icon: L.divIcon({
html: `
<div style="position:relative; width:40px; height:40px; font-family: Arial, sans-serif; font-size: 12px;">
<div style="background-color:${color}; opacity:0.6;
border-radius:50%; width:40px; height:40px;
position:absolute; top:0; left:0;"></div>
<div style="background-color:${color};
border-radius:50%; width:30px; height:30px;
position:absolute; top:5px; left:5px;
display:flex; align-items:center; justify-content:center;
font-weight:normal; color:black; font-size:12px; font-family: Arial, sans-serif;">
${feature.properties.point_count_abbreviated}
</div>
</div>`,
className: 'marker-cluster', // Optional: add a class for further customization
iconSize: L.point(40, 40) // Adjust the icon size
})
});

// Add mouseover behavior to display the convex hull
clusterMarker.on('mouseover', function() {
// Remove the previously displayed polygon, if any
if (context.currentPolygon) {
context.map.removeLayer(context.currentPolygon);
}

// Add the new polygon to the map and update the reference
if (polygonLayer) {
polygonLayer.addTo(context.map);
context.currentPolygon = polygonLayer;
}
});

// Add behavior to remove the polygon when the mouse leaves the cluster
clusterMarker.on('mouseout', function() {
if (context.currentPolygon) {
context.map.removeLayer(context.currentPolygon);
context.currentPolygon = null;
}
});

return clusterMarker;
}
}
});
2 changes: 1 addition & 1 deletion assets/javascript/clientside_callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ window.dash_clientside = Object.assign({}, window.dash_clientside, {
} else {
return {
'display': 'block',
'margin-bottom' : '10px',
'marginBottom' : '10px',
};
}
}
Expand Down
34 changes: 16 additions & 18 deletions assets/javascript/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,15 @@ window.dash_props = Object.assign({}, window.dash_props, {
function getListingUrlBlock(data) {
if (!data.listing_url) {
return `
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listing ID (MLS#)</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${data.mls_number}</td>
</tr>
<div style="text-align: center;">
<h5>${data.full_street_address}</h5>
</div>
`;
}
return `
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listing ID (MLS#)</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">
<a href='${data.listing_url}' referrerPolicy='noreferrer' target='_blank'>${data.mls_number}</a>
</td>
</tr>
<div style="text-align: center;">
<h5><a href='${data.listing_url}' referrerPolicy='noreferrer' target='_blank'>${data.full_street_address}</a></h5>
</div>
`;
}

Expand Down Expand Up @@ -63,15 +59,16 @@ window.dash_props = Object.assign({}, window.dash_props, {
return `
<div>
${imageRow}
<div style="text-align: center;">
<h5>${data.full_street_address}</h5>
</div>
${listingUrlBlock}
<table style="width:100%;border-collapse:collapse;">
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listed Date</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${formatDate(data.listed_date)}</td>
</tr>
${listingUrlBlock}
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listing ID (MLS#)</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${data.mls_number}</td>
</tr>
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">List Office Phone</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${phoneNumberBlock}</td>
Expand Down Expand Up @@ -216,15 +213,16 @@ window.dash_props = Object.assign({}, window.dash_props, {
return `
<div>
${imageRow}
<div style="text-align: center;">
<h5>${data.address}</h5>
</div>
${listingUrlBlock}
<table style="width:100%;border-collapse:collapse;">
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listed Date</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${formatDate(data.listed_date)}</td>
</tr>
${listingUrlBlock}
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">Listing ID (MLS#)</th>
<td style="padding:8px;border-bottom:1px solid #ddd;">${formatDate(data.mls_number)}</td>
</tr>
${parkNameBlock}
<tr>
<th style="text-align:left;padding:8px;border-bottom:1px solid #ddd;">List Price</th>
Expand Down
1 change: 1 addition & 0 deletions assets/us-cities.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions functions/convex_hull.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from dash_extensions.javascript import assign

generate_convex_hulls = assign("""function(feature, latlng, index, context){
// A global reference to the currently displayed polygon
if (!context.currentPolygon) {
context.currentPolygon = null;
}

// Access all the leaves of the cluster
const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children
const clusterSize = leaves.length;

// Define a color scale (mimicking Leaflet.markercluster behavior)
// Default colors are at https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css
// Because I'm coloring the polygons (instead of using the default blue) most of the default colors look way too faint on the map
// So I'm adjusting them here
const getColor = function(size) {
if (size < 50) return 'rgba(110, 204, 57, 0.6)'; // Small clusters
if (size < 200) return 'yellow'; // Medium clusters
if (size < 500) return 'orange'; // Larger clusters
return 'red'; // Very large clusters
};

// Get the appropriate color for the cluster size
const color = getColor(clusterSize);

// Collect coordinates for the cluster's children
let features = [];
for (let i = 0; i < leaves.length; ++i) {
const coords = leaves[i].geometry.coordinates;
const lng = coords[0];
const lat = coords[1];
features.push(turf.point([lng, lat]));
}

// Create a Turf.js feature collection
const fc = turf.featureCollection(features);

// Compute the convex hull
const convexHull = turf.convex(fc);

// If the convex hull exists, create it as a polygon
let polygonLayer = null;
if (convexHull) {
polygonLayer = L.geoJSON(convexHull, {
style: {
color: color, // Use the same color as the cluster icon
weight: 2, // Border thickness
fillOpacity: 0.2, // Polygon fill transparency
fillColor: color // Polygon fill color
}
});
}

// Create a custom marker for the cluster with Leaflet.markercluster font styling
const clusterMarker = L.marker(latlng, {
icon: L.divIcon({
html: `
<div style="position:relative; width:40px; height:40px; font-family: Arial, sans-serif; font-size: 12px;">
<div style="background-color:${color}; opacity:0.6;
border-radius:50%; width:40px; height:40px;
position:absolute; top:0; left:0;"></div>
<div style="background-color:${color};
border-radius:50%; width:30px; height:30px;
position:absolute; top:5px; left:5px;
display:flex; align-items:center; justify-content:center;
font-weight:normal; color:black; font-size:12px; font-family: Arial, sans-serif;">
${feature.properties.point_count_abbreviated}
</div>
</div>`,
className: 'marker-cluster', // Optional: add a class for further customization
iconSize: L.point(40, 40) // Adjust the icon size
})
});

// Add mouseover behavior to display the convex hull
clusterMarker.on('mouseover', function() {
// Remove the previously displayed polygon, if any
if (context.currentPolygon) {
context.map.removeLayer(context.currentPolygon);
}

// Add the new polygon to the map and update the reference
if (polygonLayer) {
polygonLayer.addTo(context.map);
context.currentPolygon = polygonLayer;
}
});

// Add behavior to remove the polygon when the mouse leaves the cluster
clusterMarker.on('mouseout', function() {
if (context.currentPolygon) {
context.map.removeLayer(context.currentPolygon);
context.currentPolygon = null;
}
});

return clusterMarker;
}""")
7 changes: 4 additions & 3 deletions pages/buy_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dash_extensions.javascript import Namespace
from dash.dependencies import Input, Output, State
from flask import request
from functions.convex_hull import generate_convex_hulls
from loguru import logger
from user_agents import parse
import dash
Expand All @@ -12,9 +13,8 @@
import dash_leaflet.express as dlx
import pandas as pd
import sys
import uuid
from loguru import logger
import time
import uuid

dash.register_page(
__name__,
Expand Down Expand Up @@ -274,12 +274,13 @@ def update_map(
id=str(uuid.uuid4()),
data=geojson,
cluster=True,
clusterToLayer=generate_convex_hulls,
onEachFeature=ns("on_each_feature"),
zoomToBoundsOnClick=True,
superClusterOptions={ # https://github.com/mapbox/supercluster#options
'radius': 160,
'minZoom': 3,
},
options=dict(onEachFeature=ns("on_each_feature"))
)

# Callback to toggle the visibility of dynamic components
Expand Down
Loading
Loading