Chart guide Tableau and SQL

How do I build a small multiples chart?

Give each category its own small panel with the same axes: sort the categories, number them with INDEX(), and use (INDEX()-1) % 4 for the panel column and INT((INDEX()-1) / 4) for the row. In this library system, loans fell 3.6%, yet 10 of 12 branches grew; only the panels show it.

Updated

The data

Maple Falls Public Library loans, Nov 2023 to Oct 2025

Synthetic databranch_loans.csv

A city library with 12 branches. Monthly loans per branch over two years. The Central branch moved to a small temporary site for a renovation in January 2025.

monthbranchloans
2023-11-01Brookside2405
2023-11-01Cedar Hill3333
2023-11-01Central19653
2023-11-01Eastgate1545

Step 1

One line per branch: the spaghetti chart

Twelve colors, one obvious story: Central drops in January 2025. Everything else is a tangle near the bottom.

Tableau

Columns: MONTH([month])    Rows: SUM([loans])    Color: [branch]

SQL

SELECT month, branch, SUM(loans) AS loans
FROM input
GROUP BY month, branch
ORDER BY month, branch

Rows

MonthBranchLoans
Nov 1, 2023Brookside2,405
Nov 1, 2023Cedar Hill3,333
Nov 1, 2023Central19,653
Nov 1, 2023Eastgate1,545
Nov 1, 2023Fairview3,067
Nov 1, 2023Hillside5,775
Nov 1, 2023Lakeview7,200
Nov 1, 2023Mill Creek4,311
Nov 1, 2023Northgate8,948
Nov 1, 2023Oak Park5,264
Nov 1, 2023Southport3,961
Nov 1, 2023Westbrook8,170
Dec 1, 2023Brookside2,255
Dec 1, 2023Cedar Hill3,102
Dec 1, 2023Central17,412
Dec 1, 2023Eastgate1,556
Dec 1, 2023Fairview2,777
Dec 1, 2023Hillside5,305
Dec 1, 2023Lakeview6,109
Dec 1, 2023Mill Creek4,192
Dec 1, 2023Northgate8,364
Dec 1, 2023Oak Park4,900
Dec 1, 2023Southport3,826
Dec 1, 2023Westbrook7,361
Jan 1, 2024Brookside2,777
Jan 1, 2024Cedar Hill3,892
Jan 1, 2024Central21,891
Jan 1, 2024Eastgate1,961
Jan 1, 2024Fairview3,600
Jan 1, 2024Hillside6,401
Jan 1, 2024Lakeview8,109
Jan 1, 2024Mill Creek5,306
Jan 1, 2024Northgate10,023
Jan 1, 2024Oak Park6,061
Jan 1, 2024Southport4,502
Jan 1, 2024Westbrook9,480
Feb 1, 2024Brookside2,703
Feb 1, 2024Cedar Hill3,918
Feb 1, 2024Central21,622
Feb 1, 2024Eastgate1,976
Feb 1, 2024Fairview3,323
Feb 1, 2024Hillside6,585
Feb 1, 2024Lakeview7,211
Feb 1, 2024Mill Creek4,829
Feb 1, 2024Northgate9,885
Feb 1, 2024Oak Park5,686
Feb 1, 2024Southport3,933
Feb 1, 2024Westbrook8,473
Mar 1, 2024Brookside2,579
Mar 1, 2024Cedar Hill3,989

First 50 of 288 rows.

Step 2

Wrap the branches into a grid

Each branch gets a panel, biggest first, all on the same axis. INDEX() numbers the branches in sort order; the remainder after dividing by the column count picks the column, the whole part picks the row. Compute Using must be branch, or the numbering runs along the months instead.

Tableau

// [Panel col]: (INDEX() - 1) % {knob.cols}
// [Panel row]: INT((INDEX() - 1) / {knob.cols})
// Both discrete. Compute Using: branch, sorted by SUM([loans]) descending
// Columns: [Panel col], MONTH([month])    Rows: [Panel row], SUM([loans])    Detail: [branch]

