Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
imranghory committed Apr 20, 2012
1 parent c2265a4 commit 1810bf0
Show file tree
Hide file tree
Showing 2 changed files with 395 additions and 0 deletions.
153 changes: 153 additions & 0 deletions treemap-raphael.js
@@ -0,0 +1,153 @@
/*
* Treemap Squared 0.5 - Treemap Charting library
*
* http://treemapsquared.github.com
*
* Copyright (c) 2012 Imran Ghory (imranghory@gmail.com)
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*/

(function() {
Treemap.draw = function(){
function isArray(arr) {
return arr && arr.constructor === Array;
}

function isFunction(func) {
return func && func.constructor === Function;
}

function drawTreemap(paper, treemapNodes, labels, styles, index) {
var i,j, newindex;
console.log("drawing: ")
console.log(arguments)

if(isArray(treemapNodes[0][0])) {
for(i=0; i<treemapNodes.length; i++) {
newIndex = index.slice();
newIndex.push(i);
console.log(newIndex);
drawTreemap(paper, treemapNodes[i],labels, styles, newIndex);
}
} else {
for(i=0; i<treemapNodes.length; i++) {
newindex = index.slice();
newindex.push(i);

// figure out the matching label using the index
label = labels;
for(j=0; j<newindex.length; j++){
label = label[newindex[j]];
}

styles['draw'](treemapNodes[i], label, newindex);
}
}
}


function draw(element, width, height, data, labels, styles) {
var paper, background, data, nodes, labelFormatter, boxDrawer;
styles = (typeof styles === "undefined") ? [] : styles;

/* create some default style functions */

// This label formatter calculates a font-size based upon
// average label length and the size of the box the label is
// going into. The maximum font size is set to 20px.
labelFormatter = function () {
averageLabelSize = totalLabelLength(labels) / countLabels(labels);

// total length of labels (i.e [["Italy"],["Spain", "Greece"]] -> 16)
function totalLabelLength(arr) {
var i, total = 0;
if(isArray(arr[0])) {
for(i=0; i<arr.length; i++) {
total += totalLabelLength(arr[i])
}
} else {
for (i = 0; i<arr.length; i++){
total += arr[i].length;
}
}
return total;
}

// count of labels (i.e [["Italy"],["Spain", "Greece"]] -> 3)
function countLabels(arr) {
var i, total = 0;
if(isArray(arr[0])) {
for(i=0; i<arr.length; i++) {
total += countLabels(arr[i])
}
} else {
for (i = 0; i<arr.length; i++){
total += 1;
}
}
return total;
}

function fontSize(width) {
return Math.min( width / (averageLabelSize), 20);
}

function style(coordinates, index) {
return { "fill" : "#FCFCFC", "font-size": fontSize(coordinates[2] - coordinates[0])}
}

return style;
}();


// default box & label drawing routine - in most cases this default one in combination with the other style
// will suffice. Only if you're doing something complex and want to rewrite how the treemap gets drawn
// would you replace this.
boxDrawer = function () {
function drawbox(coordinates, label, newindex) {
var x1=coordinates[0], y1=coordinates[1], x2=coordinates[2], y2=coordinates[3];

// draw box
box = paper.rect(x1, y1, x2 - x1, y2 - y1);

if (isFunction(styles['box'])) {
box.attr(styles['box'](coordinates, newindex));
} else {
box.attr(styles['box']);
}

// draw labels
var text = paper.text((x1 + x2) / 2, (y1 + y2) / 2, label);

if (isFunction(styles['label'])) {
text.attr(styles['label'](coordinates, newindex));
} else {
text.attr(styles['label']);
}
}
return drawbox;
}();

// default style for boxes
boxFormatter = { "stroke": "#FEFEFE", "fill" : "green", "fill-opacity" : "50%"};

styles['background'] = (typeof styles['background'] === "undefined") ? {fill: "green"} : styles['background'];
styles['label'] = (typeof styles['label'] === "undefined") ? labelFormatter : styles['label'];
styles['box'] = (typeof styles['box'] === "undefined") ? boxFormatter : styles['box'];
styles['draw'] = (typeof styles['draw'] === "undefined") ? boxDrawer : styles['draw'];

// create our canvas and style the background
paper = new Raphael(element, width, height);
background = paper.rect(0, 0, width, height);
background.attr(styles['background']);

// generate our treemap from our data
nodes = Treemap.generate(data, width, height);

// draw the generated treemap
drawTreemap(paper, nodes, labels, styles, []);
}

return draw;
}();
})();
242 changes: 242 additions & 0 deletions treemap-squarify.js
@@ -0,0 +1,242 @@
/*
* treemap-squarify.js - open source implementation of squarified treemaps
*
* Treemap Squared 0.5 - Treemap Charting library
*
* http://treemapsquared.github.com
*
* Copyright (c) 2012 Imran Ghory (imranghory@gmail.com)
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*
*
* Implementation of the squarify treemap algorithm described in:
*
* Bruls, Mark; Huizing, Kees; van Wijk, Jarke J. (2000), "Squarified treemaps"
* in de Leeuw, W.; van Liere, R., Data Visualization 2000:
* Proc. Joint Eurographics and IEEE TCVG Symp. on Visualization, Springer-Verlag, pp. 33–42.
*
* Paper is available online at: http://www.win.tue.nl/~vanwijk/stm.pdf
*
* The code in this file is completeley decoupled from the drawing code so it should be trivial
* to port it to any other vector drawing library. Given an array of datapoints this library returns
* an array of cartesian coordinates that represent the rectangles that make up the treemap.
*
* The library also supports multidimensional data (nested treemaps) and performs normalization on the data.
*
* See the README file for more details.
*/

