This is part 3 of a series exploring how to build a full-pledged UML editor in JavaScript. 

 

Getting real

 

In the earlier posts we've built a skeleton UML editor in JavaScript, using Backbone.js & d3.js, and then converted it to CoffeeScript, for better & more readable code. There were however several short-comings in the skeleton that needed to be fixed:

  • Out of neglect, I didn't use Backbone Views, so the diagram wasn't updated on changes
  • The UML diagram wasn't a real class-diagram, just a few class boxes
  • I didn't deploy the app anywhere, so you had to download the app to use it

 

So, in this post I'd like to fix these issues, which will also give me a chance to drill into the frameworks, & explain them using real-life code. In order to deploy the app, I'll use a simple cloud service called StackMob, that makes deployment a breeze.

 

You can play with the deployed app here.

 

 

 

Getting real with Backbone Views

 

I was implemting the skeleton very quickly, & somehow forgot to use Backbone Views for the widgets rendering the UML. This lead to unorganized code, & also caused the diagram to not update on model changes. So, let's fix this.

 

Backbone Views are objects controlling & managing presentation elements - normally in the DOM. They are not to be confused with the Views in MVC, which are the actual presentation elements. This is part of the main motivation of JavaScript MVC frameworks of decoupling the data & business-logic from the DOM. Backbone Models hold & manage the data, & Backbone Views manage the presentation of the data in the DOM.

 

The usual responsibility of Backbone Views is to listen to model events & reflect them in the presentation: create and remove DOM elements, render HTML, and handle user interaction. They are usually organized compositionally - meaning that a container View contains component Views (e.g., List contains Rows).

 

So, let's add 2 Backbone Views: one for the whole Class-Diagram, and one for each Class within it. The 2 classes need to extend Backbone.View. They're basic structure looks like this:

 

class ClassView extends Backbone.View

  initialize: =>
    @model.on "change", @render
    @model.on "destroy", @destroy
    
  render: =>

  destroy: =>


class ClassDiagramView extends Backbone.View
  class_views_by_name: {}
  
  initialize: =>
    @render()
    current_diagram.on "reset", @clear
    current_diagram.on "add", @add_class

  render: =>

  clear: =>
  	view.destroy() for name, view of @class_views_by_name
  	@class_views_by_name = {}

  add_class: (model) =>
  	class_view = new ClassView(model: model)
  	class_view.render()
  	@class_views_by_name[model.get "name"] = class_view

 

 

Backbone Views are initialized in a method called initialize. We use it mainly to bind events to methods. The first view ClassView is initialized with a Backbone Model of a Class, and needs to listen to its change and destroy events. In the render method, we will draw a class box representing the model. The destroy method will remove the elements drawn.

 

The second view ClassDiagramView manages several ClassView components, for each class in the diagram. It listens to events in the global current_diagram Backbone Collection. On add it creates a new ClassView, invokes its render method & adds it to a dictionary of children components (class_views_by_name). On reset it invokes the destroy method of each child, & then initializes the children dictionary.

 

What's missing here is the actual code that renders and destroys the class diagram visualization. To understand this code, let's drill into the framework we're using for manipulating DOM & SVG elements: d3.js.

 

 

Getting real with d3.js

 

d3.js is a framework for binding data to presentation elements. It's actually not so easy to grasp, and even after writing the UML visualization I only partially understand it. Why use it then? Because it's very very powerful, and will save us lots of code.

 

The basic usage idiom of d3.js is roughly this:

  • Select an element, using the d3.select method
  • Assign it an array of data, using the data method
  • If the data array has more items than the number of elements selected, the enter method will refer to a list of new elements per each unassigned data item
  • For each of these items we can create new elements using the append method
  • Finally, we can specify the attributes for each element created, using the attr method.

So, for example the following code will create an SVG element under the element with id "chart", set its width, height & class attributes, and return it to the instance variable called svg:

 

 


    @svg = d3.select("#chart")
      .append("svg")
      .attr("width", w)
      .attr("height", h)
      .attr("class", "pack")
      

 

In this example, we didn't have a data array, and simply created a DOM element. We'll see soon some examples of using data array as well.

 

Let's review the full render method of the ClassDiagramView class, which simply creates the SVG container in which the class diagram will be rendered (using the ClassView components):

 


  render: =>
    w = 675
    h = 360
    pack = d3.layout.pack()
      .size([ w - 4, h - 4 ])
      .value((d) -> d.size)
    @svg = d3.select("#chart")
      .append("svg")
      .attr("width", w)
      .attr("height", h)
      .attr("class", "pack")

    @svg.append("defs")
      .selectAll("marker")
      .data(["inheritance-arrow"])
      .enter()
      .append("marker")
      .attr("id", String)
      .attr("viewBox", "0 -5 10 10")
      .attr("refX", 10)
      .attr("refY", -0.5)
      .attr("markerWidth", 10)
      .attr("markerHeight", 10)
      .attr("orient", "auto")
      .append("svg:path")
      .attr("d", "M0,-5L10,0L0,5")

    @svg = @svg.selectAll("g")
      .data(["root"]).enter()
      .append("g")
      .attr("transform", "translate(2, 2)")
      

 

 

