30 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.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 tick labels are given room at the canvas edges, but not room from one another. Y-tick labels at 90 or -90 (
:y-tick-angle) run along the axis, and a label longer than the space between two ticks is drawn over its neighbours. The room a rotated label is given stops at 30% of the plot’s size, so a label longer than that is clipped at the edge. Workaround: shorten the labels, choose another angle, or thin the ticks with:n-ticks.A mark that occupies a band cannot be drawn at a place between two categories.
pj/lay-boxplotandpj/lay-violinrequire a category column and report that they do;pj/lay-bargiven a written place draws the histogram bar instead, one place wide and centred there, rather than a bar in a band of its own. Workaround: none – a mark between two bands would need a band the axis does not have.Only a straight segment carries an arrow head.
pj/lay-segmenttakes:arrow, as the Cookbook shows;pj/lay-line, which runs through several points, draws a plain stroke and ends bluntly.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 the label by widening the domain, and nothing else can – 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.(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.A 2D density is computed from all the rows.
lay-density-2dandlay-contourdraw one density per panel, coloured by its own level. Given a:colorcolumn, they warn that the colour is not drawn and keep the density legend. Workaround: facet by the column, as in(pj/facet my-pose :species), so that each panel draws the density of one group.Interaction reaches a reader only through SVG. A tooltip and a brush are behaviours a browser runs over the figure, so a plot rendered to
:bufimg, or saved as a PNG, carries neither. The table above is the per-mark half of the same question; this is the per-format half. Asking for either on such a format is reported, naming the format and the ones that answer, and the figure still renders. Interactivity covers it. Workaround: render to SVG, and save the SVG rather than a PNG where the reader is meant to hover.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 :dodgedraws no offset onlay-line, and is dropped between the pose and the plan onlay-summary.lay-pointhonours it on a categorical x, and ignores it on a numeric x, which has no bands to divide.lay-bardodges by default, so asking for it there changes nothing rather than failing. Workaround for the two that do not draw it: pre-compute the offsets withtc/group-by, or use:dxto shift a layer by a fraction of its band.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-tilewith neither:fillnor:colorcounts rows in two-dimensional bins (the:bin2dstat), which needs numeric x and y columns; a categorical axis reports “Stat :bin2d requires a numeric column”. Given:fillor:color, a tile draws one cell per row and takes categorical axes – see Troubleshooting.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, and write each cell’s scale on that cell. A cell that writes its own scale keeps it and draws its own legend; cells that map the same column and write no scale share one scale and one 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.
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.
A number has two possible readings on :x and :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:density-2doutput 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; every panel shows every group’s mark. 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 tick labels is available, but as the:x-tick-angleand:y-tick-angleplot options 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.)