Multiple types

In this tutorial we learn how to use the axis components in a smart way to emulate having multiple chart types on one chart.

Data

In this example, we use the monthly sea surface temperature anomaly from the Met Office Hadley Center. Loading and parsing the data with the Fetch API is fairly straightforward as it comes in a CSV containing numbers only (the data is copied as a Github Gist):

const URL = 'https://gist.githubusercontent.com/synesenom/38f6ac1567aab8dba68b0dbf476d8a71/raw/monthly-temperature-anomaly.csv'

(async () => {
  // Fetch our data.
  const raw = await fetch(URL)
    .then(response => response.text())
  const data = raw.split('\n')
    .map(d => {
      // For this guide's purpose, let's extract the year and anomaly
      // value only.
      const cols = d.split(',')
      return {
        x: +cols[0],
        y: +cols[2]
      }
    })
})()

A sample of what the file would be seen by Javascript:

[
    {x: 1850, y: -0.35138},
    {x: 1850, y: -0.34437},
    {x: 1850, y: -0.58001},
    {x: 1850, y: -0.34222},
    {x: 1850, y: -0.25093},
    ...
]

Note that there are no columns in the CSV file uploaded to Gist, so we simply take the first (0) and third (2) columns.

The scatter plot

We start with a scatter plot showing all anomaly values for each year. Following the example in the catalogue:

// Add scatter plot.
const scatter = dalian.ScatterPlot('scatter-plot', '#chart')
// We have a single data set in this plot.
.data([{
    name: 'monthly',
    values
}])

// Read dimensions from the container.
.width(parseFloat(d3.select('#chart').style('width')))
.height(parseFloat(d3.select('#chart').style('padding-bottom')))
.margins(50)

// Set color to grayish, add some opacity and set dot size.
.color.palette('#789')
.opacity(0.1)
.size(4)

// Set up axes.
.leftAxis.label('Time [year]')
.bottomAxis.label('Anomaly [%]')

// Render chart.
.render()

The line plot

Now we add a trend to the scatter plot which is the average of the measurements in each year. First, we need to calculate the trend, using d3.nest:

const trend = d3.nest()
    .key(d => d.x)
    .rollup(v => d3.mean(v, d => d.y))
    .entries(data)
    .map(d => Object.assign(d.value, {
        x: +d.key,
        y: d.value
    }))

This trend data is directly plottable with dalian:

const line = dalian.LineChart('trend', '#chart')
.data([{
  name: 'trend',
  values: trend
}])
.width(parseFloat(d3.select('#chart').style('width')))
.height(parseFloat(d3.select('#chart').style('padding-bottom')))
.margins(50)

// Set color.
.color.palette('firebrick')

.render()

Note how we didn't set any axis labels for the line chart.

Adjustments

As we can see, the axes are not quite aligned... This is because we applied the default axis settings which are adjusted differently for each chart. For instance, the scatter plot compresses the axis such that each circle fits in. In order to fix this, we just need to set the axis range manually, from the data. Let's calculate the range for that:

const rangeX = d3.extent(trend, d => d.x)
const rangeY = d3.extent(trend, d => d.y)

And with that, we can set hard ranges to the scatter and line plots:

scatter.xRange.min(rangeX[0])
    .xRange.max(rangeX[1])
    .yRange.min(rangeY[0])
    .yRange.max(rangeY[1])
    .yRange.compressMin(0.1)
    .yRange.compressMax(0.1)
    .render()
line.xRange.min(rangeX[0])
    .xRange.max(rangeX[1])
    .yRange.min(rangeY[0])
    .yRange.max(rangeY[1])
    .yRange.compressMin(0.1)
    .yRange.compressMax(0.1)
    .render()

Since .yRange.min and .yRange.max overwrite the default axes, we still want to have some buffer to contain all circles using .yRange.compressMin and .yRange.compressMax

Last touches

We are basically done with the last step. However, there are still some unnecessary elements there: the axis lines and ticks for the line chart. In the last step we remove those useless axis elements:

line.leftAxis.hideAxisLine(true)
    .leftAxis.hideTicks(true)
    .leftAxis.label('')
    .bottomAxis.hideAxisLine(true)
    .bottomAxis.hideTicks(true)
    .bottomAxis.label('')
    .render()

Final remarks

In order to make the steps in this tutorial isolated, we called .render after re-adjusting the ranges. Note that this is not needed and we could just as well pre-calculate the range in the beginning and apply all the adjustments right away sparing some calls to .render.

Final script

Here you can download the full working example as a stand-alone HTML file.