Ticket #14953: trac_14953.patch

File trac_14953.patch, 19.7 KB (added by ncohen, 7 years ago)
  • doc/en/reference/graphs/index.rst

    # HG changeset patch
    # User Nathann Cohen <nathann.cohen@gmail.com>
    # Date 1374529096 -7200
    #      Mon Jul 22 23:38:16 2013 +0200
    # Node ID f512168463ae7e9859d945203c3a1f0dc01b3aa1
    # Parent  16c21a4f41d4000b3f6176f458e8a60a575524a8
    Graph drawing in javascript using d3.js
    
    diff --git a/doc/en/reference/graphs/index.rst b/doc/en/reference/graphs/index.rst
    a b  
    6767   sage/graphs/linearextensions
    6868   sage/graphs/schnyder
    6969   sage/graphs/graph_plot
     70   sage/graphs/graph_plot_js
    7071   sage/graphs/graph_decompositions/vertex_separation
    7172   sage/graphs/graph_decompositions/rankwidth
    7273   sage/graphs/graph_decompositions/graph_products
  • sage/graphs/generic_graph.py

    diff --git a/sage/graphs/generic_graph.py b/sage/graphs/generic_graph.py
    a b  
    1463614636        """
    1463714637        return self.graphplot(**options).plot()
    1463814638
    14639     def show(self, **kwds):
     14639    def show(self, method = "matplotlib", **kwds):
    1464014640        """
    1464114641        Shows the (di)graph.
    1464214642
    1464314643        INPUT:
    1464414644
    14645         This method accepts any option understood by
    14646         :meth:`~sage.graphs.generic_graph.plot` (graph-specific) or by
    14647         :meth:`sage.plot.graphics.Graphics.show`.
     14645        - ``method`` --
     14646
     14647            - If ``method="matplotlib"`` (default) then graph is drawn as a
     14648              picture file, then displayed. In this situation, the method
     14649              accepts any other option understood by
     14650              :meth:`~sage.graphs.generic_graph.plot` (graph-specific) or by
     14651              :meth:`sage.plot.graphics.Graphics.show`.
     14652
     14653            - If ``method="js"`` the graph is displayed using the `d3.js
     14654              <http://d3js.org/>`_ library in a browser. In this situation, the
     14655              method accepts any other option understood by
     14656              :meth:`sage.graphs.graph_plot_js.show`.
    1464814657
    1464914658        .. NOTE::
    1465014659
    14651             See the documentation of the :mod:`sage.graphs.graph_plot` module
    14652             for information on default arguments of this method.
     14660            - See the documentation of the :mod:`sage.graphs.graph_plot` module
     14661              for information on default arguments of this method.
     14662
     14663            - For the javascript counterpart, refer to
     14664              :mod:`sage.graphs.graph_plot_js`.
    1465314665
    1465414666        EXAMPLES::
    1465514667
     
    1465714669            sage: P = C.plot(vertex_labels=False, vertex_size=0, graph_border=True)
    1465814670            sage: P.show()  # long time (3s on sage.math, 2011)
    1465914671        """
     14672        if method == "js":
     14673            from sage.graphs.graph_plot_js import show
     14674            return show(self, **kwds)
     14675
    1466014676        from graph_plot import graphplot_options
    1466114677
    1466214678        # This dictionary only contains the options that graphplot
  • new file sage/graphs/graph_plot_js.html

    diff --git a/sage/graphs/graph_plot_js.html b/sage/graphs/graph_plot_js.html
    new file mode 100644
    - +  
     1<!DOCTYPE html>
     2<head>
     3  <meta charset="utf-8">
     4  <style>
     5    .node {
     6      stroke: #fff;
     7      stroke-width: 1.5px;
     8    }
     9
     10    .link {
     11      fill:none;
     12      stroke: #000;
     13      stroke-opacity: .6;
     14      opacity: .6
     15    }
     16
     17    .directed {
     18      fill:none;
     19      stroke: #000;
     20      stroke-opacity: .6;
     21      opacity: .6
     22    }
     23    marker {
     24      fill:#bbb;
     25    }
     26  </style>
     27<script src="http://d3js.org/d3.v3.min.js"></script>
     28<script type="text/javascript">
     29window.onload = function(){
     30  var pos;
     31
     32  // Loads the graph data
     33  var mydiv = document.getElementById("mygraph")
     34  var graph_as_string = mydiv.innerHTML
     35  var graph = eval('('+graph_as_string+')');
     36
     37  var width = document.documentElement.clientWidth-32;
     38  var height = document.documentElement.clientHeight-32;
     39
     40  // List of colors
     41  var color = d3.scale.category20();
     42
     43  var force = d3.layout.force()
     44    .charge(graph.charge)
     45    .linkDistance(graph.link_distance)
     46    .linkStrength(graph.link_strength)
     47    .gravity(graph.gravity)
     48    .size([width, height])
     49    .links(graph.links)
     50    .nodes(graph.nodes);
     51
     52  // Adapts the graph layout to the javascript window's dimensions
     53  if(graph.pos.length != 0){
     54    center_and_scale(graph);
     55  }
     56
     57  // SVG window
     58  var svg = d3.select("body").append("svg")
     59    .attr("width", width)
     60    .attr("height", height)
     61
     62  // Edges
     63  var link = svg.selectAll(".link")
     64    .data(force.links())
     65    .enter().append("path")
     66    .attr("class", function(d) { return "link directed"; })
     67    .attr("marker-end", function(d) { return "url(#directed)"; })
     68    .style("stroke",function(d) { return d.color; })
     69    .style("stroke-width", graph.edge_thickness+"px");
     70
     71  // Loops
     72  var loops = svg.selectAll(".loop")
     73  .data(graph.loops)
     74  .enter().append("circle")
     75  .attr("class", "link")
     76  .attr("r", function(d) { return d.curve; })
     77  .style("stroke",function(d) { return d.color; })
     78  .style("stroke-width", graph.edge_thickness+"px");
     79
     80  // Nodes
     81  var node = svg.selectAll(".node")
     82  .data(force.nodes())
     83  .enter().append("circle")
     84  .attr("class", "node")
     85  .attr("r", graph.vertex_size)
     86  .style("fill", function(d) { return color(d.group); })
     87  .call(force.drag)
     88
     89  node.append("title").text(function(d) { return d.name; });
     90
     91  // Vertex labels
     92  if(graph.vertex_labels){
     93    var v_labels = svg.selectAll(".v_label")
     94    .data(force.nodes())
     95    .enter()
     96    .append("svg:text")
     97    .attr("vertical-align", "middle")
     98    .text(function(d) { return d.name; })
     99  }
     100  // Edge labels
     101  if(graph.edge_labels){
     102    var e_labels = svg.selectAll(".e_label")
     103    .data(force.links())
     104    .enter()
     105    .append("svg:text")
     106    .attr("text-anchor", "middle")
     107    .text(function(d) { return d.name; })
     108
     109    var l_labels = svg.selectAll(".l_label")
     110    .data(graph.loops)
     111    .enter()
     112    .append("svg:text")
     113    .attr("text-anchor", "middle")
     114    .text(function(d,i) { return graph.loops[i].name; })
     115  }
     116
     117  // Arrows, for directed graphs
     118  if(graph.directed){
     119    svg.append("svg:defs").selectAll("marker")
     120    .data(["directed"])
     121    .enter().append("svg:marker")
     122    .attr("id", String)
     123    // viewbox is a rectangle with bottom-left corder (0,-2), width 4 and height 4
     124    .attr("viewBox", "0 -2 4 4")
     125    // This formula took some time ... :-P
     126    .attr("refX", Math.ceil(2*Math.sqrt(graph.vertex_size)))
     127    .attr("refY", 0)
     128    .attr("markerWidth", 4)
     129    .attr("markerHeight", 4)
     130    .attr("preserveAspectRatio",false)
     131    .attr("orient", "auto")
     132    .append("svg:path")
     133    // triangles with endpoints (0,-2), (4,0), (0,2)
     134    .attr("d", "M0,-2L4,0L0,2");
     135  }
     136
     137  // The function 'line' takes as input a sequence of tuples, and returns a
     138  // curve interpolating these points.
     139  var line = d3.svg.line()
     140  .interpolate("cardinal")
     141  .tension(.2)
     142  .x(function(d) {return d.x;})
     143  .y(function(d) {return d.y;})
     144
     145  /////////////////////////////////////////////
     146  // This is where all movements are defined //
     147  /////////////////////////////////////////////
     148  force.on("tick", function() {
     149
     150    // Position of vertices
     151    node.attr("cx", function(d) { return d.x; })
     152    .attr("cy", function(d) { return d.y; });
     153
     154    // Position of edges
     155    link.attr("d", function(d) {
     156
     157      // Straight edges
     158      if(d.curve == 0){
     159        return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y;
     160      }
     161      // Curved edges
     162      else {
     163        p = third_point_of_curved_edge(d.source,d.target,d.curve)
     164        return line([{'x':d.source.x,'y':d.source.y},
     165        {'x':p[0],'y':p[1]},
     166        {'x':d.target.x,'y':d.target.y}])
     167      }
     168    });
     169
     170    // Position of Loops
     171    if(graph.loops.length!=0){
     172      loops
     173      .attr("cx",function(d) { return force.nodes()[d.source].x; })
     174      .attr("cy",function(d) { return force.nodes()[d.source].y-d.curve; })
     175    }
     176
     177    // Position of vertex labels
     178    if(graph.vertex_labels){
     179      v_labels
     180      .attr("x",function(d) { return d.x+graph.vertex_size; })
     181      .attr("y",function(d) { return d.y; })
     182    }
     183    // Position of the edge labels
     184    if(graph.edge_labels){
     185      e_labels
     186      .attr("x",function(d) { return third_point_of_curved_edge(d.source,d.target,d.curve+3)[0]; })
     187      .attr("y",function(d) { return third_point_of_curved_edge(d.source,d.target,d.curve+3)[1]; })
     188      l_labels
     189      .attr("x",function(d,i) { return force.nodes()[d.source].x; })
     190      .attr("y",function(d,i) { return force.nodes()[d.source].y-2*d.curve-1; })
     191    }
     192  });
     193
     194    // Returns the coordinates of a point located at distance d from the
     195    // barycenter of two points pa, pb.
     196    function third_point_of_curved_edge(pa,pb,d){
     197        var dx = pb.x - pa.x,
     198        dy = pb.y - pa.y;
     199        var ox=pa.x,oy=pa.y,dx=pb.x,dy=pb.y;
     200        var cx=(dx+ox)/2,cy=(dy+oy)/2;
     201        var ny=-(dx-ox),nx=dy-oy;
     202        var nn = Math.sqrt(nx*nx+ny*ny)
     203        return [cx+d*nx/nn,cy+d*ny/nn]
     204    }
     205
     206    // Applies an homothety to the points of the graph respecting the
     207    // aspect ratio, so that the graph takes the whole javascript
     208    // window and is centered
     209    function center_and_scale(graph){
     210        var minx = graph.pos[0][0];
     211        var maxx = graph.pos[0][0];
     212        var miny = graph.pos[0][1];
     213        var maxy = graph.pos[0][1];
     214
     215        graph.nodes.forEach(function(d, i) {
     216            maxx = Math.max(maxx, graph.pos[i][0]);
     217            minx = Math.min(minx, graph.pos[i][0]);
     218            maxy = Math.max(maxy, graph.pos[i][1]);
     219            miny = Math.min(miny, graph.pos[i][1]);
     220        });
     221
     222        var border = 60
     223        var xspan = maxx - minx;
     224        var yspan = maxy - miny;
     225
     226        var scale = Math.min((height-border)/yspan, (width-border)/xspan);
     227        var xshift = (width-scale*xspan)/2
     228        var yshift = (height-scale*yspan)/2
     229
     230        force.nodes().forEach(function(d, i) {
     231            d.x = scale*(graph.pos[i][0] - minx) + xshift;
     232            d.y = scale*(graph.pos[i][1] - miny) + yshift;
     233        });
     234    }
     235
     236    // Starts the automatic force layout
     237    force.start();
     238    if(graph.pos.length != 0){
     239        force.tick();
     240        force.stop();
     241        graph.nodes.forEach(function(d, i) {
     242            d.fixed=true;
     243        });
     244
     245    }
     246
     247}
     248</script>
     249
     250</head>
     251<body>
     252<div id="mygraph" style="display:none">
     253<!-- This is where the graph data generated by Sage will appear : -->
     254// HEREEEEEEEEEEE
     255</div>
     256</body>
     257</html>
  • new file sage/graphs/graph_plot_js.py

    diff --git a/sage/graphs/graph_plot_js.py b/sage/graphs/graph_plot_js.py
    new file mode 100644
    - +  
     1r"""
     2Graph plotting in Javascript with d3.js
     3
     4This module implements everything that can be used to draw graphs with `d3.js
     5<http://d3js.org/>`_ in Sage.
     6
     7On Python's side, this is mainly done by wrapping a graph's edges and vertices
     8in a structure that can then be used in the javascript code. This javascript
     9code is then inserted into a .html file to be opened by a browser.
     10
     11What Sage feeds javascript with is a "graph" object with the following content:
     12
     13- ``vertices`` -- each vertex is a dictionay defining :
     14
     15    - ``name``  -- The vertex's label
     16
     17    - ``group`` -- the vertex' color (integer)
     18
     19The ID of a vertex is its index in the vertex list.
     20
     21- ``edges`` -- each edge is a dictionary defining :
     22
     23    - ``source`` -- the ID (int) of the edge's source
     24
     25    - ``target`` -- the ID (int) of the edge's destination
     26
     27    - ``color``  -- the edge's color (integer)
     28
     29    - ``value`` -- thickness of the edge
     30
     31    - ``strength`` -- the edge's strength in the automatic layout
     32
     33    - ``color`` -- color (hexadecimal code)
     34
     35    -``curve`` -- distance from the barycenter of the two endpoints and the
     36      center of the edge. It defines the curve or the edge, which can be useful
     37      for multigraphs.
     38
     39- ``pos`` -- a list whose `i` th element is a dictionary defining the position of
     40  the `i` th vertex.
     41
     42It also contains the definition of some numerical/boolean variables whose
     43definition can be found in the documentation of :meth:`show` : ``directed``,
     44``charge``, ``link_distance``, ``link_strength``, ``gravity``, ``vertex_size``,
     45``edge_thickness``.
     46
     47.. TODO::
     48
     49   - Add tooltip like in `<http://bl.ocks.org/bentwonk/2514276>`_.
     50
     51   - Add a zoom through scrolling (`<http://bl.ocks.org/mbostock/3681006>`_).
     52
     53Authors:
     54
     55- Nathann Cohen, Brice Onfroy -- July 2013 --
     56  Initial version of the Sage code,
     57  Javascript code, using examples from `d3.js <http://d3js.org/>`_.
     58
     59Functions
     60---------
     61"""
     62from sage.misc.temporary_file import tmp_filename
     63from sage.plot.colors import rainbow
     64import os
     65
     66#*****************************************************************************
     67#       Copyright (C) 2013 Nathann Cohen <nathann.cohen@gmail.com>
     68#                          Brice Onfroy  <onfroy.brice@gmail.com>
     69#
     70#  Distributed under the terms of the GNU General Public License (GPL)
     71#  as published by the Free Software Foundation; either version 2 of
     72#  the License, or (at your option) any later version.
     73#                  http://www.gnu.org/licenses/
     74#*****************************************************************************
     75
     76def show(G,
     77          vertex_labels = False,
     78          edge_labels = False,
     79          vertex_partition = [],
     80          edge_partition = [],
     81          force_spring_layout = False,
     82          charge=-120,
     83          link_distance=30,
     84          link_strength=2,
     85          gravity=.04,
     86          vertex_size=7,
     87          edge_thickness=4
     88          ):
     89    r"""
     90    Displays the graph in a browser using `d3.js <http://d3js.org/>`_.
     91
     92    INPUT:
     93
     94    - ``G`` -- the graph
     95
     96    - ``vertex_labels`` (boolean) -- Whether to display vertex labels (set to
     97      ``False`` by default).
     98
     99    - ``edge_labels`` (boolean) -- Whether to display edge labels (set to
     100      ``False`` by default).
     101
     102    - ``vertex_partition`` -- a list of lists representing a partition of the
     103      vertex set. Vertices are then colored in the graph according to the
     104      partition. Set to ``[]`` by default.
     105
     106    - ``edge_partition`` -- same as ``vertex_partition``, with edges
     107      instead. Set to ``[]`` by default.
     108
     109    - ``force_spring_layout`` -- whether to take sage's position into account if
     110      there is one (see :meth:`~Graph.get_pos` and :meth:`~Graph.set_pos`), or
     111      to compute a spring layout. Set to ``False`` by default.
     112
     113    - ``vertex_size`` -- The size of a vertex' circle. Set to `7` by default.
     114
     115    - ``edge_thickness`` -- Thickness of an edge. Set to ``4`` by default.
     116
     117    - ``charge`` -- the vertices' charge. Defines how they repulse each
     118      other. See `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
     119      information. Set to ``-120`` by default.
     120
     121    - ``link_distance`` -- See
     122      `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
     123      information. Set to ``30`` by default.
     124
     125    - ``link_strength`` -- See
     126      `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
     127      information. Set to ``2`` by default.
     128
     129    - ``gravity`` -- See
     130      `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
     131      information. Set to ``0.04`` by default.
     132
     133    EXAMPLES::
     134
     135        sage: graphs.RandomTree(50).show(method="js") # optional -- internet
     136
     137        sage: g = graphs.PetersenGraph()
     138        sage: g.show(method = "js", vertex_partition=g.coloring()) # optional -- internet
     139
     140        sage: graphs.DodecahedralGraph().show(method="js", force_spring_layout=True) # optional -- internet
     141
     142        sage: graphs.DodecahedralGraph().show(method="js") # optional -- internet
     143
     144        sage: g = digraphs.DeBruijn(2,2)
     145        sage: g.allow_multiple_edges(True)
     146        sage: g.add_edge("10","10","a")
     147        sage: g.add_edge("10","10","b")
     148        sage: g.add_edge("10","10","c")
     149        sage: g.add_edge("10","10","d")
     150        sage: g.add_edge("01","11","1")
     151        sage: g.show(method="js", vertex_labels=True,edge_labels=True,
     152        ....:        link_distance=200,gravity=.05,charge=-500,
     153        ....:        edge_partition=[[("11","12","2"),("21","21","a")]],
     154        ....:        edge_thickness=4) # optional -- internet
     155
     156    """
     157    directed = G.is_directed()
     158    multiple_edges = G.has_multiple_edges()
     159
     160    # Associated an integer to each vertex
     161    v_to_id = {v:i for i,v in enumerate(G.vertices())}
     162
     163    # Vertex colors
     164    color = {i:len(vertex_partition) for i in range(G.order())}
     165    for i,l in enumerate(vertex_partition):
     166        for v in l:
     167            color[v_to_id[v]] = i
     168
     169    # Vertex list
     170    nodes = []
     171    for v in G.vertices():
     172        nodes.append({"name":str(v),"group":str(color[v_to_id[v]])})
     173
     174    # Edge colors.
     175    edge_color_default = "#aaa"
     176    color_list = rainbow(len(edge_partition))
     177    edge_color = {}
     178    for i,l in enumerate(edge_partition):
     179        for e in l:
     180            u,v,label = e if len(e) == 3 else e+(None,)
     181            edge_color[u,v,label] = color_list[i]
     182            if not directed:
     183                edge_color[v,u,label] = color_list[i]
     184
     185    # Edge list
     186    edges = []
     187    seen = {} # How many times has this edge been seen ?
     188
     189    for u,v,l in G.edges():
     190
     191        # Edge color
     192        color = edge_color.get((u,v,l),edge_color_default)
     193
     194        # Computes the curve of the edge
     195        curve = 0
     196
     197        # Loop ?
     198        if u == v:
     199            seen[u,v] = seen.get((u,v),0)+1
     200            curve = seen[u,v]*10+10
     201
     202        # For directed graphs, one also has to take into accounts
     203        # edges in the opposite direction
     204        elif directed:
     205            if G.has_edge(v,u):
     206                seen[u,v] = seen.get((u,v),0)+1
     207                curve = seen[u,v]*15
     208            else:
     209                if multiple_edges and len(G.edge_label(u,v)) != 1:
     210                    # Multiple edges. The first one has curve 15, then
     211                    # -15, then 30, then -30, ...
     212                    seen[u,v] = seen.get((u,v),0) + 1
     213                    curve = (1 if seen[u,v]%2 else -1)*(seen[u,v]//2)*15
     214
     215        elif not directed and multiple_edges:
     216            # Same formula as above for multiple edges
     217            if len(G.edge_label(u,v)) != 1:
     218                seen[u,v] = seen.get((u,v),0) + 1
     219                curve = (1 if seen[u,v]%2 else -1)*(seen[u,v]//2)*15
     220
     221        # Adding the edge to the list
     222        edges.append({"source":v_to_id[u],
     223                      "target":v_to_id[v],
     224                      "strength":0,
     225                      "color":color,
     226                      "curve":curve,
     227                      "name":str(l) if edge_labels else ""})
     228
     229    loops = [e for e in edges if e["source"]==e["target"]]
     230    edges = [e for e in edges if e["source"]!=e["target"]]
     231
     232    # Defines the vertices' layout if possible
     233    Gpos = G.get_pos()
     234    pos = []
     235    if Gpos is not None and force_spring_layout is False:
     236        charge=0;
     237        link_strength=0;
     238        gravity=0;
     239
     240        for v in G.vertices():
     241            x,y = Gpos[v]
     242            pos.append([x,-y])
     243
     244    # Encodes the data as a JSON string
     245    from json import JSONEncoder
     246    string = JSONEncoder().encode({"nodes":nodes,"links":edges,"loops":loops,"pos":pos,
     247                                   "directed":G.is_directed(),
     248                                   "charge":int(charge),"link_distance":int(link_distance),
     249                                   "link_strength":int(link_strength),"gravity":float(gravity),
     250                                   "vertex_labels":bool(vertex_labels),"edge_labels":bool(edge_labels),
     251                                   "vertex_size":int(vertex_size),
     252                                   "edge_thickness":int(edge_thickness)})
     253
     254    from sage.misc.misc import SAGE_ROOT
     255    js_code_file = open(SAGE_ROOT+"/devel/sage/sage/graphs/graph_plot_js.html",'r')
     256    js_code = js_code_file.read().replace("// HEREEEEEEEEEEE",string)
     257    js_code_file.close()
     258
     259    # Writes the temporary .html file
     260    filename = tmp_filename(ext='.html')
     261    f = open(filename,'w')
     262    f.write(js_code)
     263    f.close()
     264
     265    # Opens the browser
     266    from sage.misc.viewer import browser
     267    os.system('%s %s 2>/dev/null 1>/dev/null &'
     268              % (browser(), filename))
     269