Published
Edited
May 12, 2021
1 fork
1 star
Insert cell
Insert cell
Insert cell
container = {
const container = html`<div id="map" style="height:750px;">`;
yield container;
const map = L.map(container, {
zoomSnap: 0.25,
zoomDelta: 0.25
}).setView([38.040,-78.490], 13.75);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "&copy; <a href=https://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
}).addTo(map);

const parcels = L.geoJSON(parcelData, {
style: (feature) => {
return {
color: "black",
fillOpacity: 0.5,
weight: 0.05
};
}
}).addTo(map);

container._leaflet_parcels = parcels;
}
Insert cell
parcelData = await FileAttachment("parcel-area-details@7.geojson").json();
Insert cell
standardized = {
const formatDistance = (value) => {
return value !== null ? `${value.toFixed(0)} m` : 'N/A';
};
const transformers = [
{
weight: catWeight,
feature: "cat_distance",
preprocessor: (value) => {return Math.E ** (-1 * value / 250)},
label: 'CAT distance',
format: formatDistance,
},
{
weight: utsWeight,
feature: "uts_distance",
preprocessor: (value) => {return Math.E ** (-1 * value / 250)},
label: 'UTS distance',
format: formatDistance,
},
{
weight: parkWeight,
feature: "parks_distance",
preprocessor: (value) => {return Math.E ** (-1 * value / 250)},
label: 'Park distance',
format: formatDistance,
},
{
weight: schoolWeight,
feature: "schools_distance",
preprocessor: (value) => {return Math.E ** (-1 * value / 250)},
label: 'School distance',
format: formatDistance,
},
{
weight: uvaWeight,
feature: "uva_distance",
preprocessor: (value) => {return Math.E ** (-1 * value / 500)},
label: 'UVA distance',
format: formatDistance,
},
{
weight: roadWeight,
feature: "roads_distance_local",
preprocessor: (value) => {return Math.E ** (-1 * value / 250)},
label: 'Road distance',
format: formatDistance,
},
{
weight: walkWeight,
feature: "walkscore",
preprocessor: (value) => {return value},
label: 'Walk Score',
format: (value) => {return value.toFixed(0);},
},
{
weight: bikeWeight,
feature: "bikescore",
preprocessor: (value) => {return value},
label: 'Bike score',
format: (value) => {return value.toFixed(0);},
},
{
weight: landValueWeight,
feature: "landvaluepersqmrank_parcel",
preprocessor: (value) => {return value},
label: 'Land value %ile',
format: (value) => {
return value !== null ? `${(value * 100).toFixed(0)}%` : 'N/A';
},
},
{
weight: whiteOnlyWeight,
feature: "prop_white",
preprocessor: (value) => {return value},
label: 'Proportion white',
format: (value) => {
return value ? `${(value * 100).toFixed(0)}%` : 'N/A';
},
},
{
weight: incomeWeight,
feature: "income",
preprocessor: (value) => {return value},
label: 'Median income',
format: (value) => {
return value ? `$${value.toFixed(0)} / year` : 'N/A';
},
},
];

const zscore = (values) => {
const filtered = values.filter(value => {
return value !== null;
});
const avg = filtered.reduce((sum, value) => {
return sum + value;
}, 0) / filtered.length;
const variance = filtered.reduce((sum, value) => {
return sum + (value - avg) ** 2;
}, 0) / filtered.length;
const stddev = Math.sqrt(variance);
return values.map(value => {
return value !== null ? (value - avg) / stddev : 0;
});
};

const standardized = Object.fromEntries(transformers.map(transformer => {
return [
transformer.feature,
zscore(features.map(feature => {
return transformer.preprocessor(feature[transformer.feature]);
}))
];
}));

const evaluate = (feature, index) => {
return transformers.reduce((sum, transformer) => {
return sum + transformer.weight * standardized[transformer.feature][index];
}, 0);
};

const formatLayer = (layer) => {
const feature = gpinToFeature[layer.feature.properties.gpin];
return transformers.map(transformer => {
const value = feature[transformer.feature];
const formatted = transformer.format(value);
return `<div>${transformer.label}: ${formatted}</div>`;
}).join('\n');
};

const parcels = container._leaflet_parcels;
const values = features.map((feature, index) => {
return evaluate(feature, index);
});
console.log('range', Math.min(...values), Math.max(...values));
const scale = chroma.scale(['yellow', 'red', 'black']).correctLightness().domain([Math.min(...values), Math.max(...values)]);
parcels.eachLayer(layer => {
const feature = gpinToFeature[layer.feature.properties.gpin];
let color;
let score;
if (feature) {
score = evaluate(feature, gpinToIndex[layer.feature.properties.gpin]);
color = score !== 0 ? scale(score) : "white";
layer.bindPopup(`<h4>Parcel ${layer.feature.properties.gpin}: ${layer.feature.properties.addresses}</h4> ${formatLayer(layer)}`);
} else {
color = "white";
}
layer.setStyle({
fillColor: color,
});
});

return standardized;
}
Insert cell
L = {
const L = await require("leaflet@1/dist/leaflet.js");
if (!L._style) {
const href = await require.resolve("leaflet@1/dist/leaflet.css");
document.head.appendChild(L._style = html`<link href=${href} rel=stylesheet>`);
}
return L;
}
Insert cell
chroma = require('chroma-js')
Insert cell
features = await FileAttachment("autozone_features@37.csv").csv({typed: true});
Insert cell
gpinToFeature = Object.fromEntries(features.map(feature => {return [feature.gpin, feature]}));
Insert cell
gpinToIndex = Object.fromEntries(features.map((feature, index) => {
return [feature.gpin, index];
}));
Insert cell
import {Button, Range} from "@observablehq/inputs"

