Skip to content

Instantly share code, notes, and snippets.

@pkerpedjiev
Last active October 1, 2015 07:37
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 pkerpedjiev/05dafc7f8176550cb4b0 to your computer and use it in GitHub Desktop.
Save pkerpedjiev/05dafc7f8176550cb4b0 to your computer and use it in GitHub Desktop.
D3 Grid With Selectable Aspect Ratios

D3 Grid Layout

A layout for placing items with a given aspect ratio on grid with a fixed height and width.

Example

An example of this layout in action can be found here.

Usage

The grid layout can be instantiated with a set of parameters:

var gridLayout = d3.layout.grid()
.size([width, height])
.aspect(2.0)

It takes as input an array of objects.

var data = gridLayout([1,2,3])
console.log('data')

And returns an array of objects wrapping the data in their data property and containing a pos property indicating where this object should be positioned on the canvas:

[Object, Object, Object]
    0: Object
        data: 1
        pos: Object
            height: 170
            width: 170
            x: 0
            y: 0
            __proto__: Object
        __proto__: Object
    1: Object
    2: Object
    length: 3
    __proto__: Array[0]
circle {
fill: white;
stroke: black;
stroke-width: 1;
}
.grid-rect {
fill: blue;
stroke: black;
opacity: 0.3;
}
d3.layout.grid = function() {
var numCells = 1;
var aspect = 1;
var widthTotal = 550;
var heightTotal = 300;
function grid(d) {
numCells = d.length;
var widthIndividual = Math.sqrt(aspect * widthTotal * heightTotal / numCells);
//fill the window horizantally
var minNx = Math.floor(widthTotal / widthIndividual);
var maxNy = Math.ceil(numCells / minNx);
//fill the window vertically
var maxNx = Math.ceil(widthTotal / widthIndividual);
var minNy = Math.ceil(numCells / maxNx);
/*
var horizontalWidth = Math.min((widthTotal / maxNx),
(aspect * heightTotal / minNy ));
*/
while (widthTotal / maxNx / aspect * minNy > heightTotal ) {
maxNx += 1;
minNy = Math.ceil(numCells / maxNx);
}
var horizontalWidth = widthTotal / maxNx;
var horizontalHeight = horizontalWidth / aspect;
var verticalHeight = (heightTotal / maxNy);
var verticalWidth = verticalHeight * aspect;
var totalAreaHorizontal = numCells * horizontalWidth * horizontalHeight;
var totalAreaVertical = numCells * verticalHeight * verticalWidth;
/*
console.log('totalAreaHorizontal:', totalAreaHorizontal);
console.log('totalAreaVertical:', totalAreaVertical);
*/
//if (totalAreaHorizontal > totalAreaVertical) {
if (true) {
widthI = horizontalWidth;
heightI = horizontalHeight;
numX = maxNx;
numY = minNy;
} else {
widthI = verticalWidth;
heightI = verticalHeight;
numX = minNx;
numY = maxNy;
}
/*
console.log('horizontalHeight:', horizontalHeight);
console.log('verticalHeight:', verticalHeight);
console.log('heightI', heightI);
*/
return d.map(function(d1, i) {
return {
pos: { x: widthI * (i % numX) ,
y: heightI * Math.floor(i / numX) ,
width: widthI,
height: heightI
},
data: d1
};
});
}
grid.size = function(_) {
if (!arguments.length) return _;
else {
widthTotal = _[0];
heightTotal = _[1];
}
return grid;
};
grid.aspect = function(_) {
if (!arguments.length) return aspect;
else aspect = _;
return grid;
};
return grid;
};
<!DOCTYPE html>
<meta charset="utf-8">
<title>Grid Layout</title>
<link rel='stylesheet' href='page.css'>
<link rel='stylesheet' href='grid-layout.css'>
<h1>Grid Layout</h1>
<body>
<div id="grid-area" ></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="grid-layout.js"></script>
<script src="page.js"></script>
<script type='text/javascript'>
gridLayoutExample();
</script>
</body>
.axis {
font: 10px sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.axis .domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 10px;
stroke-linecap: round;
}
.axis .halo {
fill: none;
stroke: #ddd;
stroke-width: 8px;
stroke-linecap: round;
}
.slider .handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: crosshair;
}
xis {
font: 10px sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.axis .domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 10px;
stroke-linecap: round;
}
.axis .halo {
fill: none;
stroke: #ddd;
stroke-width: 8px;
stroke-linecap: round;
}
.slider .handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: crosshair;
}
.background-rect {
fill: grey;
opacity: 0.2;
}
function gridLayoutExample() {
var totalWidth = 550;
var totalHeight = 300;
var margin = {'top': 55, 'left': 20, 'right':20};
var width = totalWidth - margin.left - margin.right;
var height = totalHeight - margin.top;
var gridLayout = d3.layout.grid()
.size([width, height])
.aspect(1.0);
console.log(gridLayout([1,2,3]));
//when called on an array of objects, gridLayout
//will return another array, each of whose members
//contain the 'pos' and 'data' members
var svg = d3.select('#grid-area')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
// create a rectangle so we can see the extent of the svg
// canvas
var rectG = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
rectG.append('rect')
.attr('width', width)
.attr('height', height)
.classed('background-rect', true);
/************ Begin Brush stuff *************/
// the x scale for the brush
var x = d3.scale.log()
.domain([0.2, 5])
.range([0, width])
.clamp(true);
var brush = d3.svg.brush()
.x(x)
.extent([0, 0])
.on("brush", brushed)
.on('brushstart', brushstart)
.on('brushend', brushend);
// the ticks that are less than zero should be formatted
// like .1 .2 .3, rather than 0.1, 0.2, 0.3
//
var formatTick = function(d) {
if (d < 1)
return "." + d3.format("1f")(d * 10);
else
return d;
};
var sliderG = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',0)');
sliderG.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + (13 + margin.top / 2) + ")")
.call(d3.svg.axis()
.scale(x)
.orient("top")
.tickFormat(formatTick)
.tickSize(0)
.tickPadding(12))
.select(".domain")
.select(function() { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "halo");
var slider = sliderG.append("g")
.attr("class", "slider")
.call(brush);
slider.selectAll(".extent,.resize")
.remove();
// the label for the brush
// optional in any other context
sliderG.append('text')
.attr('text-anchor', 'middle')
.attr('x', width / 2)
.attr('y', 13)
.text('Aspect Ratio')
// the handle
var handle = slider.
append("circle")
.attr("class", "handle")
.attr("transform", "translate(0," + ( 13 + margin.top / 2 ) + ")")
.attr("r", 9);
slider
.call(brush.extent([1, 1]))
.call(brush.event);
function brushstart() {
}
function brushend() {
lastTime = new Date().getTime();
setTimeout( startPushing(lastTime), duration);
}
function brushed() {
var value = brush.extent()[0];
if (d3.event.sourceEvent) { // not a programmatic event
value = x.invert(d3.mouse(this)[0]);
brush.extent([value, value]);
}
handle.attr("cx", x(value));
gridLayout.aspect(value);
numbers = [];
}
/********* end brush stuff ************/
numbers = [1];
var duration = 500;
var lastTime = new Date().getTime();
function startPushing(time) {
var currentTime = time;
// add new rectangles, one at a time
// if the aspect ratio is changed, then the current time
// will change and older animations won't continue
function push() {
if (currentTime != lastTime)
return;
if (numbers.length > 135)
return;
var rectData = rectG.selectAll('.grid-rect')
.data(gridLayout(numbers));
rectData.exit().remove();
rectData.enter()
.append('rect')
.attr('x', width)
.attr('y', height)
.attr('width', 0)
.attr('height', 0)
.classed('grid-rect', true);
d3.selectAll('.grid-rect')
.transition()
.attr('x', function(d) { return d.pos.x; })
.attr('y', function(d) { return d.pos.y; })
.attr('width', function(d) { return d.pos.width; })
.attr('height', function(d) { return d.pos.height; })
.duration(duration);
console.log('length:', numbers.length);
numbers.push(numbers[numbers.length-1]+1);
setTimeout(push, duration);
}
return push;
}
// start the initial animation which will be changed
// when the user selects a new aspect ratio
lastTime = new Date().getTime();
setTimeout(startPushing(lastTime), duration);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment