Published
Edited
Apr 30, 2019
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
journey = [
{
country: "Vietnam",
numericCode: "704",
projectionScaleZoomOut: 400,
projectionScaleZoomIn: 1100,
startYear: 1968,
endYear: 1970,
title: "Vietnam War",
description: "Fought between North Vietnam and South Vietnam from 1955 to 1975, the Vietnam War was a proxy war between the United States and the Soviet Union. An estimated 1.3 to 4.2 million people were killed, with more than half being Vietnamese civilians.",
link: "https://en.wikipedia.org/wiki/Vietnam_War",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/Vietnam%20War.jpg"
},
{
country: "Cambodia",
numericCode: "116",
projectionScaleZoomOut: 1100,
projectionScaleZoomIn: 1100,
startYear: 1971,
endYear: 1977,
title: "Cambodian Genocide",
description: "With the goal of turning the country into a socialist agrarian republic, the Khmer Rouge regime forced millions into labor camps where they were starved and abused. An estimated 1.7 to 1.9 million Cambodian (21—24% of the population) died during the 4-year period.",
link: "https://en.wikipedia.org/wiki/Cambodian_genocide",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/Cambodian%20Genocide.jpg"
},
{
country: "Timor-Leste",
numericCode: "626",
projectionScaleZoomOut: 900,
projectionScaleZoomIn: 1800,
startYear: 1972,
endYear: 1977,
title: "East Timor Genocide",
description: "During the US-backed invasion and occupation of East Timor from 1975 to 1999, the Indonesian military commited genocide against the East Timorese people. An estimated 100,000 to 300,000 East Timorese (14—42% of the population) were killed or died of starvation.",
link: "https://en.wikipedia.org/wiki/East_Timor_genocide",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/Indonesian%20Invasion%20of%20East%20Timor.jpg"
},
{
country: "Iran",
numericCode: "364",
projectionScaleZoomOut: 500,
projectionScaleZoomIn: 950,
startYear: 1978,
endYear: 1982,
title: "Iran–Iraq War",
description: "In an attempt to replace Iran as the dominant Persian Gulf state, Iraq invaded Iran in 1980, starting the 8-year long war between the two countries. It is estimated that over a million lives were lost during the war.",
link: "https://en.wikipedia.org/wiki/Iran%E2%80%93Iraq_War",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/Iran%E2%80%93Iraq%20War.jpg"
},
{
country: "Rwanda",
numericCode: "646",
projectionScaleZoomOut: 500,
projectionScaleZoomIn:2000,
startYear: 1987,
endYear: 1992,
title: "Rwandan Genocide",
description: "The Rwandan genocide was a mass slaughter of the Tutsis directed by members of the Hutu-majority government during the Rwandan Civil War from 1990 to 1994. An estimated 500,000 to 1,000,000 Rwandans were killed, constituting an estimated 70% of the Tutsi population.",
link: "https://en.wikipedia.org/wiki/Rwandan_genocide",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/Rwandan%20Genocide.jpg"
},
{
country: "Zambia",
numericCode: "894",
projectionScaleZoomOut: 680,
projectionScaleZoomIn: 680,
startYear: 1985,
endYear: 2002,
title: "HIV/AIDS Pandemic in Sub-Saharan Africa",
description: "The 1990s was a bleak time in the history of AIDS in Africa. In 1993 there were an estimated 9 million people infected in the sub-Saharan region out of a global total of 14 million. In the countries most affected, AIDS lowered life expectancy by about twenty years.",
link: "https://en.wikipedia.org/wiki/HIV/AIDS_in_Africa",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/AIDS.jpg"
},
{
country: "Kazakhstan",
numericCode: "398",
projectionScaleZoomOut: 400,
projectionScaleZoomIn: 500,
startYear: 1988,
endYear: 1993,
title: "Dissolution of the Soviet Union",
description: "Following the dissolution of the Soviet Union in 1991, Russia experienced one of the most extreme increases in mortality in modern history. Hidden behind the high rates of cardiovascular disease and injury caused by alcohol abuse is a sense of being unneeded by the state.",
link: "https://en.wikipedia.org/wiki/Dissolution_of_the_Soviet_Union",
imgSrc: "https://cdn.theatlantic.com/assets/media/img/photo/2011/12/20-years-since-the-fall-of-the-soviet-union/u01_77728401/main_1200.jpg"
},
{
country: "Dem. Rep. Korea",
numericCode: "408",
projectionScaleZoomOut: 700,
projectionScaleZoomIn: 1335,
startYear: 1992,
endYear: 1997,
title: "North Korean Famine",
description: "A combination of government mismanagement, loss of Soviet support, and a series of floods and droughts lead to the North Korean famine from 1994 to 1998. An estimated 240,000 to 3.5 million North Koreans died from starvation or hunger-related illnesses.",
link: "https://en.wikipedia.org/wiki/North_Korean_famine",
imgSrc: "https://github.com/Jianan-Li/File-Hosting/raw/master/North%20Korean%20Famine.jpg"
},
{
country: "Syria",
numericCode: "760",
projectionScaleZoomOut: 500,
projectionScaleZoomIn: 1630,
startYear: 2007,
endYear: 2013,
title: "Syrian Civil War",
description: "What started as a peaceful protest in 2011 demanding democratic reforms quickly escalated into full-blown warfare involving several nations, rebel groups and terrorist organizations. An estimated 370,000 to 570,000 people have been killed, and more than 5 millions became refugees.",
link: "https://en.wikipedia.org/wiki/Syrian_Civil_War",
imgSrc: "https://www.history.com/.image/c_limit%2Ccs_srgb%2Cq_auto:good%2Cw_1720/MTU4NTQyMDg3Mzc2MDg2Mzcy/syria-civil-war-getty-462518530.webp"
},
]
Insert cell
countryIndex = {
nextButton;
console.log('Calculating the next countryIndex.')
if (typeof this === 'undefined') yield -1
else if (this === journey.length-1) {
yield 0
}
else {
let countryIndex=this
yield ++countryIndex
}
}
Insert cell
i = {
let i = this || 0;
if (countryIndex !== -1) return yield i;
while (true) {
let [a,b,c] = projection.rotate()
projection.rotate([a+1,b,c])
updateOnProjectionChange()
yield Promises.tick(100, i++);
}
}
Insert cell
handleButtonClick = {
if (countryIndex === -1) {
if (d3.select('#quote-container').empty()) {
showQuote()
}
}
else {
d3.select('#nextButton')
.text("→");
if (d3.select('#quote-container').node()) {
removeQuoteContainer()
}
if (projection.translate()[0] === width/2) {
await moveToLeftTwoThirds()
}
if (d3.select('#journey-info-container').empty()) {
addJourneyInfoContainer()
}
if (d3.select('#legend-container').empty()) {
addLegend()
}
if (d3.select('.yearText').empty()) {
addYear()
}

await visitCountry()
}
}
Insert cell
Insert cell
showQuote = () => {
let svg = d3.select(chart)
let quoteContainer = svg
.append('g')
.attr('id', 'quote-container')
quoteContainer
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height)
.attr('fill', '#aaa')
.attr('fill-opacity', '0.5')
quoteContainer
.append('text')
.attr('text-anchor', 'middle')
.attr('x', width/2)
.attr('y', height*0.4)
.style('font', `bold ${width/24}px Palatino, serif`)
.text('Human suffering anywhere concerns')
quoteContainer
.append('text')
.attr('text-anchor', 'middle')
.attr('x', width/2)
.attr('y', height*0.4 + width/24*1.4)
.style('font', `bold ${width/24}px Palatino, serif`)
.text('men and women everywhere.')
quoteContainer
.append('text')
.attr('text-anchor', 'middle')
.attr('x', width/2)
.attr('y', height*0.4 + width/24*1.4*2.8)
.style('font', `${width/30}px Palatino, serif`)
.text('Elie Wiesel')
}
Insert cell
removeQuoteContainer = () => {
let svg = d3.select(chart)
// Remove quote container
svg
.select('#quote-container')
.transition('remove-quote-container')
.duration(1000)
.style('opacity', 0)
.remove()
}
Insert cell
addLegend = () => {
let svg = d3.select(chart)
// Add legend scale
// const legendContainer = svg
// .append("g")
// .attr('id', 'legend-container')
// .attr("transform", `translate(${width/2-50},${height - 30})`);
// The right 1/3 showing relevant information
let legendContainer = svg.append('foreignObject')
.attr('x', width*(2/3))
.attr('y', height*0.85+20)
.attr('width', width*(1/3))
.attr('height', height*0.15-20)
.append("xhtml:div")
.attr('id', 'legend-container')
.style('background-color', '#fff')
.style('box-shadow', '0 1px 4px 0 rgba(0, 0, 0, 0.37)')
.style('border-radius', '5px 5px 0px 0px')
.style('margin-left', `10px`)
.style('width', `${width*(1/3)-20}px`)
.style('height', `${height*0.15-20}px`)
.style('overflow', 'hidden')
.style('opacity', 0);
let legendContainerSVG = legendContainer
.append("svg")
.style('width', `${width/3-20}px`)
.style('height', `${height*0.2-20}px`)
// The color gradient scale
legendContainerSVG.append("rect")
.attr('transform', `translate(${(width/3-20)/2},${(height*0.15-20)/2})`)
.attr("height", `${width/120}px`)
.attr("x", xScale(-2))
.attr("y", `${width/240}px`)
.attr("width", xScale(2) - xScale(-2))
.style("fill", `url(${location}#linear-gradient)`);

// Title
legendContainerSVG.append("text")
.attr('transform', `translate(${(width/3-20)/2},${(height*0.15-20)/2})`)
.attr("class", "caption")
.attr("x", 0)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", `${width/90}px`)
.text(`Change in life expectancy from the previous year`);
// Legend scale axis
legendContainerSVG
.call(xAxis)
.select(".domain")
.remove();
legendContainerSVG
.selectAll('.tick')
.selectAll('*')
.attr('transform', `translate(${(width/3-20)/2},${(height*0.15-20)/2})`)
legendContainerSVG
.selectAll('.tick')
.selectAll('text')
.attr('font-size', `${width/100}px`)
legendContainer
.transition('show-legend-container')
.duration(1000)
.style('opacity', 1)
}
Insert cell
addJourneyInfoContainer = () => {
let svg = d3.select(chart)
// The right 1/3 showing relevant information
let journeyInfoContainer = svg.append('foreignObject')
.attr('x', width*(2/3))
.attr('y', 10)
.attr('width', width*(1/3))
.attr('height', height*0.85+10)
.append("xhtml:div")
.attr('id', 'journey-info-container')
.style('background-color', '#fff')
.style('box-shadow', '0 1px 4px 0 rgba(0, 0, 0, 0.37)')
.style('border-radius', '5px')
.style('margin-left', `10px`)
.style('width', `${width*(1/3)-20}px`)
.style('height', `${height*0.85}px`)
.style('overflow', 'hidden')
.style('opacity', 0);
journeyInfoContainer.append("xhtml:img")
.attr('id', 'journey-info-img')
.style('width', '100%')
let journeyInfoTextContainer =
journeyInfoContainer.append("xhtml:div")
.attr('id', 'journey-info-text-container')
.style('padding', '20px')
journeyInfoTextContainer.append("xhtml:h2")
.attr('id', 'journey-info-header')
.style("font-size", `${width/50}px`)
journeyInfoTextContainer.append("xhtml:p")
.attr('id', 'journey-info-description')
.style('margin-top', '16px')
.style("font-size", `${width / 81.25}px`)
journeyInfoTextContainer.append("xhtml:a")
.attr('id', 'journey-info-link')
.style("font-size", `${width / 81.25}px`)
journeyInfoContainer
.transition('show-journey-info-container')
.duration(1000)
.style('opacity', 1)
}
Insert cell
addYear = () => {
let svg = d3.select(chart)
// Display the current year
let yearText = svg.append('text')
.attr("class", "yearText")
.attr("x", 5)
.attr("y", height-10)
.attr('font-size', `${width/15}px`)
.style("text-anchor", "start") //end
.text(1961)
// .call(halo, 10);
}
Insert cell
moveToLeftTwoThirds = async () => {
// Move projection to the left 2/3 of the svg
const [currentTranslationX, currentTranslationY] = projection.translate()
const targetTranslationX = width/3
const targetTranslationY = currentTranslationY
let translationXInterpolator = d3.interpolateNumber(currentTranslationX, targetTranslationX)
// Wait until completing move to new location
await d3.transition("moveToLeftTwoThirds")
.duration(3000)
.ease(d3.easeQuad)
.tween("render", () => t => {
projection.translate([translationXInterpolator(t), targetTranslationY]);
updateOnProjectionChange();
})
.end();
}
Insert cell
visitCountry = async () => {
let svg = d3.select(chart)
const currentEvent = journey[countryIndex]
const country = countries.find(d => d.id === currentEvent.numericCode);
const previousEventEndYear = (countryIndex > 0 && journey[countryIndex-1].endYear) || 1961
const currentEventTimeSpan = currentEvent.endYear - currentEvent.startYear
// Check the current projection rotation
const currentRotation = projection.rotate()
const currentScale = projection.scale()
const p1 = [currentRotation[0], currentRotation[1]]
const p2 = country.properties.centroid;
const r1 = currentRotation
const r2 = [-p2[0], tilt - p2[1], 0];
// Construct interpolators
const iv = Versor.interpolateAngles(r1, r2);
// Scaling interpolator: choose between quadratic and linear interpolator
let projectionScaleInterpolator;
let zoomOutScale = currentEvent.projectionScaleZoomOut*(width/975)
let zoomInScale = currentEvent.projectionScaleZoomIn*(width/975)
projectionScaleInterpolator = quadraticInterpolator(zoomOutScale,zoomInScale)
const yearTransitionInterpolator = d3.interpolateNumber(previousEventEndYear, currentEvent.startYear)
const yearProgressionInterpolator = d3.interpolateNumber(currentEvent.startYear, currentEvent.endYear)
updateJourneyInfo(currentEvent)
// Show country labels
svg
.select("#country-label-container")
.selectAll("text")
.transition('show-country-label')
.duration(1000)
.style('opacity', 1)
d3.transition("updateYearTransition")
.duration(500)
.ease(d3.easeQuad)
.tween("updateYear", () => t => {
mutable currentYear = Math.floor(yearTransitionInterpolator(t))
// updateOnYearChange(year)
})
// Wait until completing move to new location
await d3.transition("moveToNextLocation")
.duration(5000)
.ease(d3.easeQuad)
.tween("render", () => t => {
projection.rotate(iv(t));
projection.scale(projectionScaleInterpolator(t));
updateOnProjectionChange();
})
.end(); // .end() returns a promise so you can await on it
// (wait for it to finish before performing the next transition)
d3.transition("showEventTransition")
.duration(currentEventTimeSpan*1000)
.ease(d3.easeQuad)
.tween("showEvent", () => t => {
mutable currentYear = Math.floor(yearProgressionInterpolator(t))
// updateOnYearChange(year)
})
}
Insert cell
Insert cell
updateJourneyInfo = (currentEvent) => {

let svg = d3.select(chart);
svg.select('#journey-info-img')
.attr('src', currentEvent.imgSrc)
.attr('alt', currentEvent.title)
svg.select('#journey-info-header')
.text(currentEvent.title)
svg.select('#journey-info-description')
.text(currentEvent.description)
svg.select('#journey-info-link')
.attr('href', currentEvent.link)
.attr('target', '_blank')
.text('Learn more on Wikipedia →')
}
Insert cell
// Re-render when: 1. User rotate/zoom 2. Programmatic rotate/zoom
updateOnProjectionChange = () => {

let svg = d3.select(chart);
// Re-render all the paths elements: choropleth, border, sphere
svg
.selectAll("g")
.selectAll("path")
.attr('d', geoGenerator)
// For country lables, move, toggle visibility, change font size
svg
.select("#country-label-container")
.selectAll("text")
.attr('transform', generateCountryLabelTransformValue)
.style('visibility', d => geoGenerator(d)? 'visible':'hidden')
.style('font', generateCountryLabelFontValue)
}
Insert cell
updateOnYearChange = {
let svg = d3.select(chart);
// Add Chloropleth
svg
.select("#chloropleth-container")
.selectAll('path')
.transition('chloropleth-fill-transition')
.duration(200)
.attr("fill", d =>
life.has(d.id) && life.get(d.id)[currentYear+'delta'] ? color(+life.get(d.id)[currentYear+'delta']) : "#eee"
)
// Display the current year
svg
.selectAll('.yearText')
.text(currentYear)
// Change country label text color based on current year choropleth color value
svg
.select("#country-label-container")
.selectAll("text")
.attr('fill', d =>
life.has(d.id) && life.get(d.id)[currentYear+'delta'] && lightOrDark(color(+life.get(d.id)[currentYear+'delta'])) === 'dark' ?
"#fff" : "#000"
)
// mutable currentYear = year
}
Insert cell
Insert cell
Insert cell
// d3GeoZoom.geoZoom()
// .projection(projection)
// .northUp(true)
// .onMove(render)
// (chart)
Insert cell
Insert cell
mutable mouseOverCountry = countries[0]
Insert cell
updateTooltipText = {
tooltip.text(`${mouseOverCountry.properties.name} (${currentYear})\n${(+life.get(mouseOverCountry.id)[currentYear]).toFixed(1)}`)
}
Insert cell
onMouseMoveArea = (d) => {
console.log('Mouse move')
tooltip.style("top", (d3.event.pageY+20)+"px").style("left",(d3.event.pageX+10)+"px");
}
Insert cell
onMouseOutArea = (d) => {
console.log('Mouse out')
tooltip.text('')
tooltip.style("visibility", "hidden");
}
Insert cell
onMouseOverArea = (d) => {
console.log('Mouse over')
mutable mouseOverCountry = d

tooltip.style("visibility", "visible");
}
Insert cell
tooltip = {
return d3.select("#svg-tooltip").empty() ?
d3.select("body").append("div")
.attr("id", "svg-tooltip")
.text("")
: d3.select("#svg-tooltip")
}
Insert cell
Insert cell
Insert cell
projection = d3.geoOrthographic()
.fitExtent([[margin, margin], [width - margin, height - margin]], sphere)
.translate([width / 2, height / 2])
.precision(0.1)
Insert cell
sphere = ({type: "Sphere"})
Insert cell
xAxis = d3.axisBottom(xScale)
.tickSize(0)
.tickPadding(width/55)
.tickValues([-lifeExpectancyDeltaMax, 0, lifeExpectancyDeltaMax])
Insert cell
xScale = d3.scaleLinear()
.domain(d3.extent(color.domain()))
.rangeRound([-legendScaleWidth/2, legendScaleWidth/2]);
Insert cell
color = d3.scaleSequential(d3.interpolateRdYlGn)
.domain([-lifeExpectancyDeltaMax, lifeExpectancyDeltaMax])
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
quadraticInterpolator = (projectionScaleZoomOut, projectionScaleZoomIn) => {
let c = projection.scale()
if (c > projectionScaleZoomOut) {
let X = projectionScaleZoomIn - c //(a+b)
let Y = 4*( c - projectionScaleZoomOut ) //(b^2/a)

let b = - Math.sqrt(X*Y+(Y**2)/4) - Y/2
let a = X - b

return (t) => a*t**2 + b*t + c
} else {
let a = projectionScaleZoomIn - c // a
return (t) => a*t**2 + c
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
topojson.bbox(topojson.topology(countries[0]))
Insert cell
topojson.bbox(world)
Insert cell
Insert cell
countries = {
let result = topojson.feature(world, world.objects.countries).features
result.map(d => {
d.properties.name = countryNumericCodeToName.get(d.id)
d.properties.centroid = d3.geoCentroid(d)
d.properties.longitudeSpan = getCountryLongitudeSpan(d)
})
return result
}
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
Insert cell
html`<style>
#nextButton {
transition: background-color 0.2s;
font-size: 1rem;
width: 8rem;
height: 2.2rem;
text-decoration: none;
border-radius: 0.5rem;
outline: none;
cursor: pointer;
border: none;
background-color: #eee;
margin-top: 1.5rem;
}
#nextButton:hover {
background-color: #ddd;
}
#nextButton:active {
background-color: #ccc;
}

p, a, text{
font-family: sans-serif;
}
text.yearText{
font-weight: 700;
fill: #777;
}
#svg-tooltip {
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background: rgba(69,77,93,.9);
border-radius: .1rem;
color: #fff;
display: block;
font-size: 11px;
max-width: 320px;
padding: .2rem .4rem;
position: absolute;
text-overflow: ellipsis;
white-space: pre;
z-index: 300;
visibility: hidden;
}
.flex-container {
margin: 0;
display: flex;
justify-content: center;
height: 4rem;
}
</style>`
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