About a year ago I contributed a plotting module to SymPy, which was extended with some nice features thanks to a GSoC project just after that. The module supports multiple backends, but for the moment only matplotlib is implemented. It would have been simpler if we had decided to support only matplotlib, but that would mean limiting ourselves to producing math graphics with a tool designed for engineers. Moreover, matplotlib does not support all modes of presentation that we would like to have (e.g. nice renderings of surfaces for when we are solving systems of nonlinear equations). Finally, given its Matlab heritage and dedication to backward compatibility is rather unattractive in its default settings.

Hence I started playing around with D3.js in IPython. I was told that the IPython team will work on a canonical way to represent objects through visualization libraries after the release of version 1.0, but that does not mean that we can not concort together some “Not The Right Way” solution now. Moreover it would permit me to show how easy it is to add a new backend to the SymPy plotting module.

Anyway, if you want to obtain this:

D3.js SymPy plots in IPython

it would be sufficient to make a new backend that uses publish_html and publish_javascript in its show() method.

Start with

class IPythonD3Backend(BaseBackend):
    def __init__(self, parent):
        super(IPythonD3Backend, self).__init__(parent)
        are_3D = [s.is_3D for s in self.parent._series]
        if any(are_3D):
            raise ValueError('The D3 backend can not do 3D.')

For the moment we will restrict ourselves to single lines, as this is just a prototype:

    def process_series(self):
        parent = self.parent

        for s in self.parent._series:
            # Create the collections
            if s.is_2Dline:
                self.data = "[%s]"%(", ".join("[%s, %s]"%_ for _ in zip(*s.get_points())))
            else:
                raise ValueError('The D3 backend does not support this type of plot.')

and in show we construct the html and javascript code that we want to publish.

    def show(self):
        global _ipython_div_counter
        from IPython.core.display import publish_javascript, publish_html
        self.process_series()
        publish_html("""
        <style>
            /* tell the SVG path to be a thin blue line without any area fill */
            path {
                stroke: steelblue;
                stroke-width: 1;
                fill: none;
            }

            .axis {
              shape-rendering: crispEdges;
            }

            .y.axis .minor, .x.axis .minor {
              stroke-opacity: .5;
            }

            .x.axis line, .x.axis path, .y.axis line, .y.axis path {
              stroke: lightgrey;
            }
        </style>

        <div id="sympy_d3_plot_%d" class="aGraph" style="position:absolute;top:0px;left:0; float:left;"></div
        """%_ipython_div_counter)
        publish_javascript("""
        var script = document.createElement("script");
        script.src = "http://d3js.org/d3.v3.min.js";
        document.body.appendChild(script);

        var m = [80, 80, 80, 80]; // margins
        var w = 1000 - m[1] - m[3]; // width
        var h = 400 - m[0] - m[2]; // height

        var data = %s;

        var x = d3.scale.linear().domain([0, 10]).range([0, w]);
        var y = d3.scale.linear().domain([-25, 25]).range([h, 0]);

        var line = d3.svg.line()
            .x(function(d) {
                return x(d[0]);
            })
            .y(function(d) {
                return y(d[1]);
            })

            var graph = d3.select("#sympy_d3_plot_%d").append("svg:svg")
                  .attr("width", w + m[1] + m[3])
                  .attr("height", h + m[0] + m[2])
                  .append("svg:g")
                  .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

            var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(true);
            graph.append("svg:g")
                  .attr("class", "x axis")
                  .attr("transform", "translate(0," + h + ")")
                  .call(xAxis);

            var yAxisLeft = d3.svg.axis().scale(y).ticks(4).orient("left");
            graph.append("svg:g")
                  .attr("class", "y axis")
                  .attr("transform", "translate(-25,0)")
                  .call(yAxisLeft);

            graph.append("svg:path").attr("d", line(data));
        """ % (self.data, _ipython_div_counter))
        _ipython_div_counter += 1

    def save(self, path):
        pass

    def close(self):
        pass

The only slightly nontrivial part is loading the D3 library, which is done with the following snippet:

        var script = document.createElement("script");
        script.src = "http://d3js.org/d3.v3.min.js";
        document.body.appendChild(script);

And the nice thing is that this works for all possible line charts (so parametric charts do not need additional treatment):

D3.js SymPy plots in IPython

Obviously much more can be done, but I will probably wait for IPython 1.0 before writing this new backend.