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) } }