Insert cell
viewof catWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "CAT Access"})
Insert cell
viewof utsWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "UTS Access"})
Insert cell
viewof parkWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "Park Access"})
Insert cell
viewof schoolWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "School Access"})
Insert cell
viewof uvaWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "UVA Access"})
Insert cell
viewof roadWeight = Range([-1, 1], {value: 0.25, step: 0.05, label: "Road Access"})
Insert cell
viewof walkWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "Walk Score"})
Insert cell
viewof bikeWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "Bike Score"})
Insert cell
viewof landValueWeight = Range([-1, 1], {value: 0.75, step: 0.05, label: "Land Value"})
Insert cell
viewof whiteOnlyWeight = Range([-1, 1], {value: 1, step: 0.05, label: "% White"})
Insert cell
viewof incomeWeight = Range([-1, 1], {value: 1, step: 0.05, label: "Median Income"})
Insert cell
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input"));
}
Insert cell
function clear() {
set(viewof walkWeight, 0);
set(viewof bikeWeight, 0);
set(viewof catWeight, 0);
set(viewof utsWeight, 0);
set(viewof parkWeight, 0);
set(viewof schoolWeight, 0);
set(viewof uvaWeight, 0);
set(viewof roadWeight, 0);
set(viewof landValueWeight, 0);
set(viewof whiteOnlyWeight, 0);
set(viewof incomeWeight, 0);
}
Insert cell
function setDefaults() {
set(viewof walkWeight, 0.75);
set(viewof bikeWeight, 0.75);
set(viewof catWeight, 0.75);
set(viewof utsWeight, 0.75);
set(viewof parkWeight, 0.75);
set(viewof schoolWeight, 0.75);
set(viewof uvaWeight, 0.75);
set(viewof roadWeight, 0.25);
set(viewof landValueWeight, 0.75);
set(viewof whiteOnlyWeight, 1);
set(viewof incomeWeight, 1);
}
Insert cell
function prioritizeAmenities() {
set(viewof walkWeight, 1);
set(viewof bikeWeight, 1);
set(viewof catWeight, 1);
set(viewof utsWeight, 1);
set(viewof parkWeight, 1);
set(viewof schoolWeight, 1);
set(viewof uvaWeight, 1);
set(viewof roadWeight, 1);
set(viewof landValueWeight, 0);
set(viewof whiteOnlyWeight, 0);
set(viewof incomeWeight, 0);
}
Insert cell
function prioritizeVulnerable() {
set(viewof walkWeight, 0);
set(viewof bikeWeight, 0);
set(viewof catWeight, 0);
set(viewof utsWeight, 0);
set(viewof parkWeight, 0);
set(viewof schoolWeight, 0);
set(viewof uvaWeight, 0);
set(viewof roadWeight, 0);
set(viewof landValueWeight, 0);
set(viewof whiteOnlyWeight, 1);
set(viewof incomeWeight, 1);
}
Insert cell
viewof clearButton = Object.assign(html`<button>Clear</button>`, {onclick: clear})
Insert cell
viewof defaultButton = Object.assign(html`<button>Restore Defaults</button>`, {onclick: setDefaults})
Insert cell
viewof amenitiesButton = Object.assign(html`<button>Housing Near Amenities</button>`, {onclick: prioritizeAmenities})
Insert cell
viewof vulnerableButton = Object.assign(html`<button>Housing in White/High-income Neighborhoods</button>`, {onclick: prioritizeVulnerable})
Insert cell
import {serialize} from '@mbostock/saving-svg'
Insert cell
downloadButton = DOM.download(() => serialize(document.querySelector("#map svg")), 'cville-flum', "Download map")
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