|
<!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> |