Liking cljdoc? Tell your friends :D

scicloj.plotje.impl.aesthetics

What a mapping value means, decided in one place.

Specifying an aesthetic is two independent decisions, and this namespace answers both:

  • source -- a written value, or a reference to a column
  • scale -- whether that value passes through the aesthetic's scale, or is drawn as it stands

Two by two is four cells, and every one of them is a thing someone writes. ggplot2 has all four: aes(size=5) scales a written value and gives it a legend, geom_point(size=5) draws it, aes(size=b) scales a column, and scale_*_identity() draws one.

The two decisions are asked of different things. The source is asked of the layer's data: a name it carries is a column. The scale is asked of the value, and only of the value -- the convention sends a column through the scale whatever it holds, and nothing inspects its contents. An earlier reading did inspect it, drawing a column whose every value named a color, and three defects came out of that one idea: the same column behaved differently for "red" and "Red", :shape answered "drawn" while collect-shapes scaled it anyway and labelled a circle "cross", and :fill held the same disagreement unnoticed. Asking the value question of a column is what produced all three.

Nothing here decides by type. Deciding by type is how the old rule came to differ per aesthetic without anyone choosing that it should: column-ref? was (or (keyword? v) (string? v)), and a carve-out for a string on :color was bolted beside it. The rules here are the layer's data, then what the aesthetic can draw -- the first read from the caller's dataset, the second from pose-schema/drawn-value-schemas, with which side of the scale the answer falls on coming from defaults/aesthetic-registry. Nothing is restated here.

Two functions, meant to be called in that order:

(let [src (source v col-names)]
  (scaled? k {:source src :value v}))
What a mapping value means, decided in one place.

Specifying an aesthetic is two independent decisions, and this
namespace answers both:

- **source** -- a written value, or a reference to a column
- **scale** -- whether that value passes through the aesthetic's
  scale, or is drawn as it stands

Two by two is four cells, and every one of them is a thing someone
writes. ggplot2 has all four: `aes(size=5)` scales a written value
and gives it a legend, `geom_point(size=5)` draws it, `aes(size=b)`
scales a column, and `scale_*_identity()` draws one.

**The two decisions are asked of different things.** The source is
asked of the layer's data: a name it carries is a column. The scale
is asked of the *value*, and only of the value -- the convention
sends a column through the scale whatever it holds, and nothing
inspects its contents. An
earlier reading did inspect it, drawing a column whose every value
named a color, and three defects came out of that one idea: the
same column behaved differently for `"red"` and `"Red"`, `:shape`
answered "drawn" while `collect-shapes` scaled it anyway and
labelled a circle "cross", and `:fill` held the same disagreement
unnoticed. Asking the value question of a column is what produced
all three.

Nothing here decides by *type*. Deciding by type is how the old rule
came to differ per aesthetic without anyone choosing that it should:
`column-ref?` was `(or (keyword? v) (string? v))`, and a carve-out
for a string on `:color` was bolted beside it. The rules here are
the layer's data, then what the aesthetic can draw -- the first read
from the caller's dataset, the second from
`pose-schema/drawn-value-schemas`, with which side of the scale the
answer falls on coming from `defaults/aesthetic-registry`. Nothing
is restated here.

Two functions, meant to be called in that order:

    (let [src (source v col-names)]
      (scaled? k {:source src :value v}))
raw docstring

scicloj.plotje.impl.compositor

Composite-pose chrome layout, composite-pose->draft, and composite-draft->plan. Pure data-side: shared-scale reconciliation, chrome geometry computation, per-leaf opt adjustment. The plan-to-membrane rendering for composites lives in render/composite.clj, keeping this namespace free of membrane dependencies.

Shared scales are reconciled before drafting by stamping a forced domain on matching leaves (impl.pose/inject-shared-scales).

When the composite root carries a legend-producing mapping (:color/:size/:alpha), the chrome reserves a strip on the right of the grid; the per-leaf opts get :suppress-legend true so each cell hides its own legend, and the rendering side (render/ composite.clj) draws ONE shared legend in the reserved strip.

