150 lines
3.8 KiB
Go
150 lines
3.8 KiB
Go
package widgets
|
|
|
|
import (
|
|
"image"
|
|
"math"
|
|
|
|
. "git.tebitea.media/sashakoshka/termui/v3"
|
|
)
|
|
|
|
const (
|
|
piechartOffsetUp = -.5 * math.Pi // the northward angle
|
|
resolutionFactor = .0001 // circle resolution: precision vs. performance
|
|
fullCircle = 2.0 * math.Pi // the full circle angle
|
|
xStretch = 2.0 // horizontal adjustment
|
|
)
|
|
|
|
// PieChartLabel callback
|
|
type PieChartLabel func(dataIndex int, currentValue float64) string
|
|
|
|
type PieChart struct {
|
|
Block
|
|
Data []float64 // list of data items
|
|
Colors []Color // colors to by cycled through
|
|
LabelFormatter PieChartLabel // callback function for labels
|
|
AngleOffset float64 // which angle to start drawing at? (see piechartOffsetUp)
|
|
}
|
|
|
|
// NewPieChart Creates a new pie chart with reasonable defaults and no labels.
|
|
func NewPieChart() *PieChart {
|
|
return &PieChart{
|
|
Block: *NewBlock(),
|
|
Colors: Theme.PieChart.Slices,
|
|
AngleOffset: piechartOffsetUp,
|
|
}
|
|
}
|
|
|
|
func (self *PieChart) Draw(buf *Buffer) {
|
|
self.Block.Draw(buf)
|
|
|
|
center := self.Inner.Min.Add(self.Inner.Size().Div(2))
|
|
radius := MinFloat64(float64(self.Inner.Dx()/2/xStretch), float64(self.Inner.Dy()/2))
|
|
|
|
// compute slice sizes
|
|
sum := SumFloat64Slice(self.Data)
|
|
sliceSizes := make([]float64, len(self.Data))
|
|
for i, v := range self.Data {
|
|
sliceSizes[i] = v / sum * fullCircle
|
|
}
|
|
|
|
borderCircle := &circle{center, radius}
|
|
middleCircle := circle{Point: center, radius: radius / 2.0}
|
|
|
|
// draw sectors
|
|
phi := self.AngleOffset
|
|
for i, size := range sliceSizes {
|
|
for j := 0.0; j < size; j += resolutionFactor {
|
|
borderPoint := borderCircle.at(phi + j)
|
|
line := line{P1: center, P2: borderPoint}
|
|
line.draw(NewCell(SHADED_BLOCKS[1], NewStyle(SelectColor(self.Colors, i))), buf)
|
|
}
|
|
phi += size
|
|
}
|
|
|
|
// draw labels
|
|
if self.LabelFormatter != nil {
|
|
phi = self.AngleOffset
|
|
for i, size := range sliceSizes {
|
|
labelPoint := middleCircle.at(phi + size/2.0)
|
|
if len(self.Data) == 1 {
|
|
labelPoint = center
|
|
}
|
|
buf.SetString(
|
|
self.LabelFormatter(i, self.Data[i]),
|
|
NewStyle(SelectColor(self.Colors, i)),
|
|
image.Pt(labelPoint.X, labelPoint.Y),
|
|
)
|
|
phi += size
|
|
}
|
|
}
|
|
}
|
|
|
|
type circle struct {
|
|
image.Point
|
|
radius float64
|
|
}
|
|
|
|
// computes the point at a given angle phi
|
|
func (self circle) at(phi float64) image.Point {
|
|
x := self.X + int(RoundFloat64(xStretch*self.radius*math.Cos(phi)))
|
|
y := self.Y + int(RoundFloat64(self.radius*math.Sin(phi)))
|
|
return image.Point{X: x, Y: y}
|
|
}
|
|
|
|
// computes the perimeter of a circle
|
|
func (self circle) perimeter() float64 {
|
|
return 2.0 * math.Pi * self.radius
|
|
}
|
|
|
|
// a line between two points
|
|
type line struct {
|
|
P1, P2 image.Point
|
|
}
|
|
|
|
// draws the line
|
|
func (self line) draw(cell Cell, buf *Buffer) {
|
|
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 := self.P1, self.P2
|
|
buf.SetCell(NewCell('*', cell.Style), self.P2)
|
|
width, height := self.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.SetCell(cell, image.Pt(x, int(RoundFloat64(y))+p1.Y))
|
|
}
|
|
} 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.SetCell(cell, image.Pt(int(RoundFloat64(x))+p1.X, y))
|
|
}
|
|
}
|
|
}
|
|
|
|
// width and height of a line
|
|
func (self line) size() (w, h int) {
|
|
return AbsInt(self.P2.X - self.P1.X), AbsInt(self.P2.Y - self.P1.Y)
|
|
}
|