acb's technical journal

D2UI, or data-driven interfaces in iOS

The D3 JavaScript library allows one to create and manipulate arbitrary elements on a web page from data in an elegantly declarative fashion, making it easier to implement quite sophisticated dynamic data visualisations. Recently, I was thinking about whether the ideas behind D3 could translate to iOS and Objective C, and ended up writing a small library and example application to explore the idea.

D3 and the general update pattern

Firstly, a quick overview of how D3 converts data to elements. The general problem that has to be solved is that (a) on one side, you have some data, typically a list of records (measurements, objects, or other data points). On the other side, you have, or wish to have, a display consisting of a set of visible objects (bars, pie slices, blobs), with each object representing a data point and displaying its characteristics in some way (by position, size, shape, colour or what have you). In D3's case, the data objects are in a JavaScript array of values (often objects with keys, i.e., dictionaries) and the display elements are HTML DOM elements of some sort (often <DIV>s or SVG elements) within one part of the document's tree.

D3's approach to converting data records to DOM elements is slightly unusual at first, in that one never manually loops over the data; instead, one performs a join between the elements already there (of which there may well be none yet) and the set of data one wishes to display, also specifying how to create new elements (and, optionally, what to do to elements being removed, if simply making them disappear instantly is insufficient). For example, a query joining a list of strings with a HTML list could look like:

