diff --git a/vg/vgtex/canvas.go b/vg/vgtex/canvas.go
new file mode 100644
index 0000000000000000000000000000000000000000..92db716afed984523195bf15fdd17635b49b56c5
--- /dev/null
+++ b/vg/vgtex/canvas.go
@@ -0,0 +1,272 @@
+// Copyright 2016 The gonum Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package vgtex provides a vg.Canvas implementation for LaTeX, targeted at
+// the TikZ/PGF LaTeX package: https://sourceforge.net/projects/pgf
+//
+// vgtex generates PGF instructions that will be interpreted and rendered by LaTeX.
+// vgtex allows to put any valid LaTeX notation inside plot's strings.
+package vgtex
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"image/color"
+	"io"
+	"math"
+	"strings"
+
+	"github.com/gonum/plot/vg"
+)
+
+const degPerRadian = 180 / math.Pi
+
+const (
+	defaultHeader = `%%%%%% generated by gonum/plot %%%%%%
+\documentclass{standalone}
+\usepackage{pgf}
+\begin{document}
+`
+	defaultFooter = "\\end{document}\n"
+)
+
+// Canvas implements the vg.Canvas interface, translating drawing
+// primitives from gonum/plot to PGF.
+type Canvas struct {
+	buf   *bytes.Buffer
+	w, h  vg.Length
+	stack []context
+
+	// If document is true, Canvas.WriteTo will generate a standalone
+	// .tex file that can be fed to, e.g., pdflatex.
+	document bool
+}
+
+type context struct {
+	color      color.Color
+	dashArray  []vg.Length
+	dashOffset vg.Length
+	linew      vg.Length
+}
+
+// New returns a new LaTeX canvas.
+func New(w, h vg.Length) *Canvas {
+	return newCanvas(w, h, false)
+}
+
+// NewDocument returns a new LaTeX canvas that can be readily
+// compiled into a standalone document.
+func NewDocument(w, h vg.Length) *Canvas {
+	return newCanvas(w, h, true)
+}
+
+func newCanvas(w, h vg.Length, document bool) *Canvas {
+	c := &Canvas{
+		buf:      new(bytes.Buffer),
+		w:        w,
+		h:        h,
+		document: document,
+	}
+	if !document {
+		c.wtex(`%%%% gonum/plot created for LaTeX/pgf`)
+		c.wtex(`%%%% you need to add:`)
+		c.wtex(`%%%%   \usepackage{pgf}`)
+		c.wtex(`%%%% to your LaTeX document`)
+	}
+	c.wtex("")
+	c.wtex(`\begin{pgfpicture}`)
+	c.stack = make([]context, 1)
+	vg.Initialize(c)
+	return c
+}
+
+func (c *Canvas) ctx() *context {
+	return &c.stack[len(c.stack)-1]
+}
+
+// Size returns the width and height of the canvas.
+func (c *Canvas) Size() (w, h vg.Length) {
+	return c.w, c.h
+}
+
+// SetLineWidth implements the vg.Canvas.SetLineWidth method.
+func (c *Canvas) SetLineWidth(w vg.Length) {
+	c.ctx().linew = w
+}
+
+// SetLineDash implements the vg.Canvas.SetLineDash method.
+func (c *Canvas) SetLineDash(pattern []vg.Length, offset vg.Length) {
+	c.ctx().dashArray = pattern
+	c.ctx().dashOffset = offset
+}
+
+// SetColor implements the vg.Canvas.SetColor method.
+func (c *Canvas) SetColor(clr color.Color) {
+	c.ctx().color = clr
+}
+
+// Rotate implements the vg.Canvas.Rotate method.
+func (c *Canvas) Rotate(rad float64) {
+	c.wtex(`\pgftransformrotate{%g}`, rad*degPerRadian)
+}
+
+// Translate implements the vg.Canvas.Translate method.
+func (c *Canvas) Translate(x, y vg.Length) {
+	c.wtex(`\pgftransformshift{\pgfpoint{%gpt}{%gpt}}`, x, y)
+}
+
+// Scale implements the vg.Canvas.Scale method.
+func (c *Canvas) Scale(x, y float64) {
+	c.wtex(`\pgftransformxscale{%g}`, x)
+	c.wtex(`\pgftransformyscale{%g}`, y)
+}
+
+// Push implements the vg.Canvas.Push method.
+func (c *Canvas) Push() {
+	c.wtex(`\begin{pgfscope}`)
+	c.stack = append(c.stack, *c.ctx())
+}
+
+// Pop implements the vg.Canvas.Pop method.
+func (c *Canvas) Pop() {
+	c.stack = c.stack[:len(c.stack)-1]
+	c.wtex(`\end{pgfscope}`)
+	c.wtex("")
+}
+
+// Stroke implements the vg.Canvas.Stroke method.
+func (c *Canvas) Stroke(p vg.Path) {
+	if c.ctx().linew <= 0 {
+		return
+	}
+	c.wstyle()
+	c.wpath(p)
+	c.wtex(`\pgfusepath{stroke}`)
+	c.wtex("")
+}
+
+// Fill implements the vg.Canvas.Fill method.
+func (c *Canvas) Fill(p vg.Path) {
+	c.wstyle()
+	c.wpath(p)
+	c.wtex(`\pgfusepath{fill, stroke}`)
+	c.wtex("")
+}
+
+// FillString implements the vg.Canvas.FillString method.
+func (c *Canvas) FillString(f vg.Font, x, y vg.Length, text string) {
+	c.wcolor()
+	x += 0.5 * f.Width(text)
+	c.wtex(`\pgftext[base,at={\pgfpoint{%gpt}{%gpt}}]{%s}`, x, y, text)
+}
+
+func (c *Canvas) indent(s string) string {
+	return strings.Repeat(s, len(c.stack))
+}
+
+func (c *Canvas) wtex(s string, args ...interface{}) {
+	fmt.Fprintf(c.buf, c.indent("  ")+s+"\n", args...)
+}
+
+func (c *Canvas) wstyle() {
+	c.wdash()
+	c.wlineWidth()
+	c.wcolor()
+}
+
+func (c *Canvas) wdash() {
+	if len(c.ctx().dashArray) == 0 {
+		return
+	}
+	str := `\pgfsetdash{`
+	for _, d := range c.ctx().dashArray {
+		str += fmt.Sprintf("{%gpt}", d)
+	}
+	str += fmt.Sprintf("}{%gpt}", c.ctx().dashOffset)
+	c.wtex(str)
+}
+
+func (c *Canvas) wlineWidth() {
+	c.wtex(`\pgfsetlinewidth{%gpt}`, c.ctx().linew)
+}
+
+func (c *Canvas) wcolor() {
+	col := c.ctx().color
+	if col == nil {
+		col = color.Black
+	}
+	r, g, b, a := col.RGBA()
+	alpha := 255.0 / float64(a)
+	// FIXME(sbinet) \color will last until the end of the current TeX group
+	// use \pgfsetcolor and \pgfsetstrokecolor instead.
+	// it needs a named color: define it on the fly (storing it at the beginning
+	// of the document.)
+	c.wtex(
+		`\color[rgb]{%g,%g,%g}`,
+		float64(r)*alpha/255.0,
+		float64(g)*alpha/255.0,
+		float64(b)*alpha/255.0,
+	)
+
+	opacity := float64(a) / math.MaxUint16
+	c.wtex(`\pgfsetstrokeopacity{%g}`, opacity)
+	c.wtex(`\pgfsetfillopacity{%g}`, opacity)
+}
+
+func (c *Canvas) wpath(p vg.Path) {
+	for _, comp := range p {
+		switch comp.Type {
+		case vg.MoveComp:
+			c.wtex(`\pgfpathmoveto{\pgfpoint{%gpt}{%gpt}}`, comp.X, comp.Y)
+		case vg.LineComp:
+			c.wtex(`\pgflineto{\pgfpoint{%gpt}{%gpt}}`, comp.X, comp.Y)
+		case vg.ArcComp:
+			start := comp.Start * degPerRadian
+			angle := comp.Angle * degPerRadian
+			r := comp.Radius
+			c.wtex(`\pgfpatharc{%g}{%g}{%gpt}`, start, angle, r)
+		case vg.CloseComp:
+			c.wtex("%% path-close")
+		default:
+			panic(fmt.Errorf("vgtex: unknown path component type: %v\n", comp.Type))
+		}
+	}
+}
+
+// WriteTo implements the io.WriterTo interface, writing a LaTeX/pgf plot.
+func (c *Canvas) WriteTo(w io.Writer) (int64, error) {
+	var (
+		n   int64
+		nn  int
+		err error
+	)
+	b := bufio.NewWriter(w)
+	if c.document {
+		nn, err = w.Write([]byte(defaultHeader))
+		n += int64(nn)
+		if err != nil {
+			return n, err
+		}
+	}
+	m, err := c.buf.WriteTo(b)
+	n += m
+	if err != nil {
+		return n, err
+	}
+	nn, err = fmt.Fprintf(b, "\\end{pgfpicture}\n")
+	n += int64(nn)
+	if err != nil {
+		return n, err
+	}
+
+	if c.document {
+		nn, err = w.Write([]byte(defaultFooter))
+		n += int64(nn)
+		if err != nil {
+			return n, err
+		}
+	}
+	return n, b.Flush()
+}
diff --git a/vg/vgtex/canvas_test.go b/vg/vgtex/canvas_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..23018eebeb014c8e08791dab7a1b0a9902c11b58
--- /dev/null
+++ b/vg/vgtex/canvas_test.go
@@ -0,0 +1,50 @@
+// Copyright 2016 The gonum Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vgtex
+
+import (
+	"log"
+	"os"
+	"testing"
+
+	"github.com/gonum/plot"
+	"github.com/gonum/plot/plotter"
+	"github.com/gonum/plot/vg"
+	"github.com/gonum/plot/vg/draw"
+)
+
+func Example() {
+	scatter, err := plotter.NewScatter(plotter.XYs{{1, 1}, {0, 1}, {0, 0}})
+	if err != nil {
+		log.Fatal(err)
+	}
+	p, err := plot.New()
+	if err != nil {
+		log.Fatal(err)
+	}
+	p.Add(scatter)
+	// p.HideAxes()
+	p.Title.Text = `A scatter plot: $\sqrt{\frac{e^{3i\pi}}{2\cos 3\pi}}$`
+	p.X.Label.Text = `$x = \eta$`
+	p.Y.Label.Text = `$y$ is some $\Phi$`
+
+	c := NewDocument(5*vg.Centimeter, 5*vg.Centimeter)
+	p.Draw(draw.New(c))
+	c.FillString(p.Title.Font, 2.5*vg.Centimeter, 2.5*vg.Centimeter, "x")
+
+	f, err := os.Create("testdata/scatter.tex")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer f.Close()
+
+	if _, err = c.WriteTo(f); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func TestTexCanvas(t *testing.T) {
+	Example()
+}