package renderer
import (
"bytes"
"fmt"
"math"
"github.com/picoclaw/chart/internal/types"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
"gonum.org/v1/plot/vg/vgsvg"
)
type SVGRenderer struct {
width vg.Length
height vg.Length
}
func NewSVGRenderer() *SVGRenderer {
return &SVGRenderer{
width: 600,
height: 400,
}
}
func (r *SVGRenderer) Render(chart *types.Chart) ([]byte, error) {
p := plot.New()
if chart.Title != "" {
p.Title.Text = chart.Title
}
p.X.Label.Text = ""
p.Y.Label.Text = ""
width := r.width
height := r.height
if chart.Data.Options != nil {
if chart.Data.Options.Width > 0 {
width = vg.Length(chart.Data.Options.Width)
}
if chart.Data.Options.Height > 0 {
height = vg.Length(chart.Data.Options.Height)
}
}
switch chart.Type {
case types.ChartTypeLine:
r.addLineChart(p, &chart.Data)
case types.ChartTypeBar:
r.addBarChart(p, &chart.Data)
case types.ChartTypePie:
return r.renderPieChart(chart, false)
case types.ChartTypeScatter:
r.addScatterChart(p, &chart.Data)
case types.ChartTypeBubble:
return r.renderBubbleChart(chart)
case types.ChartTypeDonut:
return r.renderPieChart(chart, true)
case types.ChartTypeMixed:
return r.renderMixedChart(chart)
case types.ChartTypePolar:
return r.renderPolarChart(chart)
case types.ChartTypeRadar:
return r.renderRadarChart(chart)
default:
r.addLineChart(p, &chart.Data)
}
var buf bytes.Buffer
canvas := vgsvg.New(width, height)
p.Draw(draw.New(canvas))
_, err := canvas.WriteTo(&buf)
if err != nil {
return nil, fmt.Errorf("failed to write svg: %w", err)
}
return buf.Bytes(), nil
}
func (r *SVGRenderer) addLineChart(p *plot.Plot, data *types.ChartData) {
for i, dataset := range data.Datasets {
pts := make(plotter.XYs, len(dataset.Values))
for j, v := range dataset.Values {
pts[j].X = float64(j)
pts[j].Y = v
}
line, err := plotter.NewLine(pts)
if err != nil {
continue
}
colorStr := getColor(i)
if dataset.Color != "" {
colorStr = dataset.Color
}
line.Color = parseColor(colorStr)
p.Add(line)
}
if len(data.Labels) > 0 {
p.NominalX(data.Labels...)
}
}
func (r *SVGRenderer) addBarChart(p *plot.Plot, data *types.ChartData) {
if len(data.Datasets) == 0 {
return
}
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("%d", i)
}
}
barChart, err := plotter.NewBarChart(plotter.Values(dataset.Values), vg.Points(20))
if err != nil {
return
}
barChart.Color = parseColor(getColor(0))
if len(data.Datasets) > 0 && data.Datasets[0].Color != "" {
barChart.Color = parseColor(data.Datasets[0].Color)
}
p.Add(barChart)
p.NominalX(labels...)
}
func (r *SVGRenderer) renderPieChart(chart *types.Chart, isDonut bool) ([]byte, error) {
if len(chart.Data.Datasets) == 0 || len(chart.Data.Datasets[0].Values) == 0 {
return []byte{}, nil
}
dataset := chart.Data.Datasets[0]
values := dataset.Values
labels := chart.Data.Labels
total := 0.0
for _, v := range values {
total += v
}
var sb bytes.Buffer
sb.WriteString(``)
sb.WriteString(fmt.Sprintf("\n")
return sb.Bytes(), nil
}
func (r *SVGRenderer) addScatterChart(p *plot.Plot, data *types.ChartData) {
for i, dataset := range data.Datasets {
pts := make(plotter.XYs, len(dataset.Values))
for j, v := range dataset.Values {
pts[j].X = float64(j)
pts[j].Y = v
}
scatter, err := plotter.NewScatter(pts)
if err != nil {
continue
}
colorStr := getColor(i)
if dataset.Color != "" {
colorStr = dataset.Color
}
scatter.Color = parseColor(colorStr)
scatter.GlyphStyle.Radius = vg.Points(4)
p.Add(scatter)
}
if len(data.Labels) > 0 {
p.NominalX(data.Labels...)
}
}
func escapeXML(s string) string {
var result bytes.Buffer
for _, c := range s {
switch c {
case '<':
result.WriteString("<")
case '>':
result.WriteString(">")
case '&':
result.WriteString("&")
case '"':
result.WriteString(""")
case '\'':
result.WriteString("'")
default:
result.WriteRune(c)
}
}
return result.String()
}
func (r *SVGRenderer) renderBubbleChart(chart *types.Chart) ([]byte, error) {
if len(chart.Data.Datasets) == 0 {
return []byte{}, nil
}
var sb bytes.Buffer
sb.WriteString(``)
sb.WriteString(fmt.Sprintf("\n")
return sb.Bytes(), nil
}
func (r *SVGRenderer) renderMixedChart(chart *types.Chart) ([]byte, error) {
if len(chart.Data.Datasets) == 0 {
return []byte{}, nil
}
p := plot.New()
if chart.Title != "" {
p.Title.Text = chart.Title
}
p.X.Label.Text = ""
p.Y.Label.Text = ""
padding := 60.0
chartWidth := float64(r.width) - 2*padding
chartHeight := float64(r.height) - 2*padding
labels := chart.Data.Labels
if len(labels) == 0 {
labels = make([]string, len(chart.Data.Datasets[0].Values))
for i := range labels {
labels[i] = fmt.Sprintf("%d", i+1)
}
}
maxValue := 0.0
for _, dataset := range chart.Data.Datasets {
for _, v := range dataset.Values {
if v > maxValue {
maxValue = v
}
}
}
if maxValue == 0 {
maxValue = 100
}
var sb bytes.Buffer
sb.WriteString(``)
sb.WriteString(fmt.Sprintf("\n")
return sb.Bytes(), nil
}
func (r *SVGRenderer) renderPolarChart(chart *types.Chart) ([]byte, error) {
if len(chart.Data.Datasets) == 0 || len(chart.Data.Datasets[0].Values) == 0 {
return []byte{}, nil
}
dataset := chart.Data.Datasets[0]
values := dataset.Values
labels := chart.Data.Labels
maxValue := 0.0
for _, v := range values {
if v > maxValue {
maxValue = v
}
}
if maxValue == 0 {
maxValue = 100
}
var sb bytes.Buffer
sb.WriteString(``)
sb.WriteString(fmt.Sprintf("\n")
return sb.Bytes(), nil
}
func (r *SVGRenderer) renderRadarChart(chart *types.Chart) ([]byte, error) {
if len(chart.Data.Datasets) == 0 || len(chart.Data.Datasets[0].Values) == 0 {
return []byte{}, nil
}
dataset := chart.Data.Datasets[0]
values := dataset.Values
labels := chart.Data.Labels
maxValue := 0.0
for _, v := range values {
if v > maxValue {
maxValue = v
}
}
if maxValue == 0 {
maxValue = 100
}
var sb bytes.Buffer
sb.WriteString(``)
sb.WriteString(fmt.Sprintf("\n")
return sb.Bytes(), nil
}