27 Troubleshooting
Common mistakes and how to fix them.
(ns plotje-book.troubleshooting
(:require
;; Rdatasets -- standard datasets
[scicloj.metamorph.ml.rdatasets :as rdatasets]
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Tablecloth -- dataset manipulation
[tablecloth.api :as tc]
;; Plotje -- composable plotting
[scicloj.plotje.api :as pj]))Column Not Found
Symptom: "Column :foo (from :x) not found in dataset" error, listing the available columns at the end of the message.
Cause: The column reference does not match a dataset column name. Matching is strict – :foo matches keyword column :foo only, and "foo" matches string column "foo" only. The two forms do not interchange. Three common triggers:
1. Typo. A misspelled column name. Always check the spelling against the dataset’s actual columns:
(tc/column-names (rdatasets/datasets-iris))(:rownames
:sepal-length
:sepal-width
:petal-length
:petal-width
:species)2. Keyword vs string. A CSV loaded without :key-fn keyword produces string column names; using a keyword reference against that dataset throws:
(try
(-> (tc/dataset {"sepal_length" [5.0 6.0] "sepal_width" [3.0 3.5]})
(pj/pose :sepal_length :sepal_width)
pj/lay-point pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Column :sepal_width (from :y) not found in dataset. Available: (\"sepal_length\" \"sepal_width\")"The fix is to either pass {:key-fn keyword} when loading the CSV (so the dataset has keyword columns) or to use string references everywhere.
3. Whitespace or punctuation mismatch. A column literally named "sepal length" (with a space) does not match :sepal-length (with a hyphen):
(try
(-> (tc/dataset {"sepal length" [5.0 6.0] "sepal width" [3.0 3.5]})
(pj/pose :sepal-length :sepal-width)
pj/lay-point pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Column :sepal-width (from :y) not found in dataset. Available: (\"sepal length\" \"sepal width\")"Note that :key-fn keyword on "sepal length" produces :sepal length – a keyword whose printed form contains a space, not the hyphenated form a Clojure reader would normally produce. Spaces and other special characters in CSV headers usually need a custom :key-fn, e.g. (comp keyword #(clojure.string/replace % " " "-")).
Wrong Chart Type from Inference
Symptom: pj/pose produces a chart type that isn’t what you wanted – a boxplot when you wanted individual points, a line when you wanted a scatter.
Cause: pj/pose infers the layer type from column types. The defaults fit the most common use case for each column-type pair (see Inference Rules), but they can be overridden.
Fix: Use an explicit pj/lay-* function. For example, a categorical x with a numerical y defaults to a boxplot:
(-> (rdatasets/datasets-iris)
(pj/pose :species :sepal-width))Use pj/lay-point if you want the individual points instead:
(-> (rdatasets/datasets-iris)
(pj/lay-point :species :sepal-width))Numeric IDs Treated as Continuous Color
Symptom: You color by a subject/group ID column that contains numbers (e.g., 1, 2, 3), but instead of discrete colored groups you get a single continuous gradient.
Cause: The inference system sees a numeric column and treats it as continuous. Continuous color means no grouping – all data stays in one group with a gradient legend.
(def subject-scores
{:day [1 2 3 4 1 2 3 4 1 2 3 4]
:score [3 5 4 6 6 7 5 8 8 9 7 10]
:subject [1 1 1 1 2 2 2 2 3 3 3 3]})Gradient (wrong for IDs) – one line for the whole dataset, drawn in the default color rather than in any color from the gradient: a line passes through rows of twelve different values and can take none of them. The legend still shows the gradient, and Plotje reports at the REPL that the column was not used, pointing at the fix:
(-> subject-scores
(pj/lay-line :day :score {:color :subject}))Fix: Add :color-type :categorical to override the inference – three discrete groups, one line per subject:
(-> subject-scores
(pj/lay-line :day :score {:color :subject :color-type :categorical}))See Inference Rules for the full mechanism.
Numeric Column Treated as Continuous Instead of Categorical
Symptom: A column of discrete numbers (hour of day, year, subject ID) is treated as a continuous axis. A categorical-axis mark like :boxplot, :violin, or :lollipop rejects it with an error like "requires a categorical column". (pj/lay-bar does not error on a numeric axis – it draws a bar at each numeric position; use the same fix below to get evenly-spaced bands instead.)
Cause: The column contains numbers, so column-type inference classifies it as :numerical. These marks need :categorical.
A boxplot keyed by hour runs into this – :hour looks like integers, so it is inferred numerical:
(try
(-> {:hour [9 9 10 10 11 11] :value [1 2 3 4 5 6]}
(pj/lay-boxplot :hour :value)
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"lay-boxplot requires a categorical column on either :x or :y, but :hour is numerical and :value is numerical. Override with {:x-type :categorical} or {:y-type :categorical} to treat a numeric column as categorical."Fix: Add :x-type :categorical (or :y-type :categorical for horizontal layouts) to override the inferred type. No need to convert the column itself:
(-> {:hour [9 9 10 10 11 11] :value [1 2 3 4 5 6]}
(pj/lay-boxplot :hour :value {:x-type :categorical}))The override propagates into infer-column-types, so every downstream step (scale type, tick placement, domain) treats :hour as categorical. The same switch works for :y-type when a numeric column is on the y axis of a horizontal boxplot or similar layout. See Inference Rules for a worked example.
A Number Where a Category Was Meant
Symptom: A value written for a categorical axis reports "pj/plan got 2021 for :x, which is past the ends of this axis".
Cause: A number written for a categorical axis is a place among the categories rather than one of them, counted from one. On an axis built from the years 2020, 2021 and 2022 the number 2021 is a place far past the third and last category, which is what the refusal names. Placing Marks teaches the reading in full.
(try
(-> {:cohort [2020 2021 2022] :n [3 5 4]}
(pj/lay-bar :cohort :n {:x-type :categorical})
(pj/lay-text {:x 2021 :y 5.5 :text "the 2021 cohort"})
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"pj/plan got 2021 for :x, which is past the ends of this axis. Categories: [\"2020\" \"2021\" \"2022\"]. A number written for a categorical axis is a place counted from one -- 1 is the first category, 1.5 sits halfway to the second -- and this axis runs from 0.5 to 3.5. To move a mark by a distance on the page instead, use :offset-x / :offset-y, which work on any axis; to draw the number as a category, give the axis a column of them."Fix: Name the category as a value. {:x {:value "2021"}} puts the mark on that band whatever numbers the categories are written from:
(-> {:cohort [2020 2021 2022] :n [3 5 4]}
(pj/lay-bar :cohort :n {:x-type :categorical :color "#a6cee3"})
(pj/lay-text {:x {:value "2021"} :y 4.5 :align-x :center
:text "on the band"}))Moving a Mark Clear of Another
Symptom: A value label sits on top of the bar it names, and the amount that separates them has to be guessed again on every chart.
Cause: A :dx or :dy is a distance in the data – a fraction of a band on a categorical axis, a value on a numeric one – so the number that clears a mark depends on what the axis holds. A label has to clear its mark by roughly the mark’s own size, which is a length on the page.
Fix: Use :offset-x/:offset-y, which are drawing units applied after the scales and so mean the same on every axis:
(-> {:species ["setosa" "versicolor" "virginica"] :pct [33.3 33.3 33.3]}
(pj/lay-bar :species :pct)
(pj/lay-text :species :pct {:text :pct :align-x :center :offset-y -6}))To place a label relative to its own point, anchor it with :align-x/:align-y – :align-x :right tucks the label inside a bar’s end. (To spread overlapping marks on a categorical axis, use :jitter or :position :dodge.)
(-> {:species ["setosa" "versicolor" "virginica"] :pct [33.3 33.3 33.3]}
(pj/lay-bar :species :pct {:color "#a6cee3"})
(pj/lay-text :species :pct {:text :pct :align-x :right})
(pj/coord :flip))Anchoring is covered in Placing Marks, and :dx itself in the same chapter.
Log Scale via :scale-x / :scale-y Options
Symptom: Passing {:scale-x :log} (or {:scale-y :log}) to a layer or to pj/options prints a warning – "does not recognize option(s): [:scale-x]" – and the chart comes out on a linear axis.
Cause: :scale-x and :scale-y are not option keys at all. A scale is set with pj/scale, called on the pose, or inside a mapping written out in full – {:x {:column :carat :scale :log}}.
The wrong form does not throw; it warns and silently falls back to a linear axis:
(with-out-str
(-> (rdatasets/ggplot2-diamonds)
(pj/lay-point :carat :price {:scale-y :log})
pj/plan))"Warning: lay-point does not recognize option(s): [:scale-y].\n Set by pj/scale, not by an options map: [:scale-y]\n Accepted: [:alpha :color :color-type :data :dx :dy :group :in :jitter :mark :offset-x :offset-y :overlay :position :shape :size :stat :tooltip :x :x-type :y :y-type]\n"Fix: Use pj/scale:
(-> (rdatasets/ggplot2-diamonds)
(pj/lay-point :carat :price {:alpha 0.1})
(pj/scale :y :log))pj/scale takes the pose, an aesthetic – an axis (:x, :y) or a visual one (:size, :alpha, :color, :fill, :shape) – and either a type keyword (:linear, :log) or a scale specification map with :type and an optional :domain override. See the Inference Rules chapter for how scale types and domains interact with column inference.
x-Only Layer Types Do Not Accept a y Column
Symptom: "lay-histogram uses only the x column; do not pass a y column" error.
Cause: Histogram, density, and rug layer types use only the x column. Passing a y column is an error. (lay-bar is not among them – it uses a y column as the bar height when given one.)
This restricts the layer’s own columns and nothing else. A rule or a band goes on such a pose like any other layer, because its intercept is a written value rather than a column reference – see A reference line on a distribution.
(try
(-> (rdatasets/datasets-iris)
(pj/lay-histogram :sepal-length :sepal-width)
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"lay-histogram uses only the x column; do not pass a y column"Fix: Remove the y column:
(-> (rdatasets/datasets-iris)
(pj/lay-histogram :sepal-length))Categorical Column with Log Scale
Symptom: "Log scale requires numeric data" error.
Cause: Log scales only work with numerical columns. Categorical columns (strings, keywords) have no meaningful log transform.
(try
(-> (rdatasets/datasets-iris)
(pj/lay-bar :species)
(pj/scale :x :log)
pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Log scale on :x requires numeric data, but column :species is non-numeric."Fix: Use a numerical column for the log-scaled axis, or drop the log scale on the categorical axis.
Polar Coordinates with Unsupported Marks
Symptom: "Mark :line is not supported with polar coordinates. Supported polar marks: (:bar :point :rect :rug :text)" (or the same message for :area and other unsupported marks).
Cause: Polar coordinates currently support a subset of marks: :bar, :point, :rect, :rug, and :text. Layer types built on these marks (such as :histogram, which renders as bars, and :bar with a y column, which renders as rectangles) work too.
(try
(-> {:x [1 2 3 4 5] :y [2 4 3 5 4]}
(pj/lay-line :x :y)
(pj/coord :polar)
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Mark :line is not supported with polar coordinates. Supported polar marks: (:bar :point :rect :rug :text)"Fix for now: Use a supported mark. A bar chart flipped to polar becomes a rose chart:
(-> (rdatasets/datasets-chickwts)
(pj/pose :feed)
pj/lay-bar
(pj/coord :polar))Support for :line, :area, and other marks in polar is planned. See the Polar Coordinates chapter for the full set of currently supported marks and examples.
Tooltip and Brush Not Working
Symptom: You set {:tooltip true} but no tooltip appears when hovering over points.
Cause: Tooltip and brush interactivity use JavaScript that requires a compatible notebook viewer. Static HTML export or some viewers may not support it.
Fix: Use Clay or another Kindly-compatible tool that supports kind/hiccup with embedded scripts.
A second cause: the plot was rendered to a format that draws no interaction. Both are drawn by a browser reading the figure, so SVG is the only format that answers them; {:format :bufimg} and pj/save to a PNG report that the request draws nothing and name the formats that do. Read the message the plot printed before looking at the viewer.
(with-out-str
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/options {:tooltip true})
(pj/plot {:format :bufimg})))"Warning: :tooltip asked for, and the :bufimg format draws no interaction. The formats that do: :svg. The request is accepted and draws nothing.\n"The figure still renders – the request is dropped, not the plot. Fix: render to SVG, which is what a notebook shows by default:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/options {:tooltip true}))Faceting Keys in a Layer’s Options Map
Symptom: An error like "Faceting is plot-level, not layer-level. Use (pj/facet pose col) ..." when you put :facet-col, :facet-row, :facet-x, or :facet-y inside a pj/lay-* options map.
Cause: Faceting configures the plot as a whole, not a single layer. Putting these keys in a layer’s options map is rejected with a guidance message.
(try
(-> (rdatasets/datasets-iris)
(pj/pose :sepal-length :sepal-width)
(pj/lay-point {:facet-col :species})
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Faceting is plot-level, not layer-level. Use (pj/facet pose col) or (pj/facet-grid pose col-col row-col) instead of putting facet-col in a layer's options map."Fix: Use pj/facet (single-axis) or pj/facet-grid (two-axis) as a top-level step in the pipeline:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/facet :species))Constant :x or :y in a Layer’s Options
Symptom: a note is needed at one fixed spot, and no column holds the x and y it belongs at.
This is not an error. :x and :y may be given as a value, the same way a color may be "red" rather than a column. The layer below places its text at x 6.5 and y 3.5 with no dataset of its own, and a string :text on such a layer is the text itself rather than a column name:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-text {:x 6.5 :y 3.5 :text "mean"}))The other shape a value takes is beside a column. When :y is a column the layer describes the data, so the value given for :x repeats for every row – which is how a label at one fixed x is written. The five team names below line up at x 33 rather than sitting over their points, each at the revenue of its own row:
(-> {:team ["North" "South" "East" "West" "Central"]
:spend [12 19 15 24 31]
:revenue [30 45 38 62 74]}
(pj/lay-point :spend :revenue)
(pj/lay-text {:x 33 :y :revenue :text :team})):x and :y alone decide which of the two a value draws. A layer that gives both as values does not read the data at all, so it draws once; a value beside a column applies to each row, so it repeats. A string :text splits the same way: it is the text on a layer of values alone, and a column name once the layer has data to name a column in.
A value given for :x or :y is a data value like any other, so it takes part in the axis domains: a note placed beyond the data widens the axis to hold it. To place a mark on the panel instead – in drawing units from the corner of the panel background, leaving the axes alone – give the layer :in :drawing-area. The Placing Marks chapter covers that choice, and the two spaces the two readings belong to.
Reference lines remain their own layer types: pj/lay-rule-h with :y-intercept and pj/lay-rule-v with :x-intercept draw a line across the whole panel, which one :x and one :y cannot say.
A number is read against the data. A dataset built without column names is given integer ones, and a number in a mapping names such a column where the data carries it:
(-> (tc/dataset [[1 2] [3 4] [5 7]])
(pj/lay-point 0 1))Where the data carries no column of that name, the same number is a value to place a mark at, and the axis stretches to hold it. To say which reading you mean rather than letting the data decide, write the mapping in full: {:x {:column 0}} reads the column and {:x {:value 0}} places every mark at zero. Renaming the columns is the other remedy, and usually the clearer one:
(-> (tc/dataset [[1 2] [3 4] [5 7]])
(tc/rename-columns [:x :y])
(pj/lay-point :x :y))The Datasets chapter covers column names in full.
Two Panels Where One Was Wanted
Symptom: a second pj/lay-* meant to draw on the same panel draws beside it instead, and the plot comes out with two panels.
Cause: a pj/lay-* call naming :x and :y joins the panel whose own :x and :y match. When none matches it cannot share those axes, so it starts a panel of its own. Below, the second bar layer names a different value column from the first, so the plot comes out with two panels:
(-> {:cohort [:a :b :c] :growth [12 19 15] :tax [3 5 4]}
(pj/lay-bar :growth :cohort {:color "#377eb8"})
(pj/lay-bar :tax :cohort {:color "#e6550d"}))Fix: Add pj/overlay before the layers. Every layer added after it goes on the panel it is added to, keeping its own columns, and the axis covers every column drawn on it:
(-> {:cohort [:a :b :c] :growth [12 19 15] :tax [3 5 4]}
pj/overlay
(pj/lay-bar :growth :cohort {:color "#377eb8"})
(pj/lay-bar :tax :cohort {:bar-width 0.4 :color "#e6550d"}))To join one layer without changing where later layers go, write {:overlay true} in that layer’s own options map.
Joining has to be asked for because two panels are right when the columns are unrelated. There is a second remedy for the case where the incoming values are the same quantity under another name: rename those columns to the panel’s own, and the layer joins without pj/overlay. The Composition chapter shows both.
Dataset Missing Columns a Template References
Symptom: An error like "Cannot attach data: pose references column(s) [:group] not present in the dataset. Available columns: [:x :y]" when calling pj/with-data on a dataless template pose.
Cause: pj/with-data validates at attach time – every keyword column reference in the template must exist in the dataset, or the attachment fails immediately.
(def template
(-> (pj/pose nil {:x :x :y :y :color :group})
pj/lay-point))(try
(-> template
(pj/with-data {:x [1 2 3] :y [4 5 6]}))
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Cannot attach data: pose references column(s) [:group] not present in the dataset. Available columns: [:x :y]."Fix: Either rename the dataset columns to match the template (tc/rename-columns), or adjust the template to reference the columns the dataset has.
(-> (pj/pose nil {:x :x :y :y})
pj/lay-point
(pj/with-data {:x [1 2 3] :y [4 5 6]}))Horizontal Ranking Bars Draw Biggest-at-Bottom
Symptom: A horizontal bar chart made with (pj/coord :flip) shows the first row of the data at the bottom of the chart. A descending-sorted “top-N” dataset ends up with the biggest bar at the bottom instead of the top.
Cause: coord :flip draws categories bottom-to-top in the order they appear in the data (matching ggplot2’s coord_flip()).
Descending data plotted as-is – “A” (the biggest) renders at the bottom, not the top:
(-> [{:category "A" :value 100}
{:category "B" :value 50}
{:category "C" :value 25}]
(pj/lay-bar :category :value)
(pj/coord :flip))Fix for now: Sort the dataset ascending before plotting – the ascending order shows up top-to-bottom on the flipped axis, so the biggest value lands at the top:
(-> [{:category "A" :value 100}
{:category "B" :value 50}
{:category "C" :value 25}]
(tc/dataset)
(tc/order-by [:value] :asc)
(pj/lay-bar :category :value)
(pj/coord :flip))A future opt-in option (e.g. (pj/coord :flip {:reverse-categorical true})) would remove the need to pre-sort. Tracked in CHANGELOG.md Known limitations.
Point Sizes Changed From an Earlier Release
Symptom: A plot with {:size :some-column} draws its middle values larger than it used to, and the difference between the smallest and largest points looks less dramatic.
Cause: A size scale spreads the square root of the value across the radii, so the area of a mark grows with the value rather than with its square. Earlier releases spread the value itself, which exaggerates the differences. The two ends of the range are unchanged; the values between them moved.
(-> {:x [1 2 3 4 5 6] :y [1 1 1 1 1 1] :n [1 4 9 16 25 36]}
(pj/lay-point :x :y {:size :n}))Fix: {:by :linear} restores the earlier reading, and {:by :area} gives the strict one, where equal steps in value are equal steps in ink:
(-> {:x [1 2 3 4 5 6] :y [1 1 1 1 1 1] :n [1 4 9 16 25 36]}
(pj/lay-point :x :y {:size :n})
(pj/scale :size {:by :linear}))A Band Whose Two Edges Are the Same Value
Symptom: "lay-band-h requires :y-min < :y-max" error on a call that drew something, or nothing, in an earlier release.
Cause: A band shades the region between two values. Where the two are equal the region is empty, so the band covered nothing and drew a rectangle of zero thickness – present in the SVG, invisible on the plot, and reported by nothing. It is refused now.
(try
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-band-h {:y-min 3.0 :y-max 3.0}))
(catch clojure.lang.ExceptionInfo e (ex-message e)))"lay-band-h requires :y-min < :y-max, got both 3.0. A band with equal bounds covers nothing and draws nothing. For a line at that value use (pj/lay-rule-h pose {:y-intercept 3.0})."Fix: A line at one value is a rule; a band needs two distinct edges. The error names the rule constructor for the axis in question.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-rule-h {:y-intercept 3.0}))A Configuration Key From an Earlier Release
Symptom: A configuration key that used to set something is reported as unrecognized, and the plot draws the default.
Cause: The key was renamed. Plotje reports what it does not read rather than merging it in silence, and where a release renamed a key the report names the one that replaced it. :annotation-stroke sets the colour a rule draws in where its layer names none, and it is :rule-color now.
(with-out-str
(pj/with-config {:annotation-stroke "firebrick"} (constantly nil)))"Warning: pj/with-config does not recognize configuration key(s): [:annotation-stroke].\n :annotation-stroke was renamed to :rule-color\n Accepted: [:band-opacity :bar-opacity :bin-method :brush :color-midpoint :color-range :color-values :decimal-separator :default-color :domain-padding :fill-midpoint :fill-range :fit-text-domain :format :grid-stroke-width :height :label-font-size :label-offset :legend-position :legend-width :line-width :margin :margin-multi :panel-size :point-opacity :point-radius :point-stroke :point-stroke-width :rule-color :strict :strip-font-size :strip-height :theme :thousands-separator :title-font-size :title-offset :tooltip :validate :width :x-tick-angle :x-tick-label-pad :x-tick-spacing :y-tick-spacing]\n"The same report reaches pj/set-config!, a plotje.edn file, the :config option and pj/options, so the key is named wherever it can be written.
Fix: Write the current name.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-rule-h {:y-intercept 3.0})
(pj/options {:rule-color "firebrick"}))A Layer Option From an Earlier Release
Symptom: A layer option that used to shift a mark prints a warning naming a different key, and the plot draws as it always did.
Cause: The option was renamed. :nudge-x and :nudge-y are :dx and :dy. A retired layer option is not handled the way the retired configuration key above is: dropping a shift would move a label back onto the mark it was written to clear, so the old name is read as the new one and the warning names the edit. The warning is printed where the layer is built rather than where the plan is built:
(with-out-str
(-> {:team ["red" "green" "blue"] :score [3 5 4]}
(pj/lay-bar :team :score)
(pj/lay-text {:x {:value "red"} :y 3 :nudge-x 0.5 :text "note"})))"Warning: lay-text: :nudge-x was renamed to :dx, and is read as :dx for now. Write :dx.\n"Fix: Write the current name. Both names draw one picture, which is why the old name warns rather than reporting an error:
(-> {:team ["red" "green" "blue"] :score [3 5 4]}
(pj/lay-bar :team :score {:color "#a6cee3"})
(pj/lay-text {:x {:value "red"} :y 3 :align-x :center
:dx 0.5 :offset-y -10 :text "half a band"}))Under :strict the retired name reports an error instead of warning, so a notebook run that way names every retired name still written in it. Configuration covers the setting.
A :size or :alpha Column That Changes Nothing
Symptom: {:size :some-column} on a line, boxplot or lollipop draws marks of one size, and a warning names the marks that vary the aesthetic.
Cause: Only marks that draw a size per row can read a size column. Among the built-in marks that is pj/lay-point. Every other mark draws one width or one opacity for the whole layer, so the column changes nothing. No legend is drawn for it either, since the legend would describe an encoding the panel does not show.
Fix: Write the value itself for a layer-wide size – {:size 2} on a line is a stroke width – and map the column on a layer whose mark varies it.
(-> {:x [1 2 3] :y [2 4 3] :r [1 2 3]}
(pj/pose :x :y)
(pj/lay-line {:size 2})
(pj/lay-point {:size :r}))Dodge Has No Effect on Point Layers
Symptom: Adding :position :dodge to pj/lay-point (or other non-bar marks) does not spread points apart by group – the plot looks identical to the version without :position :dodge.
Cause: :position :dodge is implemented for the bar mark (pj/lay-bar). On point/line/jitter and several other marks the option is accepted but silently ignored.
The two plans below produce identical x-coordinates for the rendered points – :position :dodge has no effect on points:
(def points-data
{:x [1 1 2 2 3 3] :y [10 15 20 25 30 35] :group ["A" "B" "A" "B" "A" "B"]})(defn point-xs [pose]
(-> pose pj/plan :panels first :layers first :groups
(->> (mapcat :xs) sort vec)))(= (point-xs (-> points-data (pj/lay-point :x :y {:color :group})))
(point-xs (-> points-data (pj/lay-point :x :y {:color :group :position :dodge}))))trueFix for now: For grouped categorical layouts use pj/lay-bar (counting with x only, or using a y column as height); dodge works there. To distinguish overlapping points by group on a numeric x, encode the group with :color, :shape, or pre-compute small offsets in the data. A proper dodge for points is tracked in CHANGELOG.md Known limitations.
(-> {:cat ["A" "A" "B" "B" "C" "C"]
:y [10 20 30 40 50 60]
:group ["a" "b" "a" "b" "a" "b"]}
(pj/lay-bar :cat :y {:color :group :position :dodge}))Polar Bar Chart Has No Category Labels
Symptom: A bar chart flipped to polar ((pj/coord :polar)) renders as a rose chart, but no category text appears anywhere around the wedges.
Cause: Polar coord does not currently emit angular tick labels for bar-family marks – the underlying axis machinery places labels along Cartesian axes that polar replaces with a circular layout, and the equivalent angular ticks are not yet implemented.
The polar version shows the wedges sized by category, but the category names are absent:
(-> (rdatasets/datasets-chickwts)
(pj/pose :feed)
pj/lay-bar
(pj/coord :polar))Fix for now: Drop (pj/coord :polar) for the labeled view, or combine the polar plot with a separate Cartesian-coord version for the legend. A proper rose-chart label pass is tracked in CHANGELOG.md Known limitations.
(-> (rdatasets/datasets-chickwts)
(pj/pose :feed)
pj/lay-bar)Heatmap with Categorical Axes
Symptom: "class java.lang.String cannot be cast to class java.lang.Number" when passing a string column to pj/lay-tile.
Cause: pj/lay-tile (and the underlying :bin2d stat) requires numeric x and y columns – the tile boundaries are numeric intervals. Categorical axes are not yet supported for tile.
(try
(-> {:x ["a" "b" "c"] :y ["a" "b" "c"] :v [1 2 3]}
(pj/lay-tile :x :y {:fill :v})
pj/plan)
(catch Throwable t (.getMessage t)))"class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')"Fix: render a numeric-indexed grid (1-N integers in place of the categorical column) and pair :breaks with :tick-labels on the axis so the tick text shows the original category names:
(-> (for [day (range 1 8) hour (range 0 24)]
{:day day :hour hour :v (+ (* 0.3 (Math/sin (* 0.5 hour)))
(* 0.2 (mod day 3)))})
(pj/lay-tile :day :hour {:fill :v})
(pj/scale :x {:type :linear
:breaks [1 2 3 4 5 6 7]
:tick-labels ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]}))If a true categorical axis (with binning over labels rather than numeric intervals) is what you need, that is tracked in CHANGELOG.md Known limitations. The integer-plus-:tick-labels pattern above covers most heatmap-with-categorical-axis cases.
Empty or All-Missing Column on a Grouping Layer
Symptom: "lay-boxplot requires a categorical column on either :x or :y, but :c has no rows and :v has no rows", or "has no values" where the column holds only nil.
Cause: The layer groups its data by category, and no row carries one. This is usually a data problem upstream of the plot – a filter that matched nothing, or a join that dropped every row:
(try
(-> {:group [] :measurement []}
(pj/lay-boxplot :group :measurement)
pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"lay-boxplot requires a categorical column on either :x or :y, but :group has no rows and :measurement has no rows. A layer that groups by category needs at least one row carrying a category."A column holding nothing but nil reports having no values, which separates the two cases – rows that never arrived, against rows that arrived empty:
(try
(-> {:group [nil nil] :measurement [nil nil]}
(pj/lay-boxplot :group :measurement)
pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"lay-boxplot requires a categorical column on either :x or :y, but :group has no values and :measurement has no values. A layer that groups by category needs at least one row carrying a category."The message names neither column a type, deliberately. An empty column and an all-nil one are both typed boolean by tech.ml.dataset, and Plotje reads such a column as numerical so the rest of the pipeline has something to work with – so a message naming the type would send you to check something that is not the problem.
:text Given a Value That Names No Column
Symptom: "Column :nope (from :text) not found in dataset", ending "It is not a label either -- a label is a string".
Cause: :text takes either a column reference, drawing one row’s value at each mark, or a literal string drawn at every mark. A keyword that names no column is neither:
(try
(-> {:x [1 2] :y [1 2]}
(pj/lay-text :x :y {:text :nope})
pj/plot)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"Column :nope (from :text) not found in dataset. Available: (:x :y) It is not a label either -- a label is a string."Write a string for constant text – {:text "note"} – or name a column the dataset carries.
An Axis :domain That Is Not Two Numbers
Symptom: "pj/scale :y :domain [0] is not a pair of two finite numbers, as [0 100] is".
Cause: What a :domain means depends on the column it is read for. Against a continuous column it is the interval the panel spans, which is two finite numbers; against a categorical one it is the list of categories, in the order they are to be drawn. Since the column decides, the check is made once the column is known:
(try
(-> {:height [1 2 3] :weight [1 2 3]}
(pj/lay-point :height :weight)
(pj/scale :y {:domain [0]})
pj/plan)
(catch clojure.lang.ExceptionInfo e (ex-message e)))"pj/scale :y :domain [0] is not a pair of two finite numbers, as [0 100] is. :y here reads a continuous column, so its domain is the interval the panel spans. It has 1. To extend the interval the data gives rather than replace it, write :include."Write both ends – {:domain [0 100]} – or, to extend the interval the data gives rather than replace it, write :include instead: {:include 0} puts zero on the axis and leaves the other end to the data. Scales works through the difference.
See Also
- Core Concepts – the mapping and inference rules behind most of these symptoms
What’s Next
- Inference Rules – how defaults are chosen and overridden
- API Reference – complete function listing with docstrings
- Exploring Plans – inspect the data structures behind your plots
- Gallery – more working examples by chart type