16 Change Over Time
Line charts and their variants β showing change over a sequence.
(ns plotje-book.change-over-time
(:require
;; Tablecloth -- dataset manipulation
[tablecloth.api :as tc]
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Plotje -- composable plotting
[scicloj.plotje.api :as pj]))Line
Connected line through data points.
(def wave {:x (range 30)
:y (map #(Math/sin (* % 0.3)) (range 30))})(-> wave
(pj/lay-line :x :y))Grouped Lines
Color separates multiple series. Real datasets often start in wide form β each series in its own column. Plotje plots long form β one row per observation, with the series label in a column. Use tc/pivot->longer to reshape, then map the label column to :color. See Datasets for more on the wide-to-long reshape.
(def waves-wide
(tc/dataset
{:x (range 30)
:sin (map #(Math/sin (* % 0.3)) (range 30))
:cos (map #(Math/cos (* % 0.3)) (range 30))}))(def waves
(tc/pivot->longer waves-wide [:sin :cos]
{:target-columns :function
:value-column-name :y}))(-> waves
(pj/lay-line :x :y {:color :function}))Thick Line
Constant stroke width via :size.
(-> wave
(pj/lay-line :x :y {:size 4}))Line with Points
Overlay points on a grouped line plot.
(def growth
{:day [1 2 3 4 5 1 2 3 4 5]
:value [10 15 13 18 22 8 12 11 16 19]
:group [:a :a :a :a :a :b :b :b :b :b]})(-> growth
(pj/pose :day :value {:color :group})
pj/lay-line
pj/lay-point)Step
Horizontal-then-vertical connected points.
(-> {:x [1 2 3 4 5]
:y [2 4 1 5 3]}
(pj/lay-step :x :y)
pj/lay-point)Step by Group
Grouped step lines.
(-> growth
(pj/pose :day :value {:color :group})
pj/lay-step
pj/lay-point)Stacked Step
The groups pile up in the order the legend lists them, the first on top, with horizontal-then-vertical segments.
(-> {:x (concat (range 5) (range 5) (range 5))
:y (concat [1 2 3 4 5] [2 2 2 2 2] [3 1 2 1 2])
:group (concat (repeat 5 "A") (repeat 5 "B") (repeat 5 "C"))}
(pj/lay-step :x :y {:position :stack :color :group}))Area
Filled area under a line.
(-> {:x (range 30)
:y (map #(Math/sin (* % 0.3)) (range 30))}
(pj/lay-area :x :y))Stacked Area
The bands pile up in the order the legend lists them, the first on top.
(-> {:x (concat (range 10) (range 10) (range 10))
:y (concat [1 2 3 4 5 4 3 2 1 0]
[2 2 2 3 3 3 2 2 2 2]
[1 1 1 1 2 2 2 1 1 1])
:group (concat (repeat 10 "A") (repeat 10 "B") (repeat 10 "C"))}
(pj/lay-area :x :y {:position :stack :color :group}))Dates on the x-axis
Real time-series usually have an actual date column, not an integer step. Plotje detects temporal columns (java.util.Date via #inst, java.time.LocalDate, LocalDateTime, Instant) and picks calendar-aware tick labels automatically.
(def temp-pose
(-> {:date [#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"
#inst "2024-04-01" #inst "2024-05-01" #inst "2024-06-01"]
:temperature [3 5 9 14 19 23]}
(pj/lay-line :date :temperature)
pj/lay-point))temp-poseMultiple Series Over Time
Pass {:color :group} to get one line per category. Rows are drawn in their given order, so pre-sort by date if your data is not already sorted.
(def months
[#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"
#inst "2024-04-01" #inst "2024-05-01" #inst "2024-06-01"])(-> {:date (concat months months)
:temperature [3 5 9 14 19 23
15 17 19 22 25 28]
:city (concat (repeat 6 "Zurich")
(repeat 6 "Athens"))}
(pj/lay-line :date :temperature {:color :city})
pj/lay-point)Area Over Dates
Filled area also works on a date axis β useful for cumulative metrics where the volume below the curve carries meaning.
(-> {:date [#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"
#inst "2024-04-01" #inst "2024-05-01" #inst "2024-06-01"]
:sales [10 25 30 22 35 40]}
(pj/lay-area :date :sales))See Inference Rules for details on how dates are detected and formatted.
Two Series on One Date Axis
Two series measured over different stretches of time read against each other only if their axes agree. Stacked without saying so, each panel spans its own dates across the same width, so a reading above another stands for a different month.
(def zurich
{:date [#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"
#inst "2024-04-01" #inst "2024-05-01" #inst "2024-06-01"]
:temperature [3 5 9 14 19 23]})(def athens
{:date [#inst "2024-05-01" #inst "2024-06-01" #inst "2024-07-01"
#inst "2024-08-01" #inst "2024-09-01" #inst "2024-10-01"]
:temperature [25 28 31 32 27 22]})(def cities
[(-> zurich (pj/lay-line :date :temperature) pj/lay-point)
(-> athens (pj/lay-line :date :temperature) pj/lay-point)])(pj/arrange cities {:cols 1}):share-scales #{:x} pools the date column across the cells. The pooled range is the union of both, so each series occupies the part of the axis it covers, and both panels carry the same tick labels. One above the other, that is something to look at rather than to check: a month is at the same place in both panels.
(pj/arrange cities {:cols 1 :share-scales #{:x}})An axis holds a date as a number of milliseconds, which is what lets the two ranges be pooled at all.
The two panels line up here because their y axes label at the same width. Where they do not β one series in single digits and one in millions β each panel reserves the room its own labels need, and the shared axis comes out spanning different widths. :align-panels, which pj/arrange takes among its options, reserves the same room on every cell; the Composition chapter covers that and :share-scales on any column.
A Distribution of the Dates
A time series says what the readings were, and less about when they were taken. pj/marginal puts a distribution of the poseβs :x column in a thin panel above the plot, sharing its axis β and on a date column that distribution is how the observations are spread through time.
(def sightings
{:date [#inst "2021-03-14" #inst "2021-07-02" #inst "2021-11-28"
#inst "2022-02-09" #inst "2022-05-30" #inst "2022-06-11"
#inst "2022-06-25" #inst "2022-07-08" #inst "2022-09-17"
#inst "2023-01-22" #inst "2023-08-05" #inst "2024-02-19"]
:count [2 5 3 8 6 11 9 14 12 7 4 2]})(-> sightings
(pj/lay-point :date :count)
(pj/marginal :top :histogram))Smoothed Time Series
A LOESS smoother overlaid on a noisy time series makes the underlying trend easier to see. pj/lay-smooth works on any numerical y axis, including dates on x.
(-> {:date [#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"
#inst "2024-04-01" #inst "2024-05-01" #inst "2024-06-01"
#inst "2024-07-01" #inst "2024-08-01" #inst "2024-09-01"
#inst "2024-10-01" #inst "2024-11-01" #inst "2024-12-01"]
:sales [10 14 12 18 22 19 25 28 24 30 27 33]}
(pj/pose :date :sales)
pj/lay-line
pj/lay-smooth)Zero-Line Baseline
Time series with positive and negative values often benefit from a horizontal reference at zero, drawn with pj/lay-rule-h. The rule is drawn at a value written on the layer, :y-intercept, rather than from a data column, and it is a layer like any other: it paints in the order it was added, and the y axis reaches its value even where the series does not.
(-> {:t (range 12)
:delta [-3 -1 -2 0 2 4 -1 3 5 -2 1 4]}
(pj/lay-line :t :delta)
pj/lay-point
(pj/lay-rule-h {:y-intercept 0 :color "#888"}))Whatβs Next
- Timelines β events, intervals, and schedules on a time axis (Gantt charts, Marey diagrams, annotated time series)
- Relationships β heatmaps, contours, and 2D density
- Polar Coordinates β radial charts for cyclical data
- Gallery β more chart variations with side-by-side code