packages feed

profiteur-0.1.0.0: data/js/tree-map.js

TreeMap.HORI = 0;
TreeMap.VERT = 1;

function TreeMap(resizableCanvas, selection, sorting, zoom) {
    var _this          = this;
    this.canvas        = resizableCanvas.getCanvas();
    this.rects         = {};
    this.hover         = undefined;
    this.selection     = selection;
    this.sorting       = undefined;
    this.zoom          = zoom;
    this.renderTimeout = undefined;

    $(this.canvas).mousemove(function(event) {
        var node = _this.findMouseEventNode(event);
        if (_this.hover != node) {
            _this.hover = node;  // Only update when needed
            if (node) _this.canvas.title = node.getCanonicalName();
        }
    });

    $(this.canvas).click(function(event) {
        var node = _this.findMouseEventNode(event);
        if (node) {
            // Select a node on the first click, expand it on second.
            if (node.isSelected()) {
                node.setExpanded(true);
            } else {
                node.select();
            }
        }
    });

    resizableCanvas.addChangeListener(this);
    zoom.addChangeListener(this);
    sorting.addChangeListener(this);
    this.onChange(zoom);
}

TreeMap.prototype.mkRect = function(x, y, w, h) {
    if (x == null || y == null || w == null || h == null) {
        throw 'foo';
    }

    return {
        x: x,
        y: y,
        w: w,
        h: h
    };
};

TreeMap.prototype.mkRootRect = function() {
    return this.mkRect(0, 0, this.canvas.width, this.canvas.height);
};

TreeMap.prototype.findMouseEventNode = function(event) {
    var _this  = this;
    var offset = $(this.canvas).offset();
    var x      = event.pageX - offset.left;
    var y      = event.pageY - offset.top;

    function find(node) {
        for (var i = 0; i < node.children.length; i++) {
            var r = _this.rects[node.children[i].id];
            if (r && x >= r.x && x < r.x + r.w &&
                    y >= r.y && y < r.y + r.h) {
                return find(node.children[i]);
            }
        }

        return node;
    }

    return node = this.zoom.getZoom() ? find(this.zoom.getZoom()) : undefined;
};

TreeMap.prototype.layoutChildren = function(node) {
    var _this = this;

    if (node.children.length > 0 && _this.rects[node.children[0].id]) {
        return;  // Already layed out.
    }

    // Sum of areas
    function sumCosts(nodes) {
        var sum = 0;
        for (var i = 0; i < nodes.length; i++) sum += nodes[i].getCost();
        return sum;
    }

    // Assume that nodes is not empty. Calculates the worst aspect ratio
    // when laying out the given nodes along the given side.
    function worst(nodes, side) {
        var cost = sumCosts(nodes);

        function aspect(node) {
            return Math.max(
                    node.getCost() / cost,
                    cost / node.getCost());
        }

        var max = aspect(nodes[0]);
        for (var i = 1; i < nodes.length; i++) {
            var a = aspect(nodes[i]);
            if (a > max) max = a;
        }

        return max;
    }

    // Layout nodes after a split in the given direction.
    function layoutRow(rect, nodes, dir) {
        var cost = sumCosts(nodes);
        var x    = rect.x;
        var y    = rect.y;
        for (var i = 0; i < nodes.length; i++) {
            if (dir === TreeMap.HORI) {
                var w = rect.w;
                var h = rect.h * nodes[i].getCost() / cost;
                _this.rects[nodes[i].id] = _this.mkRect(x, y, w, h);
                y += h;
            } else {
                var w = rect.w * nodes[i].getCost() / cost;
                var h = rect.h;
                _this.rects[nodes[i].id] = _this.mkRect(x, y, w, h);
                x += w;
            }
        }
    }

    // Main recursive worker. Nodes should not be empty.
    function subdivide(rect, nodes, row, side, dir) {
        var n = nodes[0];

        if (row.length <= 0 ||
                worst(row, side) >= worst(row.concat([n]), side)) {
            var remaining = nodes.slice(1);
            if (remaining.length <= 0) {
                layoutRow(rect, row.concat([n]), dir);
            } else {
                subdivide(rect, remaining, row.concat([n]), side, dir);
            }
        } else {
            var fillCost = sumCosts(row);
            var nextCost = sumCosts(nodes);
            var cost     = fillCost + nextCost;
            var fillRect = undefined;
            var nextRect = undefined;
            if (dir === TreeMap.HORI) {
                var w = rect.w * fillCost / cost;
                fillRect = _this.mkRect(rect.x, rect.y, w, rect.h);
                nextRect = _this.mkRect(
                        rect.x + w, rect.y, rect.w - w, rect.h);
            } else {
                var h = rect.h * fillCost / cost;
                fillRect = _this.mkRect(rect.x, rect.y, rect.w, h);
                nextRect = _this.mkRect(
                        rect.x, rect.y + h, rect.w, rect.h - h);
            }

            layoutRow(fillRect, row, dir);
            squarify(nextRect, nodes);
        }
    }

    // Top-level layouter
    function squarify(rect, nodes) {
        var cost = sumCosts(nodes);
        var dir  = undefined;
        var side = undefined;
        if (rect.w > rect.h) {
            dir  = TreeMap.HORI;
            side = Math.sqrt(cost * rect.w / rect.h);
        } else {
            dir  = TreeMap.VERT;
            side = Math.sqrt(cost * rect.h / rect.w);
        }

        subdivide(rect, nodes, [], side, dir);
    }

    // Go!
    squarify(_this.rects[node.id], node.children);
};

TreeMap.prototype.render = function() {
    var _this   = this;
    window.clearTimeout(_this.renderTimeout);
    _this.renderTimeout = window.setTimeout(function() {
        var context = _this.canvas.getContext('2d');

        // Clear frame
        context.fillStyle = '#ffffff';
        context.fillRect(0, 0, _this.canvas.width, _this.canvas.height);

        // Recursively render all nodes
        _this.renderNode(_this.zoom.getZoom());

        // Render selection
        var selected = _this.selection.getSelectedNode()
        if (selected && _this.rects[selected.id]) {
            var rect = _this.rects[selected.id];
            context.lineWidth   = 3;
            context.strokeStyle = '#ffffff';
            context.strokeRect(rect.x, rect.y, rect.w, rect.h);
        }
    }, 50);
};

TreeMap.prototype.renderNode = function(node) {
    var _this   = this;
    var context = this.canvas.getContext('2d');
    var rect    = this.rects[node.id];

    // The user is not going to see it anyway.
    if (!rect || rect.w < 2 || rect.h < 2) return;

    // Fill entire area
    context.fillStyle = node.getColor();
    context.fillRect(rect.x, rect.y, rect.w, rect.h);

    // Draw children on top.
    if (node.children.length > 0) {
        _this.layoutChildren(node);
        for (var i = 0; i < node.children.length; i++) {
            _this.renderNode(node.children[i]);
        }
    }
};

TreeMap.prototype.onChange = function(source) {
    var _this = this;

    if (source instanceof ResizingCanvas ||
            source instanceof Sorting ||
            source instanceof Zoom) {
        var node = _this.zoom.getZoom();
        _this.rects = {};
        _this.rects[node.id] = _this.mkRootRect();
        _this.render();
        node.addChangeListener(this);
    } else if (source instanceof CostCentreNode) {
        _this.render();
        for (var i = 0; i < source.children.length; i++) {
            source.children[i].addChangeListener(this);
        }
    }
};