6  The book’s data

Where do gapminder_2007, medals and winds come from, and how do you get them?

Data was about a table of your own. This chapter is about the shared tables that the book’s examples draw from, listed in full under The tables. They are published beside the book as CSV files, one per table, so you can run any example in this book yourself. Each file has a fixed address:

https://psychometrician.github.io/gog-book/data/<name>.csv

gapminder_2007.csv holds the 142 countries drawn in the chapter A first plot. medals.csv holds the medal counts drawn in Reading practice, and winds.csv holds the wind observations drawn in Polar.

Reading one takes a single call, gog_table(), which comes with the package in all four languages. Nothing else needs to be installed.

6.1 Reading a table

To run the first chapter’s plot yourself, you need its table in your session. One line fetches it. gog_table() reads any of these tables by name:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
gapminder_2007 <- gog_table("gapminder_2007")
data(gapminder_2007) + point + x(gdp) + y(life)
gapminder_2007 = gog_table("gapminder_2007")
data(gapminder_2007) + point + x(col.gdp) + y(col.life)
gapminder_2007 = gog_table("gapminder_2007")
data(gapminder_2007) + point + x(:gdp) + y(:life)
const gapminder_2007 = await gog_table("gapminder_2007");
plot(data(gapminder_2007), point, x(col.gdp), y(col.life));

Those four calls draw the same picture, because the table is the same file and the engine is the same program.

gog_table() also decides each column’s type. It is right for most columns, but not for a label written as a number, or for a date. A CSV is text, so numbers arrive as text and have to be converted. A column becomes numbers when every value in it is a number, and stays text otherwise. The text argument forces a column to stay text. Nine columns in these tables need that, and Labels that look like numbers, below, explains which and why.

Why does a graphics package carry a table reader at all? R reads a CSV in one call, and Python and Julia in a few. JavaScript has no CSV reader in its standard library, so the same work there takes dozens of lines. A CSV reader cannot simply split each line on commas. Six countries in gapminder_2007.csv have a comma in the name, "Congo, Dem. Rep." among them, so that split gives those rows seven fields, while the header has six. Nobody should have to paste dozens of lines of parsing before drawing, so each package carries gog_table() instead.

Each package fetches the file with what its language already has: R’s read.csv(), urllib in Python, Downloads in Julia and fetch in JavaScript.

Read a file with a CSV reader of your own, and the rest of this page still applies, because data() takes any table your session holds. You then do two things yourself. First, check each column’s type: your CSV reader decides it, so the nine columns named below need whatever that reader offers for keeping a column as text. Second, declare the category order, because a CSV never records one. You do this after gog_table() too. Three things a CSV cannot record covers both, and dates as well.

Reading a different table is the same call with a different name: gog_table("winds"), gog_table("medals"). A misspelled name is refused, and the refusal suggests the nearest published name, or points to the list under The tables.

6.2 Three things a CSV cannot record

A CSV records the values in a column, and never says what kind of column it is. Three kinds of information are therefore lost when the file is written. You restore them when you read the file back. Each one changes the plot, and this page explains each one.

6.2.1 A declared order

The eight compass points have a fixed order, and a plot of them is correct only when it keeps that order. The winds table records one wind observation per row:

winds: first 5 of 264 rows
direction bearing speed season
N 19.612260 10.1 Winter
N 357.049499 11.0 Winter
N 349.709737 13.8 Winter
N 2.602499 7.4 Winter
N 338.210715 3.7 Summer

The direction column of winds holds the eight compass points. Their order is N, NE, E, SE, S, SW, W, NW, clockwise from north. The Data chapter shows how a declared order sets the axis, the color assignment and the legend. A CSV cannot record that declaration. gog_table("winds") therefore returns direction as plain text, and the declaration is yours to make after reading.

Until you declare it, only the file’s own row order keeps the plot correct. The winds file lists its rows in compass order, so a plot of direction made without the declaration reads correctly. Sort the rows by speed, and the same plot changes. The compass points now follow the sorted rows, and that order is not the compass order. The plot still draws, and nothing on it shows that the compass is in the wrong order.

The example below reads the table, sorts it by speed so that the rows are no longer in compass order, and then declares the compass order:

winds <- gog_table("winds")
# `base::order`, because gog's `order()` masks it
winds <- winds[base::order(winds$speed), ]
compass <- c("N", "NE", "E", "SE", "S", "SW", "W", "NW")
winds$direction <- factor(winds$direction, levels = compass)

