A map of where all the single family homes are that are valued at over $1 million dollars according to Minneapolis assessor data. Blocks displayed are based on census data.
You can pan and zoom by dragging on the map, or whatever you normally do.
| license: gpl-3.0 | |
| height: 734 | |
| scrolling: no | |
| border: yes |
A map of where all the single family homes are that are valued at over $1 million dollars according to Minneapolis assessor data. Blocks displayed are based on census data.
You can pan and zoom by dragging on the map, or whatever you normally do.
| <!DOCTYPE html> | |
| <svg width="960" height="1100"></svg> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
| <script src="https://d3js.org/topojson.v2.min.js"></script> | |
| <style type="text/css"> | |
| #data { | |
| width: calc(50%); | |
| height: 100vh; | |
| position: absolute; | |
| right: 0px; | |
| top: 0px; | |
| font-family: sans-serif; | |
| } | |
| circle.property { | |
| fill:red; | |
| fill-opacity:0.5; | |
| stroke:#F00; | |
| stroke-width:2px; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| text-align: center; | |
| width: 60px; | |
| padding: 8px; | |
| margin-top: -20px; | |
| font: 10px sans-serif; | |
| background: #ddd; | |
| pointer-events: none; | |
| } | |
| </style> | |
| <script> | |
| var svg = d3.select("svg"), | |
| width = +svg.attr("width"), | |
| height = +svg.attr("height"); | |
| var projection = d3.geoIdentity() | |
| .scale(1); | |
| var zoom = d3.zoom() | |
| .on("zoom", zoomed); | |
| var initialTransform = d3.zoomIdentity | |
| .scale(1); | |
| var path = d3.geoPath() | |
| .projection(projection); | |
| var x = d3.scaleLinear() | |
| .domain([0, 6]) | |
| .rangeRound([100, 360]); | |
| var color = d3.scaleThreshold() | |
| .domain(d3.range(0, 6)) | |
| .range(d3.schemeReds[7]); | |
| function getColor(x) { | |
| if (x > 0) { | |
| return color(x); | |
| } else { | |
| return "#FFFFFF"; | |
| } | |
| } | |
| var svg = svg | |
| .on("click", stopped, true); | |
| svg | |
| .call(zoom) // delete this line to disable free zooming | |
| .call(zoom.transform, initialTransform); | |
| var gspace = svg.append("g"); | |
| var g = svg.append("g") | |
| .attr("class", "key") | |
| .attr("transform", "translate(0,40)"); | |
| var property_notes = d3.map(); | |
| d3.queue() | |
| .defer(d3.json, "topo.json") | |
| .await(ready); | |
| var div = d3.select("body").append("div") | |
| .attr("class", "tooltip") | |
| .style("display", "none"); | |
| function mouseover() { | |
| div.style("display", "inline"); | |
| } | |
| function mousemove() { | |
| var addr = this.attributes['data-address'].value; | |
| div | |
| .text(addr + ". (Click for info).") | |
| .style("left", (d3.event.pageX - 34) + "px") | |
| .style("top", (d3.event.pageY - 12) + "px"); | |
| } | |
| function mouseclick() { | |
| var addr = this.attributes['data-pid'].value; | |
| var uri = 'http://apps.ci.minneapolis.mn.us/PIApp/ValuationRpt.aspx?pid='; | |
| window.open(uri + addr); | |
| } | |
| function mouseout() { | |
| div.style("display", "none"); | |
| } | |
| function ready (error, topology) { | |
| if (error) throw error; | |
| window.topol = topology; | |
| var meshfunc = function(a, b) { return a !== b; }; | |
| gspace.append("path") | |
| .datum(topojson.feature(topology, topology.objects.zones)) | |
| .attr("fill", "none") | |
| .attr("stroke", "#333") | |
| .attr("stroke-opacity", 1) | |
| .attr("d", path); | |
| gspace.append("path") | |
| .datum(topojson.feature(topology, topology.objects.city)) | |
| .attr("fill", "none") | |
| .attr("stroke", "#00F") | |
| .attr("stroke-opacity", .5) | |
| .attr("d", path); | |
| gspace.append('g') | |
| .attr('class', 'property') | |
| .selectAll('circle') | |
| .data(topojson.feature(topology, topology.objects.properties).features) | |
| .enter().append('circle') | |
| .attr('class', 'property') | |
| .attr("cx", function (d) { return d.geometry.coordinates[0]; }) | |
| .attr("stroke", "#F00") | |
| .attr("cy", function (d) { return d.geometry.coordinates[1]; }) | |
| .attr("data-pid", function (d) { return d.properties.APN; }) | |
| .attr("data-address", function (d) { return d.properties.FORMATTED_ADDRESS; }) | |
| .attr("r", 1) | |
| .on("mouseover", mouseover) | |
| .on("mousemove", mousemove) | |
| .on("click", mouseclick) | |
| .on("mouseout", mouseout); | |
| }; | |
| function zoomed() { | |
| var transform = d3.event.transform; | |
| if (typeof gspace !== 'undefined') { | |
| gspace.style("stroke-width", 1.5 / transform.k + "px"); | |
| gspace.attr("transform", transform); | |
| } | |
| } | |
| function stopped() { | |
| if (d3.event.defaultPrevented) d3.event.stopPropagation(); | |
| } | |
| </script> |
| PROJECTION := "d3.geoMercator()" | |
| # originally: GDAL 2.1.2, released 2016/10/24 | |
| # | |
| # NOTES: | |
| # | |
| # * `less_tmp` is for slow to build targets that don't change much while I'm | |
| # tweaking other targets | |
| # | |
| # * stuff in `in` needs to be downloaded from sources; maybe i'll edit in the | |
| # sources. Original files are a bit big for a gist. | |
| # - in/2010_Census_Blocks (.zip) | |
| # https://www.gis.leg.mn/metadata/blks2010.htm | |
| # - in/assessor.csv | |
| # http://opendata.minneapolismn.gov/datasets/assessor-parcel-data | |
| # - in/Address_Points (.zip) | |
| # https://www.hennepin.us/gisopendata | |
| # | |
| WIDTH := 640 # 960 / 1.5 | |
| HEIGHT := 733.33333 # 1100 / 1.5 | |
| all: out/topo.json | |
| ## Multiple steps: | |
| ## 1. create a topojson file to reproject | |
| ## 2. split topojson back to geojson | |
| ## 3. pre-calculate the block mesh | |
| ## 4. optimize final topojson | |
| out/topo.json: tmp/city.geojson less_tmp/blocks_2010.geojson tmp/properties.filtered.geojson | |
| @echo " --> Topojson & Reproject" | |
| @topojson \ | |
| -o tmp/topo.json \ | |
| -p APN,FORMATTED_ADDRESS,OBJECTID \ | |
| --projection 'd3.geo.mercator()' \ | |
| --width $(WIDTH) \ | |
| --height $(HEIGHT) \ | |
| -- \ | |
| city=tmp/city.geojson \ | |
| blocks=less_tmp/blocks_2010.geojson \ | |
| properties=tmp/properties.filtered.geojson | |
| @echo " --> Back to GeoJSON" | |
| @cat tmp/topo.json | topo2geo \ | |
| city=tmp/topo2geo.city.json \ | |
| blocks=tmp/topo2geo.blocks.json \ | |
| properties=tmp/topo2geo.properties.json | |
| @echo " --> Geo2topo mesh" | |
| @geo2topo \ | |
| blocks=tmp/topo2geo.blocks.json \ | |
| | topomerge -k '(""+d.properties.OBJECTID).slice(0, 3)' zones=blocks \ | |
| | topomerge --mesh -f 'a !== b' zones=blocks \ | |
| | topomerge -k 'd.count' blocks=blocks \ | |
| | topo2geo zones=tmp/blocks.mesh.json | |
| @geo2topo \ | |
| city=tmp/topo2geo.city.json \ | |
| zones=tmp/blocks.mesh.json \ | |
| properties=tmp/topo2geo.properties.json \ | |
| | toposimplify -p 1 -f \ | |
| | topoquantize 1e5 \ | |
| > $@ | |
| define BLOCKS_WITHIN_MINNEAPOLIS | |
| SELECT block.* \ | |
| FROM \ | |
| 'tmp/City_Boundary/City_Boundary.shp'.City_Boundary city, \ | |
| 'tmp/2010_Census_Blocks_Filtered/2010_Census_Blocks.shp'.2010_Census_Blocks block \ | |
| WHERE ST_Contains(city.geometry, block.geometry) GROUP BY block.OBJECTID; | |
| endef | |
| less_tmp/blocks_2010.geojson: in/2010_Census_Blocks/2010_Census_Blocks.shp | |
| @cp -R in/2010_Census_Blocks tmp/2010_Census_Blocks | |
| @cp -R in/City_Boundary tmp/City_Boundary | |
| @echo " - Reprojecting Census Blocks" | |
| @ogr2ogr -overwrite -f "ESRI Shapefile" \ | |
| tmp/2010_Census_Blocks_Filtered/ \ | |
| tmp/2010_Census_Blocks/ \ | |
| -s_srs "EPSG:26915" \ | |
| -t_srs "EPSG:4326" | |
| @echo " --> SHP" | |
| @echo " - Filtering Census Blocks" | |
| @ogr2ogr -f "ESRI Shapefile" \ | |
| tmp/2010_Census_Blocks_Cleaned/ \ | |
| tmp/2010_Census_Blocks_Filtered/ \ | |
| -dialect sqlite \ | |
| -sql "$(BLOCKS_WITHIN_MINNEAPOLIS)" | |
| @echo " --> SHP" | |
| @ogr2ogr -f "GeoJSON" "$@" tmp/2010_Census_Blocks_Cleaned | |
| @echo " --> Json" | |
| rm -rf tmp/2010_Census_Blocks_Filtered/ | |
| rm -rf tmp/2010_Census_Blocks_Cleaned/ | |
| rm -rf tmp/2010_Census_Blocks/ | |
| rm -rf tmp/City_Boundary/ | |
| tmp/city.geojson: in/City_Boundary/City_Boundary.shp | |
| @echo " - Converting City Boundary" | |
| @rm -rf tmp/City_Boundary | |
| @cp -R in/City_Boundary tmp/City_Boundary | |
| @ogr2ogr -f "GeoJSON" "$@" tmp/City_Boundary | |
| @echo " --> Json" | |
| # Merge filtered properties with another dataset containing more accurate | |
| # points | |
| tmp/properties.filtered.geojson: less_tmp/assessor.ndjson less_tmp/address_points.geojson | |
| @echo ' - Filtering SFHs worth >999999' | |
| @cat less_tmp/assessor.ndjson | ndjson-filter 'd.BUILDINGUSE == "Single Fam. Dwlg."' | ndjson-filter 'parseInt(d.TOTALVALUE) > 999999' > tmp/filtered.properties.ndjson | |
| @echo ' - Combining Assessor data & Address Points' | |
| @cat less_tmp/address_points.geojson | ndjson-cat | ndjson-split 'd.features' > tmp/address_points.ndjson | |
| @ndjson-join 'd.APN' 'd.properties.PID' tmp/filtered.properties.ndjson tmp/address_points.ndjson | ndjson-map 'd[1].properties = d[0], d[1]' > tmp/props.combined.ndjson | |
| @cat tmp/props.combined.ndjson | ndjson-reduce 'p.features.push(d), p' '{"type": "FeatureCollection", "features": []}' > $@ | |
| @echo ' --> GeoJSON' | |
| less_tmp/assessor.ndjson: in/assessor.csv | |
| @echo ' - Converting assessor data' | |
| cat $^ | csv2json | ndjson-split > $@ | |
| @echo ' --> NDJSON' | |
| ## Select address points contained within Minneapolis | |
| define ADDRESS_POINTS_IN_MINNEAPOLIS | |
| SELECT point.PID, point.Geometry \ | |
| FROM \ | |
| 'tmp/City_Boundary/City_Boundary.shp'.City_Boundary city, \ | |
| 'tmp/Address_Points_Reproj/Address_Points.shp'.Address_Points point \ | |
| WHERE ST_Contains(city.geometry, point.geometry); | |
| endef | |
| less_tmp/address_points.geojson: in/Address_Points/Address_Points.shp | |
| @echo ' - Converting Address Points' | |
| @ogr2ogr -overwrite -f "ESRI Shapefile" \ | |
| tmp/Address_Points_Reproj/ \ | |
| tmp/Address_Points/ \ | |
| -s_srs "EPSG:26915" \ | |
| -t_srs "EPSG:4326" | |
| @ogr2ogr -f "GeoJSON" \ | |
| less_tmp/address_points.geojson \ | |
| tmp/Address_Points_Reproj/ \ | |
| -dialect sqlite \ | |
| -sql "$(ADDRESS_POINTS_IN_MINNEAPOLIS)" | |
| @echo " --> GeoJSON" | |
| serve: | |
| python -m SimpleHTTPServer | |
| watch: | |
| pywatch "make clean && make init && make all && make serve" Makefile | |
| clean: | |
| rm -rf tmp out | |
| init: | |
| mkdir -p less_tmp | |
| mkdir -p tmp | |
| mkdir -p out | |
| { | |
| "name": "mpls-mansion-map", | |
| "version": "1.0.0", | |
| "description": "see README.md", | |
| "main": "index.js", | |
| "scripts": { | |
| "test": "echo \"Error: no test specified\" && exit 1" | |
| }, | |
| "author": "", | |
| "license": "ISC", | |
| "dependencies": { | |
| "@mapbox/togeojson": "^0.16.0", | |
| "csv2geojson": "^5.1.1", | |
| "csvtojson": "^2.0.8", | |
| "d3-dsv": "^1.0.8", | |
| "d3-geo-projection": "^2.4.0", | |
| "ndjson-cli": "^0.3.1", | |
| "shapefile": "^0.6.6", | |
| "topojson-client": "^3.0.0", | |
| "topojson-server": "^3.0.0", | |
| "topojson-simplify": "^3.0.2", | |
| "underscore": "^1.9.1" | |
| } | |
| } |