upgrade DATA_POINTS to require 3d array but now multi series is ok

main
Bel LaPointe 2024-02-21 13:19:01 -07:00
parent 83579698c0
commit 0dc484047c
2 changed files with 73 additions and 51 deletions

View File

@ -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{
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

16
main.go
View File

@ -33,9 +33,10 @@ 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 {
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
@ -49,6 +50,7 @@ func run(ctx context.Context) error {
maxY = y
}
}
}
if 0 <= minX && minX <= 1 {
minX = 0
@ -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
}