function HistoryGraph(id, svg, options) {
    var self = this;

    self.custom_select_node = options.select_node || function() {};
    self.custom_ctrl_click = options.ctrl_click || function() {};
    self.custom_size = options.custom_size || function() {
        return [HistoryGraph.consts.width, HistoryGraph.consts.height];
    };
    self.hint_message = options.hint_message;
    if (typeof options.hint_message === undefined) {
        self.hint_message = "Ctrl-click to diff trials";
    }
    self.graph_id = id;

    self.nodes = [];
    self.edges = [];
    self.max = 0;

    self.state = {
        selected_node: null,
        mousedown_node: null,
        just_scale: false
    };
    self.use_tooltip = false;

    self.div = d3.select("body").append("div")
        .attr("class", "now-tooltip now-history-tooltip")
        .style("opacity", 0)
        .on("mouseout", function(){
            self._close_tooltip();
        })

    svg.append("text")
        .text(self.hint_message)
        .attr("dx", 5)
        .attr("dy", 45);

    self.height = self.custom_size()[1];

    var defs = svg.append('svg:defs');
    defs.append('svg:marker')
        .attr('id', 'end-arrow')
        .attr('viewBox', '0 -5 10 10')
        .attr('refX', 6)
        .attr('markerWidth', 3)
        .attr('markerHeight', 3)
        .attr('orient', 'auto')
        .append('svg:path')
        .attr('d', 'M0,-5L10,0L0,5')
        .attr('fill', '#000');

    svg.append('svg:defs').append('svg:marker')
        .attr('id', 'start-arrow')
        .attr('viewBox', '0 -5 10 10')
        .attr('refX', 4)
        .attr('markerWidth', 3)
        .attr('markerHeight', 3)
        .attr('orient', 'auto')
        .append('svg:path')
        .attr('d', 'M10,-5L0,0L10,5')
        .attr('fill', '#000');

    self.svg = svg;
    self.svg_g = svg.append("g")
        .attr("id", self._graph_id())
        .classed(HistoryGraph.consts.graph_class, true);

    var svg_g = self.svg_g;

    self.path = svg_g.append('svg:g').selectAll('path'),
    self.circle = svg_g.append('svg:g').selectAll('g');

    // Mouse events
    svg.on("mouseup", function(d){
        self._svg_mouseup.call(self, d);
    });

    // Drag and Zoom
    self.drag = d3.behavior.drag();
    self.drag_svg = d3.behavior.zoom()
      .on("zoom", function(){
        self._zoomed.call(self);
      })
      .on("zoomstart", function(){
        d3.select('body').style("cursor", "move");
      })
      .on("zoomend", function(){
        d3.select('body').style("cursor", "auto");
      });
    svg.call(self.drag_svg).on("dblclick.zoom", null);

};

HistoryGraph.consts =  {
    selected_class: "selected",
    graph_class: "historygraph",
    move_x: 20,
    move_y: 25,
    move_y2: 10,
    radius: 20,
    spacing: 17,
    margin: 30,
    height: 100,
    width: 200
};

HistoryGraph.prototype._graph_id = function(){
    var self = this;
    return "history-graph-"+self.graph_id;
};


HistoryGraph.prototype._unselect_node = function() {
    var self = this,
        state = self.state;
    self.circle.filter(function(cd){
        return cd.id === state.selected_node.id;
    }).select('circle')
    .classed(HistoryGraph.consts.selected_class, false);

    state.selected_node = null;
};

HistoryGraph.prototype._node_mousedown = function(d3node, d){
    var self = this,
        state = self.state;
    d3.event.stopPropagation();
    state.mousedown_node = d;
    self._close_tooltip();
};

