Public
Edited
Mar 30, 2023
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
rawData = FileAttachment("output-full.csv").csv()
Insert cell
cleanedData = rawData.filter(
({count, longitude, latitude}) =>
!(isNaN(longitude) && isNaN(latitude)) /* filter out records with invalid coordinates */
&& +count > threshold /* filter out the worksites with low H1B petition count (e.g. < 20) can also filter out wrong addresses */
/* also improve graphic performance */
)
Insert cell
// define a function to aggregate NYC data, since Queens/Manhattan/Long Island/... are all located in NYC
aggregateNYC = (dataset) => {
let totalCount = 0
dataset.forEach(({worksite, count, longitude, latitude}) => {
if (longitude === "-73.9866" && latitude === "40.7306") {
totalCount += +count
}
})
// return the aggregated record
return {worksite: "New York, New York", count: totalCount, longitude: "-73.9866", latitude: "40.7306"}
}
Insert cell
// define a function to aggregate SF data, since there are many wrong addresses like "Sanfrancisco", "San Francsico"...
// their coordinates are the same because Mapbox API are smart enough to detect typos
aggregateSF = (dataset) => {
let totalCount = 0
dataset.forEach(({worksite, count, longitude, latitude}) => {
if (longitude === "-122.419906" && latitude === "37.779026") {
totalCount += +count
}
})
// return the aggregated record
return {worksite: "San Francisco, California", count: totalCount, longitude: "-122.419906", latitude: "37.779026"}
}
Insert cell
// delete all NYC and SF records, then append the aggregated NYC record and SF record
aggregatedData = [...cleanedData
.filter(({worksite, count, longitude, latitude}) =>
!(longitude === "-73.9866" && latitude === "40.7306")
&& !(longitude === "-122.419906" && latitude === "37.779026"))
, aggregateNYC(cleanedData), aggregateSF(cleanedData)]
Insert cell
// re-structure the data to fit mapboxD3 function requirements
data = aggregatedData.map(({worksite, count, longitude, latitude}) => {
return {
type: "Feature",
properties: {worksite, count},
geometry: {
type: "Point",
coordinates: [+longitude, +latitude]
}
}
})
Insert cell
Insert cell
// Custom interpolator
function interpolateTransparentBlue(t) {
const startColor = d3.rgb(64, 196, 255, 0.3); // 70% transparency
const endColor = d3.rgb(2, 119, 189, 0.9); // 10% transparency
return d3.interpolateRgb(startColor, endColor)(t);
}
Insert cell
// Custom color scale with 1 color
function constantColor() {
return d3.rgb(64, 196, 255, 0.3);
}
Insert cell
colorScale = d3
.scaleSequential(constantColor)
.domain([0, d3.max(data, (d) => +d.properties.count)]);
Insert cell
rScale = d3
.scaleSqrt()
.domain([0, d3.max(data, (d) => +d.properties.count)])
.range([0, 30])
Insert cell
// adapted from https://observablehq.com/@john-guerra/mapbox-d3