Composite-pose chrome layout, composite-pose->draft, and
composite-draft->plan. Pure data-side: shared-scale reconciliation,
chrome geometry computation, per-leaf opt adjustment. The
plan-to-membrane rendering for composites lives in
`render/composite.clj`, keeping this namespace free of membrane
dependencies.

Shared scales are reconciled before drafting by stamping a forced
domain on matching leaves (impl.pose/inject-shared-scales).

When the composite root carries a legend-producing mapping
(:color/:size/:alpha), the chrome reserves a strip on the right
of the grid; the per-leaf opts get :suppress-legend true so each
cell hides its own legend, and the rendering side (render/
composite.clj) draws ONE shared legend in the reserved strip.
raw docstring

scicloj.plotje.impl.draft-schema

Malli schemas for the draft data model -- the records returned by pj/pose->draft (and pj/draft).

Drafts are the intermediate stage between pose and plan. They are user-observable (the pj/draft shortcut and the predicates pj/leaf-draft? / pj/composite-draft? are public) but are primarily inspected, not traversed programmatically.

DraftLayer models the post-scope-merge layer shape. It takes its aesthetic value grammar from impl.pose-schema, so the two states are one description read at two points rather than a copy that can drift -- which is what an earlier version of this namespace declined to write down. What the draft adds is PositionalAestheticsNameColumns: the invariant impl.pose/resolve-positional-values establishes, which is the one the appearance aesthetics would extend if they were ever normalized the same way.

Backend authors who consume drafts directly should rely on destructuring :layers and :opts on a leaf draft, or :sub-drafts / :chrome-spec / :layout on a composite, and apply the layer-type registry to interpret each layer's :layer-type, :mark, and :stat.

Malli schemas for the draft data model -- the records returned by
`pj/pose->draft` (and `pj/draft`).

Drafts are the intermediate stage between pose and plan. They are
user-observable (the `pj/draft` shortcut and the predicates
`pj/leaf-draft?` / `pj/composite-draft?` are public) but are
primarily inspected, not traversed programmatically.

`DraftLayer` models the post-scope-merge layer shape. It takes its
aesthetic value grammar from `impl.pose-schema`, so the two states
are one description read at two points rather than a copy that can
drift -- which is what an earlier version of this namespace declined
to write down. What the draft adds is
`PositionalAestheticsNameColumns`: the invariant
`impl.pose/resolve-positional-values` establishes, which is the one
the appearance aesthetics would extend if they were ever normalized
the same way.

Backend authors who consume drafts directly should rely on
destructuring `:layers` and `:opts` on a leaf draft, or
`:sub-drafts` / `:chrome-spec` / `:layout` on a composite, and
apply the layer-type registry to interpret each layer's
`:layer-type`, `:mark`, and `:stat`.
raw docstring

scicloj.plotje.impl.extract

Extract data-space geometry from resolved draft layers and stat results. Produces layer descriptor maps — plain Clojure maps with mark type, style, and groups of data-space coordinates.

Extract data-space geometry from resolved draft layers and stat results.
Produces layer descriptor maps — plain Clojure maps with mark type,
style, and groups of data-space coordinates.
raw docstring

scicloj.plotje.impl.frames

Drawing-space geometry of a plan: where each panel's frames sit on the canvas, and how a data position maps into them and back.

A frame is a rectangle measured in drawing units. Three are named:

  • :canvas -- the whole output image
  • :panel-box -- one panel including its axis margin
  • :drawing-area -- the panel background inside that margin, where data marks are clipped

The renderer builds the same panel origins and the same scales from the same plan values. That computation lives here so the public mapping (pj/frames) and the drawn output cannot drift apart: render/membrane.clj calls panel-origin, and render/panel.clj builds its scales the way panel-scales does.

Drawing-space geometry of a plan: where each panel's frames sit on the
canvas, and how a data position maps into them and back.

