Learning ipywidgets: a very simple d3 "CircleWidget" with a js CircleView

This simple notebook demonstrates:

  • the creation of a d3 canvas for displaying circles
  • python function calls to draw circles (with red borders)
  • mouse click to create circles directly on the d3 svg canvas (with blue borders)
  • syncing of the circleCount variable from browser back to kernel
  • two html/jQuery visual elements: a circleCount readout, a "clear all circles" button
  • state (the circles currently on the canvas) is recorded in javascript, not in python

In [1]:
import ipywidgets as widgets
from traitlets import Int, Unicode, List

class CircleWidget(widgets.DOMWidget):

    _view_name = Unicode('CircleView').tag(sync=True)
    _view_module = Unicode('circle').tag(sync=True)
    newCircleRequest = List().tag(sync=True)
    circleCount = Int(0).tag(sync=True);

    def drawCircle(self, x, y, radius):
       newCircle = {"x": x,  "y": y, "radius": radius};
       self.newCircleRequest = [newCircle];
        
    def getCount(self):
       return(self.circleCount);

In [5]:
%%javascript
"use strict";
require.config({
    paths: {
        d3: '//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min'
       },
    });

require.undef('circle');

define('circle', ["jupyter-js-widgets", "d3"], function(widgets, d3) {
    
    var CircleView = widgets.DOMWidgetView.extend({

        initialize: function() {
           this.options = {};  // if this is missing: "Error setting state: view.options is undefined"
           this.circles = [];
           this.circleCount = 0;
           },

        createDiv: function(){
            var toolbarDiv = $("<div id='toolbarDiv' style='border:1px solid gray; height: 30px; width: 600px'></div>");
            var div = $("<div id='d3DemoDiv' style='border:1px solid red; height: 300px; width: 600px'></div>");
            div.append(toolbarDiv);
            this.circleCountReadout = $("<input type='text' id='circleCountReadout' value='0'/>");
            toolbarDiv.append(this.circleCountReadout);
            var circleView = this;
            function clearCircles() {
               circleView.circles = [];
               circleView.circleCount = 0;
               $("#circleCountReadout").val(0);
               $("#svg").children().remove();
               circleView.model.set("circleCount", 0);
               circleView.touch();
               };

            var clearButton = $('<button>Clear Circles</button>').click(clearCircles);
            toolbarDiv.append(clearButton);
            return(div);
            },
  
        createCanvas: function(){
            debugger;
            var svg = d3.select("#d3DemoDiv")
                       .append("svg")
                       .attr("id", "svg").attr("width", 600).attr("height", 300);
           this.svg = svg;
           var circleView = this;
           svg.on('click', function() {
              var coords = d3.mouse(this);
              var newCircle = {x: coords[0], y: coords[1], radius: 20,
                               borderColor: "black", fillColor: "beige"};
              circleView.circles.push(newCircle);
              circleView.drawCircle(newCircle, "blue");
              });
           }, 

        drawCircle: function(obj, color){
           this.svg.append("circle")
              .style("stroke", color)
              .style("fill", "white")
              .attr("r", obj.radius)
              .attr("cx", obj.x)
              .attr("cy", obj.y)
              .on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
              .on("mouseout",  function(){d3.select(this).style("fill", "white");});
           this.circleCount += 1;
           $("#circleCountReadout").val(this.circleCount);
           this.model.set("circleCount", this.circleCount);
           this.touch();
           },

        render: function() { 
            this.$el.append(this.createDiv());
            this.listenTo(this.model, 'change:newCircleRequest', this.newCircleRequested, this);
            var circleView = this;
            function delayCanvasCreationUntilDivExists(){ 
                 // would be better to trigger on DOM div creation. not knowing how to do that, 
                 // this setTimeout hack will have to suffice.
                circleView.createCanvas()
               }
            setTimeout(delayCanvasCreationUntilDivExists, 0);
            },

        newCircleRequested: function() {
           var newCircle = this.model.get("newCircleRequest")[0];
           this.circles.push(newCircle);
           this.drawCircle(newCircle, "red");
           }

    });
    return {
        CircleView : CircleView
    };
});



In [7]:
cw = CircleWidget(width=500, height=300)
cw



In [4]:
cw.drawCircle(x=30, y=30, radius=10)

In [5]:
cw.drawCircle(x=400, y=200, radius=30)

In [7]:
cw.getCount()


Out[7]:
3

In [ ]: