# Areas

## Area charts

Area charts are like line charts, except that the area below the plotted line is filled in with color, usually to indicate volume. Area charts can also be stacked, representing cumulated totals. The totals are broken down into multiple categories, visualized by stacked areas of different colors.

In this chapter we will look at the D3 area generator and the D3 stack generator, which can be used to construct a stacked area chart. Stacked bar charts are also constructed using a stack generator (but not an area generator) in D3.

## Area generator

`d3.area()` returns an area generator that produces an area defined by a topline and a baseline. Typically (but not necessarily), the topline and the baseline coordinates share the same x-values and differ in y-values (`y0` and `y1`).

``````
const area = d3.area()
.x(d => scaleX(d.date))
.y0(scaleY(0)) // baseline is the x-axis in the example below.
.y1(d => scaleY(d.interest));
``````

The next example shows an area chart of the bank interest on outstanding business loans in the Netherlands. Compare to a line chart.

``````
const width = 640;
const height = 400;
const margin = {
top: 35,
right: 20,
bottom: 35,
left: 35,
}
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");

function drawChart(data) {

const scaleX = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
//.nice();

const scaleY = d3.scaleLinear()
.domain([0, d3.max(data, d => d.interest)])
.range([height - margin.bottom, margin.top])
.nice();

// Declare the area generator.
const area = d3.area()
.x(d => scaleX(d.date))
.y0(scaleY(0))
.y1(d => scaleY(d.interest));

// Append a path for the area.
svg.append("path")
.attr("fill", "tomato")
.attr("stroke", "none")
.attr("d", area(data));

svg.append("g")
.attr("transform", `translate(0, \${height - margin.bottom})`)
.call(d3.axisBottom(scaleX).ticks(null, "%Y").tickSizeOuter(0));

svg.append("g")
.attr("transform", `translate(\${margin.left},0)`)
.call(d3.axisLeft(scaleY))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Interest (%)"))
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - margin.left - margin.right)
.attr("stroke-dasharray", "2,2")
.attr("stroke-opacity", 0.3));

svg.append("g")
.selectAll("g")
.data(data)
.join("g")
.attr("transform", d => `translate(\${scaleX(d.date)}, 0)`)
.attr("fill", "transparent")
.attr("stroke", "transparent")
.call(g => g.append("rect")
.attr("x", -0.5)
.attr("width", 1)
.attr("height", height)
.attr("stroke-width", 6))
.call(g => g.append("circle")
.attr("cx", 0)
.attr("cy", (d) => scaleY(d.interest))
.attr("r", 4)
.attr("stroke-width", 6))
.on("mouseover", function (e, d) {
const group = d3.select(this)
group.select("circle")
.attr('fill', "currentColor")
.attr('fill-opacity', 0.8)
.attr('stroke', "currentColor")
.attr('stroke-opacity', 0.4);
group.select("rect")
.attr('fill', "currentColor")
})
.on("mouseout", function (e, d) {
const group = d3.select(this)
group.select("circle")
.attr('fill', "transparent")
.attr('stroke', "transparent")
group.select("rect")
.attr('fill', "transparent")
})
.append("title")
.text(d => `\${months[d.date.getMonth()]} \${d.date.getFullYear()}: \${d.interest}%`);
};

// Fetch the CSV data file,
// and then call 'drawChart' to draw the chart:
(async function() {
try {
const response = await d3.csv('data/(23-11-13)_Bank_interest_on_outstanding_business_loans.csv', d => {
d.date = new Date(d.date);
d.interest = +d.interest;
return d;
})
drawChart(response);
}
catch (error) {
document.querySelector("#errorMessage").textContent = error;
}
})();
``````

Result:

Data source: De Nederlandsche Bank - Rente. Bank interest on outstanding business loans in the Netherlands (up to 23-11-13).

Next an example with vertical baselines, both with curved lines:

``````
const width = 400;
const height = 200;

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");

const data = [
{left: 20, right: 80},
{left: 30, right: 70},
{left: 20, right: 80},
{left: 40, right: 60},
{left: 35, right: 65},
{left: 10, right: 90}
];

const scaleX = d3.scaleLinear()
.domain([
d3.min(data, d => d.left),
d3.max(data, d => d.right)
])
.range([0, width]);
const scaleY = d3.scaleLinear()
.domain([0, data.length - 1])
.range([height, 0]);

const areaGenerator = d3.area()
.x0(d => scaleX(d.left))
.x1(d => scaleX(d.right))
.y((d, i) => scaleY(i))
.curve(d3.curveCardinal);
const pathData = areaGenerator(data);

svg.append("path")
.attr("fill", "DarkOliveGreen")
.attr("stroke", "none")
.attr("d", pathData);
``````

Result:

As with the line generator you can handle "undefined" data using `area.defined()` and render to HTML `<canvas>` using `area.context(context)`.

