HiveBrain v1.2.0
Get Started
← Back to all entries
patternhtmlMinor

Simple grid using SVG

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
usingsvgsimplegrid

Problem

I developed this grid which displays number of records for X & Y, user can click each number to see a list. (I didn't add list markup to fiddle yet - so lets not worried about it for now.)

What I believe is that I overcomplicated this simple project by choosing SVG, and it can be improved massively by using simple HTML, maybe in the future (years time I would like to add zooming to it, maybe which is why I used SVG).

Could you improve it to simplest level please, preferably using HTML and avoiding SVG?



`function CreateHeatmap() {
$("#svgContainer").empty();
var
width = $(window).innerWidth() - ($(window).innerWidth() / 50),
height = $(window).innerHeight() - ($(window).innerHeight() / 40),
sideRectW = width / 17,
sideRectH = height / 17,
boxW = (width - sideRectW) / (4 + 1), // getting number "4" from web services as number of boxes depends
boxH = (height - sideRectH) / (4 + 1), // getting number "4" from web services as number of boxes depends
boxSize = boxW + boxH;

var svgContainer = d3.select("#mainContainer")
.append("svg")
.attr("id", "svgContainer")
.attr("width", "100%")
.attr("height", "100%")
.attr("preserveAspectRatio", "none")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("float", "right")
.attr("overflow", "hidden");
svgContainer.append("rect")
.attr("fill", "url(#svgLG1)")
.attr("width", "100%")
.attr("height", "100%");

var CreateGradient = function () {
var gradient = svgContainer.append("svg:linearGradient").attr("id", "svgLG1").attr("x1", "0").attr("y1", "1").attr("x2", "1").attr("y2", "0");
gradient.append("svg:stop").attr("offset", "0").attr("stop-color", "#0f0");
gradient.append("svg:stop").attr("offset", "0.7").attr("stop-color", "#ff0");
gradient.append("svg:stop").attr("offset", "1").attr("stop-color", "#f00");
};
var ShowList

Solution

Indeed, D3 is perfectly suited for this task (besides being joy to work with). The following is a very simple implementation, which delegates to CSS as much as possible. Much of any further customisation one may need can be implemented simply by fiddling with CSS. The whole thing should be easy to wrap up as a module.

We'll append the result to the #grid-container div:



We'll work with this fake data:

function rint(limit) { return Math.floor(Math.random() * limit); }

var rowCount = rint(3) + 3;
var colCount = rint(3) + 3;

var data = d3.range(25).map(function(){ // fake data (refresh few times for variety)
    return {
        x: rint(colCount),
        y: rint(rowCount)
    };
});


We'll map data into a three-dimensional array heat according to those x and y values – heat is what we'll visualise with the help of d3. Each heat[y][x] will contain an array of objects with corresponding x and y values:

var heat = d3.range(rowCount).map(function(){ 
    return d3.range(colCount).map(function(){ 
        return []; 
    });
});

data.forEach(function(d){
    heat[heat.length - d.y - 1][d.x].push(d);
});


Find the "hottest" value for the heat map – we'll use this later to generate a full range of colours:

var maxHeat = d3.max([].concat.apply([], heat), function(box){ 
    return box.length; 
});


Now we do the d3 thing. First, generate row selection and populate it with heat data:

var row = d3.select("#grid-container").selectAll(".row").data(heat);


Append the missing div.rows:

row.enter().append("div").attr("class", "row");


Use row selection to create a nested box selection. Each box will inherit the corresponding datum from row's data:

var box = row.selectAll(".box").data(function(d) { return d; });


We now enter and append all the boxes in the grid:

box.enter().append("div")
    .attr("class", function(d, x, y){ 
        var klas = ["box"]
        if (y == heat.length - 1) klas.push("bottom");
        if (x == 0) klas.push("left");
        return klas.join(" "); 
    })
    .attr({
        "data-row": function(d, x, y){ return heat.length - 1 - y; },
        "data-col": function(d, x, y){ return x; }
    })
    .style("background-color", function(d) {
        return d3.hsl( (1 - (d.length / maxHeat)) * 210, 0.8, 0.4 );
    })
    .text(function(d){ 
        return d.length > 0 ? d.length : ""; 
    });


Notice that we are tagging the left column and the bottom row of boxes with classes .left and .bottom, respectively. We are also appending data-row and data-col attributes that we can use to provide CSS content for pseudo elements which will serve as column and row numbers:

.box.left:before {
        content: attr(data-row);
    }

    .box.bottom:after {
        content: attr(data-col);
    }


Importantly, note that I am here merely using the length of the datum d as the innerText of each box. Keep in mind that that d is a list of objects that share the same x and y. This means that the items of d contain all the information you need to enrich the interface of each div.box (with names, links, images, etc.), or, alternatively, to generate (say, on mouse over event) a second visualisation that would show the list of objects as a "detail" view into the data.

Complete code at: http://jsfiddle.net/vpgsLw1v/2/

Code Snippets

<div id="grid-container"></div>
function rint(limit) { return Math.floor(Math.random() * limit); }

var rowCount = rint(3) + 3;
var colCount = rint(3) + 3;

var data = d3.range(25).map(function(){ // fake data (refresh few times for variety)
    return {
        x: rint(colCount),
        y: rint(rowCount)
    };
});
var heat = d3.range(rowCount).map(function(){ 
    return d3.range(colCount).map(function(){ 
        return []; 
    });
});

data.forEach(function(d){
    heat[heat.length - d.y - 1][d.x].push(d);
});
var maxHeat = d3.max([].concat.apply([], heat), function(box){ 
    return box.length; 
});
var row = d3.select("#grid-container").selectAll(".row").data(heat);

Context

StackExchange Code Review Q#90371, answer score: 5

Revisions (0)

No revisions yet.