diff --git a/config.go b/config.go new file mode 100644 index 0000000..51036cb --- /dev/null +++ b/config.go @@ -0,0 +1,151 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "slices" + + "github.com/go-echarts/go-echarts/v2/opts" +) + +type Config struct { + Data Data +} + +type Data struct { + Points [][]float64 + Labels []string + Weight struct { + Floor int + Range int + } + Title string + Subtitle string +} + +func newConfig() (Config, error) { + var result Config + result.Data.Points = [][]float64{ + []float64{1, 1}, + []float64{2, 2}, + []float64{2, 2}, + } + result.Data.Weight.Floor = 12 + result.Data.Weight.Range = 20 + + if js := os.Getenv("POINTS_JSON"); js == "" { + } else if err := json.Unmarshal([]byte(js), &result.Data.Points); err != nil { + return result, err + } + + if len(result.Data.Points) > 0 { + for i := range result.Data.Points { + if want := len(result.Data.Points[0]); want != len(result.Data.Points[i]) { + return result, fmt.Errorf("point [%d] expected %d dimensions but found %d", i, want, len(result.Data.Points[i])) + } + } + result.Data.Labels = make([]string, len(result.Data.Points[0])) + for i := range result.Data.Labels { + result.Data.Labels[i] = fmt.Sprintf("%c", byte(int('A')+i)) + } + + if js := os.Getenv("LABELS_JSON"); js == "" { + } else if err := json.Unmarshal([]byte(js), &result.Data.Labels); err != nil { + return result, err + } else if len(result.Data.Labels) < len(result.Data.Points[0]) { + return result, fmt.Errorf("expected at least %d labels but got %d", len(result.Data.Points[0]), len(result.Data.Labels)) + } + } + + if js := os.Getenv("WEIGHT_RANGE"); js == "" { + } else if err := json.Unmarshal([]byte(js), &result.Data.Weight.Range); err != nil { + return result, err + } else if result.Data.Weight.Range < 0 { + return result, fmt.Errorf("found negative weight range %d", result.Data.Weight.Range) + } + + if js := os.Getenv("WEIGHT_FLOOR"); js == "" { + } else if err := json.Unmarshal([]byte(js), &result.Data.Weight.Floor); err != nil { + return result, err + } else if result.Data.Weight.Floor < 0 { + return result, fmt.Errorf("found negative weight floor %d", result.Data.Weight.Floor) + } + + result.Data.Title = os.Getenv("TITLE") + result.Data.Subtitle = os.Getenv("SUBTITLE") + + return result, nil +} + +func (d Data) AsScatterData() (string, []float64, string, []opts.ScatterData, error) { + if !(2 <= len(d.Labels) && len(d.Labels) <= 3) { + return "", nil, "", nil, fmt.Errorf("cannot map %d dimensions to [2,3]", len(d.Labels)) + } + d = d.weighXYasZ() + + xs := make([]float64, 0) + for i := range d.Points { + xs = append(xs, d.Points[i][0]) + } + slices.Sort(xs) + xs = slices.Compact(xs) + + zs := make([]float64, 0) + for i := range d.Points { + zs = append(zs, d.Points[i][2]) + } + slices.Sort(zs) + zmin, zmax := zs[0], zs[len(zs)-1] + zrange := zmax - zmin + if zrange == 0 { + zrange = 1 + } + log.Printf("d=%+v zmin=%v zmax=%v zrange=%v", d.Points, zmin, zmax, zrange) + + ys := make([]opts.ScatterData, 0) + for i := range xs { + for j := range d.Points { + x := d.Points[j][0] + if x == xs[i] { + ys = append(ys, opts.ScatterData{ + Value: d.Points[j][1], + SymbolSize: d.Weight.Floor + int((float64(d.Weight.Range)*(d.Points[j][2]-zmin))/zrange), + }) + } + } + } + return d.Labels[0], xs, d.Labels[1], ys, nil +} + +func (d Data) weighXYasZ() Data { + xyz := map[float64]map[float64]float64{} + for i := range d.Points { + x := d.Points[i][0] + if _, ok := xyz[x]; !ok { + xyz[x] = map[float64]float64{} + } + y := d.Points[i][1] + z := 1.0 + if len(d.Labels) > 2 { + z = d.Points[i][2] + } + if _, ok := xyz[x][y]; !ok { + xyz[x][y] = 0.0 + } + xyz[x][y] += z + } + result := Data{ + Labels: d.Labels[:], + Points: [][]float64{}, + Weight: d.Weight, + } + for x := range xyz { + for y, z := range xyz[x] { + result.Points = append(result.Points, []float64{x, y, z}) + } + } + return result +} diff --git a/go.mod b/go.mod index 1f36aa3..b0899fb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module charts go 1.21.1 + +require github.com/go-echarts/go-echarts/v2 v2.3.3 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c9296fd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/go-echarts/go-echarts/v2 v2.3.3 h1:uImZAk6qLkC6F9ju6mZ5SPBqTyK8xjZKwSmwnCg4bxg= +github.com/go-echarts/go-echarts/v2 v2.3.3/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= diff --git a/main.go b/main.go index 9fec66d..11e5b55 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,12 @@ package main import ( "context" - "io" + "log" + "os" "os/signal" + + "github.com/go-echarts/go-echarts/v2/charts" + "github.com/go-echarts/go-echarts/v2/opts" ) func main() { @@ -15,5 +19,43 @@ func main() { } func run(ctx context.Context) error { - return io.EOF + config, err := newConfig() + if err != nil { + return err + } + log.Printf("%+v", config) + + x, xs, y, ys, err := config.Data.AsScatterData() + if err != nil { + return err + } + log.Printf("%s: %v", x, xs) + log.Printf("%s: %v", y, ys) + + scatter := charts.NewScatter() + scatter.SetGlobalOptions(charts.WithTitleOpts(opts.Title{ + Title: config.Data.Title, + Subtitle: config.Data.Subtitle, + })) + scatter.SetGlobalOptions(charts.WithLegendOpts(opts.Legend{Show: false})) + scatter.SetGlobalOptions(charts.WithXAxisOpts(opts.XAxis{ + Name: x, + NameLocation: "middle", + NameGap: config.Data.Weight.Floor + config.Data.Weight.Range, + })) + scatter.SetGlobalOptions(charts.WithYAxisOpts(opts.YAxis{ + Name: y, + NameLocation: "middle", + NameGap: config.Data.Weight.Floor + config.Data.Weight.Range, + })) + scatter.SetXAxis(xs). + AddSeries(y, ys) + f, _ := os.Create("/tmp/f") + defer f.Close() + if err := scatter.Render(f); err != nil { + return err + } + + log.Println("firefox /tmp/f") + return nil }