Published
Edited
Nov 15, 2019
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Add a title element inside the element to be annotated
// Try to move your mouse over the Pokeball and the button on it!

html`
<svg width=120 height=120>
<circle cx=60 cy=60 r=50 stroke='black' fill='white'>

<!-- Title element can contain any form of text, and text only -->
<title>I'm a Pokeball!</title>

</circle>

<!-- When fill='none', it needs "pointer-events='visible'" to make the tooltip triggerable -->
<circle cx=60 cy=60 r=10 stroke='black' fill='none' pointer-events='visible'>

<!-- Move your mouse to the small circle -->
<title>What's inside!?</title>

</circle>
<line x1=10 x2=50 y1=60 y2=60 stroke='black' />
<line x1=70 x2=110 y1=60 y2=60 stroke='black' />
</svg>
`
Insert cell
Insert cell
// D3.js is built on top of SVG, so it is very similar to the SVG example
// Try to move your mouse over the circle above!

{
const height = 60
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
svg.selectAll('circle')
.data([{ x: 30, y: 30, r: 30, title: 'Details on demand!' }])
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', 'SteelBlue')
// Append a title element inside the circle
.append('title')
// It can be multilined, but becareful with the indentation
.text(d => `${d.title}
cx: ${d.x}
cy: ${d.y}
r: ${d.r}`)
return svg.node()
}
Insert cell
Insert cell
// Variable to demonstrate mouseover and mouseout events

mutable hovering = 'None'
Insert cell
// Try to move your mouse over the circles above!

{
const height = 60
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const data = [
{ x: 30, y: 30, r: 30, title: 'I am the 1st circle!' },
{ x: 90, y: 30, r: 30, title: 'I am the 2nd circle!' },
{ x: 150, y: 30, r: 30, title: 'I am the 3rd circle!' },
{ x: 210, y: 30, r: 30, title: 'I am the 4th circle!' }
]
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', 'SteelBlue')
// Listen to the "mouseover" event, and set the value of "hovering" and change the color
.on('mouseover', function (d, i) {
mutable hovering = data[i].title
d3.select(this)
.attr('opacity', 0.5)
})
// Listen to the "mouseout" event, and reset the value of "hovering" and color
.on('mouseout', function (d, i) {
mutable hovering = 'None'
d3.select(this)
.attr('opacity', 1)
})
return svg.node()
}
Insert cell
// Variable to reflect the clicking, change its value in your code below!

mutable clicked = "None"
Insert cell
/******************************************************************************
* TODO: *
* Try to replace the "mouseover" event to "click" event, and change the *
* value of the "clicked" variable! *
******************************************************************************/

{
const height = 60
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const data = [
{ x: 30, y: 30, r: 30, title: 'I am the 1st circle!' },
{ x: 90, y: 30, r: 30, title: 'I am the 2nd circle!' },
{ x: 150, y: 30, r: 30, title: 'I am the 3rd circle!' },
{ x: 210, y: 30, r: 30, title: 'I am the 4th circle!' }
]
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', 'SteelBlue')
.on('click', function (d, i) {
mutable clicked = data[i].title
svg.selectAll('circle').attr('opacity', 1.0);
d3.select(this)
.attr('opacity', 0.5)
})
return svg.node()
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 50, bottom: 20 }
const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right])
.domain(d3.extent(pokemonBaseStats.map(d => d.x)))
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain(d3.extent(pokemonBaseStats.map(d => d.y)))
// Setup the tooltip style, these are html and css, you can always lookup for their usage
// by searching the keywords
const tooltip = d3tip()
.style('border', 'solid 3px black')
.style('background-color', 'white')
.style('border-radius', '10px')
.style('float', 'left')
.style('font-family', 'monospace')
.html(d => `
<img style='float: left' width=96 height=96 src="${getPokemonPNG(d.pokedex_number)}"/>
<div style='float: right'>
Pokedex: ${d.pokedex_number} <br/>
Name: ${d.name} <br/>
Base Total: ${d.base_total} <br/>
Types: ${d.type1} ${d.type2}
</div>`)
// Apply tooltip to our SVG
svg.call(tooltip)
svg.selectAll('circle')
.data(pokemonBaseStats)
.enter()
.append('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 5)
.attr('opacity', 0.5)
.attr('fill', d => d.is_legendary ? 'GoldenRod' : 'SteelBlue')
// When "mouseover" event triggers, show the tooltip
.on('mouseover', tooltip.show)
// Hide the tooltip when "mouseout"
.on('mouseout', tooltip.hide)
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Dropdown menu contains the 10 songs in spotifySongs, with title and a default value

// "viewof" is a keyword in Observable notebook, it creates a binding between "trackName"
// and the underlying dropdown menu HTML input element
viewof trackName = select({
title: 'Pick a song:',
options: spotifySongs.map(song => song.trackName),
value: spotifySongs[0].trackName
})
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 400
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 70, top: 10, right: 30, bottom: 50 }
// Find the track information by "trackName", mostly we want the trackId
const song = _.find(spotifySongs, { trackName })
// Go through a chain of process with "spotifyDailyGlobalRanking" dataset
const data = _.chain(spotifyDailyGlobalRanking)
// Filter the dataset to only the data of selected track
.filter(d => d.trackId === song.trackId)
// Group data by month
.groupBy(d => d.date.substring(0, 7))
// Calculate the average of each month
.map((list, date) => ({ date, streams: _.sumBy(list, 'streams') / list.length }))
// Lodash keeps track of the chain with other information, .value() declares we are
// done with the process, and returns the end result, which in our case, is an array
// of the daily average streams of each month
.value()
const xScale = d3.scaleBand()
.padding(0.1)
.range([margin.left, width - margin.right])
.domain(_.range(1, 13).map(d => moment(`2017-${d}`).format('YYYY-MM')))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Use "maxDailyStreams" instead of calculating from the filtered data to keep the
// chart consistent across different songs
.domain([0, maxDailyStreams])
svg.append('g')
.call(d3.axisLeft(yScale).tickFormat(d3.format('.2s')))
.attr('transform', `translate(${margin.left},0)`)
// Make the column chart with "rect", its attributes can be found in the documentation:
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', d => xScale(d.date))
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.attr('width', xScale.bandwidth())
.attr('fill', 'SteelBlue')
.append('title')
.text(d => `${d.date}: ${Math.round(d.streams)}`)
return svg.node()
}
Insert cell
Insert cell
// Update this variable using a click event

