166 lines
4.4 KiB
Go
166 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
|
|
"slices"
|
|
|
|
"github.com/go-echarts/go-echarts/v2/opts"
|
|
)
|
|
|
|
type Config struct {
|
|
Data Data
|
|
Output url.URL
|
|
}
|
|
|
|
type Data struct {
|
|
Points [][]float64
|
|
Labels []string
|
|
Weight struct {
|
|
Floor int
|
|
Range int
|
|
}
|
|
Title string
|
|
Subtitle string
|
|
}
|
|
|
|
func newConfig() (Config, error) {
|
|
dataPoints := envOr("DATA_POINTS", `[[1, 1], [1, 2], [2, 1], [2, 1], [4, 3]]`)
|
|
dataLabels := envOr("DATA_LABELS", ``)
|
|
dataWeightFloor := envOr("DATA_WEIGHT_FLOOR", `12`)
|
|
dataWeightRange := envOr("DATA_WEIGHT_RANGE", `20`)
|
|
dataTitle := envOr("DATA_TITLE", ``)
|
|
dataSubtitle := envOr("DATA_SUBTITLE", ``)
|
|
output := envOr("OUTPUT", `file:///tmp/f`)
|
|
|
|
fs := flag.NewFlagSet("cmd", flag.ContinueOnError)
|
|
fs.StringVar(&dataPoints, "data-points", dataPoints, `JSON [[x, y, z, ...]]`)
|
|
fs.StringVar(&dataLabels, "data-labels", dataLabels, `(optional) JSON ["foo", "bar", "baz"]`)
|
|
fs.StringVar(&dataWeightFloor, "data-weight-floor", dataWeightFloor, `int min point size`)
|
|
fs.StringVar(&dataWeightRange, "data-weight-range", dataWeightRange, `int max point scale up`)
|
|
fs.StringVar(&dataTitle, "data-title", dataTitle, `chart title`)
|
|
fs.StringVar(&dataSubtitle, "data-subtitle", dataSubtitle, `chart subtitle`)
|
|
fs.StringVar(&output, "output", output, `scheme://location`)
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
var result Config
|
|
|
|
if err := json.Unmarshal([]byte(dataPoints), &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 dataLabels == "" {
|
|
} else if err := json.Unmarshal([]byte(dataLabels), &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 err := json.Unmarshal([]byte(dataWeightRange), &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 err := json.Unmarshal([]byte(dataWeightFloor), &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)
|
|
}
|
|
|
|
if u, err := url.Parse(output); err != nil {
|
|
return result, fmt.Errorf("failed to parse $OUTPUT (%s): %w", output, err)
|
|
} else {
|
|
result.Output = *u
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func envOr(k, v string) string {
|
|
if s := os.Getenv(k); s != "" {
|
|
return s
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (d Data) AsScatterData() (string, string, []opts.ScatterData, error) {
|
|
if !(2 <= len(d.Labels) && len(d.Labels) <= 3) {
|
|
return "", "", nil, fmt.Errorf("cannot map %d dimensions to [2,3]", len(d.Labels))
|
|
}
|
|
d = d.weighXYasZ()
|
|
|
|
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)
|
|
|
|
result := make([]opts.ScatterData, 0)
|
|
for i := range d.Points {
|
|
x := d.Points[i][0]
|
|
y := d.Points[i][1]
|
|
z := d.Points[i][2]
|
|
result = append(result, opts.ScatterData{
|
|
Value: []float64{x, y},
|
|
SymbolSize: d.Weight.Floor + int((float64(d.Weight.Range)*(z-zmin))/zrange),
|
|
})
|
|
}
|
|
return d.Labels[0], d.Labels[1], result, 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
|
|
}
|