Annotations created using the d3.ringNote plugin.
| d3.ringNote = function() { | |
| var draggable = false, | |
| controlRadius = 15; | |
| var dragCenter = d3.behavior.drag() | |
| .origin(function(d) { return { x: 0, y: 0}; }) | |
| .on("drag", dragmoveCenter); | |
| var dragRadius = d3.behavior.drag() | |
| .origin(function(d) { return { x: 0, y: 0 }; }) | |
| .on("drag", dragmoveRadius); | |
| var dragText = d3.behavior.drag() | |
| .origin(function(d) { return { x: 0, y: 0 }; }) | |
| .on("drag", dragmoveText); | |
| var path = d3.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 = d3.select(this.parentNode.parentNode); | |
| d.cx += d3.event.x; | |
| d.cy += d3.event.y; | |
| gRingNote | |
| .attr("transform", function(d) { | |
| return "translate(" + d.cx + "," + d.cy + ")"; | |
| }); | |
| } | |
| function dragmoveRadius(d) { | |
| var gRingNote = d3.select(this.parentNode.parentNode), | |
| gAnnotation = gRingNote.select(".annotation"), | |
| circle = gAnnotation.select("circle"), | |
| line = gAnnotation.select("path"), | |
| text = gAnnotation.select("text"), | |
| radius = d3.select(this); | |
| d.r += d3.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 = d3.select(this.parentNode), | |
| line = gAnnotation.select("path"), | |
| text = d3.select(this); | |
| d.textOffset[0] += d3.event.dx; | |
| d.textOffset[1] += d3.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); | |
| d3.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"; | |
| d3.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"; | |
| } | |
| } | |
| function wrapText(text, width, orientation) { | |
| text.each(function(d) { | |
| var text = d3.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; | |
| }; |
| <html> | |
| <head> | |
| <style> | |
| html { | |
| font-family: sans-serif; | |
| font-size: 12px; | |
| } | |
| label { | |
| position: absolute; | |
| top: 15px; | |
| right: 15px; | |
| } | |
| .annotation circle { | |
| fill: none; | |
| stroke: black; | |
| } | |
| .annotation path { | |
| fill: none; | |
| stroke: black; | |
| shape-rendering: crispEdges; | |
| } | |
| .annotation .shaded { | |
| fill: grey; | |
| fill-opacity: 0.2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <label> | |
| <input type="checkbox"> | |
| Hide controls | |
| </label> | |
| <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
| <script src="d3-ring-note.js"></script> | |
| <script> | |
| var annotations = [ | |
| { | |
| "cx": 232, | |
| "cy": 123, | |
| "r": 103, | |
| "text": "This is a \"ring note\"", | |
| "textOffset": [ | |
| 114, | |
| 88 | |
| ] | |
| }, | |
| { | |
| "cx": 691, | |
| "cy": 101, | |
| "r": 54, | |
| "text": "Drag this text to move it. Text wraps automatically. Just set the width", | |
| "textWidth": 150, | |
| "textOffset": [ | |
| 0, | |
| 66 | |
| ] | |
| }, | |
| { | |
| "cx": 347, | |
| "cy": 370, | |
| "r": 46, | |
| "text": "Drag the dashed circles to change the annotation's position and size. You can remove these controls after you settle on a final layout", | |
| "textWidth": 200, | |
| "textOffset": [ | |
| -68, | |
| -37 | |
| ] | |
| }, | |
| { | |
| "cx": 760, | |
| "cy": 361, | |
| "r": 67, | |
| "text": "Styling individual annotations is straightforward", | |
| "textWidth": 150, | |
| "textOffset": [ | |
| -75, | |
| -5 | |
| ], | |
| "shaded": true | |
| } | |
| ]; | |
| var width = 960, | |
| height = 500; | |
| var ringNote = d3.ringNote() | |
| .draggable(true); | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", width) | |
| .attr("height", height); | |
| var gAnnotations = svg.append("g") | |
| .attr("class", "annotations") | |
| .call(ringNote, annotations); | |
| // Styling individual annotations based on bound data | |
| gAnnotations.selectAll(".annotation circle") | |
| .classed("shaded", function(d) { return d.shaded; }); | |
| // Hide or show the controls | |
| var draggable = true; | |
| d3.select("input") | |
| .on("change", function() { | |
| ringNote.draggable(draggable = !draggable); | |
| gAnnotations | |
| .call(ringNote, annotations) | |
| .selectAll(".annotation circle") | |
| .classed("shaded", function(d) { return d.shaded; }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment