Published
Edited
May 12, 2020
Importers
1 star
Insert cell
Insert cell
function mapShade(countyLatLng, mapData, mapboxApiAccessToken, radius=4) {
const container = document.createElement('div');
container.style.height = '600px';

new deck.DeckGL({
container,
map: mapboxgl,
mapboxApiAccessToken,
mapStyle: 'mapbox://styles/mapbox/dark-v9',
initialViewState: {
longitude: countyLatLng.lng,
latitude: countyLatLng.lat,
zoom: 10.5,
minZoom: 5,
maxZoom: 25
},
layers: [
new deck.GeoJsonLayer({
id: 'geojson',
data: mapData.allData.features,
stroked: true,
filled: true,
extruded: false,
lineWidthMinPixels: 0.5,
lineWidthMinPixels: 0.5,
getFillColor: mapData.colorFn,
getLineColor: d => [0, 0, 0, 255],
pickable: true,
onHover: ({object, x, y}) => true
}),
],
getTooltip: mapData.tooltipFn,
controller: true
});
return container;
}
Insert cell
function mapDots(countyLatLng, mapData, mapboxApiAccessToken, radius=4) {
const container = document.createElement('div');
container.style.height = '600px';

new deck.DeckGL({
container,
map: mapboxgl,
mapboxApiAccessToken,
mapStyle: 'mapbox://styles/mapbox/dark-v9',
initialViewState: {
longitude: countyLatLng.lng,
latitude: countyLatLng.lat,
zoom: 10.5,
minZoom: 5,
maxZoom: 25
},
layers: [
new deck.ScatterplotLayer({
id: 'scatterplot',
data: mapData.allData.features,
getPosition: d => d.geometry.coordinates,
getFillColor: mapData.colorFn,
radiusMinPixels: radius,
radiusMaxPixels: radius,
pickable: true,
onHover: ({object, x, y}) => true
}),
],
getTooltip: mapData.tooltipFn,
controller: true
});
return container;
}
Insert cell
explode = (geoJson, property) => {
// Translates polygon geojson data into a number of points in each poly
// based on the given property
const points = [];
geoJson.features.forEach(feature => {
randomPoints(feature.properties[property], feature).forEach(point => points.push({...point, properties: feature.properties}));
});
return {type: 'FeatureCollection', features: points};
};
Insert cell
statScale = async (countyLatLng, allData, property, statsKey=null) => {
// Get bounds of income
const propertyBounds = maxMin(allData.features.map(x => x.properties[property]));
return {
propertyBounds,
allData,
colorFn: d => {
const x = d.properties[property];
if (x == null) return [0, 0, 0, 0];
const scaledX = scale(x, propertyBounds[0], (propertyBounds[0] + propertyBounds[1]) / 2, propertyBounds[1]);
return redSequential(scaledX);
},
tooltipFn: info => {
if (info.object == null) return;
if (info.object.type == 'Feature') {
const properties = info.object.properties;
const propertyValue = properties[property];
if (propertyValue != null) {
return {
text: `${propertyValue.toLocaleString()}`,
};
}
}
},
};
};
Insert cell
medianIncomeScale = async (countyLatLng, allData, statsKey=null) => {
// Get bounds of income
const incomeBounds = maxMin(allData.features.map(x => x.properties[medianIncome]).filter(x => x > 0));
const [countyMedianIncome, countyPopulation] = await censusCountyStat(countyLatLng, [medianIncome, population], statsKey);
return {
countyMedianIncome,
incomeBounds,
countyPopulation,
allData,
colorFn: d => {
const x = d.properties[medianIncome];
// transparent shade for no data
if (x < 0) return [0, 0, 0, 0];
const scaledX = scale(x, incomeBounds[0], countyMedianIncome, countyMedianIncome * 2 - incomeBounds[0]);
return brBgDiverging(scaledX);
},
tooltipFn: info => {
if (info.object == null) return;
if (info.object.type == 'Feature') {
const properties = info.object.properties;
const income = properties[medianIncome];
if (income > 0) {
return {
text: `$${income.toLocaleString()} median income`,
};
}
}
},
};
};
Insert cell
function joinPoints(pointCollection, valueColumn, latColumn, lngColumn, geoJson) {
return turf.tag(({type: "FeatureCollection", features: pointCollection.map(x => turf.point([x[lngColumn], x[latColumn], x]))}), geoJson, valueColumn, valueColumn);
}
Insert cell
censusCountyStat = async(countyLatLng, variables, statsKey=null, year=2018) => {
const response = await census({
vintage: year,
geoHierarchy: { county: countyLatLng },
sourcePath: ["acs", "acs5"],
values: variables,
geoResolution: "500k",
statsKey
});
return variables.map(x => response.features[0].properties[x]);
}
Insert cell
censusZips = async (variables=[], statsKey=null, year=2018) => {
return await census({
vintage: year,
geoHierarchy: {
"zip code tabulation area": "*"
},
sourcePath: ["acs", "acs5"],
values: variables,
geoResolution: "500k",
statsKey
});
}
Insert cell
censusTracts = async (countyLatLng, variables=[], statsKey=null, year=2018) => {
return await census({
vintage: year,
geoHierarchy: {
"county": countyLatLng,
"tract": "*"
},
sourcePath: ["acs", "acs5"],
values: variables,
geoResolution: "500k",
statsKey
});
}
Insert cell
censusBlockGroups = async (countyLatLng, variables=[], statsKey=null, year=2018) => {
return await census({
vintage: year,
geoHierarchy: {
"county": countyLatLng,
"block group": "*"
},
sourcePath: ["acs", "acs5"],
values: variables,
geoResolution: "500k",
statsKey
});
}
Insert cell
us_counties = census({
vintage: 2018,
geoHierarchy: {
"county": {
lat: 41.8781,
lng: -87.6298,
},
},
geoResolution: "500k",
});
Insert cell
Insert cell
randomPoints = (n, poly) => {
const results = [];
for (let i = 0; i < n; i++) {
results.push(randomPoint(poly));
}
return results;
}
Insert cell
// Return a random point guaranteed to be within a poly
function randomPoint(poly) {
const bbox = poly.geometry.bbox;
while (true) {
const lng = bbox[0] + Math.random() * (bbox[2] - bbox[0]);
const lat = bbox[1] + Math.random() * (bbox[3] - bbox[1]);
const point = turf.point([lng, lat]);
if (turf.pointsWithinPolygon(point, poly).features.length > 0) return point;
}
}
Insert cell
Insert cell
// Return a number from 0 - 1 with 0.5 at middle using a linear scale
scale = (x, min, middle, max) => {
const bounds = Math.max(max - middle, middle - min);
const val = (x - (middle - bounds)) / (bounds * 2);
// Constrain to 0-1
return Math.max(Math.min(val, 1), 0);
}
Insert cell
lerpColor = (x, color1, color2) => {
return [
color1[0] + x * (color2[0] - color1[0]),
color1[1] + x * (color2[1] - color1[1]),
color1[2] + x * (color2[2] - color1[2]),
color1[3] + x * (color2[3] - color1[3]),
];
}
Insert cell
divergentColorScale = (x, minColor, middleColor, maxColor) => {
if (x <= 0.5) {
return lerpColor(x * 2, minColor, middleColor);
} else {
return lerpColor((x - 0.5) * 2, middleColor, maxColor);
}
}
Insert cell
Insert cell
// Based on 8-class Color Brewer BrBG
brBgDiverging = x => divergentColorScale(x, [140,81,10,255], [245,245,245,255], [1,102,94,255])
Insert cell
redSequential = x => lerpColor(x, [255, 255, 255, 255], [255, 0, 0, 255])
Insert cell
Insert cell
lerp = (x, bounds) => (x - bounds[0]) / (bounds[1] - bounds[0])
Insert cell
maxMin = x => [min(x), max(x)]
Insert cell
function max(l) {
let i = null;
l.forEach(x => (i == null || x > i) && (i = x));
return i;
}
Insert cell
function min(l) {
let i = null;
l.forEach(x => (i == null || x < i) && (i = x));
return i;
}
Insert cell
function average(l) {
return sum(l) / l.length;
}
Insert cell
function median(l) {
const center = (l.length - 1) / 2;
const sorted = l.sort();
if (Math.abs(center - Math.floor(center)) > 0.1) {
return (sorted[Math.floor(center)] + sorted[Math.ceil(center)]) / 2;
}
return sorted[center];
}
Insert cell
function sum(l) {
let total = 0;
l.forEach(i => total += i);
return total;
}
Insert cell
Insert cell
deck = require.alias({
// optional dependencjei
h3: {},
s2Geometry: {}
})('deck.gl@~8.0.0/dist.min.js')
Insert cell
mapboxgl = require('mapbox-gl@^0.53.1/dist/mapbox-gl.js')
Insert cell
turf = require('@turf/turf')
Insert cell
// Parse CSVs, color scales
d3 = require('d3-dsv', 'd3-scale@1', 'd3-geo')
Insert cell
Insert cell
loadCsv = async request => {
return d3.csvParse(await request.text());
}
Insert cell
md`## Census Variables`
Insert cell
// INCOME IN THE PAST 12 MONTHS (IN 2018 INFLATION-ADJUSTED DOLLARS)
medianIncome = 'B19013_001E'
Insert cell
population = 'B01001_001E'
Insert cell
md`## Census API`
Insert cell
censusVariables = {
const response = await fetch('https://api.census.gov/data/2018/acs/acs5/variables.json');
const json = await response.json();
return json;
}
Insert cell
// Load CitySDK from the GitHub page
census = {
const census = await require('https://uscensusbureau.github.io/citysdk/assets/citysdk.js').catch(() => window.census);
return params => new Promise(async (resolve, reject) => {
// Try to fetch from cache first
const jsonParams = JSON.stringify(params);
const key = `census-${jsonParams}`;
const cached = await localForage.getItem(key);
if (cached != null) {
console.log('CACHE HIT');
return resolve(JSON.parse(cached));
}
console.log('CACHE MISS');
// Not cached. Fetch from census
census(
params,
async (err, res) => {
if (err != null) return reject(err);
// Remove stats key from response
delete res.statsKey;
// Cache compressed version
const jsonResponse = JSON.stringify(res);
await localForage.setItem(key, jsonResponse);
// Return response
return resolve(res);
}
);
});
}
Insert cell
localForage = require('localforage')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more