Functional Weave
Code in Python

charts.shape@1.0.1

README.md

5,399 bytes · view raw

# charts.shape

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.

## 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).

## Notices

Portions follow d3-shape (https://github.com/d3/d3-shape), Copyright 2010-2022 Mike Bostock, under the ISC License; the full notice is in NOTICE.

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.