Published
Edited
Aug 20, 2019
1 fork
1 star
Insert cell
md`# Label positioning - vertically

The problem is to position the labels of a bottle chart:

- vertically aligned labels
- with connecteros pointing to related slice of a bottle
- should be bounded in a specified box, from top and bottom
`
Insert cell
md`## Labels initial position`
Insert cell
{
const data = getDataPos();
const svg = d3.select(DOM.svg(width, height));
addBox(svg, bbox);
addRecords(svg, data);
addLines(svg, data);
const nodes = svg.selectAll('text').data(data);
nodes.enter().append('text').text(d => d.label)
.attr('x', d=>d.x).attr('y', d => d.y + d.value/2 + lineHeight/2);
return svg.node();
}
Insert cell
md`## Lables positioned by forced-layout`
Insert cell
{
let data = getDataPos()
const svg = d3.select(DOM.svg(width, height));
addBox(svg, bbox);
addRecords(svg, data);
data = positionAndRemove(data);
addLines(svg, data)
.attr('y2', d => d.y - lineHeight / 2);
let nodes = svg.selectAll('text').data(data);
let nodesEnter = nodes.enter().append('text').text(d=>d.label).attr('x', d => d.x)
.attr('y', d=> d._y + d.value/2 + lineHeight/2)
.attr('x', d => d.x).attr('y', d => d.y)

return svg.node();
}
Insert cell
position = (data) => {
var iters = 600;
var thresh = 0.001;
const rCollide = 10;
data.forEach(d => { d.y = d._y + d.value/2 + lineHeight /2});
// console.log('process> data.length', data.length);
const max = d3.max(data, d=>d.value);
function boxForce(alpha) {
data.forEach(d => {
let nextY = d.y + d.vy;
if (nextY < bbox.y) { d.vy = 0, d.y = bbox.y }
if (nextY > bbox.y + bbox.height) { d.vy = 0, d.y = bbox.y + bbox.height }
});
}

// FORCE ========================
const simulation = d3.forceSimulation(data)
.force('charge', d3.forceCollide().strength(0.7).radius(rCollide))
.force('bounded', boxForce)
.force('y', d3.forceY().strength(d=>(d.value/max)**2).y(d => d._y + d.value/2 + lineHeight/2))
// .alpha(0.1) // Defaults to alpha = 0.1
.stop();
for (var i = iters; i > 0; --i) {
simulation.tick();
if(simulation.alpha() < thresh) {
break;
}
}
return data;
}
Insert cell
positionAndRemove = data => {
const OVERLAP_THRESHOLD = 0
let iters = 10;
for (let i = iters; i > 0; --i) {
data = position(data);
let maxOverlap;
if (i==1) break;
for (let d1 of data) {
for (let d2 of data) {
if (d1.id == d2.id) continue;
const overlap = calcOverlap(d1, d2);
// console.log(overlap);
if (!maxOverlap || maxOverlap.overlap < overlap) {
maxOverlap = ({overlap, id: d1.value < d2.value ? d1.id : d2.id, d1, d2});
}
}
}
console.log('maxOverlap', maxOverlap);
if (maxOverlap.overlap < OVERLAP_THRESHOLD) break;
data = data.filter(d => d.id !== maxOverlap.id);
}
return data;
}
Insert cell
addLines = (svg, data) => {
const x = 10;
const rw = 40;

let lines = svg.selectAll('line').data(data)
const linesEnter = lines.enter().append('line')
.attr('x1', d => x + rw).attr('y1', d => d._y + d.value/2)
.attr('x2', 100).attr('y2', d => d._y + d.value/2).style('stroke', 'blue')
lines = lines.merge(linesEnter);
return lines;
}
Insert cell
addRecords = (svg, data) => {
const x = 10;
const rw = 40;

svg.selectAll('rect.record').data(data).enter()
.append('rect').attr('class', 'record').attr('width', rw).attr('height', d => d.value)
.attr('x', x).attr('y', d => d._y).style('fill', 'none').style('stroke', 'red');
}
Insert cell
addBox = (svg, {width, height, x, y}) => {
svg.append('rect')
.attr('width', width).attr('height', height)
.attr('x', x).attr('y', y).style('stroke', 'black')
.attr('fill', 'none');
}
Insert cell
calcOverlap({y: 25, value: 20}, {y: 22, value: 25})
Insert cell
calcOverlap = (box1, box2) => {
return Math.max(0, Math.min(box1.y + lineHeight, box2.y + lineHeight) - Math.max(box1.y, box2.y))
}
Insert cell
getDataPos = () => data.map((d, i) => ({
id: i,
label: `water-#${i}-${d}`,
value: d,
x: width/4,
fx: 100,
y: height/3 + d3.sum(data.slice(0, i), d=>d),
_y: height/3 + d3.sum(data.slice(0, i), d=>d),
}))
Insert cell
getDataPos()
Insert cell
data = [1,1,2,30, 3,1,1,1,1,1,1,1, 4, 40].sort((a,b)=>a-b)
Insert cell
bbox = { return {width: 200, height: 100, x: 80, y: 90} };
Insert cell
lineHeight = 10;
Insert cell
width = 400;
Insert cell
height = 300;
Insert cell
d3 = require("d3")
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