After creating the SVG dom element, we start appending to it sub-elements in the SVG language. What is this language?

 

SVG (Scalable Vector Graphics) is a veteran graphic language, specified by the W3C, that enables defining rich & complex graphics using declarative language, similar to HTML. The nice thing is that this language integrates very well inside the DOM, and can embed & be manipulated by JavaScript and CSS just like HTML. It is designed to be both powerful in terms of graphics, but also offer a simple programming model, with concepts such as defs, g, use which enable abstractions and reuse of elements.

 

Although SVG exists for more than 10 years, it is considered a part of HTML5, and is supported by all modern browsers. HTML5 contains another graphics element called Canvas, whose programming model is not declarative - you need to write the JavaScript code that renders the graphics, and not just add DOM-like elements as in SVG. When you "Inspect Element" on an SVG, you can see it's DOM-like elements, & manipulate them, which is something you can't do in Canvas. Being declarative & inherent part of the DOM is very powerful, but ecause of this, Canvas is considered more suited for visualizations that contain many many elements.

 

I will try to drill in more into SVG in later posts.

 

Let's review now the full views.cofee file, with the render methods that basically generate the SVG elements for the class boxes and inheritance arrows:

 



class ClassView extends Backbone.View

  initialize: =>
    @model.on "change", @render
    @model.on "destroy", @destroy

  set_context: (context) =>
    @svg = context.svg
    @offset = context.offset
    @default_class_width = context.default_class_width
    @default_member_height = context.default_member_height
    @default_member_spacing = context.default_member_spacing
    @class_views_by_name = context.class_views_by_name

  get_offset: (level) =>
    if not @offset[level]
      @offset[level] = 0
    @offset[level]

  set_offset: (level, offset) =>
    @offset[level] = offset

  render: =>
    data = @model.get_all_members()
    if data.length < 2
      data.push(" ")
      data.push(" ")
    level = @model.get_inheritance_depth()
    y = level * 120 + 20
    x = @get_offset(level)
    h = data.length * @default_member_height
    w = @default_class_width
    margin = 10
    @el = @svg.append("g")
      .attr("x", x)
      .attr("y", y)

    @el.append("rect")
      .attr("x", x)
      .attr("y", y)
      .attr("rx", 5)
      .attr("ry", 5)
      .attr("width", w)
      .attr("height", h)
    @el.selectAll("text")
      .data(data)
      .enter()
      .append("text")
      .attr("x", x + margin)
      .attr("y", (d, i) => y + margin + i * @default_member_height)
      .attr("dy", ".35em")
      .text String
    @el.append("line")
      .attr("x1", x)
      .attr("y1", y + @default_member_height)
      .attr("x2", x + @default_class_width)
      .attr("y2", y + @default_member_height)
    @set_offset level, x + @default_class_width + @default_member_spacing

    @top_connect_point =
        x: x + w/2
        y: y

    @bottom_connect_point =
        x: x + w/2
        y: y + h

    if @model.has_super()
      for sc in @model.get "super_classes"
        super_view = @class_views_by_name[sc]
        points = []
        points[0] = x: @top_connect_point.x, y: @top_connect_point.y
        points[1] = x: super_view.bottom_connect_point.x, y: super_view.bottom_connect_point.y
        path_d = "M" + points[0].x + "," + points[0].y
        path_d += " C" + points[0].x + "," + (points[0].y-50)
        path_d += " " + points[1].x + "," + (points[1].y+50)
        path_d += " " + points[1].x + "," + points[1].y
        # TODO use d3.js helper functions
        @el.append("path")
          .attr("d", path_d)
          .attr("stroke", "black")
          .attr("stroke-width", "0.75px")
          .attr("fill", "none")
          .attr("marker-end", "url(#inheritance-arrow)")

    return @

  destroy: =>
    @svg[0][0].removeChild(@el[0][0])