A frame is a rectangle measured in drawing units. Three are named:

- `:canvas` -- the whole output image
- `:panel-box` -- one panel including its axis margin
- `:drawing-area` -- the panel background inside that margin, where
  data marks are clipped

The renderer builds the same panel origins and the same scales from
the same plan values. That computation lives here so the public
mapping (`pj/frames`) and the drawn output cannot drift apart:
`render/membrane.clj` calls `panel-origin`, and `render/panel.clj`
builds its scales the way `panel-scales` does.
raw docstring

scicloj.plotje.impl.layout

Layout inference pipeline. Three pure functions turn scene facts and configuration into concrete pixel dimensions.

Under the new semantics, :width and :height in opts mean the TOTAL SVG dimensions. Panel dimensions are derived by subtracting layout overhead (titles, axis labels, legends, facet strips) from the total. :panel-width/:panel-height are escape hatches: when set, they pin the panel size on that axis and :width/:height become the derived total.

The classic width→tick-count→label-width→y-label-pad→panel-width cycle is broken by a single reformulation: max-label-pixel-width runs the tick picker at a pixel budget equal to the user-supplied :height (or :width), not the actual panel size. Label width is monotonic non-decreasing in tick count across every scale type we support (verified at the REPL), so this over-estimate is always safe.

Pipeline:

compute-scene scene data from resolved draft layers + opts (no pixels) compute-padding scene + cfg + opts -> padding map (no pixel dims yet) compute-dims scene + padding + cfg + opts -> pw/ph/total-w/total-h

None of these need actual per-panel tick positions -- the tick budget is baked into y-label-pad in compute-padding via the over-estimate trick. Real per-panel ticks are computed AFTER compute-dims when the final pw/ph are known.

Layout inference pipeline. Three pure functions turn scene facts
and configuration into concrete pixel dimensions.

Under the new semantics, `:width` and `:height` in opts mean the
TOTAL SVG dimensions. Panel dimensions are derived by subtracting
layout overhead (titles, axis labels, legends, facet strips) from
the total. `:panel-width`/`:panel-height` are escape hatches: when
set, they pin the panel size on that axis and `:width`/`:height`
become the derived total.

The classic width→tick-count→label-width→y-label-pad→panel-width
cycle is broken by a single reformulation: `max-label-pixel-width`
runs the tick picker at a pixel budget equal to the user-supplied
`:height` (or `:width`), not the actual panel size. Label width is
monotonic non-decreasing in tick count across every scale type we
support (verified at the REPL), so this over-estimate is always safe.

Pipeline:

  compute-scene    scene data from resolved draft layers + opts (no pixels)
  compute-padding  scene + cfg + opts -> padding map (no pixel dims yet)
  compute-dims     scene + padding + cfg + opts -> pw/ph/total-w/total-h

None of these need actual per-panel tick positions -- the tick
budget is baked into y-label-pad in `compute-padding` via the
over-estimate trick. Real per-panel ticks are computed AFTER
`compute-dims` when the final pw/ph are known.
raw docstring

scicloj.plotje.impl.membrane

Defines PlotjeMembrane -- the value type returned by the membrane stage of the pipeline.

A PlotjeMembrane is a Membrane UI component implementing IOrigin, IBounds, and IChildren, so it composes naturally with other Membrane components and renders through any Membrane backend without special-casing.

Fields:

  • :drawables a vector of membrane.ui drawing primitives
  • :width canvas width in drawing units
  • :height canvas height in drawing units

Plus optional namespaced attributes carried as map entries:

  • :plotje/title the plot title, or absent when unset

