417 lines
13 KiB
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
|
|
}
|