Sample Compost data visualizations

This page contains various examples of using Compost. The code is executed on the fly, so each of the code blocks is fully standalone. This also makes some examples longer, because we cannot reuse helper functions between files. All sample data is defined in the docs.js file on GitHub.

UK elections: Basic bar chart

The following code sample shows how to construct a basic bar chart.

let bars =
  c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
    elections.map(e =>
      c.padding(0, 10, 0, 10,
        c.fillColor(e.color, c.column(e.party, e.y17))
      ))
    )))

c.render("out1a", bars)

UK elections: Double bar chart

The following chart shows two bars for each political party. The x coordinates of a bar is given as ["Green", 0] and ["Green", 0.5] for the first bar and ["Green", 0.5] and ["Green", 1] for the second bar.

// Makes a color given in "#rrggbb" format darker or lighter
// (by multiplying each component by the specified number k)
function adjust(color, k) {
  let r = parseInt(color.substr(1, 2), 16)
  let g = parseInt(color.substr(3, 2), 16)
  let b = parseInt(color.substr(5, 2), 16)
  let f = n => n*k > 255 ? 255 : n*k;
  return "#" + ((f(r) << 16) + (f(g) << 8) + (f(b) << 0)).toString(16);
}

// Creates a bar of height 'y' that is witin a categorical value 'x'
// starting at the offset 'f' and ending at the offset 't'.
function partColumn(f, t, x, y) {
  return c.shape([ [ [x,f], y ], [ [x,t], y ], [ [x,t], 0 ], [ [x,f], 0 ] ])
}

let bars =
  c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
    elections.map(e =>
      c.padding(0, 10, 0, 10, c.overlay([
        c.fillColor(adjust(e.color, 0.8), partColumn(0, 0.5, e.party, e.y17)),
        c.fillColor(adjust(e.color, 1.2), partColumn(0.5, 1, e.party, e.y19))
      ]))
    )
  )))

c.render("out1b", bars)

UK elections: Double bar chart with a title

The following chart shows two bars for each political party. The x coordinates of a bar is given as ["Green", 0] and ["Green", 0.5] for the first bar and ["Green", 0.5] and ["Green", 1] for the second bar. We also add a title using the c.nest primitive to align the title and the chart itself.

// Makes a color given in "#rrggbb" format darker or lighter
// (by multiplying each component by the specified number k)
function adjust(color, k) {
  let r = parseInt(color.substr(1, 2), 16)
  let g = parseInt(color.substr(3, 2), 16)
  let b = parseInt(color.substr(5, 2), 16)
  let f = n => n*k > 255 ? 255 : n*k;
  return "#" + ((f(r) << 16) + (f(g) << 8) + (f(b) << 0)).toString(16);
}

// A derived Compost operation that adds a title to any given chart.
// This works by creating text element and using 'nest' to allocate top
// 15% of space for the title and the remaining 85% of space for the title.
function title(text, chart) {
  let title = c.scale(s.continuous(0, 100), s.continuous(0, 100),
    c.font("11pt arial", "black", c.text(50, 80, text)))
  return c.overlay([
    c.nest(0, 100, 85, 100, title),
    c.nest(0, 100, 0, 85, chart)
  ])
}

// Creates a bar of height 'y' that is witin a categorical value 'x'
// starting at the offset 'f' and ending at the offset 't'.
function partColumn(f, t, x, y) {
  return c.shape([ [ [x,f], y ], [ [x,t], y ], [ [x,t], 0 ], [ [x,f], 0 ] ])
}

let bars =
  c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
    elections.map(e =>
      c.padding(0, 10, 0, 10, c.overlay([
        c.fillColor(adjust(e.color, 0.8), partColumn(0, 0.5, e.party, e.y17)),
        c.fillColor(adjust(e.color, 1.2), partColumn(0.5, 1, e.party, e.y19))
      ]))
    )
  )))

c.render("out1c", title("United Kingdom general elections (2017 vs 2019)", bars))

Exchange rates: Line chart with explicit scale

This demo creates a basic line chart showing GBP/USD exchange rate around 23 June 2016. This is done using c.line which takes an array of x and y coordinates. We then change the line color, explicitly set the y scale (to make it look nicer) and add axes.

let rates = c.axes("left right bottom",
    c.scaleY(s.continuous(1.25, 1.52), c.strokeColor("#202020",
      c.line(gbpusd.map((v, i) => [i, v])))
  ))
c.render("out2a", rates)

Exchange rates: Line chart with highlighted background

This is the demo from the Compost home page. We create a line chart, but with two areas highlighted in different colors. This is achieved by overlaying two rectangular shapes and a line.

let lo = 1.25, hi = 1.52
let rates = c.axes("left right bottom", c.overlay([
    c.fillColor("#1F77B460",  c.shape(
      [ [0,lo], [16,lo], [16,hi], [0,hi] ])),
    c.fillColor("#D6272860",  c.shape(
      [ [gbpusd.length-1,lo], [16,lo], [16,hi], [gbpusd.length-1,hi] ])),
    c.strokeColor("#202020",
      c.line(gbpusd.map((v, i) => [i, v])))
  ]))
c.render("out2b", rates)

Exchange rates: Highlighting area under a line

What if we wanted to highlight just the area under a line, rather than the whole background of the chart? The c.shape primitive can fill any polygon, so we are not limited to specifying a rectangle as above. In the following, we generate the background shapes so that the bottom is flat, but the top is aligned with the data points on the line.

let lo = 1.25, hi = 1.52
let rates = c.axes("left right bottom", c.overlay([
    c.fillColor("#1F77B460",  c.shape(
      gbpusd.slice(0, 17).map((v, i) => [i, v])
        .concat([[16, lo], [0, lo]]) )),
    c.fillColor("#D6272860",  c.shape(
      gbpusd.slice(16).map((v, i) => [i+16, v])
        .concat([[gbpusd.length-1, lo], [16, lo]]) )),
    c.strokeColor("#202020",
      c.line(gbpusd.map((v, i) => [i, v])))
  ]))
c.render("out2c", rates)

Exchange rates: Two line charts with shared axis

In this demo, we compare two different exchange rates. To do this, we create two line charts and then place one above the other. The charts will share the X axis, but they will have separate Y axes. This is done using the c.nestY primitive, which nests a scale of a shape inside a newly defined locations. Here, the bottom chart takes are from 0 to 50 and the top chart takes an area from 50 to 100.

function body(lo, hi, data) {
  return c.axes("left right bottom", c.overlay([
    c.fillColor("#1F77B460",  c.shape(
      [ [0,lo], [16,lo], [16,hi], [0,hi] ])),
    c.fillColor("#D6272860",  c.shape(
      [ [data.length-1,lo], [16,lo], [16,hi], [data.length-1,hi] ])),
    c.strokeColor("#202020",
      c.line(data.map((v, i) => [i, v])))
  ]))
}
let rates = c.overlay([
  c.nestY(0, 50, body(1.25, 1.52, gbpusd)),
  c.nestY(50, 100, body(1.15, 1.32, gbpeur)),
])

c.render("out2d", rates)

Iris dataset: Creating basic scatterplot

This example shows how to create a scatterplot comparing two features of the data from the Iris dataset. This is done by overlaying bubbles. In addition, we use the Iris species to set the colors of the dots.

let irisColors = { Setosa:"blue",
    Virginica:"green", Versicolor:"red" }
let x = "sepal_width", y = "petal_width"

let scatter =
  c.axes("left bottom", c.overlay(
    iris.map(i => c.strokeColor(
      irisColors[i.species], c.bubble(i[x], i[y], 1, 1)))
  ))
c.render("out3a", scatter)

Iris dataset: Creating basic histogram

Compost does not have a built-in histogram chart, but we can create this ourselves by writing a helper function that takes an array of data, splits it into bins of equal size and then counts the number of data points in each bin. The results can then be easily rendered by overlaying rectangular shapes.

