Building a UML editor in JavaScript - part 1

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

 

Why JavaScript

I really love Python & its wonderful frameworks - they brought me pure joy & satisfaction for the past 6 years.

However, I'm forced to acknowledge that the times they are a-changin: logic moves back to the client-side, leaving the server-side to do almost nothing (just data access, usually). Further more, the server side turned into a series of disporate API's, which the client consumes, provided by different vendors - so now even the data access moves to the client, which talks directly with datastore providers.

 

The lingua-franca of the client-side, available anywhere there's a browser, is JavaScript. And, when I needed to write some very scalable server-side service, I also found JavaScript (node.js) doing a really great job even there.

With HTML5, there's no limitation to the abilities of JavaScript on the client-side.

 

In addition, the platform of computing is going thru an even larger change: mobile devices talking with cloud back-ends. I don't know whether the mobile-specific platforms will remain the mobile client-side technology, or the open-stack of the Web will arrive to the client. Time will tell.

 

The application

So, in this series I will develop an application in pure JavaScript, for Web & Mobile platforms. 

I do design very often, but don't have a UML editor that works for me: either they're not cloud-based & don't support collaboration, or they're too unusable, or just costing too much money.

So, I'll try to develop in this series a simple UML editor, which is

  • Cloud-based
  • Supports real-time collaboration
  • Works well on tablet & mobile
  • Free & open-source

 

Mock-up of the application:

 

 

 

Regarding the application name: the main consideration for a name should obviously be SEO - how easy it will be to find it in Google. So, given this imperative consideration, I came up with the name: model 

 

It's going to be pure client-side app - all static files downloaded from CDN to the client The app will consume remote 3rd-party API's, for example, a remote storage for persistence. Using a tool such as PhoneGap, I'll try to make it usable also as a Mobile app. Of course a pure MVC will be enforced: clear separations between data, business logic & presentation .

 

If you were given the task of writing a UML editor, which languages would you use?

 

 

Initial survey

 

Before starting out I've sent a couple of colleagues a survey, asking them about their activities & needs in respect to UML. The idea is to try understand who will be the 1st users of the editor (except for myself) & how to make it useful for them.

You can see (& fill out) the survey here.

 

 

I received some 7 responses, from which I've learnt that:
 

  •  Usage: A third of the responders use it often (every week). A third uses it once a month or few. Another third don't use UML at all.
  •  Tools: Half of the users use desktop tools (StarUML, Sparx, OpenOffice, Violet). Another hald uses online tools (WebSequenceDiagrams, Google Docs). Many said they also use paper & whiteboard.
  •  Diagram types: Most use Class diagram, Sequence diagram & Use-case. Also: Activity, State, Relationship, Work-flow.
  •  Activities: Almost everyone: Embed diagrams in design documents, Present them in design reviews, Collaborate on them with others. Also: Save them in the repository, Revise them when design changes. 
  •  Problems & needs: People expressed the need for online editor. Another requested feature is reverse engineering from existing source repository                      

 

Decisions

 

Possible frameworks & technologies

 

I made a check-list of frameworks & technologies that I'd like to use in this app:

  • CoffeeScript - a high-level language that compiles into JavaScript, & abstracts some of the difficult & bad parts of the language
  • BrowserID - an email-based authentication mechanism by Mozilla
  • RemoteStorage - a mechanism to store data from JavaScript on remote providers
  • Datastore providers: IrisCouch
  • Backbone.js - a popular MVC framework for javascript
  • Backbone.dualStorage - for Backbone storage on both local (HTML5) & remote storage
  • Backbone.boilerplate - template utilities for Backbone
  • Underscore.js - clean & feature-rich JavaScript framework
  • d3.js - data-driven visualization framework
  • Bootstrap2 - HTML5 template
  • Uijet - UI widgets framework
  • Faye & Kue- for real-time collaboration
  • Selenium2 - unit-tests framework for JavaScript * Sauce Labs - hosted service for cross-browser testing using Selenium
  • PhoneGap - framework for generatubg mobile editions of a Web app
  • Google Page Speed- for optimizing loading & performance
  • Google AnalyticsProvide the analytics on a client-side app
  • CloudBees - Continuous Integration & Testing
  • ??? - Hosting & continuous deployment

 

 

Code hosting & IDE

 

BitBucket - source hosting
Since I prefer mercurial, I usually host my projects on BitBucket. The source code can be found in: https://bitbucket.org/dibau/model

 

WebStorm - IDE
This full-pledged JavaScript IDE is based on JetBrains' Java IDE: IntelliJ IDEA. 

The main advantage of using it is the amazing intelligence it has in understanding code, detecting errors & enabling refactoring

I prefer it on any other IDE on this sole ground: especially with dynamic languages, it's crucial to have an editor that understands your code, & can validate it intelligently

 

