Compare commits
5 Commits
3aa9f635be
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94fb1a97fe | ||
|
|
ac5f3bdc1c | ||
|
|
3ff2bf0c91 | ||
|
|
0dc484047c | ||
|
|
83579698c0 |
100
config.go
100
config.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -16,10 +17,13 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Data Data
|
Data Data
|
||||||
Output url.URL
|
Output url.URL
|
||||||
|
Graph struct {
|
||||||
|
Type string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Points [][]float64
|
Points [][][]float64
|
||||||
Labels []string
|
Labels []string
|
||||||
Weight struct {
|
Weight struct {
|
||||||
Floor int
|
Floor int
|
||||||
@@ -30,22 +34,24 @@ type Data struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newConfig() (Config, error) {
|
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", ``)
|
dataLabels := envOr("DATA_LABELS", ``)
|
||||||
dataWeightRange := envOr("DATA_WEIGHT_RANGE", `20`)
|
dataWeightRange := envOr("DATA_WEIGHT_RANGE", `20`)
|
||||||
dataWeightFloor := envOr("DATA_WEIGHT_FLOOR", `12`)
|
dataWeightFloor := envOr("DATA_WEIGHT_FLOOR", `12`)
|
||||||
dataTitle := envOr("DATA_TITLE", ``)
|
dataTitle := envOr("DATA_TITLE", ``)
|
||||||
dataSubtitle := envOr("DATA_SUBTITLE", ``)
|
dataSubtitle := envOr("DATA_SUBTITLE", ``)
|
||||||
output := envOr("OUTPUT", `file:///tmp/f`)
|
output := envOr("OUTPUT", `file:///tmp/f.html`)
|
||||||
|
graphType := envOr("GRAPH_TYPE", `scatter`)
|
||||||
|
|
||||||
fs := flag.NewFlagSet("cmd", flag.ContinueOnError)
|
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(&dataLabels, "data-labels", dataLabels, `(optional) JSON ["foo", "bar", "baz"]`)
|
||||||
fs.StringVar(&dataWeightFloor, "data-weight-floor", dataWeightFloor, `int min point size`)
|
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(&dataWeightRange, "data-weight-range", dataWeightRange, `int max point scale up`)
|
||||||
fs.StringVar(&dataTitle, "data-title", dataTitle, `chart title`)
|
fs.StringVar(&dataTitle, "data-title", dataTitle, `chart title`)
|
||||||
fs.StringVar(&dataSubtitle, "data-subtitle", dataSubtitle, `chart subtitle`)
|
fs.StringVar(&dataSubtitle, "data-subtitle", dataSubtitle, `chart subtitle`)
|
||||||
fs.StringVar(&output, "output", output, `scheme://location`)
|
fs.StringVar(&output, "output", output, `scheme://location`)
|
||||||
|
fs.StringVar(&graphType, "graph-type", graphType, `[scatter, line]`)
|
||||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
@@ -56,13 +62,16 @@ func newConfig() (Config, error) {
|
|||||||
return result, err
|
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 {
|
for i := range result.Data.Points {
|
||||||
if want := len(result.Data.Points[0]); want != len(result.Data.Points[i]) {
|
for j := range result.Data.Points[i] {
|
||||||
return result, fmt.Errorf("point [%d] expected %d dimensions but found %d", i, want, len(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 {
|
for i := range result.Data.Labels {
|
||||||
result.Data.Labels[i] = fmt.Sprintf("%c", byte(int('A')+i))
|
result.Data.Labels[i] = fmt.Sprintf("%c", byte(int('A')+i))
|
||||||
}
|
}
|
||||||
@@ -70,8 +79,8 @@ func newConfig() (Config, error) {
|
|||||||
if dataLabels == "" {
|
if dataLabels == "" {
|
||||||
} else if err := json.Unmarshal([]byte(dataLabels), &result.Data.Labels); err != nil {
|
} else if err := json.Unmarshal([]byte(dataLabels), &result.Data.Labels); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
} else if len(result.Data.Labels) < len(result.Data.Points[0]) {
|
} else if len(result.Data.Labels) < expect {
|
||||||
return result, fmt.Errorf("expected at least %d labels but got %d", len(result.Data.Points[0]), len(result.Data.Labels))
|
return result, fmt.Errorf("expected at least %d labels but got %d", expect, len(result.Data.Labels))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +98,7 @@ func newConfig() (Config, error) {
|
|||||||
|
|
||||||
result.Data.Title = dataTitle
|
result.Data.Title = dataTitle
|
||||||
result.Data.Subtitle = dataSubtitle
|
result.Data.Subtitle = dataSubtitle
|
||||||
|
result.Graph.Type = graphType
|
||||||
|
|
||||||
if u, err := url.Parse(output); err != nil {
|
if u, err := url.Parse(output); err != nil {
|
||||||
return result, fmt.Errorf("failed to parse $OUTPUT (%s): %w", output, err)
|
return result, fmt.Errorf("failed to parse $OUTPUT (%s): %w", output, err)
|
||||||
@@ -101,20 +111,42 @@ func newConfig() (Config, error) {
|
|||||||
|
|
||||||
func envOr(k, v string) string {
|
func envOr(k, v string) string {
|
||||||
if s := os.Getenv(k); s != "" {
|
if s := os.Getenv(k); s != "" {
|
||||||
|
log.Printf("found $%s = %s", k, s)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Data) AsScatterData() (string, string, []opts.ScatterData, error) {
|
func scatterAsLineData(data [][]opts.ScatterData) [][]opts.LineData {
|
||||||
|
result := make([][]opts.LineData, len(data))
|
||||||
|
for i := range data {
|
||||||
|
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) {
|
||||||
if !(2 <= len(d.Labels) && len(d.Labels) <= 3) {
|
if !(2 <= len(d.Labels) && len(d.Labels) <= 3) {
|
||||||
return "", "", nil, fmt.Errorf("cannot map %d dimensions to [2,3]", len(d.Labels))
|
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)
|
zs := make([]float64, 0)
|
||||||
for i := range d.Points {
|
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)
|
slices.Sort(zs)
|
||||||
zmin, zmax := zs[0], zs[len(zs)-1]
|
zmin, zmax := zs[0], zs[len(zs)-1]
|
||||||
@@ -124,42 +156,48 @@ func (d Data) AsScatterData() (string, string, []opts.ScatterData, error) {
|
|||||||
}
|
}
|
||||||
log.Printf("d=%+v zmin=%v zmax=%v zrange=%v", d.Points, zmin, zmax, zrange)
|
log.Printf("d=%+v zmin=%v zmax=%v zrange=%v", d.Points, zmin, zmax, zrange)
|
||||||
|
|
||||||
result := make([]opts.ScatterData, 0)
|
result := make([][]opts.ScatterData, 0)
|
||||||
for i := range d.Points {
|
for _, series := range d.Points {
|
||||||
x := d.Points[i][0]
|
subresult := make([]opts.ScatterData, 0)
|
||||||
y := d.Points[i][1]
|
for i := range series {
|
||||||
z := d.Points[i][2]
|
x := series[i][0]
|
||||||
result = append(result, opts.ScatterData{
|
y := series[i][1]
|
||||||
Value: []float64{x, y},
|
z := series[i][2]
|
||||||
SymbolSize: d.Weight.Floor + int((float64(d.Weight.Range)*(z-zmin))/zrange),
|
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
|
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{}
|
xyz := map[float64]map[float64]float64{}
|
||||||
for i := range d.Points {
|
for i := range input {
|
||||||
x := d.Points[i][0]
|
x := input[i][0]
|
||||||
if _, ok := xyz[x]; !ok {
|
if _, ok := xyz[x]; !ok {
|
||||||
xyz[x] = map[float64]float64{}
|
xyz[x] = map[float64]float64{}
|
||||||
}
|
}
|
||||||
y := d.Points[i][1]
|
y := input[i][1]
|
||||||
z := 1.0
|
z := 1.0
|
||||||
if len(d.Labels) > 2 {
|
if len(input[i]) > 2 {
|
||||||
z = d.Points[i][2]
|
z = input[i][2]
|
||||||
}
|
}
|
||||||
if _, ok := xyz[x][y]; !ok {
|
if _, ok := xyz[x][y]; !ok {
|
||||||
xyz[x][y] = 0.0
|
xyz[x][y] = 0.0
|
||||||
}
|
}
|
||||||
xyz[x][y] += z
|
xyz[x][y] += z
|
||||||
}
|
}
|
||||||
result := d
|
result := [][]float64{}
|
||||||
result.Points = [][]float64{}
|
|
||||||
for x := range xyz {
|
for x := range xyz {
|
||||||
for y, z := range xyz[x] {
|
for y, z := range xyz[x] {
|
||||||
result.Points = append(result.Points, []float64{x, y, z})
|
result = append(result, []float64{x, y, z})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i][0] < result[j][0]
|
||||||
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
60
main.go
60
main.go
@@ -33,20 +33,22 @@ func run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
log.Printf("%s: (%s, %s) = %v", config.Data.Title, x, y, data)
|
log.Printf("%s: (%s, %s) = %v", config.Data.Title, x, y, data)
|
||||||
|
|
||||||
minX, maxX := data[0].Value.([]float64)[0], data[0].Value.([]float64)[0]
|
minX, maxX := data[0][0].Value.([]float64)[0], data[0][0].Value.([]float64)[0]
|
||||||
minY, maxY := data[0].Value.([]float64)[1], data[0].Value.([]float64)[1]
|
minY, maxY := data[0][0].Value.([]float64)[1], data[0][0].Value.([]float64)[1]
|
||||||
for _, datum := range data {
|
for _, series := range data {
|
||||||
x := datum.Value.([]float64)[0]
|
for _, datum := range series {
|
||||||
if x < minX {
|
x := datum.Value.([]float64)[0]
|
||||||
minX = x
|
if x < minX {
|
||||||
} else if x > maxX {
|
minX = x
|
||||||
maxX = x
|
} else if x > maxX {
|
||||||
}
|
maxX = x
|
||||||
y := datum.Value.([]float64)[1]
|
}
|
||||||
if y < minY {
|
y := datum.Value.([]float64)[1]
|
||||||
minY = y
|
if y < minY {
|
||||||
} else if y > maxY {
|
minY = y
|
||||||
maxY = y
|
} else if y > maxY {
|
||||||
|
maxY = y
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +66,7 @@ func run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
log.Printf("x=[%v, %v] y=[%v, %v]", minX, maxX, minY, maxY)
|
log.Printf("x=[%v, %v] y=[%v, %v]", minX, maxX, minY, maxY)
|
||||||
|
|
||||||
scatter := charts.NewScatter()
|
globalOpts := []charts.GlobalOpts{
|
||||||
scatter.SetGlobalOptions(
|
|
||||||
charts.WithTitleOpts(opts.Title{
|
charts.WithTitleOpts(opts.Title{
|
||||||
Title: config.Data.Title,
|
Title: config.Data.Title,
|
||||||
Subtitle: config.Data.Subtitle,
|
Subtitle: config.Data.Subtitle,
|
||||||
@@ -74,7 +75,7 @@ func run(ctx context.Context) error {
|
|||||||
Show: true,
|
Show: true,
|
||||||
Formatter: `{c}`,
|
Formatter: `{c}`,
|
||||||
}),
|
}),
|
||||||
charts.WithLegendOpts(opts.Legend{Show: false}),
|
charts.WithLegendOpts(opts.Legend{Show: true}),
|
||||||
charts.WithXAxisOpts(opts.XAxis{
|
charts.WithXAxisOpts(opts.XAxis{
|
||||||
Name: x,
|
Name: x,
|
||||||
NameLocation: "middle",
|
NameLocation: "middle",
|
||||||
@@ -89,12 +90,29 @@ func run(ctx context.Context) error {
|
|||||||
Min: minY,
|
Min: minY,
|
||||||
Max: maxY,
|
Max: maxY,
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
scatter.AddSeries(y, data)
|
|
||||||
|
|
||||||
buff := bytes.NewBuffer(nil)
|
buff := bytes.NewBuffer(nil)
|
||||||
if err := scatter.Render(buff); err != nil {
|
|
||||||
return err
|
switch config.Graph.Type {
|
||||||
|
case "line":
|
||||||
|
line := charts.NewLine()
|
||||||
|
line.SetGlobalOptions(globalOpts...)
|
||||||
|
for i, series := range scatterAsLineData(data) {
|
||||||
|
line.AddSeries(fmt.Sprintf("%s[%d]", y, i), series)
|
||||||
|
}
|
||||||
|
if err := line.Render(buff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "scatter":
|
||||||
|
scatter := charts.NewScatter()
|
||||||
|
scatter.SetGlobalOptions(globalOpts...)
|
||||||
|
for i, series := range data {
|
||||||
|
scatter.AddSeries(fmt.Sprintf("%s[%d]", y, i), series)
|
||||||
|
}
|
||||||
|
if err := scatter.Render(buff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch config.Output.Scheme {
|
switch config.Output.Scheme {
|
||||||
|
|||||||
Reference in New Issue
Block a user