Future per-membrane attributes (subtitle, caption, ...) are added as :plotje/* keys via assoc, not as new record fields, so the record arity stays stable.

This namespace requires [membrane.ui]. Other impl/ namespaces keep that dependency at arm's length, but the membrane stage's value type cannot avoid it -- a PlotjeMembrane is, by definition, a Membrane component.

Defines `PlotjeMembrane` -- the value type returned by the
membrane stage of the pipeline.

A `PlotjeMembrane` is a Membrane UI component implementing
`IOrigin`, `IBounds`, and `IChildren`, so it composes naturally
with other Membrane components and renders through any Membrane
backend without special-casing.

Fields:

- `:drawables`     a vector of `membrane.ui` drawing primitives
- `:width`         canvas width in drawing units
- `:height`        canvas height in drawing units

Plus optional namespaced attributes carried as map entries:

- `:plotje/title`  the plot title, or absent when unset

Future per-membrane attributes (subtitle, caption, ...) are added
as `:plotje/*` keys via `assoc`, not as new record fields, so the
record arity stays stable.

This namespace requires `[membrane.ui]`. Other `impl/` namespaces
keep that dependency at arm's length, but the membrane stage's
value type cannot avoid it -- a `PlotjeMembrane` is, by
definition, a Membrane component.
raw docstring

scicloj.plotje.impl.plan

Draft-to-plan pipeline: domains, ticks, legends, layout, and grid inference. Takes draft maps (from pose/leaf->draft) and produces a Plan record with all geometry needed for rendering.

Draft-to-plan pipeline: domains, ticks, legends, layout, and grid inference.
Takes draft maps (from pose/leaf->draft) and produces a Plan record
with all geometry needed for rendering.
raw docstring

scicloj.plotje.impl.pose

Pose substrate -- the recursive plain-map type that is the library's spec vocabulary. This namespace holds the pure tree operations (resolve, layout, shared-scale injection) and the leaf->draft emitter that feeds plan.clj.

Shape of a pose: {:data ? dataset (inherited from ancestor if absent) :mapping ? aesthetic mappings (merges with ancestors) :layers ? layers at this level (accumulate into leaves) :poses ? sub-poses; absence = leaf :layout ? {:direction :horizontal|:vertical :weights [pos-num ...]} :opts ? plot options (inheritable) :share-scales ? #{:x :y} for composites}

Pose substrate -- the recursive plain-map type that is the
library's spec vocabulary. This namespace holds the pure tree
operations (resolve, layout, shared-scale injection) and the
leaf->draft emitter that feeds plan.clj.

Shape of a pose:
  {:data         ?  dataset (inherited from ancestor if absent)
   :mapping      ?  aesthetic mappings (merges with ancestors)
   :layers       ?  layers at this level (accumulate into leaves)
   :poses       ?  sub-poses; absence = leaf
   :layout       ?  {:direction :horizontal|:vertical
                     :weights   [pos-num ...]}
   :opts         ?  plot options (inheritable)
   :share-scales ?  #{:x :y}  for composites}
raw docstring

scicloj.plotje.impl.pose-schema

Malli schema for the Pose data model.

A pose is a plain recursive map. A leaf pose has no :poses (or an empty :poses vector). A composite pose has :poses and an optional :layout describing how sub-poses tile a bounding rectangle.

Validation is not wired into any runtime path yet; Phase 6 of the pre-alpha refactor adds validation at public API boundaries. Until then, impl.pose operates on structurally-valid poses by convention; this schema is the authoritative definition of that convention.

The three stages are documented to different depths and enforced differently. The plan schema is checked on every plan, unless the :validate configuration key turns it off -- it is what raises Plan does not conform to schema, which is where a mapping value with no reading tends to surface. Pose and draft are documented and unenforced. What keeps this schema honest instead is impl/pose_schema_test.clj, which validates a pose from each public constructor.

Decisions made in Phase 2:

  • A leaf with no :data and no :mapping is valid -- leaves inherit context from ancestors via impl.pose/resolve-tree.
  • :share-scales is structurally allowed on any pose; it is a no-op on leaves (nothing to share).
  • :layout :weights length is not required to equal (count :poses); impl.pose/compute-layout tolerates short/long weight vectors.

aesthetic-value-schemas below writes down the mapping value grammar per aesthetic, as the code behaves today. It decides nothing: where two aesthetics disagree, both readings are recorded, because what a mapping value should mean is an open design question and this schema is a description of the answer in force rather than an argument for one.

Malli schema for the Pose data model.

A pose is a plain recursive map. A leaf pose has no :poses (or
an empty :poses vector). A composite pose has :poses and an
optional :layout describing how sub-poses tile a bounding
rectangle.

Validation is not wired into any runtime path yet; Phase 6 of the
pre-alpha refactor adds validation at public API boundaries. Until
then, impl.pose operates on structurally-valid poses by convention;
this schema is the authoritative definition of that convention.

The three stages are documented to different depths and enforced
differently. The plan schema is checked on every plan, unless the
`:validate` configuration key turns it off -- it is what raises
`Plan does not conform to schema`, which is where a mapping value
with no reading tends to surface. Pose and draft are documented and
unenforced. What keeps this schema honest instead is
`impl/pose_schema_test.clj`, which validates a pose from each public
constructor.

Decisions made in Phase 2:
- A leaf with no :data and no :mapping is valid -- leaves inherit
  context from ancestors via impl.pose/resolve-tree.
- :share-scales is structurally allowed on any pose; it is a no-op
  on leaves (nothing to share).
- :layout :weights length is not required to equal (count :poses);
  impl.pose/compute-layout tolerates short/long weight vectors.

`aesthetic-value-schemas` below writes down the mapping value
grammar per aesthetic, as the code behaves today. It decides
nothing: where two aesthetics disagree, both readings are
recorded, because what a mapping value should mean is an open
design question and this schema is a description of the answer
in force rather than an argument for one.
raw docstring

scicloj.plotje.impl.position

Position adjustment — composable transforms on layer descriptors. Runs between extract-layer and build-panels in the plan pipeline.

Position types: :identity — no adjustment (default) :dodge — side-by-side within a categorical band (annotation) :stack — cumulative y-values across groups (data transform) :fill — normalized cumulative y, sums to 1.0 (data transform)

Position adjustment — composable transforms on layer descriptors.
Runs between extract-layer and build-panels in the plan pipeline.

Position types:
  :identity — no adjustment (default)
  :dodge    — side-by-side within a categorical band (annotation)
  :stack    — cumulative y-values across groups (data transform)
  :fill     — normalized cumulative y, sums to 1.0 (data transform)
raw docstring

scicloj.plotje.impl.text

Estimated pixel extent of drawn text.

Text is the one mark whose size is fixed in pixels rather than derived from the data, so two stages need the same answer to how much room a string takes: the renderer, which places the glyphs and the box behind them, and the plan, which widens a numeric domain so a label near an edge is drawn in full. Both read the estimate from here, so the two cannot drift apart.

The estimate is a per-character advance of 0.6 times the font size. Real glyph metrics are backend-specific -- the SVG target has none at all -- and a slight over-estimate is the safe direction for both callers: the renderer draws a box a shade wide, and the plan leaves a shade more room than the glyphs need.

A third caller cannot use an average at all. Where a label is pushed away from an edge to keep it whole, reading low by a pixel is what cuts the glyph the move was meant to save, so that caller measures with max-text-width instead.

Estimated pixel extent of drawn text.

Text is the one mark whose size is fixed in pixels rather than
derived from the data, so two stages need the same answer to how
much room a string takes: the renderer, which places the glyphs and
the box behind them, and the plan, which widens a numeric domain so
a label near an edge is drawn in full. Both read the estimate from
here, so the two cannot drift apart.

The estimate is a per-character advance of 0.6 times the font size.
Real glyph metrics are backend-specific -- the SVG target has none
at all -- and a slight over-estimate is the safe direction for both
callers: the renderer draws a box a shade wide, and the plan leaves
a shade more room than the glyphs need.

A third caller cannot use an average at all. Where a label is pushed
away from an edge to keep it whole, reading low by a pixel is what
cuts the glyph the move was meant to save, so that caller measures
with `max-text-width` instead.
raw docstring

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