Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: ci

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

# The fleet's CI shape (go-fft/fft): a 100% statement-coverage gate over every
# package on three operating systems, then the test binary on six 64-bit
# architectures (two native, four under qemu-user), then cross-builds for the
# 32-bit and wasm targets. Every fixture the tests read is //go:embed-ded, so
# the test binary runs anywhere without the source tree.
jobs:
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
- run: go version
# Not on Windows: its checkout converts line endings to CRLF, and gofmt
# then flags every file. Formatting does not depend on the OS.
- name: gofmt
if: runner.os != 'Windows'
shell: bash
run: |
unformatted=$(gofmt -l .)
[ -z "$unformatted" ] || { echo "::error::not gofmt-formatted:"; echo "$unformatted"; exit 1; }
- run: go vet ./...
- run: go build ./...
- name: Test + 100% coverage gate
shell: bash
run: |
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -n 25
cov=$(go tool cover -func=cover.out | tail -1 | awk '{print $3}' | tr -d '%')
echo "coverage: ${cov}%"
awk -v t="$cov" 'BEGIN { exit !(t+0 >= 100.0) }' || {
echo "::error::coverage ${cov}% < 100%"; exit 1; }

security:
name: govulncheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...

arch-native:
name: arch ${{ matrix.label }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, label: amd64 }
- { os: ubuntu-24.04-arm, label: arm64 }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
- run: go env GOARCH
- run: |
go vet ./...
go test ./...

# qemu-user runs the test binary of every package on the targets without a
# hosted runner. s390x is the one big-endian target: the font tables, the
# PNG encoder and the colormap bytes all have to come out the same there.
arch-qemu:
name: arch ${{ matrix.goarch }} (qemu)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { goarch: riscv64, qemu: qemu-riscv64-static }
- { goarch: loong64, qemu: qemu-loongarch64-static }
- { goarch: ppc64le, qemu: qemu-ppc64le-static }
- { goarch: s390x, qemu: qemu-s390x-static }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
- run: sudo apt-get update && sudo apt-get install -y qemu-user-static
- name: Vet + test under qemu (${{ matrix.goarch }})
run: |
GOOS=linux GOARCH=${{ matrix.goarch }} go vet ./...
GOARCH=${{ matrix.goarch }} go test -exec=${{ matrix.qemu }} ./...

# 32-bit and wasm: everything must build (CGO_ENABLED=0), and the linux/386
# test binaries must build too.
cross:
name: cross-build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
- name: Build for 386, arm, js/wasm, wasip1/wasm
env:
CGO_ENABLED: '0'
run: |
GOOS=linux GOARCH=386 go build ./...
GOOS=linux GOARCH=386 go test -c -o /dev/null .
GOOS=linux GOARCH=arm go build ./...
GOOS=js GOARCH=wasm go build ./...
GOOS=wasip1 GOARCH=wasm go build ./...
- name: Run the tests on linux/386
run: GOARCH=386 go test ./...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/cover.out
*.test
/.venv/
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
BSD 3-Clause License

Copyright (c) 2026, go-plot
Copyright (c) 2026, the go-plot/plot authors
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
114 changes: 112 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,112 @@
# plot
Pure-Go (CGO=0) plotting for go-ndarray: matplotlib-class figures to SVG, PNG and the screen, with real LaTeX labels
# go-plot/plot

[![CI](https://github.com/go-plot/plot/actions/workflows/ci.yml/badge.svg)](https://github.com/go-plot/plot/actions/workflows/ci.yml)
[![pkg.go.dev](https://img.shields.io/badge/pkg.go.dev-plot-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/go-plot/plot)
![coverage](https://img.shields.io/badge/coverage-100%25-1a7f37)
![go](https://img.shields.io/badge/Go-1.27.2%2B-00ADD8?logo=go&logoColor=white)
![cgo](https://img.shields.io/badge/CGO-0-555)
![status](https://img.shields.io/badge/status-v0-9a6700)
[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue)](./LICENSE)

Pure-Go (CGO=0) plotting for [go-ndarray](https://github.com/go-ndarray/ndarray):
matplotlib-class figures to SVG and PNG, with real TeX in the labels.

![A line plot of two signals with TeX labels, drawn by go-plot](docs/readme.svg)

The object model and the defaults are matplotlib's — figure size, fonts, colour
cycle, margins, tick placement and tick labels — so a figure looks the way a
matplotlib user expects, and the parts that can be equal are **tested equal to
matplotlib**: tick locations and labels on 408 real axes, eight colormaps entry
for entry, and the layout of 30 figures (Axes positions, view limits, tick
labels, and positions after `TightLayout`). The API is the object-oriented one
that most notebooks use (`fig, ax = plt.subplots(); ax.plot(...)`). What Go adds: one static binary, `js/wasm`, no Python, and TeX labels
set by a real TeX math typesetter ([go-tex/math](https://github.com/go-tex/math)).

## Example

```go
package main

import (
"math"
"os"

"github.com/go-plot/plot"
)

func main() {
x := make([]float64, 200)
y := make([]float64, 200)
for i := range x {
x[i] = float64(i) / 20
y[i] = math.Sin(x[i])
}
fig, axs := plot.Subplots(1, 1)
ax := axs[0][0]
ax.Plot(x, y, plot.Label(`$\sin t$`))
ax.Plot(x, y, plot.Marker("o"), plot.LineStyle("None"), plot.MarkerSize(2))
ax.SetXLabel(`time $t$ (s)`)
ax.Legend()
fig.SavePNG(os.Stdout)
}
```

Data arguments take `[]float64`, other numeric slices, or a `*ndarray.Array` of
any dtype. A notebook kernel calls `fig.Display()`, which returns the figure as
`image/svg+xml` and `image/png`.

## What is in v0

| | |
|---|---|
| figures | `NewFigure`, `Subplots(rows, cols)`, `AddAxes`, size in inches and dpi, `TightLayout` |
| plots | `Plot` (lines, 17 markers, 4 line styles), `Scatter` (size and colour per point, colormaps), `Bar`, `Barh`, `Hist`, `FillBetween`, `ErrorBar`, `Imshow` (2-D data or RGB(A)), `Contour`, `Contourf`, `Axhline`, `Axvline`, `Text`, `Annotate` |
| axes | titles, labels, `Legend` (with `"best"`), `Grid`, `SetXLim`/`SetYLim`, linear and log scales, `TwinX`, manual ticks, aspect |
| colour | matplotlib's named colours, tab10 cycle, viridis, plasma, inferno, magma, cividis, gray, coolwarm, RdBu (and `_r`), `Colorbar`, `LogNorm` |
| output | `SaveSVG`, `SavePNG`, `Display` |

Not yet: symlog/date axes, `constrained_layout`, boxplot/pie/violin, hatching, windows
(go-widgets/window), PDF. The full list, and every place where go-plot differs
from matplotlib, is in [docs/design.md](docs/design.md).

## Compared with matplotlib

Rendering: twelve reference figures drawn by both (same data, size, dpi) put
every axes frame and every tick mark on the same pixel, and after
`TightLayout` within 2 pixels; the curves overlap to 0.970–1.000 (see [docs/design.md](docs/design.md#rendering-comparison), with the
negative controls the comparison is required to fail).

Speed, in-process, medians of 9 interleaved fresh-process runs on a loaded
16-core host ([docs/perf.md](docs/perf.md)):

| figure | go-plot | matplotlib 3.11.2 | |
|---|---|---|---|
| 1e6-point line → PNG | 0.055 s | 0.065 s | 1.2x |
| 1e6-point line → SVG | 0.019 s | 0.053 s | 2.8x |
| 1000² imshow → PNG | 0.025 s | 0.042 s | 1.7x |
| 1000² imshow → SVG | 0.020 s | 0.042 s | 2.1x |

Python's start and `import matplotlib.pyplot` (about 0.5 s) are not counted.

## Built on

[go-widgets/painter](https://github.com/go-widgets/painter) (compositing),
[go-gfx/gfx](https://github.com/go-gfx/gfx) (anti-aliased coverage),
[go-opentype](https://github.com/go-opentype) (DejaVu Sans, glyph outlines,
kerning), [go-tex/math](https://github.com/go-tex/math) (TeX),
[go-ndarray](https://github.com/go-ndarray/ndarray) (data).

## Development

```sh
go test ./... # includes the matplotlib fixtures
uv run --with matplotlib --with numpy python tools/gen_ticks.py # regenerate a fixture
cd tools/compare && go run . -out DIR && uv run --with matplotlib --with numpy python figures.py DIR \
&& uv run --with matplotlib --with numpy python compare.py DIR line=line ...
```

Write rendered images to a directory outside the repository.

## License

BSD 3-Clause. See [LICENSE](./LICENSE).
Loading
Loading