Public
Edited
Nov 14, 2022
Insert cell
Insert cell
Insert cell
Insert cell
// Draw a Pokeball with SVG! Writing SVG code is exacly like writing HTML code
// Work done by Li FUk Sang

html`
<svg width=120 height=120>
<circle cx=60 cy=60 r=50 stroke='black' fill='none' /> <!-- outter circle -->
<circle cx=60 cy=60 r=10 stroke='black' fill='none' /> <!-- inner circle -->
<line x1=10 x2=50 y1=60 y2=60 stroke='black' /> <!-- left horizontal bar -->
<line x1=70 x2=110 y1=60 y2=60 stroke='black' /> <!-- right horizontal bar -->
</svg>
`
Insert cell
Insert cell
// Linear scale, mapping 0 - 40 to 0 - 100

linearScale = d3.scaleLinear()
.range([0, 100])
.domain([0, 40])
Insert cell
[10, 20, 30, 40].map(n => linearScale(n))
Insert cell
/******************************************************************************
* TODO: *
* Try to make a scale that maps 0 - 12 to 0 - 720! e.g. 1.5 -> 90 *
******************************************************************************/

{
let newScale = d3.scaleLinear().range([0,720]).domain([0,12]);
return (newScale(1.5));
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
// We also use scale when encoding values with color
// Remember that color scale can be diverging, sequential and categorical
// the following demonstrate how to build a sequential color scale

colorScale = d3.scaleLinear()
.interpolate(() => d3.interpolateOranges)
.domain([0, 40])
Insert cell
[10, 20, 30, 40].map(n => colorScale(n))
Insert cell
legend(colorScale)
Insert cell
/******************************************************************************
* TODO: *
* Try to make a diverging color scale with "d3.interpolateRdYlBu" color *
* scheme with domain [0, 100]! *
******************************************************************************/

{
let diverging = d3.scaleLinear().interpolate(()=>d3.interpolateRdYlBu).domain([0, 100]);
return (legend(diverging));
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
Insert cell
Insert cell
// Width of available space of an output cell on Observable notebook
// We use it as the maximum width of our drawing "canvas", it is a global
// variable, declared and set by Observable notebook

width
Insert cell
{
// While width is given, we need to define the height of our drawing "canvas"
const height = 60
// Define our svg
const svg = d3.select(DOM.svg(width, height))
// Make some empty space around
// Try to set the margins to zeros and see what happens!
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const xScale = d3.scaleLinear()
// Instead of mapping (0 - 40) to (0 - 100), we map (0 - 40) to (30 - 945) to fill the width
.range([margin.left, width - margin.right])
.domain([0, 40])
// Append it to svg
svg.append('g').call(d3.axisBottom(xScale))
// Return it, and Observable will show it as output
return svg.node()
}
Insert cell
Insert cell
{
const height = 600
const svg = d3.select(DOM.svg(60, height))

const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const yScale = d3.scaleLinear()
// For y-axis, we map (0 - 40) to (580 - 10) to fill the height
// If you wonder why we are mapping the values reversely, 0 -> 580 and 40 -> 10,
// as in the coordination system of our screens, the top left corner is (0, 0)
// and bottom left corner is (0, 600)
.range([height - margin.bottom, margin.top])
.domain([0, 40])
// Append it to svg
svg.append('g')
.call(d3.axisLeft(yScale))
// Need to shift the axis a little bit in order to show the tick labels
// See more about "transform" in the cell below
.attr('transform', `translate(${margin.left},0)`)
return svg.node()
}
Insert cell
Insert cell
Insert cell
{
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})`)
// ----- The above are the same as plotting x-axis in the previous section -----
// Add circles to svg, more explanation in the cell below
svg.selectAll('circle')
.data([10, 20, 30, 40])
.enter()
.append('circle')
.attr('cx', d => xScale(d))
.attr('cy', 20)
.attr('r', 10)
.attr('fill', 'SteelBlue')
return svg.node()
}
Insert cell
Insert cell
/******************************************************************************
* TODO: *
* Try to draw the 4 circles vertically with y-axis! *
* Also try to change their color, radius, stroke-color, etc.! *
******************************************************************************/

{
// This code is taken from the previous cell of plotting the y axis
const height = 600
const svg = d3.select(DOM.svg(60, height))

const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const yScale = d3.scaleLinear()
// For y-axis, we map (0 - 40) to (580 - 10) to fill the height
// If you wonder why we are mapping the values reversely, 0 -> 580 and 40 -> 10,
// as in the coordination system of our screens, the top left corner is (0, 0)
// and bottom left corner is (0, 600)
.range([height - margin.bottom, margin.top])
.domain([0, 40])
// Append it to svg
svg.append('g')
.call(d3.axisLeft(yScale))
// Need to shift the axis a little bit in order to show the tick labels
// See more about "transform" in the cell below
.attr('transform', `translate(${margin.left},0)`)

//Modified from previous cell in plotting the x axis
svg.selectAll('circle')
.data([10, 20, 30, 40]) //The data
.enter()
.append('circle')
.attr('cy', d => yScale(d)) //Modify the height attribute
.attr('cx', 50)
.attr('r', 10)
.attr('fill', 'SteelBlue')
return svg.node()
}


/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
Insert cell
// Example dataset grows in exponential scale

exponentialData = _.range(0, 5).map(x => ({ x, y: 5 * Math.pow(2, x) }))
Insert cell
exponentialData
Insert cell
d3.extent(exponentialData.map(d => d.x))
Insert cell
d3.extent(exponentialData.map(d => d.y))
Insert cell
/******************************************************************************
* TODO: *
* Try to make another exponential dataset with exponents of 3 and use *
* d3.extent to find the minimum and maximum values of the y values! *
******************************************************************************/

{
// This is lodash
let exponential3 = _.range(0, 5).map((x) => ({ x, y: 5 * Math.pow(3, x) })); //implicit return

return(d3.extent(exponential3.map(d => d.y)));
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
Insert cell
{
const height = 600
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(d3.extent(exponentialData.map(d => d.x)))
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(d3.extent(exponentialData.map(d => d.y)))
svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
svg.selectAll('circle')
.data(exponentialData)
.enter()
.append('circle')
// Circles are distributed across x-axis
.attr('cx', d => xScale(d.x))
// Across y-axis as well, and it becomes two dimensional
.attr('cy', d => yScale(d.y))
.attr('r', 10)
.attr('fill', 'SteelBlue')
return svg.node()
}
Insert cell
Insert cell
Insert cell
/******************************************************************************
* TODO: *
* Try to plot the pokemon dataset! *
* Also try to encode with color, radius, stroke-color, etc.! *
******************************************************************************/
//Code modified from prev cell on exponential data
//Plotting the attack as y axis and defense as x axis
//Also encoding the health with radius

{
const height = 600
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(d3.extent(pokemonBaseStats.map(d => d.defense)))
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(d3.extent(pokemonBaseStats.map(d => d.attack)))

const healthScale = d3.scaleLinear()
.range([0, 10])
.domain([0, 255]) //Code cell below
svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
svg.selectAll('circle')
.data(pokemonBaseStats)
.enter()
.append('circle')
.attr('cx', d => xScale(d.attack))
.attr('cy', d => yScale(d.defense))
.attr('r', d => healthScale(d.hp))
.attr('fill', 'SteelBlue')
return svg.node()
}


/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
d3.extent(pokemonBaseStats.map(d => d.hp))
Insert cell
pokemonBaseStats
Insert cell
Insert cell
Insert cell
Insert cell
// Drawing a line with SVG
// See the "d" attribute of the "path" element? How on earth do we come up with that string!?

html`
<svg width=600 height=200>
<path d='M100,190L200,180L300,160L400,120L500,40' stroke='SteelBlue' fill='none'/>
</svg>
`
Insert cell
Insert cell
// Create a line generator

lineGenerator = d3.line()
Insert cell
// Adjust the logarithmic data to a scale nicer for plotting
// We will later use a scale function instead

lineData = exponentialData.map(d => [d.x * 100, 200 - d.y])
Insert cell
// Generate the string used for the "d" attribute above

lineGenerator(lineData)
Insert cell
Insert cell
{
const height = 200
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(d3.extent(exponentialData.map(d => d.x)))
const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain(d3.extent(exponentialData.map(d => d.y)))

// Line generator, scaling x and y to fill the width and height
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))

svg.selectAll('path')
// .data() expects an array of multiple series, each series with multiple data points
// We have only one line to plot, so we pass in an array with only one series, and this
// series has many data points in it
.data([exponentialData])
.enter()
.append('path')
// Reminded that d is a series of data points, that is, an array of values
// And the line function call takes in this array of data points
.attr('d', d => line(d))
// Remove the default value of color fill, try to comment out this line and see what happens
.attr('fill', 'none')
// The default of stroke is none, while we disabled the color fill, we make the line visible
// by setting the stroke color
.attr('stroke', 'SteelBlue')

return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
// This is rather very similar to the line chart with logarithmicData
// Only changed 3 lines of code, and substituted the dataset

{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
// Use d3.scaleTime() instead of d3.scaleLinear() for datetime
const xScale = d3.scaleTime()
.range([margin.left, width - margin.right])
// Substitute dataset
.domain(d3.extent(HKDailyTemperature.map(d => d.date)))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)

const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Substitute dataset
.domain(d3.extent(HKDailyTemperature.map(d => d.mean)))

svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.mean))

svg.append('g')
.selectAll('path')
// Substitute dataset
.data([HKDailyTemperature])
.enter()
.append('path')
.attr('d', d => line(d))
.attr('fill', 'none')
.attr('stroke', 'SteelBlue')

return svg.node()
}
Insert cell
/******************************************************************************
* TODO: *
* Try to plot the HKDailyTemperature dataset with max instead of mean! *
******************************************************************************/

//Code modified from prev cell
{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
// Use d3.scaleTime() instead of d3.scaleLinear() for datetime
const xScale = d3.scaleTime()
.range([margin.left, width - margin.right])
// Substitute dataset
.domain(d3.extent(HKDailyTemperature.map(d => d.date)))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)

const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Substitute dataset
// Changing the extent to max
.domain(d3.extent(HKDailyTemperature.map(d => d.max)))

svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.max))

svg.append('g')
.selectAll('path')
// Substitute dataset
.data([HKDailyTemperature])
.enter()
.append('path')
.attr('d', d => line(d))
.attr('fill', 'none')
.attr('stroke', 'SteelBlue')

return svg.node()
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
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: 10, bottom: 20 }
const xScale = d3.scaleTime()
.range([margin.left, width - margin.right])
.domain(d3.extent(HKMonthlyTemperature.map(d => d.month)))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)

const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Find the domain of temperature value
.domain(d3.extent(_.flatMap(HKMonthlyTemperature, d => [d.min, d.max])))

svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
const categories = ['min', 'max', 'mean']
// Use categorical color scheme to encode different lines
const colorScale = d3.scaleOrdinal(d3.schemeAccent)
.domain(categories)

const line = d3.line()
// Use i to retrieve the month value
.x((d, i) => xScale(HKMonthlyTemperature[i].month))
.y(d => yScale(d))
svg.append('g')
.selectAll('path')
// Two series of data instead of one
.data([
_.map(HKMonthlyTemperature, 'min'),
_.map(HKMonthlyTemperature, 'mean'),
_.map(HKMonthlyTemperature, 'max')
])
.enter()
.append('path')
.attr('d', d => line(d))
.attr('fill', 'none')
// Color encodes the category of the line (min, mean or max)
.attr('stroke', (d, i) => colorScale(categories[i]))

return svg.node()
}
Insert cell
/******************************************************************************
* TODO: *
* Try to plot the HKDailyTemperature dataset with max instead of mean! *
******************************************************************************/
//Code modified from prev cell
{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 10, right: 10, bottom: 20 }
const xScale = d3.scaleTime()
.range([margin.left, width - margin.right])
//Changing Scale
.domain(d3.extent(HKDailyTemperature.map(d => d.date)))
svg.append('g')
.call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${height - margin.bottom})`)

const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
// Find the domain of temperature value
.domain(d3.extent(_.flatMap(HKDailyTemperature, d => [d.min, d.max])))

svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left},0)`)
const categories = ['min', 'max', 'mean']
// Use categorical color scheme to encode different lines
const colorScale = d3.scaleOrdinal(d3.schemeAccent)
.domain(categories)

const line = d3.line()
// Use i to retrieve the month value
.x((d, i) => xScale(HKDailyTemperature[i].date))
.y(d => yScale(d))
svg.append('g')
.selectAll('path')
// Two series of data instead of one
.data([
_.map(HKDailyTemperature, 'min'),
_.map(HKDailyTemperature, 'mean'),
_.map(HKDailyTemperature, 'max')
])
.enter()
.append('path')
.attr('d', d => line(d))
.attr('fill', 'none')
// Color encodes the category of the line (min, mean or max)
.attr('stroke', (d, i) => colorScale(categories[i]))

return svg.node()
}

/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
Insert cell
HKDailyTemperature
Insert cell
Insert cell
// Create a legend

{
const height = 80
const svg = d3.select(DOM.svg(width, height))
// Need to yield the svg node first before adding the legend
// It is rather a technical issue, see https://observablehq.com/@mbostock/d3-legend
yield svg.node()
const legend = d3.legendColor()
.scale(d3.scaleOrdinal(d3.schemeAccent).domain(['min', 'mean', 'max']))
svg.append("g").call(legend);
}
Insert cell
Insert cell
Insert cell
{
const height = 600
const svg = d3.select(DOM.svg(width, height))
const margin = { left: 30, top: 30, right: 10, bottom: 20 }
const dimensions = ['hp', 'attack', 'sp_attack', 'defense', 'sp_defense', 'speed']
const data = pokemonBaseStats.map(d => dimensions.map(dimension => d[dimension]))
// Use scalePoint because x-axis domain is discrete
const xScale = d3.scalePoint()
.range([margin.left, width - margin.right])
.domain(dimensions)
// Plot x-axis at the top, remove the line stroke
svg.append('g')
.call(d3.axisTop(xScale))
.attr('transform', `translate(0,${margin.top})`)
.selectAll('path')
.attr('stroke', 'none')

// Make one y-scale for each dimension
const yScales = dimensions.map(dimension =>
d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain(d3.extent(pokemonBaseStats.map(d => d[dimension])))
)

// Plot axes for each dimension
dimensions.forEach((dimension, i) => {
svg.append('g')
.call(d3.axisLeft(yScales[i]))
.attr('transform', `translate(${xScale(dimension)},0)`)
})

// Line generator, carefully handle each dimension
const line = d3.line()
.x((d, i) => xScale(dimensions[i]))
.y((d, i) => yScales[i](d))
// Just like a line chart!
svg.append('g')
.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', d => line(d))
.attr('fill', 'none')
.attr('stroke', 'SteelBlue')

return svg.node()
}
Insert cell
Insert cell
Insert cell
// My favorite Pokemon, Snorlax, a.k.a. 卡比獸 in Chinese

pokemonBaseStats[142]
Insert cell
Insert cell
{
// Radius of radar chart
const r = 200
const margin = { left: 30, top: 30, right: 30, bottom: 30 }
// Need to handle the view box carefully, to ensure the plot is centered, surrounded by margins
const svg = d3.select(DOM.svg(r * 2, r * 2))
.attr('viewBox',
`-${margin.left},
-${margin.top},
${r * 2 + margin.left + margin.right},
${r * 2 + margin.bottom + margin.top}`)
const dimensions = ['hp', 'attack', 'sp_attack', 'defense', 'sp_defense', 'speed']
// Line generator for radial lines
const radialLine = d3.lineRadial()
// Radar chart is a circle, the length of each axis is the radius of the circle
// Mapping 0 - 255 to 0 - r
const yScale = d3.scaleLinear()
.range([0, r])
.domain([0, 255])
// The default tick marks is not ideal, override it with a customized one
const ticks = [50, 100, 150, 200, 255]
// One axis for each dimension
dimensions.forEach((dimension, i) => {
// We first build an axis at the origin, enclosed inside an "g" element
// then transform it to the right position and right orientation
const g = svg.append('g')
.attr('transform', `translate(${r}, ${r}) rotate(${i * 60})`)

// Combining a left oriented axis with a right oriented axis
// to make an axis with tick marks on both side
// Reminded that, these are "g" elements inside the outer "g" element
// and will be transformed to the right position with its parent element
g.append('g')
.call(d3.axisLeft(yScale).tickFormat('').tickValues(ticks))
g.append('g')
.call(d3.axisRight(yScale).tickFormat('').tickValues(ticks))

// Add a text label for each axis, put it at the edge
// Again, this "text" element is inside the outer "g" element,
// and will be transformed to the right position with its parent element
g.append('text')
.text(dimension)
.attr('text-anchor', 'middle')
.attr('transform', `translate(0, -${r + 10})`)
})
// Line for the base stats of Snorlax
svg.append('g')
.selectAll('path')
.data([pokemonBaseStats[142]])
.enter()
.append('path')
.attr('d', d =>
radialLine([
d.hp,
d.attack,
d.sp_attack,
d.defense,
d.sp_defense,
d.speed,
d.hp // hp again to close the loop
].map((v, i) => [Math.PI * 2 * i / 6 /* radian */, yScale(v) /* distance from the origin */]))
)
// Move to the center
.attr('transform', `translate(${r}, ${r})`)
.attr('stroke', 'SteelBlue')
.attr('stroke-width', 5)
.attr('fill', 'rgba(70, 130, 180, 0.3)')
// Gird lines for references
svg.append('g')
.selectAll('path')
.data(ticks)
.enter()
.append('path')
.attr('d', d => radialLine(_.range(7).map((v, i) => [Math.PI * 2 * i / 6, yScale(d)])))
.attr('transform', `translate(${r}, ${r})`)
.attr('stroke', 'grey')
.attr('opacity', 0.5)
.attr('fill', 'none')

return svg.node()
}
Insert cell
pokemonBaseStats
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Warning: The following plot is rather computation intensive, comment it out if you experience lagging.

chart = {
const r = 30
const height = 600
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 + r, width - margin.right - r])
.domain(d3.extent(pokemonBaseStats.map(d => d.x)))
const yScale = d3.scaleLinear()
.range([height - margin.bottom - r, margin.top + r])
.domain(d3.extent(pokemonBaseStats.map(d => d.y)))
const radialLine = d3.lineRadial()
const radialScale = d3.scaleLinear()
.range([0, r])
.domain([0, 255])
svg.selectAll('circle')
.data(pokemonBaseStats)
.enter()
// For each Pokemon
.each(function (d) {
d3.select(this)
.append('g')
.selectAll('path')
.data([d])
.enter()
.append('path')
.attr('d', d => radialLine([
d.hp,
d.attack,
d.sp_attack,
d.defense,
d.sp_defense,
d.speed,
d.hp
].map((v, i) => [Math.PI * i / 3, radialScale(v)])) )
.attr('transform', `translate(${xScale(d.x)}, ${yScale(d.y)})`)
.attr('stroke', 'black')
.attr('stroke-width', d => d.is_legendary ? 3 : 2)
.attr('fill', d => d.is_legendary ? 'DarkGoldenRod' : 'Crimson')
.attr('opacity', 0.4)
})

return svg.node()
}
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