Introduction to Deneb.jl

This tutorial (as much of the documentation and example gallery) is adapted from Vega-Lite's and Altair's documentation.

The tutorial will guide through the process of writing a visualization specification in Deneb.jl. We will walk you through all main components of Deneb by adding each of them to an example specification one-by-one. This tutorial assumes that you are working an environment that can render rich MIME types (like Jupyter, Pluto, VSCode, ...) so the plots are automatically displayed.

The Data

Deneb.jl accepts any tabular data that supports the Tables.jl interface (e.g. DataFrames.jl). For the purpose of this tutorial we will use data with a categorical variable in in the first column a and a numerical variable in a second column b. We'll represent this data as a NamedTuple (but we could choose any other representation that supports Tables.jl interface):

data = (
    a = collect("CCCDDDEEE"),
    b = [2, 7, 4, 1, 2, 6, 8, 4, 7],
)
(a = ['C', 'C', 'C', 'D', 'D', 'D', 'E', 'E', 'E'], b = [2, 7, 4, 1, 2, 6, 8, 4, 7])

Deneb.jl can now consume this data with Data:

using Deneb
Data(data)
Deneb.DataSpec: 
{
  "values": [
    {
      "a": "C",
      "b": 2
    },
    {
      "a": "C",
      "b": 7
    },
    {
      "a": "C",
      "b": 4
    },
    {
      "a": "D",
      "b": 1
    },
    {
      "a": "D",
      "b": 2
    },
    {
      "a": "D",
      "b": 6
    },
    {
      "a": "E",
      "b": 8
    },
    {
      "a": "E",
      "b": 4
    },
    {
      "a": "E",
      "b": 7
    }
  ]
}

This created a Deneb.DataSpec that contains the data property of a Vega-Lite specification that defines the data source of a visualization. Deneb.jl also supports other types of data sources supported by Vega-Lite, besides tabular data, as described in the Data section.

Encoding Data with Marks

Basic graphical elements in Vega-Lite are marks. Marks provide basic shapes whose properties (such as position, size, and color) can be used to visually encode data, either from a data field (or a variable), or a constant value.

In Deneb.jl the mark of a Vega-Lite specification can be created with Mark. For instance we can set the mark property as a point:

Mark(:point)
Deneb.MarkSpec: 
{
  "type": "point"
}

To show a Vega-Lite visualization of the data as a point, we can compose the DataSpec and the Deneb.MarkSpec with the * operator to build a showable Deneb.VegaLiteSpec:

Data(data) * Mark(:point)

Now, it looks like we get a point. In fact, Vega-Lite renders one point for each object in the array, but they are all overlapping since we have not specified each point’s position.

To visually separate the points, data variables can be mapped to visual properties of a mark. For example, we can encode the variable a of the data with x channel, which represents the x-position of the points. We can do that by adding an encoding object with its key x mapped to a channel definition that describes variable a. In Deneb.jl this can be achieved with Encoding:

Encoding(x=(field=:a, type=:nominal))
Deneb.EncodingSpec: 
{
  "x": {
    "field": "a",
    "type": "nominal"
  }
}

Or alternatively using this convenient shortcut (borrowed from Altair) for a field and type definition:

Encoding(x="a:N")
Deneb.EncodingSpec: 
{
  "x": {
    "field": "a",
    "type": "nominal"
  }
}

We can now compose the data, mark and encoding specs to create the following visualization:

Data(data) * Mark(:point) * Encoding(x="a:N")

The Encoding object is a key-value mapping between encoding channels (such as x, y) and definitions of the mapped data fields. The channel definition describes the field’s name (field) and its data type (type). In this example, we map the values for field a to the encoding channel x (the x-location of the points) and set a’s data type to nominal, since it represents categories.

In the visualization above, Vega-Lite automatically adds an axis with labels for the different categories as well as an axis title. However, 3 points in each category are still overlapping. So far, we have only defined a visual encoding for the field a. We can also map the field b to the y channel.

Encoding(y="b:Q")
Deneb.EncodingSpec: 
{
  "y": {
    "field": "b",
    "type": "quantitative"
  }
}

This time we set the field type to be quantitative (with the shorthand ":Q") because the values in field b are numeric.

Data(data) * Mark(:point) * Encoding(
    x="a:N",
    y="b:Q"
)

Now we can see the raw data points. Note that Vega-Lite automatically adds grid lines to the y-axis to facilitate comparison of the b values.

Data Transformation: Aggregation

Vega-Lite also supports data transformation such as aggregation. For example, we can set the aggregate property to average in the y channel encoding:

Encoding(
    y=(field=:b, aggregate=:average, type=:nominal)
)
Deneb.EncodingSpec: 
{
  "y": {
    "field": "b",
    "aggregate": "average",
    "type": "nominal"
  }
}

Or using the following convenient Deneb.jl shorthand syntax for aggregation:

