Public
Edited
May 16, 2024
2 stars
Insert cell
Insert cell
dumbbell(data.data, {
title: data.label,
pairFill: [parentCol, childCol],
lineCol: lineCol,
// yDomain: null
})
Insert cell
Insert cell
Insert cell
Insert cell
function dumbbell (data, {
title = null,
pairLabels = null, // labels for the pair and order of dumbbell values/groups e.g. ['A', 'B']
valueKey = 'values', // keys to take data from that match the sets above
labelKey = 'label', // key for label e.g. 2022
pairFill = ['#0b3536', '#0098d8'], // colours for circles
width = 230,
height = 300,
radius = [8, 8], // size of circles ['A', 'B']
margin = {left: 60, top: 20, right: 60, bottom: 40},
yDomain = [0, 100], // pass in 'null' to calculate on min/max
bgdCol = '#efefef',
lineCol = '#f54123', // col of connecting line, the GAP
bgdLineCol = '#bfbabe', // background line
circleTextFill = bgdCol,
titleCol = '#555', // main title e.g. 'Group 1'
subTitleCol = '#888 ', // sub-titles e.g. 2022
} = {}) {

// have all the values in an flat array
const values = data.map(d => [ d[valueKey][0], d[valueKey][1] ]).flat()

const w = width + (margin.left + margin.right)
const h = height + (margin.top + margin.bottom)
const min = (yDomain) ? yDomain[0] : d3.min(values);
const max = (yDomain) ? yDomain[1] : d3.max(values);
const yScale = d3.scaleLinear()
.domain([min, max])
.range([h - margin.bottom, margin.top]);
const xScale = d3.scaleLinear()
.domain([0, data.length -1])
.range([margin.left, w - margin.right]);
const svg = DOM.svg(w, h);
const sel = d3.select(svg);
sel.append('rect')
.attr('width', w)
.attr('height', h)
.attr('rx', 10)
.attr('fill', bgdCol);
// append the title
sel.append('text')
.attr('class', 'title')
.attr('y', margin.top)
.attr('x', w / 2)
.attr('text-anchor', 'middle')
.style('font-size', '13px')
.style('fill', titleCol)
.style('line-height', '160%')
.text(title);
// group for each segment
const join = sel.selectAll('g')
.data(data)
.join('g')
.attr('transform', (d, i) => {
return `translate(${xScale(i)}, ${margin.top})`
});
// lines for each column
join.append('line')
.attr('x1', 0)
.attr('y1', margin.top)
.attr('x2', 0)
.attr('y2', height)
.attr('stroke', bgdLineCol)
.attr('stroke-dasharray', 2);

// connecting line line
join.append('line')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1',d => yScale(d[valueKey][0]))
.attr('y2', d => yScale(d[valueKey][1]))
.attr('stroke', lineCol)
.attr('stroke-width', 4);
// show the difference
join.append('text')
.attr('text-anchor', 'start')
.style('font-size', '11px')
.style('fill', d3.rgb(lineCol).darker(1))
.attr('dy', 4)
.attr('y', d => {
const diff = d[valueKey][0] - d[valueKey][1]
console.log('diff', diff)
if (diff >= 0) {
return yScale(d[valueKey][0] - (diff/2))
} else {
return yScale(d[valueKey][1] + (diff/2))
}
})
.attr('dx', 12)
.text(d => {
const diff = d[valueKey][0] - d[valueKey][1]
if (diff > 0) {
return `+${diff}`
} else {
return `${diff}`
}
})

// render set 1
renderCircles(join, 0)
// render set 2
renderCircles(join, 1)

function renderCircles(sel, index) {
// circles for set one
sel.append('circle')
.attr('fill', pairFill[index])
.attr('r', radius[index])
.attr('stroke', bgdCol)
.attr('stroke-width', 1)
.attr('cy', d => yScale(d[valueKey][index]));
// show pct
sel.append('text')
// .attr('text-anchor', (index === 0 ) ? 'start' : 'end')
.attr('text-anchor', 'end')
.style('font-size', '11px')
.attr('dy', 4)
.attr('y', d => yScale(d[valueKey][index]))
// .attr('dx', (index === 0 ) ? 12 : - 44) // place labels on left or right
.attr('dx', - 14) // place labels on left so diff is on right
.text((d, i) => (i === 0) ? `${d[valueKey][index]}%` : d[valueKey][index]); // show % on first value only
}

// show date/cat
join.append('text')
.attr('text-anchor', 'middle')
.style('font-size', '11px')
.attr('dy', 3)
.attr('y', h - margin.bottom)
.style('fill', subTitleCol)
.text(d => d.label);
return svg;
}
Insert cell
Insert cell
data = { return {
label: 'Group A',
pair: ['Parent', 'Child'], // labels for the sets and order of values
data: [
{
label: '2020',
values: [12.6, 34.5] // sets: ['Parent', 'Child']
},
{
label: '2021',
values: [62.6, 22.9]
},
{
label: '2022',
values: [92.6, 86.1]
}
]
}
};
Insert cell
<hr>
<link href="https://fonts.googleapis.com/css?family=Space+Mono" rel="stylesheet">
<style>
text {
font-family:'Space Mono',monospace;
fill: #130C0E;
}
</style>
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