6
\$\begingroup\$

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 = function (item) {
        var id = $(item).attr("id");
        var quantity = id.charAt(1);
        var quality = id.charAt(2);
    };
    var CreateRect = function (x, y, boxId) {
        svgContainer.append("g").attr("id", "g" + boxId).on("click", function () {
            ShowList(this);
        }).append("rect").attr("x", x).attr("y", y).attr("id", "rectBoxNo" + boxId).attr("width", boxW).attr("height", boxH).attr("stroke", "black").attr("stroke-width", "1").attr("fill", "none");
    };
    var CreateRects = function () {
        for (var i = 0; i <= 4; i++) { // getting number of x and y from web services, but for example let assume it's 4
            var b = 4;
            for (var j = 0; j <= 4; j++) {
                CreateRect(sideRectW + (boxW * i), boxH * b, "" + i + j);
                b--;
            }
        }
    };
    var CreateCoordinatesRect = function (x, y, w, h, boxColor) {
        svgContainer.append("rect").attr("x", x).attr("y", y).attr("width", w).attr("height", h).attr("fill", boxColor);
    };
    var CreateText = function (x, y, text, textColor, size) {
        svgContainer.append("text").attr("x", x).attr("y", y).attr("fill", textColor).attr("font-size", size).text(text);
    };
    var CreateText90Degree = function (x, y, text, textColor, size) {
        svgContainer.append("text").attr("x", x).attr("y", y).attr("fill", textColor).attr("font-size", size).attr("transform", "rotate(-90," + x + 13 + ", " + y + 15 + ")").text(text);
    };
    var CreateCoordinatesNumbers = function () {
        var j = 0;
        for (var i = 0; i <= 4; i++) {
            CreateText(sideRectW + (boxW * i) + (boxW / 2), height - sideRectH / 2, "" + (j++), "white", 12);
        }
        j = 4;
        for (var i = 0; i <= 4; i++) {
            CreateText(sideRectW / 2, (boxH * i) + (boxH / 2), "" + (j--), "white", 12);
        }
    };
    CreateGradient();
    CreateCoordinatesRect(0, 0, sideRectW, $("#mainContainer").height(), "Black");
    CreateCoordinatesRect(0, height - sideRectH, width, sideRectH, "Black");
    CreateText(((width - sideRectW) / 2) + 13, (height - sideRectH / 5) + 4, "X", "white", 16);
    CreateText90Degree(0, ((height - sideRectH) / 2) + 20, "Y", "white", 16);
    CreateCoordinatesNumbers();
    CreateRects();
}

function PopulateHeatmap(data) {
    var AddNumberOfFruitCratesToEachBox = function (text, xy) {
        var rectContainer = d3.selectAll("#rectBoxNo" + xy);
        var xDivider = 2.5, yDivider = 1.6;
        if (text >= 10 && text <= 99) xDivider = xDivider + (0.6);
        else if (text >= 100) yDivider = yDivider + (0.6);
        var w = Number(rectContainer.attr("width"));
        var h = Number(rectContainer.attr("height"));
        var x = Number(rectContainer.attr("x")) + (w / xDivider);
        var y = Number(rectContainer.attr("y")) + (h / yDivider);
        var svgContainer = d3.selectAll("#svgContainer");

        svgContainer.selectAll("#g" + xy).append("text")
            .attr("x", x)
            .attr("y", y)
            .attr("fill", "white")
            .attr("font-size", 0.15 * (w + h))
            .attr("class", "hover_grouptext").text(text);
    };

    var fCount = {};
    for (var i = 0; i < 4; i += 1) {
        for (var j = 0; j < 4; j += 1) {
            fCount["" + i + j] = 0;
        }
    }
    $.each(data, function (index, item) {
        if (item.x > -1 && item.y > -1) {
            fCount["" + item.x + item.y]++;
        }
    });
    for (var i = 0; i < 4; i += 1) {
        for (var j = 0; j < 4; j += 1) {
            if (fCount["" + i + j] > 0) {
                AddNumberOfFruitCratesToEachBox(fCount["" + i + j], "" + i + j);
            }
        }
    }
}

function OwResize() {
    $("#mainContainer").height($(window).innerHeight() - ($(window).innerHeight() / 40));
}

var containerData = [
  {
    "x": 0,
    "y": 0,
    "Title": "Crate Number 1",
    "Url": "http:www.fruitfactory.com\crates\1",
    "Comments": null,
    "ObjectOther1": [

    ],
    "ObjectOther2": [

    ]
  },
  {
    "x": 0,
    "y": 0,
    "Title": "Crate Number 2",
    "Url": "http:www.fruitfactory.com\crates\2",
    "Comments": null,
    "ObjectOther1": [

    ],
    "ObjectOther2": [

    ]
  },
  {
    "x": 3,
    "y": 3,
    "Title": "Crate Number 3",
    "Url": "http:www.fruitfactory.com\crates\3",
    "Comments": null,
    "ObjectOther1": [

    ],
    "ObjectOther2": [

    ]
  }
];


$(window).resize(OwResize);
CreateHeatmap();
PopulateHeatmap(containerData);
.hover_group:hover {
      opacity: 0.5;
  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<div id="mainContainer">
    <div id="fruit-grid"></div>
</div>

You are free to use anything but it should work with IE-8 & Chrome.

Fiddle

http://jsfiddle.net/nhe613kt/388/

Please Note:

In example code I am adding heatmap and populating them one after another but in real, I am creating it at one point and populating at another, I can't do both at same time as I am getting data from ajax get request.

Bounty Edit

I would expect a better version of what I developed in html using any third party library you want, just one thing need to keep in mind is that, I am creating rectangles or boxes you can say by getting numbers from web services, so hard coding won't help here for number of boxes here as I can do that myself using a table. Thanks for your time :)

\$\endgroup\$
0

1 Answer 1

5
+100
\$\begingroup\$

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:

<div id="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/

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.