Annotations created using the d3.ringNote plugin.
Last active
May 24, 2016 23:20
-
-
Save armollica/67f3cf7bf08a02d95d48dc9f0c91f26c to your computer and use it in GitHub Desktop.
Ring Notes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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