File: piechart.go

package info (click to toggle)
golang-github-gizak-termui 3.1.0-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 464 kB
  • sloc: python: 37; makefile: 7
file content (149 lines) | stat: -rw-r--r-- 3,827 bytes parent folder | download
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)
}