class ClassDiagramView extends Backbone.View
  svg: null
  class_views_by_name: {}
  current_offset: {}
  default_class_width: 150
  default_member_height: 20
  default_member_spacing: 20

  initialize: =>
    @render()
    current_diagram.on "reset", @clear
    current_diagram.on "add", @add_class

  render: =>
    w = 675
    h = 360
    pack = d3.layout.pack()
      .size([ w - 4, h - 4 ])
      .value((d) -> d.size)
    @svg = d3.select("#chart")
      .append("svg")
      .attr("width", w)
      .attr("height", h)
      .attr("class", "pack")

    @svg.append("defs")
      .selectAll("marker")
      .data(["inheritance-arrow"])
      .enter()
      .append("marker")
      .attr("id", String)
      .attr("viewBox", "0 -5 10 10")
      .attr("refX", 10)
      .attr("refY", -0.5)
      .attr("markerWidth", 10)
      .attr("markerHeight", 10)
      .attr("orient", "auto")
      .append("svg:path")
      .attr("d", "M0,-5L10,0L0,5")

    @svg = @svg.selectAll("g")
      .data(["root"]).enter()
      .append("g")
      .attr("transform", "translate(2, 2)")

  clear: =>
    view.destroy() for name, view of @class_views_by_name
    @class_views_by_name = {}
    @current_offset = {}

  add_class: (model) =>
    class_view = new ClassView(model: model)
    class_view.set_context
      svg:                    @svg
      offset:                 @current_offset
      default_class_width:    @default_class_width
      default_member_height:  @default_member_height
      default_member_spacing: @default_member_spacing
      class_views_by_name:    @class_views_by_name
    class_view.render()
    @current_offset = class_view.offset
    @class_views_by_name[model.get "name"] = class_view

view = new ClassDiagramView


 

 

You'll notice that in order for the visualization to work, I added some methods also to the Class model, such as: get_inheritance_depth() which is used to set the y position of each class box. The implementation of these methods is quite straight forward, so here's the full code of the models.cofee file:

 


class DiagramElement extends Backbone.Model
  defaults:
    x: 0
    y: 0
    width: 40
    height: 100

class Class extends DiagramElement
  defaults:
    name: ""
    super_classes: []
    public_methods: []
    private_methods: []

  has_super: =>
    @get("super_classes").length > 0

  get_all_members: =>
    result = []
    name = @get("name")
    if @get("super_classes").length > 0
      name += " : "
      name += @get("super_classes").join(",")
    result.push name
    _.each @get("public_methods"), (n) ->
      result.push n

    _.each @get("private_methods"), (n) ->
      result.push n

    result

  get_super_classes: =>
    return (@collection.find_by_name(name) for name in @get("super_classes"))

  get_inheritance_depth: =>
    if @get("super_classes").length > 0
      supers_depth = (s.get_inheritance_depth() for s in @get_super_classes())
      supers_max = Math.max supers_depth...
      return supers_max + 1
    0

class ClassDiagram extends Backbone.Collection
  model: Class
  source: ""
  get_classes: =>
    (model.get 'name' for model in @models)

  find_by_name: (name) =>
    # TODO use the Backbone.Collection find method
    for m in @models
      if m.get("name") is name
        return m
    null

exports = this
exports.Class = Class
exports.current_diagram = new ClassDiagram


 

 

 

Deploying the app

 

The final thing we want to fix is the fact that our app wasn't deployed anywhere for you to try it. My initial idea was just to throw the static files unto some CDN'ed cloud storage service, such as Amazon S3. However, since I'm using version control, I wished for some better cloud service, that integrates with my VCS. I considered using Heroku, which is very easy to use, integrates great with version control systems, and even supports Node.js and CoffeeScript. But I chose not to use it, because it is intended for Client-Server web apps, and my app is client-side only.

 

So, I decided to try a simple PaaS service called StackMob, intended primarily to serve Mobile apps, but also supporting HTML5 apps, and even Backbone. It seems to be inspired by Heroku, and offers also integration with version control, namely GitHub.

 

My initial version control system was Mercurial, hosted in the BitBucket service, but since StackMob (& many other services) integrate so well with GitHub, I decided to migrate the repository to Git & GitHub. You can find it now in: https://github.com/dibaunaumh/model.

 

The steps for using StackMob are:

  1. Sign up to the service
  2. Enter an application name
  3. Choose HTML5 platform
  4. Click "Manage HTML5" in the side menu
  5. Click on the button "Link StackMob with GitHub":

  6. Authorize StackMob in GitHub
  7. Choose your repository in GitHub
  8. You'll get a screen saying that your app is now deployed, using the latest commit in GitHub.
  9. You'll find in the middle of this page a token and instructions on how to setup a hook in GitHub, so that each push to GitHub will update your app.

 

 

And that's it. We now have our app deployed in: http://dev.model.udibauman.stackmobapp.com/

 

Next..

 

In the next post we'll use other services of StackMob, mainly as Persistence layer of our UML models - StackMob offers a simple integration to Backbone.js, that enable you to persist your Backbone models to their storage backend. That will be quite interesting, because persistence was so far the main reason for having a Server back-end for Client-centric Web apps, and using a 3rd-party Data store service (such as StackMob or IrisCouch) means we don't need to write & maintain any backend.