Published
Edited
Jun 23, 2021
Insert cell
md`# Stacked bar chart with hover

### References:

- http://bl.ocks.org/mstanaland/6100713
- https://observablehq.com/@d3/line-chart-with-tooltip

`
Insert cell
import {slider } from "@jashkenas/inputs"
Insert cell
viewof filter_year = slider({
min: 2019,
max: 2021,
title: "Years:",
step: 1
})
Insert cell
chart = {
const div = d3.create('div')
const legendContainer = div.append('div')
.attr('id', 'legend')
.style('margin-left', `${ margin.left + 25 }px`)
const svg = div.append('svg')
.attr('viewBox', [ 0, 0, width, height ])
const bar = svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", d => color(d.key))
const barSegment = bar.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", (d, i) => x(d.data.name))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth())
svg.append('g')
.call(xAxis)
svg.append('g')
.call(yAxis)
legendContainer
.call(legend)
yield div.node()
svg.call(hover, bar)
}
Insert cell
function hover(svg, bar) {
const textOffset = 45
const tooltip = svg.append('g')
.attr('class', 'tooltip')
.style('display', 'none')
const hoverBox = tooltip.append('rect')
.attr('width', 30)
.attr('height', 30)
.attr('fill', 'white')
.style('opacity', 0.5)
const text = tooltip.append('text')
.attr('x', textOffset)
.attr('dy', '12em')
.style('text-anchor', 'start')
.attr('font-size', '26px')
.attr('font-weight', 'bold')
bar
.on('mouseover', () => {
tooltip.style('display', null)
let { width } = text.node().getBBox()
hoverBox.attr('width', `${ width + textOffset+10 }px`)
})
.on('mouseout', () => tooltip.style('display', 'none'))
.on('mousemove', function(d) {
let xPosition = d3.mouse(this)[0] - 15
let yPosition = d3.mouse(this)[1] - 25
let index = datumIndexByXCoord(d3.mouse(this)[0])

tooltip.attr('transform', `translate( ${ xPosition }, ${ yPosition })`)
tooltip.select('text').text(`${ d.key }: ${ d[index][1] - d[index][0] }`)
})
}
Insert cell
function datumIndexByXCoord(layerX) {
let names = data.map(d => d.name)
let bandwidth = x.bandwidth()
/***
* Build a scale that looks like
*
* d3.scaleThreshold()
* .domain([ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45 ])
* .range([ null, 1, null, 2, null, 3, null, 4, null, 5, null ])
*
* so that any given x coordinate maps either to the key for bar it sits in (the bounds being
* the x coord of the bar, or that plus the bar's width), or to null when it sits in between bars.
*/
let thresholds = names.flatMap(n => [ x(n), x(n) + bandwidth + 1 ])
let results = [ null ]
names.forEach(n => results.push(n, null))
let scale = d3.scaleThreshold()
.domain(thresholds)
.range(results)
let name = scale(layerX)
console.log(name)
return data.map(d => d.name).indexOf(name)
}
Insert cell
function legend(div) {
const elements = div.selectAll('div.element')
.data(series)
.join('div.element')
.style('float', 'left')
.style('position', 'relative')
.style('font-family', 'sans-serif')
.style('margin-right', '30px')
elements.append('span.color')
.style('position', 'absolute')
.style('left', 0)
.style('top', 0)
.style('background', d => color(d.key))
.style('width', '18px')
.style('height', '80%')
elements.append('span.legend-key')
.style('margin-left', '39px')
.text(d => d.key)
}
Insert cell
yAxis = g => g
.attr('transform', `translate(${ margin.left }, 0)`)
.call(d3.axisLeft(y).ticks(null, 's'))
.call(g => g.selectAll('.domain').remove())
Insert cell
xAxis = g => g
.attr('transform', `translate(0, ${ height - margin.bottom })`)
.call(d3.axisBottom(x).tickSizeOuter(0))
.call(g => g.selectAll('.domain').remove())
Insert cell
y = d3.scaleLinear()
.domain([ 0, d3.max(series, d => d3.max(d, d => d[1]))])
.rangeRound([ height - margin.bottom, margin.top ])
Insert cell
x = d3.scaleBand()
.domain(data.map(d => d.name))
.range([margin.left, width - margin.right])
.padding(0.34)
Insert cell
color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
// TODO: annotate what is happening here
.range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), series.length).reverse())
.unknown('#ccc')
Insert cell
height = 400
Insert cell
margin = ({ top: 50, right: 10, bottom: 20, left: 40 })
Insert cell
series = d3.stack().keys(data.columns.slice(1))(data) // TODO: annotate what is actually happening here
Insert cell
data = Object.assign(_data, { columns: _.keys(_data[0])})
Insert cell
/***
* As if we had pivoted on gender, asked for experience overall, and are charting the counts of each
* gender that responded with each possible score
*/
_data = [
{ name: 'prototype', applied: 20, supported: 5},
{ name: 'startup', applied: 25, supported: 11},

]
Insert cell
d3 = require('d3@5')

Insert cell
_ = require('lodash')
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