Christian Turner-Bridger

Moving stock between terminals, part 2: who is moving the stock

· 8 min read Rdata visualisationretail

Part 2 of 3. Part 1 found the story, and part 3 asks whether it pays.

Part 1 ended with a table and a slopegraph that both said the same thing: Terminal 5 now receives 56% of all the stock moved between Heathrow’s terminals, and what it received grew 38% in a year. The data is simulated, as it is throughout this series, but it’s built to behave like the real thing.

Summing by terminal made that easy to see, and it hid who was doing the sending. A terminal doesn’t move stock; a brand does, and only between its own shops. A brand with shops in Terminals 3 and 5 can send stock between them and nowhere else. So the data is really 23 small networks, one per brand, laid over the same four terminals. To see who is behind the flow into Terminal 5, I need every brand’s shops on the page at once.

Every shop on one line

An arc diagram puts every node on a single line, so the labels all run the same way and stay readable. I’d started with one (it was the chart part 1 took apart), and I rebuilt it to fix what was wrong.

An arc diagram. Brands’ shops sit as dots along a line, grouped under headings for Terminals 2 to 5, with brand names rotated along the bottom. Arcs above the line run to a higher-numbered terminal and flatter, fainter arcs below run back. Watch brands are blue and everything else grey. The thickest arc by far is blue, from Terminal 3 to Terminal 5.
Year 2. A dot is one brand's shop in one terminal, sized by the number of transfers it handled. An arc is one route, as wide as the value of stock moved along it. The chart is wider than the page: select it to see it full size.

Each dot is one brand’s shop in one terminal. Within each terminal the shops follow the same category order, watches first, so a brand sits in roughly the same place in every terminal and a reader can follow it along the line. Arcs above the line carry stock to a higher-numbered terminal and arcs below carry it back. The ones below are flatter and fainter, because the story is about what goes up into Terminal 5.

Four decisions shaped it.

  • Arc width is the value moved on the route. In my first version it was the number of transfers, which made cheap goods sent often look as important as watches. I changed it after asking what an arc was supposed to mean to someone reading it. The answer was money.
  • Dot size is the number of transfers the shop handled, sent and received together. That’s workload, and it’s worth seeing next to the money: a shop with a big dot and thin arcs is spending a lot of someone’s time on cheap items.
  • There is one accent colour. Watch brands are blue, and every other brand is grey. I tried blue for watches and orange for fashion, whose demand falls in the simulation, and two stories at once was too busy for one chart.
  • The terminal splits are plain. Each terminal has a labelled heading rule, with dashed dividers between them. I tried shaded bands, and then large terminal numbers set behind the arcs. The arcs covered the numbers.

There are no annotations on the chart itself. The title says what to see: the value of watches sent to Terminal 5 more than doubled, from £2.1m in year 1 to £4.6m. The thick blue arc from Terminal 3 is where it went.

Brand by brand

An arc diagram of the whole airport is good at showing where the weight sits. It’s poor at comparing one brand with another, or one year with the next. For that I switched to small multiples in Edward Tufte’s style: one row per brand, the same small chart repeated, serif type, no boxes, and notes in the margin.

A table of small charts. Rows are Watches A, B and C, then fashion, bags and leather goods, fragrance and beauty, jewellery and accessories, and electronics. For each row, a small arc diagram over Terminals 2 to 5 for year 1 and another for year 2, then a ‘Stock moved’ column with a hollow dot for year 1 and a solid dot for year 2. Watches A has the longest line, from £2.3m to £4.4m. Fashion’s solid dot sits to the left of its hollow one.
Each row is a brand, or a whole category. Dots in the arc panels are terminals, sized by the number of transfers handled, and a ring marks the shop holding the brand's main stock. In the last column, the hollow dot is year 1 and the solid dot year 2. The chart is wider than the page: select it to see it full size.

The three watch brands get a row each. Every other category is one row, because five rows of fashion brands were clutter that told the same story five times. Every arc panel shares one scale, so a thick arc means the same thing in every row and the panels can be compared at a glance.

The ring took a correction. A brand usually keeps its main stock in one shop, which sends far more than it receives, and I wanted to mark it. My first mark was a hollow dot. But a hollow dot already meant year 1 in the column on the right, so the chart used one symbol for two meanings. Now every terminal dot is solid, the main-stock shop gets a ring around it, and hollow only ever means year 1. It’s a small change, and a careful reader no longer has to stop and work out which hollow dot is which.

