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/:height or 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’s coord_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-domain widens 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: a pj/lay-point given a large :size at 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 with pj/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-boxplot and pj/lay-violin require a category column and report that they do; pj/lay-bar given 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-segment takes :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’s ggrepel, is designed but not implemented.

  • A long label at the right edge of a panel costs axis range. :fit-text-domain makes 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.

  • :in places 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: :caption and :subtitle for text below and above the panels.

  • pj/marginal draws on :top and :right. :bottom and :left would 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/marginal takes 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/marginal builds a composite whose marginal cell is the first sub-pose for :top and the second for :right, so (update-in m [:poses 0] pj/scale :y {:include n}) with n above the tallest count gives the strip more room over the bars.

  • The pose’s :color mapping 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-point warned, and no legend is drawn for it
    :alpha (column ref) lay-point warned, and no legend is drawn for it (a written :alpha N works on most via :fixed-alpha)
    numeric (continuous) :color every 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 it lay-point, lay-interval-h warned, and no tooltip is drawn

    Only lay-point varies 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 name lay-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 alongside lay-point.

    :shape is not in this table: it is drawn only by lay-point, and passing it anywhere else is rejected rather than ignored – every other layer type warns and names lay-point as the one that takes it. :text is the same, and names lay-text and lay-label.

    Workaround for :size and :alpha: use lay-point where the aesthetic must vary per row. For a numeric :color on 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-2d and lay-contour draw one density per panel, coloured by its own level. Given a :color column, 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-h rendered under (pj/coord :flip) becomes a vertical line; pj/lay-rule-v becomes 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 :dodge draws no offset on lay-line, and is dropped between the pose and the plan on lay-summary. lay-point honours it on a categorical x, and ignores it on a numeric x, which has no bands to divide. lay-bar dodges 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 with tc/group-by, or use :dx to 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-tile with neither :fill nor :color counts rows in two-dimensional bins (the :bin2d stat), which needs numeric x and y columns; a categorical axis reports “Stat :bin2d requires a numeric column”. Given :fill or :color, a tile draws one cell per row and takes categorical axes – see Troubleshooting.

  • Horizontal value bars (pj/lay-bar with the category on :y) support plain and dodged layouts, but not :position :stack or :position :fill – stacking accumulates along the y-axis, which is the categorical band for a horizontal bar. Workaround: put the category on :x and add (pj/coord :flip), where stacking works.

  • Numeric/temporal-position bars (pj/lay-bar with both axes numeric or temporal) draw one bar per row at its x position; grouped bars (via :color) overlap rather than dodge, and :position :stack is not applied. Pre-aggregate or jitter positions to separate groups.

  • Value bars (pj/lay-bar with 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-area and pj/lay-lollipop rest 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 :bufimg for 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, :color or :fill are refused rather than drawn, since a plot has one legend per aesthetic. Two layers naming different scales for :x or :y are refused as well, because a panel has one of each axis. Workaround in every case: put the layers in separate poses with pj/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/arrange again.

Options and Configuration

  • :panel-size is 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 :width key on a pj/plan result preserves the user’s original request even when :panel-width pins the real size – inspect :total-width/:total-height for 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 :color is 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 :fill aesthetic is currently consumed only by lay-tile (and the :density-2d output beneath lay-density-2d). On filled marks like lay-bar, lay-area, and lay-violin, :color paints the interior; there is no separate stroke aesthetic.

  • The :linetype aesthetic (ggplot2’s aes(linetype=...) for solid vs. dashed lines) is not implemented. Workaround: encode the same distinction via :color instead.

  • A layer’s own :data is not split by pj/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, so geom_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 with pj/arrange rather than pj/facet, so each cell is its own pose with its own layer data.

  • No after_stat() analog. ggplot2 idioms like geom_bar(aes(label=after_stat(count))) and geom_histogram(aes(y=after_stat(density))) reference computed stat values inside aesthetic mappings; Plotje requires a pre-computed column. Workaround: aggregate the data first via tc/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, and legend.position by coordinate are not yet exposed. Rotating tick labels is available, but as the :x-tick-angle and :y-tick-angle plot 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, :log and :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.)

source: notebooks/plotje_book/known_limitations.clj