Skip to content

Instantly share code, notes, and snippets.

@michaelwooley
Last active September 6, 2017 01:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaelwooley/7a1727322376caed3bc13e494a03867c to your computer and use it in GitHub Desktop.
Save michaelwooley/7a1727322376caed3bc13e494a03867c to your computer and use it in GitHub Desktop.
Drawing With d3.js (Part 2 -- Panning and Zooming)

Controls:

  • Click + Drag: Draw new rectangle.
  • Shift + Click + Drag: Pan in workspace.
  • scroll: Zoom In/Out

Bug

There is a bug that occurs only when viewing the output on bl.ocks. It arises when you zoom and pan simultaneously. The result is that--only when scale=1--the rectangles are rendered oddly.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Adding Rectangles</title>
<style>
rect.rect-main {
stroke: #d32f2f;
stroke-width: 2;
fill-opacity: 0;
stroke-opacity: 0.5;
}
div.sample-div {
position: absolute;
top: 25%;
left: 25%;
}
</style>
</head>
<body>
<div class="sample-div">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.js"></script>
<script type="text/javascript">
function SVGCanvas(options) {
// An SVG-based drawing
var self = this;
// Define the global SVG options
this.options = options || {};
this.options.h = options.h || 250; // SVG Height and Width
this.options.w = options.w || 250;
this.options.addTo = options.addTo || 'body'; // Where to add the SVG (a css selector)
this.options.addBorderRect = options.addBorderRect || true; // Whether to add a border around the SVG.
// Make the SVG
this.svg = d3.select(this.options.addTo)
.append('svg')
.attr('height', this.options.h)
.attr('width', this.options.w)
.attr('class', 'display-svg');
// Add border if requested
if (this.options.addBorderRect) {
this.svg.append('rect')
.attr('height', this.options.h)
.attr('width', this.options.w)
.attr('stroke', 'black')
.attr('stroke-width', 4)
.attr('opacity', 0.25)
.attr('fill-opacity', 0.0)
.attr('class', 'border-rect');
}
// Add zoom and pan group
this.zoomG = this.svg
.append('g')
.attr('class', 'zoom-group');
// Rectangles
this.Rect = {
'r': null,
'x0': null,
'y0': null,
}; // Current Selection
this.Rects = []; // Collection
// Transformation state
this.transform = d3.zoomTransform(this.zoomG.node());
// Load methods for behaviors
this.makeAddRect(); // Add Rectangle Methods
this.makeZoomPan(); // SVG Zooming and Panning Methods
this.makeDragBehavior();
// Dragging Behavior - account for both addRect and pan.
this.svg.call(
d3.drag()
.on('start', self.dragBehavior.start())
.on('drag', self.dragBehavior.drag())
.on('end', self.dragBehavior.end())
);
// Zooming behavior
this.svg.call(
d3.zoom()
.scaleExtent([1, 10])
.on('zoom', this.zoomPan.zoom)
)
.on('.zoom', this.zoomPan.zoom)
.on('mousedown.zoom', null)
.on('mousemove.zoom', null)
.on('mouseup.zoom', null);
}
SVGCanvas.prototype.makeZoomPan = function() {
// Defines zooming and panning behavior from zoom listener
var self = this;
zoom = function() {
self.transform = d3.event.transform;
self.zoomG.attr('transform', self.transform);
// Go back to initial position if zoomed out.
if (d3.event.transform.k === 1) {
self.zoomG
.transition(d3.transition()
.duration(100)
.ease(d3.easeLinear))
.attr('transform', 'translate(0,0) scale(1, 1)');
self.transform = d3.zoomTransform(self.zoomG.node());
}
}
var pan = function() {
var m = d3.event;
self.transform.x += m.dx;
self.transform.y += m.dy;
// Update Attribute
d3.select('g.zoom-group').attr('transform', self.transform);
}
self.zoomPan = {
zoom: zoom,
pan: pan
};
}
SVGCanvas.prototype.mouseOffset = function() {
var m = d3.event;
m.x = (-this.transform.x + m.x) / this.transform.k;
m.y = (-this.transform.y + m.y) / this.transform.k;
return m;
}
SVGCanvas.prototype.makeAddRect = function() {
var self = this;
start = function() {
//Add a rectangle
// 1. Get mouse location in SVG
var m = self.mouseOffset(); //d3.event;
self.Rect.x0 = m.x;
self.Rect.y0 = m.y;
// 2. Make a rectangle
self.Rect.r = self.zoomG
.append('g')
.append('rect') // An SVG `rect` element
.attr('x', self.Rect.x0) // Position at mouse location
.attr('y', self.Rect.y0)
.attr('width', 1) // Make it tiny
.attr('height', 1)
.attr('class', 'rect-main') // Assign a class for formatting purposes
;
}
drag = function() {
// What to do when mouse is dragged
// 1. Get the new mouse position
var m = self.mouseOffset(); //d3.event;
// 2. Update the attributes of the rectangle
self.Rect.r.attr('x', Math.min(self.Rect.x0, m.x))
.attr('y', Math.min(self.Rect.y0, m.y))
.attr('width', Math.abs(self.Rect.x0 - m.x))
.attr('height', Math.abs(self.Rect.y0 - m.y));
}
end = function() {
// What to do on mouseup
self.Rects.push(self.Rect);
}
self.addRect = {
start: start,
drag: drag,
end: end
};
}
SVGCanvas.prototype.makeDragBehavior = function() {
var self = this;
var start = function() {
return function() {
if (!d3.event.sourceEvent.shiftKey) {
self.addRect.start();
}
if (d3.event.sourceEvent.shiftKey) {
null;
}
}
}
var drag = function() {
return function() {
if (!(self.Rect.r === null) && !(d3.event.sourceEvent.shiftKey)) {
self.addRect.drag();
}
if (d3.event.sourceEvent.shiftKey) {
self.zoomPan.pan();
}
}
}
var end = function() {
return function() {
if (!(self.Rect.r === null) & !(d3.event.sourceEvent.shiftKey)) {
self.addRect.end();
}
if (d3.event.sourceEvent.shiftKey) {
null;
}
}
}
self.dragBehavior = {
start: start,
drag: drag,
end: end
};
}
/**********
SETUP
**********/
options = {
h: 300,
w: 500,
addTo: '.sample-div',
addBorderRect: true,
}
var c = new SVGCanvas(options);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment