Encodable: Configurable Grammar for Visualization Components

09/01/2020 ∙ by Krist Wongsuphasawat, et al. ∙ Airbnb, Inc. 0

There are so many libraries of visualization components nowadays with their APIs often different from one another. Could these components be more similar, both in terms of the APIs and common functionalities? For someone who is developing a new visualization component, how should the API look like? This work drew inspiration from visualization grammar, decoupled the grammar from its rendering engine and adapted it into a configurable grammar for individual components called Encodable. Encodable helps component authors define grammar for their components, and parse encoding specifications from users into utility functions for the implementation. This paper explains the grammar design and demonstrates how to build components with it.



There are no comments yet.


page 1

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Related Work

There are many ways to create a visualization. The programmatic approaches, mainly on the web, can be grouped as follows:

A) Graphics Manipulation: Processing [Reas2003, Reas2005] and others [p5js, Raphael] let a developer draw or interact with visual elements directly. They have the maximum level of expressiveness and in return require the most effort to produce the same visualizations.

B) Low-level Composition: D3 [Bostock2011] learns from the early approaches [Heer2005, Heer2010, Bostock2009] and introduces low-level building blocks, such as selection, scales, formatting, etc. It leverages the common standards such as SVG instead of defining all constructs by itself. vx [vx] bridges D3 and SVG for React [React] framework. Visualizations can be created from very flexible combinations of these building blocks.

C) Visualization Grammar: Heavily inspired by the Grammar of Graphics [Wilkinson2013], there is no concept of chart type. Developers learn how to express the visualizations they desire in the given grammar, i.e. a domain-specific language provided by each library that describes how to transform and encode data into visual marks and their properties [Wickham2006, Wickham2010, Satyanarayan2014, Satyanarayan2016, Satyanarayan2017, Vanderplas2018, Park2018, G2]. The most famous one is ggplot2 [Wickham2006, Wickham2010]

which dominates the R and data science communities.

Vega [Satyanarayan2014] let users describe visualizations in JSON, and generate interactive views using either HTML5 Canvas or SVG. Vega-Lite [Satyanarayan2017] provides a higher-level grammar equivalent to ggplot2 level with interactions.

D) High-level Composition: Similar to the convention of MS Excel [Excel], this group uses series

to abstract a group of graphic elements that encode data. For example, bars in a cartesian coordinate system form a series. More complex combinations such as candlestick, bullet or other chart types can also be abstracted as a series. The data and options are often mixed within the series definition.

ECharts [Li2018] and others [Highcharts, Plotly] employ the all-in-one JSON option to declare a visualization. Many libraries such as Victory [Victory] and others [React-Vis, data-ui, Semiotic] provide similar level of abstraction in React syntax, such as <XYPlot>, <CandleStickSeries>, or <XAxis>, that can be composed into the desired visualizations.

E) Chart Templates: Google Charts [GoogleCharts] and others [nivo, FusionCharts, JIT, Chartjs, Recharts] let developers choose a chart type from its catalog, prepare data in the specified format and plug them together. Some libraries provide multiple levels of abstraction. For instance, G2Plot [G2Plot] provides chart templates on top of G2 [G2] grammar.

Encodable was designed to complement these approaches. It does not render the output and therefore cannot create a visualization by itself alone. Instead, it bridges the gap between the component authors and users. A component author uses Encodable to define the component API, uses it again to parse the users’ specification into an Encoder, then choose from the approaches A-D, or even E under the hood for rendering (Section 3). The resulting component fits into the chart templates (E) level.

There are also some GUI approaches for creating visualizations with relevant concepts. Encodable is similar in spirit to Data-Driven Guides [Kim2017] and the followings [Liu2018, Ren2019] which let users pick visual properties from any arbitrary shape and encode them with data.

2 Goals & Requirements

This project aims to provide the following convenience:

Component authors, who create reusable components, should be able to create a component with encoding grammar that conforms to this Encodable grammar, with little effort required to make the component support the grammar.

Component users, who use the reusable components, should benefit from the consistent encoding grammar across components and standardized features even though the components are from different authors. To avoid mistakes when providing an encoding specification (spec) for a component, users should also receive syntax verification that the spec is grammatically correct.

The goals above are broken down into the following requirements:

  • R1: Provide a configurable grammar for encoding a component with data. The component author can customize grammar to be tailored for component as . is still a subset of and ensures consistency across different components even though they are implemented by different component authors, e.g.

  • R2: Handle specification parsing for the component author. Parse the specification into something that helps with the component implementation. This will also reduce the inconsistencies due to implementation of the parser.

  • R3: Provide mechanism to verify specification from the component users. Learning a new grammar can take time and mistakes are inevitable. Immediate feedback when coding is very valuable to reduce mistakes from providing invalid specifications.

  • R4: The library should be lightweight. For this utility to be a dependency of any reusable component, it should not be so large that no one wants to import.

3 Proposed Solution

A grammar and parser was written in TypeScript (TS), which is a strict syntactical superset of JavaScript (JS) that adds static typing and transcompiles to JS. By using TS, the grammar (R1) can be defined as type definitions and utilize static type checking to compare incoming specifications against the type definitions. This will ensure that the component users have specified the specifications that are grammatically correct (R3). The overall architecture of Encodable can be seen in Fig. Encodable: Configurable Grammar for Visualization Components. Encodable components assume the datasets are in tabular format such as:

[fontsize=]json [ ”kind”:”Cat”, ”count”:9, ”kind”:”Dog”, ”count”:11 ]

The code snippets in this paper are simplified for explanation purposes and may omit some details for brevity. Please see the supplementary materials for more details or repository (github.com/kristw/encodable) for the full and latest implementation.

3.1 The Grammar

The first principle of Encodable is each visualization has one or more channels to encode data, such as color, x, y, etc. For example, a simple word cloud component has size and text channels. If there is a grammar to describe what size and text can be, one can describe how to encode this word cloud component with the given data based on these two channels. Hence, in its simplest form, Encodable grammar is defined as key-value pairs of channel names and their definitions.

3.1.1 Channel Definition

This work was heavily inspired by Vega-Lite, which includes channel definitions as part of its grammar. Its grammar is also pure JSON and can be serialized into a simple text file. In Vega-Lite, this is how to encode a bar chart that shows number of each animal:

const vegaLiteBarSpec = ”mark”: ”bar”, ”encoding”: ”x”: ”field”: ”kind”, ”type”: ”ordinal”, ”y”: ”field”: ”count”, ”type”: ”quantitative” ;

In the example above, the first channel name is x and its channel definition is {"field": "kind", "type": "ordinal"}, telling the rendering engine to encode the kind field for -position and count field for -position, or bar height. Encodable adopts a subset of grammar from Vega-Lite for channel definition (ChannelDef).

interface ValueDef value: number — string — boolean — Date — null; interface FieldDef field: string; format?: string; title?: string; interface ScaleFieldDef extends FieldDef type: ’quantitative’—’ordinal’—’temporal’—’nominal’ scale?: ScaleDef; /* See Supp. Materials */ interface PositionFieldDef extends ScaleFieldDef axis?: AxisDef; /* See Supp. Materials */ type ChannelDef = ValueDef — FieldDef — ScaleFieldDef — PositionFieldDef;

According to the grammar defined above, a channel definition can be one of the followings:

(a) Fixed value (ValueDef) – such as making color of text in a word cloud always red.

(b) Dynamic value based on a field in the data (FieldDef) – such as using the field kind for each word in word cloud.

(c) Dynamic value with scale (ScaleFieldDef) – Many channels use scale to map input value into output such as mapping kind into color, count into fontSize. Inside the scale field, the component users can define how they want to customize the scale. The type field in channel definition will help the filler choose the appropriate scale or format when not specified. E.g., a quantitative field uses a linear scale with number formatter by default while a temporal field uses a time scale with time formatter by default. The two scale types handle ticks and domain rounding differently.

(d) Dynamic value with scale and axis (PositionFieldDef) – Channels such as x or y can optionally include definition for axes.

const color:ValueDef = value: ’red’ ; const text:FieldDef = field: ’kind’ ; const color:ScaleFieldDef = type: ’nominal’, field: ’kind’, scale: type: ’ordinal’, range: [’pink’, ’blue’] ; const fontSize:ScaleFieldDef = type: ’quantitative’, field: ’count’, scale: range: [0, 36] ; const y:PositionFieldDef = type: ’quantitative’, field: ’count’, scale: nice: true , axis: orient: ’left’ ;

3.1.2 Define Component-specific Channels

At the time of this writing, Vega-Lite has 35 channels (x, y, color, etc.) Even so, there are still edge cases that are beyond these fixed set of channels. For example, if the developer is trying to encode data into font-family, there is no such channel in Vega-Lite and therefore you cannot use it. So a fixed number of channels does not sound like a good idea. Earlier in Section 3.1.1, Encodable grammar is defined broadly as a key-value object (Encoding) with key being channel name and value being channel definition. This basically allows unlimited number of channels.

[fontsize=]TypeScript—interface Encoding [channelName: string]: ChannelDef —

However, this is too ambiguous and problematic. channelName can be any string. There is nothing to enforce component users to specify the correct channel names, which basically violates R3. Users may specify channel color when there is no such channel in the component. Also each channel may support only a subset of the ChannelDef type. E.g., a text channel does not care about axis or scale and should only be ValueDef or FieldDef.

Therefore, the second principle of Encodable is the component authors can define channel names and definitions specific to their components via a configuration below.

type ChannelType = ’X’—’Y’—’Numeric’—’Category’—’Color’—’Text’; type Output = number — string — boolean — null; interface EncodingConfig [name: string]: [ChannelType, Output, ’multiple’?];

Component authors must list their channel names with their types, expected output type, and whether it can take multiple (array of) definitions (such as a tooltip channel can accept multiple fields to be displayed). For example, to create a word cloud component that can be encoded by color and font size and accept multiple fields for tooltip, the component author will write this configuration (Fig. Encodable: Configurable Grammar for Visualization Components-A) and derive the encoding grammar from the config (Fig. Encodable: Configurable Grammar for Visualization Components-B).

import DeriveEncoding from ’encodable’; interface WordCloudConfig color: [’Color’, string]; fontSize: [’Numeric’, number]; text: [’Text’, string]; tooltip: [’Text’, string, ’multiple’] type WordCloudEncoding = DeriveEncoding¡WordCloudConfig¿;

In DeriveEncoding (Fig. Encodable: Configurable Grammar for Visualization Components-B), each ChannelType in the config is mapped to an appropriate subset of channel definition as follows:

Channel Type Channel Definition
X, Y PositionFieldDef|ValueDef
Numeric, Category, Color ScaleFieldDef|ValueDef
Text FieldDef|ValueDef

X and Y channel types represent x- and y- positions. Numeric channel type means a numeric attribute, e.g., size, opacity. Category channel type defines a categorical attribute, e.g., visibility, shape. Color channel type defines a color attribute, e.g., fill, stroke. Text channel type defines a plain text attribute, e.g. tooltip, label. The grammar WordCloudEncoding derived from the WordCloudConfig is equivalent to the manually-defined WordCloudEncoding below. However, the extra information in config that a channel is a Color type, not an ordinary Category will be useful during parsing, which the manual one cannot capture.

type WordCloudEncoding = color: ValueDef — ScaleFieldDef¡string¿; fontSize: ValueDef — ScaleFieldDef¡number¿; text: ValueDef — FieldDef¡string¿; tooltip: (ValueDef — FieldDef¡string¿)[]; /* array */

3.2 The Encoder

Encodable takes encoding config (Fig. Encodable: Configurable Grammar for Visualization Components-A) from the author and encoding specification from the user (Fig. Encodable: Configurable Grammar for Visualization Components-F), and parses it into an Encoder (Fig. Encodable: Configurable Grammar for Visualization Components-K) that encapsulates the logic how to encode each channel from data (R2). During parsing, each channel definition is parsed separately. Since many fields are optional, the filler (Fig. Encodable: Configurable Grammar for Visualization Components-I) will expand the incoming definition into a completed definition via smart defaults and inference. After that, each channel definition is parsed into a ChannelEncoder (Fig. Encodable: Configurable Grammar for Visualization Components-J), which is a utility class that provides several useful functions, such as: encodeDatum(datum) which converts input datum into output value for that channel and getValueFromDatum(datum) which returns the raw field value from input datum, or fixed value, for that channel. All ChannelEncoder instances are nested under an Encoder instance and referred to by encoder.channels[channelName]. The author then can use the Encoder and these ChannelEncoder to help with the rendering (Fig. Encodable: Configurable Grammar for Visualization Components-L) of the visualization.

The Encodable library, at the time of this writing, is 25.2kB (minified), which is relatively small (R4). In comparison, Vega-Lite is 237.1kB, G2 is 414.9kB and Echarts is 817kB.

4 Demonstration

The code below demonstrates how to implement the rendering logic of the word cloud component. It is the completed version of Fig. Encodable: Configurable Grammar for Visualization Components-L. Line 5 defines encoding grammar of this component as the WordCloudEncoding defined earlier in Section 3.1.2. Line 8 parse incoming encoding specification into an Encoder. Line 9 sets the domain from data. For example, if color is based on the field count, this call will set the domain of the color channel to [min(count), max(count)]. This single call applies the same operation to all channels. When rendering the HTML <span> (line 11-16), the three ChannelEncoder: size, color and text are used to computed the output for each channel from each datum. Whether the color is a fixed value, comes from what field, uses a quantized scale or other scales, the component author does not need to know because these logic are encapsulated within the ChannelEncoder. Although the example is based on React, Encodable is independent from React and can work with other frameworks, or even plain JS.

import createEncoder from ’encodable’; export function WordCloud( encoding, width, height, data : encoding: WordCloudEncoding; width: number; height: number; data: object[]; ) const encoder = createEncoder¡WordCloudConfig¿(encoding); encoder.setDomainFromDataset(data); return (¡div style= width, height ¿ data.map(d =¿ (¡span style= color: encoder.channels.color.encodeDatum(d), fontSize: encoder.channels.size.encodeDatum(d), ¿ text.getValueFromDatum(d) ¡/span¿)) ¡/div¿);

Encodable greatly reduces the overhead in adding or removing encoding channels. Adding a fontWeight channel to the word cloud above requires only two small changes: (1) Add the fontWeight channel to WordCloudConfig. (2) Add fontWeight property to the <span> on line 14 above. With this lighter overhead, the author can develop a prototype with the core visual elements first and decide on adding, changing or removing encoding channels later. This also helps with standardizing and converting an existing component or custom graphics into a reusable component by substituting hard-coded value or old code with an encoding channel.

Figure 1: A China map component with a mini-map. The SVG rendering and zoom/pan interactions are implemented with React. The visual encoding is handled by Encodable. The component author includes the following channels: location, fill, stroke, texture and tooltip.
Figure 2: A bespoke visualization component of coffee cups. The visual encoding is handled by Encodable with the following channels: drinkLevel, label, drinkColor and useToGoCup.

While the word cloud example is easier to explain, it does not represent the full potential of this work. Encodable is capable of enabling more complex components. In the China map example (Fig. 1), the author builds a traditional map component that has several encoding channels, then a user encodes fill channel with numStudents, resulting in a choropleth map with a sequential color scale. The coffee chart (Fig. 2) allows different parts of a coffee cup to be encoded by data. A user then encodes the cups to represent his productivity and coffee consumption over the week.

¡ChinaMap data=data encoding= location: field: ’province’ , fill: field: ’numStudents’, type: ’quantitative’ /¿ ¡CoffeeChart data=productivityData encoding= label: field: ’day’ , drinkLevel: field: ’numCoffee’, type: ’quantitative’ , drinkColor: field: ’productivity’, type: ’quantitative’ , useToGoCup: field: ’goToOffice’, type: ’ordinal’ /¿

For real applications, Encodable

was used to build several components (scatter plot, box plot, line chart, map, etc.) for the open-source project

Apache Superset. The components are now part of the official application release. The package encodable is also available on npm registry with 3,000 weekly downloads.

5 Conclusion and Future Work

This work envisions a world where visualization components from different authors can have consistent APIs and behavior. This does not limit to traditional charts, but also applies to bespoke visualizations. Inspired by Vega-Lite, a new configurable grammar independent from rendering called Encodable is introduced. It lets a component author declare a grammar for encoding channels of his/her component, which looks like a subset of Vega-Lite grammar. To ease the implementation burden, the grammar is accompanied with a parser that parses specifications from component users into utility functions to help with the rendering. To provide feedback for the component users, the specifications can also be verified against the grammar. The demonstration shows that it is easy to configure encoding channels and can support a broad range of components, making component authoring convenient and flexible.

Looking ahead, there are still many things that could be added to this configurable grammar, such as legend and axis support, more features in the channel definition, etc. Expanding these new ideas while keeping the library lightweight will be an interesting challenge.

Thanks to Kanit Wongsuphasawat, Dominik Moritz, Chris Williams & Airbnb Data Experience team for their feedback and support.