Charts and data
kinemo has two levels of charts:
- Axes and functions (
k.Axes,k.NumberLine,k.PolarAxes) for explanations: a curve that grows while a signal advances, a dot that slides along it, a tangent that turns, an animated zoom. - Data charts (
k.BarChart,k.LineChart,k.Table) for tables: you pass a DataFrame andchart.to(data=df2)animates the change.
Both are ordinary components. You place them with .place, enter them with k.draw, and
every part is an object.
Data comes in through Apache Arrow: polars, pandas (2.2 or later), pyarrow and duckdb objects are read without copying, and dicts of lists or lists of dicts work for small cases. kinemo does not import any of these libraries itself.
Axes#
k.Axes(x=(0, 10), y=(0, 5), *, labels=None, grid=False, width=8.0, height=4.5, tick_labels=True)
x=(min, max, step)andy=(min, max, step)give the visible ranges and the tick step (the step is optional).labels=("x", "y")names the axes;grid=Trueadds grid lines.width=/height=are the size in scene units (the frame is 16 × 9).
The visible ranges are signals. ax.zoom_to(x=(a, b), y=(c, d)) animates them, and every
curve, tick and marker on the axes follows. It is a named transition, equivalent to
ax.to(x_range=..., y_range=...).
Plotting functions#
ax.plot(fn, *, until=None, from_=None, domain=None, color=None, label=None, samples=160)
draws y = fn(x) with adaptive sampling (more points where the curve bends) and breaks at
discontinuities instead of drawing asymptotes.
Write fn with k functions (k.sin, k.exp, k.where, k.max). The same function then
works with floats (for sampling) and with signals (for point_at, slope_at and .map,
which are traced). math.sin fails with K0310 as soon as something traces the function.
until=/from_=accept signals: the curve grows as the signal moves.label=puts a label at the end of the curve.- Curves are clipped to the visible ranges. Choose
y=to cover the values that matter.
Points, tangents and slopes on a curve#
ax.plot returns a curve with reactive helpers:
| Call | Returns |
|---|---|
curve.point_at(x) |
World position of the curve at x. Use it with place(at=...). |
curve.value_at(x) |
The y value at x |
curve.slope_at(x) |
The numerical derivative at x. Read it with curve.slope_at(x)() inside a lambda. |
curve.tangent_at(x, length=2.0, **style) |
A tangent segment centered on the curve, added to the axes |
ax.point(x, y) |
World position of a data point, for markers |
All of them are reactive when x is a signal:
import kinemo as k
def f(x: float) -> float:
return 0.15 * x**3 - 0.9 * x + 1.5
@k.scene
def derivative(s: k.Scene):
ax = k.Axes(x=(-3, 3, 1), y=(-1, 5, 1), labels=("x", "y"), grid=True).place(at="center")
t = k.signal(-3.0)
curve = ax.plot(f, until=t, color=k.YELLOW, label="f")
s.play(k.draw(ax))
s.play(t.to(3), duration=2, ease=k.ease.linear)
x = k.signal(-2.0)
dot = k.Dot(r=0.1, fill=k.RED).place(at=curve.point_at(x))
curve.tangent_at(x, length=2.5, stroke=k.RED)
slope = k.Text(lambda: f"slope = {curve.slope_at(x)():.2f}", size=0.4).place(at="top", margin=0.5)
s.play(k.fade_in(dot, slope))
s.play(x.to(2), duration=3)
s.play(ax.zoom_to(x=(0, 3), y=(0, 4)), duration=1.5)
s.wait(0.5)
Objects that belong to the axes (plots, tangents, vline/hline, areas) are added to it
directly and enter with it. Markers you create yourself, like dot, enter with a verb.
Areas, lines and limits#
ax.area(curve, domain=(a, b), fill=..., fill_opacity=...)fills the region under a curve down to the x axis.between=otherfills between two curves.until=accepts a signal, so the area can grow.ax.vline(at=x)andax.hline(at=y)draw vertical and horizontal lines.at=accepts a signal, andstyle="dashed"makes them dashed.
import kinemo as k
def top(x: float) -> float:
return 3 - 0.2 * (x - 3) ** 2
def bottom(x: float) -> float:
return 0.5 + 0.1 * x
@k.scene
def between(s: k.Scene):
ax = k.Axes(x=(0, 6, 1), y=(0, 4, 1)).place(at="center")
f = ax.plot(top, color=k.BLUE)
g = ax.plot(bottom, color=k.ORANGE)
t = k.signal(0.0)
ax.area(f, between=g, domain=(1, 5), until=t, fill=k.GREEN, fill_opacity=0.3)
ax.hline(at=3, style="dashed", stroke=k.GRAY)
s.play(k.draw(ax))
s.play(t.to(5), duration=2)
s.wait(0.5)
Parametric curves, bars and scatter#
ax.parametric(fx, fy, t=(start, end))draws(fx(t), fy(t)).ax.bars(xs, heights, width=0.6)draws bars in data units (widthtoo). They follow the axes when it zooms.ax.scatter(xs, ys, radius=0.06)draws a group ofk.Dot. For thousands of points, usek.Pointsinstead (see Mass objects).
xs, heights and ys accept lists, numpy arrays and Arrow columns.
import kinemo as k
def fx(t: float) -> float:
return k.cos(t) * 1.2
def fy(t: float) -> float:
return k.sin(2 * t)
@k.scene
def shapes(s: k.Scene):
ax = k.Axes(x=(-1.5, 6, 1), y=(-1.5, 3, 1), width=10, height=5).place(at="center")
loop = ax.parametric(fx, fy, t=(0, 2 * k.pi), color=k.TEAL)
bars = ax.bars([2, 3, 4, 5], [1, 2.5, 1.5, 2], width=0.6, fill=k.PURPLE)
pts = ax.scatter([2, 3, 4, 5], [1.2, 2.7, 1.7, 2.2], radius=0.08, fill=k.YELLOW)
s.play(k.draw(ax))
s.play(k.draw(loop), k.stagger([k.grow(b, from_="bottom") for b in bars], lag=0.1))
s.play(k.fade_in(pts))
s.wait(0.5)
Number lines and polar axes#
k.NumberLine(x=(0, 10, 1), width=10.0) is an Axes with only the x axis. Position markers
with nl.point(x, 0).
k.PolarAxes(r=(0, r_max, step), radius=3.0, spokes=12) draws rings and spokes.
pa.plot(fn) draws r = fn(θ) (θ in radians, from 0 to 2π by default, theta= to change
it), and pa.point(r, θ) gives a world position.
import kinemo as k
def petals(a: float) -> float:
return abs(k.cos(3 * a))
@k.scene
def polar(s: k.Scene):
pa = k.PolarAxes(r=(0, 1, 0.25), radius=2.5, spokes=12).place(at="left", margin=2.5)
rose = pa.plot(petals, color=k.PINK)
angle = k.signal(0.0)
marker = k.Dot(r=0.12, fill=k.YELLOW).place(at=pa.point(1, angle))
nl = k.NumberLine(x=(0, 2, 0.5), width=5).place(at="right", margin=1)
pos = k.signal(0.0)
tick = k.Dot(r=0.12, fill=k.RED).place(at=nl.point(pos, 0))
s.add(pa, nl)
s.play(k.draw(rose), k.fade_in(marker, tick), duration=2)
s.play(angle.to(k.pi), pos.to(2), duration=2)
s.wait(0.5)
Data charts#
Where data comes from#
Every data-taking API (k.BarChart, k.LineChart, k.Table, ax.scatter, ax.bars,
k.interp, k.Points) accepts:
| Input | Example |
|---|---|
| polars (the reference in these docs) | pl.DataFrame(...), df["gwh"] |
| pandas 2.2 or later (with pyarrow) | pd.DataFrame(...), df["gwh"] |
| pyarrow | pa.table(...), pa.array(...) |
| duckdb | a relation |
| numpy (columns and arrays) | np.array([...]) |
| plain Python | {"country": [...], "gwh": [...]} or [{"country": "PT", "gwh": 50}, ...] |
Anything that implements the Arrow PyCapsule Interface is read without copying. An object
that does not is K1201, and the fix suggests a conversion such as pl.from_pandas(df).
k.BarChart#
k.BarChart(data, x, y, *, key=None, width=8.0, height=4.5, color=None, labels=True, grid=False, bar_ratio=0.7, label_size=0.28)
x= is the category column, y= the value column, and key= identifies each bar across
data changes (default: the category). chart.to(data=df2) animates to the new table: bars
grow and shrink, move to their new slot, enter and leave by key, and the value axis rescales.
chart.bar("IT") returns one bar and chart.keys the keys in order. color= takes one
color or a dict from key to color.
import polars as pl
import kinemo as k
Y2020 = pl.DataFrame({"country": ["PT", "ES", "FR"], "gwh": [50, 260, 540]})
Y2025 = pl.DataFrame({"country": ["ES", "PT", "FR", "IT"], "gwh": [300, 80, 600, 420]})
@k.scene
def generation(s: k.Scene):
title = k.Text("Solar generation (GWh)", size=0.5).place(at="top", margin=0.6)
chart = k.BarChart(Y2020, x="country", y="gwh", key="country", width=8, height=4).place(below=title, gap=0.6)
s.play(k.write(title), k.draw(chart))
s.play(chart.to(data=Y2025), duration=2)
s.play(k.indicate(chart.bar("IT")))
s.wait(0.5)
k.LineChart#
k.LineChart(data, x, y, *, x_range=None, y_range=None, width=8.0, height=4.5, colors=None, dots=False, legend=True)
A k.Axes with one line per y= column (a string or a list), connecting the points in x=
order. With several columns, a legend names them. chart.to(data=df2) morphs the lines point
by point. The axis ranges stay fixed after construction: pass y_range= (and
x_range=) covering every dataset you will show.
import pandas as pd
import kinemo as k
DAY = pd.DataFrame({"hour": [0, 6, 12, 18, 24], "solar": [0, 2, 6, 2, 0], "load": [1, 2, 3, 5, 2]})
CLOUDY = pd.DataFrame({"hour": [0, 6, 12, 18, 24], "solar": [0, 1, 2.5, 1, 0], "load": [1, 2, 3, 5, 2]})
@k.scene
def day(s: k.Scene):
chart = k.LineChart(DAY, x="hour", y=["solar", "load"], y_range=(0, 7), dots=True).place(at="center")
s.play(k.draw(chart), duration=2)
s.play(chart.to(data=CLOUDY), duration=2)
s.wait(0.5)
k.Table#
k.Table(data, columns=None, *, size=0.32, header_color=None, rule=True)
A table of k.Text with a highlighted header. columns= selects and orders the columns.
table.to(data=df2) updates the cells: changed texts cross-fade to the new value, new rows
fade in and removed rows fade out. table.cells[r][c] are the body cells and
table.header[c] the header texts.
import pyarrow as pa
import kinemo as k
BEFORE = pa.table({"site": ["A", "B"], "mw": [12.5, 8.0]})
AFTER = pa.table({"site": ["A", "B", "C"], "mw": [14.0, 8.0, 3.2]})
@k.scene
def sites(s: k.Scene):
table = k.Table(BEFORE, size=0.4).place(at="center")
s.play(k.fade_in(table))
s.play(table.to(data=AFTER))
s.play(table.cells[0][1].to(color=k.YELLOW))
s.wait(0.5)
Columns in functions: k.interp#
k.interp(x, xs, ys) and k.spline(x, xs, ys) interpolate a table natively. xs/ys can
be Arrow columns or numpy arrays, and the function works in ax.plot and .map alike:
import numpy as np
import polars as pl
import kinemo as k
PROFILE = pl.DataFrame({"hour": [0, 6, 12, 18, 24], "kw": [0.5, 0.8, 1.2, 2.5, 0.6]})
HOURS = PROFILE["hour"]
KW = np.asarray(PROFILE["kw"])
def load(h: float) -> float:
return k.interp(h, HOURS, KW)
@k.scene
def profile(s: k.Scene):
ax = k.Axes(x=(0, 24, 6), y=(0, 3, 1), labels=("h", "kW")).place(at="center")
hour = k.signal(0.0)
ax.plot(load, until=hour, color=k.RED)
ax.vline(at=hour, style="dashed")
s.play(k.draw(ax))
s.play(hour.to(24), duration=3, ease=k.ease.linear)
s.wait(0.5)
Common mistakes#
Diagnostic What happened Fix K0310The plotted function uses math.sin(orif,min()), andpoint_at,slope_ator.maptraced it.Use k.sin,k.where,k.min; for opaque code,k.python(fn).K0401ax.to(x=(0, 3))to change the range. In.to(),xis the position prop, which.place(...)holds; the constructor'sx=range is a different thing.ax.zoom_to(x=(0, 3))to change ranges;ax.to_place(...)to move the axes.many W1001chart.to(data=...)on aLineChartwhose new values exceed the ranges fixed at construction. The lines grow past the axes and the chart is re-centered.Pass y_range=covering all datasets.K1201The data object does not implement the Arrow PyCapsule Interface (for example, pandas older than 2.2). pl.from_pandas(df), or pass a dict of lists.K1202x=,y=orkey=names a column that does not exist. The fix lists the available columns.Use the exact column name (it is case-sensitive). K1203The value column is not numeric. Cast it before charting ( df.with_columns(pl.col("gwh").cast(pl.Float64))).K1204key=has duplicate values.Aggregate first ( df.group_by("country").sum()) or choose another key.W0901ax.scatter(or a loop ofk.Dot) with more than 1000 points.k.Points(x=..., y=...).
See also: Charts reference,
k.interp,
Diagnostics.