Liking cljdoc? Tell your friends :D

Change Log

All notable changes to this project will be documented in this file. This change log follows the conventions of keepachangelog.com.

[Unreleased]

[0.17.0 - 2026-11-06]

This release breaks with 0.16.1 on where a layer is drawn when it names columns other than its pose's. In 0.17.0, a layer added to a pose that draws one panel is drawn on that panel, whatever columns the layer names. In 0.16.1, a layer naming other columns was given a panel of its own, unless the pose was given to pj/overlay. Separate panels are made with pj/facet or pj/arrange. Three other ways of writing a plot that drew several panels in 0.16.1 also change:

  • pj/pose given other columns on an existing pose replaces the pose's columns;
  • a vector of columns draws all the columns together on a single panel;
  • a vector of column pairs reports an error that shows how to write the panels.

For this release, a layer naming other columns, pj/pose replacing a column, and a vector of columns each print a Note that names the 0.16.1 behaviour.

The main changes:

  • A layer added to a pose that draws one panel is drawn on that panel, whatever columns the layer names. Each axis is titled by every column drawn on it, and a layer with no colour of its own takes a colour and a legend entry. pj/overlay is retired.
  • More ways to create panels with pj/facet and pj/arrange. pj/facet takes a map, and draws a panel for each category of a column, a panel for each of several columns, or a grid that crosses the two. pj/matrix draws a scatter plot matrix. A cell of pj/arrange can be anything pj/pose accepts after the data -- a column, a mapping or a pose -- and a vector in the list of cells is a list of cells.
  • A vector of columns, written where one column goes, draws all those columns together on a single panel, as a series with a colour and a legend entry for each column.
  • pj/pose given an existing pose updates that pose instead of adding a panel. Its mapping is merged into the pose's mapping, and a column given again replaces the old one. A scale, :where or :theme given twice is combined key by key.
  • One option, :share-scales, says which axes panels share, on a facet and on an arrangement alike. A shared axis spans the values drawn in all the panels that share it.

Also new: :where filters the rows a pose draws, panels can be given unequal sizes, and an axis that shows a computed count or density is titled count or density.

The signature of pj/pose -- data or a pose, then columns, then a mapping -- began in #61, which also moved the crossing of columns out of pj/pose into a function of its own, the idea behind pj/matrix, and let pj/arrange take columns and poses as cells. Thanks to @timothypratley for that pull request, and for the questions, discussions and reports on Zulip that shaped this release.

Plots that look different after upgrading

  • A layer naming columns other than its pose's, added to a pose that draws one panel, is drawn on that panel, and each axis is titled by every column drawn on it. A layer with no colour of its own takes a colour and a legend entry naming its columns, and a layer that specifies a colour, such as a black trend line, keeps it. A layer beside a series is drawn on the series' panel in the same way. For this release a Note says so; for a panel per column, write (pj/facet pose {:y [:hwy :cty]}) or use pj/arrange.

  • A vector of columns given where one column goes, as in (pj/lay-density data [:a :b :c]), draws all the columns together on a single panel, as a series with a colour and a legend entry for each column. For this release a Note says so; for a panel per column, write (-> data (pj/arrange [:a :b :c]) pj/lay-density).

  • pj/pose given a mapping on an existing pose merges the mapping into the pose's mapping and adds no panel. A new :x or :y replaces the old one, so (-> data (pj/pose :a :b) (pj/pose :c :d)) draws one plot of :c and :d. On a composite the mapping goes to the root. For this release a Note says when pj/pose replaces a column; for panels side by side, write (pj/arrange data [{:x :a :y :b} {:x :c :y :d}]).

  • A scale or a :theme given twice is combined key by key. After (pj/scale pose :x :log), (pj/pose pose :a :b {:color :c}) keeps the log scale, and pj/options given a :theme twice keeps the keys of both. A warning names a :domain or another column-specific scale setting kept when the aesthetic is given another column, and (pj/pose pose {:x nil}) starts the aesthetic afresh. Every other option is replaced whole.

  • A series titles its axis by its columns, revenue, cost, and lists its legend entries in the order the columns are written. The legend has no title unless :as names the key column or a title is specified. A counting layer beside a series, such as a bar, counts each row of the data once.

  • A series faceted by its key column, (pj/facet pose :series), draws a panel per column, each titled by its column, with a value axis of its own and no legend.

  • A written colour on a layer, such as (pj/lay-smooth pose {:color "black"}), is drawn as written and has no legend entry, also where the pose sets a colour scale. {:color {:value "trend" :scale true}} reads a value through the scale.

  • An axis title too long for the plot is broken into lines after the commas between the columns it names. At most four lines are drawn.

  • A shared axis spans the values drawn in all the panels that share it, padded once, in a facet and in an arrangement alike. So each panel has the axis the plot would have without the facet. The values are measured after the statistics, so a shared axis reaches the top of a stack and the ends of an error bar.

  • Cells drawn from the data of the pose they are arranged into, as in (-> data (pj/arrange [{:x :a :y :b} {:x :a :y :c}]) pj/lay-point), share each axis on which they draw one column. The column is titled, and its ticks labelled, on the outer cells only. :share-scales sets the sharing, and #{} labels the ticks of every cell.

  • A :domain on one cell of a shared axis is the domain of every cell sharing that axis. Cells specifying different domains for one shared axis report an error naming both.

  • A count or density axis is shared among panels that compute the same thing from the same column, in a facet and in an arrangement. A count is not shared with a density or with a column's values, and {:share-scales {:y :all}} shares the counts of different columns.

  • A colour or shape legend under a facet lists its categories in the order of the rows of the whole data, as the same plot without the facet does. So a category keeps its colour when a facet is added. A :domain on the scale sets the order.

  • Strips of a grid of cells, as in a scatter plot matrix, stand where a facet's do: column strips above the panels, and row strips at the right. They name the columns as the axis titles do, sepal length.

  • The diagonal of a scatter plot matrix draws a numerical column's histogram or density within that column's range, on its row's :y axis, so each row's tick labels stand at the left edge.

  • Panels arranged side by side or stacked come out the same size where some cells draw no tick labels on an axis they share. The rows of a scatter plot matrix stand as close together as its columns.

  • Titles in an arrangement. Under an arrangement with a :title, cells with titles of their own keep them, each above its cell. An arrangement or a facet over columns placed as a cell draws its :title and :subtitle once above its cells, and its :caption once below them.

  • pj/arrange with :cols whose last row is short draws the cells of that row at the size of the cells above.

  • A lay-* call naming columns no cell of an arrangement draws adds a cell to the arrangement's list of cells: added to a row of two cells, the new cell is the third in that row.

  • A plot option given to pj/arrange, such as {:x-label "X"}, is written on the arrangement as pj/options writes it. A key pj/arrange does not read warns.

  • :panel-width or :panel-height written on an arrangement sets the size of the panels of its cells, and the arrangement's size is worked out from them, as a single plot's is. {:panel-width 150} on a row of two cells draws each panel 150 wide.

  • Bars drawn across, with the value on :x, start at the panel's edge: the :x axis begins at zero and is padded only past the longest bar, as the :y axis is under upright bars.

  • A histogram, a density or a count bar titles the axis it computes count or density, also on a pose that names a :y column the layer does not read. {:y-label ""} removes an axis title and the room it takes. Reported on Zulip - thanks, @timothypratley

  • A histogram, a density, a summary or a smooth grouped or coloured by a column with missing values removes the rows with no value in that column and prints how many, as the other layer types do.

  • Plots under (pj/coord :polar) draw axis titles and tick labels: the angle's labels around the circle, and the radius's at the left edge.

  • Plots under (pj/coord :fixed) draw one data unit at the same length on both axes, measured inside the panel's padding.

  • A short numeric axis, as on a small panel, carries at least two tick labels where they fit.