mutable selectedMonth = '2017-12'
Insert cell
/******************************************************************************
* TODO: *
* Try to add a click event to the column chart above, let the viewers to *
* select a month by clicking on the "rect"! *
* Hint: use .on('click', d => {}) to set the value of "selectedMonth" in the *
* format of "2017-12" *
******************************************************************************/
{
const height = 400
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 70, top: 10, right: 30, bottom: 50 }
// Find the track information by "trackName", mostly we want the trackId
const song = _.find(spotifySongs, { trackName })
// Go through a chain of process with "spotifyDailyGlobalRanking" dataset
const data = _.chain(spotifyDailyGlobalRanking)
// Filter the dataset to only the data of selected track
.filter(d => d.trackId === song.trackId)
// Group data by month
.groupBy(d => d.date.substring(0, 7))
// Calculate the average of each month
.map((list, date) => ({ date, streams: _.sumBy(list, 'streams') / list.length }))
// Lodash keeps track of the chain with other information, .value() declares we are
// done with the process, and returns the end result, which in our case, is an array
// of the daily average streams of each month
.value()
const xScale = d3.scaleBand()
.padding(0.1)
.range([margin.left, width - margin.right])
.domain(_.range(1, 13).map(d => moment(`2017-${d}`).format('YYYY-MM')))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Use "maxDailyStreams" instead of calculating from the filtered data to keep the
// chart consistent across different songs
.domain([0, maxDailyStreams])
svg.append('g')
.call(d3.axisLeft(yScale).tickFormat(d3.format('.2s')))
.attr('transform', `translate(${margin.left},0)`)
// Make the column chart with "rect", its attributes can be found in the documentation:
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', d => xScale(d.date))
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.attr('width', xScale.bandwidth())
.attr('fill', 'SteelBlue')
.on('click', d => {
mutable selectedMonth = moment(d.date).format('YYYY-MM')
})
.append('title')
.text(d => `${d.date}: ${Math.round(d.streams)}`)
return svg.node()
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
Insert cell
Insert cell
{
// Prepare the data
const song = _.find(spotifySongs, { trackName })
const data = spotifyDailyGlobalRanking
.filter(d => d.trackId === song.trackId && d.date.substring(0, 7) === selectedMonth)

// And then visualize
return vegaEmbed({
data: { values: data },
width: width - 200,
height: 400,
mark: 'bar',
encoding: {
x: {
field: 'date',
type: 'o',
timeUnit: 'date',
scale: {
// Instead of automatically inferring from data, we provide the range
// Otherwise, the missing data will not be shown as a missing bar
domain: _.range(1, moment(selectedMonth).daysInMonth() + 1)
}
},
// Adjust the axis label to 1 significant digit after decimal point
y: { field: 'streams', type: 'q', axis: { format: '.1s' } }
}
})
}
Insert cell
Insert cell
/******************************************************************************
* TODO: *
* Try to plot the column chart using D3 instead! *
* You don't need to make the exact same plot as Vega-Lite, just the *
* essential components will be good enough. *
******************************************************************************/
{
const height = 400
const svg = d3.select(DOM.svg(width, height))
const tooltip = d3tip()
.style('border', 'solid 1px black')
.style('background-color', 'white')
.style('border-radius', '10px')
.style('float', 'left')
.style('font-family', 'monospace')
.style('font-size', '12px')
.html(d => `
<div>
Date (date): ${moment(d.date).date()} <br/>
Streams: ${d.streams} <br/>
</div>`)
svg.call(tooltip)
const margin = { left: 70, top: 10, right: 30, bottom: 50 }
const song = _.find(spotifySongs, { trackName })
const data = spotifyDailyGlobalRanking
.filter(d => d.trackId === song.trackId && d.date.substring(0, 7) === selectedMonth)
const xScale = d3.scaleBand()
.padding(0.1)
.range([margin.left, width - margin.right])
.domain(_.range(1, moment(selectedMonth).daysInMonth() + 1))
svg.append('g')
.call(d3.axisBottom(xScale).tickFormat(d3.format('02d')))
.attr('transform', `translate(0,${height - margin.bottom})`)
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(data.map(d => d.streams))])
svg.append('g')
.call(d3.axisLeft(yScale).tickFormat(d3.format('.2s')))
.attr('transform', `translate(${margin.left},0)`)
svg.append('text')
.attr('transform', `translate(${width/2},${height})`)
.style('text-anchor', 'middle')
.style('font-weight', 'bold')
.text('Date (date)')
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 0)
.attr('x',0 - (height / 2))
.attr('dy', '1em')
.style('text-anchor', 'middle')
.style('font-weight', 'bold')
.text('Streams')
svg.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('x', d => xScale(moment(d.date).date()))
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.attr('width', xScale.bandwidth())
.attr('fill', 'SteelBlue')
.on('mouseover', tooltip.show)
.on('mousemove', tooltip.show)
.on('mouseout', tooltip.hide)
return svg.node()
}