Encoding(y="average(b):N")
Deneb.EncodingSpec: 
{
  "y": {
    "aggregate": "average",
    "field": "b",
    "type": "nominal"
  }
}

We can then visualize the average of of all b values in each a category:

Data(data) * Mark(:point) * Encoding(
    x="a:N",
    y="average(b):Q",
)

Great! You computed the aggregate values for each category and visualized the resulting value as a point. Typically aggregated values for categories are visualized using bar charts. To create a bar chart, we have to change the mark type from point to bar.

Data(data) * Mark(:bar) * Encoding(
    x="a:N",
    y="average(b):Q",
)

Since the quantitative value is on y, you automatically get a vertical bar chart. If we swap the x and y channel, we get a horizontal bar chart instead.

Data(data) * Mark(:bar) * Encoding(
    y="a:N",
    x="average(b):Q",
)

Customize your Visualization

Vega-Lite automatically provides default properties for the visualization. You can further customize these values by adding more properties. Deneb.jl provides an API to conveniently set Vega-Lite's properties to customize the looks of the visualization. For instance, example, we can specify the axis titles using the the title property in each of the encoding channels via the field method, and we can specify the color of the mark by setting the color property in the Mark:

Data(data) * Mark(:bar, color=:tomato) * Encoding(
    y=field("a:N", title=:category),
    x=field("average(b):Q", title="Mean of b"),
)

Publish your Visualization

If you are running Deneb under an environment that can render rich MIME types (Jupyter, Pluto, VSCode, ...) then charts will be automatically displayed. You can inspect the raw JSON VegaLite specification by using the print or the Deneb.json methods:

chart = Data(data) * Mark(:bar, color=:tomato) * Encoding(
    y=field("a:N", title=:category),
    x=field("average(b):Q", title="Mean of b"),
)
print(chart)
{
  "config": {
    "view": {
      "continuousWidth": 300,
      "continuousHeight": 300,
      "step": 25
    },
    "mark": {
      "tooltip": true
    }
  },
  "data": {
    "values": [
      {
        "a": "C",
        "b": 2
      },
      {
        "a": "C",
        "b": 7
      },
      {
        "a": "C",
        "b": 4
      },
      {
        "a": "D",
        "b": 1
      },
      {
        "a": "D",
        "b": 2
      },
      {
        "a": "D",
        "b": 6
      },
      {
        "a": "E",
        "b": 8
      },
      {
        "a": "E",
        "b": 4
      },
      {
        "a": "E",
        "b": 7
      }
    ]
  },
  "mark": {
    "type": "bar",
    "color": "tomato"
  },
  "encoding": {
    "y": {
      "field": "a",
      "type": "nominal",
      "title": "category"
    },
    "x": {
      "aggregate": "average",
      "field": "b",
      "type": "quantitative",
      "title": "Mean of b"
    }
  }
}

Note that the config property of the specification was automatically generated by the default Deneb.jl theme.

You can publish your visualization somewhere in the web using Vega-Embed to embed the Vega-Lite specification in a webpage. A simple example of a stand-alone HTML document can be generated for any chart using the Deneb.html method:

print(Deneb.html(chart))
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
    <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
    <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>
<body>
    <div id="vis-78b90e24-bba3-4c77-b3c0-70317f1067b9"></div>

    <script type="text/javascript">
    var spec = {"config":{"view":{"continuousWidth":300,"continuousHeight":300,"step":25},"mark":{"tooltip":true}},"data":{"values":[{"a":"C","b":2},{"a":"C","b":7},{"a":"C","b":4},{"a":"D","b":1},{"a":"D","b":2},{"a":"D","b":6},{"a":"E","b":8},{"a":"E","b":4},{"a":"E","b":7}]},"mark":{"type":"bar","color":"tomato"},"encoding":{"y":{"field":"a","type":"nominal","title":"category"},"x":{"aggregate":"average","field":"b","type":"quantitative","title":"Mean of b"}}};
    var embedOpt = {'mode': 'vega-lite'};
    function showError(el, error){
        el.innerHTML = ('<div class="error" style="color:red;">'
                        + '<p>JavaScript Error: ' + error.message + '</p>'
                        + "<p>This usually means there's a typo in your chart specification. "
                        + "See the javascript console for the full traceback.</p>"
                        + '</div>');
        throw error;
    }
    const el = document.getElementById('vis-78b90e24-bba3-4c77-b3c0-70317f1067b9');
    vegaEmbed('#vis-78b90e24-bba3-4c77-b3c0-70317f1067b9', spec, embedOpt).catch(error => showError(el, error));
    </script>
</body>
</html>

You can also save the visualization to a file as an stand-alone HTM document using the save method:

save("chart.html", chart)

The visualization can also be saved as a JSON file or as an image by using any of the following extensions: .json, .png, .svg, .pdf.