// 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 }