[feat] package chartbar: 支持负数; 支持只绘制容器中的部分元素; 支持配置柱状图样式

pull/3/head
q191201771 3 years ago
parent 0c31ba105b
commit 4e6996f8db

@ -6,17 +6,11 @@
//
// Author: Chef (191201771@qq.com)
// Package chartbar 控制台绘制ascii柱状图
//
package chartbar
import (
"encoding/csv"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
)
// TODO(chef): 如果总数小于绘制长度并且都是正整数,可以考虑按原始值而非比例绘制
const (
OrderOrigin Order = iota + 1 // 原始序
@ -24,6 +18,8 @@ const (
OrderDescCount // 按计数值降序排序
OrderAscName // 按字段名称升序排序
OrderDescName // 按字段名称降序排序
NoNumLimit = -1
)
type Item struct {
@ -35,164 +31,50 @@ type Item struct {
type Order int
var (
// config
//barList = "▏▎▍▌▋▊▉█"
maxLength = 50
)
type Option struct {
Order Order
}
var defaultOption = &Option{
Order: OrderDescCount,
}
func WithItems(items []Item, option *Option) string {
if option == nil {
option = defaultOption
}
// 最大的画满柱状条,其他的按与最大占比画
maxNum := calcMaxNum(items)
for i := range items {
items[i].count = int(math.Round(items[i].Num * float64(maxLength) / maxNum))
// 最小可能和最大的比太小了
if items[i].count == 0 {
items[i].count = 1
}
}
switch option.Order {
case OrderOrigin:
// noop
case OrderAscCount:
sort.Slice(items, func(i, j int) bool {
return items[i].Num < items[j].Num
})
case OrderDescCount:
sort.Slice(items, func(i, j int) bool {
return items[i].Num > items[j].Num
})
case OrderAscName:
sort.Slice(items, func(i, j int) bool {
return items[i].Name < items[j].Name
})
case OrderDescName:
sort.Slice(items, func(i, j int) bool {
return items[i].Name > items[j].Name
})
}
maxNameLength := calcMaxNameLen(items)
maxCountLength := calcMaxCount(items)
maxLengthOfNum := calcMaxLengthOfNum(items)
tmpl := fmt.Sprintf("%%%d.2f | %%-%ds | %%-%ds\n", maxLengthOfNum, maxCountLength, maxNameLength)
_ = maxNameLength
var out string
for _, item := range items {
bar := strings.Repeat("█", item.count)
out += fmt.Sprintf(tmpl, item.Num, bar, item.Name)
}
return out
}
func WithMap(m map[string]int, option *Option) string {
var items []Item
for k, v := range m {
item := Item{
Name: k,
Num: float64(v),
}
items = append(items, item)
}
MaxBarLength int
DrawIconBlock string
DrawIconPadding string
return WithItems(items, option)
Order Order
PrefixNumLimit int
SuffixNumLimit int
}
func WithMapFloat(m map[string]float64, option *Option) string {
var items []Item
var defaultOption = Option{
// 50 "▇" " "
// 18 "口" " "
MaxBarLength: 50, // MaxBarLength 柱状图形的最大长度
DrawIconBlock: "▇", // 柱状图实体绘制内容
DrawIconPadding: " ", // 柱状图空余部分绘制内容
for k, v := range m {
item := Item{
Name: k,
Num: v,
}
items = append(items, item)
}
return WithItems(items, option)
Order: OrderDescCount,
PrefixNumLimit: NoNumLimit,
SuffixNumLimit: NoNumLimit,
}
func WithCsv(filename string, option *Option) (string, error) {
// 读取
fp, err := os.Open(filename)
if err != nil {
return "", err
}
defer fp.Close()
r := csv.NewReader(fp)
records, err := r.ReadAll()
if err != nil {
return "", err
}
var items []Item
for _, line := range records {
var item Item
item.Name = line[0]
item.Num, err = strconv.ParseFloat(line[1], 64)
if err != nil {
return "", err
}
items = append(items, item)
}
// ---------------------------------------------------------------------------------------------------------------------
return WithItems(items, option), nil
}
var DefaultCtx = NewCtx()
func isFloat(v string) bool {
return strings.Contains(v, ".")
}
type ModOption func(option *Option)
func calcMaxNum(items []Item) float64 {
var max float64
for _, item := range items {
if item.Num > max {
max = item.Num
}
func NewCtx(modOptions ...ModOption) *Ctx {
option := defaultOption
for _, fn := range modOptions {
fn(&option)
}
return max
}
func calcMaxNameLen(items []Item) int {
var max int
for _, item := range items {
if len(item.Name) > max {
max = len(item.Name)
}
return &Ctx{
option: option,
}
return max
}
func calcMaxCount(items []Item) int {
var max int
for _, item := range items {
if item.count > max {
max = item.count
}
func NewCtxWith(ctx *Ctx, modOptions ...ModOption) *Ctx {
option := ctx.option
for _, fn := range modOptions {
fn(&option)
}
return max
}
func calcMaxLengthOfNum(items []Item) int {
var max float64
for _, item := range items {
if item.Num > max {
max = item.Num
}
return &Ctx{
option: option,
}
return len(fmt.Sprintf("%0.2f", max))
}

@ -14,14 +14,42 @@ import (
)
func TestWithItems(t *testing.T) {
v := []Item{
var v []Item
// 测试中文
v = []Item{
//{Name: "China", Num: 1},
{Name: "中", Num: 22},
{Name: "中国", Num: 333},
{Name: "中国啊", Num: 4444},
}
for i := range v {
fmt.Println(len(v[i].Name))
fmt.Println(DefaultCtx.WithItems(v))
// 测试负数
v = []Item{
{Name: "q", Num: -10},
{Name: "w", Num: -8},
{Name: "e", Num: -4},
{Name: "r", Num: -1},
{Name: "t", Num: 0},
{Name: "y", Num: 2},
{Name: "u", Num: 3},
{Name: "i", Num: 6},
{Name: "o", Num: 10},
}
fmt.Println(WithItems(v, nil))
fmt.Println(DefaultCtx.WithItems(v))
// 测试范围
fmt.Println(NewCtx(func(option *Option) {
option.PrefixNumLimit = 3
}).WithItems(v))
fmt.Println(NewCtx(func(option *Option) {
option.SuffixNumLimit = 4
}).WithItems(v))
fmt.Println(NewCtx(func(option *Option) {
option.PrefixNumLimit = 3
option.SuffixNumLimit = 4
}).WithItems(v))
// TODO(chef): 更精细的绘制 " ▏▎▍▌▋▊▉█"
}

@ -0,0 +1,221 @@
// Copyright 2021, Chef. All rights reserved.
// https://github.com/q191201771/naza
//
// Use of this source code is governed by a MIT-style license
// that can be found in the License file.
//
// Author: Chef (191201771@qq.com)
package chartbar
import (
"encoding/csv"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"github.com/q191201771/naza/pkg/dataops"
)
type Ctx struct {
option Option
}
//func (ctx *Ctx) ModOptions(modOptions ...ModOption) *Ctx {
// for _, fn := range modOptions {
// fn(&ctx.option)
// }
// return ctx
//}
// WithItems
//
// @param items: 注意,内部不会修改切片底层数据的值以及顺序
//
func (ctx *Ctx) WithItems(items []Item) string {
// 拷贝一份,避免修改外部切片的原始顺序
if ctx.option.Order != OrderOrigin {
clone := make([]Item, len(items))
copy(clone, items)
items = clone
}
switch ctx.option.Order {
case OrderOrigin:
// noop
case OrderAscCount:
sort.Slice(items, func(i, j int) bool {
return items[i].Num < items[j].Num
})
case OrderDescCount:
sort.Slice(items, func(i, j int) bool {
return items[i].Num > items[j].Num
})
case OrderAscName:
sort.Slice(items, func(i, j int) bool {
return items[i].Name < items[j].Name
})
case OrderDescName:
sort.Slice(items, func(i, j int) bool {
return items[i].Name > items[j].Name
})
}
var newItems []Item
dataops.SliceLimit(items, ctx.option.PrefixNumLimit, ctx.option.SuffixNumLimit, func(index int) {
newItems = append(newItems, items[index])
})
items = newItems
var (
maxCountLength int // count柱状最长画多长
maxLengthOfNum int // num字段多长
)
minNum, maxNum := calcMinMaxNum(items)
if minNum > 0.00 {
// 都是正数的情况,最大的画满柱状条,其他的按与最大占比画
for i := range items {
// round四舍五入
items[i].count = int(math.Round(items[i].Num * float64(ctx.option.MaxBarLength) / maxNum))
// 最小可能和最大的比太小了
if items[i].count == 0 {
items[i].count = 1
}
}
maxCountLength = calcMaxCount(items)
maxLengthOfNum = len(fmt.Sprintf("%0.2f", maxNum))
} else {
// 有负数的情况最小的负数画1最大的画满
for i := range items {
items[i].count = int(math.Round((items[i].Num - minNum) * float64(ctx.option.MaxBarLength) / (maxNum - minNum)))
if items[i].count == 0 {
items[i].count = 1
}
}
maxCountLength = calcMaxCount(items)
maxn := len(fmt.Sprintf("%0.2f", maxNum))
minn := len(fmt.Sprintf("%0.2f", minNum))
if maxn > minn {
maxLengthOfNum = maxn
} else {
maxLengthOfNum = minn
}
}
maxLengthOfName := calcMaxLengthOfName(items)
//tmpl := fmt.Sprintf("%%%d.2f | %%-%ds | %%-%ds\n", maxLengthOfNum, maxCountLength, maxLengthOfName)
tmpl := fmt.Sprintf("%%%d.2f | %%s%%s | %%-%ds\n", maxLengthOfNum, maxLengthOfName)
var out string
for _, item := range items {
bar := strings.Repeat(ctx.option.DrawIconBlock, item.count)
padding := strings.Repeat(ctx.option.DrawIconPadding, maxCountLength-item.count)
out += fmt.Sprintf(tmpl, item.Num, bar, padding, item.Name)
}
return out
}
func (ctx *Ctx) WithAnySlice(a interface{}, iterateTransFn func(originItem interface{}) Item, modOptions ...ModOption) string {
var items []Item
dataops.IterateInterfaceAsSlice(a, func(iterItem interface{}) {
items = append(items, iterateTransFn(iterItem))
})
return ctx.WithItems(items)
}
func (ctx *Ctx) WithMap(m map[string]int) string {
var items []Item
for k, v := range m {
item := Item{
Name: k,
Num: float64(v),
}
items = append(items, item)
}
return ctx.WithItems(items)
}
func (ctx *Ctx) WithMapFloat(m map[string]float64) string {
var items []Item
for k, v := range m {
item := Item{
Name: k,
Num: v,
}
items = append(items, item)
}
return ctx.WithItems(items)
}
func (ctx *Ctx) WithCsv(filename string) (string, error) {
// 读取
fp, err := os.Open(filename)
if err != nil {
return "", err
}
defer fp.Close()
r := csv.NewReader(fp)
records, err := r.ReadAll()
if err != nil {
return "", err
}
var items []Item
for _, line := range records {
var item Item
item.Name = line[0]
item.Num, err = strconv.ParseFloat(line[1], 64)
if err != nil {
return "", err
}
items = append(items, item)
}
return ctx.WithItems(items), nil
}
// ---------------------------------------------------------------------------------------------------------------------
// Num最大值
func calcMinMaxNum(items []Item) (min, max float64) {
max = math.SmallestNonzeroFloat64
min = math.MaxFloat64
for _, item := range items {
if item.Num > max {
max = item.Num
}
if item.Num < min {
min = item.Num
}
}
return
}
// count最大值
func calcMaxCount(items []Item) int {
var max int
for _, item := range items {
if item.count > max {
max = item.count
}
}
return max
}
func calcMaxLengthOfName(items []Item) int {
var max int
for _, item := range items {
if len(item.Name) > max {
max = len(item.Name)
}
}
return max
}
// ---------------------------------------------------------------------------------------------------------------------
Loading…
Cancel
Save