- Support 9 chart types: line, bar, pie, scatter, bubble, donut, mixed, polar, radar - Multi-format output: ANSI, SVG, PNG, Markdown - Go + Fiber + gonum/plot - Docker support - Morandi color palette
199 lines
4.1 KiB
Go
199 lines
4.1 KiB
Go
package renderer
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/picoclaw/chart/internal/types"
|
|
)
|
|
|
|
type TextRenderer struct {
|
|
width int
|
|
height int
|
|
}
|
|
|
|
func NewTextRenderer() *TextRenderer {
|
|
return &TextRenderer{
|
|
width: 60,
|
|
height: 15,
|
|
}
|
|
}
|
|
|
|
func (r *TextRenderer) Render(chart *types.Chart) ([]byte, error) {
|
|
var sb strings.Builder
|
|
|
|
if chart.Title != "" {
|
|
sb.WriteString(fmt.Sprintf("[ Chart: %s ]\n", chart.Title))
|
|
}
|
|
|
|
if len(chart.Data.Datasets) == 0 {
|
|
sb.WriteString("No data available.\n")
|
|
return []byte(sb.String()), nil
|
|
}
|
|
|
|
switch chart.Type {
|
|
case types.ChartTypePie, types.ChartTypeDonut:
|
|
r.renderPieChart(&sb, &chart.Data)
|
|
case types.ChartTypeBubble:
|
|
r.renderBubbleChart(&sb, &chart.Data)
|
|
case types.ChartTypeMixed:
|
|
r.renderMixedChart(&sb, &chart.Data)
|
|
case types.ChartTypePolar, types.ChartTypeRadar:
|
|
r.renderRadarLikeChart(&sb, &chart.Data)
|
|
default:
|
|
r.renderBarLikeChart(&sb, &chart.Data)
|
|
}
|
|
|
|
return []byte(sb.String()), nil
|
|
}
|
|
|
|
func (r *TextRenderer) renderPieChart(sb *strings.Builder, data *types.ChartData) {
|
|
dataset := data.Datasets[0]
|
|
values := dataset.Values
|
|
labels := data.Labels
|
|
|
|
total := 0.0
|
|
for _, v := range values {
|
|
total += v
|
|
}
|
|
|
|
for i, v := range values {
|
|
label := fmt.Sprintf("Item %d", i+1)
|
|
if i < len(labels) {
|
|
label = labels[i]
|
|
}
|
|
|
|
percentage := v / total * 100
|
|
|
|
barLen := int(percentage / 4)
|
|
if barLen > 12 {
|
|
barLen = 12
|
|
}
|
|
bar := strings.Repeat("#", barLen)
|
|
|
|
sb.WriteString(fmt.Sprintf("%-12s |%s [%.1f%%]\n", label, bar, percentage))
|
|
}
|
|
}
|
|
|
|
func (r *TextRenderer) renderBubbleChart(sb *strings.Builder, data *types.ChartData) {
|
|
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
|
|
}
|
|
}
|
|
|
|
for i, value := range dataset.Values {
|
|
label := labels[i]
|
|
bubbleSize := 2 + int((value/maxValue)*5)
|
|
|
|
bubble := strings.Repeat("o", bubbleSize)
|
|
|
|
sb.WriteString(fmt.Sprintf("%-8s |%s %.2f\n", label, bubble, value))
|
|
}
|
|
}
|
|
|
|
func (r *TextRenderer) renderMixedChart(sb *strings.Builder, data *types.ChartData) {
|
|
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
|
|
}
|
|
}
|
|
|
|
for i, value := range dataset.Values {
|
|
label := labels[i]
|
|
|
|
barLen := int((value / maxValue) * float64(r.width-20))
|
|
if barLen < 1 {
|
|
barLen = 1
|
|
}
|
|
|
|
bar := strings.Repeat("#", barLen)
|
|
|
|
sb.WriteString(fmt.Sprintf("%-8s |%s %.2f [mixed]\n", label, bar, value))
|
|
}
|
|
}
|
|
|
|
func (r *TextRenderer) renderRadarLikeChart(sb *strings.Builder, data *types.ChartData) {
|
|
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("[ Radar/Polar Chart: %d dimensions ]\n", len(values)))
|
|
|
|
for i, v := range values {
|
|
label := fmt.Sprintf("Dim %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("%-12s |%s %.2f\n", label, bar, v))
|
|
}
|
|
}
|
|
|
|
func (r *TextRenderer) renderBarLikeChart(sb *strings.Builder, data *types.ChartData) {
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
bar := strings.Repeat("#", barLen)
|
|
sb.WriteString(fmt.Sprintf("%-8s |%s %.2f\n", label, bar, value))
|
|
}
|
|
}
|