4
\$\begingroup\$

I started implementing a graph drawing application in javascript and < canvas > element, and i just wanted to hear your thought on the progress so far. I'm very open to suggestions and i'm very interested to hear what you have to say.
So you can see the progress so far in the present code, if you have any questions about it feel free to ask.

var graph = {

init: function(edges) {
    graph.vertices = {};
    graph.edges = {};
    graph.canvas = document.getElementById('platno');
    graph.width = graph.canvas.width;
    graph.height = graph.canvas.height;
    graph.hookes_test = true;
    graph.ctx = graph.canvas.getContext('2d');

    document.addEventListener("mousedown", graph.klik, false);
    document.addEventListener("mouseup", graph.drop, false);
    document.addEventListener("dblclick", graph.dblclick, false);
    graph.addNodesFromEdgesList(edges);
    graph.addEdgesFromEdgesList(edges);
    graph.mapEdges(graph.oDuljina);
    setInterval(graph.draw, 1024 / 24);
},
addNodesFromEdgesList: function(EdgesList) {
    for (var r1 = 0; r1 < EdgesList.length; r1++) {
        for (var r2 = 0; r2 < EdgesList[r1].length - 1; r2++) {

            if ((typeof graph.vertices[EdgesList[r1][r2]]) === "undefined") {
                graph.addNode({
                    id: EdgesList[r1][r2],
                    x: Math.floor(graph.width / 2 + 100 * Math.cos(Math.PI * (EdgesList[r1][r2] * 2) / 11)),
                    y: Math.floor(graph.height / 2 + 100 * Math.sin(Math.PI * (EdgesList[r1][r2] * 2 / 11))),
                    size: 6,
                    ostalo: 100
                });
            }
        }
    }
},
addEdgesFromEdgesList: function(EdgesList) {
    for (var a = 0; a < EdgesList.length; a++) {
        graph.addEdge({
            from: EdgesList[a][0],
            to: EdgesList[a][1],
            id: a
        });
    }
},
node: function(node) {
    this.id = node.id;
    this.pos = new vektor(node.x, node.y);
    this.size = node.size;
    this._size = node.size;
    this.expanded = false;
},
addNode: function(node) {
    (typeof graph.vertices[node.id]) === "undefined" ? graph.vertices[node.id] = new graph.node(node) : console.log("Duplikat cvora! Id:" + node.id);
},
removeNode: function(id) {
    if (typeof graph.vertices[id] !== "undefined") {
        graph.removeEdgeByNodeId(id);
        if (id == graph.info_node.id) graph.info_node = false;
        delete graph.vertices[id];
    } else {
        console.log("Ne postoji node! Id:" + id);
    }
},
edge: function(edge) {
    this.id = edge.id;
    this.from = graph.vertices[edge.from];
    this.to = graph.vertices[edge.to];
},
addEdge: function(edge) {
    (typeof graph.edges[edge.id]) === "undefined" ? graph.edges[edge.id] = new graph.edge(edge) : console.log("Duplikat brida! Id:" + edge.id);
},
removeEdgeByEdgeId: function(id) {
    (typeof graph.edges[id]) !== "undefined" ? delete graph.edges[id] : console.log("Ne postoji brid! Id:" + id);
},
removeEdgeByNodeId: function(id) {
    if (typeof graph.vertices[id] !== "undefined") {
        for (var edge in graph.edges) {
            if (graph.edges.hasOwnProperty(edge) && (graph.edges[edge].from.id == id || graph.edges[edge].to.id == id)) {
                delete graph.edges[edge];
            }
        }
    } else {
        console.log("Ne postoji cvor! Id:" + id);
    }

},
clearGraph: function() {
    for (var id in graph.vertices) {
        if (graph.vertices.hasOwnProperty(id)) {
            graph.removeNode(id)
        }
    }
},
mapNodes: function(funkcija, obj) {
    var res = [],
        tmp, id;
    for (id in graph.vertices) {
        if (graph.vertices.hasOwnProperty(id)) {
            tmp = funkcija.apply(graph, [graph.vertices[id], obj || {}]);
            if (tmp) res.push(tmp);
        }
    }
    return res;
},
mapEdges: function(funkcija) {
    for (var id in graph.edges) {
        if (graph.edges.hasOwnProperty(id)) {
            funkcija.apply(graph, [graph.edges[id].from, graph.edges[id].to, graph.edges[id].id]);
        }
    }
},
vuci: function(e) {

    if (graph.drag) {
        graph.drag.pos.x = (e.pageX - graph.canvas.offsetLeft);
        graph.drag.pos.y = (e.pageY - graph.canvas.offsetTop);
        if (graph.drag.pos.x > graph.width - 6) graph.drag.pos.x = graph.width - 6;
        else if (graph.drag.pos.x < 6) graph.drag.pos.x = 6;
        else if (graph.drag.pos.y > graph.height - 6) graph.drag.pos.y = graph.height - 6;
        else if (graph.drag.pos.y < 6) graph.drag.pos.y = 6;
    }

},

klik: function(e) {

    graph.drag = graph.getNodeFromXY(e.pageX - graph.canvas.offsetLeft, e.pageY - graph.canvas.offsetTop)[0];
    document.addEventListener("mousemove", graph.vuci, false);
},
drop: function() {
    graph.drag = false;
    document.removeEventListener("mousemove", graph.vuci, false);
},
getNodeFromXY: function(_x, _y) {

    return graph.mapNodes(function(node, obj) {
        if ((obj.x > node.pos.x - node.size) && (obj.x < node.pos.x + node.size) && (obj.y > node.pos.y - node.size) && (obj.y < node.pos.y + node.size)) {
            return node;
        } else {
            return false
        };

    }, {
        x: _x,
        y: _y
    });

},
draw: function() {

    graph.ctx.clearRect(0, 0, graph.width, graph.height);
    background();
    graph.mapEdges(crtaj_v);
    graph.mapNodes(crtaj_n);
    graph.info();
    if (!graph.hookes_test) graph.mapEdges(graph.hookes);

    function background() {
        var grd = graph.ctx.createRadialGradient(graph.width / 2, graph.height / 2, 30, graph.width / 2, graph.height / 2, graph.height);
        grd.addColorStop(0, "#42586d");
        grd.addColorStop(0.5, "#36495a");
        grd.addColorStop(1, "#26323e");
        graph.ctx.fillStyle = grd;
        graph.ctx.fillRect(0, 0, graph.width, graph.height);

    }

    function crtaj_n(v) {
        graph.ctx.fillStyle = 'rgba(0,0,0,0.4)';
        graph.ctx.beginPath();
        graph.ctx.arc(v.pos.x, v.pos.y, v.size, 0, Math.PI * 2, true);
        graph.ctx.fill();
        graph.ctx.strokeStyle = '#818f9a'
        graph.ctx.arc(v.pos.x, v.pos.y, v.size, 0, Math.PI * 2, true);
        graph.ctx.stroke();
        return false;
    }

    function crtaj_v(v1, v2) {
        graph.ctx.beginPath();
        graph.ctx.strokeStyle = 'rga(129,143,154,0.1)';
        var duljina = [v1.pos.udaljenost(v2.pos) - v1.size, v1.pos.udaljenost(v2.pos) - v2.size];
        var kut = Math.atan2(v2.pos.y - v1.pos.y, v2.pos.x - v1.pos.x);
        graph.ctx.moveTo(v2.pos.x - (duljina[0] * Math.cos(kut)), v2.pos.y - (duljina[0] * Math.sin(kut)));
        graph.ctx.lineTo(v1.pos.x + (duljina[1] * Math.cos(kut)), v1.pos.y + (duljina[1] * Math.sin(kut)));
        graph.ctx.stroke();
    }

},

dblclick: function(e) {
    var dbl = graph.getNodeFromXY(e.pageX - platno.offsetLeft, e.pageY - platno.offsetTop)[0] || false;
    if (dbl.expanded) {
        dbl.size = dbl._size;
        dbl.expanded = false;
        graph.info_node = false;
    } else if (dbl) {
        graph.mapNodes(function(v1) {
            if (v1.expanded) {
                v1.size = v1._size;
                v1.expanded = false;
                graph.info_node = false;
            }
        })
        dbl.size = 30;
        dbl.expanded = true;
        graph.info_node = dbl;
    }
},
info: function() {
    if (graph.info_node) {
        graph.ctx.font = "10px Verdana";
        graph.ctx.textAlign = "center";
        graph.ctx.fillStyle = '#ffffff';
        graph.ctx.fillText("Node: " + graph.info_node.id, graph.info_node.pos.x, graph.info_node.pos.y + 3, 30);
    }

},
hookes: function(v1, v2, id) {
    var duljina = v1.pos.oduzmi(v2.pos),
        udaljenost = duljina.duljina() - (graph.edges[id].duljina),
        HL = 20 * (udaljenost / duljina.duljina()),
        kut = Math.atan2(v2.pos.y - v1.pos.y, v2.pos.x - v1.pos.x);
    (graph.drag && (graph.drag.id != v1.id)) || !graph.drag ? graph.zbrojiLokacija(v1, kut, HL) : false;
    (graph.drag && (graph.drag.id != v2.id)) || !graph.drag ? graph.oduzmiLokacija(v2, kut, HL) : false;

},
oDuljina: function(v1, v2, id) {
    graph.hookes_test = false;
    graph.edges[id].duljina = v1.pos.oduzmi(v2.pos).duljina();
},
zbrojiLokacija: function(v1, kut, HL) {
    var dis = new vektor(HL * Math.cos(kut), HL * Math.sin(kut))
    if (v1.pos.x + dis.x > graph.width - v1.size || v1.pos.x + dis.x < 0 + v1.size) {
        v1.pos.x += dis.x * (-1);
        v1.pos.y += dis.y;
    } else if (v1.pos.y + dis.y > graph.height - v1.size || v1.pos.y + dis.y < 0 + v1.size) {
        v1.pos.x += dis.x;
        v1.pos.y += dis.y * (-1);
    } else {
        v1.pos = v1.pos.zbroji(dis)
    }
},

oduzmiLokacija: function(v1, kut, HL) {
    var dis = new vektor(HL * Math.cos(kut), HL * Math.sin(kut))
    if (v1.pos.x + dis.x > graph.width - v1.size || v1.pos.x + dis.x < 0 + v1.size) {
        v1.pos.x -= dis.x * (-1);
        v1.pos.y -= dis.y;
    } else if (v1.pos.y + dis.y > graph.height - v1.size || v1.pos.y + dis.y < 0 + v1.size) {
        v1.pos.x -= dis.x;
        v1.pos.y -= dis.y * (-1);
    } else {
        v1.pos = v1.pos.oduzmi(dis)
    }
}

}

function vektor(x, y) {
    this.x = x;
    this.y = y;
}
vektor.prototype.zbroji = function(v1) {
    return new vektor(this.x + v1.x, this.y + v1.y);
}
vektor.prototype.oduzmi = function(v1) {
    return new vektor(this.x - v1.x, this.y - v1.y);
}

vektor.prototype.division = function(x) {
    return new vektor(this.x / x, this.y / x);
}

vektor.prototype.multiply = function(x) {
    return new vektor(this.x * x, this.y * x);
}

vektor.prototype.udaljenost = function(v1) {
    return Math.sqrt(Math.pow(v1.x - this.x, 2) + Math.pow(v1.y - this.y, 2));
}

vektor.prototype.duljina = function() {
    return Math.max(20, Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2)));
}
//////////////////////////////////////
//  Test data                       //
/////////////////////////////////////
var edges = [
    [1, 2, 1],
    [1, 3, 1],
    [2, 3, 1],
    [3, 4, 1],
    [3, 5, 1],
    [3, 6, 1],
    [4, 1, 1],
    [4, 2, 1],
    [5, 6, 1]
    ];
graph.init(edges);

UPDATE: added new code, this is just a preview i didn't have time to optimize the code, and also some of function names and varibles are written in my native language.

Also I've added a jsfiddle link so you can see the work so far in action. http://jsfiddle.net/nNcHJ/1/

