Piechart
This commit is contained in:
parent
19bab32e9c
commit
657b77a1ed
62
_example/piechart.go
Normal file
62
_example/piechart.go
Normal file
@ -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()
|
||||
}
|
292
piechart.go
Normal file
292
piechart.go
Normal file
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user