Public
Edited
Nov 30, 2023
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 (event, d) {
mutable hovering = d.title
d3.select(this)
.attr('opacity', 0.5)
})
// Listen to the "mouseout" event, and reset the value of "hovering" and color
.on('mouseout', function (event, d) {
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', 'lavender')
.on('click', function (event, d){
mutable clicked = d.title
d3.select(this)
.attr('opacity', 0.5)
})
.on('contextmenu', function (event, d){
mutable clicked = 'None'
d3.select(this)
.attr('opacity', 1)
})
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((event, 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>`)
// 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}`, 'YYYY-M').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}`, 'YYYY-M').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', function (event, d) {
mutable selectedMonth = d.date
d3.select(this)
.attr('opacity', 0.5)
})
.on('dblclick', function (event, d) {
mutable selectedMonth = 'None'
d3.select(this)
.attr('opacity', 1)
})
.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
{
const song = _.find(spotifySongs, { trackName })
const data = spotifyDailyGlobalRanking
.filter(d => d.trackId === song.trackId && d.date.substring(0, 7) === selectedMonth)
const maxStreams = data.reduce((max, item) => {
return (item.streams > max) ? item.streams : max;
}, 0);
return maxStreams
}
Insert cell
moment(selectedMonth).daysInMonth()
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
// Prepare the data
const song = _.find(spotifySongs, { trackName })
const data = spotifyDailyGlobalRanking
.filter(d => d.trackId === song.trackId && d.date.substring(0, 7) === selectedMonth)
const svg = d3.select(DOM.svg(width - 200, height))
const margin = { left: 70, top: 10, right: 30, bottom: 50 }
// And then visualize
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))
.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, 817490])
svg.append('g')
.call(d3.axisLeft(yScale).tickFormat(d3.format('.2s')))
.attr('transform', `translate(${margin.left},0)`)
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', d => xScale(parseInt((d.date.substring(8,10)), 10)))
.attr('y', d => yScale(d.streams))
.attr('height', d => yScale(0) - yScale(d.streams))
.attr('width', xScale.bandwidth())
.attr('fill', 'SteelBlue')
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, 25])
// 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 *
*******************************************************************************/
// 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.easeBounceOut)
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((event, 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(2000)
.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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more