HistoryGraph.prototype._node_mouseup = function(d3node, d){
    var self = this,
        state = self.state,
        consts = self.consts;

    var mousedown_node = state.mousedown_node;

    if (!mousedown_node) return;

    if (state.just_scale) {
        // dragged, not clicked
        state.just_scale = false;
    } else{
        if (d3.event.ctrlKey) {
            self.custom_ctrl_click(d, state.selected_node);
            return;
        }
        if (state.selected_node) {
            self._unselect_node();
        }

        d3node.classed(HistoryGraph.consts.selected_class, true);
        state.selected_node = d;
        self.custom_select_node(d);
    }

    state.mousedown_node = null;
    return;
};

HistoryGraph.prototype._svg_mouseup = function(){
    var state = this.state;
    if (state.just_scale) {
      // dragged not clicked
      state.just_scale = false;
    }
};

HistoryGraph.prototype._update_path = function(path){
    var self = this,
        consts = HistoryGraph.consts,
        state = this.state,
        colors = d3.scale.category10();

    return path.attr("d", function(d){
        var deltaX = d.target.x - d.source.x,
            deltaY = d.target.y - d.source.y,
            dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
            normX = deltaX / dist,
            normY = deltaY / dist,
            sourcePadding = consts.radius + (d.left ? 3 : -5),
            targetPadding = consts.radius + (d.right ? 3 : -5),
            sourceX = d.source.x + (sourcePadding * normX),
            sourceY = d.source.y + (sourcePadding * normY),
            targetX = d.target.x - (targetPadding * normX),
            targetY = d.target.y - (targetPadding * normY);
        var step = 0;
        if (d.level > 0) {
            step += consts.move_y;
            step += (d.level - 1) * consts.move_y2;
        }

        result = 'M' + sourceX + ',' + sourceY;
        result += 'C' + (sourceX - consts.move_x/2) + ',' + (sourceY);
        result += ',' + (sourceX - consts.move_x/2) + ',' + (sourceY + 3*step/4);
        result += ',' + (sourceX - consts.move_x) + ',' + (sourceY + step);

        result += 'L' + (sourceX - consts.move_x) + ',' + (sourceY + step);
        result += ',' + (targetX + consts.move_x) + ',' + (sourceY + step);

        result += 'C' + (targetX + consts.move_x/2) + ',' + (sourceY + 3*step/4);
        result += ',' + (targetX + consts.move_x/2) + ',' + (sourceY);
        result += ',' + targetX + ',' + targetY;

        return result;
    }).style('marker-start', function(d) {
        return d.left ? 'url(#start-arrow)' : '';
    }).style('marker-end', function(d) {
        return d.right ? 'url(#end-arrow)' : '';
    }).style('stroke', function(d) {
        return d3.rgb(colors(d.level)).darker().toString();
    });
};

HistoryGraph.prototype._update_circle = function(circle) {
    var self = this,
        consts = HistoryGraph.consts,
        state = this.state;

    return circle.attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
    }).classed('reflexive', function(d) {
        return d.reflexive;
    }).classed('finished', function(d) {
        return d.info.status == 'Finished';
    }).classed('unfinished', function(d) {
        return d.info.status == 'Unfinished';
    }).classed('backup', function(d) {
        return d.info.status == 'Backup';
    });
};

HistoryGraph.prototype._zoomed = function(){
    var self = this;
    self.state.just_scale = true;
    self._close_tooltip();
    d3.select("#"+self._graph_id())
      .attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); 
};

HistoryGraph.prototype._show_tooltip = function(d) {
    var self = this;
    self.div.classed("hidden", false);
    self.div.transition()
        .duration(200)
        .style("opacity", .9)
    self.div.html(d.tooltip)
        .style("left", (d3.event.pageX - 3) + "px")
        .style("top", (d3.event.pageY - 28) + "px");
};

HistoryGraph.prototype._close_tooltip = function() {
    var self = this;
    self.div.transition()
        .duration(500)
        .style("opacity", 0);
    self.div.classed("hidden", true);
};

