Skip to content

Instantly share code, notes, and snippets.

@ptgolden
Last active August 29, 2015 14:09
Show Gist options
  • Save ptgolden/02a4d14a19e4999913cb to your computer and use it in GitHub Desktop.
Save ptgolden/02a4d14a19e4999913cb to your computer and use it in GitHub Desktop.
Drag drop lines
<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body { margin: 0; padding: 0; }
#message { height: 3em; width: 800px; text-align: center; padding: 1em 0; box-sizing: border-box;}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="message"></div>
<script>
var height = 300
, width = 800
, padding = 25
, sensitiveZone = 100
var svg = d3.select('body').append('svg')
.attr('height', height + 100)
.attr('width', width)
var y = d3.scale.linear()
.domain([0, 1])
.range([height - padding, 0])
// First, I'll set a number of variables that will be changed/monitored by
// different event callbacks.
// The line that will extend from one axis to another
var line = null;
// The group of sensitive rectangles that will be "sticky" to receive the line
var sensitiveRects = null;
// The current sensitive rectangle that is being hovered over
var enteredEl = null;
// The source and destination axes of the line.
var source = null, end = null;
// A function that returns an absolute X coordinate, relative to the SVG canvas,
// that is in the middle of the axis path. Takes the <g> axis element as an
// argument.
function xCoordFromAxis(axisGroup) {
var boundingRect = d3.select(axisGroup)
.select('.domain')[0][0]
.getBoundingClientRect()
return boundingRect.left + (boundingRect.width / 2);
}
// The behavior that will be called on the sensitive rectangles that are over
// the axes. Monitors drag events and handles drawing the line.
var drag = d3.behavior.drag()
.on('dragstart', function () {
// Save the reference to the sensitive rectangle which is the source of the
// drag event.
var that = this;
// Reset the message at the top.
document.querySelector('#message').innerHTML = '';
// Save the source axis and measure.
// (y.invert(n) is the opposite of y(n)- it takes a pixel measure and returns
// the scaled value)
source = { axis: this, measure: y.invert(d3.mouse(this)[1]) }
// Add event listeners for all the sensitive rectangles that are *not* the
// source of this event. Store that element when one has been entered
// (mouseover), and unset that reference when it has been exited (mouseout)
sensitiveRects = d3.selectAll('rect')
.filter(function () { return that !== this })
.on('mouseover', function () { enteredEl = this })
.on('mouseout', function () { enteredEl = null });
})
.on('drag', function (e) {
// Get the position of the mouse relative to the SVG element
var mousePos = d3.mouse(svg[0][0])
, mouseX = mousePos[0]
, mouseY = mousePos[1]
// If we have not drawn the line yet, create a new line element and make its
// origin x coordinate the middle of its source axis and its origin y
// coordinate the y position of the mouse.
if (!line) {
line = svg.insert('line', ':first-child')
.attr('x1', xCoordFromAxis(source.axis.parentNode))
.attr('y1', mouseY)
}
// If the mouse is over a sensitive rectangle (as set by the mouseover and
// mouseout listeners set in dragstart), set the terminating point of the
// line at the middle of the the destination axis and y coordinate of the
// mouse. Otherwise, set it at the x and y position of the mouse.
if (enteredEl) {
dest = { axis: enteredEl, measure: y.invert(d3.mouse(enteredEl)[1]) }
line.attr('x2', xCoordFromAxis(dest.axis.parentNode))
line.attr('stroke', 'red');
} else {
line.attr('x2', mouseX);
line.attr('stroke', 'black');
}
line.attr('y2', mouseY)
})
.on('dragend', function (e) {
// If the drag ended while over a sensitive rectangle, report the source
// and desitination mesaures.
if (enteredEl) {
document.querySelector('#message').innerHTML = (
d3.select(source.axis).datum().label +
'(' + source.measure.toFixed(2) + ') to ' +
d3.select(dest.axis).datum().label +
'(' + dest.measure.toFixed(2) + ').'
)
var newline = svg.insert('g', ':first-child');
var lineCoords = {
x1: xCoordFromAxis(source.axis.parentNode),
y1: y(source.measure) + 20,
x2: xCoordFromAxis(dest.axis.parentNode),
y2: y(dest.measure) + 20,
}
newline.append('line')
.attr(lineCoords)
.attr('stroke', 'blue')
newline.append('line')
.attr(lineCoords)
.attr('stroke', 'blue')
.attr('stroke-width', 10)
.attr('opacity', 0)
newline.on('dblclick', function (e) { d3.select(this).remove(); return false; })
}
// Remove the listeners on the sensitive zones, remove the line.
sensitiveRects.on('mouseover', null).on('mouseout', null);
line.remove();
// Reset all the variables.
line = null;
sensitiveRects = null;
enteredEl = null;
source = null;
dest = null;
})
var axis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(5)
// Draw 4 different axes
for (var i = 0; i < 4; i++) {
var axisG = svg.append('g')
.datum({ label: 'Axis #' + (i + 1) })
.call(axis)
.attr('transform', 'translate(' + (50 + i * 200) + ',20)')
axisG.append('text')
.text(function (d) { return d.label })
.attr('y', height)
.attr('text-anchor', 'middle')
// Draw a rectangle over the axis, and call the drag behavior on it.
var target = axisG
.append('rect')
.attr('height', function () {
return d3.select(this.parentNode).select('.domain')[0][0].getBoundingClientRect().height
})
.attr('width', sensitiveZone)
.attr('transform', 'translate(-' + (sensitiveZone / 2 + 4) + ',0)')
.attr('fill', 'grey')
.style('opacity', '0')
.call(drag)
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment