27th May 2014
Building Dashboards with ReactJS

So I wanted to riff a bit more about Javascript before some World Cup and tennis modelling in the next few weeks.

If you're working with data you often end up a wanting dashboard- style view - a single page of independent but related widgets (tables, charts, etc) which facilitates an at-a-glace summary of the dataset you're working with.

Now this is actually something Excel does quite well; but of course no one in their right mind would use Excel as a data analysis tool - no web interaction, no parsing libraries, no version control, Reinhardt Rogoff error etc.

But how can you replicate that Excel dashboard experience using the web ?

The difficulty here is that the web was designed as a document rendering system, whereas a dashboard requires something more akin to a component management system. And this bias is built into most common webservers, which require you to set up templates for each view and then inject variables into those templates. Which is fine if you only have one table per page, but things quickly becomes unmanageable if you're trying to manage multiple components - you find yourself replicating template markup all over the place and hacking together custom JS code to manage each component's state.

1995 Web 2015 Web

Worse still, an individual component can't update itself in isolation - the only option is to re- render the entire page, with all the performance implications that entails; you can forget about real time updates for a start.

Now Python doesn't have the answer here as it's limited to the server side; every Python web server I've ever seen expects you to use a template library. If we cast our net a little wider however, there exists a wonderful Smalltalk web framework called Seaside, which allows you to build your views as a collection of widgets; a much closer mental model to the dashboard experience we're looking for.

Now Seaside is a long way from perfect. I don't think it's been updated for a while (I last looked at it in 2008), and it's based on Disney's (!!) Squeak; but it contains the kernel of a very good idea. If you look through the documentation you'll see that Seaside components are designed around a renderContentOn() function; each component is an object which contains a method to render itself to HTML. Each component can also contain references to child components; so the problem of building a web UI ultimately defaults to the task building a tree of widgets.

Now mercifully much has changed in the web space in 2008. JQuery has smoothed over most (all?) of the differences in DOM implementations, and Javascript engine performance has increased in leaps and bounds. The result is the many new web frameworks shift the burden of UI rendering away from the server and back towards the client, to the JS engine; and so when I saw that ReactJS was built around the render() function, it was immediately clear it was worth a look.

Here's a simple ReactJS Hello World example. First, an HTML attachment point:

<div id="helloworld" style="margin-bottom:20px"></div> <!-- NB no single tags -->

And then a Label class to print a message to screen:

var Label=React.createClass({
    render: function() {
        return React.DOM.h2({children: this.props.message});
    }
});

React.renderComponent(Label({message: "Hello World from React!"}), $("#helloworld")[0]);

Basic stuff, but illuminating if you care to look under the hood.

We've got a 'Label' class which can contain a series of methods. The only method currently defined is 'render()', which renders our Label to screen as an 'h2' element. The Label class accepts constructor values ('message') which are then accessible from within that class using the 'props' object (this.props.message).

See the similarity with object- orientation ? You can think of each component as an object, with methods determining what it does on startup, how it changes on user interaction, how it is rendered to screen etc. And since a single component definition can spawn an infinite number of instances, we're already well on the way to ditching templates; it's simply a matter of passing different configurations to each class instance.

OK but our Label example is pretty dull; let's do something with a bit more user interest.

What about a table component which ranks the teams in the top European leagues against one another ? Mildly interesting as it's rare to see all the teams in a single place; mostly they are shown on a per- league basis.

Let's grab all the data from the BBC:

import lxml.html, re, urllib, yaml

BBCLeagueTableUrl="http://www.bbc.co.uk/sport/football/tables/partial/%i"

BBCLeagueCodes=yaml.load("""
ENG.1: 118996114
FRA.1: 119000981
GER.1: 119000986
ITA.1: 119001017
SPA.1: 119001074
""")

def get_table(leaguename, fields=["team-name", "points","played",
                                  "won", "drawn", "lost",
                                  "for", "against", "goal-difference"]):
    url=BBCLeagueTableUrl % BBCLeagueCodes[leaguename]
    http=urllib.urlopen(url)
    doc=lxml.html.fromstring(http.read())
    div=doc.xpath("//div[@class='league-table full-table-wide']")[0]
    def get_text(cell):
        children=cell.getchildren()
        if children in [[], None]:
            return cell.text
        return children[0].text
    def parse_cellvalue(value):
        if re.search("^\\d+$", value)!=None:
            return int(value)
        return value
    table=[dict([(cell.attrib["class"], 
                  parse_cellvalue(get_text(cell)))
                  for cell in row.xpath("td")
                  if cell.attrib["class"] in fields])
           for row in div.xpath("table/tbody/tr")]
    for item in table:
        item["league"]=leaguename
        item["ppg"]=item["points"]/float(item["played"])
    return table
def get_teams(leaguenames):
    teams=[]
    for leaguename in leaguenames:
        print leaguename
        teams+=get_table(leaguename)
    return sorted(teams, key=lambda x: -x["ppg"])

Teams=get_teams(sorted(BBCLeagueCodes.keys()))

print
print "%i teams" % len(Teams)
ENG.1
FRA.1
GER.1
ITA.1
SPA.1

98 teams

Now you may remember from last time that our Javascript code can't load data directly from an IPython cell; instead we have to post it to a place where both runtimes can see it; and Amazon's S3 cloud datastore does the job nicely.

We'll add column configurations to our row data, and then save everything to S3:

Cols=yaml.load("""
- label: Team
  key: "team-name"
  type: str
- label: Played
  key: played
  type: int
- label: Won
  key: won
  type: int
- label: Drawn
  key: drawn
  type: int
- label: Lost
  key: lost
  type: int
- label: For
  key: for
  type: int
- label: Against
  key: against
  type: int
- label: Points
  key: points
  type: int
- label: GD
  key: "goal-difference"
  type: int
- label: PPG
  key: ppg
  type: float
""")

Struct={"rows": Teams,
        "cols": Cols}

upload_bucket_json(path="data/hello_world_react/teams.json", struct=Struct)
<Key: www.sportshacker.net,data/hello_world_react/teams.json>

And now we're ready to go. First an attachment point:

<style type="text/css">

table {
  margin-bottom: 20px;
  width: 100%;
}

tr.even {
  background-color: #333333;
}

th, td {
  text-align: center;
  padding: 5px;
}

</style>

<div id="table"></div>

And then our component code, this time to render our sample data as an HTML table:

var Table=React.createClass({
    getInitialState: function() {return {};},
    componentDidMount: function() {
      $.ajax({
          url: this.props.url,
          dataType: "json",
          success: function(struct) {
              this.setState({rows: struct.rows.slice(0, 8),
                             cols: struct.cols});
          }.bind(this)
      });
    },
    formatters: {
        int: function(value){return value;},
        float: function(value){return value.toFixed(2);},
        str: function(value){return value;}
    },
    render: function() {
        return React.DOM.table({
            children: this.state.rows ? [
                React.DOM.thead({
                    children: React.DOM.tr({
                        children: this.state.cols.map(function(col) {
                            return React.DOM.th({children: col.label});
                        }.bind(this))
                    })
                }),
                React.DOM.tbody({
                    children: this.state.rows.map(function(row, i) {           
                        return React.DOM.tr({
                            className: (0==i%2) ? "even" : "odd",
                            children: this.state.cols.map(function(item) {
                                var formatter=this.formatters[item.type];
                                return React.DOM.td({
                                    children: formatter(row[item.key])
                                });
                            }.bind(this))
                        });
                    }.bind(this))
                })
            ] : []
        });
    }
});

var Url="http://www.sportshacker.net/data/hello_world_react/teams.json";

React.renderComponent(Table({url: Url}), $("#table")[0]);

OK so this new Table class is much more complex than the simple Label class we defined earlier, but there's really only a couple of additional features:

  • the Table element is now passed a URL rather than a static set of data. The 'componentDidMount' function (part of the React class lifecycle) is called when the object is first mounted, loads data from the URL using the loadContent() function, which in turn stuffs the received data (rows, columns) into the 'state' object (this.state)

  • the render() function contains much more code than earlier, because we're rendering a more complex object (a table rather than a label). However the code is perfectly simple; iterating over row and column values (this.state.rows, this.state.cols) to produce a tree of the tags typically involved in rendering a table (table, tbody, thead, tr, td etc).

The result is a small table showing the top- rated teams across all the major European leagues; a small table because I took the liberty of truncating the data after the first eight rows, to avoid having the table take up the entire page; we'll deal with that shortly.

But think about this structure for a moment. To render the table, all we've had to do is point a Table component to a data source URL, and bam! the data is magically rendered on the screen, without any messing with templates or MVC patterns or anything; exactly the same way you can point the browser to an image URL, and have that image magically rendered as part of your web page.

Effectively, React does for data what the standard HTML protocol already does for images; it's rather like a graphics renderer for data and other non- image classes.

Want an interactive component ? No problem. Let's deal with our data truncation issue. As noted above, I truncated the data at the first eight rows; but we might want to look at some lesser rated teams lower down the table, without rendering the entire table in one go.

Solution ? Add a paginator, to allow you to select a given subset of rows.

Again, an attachment point first:

<style type="text/css">

ul.paginator {
  height: 60px;
}

ul.paginator li {
  float: left;
  list-style: none;
  width: 30px;
  height: 30px;
  text-align: center;
  padding-top: 10px;
  border: 1px solid #AAAAAA;
  margin-right: 5px;
}

ul.paginator li.selected {
  background-color: #32CD32;
  color: #000000;
}

</style>

<div id="dynatable"></div>

[try clicking on a paginator cell]

And now the code to generate our new paginated table:

var DynamicTable=React.createClass({
    Table: React.createClass({
        formatters: {
            int: function(value){return value;},
            float: function(value){return value.toFixed(2);},
            str: function(value){return value;}
        },
        render: function() {
            return React.DOM.table({
                children: [
                    React.DOM.thead({
                        children: React.DOM.tr({
                            children: this.props.cols.map(function(col) {
                                return React.DOM.th({children: col.label});
                            }.bind(this))
                        })
                    }),
                    React.DOM.tbody({
                        children: this.props.rows.map(function(row, i) {           
                            return React.DOM.tr({
                                className: (0==i%2) ? "even" : "odd",
                                children: this.props.cols.map(function(item) {
                                    var formatter=this.formatters[item.type];
                                    return React.DOM.td({
                                        children: formatter(row[item.key])
                                    });
                                }.bind(this))
                            });
                        }.bind(this))
                    })
                ]
            });     
        }
    }),
    Paginator: React.createClass({
        // http://stackoverflow.com/questions/3895478/does-javascript-have-a-range-equivalent
        range: function(n) {
            return Array.apply(null, Array(n)).map(function (_, i) {return i;});
        },
        render: function() {
            return React.DOM.ul({
                className: "paginator",
                children: this.range(this.props.nPages).map(function(i) {
                    var item={
                        label: i+1,
                        key: i
                    };
                    return React.DOM.li({
                        className: (this.props.selected.key==i) ? "selected" : "",
                        onClick: this.props.clickHandler.bind(null, item),
                        children: item.label
                    });
                }.bind(this))
            })
        }
    }),
    getInitialState: function() {
        return {
            selectedPage: {label: 1, key: 0}
        };
    },
    componentDidMount: function() {
      $.ajax({
          url: this.props.url,
          dataType: "json",
          success: function(struct) {
              struct.nPages=Math.ceil(struct.rows.length/this.props.nRows);
              this.setState(struct);
          }.bind(this)
      });
    },   
    handlePaginatorClicked: function(item) {
        this.setState({selectedPage: item});
        console.log("Page "+item.label);
    },
    filterSelectedRows: function(rows, selectedPage) {
        var n=this.props.nRows;
        var i=selectedPage.key;
        return rows.slice(i*n, (i+1)*n);
    },
    render: function() {
        return React.DOM.div({
            children: [
                (this.state.cols && this.state.rows && this.state.selectedPage) ? this.Table({
                    cols: this.state.cols,
                    rows: this.filterSelectedRows(this.state.rows, 
                                                  this.state.selectedPage)
                }) : undefined,
                this.state.nPages ? this.Paginator({
                    nPages: this.state.nPages,
                    clickHandler: this.handlePaginatorClicked,
                    selected: this.state.selectedPage
                }) : undefined
            ]                         
        });
    }
});

var Url="http://www.sportshacker.net/data/hello_world_react/teams.json";

React.renderComponent(DynamicTable({url: Url, nRows: 8}), $("#dynatable")[0]);

A lot of code here, but only a couple of key changes. The most obvious is the addition of a new Paginator class, and a new DynamicTable container to wrap both the original Table class and the new Paginator.

The key reason for the container is that both Table and Paginator need access to the current 'state' (the currently selected page). The Table needs to render the particular subset of rows for that page, whilst the Paginator needs to highlight the currently selected page. The Table and Paginator classes are defined as subclasses of the main DynamicTable parent class; the parent passes the Paginator a 'click handler' function so that the current page can be updated when the paginator is clicked; the Table class then re- renders the subset of newly- selected rows.

And you don't have to stop here. It wouldn't take much imagination to add a column sorter to the table headers, or add a filter to view one league at a time. And all the code is still encapsulated within the React component.

See how you have a dashboard solution here ? You can build up a library of components - tables, charts, filters, tabs, pages, menu etc - all part of the same container, but all independent because they reference different URLs. Of course you need a network of server- side controllers to supply all this data; but updating a single component is then simply a matter of re- calling its URL, which you can do quite simply on the client side using an AJAX timer.

Of course React isn't the only possible solution here. You can tell this whole browser UI rendering issue is a big hairy problem for many people because of the sheer number of Javascript frameworks jockeying for position in the space. Why favour React over Angular or Backbone ?

Well the key point for me is how well React maps to this natural metaphor of building UIs as collections of components.

But there's also the question of simplicity. Just look at this comparison of React vs Angular. In React, everything is just a component. No models, controllers, views, directives; just components.

There's a Lego parable here. When I was a kid all the Lego bricks were simple and uniform, but you could build great things with them. I remember - circa 1975 - knocking up a copy of a dictaphone my dad had brought home. Fast forward 40 years, my son now plays with 'Lego Chima', fantasy Lego a vast number of finely- tooled, individual parts. So finely tooled that the only thing you can build with them is the single model in the instructions.

Simple = good, complex = bad. Favour Lisp, Python and React over Java, C++ and Angular.

Which pattern do you think does the better job of fostering creativity ?

comments powered by Disqus