Skip to content

Instantly share code, notes, and snippets.

@jonboiser
Forked from mbostock/.block
Last active January 27, 2016 16:25
Show Gist options
  • Save jonboiser/6296bb833067be275e0f to your computer and use it in GitHub Desktop.
Save jonboiser/6296bb833067be275e0f to your computer and use it in GitHub Desktop.
Pan+Zoom
node_modules/

An example of d3.behavior.zoom applied using x- and y-scales. This example can be extended with programmatic control to animate between preset views.

Code for shifting labels

// d3.selectAll(".x.axis .tick text").attr({transform: "translate(100,0)"})
// [Array[4]]
// d3.selectAll(".x.axis .tick text").attr({transform: "translate(100+20,0)"})
// 4d3.v3.min.js:1 Error: Invalid value for <text> attribute transform="translate(100+20,0)"u @ d3.v3.min.js:1(anonymous function) @ d3.v3.min.js:3Y @ d3.v3.min.js:1Aa.each @ d3.v3.min.js:3Aa.attr @ d3.v3.min.js:3(anonymous function) @ VM21182:2InjectedScript._evaluateOn @ VM20195:875InjectedScript._evaluateAndWrap @ VM20195:808InjectedScript.evaluate @ VM20195:664
// [Array[4]]
// d3.selectAll(".x.axis .tick text").attr({transform: "translate(120,0)"})
// [Array[4]]
<!DOCTYPE html>
<meta charset="utf-8">
<title>Zoom + Pan</title>
<style>
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: #ddd;
}
rect.field {
fill: #fff;
}
.axis path,
.axis line {
fill: none;
stroke: #444;
}
rect.field-rep {
fill: #B4D8B4;
stroke: black;
stroke-width: 2;
}
.axis .tick line {
stroke-width: 1;
opacity: 0.8;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="place-reps.js" charset="utf-8"></script>
</body>
// Constants
var GRID_SQUARE_SIZE = 50;
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
};
var numCols = 20; // from data.maxCols
var numRanges = 20; // from data.maxRanges
// Should this be limited
var gridWidth = GRID_SQUARE_SIZE * (numCols + 1);
var gridHeight = GRID_SQUARE_SIZE * (numRanges + 1);
var width = gridWidth + margin.left + margin.right;
var height = gridHeight + margin.top + margin.bottom;
// Scales
var x = d3.scale.linear()
.domain([0, numCols])
.range([0, gridWidth]);
var y = d3.scale.linear()
.domain([0, numRanges])
.range([gridHeight, 0]);
// Axes
var xAxis = d3.svg.axis()
.scale(x)
.orient('top')
.tickValues(d3.range(0, numCols + 1))
.tickFormat(d3.format("0d"))
.tickSize(-gridHeight)
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.tickValues(d3.range(0, numRanges + 1))
.tickFormat(d3.format("0d"))
.tickSize(-gridWidth);
// Zoom behavior (does this work for touch?)
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([0.25, 2])
.on('zoom', zoomed);
function zoomed() {
// Redraw axes
svg.select('.x.axis').call(xAxis);
svg.select('.y.axis').call(yAxis);
d3.selectAll(".x.axis .tick text")
.call(shiftXAxisLabels);
d3.selectAll(".y.axis .tick text")
.call(shiftYAxisLabels);
// Transform the repsG
var t = zoom.translate(),
tx = t[0],
ty = t[1];
// This prevents user from moving the field completely off the graph.
// TODO make it zoom dependent (e.g. if zoom far enough where whole thing
// fits, then restrict pan so whole field is visible.)
tx = Math.max(tx, -gridWidth * zoom.scale() * 0.99)
tx = Math.min(tx, gridWidth * 0.99);
ty = Math.max(ty, -gridHeight * zoom.scale() * 0.99)
ty = Math.min(ty, gridHeight * 0.99);
zoom.translate([tx,ty]);
repsG.attr({
transform: "translate("+ zoom.translate() + ")scale(" + zoom.scale() +")"
})
}
// Top-level svg node
var svg = d3.select('body')
.append('svg')
.attr({
width: width,
height: height
});
// Container group for axes and field grid
var gridContainerG = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(zoom);
// Container group for the reps
var repsContainerG = gridContainerG.append('g');
// The background rect
repsContainerG.append('rect')
.attr({
width: gridWidth,
height: gridHeight
});
// Draw axes and gridlines
gridContainerG.append("g")
.attr("class", "x axis")
.call(xAxis);
gridContainerG.append("g")
.attr("class", "y axis")
.call(yAxis);
d3.selectAll(".axis .tick:first-child text").attr({display: "none"})
d3.selectAll(".x.axis .tick text").call(shiftXAxisLabels);
d3.selectAll(".y.axis .tick text").call(shiftYAxisLabels);
// The group holding the reps rects and big field rect
var repsG = repsContainerG.append('g')
.attr({
id: 'gridContainerG'
});
// Rect demarcating the whole field
repsG.append('rect')
.attr({
class: 'field',
width: gridWidth,
height: gridHeight
});
// UTILITY FUNCTIONS
function addRep(coordinates, dimensions, info) {
var repG = repsG.append('g').attr({
transform: 'translate(' + x(coordinates.x - 1) + ',' + y(coordinates.y + dimensions.r - 1) + ')'
});
repG.append('rect')
.attr({
class: 'field-rep',
width: x(dimensions.c),
height: x(dimensions.r)
})
// TODO get it exactly in the middle and figure out how to handle
// overflowing text
repG.append('text')
.attr('y', 10)
.attr('x', 3)
.text(info.set)
}
// TODO labels are still slightly off because of text-anchor property.
function shiftXAxisLabels(sel) {
var scale = d3.event ? d3.event.scale : 1;
sel.attr('x', -GRID_SQUARE_SIZE / 2 * scale)
}
function shiftYAxisLabels(sel) {
var scale = d3.event ? d3.event.scale : 1;
sel.attr('y', GRID_SQUARE_SIZE / 2 * scale);
}
// Tests with loading data
d3.json('rep-data.json', function(data) {
data.rep.map(function(rep) {
addRep({
x: rep.x,
y: rep.y
}, {
c: rep.columnsWide,
r: rep.rangesTall
}, {
set: rep.set
})
})
})
{
"fieldName": "",
"program": "",
"season": "",
"fieldId": 12345,
"maxCols": 50,
"maxRanges": 50,
"rep": [
{
"set": "NCB_ENJ001",
"columnsWide": 2,
"rangesTall": 4,
"x": 2,
"y": 14,
"trackId": "",
"repNumber": 1,
"treatmentName": ""
},
{
"set": "NCB_ENJ002",
"columnsWide": 4,
"rangesTall": 4,
"x": 9,
"y": 14,
"trackId": "",
"repNumber": 1,
"treatmentName": ""
},
{
"set": "NCB_ENJ003",
"columnsWide": 4,
"rangesTall": 4,
"x": 13,
"y": 14,
"trackId": "",
"repNumber": 1,
"treatmentName": ""
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment