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:
see also: https://bl.ocks.org/ee2dev/fc880e1cfbb80f649878f3d5b9e8ed93
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js