Skip to content

Rendering

sekai.rendering. See the Rendering guide; the classic-control and finance renderers are documented under Environments, and the market charts under Finance.

Renderer

Bases: Protocol

Protocol for rendering backends: any object with render(env) and close().

Renderers are separate from Env — they are injected and managed independently, so you can swap, compose, or record without touching the environment. Subclass BaseRenderer to also get a no-op close() and context-manager support.

Example
renderer = CartPoleRenderer()
result = env.reset(seed=0)
frames = [renderer.render(env)]
while not result.done:
    result = env.step(env.action_space.sample())
    frames.append(renderer.render(env))

render

render(env: Any) -> RenderFrame

Render the current state of env.

close

close() -> None

Release renderer resources.

Canvas

Canvas(width: int, height: int, background: Color = (255, 255, 255), *, antialias: bool = False)

A tiny dependency-free RGB raster for drawing environment frames.

Coordinates are in pixels with the origin at the bottom-left and y pointing up, which suits physics drawings. to_array() returns an (H, W, 3) uint8 array, the RGB frame format used throughout sekai.rendering.

By default shapes are filled by testing pixel centres (crisp, no anti-aliasing). With antialias=True every shape is drawn with exact coverage at its edges: smooth lines and curves, rectangles that land on fractional pixels, round line caps and joins. Text is always anti-aliased. Every shape takes alpha (opacity in [0, 1]), and clip() restricts drawing to a rectangle.

Parameters:

Name Type Description Default
width int

Width in pixels.

required
height int

Height in pixels.

required
background Color

Initial fill colour.

(255, 255, 255)
antialias bool

Draw shapes with anti-aliased edges.

False

clear

clear(color: Color = (255, 255, 255)) -> None

Fill the whole canvas (ignoring any clip).

clip

clip(left: float, bottom: float, right: float, top: float) -> Iterator[None]

Restrict drawing to a rectangle while the with block runs.

Pixels partly inside the rectangle are drawable; nested clips intersect.

Example
with canvas.clip(40, 20, 300, 180):
    canvas.polyline(points, (0, 0, 0))  # nothing lands outside the box

polygon

polygon(points: ArrayLike, color: Color, *, alpha: float = 1.0) -> None

Fill a simple polygon given as a sequence of (x, y) vertices.

rect

rect(left: float, bottom: float, right: float, top: float, color: Color, *, alpha: float = 1.0, radius: float = 0.0) -> None

Fill an axis-aligned rectangle, optionally with corners rounded by radius.

circle

circle(cx: float, cy: float, radius: float, color: Color, *, alpha: float = 1.0) -> None

Fill a disc.

line

line(start: Sequence[float], end: Sequence[float], color: Color, width: float = 1.0, *, alpha: float = 1.0, dash: tuple[float, float] | None = None) -> None

Draw a straight segment of the given width (in pixels).

dash=(on, off) draws it dashed, on pixels drawn then off skipped.

polyline

polyline(points: ArrayLike, color: Color, width: float = 1.0, *, alpha: float = 1.0, dash: tuple[float, float] | None = None) -> None

Draw connected segments through points ((x, y) rows).

A non-finite point breaks the line, so NaN marks a gap in a series. Anti-aliased polylines have round joins and caps, and overlapping segments are drawn once (a translucent line stays even). dash=(on, off) draws it dashed.

fill_between

fill_between(x: ArrayLike, y1: ArrayLike, y2: ArrayLike, color: Color, *, alpha: float = 1.0) -> None

Fill the area between two curves sampled at increasing x.

The curves are linear between samples; a non-finite y leaves a gap. For a step (piecewise-constant) curve, repeat each x with the old and new value.

image

image(image: ArrayLike, left: float, bottom: float, right: float, top: float, *, alpha: float = 1.0) -> None

Draw an RGB (h, w, 3) or RGBA (h, w, 4) uint8 image scaled into a box.

Each source pixel becomes a crisp block (nearest-neighbour scaling): this draws heatmaps and matrices of cells, with an image's first row at the top.

text

text(x: float, y: float, text: str, color: Color, *, size: float = 12, bold: bool = False, align: HAlign = 'left', valign: VAlign = 'baseline', alpha: float = 1.0) -> float

Draw a line of text and return its width in pixels.

align places x at the text's left edge, centre or right edge. valign places y at the baseline, the middle of the capital letters, their top, or the bottom of the descenders. The typeface is BDO Grotesk at sizes 9-32 px (other sizes snap to the nearest; larger ones are scaled), regular or bold, with tabular digits; see sekai.rendering.text.

rotate

rotate(points: ArrayLike, angle: float, origin: Sequence[float] = (0.0, 0.0)) -> NDArray[float64]

Rotate (x, y) points counter-clockwise by angle (radians) about origin.

text_width

text_width(text: str, size: float = 12, bold: bool = False) -> float

The width in pixels of text drawn by Canvas.text at this size and weight.

RecordFrames

RecordFrames(env: Env[ObsType, ActType], renderer: Renderer, *, enabled: bool = True, max_frames: int | None = None)

Bases: Wrapper[ObsType, ActType, ObsType, ActType]

Captures a frame from renderer after every reset() and step().

Rendering stays outside the env: any Renderer works, and recording can be switched off (enabled = False) without touching the environment.

Example
env = RecordFrames(sekai.make("CartPole-v1"), CartPoleRenderer())
... run an episode ...
env.save("cartpole.gif")

save

save(path: str | Path, fps: float | None = None) -> Path

Save the captured frames as .gif (Pillow) or any video format imageio supports.

fps defaults to the env's metadata["render_fps"] (30 if it has none). The frames must be RGB arrays; frames from a text ("ansi") renderer cannot be saved.

save_gif

save_gif(frames: Sequence[NDArray[uint8]], path: str | Path, fps: float = 30) -> Path

Write RGB frames to an animated GIF. Needs Pillow (pip install 'sekai[render]').

frames: a list of (H, W, 3) arrays or one (T, H, W, 3) array, all of one shape, with integer values in [0, 255]; fps must be > 0. Bad frames raise before anything is written.

save_video

save_video(frames: Sequence[NDArray[uint8]], path: str | Path, fps: float = 30) -> Path

Write RGB frames to a video file (e.g. .mp4). Needs imageio with ffmpeg.

frames: a list of (H, W, 3) arrays or one (T, H, W, 3) array, all of one shape, with integer values in [0, 255]; fps must be > 0. Bad frames raise before anything is written.

Dashboards

Dashboard

Dashboard(*items: Any, title: str | Source | None = None, subtitle: str | Source | None = None, stats: Sequence[Stat] = (), heights: Sequence[float] | None = None, width: int = 720, height: int = 450, theme: Theme | str = 'light', step: Source | None = 'elapsed_steps', horizon: float | Source | None = 'auto', window: int | None = None)

Bases: BaseRenderer

A renderer that draws an environment's episode as a dashboard of charts.

Parameters:

Name Type Description Default
*items Any

The layout, top to bottom: Panels, Rows / Columns of them, or lists (a list is a Row).

()
title str | Source | None

The heading (default: the env's registered id or class name); a string or a source.

None
subtitle str | Source | None

A line under the title (a string or a source), e.g. the date.

None
stats Sequence[Stat]

Headline numbers shown at the top right.

()
heights Sequence[float] | None

Relative heights of the top-level items (default: their own height).

None
width int

Frame width in pixels.

720
height int

Frame height in pixels.

450
theme Theme | str

"light", "dark" or a Theme.

'light'
step Source | None

The episode clock, which is also the time axis (default: elapsed_steps; any number works, e.g. a session time); None counts renders.

'elapsed_steps'
horizon float | Source | None

The clock's value at the end of an episode, which fixes the time axis so the episode draws from left to right: a number, a source, None (the axis grows with the episode) or "auto" (the env's episode_length or n_steps, else the max_episode_steps of its spec).

'auto'
window int | None

Show only the latest window steps (a scrolling chart).

None

set_layout

set_layout(*items: Any, heights: Sequence[float] | None = None) -> None

Replace the charts (and forget recorded histories): the same arguments as above.

A renderer that sizes its charts from the env (one line per trader, say) can call this from check() the first time it sees the env.

check

check(env: Any) -> None

Raise if env cannot be drawn (subclasses check its type and that it was reset).

reset

reset(env: Any = None) -> None

Forget the recorded history of env (of every env if None).

render

render(env: Any) -> NDArray[uint8]

Record the env's current state and draw the episode so far.

Panel

Panel(title: str | None = None, *, width: float = 1.0, height: float = 1.0)

One chart of a dashboard. Subclass it to draw your own.

A panel declares the values it needs (sources(), recorded every frame, and context(), read once per episode) and draws them in draw(canvas, box, view), reading them back from the View with the same keys. Time-series panels (timeseries = True) share the dashboard's time axis; the helper frame() draws the title row and legend and returns the plot area.

Parameters:

Name Type Description Default
title str | None

Shown at the panel's top left.

None
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

sources

sources() -> Mapping[str, Source]

Values to record every frame, by key.

context

context() -> Mapping[str, Source]

Values to read once, at the first frame of an episode, by key.

x_start

x_start(view: View) -> float | None

The earliest time this panel shows, if before the first recorded step.

frame

frame(canvas: Canvas, box: Box, view: View, legend_items: Sequence[tuple[str, ColorLike, Swatch]] = (), *, gutter: bool | None = None) -> Box

Draw the title row (title left, legend right) and return the plot area.

Time-series panels reserve the dashboard's y-axis gutter on the right and, when they show the time axis, a band underneath for its labels.

time_axis

time_axis(plot: Plot, view: View) -> None

Label the time axis under plot, if this panel shows it.

View

View(env: Any, history: _History, prefix: str, theme: Theme, scale: float, x_domain: tuple[float, float], show_x: bool, gutter: float)

What a panel sees when it draws: the recorded episode, the time axis and the style.

Attributes:

Name Type Description
env

The environment being rendered (as passed to render).

theme

The colours.

scale

Size multiplier (1 at 720 x 450).

steps NDArray[float64]

The episode clock of each recorded sample.

x_domain

The time axis shared by every time-series panel.

show_x

Whether this panel draws the time axis labels (the lowest in its column).

gutter

Width reserved right of time-series plots for y-axis labels.

step property

step: float

The current value of the episode clock.

raw

raw(key: str) -> list[Any]

Every recorded value of a source, as recorded.

latest

latest(key: str) -> Any

The current value of a source.

values

values(key: str) -> NDArray[float64]

A source's recorded values as floats: (n,) for numbers, (n, k) for vectors.

Missing values are NaN; vectors of different lengths are padded with NaN.

context

context(key: str) -> Any

A context source, read once at the start of the episode.

size

size(px: float) -> float

A size in pixels at 720 x 450, scaled to this frame.

Row

Row(*children: Any, widths: Sequence[float] | None = None, height: float = 1.0)

Panels or columns side by side. widths overrides the children's own widths.

Column

Column(*children: Any, heights: Sequence[float] | None = None, width: float = 1.0)

Panels or rows stacked top to bottom. heights overrides the children's own.

Series dataclass

Series(source: Source, label: str | None = None, color: ColorSpec = None, fill: bool = False, signed: bool = False, step: bool = False, dashed: bool = False, width: float = 2.0, rebase: float | None = None)

One line of a LineChart.

Parameters:

Name Type Description Default
source Source

The value per step.

required
label str | None

Legend label (charts with two or more labelled series show a legend).

None
color ColorSpec

Theme role, hex, RGB or palette slot; default: the slot of its position.

None
fill bool

