Skip to content

Instantly share code, notes, and snippets.

@armollica
Last active May 24, 2016 23:20
Show Gist options
  • Save armollica/67f3cf7bf08a02d95d48dc9f0c91f26c to your computer and use it in GitHub Desktop.
Save armollica/67f3cf7bf08a02d95d48dc9f0c91f26c to your computer and use it in GitHub Desktop.
Ring Notes
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