Skip to content

Instantly share code, notes, and snippets.

@EE2dev
Last active November 7, 2017 22:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EE2dev/6b3ef8e44f9bfaada80fd33ea3dd942e to your computer and use it in GitHub Desktop.
Save EE2dev/6b3ef8e44f9bfaada80fd33ea3dd942e to your computer and use it in GitHub Desktop.
Alignment of text labels - 2

This is an example how to compute the position of text labels when the axis is rescaled. The text labels and the ticks of the axis are transitioned to the destination while satisfying:

  • that the labels do not vertically overlap with each other
  • that the labels stay vertically within a give frame
  • that the labels are transitioned left or right of the new element depending on the empty space

see also: https://bl.ocks.org/ee2dev/fc880e1cfbb80f649878f3d5b9e8ed93

<!DOCTYPE html>
<meta charset="utf-8">
<style>
rect {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
rect.before {
stroke: red;
}
rect.after {
stroke: orange;
}
rect.final {
stroke: green;
}
</style>
<div>
<button type="button" onclick="toggleTrans()">Toggle transition</button>
</div>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!--script src="../lib/d3_v4_2_1/d3.js"></script-->
<script>
var categories = ["cat1", "cat2", "cat3", "cat4", "cat5", "cat6", "cat7", "cat8"];
var catSubset = ["cat1", "cat2", "cat3", "cat4", "cat5", "cat6", "cat7", "cat8"];
var toggle = true;
// set up basic SVG
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("rect")
.attr("class", "frame")
.attr("width", width)
.attr("height", height);
var yScale = d3.scalePoint()
.domain(categories)
.range([height, 40]);
var axisLeft = d3.axisLeft(yScale);
var axisSelection = g.append("g")
.attr("class", "axis left")
.attr("transform", "translate(80, -20)")
.call(axisLeft);
// generate sorted data
var data = [];
var dataSubset = [];
for (var i=0; i<categories.length; i++) { data.push(20 + i* (height - 40)/(categories.length - 1));}
data.sort(function(a, b){return b - a});
// draw elements
drawElements(g, data);
function drawElements(sel, data){
var gNew = sel.append("g")
.attr("transform", "translate(80, 0)");
gNew1 = gNew.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("class", "position")
.attr("transform", function(d) {return "translate(100, " + d + ")";});
gNew1.append("line")
.style("stroke", "steelblue")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 100)
.attr("y2", 0);
}
function updateElements(_gd){
var data = _gd.ds;
var xTrans = _gd.xStart;
var transition = d3.transition().duration(2000);
d3.selectAll("g.position")
.data(data)
.transition(transition)
.attr("transform", function(d) {return "translate(" + xTrans + ", " + d + ")";});
}
function toggleTrans() {
if (toggle) {
var gData = generateData();
updateElements(gData);
transitionAxisToLabels(elementsToLabel, catSubset, axisSelection, height, width);
} else {
transitionBack(axisSelection, catSubset);
}
toggle = !toggle;
}
function transitionAxisToLabels(elementsToLabel, catSubset, axis, height, width){
var pData = getPositionData(elementsToLabel, catSubset, height, width);
transitionAxis(pData, axis, catSubset);
}
function generateData() {
// generate random but sorted data
var data = [];
var dataSubset = [];
for (var i=0; i<8; i++) { data.push(Math.round(Math.random() * (height)));}
data.sort(function(a, b){return b - a});
categories.forEach(function(d,i) {
if (catSubset.indexOf(d) !== -1) { dataSubset.push(data[i]); }
});
var xStartElement = Math.round(Math.random() * 200);
elementsToLabel = {};
catSubset.forEach(function(d,i) {
elementsToLabel[d] = {
index: i,
xLeft: xStartElement,
xRight: xStartElement + 100,
yDest: dataSubset[i],
value: "12.5% " + d
};
});
return {ds: dataSubset, xStart: xStartElement} ;
}
// transition labels
function transitionAxis(_pData,_axis, _catSubset) {
var transition = d3.transition().duration(2000);
var transitionEnd = d3.transition().duration(0);
var ele = _pData[Object.keys(_pData)[0]]; // pick first element since x values are the same
translateX = ele.xLabel; // x value of element - x translation of scale
var sel = _axis.selectAll("g.tick").data(_catSubset, function(d){ return d;})
.each(function(d,i){
// compute y transform
var yOffset = d3.select(this).attr("transform");
var translate = yOffset.indexOf(" ") !== -1 ? // hack
yOffset.substring(yOffset.indexOf("(")+1, yOffset.indexOf(")")).split(" ") //for IE with translate(x y)
: yOffset.substring(yOffset.indexOf("(")+1, yOffset.indexOf(")")).split(","); //for browsers with translate(x,y)
var transYOffset = +translate[1] - 20 + 0.5; // 20: translate g.axis, 0.5 translate line
var translateY = _pData[d].yLabel - transYOffset;
d3.select(this).selectAll("text")
.transition(transition)
.attr("transform", function(d) {
var translateXNew = (_pData[d].labelLeft) ? translateX : translateX + _pData[d].textWidth;
return "translate(" + translateXNew + ", " + translateY + ")";})
.style("font-size", "16px")
.attr("dy", "0.35em") // slight transition from 0.32 otherwise animation ends with jump
.on("end", function (){
d3.select(this).text(function(d) {return _pData[d].value;});
});
d3.select(this).selectAll("line")
.transition(transition)
.attr("x1", 0)
.attr("y1", function(d){ return 0.5 + _pData[d].yTrans;})
.attr("x2", function(d){ return ele.labelLeft ? -6 : 6;})
.attr("transform", function(d) {return "translate(" + translateX + ", " + translateY + ")";});
});
sel.exit()
.transition(transition)
.style("opacity", 0);
_axis.selectAll("path.domain")
.transition(transition)
.style("opacity", 0);
}
// transition back
function transitionBack(_axis, _catSubset) {
var transition = d3.transition().duration(2000);
var sel = _axis.selectAll("g.tick").data(_catSubset, function(d){ return d;})
.each(function(d,i){
d3.select(this).selectAll("text")
.text(function(d) {return d;})
.transition(transition)
.attr("transform", "translate(0, 0)")
.style("font-size", "10px")
.attr("dy", "0.32em");
d3.select(this).selectAll("line")
.transition(transition)
.attr("x1", 0)
.attr("y1", 0.5)
.attr("x2", -6)
.attr("y2", 0.5)
.attr("transform", "translate(0,0)");
});
sel.exit()
.transition(transition)
.style("opacity", 1);
_axis.selectAll("path.domain")
.transition(transition)
.style("opacity", 1);
}
// compute values for transition labels
function getPositionData(_elementsToLabel, _catSubset, height, width) {
var labelSubset = [];
var pData = {};
_catSubset.forEach(function(d) { pData[d] = {}; });
for (var key in _elementsToLabel) {
if (_elementsToLabel.hasOwnProperty(key)) {
labelSubset.push(_elementsToLabel[key].yDest);
}
}
pData.rect = getLabelSize(_elementsToLabel, 16);
var xPos = getXPositions(_elementsToLabel, pData.rect, width);
// rest is cat specific
pData.yPos = getYPositions(labelSubset, pData.rect.height, height);
_catSubset.forEach(function(d,i) {
pData[d].xLabel = xPos.x;
pData[d].labelLeft = xPos.labelLeft;
pData[d].yLabel = pData.yPos[i];
pData[d].yDest = labelSubset[i];
pData[d].yTrans = pData[d].yDest - pData[d].yLabel;
pData[d].value = _elementsToLabel[d].value;
});
pData.lineX1 = 0;
pData.lineY1 = 0;
function getLabelSize(_elementsToLabel, fontSize) {
var maxWidth = 0;
var maxHeight = 0;
var dummy = d3.select("svg")
.append("g")
.attr("class", "dummy l1")
.style("opacity", 0)
.selectAll("text")
.data(Object.keys(_elementsToLabel))
.enter()
.append("text")
.style("font-size", fontSize + "px")
.style("font-family", "sans-serif")
.text(function(d) { return elementsToLabel[d].value; });
dummy.each(function (d) {
var ele = d3.select(this).node();
maxWidth = (ele.getBBox().width > maxWidth) ? ele.getBBox().width : maxWidth;
maxHeight = (ele.getBBox().height > maxHeight) ? ele.getBBox().height : maxHeight;
pData[d].textWidth = ele.getBBox().width + 18; // 2* (+ 6 (tick length) + 3 (space between tick and text))
});
d3.selectAll("g.dummy").remove();
return {
width: maxWidth + 10, // add space for line
height: maxHeight + 10 // add space for better vertical separation
};
}
// returns object with x position for label transition destination
function getXPositions(_elementsToLabel, rect, width) {
var res = {};
var ele = _elementsToLabel[Object.keys(_elementsToLabel)[0]]; // pick first element assuming x values are the same
if (rect.width < ele.xLeft) {
res.x = ele.xLeft;
res.labelLeft = true;
} else {
res.x = ele.xRight;
res.labelLeft = false;
}
return res;
}
// returns array of y positions for label transition destination without overlaps
function getYPositions(data, rectHeight, height) {
var dataObject = createObject(data);
dataObject = adjustBottoms(dataObject);
var positionEnd = trimObject(dataObject);
if (positionEnd[positionEnd.length-1] < rectHeight/2) { // second pass if out of range
dataObject = adjustTops(dataObject);
positionEnd = trimObject(dataObject);
}
function createObject(data) {
// setup data structure with rectangles from bottom to the top
var dataObject = [];
var obj = {top: height, bottom: height + rectHeight}; // add dummy rect for lower bound
dataObject.push(obj);
data.forEach(function(d,i){
obj = {top: d - rectHeight/2, bottom: d + rectHeight/2}
dataObject.push(obj);
});
obj = {top: 0 - rectHeight, bottom: 0}; // add dummy rect for upper bound
dataObject.push(obj);
return dataObject;
}
function trimObject(dataObject) { // convert back to original array of values, also remove dummies
var data3 = [];
dataObject.forEach(function(d,i){
if (!(i === 0 || i === dataObject.length-1)) {
data3.push(d.top + rectHeight/2);
}
});
return data3;
}
function adjustBottoms(dataObject){
dataObject.forEach(function(d,i){
if (!(i === 0 || i === dataObject.length-1)) {
var diff = dataObject[i-1].top - d.bottom;
if (diff < 0) { // move rect up
d.top += diff;
d.bottom += diff;
}
}
});
return dataObject;
}
function adjustTops(dataObject){
for (var i = dataObject.length; i-- > 0; ){
if (!(i === 0 || i === dataObject.length-1)) {
var diff = dataObject[i+1].bottom - dataObject[i].top;
if (diff > 0) { // move rect down
dataObject[i].top += diff;
dataObject[i].bottom += diff;
}
}
};
return dataObject;
}
return positionEnd;
}
return pData;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment