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:

layout.tsx
// app/layout.tsx (Next.js App Router)
import "mafs/core.css"

// Or in main.tsx (Vite)
import "mafs/core.css"
Warning: You must import 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.

MyFirstChart.tsx
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>
  )
}
Scroll to load
Tip: Every component must be a child of <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}>
Scroll to load
PropTypeDefaultDescription
heightnumber500Canvas height in pixels. Width is always 100% of the container.
widthnumber | 'auto''auto'Canvas width. Use 'auto' to fill the container.
viewBox{ x: [min, max], y: [min, max] }autoThe visible coordinate range. Without this, Mafs auto-calculates based on content.
preserveAspectRatiobooleantrueWhen 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 } | falsefalseEnable scroll-to-zoom and drag-to-pan. min/max control the zoom limits.
paddingnumber0.5Extra space around the visible area in coordinate units.
Tip: If your function has a very different scale on x vs y (like plotting stock prices over time), set 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`
  }}
/>
Scroll to load
PropTypeDefaultDescription
subdivisionsnumber1Number 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>
Scroll to load
Tip: Polar coordinates don't have a dedicated "polar plot" component. Instead, convert polar (r, theta) to cartesian (x, y) using 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}
/>
Scroll to load
PropTypeDefaultDescription
y(x: number) => numberThe function to plot. Receives x, must return y. Called many times per render for adaptive sampling.
colorstringforegroundStroke color. Use Theme.indigo, Theme.pink, etc. or any CSS color.
opacitynumber1Line opacity (0 to 1).
weightnumber2Line thickness in pixels.
minSamplingDepthnumber8Minimum adaptive sampling recursion. Higher = more samples near curves. Increase for very curvy functions.
maxSamplingDepthnumber14Maximum 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}
/>
Scroll to load
PropTypeDefaultDescription
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].
colorstringStroke color.
opacitynumber1Line 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
Scroll to load
PropTypeDefaultDescription
xnumberX coordinate.
ynumberY coordinate.
colorstringforegroundPoint 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} />
Scroll to load
PropTypeDefaultDescription
tail[number, number]Start point of the arrow.
tip[number, number]End point (where the arrowhead is).
colorstringVector color.
opacitynumber1Opacity.

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" />
Scroll to load
PropTypeDefaultDescription
point1[number, number]First point.
point2[number, number]Second point.
point[number, number](PointSlope only) The point the line passes through.
slopenumber(PointSlope only) The slope of the line.
colorstringLine color.
style'solid' | 'dashed''solid'Line style.
weightnumber2Line thickness in pixels.
opacitynumber1Line 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
/>
Scroll to load
PropTypeDefaultDescription
center[number, number]Center of the circle.
radiusnumberRadius in coordinate units.
colorstringStroke and fill color.
fillOpacitynumber0.15Fill opacity. Set to 0 for outline only.
strokeOpacitynumber1Stroke 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}
/>
Scroll to load

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 */}
PropTypeDefaultDescription
xnumberX position.
ynumberY position.
sizenumber30Font size in pixels.
colorstringText color.
attach'n'|'s'|'e'|'w'|'ne'|'nw'|'se'|'sw'Anchor direction. 'n' puts the text above the point, 'e' to the right, etc.
attachDistancenumber0Extra 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>
  )
}
Scroll to load
Warning: Always render {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))
  ]
})
Tip: The constrain function receives the raw (unconstrained) position and must return the constrained position. It runs on every mouse move, so keep it fast - avoid heavy computations.

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>
Scroll to load
PropTypeDefaultDescription
translate[number, number]Translation offset [dx, dy].
rotatenumberRotation angle in radians.
scalenumber | [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>
  )
}
Scroll to load
Warning: 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)
Scroll to load

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

MyChart.tsx
// 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
}
Tip: If using Next.js with 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>
  )
}
Tip: Custom components must return SVG elements (not HTML). Use <g>, <circle>, <path>, etc. The coordinate-to-pixel transform is handled by the parent <Mafs> SVG viewBox.
0 FPS