Skip to content

Instantly share code, notes, and snippets.

@rtxanson
Last active September 6, 2018 19:54
Show Gist options
  • Save rtxanson/9d229f7ac61bf3c034c74887c01f0943 to your computer and use it in GitHub Desktop.
Save rtxanson/9d229f7ac61bf3c034c74887c01f0943 to your computer and use it in GitHub Desktop.
Minneapolis Million Dollar Mansion District
license: gpl-3.0
height: 734
scrolling: no
border: yes
<!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"
}
}
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment