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.7.0 - 2026-08-03]

  • Kindly moves from 4-beta23 to 4-beta25, and is now a minimum rather than a preference: pj/plot asks for :kind/hiccup2 (see the markup entry below), which 4-beta23 does not define. A project pinning the older version gets No such var: kind/hiccup2 when it renders a plot.
  • fix: (pj/options {:legend-position :top}) draws its legend inside the image. Layout reserved a band for it between the title and the panels, but the renderer left the panels at the top and drew the legend a band's height above them -- measured, y = -70 on a 400-pixel image -- so the plot lost the space, the empty band opened up at the bottom, and the legend landed where nothing is visible. Every legend kind was affected; :right, :bottom and :none were always correct.
  • a keyword category reads as words wherever it is shown, so :not-applicable labels a tick, a legend entry or a facet strip as not applicable. A keyword cannot hold a space, so a hyphen or underscore in one nearly always stands for a word break -- the rule column names already followed, now applied to the data as well. String categories are untouched. :a-b and :a_b consequently format alike, which combines them into one band on a categorical axis (not under :color, where only the legend text repeats); Plotje now warns and names both values and the column when that happens. - thanks, @timothypratley
  • a string column name titles its axis exactly as written, where its hyphens and underscores were previously turned into spaces -- a column named Cost-Benefit Ratio titled its axis Cost Benefit Ratio. A string can hold a space, so a hyphen in one was chosen rather than substituted. Keyword and symbol names are unchanged, since neither can hold a space, and :x-label / :y-label still override any derived title. - thanks, @timothypratley
  • fix: every pj/lay-* arity gives the same answer when a position is not a column reference. pj/pose has always rejected (pj/pose data {:x 5}) by naming the column and pointing at tc/add-column for a constant position, but an x or y passed positionally skipped that check, so each arity failed its own way: (pj/lay-point data :x 5) reported find not supported on type: java.lang.Long, (pj/lay-histogram data 5) dropped the argument in silence, and (pj/lay-point data 0 1 {}) accepted an integer column name and plotted it. All now run the same check. Breaking for that last spelling -- rename the columns first with (tc/rename-columns ds [:x :y]).
  • fix: an axis is labelled without crashing on a dataset whose column names are neither keywords nor strings. tc/dataset names columns by position when it is given none, so (pj/plot (tc/dataset [[1 2] [3 4]])) threw class java.lang.Long cannot be cast to class clojure.lang.Named. Such a name now formats as its printed form, giving an axis reading 0. Integer names remain readable rather than writable, so (tc/rename-columns ds [:x :y]) is still the way to go on to plot such a dataset by name. (PR #32) - thanks, @timothypratley
  • fix: a data label at the largest value is drawn in full instead of being cut off at the edge of the panel. (-> data (pj/lay-bar :tickets :violation) (pj/lay-label {:text :tickets})) labelled its longest bar 4623 where the value was 462389: a text mark's size is fixed in pixels rather than measured in data units, so a label anchored at the extreme value reaches past the end of the axis. A numeric domain is now widened by however much its text marks need. Nothing moves on a plot whose labels already fit, and a domain pinned with pj/scale is never widened. (Closes #18) - thanks, @behrica
  • new configuration key :fit-text-domain (default true) turns that widening off, for a plot whose axis range matters more than its labels.
  • fix: an x tick label at the very end of an axis is no longer cut in half by the edge of the image. A centered tick label needs half its width on each side of its tick, and nothing reserved that room the way space is reserved for y tick labels, so a domain ending on a round number -- (pj/scale :x {:domain [0 500000]}) -- drew its last label as 50000. The outermost labels now shift inward by at most half their width, and only when they would otherwise be clipped.
  • fix: mapping a column to :shape produces a legend. (pj/lay-point :sepal-length :sepal-width {:shape :species}) drew three different markers and said nowhere what they meant, because the category-to-symbol assignment was made at render time, per layer, so nothing in the plan knew which category drew which symbol. It is decided once at plan time now, and read by both the marks and the legend, which is what keeps the two from disagreeing. One column driving both :color and :shape gives one merged legend whose keys carry both, 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 -- read it to pick a selection for pj/scale :shape {:values [...]}.
  • seven shape symbols are available where there were four: :circle, :square, :triangle, :diamond, :triangle-down, :plus, :cross, in that assignment order. A plot with more categories than symbols repeats one, so two categories become indistinguishable; that now warns instead of passing silently.
  • pj/scale reads :shape. Its :domain sets the category order -- which is also the legend order, and so the symbol assignment -- and its new :values supplies the symbols to use, paired with the categories in the same order: (pj/scale pose :shape {:domain ["gold" "silver"] :values [:diamond :cross]}). :domain on :shape was documented and read nowhere, so it and the whole :shape-scale key were silently ignored before. A symbol Plotje cannot draw is rejected, as is :values on any other channel: an unrecognized symbol would have drawn as a circle while the legend named the symbol, which is the disagreement the shape legend exists to remove.
  • new plot option :shape-label, overriding the shape legend's inferred column-name title, as :color-label and :size-label do for theirs. Naming one of a merged legend's halves also splits it back into two, since a caller who renames one is asking for two distinct legends.
  • fix: pj/svg-summary no longer counts legend symbols as plot data. A legend key drawn as a triangle, diamond, plus or cross emits a polygon, and polygons were collected across the whole SVG while rects already skipped legend chrome, so a shape legend inflated :polygons and :colors with its own keys.
  • fix: a label layer grouped by the column its bars are dodged by is now dodged with them, so each label sits over the bar it names. Before, every label in a category was drawn at the band centre, so a two-group chart piled both labels on the boundary between the two bars. The label layer joins the bars' dodge cohort, so one group-to-index map covers every layer in it and a label cannot drift from its bar. Marks that position other geometry from a dodge -- errorbar, 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 with its own count: (-> data (pj/lay-bar :species) (pj/lay-label {:stat :count})). A counting bar has no column holding its counts to point :text at, so labelling one previously meant aggregating the data a second time by hand; passing {:stat :count} drew nothing at all, silently. The counting stat reports its counts as points to do this, so (pj/lay-point :species {:stat :count}) and (pj/lay-line :species {:stat :count}) now plot them where before they drew nothing. (Closes #14) - thanks, @behrica
  • fix: PNG output places its text where the SVG does. Membrane's Java2D backend draws every string rightwards from its origin and a line-height below it, reading neither the alignment nor the rotation the SVG backend honors, so a saved PNG ((pj/save pose "plot.png"), and the :bufimg path generally) misplaced all of its chrome text: y tick labels ran into the panel over the data, x tick labels and titles sat half a label too far right, and every string sat about four pixels low. Measured, the two formats now agree within a pixel.
  • fix: :x-tick-angle rotates the tick labels in PNG output. Rotation was applied only in SVG, so the option appeared to do nothing at all to a saved PNG.
  • fix: a long :y-label renders in full in PNG output, where it was cut off after about six characters. This was listed under Known Limitations as an upstream Membrane bug; it was the missing rotation above -- drawn horizontally, the axis title ran straight out of the margin reserved for it.
  • Breaking: the :label mark is gone; :text and :label are now two layer types drawn by the same :text mark, so a label layer reports :mark :text in a plan (code filtering plan layers for :label must look for :text). :label remains a registered layer type and pj/lay-label is unchanged, so ordinary calls and (pj/lay :label ...) keep working. The mark count drops from 22 to 21; the 25 layer types are unchanged. The reason is that a background box is appearance only, and Plotje expresses appearance-only variation as an option -- :stroke on an area, :stroke-dash on a line -- rather than as a second way to draw the same thing.
  • layer types can preset layer options. A registry entry may carry :defaults, a map of option values the layer type starts with and the call site overrides: :label is :text with {:box true}. This is how two layer types sharing one mark differ by the options they begin with, alongside the existing way they differ by stat (:histogram is the :bar mark with :stat :bin).
  • text marks can be drawn on a background box: :box true gives the default box (a white rounded panel with a thin border), and a map sets its properties -- {:box {:corner-radius 8}}, or 0 for square corners. The :label layer type presets it, so (pj/lay-label ... {:box false}) is bare text and (pj/lay-text ... {:box true}) is a label. A misspelled box property or a negative radius is rejected rather than quietly ignored. (Closes #19, together with :thousands-separator below; the arrows and out-of-panel text also visible in that issue's image are tracked as #17 and #18) - thanks, @behrica
  • a background box now has rounded corners by default (radius 3, after ggplot2's geom_label), where it was drawn square before.
  • fix: a background box's border is drawn as a stroke. It was a grey rectangle filled over the white box -- a bare membrane rectangle paints filled in both backends -- so every label read as a flat grey block instead of a white box with a thin outline.
  • fix: pj/svg-summary no longer counts a label's background box as plot data. The square boxes carried no corner radius and so fell into :tiles, reporting six phantom heatmap tiles (two rects per label) for a plot with none. Boxes are now reported on their own as :label-boxes, one per label.
  • pj/lay-text and pj/lay-label accept :font-weight (:normal or :bold) and :font-style (:normal or :italic), so one label can be emphasized among several or set apart as an aside. The two are independent and combine, and both render through the SVG path and the PNG (Java2D) path. Any other value, including 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 (counts of texts carrying each style), for asserting that a label rendered bold or italic.
  • fix: an SVG's clipPath ids are derived from the clip region's geometry instead of a process-global counter, so rendering the same plot twice produces the same bytes. A file's ids previously depended on how many plots the JVM had drawn before it, so re-rendering saved SVG (the readme images, docs/) rewrote every file with no visual change and buried real rendering diffs in the noise. Ids still differ wherever the regions differ, which is what keeps several plots on one notebook page from borrowing each other's clip.
  • size, alpha and continuous color legends honor :thousands-separator too, so a legend no longer reads 400000 beside an axis reading 400,000. A legend number that is a whole value also drops its trailing .0 -- a size legend for a count now reads 100,000 rather than 100,000.0. (A continuous color bar prints large values in scientific notation, which has no digit groups to separate.)
  • pj/layer-option-docs describes every layer option that exists. Fourteen were missing -- :x, :y, :data, :mark, :stat, :bins, :binwidth, :box-width, :cap-width, :length, :level, :stroke, :stroke-width, :stroke-dash -- so the Layer Types chapter named :binwidth in one table and gave no description for it in the next, and rendered :x and :y, the two most fundamental keys in the library, with empty Description cells.
  • 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, rather than only saying what :values is not for.
  • new configuration key :thousands-separator groups the digits of large numbers: the string is inserted between each group of three digits in numeric tick labels and in the text a pj/lay-text or pj/lay-label mark takes from a column, so (pj/options {:thousands-separator ","}) draws 462,389 and an axis reading 100,000. Any string works, covering the space and point conventions used outside the anglophone world. It is off by default, since grouping is wrong for a number that identifies rather than measures and a year axis would otherwise read 2,026; category names, legend entries and facet strip labels are left alone for the same reason. The space reserved for tick labels grows to match. Part of #19 - thanks, @behrica
  • an options map that rejects a key now names where that key does belong, instead of only listing the keys it accepts: (pj/lay-bar :x :y {:x-label "sales"}) adds Plot options belong in pj/options: [:x-label]. The pointer works in every direction -- a layer option passed to pj/options or pj/pose points back to a pj/lay-* options map; a layer option belonging to some other layer type names the layer types that take it (:bandwidth on pj/lay-bar names :density, :ridgeline, :smooth, :violin); a configuration key names pj/options for one plot and pj/set-config! for every plot; :scale-x / :scale-y name pj/scale. A plain typo still gets the accepted-key list and no guess, and under :strict the same text appears in the thrown exception. - thanks, @timothypratley
  • the pj/lay-point docstring called its trailing map "aesthetic opts". That map holds layer options generally -- :bandwidth, :trim, :mark, :stat, :data as well as aesthetics -- and naming it for one of its parts hid the rest.
  • fix: text made of the characters markup is made of survives the trip to a notebook. A category named R&D, or (pj/options {:title "Q1 <profit> & loss"}), was handed to the rendering library under a kind that leaves strings alone, so the & and the <profit> reached the page as markup rather than as themselves. pj/plot now asks for :kind/hiccup2, the kind that escapes what it renders. A saved SVG was never affected -- that path writes the file here and has always escaped -- and nothing about the plot itself changes. (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