From c131833d5f6b84615151852bb12c02e1205d95b1 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:11:10 -0800 Subject: [PATCH 01/20] convex hull shading kinda works --- assets/dashExtensions_default.js | 104 ++++++++++++++++++++++ assets/us-cities.json | 1 + test.py | 144 +++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 assets/dashExtensions_default.js create mode 100644 assets/us-cities.json create mode 100644 test.py diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js new file mode 100644 index 00000000..c364e69e --- /dev/null +++ b/assets/dashExtensions_default.js @@ -0,0 +1,104 @@ +window.dashExtensions = Object.assign({}, window.dashExtensions, { + default: { + function0: function(feature, layer, context) { + if (feature.properties.city) { + layer.bindTooltip(`${feature.properties.city} (${feature.properties.density})`) + } + }, + function1: function(feature, latlng, context) { + const { + min, + max, + colorscale, + circleOptions, + colorProp + } = context.hideout; + const csc = chroma.scale(colorscale).domain([min, max]); + circleOptions.fillColor = csc(feature.properties[colorProp]); + return L.circleMarker(latlng, circleOptions); + }, + function2: function(feature, latlng, index, context) { + const { + min, + max, + colorscale, + circleOptions, + colorProp + } = context.hideout; + const csc = chroma.scale(colorscale).domain([min, max]); + + // Access the leaves of the cluster + const leaves = index.getLeaves(feature.properties.cluster_id); + + // Collect coordinates and sum property values + let features = []; + let valueSum = 0; + + for (let i = 0; i < leaves.length; ++i) { + const coords = leaves[i].geometry.coordinates; + const lng = coords[0]; + const lat = coords[1]; + + // Create a Turf.js point feature + features.push(turf.point([lng, lat])); + + // Sum the property values + valueSum += leaves[i].properties[colorProp]; + } + + // Calculate the mean value for color scaling + const valueMean = valueSum / leaves.length; + + // Create a Turf.js feature collection + const fc = turf.featureCollection(features); + + // Compute the convex hull + const convexHull = turf.convex(fc); + + // Create a Leaflet layer for the convex hull polygon + let polygonLayer = null; + if (convexHull) { + polygonLayer = L.geoJSON(convexHull, { + style: { + color: csc(valueMean), + weight: 1, + fillOpacity: 0.2 + } + }); + } else { + // If convex hull couldn't be computed (e.g., less than 3 points), create a circle + polygonLayer = L.circle(latlng, { + radius: 50000, // Adjust radius as needed + color: csc(valueMean), + weight: 1, + fillOpacity: 0.2 + }); + } + + // Customize the cluster icon + const scatterIcon = L.DivIcon.extend({ + createIcon: function(oldIcon) { + let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon); + icon.style.backgroundColor = this.options.color; + return icon; + } + }); + + const icon = new scatterIcon({ + html: '
' + feature.properties.point_count_abbreviated + '
', + className: "marker-cluster", + iconSize: L.point(40, 40), + color: csc(valueMean) + }); + + const marker = L.marker(latlng, { + icon: icon + }); + + // Create a layer group containing the polygon and the marker + const clusterLayer = L.layerGroup([polygonLayer, marker]); + + return clusterLayer; + } + } +}); \ No newline at end of file diff --git a/assets/us-cities.json b/assets/us-cities.json new file mode 100644 index 00000000..5fa1141f --- /dev/null +++ b/assets/us-cities.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3921, 46.9994]}, "properties": {"city": "South Creek", "density": 125.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.0989, 47.2507]}, "properties": {"city": "Roslyn", "density": 84.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.9713, 47.3048]}, "properties": {"city": "Sprague", "density": 163.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5968, 47.3352]}, "properties": {"city": "Gig Harbor", "density": 622.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.092, 48.0639]}, "properties": {"city": "Lake Cassidy", "density": 131.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8607, 46.8537]}, "properties": {"city": "Tenino", "density": 491.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0911, 48.1229]}, "properties": {"city": "Jamestown", "density": 191.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9924, 47.942]}, "properties": {"city": "Three Lakes", "density": 112.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6663, 48.7311]}, "properties": {"city": "Curlew Lake", "density": 50.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9861, 47.9038]}, "properties": {"city": "Chain Lake", "density": 156.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.8994, 48.0536]}, "properties": {"city": "Pateros", "density": 536.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.6344, 46.3076]}, "properties": {"city": "Rosburg", "density": 6.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4376, 47.1417]}, "properties": {"city": "Parkland", "density": 1653.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7543, 48.923]}, "properties": {"city": "Birch Bay", "density": 208.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0113, 46.7537]}, "properties": {"city": "Ashford", "density": 30.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6655, 47.5508]}, "properties": {"city": "Navy Yard City", "density": 1978.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.5159, 48.4228]}, "properties": {"city": "Omak", "density": 478.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0464, 47.0887]}, "properties": {"city": "Farmington", "density": 164.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.02, 47.1615]}, "properties": {"city": "Buckley", "density": 482.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1058, 47.8525]}, "properties": {"city": "Cathcart", "density": 250.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6408, 47.7417]}, "properties": {"city": "Poulsbo", "density": 869.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6292, 48.4878]}, "properties": {"city": "Anacortes", "density": 559.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5689, 47.3715]}, "properties": {"city": "Maplewood", "density": 248.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.52, 47.7595]}, "properties": {"city": "Creston", "density": 212.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1307, 47.4513]}, "properties": {"city": "Rockford", "density": 271.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.9698, 45.6599]}, "properties": {"city": "Wishram", "density": 131.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9695, 46.7225]}, "properties": {"city": "Centralia", "density": 883.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2848, 47.1203]}, "properties": {"city": "South Hill", "density": 1185.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9577, 46.1796]}, "properties": {"city": "Longview Heights", "density": 337.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7021, 47.5973]}, "properties": {"city": "Erlands Point-Kitsap Lake", "density": 655.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4795, 45.9122]}, "properties": {"city": "Amboy", "density": 78.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5887, 47.0915]}, "properties": {"city": "St. John", "density": 322.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.1749, 45.6316]}, "properties": {"city": "Dallesport", "density": 99.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6343, 47.1569]}, "properties": {"city": "Ketron Island", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0013, 46.5737]}, "properties": {"city": "Mesa", "density": 119.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3518, 47.2328]}, "properties": {"city": "Fife", "density": 684.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.3186, 47.1949]}, "properties": {"city": "Moses Lake North", "density": 264.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.7336, 48.2929]}, "properties": {"city": "Chewelah", "density": 341.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.0898, 46.8304]}, "properties": {"city": "Grayland", "density": 42.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4191, 46.9837]}, "properties": {"city": "Kittitas", "density": 729.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4708, 48.1865]}, "properties": {"city": "Camano", "density": 155.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9882, 47.883]}, "properties": {"city": "Monroe North", "density": 282.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9689, 45.6425]}, "properties": {"city": "North Bonneville", "density": 154.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.1626, 45.818]}, "properties": {"city": "Klickitat", "density": 147.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.6298, 48.0685]}, "properties": {"city": "Loon Lake", "density": 92.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0545, 48.1788]}, "properties": {"city": "Newport", "density": 624.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.35, 47.7795]}, "properties": {"city": "Mead", "density": 364.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4671, 47.5175]}, "properties": {"city": "Cashmere", "density": 1061.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.7791, 47.0587]}, "properties": {"city": "Aberdeen Gardens", "density": 67.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1521, 46.9685]}, "properties": {"city": "Ocean Shores", "density": 268.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1567, 46.5891]}, "properties": {"city": "Basin City", "density": 155.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0495, 47.1034]}, "properties": {"city": "Wilkeson", "density": 420.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5614, 45.7524]}, "properties": {"city": "Meadow Glade", "density": 307.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.063, 48.2119]}, "properties": {"city": "Arlington Heights", "density": 129.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0861, 46.5391]}, "properties": {"city": "Uniontown", "density": 143.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2465, 47.1305]}, "properties": {"city": "Oakesdale", "density": 164.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9178, 46.1841]}, "properties": {"city": "West Side Highway", "density": 901.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.7513, 48.5571]}, "properties": {"city": "Conconully", "density": 300.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1557, 47.2822]}, "properties": {"city": "Latah", "density": 225.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.4112, 47.0055]}, "properties": {"city": "Elma", "density": 468.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.756, 46.7026]}, "properties": {"city": "Tieton", "density": 570.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5298, 47.5126]}, "properties": {"city": "Southworth", "density": 260.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.5068, 48.9938]}, "properties": {"city": "Danville", "density": 41.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3125, 46.3807]}, "properties": {"city": "Toppenish", "density": 1627.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.1276, 48.0677]}, "properties": {"city": "River Road", "density": 1055.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4064, 48.7461]}, "properties": {"city": "Geneva", "density": 958.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.5496, 46.5923]}, "properties": {"city": "Yakima", "density": 1302.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3968, 46.5638]}, "properties": {"city": "Moxee", "density": 670.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3533, 47.8114]}, "properties": {"city": "Edmonds", "density": 1829.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8393, 46.0171]}, "properties": {"city": "Kalama", "density": 297.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.2248, 48.9974]}, "properties": {"city": "Laurier", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4726, 47.4122]}, "properties": {"city": "Vashon", "density": 100.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.4197, 47.7478]}, "properties": {"city": "Country Homes", "density": 1339.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6029, 48.2546]}, "properties": {"city": "Darrington", "density": 306.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3706, 48.8616]}, "properties": {"city": "Metaline Falls", "density": 446.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.2805, 45.6956]}, "properties": {"city": "Lyle", "density": 299.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.3272, 47.1371]}, "properties": {"city": "Cascade Valley", "density": 232.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3812, 47.4299]}, "properties": {"city": "Spangle", "density": 339.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.0056, 48.1289]}, "properties": {"city": "Methow", "density": 44.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.0707, 47.648]}, "properties": {"city": "Waterville", "density": 533.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9129, 47.9839]}, "properties": {"city": "Lake Roesiger", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3154, 47.2522]}, "properties": {"city": "Milton", "density": 1185.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9886, 47.4105]}, "properties": {"city": "Krupp", "density": 32.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2192, 47.1718]}, "properties": {"city": "Alderton", "density": 240.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6496, 47.1079]}, "properties": {"city": "DuPont", "density": 629.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2832, 47.2309]}, "properties": {"city": "Edgewood", "density": 515.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2742, 47.2015]}, "properties": {"city": "North Puyallup", "density": 684.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1967, 47.215]}, "properties": {"city": "Pacific Beach", "density": 171.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.1563, 47.8858]}, "properties": {"city": "Manson", "density": 472.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.7751, 48.1034]}, "properties": {"city": "Brewster", "density": 785.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5413, 45.7764]}, "properties": {"city": "Battle Ground", "density": 938.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3426, 47.7564]}, "properties": {"city": "Shoreline", "density": 1864.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.7725, 46.9541]}, "properties": {"city": "Cosmopolis", "density": 452.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5792, 47.6459]}, "properties": {"city": "Airway Heights", "density": 524.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9084, 46.2431]}, "properties": {"city": "Grandview", "density": 670.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.2372, 48.3378]}, "properties": {"city": "Inchelium", "density": 6.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0031, 47.9391]}, "properties": {"city": "Grand Coulee", "density": 330.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0952, 47.3475]}, "properties": {"city": "Union", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6689, 48.6772]}, "properties": {"city": "Torboy", "density": 19.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8909, 46.1248]}, "properties": {"city": "Kelso", "density": 580.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.112, 46.8919]}, "properties": {"city": "Westport", "density": 214.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.7651, 46.9713]}, "properties": {"city": "Junction City", "density": 2.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.348, 48.282]}, "properties": {"city": "Lake Ketchum", "density": 166.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1684, 46.2497]}, "properties": {"city": "West Pasco", "density": 280.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3154, 47.3914]}, "properties": {"city": "Des Moines", "density": 1875.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.1519, 47.6551]}, "properties": {"city": "Davenport", "density": 395.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3244, 47.6211]}, "properties": {"city": "Seattle", "density": 3336.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2596, 47.8709]}, "properties": {"city": "Lake Stickney", "density": 2441.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9048, 47.6446]}, "properties": {"city": "Carnation", "density": 725.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.167, 47.9524]}, "properties": {"city": "Everett", "density": 1278.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0522, 46.9674]}, "properties": {"city": "Warden", "density": 366.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0751, 46.9103]}, "properties": {"city": "Palouse", "density": 388.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4482, 46.6024]}, "properties": {"city": "Terrace Heights", "density": 377.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0502, 46.4152]}, "properties": {"city": "Clarkston", "density": 1430.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9534, 48.0]}, "properties": {"city": "Elmer City", "density": 401.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4887, 46.5305]}, "properties": {"city": "Mossyrock", "density": 444.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3468, 47.1343]}, "properties": {"city": "Summit View", "density": 885.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.583, 47.7238]}, "properties": {"city": "Suquamish", "density": 232.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1633, 47.5303]}, "properties": {"city": "Newcastle", "density": 1014.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3426, 48.2451]}, "properties": {"city": "Stanwood", "density": 937.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2272, 46.9727]}, "properties": {"city": "Kapowsin", "density": 36.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3923, 46.4737]}, "properties": {"city": "Donald", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.6145, 46.2699]}, "properties": {"city": "Altoona", "density": 4.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.094, 47.1373]}, "properties": {"city": "South Prairie", "density": 441.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6337, 46.667]}, "properties": {"city": "Eschbach", "density": 123.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5142, 47.7566]}, "properties": {"city": "Indianola", "density": 258.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.8778, 47.6698]}, "properties": {"city": "Reardan", "density": 451.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.7154, 46.6729]}, "properties": {"city": "Cowiche", "density": 708.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2684, 48.2306]}, "properties": {"city": "Sunday Lake", "density": 148.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6668, 47.5882]}, "properties": {"city": "Rocky Point", "density": 790.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9728, 46.1991]}, "properties": {"city": "Burbank", "density": 99.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2814, 47.3374]}, "properties": {"city": "Lakeland North", "density": 1452.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9704, 48.0874]}, "properties": {"city": "Granite Falls", "density": 632.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4306, 45.6003]}, "properties": {"city": "Camas", "density": 646.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.1579, 47.3298]}, "properties": {"city": "Skokomish", "density": 43.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2961, 48.3328]}, "properties": {"city": "Cusick", "density": 161.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3769, 47.2363]}, "properties": {"city": "Rosalia", "density": 348.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7739, 47.2257]}, "properties": {"city": "Longbranch", "density": 62.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.1926, 46.3455]}, "properties": {"city": "Granger", "density": 841.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5966, 47.1221]}, "properties": {"city": "North Fort Lewis", "density": 328.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.8727, 46.5325]}, "properties": {"city": "Tampico", "density": 46.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2727, 47.4748]}, "properties": {"city": "Tukwila", "density": 847.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3835, 46.2056]}, "properties": {"city": "Cathlamet", "density": 428.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3286, 47.4338]}, "properties": {"city": "Wenatchee", "density": 1314.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7006, 47.9101]}, "properties": {"city": "Port Ludlow", "density": 88.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9057, 46.2717]}, "properties": {"city": "Castle Rock", "density": 433.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9323, 48.8903]}, "properties": {"city": "Glacier", "density": 13.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.5556, 47.8212]}, "properties": {"city": "Index", "density": 384.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.7326, 48.6477]}, "properties": {"city": "Republic", "density": 274.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.155, 47.5953]}, "properties": {"city": "Bellevue", "density": 1666.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6119, 48.7997]}, "properties": {"city": "Malo", "density": 236.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7037, 47.1577]}, "properties": {"city": "Anderson Island", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.546, 47.2148]}, "properties": {"city": "University Place", "density": 1547.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.5858, 47.0102]}, "properties": {"city": "Montesano", "density": 147.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6867, 46.8917]}, "properties": {"city": "Rainier", "density": 473.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0029, 46.7461]}, "properties": {"city": "Fords Prairie", "density": 188.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5423, 46.9971]}, "properties": {"city": "Roy", "density": 652.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9237, 48.2836]}, "properties": {"city": "Oso", "density": 29.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.4346, 48.5378]}, "properties": {"city": "Marblemount", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3671, 47.0448]}, "properties": {"city": "Elk Plain", "density": 716.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3298, 48.4677]}, "properties": {"city": "Burlington", "density": 792.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7146, 47.7227]}, "properties": {"city": "Bangor Base", "density": 221.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2009, 47.5854]}, "properties": {"city": "Beaux Arts Village", "density": 1524.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5571, 47.8487]}, "properties": {"city": "Port Gamble Tribal Community", "density": 83.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8959, 47.0417]}, "properties": {"city": "Olympia", "density": 1092.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.2809, 47.4172]}, "properties": {"city": "East Wenatchee", "density": 1425.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.8372, 48.3588]}, "properties": {"city": "Addy", "density": 105.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6204, 45.6577]}, "properties": {"city": "Minnehaha", "density": 2056.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2411, 47.4949]}, "properties": {"city": "Bryn Mawr-Skyway", "density": 2500.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.4737, 47.0223]}, "properties": {"city": "Satsop", "density": 31.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7491, 47.4651]}, "properties": {"city": "Riverbend", "density": 294.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.1733, 47.2405]}, "properties": {"city": "Easton", "density": 47.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6828, 47.6663]}, "properties": {"city": "Silverdale", "density": 631.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9007, 46.5844]}, "properties": {"city": "Napavine", "density": 252.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0411, 48.0597]}, "properties": {"city": "Lochsloy", "density": 164.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.547, 46.9994]}, "properties": {"city": "Ellensburg", "density": 1022.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.55, 46.9365]}, "properties": {"city": "McKenna", "density": 347.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1748, 47.3848]}, "properties": {"city": "Fairfield", "density": 387.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3501, 48.2623]}, "properties": {"city": "Northwest Stanwood", "density": 77.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.638, 47.8119]}, "properties": {"city": "Mansfield", "density": 401.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9725, 47.7354]}, "properties": {"city": "Duvall", "density": 1230.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9814, 46.9155]}, "properties": {"city": "Markham", "density": 82.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3699, 47.2035]}, "properties": {"city": "Waller", "density": 391.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.963, 46.146]}, "properties": {"city": "Longview", "density": 976.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.2937, 46.2826]}, "properties": {"city": "Richland", "density": 556.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6368, 47.3453]}, "properties": {"city": "Rosedale", "density": 379.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.597, 45.6367]}, "properties": {"city": "Vancouver", "density": 1386.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6175, 47.4874]}, "properties": {"city": "Bethel", "density": 448.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6397, 48.9143]}, "properties": {"city": "Custer", "density": 31.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6564, 47.8113]}, "properties": {"city": "Lofall", "density": 427.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.143, 47.4467]}, "properties": {"city": "Fairwood", "density": 1592.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9342, 46.4907]}, "properties": {"city": "Winlock", "density": 405.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2338, 47.219]}, "properties": {"city": "Sumner", "density": 517.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0755, 47.7466]}, "properties": {"city": "Cottage Lake", "density": 406.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3829, 46.1742]}, "properties": {"city": "Puget Island", "density": 45.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8982, 47.8821]}, "properties": {"city": "Woods Creek", "density": 160.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3433, 45.5812]}, "properties": {"city": "Washougal", "density": 1108.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1076, 48.1188]}, "properties": {"city": "Sisco Heights", "density": 86.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6022, 47.3766]}, "properties": {"city": "Canterwood", "density": 576.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4215, 46.4434]}, "properties": {"city": "Wapato", "density": 1646.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3698, 46.2185]}, "properties": {"city": "Lower Elochoman", "density": 40.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2804, 48.1387]}, "properties": {"city": "Lake Goodwin", "density": 374.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.276, 47.6315]}, "properties": {"city": "Banks Lake South", "density": 42.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2128, 47.3887]}, "properties": {"city": "Kent", "density": 1470.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3081, 46.0056]}, "properties": {"city": "Bickleton", "density": 5.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0441, 46.375]}, "properties": {"city": "Ryderwood", "density": 797.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0746, 47.9845]}, "properties": {"city": "Cavalero", "density": 1243.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4945, 48.3925]}, "properties": {"city": "La Conner", "density": 905.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2326, 47.848]}, "properties": {"city": "Martha Lake", "density": 1560.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.8598, 47.0845]}, "properties": {"city": "George", "density": 151.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.097, 47.9277]}, "properties": {"city": "Snohomish", "density": 1119.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2698, 46.8682]}, "properties": {"city": "Eatonville", "density": 633.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8219, 45.7353]}, "properties": {"city": "Carson", "density": 216.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9369, 47.7105]}, "properties": {"city": "Almira", "density": 205.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3211, 48.9276]}, "properties": {"city": "Nooksack", "density": 852.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6315, 45.7373]}, "properties": {"city": "Mount Vista", "density": 628.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0281, 47.7968]}, "properties": {"city": "High Bridge", "density": 167.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2831, 47.2784]}, "properties": {"city": "Lakeland South", "density": 1087.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2459, 47.7532]}, "properties": {"city": "Kenmore", "density": 1438.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.6115, 48.3612]}, "properties": {"city": "Neah Bay", "density": 163.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.042, 46.9213]}, "properties": {"city": "Cliffdell", "density": 17.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3695, 48.1041]}, "properties": {"city": "Port Angeles East", "density": 371.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3393, 47.4762]}, "properties": {"city": "Burien", "density": 1983.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.2728, 47.3283]}, "properties": {"city": "Taholah", "density": 79.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6694, 47.386]}, "properties": {"city": "Wauna", "density": 268.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.8405, 46.6624]}, "properties": {"city": "Connell", "density": 275.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.2299, 48.345]}, "properties": {"city": "Disautel", "density": 2.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.2327, 47.2975]}, "properties": {"city": "Santiago", "density": 6.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2111, 47.0966]}, "properties": {"city": "Orting", "density": 1138.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.8986, 48.5454]}, "properties": {"city": "Colville", "density": 596.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1558, 48.081]}, "properties": {"city": "Marysville", "density": 1289.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3365, 46.0669]}, "properties": {"city": "Walla Walla", "density": 938.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.2717, 46.4083]}, "properties": {"city": "Zillah", "density": 699.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6045, 48.8777]}, "properties": {"city": "Curlew", "density": 75.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7505, 47.3376]}, "properties": {"city": "Key Center", "density": 100.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.1542, 47.402]}, "properties": {"city": "Hoodsport", "density": 121.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4233, 47.0979]}, "properties": {"city": "Spanaway", "density": 1344.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9019, 46.7364]}, "properties": {"city": "Mattawa", "density": 2388.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.2579, 48.2536]}, "properties": {"city": "Clallam Bay", "density": 249.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1408, 47.1443]}, "properties": {"city": "Prairie Ridge", "density": 1202.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6065, 48.4854]}, "properties": {"city": "Rockport", "density": 80.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7154, 47.3814]}, "properties": {"city": "Stansberry Lake", "density": 510.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1722, 47.1205]}, "properties": {"city": "Copalis Beach", "density": 58.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3594, 47.0914]}, "properties": {"city": "Frederickson", "density": 725.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0433, 46.3371]}, "properties": {"city": "Asotin", "density": 483.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2034, 47.8633]}, "properties": {"city": "Mill Creek", "density": 1747.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.087, 48.055]}, "properties": {"city": "Bell Hill", "density": 127.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.277, 47.819]}, "properties": {"city": "Green Bluff", "density": 27.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5612, 48.0013]}, "properties": {"city": "Clayton", "density": 108.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2902, 47.1794]}, "properties": {"city": "Puyallup", "density": 1119.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4833, 45.7301]}, "properties": {"city": "Hockinson", "density": 147.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2317, 48.3129]}, "properties": {"city": "Lake McMurray", "density": 63.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.868, 46.7968]}, "properties": {"city": "Bucoda", "density": 401.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0444, 47.542]}, "properties": {"city": "Issaquah", "density": 1192.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6895, 47.3326]}, "properties": {"city": "Odessa", "density": 417.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6553, 45.6797]}, "properties": {"city": "Hazel Dell", "density": 1718.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2374, 47.1286]}, "properties": {"city": "McMillin", "density": 390.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.711, 47.7583]}, "properties": {"city": "Wilbur", "density": 249.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.4243, 46.3004]}, "properties": {"city": "Skamokawa Valley", "density": 4.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.9081, 45.754]}, "properties": {"city": "Centerville", "density": 12.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.9455, 46.8368]}, "properties": {"city": "Nile", "density": 49.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2346, 47.6625]}, "properties": {"city": "Spokane Valley", "density": 1001.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7503, 45.9149]}, "properties": {"city": "Woodland", "density": 575.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5144, 45.8127]}, "properties": {"city": "Lewisville", "density": 107.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9923, 46.9453]}, "properties": {"city": "Vantage", "density": 21.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7057, 47.5237]}, "properties": {"city": "Gorst", "density": 160.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.103, 47.3325]}, "properties": {"city": "Lake Morton-Berrydale", "density": 351.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.4223, 48.7402]}, "properties": {"city": "Ione", "density": 330.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.24, 47.8056]}, "properties": {"city": "Bothell West", "density": 1918.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7773, 47.355]}, "properties": {"city": "Vaughn", "density": 57.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0181, 47.313]}, "properties": {"city": "Black Diamond", "density": 262.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5893, 48.8525]}, "properties": {"city": "Ferndale", "density": 775.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2293, 48.7146]}, "properties": {"city": "Acme", "density": 11.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.348, 47.5086]}, "properties": {"city": "White Center", "density": 2614.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.6485, 47.6187]}, "properties": {"city": "Fairchild AFB", "density": 172.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2507, 47.2611]}, "properties": {"city": "Pacific", "density": 1149.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.2926, 46.0228]}, "properties": {"city": "Glenwood", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3394, 46.9032]}, "properties": {"city": "Colfax", "density": 299.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2459, 48.2018]}, "properties": {"city": "Silvana", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.3904, 47.9526]}, "properties": {"city": "Forks", "density": 359.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2041, 47.6997]}, "properties": {"city": "Kirkland", "density": 1921.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.5784, 48.3671]}, "properties": {"city": "Okanogan", "density": 493.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3342, 48.1387]}, "properties": {"city": "Kayak Point", "density": 119.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0284, 47.6788]}, "properties": {"city": "Union Hill-Novelty Hill", "density": 352.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.0923, 46.3311]}, "properties": {"city": "Outlook", "density": 893.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1696, 47.2305]}, "properties": {"city": "Lake Tapps", "density": 409.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1054, 46.8656]}, "properties": {"city": "Cohassett Beach", "density": 211.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.4412, 48.7077]}, "properties": {"city": "Tonasket", "density": 459.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.0058, 46.3159]}, "properties": {"city": "Sunnyside", "density": 843.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.5043, 47.3755]}, "properties": {"city": "Lakeview", "density": 301.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0536, 47.0805]}, "properties": {"city": "Carbonado", "density": 649.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1669, 46.734]}, "properties": {"city": "Pullman", "density": 1206.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9897, 48.013]}, "properties": {"city": "Blyn", "density": 1.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9249, 47.6689]}, "properties": {"city": "Brinnon", "density": 28.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0655, 48.5324]}, "properties": {"city": "Friday Harbor", "density": 430.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5305, 45.6888]}, "properties": {"city": "Orchards", "density": 1521.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6114, 47.3938]}, "properties": {"city": "Purdy", "density": 178.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0732, 47.2247]}, "properties": {"city": "Tekoa", "density": 254.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6921, 47.4813]}, "properties": {"city": "Tanner", "density": 91.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6709, 47.0656]}, "properties": {"city": "Thorp", "density": 93.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.639, 47.3024]}, "properties": {"city": "Artondale", "density": 372.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1032, 47.6687]}, "properties": {"city": "Liberty Lake", "density": 621.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5789, 47.4901]}, "properties": {"city": "Cheney", "density": 1106.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0419, 47.6019]}, "properties": {"city": "Sammamish", "density": 1219.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.9553, 47.1946]}, "properties": {"city": "Cle Elum", "density": 179.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3963, 45.637]}, "properties": {"city": "Fern Prairie", "density": 200.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2312, 47.338]}, "properties": {"city": "Waverly", "density": 101.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5299, 47.1628]}, "properties": {"city": "Lakewood", "density": 1362.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9152, 47.6926]}, "properties": {"city": "Lake Marcel-Stillwater", "density": 368.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.7018, 46.9705]}, "properties": {"city": "Central Park", "density": 300.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.6737, 48.0063]}, "properties": {"city": "Bridgeport", "density": 961.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6686, 47.3293]}, "properties": {"city": "Raft Island", "density": 475.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2527, 47.8429]}, "properties": {"city": "Larch Way", "density": 1428.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9812, 48.0532]}, "properties": {"city": "Lake Bosworth", "density": 74.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6114, 45.7136]}, "properties": {"city": "Barberton", "density": 600.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.3012, 48.2637]}, "properties": {"city": "Sekiu", "density": 150.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2734, 47.7922]}, "properties": {"city": "Brier", "density": 1206.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.433, 47.6671]}, "properties": {"city": "Spokane", "density": 1218.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9134, 47.5717]}, "properties": {"city": "Fall City", "density": 258.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1303, 46.2506]}, "properties": {"city": "Pasco", "density": 832.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0634, 48.523]}, "properties": {"city": "Lyman", "density": 269.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.2333, 46.8403]}, "properties": {"city": "Oakville", "density": 498.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4153, 47.3131]}, "properties": {"city": "Dash Point", "density": 613.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8348, 47.2645]}, "properties": {"city": "Herron Island", "density": 55.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.063, 46.4023]}, "properties": {"city": "West Clarkston-Highland", "density": 816.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.6142, 46.9]}, "properties": {"city": "Royal City", "density": 639.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4721, 45.786]}, "properties": {"city": "Venersborg", "density": 149.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9023, 46.9863]}, "properties": {"city": "Hoquiam", "density": 344.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.6648, 46.6755]}, "properties": {"city": "Willapa", "density": 283.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9058, 47.8369]}, "properties": {"city": "Quilcene", "density": 18.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5358, 48.0185]}, "properties": {"city": "Freeland", "density": 168.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.2723, 47.0584]}, "properties": {"city": "McCleary", "density": 318.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.2161, 45.7476]}, "properties": {"city": "Roosevelt", "density": 10.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.0556, 46.3559]}, "properties": {"city": "Long Beach", "density": 395.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.5291, 45.9976]}, "properties": {"city": "Trout Lake", "density": 36.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9761, 47.9688]}, "properties": {"city": "Coulee Dam", "density": 576.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7136, 48.2829]}, "properties": {"city": "Swede Heaven", "density": 55.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6834, 48.6502]}, "properties": {"city": "Pine Grove", "density": 87.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4084, 48.0337]}, "properties": {"city": "Langley", "density": 430.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2321, 48.5113]}, "properties": {"city": "Sedro-Woolley", "density": 1057.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.8277, 46.7735]}, "properties": {"city": "Hatton", "density": 104.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.2175, 47.678]}, "properties": {"city": "Entiat", "density": 216.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5434, 47.6439]}, "properties": {"city": "Bainbridge Island", "density": 342.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.211, 47.3041]}, "properties": {"city": "Auburn", "density": 1057.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.162, 47.0763]}, "properties": {"city": "Ocean City", "density": 19.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.7382, 46.3769]}, "properties": {"city": "White Swan", "density": 140.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7103, 45.7138]}, "properties": {"city": "Felida", "density": 1077.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7934, 47.0458]}, "properties": {"city": "Lacey", "density": 1132.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6927, 47.8566]}, "properties": {"city": "Gold Bar", "density": 824.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.218, 47.6303]}, "properties": {"city": "Clyde Hill", "density": 1213.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3247, 46.2403]}, "properties": {"city": "Upper Elochoman", "density": 18.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.0601, 48.6642]}, "properties": {"city": "Marcus", "density": 316.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9831, 48.1157]}, "properties": {"city": "Canyon Creek", "density": 209.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.3035, 46.9779]}, "properties": {"city": "Malone", "density": 23.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1844, 47.8064]}, "properties": {"city": "Bothell East", "density": 1931.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2179, 47.6452]}, "properties": {"city": "Yarrow Point", "density": 1190.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3502, 47.0075]}, "properties": {"city": "Steptoe", "density": 31.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.3997, 46.3115]}, "properties": {"city": "West Richland", "density": 254.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.2059, 48.8637]}, "properties": {"city": "Orient", "density": 117.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3359, 47.309]}, "properties": {"city": "Federal Way", "density": 1679.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.2982, 46.5712]}, "properties": {"city": "Pe Ell", "density": 426.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.6984, 46.3521]}, "properties": {"city": "Deep River", "density": 4.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2044, 47.7736]}, "properties": {"city": "Bothell", "density": 1287.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.2829, 47.3886]}, "properties": {"city": "South Wenatchee", "density": 683.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.4109, 47.4014]}, "properties": {"city": "Snoqualmie Pass", "density": 31.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1081, 47.6895]}, "properties": {"city": "Hartline", "density": 178.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2878, 47.7597]}, "properties": {"city": "Lake Forest Park", "density": 1469.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1225, 47.0843]}, "properties": {"city": "Crocker", "density": 71.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1946, 46.7653]}, "properties": {"city": "Elbe", "density": 356.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9692, 47.3563]}, "properties": {"city": "Ravensdale", "density": 136.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1446, 48.1697]}, "properties": {"city": "Arlington", "density": 756.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4924, 47.1328]}, "properties": {"city": "McChord AFB", "density": 216.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.5509, 46.5641]}, "properties": {"city": "Lebam", "density": 26.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4544, 48.9503]}, "properties": {"city": "Lynden", "density": 1019.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6775, 48.2172]}, "properties": {"city": "Coupeville", "density": 586.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3126, 46.2988]}, "properties": {"city": "Prescott", "density": 292.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.8008, 46.668]}, "properties": {"city": "South Bend", "density": 394.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9938, 46.2115]}, "properties": {"city": "Mabton", "density": 1064.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7738, 47.4898]}, "properties": {"city": "North Bend", "density": 598.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8413, 47.3316]}, "properties": {"city": "Grapeview", "density": 103.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1395, 47.0103]}, "properties": {"city": "Garfield", "density": 252.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0722, 46.8288]}, "properties": {"city": "Rochester", "density": 484.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.9769, 46.3168]}, "properties": {"city": "Dayton", "density": 658.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2504, 47.2822]}, "properties": {"city": "Algona", "density": 933.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1859, 46.7193]}, "properties": {"city": "Mineral", "density": 111.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.1329, 48.7783]}, "properties": {"city": "Barstow", "density": 82.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.2025, 47.2348]}, "properties": {"city": "Moclips", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4687, 48.7543]}, "properties": {"city": "Bellingham", "density": 1241.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9569, 46.4045]}, "properties": {"city": "Vader", "density": 266.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2762, 47.8533]}, "properties": {"city": "North Lynnwood", "density": 2549.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6533, 47.6095]}, "properties": {"city": "Tracyton", "density": 1195.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.4853, 45.7282]}, "properties": {"city": "White Salmon", "density": 657.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1406, 48.9477]}, "properties": {"city": "Peaceful Valley", "density": 95.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0854, 47.4718]}, "properties": {"city": "East Renton Highlands", "density": 393.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.3899, 48.854]}, "properties": {"city": "Metaline", "density": 227.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2313, 48.8381]}, "properties": {"city": "Deming", "density": 17.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.6893, 47.5712]}, "properties": {"city": "Medical Lake", "density": 567.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.8095, 46.9757]}, "properties": {"city": "Aberdeen", "density": 585.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9647, 46.6637]}, "properties": {"city": "Chehalis", "density": 500.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7112, 46.5783]}, "properties": {"city": "Onalaska", "density": 171.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8496, 46.4414]}, "properties": {"city": "Toledo", "density": 738.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.351, 48.9123]}, "properties": {"city": "Everson", "density": 797.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8487, 47.4495]}, "properties": {"city": "Belfair", "density": 212.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3035, 47.8285]}, "properties": {"city": "Lynnwood", "density": 1890.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.1285, 46.5675]}, "properties": {"city": "Colton", "density": 292.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.4731, 47.2296]}, "properties": {"city": "Malden", "density": 122.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9664, 47.6346]}, "properties": {"city": "Ames Lake", "density": 368.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1176, 47.423]}, "properties": {"city": "Wilson Creek", "density": 85.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6029, 46.9647]}, "properties": {"city": "North Yelm", "density": 334.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9936, 47.4616]}, "properties": {"city": "Mirrormont", "density": 136.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6616, 48.3387]}, "properties": {"city": "Whidbey Island Station", "density": 113.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.4221, 47.7259]}, "properties": {"city": "Town and Country", "density": 1607.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9839, 46.709]}, "properties": {"city": "Tokeland", "density": 248.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8412, 47.5293]}, "properties": {"city": "Snoqualmie", "density": 725.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7121, 47.5436]}, "properties": {"city": "Bremerton", "density": 556.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.358, 46.1968]}, "properties": {"city": "East Cathlamet", "density": 113.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5672, 47.0955]}, "properties": {"city": "Fort Lewis", "density": 525.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6632, 45.7099]}, "properties": {"city": "Salmon Creek", "density": 1281.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.3586, 47.7103]}, "properties": {"city": "Skykomish", "density": 267.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4531, 47.2431]}, "properties": {"city": "Tacoma", "density": 1657.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9853, 47.8596]}, "properties": {"city": "Monroe", "density": 1183.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.6398, 48.8235]}, "properties": {"city": "Loomis", "density": 52.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5967, 46.4737]}, "properties": {"city": "Pomeroy", "density": 303.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5483, 45.725]}, "properties": {"city": "Brush Prairie", "density": 139.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1497, 47.8635]}, "properties": {"city": "Silver Firs", "density": 1292.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9722, 48.167]}, "properties": {"city": "Nespelem", "density": 622.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3156, 47.8583]}, "properties": {"city": "Meadowdale", "density": 1119.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0764, 47.4017]}, "properties": {"city": "Shadow Lake", "density": 197.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.9013, 46.8021]}, "properties": {"city": "LaCrosse", "density": 146.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7512, 48.5371]}, "properties": {"city": "Concrete", "density": 241.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2695, 48.9956]}, "properties": {"city": "Sumas", "density": 384.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.51, 47.2982]}, "properties": {"city": "Ruston", "density": 1202.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6764, 45.8589]}, "properties": {"city": "La Center", "density": 513.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6933, 48.0568]}, "properties": {"city": "Marrowstone", "density": 66.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9882, 47.7995]}, "properties": {"city": "Chelan Falls", "density": 118.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0122, 48.3225]}, "properties": {"city": "Lake Cavanaugh", "density": 89.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.647, 45.7838]}, "properties": {"city": "Duluth", "density": 89.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0944, 48.0747]}, "properties": {"city": "Sequim", "density": 432.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.4814, 46.2624]}, "properties": {"city": "Benton City", "density": 533.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.2741, 47.1284]}, "properties": {"city": "Moses Lake", "density": 499.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5157, 47.2308]}, "properties": {"city": "Fircrest", "density": 1665.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1651, 46.8222]}, "properties": {"city": "Othello", "density": 790.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2265, 47.5625]}, "properties": {"city": "Mercer Island", "density": 1528.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9897, 47.2017]}, "properties": {"city": "Enumclaw", "density": 878.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7198, 47.6234]}, "properties": {"city": "Chico", "density": 444.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6058, 47.5885]}, "properties": {"city": "Enetai", "density": 838.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.229, 47.642]}, "properties": {"city": "Hunts Point", "density": 545.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3577, 47.9731]}, "properties": {"city": "Clinton", "density": 309.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3201, 48.013]}, "properties": {"city": "Hat Island", "density": 26.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5566, 48.7932]}, "properties": {"city": "Marietta-Alderwood", "density": 294.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.151, 46.2696]}, "properties": {"city": "Waitsburg", "density": 388.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3493, 47.4913]}, "properties": {"city": "Sunnyslope", "density": 175.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.9525, 47.1862]}, "properties": {"city": "South Cle Elum", "density": 544.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.5389, 46.6482]}, "properties": {"city": "Selah", "density": 662.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6165, 46.9704]}, "properties": {"city": "Lind", "density": 193.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5738, 45.6883]}, "properties": {"city": "Five Corners", "density": 1221.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.179, 48.4716]}, "properties": {"city": "Winthrop", "density": 182.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3436, 47.4344]}, "properties": {"city": "Normandy Park", "density": 1027.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7744, 47.279]}, "properties": {"city": "Home", "density": 83.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2757, 47.0407]}, "properties": {"city": "Graham", "density": 297.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4059, 45.865]}, "properties": {"city": "Yacolt", "density": 1210.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6702, 46.6085]}, "properties": {"city": "Packwood", "density": 89.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.3385, 47.5314]}, "properties": {"city": "Queets", "density": 55.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0446, 46.1697]}, "properties": {"city": "Finley", "density": 201.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.7796, 48.9159]}, "properties": {"city": "Northport", "density": 202.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2338, 47.6256]}, "properties": {"city": "Medina", "density": 879.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4369, 47.3036]}, "properties": {"city": "Browns Point", "density": 1076.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3628, 47.1694]}, "properties": {"city": "Summit", "density": 597.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2511, 46.7916]}, "properties": {"city": "Albion", "density": 598.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3468, 48.7199]}, "properties": {"city": "Sudden Valley", "density": 470.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0943, 47.9618]}, "properties": {"city": "Bunk Foss", "density": 359.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6025, 46.6594]}, "properties": {"city": "Gleed", "density": 176.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.807, 45.6928]}, "properties": {"city": "Maryhill", "density": 10.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1632, 47.0233]}, "properties": {"city": "Oyehut", "density": 0.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2667, 46.7872]}, "properties": {"city": "Alder", "density": 15.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7052, 45.8116]}, "properties": {"city": "Ridgefield", "density": 422.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1638, 48.24]}, "properties": {"city": "Bryant", "density": 125.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6728, 47.0178]}, "properties": {"city": "Nisqually Indian Community", "density": 94.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.2736, 46.9489]}, "properties": {"city": "Porter", "density": 9.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.062, 48.6055]}, "properties": {"city": "Kettle Falls", "density": 585.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.027, 47.8417]}, "properties": {"city": "Chelan", "density": 246.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.431, 48.5602]}, "properties": {"city": "Edison", "density": 104.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.8782, 47.4031]}, "properties": {"city": "Neilton", "density": 14.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6255, 47.2475]}, "properties": {"city": "Fox Island", "density": 286.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1905, 47.4758]}, "properties": {"city": "Renton", "density": 1669.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.4803, 47.7661]}, "properties": {"city": "Baring", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.996, 47.412]}, "properties": {"city": "Hobart", "density": 138.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.9056, 46.0847]}, "properties": {"city": "Wallula", "density": 1212.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.6724, 46.0412]}, "properties": {"city": "Touchet", "density": 138.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6633, 47.5948]}, "properties": {"city": "Leavenworth", "density": 580.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1341, 47.9391]}, "properties": {"city": "Fobes Hill", "density": 230.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.2552, 47.4802]}, "properties": {"city": "Harrington", "density": 404.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.661, 47.5164]}, "properties": {"city": "Port Orchard", "density": 559.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2676, 47.8147]}, "properties": {"city": "Alderwood Manor", "density": 1711.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.1732, 46.1979]}, "properties": {"city": "Kennewick", "density": 1147.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6109, 46.5583]}, "properties": {"city": "Ahtanum", "density": 171.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7691, 47.4463]}, "properties": {"city": "Wilderness Rim", "density": 895.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0515, 47.9917]}, "properties": {"city": "Machias", "density": 139.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.4704, 45.7139]}, "properties": {"city": "Bingen", "density": 452.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.5891, 47.5582]}, "properties": {"city": "Four Lakes", "density": 46.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.7466, 48.0572]}, "properties": {"city": "Springdale", "density": 100.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3106, 46.7538]}, "properties": {"city": "Washtucna", "density": 120.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1044, 47.3668]}, "properties": {"city": "Covington", "density": 1363.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8386, 47.3863]}, "properties": {"city": "Allyn", "density": 640.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8251, 47.8827]}, "properties": {"city": "North Sultan", "density": 157.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4977, 46.5567]}, "properties": {"city": "Union Gap", "density": 426.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.1127, 47.2188]}, "properties": {"city": "Shelton", "density": 675.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9425, 46.2755]}, "properties": {"city": "Chinook", "density": 114.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7414, 48.9839]}, "properties": {"city": "Blaine", "density": 365.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.789, 48.0316]}, "properties": {"city": "Port Hadlock-Irondale", "density": 217.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6377, 47.4166]}, "properties": {"city": "Burley", "density": 157.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3116, 48.4202]}, "properties": {"city": "Mount Vernon", "density": 1099.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0475, 47.922]}, "properties": {"city": "Electric City", "density": 189.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.0335, 47.2338]}, "properties": {"city": "Ronald", "density": 118.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.0244, 48.1715]}, "properties": {"city": "Nespelem Community", "density": 4.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9194, 47.4532]}, "properties": {"city": "Amanda Park", "density": 5.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0831, 46.3876]}, "properties": {"city": "Clarkston Heights-Vineland", "density": 413.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3159, 47.5135]}, "properties": {"city": "Boulevard Park", "density": 1264.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2422, 48.3933]}, "properties": {"city": "Big Lake", "density": 182.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.1321, 48.7245]}, "properties": {"city": "Boyds", "density": 150.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5986, 47.5266]}, "properties": {"city": "Parkwood", "density": 972.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.4397, 47.9642]}, "properties": {"city": "Deer Park", "density": 229.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9348, 47.2491]}, "properties": {"city": "Humptulips", "density": 11.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2986, 47.4445]}, "properties": {"city": "SeaTac", "density": 1126.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0368, 47.3659]}, "properties": {"city": "Maple Valley", "density": 1656.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.2806, 47.6856]}, "properties": {"city": "Millwood", "density": 989.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0576, 48.9883]}, "properties": {"city": "Point Roberts", "density": 98.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.148, 46.1392]}, "properties": {"city": "Dixie", "density": 182.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6183, 47.5193]}, "properties": {"city": "East Port Orchard", "density": 1338.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2815, 46.5575]}, "properties": {"city": "Morton", "density": 544.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3369, 48.6176]}, "properties": {"city": "Alger", "density": 34.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.5556, 46.6437]}, "properties": {"city": "Kahlotus", "density": 182.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3457, 47.2593]}, "properties": {"city": "Fife Heights", "density": 753.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5764, 45.8021]}, "properties": {"city": "Cherry Grove", "density": 112.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1452, 47.8292]}, "properties": {"city": "Clearview", "density": 325.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.632, 47.1465]}, "properties": {"city": "Greenwater", "density": 7.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3079, 47.8745]}, "properties": {"city": "Picnic Point", "density": 1157.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1383, 48.9168]}, "properties": {"city": "Kendall", "density": 17.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.8219, 47.6437]}, "properties": {"city": "Seabeck", "density": 132.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7495, 47.8671]}, "properties": {"city": "Startup", "density": 64.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5935, 47.1699]}, "properties": {"city": "Steilacoom", "density": 1215.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.9049, 47.2007]}, "properties": {"city": "Lamont", "density": 98.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4628, 48.487]}, "properties": {"city": "Bay View", "density": 92.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.7244, 48.173]}, "properties": {"city": "Valley", "density": 154.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1459, 48.1111]}, "properties": {"city": "North Marysville", "density": 51.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.5321, 47.0064]}, "properties": {"city": "Brady", "density": 22.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.6743, 47.8555]}, "properties": {"city": "May Creek", "density": 396.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2378, 48.4561]}, "properties": {"city": "Clear Lake", "density": 201.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.7164, 48.0852]}, "properties": {"city": "Keller", "density": 9.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1169, 47.6762]}, "properties": {"city": "Redmond", "density": 1498.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.0422, 46.3108]}, "properties": {"city": "Ilwaco", "density": 176.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3076, 47.7921]}, "properties": {"city": "Mountlake Terrace", "density": 2021.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.4668, 46.5022]}, "properties": {"city": "Parker", "density": 194.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.4892, 47.3914]}, "properties": {"city": "Soap Lake", "density": 403.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.0119, 46.8053]}, "properties": {"city": "Grand Mound", "density": 306.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5517, 47.5519]}, "properties": {"city": "Manchester", "density": 719.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1048, 47.1529]}, "properties": {"city": "Prairie Heights", "density": 476.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.7679, 48.1002]}, "properties": {"city": "Verlot", "density": 16.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.4459, 48.444]}, "properties": {"city": "North Omak", "density": 23.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6945, 46.7274]}, "properties": {"city": "Naches", "density": 472.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.6514, 46.598]}, "properties": {"city": "Summitview", "density": 180.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3827, 47.1404]}, "properties": {"city": "Clover Creek", "density": 407.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.7662, 46.2066]}, "properties": {"city": "Prosser", "density": 529.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.8013, 46.365]}, "properties": {"city": "Naselle", "density": 63.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.1382, 47.3743]}, "properties": {"city": "Rock Island", "density": 478.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.8233, 45.8192]}, "properties": {"city": "Goldendale", "density": 454.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1044, 47.8027]}, "properties": {"city": "Maltby", "density": 228.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6331, 48.2965]}, "properties": {"city": "Oak Harbor", "density": 930.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.781, 47.0512]}, "properties": {"city": "Tanglewilde", "density": 1658.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.6867, 46.9282]}, "properties": {"city": "Endicott", "density": 397.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1877, 47.8361]}, "properties": {"city": "Mill Creek East", "density": 1774.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.0973, 47.4441]}, "properties": {"city": "Maple Heights-Lake Desire", "density": 324.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.412, 47.1734]}, "properties": {"city": "Midland", "density": 1117.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.4989, 47.8014]}, "properties": {"city": "Kingston", "density": 387.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3469, 47.7937]}, "properties": {"city": "Esperance", "density": 2100.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.589, 46.3608]}, "properties": {"city": "Grays River", "density": 8.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8044, 47.8708]}, "properties": {"city": "Sultan", "density": 623.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.2962, 46.8199]}, "properties": {"city": "La Grande", "density": 10.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3808, 47.1254]}, "properties": {"city": "Ritzville", "density": 363.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.6954, 48.2919]}, "properties": {"city": "Malott", "density": 124.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.2892, 47.612]}, "properties": {"city": "Coulee City", "density": 240.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.738, 46.6838]}, "properties": {"city": "Raymond", "density": 275.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5769, 47.2828]}, "properties": {"city": "Wollochet", "density": 414.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.8522, 47.2347]}, "properties": {"city": "Quincy", "density": 483.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3024, 46.0517]}, "properties": {"city": "Walla Walla East", "density": 558.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.9834, 48.5282]}, "properties": {"city": "Hamilton", "density": 105.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1477, 47.7569]}, "properties": {"city": "Woodinville", "density": 824.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.9533, 46.6211]}, "properties": {"city": "Bay Center", "density": 88.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.7872, 48.122]}, "properties": {"city": "Port Townsend", "density": 530.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.5102, 48.5047]}, "properties": {"city": "Riverside", "density": 118.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3851, 47.7903]}, "properties": {"city": "Woodway", "density": 454.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.1903, 47.2502]}, "properties": {"city": "Qui-nai-elt Village", "density": 41.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-121.8932, 45.6944]}, "properties": {"city": "Stevenson", "density": 364.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-124.0443, 46.4961]}, "properties": {"city": "Ocean Park", "density": 227.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.4565, 48.1142]}, "properties": {"city": "Port Angeles", "density": 716.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1698, 47.1789]}, "properties": {"city": "Bonney Lake", "density": 984.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.4304, 48.942]}, "properties": {"city": "Oroville", "density": 382.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3876, 46.0633]}, "properties": {"city": "Garrett", "density": 330.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-117.0854, 47.703]}, "properties": {"city": "Otis Orchards-East Farms", "density": 306.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3442, 48.1649]}, "properties": {"city": "Warm Beach", "density": 236.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.359, 48.2622]}, "properties": {"city": "Twin Lakes", "density": 9.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3444, 48.3357]}, "properties": {"city": "Conway", "density": 18.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.3879, 46.0418]}, "properties": {"city": "College Place", "density": 1199.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1817, 47.8968]}, "properties": {"city": "Eastmont", "density": 1665.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1048, 48.9135]}, "properties": {"city": "Maple Falls", "density": 11.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.5344, 47.3119]}, "properties": {"city": "Ephrata", "density": 298.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.3176, 46.4299]}, "properties": {"city": "Buena", "density": 763.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.9173, 46.989]}, "properties": {"city": "Tumwater", "density": 509.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-119.9311, 46.6873]}, "properties": {"city": "Desert Aire", "density": 326.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.1335, 47.2986]}, "properties": {"city": "Lake Holm", "density": 150.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.3035, 47.9094]}, "properties": {"city": "Mukilteo", "density": 1323.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.1169, 48.3618]}, "properties": {"city": "Twisp", "density": 314.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6911, 45.6911]}, "properties": {"city": "Lake Shore", "density": 1783.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6232, 47.6993]}, "properties": {"city": "Keyport", "density": 348.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.096, 48.0029]}, "properties": {"city": "Lake Stevens", "density": 1424.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-118.1245, 46.5182]}, "properties": {"city": "Starbuck", "density": 237.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-120.5422, 46.4071]}, "properties": {"city": "Harrah", "density": 1131.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.5579, 47.8978]}, "properties": {"city": "Hansville", "density": 138.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-123.1697, 48.0842]}, "properties": {"city": "Carlsborg", "density": 277.0}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-122.6261, 46.9398]}, "properties": {"city": "Yelm", "density": 618.0}}]} \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 00000000..9407d37d --- /dev/null +++ b/test.py @@ -0,0 +1,144 @@ +import dash_leaflet as dl +from dash_extensions.javascript import assign +from dash import Dash + +# External scripts +chroma = "https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js" +turf = "https://cdnjs.cloudflare.com/ajax/libs/Turf.js/6.5.0/turf.min.js" + +# Color scale and properties +colorscale = ['red', 'yellow', 'green', 'blue', 'purple'] +color_prop = 'density' +vmin = 0 +vmax = 1000 + +# Create a colorbar +colorbar = dl.Colorbar( + colorscale=colorscale, + width=20, + height=150, + min=vmin, + max=vmax, + unit='/km2' +) + +# JavaScript functions +on_each_feature = assign("""function(feature, layer, context){ + if(feature.properties.city){ + layer.bindTooltip(`${feature.properties.city} (${feature.properties.density})`) + } +}""") + +point_to_layer = assign("""function(feature, latlng, context){ + const {min, max, colorscale, circleOptions, colorProp} = context.hideout; + const csc = chroma.scale(colorscale).domain([min, max]); + circleOptions.fillColor = csc(feature.properties[colorProp]); + return L.circleMarker(latlng, circleOptions); +}""") + +cluster_to_layer = assign("""function(feature, latlng, index, context){ + const {min, max, colorscale, circleOptions, colorProp} = context.hideout; + const csc = chroma.scale(colorscale).domain([min, max]); + + // Access the leaves of the cluster + const leaves = index.getLeaves(feature.properties.cluster_id); + + // Collect coordinates and sum property values + let features = []; + let valueSum = 0; + + for (let i = 0; i < leaves.length; ++i) { + const coords = leaves[i].geometry.coordinates; + const lng = coords[0]; + const lat = coords[1]; + + // Create a Turf.js point feature + features.push(turf.point([lng, lat])); + + // Sum the property values + valueSum += leaves[i].properties[colorProp]; + } + + // Calculate the mean value for color scaling + const valueMean = valueSum / leaves.length; + + // Create a Turf.js feature collection + const fc = turf.featureCollection(features); + + // Compute the convex hull + const convexHull = turf.convex(fc); + + // Create a Leaflet layer for the convex hull polygon + let polygonLayer = null; + if (convexHull) { + polygonLayer = L.geoJSON(convexHull, { + style: { + color: csc(valueMean), + weight: 1, + fillOpacity: 0.2 + } + }); + } else { + // If convex hull couldn't be computed (e.g., less than 3 points), create a circle + polygonLayer = L.circle(latlng, { + radius: 50000, // Adjust radius as needed + color: csc(valueMean), + weight: 1, + fillOpacity: 0.2 + }); + } + + // Customize the cluster icon + const scatterIcon = L.DivIcon.extend({ + createIcon: function(oldIcon) { + let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon); + icon.style.backgroundColor = this.options.color; + return icon; + } + }); + + const icon = new scatterIcon({ + html: '
' + feature.properties.point_count_abbreviated + '
', + className: "marker-cluster", + iconSize: L.point(40, 40), + color: csc(valueMean) + }); + + const marker = L.marker(latlng, {icon: icon}); + + // Create a layer group containing the polygon and the marker + const clusterLayer = L.layerGroup([polygonLayer, marker]); + + return clusterLayer; +}""") + +# Create the GeoJSON layer +geojson = dl.GeoJSON( + url="/assets/us-cities.json", + cluster=True, + zoomToBounds=True, + pointToLayer=point_to_layer, + onEachFeature=on_each_feature, + clusterToLayer=cluster_to_layer, + zoomToBoundsOnClick=True, + superClusterOptions=dict(radius=150), + hideout=dict( + colorProp=color_prop, + circleOptions=dict(fillOpacity=1, stroke=False, radius=5), + min=vmin, + max=vmax, + colorscale=colorscale + ) +) + +# Create the app +app = Dash(external_scripts=[chroma, turf], prevent_initial_callbacks=True) +app.layout = dl.Map( + [dl.TileLayer(), geojson, colorbar], + center=[37.0902, -95.7129], # Center of the US + zoom=4, + style={'height': '100vh'} +) + +if __name__ == '__main__': + app.run_server() From 8005f691547a5ee5b1a28dc6271bcad99d1f4d2e Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:41:14 -0800 Subject: [PATCH 02/20] Remove the dumb ass circles, but still kinda weird --- app.py | 3 ++ assets/dashExtensions_default.js | 90 ++++++++------------------------ pages/lease_page.py | 64 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 68 deletions(-) diff --git a/app.py b/app.py index b96721db..87fb2d60 100755 --- a/app.py +++ b/app.py @@ -22,6 +22,9 @@ meta_tags = [ {"name": "viewport", "content": "width=device-width, initial-scale=1"} ], + external_scripts=[ + 'https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js' + ] ) # Set the page title diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index c364e69e..3c8173d7 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -1,104 +1,58 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { default: { - function0: function(feature, layer, context) { - if (feature.properties.city) { - layer.bindTooltip(`${feature.properties.city} (${feature.properties.density})`) - } - }, - function1: function(feature, latlng, context) { - const { - min, - max, - colorscale, - circleOptions, - colorProp - } = context.hideout; - const csc = chroma.scale(colorscale).domain([min, max]); - circleOptions.fillColor = csc(feature.properties[colorProp]); - return L.circleMarker(latlng, circleOptions); - }, - function2: function(feature, latlng, index, context) { - const { - min, - max, - colorscale, - circleOptions, - colorProp - } = context.hideout; - const csc = chroma.scale(colorscale).domain([min, max]); - + function0: function(feature, latlng, index, context) { // Access the leaves of the cluster const leaves = index.getLeaves(feature.properties.cluster_id); - // Collect coordinates and sum property values + // Collect coordinates for the cluster's children let features = []; - let valueSum = 0; - for (let i = 0; i < leaves.length; ++i) { const coords = leaves[i].geometry.coordinates; const lng = coords[0]; const lat = coords[1]; - - // Create a Turf.js point feature features.push(turf.point([lng, lat])); - - // Sum the property values - valueSum += leaves[i].properties[colorProp]; } - // Calculate the mean value for color scaling - const valueMean = valueSum / leaves.length; - // Create a Turf.js feature collection const fc = turf.featureCollection(features); // Compute the convex hull const convexHull = turf.convex(fc); - // Create a Leaflet layer for the convex hull polygon + // If the convex hull exists, render it as a polygon let polygonLayer = null; if (convexHull) { polygonLayer = L.geoJSON(convexHull, { style: { - color: csc(valueMean), - weight: 1, - fillOpacity: 0.2 + color: '#3388ff', // Border color (default Leaflet cluster color) + weight: 2, // Border thickness + fillOpacity: 0.2, // Polygon fill transparency + fillColor: '#3388ff' // Polygon fill color } }); - } else { - // If convex hull couldn't be computed (e.g., less than 3 points), create a circle - polygonLayer = L.circle(latlng, { - radius: 50000, // Adjust radius as needed - color: csc(valueMean), - weight: 1, - fillOpacity: 0.2 - }); } - // Customize the cluster icon - const scatterIcon = L.DivIcon.extend({ - createIcon: function(oldIcon) { - let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon); - icon.style.backgroundColor = this.options.color; - return icon; - } + // Create a custom marker for the cluster + const clusterMarker = L.marker(latlng, { + icon: L.divIcon({ + html: '
' + + feature.properties.point_count_abbreviated + '
', + className: 'cluster-marker', + iconSize: L.point(30, 30) + }) }); - const icon = new scatterIcon({ - html: '
' + feature.properties.point_count_abbreviated + '
', - className: "marker-cluster", - iconSize: L.point(40, 40), - color: csc(valueMean) + // Show the convex hull on hover + clusterMarker.on('mouseover', function() { + if (polygonLayer) polygonLayer.addTo(context.map); }); - const marker = L.marker(latlng, { - icon: icon + // Hide the convex hull when the hover ends + clusterMarker.on('mouseout', function() { + if (polygonLayer) context.map.removeLayer(polygonLayer); }); - // Create a layer group containing the polygon and the marker - const clusterLayer = L.layerGroup([polygonLayer, marker]); - - return clusterLayer; + return clusterMarker; } } }); \ No newline at end of file diff --git a/pages/lease_page.py b/pages/lease_page.py index bcab8918..d3dc49a9 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -14,6 +14,8 @@ import sys import time import uuid +from dash_extensions.javascript import assign + dash.register_page( __name__, @@ -234,6 +236,67 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # comparison_df.to_csv('missing_rows_comparison.csv', index=False) # logger.info(f"Comparison of missing rows has been saved to 'missing_rows_comparison.csv'") + # JavaScript function to render clusters with convex hull polygons + cluster_to_layer = assign("""function(feature, latlng, index, context){ + // Access the leaves of the cluster + const leaves = index.getLeaves(feature.properties.cluster_id); + + // 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, render it as a polygon + let polygonLayer = null; + if (convexHull) { + polygonLayer = L.geoJSON(convexHull, { + style: { + color: '#3388ff', // Border color (default Leaflet cluster color) + weight: 2, // Border thickness + fillOpacity: 0.2, // Polygon fill transparency + fillColor: '#3388ff' // Polygon fill color + } + }); + } + + // Create a custom marker for the cluster + const clusterMarker = L.marker(latlng, { + icon: L.divIcon({ + html: '
' + + feature.properties.point_count_abbreviated + '
', + className: 'cluster-marker', + iconSize: L.point(30, 30) + }) + }); + + // Show the convex hull on hover + clusterMarker.on('mouseover', function() { + if (polygonLayer) polygonLayer.addTo(context.map); + }); + + // Hide the convex hull when the hover ends + clusterMarker.on('mouseout', function() { + if (polygonLayer) context.map.removeLayer(polygonLayer); + }); + + return clusterMarker; +}""") + + + + + + ns = Namespace("dash_props", "module") # Generate the map return dl.GeoJSON( @@ -241,6 +304,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental #children=[dl.Popup(id='2popup')], data=geojson, cluster=True, + clusterToLayer=cluster_to_layer, zoomToBoundsOnClick=True, superClusterOptions={ # https://github.com/mapbox/supercluster#options 'radius': 160, From fa8b0c5a4bf2326c906b9fdd2a2df6b029103a39 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:43:15 -0800 Subject: [PATCH 03/20] Retrieve all leaves so that the polygon matches all boundaries/markers --- assets/dashExtensions_default.js | 6 +++--- pages/lease_page.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 3c8173d7..4b73add5 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -1,8 +1,8 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { default: { function0: function(feature, latlng, index, context) { - // Access the leaves of the cluster - const leaves = index.getLeaves(feature.properties.cluster_id); + // Access all the leaves of the cluster + const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children // Collect coordinates for the cluster's children let features = []; @@ -24,7 +24,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { if (convexHull) { polygonLayer = L.geoJSON(convexHull, { style: { - color: '#3388ff', // Border color (default Leaflet cluster color) + color: '#3388ff', // Border color weight: 2, // Border thickness fillOpacity: 0.2, // Polygon fill transparency fillColor: '#3388ff' // Polygon fill color diff --git a/pages/lease_page.py b/pages/lease_page.py index d3dc49a9..94b2713e 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -238,8 +238,8 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # JavaScript function to render clusters with convex hull polygons cluster_to_layer = assign("""function(feature, latlng, index, context){ - // Access the leaves of the cluster - const leaves = index.getLeaves(feature.properties.cluster_id); + // Access all the leaves of the cluster + const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children // Collect coordinates for the cluster's children let features = []; @@ -261,7 +261,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental if (convexHull) { polygonLayer = L.geoJSON(convexHull, { style: { - color: '#3388ff', // Border color (default Leaflet cluster color) + color: '#3388ff', // Border color weight: 2, // Border thickness fillOpacity: 0.2, // Polygon fill transparency fillColor: '#3388ff' // Polygon fill color @@ -297,6 +297,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental + ns = Namespace("dash_props", "module") # Generate the map return dl.GeoJSON( From 5ae082bf10563e82c6cb4a1e70258bea551e1a82 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:46:55 -0800 Subject: [PATCH 04/20] Remove the previous polygon so it doesn't clutter up the map --- assets/dashExtensions_default.js | 27 ++++++++++++++++++++++----- pages/lease_page.py | 27 ++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 4b73add5..cca4eac3 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -1,6 +1,11 @@ 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 @@ -19,7 +24,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { // Compute the convex hull const convexHull = turf.convex(fc); - // If the convex hull exists, render it as a polygon + // If the convex hull exists, create it as a polygon let polygonLayer = null; if (convexHull) { polygonLayer = L.geoJSON(convexHull, { @@ -42,14 +47,26 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { }) }); - // Show the convex hull on hover + // Add mouseover behavior to display the convex hull clusterMarker.on('mouseover', function() { - if (polygonLayer) polygonLayer.addTo(context.map); + // 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; + } }); - // Hide the convex hull when the hover ends + // Add behavior to remove the polygon when the cluster is clicked or no longer needed clusterMarker.on('mouseout', function() { - if (polygonLayer) context.map.removeLayer(polygonLayer); + if (context.currentPolygon) { + context.map.removeLayer(context.currentPolygon); + context.currentPolygon = null; + } }); return clusterMarker; diff --git a/pages/lease_page.py b/pages/lease_page.py index 94b2713e..845ae52f 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -238,6 +238,11 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # JavaScript function to render clusters with convex hull polygons cluster_to_layer = 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 @@ -256,7 +261,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental // Compute the convex hull const convexHull = turf.convex(fc); - // If the convex hull exists, render it as a polygon + // If the convex hull exists, create it as a polygon let polygonLayer = null; if (convexHull) { polygonLayer = L.geoJSON(convexHull, { @@ -279,14 +284,26 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental }) }); - // Show the convex hull on hover + // Add mouseover behavior to display the convex hull clusterMarker.on('mouseover', function() { - if (polygonLayer) polygonLayer.addTo(context.map); + // 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; + } }); - // Hide the convex hull when the hover ends + // Add behavior to remove the polygon when the cluster is clicked or no longer needed clusterMarker.on('mouseout', function() { - if (polygonLayer) context.map.removeLayer(polygonLayer); + if (context.currentPolygon) { + context.map.removeLayer(context.currentPolygon); + context.currentPolygon = null; + } }); return clusterMarker; From b10263e0d9b1ca3a00f78a873fb3eef5fe648c40 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:54:42 -0800 Subject: [PATCH 05/20] First attempt at cluster colors --- assets/dashExtensions_default.js | 18 ++++++++++++++---- pages/lease_page.py | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index cca4eac3..cf1aa762 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -8,6 +8,16 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { // 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 (simple gradient based on cluster size) + const getColor = function(size) { + if (size < 10) return '#00ff00'; // Green for small clusters + if (size < 50) return '#ffff00'; // Yellow for medium clusters + if (size < 100) return '#ffa500'; // Orange for larger clusters + return '#ff0000'; // Red for very large clusters + }; + const color = getColor(clusterSize); // Collect coordinates for the cluster's children let features = []; @@ -29,18 +39,18 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { if (convexHull) { polygonLayer = L.geoJSON(convexHull, { style: { - color: '#3388ff', // Border color + color: color, // Use the same color as the cluster icon weight: 2, // Border thickness fillOpacity: 0.2, // Polygon fill transparency - fillColor: '#3388ff' // Polygon fill color + fillColor: color // Polygon fill color } }); } - // Create a custom marker for the cluster + // Create a custom marker for the cluster with dynamic color const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: '
' + + html: '
' + feature.properties.point_count_abbreviated + '
', className: 'cluster-marker', iconSize: L.point(30, 30) diff --git a/pages/lease_page.py b/pages/lease_page.py index 845ae52f..761c58d0 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -245,6 +245,16 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental // 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 (simple gradient based on cluster size) + const getColor = function(size) { + if (size < 10) return '#00ff00'; // Green for small clusters + if (size < 50) return '#ffff00'; // Yellow for medium clusters + if (size < 100) return '#ffa500'; // Orange for larger clusters + return '#ff0000'; // Red for very large clusters + }; + const color = getColor(clusterSize); // Collect coordinates for the cluster's children let features = []; @@ -266,18 +276,18 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental if (convexHull) { polygonLayer = L.geoJSON(convexHull, { style: { - color: '#3388ff', // Border color + color: color, // Use the same color as the cluster icon weight: 2, // Border thickness fillOpacity: 0.2, // Polygon fill transparency - fillColor: '#3388ff' // Polygon fill color + fillColor: color // Polygon fill color } }); } - // Create a custom marker for the cluster + // Create a custom marker for the cluster with dynamic color const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: '
' + + html: '
' + feature.properties.point_count_abbreviated + '
', className: 'cluster-marker', iconSize: L.point(30, 30) From 6ecc7b050feb9b42684dbbb04a6ca37d4fb27a77 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 13:56:40 -0800 Subject: [PATCH 06/20] Refactor cluster color scale and marker customization for improved clarity and consistency --- assets/dashExtensions_default.js | 28 ++++++++++++++++++---------- pages/lease_page.py | 30 ++++++++++++++++++------------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index cf1aa762..0627eef8 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -10,13 +10,15 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children const clusterSize = leaves.length; - // Define a color scale (simple gradient based on cluster size) + // Define a color scale function (mimicking Leaflet.markercluster behavior) const getColor = function(size) { - if (size < 10) return '#00ff00'; // Green for small clusters - if (size < 50) return '#ffff00'; // Yellow for medium clusters - if (size < 100) return '#ffa500'; // Orange for larger clusters - return '#ff0000'; // Red for very large clusters + if (size < 10) return 'green'; // Small clusters + if (size < 50) return 'yellow'; // Medium clusters + if (size < 100) 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 @@ -50,10 +52,16 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { // Create a custom marker for the cluster with dynamic color const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: '
' + - feature.properties.point_count_abbreviated + '
', - className: 'cluster-marker', - iconSize: L.point(30, 30) + html: `
${feature.properties.point_count_abbreviated}
`, + className: 'marker-cluster', // Optional: add a class for further customization + iconSize: L.point(40, 40) // Adjust the icon size if needed }) }); @@ -71,7 +79,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { } }); - // Add behavior to remove the polygon when the cluster is clicked or no longer needed + // Add behavior to remove the polygon when the mouse leaves the cluster clusterMarker.on('mouseout', function() { if (context.currentPolygon) { context.map.removeLayer(context.currentPolygon); diff --git a/pages/lease_page.py b/pages/lease_page.py index 761c58d0..8c171271 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -236,7 +236,6 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # comparison_df.to_csv('missing_rows_comparison.csv', index=False) # logger.info(f"Comparison of missing rows has been saved to 'missing_rows_comparison.csv'") - # JavaScript function to render clusters with convex hull polygons cluster_to_layer = assign("""function(feature, latlng, index, context){ // A global reference to the currently displayed polygon if (!context.currentPolygon) { @@ -247,13 +246,15 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children const clusterSize = leaves.length; - // Define a color scale (simple gradient based on cluster size) + // Define a color scale function (mimicking Leaflet.markercluster behavior) const getColor = function(size) { - if (size < 10) return '#00ff00'; // Green for small clusters - if (size < 50) return '#ffff00'; // Yellow for medium clusters - if (size < 100) return '#ffa500'; // Orange for larger clusters - return '#ff0000'; // Red for very large clusters + if (size < 10) return 'green'; // Small clusters + if (size < 50) return 'yellow'; // Medium clusters + if (size < 100) 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 @@ -287,10 +288,16 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental // Create a custom marker for the cluster with dynamic color const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: '
' + - feature.properties.point_count_abbreviated + '
', - className: 'cluster-marker', - iconSize: L.point(30, 30) + html: `
${feature.properties.point_count_abbreviated}
`, + className: 'marker-cluster', // Optional: add a class for further customization + iconSize: L.point(40, 40) // Adjust the icon size if needed }) }); @@ -308,7 +315,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental } }); - // Add behavior to remove the polygon when the cluster is clicked or no longer needed + // Add behavior to remove the polygon when the mouse leaves the cluster clusterMarker.on('mouseout', function() { if (context.currentPolygon) { context.map.removeLayer(context.currentPolygon); @@ -324,7 +331,6 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental - ns = Namespace("dash_props", "module") # Generate the map return dl.GeoJSON( From 1bad28af655884ee38b8e4d52657e7834746ddcf Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 14:00:01 -0800 Subject: [PATCH 07/20] Make the cluster icon two concentric circles & change the marker count from white to black for better visibility --- assets/dashExtensions_default.js | 27 ++++++++++++++++----------- pages/lease_page.py | 27 ++++++++++++++++----------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 0627eef8..121f67d0 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -10,7 +10,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children const clusterSize = leaves.length; - // Define a color scale function (mimicking Leaflet.markercluster behavior) + // Define a color scale (mimicking Leaflet.markercluster behavior) const getColor = function(size) { if (size < 10) return 'green'; // Small clusters if (size < 50) return 'yellow'; // Medium clusters @@ -49,19 +49,24 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { }); } - // Create a custom marker for the cluster with dynamic color + // Create a custom marker for the cluster with dual circle style const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: `
${feature.properties.point_count_abbreviated}
`, + html: ` +
+
+
+ ${feature.properties.point_count_abbreviated} +
+
`, className: 'marker-cluster', // Optional: add a class for further customization - iconSize: L.point(40, 40) // Adjust the icon size if needed + iconSize: L.point(40, 40) // Adjust the icon size }) }); diff --git a/pages/lease_page.py b/pages/lease_page.py index 8c171271..1e68a1a7 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -246,7 +246,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental const leaves = index.getLeaves(feature.properties.cluster_id, Infinity); // Retrieve all children const clusterSize = leaves.length; - // Define a color scale function (mimicking Leaflet.markercluster behavior) + // Define a color scale (mimicking Leaflet.markercluster behavior) const getColor = function(size) { if (size < 10) return 'green'; // Small clusters if (size < 50) return 'yellow'; // Medium clusters @@ -285,19 +285,24 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental }); } - // Create a custom marker for the cluster with dynamic color + // Create a custom marker for the cluster with dual circle style const clusterMarker = L.marker(latlng, { icon: L.divIcon({ - html: `
${feature.properties.point_count_abbreviated}
`, + html: ` +
+
+
+ ${feature.properties.point_count_abbreviated} +
+
`, className: 'marker-cluster', // Optional: add a class for further customization - iconSize: L.point(40, 40) // Adjust the icon size if needed + iconSize: L.point(40, 40) // Adjust the icon size }) }); From 38db2f86bc459fb51c47969dc6ce117faeb0c6f1 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 14:01:14 -0800 Subject: [PATCH 08/20] Don't bold the marker count --- assets/dashExtensions_default.js | 2 +- pages/lease_page.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 121f67d0..da8788a5 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -61,7 +61,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { border-radius:50%; width:30px; height:30px; position:absolute; top:5px; left:5px; display:flex; align-items:center; justify-content:center; - font-weight:bold; color:black; font-size:14px;"> + font-weight:normal; color:black; font-size:14px;"> ${feature.properties.point_count_abbreviated}
`, diff --git a/pages/lease_page.py b/pages/lease_page.py index 1e68a1a7..08a76b99 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -297,7 +297,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental border-radius:50%; width:30px; height:30px; position:absolute; top:5px; left:5px; display:flex; align-items:center; justify-content:center; - font-weight:bold; color:black; font-size:14px;"> + font-weight:normal; color:black; font-size:14px;"> ${feature.properties.point_count_abbreviated} `, From 197e9f2c621422f5462f391d0b36184f0a054ba6 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 14:32:26 -0800 Subject: [PATCH 09/20] Match default Leaflet.markercluster font and styling --- assets/dashExtensions_default.js | 6 +++--- pages/lease_page.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index da8788a5..98792fbd 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -49,11 +49,11 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { }); } - // Create a custom marker for the cluster with dual circle style + // Create a custom marker for the cluster with Leaflet.markercluster font styling const clusterMarker = L.marker(latlng, { icon: L.divIcon({ html: ` -
+
@@ -61,7 +61,7 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { 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:14px;"> + font-weight:normal; color:black; font-size:12px; font-family: Arial, sans-serif;"> ${feature.properties.point_count_abbreviated}
`, diff --git a/pages/lease_page.py b/pages/lease_page.py index 08a76b99..dbcc7fa3 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -285,11 +285,11 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental }); } - // Create a custom marker for the cluster with dual circle style + // Create a custom marker for the cluster with Leaflet.markercluster font styling const clusterMarker = L.marker(latlng, { icon: L.divIcon({ html: ` -
+
@@ -297,7 +297,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental 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:14px;"> + font-weight:normal; color:black; font-size:12px; font-family: Arial, sans-serif;"> ${feature.properties.point_count_abbreviated}
`, @@ -335,7 +335,6 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental - ns = Namespace("dash_props", "module") # Generate the map return dl.GeoJSON( From 14fa14b5d718896ddf2bd288aa2652c2ec799a97 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 17:31:06 -0800 Subject: [PATCH 10/20] - Fix unintended page refresh when updating the map. - Sort Dash app params alphabetically? --- app.py | 12 ++--- functions/convex_hull.py | 96 ++++++++++++++++++++++++++++++++++++ pages/lease_page.py | 104 +-------------------------------------- 3 files changed, 104 insertions(+), 108 deletions(-) create mode 100644 functions/convex_hull.py diff --git a/app.py b/app.py index 87fb2d60..6b0dacf9 100755 --- a/app.py +++ b/app.py @@ -14,17 +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"} ], - external_scripts=[ - 'https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js' - ] + use_pages = True, ) # Set the page title diff --git a/functions/convex_hull.py b/functions/convex_hull.py new file mode 100644 index 00000000..40d1c3a0 --- /dev/null +++ b/functions/convex_hull.py @@ -0,0 +1,96 @@ +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) + const getColor = function(size) { + if (size < 10) return 'green'; // Small clusters + if (size < 50) return 'yellow'; // Medium clusters + if (size < 100) 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: ` +
+
+
+ ${feature.properties.point_count_abbreviated} +
+
`, + 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; +}""") \ No newline at end of file diff --git a/pages/lease_page.py b/pages/lease_page.py index dbcc7fa3..cf90378a 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -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 @@ -14,8 +15,6 @@ import sys import time import uuid -from dash_extensions.javascript import assign - dash.register_page( __name__, @@ -236,105 +235,6 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # comparison_df.to_csv('missing_rows_comparison.csv', index=False) # logger.info(f"Comparison of missing rows has been saved to 'missing_rows_comparison.csv'") - cluster_to_layer = 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) - const getColor = function(size) { - if (size < 10) return 'green'; // Small clusters - if (size < 50) return 'yellow'; // Medium clusters - if (size < 100) 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: ` -
-
-
- ${feature.properties.point_count_abbreviated} -
-
`, - 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; -}""") - - - - - ns = Namespace("dash_props", "module") # Generate the map return dl.GeoJSON( @@ -342,7 +242,7 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental #children=[dl.Popup(id='2popup')], data=geojson, cluster=True, - clusterToLayer=cluster_to_layer, + clusterToLayer=generate_convex_hulls, zoomToBoundsOnClick=True, superClusterOptions={ # https://github.com/mapbox/supercluster#options 'radius': 160, From 6d84cb2a71a4263adbcc3e79f4950b86d57b35f7 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 17:31:25 -0800 Subject: [PATCH 11/20] Remove dead code --- pages/lease_page.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/lease_page.py b/pages/lease_page.py index cf90378a..686c9d4e 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -239,7 +239,6 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental # Generate the map return dl.GeoJSON( id=str(uuid.uuid4()), - #children=[dl.Popup(id='2popup')], data=geojson, cluster=True, clusterToLayer=generate_convex_hulls, From f12ea0f9e1e2b4e96d2bdd61ca64b828315f8841 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 18:21:36 -0800 Subject: [PATCH 12/20] Match Leaflet.markercluster default color scale --- assets/dashExtensions_default.js | 7 ++++--- functions/convex_hull.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 98792fbd..b769f5c8 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -11,10 +11,11 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { 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 const getColor = function(size) { - if (size < 10) return 'green'; // Small clusters - if (size < 50) return 'yellow'; // Medium clusters - if (size < 100) return 'orange'; // Larger clusters + if (size < 50) return 'rgba(110, 204, 57, 0.6)'; // Small clusters + if (size < 200) return 'rgba(240, 194, 12, 0.6)'; // Medium clusters + if (size < 500) return 'rgba(241, 128, 23, 0.6)'; // Larger clusters return 'red'; // Very large clusters }; diff --git a/functions/convex_hull.py b/functions/convex_hull.py index 40d1c3a0..ddc5101a 100644 --- a/functions/convex_hull.py +++ b/functions/convex_hull.py @@ -11,11 +11,12 @@ 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 const getColor = function(size) { - if (size < 10) return 'green'; // Small clusters - if (size < 50) return 'yellow'; // Medium clusters - if (size < 100) return 'orange'; // Larger clusters - return 'red'; // Very large clusters + if (size < 50) return 'rgba(110, 204, 57, 0.6)'; // Small clusters + if (size < 200) return 'rgba(240, 194, 12, 0.6)'; // Medium clusters + if (size < 500) return 'rgba(241, 128, 23, 0.6)'; // Larger clusters + return 'red'; // Very large clusters }; // Get the appropriate color for the cluster size From f0312a8688a3a5d7718d5e59b69225a83eb58578 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 18:29:53 -0800 Subject: [PATCH 13/20] Adjust color scale for clusters --- assets/dashExtensions_default.js | 6 ++++-- functions/convex_hull.py | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index b769f5c8..3a32ff6a 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -12,10 +12,12 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { // 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 'rgba(240, 194, 12, 0.6)'; // Medium clusters - if (size < 500) return 'rgba(241, 128, 23, 0.6)'; // Larger clusters + if (size < 200) return 'yellow'; // Medium clusters + if (size < 500) return 'orange'; // Larger clusters return 'red'; // Very large clusters }; diff --git a/functions/convex_hull.py b/functions/convex_hull.py index ddc5101a..bb0bc47e 100644 --- a/functions/convex_hull.py +++ b/functions/convex_hull.py @@ -12,12 +12,14 @@ // 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 'rgba(240, 194, 12, 0.6)'; // Medium clusters - if (size < 500) return 'rgba(241, 128, 23, 0.6)'; // Larger 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); From 16d7120e74714c323f4fc918585e5731558572f7 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 18:53:06 -0800 Subject: [PATCH 14/20] Convex hulls for buy page --- pages/buy_page.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pages/buy_page.py b/pages/buy_page.py index 2971398a..17f5bbe2 100644 --- a/pages/buy_page.py +++ b/pages/buy_page.py @@ -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 @@ -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__, @@ -274,6 +274,7 @@ def update_map( id=str(uuid.uuid4()), data=geojson, cluster=True, + clusterToLayer=generate_convex_hulls, zoomToBoundsOnClick=True, superClusterOptions={ # https://github.com/mapbox/supercluster#options 'radius': 160, From ccfdcc5c39a9e39312c7700b4349f0dc631f3460 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Wed, 27 Nov 2024 18:54:40 -0800 Subject: [PATCH 15/20] Migrate from deprecated argument --- pages/buy_page.py | 2 +- pages/lease_page.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/buy_page.py b/pages/buy_page.py index 17f5bbe2..93e470be 100644 --- a/pages/buy_page.py +++ b/pages/buy_page.py @@ -275,12 +275,12 @@ def update_map( 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 diff --git a/pages/lease_page.py b/pages/lease_page.py index 686c9d4e..dc1ae01b 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -242,12 +242,12 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental 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")) ) # Create a callback to manage the collapsing behavior From 0f18ba47f21e4b98ac99c64f19448b3c0fe655c8 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Thu, 28 Nov 2024 07:59:58 -0800 Subject: [PATCH 16/20] Migrate from deprecated argument --- pages/buy_page.py | 2 +- pages/lease_page.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/buy_page.py b/pages/buy_page.py index 17f5bbe2..93e470be 100644 --- a/pages/buy_page.py +++ b/pages/buy_page.py @@ -275,12 +275,12 @@ def update_map( 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 diff --git a/pages/lease_page.py b/pages/lease_page.py index 686c9d4e..dc1ae01b 100644 --- a/pages/lease_page.py +++ b/pages/lease_page.py @@ -242,12 +242,12 @@ def update_map(subtypes_chosen, pets_chosen, terms_chosen, garage_spaces, rental 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")) ) # Create a callback to manage the collapsing behavior From e35c0ac8759cdd02bbf1264da93a2a7e0a3bfb42 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Thu, 28 Nov 2024 08:27:27 -0800 Subject: [PATCH 17/20] Refactor style properties to use camelCase and get rid of annoying nag devtools warnings --- assets/dashExtensions_default.js | 4 +- assets/javascript/clientside_callbacks.js | 2 +- functions/convex_hull.py | 4 +- pages/components.py | 254 +++++++++++----------- test.py | 2 +- 5 files changed, 133 insertions(+), 133 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 3a32ff6a..6f02b928 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -57,10 +57,10 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { icon: L.divIcon({ html: `
-
-
-
-
' + feature.properties.point_count_abbreviated + '
', + html: '
' + feature.properties.point_count_abbreviated + '
', className: "marker-cluster", iconSize: L.point(40, 40), color: csc(valueMean) From d0997140e39e0c038c6fa1458d8e41c9938ebed1 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Thu, 28 Nov 2024 08:47:03 -0800 Subject: [PATCH 18/20] e35c0ac8759cdd02bbf1264da93a2a7e0a3bfb42 fix missing cluster icons --- assets/dashExtensions_default.js | 4 ++-- functions/convex_hull.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/dashExtensions_default.js b/assets/dashExtensions_default.js index 6f02b928..3a32ff6a 100644 --- a/assets/dashExtensions_default.js +++ b/assets/dashExtensions_default.js @@ -57,10 +57,10 @@ window.dashExtensions = Object.assign({}, window.dashExtensions, { icon: L.divIcon({ html: `
-
-
-
-
Date: Thu, 28 Nov 2024 08:54:08 -0800 Subject: [PATCH 19/20] Popup: change listing URL hyperlink from mls # to full street address heading --- assets/javascript/popup.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/assets/javascript/popup.js b/assets/javascript/popup.js index a44b2bd1..fc4b0aca 100644 --- a/assets/javascript/popup.js +++ b/assets/javascript/popup.js @@ -20,19 +20,15 @@ window.dash_props = Object.assign({}, window.dash_props, { function getListingUrlBlock(data) { if (!data.listing_url) { return ` - - Listing ID (MLS#) - ${data.mls_number} - +
+
${data.full_street_address}
+
`; } return ` - - Listing ID (MLS#) - - ${data.mls_number} - - + `; } @@ -63,15 +59,16 @@ window.dash_props = Object.assign({}, window.dash_props, { return `
${imageRow} -
-
${data.full_street_address}
-
+ ${listingUrlBlock} - ${listingUrlBlock} + + + + From cef9c33b7bce2ceeb9af6954b93ed6365a9cb3d2 Mon Sep 17 00:00:00 2001 From: Sahib Bhai Date: Thu, 28 Nov 2024 08:57:04 -0800 Subject: [PATCH 20/20] ae98279ab02b2c7d6c82fb598778a8cabaee7b3b repeat for Buy --- assets/javascript/popup.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/assets/javascript/popup.js b/assets/javascript/popup.js index fc4b0aca..aef8d011 100644 --- a/assets/javascript/popup.js +++ b/assets/javascript/popup.js @@ -213,15 +213,16 @@ window.dash_props = Object.assign({}, window.dash_props, { return `
${imageRow} -
-
${data.address}
-
+ ${listingUrlBlock}
Listed Date ${formatDate(data.listed_date)}
Listing ID (MLS#)${data.mls_number}
List Office Phone ${phoneNumberBlock}
- ${listingUrlBlock} + + + + ${parkNameBlock}
Listed Date ${formatDate(data.listed_date)}
Listing ID (MLS#)${formatDate(data.mls_number)}
List Price