25  Series

A table often carries one measure per column. Writing several of those columns where one goes reads them as several series of one layer: Plotje pivots them, invents a key column naming which measure a row holds and a value column holding the number, and maps the key column to :color. Because the measures are labelled, they can be placed against each other by a position adjustment.

The sections up to Examples teach the main ideas in order: what a series is, what the pivot makes of the columns it reads, where a series may be written, how the measures are placed against each other, how a series on each axis is read in pairs, and the limits a series has.

(ns plotje-book.series
  (:require
   [scicloj.plotje.api :as pj]
   [scicloj.kindly.v4.kind :as kind]
   [scicloj.metamorph.ml.rdatasets :as rdatasets]
   [tablecloth.api :as tc]
   [clojure.string :as str]))

The data

One row per quarter, one column per measure – the wide shape a series reads.

(def sales
  (tc/dataset {:quarter ["Q1" "Q2" "Q3" "Q4"]
               :revenue [120 150 140 190]
               :cost    [90 100 115 120]
               :tax     [18 24 21 30]
               :units   [12 15 14 19]}))
sales

_unnamed [4 5]:

:quarter :revenue :cost :tax :units
Q1 120 90 18 12
Q2 150 100 24 15
Q3 140 115 21 14
Q4 190 120 30 19

The same measures split by region and outlet, for the examples that divide the canvas as well as the band. A mark draws one shape per row, so a pose reading this one without a panel or a group per region would draw the two regions on top of each other.

(def sales-by-region
  (tc/dataset {:quarter ["Q1" "Q2" "Q3" "Q4" "Q1" "Q2" "Q3" "Q4"]
               :region  ["EU" "EU" "EU" "EU" "AS" "AS" "AS" "AS"]
               :outlet ["web" "web" "shop" "shop" "web" "web" "shop" "shop"]
               :revenue [120 150 140 190 90 120 160 210]
               :cost    [90 100 115 120 70 85 110 130]
               :tax     [18 24 21 30 14 19 26 34]
               :units   [12 15 14 19 9 12 16 21]}))
sales-by-region

_unnamed [8 7]:

:quarter :region :outlet :revenue :cost :tax :units
Q1 EU web 120 90 18 12
Q2 EU web 150 100 24 15
Q3 EU shop 140 115 21 14
Q4 EU shop 190 120 30 19
Q1 AS web 90 70 14 9
Q2 AS web 120 85 19 12
Q3 AS shop 160 110 26 16
Q4 AS shop 210 130 34 21

Several measures on one panel

A bar divides its band between the series, so each quarter’s band holds one bar per measure.

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax]))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180

The same plot from long data

A series is the wide-side spelling of something the long shape already writes. Pivoting the measures into a key column and a value column, then mapping the key column to :color, draws the same plot:

(-> sales
    (tc/pivot->longer #{:revenue :cost :tax}
                      {:target-columns :series :value-column-name :value})
    (pj/lay-bar :quarter :value {:color :series :position :dodge}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180

An adjustment divides a band between labelled competitors, and the pivot gives the measures their labels, so the adjustments below apply to a series.

Naming the key column

A series can be written out as a map rather than a bare vector. The reason to write it out is to name the key column the pivot invents, which is what titles the legend.

(-> sales
    (pj/lay-bar :quarter {:series [:revenue :cost :tax] :as :measure}
                {:position :dodge})
    (pj/options {:y-label "Euros"}))
EurosquartermeasurerevenuecosttaxQ1Q2Q3Q4020406080100120140160180

The keys a series takes

A series written out takes three keys: :series for the columns it reads, :as for the key column the pivot invents, and :scale for the scale its value column is read through. Any other key reports an error, so a misspelled :as is caught where it is written rather than leaving the legend titled series.

(try
  (-> sales
      (pj/lay-bar :quarter {:series [:revenue :cost] :label :measure}))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-bar was given a series with unexpected key(s): [:label]. A series names the columns it reads with :series, the key column the pivot invents with :as, and the scale its value column is read through with :scale."

Where a series is written

A series goes wherever a column goes. Written on the pose it reaches every layer below it, which is what scope does for every other mapping – the pivot reshapes the dataset the pose carries, and each layer then reads the columns it invented:

(-> sales
    (pj/pose :quarter [:revenue :cost])
    pj/lay-line
    pj/lay-point)
valuequarterseriesrevenuecostQ1Q2Q3Q490100110120130140150160170180190

One pivot per plot, though. A series written in a lay-* call on a pose that already reads one is two reshapes of one dataset, and the second would name columns the first consumed, so it is reported:

(try
  (-> sales
      (pj/pose {:x :quarter :y [:revenue :cost]})
      (pj/lay-line :quarter [:revenue :tax]))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-line was given a series on :y, and the pose it is added to already reads one on :y. A series pivots the data into one value column, and two pivots have no shared shape. Read one of them: drop the series from the pj/lay-line call to use the pose's, or build the layer on a pose of its own."

Within the lay-* call a series goes wherever a column goes – as an argument, or written under :x or :y in a map. The two spellings draw the same plot:

(-> sales
    (pj/lay-bar {:x :quarter :y [:revenue :cost :tax]}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180

A series goes where a column goes on an axis, so it can be written on :x as readily as on :y. On :x the measures run along the horizontal axis and the quarters go up the vertical one:

(-> sales
    (pj/lay-point {:series [:revenue :cost :tax]} :quarter))
quartervalueseriesrevenuecosttax20406080100120140160180Q1Q2Q3Q4

The bare vector means on :x what it means on :y:

(-> sales
    (pj/lay-point [:revenue :cost :tax] :quarter))
quartervalueseriesrevenuecosttax20406080100120140160180Q1Q2Q3Q4

For bars, write the series on :y and turn the plot with pj/coord as the Sideways example below does. A bar drawn from a category on :y is supported less fully – see Known Limitations.

Placing the measures against each other

:identity draws every series at the same place, so the bars overlap. It is the one that differs from the bar’s own default. Three partly transparent bars drawn from zero make a third tone wherever they cross, which reads as a stack – the axis is what tells the two apart, since here it reaches the largest measure and under :stack below it reaches their total:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :identity}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180

:dodge gives each series a slot of the band, which is what a bar does without being asked:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180

:stack piles the series, so the band’s height is their total:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4050100150200250300350

:fill piles them and normalizes, so each band reads as shares between zero and one:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :fill}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q40.00.10.20.30.40.50.60.70.80.91.0

The columns the pivot invents

The pivot drops the columns it reads and puts two columns in their place: :value holds the number, and the key column holds which measure a row came from. The key column is named :series unless :as names it otherwise.

Where the data already carries a column of one of those names, the pivot reports an error instead of overwriting the column:

(try
  (-> {:quarter ["Q1" "Q2"]
       :revenue [120 150]
       :cost    [90 100]
       :series  ["a" "b"]}
      (pj/lay-bar :quarter [:revenue :cost]))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-bar cannot pivot these columns: the pivot names its key column :series, and the data already has a :series column. Name the key column something else -- {:series [:revenue :cost] :as :measure} -- or rename the column in the data."

:as gives the key column a name the data does not use. A data column named :value has to be renamed in the data instead, since :as does not reach the value column.

(-> {:quarter ["Q1" "Q2"]
     :revenue [120 150]
     :cost    [90 100]
     :series  ["a" "b"]}
    (pj/lay-bar :quarter {:series [:revenue :cost] :as :measure}))
valuequartermeasurerevenuecostQ1Q2020406080100120140

A series on each axis

Two measures recorded at different times each have a time column of their own. Below, :reading-a was taken at the times in :time-a, and :reading-b at the times in :time-b. To draw both on one panel, write a series on :x and another on :y. They are read in pairs: the first :x column with the first :y column, the second with the second, and so on. Each pair is one series of the layer, labelled with both column names, and the pivot invents two value columns, :x-value and :y-value, one for each axis.

(-> {:time-a    [0 1 2 3]
     :time-b    [0.5 1.5 2.5 3.5]
     :reading-a [2 3 5 4]
     :reading-b [1 2 2 3]}
    (pj/lay-line [:time-a :time-b] [:reading-a :reading-b]))
y valuex valueseriestime a / reading atime b / reading b0.00.51.01.52.02.53.03.512345

The axis titles name the two invented columns. Titles of your own are written with :x-label and :y-label:

(-> {:time-a    [0 1 2 3]
     :time-b    [0.5 1.5 2.5 3.5]
     :reading-a [2 3 5 4]
     :reading-b [1 2 2 3]}
    (pj/lay-line [:time-a :time-b] [:reading-a :reading-b])
    (pj/options {:x-label "Time (s)" :y-label "Reading"}))
ReadingTime (s)seriestime a / reading atime b / reading b0.00.51.01.52.02.53.03.512345

The same pairs can be written in the pose’s mapping, as {:x [...] :y [...]}, and there :as names the key column – here :sensor, which titles the legend:

(-> {:time-a    [0 1 2 3]
     :time-b    [0.5 1.5 2.5 3.5]
     :reading-a [2 3 5 4]
     :reading-b [1 2 2 3]}
    (pj/pose {:x [:time-a :time-b]
              :y {:series [:reading-a :reading-b] :as :sensor}})
    pj/lay-line)
y valuex valuesensortime a / reading atime b / reading b0.00.51.01.52.02.53.03.512345

The two series take as many columns each, since each :x column needs a :y column to be paired with:

(try
  (-> {:time-a [0 1] :time-b [2 3] :reading-a [1 2] :reading-b [3 4]}
      (pj/lay-line [:time-a :time-b] [:reading-a :reading-b :time-a]))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-line was given 2 columns on :x, [:time-a :time-b], and 3 on :y, [:reading-a :reading-b :time-a]. Two series are read in pairs, the first :x column with the first :y column, so they take as many columns each."

Every :x column against every :y column is the product of the two lists. It is useful where the columns share units, so that each pair can be compared on one set of axes. pj/cross returns the product as a vector of pairs, (apply map vector) turns the pairs into the list of :x columns and the list of :y columns, and apply passes the two lists to the lay-* call. Here the two lengths of an iris flower are drawn against its two widths, all in centimetres:

(->> (pj/cross [:sepal-length :petal-length] [:sepal-width :petal-width])
     (apply map vector)
     (apply pj/lay-point (rdatasets/datasets-iris)))
y valuex valueseriessepal length / sepal widthsepal length / petal widthpetal length / sepal widthpetal length / petal width24680.00.51.01.52.02.53.03.54.04.5

The same pairs given to pj/pose draw a panel for each pair instead, as the scatter plot matrix in Relationships does.

One series per pose

The pivot leaves its two columns on the pose, so a second series on the same pose meets them:

(try
  (-> sales
      (pj/lay-bar :quarter [:revenue :cost])
      (pj/lay-line :quarter [:tax :units]))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-line cannot pivot these columns: the pivot names its value column :value, and the data already has a :value column. Rename that column in the data. Also, the pivot names its key column :series, which the data has as well -- name the key column something else with :as."

Both columns clash here, and neither is a column of the original data: the first series put them on the pose. So neither :as nor a rename in the data separates the two series. Give each series a pose of its own and arrange the poses:

(pj/arrange
 [(-> sales
      (pj/lay-bar :quarter [:revenue :cost] {:position :dodge}))
  (-> sales
      (pj/lay-line :quarter [:tax :units]))])
valuequarterQ1Q2Q3Q4020406080100120140160180valuequarterQ1Q2Q3Q412141618202224262830seriesrevenuecosttaxunits

Examples

Each example below combines a series with one more thing – another mark, a scale, the key column in another role, panels, another layer, a composite. The last group lists what a series refuses, and the vectors that are not series at all.

Other marks

Lines and points

(-> sales
    (pj/lay-line :quarter [:revenue :cost :tax]))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180
(-> sales
    (pj/lay-point :quarter [:revenue :cost :tax]))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

Areas and steps

(-> sales
    (pj/lay-area :quarter [:revenue :cost :tax] {:position :stack}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4050100150200250300350
(-> sales
    (pj/lay-step :quarter [:revenue :cost]))
valuequarterseriesrevenuecostQ1Q2Q3Q490100110120130140150160170180190

An area normalizes under :fill the way a bar does, so each quarter reads as shares between zero and one:

(-> sales
    (pj/lay-area :quarter [:revenue :cost :tax] {:position :fill}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q40.00.10.20.30.40.50.60.70.80.91.0

Marks that take a series

Any mark drawing from a value column takes a series. The five above are bars, lines, points, areas and steps; a lollipop is another:

(-> sales
    (pj/lay-lollipop :quarter [:revenue :cost]))
valuequarterseriesrevenuecostQ1Q2Q3Q4020406080100120140160180

The pivot does not change what a mark’s stat requires. A smooth needs a numeric x column and reports when it is drawn, since the quarter column is categorical whether the measures are pivoted or not:

(try
  (pj/plot (-> sales (pj/lay-smooth :quarter [:revenue :cost])))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"Stat :loess requires a numeric column for :x, but :quarter is categorical. A stat that bins or smooths reads numbers; a column of text names groups instead. Dates written as text stay categorical until they are parsed with tc/convert-types."

A stat over a series

A stat runs after the pivot, so it reads one series at a time. Each measure of each quarter has a row per region in sales-by-region, and pj/lay-summary draws the mean of those rows with a standard-error bar around it – one mean per quarter per measure:

(-> sales-by-region
    (pj/lay-summary :quarter [:revenue :cost]))
valuequarterseriesrevenuecostQ1Q2Q3Q480100120140160180200

A series along a date axis

The pivot reshapes the measures and leaves the axis column as it was, so a date axis stays a date axis. Two columns of the economics dataset hold counts of people in thousands – the population and the number of them unemployed – which a log scale brings onto one axis:

(-> (rdatasets/ggplot2-economics)
    (pj/lay-line :date {:series [:pop :unemploy] :scale {:type :log}}))
valuedateseriespopunemploy197019801990200020103000500010000200003000050000100000200000300000

A stat needing a numeric axis column runs here where it reported on the quarters above, since a date is not a category – one smooth per measure:

(-> (rdatasets/ggplot2-economics)
    (pj/lay-smooth :date {:series [:pop :unemploy] :scale {:type :log}}))
valuedateseriespopunemploy19701980199020002010500010000200003000050000100000200000300000

Scales and colours

A scale on the value axis

(-> sales
    (pj/lay-point :quarter {:series [:revenue :cost :tax]
                            :scale {:type :log}}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4203050100200

pj/scale on the axis the series is written on says the same thing, so the scale can go in either place:

(-> sales
    (pj/lay-point :quarter [:revenue :cost :tax])
    (pj/scale :y {:type :log}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4203050100200

A domain on the value axis

(-> sales
    (pj/lay-point :quarter [:revenue :cost])
    (pj/scale :y {:domain [0 250]}))
valuequarterseriesrevenuecostQ1Q2Q3Q4020406080100120140160180200220240

Choosing the colours

(-> sales
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/scale :color {:values ["#377eb8" "#e6550d" "#4daf4a"]}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

A scale set on the pose carries down to every panel, so the same three colours are used in each:

(-> sales-by-region
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/scale :color {:values ["#377eb8" "#e6550d" "#4daf4a"]})
    (pj/facet :region))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180200Q1Q2Q3Q4EUAS

The order of the series

The order the measures are drawn in – the legend’s order, the order of the slots in a dodged band, and the order a stack piles them – follows the dataset’s columns rather than the order they are written in. Writing the same three measures in another order draws the same plot:

(-> sales
    (pj/lay-bar :quarter [:tax :revenue :cost] {:position :stack}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4050100150200250300350

A :domain on the colour scale sets the order, since the key column is the column the colour reads:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack})
    (pj/scale :color {:domain [:tax :revenue :cost]}))
valuequarterseriestaxrevenuecostQ1Q2Q3Q4050100150200250300350

Column names and missing values

Column names that look like numbers

A wide table often names its measures by year. tc/pivot->longer reads a column name that looks like a number as one, so the key column comes out holding numbers rather than text – and the pivot says that it holds column names, by writing :color-type :categorical beside the colour it maps them to. Without it, the colour would read the years as a quantity and draw one gradient over the lot, with every measure in one colour and no legend entry naming either year.

(-> {"country" ["a" "b" "c"]
     "2019"    [10 20 30]
     "2020"    [12 25 28]}
    (pj/lay-line "country" ["2019" "2020"]))
valuecountryseries20192020abc1012141618202224262830

The written :color-type is visible on the layer, and a writer who wants the gradient after all asks for it with the same key:

(-> {"country" ["a" "b" "c"]
     "2019"    [10 20 30]
     "2020"    [12 25 28]}
    (pj/lay-line "country" ["2019" "2020"])
    :layers
    first
    :mapping)
{:color :series, :color-type :categorical}

A series whose columns are named in words needs none of this, and carries no :color-type. The dataset the pose holds is what tc/pivot->longer returns either way : the key column keeps the type Tablecloth gave it, and :color-type changes only how the colour reads the key column.

A measure with no value

A cell left empty in the wide table has no value to pivot, so that observation is not drawn. Two quarters and two measures are four observations, and three are drawn:

(-> {:quarter ["Q1" "Q2"]
     :revenue [120 nil]
     :cost    [90 100]}
    (pj/lay-point :quarter [:revenue :cost]))
Notestdout
Warning: Removed 1 rows with a missing value among the columns read as series (:revenue, :cost).
valuequarterseriesrevenuecostQ1Q29095100105110115120

Plotje says how many went, naming the columns read as series:

(with-out-str
  (pj/plan (-> {:quarter ["Q1" "Q2"]
                :revenue [120 nil]
                :cost    [90 100]}
               (pj/lay-point :quarter [:revenue :cost]))))
"Warning: Removed 1 rows with a missing value among the columns read as series (:revenue, :cost).\n"

The pivot is tc/pivot->longer with Tablecloth’s own defaults, so the dataset the pose carries is what Tablecloth gives for the same columns, and a row with no value is not in it. Writing the data long by hand reaches the same plot, and Plotje reports the same observation at a later stage and in its own words:

(with-out-str
  (pj/plan (-> {:quarter ["Q1" "Q1" "Q2" "Q2"]
                :measure ["revenue" "cost" "revenue" "cost"]
                :value   [120 90 nil 100]}
               (pj/lay-point :quarter :value {:color :measure}))))
"Warning: Removed 1 rows with non-finite values: 1 missing (nil/NaN).\n"

The key column elsewhere

Panels from the key column

The key column is an ordinary column of the pivoted dataset, so pj/facet reads it as it reads any other: one panel per measure. The panels share one value axis, which is what lets the measures be read against each other.

(-> sales
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/facet :series))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180Q1Q2Q3Q4Q1Q2Q3Q4revenuecosttax

With :as, the facet names the key column by its new name:

(-> sales
    (pj/lay-line :quarter {:series [:revenue :cost :tax] :as :measure})
    (pj/facet :measure))
valuequartermeasurerevenuecosttaxQ1Q2Q3Q420406080100120140160180Q1Q2Q3Q4Q1Q2Q3Q4revenuecosttax

The key column on the axis

Naming the key column on an axis draws the measures against each other rather than along the quarters. The pivot runs before the axis is read, so the column is there by the time the mark is drawn: one box per measure, from every row that measure has.

(-> sales-by-region
    (pj/lay-boxplot :series [:revenue :cost :tax]))
valueseriesseriesrevenuecosttaxrevenuecosttax20406080100120140160180200

A violin over the same three measures:

(-> sales-by-region
    (pj/lay-violin :series [:revenue :cost :tax]))
valueseriesseriesrevenuecosttaxrevenuecosttax20406080100120140160180200

Another aesthetic from the key column

The layer that asks for a series may map the key column again. Shape beside colour separates the measures twice over, which survives printing in one colour:

(-> sales
    (pj/lay-point :quarter [:revenue :cost :tax] {:shape :series}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

:tooltip reads the key column as readily, so in the HTML rendering of this page hovering a point says which measure it holds:

(-> sales
    (pj/lay-point :quarter [:revenue :cost :tax] {:tooltip :series}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

:size and :alpha draw a magnitude, and a measure’s name is not one, so both report:

(try
  (pj/plot (-> sales
               (pj/lay-point :quarter [:revenue :cost :tax]
                             {:size :series})))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"Aesthetic :size needs a numeric column, but :series holds categories. :size draws a magnitude, so there is nothing for a category to be. To tell categories apart, map the column to :color (a palette) or to :group (one drawn group each)."

Another column beside the series

Series and a grouping column

A grouping column keeps its own marks separate, so each measure is drawn once per region: two measures across two regions give four lines, coloured by measure.

(-> sales-by-region
    (pj/lay-line :quarter [:revenue :cost] {:group :region}))
valuequarterseriesrevenuecostQ1Q2Q3Q480100120140160180200

On a bar, a dodge gives every combination of the measure and the grouping column a slot of its own, so each quarter draws four bars – but the grouping column draws no legend, so the two bars of one measure share its colour and nothing says which region each is:

(-> sales-by-region
    (pj/lay-bar :quarter [:revenue :cost] {:group :region}))
valuequarterseriesrevenuecostQ1Q2Q3Q4020406080100120140160180200220

Facet on the column instead, as the panel sections below do, so each region’s bars are labelled by a strip.

Colouring by another column

The pivot maps the key column to :color, which is what gives each measure a colour of its own. Where the layer maps :color itself, the pivot maps the key column to :group instead. The measures stay separate marks, and the colour shows the region: still four lines, two of them red and two blue.

(-> sales-by-region
    (pj/lay-line :quarter [:revenue :cost] {:color :region}))
valuequarterregionEUASQ1Q2Q3Q480100120140160180200

The same layer in panels, one per outlet:

(-> sales-by-region
    (pj/lay-line :quarter [:revenue :cost] {:color :region})
    (pj/facet :outlet))
valuequarterregionEUASQ1Q2Q3Q480100120140160180200Q1Q2Q3Q4webshop

A :color written on the pose does not reach the marks. The pivot writes :color on the layer that asks for the series, and a layer’s own mapping wins over the pose’s, so the region separates nothing and each line runs through both regions – the same plot as one with no :color on the pose at all. Write :color on the layer, as the two examples above do:

(-> sales-by-region
    (pj/pose {:color :region})
    (pj/lay-line :quarter [:revenue :cost]))
valuequarterseriesrevenuecostQ1Q2Q3Q480100120140160180200

Panels and turned plots

Sideways

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
    (pj/coord :flip))
quartervalueseriesrevenuecosttax020406080100120140160180Q1Q2Q3Q4

Series in panels

(-> sales-by-region
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
    (pj/facet :region))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180200220Q1Q2Q3Q4EUAS
(-> sales-by-region
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack})
    (pj/facet-grid :region :outlet))
valuequarterseriesrevenuecosttax0100200300Q1Q2Q3Q40100200300Q1Q2Q3Q4EUASwebshop

Series in each panel, drawn as lines

Each region has all four quarters, so each panel draws a line per measure across the whole axis. Faceting on region and outlet together would leave two quarters per panel, so each line would join only two points.

(-> sales-by-region
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/facet :region)
    (pj/options {:title "Measures by region"}))
Measures by regionvaluequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180200Q1Q2Q3Q4EUAS

Dodged series, drawn sideways, in panels

(-> sales-by-region
    (pj/lay-bar :quarter [:revenue :cost] {:position :dodge})
    (pj/coord :flip)
    (pj/facet :region))
quartervalueseriesrevenuecost0100200Q1Q2Q3Q40100200EUAS

Shares, drawn sideways

Normalizing and turning the plot compose, so each quarter becomes a bar of shares running left to right:

(-> sales
    (pj/lay-bar :quarter [:revenue :cost :tax] {:position :fill})
    (pj/coord :flip))
quartervalueseriesrevenuecosttax0.00.20.40.60.81.0Q1Q2Q3Q4

A named series, scaled and in panels

The written-out form composes with the rest. Here the key column is named, the value column is read through a log scale, the value axis is relabelled, and the canvas is divided by region:

(-> sales-by-region
    (pj/lay-point :quarter {:series [:revenue :cost :tax]
                            :as :measure
                            :scale {:type :log}})
    (pj/facet :region)
    (pj/options {:y-label "Euros"}))
EurosquartermeasurerevenuecosttaxQ1Q2Q3Q4203050100200Q1Q2Q3Q4EUAS

Other layers beside a series

Two marks over one series

The pivot happens on the pose, so a second layer reads the columns it invented – :value for the number and :series for the key – and draws the same series in another mark. The key column is mapped to :color on the layer that asked for the series, so a second layer names it to be coloured the same way.

(-> sales
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/lay-point :quarter :value {:color :series}))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

A layer naming no columns draws at the same place in the default colour. The key column is mapped on the layer that asked for the series, and a layer added afterwards reads the pose rather than that layer, so the twelve points come out one colour:

(-> sales
    (pj/lay-line :quarter [:revenue :cost :tax])
    (pj/lay-point))
valuequarterseriesrevenuecosttaxQ1Q2Q3Q420406080100120140160180

Series beside a layer that keeps its own panel

A layer naming a column the pivot did not consume takes a panel of its own, and pj/overlay puts it on the series panel instead.

(-> sales
    (pj/lay-bar :quarter [:revenue :cost] {:position :stack})
    (pj/lay-line :quarter :units))
quarterseriesrevenuecost0100200300Q1Q2Q3Q412141618valueunits
(-> sales
    (pj/lay-bar :quarter [:revenue :cost] {:position :stack})
    (pj/lay-line :quarter :units)
    pj/overlay)
valuequarterseriesrevenuecostQ1Q2Q3Q4050100150200250300

Drawing the first of those two says what it did, and names pj/overlay as the way to one panel. The note does not offer reading the columns as one series here, as it does beside two plain columns: the pose already reads a series, and a second one would write over the columns the first invented.

(with-out-str
  (pj/plot (-> sales
               (pj/lay-bar :quarter [:revenue :cost])
               (pj/lay-line :quarter :units))))
"Note: a layer names :units where this pose draws :value on :y, so the layer was given a panel of its own. To draw both on one panel: pj/overlay for the axis the pose already names.\n"

A rule across every panel of a series

A layer naming no columns of its own is drawn on every panel.

(-> sales-by-region
    (pj/lay-bar :quarter [:revenue :cost] {:position :dodge})
    (pj/facet :region)
    (pj/lay-rule-h {:y-intercept 120}))
valuequarterseriesrevenuecostQ1Q2Q3Q4020406080100120140160180200220Q1Q2Q3Q4EUAS

Composites

Nested poses

pj/arrange places poses side by side. Each input is a pose, so a series pose goes in a cell like any other.

(pj/arrange
 [(-> sales
      (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge}))
  (-> sales
      (pj/lay-line :quarter :units))])
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180unitsquarterQ1Q2Q3Q41213141516171819

A cell may be a composite itself, so an arranged pose can be arranged again and the cell draws its own grid.

(pj/arrange
 [(pj/arrange
   [(-> sales
        (pj/lay-bar :quarter [:revenue :cost] {:position :dodge}))
    (-> sales
        (pj/lay-bar :quarter [:revenue :cost] {:position :stack}))]
   {:cols 1})
  (-> sales
      (pj/lay-line :quarter [:revenue :cost :tax]))])
valuequarterQ1Q2Q3Q4050100150valuequarterQ1Q2Q3Q40100200300valuequarterQ1Q2Q3Q420406080100120140160180seriesrevenuecosttax

Sub-plots of faceted panels of series

(pj/arrange
 [(-> sales-by-region
      (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
      (pj/facet :region))
  (-> sales-by-region
      (pj/lay-line :quarter :units)
      (pj/facet :region))])
valuequarterseriesrevenuecosttaxQ1Q2Q3Q4020406080100120140160180200220Q1Q2Q3Q4EUASunitsquarterQ1Q2Q3Q4101214161820Q1Q2Q3Q4EUAS

Sub-plots read against one axis

(pj/arrange
 (vec (for [r ["EU" "AS"]]
        (-> sales-by-region
            (tc/select-rows (fn [row] (= r (:region row))))
            (pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
            (pj/options {:title r}))))
 {:share-scales #{:y}
  :align-panels true})
EUvaluequarterQ1Q2Q3Q4020406080100120140160180200220ASvaluequarterQ1Q2Q3Q4020406080100120140160180200220seriesrevenuecosttax

A written-out composite of series

(pj/pose
 {:layout {:direction :vertical :weights [2 1]}
  :poses [(-> sales
              (pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack}))
          (-> sales
              (pj/lay-line :quarter [:revenue :cost]))]})
valuequarterQ1Q2Q3Q40100200300valuequarterQ1Q2Q3Q4100150seriesrevenuecosttax

Three levels of nesting

(pj/arrange
 [(pj/arrange
   [(-> sales
        (pj/lay-bar :quarter [:revenue :cost] {:position :dodge}))
    (-> sales
        (pj/lay-line :quarter [:revenue :cost]))]
   {:cols 1})
  (pj/arrange
   [(-> sales
        (pj/lay-area :quarter [:revenue :cost] {:position :stack}))
    (-> sales
        (pj/lay-point :quarter :units))]
   {:cols 1})]
 {:title "Measures four ways"})
valuequarterseriesrevenuecostQ1Q2Q3Q4050100150valuequarterseriesrevenuecostQ1Q2Q3Q4100150valuequarterseriesrevenuecostQ1Q2Q3Q40100200300unitsquarterQ1Q2Q3Q412141618Measures four ways

A titled grid of series

(pj/arrange
 (vec (for [pos [:identity :dodge :stack :fill]]
        (-> sales
            (pj/lay-bar :quarter [:revenue :cost :tax] {:position pos})
            (pj/options {:title (name pos)}))))
 {:cols 2})
identityvaluequarterQ1Q2Q3Q40100dodgevaluequarterQ1Q2Q3Q40100stackvaluequarterQ1Q2Q3Q40200fillvaluequarterQ1Q2Q3Q40.00.51.0seriesrevenuecosttax

A grid of panels of series

One combination has no spelling. A grid of panels built from pairs of columns is a composite whose cells share one dataset, and a series pivots the dataset of the pose it is added to, so a series added to the whole grid is reported:

(try
  (-> sales
      (pj/pose [[:quarter :revenue] [:quarter :cost]])
      (pj/lay-point :quarter [:revenue :cost]))
  (catch clojure.lang.ExceptionInfo e
    (ex-message e)))
"pj/lay-point was given several columns to read as series, on a composite pose. The pivot reshapes the dataset they are read from, and a composite's cells share one, so every cell would be reshaped. Add the layer to a cell before arranging the cells."

Both routes to the same picture are on the long side. pj/facet reaches it where the split comes from a column, and pj/arrange of series poses where it does not – which is what the sub-plot sections above do.

Refusals and look-alikes

What a series refuses

The sections above work through the refusals a writer meets most often. The rest of the set, with the message each one gives:

(kind/table
 {:column-names ["written" "what it reports"]
  :row-vectors
  (mapv (fn [[written f]]
          [(kind/code written)
           (try (do (pj/plot (f)) "draws")
                (catch Throwable e (first (str/split (ex-message e) #"\. "))))])
        [["{:color [:revenue :cost :tax]}"
          #(-> sales (pj/lay-point :quarter :revenue {:color [:revenue :cost :tax]}))]
         ["{:series [:revenue]}"
          #(-> sales (pj/lay-bar :quarter {:series [:revenue]}))]
         ["[:revenue 42]"
          #(-> sales (pj/lay-bar :quarter {:series [:revenue 42]}))]
         ["[:revenue :nope]"
          #(-> sales (pj/lay-bar :quarter [:revenue :nope]))]
         ["a mapping already naming a consumed column"
          #(-> sales (pj/pose :quarter :revenue)
               (pj/lay-bar :quarter [:revenue :cost :tax]))]
         ["a series on a composite pose"
          #(-> (pj/arrange [(pj/lay-point sales :quarter :revenue)])
               (pj/lay-bar :quarter [:revenue :cost :tax]))]
         ["a pose carrying no data"
          #(-> (pj/pose) (pj/lay-bar :quarter [:revenue :cost]))]
         ["{:y-min [:cost :tax]}"
          #(-> sales (pj/lay-errorbar :quarter :revenue {:y-min [:cost :tax]}))]
         ["[:revenue :outlet]"
          #(-> sales-by-region (pj/lay-bar :quarter [:revenue :outlet]))]])})
written what it reports
{:color [:revenue :cost :tax]}
lay-point :color was given several columns, [:revenue :cost :tax]
{:series [:revenue]}
pj/lay-bar was given 1 column in a slot where several are read as several series: [:revenue]
[:revenue 42]
pj/lay-bar was given [:revenue 42], and 42 is not a column reference -- a keyword or a string naming a column of the data.
[:revenue :nope]
pj/lay-bar was given several columns to read as series, and the data does not have [:nope]
a mapping already naming a consumed column
pj/lay-bar reads [:revenue :cost :tax] as series, and this pose's mapping names :revenue on :y
a series on a composite pose
pj/lay-bar was given several columns to read as series, on a composite pose
a pose carrying no data
pj/lay-bar was given several columns to read as series and has no data to pivot
{:y-min [:cost :tax]}
lay-errorbar :y-min was given several columns, [:cost :tax]
[:revenue :outlet]
Column :value (from :y) has mixed value types: ["Long" "String"]

What is not a series

A vector of [x y] pairs is the multi-panel form, one panel per pair:

(-> sales
    (pj/pose [[:quarter :revenue] [:quarter :cost]])
    (pj/lay-point))
quarterQ1Q2Q3Q4120140160180quarterQ1Q2Q3Q490100110120revenuecost

A vector with no other column beside it is the multi-panel form as well: a lay-* call reads it as one column per panel, and each panel draws its own mark.

(-> sales
    (pj/lay-histogram [:revenue :cost]))
12014016018001290100110120012revenuecost

A vector on :group is one compound key rather than several series. Region combined with outlet gives four keys, so the line is drawn in four pieces:

(-> sales-by-region
    (pj/lay-line :quarter :revenue {:group [:region :outlet]}))
revenuequarterQ1Q2Q3Q4100120140160180200

A vector whose elements are not column references is whatever the aesthetic it is written on reads it as. A dash pattern is two lengths, not two columns to pivot:

(-> sales
    (pj/lay-line :quarter :revenue {:stroke-dash [5 5]}))
revenuequarterQ1Q2Q3Q4120130140150160170180190

See Also

source: notebooks/plotje_book/series.clj