data(winds) + bar * count + x(direction)
winds = gog_table("winds")
by_speed = sorted(enumerate(winds["speed"]), key=lambda pair: pair[1])
winds = {name: [column[i] for i, _ in by_speed]
         for name, column in winds.items()}
compass = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
winds["direction"] = ordered(winds["direction"], compass)

data(winds) + bar * count + x(col.direction)
winds = gog_table("winds")
by_speed = sortperm(winds["speed"])
winds = Dict{String,Any}(name => column[by_speed] for (name, column) in winds)
compass = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
winds["direction"] = ordered(winds["direction"], compass)

data(winds) + bar * count + x(:direction)
const winds = await gog_table("winds");
const by_speed = winds.speed.map((_, i) => i)
  .sort((a, b) => winds.speed[a] - winds.speed[b]);
for (const name in winds) winds[name] = by_speed.map((i) => winds[name][i]);
const compass = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
winds.direction = ordered(winds.direction, compass);

plot(data(winds), layer(bar, count), x(col.direction));
N NE E SE S SW W NW 0 20 40 60 Count Direction

“Given the winds: bars derived by count, x is direction.”

The rows are in speed order, and the axis still reads N to NW. The declared order decides the axis, not the order of the rows. A declared order is kept when the space changes. The polar chapter draws this same count as a wind rose, and the change is one word. Add polar(), and the axis bends into a circle, with N at the top and the rest clockwise from it:

data(winds) + bar * count + x(direction) + polar()
data(winds) + bar * count + x(col.direction) + polar()
data(winds) + bar * count + x(:direction) + polar()
plot(data(winds), layer(bar, count), x(col.direction), polar())
N NE E SE S SW W NW 20 40 60 Count Direction

“Given the winds: bars derived by count, x is direction, in polar.”

The polar chapter explains that plot. The circle starts at the first category and runs clockwise through the rest, so the compass reads correctly in the flat plot and in the circle.

The sort exists only to undo the file’s compass order. Keep the declaration in your own code, and drop the sort. winds needs a second declaration, for season: the book’s plots show Summer before Winter, and the file lists Winter first. slab in quakes_fiji (shallowest band first) and weekday in six_weeks (Monday first) need a declared order too. So do age in quakes_2011 (“this week” first), and month and category in cyclones and storm_seasons (January first, and the weakest category first). age in census needs a declared order as well, youngest band first. So do month and cause in nightingale, April first and in the order of Nightingale’s legend. In titanic, class and survived need one too: 1st to Crew, and Yes before No. So does rank in gm_twelve, from 1st to 12th: in alphabetical order, 10th, 11th and 12th would come before 1st. So does moment in thermal_moments, the seconds of a flight from 0 to 338: in alphabetical order, 10 would come before 2. Three more need one because alphabetical order would put the wrong value first: genre in listening (Folk, Jazz, Techno, Ambient), step in cashflow (Opening to Closing) and tank in ripples (Shallow before Deep). The files of census, nightingale, titanic, gm_twelve, thermal_moments, listening, cashflow, ripples and storm_seasons already list their values in that order. The next section shows why census needs it.

6.2.2 Labels that look like numbers

The second thing a CSV cannot record is which columns are text. Nine columns in these tables hold text that gog_table() would read as numbers unless you name them. census.csv has an age column of 0, 5, 10, which are the names of age bands rather than quantities. sessions.csv has a session column of 01, 02, 03, and a number cannot keep a leading zero, so 01 comes back as 1. gm_eras.csv has an era column of 1957 and 2007. Those two values name the eras the rows compare; they do not measure anything. gm_twelve_ends.csv has a year column of 1952 and 2007, which name the two ends of a period in the same way, and ohio_turnout.csv has an election column of 2016, 2020 and 2024, which name three elections. eras.csv has the same era column as gm_eras.csv, and gm_four.csv has a period column that names four years from 1952 to 2007. thermal_moments.csv has a moment column of 0, 2, 4, which names each sample of a flight. storm_seasons.csv has a category column of 1 to 5, which names the categories of a storm’s strength. That is the whole reason gog_table() takes a text argument:

