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 }