Charts: From a Bar Chart to a Polar Ribbon
Welcome to GoFish! In this tutorial we'll start with a bar chart and gradually turn it into a polar ribbon chart. Along the way, we'll encounter the pieces that make up a GoFish chart: shapes, graphical operators, scales, and coordinate transforms.
Before you start
This tutorial assumes you've been through Basics, which covers shapes and graphical operators.
To start, duplicate this tab to follow along in the live editor!
The Dataset
The dataset we'll work with in this tutorial is counts of the number of fish caught in different lakes.
type SeafoodData = {
lake: "Lake A" | "Lake B" | "Lake C" | "Lake D" | "Lake E" | "Lake F";
species: "Bass" | "Trout" | "Catfish" | "Perch" | "Salmon";
count: number;
};
const seafood: SeafoodData[] = [
{
lake: "Lake A",
species: "Bass",
count: 23,
},
{
lake: "Lake A",
species: "Trout",
count: 31,
},
{
lake: "Lake A",
species: "Catfish",
count: 29,
},
...
];Anatomy of a GoFish Specification
A basic GoFish spec has four pieces: chart, flow, mark, and render.
chart: Data
The chart function is how you start your specification. It's where you put your data.
chart(seafood);flow: Graphical Operators
The flow method is where you specify graphical operators. Graphical operators transform your dataset (usually by applying a groupBy) and specify layout.
.flow(spread({ by: "lake", dir: "x" }))Here we're using the spread operator to create one group per lake and we arrange them horizontally thanks the dir: x option.
mark: Shapes
Lastly we call the mark method to specify the shapes we place in each of the regions created by the spread operator.
.mark(rect({ h: "count" }))In this case, we created some rectangles whose heights correspond to the count values of the different lakes. Since we didn't define the width of the rectangle, the spread operator and rect shape work together to infer it for us!
Rendering
chart(seafood, { axes: true }).render(root, { w: 500, h: 300 });The render method draws our chart to the screen! We give it a DOM container to render into (root in this case) and some options. We've specified the width and height of our chart with w and h (just like on rect). We've also told GoFish to create some axes, labels, and legends for us automatically by passing axes: true in the chart() options.
Both w and h are optional, and an omitted dimension is computed during layout per axis. An axis that scales data into pixels — a positional axis (scatter), or a data-driven size like bar heights — falls back to a default size. An axis with nothing to scale — a category axis, or fixed-size marks — keeps the marks at their natural size and shrinks to fit them. So a bar chart with no width gets default-width bars and a chart only as wide as it needs to be.
Bar Chart
The first thing we'll do is compare the number of fish in each lake. We can use a bar chart for that. We'll build it up in a few steps. First, we'll just create one bar for each lake in the dataset:
gf.chart(seafood)
.flow(gf.spread({ by: "lake", dir: "x" }))
.mark(gf.rect({ w: 32, h: 300, fill: gf.color.green[5] }))
.render(root, { w: 500, h: 300 });Note that we've added w: 32 and h: 300 to the rectangles to set manually set their widths and heights.
The spread operator
We've introduced a spread graphical operator in the .flow() method that spaces its children apart. The spread operator groups the data by the field we specify (in this case, lake) and creates one shape for each group. Here, we're spreading along the x direction with dir: "x", which will create six rectangles (one for each lake).
Data-Driven Fields
To turn this into a bar chart, we'll change the h encoding of the rect shape to a data-driven quantity.
gf.chart(seafood)
.flow(gf.spread({ by: "lake", dir: "x" }))
.mark(gf.rect({ w: 32, h: "count", fill: gf.color.green[5] }))
.render(root, { w: 500, h: 300 });Inferred Fields
We remove the w field from our spec to have GoFish infer it for us. GoFish uses the overall size of the chart we gave to render (as well as information from the graphical operators) to determine the width of each rectangle.
gf.chart(seafood)
.flow(gf.spread({ by: "lake", dir: "x" }))
.mark(gf.rect({ h: "count", fill: gf.color.green[5] }))
.render(root, { w: 500, h: 300 });Axes
Great! Now let's talk about how to add axes to your chart. GoFish can automatically infer axes from your spec as long as you pass axes: true in the chart() options like so:
gf.chart(seafood, { axes: true })
.flow(gf.spread({ by: "lake", dir: "x" }))
.mark(gf.rect({ h: "count", fill: gf.color.green[5] }))
.render(root, { w: 500, h: 300 });Voila! Now we have a y-axis and labels for each of the bars.
Stacked Bar Chart
Now we have a sense of the number of fish in each lake. It seems like Lake B has the most. What if we broke this down by species? We can use a stacked bar chart for that. A stacked bar chart is kinda like a normal bar chart, except instead of a line of rectangles, it's a line of stacked rectangles.
gf.chart(seafood, { axes: true })
.flow(
gf.spread({ by: "lake", dir: "x" }), //
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: gf.color.green[5] }))
.render(root, { w: 500, h: 300 });We've added the stack operator to stack rectangles on top of each other vertically. It's pretty similar to spread, but doesn't put any spacing between the shapes it lays out.
Now we have a rectangle for each species in each lake. But we can't tell the fish apart! Let's add a color encoding so that each rectangle's color corresponds to the species of fish.
gf.chart(seafood, { axes: true })
.flow(
gf.spread({ by: "lake", dir: "x" }), //
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: "species" }))
.render(root, { w: 500, h: 300 });Much better! Notice that we also have a color legend telling us what each color represents. This was created automatically because we passed axes: true in the chart() options.
Ribbon Chart
Data Transformation
Now we have a sense of the break down by lake, but these lakes are connected by a river! It's hard to track how the proportion of fish changes between each lake. Let's first try ordering the bars by their counts:
gf.chart(seafood, { axes: true })
.flow(
gf.spread({ by: "lake", dir: "x" }),
gf.derive((d) => _.orderBy(d, "count")),
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: "species" }))
.render(root, { w: 500, h: 300 });We've used the derive operator, which lets us add data transforms into our flow!
Some trends pop out. The salmon population spikes between lakes B and C while catfish appear to decline. We can make these trends more obvious by connecting rectangles of the same species together.
Layering and Selection
gf.layer({ axes: true }, [
gf
.chart(seafood)
.flow(
gf.spread({ by: "lake", dir: "x" }),
gf.derive((d) => _.orderBy(d, "count")),
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: "species" }).name("bars")),
gf
.chart(gf.selectAll("bars"))
.flow(gf.group({ by: "species" }))
.mark(gf.ribbon({ opacity: 0.8 })),
]).render(root, {
w: 500,
h: 300,
});Great! This is already a ribbon chart but it's a little funky. We'll fix the funkiness in a second, but first let's understand what's going on.
To add some ribbons, we first created a layer so we can add the ribbons as a second layer. Then we name the marks in the first layer using .name("bars") and selectAll those marks in the second layer. selectAll("bars") hands us one ref per bar; we group them by species using gf.group({ by: "species" }) and finally draw a ribbon mark for each group.
To make this look more like a traditional ribbon chart, all we have to do is change the spacing of the spread operator.
gf.layer({ axes: true }, [
gf
.chart(seafood)
.flow(
gf.spread({ by: "lake", dir: "x", spacing: 64 }),
gf.derive((d) => _.orderBy(d, "count")),
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: "species" }).name("bars")),
gf
.chart(gf.selectAll("bars"))
.flow(gf.group({ by: "species" }))
.mark(gf.ribbon({ opacity: 0.8 })),
]).render(root, {
w: 500,
h: 300,
});Polar Ribbon Chart
Finally it's time to make our polar ribbon chart! To do so, we'll add a clock coordinate transform to the layer and adjust the parameters to spread so that it looks better in polar space.
gf.layer({ coord: gf.clock(), axes: true }, [
gf
.chart(seafood)
.flow(
gf.spread({
by: "lake",
dir: "x",
spacing: (2 * Math.PI) / 6,
anchor: "middle",
y: 50,
label: false,
}),
gf.derive((d) => _.orderBy(d, "count")),
gf.stack({ by: "species", dir: "y", label: false })
)
.mark(gf.rect({ h: "count", fill: "species" }).name("bars")),
gf
.chart(gf.selectAll("bars"))
.flow(gf.group({ by: "species" }))
.mark(gf.ribbon({ opacity: 0.8 })),
]).render(root, {
w: 400,
h: 400,
});What's next?
Go check out some of our examples!
