4 // this serves as the end of an edge when creating a link
5 function EdgeEnd(pos_x,pos_y){
9 this.get_pos = function(){
10 return new Vec2(this.x,this.y);
15 // if entity_type == "node":
16 // GraphNode.destruction_callback(entity) is called where entity is a node.
17 // If it returns true the node and all connected edges are destroyed.
18 // if entity_type == "edge":
19 // GraphEdge.destruction_callback(entity) is called where entity is an edge
20 // If it returns true the edge is destroyed
21 // pos_x,pos_y is the relative position of the close button to the entity position (entity.get_pos())
23 function CloseButton(graph, entity, entity_type, pos_x,pos_y){
26 var close_button_radius = graph.style.close_button_radius || 8;
27 var close_circle = graph.r.circle( entity.get_pos().x + pos_x,
28 entity.get_pos().y + pos_y,
29 close_button_radius );
30 //the outer gray circle
31 close_circle.attr({ 'opacity': 0,
32 'fill': graph.style.close_button_color || "black",
35 close_circle.transform(graph.get_transform());
36 graph.set_scrolling(close_circle);
38 //the 'x' inside the circle
39 var close_label = graph.r.text( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y,"x");
40 close_label.attr({ 'fill': graph.style.close_button_x_color || "white",
41 'font-size': close_button_radius,
42 'cursor': 'pointer' });
44 close_label.transform(graph.get_transform());
45 graph.set_scrolling(close_label);
47 // the dummy_circle is used to catch events, and avoid hover in/out madness
48 // between the 'x' and the button
49 var dummy_circle = graph.r.circle( entity.get_pos().x + pos_x,
50 entity.get_pos().y + pos_y,
51 close_button_radius );
52 dummy_circle.attr({'opacity':1, 'fill': 'transparent', 'stroke':'none', 'cursor':'pointer'});
53 dummy_circle.transform(graph.get_transform());
54 graph.set_scrolling(dummy_circle);
56 this.get_pos = function(){
57 return entity.get_pos().add_xy(pos_x,pos_y);
60 this.update_pos = function(){
61 var pos = self.get_pos();
62 close_circle.attr({'cx':pos.x, 'cy':pos.y});
63 dummy_circle.attr({'cx':pos.x, 'cy':pos.y});
64 close_label.attr({'x':pos.x, 'y':pos.y});
68 if(!visible){ return; }
69 close_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic');
70 dummy_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic');
73 if(!visible){ return; }
74 close_circle.animate({'r': close_button_radius},400,'linear');
75 dummy_circle.animate({'r': close_button_radius},400,'linear');
77 dummy_circle.hover(hover_in,hover_out);
79 function click_action(){
80 if(!visible){ return; }
82 close_circle.attr({'r': close_button_radius * 2 });
83 dummy_circle.attr({'r': close_button_radius * 2 });
84 close_circle.animate({'r': close_button_radius }, 400, 'linear');
85 dummy_circle.animate({'r': close_button_radius }, 400, 'linear');
87 if(entity_type == "node"){
88 if(GraphNode.destruction_callback(entity)){
89 //console.log("remove node",entity);
92 }else if(entity_type == "edge"){
93 if(GraphEdge.destruction_callback(entity)){
94 //console.log("remove edge",entity);
99 dummy_circle.click(click_action);
101 this.show = function(){
103 close_circle.animate({'opacity':1}, 100, 'linear');
104 close_label.animate({'opacity':1}, 100, 'linear');
108 this.hide = function(){
110 close_circle.animate({'opacity':0}, 100, 'linear');
111 close_label.animate({'opacity':0}, 100, 'linear');
115 //destroy this object and remove it from the graph
116 this.remove = function(){
119 close_circle.animate({'opacity':0}, 100, 'linear');
120 close_label.animate({'opacity':0}, 100, 'linear',self.remove);
122 close_circle.remove();
123 close_label.remove();
124 dummy_circle.remove();
129 // connectors are start and end point of edge creation drags.
130 function Connector(graph,node,pos_x,pos_y){
132 var conn_circle = graph.r.circle(node.get_pos().x + pos_x, node.get_pos().y + pos_y,4);
133 conn_circle.attr({ 'opacity': 0,
134 'fill': graph.style.node_outline_color,
136 conn_circle.transform(graph.get_transform());
137 graph.set_scrolling(conn_circle);
141 this.update_pos = function(){
142 conn_circle.attr({'cx':node.get_pos().x + pos_x, 'cy':node.get_pos().y + pos_y});
144 this.get_pos = function(){
145 return new node.get_pos().add_xy(pos_x,pos_y);
147 this.remove = function(){
148 conn_circle.remove();
151 if(!visible){ return;}
152 conn_circle.animate({'r':8},300,'elastic');
153 if(graph.creating_edge){
154 graph.target_node = node;
155 conn_circle.animate({ 'fill': graph.style.connector_active_color,
156 'stroke': graph.style.node_outline_color,
157 'stroke-width': graph.style.node_selected_width,
161 function hover_out(){
162 if(!visible){ return;}
163 conn_circle.animate({ 'r':graph.style.connector_radius,
164 'fill':graph.style.node_outline_color,
165 'stroke':'none'},400,'linear');
166 graph.target_node = null;
168 conn_circle.hover(hover_in,hover_out);
171 var drag_down = function(){
172 if(!visible){ return; }
173 self.ox = conn_circle.attr("cx");
174 self.oy = conn_circle.attr("cy");
175 self.edge_start = new EdgeEnd(self.ox,self.oy);
176 self.edge_end = new EdgeEnd(self.ox, self.oy);
177 self.edge_tmp = new GraphEdge(graph,'',self.edge_start,self.edge_end,true);
178 graph.creating_edge = true;
180 var drag_move = function(dx,dy){
181 if(!visible){ return; }
182 self.edge_end.x = self.ox + dx;
183 self.edge_end.y = self.oy + dy;
184 self.edge_tmp.update();
186 var drag_up = function(){
187 if(!visible){ return; }
188 graph.creating_edge = false;
189 self.edge_tmp.remove();
190 if(graph.target_node){
191 var edge_prop = GraphEdge.creation_callback(node,graph.target_node);
193 var new_edge = new GraphEdge(graph,edge_prop.label, node,graph.target_node);
194 GraphEdge.new_edge_callback(new_edge);
198 conn_circle.drag(drag_move,drag_down,drag_up);
202 conn_circle.animate({'opacity':1}, 100, 'linear');
208 conn_circle.animate({'opacity':0}, 100, 'linear');
216 //Creates a new graph on raphael document r.
217 //style is a dictionary containing the style definitions
218 //viewport (optional) is the dom element representing the viewport of the graph. It is used
219 //to prevent scrolling to scroll the graph outside the viewport.
221 function Graph(r,style,viewport){
223 var nodes = []; // list of all nodes in the graph
224 var edges = []; // list of all edges in the graph
225 var graph = {}; // graph[n1.uid][n2.uid] -> list of all edges from n1 to n2
226 var links = {}; // links[n.uid] -> list of all edges from or to n
227 var uid = 1; // all nodes and edges have an uid used to order their display when they are curved
228 var selected_entity = null; //the selected entity (node or edge)
230 self.creating_edge = false; // true if we are dragging a new edge onto a node
231 self.target_node = null; // this holds the target node when creating an edge and hovering a connector
232 self.r = r; // the raphael instance
233 self.style = style; // definition of the colors, spacing, fonts, ... used by the elements
234 var tr_x = 0, tr_y = 0; // global translation coordinate
236 var background = r.rect(0,0,'100%','100%').attr({'fill':'white', 'stroke':'none', 'opacity':0, 'cursor':'move'});
238 // return the global transform of the scene
239 this.get_transform = function(){
240 return "T"+tr_x+","+tr_y
244 // translate every element of the graph except the background.
245 // elements inserted in the graph after a translate_all() must manually apply transformation
246 // via get_transform()
247 var translate_all = function(dx,dy){
250 var tstr = self.get_transform();
252 r.forEach(function(el){
253 if(el != background){
258 //returns {minx, miny, maxx, maxy}, the translated bounds containing all nodes
259 var get_bounds = function(){
260 var minx = Number.MAX_VALUE;
261 var miny = Number.MAX_VALUE;
262 var maxx = Number.MIN_VALUE;
263 var maxy = Number.MIN_VALUE;
265 for(var i = 0; i < nodes.length; i++){
266 var pos = nodes[i].get_pos();
267 minx = Math.min(minx,pos.x);
268 miny = Math.min(miny,pos.y);
269 maxx = Math.max(maxx,pos.x);
270 maxy = Math.max(maxy,pos.y);
273 minx = minx - style.node_size_x / 2 + tr_x;
274 miny = miny - style.node_size_y / 2 + tr_y;
275 maxx = maxx + style.node_size_x / 2 + tr_x;
276 maxy = maxy + style.node_size_y / 2 + tr_y;
278 return { minx:minx, miny:miny, maxx:maxx, maxy:maxy };
281 // returns false if the translation dx,dy of the viewport
282 // hides the graph (with optional margin)
283 var translation_respects_viewport = function(dx,dy,margin){
287 margin = margin || 0;
288 var b = get_bounds();
289 var width = viewport.offsetWidth;
290 var height = viewport.offsetHeight;
292 if( ( dy < 0 && b.maxy + dy < margin ) ||
293 ( dy > 0 && b.miny + dy > height - margin ) ||
294 ( dx < 0 && b.maxx + dx < margin ) ||
295 ( dx > 0 && b.minx + dx > width - margin ) ){
301 //Adds a mousewheel event callback to raph_element that scrolls the viewport
302 this.set_scrolling = function(raph_element){
303 $(raph_element.node).bind('mousewheel',function(event,delta){
305 if( translation_respects_viewport(0,dy, style.viewport_margin) ){
312 // Graph translation when background is dragged
313 var bg_drag_down = function(){
316 var bg_drag_move = function(x,y){
321 if( translation_respects_viewport(dx,dy, style.viewport_margin) ){
322 translate_all(dx,dy);
325 var bg_drag_up = function(){};
326 background.drag( bg_drag_move, bg_drag_down, bg_drag_up);
328 this.set_scrolling(background);
330 //adds a node to the graph and sets its uid.
331 this.add_node = function (n){
336 //return the list of all nodes in the graph
337 this.get_node_list = function(){
341 //adds an edge to the graph and sets its uid
342 this.add_edge = function (n1,n2,e){
345 if(!graph[n1.uid]) graph[n1.uid] = {};
346 if(!graph[n1.uid][n2.uid]) graph[n1.uid][n2.uid] = [];
347 if(!links[n1.uid]) links[n1.uid] = [];
348 if(!links[n2.uid]) links[n2.uid] = [];
350 graph[n1.uid][n2.uid].push(e);
351 links[n1.uid].push(e);
353 links[n2.uid].push(e);
357 //removes an edge from the graph
358 this.remove_edge = function(edge){
359 edges = _.without(edges,edge);
360 var n1 = edge.get_start();
361 var n2 = edge.get_end();
362 links[n1.uid] = _.without(links[n1.uid],edge);
363 links[n2.uid] = _.without(links[n2.uid],edge);
364 graph[n1.uid][n2.uid] = _.without(graph[n1.uid][n2.uid],edge);
365 if ( selected_entity == edge ){
366 selected_entity = null;
369 //removes a node and all connected edges from the graph
370 this.remove_node = function(node){
371 var linked_edges = self.get_linked_edge_list(node);
372 for(var i = 0; i < linked_edges.length; i++){
373 linked_edges[i].remove();
375 nodes = _.without(nodes,node);
377 if ( selected_entity == node ){
378 selected_entity = null;
383 //return the list of edges from n1 to n2
384 this.get_edge_list = function(n1,n2){
386 if(!graph[n1.uid]) return list;
387 if(!graph[n1.uid][n2.uid]) return list;
388 return graph[n1.uid][n2.uid];
390 //returns the list of all edge connected to n
391 this.get_linked_edge_list = function(n){
392 if(!links[n.uid]) return [];
395 //return a curvature index so that all edges connecting n1,n2 have different curvatures
396 this.get_edge_curvature = function(n1,n2,e){
397 var el_12 = this.get_edge_list(n1,n2);
398 var c12 = el_12.length;
399 var el_21 = this.get_edge_list(n2,n1);
400 var c21 = el_21.length;
401 if(c12 + c21 == 1){ // only one edge
405 for(var i = 0; i < c12; i++){
406 if (el_12[i].uid < e.uid){
410 if(c21 == 0){ // all edges in the same direction
411 return index - (c12-1)/2.0;
419 // Returns the angle in degrees of the edge loop. We do not support more than 8 loops on one node
420 this.get_loop_angle = function(n,e){
421 var loop_list = this.get_edge_list(n,n);
423 var slots = []; // the 8 angles where we can put the loops
424 for(var angle = 0; angle < 360; angle += 45){
425 slots.push(Vec2.new_polar_deg(1,angle));
428 //we assign to each slot a score. The higher the score, the closer it is to other edges.
429 var links = this.get_linked_edge_list(n);
430 for(var i = 0; i < links.length; i++){
432 if(!edge.is_loop || edge.is_loop()){
435 var end = edge.get_end();
437 end = edge.get_start();
439 var dir = end.get_pos().sub(n.get_pos()).normalize();
440 for(var s = 0; s < slots.length; s++){
441 var score = slots[s].dot(dir);
443 score = -0.2*Math.pow(score,2);
445 score = Math.pow(score,2);
448 slots[s].score = score;
450 slots[s].score += score;
454 //we want the loops with lower uid to get the slots with the lower score
455 slots.sort(function(a,b){ return a.score < b.score ? -1: 1; });
458 for(var i = 0; i < links.length; i++){
460 if(!edge.is_loop || !edge.is_loop()){
463 if(edge.uid < e.uid){
467 index %= slots.length;
469 return slots[index].angle_deg();
472 //selects a node or an edge and deselects everything else
473 this.select = function(entity){
475 if(selected_entity == entity){
478 if(selected_entity.set_not_selected){
479 selected_entity.set_not_selected();
481 selected_entity = null;
484 selected_entity = entity;
485 if(entity && entity.set_selected){
486 entity.set_selected();
491 // creates a new Graph Node on Raphael document r, centered on [pos_x,pos_y], with label 'label',
492 // and of type 'circle' or 'rect', and of color 'color'
493 function GraphNode(graph,pos_x, pos_y,label,type,color){
496 var sy = graph.style.node_size_y;
497 var sx = graph.style.node_size_x;
499 var selected = false;
500 this.connectors = [];
501 this.close_button = null;
504 graph.add_node(this);
506 if(type == 'circle'){
507 node_fig = r.ellipse(pos_x,pos_y,sx/2,sy/2);
509 node_fig = r.rect(pos_x-sx/2,pos_y-sy/2,sx,sy);
511 node_fig.attr({ 'fill': color,
512 'stroke': graph.style.node_outline_color,
513 'stroke-width': graph.style.node_outline_width,
514 'cursor':'pointer' });
515 node_fig.transform(graph.get_transform());
516 graph.set_scrolling(node_fig);
518 var node_label = r.text(pos_x,pos_y,label);
519 node_label.attr({ 'fill': graph.style.node_label_color,
520 'font-size': graph.style.node_label_font_size,
521 'cursor': 'pointer' });
522 node_label.transform(graph.get_transform());
523 graph.set_scrolling(node_label);
525 // redraws all edges linked to this node
526 var update_linked_edges = function(){
527 var edges = graph.get_linked_edge_list(self);
528 for(var i = 0; i < edges.length; i++){
533 // sets the center position of the node
534 var set_pos = function(pos){
535 if(type == 'circle'){
536 node_fig.attr({'cx':pos.x,'cy':pos.y});
538 node_fig.attr({'x':pos.x-sx/2,'y':pos.y-sy/2});
540 node_label.attr({'x':pos.x,'y':pos.y});
541 for(var i = 0; i < self.connectors.length; i++){
542 self.connectors[i].update_pos();
544 if(self.close_button){
545 self.close_button.update_pos();
547 update_linked_edges();
549 // returns the figure used to draw the node
550 var get_fig = function(){
553 // returns the center coordinates
554 var get_pos = function(){
555 if(type == 'circle'){
556 return new Vec2(node_fig.attr('cx'), node_fig.attr('cy'));
558 return new Vec2(node_fig.attr('x') + sx/2, node_fig.attr('y') + sy/2);
561 // return the label string
562 var get_label = function(){
563 return node_label.attr("text");
565 // sets the label string
566 var set_label = function(text){
567 node_label.attr({'text':text});
569 var get_bound = function(){
570 if(type == 'circle'){
571 return new BEllipse(get_pos().x,get_pos().y,sx/2,sy/2);
573 return BRect.new_centered(get_pos().x,get_pos().y,sx,sy);
576 // selects this node and deselects all other nodes
577 var set_selected = function(){
580 node_fig.attr({ 'stroke': graph.style.node_selected_color,
581 'stroke-width': graph.style.node_selected_width });
582 if(!self.close_button){
583 self.close_button = new CloseButton(graph,self, "node" ,sx/2 , - sy/2);
584 self.close_button.show();
586 for(var i = 0; i < self.connectors.length; i++){
587 self.connectors[i].show();
591 // deselect this node
592 var set_not_selected = function(){
594 node_fig.animate({ 'stroke': graph.style.node_outline_color,
595 'stroke-width': graph.style.node_outline_width },
597 if(self.close_button){
598 self.close_button.remove();
599 self.close_button = null;
603 for(var i = 0; i < self.connectors.length; i++){
604 self.connectors[i].hide();
607 var remove = function(){
608 if(self.close_button){
609 self.close_button.remove();
611 for(var i = 0; i < self.connectors.length; i++){
612 self.connectors[i].remove();
614 graph.remove_node(self);
620 this.set_pos = set_pos;
621 this.get_pos = get_pos;
622 this.set_label = set_label;
623 this.get_label = get_label;
624 this.get_bound = get_bound;
625 this.get_fig = get_fig;
626 this.set_selected = set_selected;
627 this.set_not_selected = set_not_selected;
628 this.update_linked_edges = update_linked_edges;
629 this.remove = remove;
632 //select the node and play an animation when clicked
633 var click_action = function(){
634 if(type == 'circle'){
635 node_fig.attr({'rx':sx/2 + 3, 'ry':sy/2+ 3});
636 node_fig.animate({'rx':sx/2, 'ry':sy/2},500,'elastic');
638 var cx = get_pos().x;
639 var cy = get_pos().y;
640 node_fig.attr({'x':cx - (sx/2) - 3, 'y':cy - (sy/2) - 3, 'ẃidth':sx+6, 'height':sy+6});
641 node_fig.animate({'x':cx - sx/2, 'y':cy - sy/2, 'ẃidth':sx, 'height':sy},500,'elastic');
645 node_fig.click(click_action);
646 node_label.click(click_action);
648 //move the node when dragged
649 var drag_down = function(){
650 this.opos = get_pos();
652 var drag_move = function(dx,dy){
653 // we disable labels when moving for performance reasons,
654 // updating the label position is quite expensive
655 // we put this here because drag_down is also called on simple clicks ... and this causes unwanted flicker
656 var edges = graph.get_linked_edge_list(self);
657 for(var i = 0; i < edges.length; i++){
658 edges[i].label_disable();
660 if(self.close_button){
661 self.close_button.hide();
663 set_pos(this.opos.add_xy(dx,dy));
665 var drag_up = function(){
667 var edges = graph.get_linked_edge_list(self);
668 for(var i = 0; i < edges.length; i++){
669 edges[i].label_enable();
671 if(self.close_button){
672 self.close_button.show();
675 node_fig.drag(drag_move,drag_down,drag_up);
676 node_label.drag(drag_move,drag_down,drag_up);
678 //allow the user to create edges by dragging onto the node
680 if(graph.creating_edge){
681 graph.target_node = self;
684 function hover_out(){
685 graph.target_node = null;
687 node_fig.hover(hover_in,hover_out);
688 node_label.hover(hover_in,hover_out);
690 function double_click(){
691 GraphNode.double_click_callback(self);
693 node_fig.dblclick(double_click);
694 node_label.dblclick(double_click);
696 this.connectors.push(new Connector(graph,this,-sx/2,0));
697 this.connectors.push(new Connector(graph,this,sx/2,0));
698 this.connectors.push(new Connector(graph,this,0,-sy/2));
699 this.connectors.push(new Connector(graph,this,0,sy/2));
701 this.close_button = new CloseButton(graph,this,"node",sx/2 , - sy/2 );
704 GraphNode.double_click_callback = function(node){
705 console.log("double click from node:",node);
708 // this is the default node destruction callback. It is called before the node is removed from the graph
709 // and before the connected edges are destroyed
710 GraphNode.destruction_callback = function(node){ return true; };
712 // creates a new edge with label 'label' from start to end. start and end must implement get_pos_*,
713 // if tmp is true, the edge is not added to the graph, used for drag edges.
714 // replace tmp == false by graph == null
715 function GraphEdge(graph,label,start,end,tmp){
718 var curvature = 0; // 0 = straight, != 0 curved
719 var s,e; // positions of the start and end point of the line between start and end
720 var mc; // position of the middle of the curve (bezier control point)
721 var mc1,mc2; // control points of the cubic bezier for the loop edges
722 var elfs = graph.style.edge_label_font_size || 10 ;
723 var label_enabled = true;
724 this.uid = 0; // unique id used to order the curved edges
725 var edge_path = ""; // svg definition of the edge vector path
726 var selected = false;
729 graph.add_edge(start,end,this);
732 //Return the position of the label
733 function get_label_pos(path){
734 var cpos = path.getTotalLength() * 0.5;
735 var cindex = Math.abs(Math.floor(curvature));
736 var mod = ((cindex % 3)) * (elfs * 3.1) - (elfs * 0.5);
737 var verticality = Math.abs(end.get_pos().sub(start.get_pos()).normalize().dot_xy(0,1));
738 verticality = Math.max(verticality-0.5,0)*2;
740 var lpos = path.getPointAtLength(cpos + mod * verticality);
741 return new Vec2(lpos.x,lpos.y - elfs *(1-verticality));
744 //used by close_button
745 this.get_pos = function(){
747 return start.get_pos().lerp(end.get_pos(),0.5);
750 return get_label_pos(edge);
752 var bbox = edge_label.getBBox();
753 return new Vec2(bbox.x + bbox.width, bbox.y);
756 //Straight line from s to e
757 function make_line(){
758 return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y ;
760 //Curved line from s to e by mc
761 function make_curve(){
762 return "M" + s.x + "," + s.y + "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y;
764 //Curved line from s to e by mc1 mc2
765 function make_loop(){
766 return "M" + s.x + " " + s.y +
767 "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " + mc2.y + " " + e.x + " " + e.y;
770 //computes new start and end line coordinates
771 function update_curve(){
774 curvature = graph.get_edge_curvature(start,end,self);
781 mc = s.lerp(e,0.5); //middle of the line s->e
784 se = se.rotate_deg(-90);
785 se = se.scale(curvature * graph.style.edge_spacing);
789 var col = start.get_bound().collide_segment(s,mc);
795 var col = end.get_bound().collide_segment(mc,e);
802 edge_path = make_curve();
804 edge_path = make_line();
806 }else{ // start == end
807 var rad = graph.style.edge_loop_radius || 100;
811 var r = Vec2.new_polar_deg(rad,graph.get_loop_angle(start,self));
813 var p = r.rotate_deg(90);
814 mc1 = mc.add(p.set_len(rad*0.5));
815 mc2 = mc.add(p.set_len(-rad*0.5));
818 var col = start.get_bound().collide_segment(s,mc1);
822 var col = start.get_bound().collide_segment(e,mc2);
827 edge_path = make_loop();
832 var edge = r.path(edge_path).attr({ 'stroke': graph.style.edge_color,
833 'stroke-width': graph.style.edge_width,
834 'arrow-end': 'block-wide-long',
835 'cursor':'pointer' }).insertBefore(graph.get_node_list()[0].get_fig());
836 var labelpos = get_label_pos(edge);
837 var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr({
838 'fill': graph.style.edge_label_color,
840 'font-size': elfs });
842 edge.transform(graph.get_transform());
843 graph.set_scrolling(edge);
845 edge_label.transform(graph.get_transform());
846 graph.set_scrolling(edge_label);
849 //since we create an edge we need to recompute the edges that have the same start and end positions as this one
851 var edges_start = graph.get_linked_edge_list(start);
852 var edges_end = graph.get_linked_edge_list(end);
853 var edges = edges_start.length < edges_end.length ? edges_start : edges_end;
854 for(var i = 0; i < edges.length; i ++){
855 if(edges[i] != self){
860 function label_enable(){
862 label_enabled = true;
863 edge_label.animate({'opacity':1},100,'linear');
864 if(self.close_button){
865 self.close_button.show();
870 function label_disable(){
872 label_enabled = false;
873 edge_label.animate({'opacity':0},100,'linear');
874 if(self.close_button){
875 self.close_button.hide();
879 //update the positions
882 edge.attr({'path':edge_path});
884 var labelpos = get_label_pos(edge);
885 edge_label.attr({'x':labelpos.x, 'y':labelpos.y - 14});
888 // removes the edge from the scene, disconnects it from linked
889 // nodes, destroy its drawable elements.
894 graph.remove_edge(self);
896 if(start.update_linked_edges){
897 start.update_linked_edges();
899 if(start != end && end.update_linked_edges){
900 end.update_linked_edges();
902 if(self.close_button){
903 self.close_button.remove();
907 this.set_selected = function(){
910 edge.attr({ 'stroke': graph.style.node_selected_color,
911 'stroke-width': graph.style.node_selected_width });
912 edge_label.attr({ 'fill': graph.style.node_selected_color });
913 if(!self.close_button){
914 self.close_button = new CloseButton(graph,self,"edge",6,-6);
915 self.close_button.show();
920 this.set_not_selected = function(){
923 edge.animate({ 'stroke': graph.style.edge_color,
924 'stroke-width': graph.style.edge_width }, 100,'linear');
925 edge_label.animate({ 'fill': graph.style.edge_label_color}, 100, 'linear');
926 if(self.close_button){
927 self.close_button.remove();
928 self.close_button = null;
932 function click_action(){
935 edge.click(click_action);
936 edge_label.click(click_action);
938 function double_click_action(){
939 GraphEdge.double_click_callback(self);
942 edge.dblclick(double_click_action);
943 edge_label.dblclick(double_click_action);
946 this.label_enable = label_enable;
947 this.label_disable = label_disable;
948 this.update = update;
949 this.remove = remove;
950 this.is_loop = function(){ return start == end; };
951 this.get_start = function(){ return start; };
952 this.get_end = function(){ return end; };
955 GraphEdge.double_click_callback = function(edge){
956 console.log("double click from edge:",edge);
959 // this is the default edge creation callback. It is called before an edge is created
960 // It returns an object containing the properties of the edge.
961 // If it returns null, the edge is not created.
962 GraphEdge.creation_callback = function(start,end){
964 edge_prop.label = 'new edge!';
967 // This is is called after a new edge is created, with the new edge
969 GraphEdge.new_edge_callback = function(new_edge){};
971 // this is the default edge destruction callback. It is called before
972 // an edge is removed from the graph.
973 GraphEdge.destruction_callback = function(edge){ return true; };
977 // returns a new string with the same content as str, but with lines of maximum 'width' characters.
978 // lines are broken on words, or into words if a word is longer than 'width'
979 function wordwrap( str, width) {
980 // http://james.padolsey.com/javascript/wordwrap-for-javascript/
984 if (!str) { return str; }
985 var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
986 return str.match(new RegExp(regex, 'g') ).join( brk );
989 window.CuteGraph = Graph;
990 window.CuteNode = GraphNode;
991 window.CuteEdge = GraphEdge;
993 window.CuteGraph.wordwrap = wordwrap;