1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
package widgets
import (
"image"
"math"
. "github.com/gizak/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)
}
|