From 657b77a1edf74265c8186a1db59c964031328a25 Mon Sep 17 00:00:00 2001 From: Bernd Louis Date: Fri, 17 Feb 2017 17:41:56 +0100 Subject: [PATCH] Piechart --- _example/piechart.go | 62 +++++++++ piechart.go | 292 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 _example/piechart.go create mode 100644 piechart.go diff --git a/_example/piechart.go b/_example/piechart.go new file mode 100644 index 0000000..7344652 --- /dev/null +++ b/_example/piechart.go @@ -0,0 +1,62 @@ +// +build ignore + +package main + +import ( + "fmt" + "github.com/gizak/termui" + "math" + "math/rand" + "time" +) + +func main() { + if err := termui.Init(); err != nil { + panic(err) + } + defer termui.Close() + rand.Seed(time.Now().UTC().UnixNano()) + randomDataAndOffset := func() (data []float64, offset float64) { + noSlices := 1 + rand.Intn(5) + data = make([]float64, noSlices) + for i := range data { + data[i] = rand.Float64() + } + offset = 2.0 * math.Pi * rand.Float64() + return + } + run := true + + pc := termui.NewPieChart() + pc.BorderLabel = "Pie Chart" + pc.Width = 70 + pc.Height = 36 + pc.Data = []float64{.25, .25, .25, .25} + pc.Offset = -.5 * math.Pi + pc.Label = func(i int, v float64) string { + return fmt.Sprintf("%.02f", v) + } + + termui.Handle("/timer/1s", func(e termui.Event) { + if run { + pc.Data, pc.Offset = randomDataAndOffset() + termui.Render(pc) + } + }) + + termui.Handle("/sys/kbd/s", func(termui.Event) { + run = !run + if run { + pc.BorderLabel = "Pie Chart" + } else { + pc.BorderLabel = "Pie Chart (Stopped)" + } + termui.Render(pc) + }) + + termui.Handle("/sys/kbd/q", func(termui.Event) { + termui.StopLoop() + }) + termui.Render(pc) + termui.Loop() +} diff --git a/piechart.go b/piechart.go new file mode 100644 index 0000000..15aef60 --- /dev/null +++ b/piechart.go @@ -0,0 +1,292 @@ +package termui + +import ( + "container/list" + "image" + "math" +) + +const ( + piechartOffsetUp = -.5 * math.Pi // the northward angle + sectorFactor = 7.0 // circle resolution: precision vs. performance + fullCircle = 2.0 * math.Pi // the full circle angle + xStretch = 2.0 // horizontal adjustment + solidBlock = '░' +) + +var ( + defaultColors = []Attribute{ + ColorRed, + ColorGreen, + ColorYellow, + ColorBlue, + ColorMagenta, + ColorCyan, + ColorWhite, + } +) + +// PieChartLabel callback, i is the current data index, v the current value +type PieChartLabel func(i int, v float64) string + +type PieChart struct { + Block + Data []float64 // list of data items + Colors []Attribute // colors to by cycled through (see defaultColors) + BorderColor Attribute // color of the pie-border + Label PieChartLabel // callback function for labels + Offset float64 // where to start drawing? (see piechartOffsetUp) +} + +// Creates a new pie chart with reasonable defaults and no labels +func NewPieChart() *PieChart { + pc := &PieChart{Block: *NewBlock()} + pc.Colors = defaultColors + pc.Offset = piechartOffsetUp + pc.BorderColor = ColorDefault + return pc +} + +// computes the color for a given data index +func (pc *PieChart) colorFor(i int) Attribute { + return pc.Colors[i%len(pc.Colors)] +} + +// creates the buffer for the pie chart +func (pc *PieChart) Buffer() Buffer { + buf := pc.Block.Buffer() + w, h := pc.innerArea.Dx(), pc.innerArea.Dy() + center := image.Point{X: w / 2, Y: h / 2} + + // radius for the border + r := 0.0 + if h < w/xStretch { + r = float64(h/2) - 1.0 + } else { + r = float64(w/2/xStretch) - 1.0 + } + + // make border + borderCircle := &circle{Point: center, radius: r} + drawBorder := func() { + borderCircle.draw(&buf, Cell{Ch: solidBlock, Fg: pc.BorderColor, Bg: ColorDefault}) + } + drawBorder() + + if len(pc.Data) == 0 { // nothing to draw? + return buf + } + + // compute slice sizes + sum := sum(pc.Data) + sliceSizes := make([]float64, len(pc.Data)) + for i, v := range pc.Data { + sliceSizes[i] = v / sum * fullCircle + } + + // draw slice borders + phi := pc.Offset + for i, v := range sliceSizes { + p := borderCircle.at(phi) + l := line{P1: center, P2: p} + l.draw(&buf, &Cell{Ch: solidBlock, Fg: pc.colorFor(i), Bg: ColorDefault}) + phi += v + } + + // fill slices + middleCircle := circle{Point: center, radius: r / 2.0} + _, sectorSize := borderCircle.sectors() + phi = pc.Offset + for i, v := range sliceSizes { + if v > sectorSize { // do not render if slice is too small + cell := Cell{Ch: solidBlock, Fg: pc.colorFor(i), Bg: ColorDefault} + halfSlice := phi + v/2.0 + fill(borderCircle.inner(halfSlice), &cell, &buf) + fill(middleCircle.at(halfSlice), &cell, &buf) + for f := phi; f < phi+v; f += sectorSize { + line{P1: center, P2: borderCircle.inner(f)}.draw(&buf, &cell) + } + } + phi += v + } + + // labels + if pc.Label != nil { + drawLabel := func(p image.Point, label string, fg, bg Attribute) { + offset := p.Add(image.Point{X: -int(round(float64(len(label)) / 2.0)), Y: 0}) + for i, v := range []rune(label) { + buf.Set(offset.X+i, offset.Y, Cell{Ch: v, Fg: fg, Bg: bg}) + } + } + phi = pc.Offset + for i, v := range sliceSizes { + labelAt := middleCircle.at(phi + v/2.0) + if len(pc.Data) == 1 { + labelAt = center + } + drawLabel(labelAt, pc.Label(i, pc.Data[i]), pc.colorFor(i), ColorDefault) + phi += v + } + } + drawBorder() + return buf +} + +// fills empty cells from position +func fill(p image.Point, c *Cell, buf *Buffer) { + empty := func(x, y int) bool { + return buf.At(x, y).Ch == ' ' + } + if !empty(p.X, p.Y) { + return + } + q := list.New() + q.PushBack(p) + buf.Set(p.X, p.Y, *c) + for q.Front() != nil { + p := q.Remove(q.Front()).(image.Point) + w, e, row := p.X, p.X, p.Y + + for empty(w-1, row) { + w -= 1 + } + for empty(e+1, row) { + e += 1 + } + for x := w; x <= e; x++ { + buf.Set(x, row, *c) + if empty(x, row-1) { + q.PushBack(image.Point{X: x, Y: row - 1}) + } + if empty(x, row+1) { + q.PushBack(image.Point{X: x, Y: row + 1}) + } + } + } +} + +type circle struct { + image.Point + radius float64 +} + +// computes the point at a given angle phi +func (c circle) at(phi float64) image.Point { + x := c.X + int(round(xStretch*c.radius*math.Cos(phi))) + y := c.Y + int(round(c.radius*math.Sin(phi))) + return image.Point{X: x, Y: y} +} + +// computes the "inner" point at a given angle phi +func (c circle) inner(phi float64) image.Point { + p := image.Point{X: 0, Y: 0} + outer := c.at(phi) + if c.X < outer.X { + p.X = -1 + } else if c.X > outer.X { + p.X = 1 + } + if c.Y < outer.Y { + p.Y = -1 + } else if c.Y > outer.Y { + p.Y = 1 + } + return outer.Add(p) +} + +// computes the perimeter of a circle +func (c circle) perimeter() float64 { + return 2.0 * math.Pi * c.radius +} + +// computes the number of sectors and the size of each sector +func (c circle) sectors() (sectors float64, sectorSize float64) { + sectors = c.perimeter() * sectorFactor + sectorSize = fullCircle / sectors + return +} + +// draws the circle +func (c circle) draw(buf *Buffer, cell Cell) { + sectors, sectorSize := c.sectors() + for i := 0; i < int(round(sectors)); i++ { + phi := float64(i) * sectorSize + point := c.at(float64(phi)) + buf.Set(point.X, point.Y, cell) + } +} + +// a line between two points +type line struct { + P1, P2 image.Point +} + +// draws the line +func (l line) draw(buf *Buffer, cell *Cell) { + isLeftOf := func(p1, p2 image.Point) bool { + return p1.X <= p2.X + } + isTopOf := func(p1, p2 image.Point) bool { + return p1.Y <= p2.Y + } + p1, p2 := l.P1, l.P2 + buf.Set(l.P2.X, l.P2.Y, Cell{Ch: '*', Fg: cell.Fg, Bg: cell.Bg}) + width, height := l.size() + if width > height { // paint left to right + if !isLeftOf(p1, p2) { + p1, p2 = p2, p1 + } + flip := 1.0 + if !isTopOf(p1, p2) { + flip = -1.0 + } + for x := p1.X; x <= p2.X; x++ { + ratio := float64(height) / float64(width) + factor := float64(x - p1.X) + y := ratio * factor * flip + buf.Set(x, int(round(y))+p1.Y, *cell) + } + } else { // paint top to bottom + if !isTopOf(p1, p2) { + p1, p2 = p2, p1 + } + flip := 1.0 + if !isLeftOf(p1, p2) { + flip = -1.0 + } + for y := p1.Y; y <= p2.Y; y++ { + ratio := float64(width) / float64(height) + factor := float64(y - p1.Y) + x := ratio * factor * flip + buf.Set(int(round(x))+p1.X, y, *cell) + } + } +} + +// width and height of a line +func (l line) size() (w, h int) { + return abs(l.P2.X - l.P1.X), abs(l.P2.Y - l.P1.Y) +} + +// rounds a value +func round(x float64) float64 { + return math.Floor(x + 0.5) +} + +// fold a sum +func sum(data []float64) float64 { + sum := 0.0 + for _, v := range data { + sum += v + } + return sum +} + +// math.Abs for ints +func abs(x int) int { + if x >= 0 { + return x + } else { + return -x + } +}