Data visualizations: Why you should be using a two-layer architecture

Building framework-independent Data Visualizations using D3 and TypeScript

A Bit of Context

A few months ago, my team decided to build a new web application to provide all our users with a single point of entry to our data visualizations and reports. Prior to this moment, every visualization we created was implemented as a standalone project, but this caused a few important problems:

  • Multiple URLs: each visualization was deployed under a different URL, meaning our users couldn’t access our complete visualizations catalogue on a single page.
  • Data access issues: as standalone applications, every visualization was responsible for retrieving the data to represent; this meant they also had to manage critical aspects like user authentication and authorization, a task that’s better delegated to a unique application.
  • Actionability limitations: visualizing data is the first step in making business decisions and taking action. Our isolated visualizations weren’t integrated with the applications responsible for executing those actions, reducing their usefulness.

While looking for a solution to these problems, I read many articles that discussed the integration of D3 with front-end frameworks such as Angular, React and Vue. I would highly recommend Nicolas Hery’s article on Integrating D3.js visualizations in a React app. After reading, I reached the following conclusion:

Use D3 to do what it does best. Let it show the data. Your web application can be left to manage the rest.

Two-layer Architecture

Below is a diagram that illustrates an example of the architecture you might use to execute the last conclusion. The visualization is split into two layers: the web application’s wrapper component and the visualization itself.

Two-layer architecture

The wrapper component looks after the where and the what. It’s responsible for creating the container in which the visualization will be displayed and it will gather the data to be visualized. The visualization, following a specific configuration, is responsible for representing the data sent by the wrapper: in other words, the how.

So, what’s the benefit of keeping our visualizations separate from our web application? By using this layered architecture we are able to represent the data using only TypeScript (or JavaScript) and D3. Anything complex or not dependent on data is managed by the wrapper. That also means our visualizations are now compatible with any web application, no matter the framework they use: we just import them as dependencies using npm.

The Interface

With the necessary architecture and the logic implemented by each item within it defined, I should now explain how they are going to communicate with each other. Time for some code!

export abstract class AbstractVisualization<ConfigType, DataType> {
  protected config: ConfigType;
  constructor(protected containerElement: Element, config?: Partial<ConfigType>, protected data?: DataType) {
this.config = Object.assign(this.getDefaultConfig(), config);
}
  public setData(data: DataType): void {
this.data = data;
}
  public setConfig(config: Partial<ConfigType>): void {
Object.assign(this.config, config);
}
  public resize(): void {}
  public destroy(): void {}
  protected abstract getDefaultConfig(): ConfigType;
}

The code above corresponds to the abstract class that each visualization has to extend, including all the public methods accessible by its wrapper component. Let’s dig into the details:

ConfigType

This is the model of the configuration properties object that the visualization will handle. The wrapper component can use this object to specify to the visualization how the data will be represented. We can use this object, for example, to manage internationalization: specifying a locale property, the visualization could respond by displaying its labels in the appropriate language.

DataType

The model used to send the data to the visualization. This model’s format is defined by the visualization. No matter the format returned by the data sources it’s using, the wrapper component must respect whatever the visualization defines.

constructor()

When creating an instance of the visualization, the wrapper component must specify the container element where the visualization is going to be displayed. It may optionally specify an initial configuration and data objects also.

Each visualization must extend this constructor to implement the way its items are created.

setData()

Every time the user requests new data to display, the wrapper component will call this method. Here is where all the D3 magic is implemented to visualize the new data.

setConfig()

Following the same idea as the setData() method, this covers when the wrapper component needs to update the visualization config. For example, after changing the language of the whole application.

resize()

This method must be called on every time the size of the visualization container has changed. As the wrapper component is responsible for this container, it has to watch that size and call on this method when needed.

On the visualization side, its implementation will contain all the DOM changes necessary to adapt the data to the new size.

destroy()

Called before the wrapper component is destroyed in the web application, (e.g. by navigating to another page). This method is used to ensure all elements created by the visualization are removed, such as global listeners, timing functions, etc..


In Conclusion

My team and I have been using and improving this approach for a few months. It’s done pretty well at meeting our requirements. It allows us to decouple our visualizations from our web application, focusing it solely on data representation. We’re even starting to use it to create our own library of D3 components, but we’ll talk about that another time…