/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
// Slider with integer values range from 0 to 364, each corresponding to a date in 2017

viewof dateIn2017Slider = slider({
title: 'Pick a date:',
value: 180,
min: 0,
max: moment([2017]).isLeapYear() ? 365 : 364,
step: 1,
display: v => moment([2017, 0, 1]).add(v, 'd').format('dddd, MMMM D YYYY')
})
Insert cell
Insert cell
{
const date = moment([2017, 0, 1]).add(dateIn2017Slider, 'd').format('YYYY-MM-DD')
const data = spotifyDailyGlobalRanking.filter(d => d.date === date)
.map(d => ({ ...d, trackName: _.find(spotifySongs, { trackId: d.trackId }).trackName }))
return vegaEmbed({
data: { values: data },
width: width - 200,
height: 400,
mark: 'bar',
encoding: {
x: {
field: 'trackName',
type: 'n',
// We provide the domain instead of auto-infer, to keep the plot consistent among changes
scale: { domain: spotifySongs.map(song => song.trackName) }
},
y: {
field: 'streams',
type: 'q',
axis: { format: '.1s' },
// Same reason as above to provide the domain
scale: { domain: [0, maxDailyStreams] }
}
}
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Choropleth plot (map regions encoded with colors)

{
// Prepare data
const song = _.find(spotifySongs, { trackName })
const date = moment([2017, 0, 1]).add(dateIn2017Slider2, 'd').format('YYYY-MM-DD')
const data = spotifyDailyRanking
// Filter out the selected song and selected date
.filter(d => d.trackId === song.trackId && d.date === date)
// Because we need to lookup the geographic information of the region, we need to find the
// cca3 code of the region using the countries dataset
.map(d => {
const country = countries[d.region.toUpperCase()]
return { ...d, cca3: country.cca3, country: country.name }
})

return vegaEmbed({
width: width - 200,
height: 400,
data: {
// Visualization with geographic data is not drawing data onto a map, instead,
// we draw the map while color encoding according to the data values
// This "worldTopoJson" dataset contains the borders of all the countries on Earth and
// we draw them out one by one, if there is a record in our data, we give that region a
// color according to the number of streams
values: worldTopoJson,
format: {
type: 'topojson',
feature: 'countries-land-10km'
}
},
// Vega-Lite transform, looking up values from our data, like a table join
transform: [{
lookup: 'properties.A3',
from: {
data: { values: data },
key: 'cca3',
fields: ['streams', 'country']
}
}],
// How do we project the latitude and longitude onto 2D screen
// See https://vega.github.io/vega/examples/world-map/ for more available projections
projection: {
type: 'equirectangular'
},
mark: 'geoshape',
encoding: {
// Color encode the number of streams in different regions
color: {
field: 'streams',
type: 'q',
scale: { scheme: 'blues', domain: [0, maxDailyRegionStream] }
},
// Provide a tooltip to show the details
tooltip: [
{field: 'streams', type: 'q'},
{field: 'country', type: 'n'}
]
}
})
}
Insert cell
Insert cell
Insert cell
// Variable holding the steps of the animation below
// Rerun this cell to see the animation below

mutable dateIn2017Mutable = 0
Insert cell
{
if (dateIn2017Mutable < 364) {
// Delay for 500 ms, yield is a keyword in Javascript, which is relayed to the concept
// of generator. It will take up a long passage (if not an article) to explain, just
// take it as a loop that keep updating the value of "dateIn2017Mutable" until 364
yield Promises.delay(500, dateIn2017Mutable)
// Then increment the variable
mutable dateIn2017Mutable++
} else {
// Do not increment if it is 31st December
yield dateIn2017Mutable
}
}
Insert cell
Insert cell
Insert cell
// Keep producing a new plot each half a second, we now have an animation of 2 frames per second
{
const date = moment([2017, 0, 1]).add(dateIn2017Mutable, 'd').format('YYYY-MM-DD')
const data = spotifyDailyGlobalRanking.filter(d => d.date === date)
.map(d => ({ ...d, trackName: _.find(spotifySongs, { trackId: d.trackId }).trackName }))
return vegaEmbed({
data: { values: data },
width: width - 200,
height: 400,
mark: 'bar',
encoding: {
x: {
field: 'trackName',
type: 'n',
scale: { domain: spotifySongs.map(song => song.trackName) }
},
y: {
field: 'streams',
type: 'q',
axis: { format: '.1s' },
scale: { domain: [0, maxDailyStreams] }
}
}
})
}
Insert cell
Insert cell
Insert cell
Insert cell
// Rerun this cell to see the animated transition
{
const height = 60
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }

const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right])
.domain([0, 40])
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)
// Just append a circle, nothing special here yet!
svg.selectAll('circle')
.data([5])
.enter()
.append('circle')
.attr('cx', d => xScale(d))
.attr('cy', 20)
.attr('r', 10)
.attr('fill', 'SteelBlue')
// This is the update!
svg.selectAll('circle')
// Gives a new dataset, replacing 5 with 30
.data([30])
// Tell D3.js to make a transition
.transition()
// Update the visual representation, it can be any attributes, cx, cy, r, color, etc.
// And it must be placed after .transition()
.attr('cx', d => xScale(d))
// That last for 5000 ms
.duration(5000)
// With "d3.easeCubic" easing
.ease(d3.easeCubic)
return svg.node()
}
Insert cell
/******************************************************************************
* TODO: *
* Try other easing methods, duration and data value! e.g. d3.easeBounceOut! *
* A full list of easing methods can be found in the documentation: *
* https://github.com/d3/d3-ease *
*******************************************************************************/

{
const height = 60
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }

const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right])
.domain([0, 40])
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)
svg.selectAll('circle')
.data([5])
.enter()
.append('circle')
.attr('cx', d => xScale(d))
.attr('cy', 20)
.attr('r', 10)
.attr('fill', 'SteelBlue')
svg.selectAll('circle')
.data([30])
.transition()
.attr('cx', d => xScale(d))
.duration(1500)
.ease(d3.easeBackOut)
return svg.node()
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
Insert cell
{
const height = 300
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 30, right: 30, bottom: 20 }

const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right])
.domain([0, 10])
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, 40])
svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
const colorScale = d3.scaleOrdinal(d3.schemeSet3)
.domain([0, 10])
let circles = svg.selectAll('circle')
let data = []
while (true) {
// No effect to this plot, just to print the values in the cell above
mutable dataArray = data

yield svg.node()

// Update data
const update = circles
.data(data)
// Remove SVG elements associated with the deleted data points
update.exit().remove()
// Add SVG elements for the newly added data points
circles = update.enter()
.append('circle')
.attr('cx', (d, i) => xScale(i + 1))
.attr('r', 20)
.attr('fill', (d, i) => colorScale(i))
// We want the variable "circles" to hold not just the set of newly added
// data points, but all the exisiting data points
// .merge is the function to combine two sets
.merge(circles)
// If nothing to animate, don't wait
if (data.length !== 0) {
// Wait until the transition animation is finished
await circles
.transition()
.attr('cy', d => yScale(d))
.ease(d3.easeBounceOut)
.duration(1000)
// calling .end() is important, it returns a "waitable" object for "await" to
// wait for. It is a concept call "Promise", similar to the concept of generator,
// it will take a long passage to explain, just take it as a sandglass to wait for
.end()
}
if (data.length >= 1)
data[data.length - 1] = 0
if (data.length <= 10)
data.push(40)
if (data.length > 10 && data[data.length - 1] === 0)
data = []
}
}
Insert cell
Insert cell
Insert cell
// Rerun this cell to see the animation
{
const height = 400
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 70, top: 10, right: 30, bottom: 50 }
// A function to retrieve the data of the specified date
// It always returns an array with all 10 songs
const rankingOnDate = (date) => {
const dateString = moment(date).format('YYYY-MM-DD')
const data = spotifyDailyGlobalRanking.filter(d => d.date === dateString)
return spotifySongs.map(song => {
const ranking = _.find(data, { trackId: song.trackId })
return ranking || { trackId: song.trackId, streams: 0 }
})
}
const xScale = d3.scaleBand()
.padding(0.1)
.range([margin.left, width - margin.right])
.domain(spotifySongs.map(song => song.trackId))
svg.append('g')
.call(d3.axisBottom(xScale)
.tickFormat(v => _.find(spotifySongs, { trackId: v }).trackName.substr(0, 15)))
.attr('transform', `translate(0,${height - margin.bottom})`)
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, maxDailyStreams])
svg.append('g')
.call(d3.axisLeft(yScale).tickFormat(d3.format('.2s')))
.attr('transform', `translate(${margin.left},0)`)
// Make the text at the top right corner
const dateText = svg.append('text')
.text(`Spotify Streams on ${moment([2017, 0, 1]).format('YYYY-MM-DD')}`)
.attr('transform', `translate(${width - 300}, 20)`)
const bars = svg.selectAll('rect')
.data(rankingOnDate('2017-01-01'))
.enter()
.append('rect')
.attr('x', d => xScale(d.trackId))
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.attr('width', xScale.bandwidth())
.attr('fill', 'SteelBlue')
let date = moment([2017, 0, 1])
while (true) {
yield svg.node();

// Increment by 1 day
date = date.add(1, 'd')
// Stop if it is 31st December, reminded that month starts counting from 0
if (date.isAfter([2017, 11, 31])) break
// Update the text at the top right corner
dateText.text(`Spotify Streams on ${date.format('YYYY-MM-DD')}`)
await bars
// Because the "rankingOnDate" always return all 10 songs, so that we only
// need to focus on updating the values and transitions, no .enter(), .exit()
// or .merge() is needed
.data(rankingOnDate(date))
.transition()
.duration(500)
.ease(d3.easeCubic)
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.end()
}
}
Insert cell
Insert cell
// Rerun this cell to see the animation
{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 50, bottom: 20 }
const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right])
.domain(d3.extent(pokemonBaseStats.map(d => d.x)))
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain(d3.extent(pokemonBaseStats.map(d => d.y)))
const tooltip = d3tip()
.style('border', 'solid 3px black')
.style('background-color', 'white')
.style('border-radius', '10px')
.style('float', 'left')
.style('font-family', 'monospace')
.html(d => `
<img width=96 height=96 src="${getPokemonPNG(d.pokedex_number)}"/>
<div style='float: right'>
Pokedex: ${d.pokedex_number} <br/>
Name: ${d.name} <br/>
Base Total: ${d.base_total} <br/>
Types: ${d.type1} ${d.type2}
</div>`)
svg.call(tooltip)
let circles = svg.selectAll('circle')
let data = []
while(true) {
yield svg.node()
const update = circles
.data(data)
update.exit().remove()
circles = update.enter()
.append('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 0)
.attr('opacity', 0.5)
.attr('fill', d => d.is_legendary ? 'GoldenRod' : 'SteelBlue')
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide)
.merge(circles)
if (data.length !== 0) {
await circles
.transition()
.attr('r', 10)
.ease(d3.easeElasticOut)
.duration(1000)
.end()
}
if (data.length < pokemonBaseStats.length) {
data.push(pokemonBaseStats[data.length])
} else {
break
}
}
}
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