///// How to drag and rotate labels in D3 ///// 2016 by Ironfrown ///// // Working space var width = 600; var height = 600; // Some "constants" var uniqNo = randInRange(0, 1000); var allCols = [ "blue", "yellow", "green", "orange", "purple", "darkblue", "gray", "brown", "black", "lightgreen", "darkgray", "white"]; var lastPickedCol = "red"; // Create elements as groups with circles and text var dotXDown = Math.floor(width * 0.2); var dotXUp = Math.floor(width * 0.8); var dotYDown = Math.floor(height * 0.2); var dotYUp = Math.floor(height * 0.8); var dotData = [ {id: "n-"+uniqSeq(), x: 300, y: 200, t: "Center", col: "red", center: "yes"}, {id: "n-"+uniqSeq(), x: 150, y: 160, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 280, y: 80, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 330, y: 85, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 500, y: 150, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 510, y: 230, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 410, y: 280, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 470, y: 250, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 350, y: 300, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 230, y: 270, t: pickCol(), col: lastCol(), center: "no"}, {id: "n-"+uniqSeq(), x: 120, y: 210, t: pickCol(), col: lastCol(), center: "no"} ]; ///// ///// Setting up SVG data structures ///// // Top SVG var main = d3.select("body") .append("svg") .attr("width", width) .attr("height", height); // All elements are groups of a circle and a text label // One of the elements is nominated as a "center" // Note that groups "g" can only be positioned with a transform // Coordinates of elements are {0, 0}, the position is via offset // Element keep track of their positions in "g" as {gx, gy} var holder = main.selectAll("g") .data(dotData) .enter() .append("g") .attr("id", function(d){ return d.id; }) .attr('gx', function(d){ return d.x; }) .attr('gy', function(d){ return d.y; }) .attr("transform", function(d) { return "translate("+d.x+","+d.y+") rotate(0)"; } ) .classed('center', function(d) { return d.center === "yes"; }) .classed('node', true); holder .append("circle") .attr("id", function(d){ return d.id+"-circle"; }) .attr("cx", 0) .attr("cy", 0) .attr("r", 5) .style("fill", function(d){ return d.col; }) .style("stroke", "black"); holder .append("text") .attr("id", function(d){ return d.id+"-text"; }) .style("fill", "black") .style("font-size", "18px") .attr("class", "label") .attr("dx", ".5em") .attr("alignment-baseline", "central") .style("dominant-baseline", "central") .text(function(d) {return d.t;}); ///// ///// Dragging of elements is by either circles or text ///// If text is not to be draggable set it up in CSS ///// // Allow the groups "g" to be draggable, set the origin var dragBehavior = d3.behavior.drag() .origin(dragOrigin) .on("drag", dragged) .on("dragend", dragended); holder.call(dragBehavior); // The origin is defined by tramsform offset function dragOrigin(d) { var t = d3.select(this); return {x: t.attr("x") + d3.transform(t.attr("transform")).translate[0], y: t.attr("y") + d3.transform(t.attr("transform")).translate[1]}; } // Stop propagation of events during dragging function dragstarted(d) { d3.event.sourceEvent.stopPropagation(); d3.select(this).classed("dragging", true); } // Record the position of the element in the "g" attributes {gx, gy} // Then update the direction of text of all elements function dragged(d) { d3.select(this).attr("transform", function(d,i){ return "translate(" + [ d3.event.x,d3.event.y ] + ")"; }); d3.select(this).attr("gx",d3.event.x); d3.select(this).attr("gy",d3.event.y); update(); } // Restart event propagation after dragging stopped function dragended(d) { var t = d3.select(this); var circ = t.select("circle"); t.classed("dragging", false); update(); } ///// ///// Utility functions ///// function toRads(degs) { return degs * (Math.PI / 180.0); } function toDegs(rads) { return rads * (180.0 / Math.PI); } function randInRange(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function uniqSeq() { return ++uniqNo; } function lastCol() { return lastPickedCol; } function pickCol() { lastPickedCol = allCols[randInRange(0, allCols.length-1)]; return lastPickedCol; } // Truncates an angle in radians to -pi and +pi function angleTrunc(a) { while(a < 0.0) a += Math.PI * 2; return ((a + Math.PI) % (Math.PI * 2)) - Math.PI; } // Angle between a line and a horiz line give by two points, in radians function getAngleBetweenPoints(x_orig, y_orig, x_landmark, y_landmark) { deltaY = y_landmark - y_orig; deltaX = x_landmark - x_orig; return angleTrunc(Math.atan2(deltaY, deltaX)); } ///// ///// The main ///// // Rotate text of the object to be away from the reference object function rotateTextAway(textObj, refObj) { var d3Text = textObj.select("text"); var cx = textObj.attr("gx"); var cy = textObj.attr("gy"); var rx = refObj.attr("gx"); var ry = refObj.attr("gy"); var textAngle = toDegs(getAngleBetweenPoints(rx, ry, cx, cy)); var angleRight = Math.sign(Math.cos(textAngle * (Math.PI / 180))); if (angleRight >= 0) { d3Text .attr("text-anchor", "begin") .attr("dx", "0.5em") .attr("transform", "rotate("+textAngle+")"); } else { d3Text .attr("text-anchor", "end") .attr("dx", "-0.5em") .attr("transform", "rotate("+Math.sign(textAngle) * (Math.abs(textAngle) - 180)+")"); } } // Update all chart elements for text to be away from the central point // If more than one element is nominated as a center, the first is used function update() { var gs = d3.selectAll(".node")[0]; gs.forEach(function(d){ var gsNode = d3.select(d); var gsCenter = d3.select('.center'); if (gsNode.attr("id") !== gsCenter.attr("id")) rotateTextAway(gsNode, gsCenter); }); } ///// ///// Start by initialising all elements ///// update(0);