chart = {
const rowHeight = 45
const bands = 4
const padding = { top: 20, right: 10, bottom: 20, left: 10 }
const grouped = d3.group(data, d => d.name, d => d.gender)
const height = grouped.size * rowHeight
const x = d3.scaleTime()
.domain(yearExtent.map(y => new Date(y, 0, 1)))
.range([0, width - padding.left - padding.right])
const y = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.number) / bands])
.range([rowHeight, 0])
const color = d3.scaleOrdinal(d3.schemeAccent)
const area = d3.area()
.x(d => x(new Date(d.year, 0, 1)))
.y0(y(0))
.y1(d => y(d.number))
const svg = DOM.svg(width + padding.left + padding.right,
height + padding.top + padding.bottom)
const clip = DOM.uid('clip')
d3.select(svg).append('clipPath')
.attr('id', clip.id)
.append('rect')
.attr('width', width)
.attr('height', rowHeight)
const chart = d3.select(svg).append('g')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
.style('user-select', 'none')
.style('-moz-user-select', 'none')
.style('-webkit-user-select', 'none')
// axes
const axes = chart.selectAll('g.axis').data([0, height])
.enter().append('g')
.attr('transform', d => `translate(0, ${d})`)
.each(function (d) {
d3.select(this).call(d === 0 ? d3.axisTop(x) : d3.axisBottom(x))
})
// names
const names = chart.selectAll('g.name').data(Array.from(grouped))
.enter().append('g')
.attr('transform', (d, i) => `translate(0, ${i * rowHeight})`)
.attr('clip-path', clip)
// genders
const genders = names.selectAll('g.gender').data(d => Array.from(d[1]))
.enter().append('g')
for (let i = 0; i < bands; i++)
genders.append('path')
.attr('fill', d => color(d[0]))
.attr('opacity', 1/bands)
.attr('transform', `translate(0, ${i * rowHeight})`)
.attr('d', d => area(d[1]))
// name
names.append('text')
.attr('y', rowHeight - 5)
.text(d => d[0])
// values, brush
const rule = chart.append('g')
rule.append('rect')
.attr('width', 1)
.attr('height', height)
const brushTicks = rule.selectAll('text').data([-8.5, height + 17])
.enter().append('text')
.attr('y', d => d)
.attr('text-anchor', 'middle')
.attr('font-size', 12)
const opacity = d3.scalePow([0, 10], [0, 1]).exponent(2).clamp(true)
const values = genders.append('text')
.attr('text-anchor', 'end')
.attr('fill', d => color(d[0]))
.attr('y', d => d[0] === 'F' ? rowHeight - 5 : rowHeight - 25)
brush(x(x.domain()[1]))
d3.select(svg)
.on('mousemove', () => brush(d3.mouse(chart.node())[0]))
.on('touchmove', () => brush(d3.touches(chart.node())[0][0]))
function brush(px) {
const year = x.invert(px).getFullYear()
rule
.attr('transform', `translate(${px}, 0)`)
brushTicks
.text(year)
axes.selectAll('text')
.attr('opacity', d => opacity(Math.abs(d.getFullYear() - year)))
values
.attr('x', px - 5)
.text(d => {
const found = d[1].find(v => v.year == year) // TODO: bisect
return found ? format(found.number) : ''
})
}
return svg
}