29 Known Limitations
Gaps in the current Plotje release. None produce crashes on canonical inputs; each is documented and tracked for post-alpha work.
(ns plotje-book.known-limitations
(:require
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]))Layout and Visuals
Multi-layer overlays like
(-> data (pj/lay-point ...) (pj/lay-smooth {:stat :linear-model}) (pj/lay-smooth {:stat :loess}))do not auto-generate a layer-kind legend to distinguish the two regression curves. Workaround: color each layer explicitly.Histograms, stacked bars, step plots, and other stat-derived marks do not default to a
"count"or"density"y-label.Linear continuous color legends (numeric
:colormapping with:linearscale) label only the endpoint tick marks on the gradient bar. Intermediate values are unlabeled, making it hard to map interior colors back to data values. Log-scaled color and fill legends do carry intermediate ticks.SPLOMs with 6+ variables at the default 600x400 have tight panels. Increase
:width/:heightor pin:panel-width/:panel-height.Horizontal bars from
(pj/coord :flip)render the first row of data at the bottom of the chart, so a dataset sorted descending (natural “top N” order) produces the biggest bar at the bottom. The behavior matches ggplot2’scoord_flip(). Workaround: sort the dataset ascending before plotting, e.g.(tc/order-by data [:value] [:asc]). A future opt-in flag such as(pj/coord :flip {:reverse-categorical true})would spare users the sort.:fit-text-domainwidens a numeric domain so that text and label marks near its edge are drawn in full. Nothing widens the domain for a mark whose size is in drawing units for another reason: apj/lay-pointgiven a large:sizeat the extreme of its domain is cut at the panel edge, as is a long rug tick. The 5% domain padding absorbs this at ordinary radii, and a scale given a wide:range– say(pj/scale pose :size {:range [3 20]})– reaches past it. Workaround: widen the domain withpj/scale.Rotated x-tick labels (
:x-tick-angle) reserve extra vertical space below the panel, but not extra horizontal space. A long label rotated to a diagonal extends to the left of its tick, so very long category names on the leftmost tick can run past the left edge of the plotting area. Workaround: shorten the labels, reduce the angle, or widen the plot with:width.A categorical axis has no place between two categories, and a number written for one is read as another category name. ggplot2 puts categories at 1, 2, 3 and draws a mark at 1.5, halfway along the gap; Plotje’s categorical axis is a band scale, which has a place for each category and none between them. So
(pj/lay-label {:x 1.5 :y 1.5 :text "note"})over a categorical y of["x" "y" "z"]does not report an error and does not go halfway: it adds a fourth category, draws a fourth tick labelled1.5, and puts the label in the new band. Workarounds: name an existing category with{:y {:value "z"}}, which adds no tick, and shift from there with:offset-x/:offset-y, which move a mark by a distance on the page and work on any axis; or draw the axis from a numeric column and label its ticks, since(pj/scale pose :x {:breaks [1 2 3] :tick-labels ["A" "B" "C"]})takes 1.5 as an ordinary value – at the cost of the column no longer being categorical, so bars, boxplots and dodging lose their bands. Wanting a place genuinely between two categories has no workaround; it needs the categorical axis to become a continuous scale carrying a label table, which is designed but not built.A line has no arrowhead.
pj/lay-linedraws a plain stroke, with:stroke-dashfor dashed and dotted styles, so a leader line pointing from a note to the mark it describes ends bluntly. Workaround: end the line on a smallpj/lay-pointmarker, as the callout recipe in the Cookbook does.Nothing moves a label out of the way of another label. Two text marks at nearby positions are drawn on top of each other, and labelling every point of a dense scatter produces an unreadable pile. Workarounds: label a subset, or separate them by hand with
:offset-x/:offset-y. Automatic placement, as in R’sggrepel, is designed but not implemented.A long label at the right edge of a panel costs axis range.
:fit-text-domainmakes room for it by widening the domain, which is the only lever available – there is no way to reserve drawing-space room for labels outside the drawing area. Several sentence-length labels at the line ends can push the axis well past the data. Workarounds: shorten the end labels and put the sentences in a callout, or turn the widening off with{:fit-text-domain false}and accept the clipping.:inplaces a layer in the data or on the panel (:in :drawing-area), and nothing places one on the canvas as a whole. A note outside every panel – beside the legend, or under the title – has no position to be given. Workaround::captionand:subtitlefor text below and above the panels.pj/marginaldraws on:topand:right.:bottomand:leftwould put the strip between the panel and the axis that describes it, so they are reported rather than drawn.A pose gets one marginal at a time.
pj/marginaltakes a leaf pose and returns a composite, and a composite has no single panel to put a second strip beside, so a scatter with a density above it and another to its right has to be written out as a composite by hand.A
pj/marginalon a faceted pose draws each facet’s strip label twice, once over the marginal row and once over the row below it. Faceting lives in the pose’s:opts, which the marginal copies onto the composite it builds, so both rows facet and each marginal describes its own facet. Only the repeated label is wrong, and there is no workaround for it.(pj/marginal pose :top :histogram)draws its tallest bar close to the top of the marginal panel. The value axis is padded by the same fraction any axis is, and the strip is a quarter of the height, so the gap comes to a few drawing units – which a density hides, since its tail approaches zero, and a histogram does not. Workaround:pj/marginalbuilds a composite whose marginal cell is the first sub-pose for:topand the second for:right, so(update-in m [:poses 0] pj/scale :y {:include n})withnabove the tallest count gives the strip more room over the bars.The pose’s
:colormapping does not reach its marginal. A marginal describes one whole column, so one undivided curve or set of bars is drawn beside however many colored groups the panel carries. Workaround: facet instead of coloring – faceting does reach the marginal, giving one distribution per panel.
Marks
Aesthetic-gate versus mark-consumer asymmetry. Several aesthetics are accepted at the universal pose-mapping gate but consumed only by one or two mark extractors. Setting them on other marks does not vary the picture.
Aesthetic Consumed on Elsewhere :size(column ref)lay-pointwarned, and no legend is drawn for it :alpha(column ref)lay-pointwarned, and no legend is drawn for it (a written :alpha Nworks on most via:fixed-alpha)numeric (continuous) :colorevery mark drawn once per row a mark drawn once for many rows – a line, an area, a histogram bin, a box – draws one color and reports that the column is not painted on it :tooltip, and the row-index plumbing beside itlay-point,lay-interval-hwarned, and no tooltip is drawn Only
lay-pointvaries a radius or an opacity from row to row, so{:size {:column :r :scale false}}– which asks for the column’s own values as radii – is refused elsewhere rather than warned: there is no reading for it at all. The warning and the refusal both namelay-point. Which marks those are is read from the registry, where each layer type declares what its mark varies under:varies, so an extension that draws a per-row size says so and is named alongsidelay-point.:shapeis not in this table: it is drawn only bylay-point, and passing it anywhere else is rejected rather than ignored – every other layer type warns and nameslay-pointas the one that takes it.:textis the same, and nameslay-textandlay-label.Workaround for
:sizeand:alpha: uselay-pointwhere the aesthetic must vary per row. For a numeric:coloron a mark drawn once for many rows, pre-bin the column into categories, or draw the quantity with a mark that stands for one row.pj/lay-rule-hrendered under(pj/coord :flip)becomes a vertical line;pj/lay-rule-vbecomes a horizontal line. The mark name still reflects the unflipped semantics. Add a one-line note in the surrounding prose if the chart’s flipped state is non-obvious.:position :dodgeis ignored at render-time on several marks. The request fails in one of two ways: onlay-barandlay-summarythe dodge request is dropped at construction; onlay-pointandlay-linethe dodge metadata reaches the plan but no geometric offset is applied. Workaround: pre-compute dodge offsets viatc/group-by.Polar plots for bar-family marks don’t auto-emit category labels – rose charts currently render with zero text.
Stacked bars don’t split positive and negative values; all-positive data works, but mixed-sign data stacks incorrectly.
pj/lay-tile(and the underlying:bin2dstat) requires numeric x and y columns. Passing a categorical axis throws a clear “Stat :bin2d requires a numeric column” error at plan time. The recommended workaround is to render a numeric-indexed grid (1-N integers in place of the categorical column) and pair:breakswith:tick-labelson the axis – see Customization and Troubleshooting for a worked example. For a true categorical axis (binning over labels rather than numeric intervals),pj/lay-barwith aycolumn and{:color :value}gives a categorical “heatmap” look.Horizontal value bars (
pj/lay-barwith the category on:y) support plain and dodged layouts, but not:position :stackor:position :fill– stacking accumulates along the y-axis, which is the categorical band for a horizontal bar. Workaround: put the category on:xand add(pj/coord :flip), where stacking works.Numeric/temporal-position bars (
pj/lay-barwith both axes numeric or temporal) draw one bar per row at its x position; grouped bars (via:color) overlap rather than dodge, and:position :stackis not applied. Pre-aggregate or jitter positions to separate groups.Value bars (
pj/lay-barwith a value column) cannot use a log scale on the value axis: a bar is measured from zero, so its axis always reaches zero, and a log scale has no reading for zero. Plotje reports this and names the alternatives – a linear value axis, or a mark that is not measured from a baseline (pj/lay-point,pj/lay-line). Count bars and histograms do support a log axis, as do the other marks that are measured from a baseline:pj/lay-areaandpj/lay-lollipoprest on the panel’s smallest value there, as a count bar does.Rules and bands are refused under
(pj/coord :polar). A polar rule would need to draw as a circle (fixed radius) or a spoke (fixed angle); those shapes are not implemented, and the message names the marks polar does draw. Use Cartesian or flip coords for annotated plots.Large scatters produce large SVGs (~220 bytes/point). For >10k points, use
:format :bufimgfor raster output.LOESS with confidence bands is O(n^2); subsample above ~5k rows.
Scales
One pose reads an aesthetic through one scale. Two layers asking for different scales on
:size,:alpha,:coloror:fillare refused rather than drawn, since a plot has one legend per aesthetic. Two layers naming different scales for:xor:yare refused as well, because a panel has one of each axis. Workaround in every case: put the layers in separate poses withpj/arrange, where each cell has its own scales and its own legend.Facet panels share their scale types.
{:scales :free}gives each panel its own domain, but there is no equivalent for giving one panel a log axis and another a linear one. Workaround:pj/arrangeagain.
Options and Configuration
:panel-sizeis a legacy configuration key from the pre-total-first layout. It now emits a deprecation warning and is ignored. Use:panel-width/:panel-height(which set the panel size directly).The
:widthkey on apj/planresult preserves the user’s original request even when:panel-widthpins the real size – inspect:total-width/:total-heightfor the rendered canvas.pj/plancalled on a plan or on a hiccup value now throws a clear error. Callpj/planonly on poses.
Mixing Keyword and String Column References
- Mapping the same column with a keyword in one place and a string in another (e.g.
(pj/pose ds {:color :group})then(pj/lay-point :x :y {:color "group"})) is not normalized: the scope hierarchy treats them as different keys. The mismatch is reported rather than drawn – the spelling that does not match a column name is looked up, found missing, and named alongside the columns the data does carry. The exception is a string that happens to name something the aesthetic can draw:"red"on:coloris that color, so the mapping quietly stops grouping instead. Workaround: pick one form (keyword or string) and use it consistently within a pose.
Integer Column Names
A dataset built without column names gets integer ones, and Plotje reads them wherever a mapping is written:
(pj/lay-point ds {:x 0 :y 1}),(pj/lay-point ds 0 1)and(pj/pose ds {:x 0 :y 1})all plot those two columns, and the derived axis titles read as the names.What remains is that a number has two possible readings on
:xand:y, and the data decides between them: a number the data carries as a column name is that column, and any other number is a value to place a mark at. So the same code can change meaning on a dataset whose column names differ. Where that matters, write the mapping out in full –{:x {:column 0}}and{:x {:value 0}}are each unambiguous – or rename with(tc/rename-columns ds [:x :y])as shown in the Datasets chapter.The appearance aesthetics work the same way.
{:size 1}reads column 1 where the data has one, and is a radius where it does not;{:size {:value 1}}insists on the radius.
ggplot2 Features Not Yet Implemented
The
:fillaesthetic is currently consumed only bylay-tile(and the:bin2doutput beneathlay-density-2d). On filled marks likelay-bar,lay-area, andlay-violin,:colorpaints the interior; there is no separate stroke aesthetic.The
:linetypeaesthetic (ggplot2’saes(linetype=...)for solid vs. dashed lines) is not implemented. Workaround: encode the same distinction via:colorinstead.A layer’s own
:datais not split bypj/facet. The pose’s data is divided among the panels, but a layer carrying its own dataset is drawn whole in every panel, whether or not that dataset has the facet column. ggplot2 divides a layer’s data the same way it divides the plot’s, sogeom_hline(data = means, aes(yintercept = m))puts one line in each panel where Plotje puts all of them in each. Nothing errors; the picture simply shows every group’s mark in every panel. Workaround: build the panels withpj/arrangerather thanpj/facet, so each cell is its own pose with its own layer data.No
after_stat()analog. ggplot2 idioms likegeom_bar(aes(label=after_stat(count)))andgeom_histogram(aes(y=after_stat(density)))reference computed stat values inside aesthetic mappings; Plotje requires a pre-computed column. Workaround: aggregate the data first viatc/group-by+tc/aggregate, then map the count or density column directly.Theme support is shallow versus ggplot2: today the theme map carries
:bg,:grid,:font-size, and a small handful of other keys. Named theme presets (theme_minimal,theme_bw,theme_classic), panel borders, strip text styling, andlegend.positionby coordinate are not yet exposed. Rotating the x-axis tick labels is available, but as the:x-tick-angleplot option rather than through the theme – see Customization.Per-layer
data,guides()for per-aesthetic legend control,scale_*_sqrt/reverse/date. All tracked in the backlog. The axis types are:linear,:logand:categorical; a square-root axis is not among them. (pj/scale :size {:by :sqrt}is a different setting – it says how a size spreads across the radii, not how an axis is transformed.)