Shade the area under the line (a 10% wash, down to the chart's baseline).

False
signed bool

Shade the area in the up colour above the baseline and down below.

False
step bool

Hold each value until the next step (for inventories, positions).

False
dashed bool

Draw dashed (benchmarks, plans and other references).

False
width float

Line width in pixels (at 720 x 450).

2.0
rebase float | None

Divide by the episode's first value and multiply by this (index the series, e.g. rebase=100), so differently scaled series share an axis.

None

Stat dataclass

Stat(label: str, source: Source, format: str | Callable[[Any], str] = '{:,.2f}', delta: Literal['relative', 'absolute'] | None = None, higher_is_better: bool = True)

A headline number in a dashboard's header.

Parameters:

Name Type Description Default
label str

What it is ("Equity").

required
source Source

Its value.

required
format str | Callable[[Any], str]

A format string ("{:.2f}") or a function of the value returning text.

'{:,.2f}'
delta Literal['relative', 'absolute'] | None

Also show its change since the episode began: "relative" (percent) or "absolute", with a direction triangle.

None
higher_is_better bool

Whether a rise is good: it picks the delta's colour.

True

LineChart

LineChart(*series: Series | Source, title: str | None = None, baseline: float | None = None, percent: bool = False, tag: bool = True, limits: Sequence[float] = (), shade: Source | None = None, shade_label: str | None = None, shade_color: ColorSpec = 'down', width: float = 1.0, height: float = 1.0)

Bases: Panel

Series over time, on one shared y-axis.

Parameters:

Name Type Description Default
*series Series | Source

Series, or sources (each becomes a plain Series).

()
title str | None

Panel title.

None
baseline float | None

A reference value drawn as a baseline and kept in view (e.g. 0, or 1.0 for equity that starts at 1).

None
percent bool

Label the y-axis (and the value tag) as percentages.

False
tag bool

Show the first series' latest value in a tag on the y-axis.

True
limits Sequence[float]

Reference values drawn as dashed lines (e.g. inventory limits).

()
shade Source | None

A source that is true while a condition holds (a regime, a drawdown, market hours): those steps are shaded behind the series.

None
shade_label str | None

Legend label for the shading.

None
shade_color ColorSpec

Colour of the shading (default: the down role).

'down'
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

BarChart

BarChart(source: Source, *, labels: Sequence[str] | Source | None = None, colors: Sequence[ColorSpec] | None = None, color: ColorSpec = None, negative_color: ColorSpec = 'down', available: Source | None = None, percent: bool = True, limit: float | None = None, title: str | None = None, width: float = 1.0, height: float = 1.0)

Bases: Panel

The current values of a vector as labelled horizontal bars (weights, holdings).

Parameters:

Name Type Description Default
source Source

A vector per step.

required
labels Sequence[str] | Source | None

Bar labels: a sequence, or a source giving one per frame.

None
colors Sequence[ColorSpec] | None

One colour per bar (theme roles, hex, RGB or slots); default: slot i for bar i, when bars are separate entities shown elsewhere in those colours.

None
color ColorSpec

One colour for every bar instead (a single series identified by its labels), e.g. "accent". Negative values use negative_color.

None
negative_color ColorSpec

Colour of negative bars when color is given.

'down'
available Source | None

A source of a boolean per bar; unavailable bars are greyed out.

None
percent bool

Write values as percentages.

True
limit float | None

The value a full-length bar stands for (default: a round number just above the largest magnitude).

None
title str | None

Panel title.

None
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

StackedArea

StackedArea(source: Source, *, labels: Sequence[str] | None = None, colors: Sequence[ColorSpec] | None = None, percent: bool = True, legend: bool = False, title: str | None = None, width: float = 1.0, height: float = 1.0)

Bases: Panel

A vector over time as stacked areas (an allocation's composition, say).

Positive parts stack up from zero, negative parts down. Layers are separated by a thin line in the surface colour.

Parameters:

Name Type Description Default
source Source

A vector per step.

required
labels Sequence[str] | None

Layer labels for the legend.

None
colors Sequence[ColorSpec] | None

Layer colours; default: slot i for layer i.

None
percent bool

Label the axis as percentages.

True
title str | None

Panel title.

None
width float

Relative width in its row.

1.0
height float

Relative height in its column.

1.0

Themes

Theme dataclass

Theme(name: str, background: Color, grid: Color, axis: Color, text: Color, text_secondary: Color, text_muted: Color, price: Color, accent: Color, neutral: Color, up: Color, down: Color, highlight: Color, series: tuple[Color, ...])

The colours of a chart, by role.

Attributes:

Name Type Description
name str

A short name ("light", "dark", ...).

background Color

The frame and plot surface.

grid Color

Hairline gridlines, one step off the surface.

axis Color

Baselines and the progress-bar track.

text Color

Primary ink: titles and values.

text_secondary Color

Panel titles and legends.

text_muted Color

Axis labels and stat labels.

price Color

The main price line.

accent Color

The series a chart is about, e.g. the agent's equity.

neutral Color

De-emphasised series (benchmarks, cash) and reference lines.

up Color

Long positions, buys, bids, gains.

down Color

Short positions, sells, asks, losses.

highlight Color

The agent's own orders and quotes.

series tuple[Color, ...]

The categorical palette, in assignment order.

color

color(slot: int) -> Color

Categorical colour slot (0-based); slots past the palette get neutral.

Colours are never reused: a ninth series is folded into the neutral grey rather than given a colour that could be mistaken for the first.

ink_on

ink_on(fill: ColorLike) -> Color

The text colour (light or dark ink) that reads best on a filled fill.

with_colors

with_colors(*, series: Sequence[ColorLike] | None = None, **colors: ColorLike) -> Theme

A copy with some roles changed, e.g. LIGHT.with_colors(up="#2a78d6").

series replaces the categorical palette (validate a new one for colour-vision deficiency before you rely on it).

LIGHT module-attribute

LIGHT = _theme('light', ('#107c5a', '#bd8928', '#82498b', '#c45e3d', '#0098a9', '#7b8935', '#3d73b6', '#cc6b83'), background='#f8f6f2', grid='#eae3d7', axis='#cfc7b8', text='#1f271b', text_secondary='#4d5545', text_muted='#6f6c5f', price='#1f271b', accent='#107c5a', neutral='#a8a294', up='#439458', down='#a12f2f', highlight='#bd8928')

Warm paper surface, forest ink: the Qian Capital light palette.

DARK module-attribute

DARK = _theme('dark', ('#3b9a76', '#bc8726', '#a86cb1', '#cf6746', '#0ba5b6', '#808f3b', '#578ed4', '#d16f88'), background='#161c13', grid='#262f22', axis='#3b4636', text='#ece3d0', text_secondary='#c4bfae', text_muted='#8e9483', price='#ece3d0', accent='#3b9a76', neutral='#6f7566', up='#3aa273', down='#b94642', highlight='#c0aa81')

Forest-ink surface, gold-white ink: the Qian Capital dark palette.

get_theme

get_theme(theme: Theme | str) -> Theme

A Theme, or the built-in one named "light" or "dark".

Chart helpers

sekai.rendering.charts: what the charts above are drawn with, for drawing your own.

Chart drawing on a Canvas: layout boxes, nice ticks, number formats and plot marks.

These are the pieces sekai's dashboards (sekai.rendering.Dashboard) and finance renderers are drawn with, and they are public so a custom renderer can draw charts in the same style:

canvas = Canvas(640, 360, LIGHT.background, antialias=True)
plot = Plot(canvas, Box(16, 24, 580, 340), (0, 100), (0.9, 1.2), LIGHT)
plot.grid(nice_ticks(0.9, 1.2))
plot.line(steps, equity, LIGHT.accent)

Marks follow fixed specs: 2 px lines with round caps and joins, hairline (1 px) solid gridlines one step off the surface, area fills as a 10% wash of the series colour, end markers with a 2 px ring in the surface colour, and text in the ink colours rather than series colours. Sizes scale with scale.

Box dataclass

Box(left: float, bottom: float, right: float, top: float)

An axis-aligned rectangle in canvas coordinates (origin bottom-left, y up).

inset

inset(left: float = 0.0, bottom: float = 0.0, right: float = 0.0, top: float = 0.0) -> Box

The box shrunk by the given margins.

rows

rows(weights: Sequence[float], gap: float = 0.0) -> list[Box]

Split into stacked boxes, top first, sized by weights, gap apart.

columns

columns(weights: Sequence[float], gap: float = 0.0) -> list[Box]

Split into side-by-side boxes, left first, sized by weights, gap apart.

snapped

snapped() -> Box

The box with its edges on whole pixels (for crisp hairlines and fills).

Plot

Plot(canvas: Canvas, box: Box, x_domain: tuple[float, float], y_domain: tuple[float, float], theme: Theme, scale: float = 1.0)

A box of the canvas mapped to data coordinates, with methods that draw marks.

Marks are clipped to the box; labels and value tags may extend into the space to its right (the y-axis gutter, where tick labels go).

Parameters:

Name Type Description Default
canvas Canvas

The canvas to draw on (anti-aliased canvases look best).

required
box Box

The plot area.

required
x_domain tuple[float, float]

Data range shown across the box.

required
y_domain tuple[float, float]

Data range shown from bottom to top.

required
theme Theme

Colours.

required
scale float

Size multiplier for line widths, markers and text (1 at 720 x 450).

1.0

x

x(values: ArrayLike) -> NDArray[float64]

Canvas x of data x.

y

y(values: ArrayLike) -> NDArray[float64]

Canvas y of data y.

grid

grid(ticks: ArrayLike, labels: Sequence[str] | None = None, *, size: float = 10, avoid: Sequence[float | None] = ()) -> None

Hairline gridlines at y ticks, labelled in the gutter right of the box.

Labels within a tag's height of a data y in avoid are left out, so a value tag drawn there (tag()) does not cover one.

x_ticks

x_ticks(ticks: ArrayLike, labels: Sequence[str], *, size: float = 10) -> None

Labels under the box at x ticks (no gridlines: time runs left to right).

hline

hline(value: float, color: ColorLike, *, width: float = 1.0, dash: tuple[float, float] | None = None, alpha: float = 1.0) -> None

A horizontal line across the box at data y value (a baseline or reference).

vline

vline(value: float, color: ColorLike, *, width: float = 1.0, alpha: float = 1.0, dash: tuple[float, float] | None = None) -> None

A vertical line across the box at data x value (e.g. the start of an episode).

line

line(xs: ArrayLike, ys: ArrayLike, color: ColorLike, *, width: float = 2.0, dash: tuple[float, float] | None = None, alpha: float = 1.0, step: bool = False) -> None

A series as a line (NaN leaves a gap); step=True holds values between samples.

area

area(xs: ArrayLike, ys: ArrayLike, color: ColorLike, *, base: ArrayLike | float | None = None, alpha: float = 0.1, step: bool = False) -> None

Fill between a series and base (default: the bottom of the box).

shade

shade(x0: float, x1: float, color: ColorLike, *, alpha: float = 0.08) -> None

A vertical band over the box's full height, from data x x0 to x1.

marker

marker(x: float, y: float, color: ColorLike, *, shape: Marker = 'circle', radius: float = 4.0, ring: bool = True) -> None

A marker (radius >= 4 px, i.e. at least 8 px across) with a surface ring.

tag

tag(y: float, text: str, color: ColorLike, *, size: float = 10) -> None

A value tag: a pill in the gutter at data y y, filled with color.

nice_ticks

nice_ticks(low: float, high: float, count: int = 4, *, integer: bool = False) -> NDArray[float64]

Round tick values inside [low, high], about count intervals apart.

integer=True places ticks on whole numbers only.

nice_step

nice_step(span: float, count: int = 4, *, integer: bool = False) -> float

A round step (1, 2, 2.5 or 5 times a power of ten) giving about count intervals.

integer=True keeps the step a whole number (1, 2, 5, 10, ...), for counts.

tick_labels

tick_labels(ticks: ArrayLike, *, percent: bool = False, compact: bool | None = None) -> list[str]

Labels for evenly spaced ticks, all with the same number of decimals.

format_number

format_number(value: float, decimals: int = 2, *, sign: bool = False, percent: bool = False, compact: bool = False) -> str

A number for display: thousands separators, a true minus sign, optional +.

percent=True multiplies by 100 and appends %; compact=True writes large magnitudes as 12.5K, 3.4M, 1.2B. NaN and None are written as an em dash.

value_range

value_range(*arrays: ArrayLike, include: Sequence[float] = (), pad: float = 0.08) -> tuple[float, float]

The finite range of some data (plus include), padded by pad of its span.

A flat or empty range is widened so a scale can always be built from it.

legend

legend(canvas: Canvas, x: float, y: float, items: Sequence[tuple[str, ColorLike, Swatch]], theme: Theme, *, scale: float = 1.0, size: float = 10, align: Literal['left', 'right'] = 'right') -> float

Legend entries (label, colour, swatch kind) on one line; returns its width.

x is the right edge (align="right") or left edge of the legend, y the middle of its line. Labels are in secondary ink: the swatch carries identity.