Public
Edited
Mar 30, 2020
58 stars
Also listed in…
Tutorials
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
projection = d3.geoMercator().fitExtent([[margin, margin], [width - margin, height - margin]], streets)
Insert cell
pathGenerator = d3.geoPath().projection(projection)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
color = d3.scaleOrdinal().domain(['neutral','male','female']).range(['#bbbbbb','#4444ff','#ee0000'])
Insert cell
yCoord = d3.scaleOrdinal().domain(['neutral','male','female']).range([margin, margin + 40, margin + 80])
Insert cell
Insert cell
stats = {
let stats = {
length: { neutral: 0, male: 0, female: 0 }, // total length in meters
count: { neutral: 0, male: 0, female: 0 } // number of streets
}
streets.features.forEach(street => {
let gender = street.properties.gender;

stats.length[gender] += street.properties.length;
stats.count[gender] += 1;
})

return stats
}
Insert cell
Insert cell
Insert cell
xCoord = d3.scaleLinear().domain([0,stats.length.neutral]).range([margin, width - margin])
Insert cell
Insert cell
function xCoordinateGenerator(xMin, xMax, y) {
let delta = xMax-xMin;
return function(pos) {
return [xMin + delta*pos, y];
}
}
Insert cell
Insert cell
function prepareStreets(streetsArray) {
// keep track of the length of streets processed so far
let lengthCounter = {
male: 0,
female: 0,
neutral: 0
}
// calculate the path geometries for each street
streetsArray.forEach(d => {
// the path for drawing the street on the map
d.properties.mapPath = pathGenerator(d)
let xGen = xCoordinateGenerator(
xCoord(lengthCounter[d.properties.gender]),
xCoord(lengthCounter[d.properties.gender] + d.properties.length),
yCoord(d.properties.gender)
)
// the path for drawing the street in the diagram
d.properties.diagramPath = normalizePath(d.properties.mapPath, xGen)
lengthCounter[d.properties.gender] += d.properties.length
})
return streetsArray
}
Insert cell
preparedStreets = prepareStreets(streets.features)
Insert cell
Insert cell
Insert cell
{
let button = html`<input type="button" value="Transition to Diagram">`
button.style.fontSize = "1.2em";
// keep track of map state
let isMap = true
button.addEventListener('click', function() {
d3.select(streetSVG1).selectAll('path')
.interrupt() // cancel any ongoing transitions
.transition()
.duration(1500)
.delay((d,i)=>i*2) // use a staggered delay, depending on the street's position in the data
.ease(d3.easeLinear) // use linear motion
// use the appropriate path for the current state
.attr('d',d => isMap ? d.properties.diagramPath : d.properties.mapPath)
button.value = isMap ? "Transition to Map" : "Transition to Diagram"
isMap = !isMap
})
return button
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Sorting function for going from diagram to map
function mapOrder(a,b) {
// first compare gender
if ((genderOrder[a.properties.gender]) > (genderOrder[b.properties.gender])) return 1;
if ((genderOrder[a.properties.gender]) < (genderOrder[b.properties.gender])) return -1;
// then latitude
if (getFirstPoint(a.geometry)[1] > getFirstPoint(b.geometry)[1]) return 1;
if (getFirstPoint(a.geometry)[1] < getFirstPoint(b.geometry)[1]) return -1;
return 0;
}
Insert cell
// Sorting function for going from map to diagram
function chartOrder(a,b) {
// first compare gender
if ((genderOrder[a.properties.gender]) > (genderOrder[b.properties.gender])) return 1;
if ((genderOrder[a.properties.gender]) < (genderOrder[b.properties.gender])) return -1;
// then latitude - note that this is the reverse order than above
if (getFirstPoint(a.geometry)[1] > getFirstPoint(b.geometry)[1]) return -1;
if (getFirstPoint(a.geometry)[1] < getFirstPoint(b.geometry)[1]) return 1;
return 0;
}
Insert cell
Insert cell
sortedStreets = {
// make a copy of the original data, to avoid overwriting the properties set by part 2
// https://stackoverflow.com/a/4591639
let streets2 = JSON.parse(JSON.stringify(streets.features))
// use sorted arrays to assign a chartOrder and mapOrder property to each street
streets2.sort(mapOrder)
streets2.forEach((d,i) => d.properties.mapOrder = i)
streets2.sort(chartOrder)
streets2.forEach((d,i) => d.properties.chartOrder = i)
// prepare streets like in part 2
prepareStreets(streets2)
return streets2
}
Insert cell
Insert cell
Insert cell
button = {
let button = html`<input type="button" value="Transition to Diagram">`
button.style.fontSize = "1.2em";
let isMap = true
button.addEventListener('click', function() {
button.disabled = true
d3.select(streetSVG2).selectAll('path')
.interrupt() // cancel any ongoing transitions
.transition()
.duration(1500)
// stagger the transitions and introduce a delay of 1.5 seconds between each group of streets
.delay(d => (isMap ? d.properties.chartOrder : d.properties.mapOrder) + genderOrder[d.properties.gender] * 1500)
.ease(d3.easeLinear) // use linear motion
.attr('d',d => isMap ? d.properties.diagramPath : d.properties.mapPath)
.on('end', function(d,i){
if (i == sortedStreets.length-1) {
// last element has transitioned
button.value = isMap ? "Transition to Diagram" : "Transition to Map";
button.disabled = false;
}
});
isMap = !isMap
button.value = "... in transition ...";
})
return button
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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