SQL

WITH t AS (
  SELECT branch, SUM(loans) AS total
  FROM input
  GROUP BY branch
), k AS (
  SELECT
    branch,
    ROW_NUMBER() OVER (ORDER BY total DESC, branch) - 1 AS idx
  FROM t
)
SELECT
  i.month,
  i.branch,
  i.loans,
  k.idx % 4 AS panel_col,
  k.idx // {knob.cols} AS panel_row FROM input i JOIN k USING (branch) ORDER BY panel_row, panel_col, i.month

Rows

MonthBranchLoansPanel colPanel row
Nov 1, 2023Central19,65300
Dec 1, 2023Central17,41200
Jan 1, 2024Central21,89100
Feb 1, 2024Central21,62200
Mar 1, 2024Central21,38600
Apr 1, 2024Central19,63700
May 1, 2024Central19,43300
Jun 1, 2024Central22,52500
Jul 1, 2024Central24,81900
Aug 1, 2024Central24,16600
Sep 1, 2024Central19,87800
Oct 1, 2024Central20,35400
Nov 1, 2024Central18,90000
Dec 1, 2024Central18,66800
Jan 1, 2025Central12,64100
Feb 1, 2025Central12,62500
Mar 1, 2025Central13,70500
Apr 1, 2025Central13,01100
May 1, 2025Central12,69100
Jun 1, 2025Central14,17100
Jul 1, 2025Central15,63700
Aug 1, 2025Central14,75100
Sep 1, 2025Central12,15000
Oct 1, 2025Central12,55300
Nov 1, 2023Northgate8,94810
Dec 1, 2023Northgate8,36410
Jan 1, 2024Northgate10,02310
Feb 1, 2024Northgate9,88510
Mar 1, 2024Northgate10,49710
Apr 1, 2024Northgate9,77310
May 1, 2024Northgate9,40910
Jun 1, 2024Northgate10,90910
Jul 1, 2024Northgate12,52010
Aug 1, 2024Northgate11,94610
Sep 1, 2024Northgate9,62410
Oct 1, 2024Northgate9,56810
Nov 1, 2024Northgate10,09110
Dec 1, 2024Northgate8,83710
Jan 1, 2025Northgate11,29810
Feb 1, 2025Northgate10,69710
Mar 1, 2025Northgate10,88310
Apr 1, 2025Northgate10,62410
May 1, 2025Northgate9,82810
Jun 1, 2025Northgate11,43610
Jul 1, 2025Northgate13,62810
Aug 1, 2025Northgate12,12210
Sep 1, 2025Northgate9,57210
Oct 1, 2025Northgate11,13010
Nov 1, 2023Westbrook8,17020
Dec 1, 2023Westbrook7,36120

First 50 of 288 rows.

Step 3

Index each panel to its own first year

On the shared axis, the small branches are flat lines. Switch the Y axis to the index and every branch starts near 100, so you compare shape, not size. Eastgate, the smallest branch, turns out to be the fastest-growing.

Tableau