var Treemap = {};

(function() {
Treemap.generate = function(){

function Container(xoffset, yoffset, width, height) {
this.xoffset = xoffset; // offset from the the top left hand corner
this.yoffset = yoffset; // ditto
this.height = height;
this.width = width;

this.shortestEdge = function () {
return Math.min(this.height, this.width);
};

// getCoordinates - for a row of boxes which we've placed
// return an array of their cartesian coordinates
this.getCoordinates = function (row) {
var coordinates = [];
var subxoffset = this.xoffset, subyoffset = this.yoffset; //our offset within the container
var areawidth = sumArray(row) / this.height;
var areaheight = sumArray(row) / this.width;
var i;

if (this.width >= this.height) {
for (i = 0; i < row.length; i++) {
coordinates.push([subxoffset, subyoffset, subxoffset + areawidth, subyoffset + row[i] / areawidth]);
subyoffset = subyoffset + row[i] / areawidth;
}
} else {
for (i = 0; i < row.length; i++) {
coordinates.push([subxoffset, subyoffset, subxoffset + row[i] / areaheight, subyoffset + areaheight]);
subxoffset = subxoffset + row[i] / areaheight;
}
}
return coordinates;
};

// cutArea - once we've placed some boxes into an row we then need to identify the remaining area,
// this function takes the area of the boxes we've placed and calculates the location and
// dimensions of the remaining space and returns a container box defined by the remaining area
this.cutArea = function (area) {
var newcontainer;

if (this.width >= this.height) {
var areawidth = area / this.height;
var newwidth = this.width - areawidth;
newcontainer = new Container(this.xoffset + areawidth, this.yoffset, newwidth, this.height);
} else {
var areaheight = area / this.width;
var newheight = this.height - areaheight;
newcontainer = new Container(this.xoffset, this.yoffset + areaheight, this.width, newheight);
}
return newcontainer;
};
}



// normalize - the Bruls algorithm assumes we're passing in areas that nicely fit into our
// container box, this method takes our raw data and normalizes the data values into
// area values so that this assumption is valid.
function normalize(data, area) {
var normalizeddata = [];
var sum = sumArray(data);
var multiplier = area / sum;
var i;

for (i = 0; i < data.length; i++) {
normalizeddata[i] = data[i] * multiplier;
}
return normalizeddata;
}

// treemapMultidimensional - takes multidimensional data (aka [[23,11],[11,32]] - nested array)
// and recursively calls itself using treemapSingledimensional
// to create a patchwork of treemaps and merge them
function treemapMultidimensional(data, width, height, xoffset, yoffset) {
xoffset = (typeof xoffset === "undefined") ? 0 : xoffset;
yoffset = (typeof yoffset === "undefined") ? 0 : yoffset;

var mergeddata = [];
var mergedtreemap;
var results = [];

if(isArray(data[0])) { // if we've got more dimensions of depth
for(i=0; i<data.length; i++) {
mergeddata[i] = sumMultidimensionalArray(data[i]);
}
mergedtreemap = treemapSingledimensional(mergeddata, width, height, xoffset, yoffset)

for(i=0; i<data.length; i++) {
results.push(treemapMultidimensional(data[i], mergedtreemap[i][2] - mergedtreemap[i][0], mergedtreemap[i][3] - mergedtreemap[i][1], mergedtreemap[i][0], mergedtreemap[i][1]));
}
} else {
results = treemapSingledimensional(data,width,height, xoffset, yoffset);
}
return results;
}



// treemapSingledimensional - simple wrapper around squarify
function treemapSingledimensional(data, width, height, xoffset, yoffset) {
xoffset = (typeof xoffset === "undefined") ? 0 : xoffset;
yoffset = (typeof yoffset === "undefined") ? 0 : yoffset;

var rawtreemap = squarify(normalize(data, width * height), [], new Container(xoffset, yoffset, width, height), []);
return flattenTreemap(rawtreemap)
}

// flattenTreemap - squarify implementation returns an array of arrays of coordinates
// because we have a new array everytime we switch to building a new row
// this converts it into an array of coordinates.
function flattenTreemap(rawtreemap) {
var flattreemap = [];
var i, j;

for (i = 0; i < rawtreemap.length; i++) {
for (j = 0; j < rawtreemap[i].length; j++) {
flattreemap.push(rawtreemap[i][j]);
}
}
return flattreemap;
}

// squarify - as per the Bruls paper
// plus coordinates stack and containers so we get
// usable data out of it
function squarify(data, currentrow, container, stack) {
var length;
var nextdata;

if (data.length === 0) {
stack.push(container.getCoordinates(currentrow));
return;
}

length = container.shortestEdge();
nextdatapoint = data[0];

if (improvesRatio(currentrow, nextdatapoint, length)) {
currentrow.push(nextdatapoint);
squarify(data.slice(1), currentrow, container, stack);
} else {
newcontainer = container.cutArea(sumArray(currentrow), stack);
stack.push(container.getCoordinates(currentrow));
squarify(data, [], newcontainer, stack);
}
return stack;
}

// improveRatio - implements the worse calculation and comparision as given in Bruls
// (note the error in the original paper; fixed here)
function improvesRatio(currentrow, nextnode, length) {
var newrow;

if (currentrow.length === 0) {
return true;
}

newrow = currentrow.slice();
newrow.push(nextnode);

var currentratio = calculateRatio(currentrow, length);
var newratio = calculateRatio(newrow, length);

// the pseudocode in the Bruls paper has the direction of the comparission
// wrong, this is the correct one.
return currentratio >= newratio;
}

// calculateRatio - calculates the maximum width to height ratio of the
// boxes in this row
function calculateRatio(row, length) {
var min = Math.min.apply(Math, row);
var max = Math.max.apply(Math, row);
var sum = sumArray(row);
return Math.max(Math.pow(length, 2) * max / Math.pow(sum, 2), Math.pow(sum, 2) / (Math.pow(length, 2) * min));
}

// isArray - checks if arr is an array
function isArray(arr) {
return arr && arr.constructor === Array;
}

// sumArray - sums a single dimensional array
function sumArray(arr) {
var sum = 0;
var i;

for (i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}

// sumMultidimensionalArray - sums the values in a nested array (aka [[0,1],[[2,3]]])
function sumMultidimensionalArray(arr) {
var i, total = 0;

if(isArray(arr[0])) {
for(i=0; i<arr.length; i++) {
total += sumMultidimensionalArray(arr[i])
}
} else {
total = sumArray(arr)
}
return total;
}

return treemapMultidimensional;
}();
})();

0 comments on commit 1810bf0

Please sign in to comment.