\$\endgroup\$
  • \$\begingroup\$ Maybe you can provide an example of using it? \$\endgroup\$ – Joseph Weissman Sep 4 '11 at 18:33
  • \$\begingroup\$ @Muha An example would be good. I would also start from the API, how would you expect the user to enter the values of a graph. Say you were to define a Traveling Salesman problem? See github.com/ajaxorg/apf/blob/master/CODING_STANDARDS for a good style guide \$\endgroup\$ – yannis Sep 4 '11 at 18:53
  • \$\begingroup\$ @Muha Thanks, you will get a better review, if you translate some of the code to English, klik and vektor are fine but what is zbrojiLokacija? (See en.wikipedia.org/wiki/Hungarian_notation). In general though it is shaping up and looks interesting. Also you need to change the canvas size in the jsFiddle in order to see it say 300px x 300px. \$\endgroup\$ – yannis Sep 4 '11 at 20:42
  • \$\begingroup\$ thank you very much for your feedback, i'm planning to convert the names but i didn't have plenty of time today. but i will get to it soon. \$\endgroup\$ – Muha Sep 4 '11 at 21:37
  • \$\begingroup\$ I would consider using a library to help abstract out some of the Canvas stuff you are doing. I haven't done anything on Canvas yet, but when I used to do stuff like this I would use raphaeljs.com. (Generates SVG). \$\endgroup\$ – Travis Sep 5 '11 at 13:45
2
\$\begingroup\$

I like the code on the whole, I could work with this. My 2 cents:

  • graph.hookes_test , please use lowerCaseCamel, so graph.hookTest
  • graph.oDuljina , please use English
  • document.getElementById('platno'); , if you provide an options parameter to init(edges), then you can write something like document.getElementById( options.canvasID || 'platno');
  • setInterval(graph.draw, 1024 / 24); , put magical numbers together in a section, preferably with a comment as to what the value means, consider to have this overridable by options as well.
  • You have this.size = node.size; and also this._size = node.size;, whatever problem you solve with this should probably be addressed differently
  • typeof graph.vertices[node.id]) === "undefined" probably deserves its own one line function, nodeExists(), you use that expression all over the place
  • clearGraph, why can you not simply either re-init() or set vertices and edges to {} ?
  • in vuci , the magical constant 6 should be a var
  • in draw and info , you should have a var section with all your style info
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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