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]))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}))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"}))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)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]}))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))The bare vector means on :x what it means on :y:
(-> sales
(pj/lay-point [:revenue :cost :tax] :quarter))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})):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})):stack piles the series, so the bandβs height is their total:
(-> sales
(pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack})):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}))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}))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]))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"}))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)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)))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]))])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]))(-> sales
(pj/lay-point :quarter [:revenue :cost :tax]))Areas and steps
(-> sales
(pj/lay-area :quarter [:revenue :cost :tax] {:position :stack}))(-> sales
(pj/lay-step :quarter [:revenue :cost]))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}))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]))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]))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}}))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}}))Scales and colours
A scale on the value axis
(-> sales
(pj/lay-point :quarter {:series [:revenue :cost :tax]
:scale {:type :log}}))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}))A domain on the value axis
(-> sales
(pj/lay-point :quarter [:revenue :cost])
(pj/scale :y {:domain [0 250]}))Choosing the colours
(-> sales
(pj/lay-line :quarter [:revenue :cost :tax])
(pj/scale :color {:values ["#377eb8" "#e6550d" "#4daf4a"]}))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))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}))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]}))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"]))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]))Warning: Removed 1 rows with a missing value among the columns read as series (:revenue, :cost).
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))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))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]))A violin over the same three measures:
(-> sales-by-region
(pj/lay-violin :series [:revenue :cost :tax]))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})):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})):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}))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}))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}))The same layer in panels, one per outlet:
(-> sales-by-region
(pj/lay-line :quarter [:revenue :cost] {:color :region})
(pj/facet :outlet))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]))Panels and turned plots
Sideways
(-> sales
(pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
(pj/coord :flip))Series in panels
(-> sales-by-region
(pj/lay-bar :quarter [:revenue :cost :tax] {:position :dodge})
(pj/facet :region))(-> sales-by-region
(pj/lay-bar :quarter [:revenue :cost :tax] {:position :stack})
(pj/facet-grid :region :outlet))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"}))Dodged series, drawn sideways, in panels
(-> sales-by-region
(pj/lay-bar :quarter [:revenue :cost] {:position :dodge})
(pj/coord :flip)
(pj/facet :region))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"}))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}))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))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))(-> sales
(pj/lay-bar :quarter [:revenue :cost] {:position :stack})
(pj/lay-line :quarter :units)
pj/overlay)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}))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))])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]))])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))])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})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]))]})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"})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})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 |
|---|---|
|
lay-point :color was given several columns, [:revenue :cost :tax] |
|
pj/lay-bar was given 1 column in a slot where several are read as several series: [:revenue] |
|
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. |
|
pj/lay-bar was given several columns to read as series, and the data does not have [:nope] |
|
pj/lay-bar reads [:revenue :cost :tax] as series, and this pose's mapping names :revenue on :y |
|
pj/lay-bar was given several columns to read as series, on a composite pose |
|
pj/lay-bar was given several columns to read as series and has no data to pivot |
|
lay-errorbar :y-min was given several columns, [:cost :tax] |
|
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))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]))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]}))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]}))See Also
Faceting β panels from a columnβs values
Composition β arranging poses into grids
Placing Marks β every position adjustment in full
source: notebooks/plotje_book/series.clj