d3v3.ringNote = function() { var draggable = false, controlRadius = 15; var dragCenter = d3v3.behavior.drag() .origin(function(d) { return { x: 0, y: 0}; }) .on("drag", dragmoveCenter); var dragRadius = d3v3.behavior.drag() .origin(function(d) { return { x: 0, y: 0 }; }) .on("drag", dragmoveRadius); var dragText = d3v3.behavior.drag() .origin(function(d) { return { x: 0, y: 0 }; }) .on("drag", dragmoveText); var path = d3v3.svg.line(); function draw(selection, annotation) { selection.selectAll(".ring-note").remove(); var gRingNote = selection.selectAll(".ring-note") .data(annotation) .enter().append("g") .attr("class", "ring-note") .attr("transform", function(d) { return "translate(" + d.cx + "," + d.cy + ")"; }); var gAnnotation = gRingNote.append("g") .attr("class", "annotation"); var circle = gAnnotation.append("circle") .attr("r", function(d) { return d.r; }); var line = gAnnotation.append("path") .call(updateLine); var text = gAnnotation.append("text") .call(updateText); if (draggable) { var gControls = gRingNote.append("g") .attr("class", "controls"); // Draggable circle that moves the circle's location var center = gControls.append("circle") .attr("class", "center") .call(styleControl) .call(dragCenter); // Draggable circle that changes the circle's radius var radius = gControls.append("circle") .attr("class", "radius") .attr("cx", function(d) { return d.r; }) .call(styleControl) .call(dragRadius); // Make text draggble text .style("cursor", "move") .call(dragText); } return selection; } draw.draggable = function(_) { if (!arguments.length) return draggable; draggable = _; return draw; }; // Region in relation to circle, e.g., N, NW, W, SW, etc. function getRegion(x, y, r) { var px = r * Math.cos(Math.PI/4), py = r * Math.sin(Math.PI/4); var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); if (distance < r) { return null; } else { if (x > px) { // East if (y > py) return "SE"; if (y < -py) return "NE"; if (x > r) return "E"; return null; } else if (x < -px) { // West if (y > py) return "SW"; if (y < -py) return "NW"; if (x < -r) return "W"; return null; } else { // Center if (y > r) return "S"; if (y < -r) return "N"; } } } function dragmoveCenter(d) { var gRingNote = d3v3.select(this.parentNode.parentNode); d.cx += d3v3.event.x; d.cy += d3v3.event.y; gRingNote .attr("transform", function(d) { return "translate(" + d.cx + "," + d.cy + ")"; }); } function dragmoveRadius(d) { var gRingNote = d3v3.select(this.parentNode.parentNode), gAnnotation = gRingNote.select(".annotation"), circle = gAnnotation.select("circle"), line = gAnnotation.select("path"), text = gAnnotation.select("text"), radius = d3v3.select(this); d.r += d3v3.event.dx; circle.attr("r", function(d) { return d.r; }); radius.attr("cx", function(d) { return d.r; }); line.call(updateLine); text.call(updateText); } function dragmoveText(d) { var gAnnotation = d3v3.select(this.parentNode), line = gAnnotation.select("path"), text = d3v3.select(this); d.textOffset[0] += d3v3.event.dx; d.textOffset[1] += d3v3.event.dy; text.call(updateText); line.call(updateLine); } function updateLine(selection) { return selection.attr("d", function(d) { var x = d.textOffset[0], y = d.textOffset[1], lineData = getLineData(x, y, d.r); return path(lineData); }); } function getLineData(x, y, r) { var region = getRegion(x, y, r); if (region == null) { // No line if text is inside the circle return []; } else { // Cardinal directions if (region == "N") return [[0, -r], [0, y]]; if (region == "E") return [[r, 0], [x, 0]]; if (region == "S") return [[0, r], [0, y]]; if (region == "W") return [[-r, 0],[x, 0]]; var d0 = r * Math.cos(Math.PI/4), d1 = Math.min(Math.abs(x), Math.abs(y)) - d0; // Intermediate directions if (region == "NE") return [[ d0, -d0], [ d0 + d1, -d0 - d1], [x, y]]; if (region == "SE") return [[ d0, d0], [ d0 + d1, d0 + d1], [x, y]]; if (region == "SW") return [[-d0, d0], [-d0 - d1, d0 + d1], [x, y]]; if (region == "NW") return [[-d0, -d0], [-d0 - d1, -d0 - d1], [x, y]]; } } function updateText(selection) { return selection.each(function(d) { var x = d.textOffset[0], y = d.textOffset[1], region = getRegion(x, y, d.r), textCoords = getTextCoords(x, y, region); d3v3.select(this) .attr("x", textCoords.x) .attr("y", textCoords.y) .text(d.text) .each(function(d) { var x = d.textOffset[0], y = d.textOffset[1], textAnchor = getTextAnchor(x, y, region); var dx = textAnchor == "start" ? "0.33em" : textAnchor == "end" ? "-0.33em" : "0"; var dy = textAnchor !== "middle" ? ".33em" : ["NW", "N", "NE"].indexOf(region) !== -1 ? "-.33em" : "1em"; var orientation = textAnchor !== "middle" ? undefined : ["NW", "N", "NE"].indexOf(region) !== -1 ? "bottom" : "top"; d3v3.select(this) .style("text-anchor", textAnchor) .attr("dx", dx) .attr("dy", dy) .call(wrapText, d.textWidth || 960, orientation); }); }); } function getTextCoords(x, y, region) { if (region == "N") return { x: 0, y: y }; if (region == "E") return { x: x, y: 0 }; if (region == "S") return { x: 0, y: y }; if (region == "W") return { x: x, y: 0 }; return { x: x, y: y }; } function getTextAnchor(x, y, region) { if (region == null) { return "middle"; } else { // Cardinal directions if (region == "N") return "middle"; if (region == "E") return "start"; if (region == "S") return "middle"; if (region == "W") return "end"; var xLonger = Math.abs(x) > Math.abs(y); // Intermediate directions` if (region == "NE") return xLonger ? "start" : "middle"; if (region == "SE") return xLonger ? "start" : "middle"; if (region == "SW") return xLonger ? "end" : "middle"; if (region == "NW") return xLonger ? "end" : "middle"; } } // Adapted from: https://bl.ocks.org/mbostock/7555321 function wrapText(text, width, orientation) { text.each(function(d) { var text = d3v3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 1, lineHeight = 1.1, // ems x = text.attr("x"), dx = text.attr("dx"), tspan = text.text(null).append("tspan").attr("x", x).attr("dx", dx); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = text.append("tspan") .attr("x", x) .attr("dx", dx) .attr("dy", lineHeight + "em") .text(word); lineNumber++; } } var dy; if (orientation == "bottom") { dy = -lineHeight * (lineNumber-1) - .33; } else if (orientation == "top") { dy = 1; } else { dy = -lineHeight * ((lineNumber-1) / 2) + .33; } text.attr("dy", dy + "em"); }); } function styleControl(selection) { selection .attr("r", controlRadius) .style("fill-opacity", "0") .style("stroke", "black") .style("stroke-dasharray", "3, 3") .style("cursor", "move"); } return draw; };