From 0f03bd2e49110dd7fd4f4e2315376010b905b89d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 11 Oct 2024 09:44:16 -0700 Subject: [PATCH] quickplot --- pkg/flatjson/flatjson.go | 190 ++++++++++++++++++++++++++++++++++++++ pkg/wshrpc/wshrpctypes.go | 31 +++++++ 2 files changed, 221 insertions(+) create mode 100644 pkg/flatjson/flatjson.go diff --git a/pkg/flatjson/flatjson.go b/pkg/flatjson/flatjson.go new file mode 100644 index 000000000..b0cfceaa2 --- /dev/null +++ b/pkg/flatjson/flatjson.go @@ -0,0 +1,190 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package flatjson + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/wavetermdev/waveterm/pkg/ijson" +) + +type FlatJsonVal struct { + Key string + Val any +} + +type FlatJson []FlatJsonVal + +func (fj FlatJson) Pack() ([]byte, error) { + var buf bytes.Buffer + for _, fjv := range fj { + buf.WriteString(fjv.Key) + buf.WriteString("=") + barr, err := json.Marshal(fjv.Val) + if err != nil { + return nil, fmt.Errorf("error marshalling key %s: %w", fjv.Key, err) + } + buf.Write(barr) + buf.WriteString("\n") + } + return buf.Bytes(), nil +} + +func Unpack(barr []byte) (FlatJson, error) { + rtn := make(FlatJson, 0) + // dont use split, keep a pos and scan for \n + pos := 0 + lineNum := 1 + for { + // find newline + newlinePos := bytes.IndexByte(barr[pos:], '\n') + if newlinePos == -1 { + return nil, fmt.Errorf("no newline found, possibly truncated") + } + line := barr[pos : pos+newlinePos] + eqPos := bytes.IndexByte(line, '=') + if eqPos == -1 { + return nil, fmt.Errorf("no = found on line %d", lineNum) + } + key := string(line[:eqPos]) + valStr := string(line[eqPos+1:]) + var val any + err := json.Unmarshal([]byte(valStr), &val) + if err != nil { + return nil, fmt.Errorf("error unmarshalling value on line %d: %w", lineNum, err) + } + rtn = append(rtn, FlatJsonVal{Key: key, Val: val}) + lineNum++ + pos += newlinePos + if pos == len(barr) { + break + } + } + return rtn, nil +} + +func keyToPath(key string) []any { + keyParts := strings.Split(key, ":") + path := make([]any, len(keyParts)) + for idx, keyPart := range keyParts { + ival, isInt := asInt(keyPart) + if isInt { + path[idx] = ival + } else { + path[idx] = keyPart + } + } + return path +} + +func asInt(s string) (int, bool) { + ival, err := strconv.Atoi(s) + if err != nil { + return 0, false + } + return ival, true +} + +func reverseAndRemoteDups(fj FlatJson) FlatJson { + seen := make(map[string]bool) + rtn := make(FlatJson, 0) + // iterate backwards + for i := len(fj) - 1; i >= 0; i-- { + if !seen[fj[i].Key] { + seen[fj[i].Key] = true + rtn = append(rtn, fj[i]) + } + } + return rtn +} + +func (fj FlatJson) ToJson(budget int) (map[string]any, map[string]error) { + rtn := make(map[string]any) + errs := make(map[string]error) + fj = reverseAndRemoteDups(fj) + opts := ijson.SetPathOpts{Budget: budget} + for _, fjv := range fj { + if fjv.Key == "" { + // must be a map + valMap, ok := fjv.Val.(map[string]any) + if !ok { + errs[""] = fmt.Errorf("bad key, does not produce a map") + continue + } + rtn = valMap + continue + } + path := keyToPath(fjv.Key) + newRtn, err := ijson.SetPath(rtn, path, fjv.Val, &opts) + if err != nil { + errs[fjv.Key] = err + } else { + newRtnMap, ok := newRtn.(map[string]any) + if !ok { + errs[fjv.Key] = fmt.Errorf("bad key, does not produce a map") + continue + } + rtn = newRtnMap + } + } + return rtn, errs +} + +func fromJsonArray(v []any, prefix string, fj *FlatJson) error { + for idx, val := range v { + newKey := fmt.Sprintf("%s:%d", prefix, idx) + switch val := val.(type) { + case map[string]any: + err := fromJsonMap(val, newKey, fj) + if err != nil { + return err + } + case []any: + err := fromJsonArray(val, newKey, fj) + if err != nil { + return err + } + default: + *fj = append(*fj, FlatJsonVal{newKey, fmt.Sprintf("%v", val)}) + } + } + return nil +} + +var validKeyRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`) + +func fromJsonMap(v map[string]any, prefix string, fj *FlatJson) error { + for key, val := range v { + if !validKeyRe.MatchString(key) { + return fmt.Errorf("in map at prefix %q, invalid key %q", prefix, key) + } + newKey := key + if prefix != "" { + newKey = prefix + ":" + key + } + switch val := val.(type) { + case map[string]any: + fromJsonMap(val, newKey, fj) + case []any: + fromJsonArray(val, newKey, fj) + default: + *fj = append(*fj, FlatJsonVal{newKey, fmt.Sprintf("%v", val)}) + } + } + return nil +} + +func FromJson(v map[string]any) (FlatJson, error) { + rtn := make(FlatJson, 0) + err := fromJsonMap(v, "", &rtn) + if err != nil { + return nil, err + } + return rtn +} diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index fbd06a27d..816fb744a 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -385,3 +385,34 @@ type WaveNotificationOptions struct { Body string `json:"body,omitempty"` Silent bool `json:"silent,omitempty"` } + +type PlotSeries struct { + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Key string `json:"key,omitempty"` + Color string `json:"color,omitempty"` + Fill string `json:"fill,omitempty"` + Width float64 `json:"width,omitempty"` + Marker string `json:"marker,omitempty"` + MarkerSize float64 `json:"markersize,omitempty"` +} + +type PlotAxis struct { + Title string `json:"title,omitempty"` + Color string `json:"color,omitempty"` + MinVal float64 `json:"minval,omitempty"` + MaxVal float64 `json:"maxval,omitempty"` + AxisType string `json:"axistype,omitempty"` + Formatter string `json:"formatter,omitempty"` +} + +type PlotOptions struct { + Title string `json:"title,omitempty"` + Bg string `json:"bg,omitempty"` + BgOpacity float64 `json:"bg:opacity,omitempty"` + BgBlendMode string `json:"bg:blendmode,omitempty"` + Series []PlotSeries `json:"series"` + XAxis *PlotAxis `json:"xaxis,omitempty"` + YAxis *PlotAxis `json:"yaxis,omitempty"` + MaxData int `json:"maxdata,omitempty"` +}