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

Two panels in a nested subfigure with measured panel letters

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).

Search documentation

Type to search guides, examples and the API.