Next example is derived from the Observable notebook: "Radial area chart". It uses `d3.areaRadial()` to create a radial area generator.

``````
const width = 400;
const height = width;
const margin = 10;
const outerRadius = width / 2 - margin;

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");
const container = svg.append("g")
.attr("transform", `translate(\${width/2}, \${height/2})`); // move origin to center

// Produce some random data:
const data = Array.from({ length: 200 }, (v, i) => {
const nr = getRandomArbitrary(0, 40);
return {
min: nr,
max: nr + getRandomArbitrary(40, 80),
}
});

const scaleAngle = d3.scaleLinear()
.domain([0, data.length - 1])
.range([0, 2 * Math.PI]);
.domain([
d3.min(data, d => d.min),
d3.max(data, d => d.max)
])

.angle((d, i) => scaleAngle(i))
container.append("path")
.attr("fill", "steelblue")
.attr("fill-opacity", 0.2)
.attr("stroke", "none")
.attr("d", areaGenerator(data));

.angle((d, i) => scaleAngle(i))
container.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1)
.attr("d", lineGenerator(data));

container.append("g")
.selectAll()
.data(scaleAngle.ticks()) // see 1)
.join("g")
.call(g => g.append("path")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.2)
// see 2):
.attr("d", d => `
);
.call(g => g.append("text")
.attr("fill", "currentColor")
.attr("stroke", "none")
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.text(d => d)
);

container.append("g")
.attr("text-anchor", "middle")
.selectAll()
.join("g")
.call(g => g.append("circle")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
);

function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
};
``````

Result:

1) `scale.ticks()` returns an array of values sampled from the scale’s domain. The ticks depend on the scale’s type and domain, but not its range.

2) `d3.pointRadial(angle, radius)` returns Cartesian coordinates `[x, y]` for the given `angle` in radians, and the given `radius`.

## Stacks

The D3 stack generator (`d3.stack()`) can be used to create stacked bar charts and stacked area charts.

A stack generator does not produce a shape directly. Instead, it computes positions (for topline and baseline) which can be passed to an area generator or can be used to position bars in a bar chart.

The stack generator generates an empty array of positions if no `keys` are specified. `stack.keys(keys)` defines `keys` and for each key a series or layer is generated. A series or a layer is a category with a specific color and the different series or layers are stacked in the chart. The `keys` refer to the data properties to be stacked. In the example below the number of apples, bananas and cherries (the series identified by the `keys`) per month are stacked.

Each series contains a number of points, i.e. arrays with a lower and an upper value defining its baseline and topline. The points are an array with rows representing the series and columns representing all points with an equal value on the x-axis; the dates in the example below. Each point also has a data property that contains the data associated with the column to which it belongs. All this is shown in the example below.

``````
const width = 640;
const height = 400;
const margin = {
top: 35,
right: 20,
bottom: 35,
left: 35,
}

const data = [
{month: new Date("2015-01-01"), apples: 10, bananas: 20, cherries: 15},
{month: new Date("2015-02-01"), apples: 15, bananas: 15, cherries: 15},
{month: new Date("2015-03-01"), apples: 20, bananas: 25, cherries: 15}
];

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");

const seriesGen = d3.stack()
.keys(["apples", "bananas", "cherries"]);

const series = seriesGen(data);
console.log(series); // logs an array of series.
/*
[ //  0) data ↓   ,    1) data ↓     ,    2) data ↓     ,
[[0,  10, data: …], [0,  15, data: …], [0,  20, data: …], key: "apples"],    // series "apples"
[[10, 30, data: …], [15, 30, data: …], [20, 45, data: …], key: "bananas"],   // series "bananas"
[[30, 45, data: …], [30, 45, data: …], [45, 60, data: …], key: "cherries"]   // series "cherries"
]

0) data = { apples: 10, bananas: 20, cherries: 15, month: Date Thu Jan 01 2015 }
1) data = { apples: 15, bananas: 15, cherries: 15, month: Date Sun Feb 01 2015 }
2) data = { apples: 20, bananas: 25, cherries: 15, month: Date Sun Mar 01 2015 }
*/

const scaleX = d3.scaleTime()
.domain([data[0].month, data[2].month])
.range([margin.left, width - margin.right]);

const scaleY = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top])
.nice();

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10.slice(0, series.length))
.unknown("#ccc");

const area = d3.area()
.x((d) => scaleX(d.data.month))
.y0((d) => scaleY(d[0]))
.y1((d) => scaleY(d[1]));

svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("d", area)
.attr("fill", (d) => color(d.key));
``````

Result:

The same date and stack generator to create a bar chart:

``````
…

const scaleX = d3.scaleBand()
.domain(data.map(d => d.month)) // array with all dates
.range([margin.left, width - margin.right])

const scaleY = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top])
.nice();

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10.slice(0, series.length))
.unknown("#ccc");

svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", d => color(d.key))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => scaleX(d.data.month))
.attr("y", d => scaleY(d[1]))
.attr("height", d => scaleY(d[0]) - scaleY(d[1]))
.attr("width", scaleX.bandwidth());
``````

Result:

### `stack.value()`

Each series contains a number of points, arrays with a lower and an upper value defining their baseline and topline. Optional method `stack.value()` can be used to define the data property values associated with the point values in the series. By default, `stack.value()` tries to find the data object properties appointed by `stack.keys()` directly within the data. But if the data properties are in a nested object, we can specify an accessor to change the default behavior.

``````
// The stack.value accessor defaults to:
const stack = d3.stack().value((d, key) => d[key]);
``````

In the next example the data properties are in a nested object. `stack.value()` is needed to define them for the stack generator.

``````
…

const data = [
{month: new Date("2015-01-01"), fruitSales: {apples: 10, bananas: 20, cherries: 15}},
{month: new Date("2015-02-01"), fruitSales: {apples: 15, bananas: 15, cherries: 15}},
{month: new Date("2015-03-01"), fruitSales: {apples: 20, bananas: 25, cherries: 15}}
];

…

const seriesGen = d3.stack()
.keys(["apples", "bananas", "cherries"])
.value((obj, key) => obj.fruitSales[key]);

…
``````

Result:

Next example handles a more complex scenario. Now there are multiple data points per column. Data needs to be grouped before the stack generator can be implemented. This is actually the elaborated example presented in the D3 documentation.

``````
const width = 640;
const height = 400;
const margin = {
top: 35,
right: 20,
bottom: 35,
left: 35,
}

const data = [
{date: new Date("2015-01-01"), fruit: "apples", sales: 3840},
{date: new Date("2015-01-01"), fruit: "bananas", sales: 1920},
{date: new Date("2015-01-01"), fruit: "cherries", sales: 960},
{date: new Date("2015-01-01"), fruit: "durians", sales: 400},
{date: new Date("2015-02-01"), fruit: "apples", sales: 1600},
{date: new Date("2015-02-01"), fruit: "bananas", sales: 1440},
{date: new Date("2015-02-01"), fruit: "cherries", sales: 960},
{date: new Date("2015-02-01"), fruit: "durians", sales: 400},
{date: new Date("2015-03-01"), fruit: "apples", sales: 640},
{date: new Date("2015-03-01"), fruit: "bananas", sales: 960},
{date: new Date("2015-03-01"), fruit: "cherries", sales: 640},
{date: new Date("2015-03-01"), fruit: "durians", sales: 400},
{date: new Date("2015-04-01"), fruit: "apples", sales: 320},
{date: new Date("2015-04-01"), fruit: "bananas", sales: 480},
{date: new Date("2015-04-01"), fruit: "cherries", sales: 640},
{date: new Date("2015-04-01"), fruit: "durians", sales: 400}
];

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");

const seriesGen = d3.stack()
.keys(d3.union(data.map(d => d.fruit))) // sets the keys: [apples, bananas, cherries, durians]
// d3.union erases duplicate fruits
.value(([, group], key) => group.get(key).sales); // see "Grouping data" below

const indexedData = d3.index(data, d => d.date, d => d.fruit); // see "Grouping data" below
const series = seriesGen(indexedData);

//console.log(series);
/*
[
[[   0, 3840], [   0, 1600], [   0,  640], [   0,  320]], // series: key: apples
[[3840, 5760], [1600, 3040], [ 640, 1600], [ 320,  800]], // series: key: bananas
[[5760, 6720], [3040, 4000], [1600, 2240], [ 800, 1440]], // series: key: cherries
[[6720, 7120], [4000, 4400], [2240, 2640], [1440, 1840]]  // series: key: durians
]
0) data = [
Date Jan 01 2015,
{
apples → Object { date: Date Jan 01 2015, fruit: "apples", sales: 3840 },
bananas → Object { date: Date Jan 01 2015, fruit: "bananas", sales: 1920 },
cherries → Object { date: Date Jan 01 2015, fruit: "cherries", sales: 960 }
durians → Object { date: Date Jan 01 2015, fruit: "durians", sales: 400 }
}]
*/

const scaleX = d3.scaleBand()
//.domain(d3.union(data.map(d => d.date))); // or:
.domain([...new Set(data.map(d => d.date))]) // erase duplicate dates.
.range([margin.left, width - margin.right])

const scaleY = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top])
.nice();

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key)) // array of all the fruits
.range(["#608000", "#e6e600", "#D2042D", "#b38600"])
.unknown("#ccc");

svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", d => color(d.key))
.call(g => g.append("title")
.append("title")
.text(d => `\${d.key}`))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => scaleX(d.data[0])) // date
.attr("y", d => scaleY(d[1]))
.attr("height", d => scaleY(d[0]) - scaleY(d[1]))
.attr("width", scaleX.bandwidth());

const timeAxis = d3.axisBottom(scaleX)
.tickFormat(d3.timeFormat("%B %Y"))
.tickSizeOuter(0);
svg.append("g")
.attr("transform", `translate(0, \${height - margin.bottom})`)
.call(timeAxis);

d3.formatDefaultLocale({
thousands: "\u{2009}", // thin space, SI standard thousands separator.
grouping: [3],
});
svg.append("g")
.attr("transform", `translate(\${margin.left}, 0)`)
//.call(d3.axisLeft(scaleY).tickFormat(d3.format(""))); // no thousands separator
.call(d3.axisLeft(scaleY).tickFormat(d3.format(","))); // use thousands separator
``````

Result:

PS. Why is the data in the example above not suitable for an area graph?

### `stack.order()`

Only the bottom layer of a stacked chart is aligned, so comparing across categories is not always easy. It is important to chose the stack order carefully. The stack order of the series is the same as the order of how the keys are specified in the parameter array of `stack.keys()`. But the stack order can also be configured using `stack.order()`, for example, the smallest series at the bottom or the largest series in the middle.

The default order accessor of `stack.order()` is `stackOrderNone`, which uses the order given by the `stack.keys()` accessor. The order accessor can be a user defined function or one of the D3 built-in stack orders.

``````
const stack = d3.stack().keys(["apples", "bananas", "cherries", "durians"]);
``````
...equals:
``````
const stack = d3.stack()
.keys(["apples", "bananas", "cherries", "durians"])
.order(d3.stackOrderNone);
``````

The image below shows an area chart with default order and its clone, but with a stack order reversed from that given by the key accessor.

### `stack.offset()`

By default, the baseline of an area graph is zero (generally this means the stacking starts at y = 0). The baseline and topline can also be configured using `stack.offset()`.

The default offset accessor of `stack.offset()` is `stackOffsetNone`. The offset accessor can be a user defined function or one of the D3 built-in stack offsets.

``````
const stack = d3.stack().keys(["apples", "bananas", "cherries", "durians"]);
``````
...equals:
``````
const stack = d3.stack()
.keys(["apples", "bananas", "cherries", "durians"])
.offset(d3.stackOffsetNone);
``````

Next example shows a streamgraph. For a streamgraph it is recommended to use order `d3.stackOrderInsideOut` in conjunction with offset `d3.stackOffsetWiggle`, which minimizes the wiggle of layers.

``````
const width = 640;
const height = 400;
const margin = {
top: 35,
right: 20,
bottom: 60,
left: 35,
}

const data = [
{month: new Date("2015-01-01"), fruitSales: {apples: 10, bananas: 20, cherries: 15}},
{month: new Date("2015-02-01"), fruitSales: {apples: 15, bananas: 15, cherries: 15}},
{month: new Date("2015-03-01"), fruitSales: {apples: 20, bananas: 25, cherries: 15}},
{month: new Date("2015-04-01"), fruitSales: {apples: 25, bananas: 10, cherries: 20}},
{month: new Date("2015-05-01"), fruitSales: {apples: 15, bananas: 15, cherries: 10}}
];

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 \${width} \${height}`)
.attr("style", "width: 100%; height: auto;");