The numbers sit in the last column. Watches A moved £2.3m of stock in year 1 and £4.4m in year 2, up 91%. Watches C was up 77% and Watches B 36%. Fashion fell by 40%, and everything else barely moved.

Round trips

Not all of that movement is useful. Stock sent to Terminal 5 that doesn’t sell there tends to come back to the shop it came from. I call these round trips. In real data you’d flag them, so they can be separated from ordinary restocking, and the simulation flags them too.

The same small multiples, now with arcs for round trips drawn in red, and a ‘Sent back from T5’ column added on the right. Watches A has the most red, and its share sent back rises from 14% to 37%. Watches B has no red in year 2, and its share falls from 7% to 0%.
As above, with round trips in red: stock sent to Terminal 5 that came back to the shop that sent it. The last column is the share of what each row sent to Terminal 5 that returned. The chart is wider than the page: select it to see it full size.

Watches A now sends back 37% of what it ships to Terminal 5, up from 14% the year before. Watches B went the other way, from 7% to nothing at all. Watches C and the fashion brands send back about one in eight, and the rest a few per cent.

The two biggest watch brands are running Terminal 5 in opposite ways. Watches A stocks it ahead of demand: it sends stock in case someone asks for it, and brings home what didn’t sell. Watches B sends only what Terminal 5 has asked for, so nothing comes back. On stock discipline, Watches B wins easily.

I had to correct this chart’s subtitle once. An earlier version said every other brand sent back ‘a few per cent at most’. That was true of most rows but not of fashion, at 12%, or Watches C, at 13%. The subtitle now names them.

The bug the red arcs caught

The red arcs also caught a bug. A round trip is a transfer to Terminal 5 with its ends swapped, so in an early version I wrote:

transmute(year, retailer, from = to, to = from, value, category, returned = TRUE)

It looks like a swap, and it isn’t. transmute(), like mutate(), works through its arguments in order, and each one can see the columns made before it. By the time it gets to to = from, from has already been overwritten with the old to. Every return became a trip from Terminal 5 to Terminal 5, with no length, and an arc with no length draws nothing.

What gave it away was the chart: the red arcs I’d built the data to produce weren’t there, and two totals that should have agreed didn’t. The fix swaps the ends through temporary names:

transmute(year, retailer, back_from = to, back_to = from, value, category, returned = TRUE) |>
  rename(from = back_from, to = back_to)

It’s an argument for drawing the data early, and for building in a pattern you know should appear.

How it’s built

The rebuilt charts use ggplot2 only. Every arc is a list of points worked out by hand and drawn as a path. Each shop has a position x along the line, and an arc is half an ellipse between two of them:

k_up <- 0.55    # arc height per unit of half-span, above the line
k_down <- 0.22  # and below it

arcs <- routes |>
  pmap(\(id, x_from, x_to, up, ...) {
    theta <- seq(0, pi, length.out = 90)
    r <- abs(x_to - x_from) / 2
    side <- if_else(up, 1, -1)
    tibble(id, x = (x_from + x_to) / 2 - side * r * cos(theta),
           y = side * r * sin(theta) * if_else(up, k_up, k_down))
  }) |>
  list_rbind()

An arc’s height depends only on how far apart its two shops are, so height carries no data, and it’s free to be used for something else. Squashing the arcs below the line to less than half the height of those above is what makes the stock coming back read as quieter than the stock going up. The width comes from a column still called n, a leftover from when arcs counted transfers rather than adding up their value.

Drawing the arcs myself is also what made the small multiples possible: the same function draws every panel, at a shared scale, and patchwork lays them out alongside the measure columns. The full script has the rest.

Next: does it pay?

So far Watches A looks like the brand to worry about. It moves the most stock and wastes the most journeys, sending more than a third of what it ships to Terminal 5 straight back. Watches B looks like the model to follow.

Except that Watches A is the brand selling best. Part 3 looks at sales, and asks whether stock that keeps moving is part of the reason.

Inspiration

The arc diagrams in this series started with Gaston Sanchez’s arcdiagram package for R. My first version, the one part 1 took apart, was drawn with its arcplot() function, and the rebuilt charts keep its layout: every node on one line, with arcs above it and below it. I moved to ggplot2 when I needed the same diagram many times over, on a shared scale. The data-to-viz page on arc diagrams is a good short guide to when the form works.