Skip to content
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ go run widgets/gauge/gaugedemo/gaugedemo.go

[<img src="./doc/images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)

## The Pie

Visualizes series of values as pie slices in the pie chart. Run the
[pie demo](widgets/pie/piedemo/piedemo.go).

```go
go run widgets/pie/piedemo/piedemo.go
```

## The Donut

Visualizes progress of an operation as a partial or a complete donut. Run the
Expand Down
57 changes: 57 additions & 0 deletions widgets/pie/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pie
Comment thread
mum4k marked this conversation as resolved.

import (
"github.com/mum4k/termdash/cell"
)

// Option defines a function that sets a specific option for the Pie widget.
type Option interface {
// set sets the provided option.
set(*options)
}

// option implements Option.
type option func(*options)

// set implements Option.set.
func (o option) set(opts *options) {
o(opts)
}

// options stores the provided options.
type options struct {
colors []cell.Color
}

// validates the provided options
// at the moment no validation is performed cause options are not required
func (o *options) validate() error {
return nil
}

// ColorOption sets custom colors for the pie chart segments.
func ColorOption(colors []cell.Color) Option {
return option(func(opts *options) {
opts.colors = colors
})
}

// newOptions creates a new options instance.
func newOptions() *options {
return &options{
colors: DefaultColors,
Comment thread
mum4k marked this conversation as resolved.
}
}

// DefaultColors defines a default set of colors used for rendering pie chart segments.
// These colors are chosen from the predefined cell.Color constants and include a variety
// of primary and secondary colors to ensure visual distinction between segments.
Comment thread
mum4k marked this conversation as resolved.
var DefaultColors = []cell.Color{
cell.ColorRed,
cell.ColorGreen,
cell.ColorBlue,
cell.ColorYellow,
cell.ColorMagenta,
cell.ColorCyan,
cell.ColorWhite,
}
162 changes: 162 additions & 0 deletions widgets/pie/pie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package pie

import (
"errors"
"fmt"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/canvas/braille"
"github.com/mum4k/termdash/private/draw"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"image"
"sync"
)

// Pie is the widget that displays a pie chart.
type Pie struct {
mu sync.Mutex
values []int
total int
colors []cell.Color
opts *options
}

// New returns a new Pie widget.
func New(opts ...Option) (*Pie, error) {
opt := newOptions()
for _, o := range opts {
o.set(opt)
}
return &Pie{
opts: opt,
}, nil
}

// Values must be provided before calling Draw.
func (p *Pie) Values(values []int, opts ...Option) error {
// The values must be non-negative and a color must be provided for each value.
// If not enough colors are provided, they will be reused.
p.mu.Lock()
defer p.mu.Unlock()

if len(values) == 0 {
return errors.New("values cannot be empty")
}

for _, opt := range opts {
opt.set(p.opts)
}

p.values = values
p.total = 0
if len(p.colors) == 0 {
p.colors = DefaultColors
Comment thread
mum4k marked this conversation as resolved.
}
for _, v := range values {
if v < 0 {
return errors.New("all values must be non-negative")
}
p.total += v
}

return nil
}

// it returns the center point and horizontal and vertical radii.
func pieChartMidAndRadii(ar image.Rectangle) (image.Point, int) {
width := ar.Dx() * braille.ColMult
height := ar.Dy() * braille.RowMult

radiusX := width/2 - 2
if radiusX < 1 {
radiusX = 1
}
mid := image.Point{
X: ar.Min.X*braille.ColMult + width/2,
Y: ar.Min.Y*braille.RowMult + height/2,
}
return mid, radiusX
}

// Draw renders the Pie widget onto the provided canvas. It calculates the
// pie chart slices based on the values and colors defined in the Pie struct.
// Each slice is drawn as a series of radial lines from the inner radius to
// the outer radius. The method ensures thread safety by locking the Pie's
// mutex during the drawing process.
//
// The number of colors in the list is not significant. If there are more values than
// colors, the colors will be reused in a round-robin fashion. This ensures that all
// segments are assigned a color, even if the number of values exceeds the number of
// available colors in the list.
//
// Parameters:
// - cvs: The canvas onto which the pie chart will be drawn.
// - meta: Metadata about the widget's environment.
//
// Returns:
// - error: An error if the drawing process fails, or nil if successful.
func (p *Pie) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
p.mu.Lock()
defer p.mu.Unlock()

if p.total <= 0 {
return nil
}

bc, err := braille.New(cvs.Area())
if err != nil {
return fmt.Errorf("braille.New => %v", err)
}

mid, radiusX := pieChartMidAndRadii(cvs.Area())

innerRadiusX := int(float64(radiusX) * 0.6)

currentAngle := 0.0
for i, value := range p.values {
endAngle := currentAngle + float64(value)/float64(p.total)*360 // Convert to degrees
color := p.colors[i%len(p.colors)]

for j := innerRadiusX; j <= radiusX; j++ {
// Draw the arc for the current slice.
if err := draw.BrailleCircle(
bc,
mid,
j,
draw.BrailleCircleArcOnly(int(currentAngle), int(endAngle)),
draw.BrailleCircleCellOpts(cell.FgColor(color)),
); err != nil {
return fmt.Errorf("failed to draw pie slice arc: %v", err)
}
}

currentAngle = endAngle
}

if err := bc.CopyTo(cvs); err != nil {
return err
}

return nil
}

// Keyboard input isn't supported on the Pie widget.
func (*Pie) Keyboard(k *terminalapi.Keyboard, meta *widgetapi.EventMeta) error {
return errors.New("the Pie widget doesn't support keyboard events")
}

// Mouse input isn't supported on the Pie widget.
func (*Pie) Mouse(m *terminalapi.Mouse, meta *widgetapi.EventMeta) error {
return errors.New("the Pie widget doesn't support mouse events")
}

// Options implements widgetapi.Widget.Options.
func (p *Pie) Options() widgetapi.Options {
return widgetapi.Options{
Ratio: image.Point{braille.RowMult, braille.ColMult},
MinimumSize: image.Point{5, 5},
WantKeyboard: widgetapi.KeyScopeNone,
WantMouse: widgetapi.MouseScopeNone,
}
}
Loading