100 * SUM([loans]) / MIN({ FIXED [branch] : SUM(IF [month] < #2024-11-01# THEN [loans] END) / 12 })

SQL

WITH t AS (
  SELECT
    branch,
    SUM(loans) AS total,
    AVG(loans) FILTER (WHERE month < DATE '2024-11-01') AS base
  FROM input
  GROUP BY branch
), k AS (
  SELECT
    branch,
    base,
    ROW_NUMBER() OVER (ORDER BY total DESC, branch) - 1 AS idx
  FROM t
)
SELECT
  i.month,
  i.branch,
  i.loans,
  ROUND(100.0 * i.loans / k.base, 1) AS indexed,
  k.idx % 4 AS panel_col,
  k.idx // {knob.cols} AS panel_row FROM input i JOIN k USING (branch) ORDER BY panel_row, panel_col, i.month

Rows

MonthBranchLoansIndexedPanel colPanel row
Nov 1, 2023Central19,65393.300
Dec 1, 2023Central17,41282.700
Jan 1, 2024Central21,89110400
Feb 1, 2024Central21,62210300
Mar 1, 2024Central21,38610200
Apr 1, 2024Central19,63793.200
May 1, 2024Central19,43392.300
Jun 1, 2024Central22,52510700
Jul 1, 2024Central24,81911800
Aug 1, 2024Central24,16611500
Sep 1, 2024Central19,87894.400
Oct 1, 2024Central20,35496.600
Nov 1, 2024Central18,90089.700
Dec 1, 2024Central18,66888.600
Jan 1, 2025Central12,6416000
Feb 1, 2025Central12,62559.900
Mar 1, 2025Central13,70565.100
Apr 1, 2025Central13,01161.800
May 1, 2025Central12,69160.200
Jun 1, 2025Central14,17167.300
Jul 1, 2025Central15,63774.200
Aug 1, 2025Central14,7517000
Sep 1, 2025Central12,15057.700
Oct 1, 2025Central12,55359.600
Nov 1, 2023Northgate8,94888.410
Dec 1, 2023Northgate8,36482.610
Jan 1, 2024Northgate10,0239910
Feb 1, 2024Northgate9,88597.710
Mar 1, 2024Northgate10,49710410
Apr 1, 2024Northgate9,77396.610
May 1, 2024Northgate9,4099310
Jun 1, 2024Northgate10,90910810
Jul 1, 2024Northgate12,52012410
Aug 1, 2024Northgate11,94611810
Sep 1, 2024Northgate9,62495.110
Oct 1, 2024Northgate9,56894.510
Nov 1, 2024Northgate10,09199.710
Dec 1, 2024Northgate8,83787.310
Jan 1, 2025Northgate11,29811210
Feb 1, 2025Northgate10,69710610
Mar 1, 2025Northgate10,88310810
Apr 1, 2025Northgate10,62410510
May 1, 2025Northgate9,82897.110
Jun 1, 2025Northgate11,43611310
Jul 1, 2025Northgate13,62813510
Aug 1, 2025Northgate12,1221210
Sep 1, 2025Northgate9,57294.610
Oct 1, 2025Northgate11,13011010
Nov 1, 2023Westbrook8,17091.920
Dec 1, 2023Westbrook7,36182.820

First 50 of 288 rows.

Step 4

Color each panel by its year-over-year change

Ten panels go one color, two go the other. The system total fell because one big branch moved into a small building, not because people stopped borrowing.

Tableau

// [YoY change]
{ FIXED [branch] : SUM(IF [month] >= #2024-11-01# THEN [loans] END) }
/ { FIXED [branch] : SUM(IF [month] < #2024-11-01# THEN [loans] END) } - 1
// Color: [YoY change] > 0

SQL

WITH t AS (
  SELECT
    branch,
    SUM(loans) AS total,
    AVG(loans) FILTER (WHERE month < DATE '2024-11-01') AS base,
    SUM(loans) FILTER (WHERE month >= DATE '2024-11-01')
      * 1.0
      / SUM(loans) FILTER (WHERE month < DATE '2024-11-01')
      - 1 AS yoy_change
  FROM input
  GROUP BY branch
), k AS (
  SELECT
    branch,
    base,
    yoy_change,
    ROW_NUMBER() OVER (ORDER BY total DESC, branch) - 1 AS idx
  FROM t
)
SELECT
  i.month,
  i.branch,
  i.loans,
  ROUND(100.0 * i.loans / k.base, 1) AS indexed,
  k.yoy_change,
  k.idx % 4 AS panel_col,
  k.idx // {knob.cols} AS panel_row FROM input i JOIN k USING (branch) ORDER BY panel_row, panel_col, i.month

Rows

MonthBranchLoansIndexedYoY changePanel colPanel row
Nov 1, 2023Central19,65393.3−32.2%00
Dec 1, 2023Central17,41282.7−32.2%00
Jan 1, 2024Central21,891104−32.2%00
Feb 1, 2024Central21,622103−32.2%00
Mar 1, 2024Central21,386102−32.2%00
Apr 1, 2024Central19,63793.2−32.2%00
May 1, 2024Central19,43392.3−32.2%00
Jun 1, 2024Central22,525107−32.2%00
Jul 1, 2024Central24,819118−32.2%00
Aug 1, 2024Central24,166115−32.2%00
Sep 1, 2024Central19,87894.4−32.2%00
Oct 1, 2024Central20,35496.6−32.2%00
Nov 1, 2024Central18,90089.7−32.2%00
Dec 1, 2024Central18,66888.6−32.2%00
Jan 1, 2025Central12,64160−32.2%00
Feb 1, 2025Central12,62559.9−32.2%00
Mar 1, 2025Central13,70565.1−32.2%00
Apr 1, 2025Central13,01161.8−32.2%00
May 1, 2025Central12,69160.2−32.2%00
Jun 1, 2025Central14,17167.3−32.2%00
Jul 1, 2025Central15,63774.2−32.2%00
Aug 1, 2025Central14,75170−32.2%00
Sep 1, 2025Central12,15057.7−32.2%00
Oct 1, 2025Central12,55359.6−32.2%00
Nov 1, 2023Northgate8,94888.4+7.1%10
Dec 1, 2023Northgate8,36482.6+7.1%10
Jan 1, 2024Northgate10,02399+7.1%10
Feb 1, 2024Northgate9,88597.7+7.1%10
Mar 1, 2024Northgate10,497104+7.1%10
Apr 1, 2024Northgate9,77396.6+7.1%10
May 1, 2024Northgate9,40993+7.1%10
Jun 1, 2024Northgate10,909108+7.1%10
Jul 1, 2024Northgate12,520124+7.1%10
Aug 1, 2024Northgate11,946118+7.1%10
Sep 1, 2024Northgate9,62495.1+7.1%10
Oct 1, 2024Northgate9,56894.5+7.1%10
Nov 1, 2024Northgate10,09199.7+7.1%10
Dec 1, 2024Northgate8,83787.3+7.1%10
Jan 1, 2025Northgate11,298112+7.1%10
Feb 1, 2025Northgate10,697106+7.1%10
Mar 1, 2025Northgate10,883108+7.1%10
Apr 1, 2025Northgate10,624105+7.1%10
May 1, 2025Northgate9,82897.1+7.1%10
Jun 1, 2025Northgate11,436113+7.1%10
Jul 1, 2025Northgate13,628135+7.1%10
Aug 1, 2025Northgate12,12212+7.1%10
Sep 1, 2025Northgate9,57294.6+7.1%10
Oct 1, 2025Northgate11,130110+7.1%10
Nov 1, 2023Westbrook8,17091.9+5.5%20
Dec 1, 2023Westbrook7,36182.8+5.5%20

First 50 of 288 rows.

Why did loans fall when most branches grew?

Because one branch did a quarter of the lending. Maple Falls lent 3.6% fewer books this year than last, and the one-line chart blames everyone. The panels don't: 10 of 12 branches grew, Eastgate by 48.2%. Central fell 32.2% after it moved into a temporary building, and that alone outweighs everyone else's growth.

Y axis
Columns in the grid

In Tableau

The shelves for the finished chart

Marks Line

  • Label[branch]
  • Detail[branch]
Columns[Panel col]MONTH([month])
Rows[Panel row]SUM([loans])

Compute Using: branch, sorted by SUM([loans]) descending

Green pills are continuous, blue are discrete, as in Tableau.

Variants

Other ways to draw it

Two dimensions, no table calc

When two fields already form a grid, put one on Columns and one on Rows and skip INDEX(). Here: year on Columns, branch on Rows.

Point at a panel to read it.
202320242025Brookside024.8KCedar Hill024.8KCentral024.8KEastgate024.8KFairview024.8KHillside024.8KLakeview024.8KMill Creek024.8KNorthgate024.8KOak Park024.8KSouthport024.8KWestbrook024.8K1 to 12

Two columns, for phones

Set the column count to 2 and the grid becomes six rows that scroll.

Point at a panel to read it.
024.8KCentralNorthgate024.8KWestbrookLakeview024.8KHillsideOak Park024.8KMill CreekSouthport024.8KCedar HillFairview024.8KBrooksideEastgateNov 1, 2023 to Oct 1, 2025

Common mistake

Compute Using runs along the months

With the default Compute Using (Table across), INDEX() counts months, not branches, so every branch puts each month in a different panel and the grid turns to confetti. Compute along branch only.

Wrong

Tableau

(INDEX() - 1) % 4
// Compute Using: Table (across)
Right

Tableau

(INDEX() - 1) % 4
// Compute Using: branch

Common mistake

Panels in alphabetical order

Sorted A to Z, two of the smallest branches, Brookside and Eastgate, share the top row with Central, and Northgate, the second biggest, sits in the last row. Sort the branches by the measure, descending, so the panels read from biggest to smallest and the eye starts where the volume is.

WrongStarts with Brookside, Cedar Hill, Central…
Point at the line to read it.
0510Branches in this order →

Tableau

Compute Using: branch, sorted alphabetically

SQL

SELECT branch, ROW_NUMBER() OVER (ORDER BY branch) - 1 AS idx
FROM input
GROUP BY branch
ORDER BY idx
RightStarts with Central, Northgate, Westbrook…
Point at the line to read it.
0510Branches in this order →

Tableau

Compute Using: branch, sorted by SUM([loans]) descending

SQL

SELECT
  branch,
  ROW_NUMBER() OVER (ORDER BY SUM(loans) DESC, branch) - 1 AS idx
FROM input
GROUP BY branch
ORDER BY idx

Common mistake

Reading the total as the trend

The system total fell 3.6%, and a single total line would have sent the story to every branch. Central alone fell 32.2%; Eastgate grew 48.2%. Split the total before you explain it.

WrongStarts with 2023-11-01, 2023-12-01, 2024-01-01…
Point at the line to read it.
050K100KMonths in this order →

SQL

SELECT month, SUM(loans) AS loans
FROM input
GROUP BY month
ORDER BY month
RightStarts with 2023-11-01, 2023-12-01, 2024-01-01…
Point at the line to read it.
050K100KMonths in this order →

SQL

SELECT month, branch, SUM(loans) AS loans
FROM input
GROUP BY month, branch
ORDER BY branch, month

Run it on your own data

Any table with a date, a category and a measure works: sales by store, tickets by team, signups by country. Drop your CSV, pick the three columns and a column count, and the grid lays itself out in your browser. Nothing is uploaded.

Drop a CSV anywhere on this page

Up to 10,000 rows. The sample, branch_loans.csv, shows the shape.

Questions

When should I switch from one line chart to small multiples?

When there are more than five or six lines, or when one big series flattens the rest. Readers can compare a handful of colors; past that they start matching legend entries instead of reading the data.

How many columns should the grid have?

Three to five on a laptop screen, two on a phone. More columns squeeze each time axis until the seasonal shape disappears.

Shared axis or independent axes?

Share the axis when size matters (which branch lends the most). Index each panel to its own baseline when shape matters (which branch is growing). Independent axes in Tableau make every panel look equally important, which is rarely true.

Why do my panels jumble when a category has a missing month?

INDEX() counts the branches present in each month. If a branch has no row for a month, every branch after it shifts one position in that month and lands in the wrong panel. Fill missing months with zero rows, or number branches with a FIXED rank instead.

Can I make this without table calculations?

Yes, with two discrete dimensions that already form a grid, such as region on Columns and segment on Rows. The INDEX() wrap is for one dimension with many values.

Sources