Last active
February 19, 2017 17:17
-
-
Save evanjmg/ea3e59e67b4256c8831d3fc80f71294b to your computer and use it in GitHub Desktop.
Drag, Zoom in Grid d3 v4
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
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
.axis path { | |
display: none; | |
} | |
.axis line { | |
stroke-opacity: 0.3; | |
shape-rendering: crispEdges; | |
} | |
input[type="range"] { | |
right: 0; | |
top: 0; | |
position: absolute; | |
} | |
svg, | |
#chart-container { | |
width: 100%; | |
height: 100vh; | |
display: block; | |
} | |
</style> | |
<body> | |
<div id="chart-container"></div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.js"></script> | |
<script> | |
// select chart div and get the width and height of it. | |
var containerStyle = document.querySelector('#chart-container').getBoundingClientRect(); | |
var svg = null, | |
width = containerStyle.width, | |
height = containerStyle.height, | |
gX = null, | |
gY = null, | |
currentTransform = null, | |
svg = d3.select("#chart-container").append('svg'), | |
view = svg.append("g") | |
.attr("class", "view"); | |
if (currentTransform) view.attr('transform', currentTransform); | |
var xScale = d3.scaleLinear() | |
.domain([-width / 2, width / 2]) | |
.range([0, width]); | |
var yScale = d3.scaleLinear() | |
.domain([-height / 2, height / 2]) | |
.range([height, 0]); | |
var xAxis = d3.axisBottom(xScale) | |
.ticks((width + 2) / (height + 2) * 10) | |
.tickSize(height) | |
.tickPadding(8 - height); | |
var yAxis = d3.axisRight(yScale) | |
.ticks(10) | |
.tickSize(width) | |
.tickPadding(8 - width); | |
gX = svg.append("g") | |
.attr("class", "axis axis--x") | |
.call(xAxis); | |
gY = svg.append("g") | |
.attr("class", "axis axis--y") | |
.call(yAxis); | |
var zoom = d3.zoom() | |
.scaleExtent([0.5, 5]) | |
.translateExtent([ | |
[-width * 2, -height * 2], | |
[width * 2, height * 2] | |
]) | |
.on("zoom", zoomed); | |
function zoomed() { | |
currentTransform = d3.event.transform; | |
view.attr("transform", currentTransform); | |
gX.call(xAxis.scale(d3.event.transform.rescaleX(xScale))); | |
gY.call(yAxis.scale(d3.event.transform.rescaleY(yScale))); | |
slider.property("value", d3.event.scale); | |
} | |
var previousDraggedPosition = null, | |
selected = null; | |
// snap to grid is simply rounding to the nearest resolution of the square | |
function snapToGrid(p, r) { | |
return Math.round(p / r) * r; | |
} | |
// we'll use a resolution of 50 here | |
var cubeResolution = 50; | |
// randomly generate points, but make sure they snap to grid | |
var points = d3.range(10).map(() => { | |
return { | |
x: snapToGrid(Math.random() * 500, cubeResolution), | |
y: snapToGrid(Math.random() * 500, cubeResolution) | |
}; | |
}); | |
var itemContainer = view.selectAll("g").attr("class", "itemContainer") | |
// add group to view | |
.data(points).enter().append('g') | |
// and center the group in the middle | |
.attr("transform", () => 'translate(' + xScale(0) + ',' + yScale(0) + ')') | |
.append('g') | |
// make this entire group draggable - this is useful for adding text elements later | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
// add the square to each group | |
var item = itemContainer.append('rect').attr('class', 'table-graphic') | |
.attr('x', d => d.x) | |
.attr('y', d => d.y) | |
.attr('data-rotation', 0) | |
.attr('width', cubeResolution) | |
.attr('height', cubeResolution) | |
.attr('fill', 'blue') | |
.on('click', function() { | |
selected = this.parentNode; | |
}); | |
function dragged(d) { | |
selected = this; | |
// update the position of the rect (square) and snap to grid | |
var el = d3.select(this).select('.table-graphic').attr("x", (d) => snapToGrid(d3.event.x, cubeResolution)).attr("y", () => snapToGrid(d3.event.y, cubeResolution)) | |
// get center point and make sure rotation is correct on drag. | |
var center = getCenter(el.attr('x'), el.attr('y'), cubeResolution, cubeResolution); | |
el.attr('transform', () => { | |
return "rotate(" + el.attr('data-rotation') + "," + center.x + ',' + center.y + ")"; | |
}); | |
} | |
function dragended(d) { | |
d3.select(this).classed("dragging", false); | |
var newEl = d3.select(this).select('.table-graphic'); | |
var newPt = { | |
x: newEl.attr('x'), | |
y: newEl.attr('y') | |
}; | |
// save and update position for redraw | |
var pt = findAndUpdate(coorNum(previousDraggedPosition), coorNum(newPt)); | |
if (pt) { | |
previousDraggedPosition = pt | |
}; | |
} | |
function dragstarted(d) { | |
var el = d3.select(this); | |
// save previous drag point for collisions and redraws | |
savePreviousDragPoint(el); | |
// raise the z-index to the top and set class to dragging | |
el.raise().classed("dragging", true); | |
} | |
// helper to convert strings to integers | |
function coorNum(pt) { | |
return { | |
x: parseInt(pt.x, 10), | |
y: parseInt(pt.y, 10) | |
}; | |
} | |
function savePreviousDragPoint(el) { | |
var elBox = el.nodes()[0].getBBox(); | |
if (!el.nodes()[0].classList.contains('dragging')) { | |
previousDraggedPosition = { | |
x: elBox.x, | |
y: elBox.y | |
}; | |
} | |
} | |
// helper for drag recentering | |
function getCenter(x, y, w, h) { | |
return { | |
x: parseInt(x, 10) + parseInt(w, 10) / 2, | |
y: parseInt(y, 10) + parseInt(h) / 2 | |
} | |
}; | |
// add slider instead of mousewheel zoom to improve user experience | |
// have it start at min 50% and max out at 5x the amount. | |
var slider = d3.select("body").append("input") | |
.datum({}) | |
.attr("type", "range") | |
.attr("value", 1) | |
.attr("min", zoom.scaleExtent()[0]) | |
.attr("max", zoom.scaleExtent()[1]) | |
.attr("step", (zoom.scaleExtent()[1] - zoom.scaleExtent()[0]) / 100) | |
.on("input", slided); | |
function slided(d) { | |
zoom.scaleTo(svg, d3.select(this).property("value")); | |
} | |
// disable zoom on mousewheel and double click | |
svg.call(zoom).on("wheel.zoom", null) | |
.on('dblclick.zoom', null); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment