diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..417dfd7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: ci
+
+on:
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: the site builds
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: peaceiris/actions-hugo@v3
+ with:
+ hugo-version: "0.167.0"
+ extended: true
+ - run: hugo --minify
+ # A page that builds and says nothing is a page nobody notices is empty.
+ - name: every repository is on it
+ run: |
+ for repo in plot docs brand; do
+ grep -q "go-plot/$repo" public/index.html \
+ || { echo "::error::$repo is not on the page"; exit 1; }
+ done
+ echo "the page names every repository this org has"
diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml
new file mode 100644
index 0000000..1f2eb6e
--- /dev/null
+++ b/.github/workflows/hugo.yml
@@ -0,0 +1,88 @@
+# Build + deploy the Hugo site to GitHub Pages on every push to main.
+# Adapted from the official Hugo template at
+# https://gohugo.io/host-and-deploy/host-on-github-pages/.
+
+name: Deploy Hugo site to Pages
+
+on:
+ push:
+ branches: [main]
+ # Build on every pull request too, so a change to the layout, the content or
+ # the Hugo version has to prove the site still builds before it lands.
+ # Without this the workflow ran only on main, which meant a dependency bump
+ # arrived with no signal at all -- its checks read "no checks", which is not
+ # the same thing as green and must not be merged as if it were.
+ pull_request:
+ workflow_dispatch:
+
+# Allow only one concurrent deployment, skipping runs queued between
+# the run in-progress and latest queued. Don't cancel in-progress
+# runs — we want them to finish before a new build replaces them.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+# The build needs nothing beyond the source and the Pages configuration it
+# reads to compute the base URL. Publishing rights are granted to the deploy
+# job alone, below, so a pull request build cannot replace what is served.
+permissions:
+ contents: read
+ pages: read
+
+env:
+ # Pin Hugo so site builds don't break when a new release ships
+ # incompatible template changes. Bump deliberately, not by
+ # surprise.
+ HUGO_VERSION: 0.135.0
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Hugo CLI
+ run: |
+ wget -O hugo.deb "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/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
+ sudo dpkg -i hugo.deb
+
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ submodules: recursive
+
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v6
+
+ - name: Build
+ env:
+ HUGO_ENVIRONMENT: production
+ HUGO_ENV: production
+ run: |
+ hugo \
+ --gc \
+ --minify \
+ --baseURL "${{ steps.pages.outputs.base_url }}/"
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: ./public
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ # Only main publishes. A pull request stops after build above: it has built
+ # the site, which is the whole point of the gate, and it has no business
+ # replacing what is served.
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml
new file mode 100644
index 0000000..a00c877
--- /dev/null
+++ b/.github/workflows/links.yml
@@ -0,0 +1,73 @@
+name: links
+
+# The landing had a link to https://github.com/go-fileshare/authnd, which is a
+# 404: authnd lives in go-authn. The organisation profile carries the SAME link
+# and has it right, because .github/ has a link check and this repository did
+# not. A dead link on a landing page breaks nothing that complains.
+#
+# ⛔ THE CHECK READS THE BUILT SITE, NOT THE LAYOUTS. The first version of this
+# file grepped layouts/ and failed on its own first run against
+# `https://github.com/go-fileshare/{{ .name }}` -- a Hugo template expression,
+# not a URL. Skipping anything with a brace in it would have "fixed" that and
+# left the per-repository card links unchecked, which is most of the page's
+# links. Building first expands them into the URLs a reader actually gets,
+# which is the only set worth checking.
+
+on:
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ links:
+ name: every link and image resolves
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ submodules: recursive
+ - uses: peaceiris/actions-hugo@v3
+ with:
+ hugo-version: "0.167.0"
+ extended: true
+ - run: hugo --minify --destination ./public
+ - name: Check
+ run: |
+ set -uo pipefail
+ # The character class matters: an extractor that stops only at ) and "
+ # carries a closing > or < into the request.
+ urls=$(grep -rhoE 'https?://[^)">< [:space:]]+' public/ | sed 's/[.,]$//' | sort -u)
+ test -n "$urls" || { echo "::error::no links found; did the site build?"; exit 1; }
+ echo "checking $(printf '%s\n' "$urls" | wc -l) distinct URLs"
+ fail=0
+ while read -r u; do
+ # img.shields.io renders anything asked of it and proves nothing.
+ case "$u" in *img.shields.io*) continue ;; esac
+ # github.com answers curl's blob pages with 503 most of the time
+ # (7 of 8 on 2026-10-10, all 3 attempts on every run), while the
+ # raw host answers 200 for a file and 404 for a missing one. A blob
+ # link claims "this file exists at this ref", so ask the raw host.
+ # (go-ndarray/docs#30; `landingkit links` does the same, tested.)
+ probe=$u
+ case "$u" in
+ https://github.com/*/*/blob/*)
+ probe=https://raw.githubusercontent.com/$(printf '%s' "${u#https://github.com/}" | sed -E 's#^([^/]+/[^/]+)/blob/#\1/#; s/[?#].*$//') ;;
+ esac
+ # Three attempts: one is a flake detector rather than a link
+ # checker, and a lane that fails at random is a lane people learn
+ # to ignore.
+ code=000
+ for attempt in 1 2 3; do
+ code=$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 25 "$probe" || echo 000)
+ case "$code" in 2*|3*) break ;; esac
+ sleep 3
+ done
+ case "$code" in
+ 2*|3*) printf ' ok %s %s\n' "$code" "$u" ;;
+ *) printf ' FAIL %s %s\n' "$code" "$u"; fail=1 ;;
+ esac
+ done <<< "$urls"
+ test "$fail" -eq 0 || { echo "::error::at least one link or image does not resolve"; exit 1; }
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..256c50e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+public/
+resources/
+.hugo_build.lock
+.DS_Store
diff --git a/LICENSE b/LICENSE
index 177cd24..3e49f47 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
BSD 3-Clause License
-Copyright (c) 2026, go-plot
+Copyright (c) 2026, the go-plot/go-plot.github.io 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:
diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 0000000..e69de29
diff --git a/hugo.toml b/hugo.toml
new file mode 100644
index 0000000..e5bc8fa
--- /dev/null
+++ b/hugo.toml
@@ -0,0 +1,73 @@
+baseURL = 'https://go-plot.github.io/'
+locale = 'en-us'
+title = 'go-plot'
+# No theme -- the layout is the go-* family template (go-fileshare.github.io's,
+# via logos/landing-kit, as go-fft, go-ndarray and go-notebook use it): one
+# self-contained layouts/index.html, partials/styles.html (the shared sheet,
+# byte-identical) and partials/theme-toggle.html (system / light / dark).
+
+disableKinds = ['taxonomy', 'term', 'rss', 'sitemap', '404']
+
+[markup.goldmark.renderer]
+ unsafe = true
+
+[params]
+ description = "go-plot: pure-Go (CGO=0) plotting for go-ndarray. matplotlib's object model and defaults, figures to SVG and PNG with real TeX labels, tested equal to matplotlib where they can be equal."
+
+# Every field below is rendered by layouts/index.html (cards in #repos, the
+# strip under the hero). A field nothing renders does not stay true.
+[[params.repos]]
+ name = "plot"
+ kind = "library"
+ href = "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/go-plot/plot"
+ blurb = "The library: Figure, Axes and every plot type, with ticker (tick locations and labels), colors (the cycle, eight colormaps, norms), contour (marching squares), render (raster and vector SVG back-ends) and text (DejaVu Sans, and TeX through go-tex/math)."
+[[params.repos]]
+ name = "docs"
+ kind = "site"
+ href = "https://go-plot.github.io/docs/"
+ blurb = "The documentation (Hugo + Hextra): getting started, the API and why it has this shape, every plot type with the code that drew it, what is exact against matplotlib, TeX labels, performance, and the differences. Source →"
+[[params.repos]]
+ name = "brand"
+ kind = "assets"
+ href = "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/go-plot/brand"
+ blurb = "Logos and icons for the organization, in SVG / PNG / JPG / ICO / ICNS across colour, white and black variants."
+
+# The cards of #exact and their count come from this list. Each statement is
+# from go-plot/plot's docs/design.md at v0.1.0.
+[[params.checks]]
+ name = "Ticks, bit for bit"
+ what = "Tick locations, labels and offset text on 408 axes of real figures (4 sizes, 37 linear and 14 log ranges): every location bit for bit, every label character for character."
+[[params.checks]]
+ name = "Locators and formatters"
+ what = "45 MaxNLocator, 42 LogLocator, 72 ScalarFormatter and 25 LogFormatterSciNotation cases a default figure never reaches, exact."
+[[params.checks]]
+ name = "Eight colormaps"
+ what = "viridis, plasma, inferno, magma, cividis, gray, coolwarm, RdBu (and `_r`): 1008 samples per map compared as floats and as bytes."
+[[params.checks]]
+ name = "Named colours and the cycle"
+ what = "All 148 CSS4 colours, the 10 `tab:` colours, single letters, `C0`–`C9`, grey levels, short and long hex: exact."
+[[params.checks]]
+ name = "Layout, then TightLayout"
+ what = "15 kinds of figure at two sizes: every Axes position and view limit to 1e-9 relative, every tick label exactly. After `TightLayout` (6 kinds at two sizes), every Axes edge within 0.005 of the figure."
+[[params.checks]]
+ name = "Pixels, side by side"
+ what = "Twelve reference figures drawn by both put every axes frame and tick mark on the same pixel, and after `TightLayout` within 2 pixels; two deliberately shifted figures must fail, and do."
+
+[params.brand]
+# Generated by logos/landing-kit/kit.py from static/img/logo.svg: the colours are
+# READ from the logo's gradient, never chosen. Do not add other keys:
+# neutrals (backgrounds, surfaces, greys) belong to the shared sheet.
+# Contrast: base on white 4.82:1, bright on the dark background 7.94:1.
+ base = "#1f77b4"
+ deep = "#13496e"
+ bright = "#5aaee6"
+ shadow = "rgba(31,119,180,.7)"
+ glow = "rgba(31,119,180,.6)"
+ wash = "rgba(31,119,180,.10)"
+ washDeep = "rgba(19,73,110,.06)"
+ chipBg = "#ebf5fc"
+ chipBorder = "#cee7f8"
+ chipFg = "#13496e"
+ chipBgDark = "rgba(90,174,230,.14)"
+ chipBorderDark = "rgba(90,174,230,.32)"
+ chipFgDark = "#a4d2f1"
diff --git a/layouts/index.html b/layouts/index.html
new file mode 100644
index 0000000..2d851b3
--- /dev/null
+++ b/layouts/index.html
@@ -0,0 +1,144 @@
+
+
+
go-plot is the plotting library of the Go scientific stack built on go-ndarray. Its object model and 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. It writes SVG and PNG, sets labels with a real TeX math typesetter, and needs no Python and no cgo: one static binary, js/wasm included.
func main() { + x := make([]float64, 200) + for i := range x { + x[i] = float64(i) / 199 * 10 + } + sin, damped := make([]float64, len(x)), make([]float64, len(x)) + for i, v := range x { + sin[i] = math.Sin(v) + damped[i] = math.Exp(-v/4) * math.Cos(2*v) + } + + fig, axs := plot.Subplots(1, 1, plot.FigSize(6.4, 4)) + ax := axs[0][0] + ax.Plot(x, sin, plot.Label(`$\sin t$`)) + ax.Plot(x, damped, plot.Label(`$e^{-t/4}\cos 2t$`), plot.LineStyle("--")) + ax.FillBetween(x, damped, nil, plot.Alpha(0.2)) + ax.SetTitle(`Two signals, labelled in TeX`) + ax.SetXLabel(`time $t$ (s)`) + ax.SetYLabel("amplitude") + ax.Grid(true) + ax.Legend() + if err := fig.SaveSVG(os.Stdout); err != nil { + panic(err) + } +}+
go get github.com/go-plot/plot
+ This is the SVG it writes, unedited: two lines from the colour cycle, a dashed style, a translucent fill, a grid, a legend placed where it covers the least data, and labels set in TeX. Twelve reference figures drawn by both go-plot and matplotlib put every axes frame and every tick mark on the same pixel, and after TightLayout within 2 pixels.
A census of 339 scientific notebooks ranks plotting first among the things that keep them from moving to Go, and most of them write fig, ax = plt.subplots(); ax.plot(...). go-plot is that API, under the same names, with Go's argument style.
Subplots, Plot, SetXLabel, Legend, SetTitle, TightLayout, SetYLim, Axhline: every call the census ranks, named as in matplotlib. Keyword arguments become options: plot.Color("C1").
Tick locations and labels, colormaps, named colours, and the layout of figures are compared with fixtures generated by matplotlib 3.11.2 itself, exactly unless stated.
$...$ is typeset by go-tex/math with STIX Two Math: fractions, radicals, big operators, limits and matrices, as TeX lays them out.
Every data argument takes []float64, other numeric slices, or a *ndarray.Array of any of its thirteen dtypes.
SaveSVG writes vector paths, SavePNG anti-aliased pixels, and Display hands a notebook kernel both.
No Python, no C. It builds for the six 64-bit Linux targets, 386, arm, js/wasm and wasip1/wasm.
Gated in CI on three operating systems, with six architectures.
Every repository of the organization.
The fixtures are written by scripts that call matplotlib, committed, and compared in the tests. What is not equal — text rasterisation, contour vertices, image resampling — is listed in the documentation, not hidden.
+{{ .what | markdownify }}
In-process time, medians of 9 interleaved fresh-process runs per case on a shared, loaded 16-core Apple M4 Max, 2026-10-10. Python's start and import matplotlib.pyplot (about 0.5 s) are not counted.
| figure | go-plot | matplotlib 3.11.2 | ratio |
|---|---|---|---|
| 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 |
{{ .blurb | safeHTML }}
+go-plot draws the data of go-ndarray, and a go-notebook cell shows a figure through Display. What is not in v0 yet — symlog and date axes, constrained_layout, boxplot and pie, PDF, windows — is listed in the documentation.