On this page
Page layout¶
A document places named cells on a physical page. Start with a grid; use a measured composition when components need an irregular arrangement.
Run the Python blocks on this page in order. They use core Inklet; generating
PNG previews additionally needs the render extra.
Rows, columns and spans¶
import inklet as i
left = i.plot_spec(x=(0, 2), y=(0, 5)).line([(0, 1), (1, 3), (2, 2)]).axes()
right = i.plot_spec(x=(-.5, 2.5), y=(0, 5))
right.bars([0, 1, 2], [2, 4, 3], width=.8)
right.axis('bottom', ticks=[0, 1, 2]).axis('left')
doc = i.document(width=180, columns=[1, 1], gap=8, row_gap=5, margin=5,
share_plot_margins=True)
doc.add('left', left, row=0, column=0, min_width=60, min_height=50)
doc.add('right', right, row=0, column=1, min_width=60, min_height=50)
doc.add('caption', i.component(i.text, 'Two simulated measurements'),
row=1, colspan=2, min_height=10)
figure = doc.compile()
assert figure.cells['caption'].width == 170
On numeric scales, bars are centred at their x positions and width is in
data units. Include their full width in the domain: these bars extend from
−0.4 to 2.4, inside the chosen limits of −0.5 to 2.5. Categorical scales
reserve a slot for each category automatically.
Rows and columns are zero-based. Omitting row appends below existing cells;
it does not fill the next unused column. When the cell's columns are free in the
last row, that is ambiguous, so add() raises a LayoutError that names both
rows: pass row= for the one you mean. rowspan and colspan reserve multiple
tracks.
A cell name is an id, used by links, lint and the figure's cells mapping. It
starts with a letter and contains letters, digits, underscores or hyphens, and
names are unique within each document. For a label with spaces, such as a region
name, give the plot a title() and keep the cell name short.
columns=2 creates equal columns. columns=[1, 2] requests a 1:2 allocation,
adjusted as needed to meet cell minima. min_width and min_height constrain
the entire cell, including axes and labels. They are not data-domain limits.
Fixed artwork reserves its measured width, including its panel letter, so a
drawing never needs a min_width to fit its column.
A plot's width given to plot_spec() is a floor. The data area is at least
that wide, with its furniture beside it, and the plot fills any extra room in its
column. When the columns cannot hold that, the plot is drawn narrower and lint
reports PLOT_WIDTH_OVERRIDDEN, with the width it was given and the one it has.
A plot whose width was left to its default is not held to it.
With no explicit page height, rows grow to meet their measured content and
minimum heights. A fixed height starts from those same natural rows and
gives the extra space to plot rows in proportion to their data heights, so
stacked plots keep one scale; nested grids and responsive components grow with
their natural height, and text and fixed artwork keep their size. A nested
subfigure is laid out this way at the height of its parent cell, so it keeps
the plot heights authored for it. When the height is too small, rows shrink in
proportion down to their minima. Plot areas resize; text and strokes retain
their physical dimensions. A plot cell's default min_height is 5 mm, so a
short authored plot keeps a short row. Set
share_plot_margins=True to align plot areas by sharing relevant axis and
legend margins within a grid. Left and right margins are shared along vertical
grid lines: stacked plots that start on the same grid line get the same left
margin, and stacked plots that end on the same grid line get the same right
margin. Plots are stacked when their rows touch, directly or through other
plots on that line; a key beside one plot does not narrow a plot further down
the page with other content between them. A plot that shares neither line
with a wide label is not narrowed by it. Top and
bottom margins and data heights are shared along each row only, so rows keep
the plot heights set for them and an axis under the last row reserves no space
under the others.
A y-axis title longer than its plot may reach above and below it, centred on the
plot, and the plot keeps its authored height. The title may not hit anything: if
it runs into a neighbouring cell or past the page edge, lint reports
AXIS_TITLE_OVERHANG, with its length, the plot's height and what it hits. Give
the plot more height, shorten the title, or break it over lines with \n.
share_plot_margins='all' shares the largest left and right margins across
the whole grid instead, and gives every plot the tallest data height, so equal
columns give equal data areas. This is
opt-in; equal cell sizes alone do not guarantee equal data-area sizes when
labels take different amounts of space.
One name for a grid¶
A grid of panels repeats its numbers on every cell, but the name of the
quantity belongs once. doc.label(x=, y=) centres the x name under the whole
grid's plot areas and turns the y name beside them, so a 4 x 2 grid gets one
"Month Year" and one "P-score (%)". The cells keep their own ticks. The grid
makes room for the names, and a fixed page height is shared with the x name.
import inklet as i
doc = i.preset('scientific.nature', format='single-column').document(columns=2)
for k in range(8):
p = i.plot_spec(38, 22, x=(0, 12), y=(0, 100))
p.line([(m, 20 + 5 * k + 3 * m) for m in range(12)])
p.axes(x_options=dict(ticks=[0, 6, 12]), y_options=dict(ticks=[0, 50, 100]))
doc.add(f'c{k}', p, row=k // 2, column=k % 2)
doc.label(x='Month Year', y='P-score (%)')
doc.save('grid.svg', 'grid.pdf')
inklet.facets(panels, x_label=, y_label=) does the same for a grid built as one
drawing, with the panels' own shared axes.
Align fixed artwork¶
Fixed artwork can align to a cell edge or corner:
aligned = i.document(width=100, height=60, columns=2).letters()
aligned.add('input', i.component(i.box, 'Input', width=30, height=18),
row=0, column=0, align='nw')
aligned.add('model', i.component(i.box, 'Model', width=30, height=30),
row=0, column=1, align='nw')
assert aligned.compile().root.width == 100
align defaults to center; it also accepts n, s, e, w, nw, ne,
sw and se. The decorated artwork, including its panel letter, aligns within
the cell. This positions geometry without scaling it. Plot cells continue to
fill their available data regions. The compiler also
preserves fixed drawings' measured letter space when a parent assigns a final
height to an automatically sized subfigure.
Letters on one line¶
lined = i.document(width=100, columns=2).letters(anchor='cell')
lined.add('trace', i.plot_spec(height=20).line([(0, 0), (1, 1)]).axes(x='t', y='F'))
lined.add('model', i.component(i.box, 'Model', width=30, height=10), row=0, column=1)
assert lined.compile().cells['model'].y0 == lined.compile().cells['trace'].y0
Letters by cell name¶
Letters follow the cells in page order. To skip a letter, or to match a letter to
a cell by name, pass names= to letters(). A cell that is not named takes the
next letter that no named cell has:
page = i.document(width=100, columns=2)
page.add('hist', i.plot_spec(30, 20, x=(0, 1), y=(0, 1)).line([(0, 0), (1, 1)]), row=0, column=0)
page.add('ci', i.plot_spec(30, 20, x=(0, 1), y=(0, 1)).line([(0, 0), (1, 1)]), row=0, column=1)
page.letters({'hist': 'a', 'ci': 'e'})
Each letter is one character, and a letter may name only one cell. A name that is not a cell of the document raises when the document compiles.
A plot's letter hangs off its data area and a drawing's off its top edge, so
letters in a row can sit at different heights. letters(anchor='cell') moves
each letter to its cell's top-left corner after placement, as journal pages
set them. Space is still reserved beside the content, so the move never
changes the layout.
Nested subfigures¶
Build a named group, then place and revise its children through the parent:
pair = i.subfigure(columns=2, gap=6, share_plot_margins=True).letters()
pair.add('control', left, row=0, column=0, min_height=50)
pair.add('treatment', right, row=0, column=1, min_height=50)
page = i.publication('double-column').document()
page.add('experiment', pair)
page['experiment']['control'].configure(y=(0, 6))
assert page.compile().root.width == 183

Rendered from the code above.
Subfigures inherit the parent theme and share its geometry cache. Labels added
by letters() are measured and have space reserved. Local names such as
control can be reused in different subfigures; compiled identities include
their containing cells. Margin sharing applies inside each grid, not across
arbitrary nested grids.
A subfigure takes the height of its parent cell. When a neighbouring cell makes
that row taller than the subfigure needs, the plots inside grow in proportion
to their authored data heights, so a stack of bar blocks keeps one bar
thickness, and text rows keep their size. To keep the natural height instead,
create the group with stretch=False and position it with the cell's align;
to hold one row at its natural height, add it with grow=False:
stack = i.subfigure(row_gap=1, stretch=False)
stack.add('key', i.text('male'), row=0)
for k, rows in enumerate([['taste peg', 'labellar'], ['leg bristle']]):
block = i.plot_spec(height=5*len(rows), x=(0, 10), y=rows)
block.bars(rows, [4]*len(rows), orient='h')
stack.add(f'block{k}', block, row=k+1)
tall = i.plot_spec(height=60, x=(0, 1), y=(0, 1)).axes()
side = i.document(width=120, columns=3)
side.add('stack', stack, row=0, column=0, align='n')
side.add('tall', tall, row=0, column=1, colspan=2)
assert side.compile().cells['stack'].height > 60
The reusable plot composition example combines independent plot variants with a responsive diagram at two physical widths.
Responsive components¶
A deferred factory can accept its available width and height:
def caption(content, *, width, height):
return i.text(content, width=width, size=i.pt(8))
page.add('caption', i.component(caption, 'A measured caption for both panels.',
responsive=True), min_height=10)
assert page.compile().cells['caption'].width > 100
During natural-height measurement, height can be None. Return a measurable
Diagram; account for its full bounds, including labels. A responsive factory
receives both keywords even if it only uses one. Ordinary component() calls
keep their factory's authored dimensions.
The engine reuses a fixed factory's result across measurement passes and page resizes when its arguments, dependencies and theme are unchanged. Factories must be deterministic. Responsive factories still receive the new cell dimensions and rebuild when those dimensions change.
Find what sets the page height¶
report = side.compile().layout_report()
assert report.splitlines()[1].split()[2] == 'tall'
layout_report() lists each row's height and the cells that set it: a cell
sets its rows when its natural height, or its min_height, fills them, so a
page gets shorter only when those cells do. It then lists the space other
cells leave unused, largest first, which shows where a drawing could grow or
a key could move. The same data is in compiled.metadata['layout'].
Let the page choose a layout¶
def keyed(**legend):
p = i.plot_spec(height=20)
p.line([(0, 0), (1, 1)], name='control').line([(0, 1), (1, 0)], name='treated')
return p.legend(**legend)
choice = i.document(width=120, columns=2)
choice.add('trend', i.choose(below=keyed(side='bottom'), inside=keyed(corner='best')))
choice.add('note', i.text('A short note'), row=0, column=1)
assert choice.compile().metadata['layout']['choices'] == {'trend': 'inside'}
i.choose() offers one cell several definitions: a key below or inside, a
flow chart drawn across or down, a wide or a stacked arrangement. The page
lays out each alternative and keeps the one that gives the shortest page;
alternatives that do not fit are skipped. List them in order of preference,
positionally or by name: an alternative replaces an earlier one only when it
saves more than 0.1 mm. With several choices on a page, each cell is settled
in turn, in page order, until no change shortens the page. layout_report()
lists the alternative kept for each cell. Every alternative is laid out at
least once, so each one adds to compile time.
Pack panels without a grid (experimental)¶
def trace():
return (i.plot_spec(40, 24, x=(0, 3), y=(0, 4))
.line([(0, 1), (1, 3), (2, 2), (3, 4)]).axes(x='Time', y='Value'))
packed = i.document(width=160, margin=2, gap=4, pack=True)
packed.add('art', i.box('tall drawing', width=40, height=80))
for name in ('one', 'two', 'three'):
packed.add(name, trace())
assert packed.compile().metadata['layout']['packing'] == '((art | one) | (two / three))'
Journal figures are rarely one grid: two plots stack beside a tall drawing,
a wide panel runs under three small ones. With pack=True the page ignores
rows and columns and packs the cells, in the order they were added, into
groups set side by side (|) or stacked (/), down to single cells. Only
cells added one after another are grouped, so the panel letters still read
left to right and top to bottom.
Each cell is measured at several widths, and the page takes the arrangement
and widths that give the shortest page. Plots pay a little for straying from
their authored proportions, and drawings for width they leave empty, so of
two equally short pages the one closer to your panels wins. A plot's data is
never narrowed below 60% of its authored width, and width past 1.25 times its
authored width counts as empty area. Beside a taller neighbor a plot grows
to at most one and a half times its height and keeps the top of its box. Content that grows taller
with width stops growing at one and a half times its smallest height, so a
drawing stays in scale with its fixed-size text; in a wider slot it is placed
by its align=. choose() cells are settled at the same time. The first line of
layout_report() and metadata['layout']['packing'] show the arrangement.
Packing is experimental: the arrangement may change between releases, plot margins are not shared between packed cells, and measuring every cell at several widths makes a large page slower to compile than a grid.
Resize and replace¶
page.configure(width=200)
page.replace('caption', i.component(caption, 'Updated caption', responsive=True))
assert page.compile().root.width == 200
replace() retains a cell's layout constraints. The document does not change
the number of columns automatically or shrink fixed drawings to fit.
An impossible layout raises LayoutError with the required and available
dimensions. Increase the cell/page size, wrap labels, or revise the arrangement.
For measured x/y expressions and connections, continue with diagrams. See troubleshooting for clipping, font sizes and common layout failures.
For required content inputs, independent instances and nested attachment points, see reusable compositions (4.0).