const seriesGen = d3.stack()
.keys(["apples", "bananas", "cherries"])
.value((obj, key) => obj.fruitSales[key])
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetWiggle);

const series = seriesGen(data);
const lowestPoint = d3.min(series, d => d3.min(d, d => d[0]));

const scaleX = d3.scaleTime()
.domain([data[0].month, data[data.length-1].month])
.range([margin.left, width - margin.right])
.nice();

const scaleY = d3.scaleLinear()
.domain([lowestPoint, d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top])
.nice();

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10.slice(0, series.length))
.unknown("#ccc");

const area = d3.area()
.x((d) => scaleX(d.data.month))
.y0((d) => scaleY(d[0]))
.y1((d) => scaleY(d[1]))
.curve(d3.curveCatmullRom.alpha(0.1));

svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("d", area)
.attr("fill", (d) => color(d.key));

svg.append("g")
.attr("transform", `translate(0, \${scaleY(lowestPoint)})`)
.call(d3.axisBottom(scaleX)
.tickFormat(d3.timeFormat("%b %Y"))
.ticks(data.length)
.tickSizeOuter(0)
);

svg.append("g")
.attr("transform", `translate(\${margin.left}, 0)`)
.call(d3.axisLeft(scaleY)
.tickFormat(d => Math.abs(d)) // remove minus sign
);
``````

Result: