mfer/vendor/github.com/pterm/pterm/barchart.go

417 lines
13 KiB
Go

package pterm
import (
"strconv"
"strings"
"github.com/mattn/go-runewidth"
"github.com/pterm/pterm/internal"
)
// BarChartPrinter is used to print bar charts.
type BarChartPrinter struct {
Bars Bars
Horizontal bool
ShowValue bool
// Height sets the maximum height of a vertical bar chart.
// The default is calculated to fit into the terminal.
// Ignored if Horizontal is set to true.
Height int
// Width sets the maximum width of a horizontal bar chart.
// The default is calculated to fit into the terminal.
// Ignored if Horizontal is set to false.
Width int
VerticalBarCharacter string
HorizontalBarCharacter string
}
var (
// DefaultBarChart is the default BarChartPrinter.
DefaultBarChart = BarChartPrinter{
Horizontal: false,
VerticalBarCharacter: "██",
HorizontalBarCharacter: "█",
// keep in sync with RecalculateTerminalSize()
Height: GetTerminalHeight() * 2 / 3,
Width: GetTerminalWidth() * 2 / 3,
}
)
// WithBars returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithBars(bars Bars) *BarChartPrinter {
p.Bars = bars
return &p
}
// WithVerticalBarCharacter returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithVerticalBarCharacter(char string) *BarChartPrinter {
p.VerticalBarCharacter = char
return &p
}
// WithHorizontalBarCharacter returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithHorizontalBarCharacter(char string) *BarChartPrinter {
p.HorizontalBarCharacter = char
return &p
}
// WithHorizontal returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithHorizontal(b ...bool) *BarChartPrinter {
b2 := internal.WithBoolean(b)
p.Horizontal = b2
return &p
}
// WithHeight returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithHeight(value int) *BarChartPrinter {
p.Height = value
return &p
}
// WithWidth returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithWidth(value int) *BarChartPrinter {
p.Width = value
return &p
}
// WithShowValue returns a new BarChartPrinter with a specific option.
func (p BarChartPrinter) WithShowValue(b ...bool) *BarChartPrinter {
p.ShowValue = internal.WithBoolean(b)
return &p
}
func (p BarChartPrinter) getRawOutput() string {
var ret string
for _, bar := range p.Bars {
ret += Sprintfln("%s: %d", bar.Label, bar.Value)
}
return ret
}
// Srender renders the BarChart as a string.
func (p BarChartPrinter) Srender() (string, error) {
maxAbsValue := func(value1 int, value2 int) int {
min := value1
max := value2
if value1 > value2 {
min = value2
max = value1
}
maxAbs := max
if min < 0 && -min > max { // This is to avoid something like "int(math.Abs(float64(minBarValue)))"
maxAbs = -min // (--) == (+)
}
return maxAbs
}
abs := func(value int) int {
if value < 0 {
return -value
}
return value
}
// =================================== VERTICAL BARS RENDERER ======================================================
type renderParams struct {
repeatCount int
bar Bar
positiveChartPartHeight int
negativeChartPartHeight int
positiveChartPartWidth int
negativeChartPartWidth int
indent string
showValue bool
moveUp bool
moveRight bool
}
renderPositiveVerticalBar := func(renderedBarRef *string, rParams renderParams) {
if rParams.showValue {
*renderedBarRef += Sprint(rParams.indent + strconv.Itoa(rParams.bar.Value) + rParams.indent + "\n")
}
for i := rParams.positiveChartPartHeight; i > 0; i-- {
if i > rParams.repeatCount {
*renderedBarRef += rParams.indent + " " + rParams.indent + " \n"
} else {
*renderedBarRef += rParams.indent + rParams.bar.Style.Sprint(p.VerticalBarCharacter) + rParams.indent + " \n"
}
}
// Used when we draw diagram with both POSITIVE and NEGATIVE values.
// In such case we separately draw top and bottom half of chart.
// And we need MOVE UP positive part to top part of chart,
// technically by adding empty pillars with height == height of chart's bottom part.
if rParams.moveUp {
for i := 0; i <= rParams.negativeChartPartHeight; i++ {
*renderedBarRef += rParams.indent + " " + rParams.indent + " \n"
}
}
}
renderNegativeVerticalBar := func(renderedBarRef *string, rParams renderParams) {
for i := 0; i > -rParams.negativeChartPartHeight; i-- {
if i > rParams.repeatCount {
*renderedBarRef += rParams.indent + rParams.bar.Style.Sprint(p.VerticalBarCharacter) + rParams.indent + " \n"
} else {
*renderedBarRef += rParams.indent + " " + rParams.indent + " \n"
}
}
if rParams.showValue {
*renderedBarRef += Sprint(rParams.indent + strconv.Itoa(rParams.bar.Value) + rParams.indent + "\n")
}
}
// =================================== HORIZONTAL BARS RENDERER ====================================================
renderPositiveHorizontalBar := func(renderedBarRef *string, rParams renderParams) {
if rParams.moveRight {
for i := 0; i < rParams.negativeChartPartWidth; i++ {
*renderedBarRef += " "
}
}
for i := 0; i < rParams.positiveChartPartWidth; i++ {
if i < rParams.repeatCount {
*renderedBarRef += rParams.bar.Style.Sprint(p.HorizontalBarCharacter)
} else {
*renderedBarRef += " "
}
}
if rParams.showValue {
// For positive horizontal bars we add one more space before adding value,
// so they will be well aligned with negative values, which have "-" sign before them
*renderedBarRef += " "
*renderedBarRef += " " + strconv.Itoa(rParams.bar.Value)
}
}
renderNegativeHorizontalBar := func(renderedBarRef *string, rParams renderParams) {
for i := -rParams.negativeChartPartWidth; i < 0; i++ {
if i < rParams.repeatCount {
*renderedBarRef += " "
} else {
*renderedBarRef += rParams.bar.Style.Sprint(p.HorizontalBarCharacter)
}
}
// In order to print values well-aligned (in case when we have both - positive and negative part of chart),
// we should insert an indent with width == width of positive chart part
if rParams.positiveChartPartWidth > 0 {
for i := 0; i < rParams.positiveChartPartWidth; i++ {
*renderedBarRef += " "
}
}
if rParams.showValue {
/*
This is in order to achieve this effect:
0
-15
0
-19
INSTEAD OF THIS:
0
-15
0
-19
*/
if rParams.repeatCount == 0 {
*renderedBarRef += " "
}
*renderedBarRef += " " + strconv.Itoa(rParams.bar.Value)
}
}
// =================================================================================================================
if RawOutput {
return p.getRawOutput(), nil
}
for i, bar := range p.Bars {
if bar.Style == nil {
p.Bars[i].Style = &ThemeDefault.BarStyle
}
if bar.LabelStyle == nil {
p.Bars[i].LabelStyle = &ThemeDefault.BarLabelStyle
}
p.Bars[i].Label = p.Bars[i].LabelStyle.Sprint(bar.Label)
}
var ret string
var maxLabelHeight int
var maxBarValue int
var minBarValue int
var maxAbsBarValue int
var rParams renderParams
for _, bar := range p.Bars {
if bar.Value > maxBarValue {
maxBarValue = bar.Value
}
if bar.Value < minBarValue {
minBarValue = bar.Value
}
labelHeight := len(strings.Split(bar.Label, "\n"))
if labelHeight > maxLabelHeight {
maxLabelHeight = labelHeight
}
}
maxAbsBarValue = maxAbsValue(maxBarValue, minBarValue)
if p.Horizontal {
panels := Panels{[]Panel{{}, {}}}
rParams.showValue = p.ShowValue
rParams.positiveChartPartWidth = p.Width
rParams.negativeChartPartWidth = p.Width
// If chart will consist of two parts - positive and negative - we should recalculate max bars WIDTH in LEFT and RIGHT parts
if minBarValue < 0 && maxBarValue > 0 {
rParams.positiveChartPartWidth = abs(internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Width)/2, float32(p.Width)/2, float32(maxBarValue)))
rParams.negativeChartPartWidth = abs(internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Width)/2, float32(p.Width)/2, float32(minBarValue)))
}
for _, bar := range p.Bars {
rParams.bar = bar
panels[0][0].Data += "\n" + bar.Label
panels[0][1].Data += "\n"
if minBarValue >= 0 {
// As we don't have negative values, draw only positive (right) part of the chart:
rParams.repeatCount = internal.MapRangeToRange(0, float32(maxAbsBarValue), 0, float32(p.Width), float32(bar.Value))
rParams.moveRight = false
renderPositiveHorizontalBar(&panels[0][1].Data, rParams)
} else if maxBarValue <= 0 {
// As we have only negative values, draw only negative (left) part of the chart:
rParams.repeatCount = internal.MapRangeToRange(-float32(maxAbsBarValue), 0, -float32(p.Width), 0, float32(bar.Value))
rParams.positiveChartPartWidth = 0
renderNegativeHorizontalBar(&panels[0][1].Data, rParams)
} else {
// We have positive and negative values, so draw both (left+right) parts of the chart:
rParams.repeatCount = internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Width)/2, float32(p.Width)/2, float32(bar.Value))
if bar.Value >= 0 {
rParams.moveRight = true
renderPositiveHorizontalBar(&panels[0][1].Data, rParams)
}
if bar.Value < 0 {
renderNegativeHorizontalBar(&panels[0][1].Data, rParams)
}
}
}
ret, _ = DefaultPanel.WithPanels(panels).Srender()
return ret, nil
} else {
renderedBars := make([]string, len(p.Bars))
rParams.showValue = p.ShowValue
rParams.positiveChartPartHeight = p.Height
rParams.negativeChartPartHeight = p.Height
// If chart will consist of two parts - positive and negative - we should recalculate max bars height in top and bottom parts
if minBarValue < 0 && maxBarValue > 0 {
rParams.positiveChartPartHeight = abs(internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Height)/2, float32(p.Height)/2, float32(maxBarValue)))
rParams.negativeChartPartHeight = abs(internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Height)/2, float32(p.Height)/2, float32(minBarValue)))
}
for i, bar := range p.Bars {
var renderedBar string
rParams.bar = bar
rParams.indent = strings.Repeat(" ", internal.GetStringMaxWidth(RemoveColorFromString(bar.Label))/2)
if minBarValue >= 0 {
// As we don't have negative values, draw only positive (top) part of the chart:
rParams.repeatCount = internal.MapRangeToRange(0, float32(maxAbsBarValue), 0, float32(p.Height), float32(bar.Value))
rParams.moveUp = false // Don't MOVE UP as we have ONLY positive part of chart.
renderPositiveVerticalBar(&renderedBar, rParams)
} else if maxBarValue <= 0 {
// As we have only negative values, draw only negative (bottom) part of the chart:
rParams.repeatCount = internal.MapRangeToRange(-float32(maxAbsBarValue), 0, -float32(p.Height), 0, float32(bar.Value))
renderNegativeVerticalBar(&renderedBar, rParams)
} else {
// We have positive and negative values, so draw both (top+bottom) parts of the chart:
rParams.repeatCount = internal.MapRangeToRange(-float32(maxAbsBarValue), float32(maxAbsBarValue), -float32(p.Height)/2, float32(p.Height)/2, float32(bar.Value))
if bar.Value >= 0 {
rParams.moveUp = true // MOVE UP positive part, because we have both positive and negative parts of chart.
renderPositiveVerticalBar(&renderedBar, rParams)
}
if bar.Value < 0 {
renderNegativeVerticalBar(&renderedBar, rParams)
}
}
labelHeight := len(strings.Split(bar.Label, "\n"))
renderedBars[i] = renderedBar + bar.Label + strings.Repeat("\n", maxLabelHeight-labelHeight) + " "
}
var maxBarHeight int
for _, bar := range renderedBars {
totalBarHeight := len(strings.Split(bar, "\n"))
if totalBarHeight > maxBarHeight {
maxBarHeight = totalBarHeight
}
}
for i, bar := range renderedBars {
totalBarHeight := len(strings.Split(bar, "\n"))
if totalBarHeight < maxBarHeight {
renderedBars[i] = strings.Repeat("\n", maxBarHeight-totalBarHeight) + renderedBars[i]
}
}
for i := 0; i <= maxBarHeight; i++ {
for _, barString := range renderedBars {
var barLine string
letterLines := strings.Split(barString, "\n")
maxBarWidth := internal.GetStringMaxWidth(RemoveColorFromString(barString))
if len(letterLines) > i {
barLine = letterLines[i]
}
letterLineLength := runewidth.StringWidth(RemoveColorFromString(barLine))
if letterLineLength < maxBarWidth {
barLine += strings.Repeat(" ", maxBarWidth-letterLineLength)
}
ret += barLine
}
ret += "\n"
}
}
return ret, nil
}
// Render prints the Template to the terminal.
func (p BarChartPrinter) Render() error {
s, _ := p.Srender()
Println(s)
return nil
}