Published
Edited
Nov 21, 2019
Insert cell
Insert cell
Insert cell
Insert cell
// Draw a Pokeball with SVG! Writing SVG code is exacly like writing HTML code

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 linearScale2 = d3.scaleLinear()
.range([0, 720])
.domain([0, 12])
let array2 = [1, 1.5, 6, 7.5, 12]
return array2.map(n => linearScale2(n))
}


/******************************************************************************
* 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 divColorScale = d3.scaleLinear()
.interpolate(() => d3.interpolateRdYlBu)
.domain([0, 100])
let deneme =[10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(n => divColorScale(n))
return legend(divColorScale)
}
/******************************************************************************
* 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: 30 }
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.! *
******************************************************************************/



/******************************************************************************
* 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
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! *
******************************************************************************/



/******************************************************************************
* 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.! *
******************************************************************************/



/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
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! *
******************************************************************************/



/******************************************************************************
* 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 3 lines (min/max/mean)! *
* Notice: If you see a chart full of cluttering, don't worry, it is expected *
******************************************************************************/



/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
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
/******************************************************************************
* TODO: *
* Try to rearrange the order of the y-axes! i.e. chaing the order of the *
* dimensions array, like [attack, defense, sp_attack, sp_defense, hp, speed] *
******************************************************************************/



/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
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
/******************************************************************************
* TODO: *
* Try to plot a radar chart for your favorite Pokemon! *
******************************************************************************/



/******************************************************************************
* END OF YOUR CODE *
******************************************************************************/
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

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