212 lines
5.1 KiB
Go
212 lines
5.1 KiB
Go
|
// Copyright 2015 The Go 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 main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math"
|
||
|
"unicode/utf8"
|
||
|
|
||
|
"github.com/aclements/go-moremath/scale"
|
||
|
"github.com/aclements/go-moremath/stats"
|
||
|
"github.com/aclements/go-moremath/vec"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// printSamples is the number of points on the X axis to
|
||
|
// sample a function at for printing.
|
||
|
printSamples = 500
|
||
|
|
||
|
// printWidth is the width of the plot area in dots.
|
||
|
printWidth = 70 * 2
|
||
|
// printHeight is the height of the plot area in dots.
|
||
|
printHeight = 3 * 4
|
||
|
|
||
|
printXMargin = 1
|
||
|
printYMargin = 1
|
||
|
)
|
||
|
|
||
|
// FprintPDF prints a Unicode representation of the PDF of each
|
||
|
// distribution in dists to w. Multiple distributions are printed
|
||
|
// stacked vertically and on the same X axis (but possibly different Y
|
||
|
// axes).
|
||
|
func FprintPDF(w io.Writer, dists ...stats.Dist) error {
|
||
|
xscale, xs := commonScale(dists...)
|
||
|
for _, d := range dists {
|
||
|
if err := fprintFn(w, d.PDF, xscale, xs); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return fprintScale(w, xscale)
|
||
|
}
|
||
|
|
||
|
// FprintCDF is equivalent to FprintPDF, but prints the CDF of each
|
||
|
// distribution.
|
||
|
func FprintCDF(w io.Writer, dists ...stats.Dist) error {
|
||
|
xscale, xs := commonScale(dists...)
|
||
|
for _, d := range dists {
|
||
|
if err := fprintFn(w, d.CDF, xscale, xs); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return fprintScale(w, xscale)
|
||
|
}
|
||
|
|
||
|
// makeScale creates a linear scale from [x1, x2) to [y1, y2).
|
||
|
func makeScale(x1, x2 float64, y1, y2 int) scale.QQ {
|
||
|
return scale.QQ{
|
||
|
Src: &scale.Linear{Min: x1, Max: x2, Clamp: true},
|
||
|
Dest: &scale.Linear{Min: float64(y1), Max: float64(y2) - 1e-10},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func commonScale(dist ...stats.Dist) (xscale scale.QQ, xs []float64) {
|
||
|
var l, h float64
|
||
|
if len(dist) == 0 {
|
||
|
l, h = -1, 1
|
||
|
} else {
|
||
|
l, h = dist[0].Bounds()
|
||
|
for _, d := range dist[1:] {
|
||
|
dl, dh := d.Bounds()
|
||
|
l, h = math.Min(l, dl), math.Max(h, dh)
|
||
|
}
|
||
|
}
|
||
|
xscale = makeScale(l, h, printXMargin, printWidth-printXMargin)
|
||
|
//xscale.Src.Nice(10)
|
||
|
src := xscale.Src.(*scale.Linear)
|
||
|
xs = vec.Linspace(src.Min, src.Max, printSamples)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func fprintScale(w io.Writer, sc scale.QQ) error {
|
||
|
img := make([][]bool, printWidth)
|
||
|
for i := range img {
|
||
|
if i < printXMargin || i >= printWidth-printXMargin {
|
||
|
img[i] = make([]bool, 2)
|
||
|
} else {
|
||
|
img[i] = []bool{true, false}
|
||
|
}
|
||
|
}
|
||
|
major, _ := sc.Src.Ticks(scale.TickOptions{Max: 3})
|
||
|
labels := make([]string, len(major))
|
||
|
lpos := make([]int, len(major))
|
||
|
for i, tick := range major {
|
||
|
x := int(sc.Map(tick))
|
||
|
img[x][1] = true
|
||
|
// TODO: It would be nice if the scale could format
|
||
|
// these ticks in a consistent way.
|
||
|
labels[i] = fmt.Sprintf("%g", tick)
|
||
|
width := len(labels[i])
|
||
|
lpos[i] = minint(maxint(x/2-width/2, 0), (printWidth+1)/2-width)
|
||
|
}
|
||
|
if err := fprintImage(w, img, []string{""}); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
curpos := 0
|
||
|
for i, label := range labels {
|
||
|
gap := lpos[i] - curpos
|
||
|
if i > 0 {
|
||
|
gap = maxint(gap, 1)
|
||
|
}
|
||
|
_, err := fmt.Fprintf(w, "%*s%s", gap, "", label)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
curpos += gap + len(label)
|
||
|
}
|
||
|
_, err := fmt.Fprintf(w, "\n")
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func fprintFn(w io.Writer, fn func(float64) float64, xscale scale.QQ, xs []float64) error {
|
||
|
ys := vec.Map(fn, xs)
|
||
|
|
||
|
yl, yh := stats.Bounds(ys)
|
||
|
if yl > 0 && yl-(yh-yl)*0.1 <= 0 {
|
||
|
yl = 0
|
||
|
}
|
||
|
yscale := makeScale(yh, yl, printYMargin, printHeight-printYMargin)
|
||
|
|
||
|
// Render the function to an image.
|
||
|
img := make([][]bool, printWidth+2)
|
||
|
for i := range img {
|
||
|
img[i] = make([]bool, printHeight)
|
||
|
}
|
||
|
for i, x := range xs {
|
||
|
img[int(xscale.Map(x))][int(yscale.Map(ys[i]))] = true
|
||
|
}
|
||
|
|
||
|
// Render Y axis.
|
||
|
ypos := printWidth
|
||
|
for y := printYMargin; y < printHeight-printYMargin; y++ {
|
||
|
img[ypos][y] = true
|
||
|
}
|
||
|
img[ypos+1][printYMargin] = true
|
||
|
img[ypos+1][len(img[0])-1-printYMargin] = true
|
||
|
|
||
|
trail := make([]string, (printHeight+3)/4)
|
||
|
trail[0] = fmt.Sprintf(" %4.3f", yh)
|
||
|
trail[len(trail)-1] = fmt.Sprintf(" %4.3f", yl)
|
||
|
|
||
|
return fprintImage(w, img, trail)
|
||
|
}
|
||
|
|
||
|
func fprintImage(w io.Writer, img [][]bool, trail []string) error {
|
||
|
var x, y int
|
||
|
bit := func(ox, oy int) byte {
|
||
|
if x+ox < len(img) && y+oy < len(img[x+ox]) && img[x+ox][y+oy] {
|
||
|
return 1
|
||
|
}
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
maxTrail := len(trail[0])
|
||
|
for _, trail1 := range trail {
|
||
|
maxTrail = maxint(maxTrail, len(trail1))
|
||
|
}
|
||
|
buf := make([]byte, 3*(len(img)+1)/2+maxTrail+1)
|
||
|
for y = 0; y < len(img[0]); y += 4 {
|
||
|
bufpos := 0
|
||
|
for x = 0; x < len(img); x += 2 {
|
||
|
// Grab the 2x4 cell of pixels and encode it
|
||
|
// into a byte with the following bit layout:
|
||
|
// 0 3
|
||
|
// 1 4
|
||
|
// 2 5
|
||
|
// 6 7
|
||
|
cell := bit(0, 0)<<0 | bit(1, 0)<<3
|
||
|
cell |= bit(0, 1)<<1 | bit(1, 1)<<4
|
||
|
cell |= bit(0, 2)<<2 | bit(1, 2)<<5
|
||
|
cell |= bit(0, 3)<<6 | bit(1, 3)<<7
|
||
|
// Translate cell into the Unicode Braille space.
|
||
|
r := 0x2800 + rune(cell)
|
||
|
bufpos += utf8.EncodeRune(buf[bufpos:], r)
|
||
|
}
|
||
|
bufpos += copy(buf[bufpos:], trail[y/4])
|
||
|
buf[bufpos] = '\n'
|
||
|
if _, err := w.Write(buf[:bufpos+1]); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// TODO: These should be exported by go-moremath.
|
||
|
|
||
|
func maxint(a, b int) int {
|
||
|
if a > b {
|
||
|
return a
|
||
|
}
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
func minint(a, b int) int {
|
||
|
if a < b {
|
||
|
return a
|
||
|
}
|
||
|
return b
|
||
|
}
|