function* mapboxD3({
width = 900,
height = 600,

features = [], // geoJSON features to draw
id = (d, i) => i,
r = null, // If not null, must be a function that returns the radius in pixels given the datum
fill = null, // If not null, must be a function that returns the color given the datum
stroke = "#777",
mapboxOptions = {
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
container = html`<div style="height:${height}px; width:${width}px">`, // where to draw
d3Background = null,
delay = null,
transitionDuration = 0,
alphaRestart = 0.3, // Alpha to set before restarting if using simulation collide = true
parentInvalidation = invalidation,
strokeWidth = 1,
strokeOpacity = 1,
controls = false // Should display zooming controls
} = {}) {
let selected = null, // clicked element
loaded = false; // has mapbox finished loading

let rScale = null,
colorScale = null,
_r = r,
simulation = null,
hoverObj;
let dots;

if (!r) {
rScale = d3
.scaleSqrt()
.domain([0, d3.max(features, (d) => d?.properties?.value)])
.range([0, 15]);
_r = (d) => rScale(d?.properties?.value);
}

if (!fill) {
colorScale =
colorScale ||
d3
.scaleSequential(d3.interpolateInferno)
.domain([0, d3.max(features, (d) => d?.properties?.value)]);
fill = (d) => colorScale(d?.properties?.value);
}

yield container; // Give the container dimensions.

const map = (container.value = new mapboxgl.Map({
container,
...mapboxOptions
}));

// Setup our svg layer that we can manipulate with d3
const bb = container.getBoundingClientRect();

const svg = d3
.select(container)
.append("svg")
.attr("class", "d3Mapbox")
.style("position", "absolute")
.style("top", 0)
.style("left", 0)
// .attr("width", bb.width)
// .attr("height", bb.height)
.attr("width", width)
.attr("height", height)
.style("background", d3Background)
.style("pointer-events", "none"); // the svg shouldn't capture mouse events, so we can have pan and zoom from mapbox

//Project any point to map's current state
function projectPoint(lon, lat) {
let point = map.project(new mapboxgl.LngLat(lon, lat));
return [point.x, point.y];
}

//Projection function
let transform = d3.geoTransform({ point: projectPoint });
let path = d3.geoPath().projection(transform);

svg.append("style").text(`
circle.highlight { stroke: red; }
`);

const updateDots = (features) => {
const dots = svg
.selectAll(".feature")
.data(features, id)
.join(r ? "circle" : "path")
.attr("class", "feature")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.on("click", (evt, d) => (selected = d))
.style("pointer-events", "all");

// hello-tippy needs nodes
if (dots.nodes().length) {
hoverObj = hover(dots, invalidation, { raise: false });
}

return dots;
};

dots = updateDots(features);

const getTransition = () => {
let t = dots;
if (transitionDuration || delay) {
t = t.transition().duration(transitionDuration).delay(delay);
}
return t;
};

const update = () => {
hoverObj && hoverObj.reset();
if (r) {
let t = getTransition();
t.attr("r", _r);
t.attr("cx", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return x;
})
.attr("cy", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return y;
});
} else {
dots.attr("d", path);
}
dots.attr("fill", fill);
dots.attr("stroke", stroke);
};

// Positioning
//position nodes manually;
if (r) {
dots
.attr("r", _r)
.attr("cx", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return x;
})
.attr("cy", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return y;
});
} else {
dots.attr("d", path);
}

// Parameters to return
function set() {
container.value = {
transform,
path,
map,
dots,
hoverObj,
selected,
loaded,
update,
simulation,
set r(_) {
rScale = null;
_r = _;
},
get r() {
return r;
},
set fill(_) {
colorScale = null;
fill = _;
},
get fill() {
return fill;
},
set rScale(_) {
rScale = _;
},
get rScale() {
return rScale;
},
set colorScale(_) {
colorScale = _;
},
get colorScale() {
return colorScale;
},
set delay(_) {
delay = _;
},
get delay() {
return delay;
},
set features(_) {
features = _;
dots = updateDots(features);
},
set width(_) {
width = _;
svg.attr("width", width).attr("height", height);
},
set height(_) {
height = _;
svg.attr("width", width).attr("height", height);
}
};
container.dispatchEvent(new Event("input", { bubbles: true }));
}

if (!map) return;
// Every time the map changes, update the dots
map.on("viewreset", update);
map.on("move", update);
map.on("moveend", update);
map.on("load", () => {
loaded = true;
set();
});

parentInvalidation.then(() => {
map.off("viewreset", update);
map.off("move", update);
map.off("moveend", update);
map.remove();
});

if (controls) {
// Add zoom and rotation controls to the map.
map.addControl(new mapboxgl.NavigationControl());
}
// disable map rotation using right click + drag
map.dragRotate.disable();
// disable map rotation using touch rotation gesture
map.touchZoomRotate.disableRotation();

update();

set();
return container;
}
Insert cell
Insert cell
Insert cell
function tipcontent(_d) {
const d = _d.properties;
return htl.html`
<div>Worksite: ${d.worksite}</div>
<div>H1B Petition Count: ${d.count}</div>
`;
}
Insert cell
Insert cell
Insert cell
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