HistoryGraph.prototype.load = function(data, width) {
    var self = this,
        nodes = [],
        edges = [],
        spacing = HistoryGraph.consts.spacing,
        margin = HistoryGraph.consts.margin;
    var spacing2 = 2 * spacing,
        spacing4 = 4 * spacing,
        end = width - margin,
        max = 0,
        id = 0,
        last = data.nodes.length - 1;

    for (var i = last; i >= 0; i--) {
        var node = data.nodes[i];
        var x = end - spacing4*id,
            y = margin + node.level * spacing2;

        nodes.push({
            id: id, x: x, y: y,
            title: node.id,
            info: node,
            tooltip: node.tooltip
        })
        max = Math.max(max, y);
        id += 1;
    }
    max += spacing2;
    self.max = max;

    for (var i = 0; i < data.edges.length; i++) {
        var edge = data.edges[i];
        edge.source = nodes[last - edge.source];
        edge.target = nodes[last - edge.target];

        edges.push(edge);
    }

    self.nodes = nodes;
    self.edges = edges;
    self.update_window();
    self.reset_zoom();
    self.restart();

    return nodes;
};

HistoryGraph.prototype.reset_zoom = function() {
    var self = this,
        scale = self.height/self.max;
    if (scale < 1.0) {
        self.drag_svg.scale(scale);
        self.drag_svg.translate([self.width*(1-scale), 0]);
        self.drag_svg.event(self.svg);
    } else {
        self.drag_svg.scale(1);
        self.drag_svg.translate([0, 0]);
        self.drag_svg.event(self.svg);
    }
    self.state.just_scale = false;
};

HistoryGraph.prototype.select_node = function(node) {
    this.state.selected_node = node;
    this.custom_select_node(node);
    d3.select($('#history text.id:contains("'+node.title+'")')
        .siblings()[0]).classed('selected', true)
};

HistoryGraph.prototype.restart = function(){

    var self = this,
        consts = HistoryGraph.consts,
        state = this.state;

    // path (link) group
    self.path = self.path.data(self.edges)

    // update existing links
    self._update_path(self.path);

    // add new paths
    path = self.path.enter().append("svg:path")
        .attr('class', 'link');

    self._update_path(path);

    // remove old links
    self.path.exit().remove();

    // circle (node) group
    self.circle = self.circle.data(self.nodes, function(d) { return d.id; });


    self._update_circle(self.circle.selectAll('circle'));

    // add new nodes
    var g = self.circle.enter().append("svg:g");

    self._update_circle(g.append('svg:circle')
            .classed(consts.cicle_class, true)
            .attr('class', 'node')
            .attr('r', consts.radius)
        ).on('mousedown', function(d) {
            self._node_mousedown.call(self, d3.select(this), d);
        }).on('mouseup', function(d) {
            self._node_mouseup.call(self, d3.select(this), d);
        }).on('mouseover', function(d) {
            if (!self.state.mousedown_node && self.use_tooltip) {
                self._close_tooltip();
                self._show_tooltip(d);
            }
        })
        .call(self.drag);

    g.append('svg:text')
        .attr('x', 0)
        .attr('y', 4)
        .attr('class', 'id')
        .attr("transform", function(d) {
            return "translate(" + d.x + "," + d.y + ")";
        })
        .text(function(d) { return d.title; })

    // remove old nodes
    self.circle.exit().remove();
};

HistoryGraph.prototype.set_use_tooltip = function(use) {
    var self = this;
    self.use_tooltip = use;
};

HistoryGraph.prototype.update_window = function(){
    var self = this,
        size = self.custom_size();
    self.width = size[0];
    self.height = size[1];
    this.svg
        .attr("width", size[0])
        .attr("height", size[1]);
};

function now_history_graph(div_selector, graph_id, data, width, height, tooltip_selector, options) {
    $(div_selector).html('');
    var svg = d3.select(div_selector)
        .append('svg')
        .attr("width", width)
        .attr("height", height);

    var history_graph = new HistoryGraph(graph_id, svg, options);
    history_graph.set_use_tooltip(d3.select(tooltip_selector).property("checked"));
    var nodes = history_graph.load(data, width);
    return {
        'graph': history_graph,
        'nodes': nodes
    }
}