// Calculate bins of a histogram. The function splits the data into 10
// equally sized bins, counts the values in each bin and returns an array
// of three-element arrays with start of the bin, end of the bin and count
function bins(data) {
  let lo = Math.min(...data), hi = Math.max(...data);
  let bins = {}
  for(var i=0; i<data.length; i++) {
    let k = Math.round((data[i]-lo)/(hi-lo)*9);
    if (bins[k]==undefined) bins[k]=1; else bins[k]++;
  }
  let keys = Object.keys(bins).map(k => k*1).sort()
  return keys.map(k =>
    [ lo + (hi - lo) * (k / 10),
      lo + (hi - lo) * ((k + 1) / 10), bins[k]]);
}

let hist =
  c.axes("left bottom", c.overlay(
    bins(iris.map(i => i["petal_width"])).map(b =>
      c.fillColor("#808080", c.shape(
        [ [b[0], b[2]], [b[1], b[2]], [b[1], 0], [b[0], 0] ])) )
    ))
c.render("out3b", hist)

Iris dataset: Histogram and scatter in a pairplot

This example combines the previous two into a single chart. It is a reimplementation of the seaborn library pairplot chart. We build a matrix of chart showing a pairwise scatter-plot for all features and a histogram of feature distributions at the diagonal.

// Calculate bins of a histogram. The function splits the data into 10
// equally sized bins, counts the values in each bin and returns an array
// of three-element arrays with start of the bin, end of the bin and count
function bins(data) {
  let lo = Math.min(...data), hi = Math.max(...data);
  let bins = {}
  for(var i=0; i<data.length; i++) {
    let k = Math.round((data[i]-lo)/(hi-lo)*9);
    if (bins[k]==undefined) bins[k]=1; else bins[k]++;
  }
  let keys = Object.keys(bins).map(k => k*1).sort()
  return keys.map(k =>
    [ lo + (hi - lo) * (k / 10),
      lo + (hi - lo) * ((k + 1) / 10), bins[k]]);
}

let irisColors = {Setosa:"blue", Virginica:"green", Versicolor:"red" }
let cats = ["sepal_width", "petal_length", "petal_width"]

let pairplot =
  c.overlay(cats.map(x => cats.reverse().map(y =>
    c.nest([x, 0], [x, 1], [y, 0], [y, 1],
      c.axes("left bottom", c.overlay(
        x == y
        ? bins(iris.map(i => i[x])).map(b =>
            c.fillColor("#808080", c.shape(
              [ [b[0], b[2]], [b[1], b[2]], [b[1], 0], [b[0], 0] ])) )
        : iris.map(i => c.strokeColor(irisColors[i.species],
            c.bubble(i[x], i[y], 1, 1)))
      ))))).flat())

c.render("out3c", pairplot)

You draw it: Interactive bar chart

This demo shows how to create interactive charts using Compost. It is inspired by the You draw it visualizations by New York Times. You can click on the chart to adjust the size of the bars. The demo is based on the Elm architecture and the main nice thing is that events are reported in domain units.

let partyColors = {}
for(var i = 0; i < elections.length; i++)
  partyColors[elections[i].party] = elections[i].color;

function update(state, evt) {
  switch (evt.kind) {
    case 'set':
      if (!state.enabled) return state;
      let newValues = state.values.map(kv =>
        kv[0] == evt.party ? [kv[0], evt.newValue] : kv)
      return { ...state, values: newValues }
    case 'enable':
      return { ...state, enabled: evt.enabled }
  }
}

function render(trigger, state) {
  return c.axes("left bottom", c.scaleY(s.continuous(0, 400),
      c.on({
        mousedown: () => trigger({ kind:'enable', enabled:true }),
        mouseup: () => trigger({ kind:'enable', enabled:false }),
        mousemove: (x, y) => trigger({ kind:'set', party:x[0], newValue:y })
      }, c.overlay(state.values.map(kv =>
        c.fillColor(partyColors[kv[0]],
          c.padding(0, 10, 0, 10, c.column(kv[0], kv[1]))) ))
    )))
}

let init = { enabled:false, values: elections.map(e => [e.party, e.y19]) }
c.interactive("out4", init, update, render)

Learn more about Compost