Functional Weave
Code in TypeScript

charts.shape

SVG path strings for lines, areas, steps, monotone curves, bars, arcs and pie slices, identical in every language.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 70 tests, run in TypeScript, Python and Rust.linePath 9 · areaPath 8 · stepPath 9 · monotoneCurve 10 · barRects 10 · arcPath 13 · pieAngles 11

What it does

SVG path data for the marks of a chart: lines, filled areas, step lines, smooth monotone curves, bar rectangles, arcs and pie angles. Inputs are already in pixels (put the data through `charts.scale` first); the outputs are strings for `<path d="...">` and numbers for `<rect>`, the same in TypeScript, Python and Rust, so a server-rendered chart and a browser-rendered one match to the character.

This is a group: install only what you draw, e.g. `require charts.shape ^1.0.0 only=linePath`. Every path function uses `linePath`'s helpers, so `linePath` always comes along.

The functions

A group: 7 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.

  1. linePath (points: Point[]) -> string
  2. areaPath (points: AreaPoint[]) -> string
  3. stepPath (points: Point[], position: StepPosition) -> string
  4. monotoneCurve (points: Point[]) -> string
  5. barRects (bars: BarSpec[], orientation: Orientation) -> Rect[]
  6. arcPath (arc: Arc) -> string
  7. pieAngles (values: float[], startAngle: float, endAngle: float, padAngle: float) -> PieSlice[]

The types it declares, generated into your project

/** A point of a line in pixels; a null y is a gap. */
export interface Point {
  readonly x: number;
  readonly y: number | null;
}

/** A point of an area in pixels: y1 is the top edge, y0 the baseline; a null y1 is a gap. */
export interface AreaPoint {
  readonly x: number;
  readonly y0: number;
  readonly y1: number | null;
}

export type StepPosition = "before" | "middle" | "after";

export type Orientation = "vertical" | "horizontal";

/** One bar in pixels, before rounding. */
export interface BarSpec {
  /** where the bar's band starts across the axis (x for vertical bars) */
  readonly band: number;
  /** the band's width, at least 0 */
  readonly thickness: number;
  /** the baseline pixel, usually where the scale puts zero */
  readonly base: number;
  /** the value's pixel */
  readonly value: number;
}

/** A rectangle ready for <rect>. */
export interface Rect {
  readonly x: number;
  readonly y: number;
  readonly width: number;
  readonly height: number;
}

/** An annular sector: angles in radians, 0 at 12 o'clock, increasing clockwise. */
export interface Arc {
  readonly cx: number;
  readonly cy: number;
  readonly innerRadius: number;
  readonly outerRadius: number;
  readonly startAngle: number;
  readonly endAngle: number;
}

/** The drawn angles of one slice, with half the pad trimmed from each side. */
export interface PieSlice {
  readonly value: number;
  readonly startAngle: number;
  readonly endAngle: number;
}

Once installed, your code imports each one from the group's module.

linePath 9 tests

export function linePath(points: readonly Point[]): string
pointsPoint[]in pixels, in drawing order; a null y breaks the line
returnsstring"M10,20L30,40", numbers rounded to 2 places; "" when no point has a y

For example

  • linePath(points ×3) → M0,0L10,20L30,15 three points
  • linePath(points ×2) → M1,2.67L3.14,0 coordinates round to 2 places on the stored value, and -0.004 prints as 0, not -0
  • linePath(points ×2) → M-1.5,2.25L0.13,100 negative and tie-breaking coordinates
import { linePath } from "#fune/charts.shape@^1";
impl/typescript/line_path.ts · 41 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { formatDecimal } from "./text_format_decimal.ts";  ← from text.format-decimal ^1.0.0 · built alongside by fune
import type { Point } from "./charts_shape_types.ts";

/**
 * A polyline as SVG path data, in d3.line's format ("M10,20L30,40").
 * A null y ends the current subpath; a run of one point is "Mx,yZ", as d3
 * closes it so a round line cap still shows a dot.
 */
export function linePath(points: readonly Point[]): string {
  let out = "";
  for (const run of pointRuns(points)) {
    run.forEach(([x, y], i) => {
      out += (i === 0 ? "M" : "L") + svgPair(x, y);
    });
    if (run.length === 1) out += "Z";
  }
  return out;
}

// Shared with the other path functions of this group.

/** "x,y" with each number rounded to 2 places, trailing zeros dropped. */
export function svgPair(x: number, y: number): string {
  return formatDecimal(x, 2, true, "") + "," + formatDecimal(y, 2, true, "");
}

/** The unbroken runs of a series: a point with a null y separates them. */
export function pointRuns(points: readonly Point[]): [number, number][][] {
  const runs: [number, number][][] = [];
  let current: [number, number][] = [];
  for (const p of points) {
    if (p.y === null || p.y === undefined) {
      if (current.length > 0) runs.push(current);
      current = [];
    } else {
      current.push([p.x, p.y]);
    }
  }
  if (current.length > 0) runs.push(current);
  return runs;
}

areaPath 8 tests

export function areaPath(points: readonly AreaPoint[]): string
pointsAreaPoint[]top edge y1 and baseline y0 per x; a null y1 breaks the area
returnsstring

For example

  • areaPath(points ×3) → M0,50L10,20L20,40L20,100L10,100L0,100Z an area down to a flat baseline
  • areaPath(points ×2) → M0,60L5,50L5,70L0,80Z a stacked band with its own baseline per point
  • areaPath(points ×4) → M0,5L0,10ZM2,6L3,7L3,10L2,10Z a null y1 splits the area; a lone point becomes a vertical sliver
import { areaPath } from "#fune/charts.shape@^1";
impl/typescript/area_path.ts · 27 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { svgPair } from "./charts_shape_line_path.ts";  ← linePath, another function of this group · built into the same file, even by a slim install
import type { AreaPoint } from "./charts_shape_types.ts";

/**
 * A filled area as SVG path data, in d3.area's format: along the top edge
 * (y1) left to right, back along the baseline (y0) right to left, closed.
 * A null y1 ends one area and starts the next.
 */
export function areaPath(points: readonly AreaPoint[]): string {
  let out = "";
  let run: AreaPoint[] = [];
  const flush = () => {
    if (run.length === 0) return;
    run.forEach((p, i) => {
      out += (i === 0 ? "M" : "L") + svgPair(p.x, p.y1 as number);
    });
    for (let i = run.length - 1; i >= 0; i--) out += "L" + svgPair(run[i].x, run[i].y0);
    out += "Z";
    run = [];
  };
  for (const p of points) {
    if (p.y1 === null || p.y1 === undefined) flush();
    else run.push(p);
  }
  flush();
  return out;
}

stepPath 9 tests

export function stepPath(points: readonly Point[], position: StepPosition): string
pointsPoint[]
positionStepPositionwhere the vertical step sits: before a point, midway, or after it
returnsstring

For example

  • stepPath(points ×3, before) → M0,0L0,10L10,10L10,5L20,5 step before: vertical first, at the previous x
  • stepPath(points ×3, middle) → M0,0L5,0L5,10L15,10L15,5L20,5 step middle: the jump halfway between points
  • stepPath(points ×3, after) → M0,0L10,0L10,10L20,10L20,5 step after: horizontal first, jump at the next x
import { stepPath } from "#fune/charts.shape@^1";
impl/typescript/step_path.ts · 35 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { pointRuns, svgPair } from "./charts_shape_line_path.ts";  ← linePath, another function of this group · built into the same file, even by a slim install
import type { Point, StepPosition } from "./charts_shape_types.ts";

/**
 * A step line as SVG path data, as d3.curveStepBefore, d3.curveStep and
 * d3.curveStepAfter draw it: horizontal then vertical moves only.
 */
export function stepPath(points: readonly Point[], position: StepPosition): string {
  let t: number;
  if (position === "before") t = 0;
  else if (position === "middle") t = 0.5;
  else if (position === "after") t = 1;
  else throw new RangeError(`step position must be before, middle or after, received ${position}`);
  let out = "";
  for (const run of pointRuns(points)) {
    out += "M" + svgPair(run[0][0], run[0][1]);
    for (let i = 1; i < run.length; i++) {
      const [px, py] = run[i - 1];
      const [x, y] = run[i];
      if (t <= 0) {
        out += "L" + svgPair(px, y) + "L" + svgPair(x, y);
      } else {
        // d3's own expression, so the midpoint rounds as d3's does.
        const x1 = px * (1 - t) + x * t;
        out += "L" + svgPair(x1, py) + "L" + svgPair(x1, y);
      }
    }
    if (t > 0 && t < 1 && run.length >= 2) {
      const [lx, ly] = run[run.length - 1];
      out += "L" + svgPair(lx, ly);
    }
    if (run.length === 1) out += "Z";
  }
  return out;
}

monotoneCurve throws on bad input 10 tests

export function monotoneCurve(points: readonly Point[]): string
pointsPoint[]x must strictly increase within each unbroken run
returnsstringcubic Bézier segments that never overshoot the data (Fritsch–Carlson, d3.curveMonotoneX)

For example

  • monotoneCurve(points ×3) → M0,0C0.33,0.5,0.67,1,1,1C1.33,1,1.67,0.5,2,0 a peak: the tangent at the top is flat, so the curve does not rise above 1
  • monotoneCurve(points ×3) → M0,0C0.33,0,0.67,0,1,0C1.33,0,1.67,5,2,10 flat then rising: a Catmull-Rom curve would dip below 0 here; this one stays flat
  • monotoneCurve(points ×4) → M0,0C0.33,0.75,0.67,1.5,1,2C1.33,2.5,1.67,2.33,2,3C2.33,3.67,2.67,4.83,3,6 four rising points, tangents limited by Fritsch-Carlson
import { monotoneCurve } from "#fune/charts.shape@^1";
impl/typescript/monotone_curve.ts · 68 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { pointRuns, svgPair } from "./charts_shape_line_path.ts";  ← linePath, another function of this group · built into the same file, even by a slim install
import type { Point } from "./charts_shape_types.ts";

function sign(x: number): number {
  return x < 0 ? -1 : 1;
}

// Fritsch–Carlson: the tangent at the middle of three points, limited so the
// curve cannot overshoot either neighbouring value. Written as d3's slope3.
function slope3(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number): number {
  const h0 = x1 - x0;
  const h1 = x2 - x1;
  const s0 = (y1 - y0) / h0;
  const s1 = (y2 - y1) / h1;
  const p = (s0 * h1 + s1 * h0) / (h0 + h1);
  return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;
}

// The tangent at an end, from the one-sided slope and the known tangent (d3's slope2).
function slope2(x0: number, y0: number, x1: number, y1: number, t: number): number {
  return (3 * (y1 - y0) / (x1 - x0) - t) / 2;
}

function bezier(x0: number, y0: number, x1: number, y1: number, t0: number, t1: number): string {
  const dx = (x1 - x0) / 3;
  return "C" + svgPair(x0 + dx, y0 + dx * t0) + "," + svgPair(x1 - dx, y1 - dx * t1) + "," + svgPair(x1, y1);
}

/**
 * A smooth line through the points that stays monotone wherever the data is,
 * so it never invents a peak or dip between samples: d3.curveMonotoneX,
 * Hermite segments with Fritsch–Carlson tangents, as cubic Béziers.
 */
export function monotoneCurve(points: readonly Point[]): string {
  let out = "";
  for (const run of pointRuns(points)) {
    for (let i = 1; i < run.length; i++) {
      if (!(run[i][0] > run[i - 1][0])) {
        throw new RangeError(`monotone curve needs x strictly increasing, but ${run[i][0]} follows ${run[i - 1][0]}`);
      }
    }
    out += "M" + svgPair(run[0][0], run[0][1]);
    const n = run.length;
    if (n === 1) {
      out += "Z";
      continue;
    }
    if (n === 2) {
      out += "L" + svgPair(run[1][0], run[1][1]);
      continue;
    }
    // Tangent at each interior point, then the ends from their neighbours.
    let t0 = 0;
    for (let i = 2; i < n; i++) {
      const [ax, ay] = run[i - 2];
      const [bx, by] = run[i - 1];
      const [cx, cy] = run[i];
      const t1 = slope3(ax, ay, bx, by, cx, cy);
      const start = i === 2 ? slope2(ax, ay, bx, by, t1) : t0;
      out += bezier(ax, ay, bx, by, start, t1);
      t0 = t1;
    }
    const [ax, ay] = run[n - 2];
    const [bx, by] = run[n - 1];
    out += bezier(ax, ay, bx, by, t0, slope2(ax, ay, bx, by, t0));
  }
  return out;
}

barRects throws on bad input 10 tests

export function barRects(bars: readonly BarSpec[], orientation: Orientation): readonly Rect[]
barsBarSpec[]
orientationOrientation
returnsRect[]edges rounded to 2 places; widths and heights are differences of rounded edges, never negative

For example

  • barRects(bars ×1, vertical) → ×1 a vertical bar up from the baseline
  • barRects(bars ×1, vertical) → ×1 a negative vertical bar hangs below the baseline with a positive height
  • barRects(bars ×1, horizontal) → ×1 a horizontal bar
import { barRects } from "#fune/charts.shape@^1";
impl/typescript/bar_rects.ts · 25 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import type { BarSpec, Orientation, Rect } from "./charts_shape_types.ts";

/**
 * Rectangles for bars. The edges are rounded to 2 places and the sizes are the
 * differences of the rounded edges, so bars that share an edge share it
 * exactly, and the height of a negative bar is still positive.
 */
export function barRects(bars: readonly BarSpec[], orientation: Orientation): readonly Rect[] {
  if (orientation !== "vertical" && orientation !== "horizontal") {
    throw new RangeError(`orientation must be vertical or horizontal, received ${orientation}`);
  }
  return bars.map((bar) => {
    if (!(bar.thickness >= 0)) throw new RangeError(`bar thickness must not be negative, received ${bar.thickness}`);
    const a0 = roundFloat(bar.band, 2);
    const a1 = roundFloat(bar.band + bar.thickness, 2);
    const v0 = roundFloat(Math.min(bar.base, bar.value), 2);
    const v1 = roundFloat(Math.max(bar.base, bar.value), 2);
    const across = roundFloat(a1 - a0, 2);
    const along = roundFloat(v1 - v0, 2);
    return orientation === "vertical"
      ? { x: a0, y: v0, width: across, height: along }
      : { x: v0, y: a0, width: along, height: across };
  });
}

arcPath throws on bad input 13 tests

export function arcPath(arc: Arc): string
arcArc
returnsstringa ring segment, a wedge to the centre when innerRadius is 0, or "" for an empty arc

For example

  • arcPath(cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 1.571) → M50,0A50,50,0,0,1,100,50L50,50Z a quarter wedge from 12 o'clock to 3 o'clock
  • arcPath(cx 50, cy 50, inner radius 25, outer radius 50, start angle 0, end angle 1.571) → M50,0A50,50,0,0,1,100,50L75,50A25,25,0,0,0,50,25Z a quarter of a donut
  • arcPath(cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 4.712) → M50,0A50,50,0,1,1,0,50L50,50Z three quarters sets the large-arc flag
import { arcPath } from "#fune/charts.shape@^1";
impl/typescript/arc_path.ts · 48 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { sinCos } from "./math_sin_cos.ts";  ← from math.sin-cos ^1.0.0 · built alongside by fune
import { svgPair } from "./charts_shape_line_path.ts";  ← linePath, another function of this group · built into the same file, even by a slim install
import type { Arc } from "./charts_shape_types.ts";

const PI = 3.141592653589793;
const TAU = 6.283185307179586;

function arcPoint(arc: Arc, r: number, angle: number): string {
  const sc = sinCos(angle);
  return svgPair(arc.cx + r * sc.sin, arc.cy - r * sc.cos);
}

function arcTo(arc: Arc, r: number, large: number, sweep: number, angle: number): string {
  const radius = svgPair(r, r);
  return "A" + radius + ",0," + large + "," + sweep + "," + arcPoint(arc, r, angle);
}

/**
 * An annular sector as SVG path data. Angles are radians from 12 o'clock,
 * clockwise, as in d3.arc. See the README for the exact path format.
 */
export function arcPath(arc: Arc): string {
  const { innerRadius: ri, outerRadius: ro, startAngle: a0, endAngle: a1 } = arc;
  if (!(ri >= 0) || !(ro >= ri)) {
    throw new RangeError(`arc radii must satisfy 0 <= innerRadius <= outerRadius, received ${ri} and ${ro}`);
  }
  const span = a1 - a0;
  if (span === 0 || ro === 0) return "";
  const sweep = span > 0 ? 1 : 0;
  const back = 1 - sweep;
  const dir = span > 0 ? 1 : -1;
  if (Math.abs(span) >= TAU) {
    // One SVG arc cannot start and end at the same point, so a full turn is
    // two half turns; the hole is drawn the other way round so it stays empty.
    const half = a0 + dir * PI;
    let out = "M" + arcPoint(arc, ro, a0) + arcTo(arc, ro, 1, sweep, half) + arcTo(arc, ro, 1, sweep, a0) + "Z";
    if (ri > 0) {
      const back = a0 - dir * PI;
      out += "M" + arcPoint(arc, ri, a0) + arcTo(arc, ri, 1, 1 - sweep, back) + arcTo(arc, ri, 1, 1 - sweep, a0) + "Z";
    }
    return out;
  }
  const large = Math.abs(span) > PI ? 1 : 0;
  let out = "M" + arcPoint(arc, ro, a0) + arcTo(arc, ro, large, sweep, a1);
  if (ri > 0) out += "L" + arcPoint(arc, ri, a1) + arcTo(arc, ri, large, back, a0);
  else out += "L" + svgPair(arc.cx, arc.cy);
  return out + "Z";
}

pieAngles throws on bad input 11 tests

export function pieAngles(values: readonly number[], startAngle: number, endAngle: number, padAngle: number): readonly PieSlice[]
valuesfloat[]zero or more each, in the order the slices are drawn
startAnglefloatradians, 0 at 12 o'clock, clockwise
endAnglefloatusually startAngle + 2 pi; the span is capped at one full turn
padAnglefloatradians of gap between neighbouring slices
returnsPieSlice[]

For example

  • pieAngles(1, 1, 2, 0, 6.283, 0) → ×3 a full pie of 1, 1 and 2 in input order
  • pieAngles(0, 3, 0, 1, 0) → ×2 a zero value has no width
  • pieAngles(0, 0, 0, 6.283, 0) → ×2 all zeros: every slice is empty
import { pieAngles } from "#fune/charts.shape@^1";
impl/typescript/pie_angles.ts · 31 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import type { PieSlice } from "./charts_shape_types.ts";

const TAU = 6.283185307179586;

/**
 * Start and end angles of pie slices in input order: d3.pie with sort(null),
 * then half the pad trimmed from each side, so the angles can go straight
 * into arcPath and neighbouring slices show a gap of padAngle.
 */
export function pieAngles(values: readonly number[], startAngle: number, endAngle: number, padAngle: number): readonly PieSlice[] {
  if (!(padAngle >= 0)) throw new RangeError(`padAngle must not be negative, received ${padAngle}`);
  const n = values.length;
  let sum = 0;
  for (const v of values) {
    if (!(v >= 0)) throw new RangeError(`pie values must be zero or more, received ${v}`);
    sum += v;
  }
  let a0 = startAngle;
  const da = Math.min(TAU, Math.max(-TAU, endAngle - a0));
  const p = n > 0 ? Math.min(Math.abs(da) / n, padAngle) : 0;
  const pa = p * (da < 0 ? -1 : 1);
  const k = sum > 0 ? (da - n * pa) / sum : 0;
  const out: PieSlice[] = [];
  for (const v of values) {
    const a1 = a0 + (v > 0 ? v * k : 0) + pa;
    out.push({ value: v, startAngle: roundFloat(a0 + pa / 2, 6), endAngle: roundFloat(a1 - pa / 2, 6) });
    a0 = a1;
  }
  return out;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add charts.shape

That builds the whole group. To build only what you call, and whatever it uses inside the group:

fune add charts.shape --only linePath
Download for TypeScript charts.shape-1.0.0-typescript.fune · 42,097 bytes sha256 8a16cf2c121a401c547f2a3282c090516e2b9e23d8e0071c7413164a7a26980f

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./charts.shape-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charts.shape@1.0.0:typescript.

The whole function, every language, is one file too: charts.shape-1.0.0.fune, 68,319 bytes, sha256 15b509f1fa14c931ee25641ba2cc66f912ffd9bb3383191ac51509f7ebe38d79. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before charts.shape.linePath
// fune: before charts.shape.areaPath
// fune: before charts.shape.stepPath
// fune: before charts.shape.monotoneCurve
// fune: before charts.shape.barRects
// fune: before charts.shape.arcPath
// fune: before charts.shape.pieAngles

after — your function gets the result and the arguments, and returns the final result.

// fune: after charts.shape.linePath
// fune: after charts.shape.areaPath
// fune: after charts.shape.stepPath
// fune: after charts.shape.monotoneCurve
// fune: after charts.shape.barRects
// fune: after charts.shape.arcPath
// fune: after charts.shape.pieAngles

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace math.round-float in charts.shape
// fune: replace math.sin-cos in charts.shape
// fune: replace text.format-decimal in charts.shape

step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show charts.shape --steps.

// fune: step charts.shape.<fn> after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

linePath 9 tests

CaseArgumentsExpected
three points points ×3 → M0,0L10,20L30,15
coordinates round to 2 places on the stored value, and -0.004 prints as 0, not -0 points ×2 → M1,2.67L3.14,0
negative and tie-breaking coordinates points ×2 → M-1.5,2.25L0.13,100
a null y breaks the line into two subpaths points ×5 → M0,0L1,1M3,3L4,4
an isolated point is closed so a round cap shows it points ×6 → M0,0ZM2,2ZM4,4L5,5
leading and trailing gaps are dropped points ×4 → M1,1L2,2
a single point points ×1 → M1234.5,0.1Z
no points →
only gaps points ×2 →

areaPath 8 tests

CaseArgumentsExpected
an area down to a flat baseline points ×3 → M0,50L10,20L20,40L20,100L10,100L0,100Z
a stacked band with its own baseline per point points ×2 → M0,60L5,50L5,70L0,80Z
a null y1 splits the area; a lone point becomes a vertical sliver points ×4 → M0,5L0,10ZM2,6L3,7L3,10L2,10Z
one point points ×1 → M3,4L3,10Z
rounding on the stored values: 0.005 up, 1.005 down, 2.675 down points ×2 → M0.01,2.67L1,1L1,1L0.01,1Z
no points →
only gaps points ×1 →
an area above and below a mid baseline points ×2 → M0,20L10,80L10,50L0,50Z

stepPath 9 tests

CaseArgumentsExpected
step before: vertical first, at the previous x points ×3, before → M0,0L0,10L10,10L10,5L20,5
step middle: the jump halfway between points points ×3, middle → M0,0L5,0L5,10L15,10L15,5L20,5
step after: horizontal first, jump at the next x points ×3, after → M0,0L10,0L10,10L20,10L20,5
two points, step before points ×2, before → M0,0L0,8L4,8
a single point is closed points ×1, middle → M3,4Z
a gap splits the steps points ×5, middle → M0,0L1,0L1,2L2,2M4,1L6,1L6,3L8,3
the midpoint of 0 and 1.01 is 0.505000000000000004, which rounds up points ×2, middle → M0,0L0.51,0L0.51,1L1.01,1
no points , after →
an unknown position is an error points ×3, centre → error: step position must be before, middle or after

monotoneCurve 10 tests

CaseArgumentsExpected
a peak: the tangent at the top is flat, so the curve does not rise above 1 points ×3 → M0,0C0.33,0.5,0.67,1,1,1C1.33,1,1.67,0.5,2,0
flat then rising: a Catmull-Rom curve would dip below 0 here; this one stays flat points ×3 → M0,0C0.33,0,0.67,0,1,0C1.33,0,1.67,5,2,10
four rising points, tangents limited by Fritsch-Carlson points ×4 → M0,0C0.33,0.75,0.67,1.5,1,2C1.33,2.5,1.67,2.33,2,3C2.33,3.67,2.67,4.83,3,6
uneven spacing points ×3 → M0,0C0.67,2,1.33,4,2,4C2.33,4,2.67,4,3,4
two points are a straight line points ×2 → M0,0L4,4
one point is closed points ×1 → M5,5Z
a gap starts a new curve points ×6 → M0,0L1,1M3,0C3.33,0.5,3.67,1,4,1C4.33,1,4.67,0.5,5,0
no points →
repeated x is an error points ×2 → error: monotone curve needs x strictly increasing
decreasing x is an error points ×3 → error: monotone curve needs x strictly increasing

barRects 10 tests

CaseArgumentsExpected
a vertical bar up from the baseline bars ×1, vertical → ×1
a negative vertical bar hangs below the baseline with a positive height bars ×1, vertical → ×1
a horizontal bar bars ×1, horizontal → ×1
a negative horizontal bar bars ×1, horizontal → ×1
neighbouring bars share a rounded edge (rounding each width alone leaves a gap) bars ×2, vertical → ×2
a zero value is a zero-height bar bars ×1, vertical → ×1
zero thickness bars ×1, vertical → ×1
no bars , vertical →
a negative thickness is an error bars ×1, vertical → error: bar thickness must not be negative
an unknown orientation is an error bars ×1, diagonal → error: orientation must be vertical or horizontal

arcPath 13 tests

CaseArgumentsExpected
a quarter wedge from 12 o'clock to 3 o'clock cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 1.571 → M50,0A50,50,0,0,1,100,50L50,50Z
a quarter of a donut cx 50, cy 50, inner radius 25, outer radius 50, start angle 0, end angle 1.571 → M50,0A50,50,0,0,1,100,50L75,50A25,25,0,0,0,50,25Z
three quarters sets the large-arc flag cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 4.712 → M50,0A50,50,0,1,1,0,50L50,50Z
exactly a half turn is not a large arc cx 0, cy 0, inner radius 0, outer radius 100, start angle 0, end angle 3.142 → M0,-100A100,100,0,0,1,0,100L0,0Z
anticlockwise clears the sweep flag cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle -1.571 → M50,0A50,50,0,0,0,0,50L50,50Z
30 degrees: the end point rounds to 50,-86.6 cx 0, cy 0, inner radius 0, outer radius 100, start angle 0, end angle 0.524 → M0,-100A100,100,0,0,1,50,-86.6L0,0Z
a full circle is two half arcs (one arc back to its start draws nothing) cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 6.283 → M50,0A50,50,0,1,1,50,100A50,50,0,1,1,50,0Z
more than a full turn is a full circle cx 50, cy 50, inner radius 0, outer radius 50, start angle 0, end angle 10 → M50,0A50,50,0,1,1,50,100A50,50,0,1,1,50,0Z
a full ring draws its hole the other way round cx 50, cy 50, inner radius 20, outer radius 50, start angle 0, end angle 6.283 → M50,0A50,50,0,1,1,50,100A50,50,0,1,1,50,0ZM50,30A20,20,0,1,0,50,70A20,20,0,1,0,50,30Z
no angle is no path cx 50, cy 50, inner radius 0, outer radius 50, start angle 1, end angle 1 →
Show the other 3 tests
CaseArgumentsExpected
no radius is no path cx 50, cy 50, inner radius 0, outer radius 0, start angle 0, end angle 1 →
an inner radius beyond the outer is an error cx 0, cy 0, inner radius 60, outer radius 50, start angle 0, end angle 1 → error: arc radii must satisfy 0 <= innerRadius <= outerRadius
a negative inner radius is an error cx 0, cy 0, inner radius -1, outer radius 50, start angle 0, end angle 1 → error: arc radii must satisfy 0 <= innerRadius <= outerRadius

pieAngles 11 tests

CaseArgumentsExpected
a full pie of 1, 1 and 2 in input order 1, 1, 2, 0, 6.283, 0 → ×3
a zero value has no width 0, 3, 0, 1, 0 → ×2
all zeros: every slice is empty 0, 0, 0, 6.283, 0 → ×2
padding: half the pad is trimmed from each side of each slice 1, 1, 0, 2, 0.2 → ×2
a pad wider than the pie allows is capped at an equal share 1, 1, 1, 1, 0, 1, 1 → ×4
an end before the start runs anticlockwise 1, 1, 0, -2, 0 → ×2
the span is capped at one full turn 1, 0, 10, 0 → ×1
a half pie from 9 o'clock to 3 o'clock 2, 1, 1, -1.571, 1.571, 0 → ×3
no values , 0, 6.283, 0 →
a negative value is an error 1, -1, 0, 6.283, 0 → error: pie values must be zero or more
Show the other 1 test
CaseArgumentsExpected
a negative pad is an error 1, 0, 6.283, -0.1 → error: padAngle must not be negative

More from the author

## Numbers in paths

Every coordinate is rounded to 2 decimal places by `math.round-float` (half away from zero on the stored double) and printed by `text.format-decimal` with trailing zeros dropped: `10`, `10.5`, `0.13`, never `10.00`, `1e-7` or `-0`. A hundredth of a pixel is invisible, and fixed text is what makes the three languages agree. The path grammar follows d3-shape exactly: commands with no spaces (`M10,20L30,40`), and `C` with its three points separated by commas.

## Lines, areas and steps

- `linePath` is d3.line with `curveLinear`. A point whose `y` is null is a gap: the line stops and starts again with a new `M`. A run of a single point becomes `Mx,yZ`, which d3 emits so that a round line cap still draws a dot. - `areaPath` is d3.area: along the top edge (`y1`) left to right, back along the baseline (`y0`) right to left, then `Z`. `y0` is per point, so stacked areas pass their lower band edge. A null `y1` is a gap. - `stepPath` is d3.curveStepBefore (`before`), d3.curveStep (`middle`) and d3.curveStepAfter (`after`), including d3's extra final `L` for `middle`. The midpoint is computed as d3 does, `x0 x (1 - t) + x1 x t`.

## Monotone curves

`monotoneCurve` is d3.curveMonotoneX: cubic Hermite segments, written as Béziers, whose tangents are chosen by the Fritsch–Carlson rule so the curve is monotone wherever the data is. It never invents a bump between two equal values or dips below a flat start before a rise, which a Catmull-Rom or cardinal spline does. The interior tangent is d3's `(sign(s0) + sign(s1)) x min(|s0|, |s1|, |p| / 2)` with `p` the weighted mean slope; the end tangents come from the one-sided slope. Two points are a straight `L`; one is `Mx,yZ`. Within an unbroken run x must strictly increase: d3 silently produces a broken path otherwise, and here it is an error.

## Bars

`barRects` turns bars given in pixels (`band` start and `thickness` across the axis, `base` and `value` pixels along it) into rectangles. Edges are rounded, and widths and heights are the differences of the rounded edges, so two bars that share an edge still share it exactly after rounding (rounding each width separately leaves hairline gaps). A bar below its baseline gets a positive height and its `y` at the top edge, as `<rect>` needs. `vertical` bars stand on the x axis; `horizontal` bars grow along x.

## Arcs

`arcPath` takes angles in radians from 12 o'clock, increasing clockwise (as d3.arc), so a point at angle `a` and radius `r` is `(cx + r sin a, cy - r cos a)`. The sine and cosine come from `math.sin-cos`, not the platform, so arc ends round the same way everywhere. The format:

- Ring segment: `M` outer start, `A ro,ro,0,large,sweep,` outer end, `L` inner end, `A ri,ri,0,large,1-sweep,` inner start, `Z`. - `innerRadius` 0 is a wedge: after the outer arc, `L cx,cy Z`. - `large` is 1 when the span is more than half a turn (exactly half is 0); `sweep` is 1 when the arc runs clockwise (end after start), 0 otherwise. - A span of a full turn or more is a full circle. One SVG arc cannot end where it starts (it draws nothing), so it is two half arcs, `M start A ... opposite A ... start Z`; a ring adds its hole as a second subpath drawn the other way round, so the nonzero fill rule leaves it empty. - Zero span or zero outer radius is `""`. Radii must satisfy 0 <= innerRadius <= outerRadius. d3's corner radius and padRadius are not supported.

## Pie angles

`pieAngles` is d3.pie with `sort(null)`: slices in input order, each value's share of the span from `startAngle` to `endAngle` (capped at one full turn; an end before the start runs anticlockwise), with `padAngle` between slices (capped at an equal share each). One difference: d3 returns each slice's angles including its pad and leaves the arc generator to trim it; here half the pad is already trimmed from each side, so the angles go straight into `arcPath`. Angles are rounded to 6 decimal places; the running total is not, so the last slice still ends at the end angle. Zero values are empty slices; negative values and a negative pad are errors (d3 quietly treats negatives as zero).

Sources: d3-shape (github.com/d3/d3-shape: line, area, curveStep, curveMonotoneX, pie); F. N. Fritsch and R. E. Carlson, "Monotone Piecewise Cubic Interpolation", SIAM Journal on Numerical Analysis 17(2), 1980, pp. 238-246; W3C, Scalable Vector Graphics (SVG) 2, "Paths" chapter (path data grammar and elliptical arc flags, section 9.3.8, and the out-of-range parameters note on arcs whose end point equals their start).

Files

PathBytes
README.md5,156
impl/python/arc_path.py2,022
impl/python/area_path.py921
impl/python/bar_rects.py1,310
impl/python/line_path.py1,474
impl/python/monotone_curve.py2,207
impl/python/pie_angles.py1,288
impl/python/step_path.py1,275
impl/rust/arc_path.rs2,703
impl/rust/area_path.rs1,522
impl/rust/bar_rects.rs2,019
impl/rust/line_path.rs1,866
impl/rust/monotone_curve.rs2,572
impl/rust/pie_angles.rs1,995
impl/rust/step_path.rs1,592
impl/typescript/arc_path.ts1,969
impl/typescript/area_path.ts850
impl/typescript/bar_rects.ts1,225
impl/typescript/line_path.ts1,350
impl/typescript/monotone_curve.ts2,502
impl/typescript/pie_angles.ts1,249
impl/typescript/step_path.ts1,284
vectors.json13,740