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" | |
} | |
} |