API Reference
Complete documentation for MafsKit. Every component includes a live interactive demo, copyable code examples, a full props table, and tips for common patterns. Built on mafs by Steven Petryk.
Installation
Install mafs from npm and import the required CSS. MafsKit works with React 18+, Next.js, Vite, Remix, and any React framework.
# Install the package npm install mafs # Or with yarn/pnpm yarn add mafs pnpm add mafs
Then import the CSS in your app's root layout or entry file:
// app/layout.tsx (Next.js App Router) import "mafs/core.css" // Or in main.tsx (Vite) import "mafs/core.css"
mafs/core.css or the coordinate system and shapes will not render correctly. This provides the base SVG styles, grid colors, and point visuals.Quick Start
The minimal code to render an interactive math visualization. Every MafsKit visualization follows this pattern: a <Mafs> wrapper, a coordinate system, and content.
import { Mafs, Coordinates, Plot, Theme } from "mafs"
import "mafs/core.css"
function MyFirstChart() {
return (
<Mafs height={300}>
<Coordinates.Cartesian />
<Plot.OfX
y={(x) => Math.sin(x)}
color={Theme.indigo}
/>
</Mafs>
)
}<Mafs>. The coordinate system is optional but recommended - without it you get a blank canvas.ViewBox & Zoom
Control what part of the coordinate plane is visible, the aspect ratio, and whether the user can zoom and pan.
// Default: auto-fit, 1:1 aspect ratio
<Mafs height={300}>
// Custom range: x from 0 to 100, y from 0 to 1
// Great for data with very different scales
<Mafs
height={300}
viewBox={{ x: [0, 100], y: [0, 1] }}
preserveAspectRatio={false}
>
// Enable zoom (scroll to zoom, drag to pan)
<Mafs zoom={{ min: 0.5, max: 4 }}>
// Remove padding around the content
<Mafs padding={0}>| Prop | Type | Default | Description |
|---|---|---|---|
height | number | 500 | Canvas height in pixels. Width is always 100% of the container. |
width | number | 'auto' | 'auto' | Canvas width. Use 'auto' to fill the container. |
viewBox | { x: [min, max], y: [min, max] } | auto | The visible coordinate range. Without this, Mafs auto-calculates based on content. |
preserveAspectRatio | boolean | true | When false, allows the x and y axes to have different scales. Set to false when your data has very different ranges (e.g., x: 0-1000, y: 0-1). |
zoom | { min: number, max: number } | false | false | Enable scroll-to-zoom and drag-to-pan. min/max control the zoom limits. |
padding | number | 0.5 | Extra space around the visible area in coordinate units. |
preserveAspectRatio={false} to stretch the axes independently.Core Concepts
Understanding the mental model behind MafsKit will help you build visualizations faster.
1. Everything is in math coordinates
When you write x={2} y={3}, that means the point at (2, 3) on the coordinate plane - not pixel position. Mafs handles the coordinate-to-pixel transformation automatically.
2. Composition over configuration
Instead of a single <Chart> component with 50 props, you compose small components: a coordinate system + plots + points + labels. Each piece is independent.
3. React state drives everything
Visualizations update when React state changes. useMovablePoint gives you reactive coordinates. useStopwatch gives you reactive time. Pass them to any component and it updates automatically.
4. SVG under the hood
Every shape renders as SVG elements. This means crisp rendering at any zoom level, accessibility via screen readers, and standard CSS styling. But it also means very complex scenes (>500 elements) can slow down - see the Performance section.
Coordinates.Cartesian
The standard x/y grid with axes, labels, and subdivision lines. This is what you'll use 90% of the time.
// Basic grid
<Coordinates.Cartesian />
// With subdivisions (4 minor lines between major lines)
<Coordinates.Cartesian subdivisions={4} />
// Custom axis labels (show pi multiples on x-axis)
<Coordinates.Cartesian
xAxis={{ lines: Math.PI, labels: labelPi }}
yAxis={{ lines: 1 }}
/>
// Custom label formatter
<Coordinates.Cartesian
xAxis={{
lines: 1,
labels: (x) => x === 0 ? "O" : `${x}m`
}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
subdivisions | number | 1 | Number of subdivision lines between major grid lines. Use 2 for halves, 4 for quarters, 10 for metric. |
xAxis | { lines?: number, labels?: (n: number) => string } | — | X axis configuration. 'lines' sets the spacing between major grid lines. 'labels' is a function that formats the axis numbers. |
yAxis | { lines?: number, labels?: (n: number) => string } | — | Y axis configuration. Same shape as xAxis. |
Coordinates.Polar
Concentric circles and radial lines for polar-coordinate visualizations. Useful for angular data, rose curves, and radar-style plots.
<Mafs height={300}>
<Coordinates.Polar
subdivisions={2} // angular subdivisions
lines={2} // radial line spacing
/>
{/* Plot a rose curve in polar coords */}
<Plot.Parametric
xy={(t) => {
const r = 2 + Math.cos(3 * t)
return [r * Math.cos(t), r * Math.sin(t)]
}}
t={[0, 2 * Math.PI]}
color={Theme.indigo}
/>
</Mafs>x = r * cos(t), y = r * sin(t) and use Plot.Parametric.Plot.OfX
Plot y as a function of x. This is the most common plot type - use it for standard functions like sin, cos, polynomials, exponentials, etc.
// Single function
<Plot.OfX y={(x) => Math.sin(x)} color={Theme.indigo} />
// Multiple functions overlaid
<Plot.OfX y={(x) => Math.sin(x)} color={Theme.indigo} />
<Plot.OfX y={(x) => x * x * 0.1 - 1} color={Theme.pink} />
<Plot.OfX y={(x) => Math.exp(-x * x)} color={Theme.green} opacity={0.6} />
// With interactive parameter
const point = useMovablePoint([1, 1])
<Plot.OfX
y={(x) => point.point[1] * Math.sin(x * point.point[0])}
color={Theme.indigo}
/>| Prop | Type | Default | Description |
|---|---|---|---|
y | (x: number) => number | — | The function to plot. Receives x, must return y. Called many times per render for adaptive sampling. |
color | string | foreground | Stroke color. Use Theme.indigo, Theme.pink, etc. or any CSS color. |
opacity | number | 1 | Line opacity (0 to 1). |
weight | number | 2 | Line thickness in pixels. |
minSamplingDepth | number | 8 | Minimum adaptive sampling recursion. Higher = more samples near curves. Increase for very curvy functions. |
maxSamplingDepth | number | 14 | Maximum adaptive sampling recursion. Lower this for performance on simple functions. |
Plot.OfY
Plot x as a function of y. The vertical equivalent of Plot.OfX - useful for inverse functions or vertical curves.
// Sideways parabola: x = y²
<Plot.OfY x={(y) => y * y} color={Theme.pink} />
// Inverse sine (vertical curve)
<Plot.OfY x={(y) => Math.asin(y)} color={Theme.indigo} />Plot.Parametric
Plot parametric curves (x(t), y(t)) over a parameter range. Use this for circles, spirals, Lissajous curves, heart shapes, and anything that can't be expressed as y=f(x).
// Lissajous figure
<Plot.Parametric
xy={(t) => [2 * Math.cos(t), Math.sin(2 * t)]}
t={[0, 2 * Math.PI]}
color={Theme.indigo}
/>
// Spiral
<Plot.Parametric
xy={(t) => [t * 0.2 * Math.cos(t * 3), t * 0.2 * Math.sin(t * 3)]}
t={[0, 10]}
color={Theme.pink}
/>
// Heart curve
<Plot.Parametric
xy={(t) => [
16 * Math.pow(Math.sin(t), 3) * 0.15,
(13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)) * 0.15
]}
t={[0, 2 * Math.PI]}
color={Theme.pink}
/>| Prop | Type | Default | Description |
|---|---|---|---|
xy | (t: number) => [number, number] | — | Function that returns [x, y] for each parameter value t. |
t | [min, max] | — | The parameter range. For closed curves, typically [0, 2*Math.PI]. |
color | string | — | Stroke color. |
opacity | number | 1 | Line opacity. |
Plotting Recipes
Common patterns and formulas you can copy-paste directly.
// ── COMMON FUNCTIONS ──
y={(x) => Math.sin(x)} // Sine wave
y={(x) => Math.cos(x)} // Cosine wave
y={(x) => Math.tan(x)} // Tangent (has discontinuities)
y={(x) => Math.exp(x)} // Exponential growth
y={(x) => Math.log(x)} // Natural log (x > 0 only)
y={(x) => 1 / x} // Hyperbola
y={(x) => Math.abs(x)} // V-shape
y={(x) => x * x} // Parabola
y={(x) => x * x * x} // Cubic
y={(x) => Math.sqrt(Math.max(0, x))} // Square root
// ── PARAMETRIC CURVES ──
// Circle of radius r
xy={(t) => [r * Math.cos(t), r * Math.sin(t)]} t={[0, 2*PI]}
// Ellipse (a, b are semi-axes)
xy={(t) => [a * Math.cos(t), b * Math.sin(t)]} t={[0, 2*PI]}
// Lissajous (a, b are frequency ratios)
xy={(t) => [A * Math.sin(a*t + d), B * Math.sin(b*t)]} t={[0, 2*PI*lcm(a,b)]}
// Archimedes spiral
xy={(t) => [k*t*Math.cos(t), k*t*Math.sin(t)]} t={[0, 6*PI]}
// Rose curve (n petals when n is odd, 2n when even)
xy={(t) => { const r = Math.cos(n*t); return [r*Math.cos(t), r*Math.sin(t)] }}Point
Render a dot at a fixed coordinate. Points are visible as small circles and are the simplest primitive.
// Static points
<Point x={2} y={3} color={Theme.indigo} />
<Point x={-1} y={0} color={Theme.pink} />
// Draggable point (see useMovablePoint section)
const p = useMovablePoint([1, 1])
{p.element} // renders the draggable handle| Prop | Type | Default | Description |
|---|---|---|---|
x | number | — | X coordinate. |
y | number | — | Y coordinate. |
color | string | foreground | Point color. |
Vector
Draw an arrow from tail to tip. Useful for showing direction, velocity, force, or any quantity with magnitude and direction.
// Basic vector from origin
<Vector tail={[0, 0]} tip={[3, 2]} color={Theme.indigo} />
// Chain of vectors (tip-to-tail addition)
<Vector tail={[0, 0]} tip={a} color={Theme.blue} />
<Vector tail={a} tip={vec.add(a, b)} color={Theme.pink} />
// Perpendicular vector: rotate 90°
<Vector tail={[0, 0]} tip={[-y, x]} color={Theme.green} />| Prop | Type | Default | Description |
|---|---|---|---|
tail | [number, number] | — | Start point of the arrow. |
tip | [number, number] | — | End point (where the arrowhead is). |
color | string | — | Vector color. |
opacity | number | 1 | Opacity. |
Line.Segment / Line.ThroughPoints / Line.PointSlope
Three ways to draw lines: between two points (Segment), through two points extending to viewport edges (ThroughPoints), or from a point with a given slope (PointSlope).
// Segment: finite line between two points
<Line.Segment point1={[-2, -1]} point2={[2, 1]} color={Theme.indigo} />
// ThroughPoints: infinite line extending to viewport edges
<Line.ThroughPoints point1={[0, 0]} point2={[1, 2]} color={Theme.pink} />
// PointSlope: line through a point with given slope
<Line.PointSlope point={[0, 1]} slope={0.5} color={Theme.green} />
// Dashed line style
<Line.ThroughPoints point1={[0, 0]} point2={[1, 1]} style="dashed" />| Prop | Type | Default | Description |
|---|---|---|---|
point1 | [number, number] | — | First point. |
point2 | [number, number] | — | Second point. |
point | [number, number] | — | (PointSlope only) The point the line passes through. |
slope | number | — | (PointSlope only) The slope of the line. |
color | string | — | Line color. |
style | 'solid' | 'dashed' | 'solid' | Line style. |
weight | number | 2 | Line thickness in pixels. |
opacity | number | 1 | Line opacity. |
Circle
Draw a circle defined by center and radius. Drag both the center and edge point in the demo below.
<Circle
center={[0, 0]}
radius={2}
color={Theme.indigo}
fillOpacity={0.08} // light fill
strokeOpacity={0.8} // visible outline
/>| Prop | Type | Default | Description |
|---|---|---|---|
center | [number, number] | — | Center of the circle. |
radius | number | — | Radius in coordinate units. |
color | string | — | Stroke and fill color. |
fillOpacity | number | 0.15 | Fill opacity. Set to 0 for outline only. |
strokeOpacity | number | 1 | Stroke opacity. Set to 0 for fill only. |
Ellipse
Draw an ellipse with center and separate x/y radii.
<Ellipse center={[0, 0]} radius={[3, 1.5]} color={Theme.indigo} fillOpacity={0.08} />Polygon
Draw a closed polygon from an array of vertices. The demo below has 4 draggable vertices.
<Polygon
points={[[-2, -1], [2, -1], [1, 2], [-1, 2]]}
color={Theme.indigo}
fillOpacity={0.1}
/>Text
Render text at a coordinate position. Supports color, sizing, and anchor direction.
<Text x={0} y={1} size={20}>Hello World</Text>
<Text x={0} y={0} size={14} color={Theme.indigo}>Colored text</Text>
// Attach controls anchor direction:
<Text x={2} y={1} attach="w"> {/* anchored to the west (left) side */}
<Text x={2} y={1} attach="ne"> {/* anchored to the northeast */}| Prop | Type | Default | Description |
|---|---|---|---|
x | number | — | X position. |
y | number | — | Y position. |
size | number | 30 | Font size in pixels. |
color | string | — | Text color. |
attach | 'n'|'s'|'e'|'w'|'ne'|'nw'|'se'|'sw' | — | Anchor direction. 'n' puts the text above the point, 'e' to the right, etc. |
attachDistance | number | 0 | Extra pixel distance from the anchor point. |
useMovablePoint
The primary way to add interactivity. Creates a draggable point that returns reactive coordinates. Drag the point in the demo - it's constrained to the sine curve.
import { useMovablePoint } from "mafs"
function Interactive() {
const point = useMovablePoint([1, 1])
// point.point → [number, number] — current position
// point.element → JSX.Element — the draggable handle (render this!)
// point.setPoint → (p) => void — imperatively move the point
return (
<Mafs>
<Coordinates.Cartesian />
<Circle center={[0, 0]} radius={vec.mag(point.point)} />
{point.element} {/* IMPORTANT: always render this */}
</Mafs>
)
}{point.element} inside your <Mafs>. If you forget, the point exists in memory but has no visual handle to drag.Constraints
Lock a movable point to a specific path - an axis, a curve, a circle, or any arbitrary constraint function.
// Constrain to x-axis (horizontal only)
useMovablePoint([1, 0], {
constrain: ([x, y]) => [x, 0]
})
// Constrain to y-axis (vertical only)
useMovablePoint([0, 1], {
constrain: ([x, y]) => [0, y]
})
// Constrain to a circle of radius 2
useMovablePoint([2, 0], {
constrain: ([x, y]) => {
const angle = Math.atan2(y, x)
return [2 * Math.cos(angle), 2 * Math.sin(angle)]
}
})
// Constrain to a curve: point rides f(x)
useMovablePoint([1, Math.sin(1)], {
constrain: ([x]) => [x, Math.sin(x)]
})
// Constrain to integers (snap to grid)
useMovablePoint([0, 0], {
constrain: ([x, y]) => [Math.round(x), Math.round(y)]
})
// Constrain to a range
useMovablePoint([0, 0], {
constrain: ([x, y]) => [
Math.max(-3, Math.min(3, x)),
Math.max(-2, Math.min(2, y))
]
})Transform
Apply translate, rotate, or scale transformations to all children. Children render in the transformed coordinate space.
// Translate: move children by [dx, dy]
<Transform translate={[2, 1]}>
<Circle center={[0, 0]} radius={1} /> {/* appears at (2, 1) */}
</Transform>
// Rotate: rotate children by angle (radians)
<Transform rotate={Math.PI / 4}> {/* 45 degrees */}
<Polygon points={[[-1,-1],[1,-1],[1,1],[-1,1]]} />
</Transform>
// Scale: scale children uniformly or per-axis
<Transform scale={2}>
<Point x={1} y={1} /> {/* appears at (2, 2) */}
</Transform>
// Combine: transformations compose
<Transform translate={[2, 0]}>
<Transform rotate={Math.PI / 6}>
<Circle center={[0, 0]} radius={0.5} />
</Transform>
</Transform>| Prop | Type | Default | Description |
|---|---|---|---|
translate | [number, number] | — | Translation offset [dx, dy]. |
rotate | number | — | Rotation angle in radians. |
scale | number | [number, number] | — | Uniform scale or [sx, sy] per-axis scale. |
useStopwatch
Returns a time value that auto-increments every animation frame (~60fps). The building block for all animations.
import { useStopwatch } from "mafs"
function AnimatedWave() {
const { time, start, stop } = useStopwatch()
// time: seconds since start() was called
// start(): begin incrementing time
// stop(): pause time at current value
return (
<Mafs>
<Coordinates.Cartesian />
<Plot.OfX y={(x) => Math.sin(x + time)} color={Theme.indigo} />
<Point x={0} y={Math.sin(time)} color={Theme.pink} />
</Mafs>
)
}useStopwatch starts paused by default. You must call start() to begin the animation. MafsKit's PlayableDemo wrapper handles this automatically.Animation Recipes
Common animation patterns you can copy-paste.
// ── TRAVELING WAVE ──
y={(x) => Math.sin(x - time * 2)} // moves right
y={(x) => Math.sin(x + time * 2)} // moves left
// ── ORBITING POINT ──
const x = radius * Math.cos(time * speed)
const y = radius * Math.sin(time * speed)
<Point x={x} y={y} />
// ── DAMPED OSCILLATION ──
const y = amplitude * Math.exp(-decay * time) * Math.cos(omega * time)
// ── GROWING CIRCLE ──
<Circle center={[0,0]} radius={time * 0.5} />
// ── PULSING OPACITY ──
<Circle center={[0,0]} radius={1} fillOpacity={(Math.sin(time * 3) + 1) / 2 * 0.3} />
// ── PARAMETRIC TRAIL (draw curve up to current time) ──
<Plot.Parametric
xy={(t) => [Math.cos(t), Math.sin(t)]}
t={[0, time]} // trail grows over time
/>vec
A collection of 2D vector math utilities. All functions work with [number, number] tuples. Essential for computing distances, angles, and physics.
import { vec } from "mafs"
// ── BASIC OPERATIONS ──
vec.add([1, 2], [3, 4]) // → [4, 6]
vec.sub([3, 4], [1, 2]) // → [2, 2]
vec.scale([1, 2], 3) // → [3, 6]
vec.neg([1, -2]) // → [-1, 2]
// ── MAGNITUDE & DISTANCE ──
vec.mag([3, 4]) // → 5
vec.dist([0, 0], [3, 4]) // → 5
vec.squareDist([0, 0], [3, 4]) // → 25 (avoids sqrt)
// ── NORMALIZATION ──
vec.normalize([3, 4]) // → [0.6, 0.8]
vec.withMag([3, 4], 10) // → [6, 8] (same direction, length 10)
// ── ROTATION ──
vec.rotate([1, 0], Math.PI / 2) // → [0, 1]
vec.rotateAbout([1, 0], Math.PI, [0.5, 0]) // rotate around a center
// ── INTERPOLATION ──
vec.lerp([0, 0], [10, 10], 0.5) // → [5, 5]
vec.midpoint([0, 0], [4, 4]) // → [2, 2]
// ── DOT PRODUCT ──
vec.dot([1, 0], [0, 1]) // → 0 (perpendicular)
vec.dot([1, 0], [1, 0]) // → 1 (parallel)labelPi
A label formatter that displays axis values as multiples of pi. Pass it to axis label props for trigonometry.
import { labelPi } from "mafs"
<Coordinates.Cartesian
xAxis={{
lines: Math.PI, // major lines at every π
labels: labelPi, // renders: -2π, -π, 0, π, 2π
}}
/>
// Combine with subdivisions for finer marks
<Coordinates.Cartesian
subdivisions={2} // marks at π/2 intervals
xAxis={{ lines: Math.PI, labels: labelPi }}
/>Theme Colors
Predefined colors that work well on both light and dark backgrounds. Always prefer these over raw hex values for consistency.
import { Theme } from "mafs"
Theme.indigo // primary blue-purple (best for main content)
Theme.pink // secondary accent
Theme.green // positive / success
Theme.blue // cool accent
Theme.orange // warm accent / warnings
Theme.red // negative / danger
Theme.yellow // highlight
// Usage on any component:
<Plot.OfX y={Math.sin} color={Theme.indigo} />
<Circle center={[0,0]} radius={1} color={Theme.pink} />
<Vector tail={[0,0]} tip={[2,1]} color={Theme.green} />
<Text x={0} y={0} color={Theme.orange}>Label</Text>Performance Guide
How to keep your visualizations running at 60fps, even with complex content.
Rule 1: Minimize SVG element count
Each Point, Line.Segment, Circle, etc. creates an SVG element that React must diff every render. Keep it under ~200 elements per Mafs instance. Use Plot.Parametric (1 SVG path) instead of many Line.Segment elements for trails.
Rule 2: Memoize expensive computations
Wrap pre-computed data in useMemo. But don't put time in the dependency array - that defeats the purpose since it changes every frame.
Rule 3: Use LazyDemo for multiple instances
If you have many Mafs instances on one page, wrap each in an IntersectionObserver and only mount when visible. This is critical - 40 Mafs instances simultaneously will destroy FPS.
Rule 4: Cap animation frame work
For live simulations (physics, particles), use useRef to persist state and compute a fixed number of substeps per frame. Never scale computation with elapsed time.
Rule 5: Reduce Coordinates complexity
Coordinates.Polar with subdivisions={4} creates many SVG elements. Keep subdivisions low (1-2) for animated scenes.
// ❌ BAD: 500 Line.Segments for a trail
{trail.map((pt, i) =>
i > 0 ? <Line.Segment point1={trail[i-1]} point2={pt} /> : null
)}
// ✅ GOOD: 1 Plot.Parametric for the same trail
<Plot.Parametric
xy={(t) => {
const i = Math.floor(t * (trail.length - 1))
return trail[Math.min(i, trail.length - 1)]
}}
t={[0, 1]}
/>
// ❌ BAD: Pre-computing 10000 steps in useMemo on mount
const sim = useMemo(() => {
for (let i = 0; i < 10000; i++) { ... } // blocks render
}, [])
// ✅ GOOD: Incremental simulation with useRef
const state = useRef(initialState)
// Compute 2-4 substeps per frame, max
const substeps = Math.min(4, Math.ceil(elapsed / dt))SSR / Next.js
MafsKit works with server-side rendering, but the interactive components need 'use client'.
// In Next.js App Router, mark your component as client-side:
"use client"
import { Mafs, Coordinates, Plot, Theme } from "mafs"
export function MyChart() {
return (
<Mafs height={300}>
<Coordinates.Cartesian />
<Plot.OfX y={Math.sin} color={Theme.indigo} />
</Mafs>
)
}
// In your server component / page:
import { MyChart } from "./MyChart"
export default function Page() {
return <MyChart /> // renders on the client
}transpilePackages, add mafs to the list in your next.config.mjs.Custom Components
Build your own components that integrate with the Mafs coordinate system using the context hooks.
import { usePaneContext, useTransformContext } from "mafs"
function CustomOverlay() {
// Access the current viewport
const pane = usePaneContext()
// pane.xMin, pane.xMax, pane.yMin, pane.yMax
// Access the current transformation matrix
const transform = useTransformContext()
// transform.userTransform — the matrix applied by <Transform>
// transform.viewTransform — the coordinate-to-pixel matrix
return (
<g>
{/* Your custom SVG here */}
{/* Coordinates are in math-space, not pixels */}
</g>
)
}<g>, <circle>, <path>, etc. The coordinate-to-pixel transform is handled by the parent <Mafs> SVG viewBox.