census <- gog_table("census", text = "age")
sessions <- gog_table("sessions", text = "session")
gm_eras <- gog_table("gm_eras", text = "era")
gm_twelve_ends <- gog_table("gm_twelve_ends", text = "year")
ohio_turnout <- gog_table("ohio_turnout", text = "election")
eras <- gog_table("eras", text = "era")
gm_four <- gog_table("gm_four", text = "period")
thermal_moments <- gog_table("thermal_moments", text = "moment")
storm_seasons <- gog_table("storm_seasons", text = "category")
census = gog_table("census", text=("age",))
sessions = gog_table("sessions", text=("session",))
gm_eras = gog_table("gm_eras", text=("era",))
gm_twelve_ends = gog_table("gm_twelve_ends", text=("year",))
ohio_turnout = gog_table("ohio_turnout", text=("election",))
eras = gog_table("eras", text=("era",))
gm_four = gog_table("gm_four", text=("period",))
thermal_moments = gog_table("thermal_moments", text=("moment",))
storm_seasons = gog_table("storm_seasons", text=("category",))
census = gog_table("census", text = ["age"])
sessions = gog_table("sessions", text = ["session"])
gm_eras = gog_table("gm_eras", text = ["era"])
gm_twelve_ends = gog_table("gm_twelve_ends", text = ["year"])
ohio_turnout = gog_table("ohio_turnout", text = ["election"])
eras = gog_table("eras", text = ["era"])
gm_four = gog_table("gm_four", text = ["period"])
thermal_moments = gog_table("thermal_moments", text = ["moment"])
storm_seasons = gog_table("storm_seasons", text = ["category"])
const census = await gog_table("census", ["age"]);
const sessions = await gog_table("sessions", ["session"]);
const gm_eras = await gog_table("gm_eras", ["era"]);
const gm_twelve_ends = await gog_table("gm_twelve_ends", ["year"]);
const ohio_turnout = await gog_table("ohio_turnout", ["election"]);
const eras = await gog_table("eras", ["era"]);
const gm_four = await gog_table("gm_four", ["period"]);
const thermal_moments = await gog_table("thermal_moments", ["moment"]);
const storm_seasons = await gog_table("storm_seasons", ["category"]);

Without the text argument, age becomes numbers. A point or a line then places the bands along a continuous axis from 0 to 85. The plot looks reasonable, so nothing tells you the axis is wrong. The population pyramid in Many groups is refused, because it sorts the bands with order(), and order() needs a categorical axis. When age is read as text, it still needs its declared order. Without the declaration, order() sorts the bands alphabetically, one character at a time. The band 5 is then drawn between 50 and 45.

Each call above names one column, and a text argument can name more than one. gm_eras holds the same values in two columns: year as a number, era as a label. The book reads only era as text, so year stays a number. To read the year as a label too, name both columns in one text argument:

gm_eras <- gog_table("gm_eras", text = c("year", "era"))
gm_eras = gog_table("gm_eras", text=("year", "era"))
gm_eras = gog_table("gm_eras", text = ["year", "era"])
const gm_eras = await gog_table("gm_eras", ["year", "era"]);

Each language writes more than one name in its own way. R puts them inside c(), Python inside parentheses, and Julia and JavaScript inside square brackets. Python no longer needs the comma that follows a single name. Julia and JavaScript only add the second name inside the brackets.

Only census, sessions, gm_eras, gm_twelve_ends, ohio_turnout, eras, gm_four, thermal_moments and storm_seasons need a text argument; no other shared table does.

6.2.3 A date

The third thing a CSV cannot record is which columns are dates. six_weeks, revenue, monitoring and quakes_2011 each have a column of dates, and gog_table() returns it as text, so the axis gets one slot per date instead of a calendar axis. After reading, convert the column to your language’s date or timestamp type: in R, as.Date() for six_weeks, revenue and quakes_2011, and as.POSIXct() for monitoring, which also records the hour. In JavaScript, write a time after each date, so that it is midnight in local time: six_weeks.day.map((d) => new Date(d + "T00:00")). For monitoring, which has a time already, replace the space with a T: new Date(t.replace(" ", "T")). The JavaScript chapter explains why. Scales shows the calendar axis you get.

6.3 The tables

The list below names every shared table this book reads, and how many rows and columns each one has. The preface introduces the nine families of table that most plots read. A family can cover more than one name below, so the list is longer than nine.

Tables outside those families are introduced on the page that first draws them. Each name goes in place of <name> in the address at the top of this page. The same name is what you give gog_table():