WebStorm supports debugging of JavaScript ran on the browser from the IDE & comes with many plugins, such as CSS-X-Fire which enables you to update CSS attributes in your browser & automatically update them in your source files. For example, WebStorm can infer the type of function parameters (which aren't typed of course) & detect when a call passes parameters of a different type!

 

 

Debug & inspection

 

Browsers debug console
All modern browser have an interactive console, crucial for debugging & exploratory development

 

JSFiddle                

An online tool to experiment with JavaScript & share it with others                  

 

JSLint

This tool by Douglas Corkford (the main guru & reviver of modern JavaScript) to validate the quality & correctness of your JavaScript code

 

 

 

Templates

 

The project can be started with several templates:

 

Boilerplate
 Many nice features for HTML5 support. Comes with Google Analytics code & also script to install Chrome Frame
Initializr
Service that let's you customize the template, which is based on Boilerplate. Customizing it is a bit annoying - because there are incompatibilities between possible elements, & it takes time to succeed resolving the conflicts. Uses the Modernizr framework for detecting & leveraging HTML5 & CSS3
Bootstrap2
Best practices template by Twitter. Includes both resources & widgets.      

All of these templates support the infra to make the app responsive - adapt to different devices (Computer, Tablet, Mobile, TV)

 

I decided to start with Bootstrap2We'd also take stuff from Initializr/Biolerplate, such as: Chrome Frame & Google Analytics

 

 

 

 

Design & code

 

The initial version of the product is very simple - it let's you describe classes in text, and draws the corresponding class diagram. Here's the back-of-the-envelope design:

                 

Main page (app.html)

The application has 1 main page, using Bootstrap2 as templae. The code is straight-forward:

 

  <body>

    <div class="navbar navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container-fluid">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="#">Model</a>
          <div class="nav-collapse">
            <ul class="nav">
              <li class="active"><a href="#">Home</a></li>
              <li><a href="#about">About</a></li>
              <li><a href="#contact">Contact</a></li>
            </ul>
            <p class="navbar-text pull-right">Logged in as <a href="#">username</a></p>
          </div><!--/.nav-collapse -->
        </div>
      </div>
    </div>

    <div class="container-fluid">
      <div class="row-fluid">
        <div class="span3">
          <div class="well sidebar-nav">
            <ul class="nav nav-list">
              <li class="nav-header">Project 1</li>
              <li class="active"><a href="#">Class Diagram 1</a></li>
              <li><a href="#">Diagram 2</a></li>
              <li><a href="#">Diagram 3</a></li>
              <li><a href="#">Diagram 4</a></li>
              <li class="nav-header">Project 2</li>
              <li><a href="#">Diagram 1</a></li>
              <li><a href="#">Diagram 2</a></li>
              <li><a href="#">Diagram 3</a></li>
              <li><a href="#">Diagram 4</a></li>
              <li><a href="#">Diagram 5</a></li>
              <li><a href="#">Diagram 6</a></li>
              <li class="nav-header">Project 3</li>
              <li><a href="#">Diagram 1</a></li>
              <li><a href="#">Diagram 2</a></li>
              <li><a href="#">Diagram 3</a></li>
            </ul>
          </div><!--/.well -->
        </div><!--/span-->
        <div class="span9">
          <div class="hero-unit">
            <h2>Class Diagram 1</h2>
            <p>This is the diagram description.</p>
          </div>

            <ul id="tab" class="nav nav-tabs">
                <li class="active"><a href="#diagram" data-toggle="tab">Diagram</a></li>
                <li class=""><a href="#source" data-toggle="tab">Source</a></li>
            </ul>
            <div id="myTabContent" class="tab-content">
                <div class="tab-pane fade active in" id="diagram">
                    <div id="chart" style="width: 675px; height: 360px;"></div>
                </div>
                <div class="tab-pane fade" id="source">
                    <div>
                        <textarea id="source_input" rows="20" style="width: 675px; height: 341px;"></textarea>
                        <p class="muted" style="float: right;">
                            For example:<br/><br/>
                            Class1 : BaseClass1<br/>
                            +method1<br/>
                            +method2<br/>
                            -method3<br/>
                            <br/>
                            Class2 : BaseClass2<br/>
                            +method1<br/>
                            +method2<br/>
                            -method3<br/>
                        </p>
                    </div>
                </div>
            </div>

        </div><!--/span-->
      </div><!--/row-->

      <hr>

      <footer>
        <p>(cc) Model UML editor 2012</p>
      </footer>

    </div><!--/.fluid-container-->

   

JavaScript models (model-models.js)

The 1st interesting place is the JavaScript in which the Backbone.js models are defined. There's a simple base class (DiagramElement), a class that extend it (Class) for representing classes, & a collection of classes (ClassDiagram).

 

 

var DiagramElement = Backbone.Model.extend({
    defaults: {
        "x": 0,
        "y": 0,
        "width": 40,
        "height": 100
    }
});

var Class = DiagramElement.extend({
    defaults: {
        "name": "",
        "super_classes": [],
        "public_methods": [],
        "private_methods": []
    },
    get_all_members: function () {
        var result = [];
        var name = this.get('name');
        if (this.get('super_classes').length > 0) {
            name += " : ";
            name += this.get('super_classes').join(",")
        }
        result.push(name);
        _.each(this.get('public_methods'), function(n) { result.push(n); });
        _.each(this.get('private_methods'), function(n) { result.push(n); });
        return result;
    }
});


var ClassDiagram = Backbone.Collection.extend({
    model: Class,
    source: "",
    get_classes: function() {
        var result = [];
        _.each(this.models, function(model) {
            result.push(model.get("name"));
        });
        return result;
    }
});

var current_diagram = new ClassDiagram();

   

JavaScript controllers (model-controllers.js)

The contoller file contains the parser of the text in which you describe classes. The parser goes over the text & creates Backbone models out of it. Then the controller creates the Drawer, that renders the class diagram, & binds it to the add event of the diagram (triggered whenever a new Class is added to the collection).

 

 

var SourceParser = {

    parse: function() {
        var previous_source = "",
            current_class = "",
            current_super_classes = [];

        var members_map = {
            "+": [],
            "-": []
        };

        var add_class = function() {
            var cls = new Class({
                "name": current_class,
                "super_classes": _.clone(current_super_classes),
                "public_methods": _.clone(members_map["+"]),
                "private_methods": _.clone(members_map["-"])
            });
            var existing_class = current_diagram.find(function(c) { return c.get('name') == cls.name});
            if (existing_class) {
                existing_class.set(cls);
            }
            else {
                current_diagram.add(cls);
            }
            current_class = "";
            current_super_classes = [];
            members_map["+"] = [];
            members_map["-"] = [];
        };

        return function() {
            var source = $("#source_input").val();
            if (source != previous_source) {
                previous_source = source;
                var lines = source.split("\n");
                _.each(lines, function(line) {
                    if (line.trim().length == 0) return;
                    var first_char = line.charAt(0);
                    if (members_map[first_char]) {
                        members_map[first_char].push(line.substr(1));
                    }
                    else {
                        if (current_class != "") {
                            add_class();
                        }
                        if (line.indexOf(":") >= 0) {
                            var parts = line.split(":");
                            current_class = parts[0].trim();
                            current_super_classes = parts[1].trim().split(",");
                        }
                        else {
                            current_class = line;
                        }
                    }
                });
                add_class();
            }
        }
    }


};


$(function () {
    drawer.init();

    var parse = SourceParser.parse();

    $('a[data-toggle="tab"]').on('shown', function (e) {
        parse();
    });

    current_diagram.on("add", drawer.draw_class, drawer);
});

   

JavaScript widgets (model-widgets.js)

The final source file defines the ClassDiagramDrawer, which uses the d3.js library to tramsform the Backbone models into SVG elements. d3.js is a very powerful library, which we'll explore more deeply in following posts. The main idea is to associate a data array with DOM elements, & create DOM elements for new data.

 

var ClassDiagramDrawer = {
    svg: null,
    current_offset: 0,
    default_class_width: 150,
    default_member_height: 20,
    default_member_spacing: 20,
    class_containers: {},

    init: function() {
        var w = 675,
            h = 360;

        var pack = d3.layout.pack()
            .size([w - 4, h - 4])
            .value(function (d) {
                return d.size;
            });

        this.svg = d3.select("#chart").append("svg")
            .attr("width", w)
            .attr("height", h)
            .attr("class", "pack")
            .append("g")
            .attr("transform", "translate(2, 2)");        
    },
    
    draw_class: function(model) {
        var data = model.get_all_members(),
            x = this.current_offset,
            y = 50,
            margin = 10;

        var g = this.svg.append('g')
            .attr("x", x)
            .attr("y", y);
        this.class_containers[model.get('name')] = g;

        g.append('rect')
            .attr("x", x)
            .attr("y", y)
            .attr("width", this.default_class_width)
            .attr("height", data.length * this.default_member_height);

        var that = this;
        g.selectAll("text")
             .data(data)
           .enter().append("text")
             .attr("x", x + margin)
             .attr("y", function(d, i) { return y + margin + i * that.default_member_height; })
             .attr("dy", ".35em") // vertical-align: middle
             .text(String);

        g.append("line")
            .attr("x1", x)
            .attr("y1", y + this.default_member_height)
            .attr("x2", x + this.default_class_width)
            .attr("y2", y + this.default_member_height);

        this.current_offset += this.default_class_width + this.default_member_spacing;
    }

};

var drawer = ClassDiagramDrawer;

 

 

 

Screenshots

 

 

Finally, here's how the 1st version looks like:

 

 

 

 

 

Next..

In the next parts, I'll try to make a real application out of this prototype, & alsp adopt better frameworks & technologies, such as CoffeeScript.

 

 

 

Thank you for your interest!

We will contact you as soon as possible.

Want to Know More?

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com
Thank you for your interest!

We will contact you as soon as possible.

Let's talk

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com