var data = [ "this", "is", "an", "example"];
var sel = d3.select("ul#my_list").selectAll("li").data(data);
// add new items
sel.enter().append("li");
sel.text(function(d){ return d; }
There are advantages to this approach, in that as well as adding and removing elements, one can change the values of existing elements, animating them from their old to their new values. Since this is done declaratively, one only has to tell D3 how to determine the identity of a data point (i.e., how to determine which data points refer to the same entity) and that one wants smoothly animated transitions, and it does the rest.

For more information, see General Update Pattern 1 in the D3 documentation.

Adapting data-driven visual elements to Objective C

It would be useful, I was thinking, to have something that works as D3 does, with the same level of abstraction and declarativeness, but operates natively in the world of Cocoa Touch, creating and manipulating UIViews and/or CALayers within a parent view from a list of dynamically changing data. The code would, much like D3, keep track of the elements in the view, associate them with data records, and determine when to create or remove elements; the user's code would only need to arrange each element to reflect the data and optionally handle the appearance and disappearance of elements and identify elements which are continuous.

D3 makes use of JavaScript's functional features; rather than instantiating persistent objects and calling their methods, one calls functions creating selections and then calls methods of those selections; joining them with data, getting subselections of entering or exiting elements, and, ultimately, manipulating the elements the selections refer to. One could conceivably map this approach to Objective C if one were to use blocks, the implementation of closures which have been in the runtime since iOS 4.0. Such an approach might look something like:

#import "UIView+DataDriven.h"

. . .

// Update the contents of view with the contents of an array
- (void) updateWithData:(NSArray*)data {
    Selection *sel = [[view selectAllOfClass:[MyElementView class]] join:data];
    [sel.enter appendItems:^(id dataRecord){ return [[MyElementView alloc] init]; } ];
    [sel updateElements:^(UIView* element, id dataRecord, NSUInteger index){
        element.frame = CGRectMake(0.0, index*20.0, [dataRecord floatValue]*view.frame.size.width, 16.0);
    }];
}

Of course, there is the question of how this would be implemented behind the scenes. For one, we would need to be able to somehow associate views with data elements (or at least data identifiers) when we iterate through them. In the browser, D3 does this by adding its data to a custom element attribute (__data__). In Objective C, it is possible to associate custom values with objects using the objc_setAssociatedObject runtime method, and as such it would be possible to duplicate D3-style element-data associations that way.

However, while this is possible, it isn't the most natural way to go about things in iOS applications. While the web browser DOM is comprised of lots of small, relatively inert objects representing elements, on which loosely-coupled code can operate, a Cocoa application consists of larger objects, often organised in hierarchies and design patterns. Code is typically in the methods of these objects (loosely-coupled functions, whilst possible with blocks, are the exception rather than the rule). So I started thinking of more natural ways to implement data-driven interface element arrangement in iOS, coming up with the Visualisation Context.

The visualisation context

The Visualisation Context is an object which embodies the data updating algorithm, and its current state relating to a specific data display. Basically, the Visualisation Context has:

  • A view it is associated with; this is a UIView, to which it adds subviews (or to whose CoreAnimation layer it adds sublayers).
  • An updateWithData: method, accepting a NSArray of arbitrary data items. (These can be anything from NSNumbers to NSDictionarys to arbitrary classes.)
  • A delegate, which implements methods called by the context, which define what exactly it does with the data items.

The delegate's methods are:

  • identifierForDatum:, which is passed an individual record of data and returns an identifier for the object referred to by this datum. This method is optional; if it is not implemented, the data record's index in the list is used.
  • updateElement: forDatum: atIndex:; this function is called once in each update cycle for each element for which there is data; it is passed the element, the data record and the element's index, and is responsible for adjusting the element's position and/or appearance to represent the data.
  • createElementForDatum:; this creates an element (i.e., a subview or a layer) representing a piece of data, adds it to the view and returns it.
  • removeElement:; this removes an element which no longer represents any visible data. This is optional; if not defined, the visualisation context will remove the element instantaneously. The purpose of this method is to allow disappearing data to be animated out.
All methods have an additional parameter giving the context.

In addition, there are optional methods which, if defined, will be called at various stages of the update loop: one when data is received (if the delegate needs to calculate any values dependent on the entire data set, such as minimum/maximum values or element count), and others called at the start and end of the update loop (which may be used for setting up animations).

An example

For example, implementing a simple bar graph from a list of numbers could be done like so:

/* Create a new bar; this is just a blue UIView. */
- (id)createElementForDatum:(id)datum inContext:(D2UIVisualisationContext*)ctx {
    UIView* v = [[UIView alloc] init];
    v.backgroundColor = [UIColor blueColor];
    [ctx addElement:v];
    return v;
}

- (void)prepareForNewData:(NSArray*)data inContext:(D2UIVisualisationContext*)ctx {
    struct D2UIIntegerExtent ext = [data integerExtent];
    _maximum = ext.max;
    _barHeight = floorf(_vizView.frame.size.height / data.count);
}

- (void)updateElement:(id)view forDatum:(id)datum atIndex:(NSInteger)i inContext:(D2UIVisualisationContext*)ctx {
    NSInteger val = [datum integerValue];
    ((UIView*)view).frame = CGRectMake(0.0, i*_barHeight, _vizView.frame.size.width*val/_maximum, _barHeight-1.0);
}
A few things to mention: _maximum and _barHeight are instance variables (of types integer and CGFloat, respectively). The context's addElement: method is a convenience method which determines if an element is a view or a layer and adds it appropriately (there is also a removeElement: method that works similarly.) And NSArray receives some extra convenience methods along the line of D3's Array extensions, among them integerExtent (which returns a struct with minimum and maximum values).

D2UI: putting it all together

I have created a small library implementing this model of data-driven view updating; it is named D2UI (short for Data-Driven User Interface), and may be found here, along with an example application which runs on either the iPhone or iPad. The app implements four example views: the basic bar graph above, a view with three dynamic scrolling bar graphs, a view with circles of various sizes and colours appearing and disappearing at arbitrary X/Y coordinates (from a list of data items), and an animated pie chart (implemented with a custom pie-slice CALayer subclass).

The library code is in the D2UI directory, and is very small; almost everything is in the D2UIVisualisationContext class. Additionally, there is the aforementioned NSArray category. All other code is in the 2UIExample_iOS application.

Further work

D2UI is currently little more than a proof of concept and a base for further work. The examples, for example, are at their most stripped down, lacking niceties such as labels, axes and scales.

Going back to D3, there are many other elements there which are absent here. Some would be fairly straightforward to borrow (i.e., functors for scales); others would require more work (whether, for example, it'd make sense to reimplement chord diagrams or force-directed graphs using CAShapeLayers, or whether that would need a traditional view with a drawing method, is an open question).

This code is currently iOS-specific (for the most part), though should be fairly straightforward to port to OSX and Cocoa.

There are no comments yet on "D2UI, or data-driven interfaces in iOS"

Want to say something? Do so here.

Post pseudonymously

Display name:
URL:(optional)
To prove that you are not a bot,
please enter the text in the image on the right
in the field below it.

Your Comment:

Please keep comments on topic and to the point. Inappropriate comments may be deleted.

Note that markup is stripped from comments; URLs will be automatically converted into links.