Table Rows Columns
actuals 5 2
asia_2007 5 6
band 1364 4
banded 1 4
botswana_arrow 2 2
botswana_label 1 3
budget 22 3
capitals 5 3
cashflow 6 3
census 36 3
channel_sales 6 3
cities 8 3
coarse 9 3
coefs 5 3
commutes 16 3
continents_2007 5 3
crop_response 72 4
cyclones 29669 5
day_cycle 25 2
decay 6 2
departments 5 2
depth_readings 4 2
drawdown 6 3
enclave 39 6
equator 1 1
eras 5812 7
far_north 4 2
filled 2906 6
fine 49 3
flight 2 2
forecast 3 2
gapminder_2007 142 6
gapminder_asia 60 6
gdp_threshold 1 1
gm_all 1704 6
gm_asia 396 6
gm_continents 1056 6
gm_eras 284 7
gm_europe_cdf 30 2
gm_europe 30 6
gm_four 568 7
gm_logged 142 7
gm_past60 877 6
gm_populous 10 6
gm_top 120 6
gm_twelve_2007 12 2
gm_twelve_ends 24 3
gm_twelve_gap 144 6
gm_twelve 144 4
healthy_band 2 3
income_note 1 3
inventory 16 3
iris_flowers 150 4
iris_measurements 600 5
life_bands 3 2
listening 104 3
lung_at_risk 12 3
lung_survival 208 3
maunga_whau 1364 3
medal_repeats 5 2
medals 5 4
meridians 5 1
milestones 2 3
mixed_signs 6 3
monitoring 37 2
news_counts 7 2
news_links 12 2
news_members 21 3
nightingale 72 4
nutrients 40 3
nyc_horizon 918 4
octaves 6 2
ohio_turnout 2943 6
peak 1322 3
petal_brackets 8 3
petal_stars 2 3
petals 100 4
policy_rates 6 2
population_spikes 120 6
prevailing_winds 2 1
quakes_2011 10698 5
quakes_fiji 1000 5
quarterly 19 2
receipts 4 2
recent 284 6
recessions 2 3
relief 1364 4
revenue 26 2
ripples 5400 5
routes 14 4
sales_box 1 4
score_band 21 4
scrambled 5 2
sessions 14 5
six_weeks 42 4
slump 1 4
span_early 1 2
span_late 1 2
span_middle 1 2
speed_target 1 1
spending 12 4
spiral 192 2
storm_seasons 98 4
target_band 1 2
target_edges 2 1
team_trend 12 3
tenure 150 2
thermal_marks 34 5
thermal_moments 340 6
thermals 340 5
three_continents 107 6
tide 8 2
titanic 32 5
trade_partners 22 3
two_readings 2 6
us_counties 23524 5
winds 264 4
world_borders 4150 5
world_median 2 2

Not every table here holds real data. The tables whose names begin with gapminder or gm hold figures from the Gapminder Foundation, which released them into the public domain. So do asia_2007, recent, three_continents and two_readings. continents_2007, healthy_band and world_median are computed from those figures. population_spikes, filled, eras and enclave join them to world_borders, the country outlines described below. iris_flowers, iris_measurements, petals, petal_stars, maunga_whau, peak, band, relief, nyc_horizon, quakes_fiji and titanic are reshaped from tables included with R, and carry R’s own license. nightingale is Florence Nightingale’s table of the British army’s deaths in the Crimean War, 1854 to 1856. It comes from the HistData package, under the same license as R’s tables. lung_survival and lung_at_risk come from R’s survival package, under that package’s own license. world_borders is Natural Earth’s public-domain outline of the countries. cyclones and storm_seasons are US government records: the National Oceanic and Atmospheric Administration’s storms of hurricane strength, 1980 to 2025, as tracks and as counts. So are quakes_2011, us_counties and ohio_turnout: the US Geological Survey’s earthquakes of magnitude 5 or more in 2011, the Census Bureau’s counties of the 48 states between Canada and Mexico, with their poverty rates in 2023, and the share of each Ohio county’s citizens who voted in 2016, 2020 and 2024, from the Election Assistance Commission and the Census Bureau. Everything else was written for this book, each table to show one thing; census is two plausible city profiles, not a real census. The terms for each are in data/LICENSE.md.

6.4 What comes next

A table holds the rows, and says nothing about how they look on the page. Choosing a mark shows how to pick the mark that draws them. There are fourteen, and your question and your columns decide which one fits.