Added

  • pj/facet takes a map. {:y [:hwy :cty]} draws a panel per column, and {:x [..] :y [..]} a panel per pair of columns. Under :col and :row the map draws a grid, a panel for each value across with each value down, and each entry is a column of categories or a map of columns: {:col :year :row :drv}, or {:col :drv :row {:y [:hwy :cty]}}. nil in a list of columns leaves a place for a marginal distribution.

  • Two pj/facet calls, one over categories and one over columns, in either order, write the pose that one call with :col and :row writes.

  • pj/facet takes a map of plot options last, the map pj/options takes: (pj/facet pose :drv {:share-scales #{:x}}).

  • A :domain on :col or :row, as in (pj/scale pose :col {:domain ["r" "f" "4"]}), orders the panels of a facet.

  • pj/matrix draws a scatter plot matrix: (pj/matrix data cols) crosses a list of columns with itself, and (pj/matrix data xs ys) crosses two lists. It returns the pose (pj/facet data {:col {:x xs} :row {:y ys}}) returns, and a map of plot options may come last. (pj/matrix data [:a nil] [nil :b]) draws a scatter plot of :a and :b with a marginal distribution of each.

  • Under a facet over columns, a layer naming no column is drawn in every panel, on that panel's columns, and a layer naming one of the facet's columns is drawn only in that column's panels. A panel no layer draws takes the layer type inferred from its columns, so a scatter plot matrix draws histograms on its diagonal.

  • pj/arrange takes columns, mappings and poses as cells: a column is a panel of that column, and a mapping such as {:x :a :y :b} a panel of the columns it names. A vector in the list of cells is a list of cells, so (pj/arrange data [[:a :b] [:c :d]]) is a 2x2 grid. The first argument may be a pose, whose data, mapping, layers, row filter and options reach every cell.

  • pj/arrange given an arrangement appends the new cells to its list of cells: (-> (pj/arrange data [:a :b]) (pj/arrange [:c])) draws one row of three panels.

  • (pj/pose data p) draws the pose p from that data, as a cell of pj/arrange does. A pose carrying data of its own keeps it.

  • pj/lay-* given nil, as in (pj/lay-point nil :a :b), builds a pose with that layer and no data. pj/with-data, or an arrangement given data, gives it the data.

  • :share-scales says which axes panels share, on a facet and on an arrangement alike. A set names the axes: #{:x} shares :x and fits :y to each panel, and #{} shares neither. A map groups the columns that share an axis: {:y [[:hwy :cty]]} shares :y between the panels drawing :hwy and :cty, and {:y :all} between every panel. A categorical axis is shared as a numerical one is, listing the categories of every sharing cell.

  • :col-weights and :row-weights, plot options, share out the columns and rows of panels of an arrangement or of a facet over columns: {:col-weights [3 1]} draws the first column's panels three times as wide as the second's, and the plot keeps its :width. On the same plots :panel-width and :panel-height take a vector, one size per column or row in drawing units. A vector of the wrong length reports an error.

  • :where, a map from column to value or set of values, draws only the rows that match. It is written in pj/pose beside a mapping, as in (pj/pose data :displ :hwy {:where {:drv "f"}}), or as a cell of pj/arrange, {:where {:drv "f"}}. Cells filtered this way are labelled by strips naming their values, as the panels of a facet are. Two filters that meet, such as a cell's under its pose's, combine column by column, and a column named in both takes the later or inner value.

  • A layer that names one column reads the panel's column on the other axis, where its layer type draws from two columns: (-> data (pj/lay-point :week :adults) (pj/lay-line {:y :children})) draws the line of :children against :week.

  • A column named on :y alone is drawn on its side by a layer type that draws from one column: (pj/pose data {:y :hwy}) draws a histogram on its side, and pj/lay-bar draws a bar chart of counts across.

  • layer-type/register! takes :column-counts, the set of numbers of columns a layer type draws from: #{1} for an x column alone, #{2} for x and y, #{1 2} for either. It decides which panels of a facet a layer is drawn in, and whether a pose naming only :y draws the layer on its side. layer-type/column-counts reads it back.

  • A stat declares what it computes for an axis with a method on [stat :computes]: :bin declares {:y :count}, or {:y :density} under :normalize :density. Sharing an axis, the Note for layers sharing a panel, and the check for a missing :y column read the declaration, so an extension's stat takes part in all three. See "What a stat computes" in the Extensibility chapter.

  • :legend-font-size, a plot option, sets the font size of a legend's entries, with the legend's title one size larger. The default is 10.

  • :strip-labels, a plot option, sets what a facet strip prints: :value, the default, prints the category alone, and :both prints the column's name before it, as drv: f.

  • :min-panel-size, a configuration key, is the smallest width and height a panel is drawn at. The default is 20.

Warnings and errors

  • The final map of a pj/lay-* call is called its mapping, as the final map of pj/pose is, and its keys that are not aesthetics, such as :stat and :bins, are called layer settings. No call changes its behaviour. pj/layer-option-docs is renamed pj/layer-mapping-docs, and layer-type/universal-layer-options is renamed layer-type/universal-mapping-keys; the old names are deprecated.

  • pj/overlay and :overlay, wherever written, warn that they are ignored, and report an error under :strict. (pj/overlay pose false) and {:overlay false} report an error naming pj/facet and pj/arrange.

  • Retired names warn, are read as their replacements, and report an error under :strict. :scales is read as the :share-scales it stands for, (pj/facet pose :drv :row) as (pj/facet pose {:row :drv}), and (pj/facet-grid pose :drv :cyl) as (pj/facet pose {:col :drv :row :cyl}).

  • pj/pose and lay-* given a vector of column pairs, such as [[:a :b] [:c :d]], report an error suggesting (pj/arrange data [{:x :a :y :b} {:x :c :y :d}]) or (pj/matrix data xs ys). pj/cross is deprecated: it warns and returns its pairs. pj/arrange given its cells one by one, (pj/arrange a b), reports an error.

  • A layer left without a column its layer type draws from reports an error naming the layer, the missing axis and the columns of the data. Drawing the pose reports the error, not building it, so a pj/lay-* call on data of four or more columns may leave its columns to a later step: (-> data pj/lay-point (pj/pose {:x :a :y :b})) draws.

  • A layer with no data draws a panel reading "no data" and prints a Note saying where to give the data, so a template can be drawn before pj/with-data gives it data. A cell of an arrangement that draws nothing prints a Note naming its row and column.

  • A lay-* call that adds a cell to an arrangement prints a Note where the new cell has no data, or where other cells draw nothing, naming the spelling that draws what was meant.

  • Under a facet over columns, a layer naming a column that is not among the facet's reports an error where it would leave a panel without a layer drawing that panel's column. A layer naming one of the facet's columns prints a Note where another panel is left to the inferred layer type, saying how to draw the layer in every panel.

  • Layers of one pose that draw columns of different kinds on one axis, such as categories beside numbers, report an error naming pj/facet and pj/arrange. Layers that draw a count or a density beside a column's values on one axis print a Note that the axis reads in two units. Numerical columns drawn on one axis print a Note where one of them takes less than a twentieth of the axis.

  • pj/facet reports an error when it is given a column the data does not hold, a facet in a direction already taken, or a map of columns on a composite. A key its map does not read reports an error naming where the key belongs.

  • A :where naming a column the data does not hold reports an error, and a value that appears nowhere in its column warns.

  • A :share-scales naming an axis that no two cells share warns, naming the columns the cells draw there.

  • The warning for a key a call does not read is worded pj/lay-point does not recognize [:foo], followed by where each key belongs.

  • A layer type given columns it cannot draw reports an error suggesting what can: pj/lay-bar given two categorical columns suggests counting one and colouring by the other, and pj/lay-histogram or pj/lay-density given two columns names both. pj/lay given a layer type that is neither registered nor a map naming its :mark reports an error.

  • Panels too small to draw report an error stating the size each panel would have and what the labels and legend take, and naming :width, :panel-width and :min-panel-size. For a facet, the error says that a facet does not wrap its panels and suggests pj/arrange.

  • pj/marginal reports an error for a key of its last map other than :size, and for a :size that is not a number between 0 and 1.

  • A grouped dataset, the result of tc/group-by, reports an error naming tc/ungroup.

  • A rule or a band given columns on a pose with one panel warns where the panel does not draw those columns, and ignores them; on a composite its columns pick the cell it is drawn in.

  • A line, step or area whose every group holds one row warns that it draws nothing visible. A smooth warns about the groups with too few rows to fit.

  • A message about a series names it by the columns written, not by the :value and :series columns it is pivoted into.

Fixed

  • Rotated x tick labels (:x-tick-angle) take at most 30% of the height of a plot or of a cell of an arrangement, so a scatter plot matrix can rotate them.

  • pj/lay-rug naming a column on :y alone, on a pose that has an :x, draws that column's ticks along :y.

  • Brushing a faceted plot, or cells filtered with :where, highlights the same rows in every panel. A draft layer's :data carries each row's number in a :scicloj.plotje/row column.

  • A pose naming a column only on :y, under a layer type that draws from two columns such as lay-point, takes the first other column of data of one to three columns as its :x.

  • A series specified on a pose that no layer is added to is drawn, with an inferred layer or with the layers of the pose it is arranged into.

  • A fitted line, a density or a histogram grouped by one column and coloured by another, as in (pj/lay-smooth data :x :y {:group :a :color :b}), draws each group in the colour of its :b value.

  • Cells of an arrangement that share an axis end on one domain where their text labels widen it. A marginal covers the extent of its panel, also where a rule or a band widens the panel.

  • Facets that leave an axis out of :share-scales label the ticks of every panel whose axis differs from its neighbour's.

  • A panel of a facet that holds no rows draws "no data" for every layer type, and the other panels are drawn. A facet of data with no rows draws one such panel. A panel whose categories are all missing adds no ? category to its axis.

  • A shape, size or alpha legend at :legend-position :top or :bottom is drawn, each in a row of its own under the colour legend.

  • A straight line fitted with {:stat :linear-model} is drawn inside the panel: the axis covers the ends of the line.

  • pj/arrange draws :subtitle under its title and :caption under the grid.

  • A layer placed in the drawing area, {:in :drawing-area}, with columns of its own data is drawn on the panel it is added to.

  • pj/facet reads a column name the data holds, of any type, so (pj/facet pose 2) on a dataset whose columns are integers draws a panel per value of column 2.

  • A scale spec's :breaks, :tick-labels, :domain, :values and :range may be any ordered sequence, such as a dtype-next column, and a :domain written high to low holds the :breaks between its ends.

  • lay-* given columns and a scale for one of them in its map, as in (pj/lay-point data :a :b {:x {:scale :log}}), draws column :a on a log scale.

  • :share-scales written at the top of a hand-written pose map is read on a single pose, as it is on a composite.

  • pj/save writes a pose whose only data is a layer's, and pj/svg-summary given data summarizes the plot pj/plot draws.

Book and project

  • The book's plots scale to the width of the page. (PR #60) - thanks, @timothypratley

  • Plotje has an icon: the README's logo and the book's browser-tab icon. (PR #62) - thanks, @timothypratley

[0.16.1 - 2026-10-02]

Added

  • nil in pj/arrange is a cell left empty: it keeps its place in the layout and draws nothing, also when the composite carries data and layers, so (pj/arrange [[a nil] [b c]]) leaves the top-right cell blank. An empty cell does not keep the other cells from sharing a legend.

Warnings and errors

  • These report an error naming the option and the value: :dx or :dy given a column, and a value written before a rule's or a band's options map that is not a column of the data, such as the 7 in (pj/lay-rule-v pose 7 {:x-intercept 2}). A layer naming a column that a series on the same pose has already read reports that the series pivoted it, and suggests passing the original data with :data.

[0.16.0 - 2026-10-02]

Most of this release is about colour: gradient legends, tiles, and composite poses whose cells agree on one colour, size or symbol per value. It also adds pj/lay-segment, for lines and arrows between two points.

Plots that look different after upgrading

  • Cells of a composite pose -- pj/arrange, pj/marginal, a grid of pairs, a composite map -- that map the same column to :color, :size, :alpha or :shape share one scale for it, covering the categories or the range of values of every cell. A category is drawn in the same colour or symbol, and a value at the same shade or size, in every cell; the one legend lists every category. A cell that writes its own scale for the aesthetic keeps it, and each cell then draws its own legend.

  • :share-scales in a grid. A cell that draws several columns on the shared axis shares each column's extent with the cells that draw that column. A histogram or density added to a grid of pairs, such as (-> data (pj/pose (pj/cross cols cols)) pj/lay-histogram), gives each cell a count or density axis fitted to its own bars. A shared axis reaches the far ends of segments and intervals and the outer edges of tiles.

  • Gradient legends. On a linear scale the legend is labelled at ticks chosen and formatted as the axis's are, such as -0.5, 0.0, 0.5 and 1.0; on a log scale, at its decades. At :legend-position :top or :bottom it is a horizontal bar with its labels underneath. A column holding one value is labelled once, and the bar is drawn without seams between its colours.

  • A gradient written as {:low ... :high ...} runs straight from one colour to the other. A :mid is drawn only where it is written, as a third stop.

  • The legend of a tile, a 2D density or a 2D histogram is titled from the same settings as its gradient: a :label in the :fill or :color scale spec, :fill-label or :color-label, and otherwise the name of the column the tile fills from. A contour's legend is titled through :color. Reported in #58 - thanks, @behrica

  • pj/lay-tile on a numeric :x or :y. The axis reaches the outer edges of the tiles, so the outer rows and columns are drawn at full size, and a missing value is left as a gap rather than closed by its neighbours. Reported in #59 - thanks, @behrica

  • The colour of a tile. A numeric :color is read as :fill is, through the same scale settings and legend. A categorical :color colours each cell from the palette, with a legend of the categories, and the fill settings warn. Given both :fill and :color, the tile is painted from :fill and warns that :color is not drawn; with :group, each cell is painted from its own row's value. Reported in #40

  • pj/lay-contour and pj/lay-density-2d. Contour lines are coloured through the settings the legend reads, so :color-midpoint, and a :domain or :type in the :color scale spec, move the lines as well as the legend. Given a :color column, both keep their density legend and warn that the colour is not drawn, since one density is computed from all the rows.

  • A :color column written on a rule or a band adds nothing to the legend, and warns that it is not drawn: a rule or a band is drawn once, from written values.

  • An overlay asked for by only one of the disagreeing layers, as in (-> data (pj/lay-line :t :a) (pj/lay-line :t :b {:overlay true})), draws what pj/overlay draws: a colour and a legend entry per layer, and an axis title naming both columns.

  • A series on :x together with a series on :y, such as {:x [:t1 :t2] :y [:v1 :v2]}, is read in pairs and drawn on one panel as the series t1 / v1 and t2 / v2. The pivot names its value columns :x-value and :y-value, and :as names the key column. To draw each pair on a panel of its own, write the pairs: (pj/pose data [[:t1 :v1] [:t2 :v2]]).

  • Long rotated tick labels get room worked out from the angle and the label lengths, below the panel and at the ends of each axis, up to 30% of the plot's size, so long labels are not cut off at the canvas edge or drawn under the axis title. Reported in #57 - thanks, @behrica

  • :breaks [] on an axis draws the axis with no ticks, tick labels or grid lines; its title stays.

  • Arrow heads and triangle markers are drawn with smooth edges.

Added

  • pj/lay-segment draws a straight line per row from :x and :y to :x-end and :y-end, each a column or a written value. An end left out keeps the start's value, so (pj/lay-segment data :index :distance {:y-end 0}) draws a stem plot. :arrow puts an arrow head on the :end, the :start or :both, and :dx/:dy move the whole segment. Asked for in #50 - thanks, @behrica. The arrow heads cover the straight case of #17.

  • :y-tick-angle rotates the y-axis tick labels, as :x-tick-angle does for x. At 90 or -90 each label runs along the axis, centred on its tick. Asked for in #51 - thanks, @behrica

  • :band-color sets the colour a band draws in where its layer names none, beside :rule-color and :band-opacity.

  • :values in a :shape scale spec may be a map from category to symbol, as it may for :color.

Warnings and errors

  • A colour or fill setting that nothing on the plot reads warns: :fill-label, :fill-range or :fill-midpoint with no mark drawn in a fill, :color-values with no categories coloured, :color-range or :color-midpoint with no gradient drawn, :color-label with no legend, and the same :range, :midpoint and :values in a scale spec. So does a written colour on a tile with no :fill, such as {:color "red"}.

  • An unrecognized option that differs from an accepted one only by hyphens, underscores or case is named in the warning, as in "Did you mean :x-end for :xend?".

  • These report an error naming the value: a date column on :color; a column name as an area's :stroke; an axis setting in pj/scale with the wrong kind of value, such as :breaks given a function; an :x-tick-angle or :y-tick-angle that is not a finite number; and a map holding a map given as data, or a plan or a draft given where a pose goes.

Fixed

  • A colour name such as "black" or "red" is read wherever a colour is written, including :point-stroke and the :low/:mid/:high of a gradient.

  • A plot's caption is drawn wholly inside the canvas.

  • A number beyond about 9.2e18 on an axis or a colour legend is printed in full.

[0.15.0 - 2026-09-23]

Faceting is a mapping: :col and :row are aesthetics, so a facet follows the scope rules every other mapping follows. Several columns written where one goes are read as a series, drawn as groups of one layer, so they can be dodged, stacked or filled against each other. pj/overlay applies to the whole pose wherever it is written, and overlaid layers that draw different columns are told apart by colour.

Thanks, @timothypratley, for the thoughtful discussion behind the main changes in this release.

Plots that look different after upgrading

  • Overlaid layers that draw different columns. Each layer takes a colour and a legend entry naming its column, the axis title names every column drawn on it, and :position :dodge gives each layer a slot of its own. Layers the writer has coloured, and an overlay only some of the disagreeing layers ask for, are unaffected.

  • pj/overlay written after the layers, or turned off with (pj/overlay pose false). The setting applies to the whole pose, wherever in the pipeline it is written. To put one layer on the panel while the others take their own, write {:overlay true} in that layer's options map.

  • Layers that name different columns on one pose. The pose stays a leaf and draws a panel per pair of columns, in one grid. An axis carries a title only where every panel draws the same column on it, and each panel's strip names the column it draws. Faceting such a pose keeps both divisions, and each drawn column keeps a domain of its own.

  • Faceting a composite, or a pose that already draws several panels. Every cell or panel is drawn once per value of the facet column. pj/marginal on a faceted pose draws each strip label once, at the top.

  • {:col [:a :b]} or {:row [:a :b]} in a mapping, and (pj/facet my-pose [:a :b]). The columns unite into one compound key, and a panel is drawn per combination the data holds.

  • A dodge whose grouping names more than one column. Each combination gets a slot of its own, so {:color :part :group :dimension :position :dodge} draws a bar per part and dimension.

  • A map naming aesthetics, such as {:x :a :y :b}. The map is a mapping, and lifts to a pose in pj/pose, pj/with-data, pj/lay-* and a pj/arrange cell. A hand-built pose map with a :mapping and no :layers is a pose too, and a layer is inferred for it. A dataset written as a map of columns is still data: a column is a sequence, a mapping a map.

  • One column on both :x and :y. Its values are drawn on both axes, so (pj/lay-point my-pose :a :a) draws the y = x diagonal.

  • {:position :fill} on a bar with a value column, or on an area. The marks are drawn as shares of the total at each place, between zero and one, and a total of zero leaves a share of zero.

  • A stacked area over a categorical axis. The bands follow the order the axis draws its categories in.

  • A cell of pj/arrange with its own :theme. The cell is drawn with that theme.

  • Keyword categories holding a hyphen or an underscore, on an axis and on :color. Each category is drawn in its own colour, including under a palette written as a map of category to colour.

  • :group with no :color. The marks are drawn in the default colour, whether :group names one column or several.

  • pj/lay-tile with a :fill column and a categorical :x or :y. Each row draws one cell, centred on its category and reaching halfway to its neighbours, so the cells meet. This covers text and keyword columns, and a numeric column read with {:x-type :categorical}.

  • A scatterplot matrix or an arrangement with no :width or :height of its own. The size comes from the configuration when the plot is drawn, so pj/set-config! and pj/with-config size it as they size a single plot. (Closes #56) - thanks, @behrica

  • Mistakes that now report an error. Several columns on an aesthetic that draws one thing, such as :color, :size or :alpha, report an error naming the aesthetic, in any of the three spellings: bare, {:series [...]} or {:column [...]}. A second facet in a direction the pose already divides reports an error naming the other ways to ask. Faceted panels whose shared axis holds categories in one panel and numbers in another report an error naming (pj/options {:scales :free-x}).

Added

  • Several columns written where one column goes are read as a series of one layer, in a lay-* call or on the pose. The columns are pivoted into a key column, named :series unless {:series [...] :as ...} names it, and a :value column. The key column is mapped to :color, or to :group where the layer maps :color itself. Column names that look like numbers, such as years, still get one colour each, and an observation with no value is dropped with a warning.

  • :col and :row are aesthetics. pj/facet and pj/facet-grid write them into the pose's mapping, where they reach every layer and sub-pose below, and a sub-pose may write its own. pj/panel-aesthetic-docs describes them.

  • pj/arrange takes data first: (-> data (pj/arrange [{:x :a :y :b} {:x :c :y :d}]) pj/lay-point). Each cell says which columns its panel draws, and the layers are added once, at the root. A composite may hold a composite.

  • The aesthetic registry records each aesthetic's :role (:positional, :appearance, :grouping or :panel) and whether it unites several columns into one key. pj/aesthetic-roles and pj/compound-key-aesthetics publish them.

Changed

  • :overlay is read where a leaf's panels are decided, rather than where a layer is added. pj/lay-* no longer builds a composite; pj/pose with a position on a positioned leaf still does.

  • A pose's facet columns are at [:mapping :col] and [:mapping :row], rather than [:opts :facet-col] and [:opts :facet-row]. A hand-built pose with the older keys is still faceted by them.

  • Where two layers name different columns on one axis, a note on standard output says each got a panel of its own, and names pj/overlay and, where it would draw, reading the columns as a series.

[0.14.0 - 2026-09-18]

A number written for a categorical axis is a place among its categories, counted from one. 1 is the first category, 1.5 sits halfway to the second, and the axis reaches half a place past each end. Every route reads it that way: a value written in a slot, a :dx, a rule's intercept, a band's edges, and pj/to-drawing. :dx and :dy are the new names for :nudge-x and :nudge-y: a shift is measured in the axis's own units, and on a categorical axis that unit is a band.

Many thanks to @timothypratley, who wrote the pull request behind the place reading, and to @behrica, who asked for it.

Plots that look different after upgrading

  • Every plot writing a number for a categorical axis. The mark is drawn at that place among the categories.
  • Every plot shifting a mark along a categorical axis. {:dx 0.5} moves a mark half a band along. Under their former names, :nudge-x and :nudge-y reported an error on that axis.
  • Every plot writing a rule or a band on a categorical axis. (pj/lay-rule-v {:x-intercept 2}) draws at the second category. These marks used to draw on the panel's left or bottom edge whatever value they were given, and a band written for that axis failed with a NullPointerException.
  • Every plot writing a number past the ends of a categorical axis. The plot reports an error naming the value, the categories and where the axis ends.
  • Every histogram of a column holding one distinct value. The bar is drawn half the panel wide. It used to be drawn zero units across, so the plot came out empty.
  • Every plot carrying a rule or a band and nothing that gives the other axis an extent. That axis runs 0 to 1 and is ticked across it. It used to run -0.05 to 1.05 and carry a tick at each end.
  • Every plot adding a rule or a band beside a density. The axis the rule spans keeps the extent the density gives it. In 0.13.0 the rule reported 0 to 1 for that axis, which won the merge and drew the density flat along the bottom.

Added

  • A number written for a categorical axis is read as a place among its categories, counted from one, so (pj/lay-label {:x 1.5 :y 3 :text "note"}) draws between the first category and the second. A :dx reads the same way, and so do a rule's intercept and a band's edges. pj/to-drawing answers a place as it answers a category. The axis runs from 0.5 to half a place past the last category, and a number outside that reports an error naming the value, the categories and the ends. Written by @timothypratley in PR #49, and asked for by @behrica.

Fixed

  • A histogram of a column holding one distinct value draws a bar a reader can see, half the panel wide. A value written on a histogram is read as one such column, so {:x 2} on a categorical axis draws a bar one place wide. A width set with :binwidth decides the bar itself and is unchanged.

  • A categorical :domain written with pj/scale orders the categories the data holds and adds none to them. A name the data does not hold is dropped and Plotje warns, and where the axis ends is counted from the categories that remain, so a place past the last one is no longer let through and drawn off the panel.

  • pj/lay-errorbar reports an error on :y-min or :y-max where the value is not a column of the layer's data, naming the mark, the key and the value, and on one bound given without the other. An errorbar reads both keys as columns, one bound per row; the same two keys take a written number on pj/lay-band-h, which shades one region between them.

  • A rule or a band gives no extent to the axis it spans. A rule names a value on one axis and reaches across the other, and across that other axis it reported 0 to 1, which flattened a density drawn beside it. Reported in #plotje > lay-rule-v regression ? - thanks, @behrica

  • A pose carrying a hiccup tooltip conforms to the pose schema, so pj/valid-pose? answers true for one. A :tooltip is a string, shown as the text it spells, or a hiccup vector, drawn as markup.

  • A :tooltip or a :brush asked for on a format that draws no interaction warns and is dropped, and the figure still renders. Both are drawn by a browser reading the figure, so SVG is the only format that answers them. - thanks, @phronmophobic

  • pj/save accepts a java.io.File as its path, which its docstring offers alongside a string.

Correction to the 0.13.0 notes. "A rule or a band can be added to a pose whose layers read only an x column -- a histogram, a density, a count bar or a rug" holds for where the rule is drawn. What it left out is that the rule also reported an extent for the axis it spans, so a density beside one was flattened against it. A histogram or a count bar counts past 1 and was unaffected.

Changed

  • :nudge-x and :nudge-y are :dx and :dy. The shift they set is measured in the axis's own units, and a band is now one of those units, so the name says a change in :x without naming a unit. Written under the old name the option is read as the new one and Plotje warns, naming the edit (an error under :strict), so a plot written before this release draws as it did. :offset-x and :offset-y, which shift a layer by a distance on the page, keep their names. - thanks, @timothypratley

[0.13.0 - 2026-09-10]

Rules and bands are ordinary layers. :rule-h, :rule-v, :band-h and :band-v are drawn in the order they were written, take the options the other marks take, and widen the axis they are written on.

Plots that look different after upgrading

  • Every plot whose rule or band was written before a data layer. The rule is drawn under the marks written after it.
  • Every plot whose rule or band sits outside the extent its data covers. The axis reaches the rule -- on a date axis the ticks run out to it, and on an axis shared with :share-scales every cell reaches it. A :domain written with pj/scale still pins the axis where the rule cannot widen it.
  • Every rule given an :alpha. The line is drawn at that opacity.
  • Every plot combining a rule or a band with (pj/coord :polar). pj/plan reports an error rather than drawing the plot with the rule left out of it.
  • Every plot whose band was written with equal bounds. pj/lay-band-h and pj/lay-band-v report an error rather than returning a pose.
  • Every plot on a date axis that asked for more ticks than fit. The axis carries fewer, larger-stepped ticks whose labels do not overlap.
  • Every plot setting :annotation-stroke. The key is :rule-color now. Under the old name Plotje warns, names the new key and drops the setting (an error under :strict), so the plot still draws and its rules take the default colour.
  • Every SVG plot drawing a filled shape under one drawing unit across -- a narrow bar, a thin interval, a small tile. Wider shapes are unchanged, and PNG is unaffected.
  • Every log axis carrying a break written with pj/scale. The break reads to six significant digits.
  • Every caller of pj/shape-symbols. It is a function now, and it answers with every symbol a mapping may name rather than the shorter list categories are assigned from, which is (pj/shape-palette).

Added

  • Rules and bands take :in, :alpha and :size. (pj/lay-rule-h {:y-intercept 40 :in :drawing-area}) draws a line forty drawing units below the top of the panel background rather than at the data value 40; :alpha sets a line's opacity or a band's fill opacity; :size sets a rule's width.

  • pj/arrange accepts :align-panels, which gives every cell the same drawing area. Without it, cells whose y axes label at different widths get different panel widths, so an axis shared with :share-scales covers a different extent in each. - thanks, @timothypratley

  • :circle-open draws a point as a ring rather than a disc, so overlapping points stay countable where filled discs merge. Name it for a layer with {:shape :circle-open} or pass it among :values to a :shape scale; it is not handed out to categories automatically. (Closes #46) - thanks, @behrica

Removed

  • The :annotation-dash configuration key, which was read by nothing. Written now, Plotje warns that it is an unrecognized configuration key and the plot still draws (an error under :strict). A dashed rule takes :stroke-dash on its layer.

Changed

  • :rule-h, :rule-v, :band-h and :band-v are ordinary marks. Each travels among a panel's :layers in the order it was written, so draw order is layer order and the extent a rule writes reaches the axis. A panel's :annotations slot is gone, along with the Annotation schema; code that walked a plan for these four reads :layers instead. (Closes #48) - thanks, @behrica

  • The :annotation-stroke configuration key is :rule-color, so no part of the public API still calls these four marks annotations. Under the old name Plotje warns and the warning names the new key.

  • pj/shape-symbols and pj/shape-palette are functions rather than values, and the two answer different questions: (pj/shape-symbols) is every symbol a mapping may name, and (pj/shape-palette) the shorter list categories are assigned from in order. Code that assigns symbols to categories wants (pj/shape-palette). This is groundwork for letting a plot add a shape symbol rather than that feature: the symbols Plotje draws are still fixed in the library, though render.mark/draw-shape now reports an error on a symbol it cannot draw rather than drawing a circle for it.

Fixed

  • A date axis draws as many ticks as its labels have room for. The count steps down until the labels stop running together, so a wider plot stays on a calendar step a reader counts in rather than moving to a finer one with longer labels.

  • A date axis widened by a rule or a band is ticked across its whole width. A date axis picks its ticks over the extent its data covers, and the value a rule or a band writes now reaches that extent as well as the axis domain.

  • A thin filled shape is drawn rather than dropped. Plotje asks the renderer to snap a filled shape's edges to device pixels only where the shape is at least one drawing unit across in both directions, and leaves anything thinner to anti-alias, so a narrow bar draws faintly instead of vanishing and two bars of different widths look different. The change is one SVG attribute: shape-rendering="crispEdges" is now conditional on that extent, and the numbers in the polygon's points are what they always were. PNG never set the attribute and is unaffected. Reported in #plotje > missing bar char variant ? - thanks, @behrica

  • A break written with pj/scale :breaks on a log scale is written to six significant digits rather than as the double holds it. The breaks a log axis picks for itself are unchanged.

  • An axis shared with :share-scales covers the values a rule or a band writes on it. A shared domain is built from the columns its cells name, and a rule names none, so a rule outside the shared extent drew off the panel and was clipped away without a word.

  • pj/lay-band-h and pj/lay-band-v report an error on equal bounds. {:y-min 3 :y-max 3} covers nothing and drew a rectangle of zero thickness that no reader could see. The error names the rule that draws a line at a single value.

  • A rule or a band can be added to a pose whose layers read only an x column -- a histogram, a density, a count bar or a rug. An intercept is a written value rather than a column reference, so these four need no y column of their own.

[0.12.0 - 2026-09-07]

A column drawn as it stands does not group. {:color {:column :c :scale false}} says the column holds colors, and those values are drawn on the marks without splitting the rows. A column read through its scale is unchanged: each distinct value is still a category, and the rows are still split into a group per category.

Plots that look different after upgrading

  • Every bar coloured by a :scale false column. The bar fills its whole band, where the band was divided between the colors as if they were a series.
  • Every plot whose :scale false color reaches a mark drawn from many rows -- a line, an area, a histogram bin, a box, a violin. The plot is reported rather than drawn.
  • Every bar, text, label, lollipop, errorbar and rug drawn with a numeric :color column. The gradient the legend shows is painted on the marks, one color per row.

Fixed

  • A color column drawn as it stands does not group. Reading a column through its scale turns each distinct value into a category and splits the rows into a group per category, which is what the legend explains and what divides a bar's band between its colors. :scale false asks for the values to be drawn rather than read, so none of that follows. Where such a column reaches a mark drawn from many rows, which can be drawn in one color only, the mapping is reported: it names the mark, and the two ways out are to write one color for the layer or to drop the :scale.

  • A mark drawn once per row wears that row's color when :color names a numeric column. A bar in a category band or at a numeric position, a text, a label, a lollipop, an errorbar and a rug tick read the per-row color buffer that :point and :interval-h have always read. A mark drawn once from many rows draws one color and reports that the column is not painted on it. Which marks those are is answered by what the layers drew rather than by a list of names, so an extension mark that paints a gradient is not reported.

[0.11.1 - 2026-09-06]

Plots that look different after upgrading

  • Every horizontal bar, boxplot or violin coloured by the column its categorical axis draws. The mark fills its band, as the upright form has since 0.11.0.

Fixed

  • A horizontal bar, boxplot or violin coloured by the column its categorical axis draws keeps its full width. The colour column is compared against the categorical axis, which is :y where a mark orients horizontally, so each band holds one colour group and nothing is dodged. A colour column naming groups the categorical axis does not draw still divides the band between them, as an explicit :position :dodge does. - thanks, @timothypratley

  • A :tooltip mapping that reaches no mark drawing hover text is reported. The point and interval-h marks draw a tooltip; a mapping written on any other layer type is accepted, draws nothing, and now says so once for the plot. A mapping written on the pose stays quiet where any layer of the plot draws it, since a point layer beside a line layer is the case the aesthetic exists for.

  • :bar-width, :box-width and :interval-thickness report what they were given when it is not a number, naming the option. Each sets one width for the whole layer, so a column has nothing to mean there.

  • The error for an unknown render format names every format a caller can ask for, including the ones whose renderer namespace loads on demand.

Correction to the 0.11.0 notes. "A bar is as wide with a colour column as without one" holds where the colour column is the one the categorical axis draws. A colour column naming other groups divides the band between them, which is a dodge and is intended.

[0.11.0 - 2026-09-05]

Which panel a layer is drawn on is most of this release. A layer whose columns match no panel of the pose is drawn on a panel of its own, and pj/overlay keeps the layer on the panel it is added to instead. pj/marginal draws a distribution of a pose's :x or :y column in a thin panel above the main panel or beside it.

Hover text is an aesthetic. Map a column to :tooltip and its values are what a point says on hover; the values are shown as they stand, so format them in the data.

Many thanks to @timothypratley, whose reports shaped much of this release.

Plots that look different after upgrading

  • Every scatterplot matrix and every composite carrying :share-scales. The shared axis is padded, so the outermost marks stand inside the panel.
  • Every bar chart with a colour column. Each bar fills the same share of its category band whether or not a colour column is mapped. Dodged bars are unchanged.
  • Every plot mapping one categorical column to both an axis and :color, where the values are keywords rather than strings. Each category takes its own palette colour, the one the legend lists beside it.
  • Every plot whose pj/lay-* options map names columns matching no panel. The layer is drawn on a panel of its own, as a layer naming those columns in the argument slots is drawn, and pj/overlay or {:overlay true} keeps the layer on the panel it is added to. A written value is not a column -- it places a mark on the panel the layer is added to and asks for no panel of its own, so annotations are unaffected.
  • Every area, density and histogram under (pj/coord :flip). Each runs across the panel, measured from the value axis.

Added

  • pj/overlay marks a pose so that every lay-* added after it joins the panel it is added to, instead of starting a panel of its own, so two measures meant to be read against one axis are drawn together. The layer keeps its own columns, each axis covers every column drawn on it, and an axis is named for the panel's own column. {:overlay true} on one lay-* call overlays that layer alone, {:overlay false} opts one layer out, and (pj/overlay pose false) turns overlaying off from there on. Where a layer's columns are written does not change which panel the layer lands on -- an :x or :y in the options map asks for a panel exactly as one in an argument slot does.

  • pj/marginal draws a distribution of one of a pose's columns in a thin panel against one edge of the plot: (pj/marginal pose :top) a density of the :x column above, (pj/marginal pose :right) a density of the :y column to the right, drawn on its side. :histogram draws bars instead of a curve, and :size sets the strip's share of the height or width, 0.25 by default. The strip shares the main panel's axis for that column, and the two drawing areas are aligned so a value sits at the same place in each. :bottom, :left, and a second marginal on the same pose each report an error.

  • :share-scales accepts a temporal axis: every cell is ticked over the union of the cells' extents, so panels sharing a date axis carry the same labels as well as the same range. A pj/arrange of time series with {:share-scales #{:x}} lines its cells up. A categorical axis still reports an error, since a category has no extent to share.

  • :tooltip is an aesthetic: map a column to it and its values are what each mark says on hover, or map a string and every mark of the layer says that. The point and interval-h marks carry a tooltip, and a mapping written on any other layer type is accepted and draws none. A column of hiccup gives a tooltip with markup; a string is shown as text. - thanks, @otfrom

  • :bar-width sets how wide a bar is drawn: on a categorical axis, the fraction of the category band it fills, 0.8 by default, as :box-width does for a box; on a numeric or temporal axis, a width in data units. A narrower bar over a wider one leaves the wider bar visible, so two bar layers can be read together. - thanks, @timothypratley

Fixed

  • An axis whose domain is shared across the cells of a composite is padded as an unshared axis is, so the outermost marks stand inside the panel rather than on its edge. A shared axis carrying bars reaches past the tallest bar while the baseline stays on the panel edge, and a :domain written with pj/scale is unchanged.

  • :share-scales accepts an axis only one cell names. A right-hand pj/marginal on a plot whose :x holds dates is drawn, and an axis that cannot be shared still reports an error naming the column.

  • An area, a density and a histogram under (pj/coord :flip) are measured from the value axis, so each runs across the panel, and an area closes at zero.

  • A categorical column mapped to both an axis and :color draws one palette colour per category, and the marks and the legend agree. - thanks, @timothypratley

  • A bar is as wide with a colour column as without one. Dodged bars still divide the band between their groups. - thanks, @timothypratley

  • A layer keeps its own mappings and layer-type options when a later pj/lay-* call splits the pose into panels, so a split does not change what the layer draws. A layer option written on a sub-pose's layer is accepted rather than reported as a typo. A layer's :mapping holds those options -- :bins, :bar-width, :jitter -- beside its aesthetics.

[0.10.1 - 2026-08-27]

Fixed

  • A scatterplot matrix -- the grid pj/pose builds from a rectangular pair list such as pj/cross produces -- places each panel by its columns: :x decides which column it sits in and :y which row, so the tick numbers along the bottom row and down the leftmost column describe every panel above and beside them. The grid was built transposed, so those numbers were true of one row and one column only and every other panel was read against a neighbour's scale. A matrix drawn before this change comes out transposed after it.

[0.10.0 - 2026-08-25]

Ticks and their labels are most of this release. A number axis, a date axis, a size or alpha legend and a gradient bar are each ticked at round values, and a column drawn on an axis and in a legend at once is labelled the same way in both.

A legend and the marks it explains are drawn from one decision, so a reader can match one against the other.

:include names a value an axis has to reach. (pj/scale pose :y {:include 0}) puts zero on the axis, so a length drawn along it is proportional to the value it stands for.

Many thanks to @phronmophobic & @timothypratley who affected this release a lot.

Renamed

The retired name reports the one to write.

GoneWrite instead
layer-type/channel-magnitude-fnlayer-type/aesthetic-magnitude-fn

Every published docstring and arglist says aesthetic where it said channel -- pj/scale takes [pose aesthetic scale-type] -- and the errors layer-type/register! throws about :varies carry :aesthetic in their data.

Plots that look different after upgrading

  • Every numeric axis, size legend and alpha legend whose values are whole numbers. The ticks are whole too: a column of 0, 1, 2 and 3 is ticked 0, 1, 2, 3. An axis carrying a value between two whole numbers, one given a :domain written with a fraction, and a :position :fill that draws proportions are ticked as before.
  • Every log axis whose domain does not reach its outermost breaks. A tick outside the domain is not drawn. A domain too narrow for two breaks is ticked across itself instead: 6 to 9 is ticked 6, 7, 8, 9. ggplot2 answers such a domain the same way round, by picking breaks inside the range rather than by filtering.
  • Every log axis spanning a few decades. Ticks are chosen by how many of them the axis draws rather than how many the generator offers, so an axis whose powers of ten mostly fall outside its domain carries 1-2-5 or 1-2-3-5 breaks between them. A domain holding three or more powers of ten inside it is ticked at those.
  • Every continuous legend on a log scale. The gradient bar is labelled only at the ticks that fall on it, and a domain too narrow for two breaks is ticked across itself as an axis is. A size legend, an alpha legend, a gradient bar and an axis read one rule for a log scale.
  • Every date axis. Its ticks step by a round number of years, months, weeks, days, hours, minutes, seconds or milliseconds, and each one lands on a multiple of that step counted from the calendar rather than from the first value. A span of 1938 to 1971 is ticked 1940, 1945, 1950, a year of monthly readings begins at January, and a weekly axis lands on Mondays. ggplot2 places its date breaks the same way.
  • Every date axis whose ticks step by a whole year or a whole month. The label carries the unit the ticks step by and nothing finer: fifteen years of January firsts are ticked 2002, 2004, 2006, and a year of month starts is ticked Jan 2020, Mar 2020, May 2020. An axis of a few months carries the month alone; one covering about a year carries the year too. ggplot2 labels both spans the same way.
  • Every date axis whose ticks step by whole days. The ticks carry their month as well as their day: Jan-02, Jan-05, Jan-08. An axis whose days run across two years names the year in their place, and one whose ticks step by hours is unchanged. ggplot2 labels the same days with a month too.
  • Every stacked bar, area and step drawn from a zero baseline. The baseline sits on the edge of the panel, where an unstacked bar's already sat. Two stacks of 30 and 70 span 0 to 73.5.
  • Every stacked bar, area and step. The groups pile up in the order the legend lists them, the first on top, and :position :fill stacks the same way round. ggplot2's position_stack stacks this way too.
  • Every horizontal dodged chart. The groups within a band run from the top down, in the order the legend lists them, and bars, boxplots, violins, error bars and point ranges are placed alike. A vertical chart is unchanged: its groups run left to right. ggplot2 runs a horizontal dodge from the bottom up; Plotje matches its legend instead.
  • Every stacked or dodged step. pj/lay-step reads :position, so a step drawn with one is stacked or dodged like any other mark.
  • A :domain on a categorical :color. The order it gives reaches the marks as well as the legend, so the segments of a stacked bar and the members of a dodged group follow the rows of the legend beside them -- bars, boxplots and violins alike.
  • Every plot setting a :midpoint. The gradient is centred on the midpoint and widened until it reaches whichever end of the domain is further from it -- ggplot2's rescale_mid -- so a departure of the same size in either direction is drawn at the same saturation, and the nearer end stops short of the extreme.
  • A :midpoint at an end of its domain. The value there is drawn at the middle of the gradient, so {:midpoint 0} on a column of zero and up draws zero at the middle colour.
  • Every faceted plot with a :size, :alpha or numeric :color column, and every plot with two such layers. The aesthetic is read against every value the plot holds rather than against one panel's or one layer's, so a value means one size, one opacity and one colour wherever it is drawn, and the legend explains all of them. ggplot2 keeps one scale across facets and layers as well.

A value the axis has to reach

  • :include on an :x or :y scale names a value the axis has to reach, as ggplot2's expand_limits(y = 0) does. The value lands exactly at the edge of the panel, where pj/lay-bar already draws its baseline -- ggplot2 pads past it instead. A collection names more than one: {:include [0 100]} gives an axis room the data does not fill. On a temporal axis it is written as a date, as :domain and :breaks are, and the axis is ticked across the domain it widened.
  • A :domain and an :include on one aesthetic are reported, as is an :include on a log axis, which has no reading for zero, or on a categorical one, which has no extent between its categories.

Checking a pose

  • pj/valid-pose? and pj/explain-pose check a pose against its schema, as the pair pj/valid-plan? and pj/valid-membrane? already did for the two later stages. The schema is structural: it says what shape a pose has, not whether the columns it names are in the data. A pose the API built conforms by construction, so the pair is for one that has been reached into and changed.

Reported rather than passed over

  • An axis :domain is checked against the column it is read for. Against a continuous column it is two finite numbers, and against a categorical one it is the list of categories that orders them.
  • :x-type, :y-type and :color-type report a type the column cannot be read as. Only :categorical can be asked of a column holding something else; :numerical and :temporal are accepted where they name the column's own type. The message names the option, the column and what the column holds.
  • A row dropped for a missing value on a non-numeric column says so, as a row dropped for a missing number already did: Removed 1 rows with a missing category in :species. A column holding no values at all reports a missing value rather than a category. Marks that keep such rows rather than dropping them stay silent.
  • A stat that needs a numeric column reports one it cannot read, whatever the column is named, and names a way forward: dates written as text stay categorical until they are parsed. The conversion it names is the one that reads that column, so a plain date is offered :local-date and a timestamp written 2015-03-21T10:00:00Z is offered :instant. pj/lay-histogram, pj/lay-density, pj/lay-smooth, pj/lay-tile and pj/lay-contour all report it.
  • A mark that groups by category says an empty column has no rows, and one holding nothing but nil that it has no values, rather than calling either numerical.
  • :text given a value that names no column says which value it was.
  • A mark that draws one colour per group reports a numeric :color column, which it cannot read as a gradient -- bars, boxplots, violins, lollipops, lines, steps, areas, histograms, densities, smooths, rugs, text and labels among them. :point is the mark that carries a colour per row, and a contour reads the :color scale its levels are built from. The message names :color-type :categorical.
  • pj/to-drawing and pj/to-data report a pose, a plan or the whole frames map written in the panel's place. A panel entry is one element of (:panels (pj/frames plot)), and each message names which of the three it was given and the call that reaches a panel entry from there.

Fixed

  • pj/lay-area and pj/lay-lollipop rest on a log value axis: an area fills to the bottom of the panel and a lollipop keeps its stems. The baseline is the panel's own smallest value, which is where pj/lay-bar already put it. Both closed to zero instead, which a log scale has no reading for, so an area drew a bowtie and a lollipop lost every stem.
  • A histogram bar that cannot be drawn on a log value axis is dropped, and the drop is reported. An empty bin counts zero, so the bar's top scaled to an infinity that reached both the SVG and the Membrane drawables: Java2D clipped it without a word, and Skia refused the drawing with Value out of range for float: Infinity. ggplot2 drops the same bars.
  • A ZonedDateTime and an OffsetDateTime column is read as a date. Each carries its own offset, so each names an instant, and both are read at UTC as the other temporal types are. A ZonedDateTime was refused by the tick generator, and an OffsetDateTime was drawn as one tick per value without a word.
  • A column of values naming a stretch of time rather than a moment is reported: a YearMonth, a Year, a MonthDay. The message names what the column holds and the line that maps it to the day it starts. Which instant a month stands for is left to the writer.
  • A temporal axis takes a :domain written as dates, as its :breaks do, and is ticked across it. A domain wider than the data reaches both ends of the axis.
  • A date axis takes :breaks written as dates: (pj/scale :x {:breaks [(jt/local-date 1940 1 1) ...]}) places a tick at each of them, and :tick-labels relabels them. A LocalDate, a LocalDateTime and a java.util.Date are all accepted.
  • A date axis spanning under two seconds is ticked on round fractions of a second, and each tick is labelled to the precision its own step needs: half a second of data reads 00.0 through 00.5. Milliseconds were the one unit the step list did not reach, so such a span was divided by the tick count instead and labelled at tenths.
  • A temporal axis with a single tick is labelled, and the tick is drawn where the value sits. The tick sat at the edge of the panel, carrying a date years away from the mark.
  • Rows written as vectors are read as data: (pj/lay-point [[1 2] [3 4]]) draws two points, with columns 0 and 1 inferred as the :x and :y aesthetics. pj/pose accepted the same shape throughout.
  • A layer that names no scale has no opinion about it on every aesthetic, as it always has on an axis, and is drawn against the one scale the plot settles on. Two layers that each name a different scale are still refused.
  • A size or alpha legend labels only values the data reaches, so a swatch always pairs with a value some mark carries.
  • A continuous legend's bar is drawn through the normalization its marks are, so a :midpoint moves the bar with them. The bar spans the stretch of gradient the data reaches and no more, and the colour at the end labelled with the domain's low value is the colour the lowest mark carries.
  • A :fill legend is drawn through the same scale its marks are. A :domain set on :fill moves the bar together with the tiles, and a setting written in a scale spec wins over the matching plot option -- :range and :midpoint alike. A tile reads :color as a synonym for :fill key by key, so a :fill spec naming only a gradient leaves the domain to :color.
  • A log :fill legend spans the values the tiles carry. Where the domain was read from the stat's zero floor, the bar carried a label for every decade from 1e-300, printed over each other. A fill that reaches zero even so -- a :domain written from it, or a density that underflows -- is reported.
  • A contour's legend reads the :color scale its levels are built from. :fill paints the interior of a tile and a contour draws lines, so a :fill scale set on one changes neither the lines nor the bar beside them.

[0.9.0 - 2026-08-19]

A mapping may now be written out in full, saying which column or value it takes and whether to read it through a scale -- {:size {:column :weight :scale :log}}. A scale is written in the mapping it belongs to, and a scale set higher up carries down to the layers below it.

Three chapters are new to the book: Specifying Aesthetics, Column Types and Scales.

Removed and renamed

The names below no longer work, and every place one can be written reports it: pj/options, pj/scale, pj/with-config, pj/set-config!, plotje.edn and the :config option.

GoneWrite instead
:palette:color-values, or :values in a :color scale
:color-scale:color-range, or :range in a :color or :fill scale
:tick-spacing-x, :tick-spacing-y:x-tick-spacing, :y-tick-spacing, or :tick-spacing in an axis scale
:labels in a scale:tick-labels
:x-scale, :y-scale, :size-scale and the rest in a pose's :optspj/scale, which now writes the mapping
pj/scale on :groupscale :color or :shape instead

Plots that look different after upgrading

  • Every plot with a :size column. A value's place in its domain is square-rooted before it becomes a radius, matching ggplot2's scale_size. {:by :linear} gives the previous behaviour, ggplot2's scale_radius.
  • Every plot with an :alpha column. The default range is 0.1 to 1.0, matching ggplot2. {:range [0.2 1.0]} restores the previous one.
  • Every plot pairing a numeric :color column with pj/scale. A scale spec changes only what it names and leaves the gradient alone. The gradient is set with :range.
  • A :size or :alpha column whose values are all equal. Every mark is drawn at the middle of the domain, read through the scale: radius 6.243 by default.
  • :from-zero on a column holding zero or negative values. Distance from zero is what counts, so -5 and 5 draw the same size.
  • A categorical :color given a :domain. The list sets the order of the categories, and the legend and the colours follow it.
  • A plot setting both a scale's :label and the matching :x-label or :y-label. The :label in the scale wins.

A mapping written in full

  • A mapping may say which column or value it takes and whether to read it through a scale: {:color {:column :variety :scale true}}, {:color {:column :hex :scale false}}, {:color {:value "blue" :scale false}}, {:color {:value "Model A" :scale true}}.
  • A column can be drawn just as it is, with no scale. {:size {:column :r :scale false}} uses the column's numbers as radii and {:color {:column :hex :scale false}} uses its strings as colours -- ggplot2's scale_size_identity() and scale_colour_identity(). Neither gets a legend.
  • :x and :y take the full form too: {:x {:column 0}} reads column 0, and {:x {:value 0}} places every mark at zero. :scale false on an axis places marks by drawing units from the top left of the panel background instead of by data value.
  • Whether a mapping value names a column or is a value to draw depends on the data, and works the same way for every aesthetic: {:color :red} reads a :red column if the data has one, and otherwise draws red.

The scale spec

  • pj/scale and a mapping's :scale take the same forms: true, false, a scale type, or a whole spec map. A scale set higher up carries down, key by key, and the setting closest to the layer wins -- so a pose that sets a range and a layer that sets a type give a plot with both.
  • pj/scale :size takes :range, :by and :from-zero; :alpha takes :range and :from-zero. :range is what the aesthetic spans: on :size that is a radius in drawing units, [2 8] by default.
  • :domain now works on :size, :alpha and :color (issue #39). A value outside a numeric domain is drawn at the nearer end instead of being dropped. The column's type decides how the domain is read, on every aesthetic that takes one. On :fill a :domain moves the marks and not the legend, so the two disagree -- issue #43.
  • An aesthetic has a single scale, so two layers cannot read it through different ones. The error suggests pj/arrange, where each cell has its own scales and legend.
  • Giving a scale a key it does not use is an error, both in pj/scale and in a mapping, and so is giving one a value it cannot carry out -- a marker symbol that does not exist, :tick-labels without :breaks. :x-end, :x-min, :x-max, :y-min, :y-max and :text take no scale at all, like :group.
  • pj/aesthetic-scales lists what each aesthetic's scale accepts, alongside pj/config-key-docs, pj/plot-option-docs and pj/layer-option-docs.

One setting, one name

Many scale settings can be written in two places: in the scale itself, or as a plot option that applies to the whole plot. The plot option is named after the aesthetic and the setting, joined by a hyphen -- :label on :x is :x-label, :values on :color is :color-values -- and the setting written closest to the layer wins. Not every setting has a plot option, but where one exists this is its name.

  • An axis takes its tick text from :tick-labels, paired with :breaks; :label sets the axis title. Using one where the other belongs is now an error.
  • :n-ticks and :tick-spacing both control how many ticks a numeric axis draws, and it uses whichever you set. A categorical axis draws a tick per category, and :n-ticks thins them.
  • :color takes its colours from its own scale: :values is the palette for a categorical column, :range the gradient for a numeric one, and :midpoint the value drawn at the middle of that gradient. :fill draws a magnitude, so it needs a numeric column and takes :range and :midpoint alone. Each of those has a plot option of the same name.
  • :label sets the title of whatever explains a scale -- the axis for :x and :y, the legend for the rest -- and every aesthetic with a scale takes it.

Extending a layer type

  • A layer type says which appearance aesthetics its mark varies per row, and what it draws them as: (layer-type/register! :bubble {:mark :bubble :varies {:size :radius} ...}). Of the built-in layer types only :point varies an aesthetic, so this mainly matters when writing an extension.
  • layer-type/channel-magnitude-fn gives a mark's renderer the function its legend is built from, and layer-type/quantities lists what a mark can draw an aesthetic as.
  • A :size or :alpha column gets no legend when no mark on the plot varies it, and Plotje says so.

Pose and plan shape

  • pj/scale writes the mapping rather than the pose's options. pj/coord and pj/facet still write :opts.
  • A size-legend entry carries :magnitude rather than :radius, and the legend carries :quantity and :swatch.

Reported rather than passed over

  • Messages say aesthetic where they used to say channel.
  • Mistakes in a full mapping -- an unknown key, a column and a value at once, a source of nil, or neither -- are reported at the pj/pose or lay-* call instead of later.
  • Setting :scale false and then setting an option for that scale is an error, since nothing would read it. :scale false on an axis is also an error on marks that cannot use it.
  • Giving :size, :alpha or :fill a categorical column reports which aesthetic and which column. :fill and :group report a value that names no column, and a hex colour missing its # is reported on annotations as well as layers.
  • A configuration key Plotje does not read is reported wherever it is written: plotje.edn, pj/set-config!, pj/with-config and the :config option. :strict turns the report into an error, as it does for options.

Fixed

  • A gradient and a colour scale type can be set together: (pj/options {:color-range :viridis}) beside (pj/scale pose :color :log).
  • A log colour legend labels its decades, as a fill legend does.
  • A size or alpha legend measures its own text, so its numbers and a long :size-label fit the canvas at any range.
  • A panel where nothing places a mark through the x scale falls back to the [0 1] domain the y axis has always used.
  • An annotation takes a colour by either spelling: (pj/lay-rule-h pose {:y-intercept 2 :color :red}) draws red.
  • :domain-padding is read through the configuration chain, so (pj/options {:domain-padding 0.0}) and pj/with-config both reach it.

Repository

  • run_tests.sh, release.sh and snapshot.sh find bash through #!/usr/bin/env bash, so they run on distributions that do not keep it at /bin/bash, such as NixOS. (PR #35) - thanks, @otfrom

[0.8.1 - 2026-08-12]

  • fix: tick labels, legend endpoints and tooltip numbers are formatted under Locale/ROOT, so a plot draws the same numbers whatever machine renders it.
  • new configuration key :decimal-separator draws the decimal point as whatever string you name: (pj/options {:thousands-separator "." :decimal-separator ","}) writes 1.234,5. Off by default.
  • fix: a continuous legend's two end labels are written to six significant digits in plain notation, with trailing zeros dropped: 123456, 0.1, 2.5.
  • fix: the tick labels of a log fill legend drop their trailing zeros too: 0.001, 0.01, 0.1.
  • fix: pj/save honors every plot option pj/plot honors, in SVG and PNG -- :title-font-size, :label-font-size, :thousands-separator, :grid-stroke-width and :annotation-stroke among them.
  • fix: a composite's cells read the composite's options. A cell still decides its own :tooltip.
  • fix: a composite's title honors :title-font-size, and its shared legend honors the configuration its cells honor, :thousands-separator among it.
  • strip labels read :strip-font-size wherever they are drawn, in a facet and in a grid composite alike. The default is 11.
  • fix: pj/to-data reads a categorical axis, answering with the category whose band holds the position, and nil outside every band.
  • fix: pj/to-drawing refuses a value a categorical axis has no position for -- a category it does not carry, or a fractional place such as 2.5 -- naming the value and the categories it could have been.
  • fix: under :coord :flip, pj/to-data's dataset arity reads a categorical x back as its categories. Both functions take and answer in data order under a flip.
  • fix: a missing column is named, with the available ones listed, on a dataset whose column names are not all of one type.
  • fix: (pj/lay-point data {:x :height :y :weight}) reads the mapping from its options map on a dataset of four or more columns. A mapping given only partly there, such as {:y :weight}, still takes its other axis from the inferred one.

[0.8.0 - 2026-08-07]

This release mostly follows the label-positioning topic thread. Many thanks to @behrica, @timothypratley, @phronmophobic, and @generateme for the fruitful work over that thread. See the new Placing Marks documentation chapter for the main fruits of this release.

  • new function pj/frames reports where a plot's panels sit on the canvas: per panel, the :panel-box and :drawing-area rectangles in drawing units, plus the :canvas once for the plot. A composite reports every cell in canvas coordinates. Plain data throughout. - thanks, @timothypratley
  • new functions pj/to-drawing and pj/to-data map between data space and drawing space for one panel of pj/frames. Both take a single x and y, or a dataset with :x and :y columns, and answer in kind. pj/to-data throws under :polar, which has no inverse; a panel's :invertible? says which case it is.
  • new layer options :offset-x and :offset-y shift a layer by a number of drawing units, after the scales run -- the distance a label needs to clear its point. A nudge cannot: it shifts by a data amount, and throws on a categorical axis. Every layer type takes them, and a numeric domain widens to keep an offset label whole. - thanks, @behrica
  • :x and :y may be given as a value, not only as a column. Both as values draws one mark: (pj/lay-text pose {:x 6.5 :y 3.5 :text "mean"}). One value beside a column repeats for every row, so {:x 33 :y :revenue} labels each row at one fixed x. The value counts toward the axis domains, as ggplot2's annotate() does. - thanks, @behrica
  • new layer option :in names the space a layer's :x and :y are in: :data (default) or :drawing-area, which measures drawing units from the top left of the panel background. (pj/lay-text pose {:in :drawing-area :x 12 :y 12 :text "n = 150"}) puts a note in the corner without touching the domains.
  • pj/lay-rule-* and pj/lay-band-* warn that they do not accept :in, instead of taking it and doing nothing. They are placed from data values only.
  • fix: :group no longer discards a fixed :color. (pj/lay-line data :x :y {:group :country :color "#d0d0d0"}) draws one line per country, all in that grey; before, each line took a different palette color.
  • fix: {:tooltip true} and {:brush true} work in the browser again. Since 0.7.0 the Scittle scripts reached the page with " where they wrote ", and a script element's content is raw text in HTML, so nothing decoded them back and neither interaction ran.
  • fix: the brush's selection rectangle follows the drag when CSS scales the plot. In a column narrow enough to halve the plot, a drag of 150 by 100 drew a marquee of 75 by 50, at half the distance from the corner. Which rows a release selects is unchanged.
  • a text mark that widens its axis to be drawn in full now leaves a few drawing units of air, so the last glyph no longer touches the panel edge.
  • docstrings say drawing units where they said pixels: one unit of the plot's :width and :height, which is a screen pixel only at the plot's natural size on a standard-resolution display.

[0.7.0 - 2026-08-03]

  • Kindly 4-beta23 -> 4-beta25, brought in by Plotje: pj/plot asks for :kind/hiccup2, which 4-beta23 does not define.
  • fix: (pj/options {:legend-position :top}) draws its legend inside the image, instead of above the top edge where nothing is visible.
  • a keyword category reads as words: :not-applicable labels a tick, legend entry or facet strip as not applicable. String categories are untouched. - thanks, @timothypratley
  • a string column name titles its axis exactly as written: Cost-Benefit Ratio, not Cost Benefit Ratio. Keyword and symbol names are unchanged. - thanks, @timothypratley
  • fix: every pj/lay-* arity rejects a non-column position, as pj/pose already did. Breaking: (pj/lay-point data 0 1 {}) no longer plots integer column names -- rename them first with (tc/rename-columns ds [:x :y]).
  • fix: an axis is labelled without crashing on column names that are neither keywords nor strings; such a name formats as its printed form. (PR #32) - thanks, @timothypratley
  • fix: a data label at the largest value is drawn in full instead of cut off at the panel edge: a numeric domain is widened by however much its text marks need. A domain set with pj/scale is never widened. (Closes #18) - thanks, @behrica
  • new configuration key :fit-text-domain (default true) turns that widening off.
  • fix: an x tick label at the end of an axis is no longer cut in half by the image edge; the outermost labels shift inward when they would otherwise be clipped.
  • fix: mapping a column to :shape produces a legend. One column driving both :color and :shape gives one merged legend, as in ggplot2; two different columns give two legends. (Closes #4) - thanks, @timothypratley
  • new public var pj/shape-symbols, the marker symbols a categorical :shape mapping draws with, in assignment order.
  • seven shape symbols where there were four: :circle, :square, :triangle, :diamond, :triangle-down, :plus, :cross. More categories than symbols repeats one, which now warns.
  • pj/scale reads :shape: :domain sets the category and legend order, and new :values supplies the symbols -- (pj/scale pose :shape {:domain ["gold" "silver"] :values [:diamond :cross]}). An undrawable symbol is rejected, as is :values on any other channel.
  • new plot option :shape-label, titling the shape legend as :color-label and :size-label do for theirs. Naming one half of a merged legend splits it in two.
  • fix: pj/svg-summary no longer counts legend symbols as plot data.
  • fix: a label layer grouped by the column its bars are dodged by is dodged with them, so each label sits over the bar it names. Errorbar and pointrange still take an explicit {:position :dodge}. (Closes #13) - thanks, @behrica
  • pj/lay-text and pj/lay-label accept {:stat :count}, labelling each bar of a counting pj/lay-bar: (-> data (pj/lay-bar :species) (pj/lay-label {:stat :count})). pj/lay-point and pj/lay-line plot those counts too. (Closes #14) - thanks, @behrica
  • fix: PNG output places its text where the SVG does, within a pixel. Membrane's Java2D backend read neither the alignment nor the rotation the SVG backend honors, so every label and title in a saved PNG was misplaced.
  • fix: :x-tick-angle rotates the tick labels in PNG output, not only in SVG.
  • fix: a long :y-label renders in full in PNG output, where it was cut off after about six characters -- the missing rotation above, not the upstream Membrane bug it was listed as.
  • Breaking: the :label mark is gone; :text and :label are two layer types drawn by the same :text mark, so a label layer reports :mark :text in a plan. pj/lay-label and (pj/lay :label ...) are unchanged. Marks drop from 22 to 21; the 25 layer types are unchanged.
  • layer types can preset layer options through a :defaults map on the registry entry, which the call site overrides: :label is :text with {:box true}.
  • text marks can be drawn on a background box: :box true gives a white rounded panel with a thin border, and a map sets its properties -- {:box {:corner-radius 8}}, or 0 for square corners. (Closes #19, with :thousands-separator below; the arrows and out-of-panel text in that issue's image are #17 and #18) - thanks, @behrica
  • a background box has rounded corners by default (radius 3, after ggplot2's geom_label), where it was square.
  • fix: a background box's border is drawn as a stroke, where it was a grey rectangle filled over the box.
  • fix: pj/svg-summary reports label background boxes as :label-boxes, one per label, instead of counting them as :tiles.
  • pj/lay-text and pj/lay-label accept :font-weight (:normal or :bold) and :font-style (:normal or :italic), independent and combining, in SVG and PNG. A numeric CSS weight such as 700, or CSS :oblique, is rejected -- Java2D draws neither. (Closes #21) - thanks, @behrica
  • pj/svg-summary reports :bold-texts and :italic-texts.
  • fix: an SVG's clipPath ids are derived from the clip region's geometry instead of a counter shared across the whole JVM, so rendering the same plot twice produces the same bytes.
  • size, alpha and continuous color legends honor :thousands-separator too, and a whole-valued legend number drops its trailing .0 -- a size legend for a count reads 100,000, not 100,000.0.
  • pj/layer-option-docs describes every layer option. Fourteen were missing, among them :x, :y, :data, :mark and :stat.
  • pj/scale :values on a channel other than :shape now names :palette as the way to choose the colors a categorical :color mapping draws with.
  • new configuration key :thousands-separator groups the digits of large numbers in numeric tick labels and in text taken from a column, so (pj/options {:thousands-separator ","}) draws 462,389. Any string works. Off by default, and never applied to category names, legend entries or facet strips. Part of #19 - thanks, @behrica
  • an options map that rejects a key now names where that key does belong: (pj/lay-bar :x :y {:x-label "sales"}) adds Plot options belong in pj/options: [:x-label]. The pointer works in every direction, and under :strict the same text appears in the thrown exception. - thanks, @timothypratley
  • the pj/lay-point docstring called its trailing map "aesthetic opts"; it holds layer options generally.
  • fix: text containing the characters markup uses reaches a notebook as text: a category named R&D, or a title "Q1 <profit> & loss", was read as markup. pj/plot now asks for :kind/hiccup2. A saved SVG was never affected. (PR #33) - thanks, @timothypratley

[0.6.0 - 2026-07-29]

  • fix: a density curve is now estimated over the data it describes, instead of over a grid running half the data's span past it on either side. That padded grid was both drawn and reported as the axis range, so pj/lay-density on a column spanning 4.3 to 7.9 drew an axis from 2.1 to 10.1 and a pj/lay-rug beside it covered only the middle of the plot. The curve now starts and ends with the rug, and the axis matches what ggplot2's geom_density() produces for the same data. (Closes #23) - thanks, @behrica
  • pj/lay-violin and pj/lay-ridgeline estimate through the same kernel density, so they are bounded by their category's values too -- each body now ends where that category's data ends instead of tapering into a long needle past it. This matches ggplot2, which trims violins by default (geom_violin(trim = TRUE)).
  • pj/lay-density, pj/lay-violin and pj/lay-ridgeline accept :trim, choosing which values each group's curve is estimated over, after ggplot2's argument of the same name and with its per-geom defaults. A density is untrimmed: every group is estimated across the whole layer, so grouped curves share one interval and each falls away to nothing rather than being cut off at its own group's extremes. {:trim true} estimates each group over its own values instead. A violin or ridgeline is trimmed, so each body ends at its category's values; {:trim false} extends it by three bandwidths on each side. This changes the appearance of a grouped pj/lay-density, which previously behaved as {:trim true}.
  • density curves are smoothed with nrd0, the bandwidth rule R's density() and ggplot2 use; the previous default smoothed about 18% wider, drawing every curve slightly flatter and broader than the same data in ggplot2. Affects pj/lay-density, pj/lay-violin and pj/lay-ridgeline. Passing an explicit :bandwidth is unchanged.
  • a density curve is drawn from 512 points rather than 100, matching ggplot2, so an enlarged plot no longer shows a faceted peak. pj/lay-violin and pj/lay-ridgeline keep 80, where the extra points make no visible difference.
  • pj/plot, pj/save, pj/draft, pj/plan, and pj/membrane now accept raw data directly, giving it a default mapping first exactly as pj/pose does. Previously (pj/plot some-dataset) (data not wrapped in pj/pose) rendered a blank figure; it now renders the same inferred default as (pj/plot (pj/pose some-dataset)).
  • new public pipeline step pj/infer-mapping: given a pose that carries data but no mapping (the bare leaf pj/->pose produces), it attaches a default position/color mapping from the first 1-3 columns; it is a no-op on any pose that already has a mapping, has layers, is composite, or has 4+ columns. This is the step the terminal shortcuts apply after pj/->pose, exposed so pipeline-minded users can build their own chains ((-> data pj/->pose pj/infer-mapping pj/pose->draft pj/draft->plan)).
  • a bare collection of scalar values -- numbers, strings, or keywords, e.g. [1 4 1 5 6] -- is now accepted as plot data and read as a single column named :value (previously only {:column [values]} maps or sequences of row-maps were accepted). Combined with the default-mapping change, (pj/plot [1 4 1 5 6]) renders a histogram.
  • pj/save now returns the written file as a java.io.File carrying :kind/image metadata (instead of the path string), so evaluating a pj/save call in a notebook also displays the saved chart. The file prints as its path and compares equal to a plain java.io.File on the same path, so (str (pj/save ...)) still yields the path string. (PR #29) - thanks, @timothypratley
  • add plotje-plot as a class on svg output (PR #28) - thanks, @timothypratley
  • fix: pj/svg-summary counts square markers. A :square marker draws as a rounded rectangle of radius 0, which fell between the summary's point test and its tile test, so (pj/lay-point :sepal-length :sepal-width {:shape :species}) on a 150-row dataset summarized as 100 marks. Squares now count in :points alongside circles, which also brings their colors and opacities into :colors and :alphas; :sizes still reports only positive radii.

[0.5.0 - 2026-07-03]

  • pj/lay-density and pj/lay-area accept an opt-in outline on the curve: :stroke (outline color) with optional :stroke-width. The fill still comes from :color, so (pj/lay-density :x {:color "lightblue" :stroke "black"}) draws a light-blue area with a black outline. The outline strokes only the top curve, not the baseline. Without :stroke the appearance is unchanged. (Closes #11) - thanks, @behrica
  • dashed and dotted strokes: pj/lay-line, pj/lay-step, pj/lay-smooth, the reference lines pj/lay-rule-h / pj/lay-rule-v, and a density/area outline accept :stroke-dash, either a named preset (:dashed, :dotted, :solid) or a raw [dash gap ...] pixel pattern ({:stroke-dash [6 3]}). Renders through both the SVG and PNG (Java2D) paths. (Closes #12) - thanks, @behrica
  • pj/svg-summary reports :dashed-lines (count of polylines carrying a stroke-dasharray) and :dash-patterns (the distinct stroke-dasharray strings), for asserting that a dashed line, rule, or area outline rendered dashed and with which pattern.

[0.4.0 - 2026-07-01]

  • Breaking: pj/lay-value-bar is removed. pj/lay-bar now covers both cases: with x only it counts each category (as before), and with a y column it uses the y value as the bar height (the former pj/lay-value-bar). The stat is inferred from whether a y column is present and is overridable with {:stat :count} or {:stat :identity}. To migrate, replace (pj/lay-value-bar data :x :y) with (pj/lay-bar data :x :y). This also lifts the previous "stacked bars reject pre-aggregated counts" limitation -- pj/lay-bar with {:position :stack} and a y column now stacks pre-computed values. - thanks, @timothypratley
  • pj/lay-bar value bars now accept the categorical axis on either x or y: (pj/lay-bar :value :category) with a categorical y draws horizontal bars directly, no pj/coord :flip needed (matching how pj/lay-boxplot auto-orients). Plain and dodged horizontal bars are supported; stacked/filled horizontal bars still need (pj/coord :flip).
  • pj/lay-bar with two numeric or temporal axes now draws a bar at each x position -- a numeric-position or time-series bar chart ((pj/lay-bar :month :revenue)), which previously errored. Bar width defaults to 0.9 of the smallest gap between adjacent x values; set it with {:bar-width n}. Grouped numeric bars currently overlap rather than dodge.
  • pj/lay-bar's categorical-x error now points to the {:x-type :categorical} override and (pj/coord :flip), matching the guidance other categorical-axis marks already give.
  • pj/valid-membrane? and pj/explain-membrane validate a membrane against its Malli schema, mirroring the existing pj/valid-plan? / pj/explain-plan pair for plans.
  • fix: render-stage options set on a pose with pj/options -- notably :theme, but also :palette -- now flow through the explicit pj/draft->membrane and pj/draft->plot steps, not only through the pj/plot / pj/membrane shortcuts. These steps default their options to the draft's own options (any options passed explicitly override per key), so a theme set before drafting is no longer dropped at the membrane stage. (Closes #20) - thanks, @behrica
  • fix: a panel's marks are now clipped to the panel. Geometry running past the axis domain -- a pj/lay-line reference line drawn beyond a narrowed pj/scale domain, say -- is masked at the panel edge instead of painting across neighbouring panels in a pj/arrange or facet layout. A narrowed :domain acts as a view window (like ggplot2's coord_cartesian): the data is kept, only the view is bounded. (Closes #16) - thanks, @behrica
  • pj/options accepts :x-tick-angle to rotate x-axis tick labels (in degrees; -45 is a common diagonal), so dense or long categorical labels stay readable instead of overlapping. :x-tick-label-pad overrides the vertical space reserved below the panel for the angled labels. The rotation flows through pj/save (SVG and PNG) as well as the notebook pj/plot path. Long labels can still run past the left plot edge (see Known Limitations). (PR #6) - thanks, @tombarys
  • pj/scale accepts :n-ticks on a categorical axis to thin a crowded axis to about that many evenly-spaced tick labels, instead of labelling every category ((pj/scale :x {:n-ticks 8})). An alternative to rotating the labels for dense categorical axes. (PR #25) - thanks, @behrica
  • fix: pj/scale :breaks and :labels now work on a categorical axis, not just numeric ones. On a discrete axis :breaks selects which categories get a tick (each matched to a category by its displayed label) and :labels relabels them; a break naming no category is dropped with a warning (an error under :strict). Previously the categorical branch ignored both. When both :breaks and :n-ticks are given, explicit :breaks win and no thinning is applied. (Closes #22) - thanks, @behrica

[0.3.1 - 2026-06-02]

  • Layers sharing a panel now paint in the order they were added -- each pj/lay-* call renders on top of the previous one -- instead of being reordered by position type. A pj/lay-text or pj/lay-label added after a bar now reads on top of it rather than being hidden underneath.
  • pj/lay-text and pj/lay-label accept :align-x (:left/:center/:right) and :align-y (:top/:center/:bottom) to set which part of the label sits on the data point -- e.g. :align-x :right places a value label inside a bar's end. Defaults :left/:center preserve the previous placement.
  • :nudge-x/:nudge-y on a categorical axis now raise a clear error pointing to :align-x/:align-y (and :jitter/:position :dodge). Nudge is a data-space shift and applies only to numeric or temporal axes.

[0.3.0 - 2026-05-28]

  • pj/lay-* with different x/y columns from the existing pose now produces a two-panel composite instead of throwing.
  • When pj/lay-* would create a new panel using columns that don't exist in the data, the error now fires at the lay call with a clear message, instead of later during pj/plan or pj/plot.
  • When a layer carries its own :data but the pose's x/y columns are missing from it, the error now names where the missing column came from and suggests two fixes: rename the column to match, or set a different x/y on the layer.

[0.2.2 - 2026-05-19]

  • fix: pj/scale :y :log now works on histograms and categorical bar charts. (Closes #5) - thanks, @harold.
  • fix: SVG coordinate formatter now pins java.util.Locale/ROOT, so plots render correctly on JVMs whose default locale uses comma as the decimal separator (Czech, German, etc.). (PR #3) - thanks, @tombarys

[0.2.1 - 2026-05-09]

  • pj/scale accepts :labels paired with :breaks -- render numeric tick positions with custom text (e.g. days of the week 1-7 labelled "Mon"-"Sun" on a tile heatmap). Length must match :breaks; :labels without :breaks throws.
  • docstring updates

[0.2.0 - 2026-05-05]

  • the membrane stage now returns a PlotjeMembrane record implementing the Membrane UI protocols (IOrigin, IBounds, IChildren), so Plotje plots compose with hand-built Membrane elements. Width and height read via (membrane.ui/width m)/(height m); title rides as :plotje/title. Replaces the prior metadata-tagged-vector contract.
  • new pj/membrane? predicate

[0.1.0 - 2026-05-03]

  • initial public alpha release
  • composable five-stage pipeline: pose -> draft -> plan -> membrane -> plot
  • layer types for distributions, ranking, time series, relationships, and polar
  • composite poses with faceting and shared scales
  • SVG and PNG rendering via membrane

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close