diff --git a/config.go b/config.go index 508c5d4..54e8178 100644 --- a/config.go +++ b/config.go @@ -22,7 +22,7 @@ type Config struct { } type Data struct { - Points [][]float64 + Points [][][]float64 Labels []string Weight struct { Floor int @@ -33,7 +33,7 @@ type Data struct { } func newConfig() (Config, error) { - dataPoints := envOr("DATA_POINTS", `[[11, 21], [11, 22], [12, 21], [12, 21], [14, 23]]`) + dataPoints := envOr("DATA_POINTS", `[[[11, 21], [12, 22]], [[12, 21], [14, 23]]]`) dataLabels := envOr("DATA_LABELS", ``) dataWeightRange := envOr("DATA_WEIGHT_RANGE", `20`) dataWeightFloor := envOr("DATA_WEIGHT_FLOOR", `12`) @@ -43,7 +43,7 @@ func newConfig() (Config, error) { graphType := envOr("GRAPH_TYPE", `scatter`) fs := flag.NewFlagSet("cmd", flag.ContinueOnError) - fs.StringVar(&dataPoints, "data-points", dataPoints, `JSON [[x, y, z, ...]]`) + 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`) @@ -61,13 +61,16 @@ func newConfig() (Config, error) { return result, err } - if len(result.Data.Points) > 0 { + if len(result.Data.Points) > 0 && len(result.Data.Points[0]) > 0 { + expect := len(result.Data.Points[0][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])) + for j := range result.Data.Points[i] { + if got := len(result.Data.Points[i][j]); expect != got { + return result, fmt.Errorf("point [%d] expected %d dimensions but found %d", i, expect, got) + } } } - result.Data.Labels = make([]string, len(result.Data.Points[0])) + result.Data.Labels = make([]string, expect) for i := range result.Data.Labels { result.Data.Labels[i] = fmt.Sprintf("%c", byte(int('A')+i)) } @@ -75,8 +78,8 @@ func newConfig() (Config, error) { 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)) + } else if len(result.Data.Labels) < expect { + return result, fmt.Errorf("expected at least %d labels but got %d", expect, len(result.Data.Labels)) } } @@ -112,26 +115,36 @@ func envOr(k, v string) string { return v } -func scatterAsLineData(data []opts.ScatterData) []opts.LineData { - result := make([]opts.LineData, len(data)) +func scatterAsLineData(data [][]opts.ScatterData) [][]opts.LineData { + result := make([][]opts.LineData, len(data)) for i := range data { - result[i] = opts.LineData{ - Value: data[i].Value, - SymbolSize: data[i].SymbolSize, + result[i] = make([]opts.LineData, len(data[i])) + for j := range data[i] { + result[i][j] = opts.LineData{ + Value: data[i][j].Value, + SymbolSize: data[i][j].SymbolSize, + } } } return result } -func (d Data) AsScatterData() (string, string, []opts.ScatterData, error) { +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() + d2 := d + d2.Points = make([][][]float64, len(d.Points)) + for i := range d.Points { + d2.Points[i] = weighXYasZ(d.Points[i]) + } + d = d2 zs := make([]float64, 0) for i := range d.Points { - zs = append(zs, d.Points[i][2]) + for j := range d.Points[i] { + zs = append(zs, d.Points[i][j][2]) + } } slices.Sort(zs) zmin, zmax := zs[0], zs[len(zs)-1] @@ -141,41 +154,44 @@ func (d Data) AsScatterData() (string, string, []opts.ScatterData, error) { } 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), - }) + result := make([][]opts.ScatterData, 0) + for _, series := range d.Points { + subresult := make([]opts.ScatterData, 0) + for i := range series { + x := series[i][0] + y := series[i][1] + z := series[i][2] + subresult = append(subresult, opts.ScatterData{ + Value: []float64{x, y}, + SymbolSize: d.Weight.Floor + int((float64(d.Weight.Range)*(z-zmin))/zrange), + }) + } + result = append(result, subresult) } return d.Labels[0], d.Labels[1], result, nil } -func (d Data) weighXYasZ() Data { +func weighXYasZ(input [][]float64) [][]float64 { xyz := map[float64]map[float64]float64{} - for i := range d.Points { - x := d.Points[i][0] + for i := range input { + x := input[i][0] if _, ok := xyz[x]; !ok { xyz[x] = map[float64]float64{} } - y := d.Points[i][1] + y := input[i][1] z := 1.0 - if len(d.Labels) > 2 { - z = d.Points[i][2] + if len(input[i]) > 2 { + z = input[i][2] } if _, ok := xyz[x][y]; !ok { xyz[x][y] = 0.0 } xyz[x][y] += z } - result := d - result.Points = [][]float64{} + result := [][]float64{} for x := range xyz { for y, z := range xyz[x] { - result.Points = append(result.Points, []float64{x, y, z}) + result = append(result, []float64{x, y, z}) } } return result diff --git a/main.go b/main.go index b902281..f731a52 100644 --- a/main.go +++ b/main.go @@ -33,20 +33,22 @@ func run(ctx context.Context) error { } log.Printf("%s: (%s, %s) = %v", config.Data.Title, x, y, data) - minX, maxX := data[0].Value.([]float64)[0], data[0].Value.([]float64)[0] - minY, maxY := data[0].Value.([]float64)[1], data[0].Value.([]float64)[1] - for _, datum := range data { - x := datum.Value.([]float64)[0] - if x < minX { - minX = x - } else if x > maxX { - maxX = x - } - y := datum.Value.([]float64)[1] - if y < minY { - minY = y - } else if y > maxY { - maxY = y + minX, maxX := data[0][0].Value.([]float64)[0], data[0][0].Value.([]float64)[0] + minY, maxY := data[0][0].Value.([]float64)[1], data[0][0].Value.([]float64)[1] + for _, series := range data { + for _, datum := range series { + x := datum.Value.([]float64)[0] + if x < minX { + minX = x + } else if x > maxX { + maxX = x + } + y := datum.Value.([]float64)[1] + if y < minY { + minY = y + } else if y > maxY { + maxY = y + } } } @@ -96,14 +98,18 @@ func run(ctx context.Context) error { case "line": line := charts.NewLine() line.SetGlobalOptions(globalOpts...) - line.AddSeries(y, scatterAsLineData(data)) + for _, series := range scatterAsLineData(data) { + line.AddSeries(y, series) + } if err := line.Render(buff); err != nil { return err } case "scatter": scatter := charts.NewScatter() scatter.SetGlobalOptions(globalOpts...) - scatter.AddSeries(y, data) + for _, series := range data { + scatter.AddSeries(y, series) + } if err := scatter.Render(buff); err != nil { return err }