246 lines
5.8 KiB
Go
246 lines
5.8 KiB
Go
|
|
package renderer
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/picoclaw/chart/internal/types"
|
||
|
|
)
|
||
|
|
|
||
|
|
type ANSIRenderer struct {
|
||
|
|
width int
|
||
|
|
height int
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewANSIRenderer() *ANSIRenderer {
|
||
|
|
return &ANSIRenderer{
|
||
|
|
width: 60,
|
||
|
|
height: 15,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) Render(chart *types.Chart) ([]byte, error) {
|
||
|
|
var sb strings.Builder
|
||
|
|
|
||
|
|
ansiGreen := "\033[32m"
|
||
|
|
ansiBlue := "\033[34m"
|
||
|
|
ansiYellow := "\033[33m"
|
||
|
|
ansiRed := "\033[31m"
|
||
|
|
ansiReset := "\033[0m"
|
||
|
|
ansiBold := "\033[1m"
|
||
|
|
ansiCyan := "\033[36m"
|
||
|
|
ansiMagenta := "\033[35m"
|
||
|
|
|
||
|
|
if chart.Title != "" {
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%s📊 %s%s\n", ansiBold, ansiGreen, chart.Title, ansiReset))
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(chart.Data.Datasets) == 0 {
|
||
|
|
return []byte(sb.String()), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
switch chart.Type {
|
||
|
|
case types.ChartTypePie, types.ChartTypeDonut:
|
||
|
|
r.renderPieChart(&sb, &chart.Data, ansiBold, ansiReset)
|
||
|
|
case types.ChartTypeBubble:
|
||
|
|
r.renderBubbleChart(&sb, &chart.Data, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiRed, ansiReset)
|
||
|
|
case types.ChartTypeMixed:
|
||
|
|
r.renderMixedChart(&sb, &chart.Data, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiReset)
|
||
|
|
case types.ChartTypePolar, types.ChartTypeRadar:
|
||
|
|
r.renderRadarLikeChart(&sb, &chart.Data, ansiBold, ansiCyan, ansiMagenta, ansiYellow, ansiReset)
|
||
|
|
default:
|
||
|
|
r.renderBarLikeChart(&sb, &chart.Data, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiRed, ansiReset)
|
||
|
|
}
|
||
|
|
|
||
|
|
return []byte(sb.String()), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) renderPieChart(sb *strings.Builder, data *types.ChartData, ansiBold, ansiReset string) {
|
||
|
|
dataset := data.Datasets[0]
|
||
|
|
values := dataset.Values
|
||
|
|
labels := data.Labels
|
||
|
|
|
||
|
|
total := 0.0
|
||
|
|
for _, v := range values {
|
||
|
|
total += v
|
||
|
|
}
|
||
|
|
|
||
|
|
colors := []string{"\033[31m", "\033[32m", "\033[34m", "\033[33m", "\033[35m", "\033[36m"}
|
||
|
|
|
||
|
|
for i, v := range values {
|
||
|
|
label := fmt.Sprintf("Item %d", i+1)
|
||
|
|
if i < len(labels) {
|
||
|
|
label = labels[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
percentage := v / total * 100
|
||
|
|
color := colors[i%len(colors)]
|
||
|
|
|
||
|
|
barLen := int(percentage / 4)
|
||
|
|
if barLen > 15 {
|
||
|
|
barLen = 15
|
||
|
|
}
|
||
|
|
bar := strings.Repeat("█", barLen)
|
||
|
|
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%-12s %s| %s%s [%.1f%%]%s\n",
|
||
|
|
color, label, ansiReset,
|
||
|
|
color, bar, percentage, ansiReset,
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) renderBubbleChart(sb *strings.Builder, data *types.ChartData, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiRed, ansiReset string) {
|
||
|
|
dataset := data.Datasets[0]
|
||
|
|
labels := data.Labels
|
||
|
|
if len(labels) == 0 {
|
||
|
|
labels = make([]string, len(dataset.Values))
|
||
|
|
for i := range labels {
|
||
|
|
labels[i] = fmt.Sprintf("X%d", i+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
maxValue := dataset.Values[0]
|
||
|
|
for _, v := range dataset.Values {
|
||
|
|
if v > maxValue {
|
||
|
|
maxValue = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
colors := []string{ansiGreen, ansiBlue, ansiYellow, ansiRed}
|
||
|
|
|
||
|
|
for i, value := range dataset.Values {
|
||
|
|
label := labels[i]
|
||
|
|
bubbleSize := 3 + int((value/maxValue)*5)
|
||
|
|
|
||
|
|
color := colors[i%len(colors)]
|
||
|
|
bubble := strings.Repeat("●", bubbleSize)
|
||
|
|
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%-8s%s %s%s %s%.2f%s\n",
|
||
|
|
ansiBold, label, ansiReset,
|
||
|
|
color, bubble, ansiReset,
|
||
|
|
ansiYellow, value, ansiReset,
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) renderMixedChart(sb *strings.Builder, data *types.ChartData, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiReset string) {
|
||
|
|
dataset := data.Datasets[0]
|
||
|
|
labels := data.Labels
|
||
|
|
if len(labels) == 0 {
|
||
|
|
labels = make([]string, len(dataset.Values))
|
||
|
|
for i := range labels {
|
||
|
|
labels[i] = fmt.Sprintf("X%d", i+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
maxValue := dataset.Values[0]
|
||
|
|
for _, v := range dataset.Values {
|
||
|
|
if v > maxValue {
|
||
|
|
maxValue = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
colors := []string{ansiGreen, ansiBlue}
|
||
|
|
|
||
|
|
for i, value := range dataset.Values {
|
||
|
|
label := labels[i]
|
||
|
|
|
||
|
|
barLen := int((value / maxValue) * float64(r.width-20))
|
||
|
|
if barLen < 1 {
|
||
|
|
barLen = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
isLine := i%2 == 1
|
||
|
|
var bar string
|
||
|
|
if isLine {
|
||
|
|
bar = strings.Repeat("●", barLen/2)
|
||
|
|
} else {
|
||
|
|
bar = strings.Repeat("█", barLen)
|
||
|
|
}
|
||
|
|
|
||
|
|
color := colors[i%len(colors)]
|
||
|
|
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%-8s%s %s%s %s%.2f [%s]%s\n",
|
||
|
|
ansiBold, label, ansiReset,
|
||
|
|
color, bar, ansiReset,
|
||
|
|
ansiYellow, value, ansiReset,
|
||
|
|
"mixed",
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) renderRadarLikeChart(sb *strings.Builder, data *types.ChartData, ansiBold, ansiCyan, ansiMagenta, ansiYellow, ansiReset string) {
|
||
|
|
dataset := data.Datasets[0]
|
||
|
|
values := dataset.Values
|
||
|
|
labels := data.Labels
|
||
|
|
|
||
|
|
maxValue := values[0]
|
||
|
|
for _, v := range values {
|
||
|
|
if v > maxValue {
|
||
|
|
maxValue = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%s雷达图/极区图 (共 %d 维度)%s\n",
|
||
|
|
ansiBold, ansiCyan, len(values), ansiReset))
|
||
|
|
|
||
|
|
for i, v := range values {
|
||
|
|
label := fmt.Sprintf("维度 %d", i+1)
|
||
|
|
if i < len(labels) {
|
||
|
|
label = labels[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
barLen := int((v / maxValue) * float64(r.width-25))
|
||
|
|
if barLen < 1 {
|
||
|
|
barLen = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
bar := strings.Repeat("◆", barLen)
|
||
|
|
|
||
|
|
sb.WriteString(fmt.Sprintf("%s%-12s%s %s%s %s%.2f%s\n",
|
||
|
|
ansiBold, label, ansiReset,
|
||
|
|
ansiMagenta, bar, ansiReset,
|
||
|
|
ansiYellow, v, ansiReset,
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *ANSIRenderer) renderBarLikeChart(sb *strings.Builder, data *types.ChartData, ansiBold, ansiGreen, ansiBlue, ansiYellow, ansiRed, ansiReset string) {
|
||
|
|
dataset := data.Datasets[0]
|
||
|
|
labels := data.Labels
|
||
|
|
if len(labels) == 0 {
|
||
|
|
labels = make([]string, len(dataset.Values))
|
||
|
|
for i := range labels {
|
||
|
|
labels[i] = fmt.Sprintf("X%d", i+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
maxValue := dataset.Values[0]
|
||
|
|
for _, v := range dataset.Values {
|
||
|
|
if v > maxValue {
|
||
|
|
maxValue = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
colors := []string{ansiGreen, ansiBlue, ansiYellow, ansiRed}
|
||
|
|
for i, value := range dataset.Values {
|
||
|
|
label := labels[i]
|
||
|
|
if i >= len(labels) {
|
||
|
|
label = fmt.Sprintf("X%d", i+1)
|
||
|
|
}
|
||
|
|
|
||
|
|
barLen := int((value / maxValue) * float64(r.width-20))
|
||
|
|
if barLen < 1 {
|
||
|
|
barLen = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
color := colors[i%len(colors)]
|
||
|
|
bar := strings.Repeat("█", barLen)
|
||
|
|
|
||
|
|
line := ansiBold + fmt.Sprintf("%-8s", label) + ansiReset + " " +
|
||
|
|
color + bar + ansiReset + " " +
|
||
|
|
ansiYellow + fmt.Sprintf("%.2f", value) + ansiReset + "\n"
|
||
|
|
sb.WriteString(line)
|
||
|
|
}
|
||
|
|
}
|