Compare commits
179 Commits
45c9f51dc6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
045ab30dfa | ||
|
|
b1cf639f39 | ||
|
|
cc546d9b2d | ||
|
|
0066a267a6 | ||
|
|
f58053ebe9 | ||
|
|
b30b1811ea | ||
|
|
3fb1ee4ea3 | ||
|
|
7a790fa31e | ||
|
|
774de58bf7 | ||
|
|
6aa549cb02 | ||
|
|
fe7c3a9682 | ||
|
|
36dcf70dbe | ||
|
|
989bc3c2ff | ||
|
|
f22d7d958b | ||
|
|
6a4e10ee2b | ||
|
|
09da985455 | ||
|
|
f52832ee82 | ||
|
|
0cafba0571 | ||
|
|
1a397dbf45 | ||
|
|
b10283d752 | ||
|
|
284613b5bc | ||
|
|
929d15c5b7 | ||
|
|
5e61378d63 | ||
|
|
c948a32458 | ||
|
|
35e2e40ce6 | ||
|
|
847cd736b6 | ||
|
|
9cf8cb0736 | ||
|
|
4997264f4c | ||
|
|
f69a850bd8 | ||
|
|
5a3d5e5610 | ||
|
|
c38e8529af | ||
|
|
7a946b7604 | ||
|
|
e0fa44eef7 | ||
|
|
6440b07d14 | ||
|
|
bae33f8c60 | ||
|
|
daa446cc02 | ||
|
|
daac3907f4 | ||
|
|
83305227db | ||
|
|
f5d82fc6aa | ||
|
|
e581b7835c | ||
|
|
19f4b614d3 | ||
|
|
58462fb5a4 | ||
|
|
2f8dba4e23 | ||
|
|
0d91cd63db | ||
|
|
9681050a46 | ||
|
|
0b4d78796e | ||
|
|
1805a087e0 | ||
|
|
fabd26e8e9 | ||
|
|
9646356d7f | ||
|
|
84bbda1031 | ||
|
|
9b4accafe3 | ||
|
|
9352ca82de | ||
|
|
058874b566 | ||
|
|
8ade122378 | ||
|
|
fe98fa67f3 | ||
|
|
5cc9b141b9 | ||
|
|
266af7353a | ||
|
|
dd20f066a3 | ||
|
|
697fc16b52 | ||
|
|
8621619433 | ||
|
|
768ce8e92e | ||
|
|
757afa603e | ||
|
|
d093db1a2b | ||
|
|
30a0414dcd | ||
|
|
1f9919a172 | ||
|
|
fb1ddc72c3 | ||
|
|
0eaeeec359 | ||
|
|
188b31aa0c | ||
|
|
af1b23731a | ||
|
|
fcd7dd208c | ||
|
|
9bbdeeae9c | ||
|
|
4d01a5e481 | ||
|
|
5cc7b7ec55 | ||
|
|
ed0ce83556 | ||
|
|
a1631019f0 | ||
|
|
d7edc1ff1d | ||
|
|
012024ac66 | ||
|
|
a5b1e653ae | ||
|
|
37f6f47375 | ||
|
|
1a517eb8f2 | ||
|
|
c3f7800dec | ||
|
|
e25d52b141 | ||
|
|
7a828f8463 | ||
|
|
117c300533 | ||
|
|
4d484b8aa4 | ||
|
|
4831914251 | ||
|
|
eb3af4b54f | ||
|
|
849a8696f5 | ||
|
|
1a2c88687f | ||
|
|
889dc48d6c | ||
|
|
881162357b | ||
|
|
62e65c47df | ||
|
|
8ab5a0edf5 | ||
|
|
d1b3da3fa3 | ||
|
|
e4d60a9e73 | ||
|
|
23bd6e29c9 | ||
|
|
5587429922 | ||
|
|
e3c1af4f5d | ||
|
|
ce19c74cb0 | ||
|
|
16bbeb8749 | ||
|
|
3211dbd73f | ||
|
|
6df7171057 | ||
|
|
6be00d6423 | ||
|
|
b8c78fe55e | ||
|
|
18a19e52f5 | ||
|
|
bb11214180 | ||
|
|
6c66b6debb | ||
|
|
518909b7f7 | ||
|
|
d69de4da11 | ||
|
|
b8323f731f | ||
|
|
912c3a2659 | ||
|
|
6026d54062 | ||
|
|
b6cf4656c8 | ||
|
|
45c4d7b684 | ||
|
|
59205fba4c | ||
|
|
1d18cb50c5 | ||
|
|
bd85dcc86a | ||
|
|
2ce78d2b42 | ||
|
|
8886442e89 | ||
|
|
1e50274681 | ||
|
|
80ca2ea61e | ||
|
|
e8f42c7a5d | ||
|
|
ae44534fc3 | ||
|
|
cd36e12b68 | ||
|
|
9a0eb89f54 | ||
|
|
fe8d80ffc8 | ||
|
|
f41386b3b4 | ||
|
|
2141e030ef | ||
|
|
ee6ce95c0a | ||
|
|
6d174b031b | ||
|
|
b6c6e83443 | ||
|
|
ddc95c9054 | ||
|
|
666965042a | ||
|
|
f6b8b92bff | ||
|
|
3e001b8ddd | ||
|
|
682dc9880e | ||
|
|
4e72d25dce | ||
|
|
0d456f4d73 | ||
|
|
9b1ad420fe | ||
|
|
e633ee07ab | ||
|
|
bdcd9fe26a | ||
|
|
2051fa1cdc | ||
|
|
f7098def0e | ||
|
|
8fd1a4fedb | ||
|
|
566b3ad571 | ||
|
|
f078a1e6a1 | ||
|
|
8ae90619d2 | ||
|
|
89a62ec30c | ||
|
|
83c8f63fe3 | ||
|
|
aaeb3f1930 | ||
|
|
6604c26204 | ||
|
|
ba6d6483e0 | ||
|
|
58cafcfaa3 | ||
|
|
b006333035 | ||
|
|
b2d233bb82 | ||
|
|
253bff9f1d | ||
|
|
09f3baee4d | ||
|
|
4de3b4a822 | ||
|
|
2f21a23a33 | ||
|
|
227de17951 | ||
|
|
a623dcc195 | ||
|
|
569e50b162 | ||
|
|
622c173264 | ||
|
|
ce5bf71f6b | ||
|
|
c75bd74823 | ||
|
|
93e5a77e04 | ||
|
|
cdad3092e1 | ||
|
|
432b2df131 | ||
|
|
cb6e23a498 | ||
|
|
f7a9bd7bb7 | ||
|
|
476c44f5cd | ||
|
|
4b2af6b85e | ||
|
|
0baf3ccc8f | ||
|
|
8b7d0e84c0 | ||
|
|
4ec42ad877 | ||
|
|
fbbcaa7f78 | ||
|
|
f0411fe6db | ||
|
|
910aa78faf | ||
|
|
8f91316d27 |
@@ -3,14 +3,19 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- 'cmd/**'
|
||||||
|
- 'src/**'
|
||||||
|
- 'build/**'
|
||||||
|
- '.gitea/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cicd:
|
ci:
|
||||||
name: cicd
|
name: ci
|
||||||
runs-on: dind
|
runs-on: dind
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: cicd
|
- name: ci
|
||||||
run: |
|
run: |
|
||||||
bash ./build/build.sh
|
bash ./build/build.sh
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.21.3-alpine3.18 as builder
|
FROM golang:1.23.9-alpine3.21 as builder
|
||||||
|
|
||||||
COPY ./ /go/src/ana-ledger
|
COPY ./ /go/src/ana-ledger
|
||||||
WORKDIR /go/src/ana-ledger
|
WORKDIR /go/src/ana-ledger
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
img=registry-app.inhome.blapointe.com:5001/bel/ana-ledger:latest
|
img=registry-app.inhome.blapointe.com:5001/bel/ana-ledger:${1:-$(date +%Y%m%d%H%M)}
|
||||||
cd "$(dirname "$(dirname "$(realpath "$BASH_SOURCE")")")"
|
cd "$(dirname "$(dirname "$(realpath "$BASH_SOURCE")")")"
|
||||||
was=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
|
was=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
|
||||||
docker build -f ./build/Dockerfile -t $img .
|
docker build -f ./build/Dockerfile -t $img .
|
||||||
now=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
|
now=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
|
||||||
if [ -n "$was" ] && [ "$was" != "$now" ] && docker inspect "$was" &> /dev/null; then
|
|
||||||
docker rmi "$was"
|
|
||||||
fi
|
|
||||||
docker push $img
|
docker push $img
|
||||||
|
if [ -n "$was" ] && [ "$was" != "$now" ] && docker inspect "$was" &> /dev/null; then
|
||||||
|
docker rmi "$was" || true
|
||||||
|
docker rmi $(docker ps | grep ${img%:*} | grep '<none>' | awk '{print $3}') || true
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../../../../../Sync/Core/ledger/bpi.dat
|
|
||||||
1
cmd/cli/bpi.dat
Symbolic link
1
cmd/cli/bpi.dat
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../Sync/Core/ledger/bpi.dat
|
||||||
21
cmd/cli/config.go
Normal file
21
cmd/cli/config.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Files FileList
|
||||||
|
BPI string
|
||||||
|
CPI string
|
||||||
|
CPIYear int
|
||||||
|
Query struct {
|
||||||
|
Period Period
|
||||||
|
Sort string
|
||||||
|
NoRounding bool
|
||||||
|
Depth int
|
||||||
|
With string
|
||||||
|
NoExchanging bool
|
||||||
|
Normalize bool
|
||||||
|
USDOnly bool
|
||||||
|
}
|
||||||
|
Compact bool
|
||||||
|
GroupDate string
|
||||||
|
NoPercent bool
|
||||||
|
}
|
||||||
1
cmd/cli/cpi.dat
Symbolic link
1
cmd/cli/cpi.dat
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../Sync/Core/ledger/cpi.dat
|
||||||
90
cmd/cli/flag.go
Normal file
90
cmd/cli/flag.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlagStringArray []string
|
||||||
|
|
||||||
|
func (array *FlagStringArray) String() string {
|
||||||
|
return strings.Join(*array, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (array *FlagStringArray) Set(s string) error {
|
||||||
|
*array = append(*array, s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileList FlagStringArray
|
||||||
|
|
||||||
|
func (fileList FileList) Strings() []string {
|
||||||
|
return ([]string)(fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileList *FileList) String() string {
|
||||||
|
return (*FlagStringArray)(fileList).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fileList *FileList) Set(s string) error {
|
||||||
|
if _, err := os.Stat(s); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("file does not exist: %s", s)
|
||||||
|
}
|
||||||
|
return (*FlagStringArray)(fileList).Set(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Period struct {
|
||||||
|
Start time.Time
|
||||||
|
Stop time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (period Period) Empty() bool {
|
||||||
|
return period.Stop.Sub(period.Start) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (period *Period) String() string {
|
||||||
|
return fmt.Sprintf("%s..%s", period.Start, period.Stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (period *Period) Set(s string) error {
|
||||||
|
ss := strings.Split(s, "..")
|
||||||
|
if err := period.setStartStop(ss[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ss) != 2 {
|
||||||
|
} else if err := period.setStop(ss[1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (period *Period) setStartStop(s string) error {
|
||||||
|
stop, err := period.setT(s, &period.Start)
|
||||||
|
period.Stop = stop
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (period *Period) setStop(s string) error {
|
||||||
|
_, err := period.setT(s, &period.Stop)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Period) setT(s string, t *time.Time) (time.Time, error) {
|
||||||
|
if result, err := time.Parse("2006", s); err == nil {
|
||||||
|
*t = result
|
||||||
|
return result.AddDate(1, 0, 0).Add(-1 * time.Minute), nil
|
||||||
|
}
|
||||||
|
if result, err := time.Parse("2006-01", s); err == nil {
|
||||||
|
*t = result
|
||||||
|
return result.AddDate(0, 1, 0).Add(-1 * time.Minute), nil
|
||||||
|
}
|
||||||
|
if result, err := time.Parse("2006-01-02", s); err == nil {
|
||||||
|
*t = result
|
||||||
|
return result.AddDate(0, 0, 1).Add(-1 * time.Minute), nil
|
||||||
|
}
|
||||||
|
return time.Time{}, fmt.Errorf("unimplemented format: %s", s)
|
||||||
|
}
|
||||||
443
cmd/cli/main.go
Normal file
443
cmd/cli/main.go
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/cache"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
|
||||||
|
"github.com/guptarohit/asciigraph"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||||
|
fs.Var(&config.Files, "f", "paths to files")
|
||||||
|
fs.Var(&config.Query.Period, "period", "period can be YYYY, YYYY-mm, YYYY-mm-dd, x..y")
|
||||||
|
fs.StringVar(&config.Query.Sort, "S", "", "sort ie date")
|
||||||
|
fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
|
||||||
|
fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
|
||||||
|
fs.StringVar(&config.Query.With, "w", "", "regexp for transactions")
|
||||||
|
fs.IntVar(&config.Query.Depth, "depth", 0, "depth grouping")
|
||||||
|
fs.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer")
|
||||||
|
fs.BoolVar(&config.Query.USDOnly, "usd", false, "filter to usd")
|
||||||
|
fs.BoolVar(&config.Query.NoExchanging, "no-exchanging", true, "omit currency exchanges")
|
||||||
|
fs.StringVar(&config.BPI, "bpi", "", "path to bpi")
|
||||||
|
fs.StringVar(&config.CPI, "cpi", "", "path to cpi")
|
||||||
|
fs.StringVar(&config.GroupDate, "group-date", "^....-..-..", "date grouping")
|
||||||
|
fs.IntVar(&config.CPIYear, "cpiy", 0, "use cpi to convert usd to this year's value")
|
||||||
|
fs.BoolVar(&config.NoPercent, "no-percent", false, "compute percent")
|
||||||
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := config.Files.Strings()
|
||||||
|
if len(files) == 0 {
|
||||||
|
panic("must specify at least one file")
|
||||||
|
}
|
||||||
|
ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
positional := fs.Args()
|
||||||
|
if len(positional) == 0 || len(positional[0]) < 3 {
|
||||||
|
panic("positional arguments required, ie bal|reg PATTERN MATCHING")
|
||||||
|
}
|
||||||
|
cmd := positional[0]
|
||||||
|
|
||||||
|
q, err := BuildQuery(config, positional[1:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deltas, err := ledgerFiles.Deltas()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if period := config.Query.Period; !period.Empty() {
|
||||||
|
after := period.Start.Format("2006-01-02")
|
||||||
|
before := period.Stop.Format("2006-01-02")
|
||||||
|
deltas = deltas.Like(
|
||||||
|
ledger.LikeAfter(after),
|
||||||
|
ledger.LikeBefore(before),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := ".*"
|
||||||
|
if depth := config.Query.Depth; depth > 0 {
|
||||||
|
patterns := []string{}
|
||||||
|
pattern = ""
|
||||||
|
for i := 0; i < depth; i++ {
|
||||||
|
pattern += "[^:]*:"
|
||||||
|
patterns = append([]string{strings.Trim(pattern, ":")}, patterns...)
|
||||||
|
}
|
||||||
|
pattern = strings.Join(patterns, "|")
|
||||||
|
}
|
||||||
|
group := ledger.GroupName(pattern)
|
||||||
|
|
||||||
|
bpis := make(ledger.BPIs)
|
||||||
|
if config.BPI != "" {
|
||||||
|
b, err := ledger.NewBPIs(config.BPI)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bpis = b
|
||||||
|
}
|
||||||
|
|
||||||
|
cpiNormalizer := ana.NewNormalizer()
|
||||||
|
if config.CPI != "" && config.CPIYear > 0 {
|
||||||
|
c, err := ledger.NewBPIs(config.CPI)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cpi := c["CPI"]
|
||||||
|
cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
|
||||||
|
if cpiy == nil {
|
||||||
|
panic(fmt.Errorf("no cpi for year %d", config.CPIYear))
|
||||||
|
}
|
||||||
|
|
||||||
|
for date, value := range cpi {
|
||||||
|
cpiNormalizer = cpiNormalizer.With(".*", date, value/(*cpiy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Query.Normalize {
|
||||||
|
deltas = ana.NewDefaultNormalizer().Normalize(deltas)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Query.With != "" {
|
||||||
|
deltas = deltas.Like(ledger.LikeWith(config.Query.With))
|
||||||
|
}
|
||||||
|
|
||||||
|
w := bufio.NewWriter(os.Stdout)
|
||||||
|
defer w.Flush()
|
||||||
|
|
||||||
|
maxAccW := 0
|
||||||
|
for _, delta := range deltas.Like(q).Group(group) {
|
||||||
|
if accW := len(delta.Name); accW > maxAccW {
|
||||||
|
maxAccW = accW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd[:3] {
|
||||||
|
case "bal": // balances
|
||||||
|
balances := deltas.Group(ledger.GroupDate(config.GroupDate)).Balances().
|
||||||
|
WithBPIs(bpis).
|
||||||
|
KindaLike(q).
|
||||||
|
KindaGroup(group).
|
||||||
|
Nonzero().
|
||||||
|
Normalize(cpiNormalizer, "9")
|
||||||
|
|
||||||
|
cumulatives := make(ledger.Balances)
|
||||||
|
cumulativesFormat := "%s%.2f"
|
||||||
|
if !config.NoPercent {
|
||||||
|
var sum float64
|
||||||
|
for key := range balances {
|
||||||
|
if _, ok := cumulatives[key]; !ok {
|
||||||
|
cumulatives[key] = make(ledger.Balance)
|
||||||
|
}
|
||||||
|
for currency, val := range balances[key] {
|
||||||
|
if currency == ledger.USD {
|
||||||
|
cumulatives[key][currency] = val
|
||||||
|
sum += val
|
||||||
|
} else {
|
||||||
|
cumulatives[key][currency] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range cumulatives {
|
||||||
|
cumulatives[key][ledger.USD] = 100 * cumulatives[key][ledger.USD] / sum
|
||||||
|
}
|
||||||
|
cumulativesFormat = "%.0f%%"
|
||||||
|
}
|
||||||
|
|
||||||
|
FPrintBalances(w, "", balances, cumulatives, config.Query.USDOnly, config.Query.Normalize, time.Now().Format("2006-01-02"), false, maxAccW, cumulativesFormat)
|
||||||
|
case "gra": // graph
|
||||||
|
dateGrouping := "^[0-9]{4}-[0-9]{2}"
|
||||||
|
if period := config.Query.Period; !period.Empty() {
|
||||||
|
day := time.Hour * 24
|
||||||
|
year := day * 365
|
||||||
|
r := period.Stop.Sub(period.Start)
|
||||||
|
if r > 10*year {
|
||||||
|
dateGrouping = "^[0-9]{4}"
|
||||||
|
} else if r > 5*year {
|
||||||
|
} else if r > year {
|
||||||
|
dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]"
|
||||||
|
} else {
|
||||||
|
dateGrouping = "^[0-9]{4}-[0-9]{2}-[0-9]{2}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deltas = deltas.Group(ledger.GroupDate(dateGrouping))
|
||||||
|
|
||||||
|
transactions := deltas.Transactions()
|
||||||
|
cumulative := make(ledger.Balances)
|
||||||
|
data := map[string][]float64{}
|
||||||
|
pushData := func() {
|
||||||
|
soFar := 0
|
||||||
|
for _, v := range data {
|
||||||
|
soFar = len(v)
|
||||||
|
}
|
||||||
|
for k, balance := range cumulative {
|
||||||
|
for curr, v := range balance {
|
||||||
|
if curr != ledger.USD {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := data[k]; !ok {
|
||||||
|
data[k] = make([]float64, soFar)
|
||||||
|
}
|
||||||
|
data[k] = append(data[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, transaction := range transactions {
|
||||||
|
if i > 0 && transactions[i-1][0].Date != transaction[0].Date {
|
||||||
|
pushData()
|
||||||
|
}
|
||||||
|
balances := ledger.Deltas(transaction).
|
||||||
|
Like(q).
|
||||||
|
Group(group).
|
||||||
|
Balances().
|
||||||
|
WithBPIsAt(bpis, transaction[0].Date).
|
||||||
|
Nonzero().
|
||||||
|
Normalize(cpiNormalizer, transaction[0].Date)
|
||||||
|
cumulative.PushAll(balances)
|
||||||
|
}
|
||||||
|
pushData()
|
||||||
|
|
||||||
|
labels := []string{}
|
||||||
|
for k := range data {
|
||||||
|
labels = append(labels, k)
|
||||||
|
}
|
||||||
|
slices.Sort(labels)
|
||||||
|
|
||||||
|
points := [][]float64{}
|
||||||
|
for _, k := range labels {
|
||||||
|
points = append(points, data[k])
|
||||||
|
}
|
||||||
|
for i := range points {
|
||||||
|
for j := range points[i] {
|
||||||
|
points[i][j] /= 1000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []asciigraph.Option{asciigraph.Precision(0)}
|
||||||
|
if _, h, err := terminal.GetSize(0); err == nil && h > 4 {
|
||||||
|
options = append(options, asciigraph.Height(h-4))
|
||||||
|
}
|
||||||
|
if len(labels) < 256 {
|
||||||
|
seriesColors := make([]asciigraph.AnsiColor, len(labels))
|
||||||
|
for i := range seriesColors {
|
||||||
|
seriesColors[i] = asciigraph.AnsiColor(i)
|
||||||
|
}
|
||||||
|
options = append(options, asciigraph.SeriesLegends(labels...))
|
||||||
|
options = append(options, asciigraph.SeriesColors(seriesColors...))
|
||||||
|
}
|
||||||
|
fmt.Println(asciigraph.PlotMany(points, options...))
|
||||||
|
case "rec": // reconcile via teller // DEAD
|
||||||
|
panic("dead and bad")
|
||||||
|
byDate := map[string]ledger.Deltas{}
|
||||||
|
for _, delta := range deltas {
|
||||||
|
delta := delta
|
||||||
|
byDate[delta.Date] = append(byDate[delta.Date], delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||||
|
defer can()
|
||||||
|
|
||||||
|
teller, err := teller.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
client := cache.New(teller)
|
||||||
|
|
||||||
|
accounts, err := client.Accounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inDay := func(date string, transaction bank.Transaction) bool {
|
||||||
|
return slices.ContainsFunc(byDate[date], func(d ledger.Delta) bool {
|
||||||
|
v := fmt.Sprintf("%.2f", d.Value)
|
||||||
|
nv := fmt.Sprintf("%.2f", -1.0*d.Value)
|
||||||
|
a := fmt.Sprintf("%.2f", transaction.Amount)
|
||||||
|
return v == a || nv == a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, acc := range accounts {
|
||||||
|
transactions, err := client.Transactions(ctx, acc)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
if inDay(transaction.Date, transaction) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "missing"
|
||||||
|
|
||||||
|
ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
|
||||||
|
dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
|
||||||
|
if inDay(dayBefore, transaction) || inDay(dayAfter, transaction) {
|
||||||
|
msg = "1dayoff"
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := " "
|
||||||
|
if transaction.Status != "posted" {
|
||||||
|
prefix = "! "
|
||||||
|
}
|
||||||
|
fmt.Printf("[%s] %s $%7.2f %s%s (%s)\n", msg, transaction.Date, transaction.Amount, prefix, transaction.Details.CounterParty.Name, transaction.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "reg": // reg
|
||||||
|
deltas = deltas.Group(ledger.GroupDate(config.GroupDate))
|
||||||
|
transactions := deltas.Transactions()
|
||||||
|
cumulative := make(ledger.Balances)
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
balances := ledger.Deltas(transaction).Like(q).Group(group).Balances().WithBPIsAt(bpis, transaction[0].Date).Nonzero().Normalize(cpiNormalizer, transaction[0].Date)
|
||||||
|
shouldPrint := false
|
||||||
|
shouldPrint = shouldPrint || len(balances) > 2
|
||||||
|
if config.Query.NoExchanging {
|
||||||
|
shouldPrint = shouldPrint || len(balances) > 1
|
||||||
|
for _, v := range balances {
|
||||||
|
shouldPrint = shouldPrint || len(v) == 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldPrint = shouldPrint || len(balances) > 0
|
||||||
|
}
|
||||||
|
if shouldPrint {
|
||||||
|
cumulative.PushAll(balances)
|
||||||
|
cumulative = cumulative.Nonzero()
|
||||||
|
FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW, "%s%.2f")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown command " + positional[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FPrintBalancesFor(description string, w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, compact bool, keyW int, cumulativeFormat string) {
|
||||||
|
if compact {
|
||||||
|
FPrintBalances(w, date+"\t", balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\n", date, description)
|
||||||
|
FPrintBalances(w, linePrefix, balances, cumulatives, usdOnly, normalized, date, compact, keyW, cumulativeFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, fullKey bool, max int, cumulativeFormat string) {
|
||||||
|
maxes := map[ledger.Currency]float64{}
|
||||||
|
keys := []string{}
|
||||||
|
for k, v := range balances {
|
||||||
|
keys = append(keys, k)
|
||||||
|
for k2, v2 := range v {
|
||||||
|
if math.Abs(v2) > math.Abs(maxes[k2]) {
|
||||||
|
maxes[k2] = math.Abs(v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
|
||||||
|
normalizer := ana.NewDefaultNormalizer()
|
||||||
|
|
||||||
|
cumulativeFormat = strings.ReplaceAll(cumulativeFormat, "%", "%%")
|
||||||
|
format := fmt.Sprintf("%s%%-%ds\t%%s%%.2f ("+cumulativeFormat+")\n", linePrefix, max)
|
||||||
|
if normalized {
|
||||||
|
format = fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f (%%.2f @%%.2f ("+cumulativeFormat+")))\n", linePrefix, max)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
printableKey := key
|
||||||
|
if fullKey {
|
||||||
|
} else if i == 0 {
|
||||||
|
} else {
|
||||||
|
commonPrefixLen := func() int {
|
||||||
|
j := 0
|
||||||
|
n := len(keys[i])
|
||||||
|
if n2 := len(keys[i-1]); n2 < n {
|
||||||
|
n = n2
|
||||||
|
}
|
||||||
|
for j = 0; j < n; j++ {
|
||||||
|
if keys[i-1][j] != keys[i][j] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for keys[i][j] != ':' && j > 0 {
|
||||||
|
j -= 1
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}()
|
||||||
|
printableKey = strings.Repeat(" ", commonPrefixLen) + keys[i][commonPrefixLen:]
|
||||||
|
}
|
||||||
|
|
||||||
|
currencies := []ledger.Currency{}
|
||||||
|
for currency := range balances[key] {
|
||||||
|
currencies = append(currencies, currency)
|
||||||
|
}
|
||||||
|
slices.Sort(currencies)
|
||||||
|
|
||||||
|
for _, currency := range currencies {
|
||||||
|
printableCurrency := currency
|
||||||
|
format := format
|
||||||
|
if printableCurrency != "$" {
|
||||||
|
printableCurrency += " "
|
||||||
|
format = strings.ReplaceAll(format, "%.2f", "%.3f")
|
||||||
|
}
|
||||||
|
if usdOnly && printableCurrency != "$" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulative := balances[key][currency]
|
||||||
|
if balance, ok := cumulatives[key]; !ok {
|
||||||
|
} else if value, ok := balance[currency]; !ok {
|
||||||
|
} else {
|
||||||
|
cumulative = value
|
||||||
|
}
|
||||||
|
|
||||||
|
printingPercents := strings.Contains(cumulativeFormat, "%%%%")
|
||||||
|
if !printingPercents {
|
||||||
|
if !normalized {
|
||||||
|
fmt.Fprintf(w, format,
|
||||||
|
printableKey,
|
||||||
|
printableCurrency, balances[key][currency],
|
||||||
|
printableCurrency, cumulative,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
factor := normalizer.NormalizeFactor(key, date)
|
||||||
|
trailingMax := maxes[currency] - math.Abs(balances[key][currency])
|
||||||
|
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, printableCurrency, factor*trailingMax)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !normalized {
|
||||||
|
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], cumulative)
|
||||||
|
} else {
|
||||||
|
factor := normalizer.NormalizeFactor(key, date)
|
||||||
|
trailingMax := maxes[currency] - math.Abs(balances[key][currency])
|
||||||
|
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, factor*trailingMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
cmd/cli/query.go
Normal file
100
cmd/cli/query.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Query struct{}
|
||||||
|
|
||||||
|
func BuildQuery(config Config, args args) (ledger.Like, error) {
|
||||||
|
var result ledger.Like
|
||||||
|
var err error
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
err = fmt.Errorf("panicked: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result, err = buildQuery(config, args)
|
||||||
|
}()
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQuery(config Config, args args) (ledger.Like, error) {
|
||||||
|
likeName := func(s string) ledger.Like {
|
||||||
|
return ledger.LikeName(s)
|
||||||
|
}
|
||||||
|
andLike := func(a, b ledger.Like) ledger.Like {
|
||||||
|
return (ledger.Likes{a, b}).All
|
||||||
|
}
|
||||||
|
orLike := func(a, b ledger.Like) ledger.Like {
|
||||||
|
return (ledger.Likes{a, b}).Any
|
||||||
|
}
|
||||||
|
notLike := func(a ledger.Like) ledger.Like {
|
||||||
|
return ledger.LikeNot(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.more() {
|
||||||
|
return likeName(""), nil
|
||||||
|
}
|
||||||
|
like := likeName("ajskfdlsjfkdasj")
|
||||||
|
|
||||||
|
for args.more() {
|
||||||
|
switch args.peek() {
|
||||||
|
case "and":
|
||||||
|
args.pop()
|
||||||
|
var and ledger.Like
|
||||||
|
switch args.peek() {
|
||||||
|
case "not":
|
||||||
|
args.pop()
|
||||||
|
log.Println("and not", args.peek())
|
||||||
|
and = notLike(likeName(args.pop()))
|
||||||
|
default:
|
||||||
|
log.Println("and ", args.peek())
|
||||||
|
and = likeName(args.pop())
|
||||||
|
}
|
||||||
|
like = andLike(like, and)
|
||||||
|
case "not":
|
||||||
|
args.pop()
|
||||||
|
log.Println("or not ", args.peek())
|
||||||
|
like = orLike(like, notLike(
|
||||||
|
likeName(args.pop()),
|
||||||
|
))
|
||||||
|
default:
|
||||||
|
log.Println("or ", args.peek())
|
||||||
|
like = orLike(like,
|
||||||
|
likeName(args.pop()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return like, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type args []string
|
||||||
|
|
||||||
|
func (args *args) pop() string {
|
||||||
|
if !args.more() {
|
||||||
|
panic("expected more arguments")
|
||||||
|
}
|
||||||
|
first := ""
|
||||||
|
if len(*args) > 0 {
|
||||||
|
first = (*args)[0]
|
||||||
|
*args = (*args)[1:]
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *args) peek() string {
|
||||||
|
if len(*args) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return (*args)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *args) more() bool {
|
||||||
|
return len(*args) > 0
|
||||||
|
}
|
||||||
12
cmd/cli/run.sh
Normal file
12
cmd/cli/run.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
cmd="bal"
|
||||||
|
case "$1" in
|
||||||
|
bal|reg|gra|graph|rec )
|
||||||
|
cmd="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
|
||||||
|
go run ../ cli "$@" $(printf " -f %s" $HOME/Sync/Core/ledger/eras/2022-/*.txt) "$cmd" :AssetAccount:
|
||||||
1
cmd/http/bpi.dat
Symbolic link
1
cmd/http/bpi.dat
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../Sync/Core/ledger/bpi.dat
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
|
||||||
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
|
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
|
||||||
go run . -http=:8081 \
|
cd ..
|
||||||
|
go run . http -http=:8081 \
|
||||||
-foo reg \
|
-foo reg \
|
||||||
-like-after 1023-08 \
|
-like-after 1023-08 \
|
||||||
-group-date ^....-.. \
|
-group-date ^....-.. \
|
||||||
-group-name '^[^:]*:[^:]*' \
|
-group-name '^[^:]*:[^:]*' \
|
||||||
-like-name '(AssetAccount|Retirement)' \
|
-like-name '(AssetAccount|Retirement)' \
|
||||||
-bpi ./bpi.dat \
|
-bpi ./http/bpi.dat \
|
||||||
"$@" \
|
"$@" \
|
||||||
macro.d/*
|
./http/macro.d/
|
||||||
1
cmd/http/ledger.dat
Symbolic link
1
cmd/http/ledger.dat
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
moolah.dat.real
|
||||||
1
cmd/http/macro.d
Symbolic link
1
cmd/http/macro.d
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../Sync/Core/ledger/eras/2022-/
|
||||||
46
cmd/http/main.go
Normal file
46
cmd/http/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
likeName := flag.String("like-name", ".", "regexp to match")
|
||||||
|
likeBefore := flag.String("like-before", "9", "date str to compare")
|
||||||
|
likeAfter := flag.String("like-after", "0", "date str to compare")
|
||||||
|
groupName := flag.String("group-name", ".*", "grouping to apply to names")
|
||||||
|
groupDate := flag.String("group-date", ".*", "grouping to apply to dates")
|
||||||
|
bpiPath := flag.String("bpi", "/dev/null", "bpi file")
|
||||||
|
httpOutput := flag.String("http", ":8080", "http output listen address, like :8080")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
panic(fmt.Errorf("positional arguments for files required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := ledger.NewFiles(flag.Args()[0], flag.Args()[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewRouter(
|
||||||
|
f,
|
||||||
|
*likeName,
|
||||||
|
*likeBefore,
|
||||||
|
*likeAfter,
|
||||||
|
*groupName,
|
||||||
|
*groupDate,
|
||||||
|
*bpiPath,
|
||||||
|
)
|
||||||
|
log.Println("listening on", *httpOutput)
|
||||||
|
if err := http.ListenAndServe(*httpOutput, r); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
3090
cmd/http/moolah.dat.real
Normal file
3090
cmd/http/moolah.dat.real
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,15 @@
|
|||||||
console.log(path, query);
|
console.log(path, query);
|
||||||
document.getElementById("graph").src = window.origin + path + query
|
document.getElementById("graph").src = window.origin + path + query
|
||||||
}
|
}
|
||||||
|
function init() {
|
||||||
|
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
||||||
|
var d = new Date()
|
||||||
|
d.setMonth(d.getMonth()-6)
|
||||||
|
document.getElementsByName("zoomStart")[0].value = `${d.getFullYear()}-${zeroPad(d.getMonth(), 2)}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</header>
|
</header>
|
||||||
<body style="height: 100%;">
|
<body style="height: 100%;" onload="init();">
|
||||||
<div id="grapher" style="width: 100%; height: 100%; display: flex; flex-direction: column;">
|
<div id="grapher" style="width: 100%; height: 100%; display: flex; flex-direction: column;">
|
||||||
<form onsubmit="draw(this); return false;" style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 1em;">
|
<form onsubmit="draw(this); return false;" style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 1em;">
|
||||||
<span>
|
<span>
|
||||||
@@ -41,7 +47,7 @@
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
<label for="likeName">likeName</label>
|
<label for="likeName">likeName</label>
|
||||||
<input name="likeName" type="text" value="AssetAccount"/>
|
<input name="likeName" type="text" value="Bel:AssetAccount"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
@@ -61,12 +67,12 @@
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
<label for="zoomStart">zoomStart</label>
|
<label for="zoomStart">zoomStart</label>
|
||||||
<input name="zoomStart" type="text" value="2023-06"/>
|
<input name="zoomStart" type="text" value="YYYY-MM"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<label for="prediction">prediction</label>
|
<label for="prediction">prediction</label>
|
||||||
<input name="prediction" type="text" value="interest=AssetAccount:Cash \$ 0.02&prediction=contributions=AssetAccount:Bonds $ 1875&prediction=interest=AssetAccount:Monthly \$ 0.03&prediction=contributions=AssetAccount:Monthly $ 2500"/>
|
<input name="prediction" type="text" value="interest=Bel:AssetAccount:Cash \$ 0.02&prediction=contributions=Bel:AssetAccount:Bonds $ 1916&prediction=interest=Bel:AssetAccount:Monthly \$ 0.03&prediction=contributions=Bel:AssetAccount:Monthly $ 3500"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
@@ -76,7 +82,7 @@
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
<label for="whatIf">whatIf</label>
|
<label for="whatIf">whatIf</label>
|
||||||
<input name="whatIf" type="text" value="AssetAccount:Cash $ -.10000"/>
|
<input name="whatIf" type="text" value="Bel:AssetAccount:Cash $ -.10000"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
28
cmd/http/public/index.html
Normal file
28
cmd/http/public/index.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<html style="height: calc(100% - 4em);">
|
||||||
|
<header>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||||
|
<script>
|
||||||
|
function init() {
|
||||||
|
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
||||||
|
var d = new Date()
|
||||||
|
d.setMonth(d.getMonth()-6)
|
||||||
|
const replacement = `${d.getFullYear()}-${zeroPad(d.getMonth(), 2)}`
|
||||||
|
var anchors = document.getElementsByTagName("a")
|
||||||
|
for (var i = 0; i < anchors.length; i++)
|
||||||
|
anchors[i].href = anchors[i].href.replace("YYYY-MM", replacement)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</header>
|
||||||
|
<body style="height: 100%;" onload="init();">
|
||||||
|
<h1>Moolah2 Hub</h1>
|
||||||
|
<ul style="line-height: 3em;">
|
||||||
|
<li><a href="/transactions.html">Transactions on Shared Chase</a></li>
|
||||||
|
<li><a href="/api/trends">Where does the house money go?</a></li>
|
||||||
|
<li><a href="/explore.html">Explore Bel's Money</a></li>
|
||||||
|
<li><a href="/api/bal?x=y&mode=bal&likeName=AssetAccount&chart=stack&predictionMonths=120&bpi=true&zoomStart=YYYY-MM&prediction=interest=AssetAccount:Cash%20\$%200.02&prediction=contributions=AssetAccount:Bonds%20$%201875&prediction=interest=AssetAccount:Monthly%20\$%200.03&prediction=contributions=AssetAccount:Monthly%20$%202500&predictFixedGrowth=VBTLX=0.02&predictFixedGrowth=GLD=0.02&predictFixedGrowth=FXAIX=0.03&predictFixedGrowth=FSPSX=0.03&whatIf=AssetAccount:Cash%20$%20-.10000&=">Project Bel's Net Worth</a></li>
|
||||||
|
<!--<li><a href="/api/reg?x=y&mode=reg&likeName=Bel:Withdrawal:&chart=stack&predictionMonths=3&bpi=false&zoomStart=YYYY-MM&prediction=autoContributions=&predictFixedGrowth=VBTLX=0&whatIf=AssetAccount:Cash%20$%20-.10000&=">Expect Bel's Expenses</a></li>-->
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
264
cmd/http/public/transactions.html
Normal file
264
cmd/http/public/transactions.html
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<html>
|
||||||
|
<header>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||||
|
<style>
|
||||||
|
input[type="text"] {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
.amended {
|
||||||
|
border: 2px solid red;
|
||||||
|
}
|
||||||
|
.revised {
|
||||||
|
border: 2px solid green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
function http(method, remote, callback, body) {
|
||||||
|
var xmlhttp = new XMLHttpRequest()
|
||||||
|
xmlhttp.onreadystatechange = function() {
|
||||||
|
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||||
|
callback(xmlhttp.responseText, xmlhttp.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xmlhttp.open(method, remote, true)
|
||||||
|
if (typeof body == "undefined") {
|
||||||
|
body = null
|
||||||
|
}
|
||||||
|
xmlhttp.send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(responseBody, responseStatus) {
|
||||||
|
}
|
||||||
|
//var f = String(window.location).split("/transactions.html")[1]
|
||||||
|
//if (!f) {
|
||||||
|
// f = "/ledger.dat"
|
||||||
|
//}
|
||||||
|
//f = "." + f
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
||||||
|
var d = new Date()
|
||||||
|
d.setMonth(d.getMonth()-6)
|
||||||
|
const replacement = `${d.getFullYear()}-${zeroPad(d.getMonth(), 2)}`
|
||||||
|
var iframe = document.getElementsByTagName("iframe")[0]
|
||||||
|
iframe.src = iframe.src.replace("YYYY-MM", replacement)
|
||||||
|
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(callback) {
|
||||||
|
http("GET", "/api/transactions", (body, status) => {
|
||||||
|
var d = JSON.parse(body)
|
||||||
|
console.log("loading", d)
|
||||||
|
loadNormalized(d.normalized)
|
||||||
|
loadBalances(d.balances)
|
||||||
|
loadTransactions(d.transactions)
|
||||||
|
if (callback != null) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNormalized(normalized) {
|
||||||
|
console.log("loading normalized", normalized)
|
||||||
|
var result = `<table>`
|
||||||
|
for (var k in normalized) {
|
||||||
|
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(normalized[k]["$"])}</td></tr>`
|
||||||
|
}
|
||||||
|
result += `</table>`
|
||||||
|
document.getElementById("norm").innerHTML = result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadBalances(balances) {
|
||||||
|
console.log("loading balances", balances)
|
||||||
|
var result = `<table>`
|
||||||
|
for (var k in balances) {
|
||||||
|
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(balances[k]["$"])}</td></tr>`
|
||||||
|
}
|
||||||
|
result += `</table>`
|
||||||
|
document.getElementById("bal").innerHTML = result
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTransactions(transactions) {
|
||||||
|
transactions.reverse()
|
||||||
|
console.log(transactions[0])
|
||||||
|
|
||||||
|
var result = `<table>`
|
||||||
|
for (var t of transactions) {
|
||||||
|
result += `<tr name="transaction-${t[0].Date}-${t[0].Description}" transaction='${btoa(JSON.stringify(t))}' style="cursor: crosshair;" onclick="stageEdit(this); return false;" class="${t[0].Transaction.includes("inbox") ? "revised" : ""}">`
|
||||||
|
result += ` <td style="width: 6em;">${t[0].Date}</td>`
|
||||||
|
result += ` <td style="width: 10em;">${t[0].Description}</td>`
|
||||||
|
result += ` <td><table style="margin: 0;">`
|
||||||
|
for (var delta of t) {
|
||||||
|
result += ` <tr>`
|
||||||
|
result += ` <td><span style="font-variant: petite-caps;">${delta.Name.split(":")[0]}</span><span style="opacity: 0.6;"> :${delta.Name.split(":").slice(1, 100).join(":")}</span></td>`
|
||||||
|
result += ` <td style="text-align: right; width: 2em;">${delta.Payee ? delta.Currency : ""}</td>`
|
||||||
|
result += ` <td style="text-align: right; width: 5em;">${delta.Payee ? delta.Value : ""}</td>`
|
||||||
|
result += ` </tr>`
|
||||||
|
}
|
||||||
|
result += ` </table></td>`
|
||||||
|
result += `</tr>`
|
||||||
|
}
|
||||||
|
result += `</table>`
|
||||||
|
|
||||||
|
document.getElementById("reg").innerHTML = result
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageEdit(row) {
|
||||||
|
const xactionJSON = atob(row.attributes.transaction.value)
|
||||||
|
const xaction = JSON.parse(xactionJSON)
|
||||||
|
console.log(xaction)
|
||||||
|
|
||||||
|
row.attributes.onclick = ""
|
||||||
|
row.onclick = ""
|
||||||
|
row.style = ""
|
||||||
|
console.log(row)
|
||||||
|
|
||||||
|
var result = `<td colspan="3">`
|
||||||
|
result += `<form method="modal">`
|
||||||
|
result += ` <div>`
|
||||||
|
result += ` <label for="date">Date</label>`
|
||||||
|
result += ` <input type="text" name="date" value='${xaction[0].Date}' style="width: 100%;"/>`
|
||||||
|
result += ` </div>`
|
||||||
|
result += ` <div>`
|
||||||
|
result += ` <label for="description">Description</label>`
|
||||||
|
result += ` <input type="text" name="description" value='${xaction[0].Description}' style="width: 100%;"/>`
|
||||||
|
result += ` </div>`
|
||||||
|
|
||||||
|
for (var i in xaction) {
|
||||||
|
var delta = xaction[i]
|
||||||
|
result += `<hr>`
|
||||||
|
result += `<div style="display: flex; flex-direction: row; width: 100%; margin-top: 1em;">`
|
||||||
|
result += ` <div style="flex-grow: 1; margin-left: 1em; margin-right: 1em;">`
|
||||||
|
result += ` <label for="name${i}">Name${i}</label>`
|
||||||
|
result += ` <input type="text" name="name${i}" value='${delta.Name}' style="width: 100%;"/>`
|
||||||
|
result += ` </div>`
|
||||||
|
result += ` <div style="margin-left: 1em; margin-right: 1em;">`
|
||||||
|
result += ` <label for="value${i}">Value${i}</label>`
|
||||||
|
result += ` <input type="text" name="value${i}" value='${delta.Value}' style="width: 100%;"/>`
|
||||||
|
result += ` </div>`
|
||||||
|
result += ` <input type="button" name="submit${i}" value="Submit${i}" onclick="submitEdit(this); return false;" delta="${btoa(JSON.stringify(delta))}"/>`
|
||||||
|
result += `</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `</form`
|
||||||
|
result += `</td>`
|
||||||
|
|
||||||
|
row.innerHTML = result
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitEdit(btn) {
|
||||||
|
const old = JSON.parse(atob(btn.attributes.delta.value))
|
||||||
|
const now = JSON.parse(atob(btn.attributes.delta.value))
|
||||||
|
|
||||||
|
const idx = btn.name.split("submit")[1]
|
||||||
|
const form = btn.parentElement.parentElement
|
||||||
|
|
||||||
|
now.Value = parseFloat(form[`value${idx}`].value)
|
||||||
|
now.Name = form[`name${idx}`].value
|
||||||
|
now.Description = form[`description`].value
|
||||||
|
now.Date = form[`date`].value
|
||||||
|
|
||||||
|
var different = false
|
||||||
|
for(var k in old) {
|
||||||
|
different = different || old[k] != now[k]
|
||||||
|
}
|
||||||
|
if (!different) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http("PUT", "/api/amend", (body, status) => {
|
||||||
|
if (status != 200)
|
||||||
|
throw(`Unexpected status ${status}: ${body}`)
|
||||||
|
load(() => {
|
||||||
|
/*
|
||||||
|
var elements = document.getElementsByName(`transaction-${old.Date}-${old.Description}`)
|
||||||
|
for (var ele of elements)
|
||||||
|
ele.className = "amended"
|
||||||
|
var elements = document.getElementsByName(`transaction-${now.Date}-${now.Description}`)
|
||||||
|
for (var ele of elements)
|
||||||
|
ele.className = "revised"
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
}, JSON.stringify({
|
||||||
|
old: old,
|
||||||
|
now: now,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function create() {
|
||||||
|
http("POST", "/api/create", (body, status) => {
|
||||||
|
if (status != 200)
|
||||||
|
throw(`Unexpected status ${status}: ${body}`)
|
||||||
|
loadTransactions([])
|
||||||
|
load()
|
||||||
|
}, JSON.stringify({}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stage(who, contributesToHouse) {
|
||||||
|
var d = new Date()
|
||||||
|
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
||||||
|
const today = `${d.getFullYear()}-${zeroPad(1+d.getMonth(), 2)}-${zeroPad(d.getDate(), 2)}`
|
||||||
|
|
||||||
|
var who_currency = who.split(":").at(-1).toUpperCase()
|
||||||
|
|
||||||
|
var benefactor = who
|
||||||
|
var benefactor_value = `-0.00 ${who_currency}`
|
||||||
|
var beneficiary = "AssetAccount:Chase:5876"
|
||||||
|
var beneficiary_value = `$0.00`
|
||||||
|
if (!contributesToHouse) {
|
||||||
|
beneficiary_value = `$-0.00`
|
||||||
|
benefactor_value = `0.00 ${who_currency}`
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`@${today} ${benefactor} gave to ${beneficiary}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</header>
|
||||||
|
<body onload="init();" style="">
|
||||||
|
<h2>Moolah2</h2>
|
||||||
|
<details open>
|
||||||
|
<summary>Normalized</summary>
|
||||||
|
<div id="norm">
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Balance</summary>
|
||||||
|
<div id="bal">
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary><i>Look at this graph</i></summary>
|
||||||
|
<iframe style="background: white; width: 100%; resize: both;" src="/api/reg?x=y&mode=reg&likeName=Withdrawal:[0123]&chart=stack&predictionMonths=6&prediction=autoContributions=&bpi=true&zoomStart=YYYY-MM"></iframe>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary><i>Where did the money go</i></summary>
|
||||||
|
<iframe style="background: white; width: 100%; resize: both;" src="/api/trends"></iframe>
|
||||||
|
</details>
|
||||||
|
</details>
|
||||||
|
<details open style="display: none;">
|
||||||
|
<summary>Edit</summary>
|
||||||
|
<div style="display:flex; flex-direction:row; width:100%; justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<input type="button" onclick="stage('AssetAccount:Zach', true)" value="Stage Zach's Payment"/>
|
||||||
|
<input type="button" onclick="stage('AssetAccount:Zach', false)" value="Stage Zach's Charge"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="button" onclick="stage('AssetAccount:Bel', true)" value="Stage Bel's Payment"/>
|
||||||
|
<input type="button" onclick="stage('AssetAccount:Bel', false)" value="Stage Bel's Charge"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details open>
|
||||||
|
<summary>Register</summary>
|
||||||
|
<form action="#" onsubmit="create(); return false;">
|
||||||
|
<button style="width: 100%">
|
||||||
|
CREATE
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="reg">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
485
cmd/http/router.go
Normal file
485
cmd/http/router.go
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed public/*
|
||||||
|
var _staticFileDir embed.FS
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
files ledger.Files
|
||||||
|
like struct {
|
||||||
|
name string
|
||||||
|
before string
|
||||||
|
after string
|
||||||
|
}
|
||||||
|
group struct {
|
||||||
|
name string
|
||||||
|
date string
|
||||||
|
}
|
||||||
|
bpiPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(files ledger.Files, likeName, likeBefore, likeAfter string, groupName, groupDate string, bpiPath string) Router {
|
||||||
|
r := Router{}
|
||||||
|
r.files = files
|
||||||
|
r.like.name = likeName
|
||||||
|
r.like.before = likeBefore
|
||||||
|
r.like.after = likeAfter
|
||||||
|
r.group.name = groupName
|
||||||
|
r.group.date = groupDate
|
||||||
|
r.bpiPath = bpiPath
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch strings.Split(r.URL.Path, "/")[1] {
|
||||||
|
case "api":
|
||||||
|
router.API(w, r)
|
||||||
|
default:
|
||||||
|
router.FS(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) FS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if os.Getenv("DEBUG") != "" {
|
||||||
|
http.FileServer(http.Dir("./http/public")).ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
sub, err := fs.Sub(_staticFileDir, "public")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
http.FileServer(http.FS(sub)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) API(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/transactions":
|
||||||
|
router.APITransactions(w, r)
|
||||||
|
case "/api/amend":
|
||||||
|
router.APIAmend(w, r)
|
||||||
|
case "/api/create":
|
||||||
|
router.APICreate(w, r)
|
||||||
|
case "/api/trends":
|
||||||
|
router.APITrends(w, r)
|
||||||
|
case "/api/reg", "/api/bal":
|
||||||
|
router.APIReg(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APITransactions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bpis, err := router.bpis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deltas, err := router.files.Deltas()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
houseRelatedDeltas := deltas.Like(ledger.LikeTransactions(
|
||||||
|
deltas.Like(ledger.LikeName(`^House`))...,
|
||||||
|
))
|
||||||
|
recent := time.Hour * 24 * 365 / 6
|
||||||
|
|
||||||
|
normalizer := ana.NewDefaultNormalizer()
|
||||||
|
|
||||||
|
{
|
||||||
|
deltas := houseRelatedDeltas.
|
||||||
|
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01")))
|
||||||
|
|
||||||
|
balances := houseRelatedDeltas.Balances().
|
||||||
|
Like(`^(Zach|Bel|House[^:]*:Debts:)`).
|
||||||
|
Group(`^[^:]*`).
|
||||||
|
WithBPIs(bpis)
|
||||||
|
|
||||||
|
transactions := houseRelatedDeltas.
|
||||||
|
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01"))).
|
||||||
|
Transactions()
|
||||||
|
|
||||||
|
normalized := normalizer.Normalize(houseRelatedDeltas).Balances().
|
||||||
|
Like(`^(Zach|Bel):`).
|
||||||
|
Group(`^[^:]*:`).
|
||||||
|
WithBPIs(bpis)
|
||||||
|
var biggest float64
|
||||||
|
for _, v := range normalized {
|
||||||
|
if v := math.Abs(v["$"]); v > biggest {
|
||||||
|
biggest = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ks := []string{}
|
||||||
|
for k := range normalized {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
for _, k := range ks {
|
||||||
|
v := normalized[k]
|
||||||
|
if v := math.Abs(v["$"]); v < biggest {
|
||||||
|
normalizedDelta := biggest - v
|
||||||
|
normalizedFactor := normalizer.NormalizeFactor(k, time.Now().Format("2006-01-02"))
|
||||||
|
normalized[fmt.Sprintf(`(%s trailing $)`, k)] = ledger.Balance{"$": normalizedDelta * normalizedFactor}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"deltas": deltas,
|
||||||
|
"balances": balances,
|
||||||
|
"normalized": normalized,
|
||||||
|
"transactions": transactions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APITrends(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bpis, err := router.bpis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deltas, err := router.files.Deltas()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recent := time.Hour * 24 * 365 / 2
|
||||||
|
|
||||||
|
pie := func(title, groupName string, min int) {
|
||||||
|
recentHouseRelatedDeltas := deltas.
|
||||||
|
Like(ledger.LikeTransactions(
|
||||||
|
deltas.Like(ledger.LikeName(`^House`))...,
|
||||||
|
)).
|
||||||
|
Like(ledger.LikeAfter(time.Now().Add(-1 * recent).Format("2006-01"))).
|
||||||
|
Group(ledger.GroupName(groupName)).
|
||||||
|
Group(ledger.GroupDate(`^[0-9]*-[0-9]*`)).
|
||||||
|
Like(ledger.LikeNotName(`^$`))
|
||||||
|
|
||||||
|
monthsToDeltas := map[string]ledger.Deltas{}
|
||||||
|
for _, delta := range recentHouseRelatedDeltas {
|
||||||
|
monthsToDeltas[delta.Date] = append(monthsToDeltas[delta.Date], delta)
|
||||||
|
}
|
||||||
|
months := []string{}
|
||||||
|
for k := range monthsToDeltas {
|
||||||
|
months = append(months, k)
|
||||||
|
}
|
||||||
|
slices.Sort(months)
|
||||||
|
|
||||||
|
catToMonth := map[string][]int{}
|
||||||
|
for _, month := range months {
|
||||||
|
balances := monthsToDeltas[month].Balances().WithBPIs(bpis)
|
||||||
|
for category, balance := range balances {
|
||||||
|
catToMonth[category] = append(catToMonth[category], int(balance[ledger.USD]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chart := view.NewChart("pie")
|
||||||
|
ttl := 0
|
||||||
|
for cat, month := range catToMonth {
|
||||||
|
for i := 0; i < len(months)-len(month); i++ {
|
||||||
|
month = append(month, 0)
|
||||||
|
}
|
||||||
|
slices.Sort(month)
|
||||||
|
median := month[len(month)/2]
|
||||||
|
ttl += median
|
||||||
|
if median > min {
|
||||||
|
chart.AddY(cat, []int{median})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "<!DOCTYPE html><h2>", title, "($", ttl, ")</h2>")
|
||||||
|
if err := chart.Render(w); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pie(fmt.Sprintf("Median Monthly Spending Since %dmo ago", int(recent/time.Hour/24/30)), `Withdrawal:[0-9]*`, 50)
|
||||||
|
pie("Median Monthly Spending (detailed)", `Withdrawal:[0-9]*:[^:]*`, 25)
|
||||||
|
pie("Median Monthly Spending (MORE detailed)", `Withdrawal:[0-9]*:[^:]*:[^:]*`, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APICreate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
new := ledger.Delta{
|
||||||
|
Name: "TODO",
|
||||||
|
Date: time.Now().Format(`2006-01-02`),
|
||||||
|
Description: "TODO",
|
||||||
|
Currency: "$",
|
||||||
|
Value: 0.01,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := router.files.Add("HouseyMcHouseface:Withdrawal:0:TODO", new); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APIAmend(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Old ledger.Delta
|
||||||
|
Now ledger.Delta
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Now.Name = strings.ReplaceAll(req.Now.Name, " ", "_")
|
||||||
|
|
||||||
|
if err := router.files.Amend(req.Old, req.Now); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APIReg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
deltas, err := router.files.Deltas()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
deltas = deltas.Group(ledger.GroupName(router.group.name), ledger.GroupDate(router.group.date))
|
||||||
|
like := ledger.Likes{
|
||||||
|
ledger.LikeName(router.like.name),
|
||||||
|
ledger.LikeBefore(router.like.before),
|
||||||
|
ledger.LikeAfter(router.like.after),
|
||||||
|
}
|
||||||
|
|
||||||
|
foolike := make(ledger.Likes, 0)
|
||||||
|
for _, v := range r.URL.Query()["likeName"] {
|
||||||
|
foolike = append(foolike, ledger.LikeName(v))
|
||||||
|
}
|
||||||
|
for _, v := range r.URL.Query()["likeAfter"] {
|
||||||
|
foolike = append(foolike, ledger.LikeAfter(v))
|
||||||
|
}
|
||||||
|
for _, v := range r.URL.Query()["likeBefore"] {
|
||||||
|
foolike = append(foolike, ledger.LikeBefore(v))
|
||||||
|
}
|
||||||
|
if len(foolike) == 0 {
|
||||||
|
foolike = like
|
||||||
|
}
|
||||||
|
deltas = deltas.Like(foolike...)
|
||||||
|
|
||||||
|
// MODIFIERS
|
||||||
|
for i, whatIf := range r.URL.Query()["whatIf"] {
|
||||||
|
fields := strings.Fields(whatIf)
|
||||||
|
date := "2001-01"
|
||||||
|
name := fields[0]
|
||||||
|
currency := ledger.Currency(fields[1])
|
||||||
|
value, err := strconv.ParseFloat(fields[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
deltas = append(deltas, ledger.Delta{
|
||||||
|
Date: date,
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Currency: currency,
|
||||||
|
Description: fmt.Sprintf("?whatIf[%d]", i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
register := deltas.Register()
|
||||||
|
predicted := make(ledger.Register)
|
||||||
|
|
||||||
|
bpis, err := router.bpis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
|
||||||
|
window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
|
||||||
|
// TODO whatif
|
||||||
|
prediction := make(ana.Prediction, 0)
|
||||||
|
for _, spec := range r.URL.Query()["prediction"] {
|
||||||
|
idx := strings.Index(spec, "=")
|
||||||
|
k := spec[:idx]
|
||||||
|
fields := strings.Fields(spec[idx+1:])
|
||||||
|
switch k {
|
||||||
|
case "interest":
|
||||||
|
apy, err := strconv.ParseFloat(fields[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
|
||||||
|
case "autoContributions":
|
||||||
|
prediction = append(prediction, ana.NewAutoContributionPredictor(register))
|
||||||
|
case "contributions":
|
||||||
|
name := fields[0]
|
||||||
|
currency := ledger.Currency(fields[1])
|
||||||
|
value, err := strconv.ParseFloat(fields[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
|
||||||
|
default:
|
||||||
|
panic(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
predicted = prediction.Predict(register, window)
|
||||||
|
|
||||||
|
for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
|
||||||
|
currency := strings.Split(currencyRate, "=")[0]
|
||||||
|
rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("bpi") == "true" {
|
||||||
|
register = register.WithBPIs(bpis)
|
||||||
|
predicted = predicted.WithBPIs(bpis)
|
||||||
|
}
|
||||||
|
if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
|
||||||
|
register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
|
||||||
|
predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
|
||||||
|
}
|
||||||
|
// /MODIFIERS
|
||||||
|
|
||||||
|
dates := register.Dates()
|
||||||
|
names := register.Names()
|
||||||
|
for _, date := range predicted.Dates() {
|
||||||
|
found := false
|
||||||
|
for i := range dates {
|
||||||
|
found = found || dates[i] == date
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
dates = append(dates, date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range predicted.Names() {
|
||||||
|
found := false
|
||||||
|
for i := range names {
|
||||||
|
found = found || names[i] == name
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instant := map[string]string{}
|
||||||
|
toChart := func(cumulative bool, display string, reg ledger.Register) view.Chart {
|
||||||
|
nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
|
||||||
|
for date, balances := range reg {
|
||||||
|
for name, balance := range balances {
|
||||||
|
for currency, value := range balance {
|
||||||
|
if _, ok := nameCurrencyDateValue[name]; !ok {
|
||||||
|
nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
|
||||||
|
}
|
||||||
|
if _, ok := nameCurrencyDateValue[name][currency]; !ok {
|
||||||
|
nameCurrencyDateValue[name][currency] = make(map[string]float64)
|
||||||
|
}
|
||||||
|
nameCurrencyDateValue[name][currency][date] += value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chart := view.NewChart("line")
|
||||||
|
if v := display; v != "" {
|
||||||
|
chart = view.NewChart(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.AddX(dates)
|
||||||
|
|
||||||
|
if cumulative {
|
||||||
|
for _, name := range names {
|
||||||
|
currencyDateValue := nameCurrencyDateValue[name]
|
||||||
|
for currency, dateValue := range currencyDateValue {
|
||||||
|
series := make([]int, len(dates))
|
||||||
|
for i := range dates {
|
||||||
|
var lastValue float64
|
||||||
|
for j := range dates[:i+1] {
|
||||||
|
if newLastValue, ok := dateValue[dates[j]]; ok {
|
||||||
|
lastValue = newLastValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
series[i] = int(lastValue)
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s (%s)", name, currency)
|
||||||
|
for i := range dates {
|
||||||
|
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
|
||||||
|
series[i] = 0
|
||||||
|
} else {
|
||||||
|
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Min(series) != 0 || slices.Max(series) != 0 {
|
||||||
|
chart.AddY(key, series)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, name := range names {
|
||||||
|
currencyDateValue := nameCurrencyDateValue[name]
|
||||||
|
for currency, dateValue := range currencyDateValue {
|
||||||
|
series := make([]int, len(dates))
|
||||||
|
for i := range dates {
|
||||||
|
var prevValue float64
|
||||||
|
var lastValue float64
|
||||||
|
for j := range dates[:i+1] {
|
||||||
|
if newLastValue, ok := dateValue[dates[j]]; ok {
|
||||||
|
prevValue = lastValue
|
||||||
|
lastValue = newLastValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
series[i] = int(lastValue - prevValue)
|
||||||
|
}
|
||||||
|
for i := range series { // TODO no prior so no delta
|
||||||
|
if series[i] != 0 {
|
||||||
|
series[i] = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s (%s)", name, currency)
|
||||||
|
for i := range dates {
|
||||||
|
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
|
||||||
|
series[i] = 0
|
||||||
|
} else {
|
||||||
|
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Min(series) != 0 || slices.Max(series) != 0 {
|
||||||
|
chart.AddY(key, series)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chart
|
||||||
|
}
|
||||||
|
primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
|
||||||
|
if len(predicted) > 0 {
|
||||||
|
primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
|
||||||
|
}
|
||||||
|
if err := primary.Render(w); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for k, v := range instant {
|
||||||
|
fmt.Fprintf(w, "<br>\n%s = %s", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Router) bpis() (ledger.BPIs, error) {
|
||||||
|
if r.bpiPath == "" {
|
||||||
|
return make(ledger.BPIs), nil
|
||||||
|
}
|
||||||
|
return ledger.NewBPIs(r.bpiPath)
|
||||||
|
}
|
||||||
3
cmd/install.sh
Normal file
3
cmd/install.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
|
||||||
|
go build -o $GOPATH/bin/ana-ledger
|
||||||
1
cmd/install_scratch.sh
Normal file
1
cmd/install_scratch.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CGO_ENABLED=1 CC=x86_64-linux-musl-gcc go build -ldflags="-linkmode external -extldflags -static" -o $HOME/Go/bin/ana-ledger
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../../../../Sync/Core/ledger/eras/2022-
|
|
||||||
490
cmd/main.go
490
cmd/main.go
@@ -1,473 +1,57 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"maps"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"os/signal"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"syscall"
|
||||||
|
|
||||||
"github.com/go-echarts/go-echarts/v2/charts"
|
"gogs.inhome.blapointe.com/ana-ledger/cmd/cli"
|
||||||
"github.com/go-echarts/go-echarts/v2/opts"
|
"gogs.inhome.blapointe.com/ana-ledger/cmd/http"
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
foo := flag.String("foo", "bal", "bal or reg")
|
switch os.Args[1] {
|
||||||
likeName := flag.String("like-name", ".", "regexp to match")
|
case "tel":
|
||||||
likeBefore := flag.String("like-before", "9", "date str to compare")
|
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||||
likeAfter := flag.String("like-after", "0", "date str to compare")
|
defer can()
|
||||||
likeLedger := flag.Bool("like-ledger", false, "limit data to these -like-* rather than zoom to these -like-*")
|
|
||||||
groupName := flag.String("group-name", ".*", "grouping to apply to names")
|
|
||||||
groupDate := flag.String("group-date", ".*", "grouping to apply to dates")
|
|
||||||
bpiPath := flag.String("bpi", "/dev/null", "bpi file")
|
|
||||||
jsonOutput := flag.Bool("json", false, "json output")
|
|
||||||
httpOutput := flag.String("http", "", "http output listen address, like :8080")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if flag.NArg() < 1 {
|
if c, err := teller.New(); err != nil {
|
||||||
panic(fmt.Errorf("positional arguments for files required"))
|
} else if _, err := c.Accounts(ctx); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
f, err := ledger.NewFiles(flag.Args()[0], flag.Args()[1:]...)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bpis := make(ledger.BPIs)
|
|
||||||
if *bpiPath != "" {
|
|
||||||
bpis, err = ledger.NewBPIs(*bpiPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if *httpOutput != "" {
|
|
||||||
foo := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !strings.HasPrefix(r.URL.Path, "/api") {
|
|
||||||
http.FileServer(http.Dir("./public")).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/api/transactions":
|
|
||||||
reqF := f
|
|
||||||
if queryF := r.URL.Query().Get("f"); queryF != "" {
|
|
||||||
reqF, err = ledger.NewFiles(queryF)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deltas, err := reqF.Deltas()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(map[string]any{
|
|
||||||
"deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))),
|
|
||||||
"balances": deltas.Balances().Like("^AssetAccount:").WithBPIs(bpis),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deltas, err := f.Deltas()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
|
|
||||||
like := ledger.Likes{
|
|
||||||
ledger.LikeName(*likeName),
|
|
||||||
ledger.LikeBefore(*likeBefore),
|
|
||||||
ledger.LikeAfter(*likeAfter),
|
|
||||||
}
|
|
||||||
|
|
||||||
foolike := make(ledger.Likes, 0)
|
|
||||||
for _, v := range r.URL.Query()["likeName"] {
|
|
||||||
foolike = append(foolike, ledger.LikeName(v))
|
|
||||||
}
|
|
||||||
for _, v := range r.URL.Query()["likeAfter"] {
|
|
||||||
foolike = append(foolike, ledger.LikeAfter(v))
|
|
||||||
}
|
|
||||||
for _, v := range r.URL.Query()["likeBefore"] {
|
|
||||||
foolike = append(foolike, ledger.LikeBefore(v))
|
|
||||||
}
|
|
||||||
if len(foolike) == 0 {
|
|
||||||
foolike = like
|
|
||||||
}
|
|
||||||
deltas = deltas.Like(foolike...)
|
|
||||||
|
|
||||||
// MODIFIERS
|
|
||||||
for i, whatIf := range r.URL.Query()["whatIf"] {
|
|
||||||
fields := strings.Fields(whatIf)
|
|
||||||
date := "2001-01"
|
|
||||||
name := fields[0]
|
|
||||||
currency := ledger.Currency(fields[1])
|
|
||||||
value, err := strconv.ParseFloat(fields[2], 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
deltas = append(deltas, ledger.Delta{
|
|
||||||
Date: date,
|
|
||||||
Name: name,
|
|
||||||
Value: value,
|
|
||||||
Currency: currency,
|
|
||||||
Description: fmt.Sprintf("?whatIf[%d]", i),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
register := deltas.Register()
|
|
||||||
predicted := make(ledger.Register)
|
|
||||||
bpis := maps.Clone(bpis)
|
|
||||||
|
|
||||||
if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
|
|
||||||
window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
|
|
||||||
// TODO whatif
|
|
||||||
prediction := make(ana.Prediction, 0)
|
|
||||||
for _, spec := range r.URL.Query()["prediction"] {
|
|
||||||
idx := strings.Index(spec, "=")
|
|
||||||
k := spec[:idx]
|
|
||||||
fields := strings.Fields(spec[idx+1:])
|
|
||||||
switch k {
|
|
||||||
case "interest":
|
|
||||||
apy, err := strconv.ParseFloat(fields[2], 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
|
|
||||||
case "autoContributions":
|
|
||||||
prediction = append(prediction, ana.NewAutoContributionPredictor(register))
|
|
||||||
case "contributions":
|
|
||||||
name := fields[0]
|
|
||||||
currency := ledger.Currency(fields[1])
|
|
||||||
value, err := strconv.ParseFloat(fields[2], 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
|
|
||||||
default:
|
|
||||||
panic(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
predicted = prediction.Predict(register, window)
|
|
||||||
|
|
||||||
for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
|
|
||||||
currency := strings.Split(currencyRate, "=")[0]
|
|
||||||
rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Query().Get("bpi") == "true" {
|
|
||||||
register = register.WithBPIs(bpis)
|
|
||||||
predicted = predicted.WithBPIs(bpis)
|
|
||||||
}
|
|
||||||
if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
|
|
||||||
register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
|
|
||||||
predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
|
|
||||||
}
|
|
||||||
// /MODIFIERS
|
|
||||||
|
|
||||||
dates := register.Dates()
|
|
||||||
names := register.Names()
|
|
||||||
for _, date := range predicted.Dates() {
|
|
||||||
found := false
|
|
||||||
for i := range dates {
|
|
||||||
found = found || dates[i] == date
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
dates = append(dates, date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, name := range predicted.Names() {
|
|
||||||
found := false
|
|
||||||
for i := range names {
|
|
||||||
found = found || names[i] == name
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
instant := map[string]string{}
|
|
||||||
toChart := func(cumulative bool, display string, reg ledger.Register) Chart {
|
|
||||||
nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
|
|
||||||
for date, balances := range reg {
|
|
||||||
for name, balance := range balances {
|
|
||||||
for currency, value := range balance {
|
|
||||||
if _, ok := nameCurrencyDateValue[name]; !ok {
|
|
||||||
nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
|
|
||||||
}
|
|
||||||
if _, ok := nameCurrencyDateValue[name][currency]; !ok {
|
|
||||||
nameCurrencyDateValue[name][currency] = make(map[string]float64)
|
|
||||||
}
|
|
||||||
nameCurrencyDateValue[name][currency][date] += value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chart := NewChart("line")
|
|
||||||
if v := display; v != "" {
|
|
||||||
chart = NewChart(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.AddX(dates)
|
|
||||||
|
|
||||||
if cumulative {
|
|
||||||
for _, name := range names {
|
|
||||||
currencyDateValue := nameCurrencyDateValue[name]
|
|
||||||
for currency, dateValue := range currencyDateValue {
|
|
||||||
series := make([]int, len(dates))
|
|
||||||
for i := range dates {
|
|
||||||
var lastValue float64
|
|
||||||
for j := range dates[:i+1] {
|
|
||||||
if newLastValue, ok := dateValue[dates[j]]; ok {
|
|
||||||
lastValue = newLastValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
series[i] = int(lastValue)
|
|
||||||
}
|
|
||||||
key := fmt.Sprintf("%s (%s)", name, currency)
|
|
||||||
for i := range dates {
|
|
||||||
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
|
|
||||||
series[i] = 0
|
|
||||||
} else {
|
} else {
|
||||||
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
|
log.Println("teller already init")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if slices.Min(series) != 0 || slices.Max(series) != 0 {
|
if err := teller.Init(ctx); err != nil {
|
||||||
chart.AddY(key, series)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, name := range names {
|
|
||||||
currencyDateValue := nameCurrencyDateValue[name]
|
|
||||||
for currency, dateValue := range currencyDateValue {
|
|
||||||
series := make([]int, len(dates))
|
|
||||||
for i := range dates {
|
|
||||||
var prevValue float64
|
|
||||||
var lastValue float64
|
|
||||||
for j := range dates[:i+1] {
|
|
||||||
if newLastValue, ok := dateValue[dates[j]]; ok {
|
|
||||||
prevValue = lastValue
|
|
||||||
lastValue = newLastValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
series[i] = int(lastValue - prevValue)
|
|
||||||
}
|
|
||||||
for i := range series { // TODO no prior so no delta
|
|
||||||
if series[i] != 0 {
|
|
||||||
series[i] = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key := fmt.Sprintf("%s (%s)", name, currency)
|
|
||||||
for i := range dates {
|
|
||||||
if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
|
|
||||||
series[i] = 0
|
|
||||||
} else {
|
|
||||||
instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if slices.Min(series) != 0 || slices.Max(series) != 0 {
|
|
||||||
chart.AddY(key, series)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chart
|
|
||||||
}
|
|
||||||
primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
|
|
||||||
if len(predicted) > 0 {
|
|
||||||
primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
|
|
||||||
}
|
|
||||||
if err := primary.Render(w); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
for k, v := range instant {
|
|
||||||
fmt.Fprintf(w, "<br>\n%s = %s", k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("listening on", *httpOutput)
|
|
||||||
if err := http.ListenAndServe(*httpOutput, http.HandlerFunc(foo)); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
case "http":
|
||||||
|
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
|
||||||
|
http.Main()
|
||||||
|
case "cli":
|
||||||
|
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
|
||||||
|
cli.Main()
|
||||||
|
case "shared":
|
||||||
|
files := os.Args[2:]
|
||||||
|
os.Args = []string{os.Args[0], "cli"}
|
||||||
|
for _, f := range files {
|
||||||
|
if strings.HasPrefix(f, "-") {
|
||||||
|
os.Args = append(os.Args, f)
|
||||||
} else {
|
} else {
|
||||||
deltas, err := f.Deltas()
|
os.Args = append(os.Args, "-f", f)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
|
|
||||||
like := ledger.Likes{ledger.LikeName(*likeName)}
|
|
||||||
if *likeLedger {
|
|
||||||
like = append(like, ledger.LikeBefore(*likeBefore))
|
|
||||||
like = append(like, ledger.LikeAfter(*likeAfter))
|
|
||||||
deltas = deltas.Like(like...)
|
|
||||||
} else {
|
|
||||||
deltas = deltas.Like(like...)
|
|
||||||
like = append(like, ledger.LikeBefore(*likeBefore))
|
|
||||||
like = append(like, ledger.LikeAfter(*likeAfter))
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResult := []any{}
|
|
||||||
|
|
||||||
switch *foo {
|
|
||||||
case "reg":
|
|
||||||
sort.Slice(deltas, func(i, j int) bool {
|
|
||||||
return deltas[i].Debug() < deltas[j].Debug()
|
|
||||||
})
|
|
||||||
register := deltas.Register()
|
|
||||||
for i := range deltas {
|
|
||||||
if like.All(deltas[i]) {
|
|
||||||
if !*jsonOutput {
|
|
||||||
fmt.Printf("%s (%+v)\n", deltas[i].Debug(), register[deltas[i].Date][deltas[i].Name].Debug())
|
|
||||||
} else {
|
|
||||||
jsonResult = append(jsonResult, map[string]any{
|
|
||||||
"name": deltas[i].Name,
|
|
||||||
"delta": deltas[i],
|
|
||||||
"balance": register[deltas[i].Date][deltas[i].Name],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
os.Args = append(os.Args,
|
||||||
case "bal":
|
"-w=^Housey",
|
||||||
deltas = deltas.Like(like...)
|
"--depth=1",
|
||||||
for k, v := range deltas.Balances() {
|
"--usd",
|
||||||
results := []string{}
|
"-n",
|
||||||
for subk, subv := range v {
|
"--no-percent",
|
||||||
results = append(results, fmt.Sprintf("%s %.2f", subk, subv))
|
"bal", "^Bel", "^Zach",
|
||||||
}
|
)
|
||||||
if len(results) > 0 {
|
main()
|
||||||
if !*jsonOutput {
|
|
||||||
fmt.Printf("%s\t%s\n", k, strings.Join(results, " + "))
|
|
||||||
} else {
|
|
||||||
jsonResult = append(jsonResult, map[string]any{
|
|
||||||
"name": k,
|
|
||||||
"balance": v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("not impl %q", *foo))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *jsonOutput {
|
|
||||||
json.NewEncoder(os.Stdout).Encode(jsonResult)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Chart interface {
|
|
||||||
AddX(interface{})
|
|
||||||
AddY(string, []int)
|
|
||||||
Render(io.Writer) error
|
|
||||||
Overlap(Chart)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChart(name string) Chart {
|
|
||||||
switch name {
|
|
||||||
case "line":
|
|
||||||
return NewLine()
|
|
||||||
case "bar":
|
|
||||||
return NewBar()
|
|
||||||
case "stack":
|
|
||||||
return NewStack()
|
|
||||||
default:
|
|
||||||
panic("bad chart name " + name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Line struct {
|
|
||||||
*charts.Line
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLine() Line {
|
|
||||||
return Line{Line: charts.NewLine()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (line Line) AddX(v interface{}) {
|
|
||||||
line.SetXAxis(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (line Line) AddY(name string, v []int) {
|
|
||||||
y := make([]opts.LineData, len(v))
|
|
||||||
for i := range y {
|
|
||||||
y[i].Value = v[i]
|
|
||||||
}
|
|
||||||
line.AddSeries(name, y).
|
|
||||||
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
|
|
||||||
Stack: "stackB",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (line Line) Overlap(other Chart) {
|
|
||||||
overlapper, ok := other.(charts.Overlaper)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("cannot overlap %T", other))
|
|
||||||
}
|
|
||||||
line.Line.Overlap(overlapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bar struct {
|
|
||||||
*charts.Bar
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBar() Bar {
|
|
||||||
return Bar{Bar: charts.NewBar()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bar Bar) AddX(v interface{}) {
|
|
||||||
bar.SetXAxis(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bar Bar) AddY(name string, v []int) {
|
|
||||||
y := make([]opts.BarData, len(v))
|
|
||||||
for i := range v {
|
|
||||||
y[i].Value = v[i]
|
|
||||||
}
|
|
||||||
bar.AddSeries(name, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bar Bar) Overlap(other Chart) {
|
|
||||||
overlapper, ok := other.(charts.Overlaper)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("cannot overlap %T", other))
|
|
||||||
}
|
|
||||||
bar.Bar.Overlap(overlapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stack struct {
|
|
||||||
Bar
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStack() Stack {
|
|
||||||
bar := NewBar()
|
|
||||||
bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
|
|
||||||
return Stack{Bar: bar}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stack Stack) AddY(name string, v []int) {
|
|
||||||
y := make([]opts.BarData, len(v))
|
|
||||||
for i := range v {
|
|
||||||
y[i].Value = v[i]
|
|
||||||
}
|
|
||||||
stack.AddSeries(name, y).
|
|
||||||
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
|
|
||||||
Stack: "stackA",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../../../../../Sync/Core/tmp/moolah.dat
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html style="height: calc(100% - 4em);">
|
|
||||||
<header>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
|
||||||
</header>
|
|
||||||
<body style="height: 100%;">
|
|
||||||
<h1>Moolah2 Hub</h1>
|
|
||||||
<ul style="line-height: 3em;">
|
|
||||||
<li><a href="/transactions.html">Transactions on Shared Chase</a></li>
|
|
||||||
<li><a href="/explore.html">Explore Bel's Money</a></li>
|
|
||||||
<li><a href="/api/bal?x=y&mode=bal&likeName=AssetAccount&chart=stack&predictionMonths=120&bpi=true&zoomStart=2023-06&prediction=interest=AssetAccount:Cash%20\$%200.02&prediction=contributions=AssetAccount:Bonds%20$%201875&prediction=interest=AssetAccount:Monthly%20\$%200.03&prediction=contributions=AssetAccount:Monthly%20$%202500&predictFixedGrowth=VBTLX=0.02&predictFixedGrowth=GLD=0.02&predictFixedGrowth=FXAIX=0.03&predictFixedGrowth=FSPSX=0.03&whatIf=AssetAccount:Cash%20$%20-.10000&=">Project Bel's Net Worth</a></li>
|
|
||||||
<li><a href="/api/reg?x=y&mode=reg&likeName=Withdrawal:&chart=stack&predictionMonths=3&bpi=false&zoomStart=2023-01&prediction=autoContributions=&predictFixedGrowth=VBTLX=0&whatIf=AssetAccount:Cash%20$%20-.10000&=">Expect Bel's Expenses</a></li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
<footer>
|
|
||||||
</footer>
|
|
||||||
</html>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<html>
|
|
||||||
<header>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
|
||||||
<script>
|
|
||||||
function http(method, remote, callback, body) {
|
|
||||||
var xmlhttp = new XMLHttpRequest();
|
|
||||||
xmlhttp.onreadystatechange = function() {
|
|
||||||
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
|
||||||
callback(xmlhttp.responseText, xmlhttp.status)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xmlhttp.open(method, remote, true);
|
|
||||||
if (typeof body == "undefined") {
|
|
||||||
body = null
|
|
||||||
}
|
|
||||||
xmlhttp.send(body);
|
|
||||||
}
|
|
||||||
function callback(responseBody, responseStatus) {
|
|
||||||
}
|
|
||||||
var f = String(window.location).split("/transactions.html")[1]
|
|
||||||
if (!f) {
|
|
||||||
f = "/moolah.dat"
|
|
||||||
}
|
|
||||||
f = "." + f
|
|
||||||
function init() {
|
|
||||||
load(f)
|
|
||||||
}
|
|
||||||
function load(f) {
|
|
||||||
http("GET", "/api/transactions?f="+f, (body, status) => {
|
|
||||||
var d = JSON.parse(body)
|
|
||||||
loadBalances(d.balances)
|
|
||||||
loadDeltas(d.deltas)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function loadBalances(balances) {
|
|
||||||
var result = `<table>`
|
|
||||||
for (var k in balances) {
|
|
||||||
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(balances[k]["$"])}</td></tr>`
|
|
||||||
}
|
|
||||||
result += `</table>`
|
|
||||||
document.getElementById("bal").innerHTML = result
|
|
||||||
}
|
|
||||||
function loadDeltas(deltas) {
|
|
||||||
console.log(deltas[0])
|
|
||||||
for (var i = 0; i < deltas.length/2; i++) {
|
|
||||||
tmp = deltas[i]
|
|
||||||
deltas[i] = deltas[deltas.length-1-i]
|
|
||||||
deltas[deltas.length-1-i] = tmp
|
|
||||||
}
|
|
||||||
console.log(deltas[0])
|
|
||||||
var result = `<table>`
|
|
||||||
for (var k of deltas) {
|
|
||||||
result += `<tr>`
|
|
||||||
result += ` <td>${k.Date}</td>`
|
|
||||||
result += ` <td>${k.Description}</td>`
|
|
||||||
result += ` <td>${k.Name}</td>`
|
|
||||||
result += ` <td style="text-align: right">${k.Currency}</td>`
|
|
||||||
result += ` <td style="text-align: right">${k.Value}</td>`
|
|
||||||
result += `</tr>`
|
|
||||||
}
|
|
||||||
result += `</table>`
|
|
||||||
document.getElementById("reg").innerHTML = result
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</header>
|
|
||||||
<body onload="init();">
|
|
||||||
<h2>Moolah2</h2>
|
|
||||||
<details>
|
|
||||||
<summary>Balance</summary>
|
|
||||||
<div id="bal">
|
|
||||||
</div>
|
|
||||||
<details>
|
|
||||||
<summary><i>Look at this graph</i></summary>
|
|
||||||
<iframe style="background: white; width: 100%;" src="/api/reg?x=y&mode=reg&likeName=Withdrawal:[0123]&chart=stack&predictionMonths=6&prediction=autoContributions=&bpi=true&zoomStart=2023-01"></iframe>
|
|
||||||
</details>
|
|
||||||
</details>
|
|
||||||
<details open>
|
|
||||||
<summary>Register</summary>
|
|
||||||
<div id="reg">
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</body>
|
|
||||||
<footer>
|
|
||||||
</footer>
|
|
||||||
</html>
|
|
||||||
16
go.mod
16
go.mod
@@ -1,5 +1,17 @@
|
|||||||
module gogs.inhome.blapointe.com/ana-ledger
|
module gogs.inhome.blapointe.com/ana-ledger
|
||||||
|
|
||||||
go 1.21.1
|
go 1.23.0
|
||||||
|
|
||||||
require github.com/go-echarts/go-echarts/v2 v2.3.1
|
toolchain go1.24.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-echarts/go-echarts/v2 v2.3.1
|
||||||
|
github.com/guptarohit/asciigraph v0.7.3
|
||||||
|
golang.org/x/crypto v0.38.0
|
||||||
|
golang.org/x/time v0.11.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/term v0.32.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -2,9 +2,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-echarts/go-echarts/v2 v2.3.1 h1:Yw0HVjVTxpYm48l974dMjRzx8ni2ql0kKi/kawSgxFE=
|
github.com/go-echarts/go-echarts/v2 v2.3.1 h1:Yw0HVjVTxpYm48l974dMjRzx8ni2ql0kKi/kawSgxFE=
|
||||||
github.com/go-echarts/go-echarts/v2 v2.3.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
|
github.com/go-echarts/go-echarts/v2 v2.3.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
|
||||||
|
github.com/guptarohit/asciigraph v0.7.3 h1:p05XDDn7cBTWiBqWb30mrwxd6oU0claAjqeytllnsPY=
|
||||||
|
github.com/guptarohit/asciigraph v0.7.3/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
89
src/ana/normalize.go
Normal file
89
src/ana/normalize.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package ana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Normalizer struct {
|
||||||
|
m map[string][]normalize
|
||||||
|
}
|
||||||
|
|
||||||
|
type normalize struct {
|
||||||
|
startDate string
|
||||||
|
factor float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultNormalizer() Normalizer {
|
||||||
|
return NewNormalizer().
|
||||||
|
With("^Zach", "2025-09-01", 151). // turtle up
|
||||||
|
With("^Zach", "2023-10-05", 139). // to turtle
|
||||||
|
With("^Zach", "2021-12-30", 135). // at pluralsight
|
||||||
|
With("^Zach", "2020-07-30", 120). // to pluralsight
|
||||||
|
With("^Zach", "2019-07-16", 77). // at fedex
|
||||||
|
With("^Zach", "2017-02-16", 49). // to fedex
|
||||||
|
With("^Bel", "2025-10-01", 225). // render up
|
||||||
|
With("^Bel", "2025-04-01", 214). // lc4 at render
|
||||||
|
With("^Bel", "2023-12-05", 190). // to render
|
||||||
|
With("^Bel", "2022-12-31", 154). // at q
|
||||||
|
With("^Bel", "2022-06-30", 148). // at q
|
||||||
|
With("^Bel", "2021-12-31", 122). // at q
|
||||||
|
With("^Bel", "2020-12-31", 118). // at q
|
||||||
|
With("^Bel", "2019-12-31", 111). // at q
|
||||||
|
With("^Bel", "2018-12-31", 92). // at q
|
||||||
|
With("^Bel", "2018-02-16", 86) // to q
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNormalizer() Normalizer {
|
||||||
|
return Normalizer{
|
||||||
|
m: make(map[string][]normalize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Normalizer) With(pattern, startDate string, factor float64) Normalizer {
|
||||||
|
n.m[pattern] = append(n.m[pattern], normalize{startDate: startDate, factor: factor})
|
||||||
|
slices.SortFunc(n.m[pattern], func(a, b normalize) int {
|
||||||
|
return -1 * strings.Compare(a.startDate, b.startDate)
|
||||||
|
})
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Normalizer) Normalize(deltas ledger.Deltas) ledger.Deltas {
|
||||||
|
deltas = slices.Clone(deltas)
|
||||||
|
|
||||||
|
patterns := []string{}
|
||||||
|
for pattern := range n.m {
|
||||||
|
patterns = append(patterns, pattern)
|
||||||
|
}
|
||||||
|
like := ledger.LikeName(strings.Join(patterns, "|"))
|
||||||
|
|
||||||
|
for i, delta := range deltas {
|
||||||
|
if !like(delta) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deltas[i] = n.NormalizeDelta(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Normalizer) NormalizeDelta(delta ledger.Delta) ledger.Delta {
|
||||||
|
delta.Value /= n.NormalizeFactor(delta.Name, delta.Date)
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Normalizer) NormalizeFactor(name, date string) float64 {
|
||||||
|
for pattern := range n.m {
|
||||||
|
if regexp.MustCompile(pattern).MatchString(name) {
|
||||||
|
for _, normalize := range n.m[pattern] {
|
||||||
|
if normalize.startDate < date {
|
||||||
|
return normalize.factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
30
src/ana/normalize_test.go
Normal file
30
src/ana/normalize_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package ana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalize(t *testing.T) {
|
||||||
|
deltas := ledger.Deltas{
|
||||||
|
ledger.Delta{Date: "2024-12-04", Value: 100, Name: "Bel:Withdrawal"},
|
||||||
|
ledger.Delta{Date: "2024-12-06", Value: 100, Name: "Bel:Withdrawal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizer := NewNormalizer().
|
||||||
|
With("^Bel:", "2024-12-05", 190_000).
|
||||||
|
With("^Bel:", "2018-02-16", 86_000)
|
||||||
|
|
||||||
|
normalized := normalizer.Normalize(deltas)
|
||||||
|
for i, delta := range normalized {
|
||||||
|
t.Logf("[%d] %+v", i, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized[0].Value != 100/float64(86_000) {
|
||||||
|
t.Errorf("earliest transaction didnt use earliest salary")
|
||||||
|
}
|
||||||
|
if normalized[1].Value != 100/float64(190_000) {
|
||||||
|
t.Errorf("latest transaction didnt use latest salary")
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/bank/cache/cache.go
vendored
Normal file
107
src/bank/cache/cache.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Client bank.Agg
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ bank.Agg = Client{}
|
||||||
|
|
||||||
|
func New(client bank.Agg) Client {
|
||||||
|
return Client{Client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
|
||||||
|
k := "accounts"
|
||||||
|
result := []bank.Account{}
|
||||||
|
if err := fromCache(k, &result); err != nil {
|
||||||
|
log.Printf("%q not in cache: %v", k, err)
|
||||||
|
} else {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.Client.Accounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toCache(k, result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
|
||||||
|
k := path.Join("accounts.d", a.Account)
|
||||||
|
result := []bank.Transaction{}
|
||||||
|
if err := fromCache(k, &result); err != nil {
|
||||||
|
log.Printf("%q not in cache: %v", k, err)
|
||||||
|
} else {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.Client.Transactions(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toCache(k, result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
d = path.Join("/tmp/ana_ledger_bank_cache.d")
|
||||||
|
)
|
||||||
|
|
||||||
|
func toCache(k string, v interface{}) {
|
||||||
|
if err := _toCache(k, v); err != nil {
|
||||||
|
log.Printf("failed to cache %s: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _toCache(k string, v interface{}) error {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := path.Join(d, k)
|
||||||
|
os.MkdirAll(path.Dir(p), os.ModePerm)
|
||||||
|
if err := os.WriteFile(p, b, os.ModePerm); err != nil {
|
||||||
|
os.Remove(p)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromCache(k string, ptr interface{}) error {
|
||||||
|
p := path.Join(d, k)
|
||||||
|
if stat, err := os.Stat(p); err != nil {
|
||||||
|
return err
|
||||||
|
} else if time.Since(stat.ModTime()) > 24*time.Hour {
|
||||||
|
return fmt.Errorf("stale")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, ptr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
50
src/bank/cache/cache_integration_test.go
vendored
Normal file
50
src/bank/cache/cache_integration_test.go
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package cache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/cache"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
tellerC, err := teller.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client := cache.New(tellerC)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
i := i
|
||||||
|
client := client
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
accounts, err := client.Accounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
account := account
|
||||||
|
t.Run(account.Account, func(t *testing.T) {
|
||||||
|
transactions, err := client.Transactions(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tr := range transactions {
|
||||||
|
t.Logf("[%d] %+v", i, tr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
client.Client = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/bank/teller/application_id.txt
Normal file
1
src/bank/teller/application_id.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
app_pdvv33dtmta4fema66000
|
||||||
29
src/bank/teller/certificate.pem
Normal file
29
src/bank/teller/certificate.pem
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIExjCCAq6gAwIBAgIIGEJSCPAVjIYwDQYJKoZIhvcNAQELBQAwYTELMAkGA1UE
|
||||||
|
BhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxDzANBgNVBAcMBkxvbmRvbjEPMA0GA1UE
|
||||||
|
CgwGVGVsbGVyMR4wHAYDVQQLDBVUZWxsZXIgQXBwbGljYXRpb24gQ0EwHhcNMjUw
|
||||||
|
NTI0MDEyMzIzWhcNMjgwNTIzMDEyMzIzWjAkMSIwIAYDVQQDDBlhcHBfcGR2djMz
|
||||||
|
ZHRtdGE0ZmVtYTY2MDAwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||||
|
tWbYAc2wQBBvKJhTlo5YPLqkM5GgaYaWUqJJ5bFht+U2PL0rJHbS7oRG9fXtgb12
|
||||||
|
jEzD3CUUPpCxj7oRgYdji2SH5NKmo3M85/Cry1y5pmotmqGlqrt01zStj2A+FuzR
|
||||||
|
BqdmVb1CIE2iJmttGn3C3f9OCbV5kMKreu4DhVdPW7eafpo+yIMaoJxx2CAMqw5Y
|
||||||
|
GK2NNpehMHzpL4Z0032yEQJaBblYeUT4zgMCxoupXQ7hCsyjn/ws7ocpMHXY9r08
|
||||||
|
6PjaY1j6wYwX9OUJDXz0zgoeShr2vcfkjBc1QavRLVQQUWgYJevS/8rCfo32B7ub
|
||||||
|
Ym1CmWz2cY3+4UV3uihJgQIDAQABo4G+MIG7MA4GA1UdDwEB/wQEAwIF4DATBgNV
|
||||||
|
HSUEDDAKBggrBgEFBQcDAjCBkwYDVR0jBIGLMIGIgBSEq++simSLxXkuNSUKjel6
|
||||||
|
pmhxmqFlpGMwYTELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxDzANBgNV
|
||||||
|
BAcMBkxvbmRvbjEPMA0GA1UECgwGVGVsbGVyMR4wHAYDVQQLDBVUZWxsZXIgQXBw
|
||||||
|
bGljYXRpb24gQ0GCCQDiNWG/vm85CTANBgkqhkiG9w0BAQsFAAOCAgEARv/Kjwcu
|
||||||
|
ppXbTf9pvsesEgo6O+OM1qW73SkmQeB5ZfF6KEOn57SujTjVQRlBGhVs//+Ezlav
|
||||||
|
GKKm4Xw0eCKUfISIgz+nY7lSlVvW2REZAdpuiY6owsqtiL/Fe/RBvkUmNSWnu8vc
|
||||||
|
OIfnpqP6flKL7KjXwQXoI/Xt9Zw6D56dHQi5hYgYtP0HqtEZn6vdtroHM3mbIL6D
|
||||||
|
Dnfdhb3ywwVZTiQE5ceQk+StDYuzTz7PNQL6IWxcH4j73dQlvFzMLSDm9yA2NguK
|
||||||
|
SEiRi4gYmluQxSiTN3gYqfOMeVv/buklHknkRHCInOTOiDWX8ku0FWwApiwsmvLS
|
||||||
|
3z/9WK4QEMLQfxBbp7UJePWx0Tq6KE61gcTgMbjqz+Xi9n2KM6RenDdMg2rQXWX4
|
||||||
|
xqC0sOOuSS19WFCGYDBRjI2JOQqfeEymq8pQsrGm+XyzCPMi+eFyLZqkRiT85MCr
|
||||||
|
IZuUWpMTcqILGP3Ar5Z0ppDI1ppVDhWMoq5EYx0iuJjNQZEtHlbu1j+cw6uVAoqJ
|
||||||
|
T/E5/qKcLRkkDKV68B+CP2z+iOXH5M1dcYFu6yEkKxVbEuTJNyN5gHhBATyIoMQ0
|
||||||
|
RdohL8F9hdOHvTLo89xrFEHiLExIT0NISjlT0M/mTqq4rbr5kp5W/ee9h5uSVUJ8
|
||||||
|
3nCv0NH3CX0Ygjzd7Czd9hWRahQz+vv55u4=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
64
src/bank/teller/init.go
Normal file
64
src/bank/teller/init.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package teller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed application_id.txt
|
||||||
|
applicationId string
|
||||||
|
//go:embed init.html
|
||||||
|
initHTML string
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(ctx context.Context) error {
|
||||||
|
environment := "development"
|
||||||
|
if sandbox := !slices.Contains(os.Args, "forreal"); sandbox {
|
||||||
|
environment = "sandbox"
|
||||||
|
}
|
||||||
|
fmt.Printf("environment=%q\n", environment)
|
||||||
|
|
||||||
|
newTokens := make(chan string)
|
||||||
|
defer close(newTokens)
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: ":20000",
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
newTokens <- string(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.New("initHTML").Parse(initHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Execute(w, map[string]string{
|
||||||
|
"applicationId": applicationId,
|
||||||
|
"environment": environment,
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
go s.ListenAndServe()
|
||||||
|
|
||||||
|
fmt.Println("Open http://localhost:20000")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case newToken := <-newTokens:
|
||||||
|
return fmt.Errorf("not impl: %q >> token.txt", newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
58
src/bank/teller/init.html
Normal file
58
src/bank/teller/init.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<button id="teller-connect">Connect to your bank</button>
|
||||||
|
|
||||||
|
<h3 id="log">
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<script src="https://cdn.teller.io/connect/connect.js"></script>
|
||||||
|
<script>
|
||||||
|
function logme(msg) {
|
||||||
|
document.getElementById("log").innerHTML += `<br>* ${msg}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function http(method, remote, callback, body) {
|
||||||
|
var xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange = function() {
|
||||||
|
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||||
|
callback(xmlhttp.responseText, xmlhttp.status)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xmlhttp.open(method, remote, true);
|
||||||
|
if (typeof body == "undefined") {
|
||||||
|
body = null
|
||||||
|
}
|
||||||
|
xmlhttp.send(body);
|
||||||
|
}
|
||||||
|
function callback(responseBody, responseStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var tellerConnect = TellerConnect.setup({
|
||||||
|
applicationId: "{{.applicationId}}",
|
||||||
|
environment: "{{.environment}}",
|
||||||
|
products: ["verify", "balance", "transactions"],
|
||||||
|
onInit: function() {
|
||||||
|
logme("Teller Connect has initialized")
|
||||||
|
},
|
||||||
|
onSuccess: function(enrollment) {
|
||||||
|
logme(`User enrolled successfully: ${enrollment.accessToken}`)
|
||||||
|
http("post", "/", callback, enrollment.accessToken)
|
||||||
|
},
|
||||||
|
onExit: function() {
|
||||||
|
logme("User closed Teller Connect")
|
||||||
|
},
|
||||||
|
onFailure: function(failure) {
|
||||||
|
logme(`Failed: type=${failure.type} code=${failure.code} message=${failure.message}`)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = document.getElementById("teller-connect");
|
||||||
|
el.addEventListener("click", function() {
|
||||||
|
tellerConnect.open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
src/bank/teller/private_key.pem
Normal file
28
src/bank/teller/private_key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1ZtgBzbBAEG8o
|
||||||
|
mFOWjlg8uqQzkaBphpZSoknlsWG35TY8vSskdtLuhEb19e2BvXaMTMPcJRQ+kLGP
|
||||||
|
uhGBh2OLZIfk0qajczzn8KvLXLmmai2aoaWqu3TXNK2PYD4W7NEGp2ZVvUIgTaIm
|
||||||
|
a20afcLd/04JtXmQwqt67gOFV09bt5p+mj7IgxqgnHHYIAyrDlgYrY02l6EwfOkv
|
||||||
|
hnTTfbIRAloFuVh5RPjOAwLGi6ldDuEKzKOf/Czuhykwddj2vTzo+NpjWPrBjBf0
|
||||||
|
5QkNfPTOCh5KGva9x+SMFzVBq9EtVBBRaBgl69L/ysJ+jfYHu5tibUKZbPZxjf7h
|
||||||
|
RXe6KEmBAgMBAAECggEAExDEEyxzIciYZkPcRS6gx4E2UNU1buHeWsED00hZZOKK
|
||||||
|
WMfpCOQUN01fx+oZFFG9a/GFhFXBUvISN3Du9hYsuDHQtpQNP5CVDiuVYsJUINF4
|
||||||
|
CZCDwPYCybuXokITRIWPUou1jb1efdaq/C6+QNKG8J4srYiNRlGvhDQP2qvag2ET
|
||||||
|
YtY+6OuQPF9QSeoKLBCJJi8bP/wy3OfOQkTqvlpyEGVOUc5utGuziITShL2mLtQw
|
||||||
|
1O22r4BIFtkvnhM4kGimbSDMYwtAgMrBrktR4Xp7JICV6eZn6fJgjpYOix/4ILxS
|
||||||
|
Ri69qPLso4qF+ML/vsTHlxQj7FwZDZozT4hm3DK3gQKBgQDdnEAdQYuKIvwrHflk
|
||||||
|
Pxb9q7MjrMJMGiA9GvSVTimeWu68kH2y/dUUQyBtr8SRCScNUmhRCfaZCBakSYR0
|
||||||
|
0v+6FLXleI28dD93Qzn70G0N6kVdKLdM3Rt0r+fojPhP+8pHn74UzvcoGUDOzoax
|
||||||
|
VPvHLe19wqrbRFIU4IAE6qjmNQKBgQDRjUFjRJIh4wgrB6rROeoTtYRRyS9RLliw
|
||||||
|
dpH78Vz7OYTx7qnvtVOl8led82Ott7hEXcK9Ihobz8uPCewW/x+sPYty13shT46K
|
||||||
|
8zEj9FoP9XhAS3MOqLDNx7h5jv7nvua8aRQEPkF30SmPqb+X4nN5+JjuJiTqWgJ+
|
||||||
|
5nfnyX4PnQKBgQCE87brVmV27GJJI+R5JfiPG7GPl5fBvHLW9hMCeDAz1u4fprgi
|
||||||
|
6HIrg9IyvB67vLf3IBeBdu7BBL9AtPKIfAX8B2zRTLAL/doNnQFud67ViFUw/Lpr
|
||||||
|
nMNaECabt+dJZRAIRGfvZ/OT1QKyj+jy/r9G0eEHcAC9J5HvAHkNehL2eQKBgAU+
|
||||||
|
a6x4Qs/mRoYNIxEpSdpEaJNDXZPCfSWtUenkGFeREOqc9lOxTe6RKfAh7xShzFKp
|
||||||
|
pf3lpJGdmZJyxR2uNLSytZKiIcqrmv2PKGOl8bsEgYXaXX64afQ8Uzl3gpl6BXwh
|
||||||
|
hQa2KB0/drLJpKnAWPNsbSdIfRQAPJ/AVK/QMv9hAoGAMtmCDJK1KVZcpFJbFSMk
|
||||||
|
pPJjcp1XRWKsiBOPfUKlwbBSFDBUnhyPcsfL9ooAwzBnBcpNf6S5I+I22AdDuk8E
|
||||||
|
S3uOBBlNhoecWN1tqVlcbTR1p5kXV0WVAcQ09hGPqja5ghrTTpWbC7SviSVNa0fx
|
||||||
|
LXKvqDx5qTleSmrPwPs5TW4=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
95
src/bank/teller/teller.go
Normal file
95
src/bank/teller/teller.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package teller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cert tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ bank.Agg = Client{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed certificate.pem
|
||||||
|
certificate []byte
|
||||||
|
//go:embed private_key.pem
|
||||||
|
privateKey []byte
|
||||||
|
//go:embed token.txt
|
||||||
|
Tokens string
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() (Client, error) {
|
||||||
|
cert, err := tls.X509KeyPair(certificate, privateKey)
|
||||||
|
return Client{cert: cert}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
|
||||||
|
var result []bank.Account
|
||||||
|
for _, token := range strings.Fields(Tokens) {
|
||||||
|
var more []bank.Account
|
||||||
|
if err := c.get(ctx, "https://api.teller.io/accounts", token, &more); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range more {
|
||||||
|
more[i].Token = token
|
||||||
|
}
|
||||||
|
result = append(result, more...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
|
||||||
|
var result []bank.Transaction
|
||||||
|
err := c.get(ctx, "https://api.teller.io/accounts/"+a.Account+"/transactions", a.Token, &result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var limiter = rate.NewLimiter(0.1, 1)
|
||||||
|
|
||||||
|
func (c Client) get(ctx context.Context, url, token string, ptr interface{}) error {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Teller.Get(%s, %s)", url, token)
|
||||||
|
httpc := &http.Client{
|
||||||
|
Timeout: time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{c.cert}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(token, "")
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
resp, err := httpc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if err := json.Unmarshal(b, &ptr); err != nil {
|
||||||
|
return fmt.Errorf("cannot unmarshal: %w: %s", err, b)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
41
src/bank/teller/teller_integration_test.go
Normal file
41
src/bank/teller/teller_integration_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package teller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
teller.Tokens = "test_token_bfu2cyvq3il6o"
|
||||||
|
|
||||||
|
c, err := teller.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
accounts, err := c.Accounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
account := account
|
||||||
|
t.Run(account.Account, func(t *testing.T) {
|
||||||
|
transactions, err := c.Transactions(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tr := range transactions {
|
||||||
|
t.Logf("[%d] %+v", i, tr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/bank/teller/teller_manual_test.go
Normal file
59
src/bank/teller/teller_manual_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//go:build manual
|
||||||
|
|
||||||
|
package teller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegration(t *testing.T) {
|
||||||
|
teller.Tokens = "test_token_bfu2cyvq3il6o"
|
||||||
|
|
||||||
|
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
|
||||||
|
cert, err := tls.LoadX509KeyPair("./certificate.pem", "./private_key.pem")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &http.Client{
|
||||||
|
Timeout: time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
|
||||||
|
|
||||||
|
for _, url := range []string{
|
||||||
|
"https://api.teller.io/accounts",
|
||||||
|
"https://api.teller.io/accounts/acc_pdvv4810fi9hmrcn6g000/transactions",
|
||||||
|
} {
|
||||||
|
url := url
|
||||||
|
t.Run(url, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.SetBasicAuth("test_token_bfu2cyvq3il6o", "")
|
||||||
|
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if code := resp.StatusCode; code >= 300 {
|
||||||
|
t.Fatalf("(%d) %s", code, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("(%d) %s", resp.StatusCode, body)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/bank/teller/token.txt
Normal file
2
src/bank/teller/token.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
token_2utqstwpn3pxwgvyno56hqdehq
|
||||||
|
token_vr6dnzvfv7c24wuxtmnnzyiqbm
|
||||||
31
src/bank/types.go
Normal file
31
src/bank/types.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package bank
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Agg interface {
|
||||||
|
Accounts(context.Context) ([]Account, error)
|
||||||
|
Transactions(context.Context, Account) ([]Transaction, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Institution struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"institution"`
|
||||||
|
Name string `json:"last_four"`
|
||||||
|
Account string `json:"id"`
|
||||||
|
Token string `json:"__token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
Amount float64 `json:"amount,string"`
|
||||||
|
Details struct {
|
||||||
|
ProcessingStatus string `json:"processing_status"`
|
||||||
|
CounterParty struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"counterparty"`
|
||||||
|
} `json:"details"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
@@ -4,13 +4,82 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Normalizer interface {
|
||||||
|
NormalizeFactor(string, string) float64
|
||||||
|
}
|
||||||
|
|
||||||
type Balances map[string]Balance
|
type Balances map[string]Balance
|
||||||
|
|
||||||
type Balance map[Currency]float64
|
type Balance map[Currency]float64
|
||||||
|
|
||||||
|
func (balances Balances) Sub(other Balances) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
for k, v := range balances {
|
||||||
|
result[k] = v.Sub(other[k])
|
||||||
|
}
|
||||||
|
for k, v := range other {
|
||||||
|
if _, ok := balances[k]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[k] = v.Invert()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balances Balances) Nonzero() Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
for k, v := range balances {
|
||||||
|
if nonzero := v.Nonzero(); len(nonzero) > 0 {
|
||||||
|
result[k] = nonzero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balances Balances) NotLike(pattern string) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
p := regexp.MustCompile(pattern)
|
||||||
|
for k, v := range balances {
|
||||||
|
if !p.MatchString(k) {
|
||||||
|
result[k] = maps.Clone(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balances Balances) KindaLike(like Like) Balances {
|
||||||
|
ks := []string{}
|
||||||
|
for k := range balances {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
|
||||||
|
result := make(Balances)
|
||||||
|
for _, k := range ks {
|
||||||
|
v := balances[k]
|
||||||
|
if like(v.kinda(k)) {
|
||||||
|
result[k] = maps.Clone(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Balance) kinda(k string) Delta {
|
||||||
|
return Delta{
|
||||||
|
Date: time.Now().Format("2006-01-02"),
|
||||||
|
Name: k,
|
||||||
|
Value: b[USD],
|
||||||
|
Description: k,
|
||||||
|
Transaction: k,
|
||||||
|
Payee: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (balances Balances) Like(pattern string) Balances {
|
func (balances Balances) Like(pattern string) Balances {
|
||||||
result := make(Balances)
|
result := make(Balances)
|
||||||
p := regexp.MustCompile(pattern)
|
p := regexp.MustCompile(pattern)
|
||||||
@@ -22,22 +91,80 @@ func (balances Balances) Like(pattern string) Balances {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (balances Balances) Group(pattern string) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
p := regexp.MustCompile(pattern)
|
||||||
|
for k, v := range balances {
|
||||||
|
k2 := k
|
||||||
|
if p.MatchString(k) {
|
||||||
|
k2 = p.FindString(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
was := result[k2]
|
||||||
|
if was == nil {
|
||||||
|
was = make(Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k3, v3 := range v {
|
||||||
|
was[k3] += v3
|
||||||
|
}
|
||||||
|
|
||||||
|
result[k2] = was
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balances Balances) KindaGroup(group Group) Balances {
|
||||||
|
ks := []string{}
|
||||||
|
for k := range balances {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
|
||||||
|
result := make(Balances)
|
||||||
|
for _, k := range ks {
|
||||||
|
v := balances[k]
|
||||||
|
k2 := k
|
||||||
|
if k3 := group(v.kinda(k)).Name; k2 != k3 {
|
||||||
|
k2 = k3
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result[k2]; !ok {
|
||||||
|
result[k2] = make(Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k3, v3 := range v {
|
||||||
|
result[k2][k3] += v3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (balances Balances) WithBPIs(bpis BPIs) Balances {
|
func (balances Balances) WithBPIs(bpis BPIs) Balances {
|
||||||
return balances.WithBPIsAt(bpis, "9")
|
return balances.WithBPIsAt(bpis, "9")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances {
|
func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances {
|
||||||
|
ks := []string{}
|
||||||
|
for k := range balances {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
|
||||||
result := make(Balances)
|
result := make(Balances)
|
||||||
for k, v := range balances {
|
for _, k := range ks {
|
||||||
|
v := balances[k]
|
||||||
if _, ok := result[k]; !ok {
|
if _, ok := result[k]; !ok {
|
||||||
result[k] = make(Balance)
|
result[k] = make(Balance)
|
||||||
}
|
}
|
||||||
for k2, v2 := range v {
|
for k2, v2 := range v {
|
||||||
scalar := 1.0
|
if k2 == USD {
|
||||||
if k2 != USD {
|
result[k][USD] = result[k][USD] + v2
|
||||||
scalar = bpis[k2].Lookup(date)
|
} else if scalar := bpis[k2].Lookup(date); scalar != nil {
|
||||||
|
result[k][USD] = result[k][USD] + v2*(*scalar)
|
||||||
|
} else {
|
||||||
|
result[k][k2] = result[k][k2] + v2
|
||||||
}
|
}
|
||||||
result[k][USD] += v2 * scalar
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -64,6 +191,39 @@ func (balances Balances) Push(d Delta) {
|
|||||||
balances[d.Name].Push(d)
|
balances[d.Name].Push(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (balance Balance) Sub(other Balance) Balance {
|
||||||
|
return balance.Sum(other.Invert())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balance Balance) Sum(other Balance) Balance {
|
||||||
|
result := make(Balance)
|
||||||
|
for k, v := range balance {
|
||||||
|
result[k] += v
|
||||||
|
}
|
||||||
|
for k, v := range other {
|
||||||
|
result[k] += v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balance Balance) Nonzero() Balance {
|
||||||
|
result := make(Balance)
|
||||||
|
for k, v := range balance {
|
||||||
|
if v != 0 {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (balance Balance) Invert() Balance {
|
||||||
|
result := make(Balance)
|
||||||
|
for k, v := range balance {
|
||||||
|
result[k] = v * -1.0
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (balance Balance) Push(d Delta) {
|
func (balance Balance) Push(d Delta) {
|
||||||
if _, ok := balance[d.Currency]; !ok {
|
if _, ok := balance[d.Currency]; !ok {
|
||||||
balance[d.Currency] = 0
|
balance[d.Currency] = 0
|
||||||
@@ -90,3 +250,14 @@ func (balance Balance) Debug() string {
|
|||||||
}
|
}
|
||||||
return strings.Join(result, " + ")
|
return strings.Join(result, " + ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (balances Balances) Normalize(n Normalizer, date string) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
for name, balance := range balances {
|
||||||
|
result[name] = make(Balance)
|
||||||
|
for currency, value := range balance {
|
||||||
|
result[name][currency] = value / n.NormalizeFactor(name, date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,4 +51,38 @@ func TestBalances(t *testing.T) {
|
|||||||
t.Error("didnt sum other", b["ab"])
|
t.Error("didnt sum other", b["ab"])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("like", func(t *testing.T) {
|
||||||
|
was := make(Balances)
|
||||||
|
was.Push(Delta{Name: "a", Currency: USD, Value: 0.1})
|
||||||
|
was.Push(Delta{Name: "ab", Currency: USD, Value: 1.2})
|
||||||
|
|
||||||
|
got := was.Like(`^ab$`)
|
||||||
|
if len(got) != 1 || got["ab"][USD] != 1.2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = was.NotLike(`^ab$`)
|
||||||
|
if len(got) != 1 || got["a"][USD] != 0.1 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("group", func(t *testing.T) {
|
||||||
|
was := make(Balances)
|
||||||
|
was.Push(Delta{Name: "a:1", Currency: USD, Value: 0.1})
|
||||||
|
was.Push(Delta{Name: "a:2", Currency: USD, Value: 1.2})
|
||||||
|
was.Push(Delta{Name: "b:1", Currency: USD, Value: 2.2})
|
||||||
|
|
||||||
|
got := was.Group(`^[^:]*`)
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got["a"][USD] != 1.3 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got["b"][USD] != 2.2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func NewBPIs(p string) (BPIs, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bpi BPI) Lookup(date string) float64 {
|
func (bpi BPI) Lookup(date string) *float64 {
|
||||||
var closestWithoutGoingOver string
|
var closestWithoutGoingOver string
|
||||||
for k := range bpi {
|
for k := range bpi {
|
||||||
if k <= date && k > closestWithoutGoingOver {
|
if k <= date && k > closestWithoutGoingOver {
|
||||||
@@ -58,7 +58,8 @@ func (bpi BPI) Lookup(date string) float64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if closestWithoutGoingOver == "" {
|
if closestWithoutGoingOver == "" {
|
||||||
return 0
|
return nil
|
||||||
}
|
}
|
||||||
return bpi[closestWithoutGoingOver]
|
f := bpi[closestWithoutGoingOver]
|
||||||
|
return &f
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,16 +157,16 @@ P 2023-10-22 07:33:56 GME $17.18
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := got["USDC"].Lookup("2099-01-01"); got != 0 {
|
if got := got["USDC"].Lookup("2099-01-01"); got != nil {
|
||||||
t.Error("default got != 0:", got)
|
t.Error("default got != 0:", got)
|
||||||
}
|
}
|
||||||
if got := got["GME"].Lookup("2099-01-01"); got != 17.18 {
|
if got := got["GME"].Lookup("2099-01-01"); got == nil || *got != 17.18 {
|
||||||
t.Errorf("shouldve returned latest but got %v", got)
|
t.Errorf("shouldve returned latest but got %v", got)
|
||||||
}
|
}
|
||||||
if got := got["GME"].Lookup("2023-09-19"); got != 18.22 {
|
if got := got["GME"].Lookup("2023-09-19"); got == nil || *got != 18.22 {
|
||||||
t.Errorf("shouldve returned one day before but got %v", got)
|
t.Errorf("shouldve returned one day before but got %v", got)
|
||||||
}
|
}
|
||||||
if got := got["GME"].Lookup("2023-09-18"); got != 18.22 {
|
if got := got["GME"].Lookup("2023-09-18"); got == nil || *got != 18.22 {
|
||||||
t.Errorf("shouldve returned day of but got %v", got)
|
t.Errorf("shouldve returned day of but got %v", got)
|
||||||
}
|
}
|
||||||
t.Log(got)
|
t.Log(got)
|
||||||
|
|||||||
@@ -14,25 +14,52 @@ type Delta struct {
|
|||||||
Value float64
|
Value float64
|
||||||
Currency Currency
|
Currency Currency
|
||||||
Description string
|
Description string
|
||||||
|
Transaction string
|
||||||
|
Payee bool
|
||||||
|
|
||||||
isSet bool
|
isSet bool
|
||||||
|
fileName string
|
||||||
|
lineNo int
|
||||||
|
with []Delta
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDelta(d, desc, name string, v float64, c string, isSet bool) Delta {
|
func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool, fileName string, lineNo int) Delta {
|
||||||
return Delta{
|
return Delta{
|
||||||
Date: d,
|
Date: d,
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: v,
|
Value: v,
|
||||||
Currency: Currency(c),
|
Currency: Currency(c),
|
||||||
Description: desc,
|
Description: desc,
|
||||||
|
Transaction: transaction,
|
||||||
|
Payee: payee,
|
||||||
|
|
||||||
isSet: isSet,
|
isSet: isSet,
|
||||||
|
fileName: fileName,
|
||||||
|
lineNo: lineNo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (delta Delta) withWith(other Delta) Delta {
|
||||||
|
other.with = nil
|
||||||
|
delta.with = append(delta.with, other)
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (delta Delta) equivalent(other Delta) bool {
|
||||||
|
delta.fileName = ""
|
||||||
|
delta.lineNo = 0
|
||||||
|
delta.with = nil
|
||||||
|
other.fileName = ""
|
||||||
|
other.lineNo = 0
|
||||||
|
other.with = nil
|
||||||
|
return fmt.Sprintf("%+v", delta) == fmt.Sprintf("%+v", other)
|
||||||
|
}
|
||||||
|
|
||||||
func (delta Delta) Debug() string {
|
func (delta Delta) Debug() string {
|
||||||
return fmt.Sprintf("{@%s %s:\"%s\" %s%.2f %s}", delta.Date, delta.Name, delta.Description, func() string {
|
return fmt.Sprintf("{@%s %s(payee=%v):\"%s\" %s%.2f %s @%s#%d}", delta.Date, delta.Name, delta.Payee, delta.Description, func() string {
|
||||||
if !delta.isSet {
|
if !delta.isSet {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "= "
|
return "= "
|
||||||
}(), delta.Value, delta.Currency)
|
}(), delta.Value, delta.Currency, delta.fileName, delta.lineNo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ import (
|
|||||||
|
|
||||||
func TestDelta(t *testing.T) {
|
func TestDelta(t *testing.T) {
|
||||||
d := "2099-08-07"
|
d := "2099-08-07"
|
||||||
delta := newDelta(d, "", "name", 34.56, "$", false)
|
delta := newDelta("x", true, d, "", "name", 34.56, "$", false, "", 0)
|
||||||
|
|
||||||
|
if delta.Transaction != "x" {
|
||||||
|
t.Error(delta.Transaction)
|
||||||
|
}
|
||||||
|
if !delta.Payee {
|
||||||
|
t.Error(delta.Payee)
|
||||||
|
}
|
||||||
if delta.Date != d {
|
if delta.Date != d {
|
||||||
t.Error(delta.Date)
|
t.Error(delta.Date)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,12 +57,8 @@ func (deltas Deltas) Balances() Balances {
|
|||||||
}
|
}
|
||||||
result[delta.Name][delta.Currency] += delta.Value
|
result[delta.Name][delta.Currency] += delta.Value
|
||||||
if result[delta.Name][delta.Currency] < 0.000000001 && result[delta.Name][delta.Currency] > -0.000000001 {
|
if result[delta.Name][delta.Currency] < 0.000000001 && result[delta.Name][delta.Currency] > -0.000000001 {
|
||||||
delete(result[delta.Name], delta.Currency)
|
result[delta.Name][delta.Currency] = 0
|
||||||
if len(result[delta.Name]) == 0 {
|
|
||||||
delete(result, delta.Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ func TestDeltas(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
balances := deltas.Balances()
|
balances := deltas.Balances()
|
||||||
if len(balances) != 1 {
|
if len(balances) != 2 {
|
||||||
t.Error(len(balances), balances)
|
t.Error(len(balances), balances)
|
||||||
}
|
}
|
||||||
|
if balances["a"][""] != 0 {
|
||||||
|
t.Error(balances["a"])
|
||||||
|
}
|
||||||
if balances["b"][""] != 1.3 {
|
if balances["b"][""] != 1.3 {
|
||||||
t.Error(balances["b"])
|
t.Error(balances["b"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ package ledger
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +23,78 @@ func NewFiles(p string, q ...string) (Files, error) {
|
|||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) Add(payee string, delta Delta) error {
|
func (files Files) Amend(old, now Delta) error {
|
||||||
|
if now.isSet {
|
||||||
|
return fmt.Errorf("cannot ammend: immutable isSet is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
xactions, err := files.transactions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction transaction
|
||||||
|
for _, xaction := range xactions {
|
||||||
|
if xaction.name != old.Transaction {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
transaction = xaction
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if transaction.payee == old.Name {
|
||||||
|
if len(transaction.recipients) != 1 {
|
||||||
|
return fmt.Errorf("cannot amend: modifying original payee, but many recipients cant share new value")
|
||||||
|
}
|
||||||
|
transaction.payee, transaction.recipients[0].name, transaction.recipients[0].value = transaction.recipients[0].name, transaction.payee, transaction.recipients[0].value*-1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := -1
|
||||||
|
for i, recipient := range transaction.recipients {
|
||||||
|
if recipient.name == old.Name && recipient.value == old.Value {
|
||||||
|
idx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx == -1 {
|
||||||
|
return fmt.Errorf("cannot amend: no recipient with name %q value %.2f found in %+v to set new value", old.Name, old.Value, transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
old.Value *= -1
|
||||||
|
return files.Add(transaction.payee, []Delta{old, now}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (files Files) paths() []string {
|
||||||
|
result := make([]string, 0, len(files))
|
||||||
|
for i := range files {
|
||||||
|
if info, err := os.Stat(files[i]); err == nil && info.IsDir() {
|
||||||
|
if err := filepath.WalkDir(files[i], func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = append(result, files[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (files Files) Add(payee string, deltas ...Delta) error {
|
||||||
|
for _, delta := range deltas {
|
||||||
|
if err := files.add(payee, delta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (files Files) add(payee string, delta Delta) error {
|
||||||
currencyValue := fmt.Sprintf("%s%.2f", delta.Currency, delta.Value)
|
currencyValue := fmt.Sprintf("%s%.2f", delta.Currency, delta.Value)
|
||||||
if delta.Currency != USD {
|
if delta.Currency != USD {
|
||||||
currencyValue = fmt.Sprintf("%.2f %s", delta.Value, delta.Currency)
|
currencyValue = fmt.Sprintf("%.2f %s", delta.Value, delta.Currency)
|
||||||
@@ -31,11 +107,13 @@ func (files Files) Add(payee string, delta Delta) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) append(s string) error {
|
func (files Files) append(s string) error {
|
||||||
if err := files.trimTrainlingWhitespace(); err != nil {
|
p := path.Join(path.Dir(files.paths()[0]), "inbox.txt")
|
||||||
|
|
||||||
|
if err := files.trimTrailingWhitespace(p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(string(files[0]), os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -45,8 +123,8 @@ func (files Files) append(s string) error {
|
|||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) trimTrainlingWhitespace() error {
|
func (files Files) trimTrailingWhitespace(p string) error {
|
||||||
idx, err := files._lastNonWhitespacePos()
|
idx, err := files._lastNonWhitespacePos(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -54,7 +132,7 @@ func (files Files) trimTrainlingWhitespace() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(string(files[0]), os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,8 +141,8 @@ func (files Files) trimTrainlingWhitespace() error {
|
|||||||
return f.Truncate(int64(idx + 1))
|
return f.Truncate(int64(idx + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) _lastNonWhitespacePos() (int, error) {
|
func (files Files) _lastNonWhitespacePos(p string) (int, error) {
|
||||||
f, err := os.Open(string(files[0]))
|
f, err := os.Open(p)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return -1, nil
|
return -1, nil
|
||||||
}
|
}
|
||||||
@@ -96,38 +174,14 @@ func (files Files) Deltas(like ...Like) (Deltas, error) {
|
|||||||
|
|
||||||
result := make(Deltas, 0, len(transactions)*2)
|
result := make(Deltas, 0, len(transactions)*2)
|
||||||
for _, transaction := range transactions {
|
for _, transaction := range transactions {
|
||||||
sums := map[string]float64{}
|
result = append(result, transaction.deltas()...)
|
||||||
for _, recipient := range transaction.recipients {
|
|
||||||
sums[recipient.currency] += recipient.value
|
|
||||||
delta := newDelta(
|
|
||||||
transaction.date,
|
|
||||||
transaction.description,
|
|
||||||
recipient.name,
|
|
||||||
recipient.value,
|
|
||||||
recipient.currency,
|
|
||||||
recipient.isSet,
|
|
||||||
)
|
|
||||||
result = append(result, delta)
|
|
||||||
}
|
|
||||||
for currency, value := range sums {
|
|
||||||
if value == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if transaction.payee == "" {
|
|
||||||
//return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction)
|
|
||||||
} else {
|
|
||||||
delta := newDelta(
|
|
||||||
transaction.date,
|
|
||||||
transaction.description,
|
|
||||||
transaction.payee,
|
|
||||||
-1.0*value,
|
|
||||||
currency,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
result = append(result, delta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
slices.SortFunc(result, func(a, b Delta) int {
|
||||||
|
if str := strings.Compare(a.Date+a.fileName, b.Date+b.fileName); str != 0 {
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
|
return a.lineNo - b.lineNo
|
||||||
|
})
|
||||||
|
|
||||||
balances := make(Balances)
|
balances := make(Balances)
|
||||||
for i := range result {
|
for i := range result {
|
||||||
|
|||||||
@@ -7,6 +7,160 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFileAmend(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
from string
|
||||||
|
old Delta
|
||||||
|
now Delta
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"multi recipient": {
|
||||||
|
from: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipientA $3.45
|
||||||
|
recipientB $6.45
|
||||||
|
payee
|
||||||
|
`,
|
||||||
|
old: Delta{
|
||||||
|
Date: "2006-01-02",
|
||||||
|
Name: "recipientB",
|
||||||
|
Value: 6.45,
|
||||||
|
Currency: "$",
|
||||||
|
Description: "description",
|
||||||
|
},
|
||||||
|
now: Delta{
|
||||||
|
Date: "2106-11-12",
|
||||||
|
Name: "recipientC",
|
||||||
|
Value: 16.45,
|
||||||
|
Currency: "T",
|
||||||
|
Description: "1description",
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipientB $-6.45
|
||||||
|
payee
|
||||||
|
2106-11-12 1description
|
||||||
|
recipientC 16.45 T
|
||||||
|
payee`,
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
from: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipient $3.45
|
||||||
|
payee $-3.45
|
||||||
|
`,
|
||||||
|
old: Delta{
|
||||||
|
Date: "2006-01-02",
|
||||||
|
Name: "recipient",
|
||||||
|
Value: 3.45,
|
||||||
|
Currency: "$",
|
||||||
|
Description: "description",
|
||||||
|
},
|
||||||
|
now: Delta{
|
||||||
|
Date: "2106-11-12",
|
||||||
|
Name: "1recipient",
|
||||||
|
Value: 13.45,
|
||||||
|
Currency: "T",
|
||||||
|
Description: "1description",
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipient $-3.45
|
||||||
|
payee
|
||||||
|
2106-11-12 1description
|
||||||
|
1recipient 13.45 T
|
||||||
|
payee`,
|
||||||
|
},
|
||||||
|
"payee": {
|
||||||
|
from: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipient $3.45
|
||||||
|
payee
|
||||||
|
`,
|
||||||
|
old: Delta{
|
||||||
|
Date: "2006-01-02",
|
||||||
|
Name: "payee",
|
||||||
|
Value: -3.45,
|
||||||
|
Currency: "$",
|
||||||
|
Description: "description",
|
||||||
|
},
|
||||||
|
now: Delta{
|
||||||
|
Date: "2106-11-12",
|
||||||
|
Name: "1payee",
|
||||||
|
Value: -13.45,
|
||||||
|
Currency: "T",
|
||||||
|
Description: "1description",
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
2006-01-02 description
|
||||||
|
payee $3.45
|
||||||
|
recipient
|
||||||
|
2106-11-12 1description
|
||||||
|
1payee -13.45 T
|
||||||
|
recipient`,
|
||||||
|
},
|
||||||
|
"was set": {
|
||||||
|
from: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipient $3.45
|
||||||
|
payee
|
||||||
|
`,
|
||||||
|
old: Delta{
|
||||||
|
Date: "2006-01-02",
|
||||||
|
Name: "recipient",
|
||||||
|
Value: 3.45,
|
||||||
|
Currency: "$",
|
||||||
|
Description: "description",
|
||||||
|
},
|
||||||
|
now: Delta{
|
||||||
|
Date: "2106-11-12",
|
||||||
|
Name: "1recipient",
|
||||||
|
Value: 13.45,
|
||||||
|
Currency: "T",
|
||||||
|
Description: "1description",
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
2006-01-02 description
|
||||||
|
recipient $-3.45
|
||||||
|
payee
|
||||||
|
2106-11-12 1description
|
||||||
|
1recipient 13.45 T
|
||||||
|
payee`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
p := path.Join(t.TempDir(), "dat")
|
||||||
|
if err := os.WriteFile(p, []byte(c.from), os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := NewFiles(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if deltas, err := files.Deltas(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if filtered := deltas.Like(func(d Delta) bool {
|
||||||
|
c.old.Transaction = d.Transaction
|
||||||
|
c.old.Payee = d.Payee
|
||||||
|
return d.equivalent(c.old)
|
||||||
|
}); len(filtered) != 1 {
|
||||||
|
t.Fatalf("input \n\t%s \ndidnt include old \n\t%+v \nin \n\t%+v: \n\t%+v", c.from, c.old, deltas, filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := files.Amend(c.old, c.now); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if b, err := os.ReadFile(path.Join(path.Dir(p), "inbox.txt")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if string(b) != c.want {
|
||||||
|
t.Fatalf("expected \n\t%q\nbut got\n\t%q\n\t%s", c.want, b, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFileAdd(t *testing.T) {
|
func TestFileAdd(t *testing.T) {
|
||||||
filesAppendDelim = " "
|
filesAppendDelim = " "
|
||||||
payee := "name:3"
|
payee := "name:3"
|
||||||
@@ -67,7 +221,7 @@ func TestFileAdd(t *testing.T) {
|
|||||||
for name, d := range cases {
|
for name, d := range cases {
|
||||||
c := d
|
c := d
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
p := path.Join(t.TempDir(), "input")
|
p := path.Join(t.TempDir(), "inbox.txt")
|
||||||
if c.given != nil {
|
if c.given != nil {
|
||||||
if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil {
|
if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -218,6 +372,10 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
Value: -97.92,
|
Value: -97.92,
|
||||||
Currency: USD,
|
Currency: USD,
|
||||||
Description: "Electricity / Power Bill TG2PJ-2PLP5",
|
Description: "Electricity / Power Bill TG2PJ-2PLP5",
|
||||||
|
Payee: true,
|
||||||
|
|
||||||
|
fileName: "",
|
||||||
|
lineNo: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Date: "2022-12-12",
|
Date: "2022-12-12",
|
||||||
@@ -225,6 +383,9 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
Value: 97.92,
|
Value: 97.92,
|
||||||
Currency: USD,
|
Currency: USD,
|
||||||
Description: "Electricity / Power Bill TG2PJ-2PLP5",
|
Description: "Electricity / Power Bill TG2PJ-2PLP5",
|
||||||
|
|
||||||
|
fileName: "",
|
||||||
|
lineNo: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Date: "2022-12-12",
|
Date: "2022-12-12",
|
||||||
@@ -232,6 +393,10 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
Value: -1.00,
|
Value: -1.00,
|
||||||
Currency: USD,
|
Currency: USD,
|
||||||
Description: "Test pay chase TG32S-BT2FF",
|
Description: "Test pay chase TG32S-BT2FF",
|
||||||
|
Payee: true,
|
||||||
|
|
||||||
|
fileName: "",
|
||||||
|
lineNo: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Date: "2022-12-12",
|
Date: "2022-12-12",
|
||||||
@@ -239,6 +404,9 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
Value: 1.00,
|
Value: 1.00,
|
||||||
Currency: USD,
|
Currency: USD,
|
||||||
Description: "Test pay chase TG32S-BT2FF",
|
Description: "Test pay chase TG32S-BT2FF",
|
||||||
|
|
||||||
|
fileName: "",
|
||||||
|
lineNo: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +419,8 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
for name, d := range cases {
|
for name, d := range cases {
|
||||||
want := d
|
want := d
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
f, err := NewFiles("./testdata/" + name + ".dat")
|
fileName := "./testdata/" + name + ".dat"
|
||||||
|
f, err := NewFiles(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -265,13 +434,34 @@ func TestFileDeltas(t *testing.T) {
|
|||||||
t.Error(len(deltas))
|
t.Error(len(deltas))
|
||||||
}
|
}
|
||||||
for i := range want {
|
for i := range want {
|
||||||
|
want[i].fileName = fileName
|
||||||
|
deltas[i].lineNo = 0
|
||||||
if i >= len(deltas) {
|
if i >= len(deltas) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if want[i] != deltas[i] {
|
if deltas[i].Transaction == "" {
|
||||||
|
t.Error(deltas[i].Transaction)
|
||||||
|
}
|
||||||
|
deltas[i].Transaction = ""
|
||||||
|
if !want[i].equivalent(deltas[i]) {
|
||||||
t.Errorf("[%d] \n\twant=%s, \n\t got=%s", i, want[i].Debug(), deltas[i].Debug())
|
t.Errorf("[%d] \n\twant=%s, \n\t got=%s", i, want[i].Debug(), deltas[i].Debug())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilesOfDir(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
files := Files([]string{d, "/dev/null"})
|
||||||
|
if paths := files.paths(); len(paths) != 1 {
|
||||||
|
t.Error(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.WriteFile(path.Join(d, "1"), []byte{}, os.ModePerm)
|
||||||
|
os.Mkdir(path.Join(d, "d2"), os.ModePerm)
|
||||||
|
os.WriteFile(path.Join(d, "d2", "2"), []byte{}, os.ModePerm)
|
||||||
|
if paths := files.paths(); len(paths) != 3 || paths[0] != path.Join(d, "1") || paths[1] != path.Join(d, "d2", "2") {
|
||||||
|
t.Error(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package ledger
|
package ledger
|
||||||
|
|
||||||
import "regexp"
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
type Group func(Delta) Delta
|
type Group func(Delta) Delta
|
||||||
|
|
||||||
@@ -17,6 +19,9 @@ func GroupDate(pattern string) Group {
|
|||||||
p := regexp.MustCompile(pattern)
|
p := regexp.MustCompile(pattern)
|
||||||
return func(d Delta) Delta {
|
return func(d Delta) Delta {
|
||||||
d.Date = p.FindString(d.Date)
|
d.Date = p.FindString(d.Date)
|
||||||
|
for i := range d.with {
|
||||||
|
d.with[i].Date = p.FindString(d.with[i].Date)
|
||||||
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +30,9 @@ func GroupName(pattern string) Group {
|
|||||||
p := regexp.MustCompile(pattern)
|
p := regexp.MustCompile(pattern)
|
||||||
return func(d Delta) Delta {
|
return func(d Delta) Delta {
|
||||||
d.Name = p.FindString(d.Name)
|
d.Name = p.FindString(d.Name)
|
||||||
|
for i := range d.with {
|
||||||
|
d.with[i].Name = p.FindString(d.with[i].Name)
|
||||||
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,36 @@ type Like func(Delta) bool
|
|||||||
|
|
||||||
type Likes []Like
|
type Likes []Like
|
||||||
|
|
||||||
|
func LikeWith(pattern string) Like {
|
||||||
|
l := LikeName(pattern)
|
||||||
|
return func(d Delta) bool {
|
||||||
|
if l(d) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, d := range d.with {
|
||||||
|
if l(d) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LikeTransactions(deltas ...Delta) Like {
|
||||||
|
return func(d Delta) bool {
|
||||||
|
for i := range deltas {
|
||||||
|
if deltas[i].Transaction == d.Transaction {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LikeTransaction(delta Delta) Like {
|
||||||
|
return LikeTransactions(delta)
|
||||||
|
}
|
||||||
|
|
||||||
func LikeBefore(date string) Like {
|
func LikeBefore(date string) Like {
|
||||||
return func(d Delta) bool {
|
return func(d Delta) bool {
|
||||||
return date >= d.Date
|
return date >= d.Date
|
||||||
@@ -20,6 +50,22 @@ func LikeAfter(date string) Like {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LikeNotName(pattern string) Like {
|
||||||
|
return func(d Delta) bool {
|
||||||
|
return !like(pattern, d.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LikeNot(like Like) Like {
|
||||||
|
return func(d Delta) bool {
|
||||||
|
return !like(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotLikeName(pattern string) Like {
|
||||||
|
return LikeNot(LikeName(pattern))
|
||||||
|
}
|
||||||
|
|
||||||
func LikeName(pattern string) Like {
|
func LikeName(pattern string) Like {
|
||||||
return func(d Delta) bool {
|
return func(d Delta) bool {
|
||||||
return like(pattern, d.Name)
|
return like(pattern, d.Name)
|
||||||
|
|||||||
@@ -36,3 +36,20 @@ func TestLikesAll(t *testing.T) {
|
|||||||
t.Error(likes.All(delta))
|
t.Error(likes.All(delta))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLikeWith(t *testing.T) {
|
||||||
|
delta := Delta{
|
||||||
|
Name: "x",
|
||||||
|
with: []Delta{
|
||||||
|
Delta{Name: "y"},
|
||||||
|
Delta{Name: "z"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !LikeWith("x")(delta) {
|
||||||
|
t.Error("like with self not caught")
|
||||||
|
}
|
||||||
|
if !LikeWith("z")(delta) {
|
||||||
|
t.Error("like with reverse not caught")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,26 @@ func (register Register) Names() []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (register Register) Like(pattern string) Register {
|
||||||
|
result := make(Register)
|
||||||
|
for k, v := range register {
|
||||||
|
if got := v.Like(pattern); len(got) > 0 {
|
||||||
|
result[k] = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (register Register) NotLike(pattern string) Register {
|
||||||
|
result := make(Register)
|
||||||
|
for k, v := range register {
|
||||||
|
if got := v.NotLike(pattern); len(got) > 0 {
|
||||||
|
result[k] = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (register Register) Dates() []string {
|
func (register Register) Dates() []string {
|
||||||
result := make([]string, 0, len(register))
|
result := make([]string, 0, len(register))
|
||||||
for k := range register {
|
for k := range register {
|
||||||
|
|||||||
@@ -3,19 +3,116 @@ package ledger
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Transaction Deltas
|
||||||
|
|
||||||
|
type Transactions []Transaction
|
||||||
|
|
||||||
|
func (transactions Transactions) Deltas() Deltas {
|
||||||
|
result := make(Deltas, 0, len(transactions))
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
result = append(result, transaction...)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deltas Deltas) Transactions() Transactions {
|
||||||
|
m := make(map[string]Transaction)
|
||||||
|
for _, d := range deltas {
|
||||||
|
m[d.Transaction] = append(m[d.Transaction], d)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(Transactions, 0, len(m))
|
||||||
|
for _, v := range m {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(result, func(a, b Transaction) int {
|
||||||
|
if a[0].Date == b[0].Date {
|
||||||
|
if a[0].Description == b[0].Description {
|
||||||
|
return strings.Compare(a[0].Transaction, b[0].Transaction)
|
||||||
|
}
|
||||||
|
return strings.Compare(a[0].Description, b[0].Description)
|
||||||
|
}
|
||||||
|
return strings.Compare(a[0].Date, b[0].Date)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transactions Transactions) NotLike(like ...Like) Transactions {
|
||||||
|
result := make(Transactions, 0, len(transactions))
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
if matching := (Deltas)(transaction).Like(like...); len(matching) == 0 {
|
||||||
|
result = append(result, transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transactions Transactions) Like(like ...Like) Transactions {
|
||||||
|
result := make(Transactions, 0, len(transactions))
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
if matching := (Deltas)(transaction).Like(like...); len(matching) > 0 {
|
||||||
|
result = append(result, transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transaction Transaction) Payee() string {
|
||||||
|
balances := Deltas(transaction).Balances()
|
||||||
|
|
||||||
|
candidates := []string{}
|
||||||
|
|
||||||
|
for name, balance := range balances {
|
||||||
|
deltas := Deltas(transaction).Like(LikeName(`^` + name + `$`))
|
||||||
|
if len(deltas) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
everyoneElse := balances.NotLike(`^` + name + `$`).Group(`^`)[""]
|
||||||
|
matches := true
|
||||||
|
for currency, value := range balance {
|
||||||
|
matches = matches && everyoneElse[currency]*-1 == value
|
||||||
|
}
|
||||||
|
if matches {
|
||||||
|
candidates = append(candidates, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(candidates)
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
panic(balances)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if strings.HasPrefix(candidate, "Withdrawal") {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates[len(candidates)-1]
|
||||||
|
}
|
||||||
|
|
||||||
type transaction struct {
|
type transaction struct {
|
||||||
date string
|
date string
|
||||||
description string
|
description string
|
||||||
payee string
|
payee string
|
||||||
recipients []transactionRecipient
|
recipients []transactionRecipient
|
||||||
|
name string
|
||||||
|
|
||||||
|
fileName string
|
||||||
|
lineNo int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t transaction) empty() bool {
|
func (t transaction) empty() bool {
|
||||||
@@ -29,14 +126,65 @@ type transactionRecipient struct {
|
|||||||
isSet bool
|
isSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t transaction) deltas() Deltas {
|
||||||
|
result := []Delta{}
|
||||||
|
sums := map[string]float64{}
|
||||||
|
for i, recipient := range t.recipients {
|
||||||
|
sums[recipient.currency] += recipient.value
|
||||||
|
result = append(result, newDelta(
|
||||||
|
t.name,
|
||||||
|
true,
|
||||||
|
t.date,
|
||||||
|
t.description,
|
||||||
|
recipient.name,
|
||||||
|
recipient.value,
|
||||||
|
recipient.currency,
|
||||||
|
recipient.isSet,
|
||||||
|
t.fileName,
|
||||||
|
t.lineNo+i,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
for currency, value := range sums {
|
||||||
|
if value == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.payee == "" {
|
||||||
|
//return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction)
|
||||||
|
} else {
|
||||||
|
result = append(result, newDelta(
|
||||||
|
t.name,
|
||||||
|
false,
|
||||||
|
t.date,
|
||||||
|
t.description,
|
||||||
|
t.payee,
|
||||||
|
-1.0*value,
|
||||||
|
currency,
|
||||||
|
false,
|
||||||
|
t.fileName,
|
||||||
|
t.lineNo,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
for j := range result {
|
||||||
|
if i != j {
|
||||||
|
result[i] = result[i].withWith(result[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (t transactionRecipient) empty() bool {
|
func (t transactionRecipient) empty() bool {
|
||||||
return t == (transactionRecipient{})
|
return t == (transactionRecipient{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) transactions() ([]transaction, error) {
|
func (files Files) transactions() ([]transaction, error) {
|
||||||
result := make([]transaction, 0)
|
result := make([]transaction, 0)
|
||||||
for i := range files {
|
for _, path := range files.paths() {
|
||||||
some, err := files._transactions(files[i])
|
some, err := files._transactions(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,8 +207,11 @@ func (files Files) _transactions(file string) ([]transaction, error) {
|
|||||||
|
|
||||||
result := make([]transaction, 0)
|
result := make([]transaction, 0)
|
||||||
for {
|
for {
|
||||||
one, err := readTransaction(r)
|
name := fmt.Sprintf("%s/%d", file, len(result))
|
||||||
|
one, err := readTransaction(name, r)
|
||||||
if !one.empty() {
|
if !one.empty() {
|
||||||
|
one.fileName = file
|
||||||
|
one.lineNo = len(result)
|
||||||
result = append(result, one)
|
result = append(result, one)
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -72,24 +223,63 @@ func (files Files) _transactions(file string) ([]transaction, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readTransaction(r *bufio.Reader) (transaction, error) {
|
func readTransaction(name string, r *bufio.Reader) (transaction, error) {
|
||||||
result, err := _readTransaction(r)
|
result, err := _readTransaction(name, r)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if result.empty() {
|
if result.empty() {
|
||||||
return result, nil
|
return result, err
|
||||||
}
|
|
||||||
if result.payee == "" && len(result.recipients) < 2 {
|
|
||||||
return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
|
|
||||||
}
|
}
|
||||||
if result.payee != "" && len(result.recipients) < 1 {
|
if result.payee != "" && len(result.recipients) < 1 {
|
||||||
return result, fmt.Errorf("found a transaction with payee but no recipeints: %+v", result)
|
return result, fmt.Errorf("found a transaction with payee but no recipeints: %+v", result)
|
||||||
}
|
}
|
||||||
return result, nil
|
if result.payee == "" {
|
||||||
|
if len(result.recipients) < 2 {
|
||||||
|
return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
|
||||||
|
}
|
||||||
|
func() {
|
||||||
|
sumPerRecipient := map[string]float64{}
|
||||||
|
recipients := []string{}
|
||||||
|
for _, recipient := range result.recipients {
|
||||||
|
recipients = append(recipients, recipient.name)
|
||||||
|
sumPerRecipient[recipient.name] += recipient.value
|
||||||
|
}
|
||||||
|
slices.Sort(recipients)
|
||||||
|
|
||||||
|
for _, k := range recipients {
|
||||||
|
n := 0
|
||||||
|
for i := range result.recipients {
|
||||||
|
if result.recipients[i].name == k {
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v := sumPerRecipient[k]
|
||||||
|
everyoneElse := 0.0
|
||||||
|
for j := range sumPerRecipient {
|
||||||
|
if k != j {
|
||||||
|
everyoneElse += sumPerRecipient[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if -1.0*v == everyoneElse {
|
||||||
|
result.payee = k
|
||||||
|
result.recipients = slices.DeleteFunc(result.recipients, func(recipient transactionRecipient) bool {
|
||||||
|
return recipient.name == k
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func _readTransaction(r *bufio.Reader) (transaction, error) {
|
func _readTransaction(name string, r *bufio.Reader) (transaction, error) {
|
||||||
readTransactionLeadingWhitespace(r)
|
readTransactionLeadingWhitespace(r)
|
||||||
|
|
||||||
firstLine, err := readTransactionLine(r)
|
firstLine, err := readTransactionLine(r)
|
||||||
@@ -107,6 +297,7 @@ func _readTransaction(r *bufio.Reader) (transaction, error) {
|
|||||||
result := transaction{
|
result := transaction{
|
||||||
date: string(dateDescriptionMatches[0][1]),
|
date: string(dateDescriptionMatches[0][1]),
|
||||||
description: string(dateDescriptionMatches[0][2]),
|
description: string(dateDescriptionMatches[0][2]),
|
||||||
|
name: name,
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -193,6 +384,7 @@ func readTransactionName(r *bufio.Reader) (string, float64, string, bool, error)
|
|||||||
return "", 0, "", false, nil
|
return "", 0, "", false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
line = bytes.Split(line, []byte(";"))[0] // comment-free
|
||||||
fields := bytes.Fields(line)
|
fields := bytes.Fields(line)
|
||||||
|
|
||||||
isSet := false
|
isSet := false
|
||||||
|
|||||||
@@ -8,6 +8,38 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestTransactionPayee(t *testing.T) {
|
||||||
|
given := Transaction{
|
||||||
|
Delta{Name: "x", Transaction: "a", Value: 9},
|
||||||
|
Delta{Name: "Withdrawal:z", Transaction: "a", Value: -3},
|
||||||
|
Delta{Name: "Withdrawal:z", Transaction: "a", Value: -6},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := given.Payee()
|
||||||
|
if got != "x" {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeltasTransactions(t *testing.T) {
|
||||||
|
given := Deltas{
|
||||||
|
Delta{Date: "2", Name: "x", Transaction: "a"},
|
||||||
|
Delta{Date: "2", Name: "y", Transaction: "a"},
|
||||||
|
Delta{Date: "1", Name: "z", Transaction: "b"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := given.Transactions()
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Error(len(got))
|
||||||
|
}
|
||||||
|
if len(got[0]) != 1 {
|
||||||
|
t.Error("first xaction is not earliest date", len(got[0]))
|
||||||
|
}
|
||||||
|
if len(got[1]) != 2 {
|
||||||
|
t.Error("second xaction is not latest date", len(got[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadTransaction(t *testing.T) {
|
func TestReadTransaction(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
input string
|
input string
|
||||||
@@ -33,7 +65,28 @@ func TestReadTransaction(t *testing.T) {
|
|||||||
want: transaction{
|
want: transaction{
|
||||||
date: "2003-04-05",
|
date: "2003-04-05",
|
||||||
description: "Reasoning here",
|
description: "Reasoning here",
|
||||||
payee: "",
|
payee: "A:B",
|
||||||
|
recipients: []transactionRecipient{
|
||||||
|
{
|
||||||
|
name: "C:D",
|
||||||
|
value: -1.0,
|
||||||
|
currency: "$",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: io.EOF,
|
||||||
|
},
|
||||||
|
"multi send": {
|
||||||
|
input: `
|
||||||
|
2003-04-05 Reasoning here
|
||||||
|
A:B $1.00
|
||||||
|
A:B $2.00
|
||||||
|
C:D
|
||||||
|
`,
|
||||||
|
want: transaction{
|
||||||
|
date: "2003-04-05",
|
||||||
|
description: "Reasoning here",
|
||||||
|
payee: "C:D",
|
||||||
recipients: []transactionRecipient{
|
recipients: []transactionRecipient{
|
||||||
{
|
{
|
||||||
name: "A:B",
|
name: "A:B",
|
||||||
@@ -41,8 +94,8 @@ func TestReadTransaction(t *testing.T) {
|
|||||||
currency: "$",
|
currency: "$",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "C:D",
|
name: "A:B",
|
||||||
value: -1.0,
|
value: 2.0,
|
||||||
currency: "$",
|
currency: "$",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -55,7 +108,7 @@ func TestReadTransaction(t *testing.T) {
|
|||||||
c := d
|
c := d
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
r := bufio.NewReader(strings.NewReader(c.input))
|
r := bufio.NewReader(strings.NewReader(c.input))
|
||||||
got, err := readTransaction(r)
|
got, err := readTransaction("", r)
|
||||||
if err != c.err {
|
if err != c.err {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/view/chart.go
Normal file
160
src/view/chart.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-echarts/go-echarts/v2/charts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Chart interface {
|
||||||
|
AddX(interface{})
|
||||||
|
AddY(string, []int)
|
||||||
|
Render(io.Writer) error
|
||||||
|
Overlap(Chart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChart(name string) Chart {
|
||||||
|
switch name {
|
||||||
|
case "line":
|
||||||
|
return NewLine()
|
||||||
|
case "bar":
|
||||||
|
return NewBar()
|
||||||
|
case "stack":
|
||||||
|
return NewStack()
|
||||||
|
case "pie":
|
||||||
|
return NewPie()
|
||||||
|
default:
|
||||||
|
panic("bad chart name " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Line struct {
|
||||||
|
*charts.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLine() Line {
|
||||||
|
return Line{Line: charts.NewLine()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (line Line) AddX(v interface{}) {
|
||||||
|
line.SetXAxis(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (line Line) AddY(name string, v []int) {
|
||||||
|
y := make([]opts.LineData, len(v))
|
||||||
|
for i := range y {
|
||||||
|
y[i].Value = v[i]
|
||||||
|
}
|
||||||
|
line.AddSeries(name, y).
|
||||||
|
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
|
||||||
|
Stack: "stackB",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (line Line) Overlap(other Chart) {
|
||||||
|
overlapper, ok := other.(charts.Overlaper)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("cannot overlap %T", other))
|
||||||
|
}
|
||||||
|
line.Line.Overlap(overlapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bar struct {
|
||||||
|
*charts.Bar
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBar() Bar {
|
||||||
|
return Bar{Bar: charts.NewBar()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bar Bar) AddX(v interface{}) {
|
||||||
|
bar.SetXAxis(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bar Bar) AddY(name string, v []int) {
|
||||||
|
y := make([]opts.BarData, len(v))
|
||||||
|
for i := range v {
|
||||||
|
y[i].Value = v[i]
|
||||||
|
}
|
||||||
|
bar.AddSeries(name, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bar Bar) Overlap(other Chart) {
|
||||||
|
overlapper, ok := other.(charts.Overlaper)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("cannot overlap %T", other))
|
||||||
|
}
|
||||||
|
bar.Bar.Overlap(overlapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stack struct {
|
||||||
|
Bar
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStack() Stack {
|
||||||
|
bar := NewBar()
|
||||||
|
bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
|
||||||
|
return Stack{Bar: bar}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stack Stack) AddY(name string, v []int) {
|
||||||
|
y := make([]opts.BarData, len(v))
|
||||||
|
for i := range v {
|
||||||
|
y[i].Value = v[i]
|
||||||
|
}
|
||||||
|
stack.AddSeries(name, y).
|
||||||
|
SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
|
||||||
|
Stack: "stackA",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pie struct {
|
||||||
|
*charts.Pie
|
||||||
|
series []opts.PieData
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPie() *Pie {
|
||||||
|
return &Pie{Pie: charts.NewPie()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pie *Pie) AddX(v interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pie *Pie) Render(w io.Writer) error {
|
||||||
|
slices.SortFunc(pie.series, func(a, b opts.PieData) int {
|
||||||
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
commonPrefixLen := -1
|
||||||
|
for i := 0; i < len(pie.series[0].Name) && i < len(pie.series[len(pie.series)-1].Name); i++ {
|
||||||
|
if pie.series[0].Name[i] != pie.series[len(pie.series)-1].Name[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
commonPrefixLen = i
|
||||||
|
}
|
||||||
|
for i := range pie.series {
|
||||||
|
pie.series[i].Name = pie.series[i].Name[commonPrefixLen+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
pie.AddSeries("", pie.series)
|
||||||
|
pie.SetGlobalOptions(charts.WithLegendOpts(opts.Legend{
|
||||||
|
Show: false,
|
||||||
|
}))
|
||||||
|
return pie.Pie.Render(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pie *Pie) AddY(name string, v []int) {
|
||||||
|
for _, v := range v {
|
||||||
|
pie.series = append(pie.series, opts.PieData{
|
||||||
|
Name: fmt.Sprintf("%s ($%d)", name, v),
|
||||||
|
Value: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pie *Pie) Overlap(other Chart) {
|
||||||
|
panic("nope")
|
||||||
|
}
|
||||||
26
todo.yaml
26
todo.yaml
@@ -1,8 +1,15 @@
|
|||||||
todo:
|
todo:
|
||||||
- cicd in gitea is hard to get it to build a docker image tho
|
- |
|
||||||
- cicd on not gogs,gitness but gitea; registry-app.inhome.blapointe.com:5001/docker
|
scale by salaries for proportional house contributions
|
||||||
stuff
|
if bel makes 190k and contributes $190
|
||||||
- html version can accept new transactions for moolah
|
equal to z making 140k and contri $140
|
||||||
|
so contrib/sal = %of_me_contributed
|
||||||
|
... is that fair tho?
|
||||||
|
i think it is--if we both give N%, then 100% of house is covered, so we were hit equally
|
||||||
|
- ui can create transactions
|
||||||
|
- combine transactions in /transactions.html
|
||||||
|
- amend /transactions.html
|
||||||
|
- cicd including run
|
||||||
scheduled: []
|
scheduled: []
|
||||||
done:
|
done:
|
||||||
- todo: balances over time window
|
- todo: balances over time window
|
||||||
@@ -70,3 +77,14 @@ done:
|
|||||||
ts: Fri Oct 27 20:37:12 MDT 2023
|
ts: Fri Oct 27 20:37:12 MDT 2023
|
||||||
- todo: cicd on not gogs,gitness but gitea
|
- todo: cicd on not gogs,gitness but gitea
|
||||||
ts: Fri Oct 27 22:22:04 MDT 2023
|
ts: Fri Oct 27 22:22:04 MDT 2023
|
||||||
|
- todo: cicd in gitea is hard to get it to build a docker image tho
|
||||||
|
ts: Sat Oct 28 09:44:55 MDT 2023
|
||||||
|
- todo: cicd on not gogs,gitness but gitea; registry-app.inhome.blapointe.com:5001/docker
|
||||||
|
stuff
|
||||||
|
ts: Sat Oct 28 09:44:55 MDT 2023
|
||||||
|
- todo: services_docker including gitea cicd
|
||||||
|
ts: Sat Oct 28 10:10:08 MDT 2023
|
||||||
|
- todo: go embed cmd/http/public dir?
|
||||||
|
ts: Sat Oct 28 10:40:50 MDT 2023
|
||||||
|
- todo: html version can accept new transactions for moolah
|
||||||
|
ts: Sun Oct 29 10:11:50 MDT 2023
|
||||||
|
|||||||
21
vendor/github.com/go-echarts/go-echarts/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/go-echarts/go-echarts/v2/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019~now chenjiandongx
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
vendor/github.com/go-echarts/go-echarts/v2/actions/global.go
generated
vendored
Normal file
29
vendor/github.com/go-echarts/go-echarts/v2/actions/global.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type kind of dispatch action
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
// Areas means select-boxes. Multi-boxes can be specified.
|
||||||
|
// If Areas is empty, all of the select-boxes will be deleted.
|
||||||
|
// The first area.
|
||||||
|
type Areas struct {
|
||||||
|
|
||||||
|
//BrushType Optional: 'polygon', 'rect', 'lineX', 'lineY'
|
||||||
|
BrushType string `json:"brushType,omitempty"`
|
||||||
|
|
||||||
|
// CoordRange Only for "coordinate system area", define the area with the
|
||||||
|
// coordinates.
|
||||||
|
CoordRange []string `json:"coordRange,omitempty"`
|
||||||
|
|
||||||
|
// XAxisIndex Assigns which of the xAxisIndex can use Area selecting.
|
||||||
|
XAxisIndex interface{} `json:"xAxisIndex,omitempty"`
|
||||||
|
}
|
||||||
63
vendor/github.com/go-echarts/go-echarts/v2/charts/bar.go
generated
vendored
Normal file
63
vendor/github.com/go-echarts/go-echarts/v2/charts/bar.go
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bar represents a bar chart.
|
||||||
|
type Bar struct {
|
||||||
|
RectChart
|
||||||
|
|
||||||
|
isXYReversal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Bar) Type() string { return types.ChartBar }
|
||||||
|
|
||||||
|
// NewBar creates a new bar chart instance.
|
||||||
|
func NewBar() *Bar {
|
||||||
|
c := &Bar{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnablePolarType enables the polar bar.
|
||||||
|
func (c *Bar) EnablePolarType() *Bar {
|
||||||
|
c.hasXYAxis = false
|
||||||
|
c.hasPolar = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis sets the X axis.
|
||||||
|
func (c *Bar) SetXAxis(x interface{}) *Bar {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Bar) AddSeries(name string, data []opts.BarData, options ...SeriesOpts) *Bar {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartBar, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// XYReversal checks if X axis and Y axis are reversed.
|
||||||
|
func (c *Bar) XYReversal() *Bar {
|
||||||
|
c.isXYReversal = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Bar) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
if c.isXYReversal {
|
||||||
|
c.YAxisList[0].Data = c.xAxisData
|
||||||
|
c.XAxisList[0].Data = nil
|
||||||
|
}
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
30
vendor/github.com/go-echarts/go-echarts/v2/charts/bar3d.go
generated
vendored
Normal file
30
vendor/github.com/go-echarts/go-echarts/v2/charts/bar3d.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bar3D represents a 3D bar chart.
|
||||||
|
type Bar3D struct {
|
||||||
|
Chart3D
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Bar3D) Type() string { return types.ChartBar3D }
|
||||||
|
|
||||||
|
// NewBar3D creates a new 3D bar chart.
|
||||||
|
func NewBar3D() *Bar3D {
|
||||||
|
c := &Bar3D{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.initChart3D()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Bar3D) AddSeries(name string, data []opts.Chart3DData, options ...SeriesOpts) *Bar3D {
|
||||||
|
c.addSeries(types.ChartBar3D, name, data, options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
430
vendor/github.com/go-echarts/go-echarts/v2/charts/base.go
generated
vendored
Normal file
430
vendor/github.com/go-echarts/go-echarts/v2/charts/base.go
generated
vendored
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"github.com/go-echarts/go-echarts/v2/actions"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/datasets"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GlobalOpts sets the Global options for charts.
|
||||||
|
type GlobalOpts func(bc *BaseConfiguration)
|
||||||
|
|
||||||
|
// GlobalActions sets the Global actions for charts
|
||||||
|
type GlobalActions func(ba *BaseActions)
|
||||||
|
|
||||||
|
// BaseConfiguration represents an option set needed by all chart types.
|
||||||
|
type BaseConfiguration struct {
|
||||||
|
opts.Legend `json:"legend"`
|
||||||
|
opts.Tooltip `json:"tooltip"`
|
||||||
|
opts.Toolbox `json:"toolbox"`
|
||||||
|
opts.Title `json:"title"`
|
||||||
|
opts.Polar `json:"polar"`
|
||||||
|
opts.AngleAxis `json:"angleAxis"`
|
||||||
|
opts.RadiusAxis `json:"radiusAxis"`
|
||||||
|
opts.Brush `json:"brush"`
|
||||||
|
*opts.AxisPointer `json:"axisPointer"`
|
||||||
|
|
||||||
|
render.Renderer `json:"-"`
|
||||||
|
opts.Initialization `json:"-"`
|
||||||
|
opts.Assets `json:"-"`
|
||||||
|
opts.RadarComponent `json:"-"`
|
||||||
|
opts.GeoComponent `json:"-"`
|
||||||
|
opts.ParallelComponent `json:"-"`
|
||||||
|
opts.JSFunctions `json:"-"`
|
||||||
|
opts.SingleAxis `json:"-"`
|
||||||
|
|
||||||
|
MultiSeries
|
||||||
|
XYAxis
|
||||||
|
|
||||||
|
opts.XAxis3D
|
||||||
|
opts.YAxis3D
|
||||||
|
opts.ZAxis3D
|
||||||
|
opts.Grid3D
|
||||||
|
opts.Grid
|
||||||
|
|
||||||
|
legends []string
|
||||||
|
// Colors is the color list of palette.
|
||||||
|
// If no color is set in series, the colors would be adopted sequentially and circularly
|
||||||
|
// from this list as the colors of series.
|
||||||
|
Colors []string
|
||||||
|
appendColor []string // append customize color to the Colors(reverse order)
|
||||||
|
|
||||||
|
// Animation whether enable the animation, default true
|
||||||
|
Animation bool `json:"animation" default:"true"`
|
||||||
|
|
||||||
|
// Array of datasets, managed by AddDataset()
|
||||||
|
DatasetList []opts.Dataset `json:"dataset,omitempty"`
|
||||||
|
|
||||||
|
DataZoomList []opts.DataZoom `json:"datazoom,omitempty"`
|
||||||
|
VisualMapList []opts.VisualMap `json:"visualmap,omitempty"`
|
||||||
|
|
||||||
|
// ParallelAxisList represents the component list which is the coordinate axis for parallel coordinate.
|
||||||
|
ParallelAxisList []opts.ParallelAxis
|
||||||
|
|
||||||
|
has3DAxis bool
|
||||||
|
hasXYAxis bool
|
||||||
|
hasGeo bool
|
||||||
|
hasRadar bool
|
||||||
|
hasParallel bool
|
||||||
|
hasSingleAxis bool
|
||||||
|
hasPolar bool
|
||||||
|
hasBrush bool
|
||||||
|
|
||||||
|
GridList []opts.Grid `json:"grid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseActions represents a dispatchAction set needed by all chart types.
|
||||||
|
type BaseActions struct {
|
||||||
|
actions.Type `json:"type,omitempty"`
|
||||||
|
actions.Areas `json:"areas,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON wraps all the options to a map so that it could be used in the base template
|
||||||
|
//
|
||||||
|
// Get data in bytes
|
||||||
|
// bs, _ : = json.Marshal(bar.JSON())
|
||||||
|
func (bc *BaseConfiguration) JSON() map[string]interface{} {
|
||||||
|
return bc.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONNotEscaped works like method JSON, but it returns a marshaled object whose characters will not be escaped in the template
|
||||||
|
func (bc *BaseConfiguration) JSONNotEscaped() template.HTML {
|
||||||
|
obj := bc.json()
|
||||||
|
buff := bytes.NewBufferString("")
|
||||||
|
enc := json.NewEncoder(buff)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.Encode(obj)
|
||||||
|
|
||||||
|
return template.HTML(buff.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONNotEscapedAction works like method JSON, but it returns a marshaled object whose characters will not be escaped in the template
|
||||||
|
func (ba *BaseActions) JSONNotEscapedAction() template.HTML {
|
||||||
|
obj := ba.json()
|
||||||
|
buff := bytes.NewBufferString("")
|
||||||
|
enc := json.NewEncoder(buff)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.Encode(obj)
|
||||||
|
|
||||||
|
return template.HTML(buff.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BaseConfiguration) json() map[string]interface{} {
|
||||||
|
obj := map[string]interface{}{
|
||||||
|
"title": bc.Title,
|
||||||
|
"legend": bc.Legend,
|
||||||
|
"animation": bc.Animation,
|
||||||
|
"tooltip": bc.Tooltip,
|
||||||
|
"series": bc.MultiSeries,
|
||||||
|
}
|
||||||
|
// if only one item, use it directly instead of an Array
|
||||||
|
if len(bc.DatasetList) == 1 {
|
||||||
|
obj["dataset"] = bc.DatasetList[0]
|
||||||
|
} else if len(bc.DatasetList) > 1 {
|
||||||
|
obj["dataset"] = bc.DatasetList
|
||||||
|
|
||||||
|
}
|
||||||
|
if bc.AxisPointer != nil {
|
||||||
|
obj["axisPointer"] = bc.AxisPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasPolar {
|
||||||
|
obj["polar"] = bc.Polar
|
||||||
|
obj["angleAxis"] = bc.AngleAxis
|
||||||
|
obj["radiusAxis"] = bc.RadiusAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasGeo {
|
||||||
|
obj["geo"] = bc.GeoComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasRadar {
|
||||||
|
obj["radar"] = bc.RadarComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasParallel {
|
||||||
|
obj["parallel"] = bc.ParallelComponent
|
||||||
|
obj["parallelAxis"] = bc.ParallelAxisList
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasSingleAxis {
|
||||||
|
obj["singleAxis"] = bc.SingleAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.Toolbox.Show {
|
||||||
|
obj["toolbox"] = bc.Toolbox
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bc.DataZoomList) > 0 {
|
||||||
|
obj["dataZoom"] = bc.DataZoomList
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bc.VisualMapList) > 0 {
|
||||||
|
obj["visualMap"] = bc.VisualMapList
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasXYAxis {
|
||||||
|
obj["xAxis"] = bc.XAxisList
|
||||||
|
obj["yAxis"] = bc.YAxisList
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.has3DAxis {
|
||||||
|
obj["xAxis3D"] = bc.XAxis3D
|
||||||
|
obj["yAxis3D"] = bc.YAxis3D
|
||||||
|
obj["zAxis3D"] = bc.ZAxis3D
|
||||||
|
obj["grid3D"] = bc.Grid3D
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.Theme == "white" {
|
||||||
|
obj["color"] = bc.Colors
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.BackgroundColor != "" {
|
||||||
|
obj["backgroundColor"] = bc.BackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bc.GridList) > 0 {
|
||||||
|
obj["grid"] = bc.GridList
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.hasBrush {
|
||||||
|
obj["brush"] = bc.Brush
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssets returns the Assets options.
|
||||||
|
func (bc *BaseConfiguration) GetAssets() opts.Assets {
|
||||||
|
return bc.Assets
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDataset adds a Dataset to this chart
|
||||||
|
func (bc *BaseConfiguration) AddDataset(dataset ...opts.Dataset) {
|
||||||
|
bc.DatasetList = append(bc.DatasetList, dataset...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillDefaultValues fill default values for chart options.
|
||||||
|
func (bc *BaseConfiguration) FillDefaultValues() {
|
||||||
|
opts.SetDefaultValue(bc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BaseConfiguration) initBaseConfiguration() {
|
||||||
|
bc.initSeriesColors()
|
||||||
|
bc.InitAssets()
|
||||||
|
bc.initXYAxis()
|
||||||
|
bc.Initialization.Validate()
|
||||||
|
bc.FillDefaultValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BaseConfiguration) initSeriesColors() {
|
||||||
|
bc.Colors = []string{
|
||||||
|
"#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de",
|
||||||
|
"#3ba272", "#fc8452", "#9a60b4", "#ea7ccc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BaseConfiguration) insertSeriesColors(colors []string) {
|
||||||
|
reversed := reverseSlice(colors)
|
||||||
|
for i := 0; i < len(reversed); i++ {
|
||||||
|
bc.Colors = append(bc.Colors, "")
|
||||||
|
copy(bc.Colors[1:], bc.Colors[0:])
|
||||||
|
bc.Colors[0] = reversed[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BaseConfiguration) setBaseGlobalOptions(opts ...GlobalOpts) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *BaseActions) setBaseGlobalActions(opts ...GlobalActions) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(ba)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *BaseActions) json() map[string]interface{} {
|
||||||
|
obj := map[string]interface{}{
|
||||||
|
"type": ba.Type,
|
||||||
|
"areas": ba.Areas,
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAreas sets the areas of the action
|
||||||
|
func WithAreas(act actions.Areas) GlobalActions {
|
||||||
|
return func(ba *BaseActions) {
|
||||||
|
ba.Areas = act
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithType sets the type of the action
|
||||||
|
func WithType(act actions.Type) GlobalActions {
|
||||||
|
return func(ba *BaseActions) {
|
||||||
|
ba.Type = act
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAngleAxisOps sets the angle of the axis.
|
||||||
|
func WithAngleAxisOps(opt opts.AngleAxis) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.AngleAxis = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRadiusAxisOps sets the radius of the axis.
|
||||||
|
func WithRadiusAxisOps(opt opts.RadiusAxis) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.RadiusAxis = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBrush sets the Brush.
|
||||||
|
func WithBrush(opt opts.Brush) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.hasBrush = true
|
||||||
|
bc.Brush = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPolarOps sets the polar.
|
||||||
|
func WithPolarOps(opt opts.Polar) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Polar = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTitleOpts sets the title.
|
||||||
|
func WithTitleOpts(opt opts.Title) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Title = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAnimation enable or disable the animation.
|
||||||
|
func WithAnimation() GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Animation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithToolboxOpts sets the toolbox.
|
||||||
|
func WithToolboxOpts(opt opts.Toolbox) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Toolbox = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSingleAxisOpts sets the single axis.
|
||||||
|
func WithSingleAxisOpts(opt opts.SingleAxis) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.SingleAxis = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTooltipOpts sets the tooltip.
|
||||||
|
func WithTooltipOpts(opt opts.Tooltip) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Tooltip = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLegendOpts sets the legend.
|
||||||
|
func WithLegendOpts(opt opts.Legend) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Legend = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInitializationOpts sets the initialization.
|
||||||
|
func WithInitializationOpts(opt opts.Initialization) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Initialization = opt
|
||||||
|
if bc.Initialization.Theme != "" &&
|
||||||
|
bc.Initialization.Theme != "white" &&
|
||||||
|
bc.Initialization.Theme != "dark" {
|
||||||
|
bc.JSAssets.Add("themes/" + opt.Theme + ".js")
|
||||||
|
}
|
||||||
|
bc.Initialization.Validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDataZoomOpts sets the list of the zoom data.
|
||||||
|
func WithDataZoomOpts(opt ...opts.DataZoom) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.DataZoomList = append(bc.DataZoomList, opt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVisualMapOpts sets the List of the visual map.
|
||||||
|
func WithVisualMapOpts(opt ...opts.VisualMap) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.VisualMapList = append(bc.VisualMapList, opt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRadarComponentOpts sets the component of the radar.
|
||||||
|
func WithRadarComponentOpts(opt opts.RadarComponent) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.RadarComponent = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGeoComponentOpts sets the geo component.
|
||||||
|
func WithGeoComponentOpts(opt opts.GeoComponent) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.GeoComponent = opt
|
||||||
|
bc.JSAssets.Add("maps/" + datasets.MapFileNames[opt.Map] + ".js")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParallelComponentOpts sets the parallel component.
|
||||||
|
func WithParallelComponentOpts(opt opts.ParallelComponent) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.ParallelComponent = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParallelAxisList sets the list of the parallel axis.
|
||||||
|
func WithParallelAxisList(opt []opts.ParallelAxis) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.ParallelAxisList = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithColorsOpts sets the color.
|
||||||
|
func WithColorsOpts(opt opts.Colors) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.insertSeriesColors(opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverseSlice reverses the string slice.
|
||||||
|
func reverseSlice(s []string) []string {
|
||||||
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGridOpts sets the List of the grid.
|
||||||
|
func WithGridOpts(opt ...opts.Grid) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.GridList = append(bc.GridList, opt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAxisPointerOpts sets the axis pointer.
|
||||||
|
func WithAxisPointerOpts(opt *opts.AxisPointer) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.AxisPointer = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
44
vendor/github.com/go-echarts/go-echarts/v2/charts/boxplot.go
generated
vendored
Normal file
44
vendor/github.com/go-echarts/go-echarts/v2/charts/boxplot.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoxPlot represents a boxplot chart.
|
||||||
|
type BoxPlot struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*BoxPlot) Type() string { return types.ChartBoxPlot }
|
||||||
|
|
||||||
|
// NewBoxPlot creates a new boxplot chart.
|
||||||
|
func NewBoxPlot() *BoxPlot {
|
||||||
|
c := &BoxPlot{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *BoxPlot) SetXAxis(x interface{}) *BoxPlot {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *BoxPlot) AddSeries(name string, data []opts.BoxPlotData, options ...SeriesOpts) *BoxPlot {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartBoxPlot, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *BoxPlot) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
67
vendor/github.com/go-echarts/go-echarts/v2/charts/chart3d.go
generated
vendored
Normal file
67
vendor/github.com/go-echarts/go-echarts/v2/charts/chart3d.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chart3D is a chart in 3D coordinates.
|
||||||
|
type Chart3D struct {
|
||||||
|
BaseConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXAxis3DOpts sets the X axis of the Chart3D instance.
|
||||||
|
func WithXAxis3DOpts(opt opts.XAxis3D) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.XAxis3D = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithYAxis3DOpts sets the Y axis of the Chart3D instance.
|
||||||
|
func WithYAxis3DOpts(opt opts.YAxis3D) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.YAxis3D = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithZAxis3DOpts sets the Z axis of the Chart3D instance.
|
||||||
|
func WithZAxis3DOpts(opt opts.ZAxis3D) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.ZAxis3D = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGrid3DOpts sets the grid of the Chart3D instance.
|
||||||
|
func WithGrid3DOpts(opt opts.Grid3D) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
bc.Grid3D = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chart3D) initChart3D() {
|
||||||
|
c.JSAssets.Add(opts.CompatibleEchartsJS)
|
||||||
|
c.JSAssets.Add("echarts-gl.min.js")
|
||||||
|
c.has3DAxis = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chart3D) addSeries(chartType, name string, data []opts.Chart3DData, options ...SeriesOpts) {
|
||||||
|
series := SingleSeries{
|
||||||
|
Name: name,
|
||||||
|
Type: chartType,
|
||||||
|
Data: data,
|
||||||
|
CoordSystem: types.ChartCartesian3D,
|
||||||
|
}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Chart3D instance.
|
||||||
|
func (c *Chart3D) SetGlobalOptions(options ...GlobalOpts) *Chart3D {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Chart3D) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
44
vendor/github.com/go-echarts/go-echarts/v2/charts/effectscatter.go
generated
vendored
Normal file
44
vendor/github.com/go-echarts/go-echarts/v2/charts/effectscatter.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EffectScatter represents an effect scatter chart.
|
||||||
|
type EffectScatter struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*EffectScatter) Type() string { return types.ChartEffectScatter }
|
||||||
|
|
||||||
|
// NewEffectScatter creates a new effect scatter chart.
|
||||||
|
func NewEffectScatter() *EffectScatter {
|
||||||
|
c := &EffectScatter{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *EffectScatter) SetXAxis(x interface{}) *EffectScatter {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the Y axis.
|
||||||
|
func (c *EffectScatter) AddSeries(name string, data []opts.EffectScatterData, options ...SeriesOpts) *EffectScatter {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartEffectScatter, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *EffectScatter) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/funnel.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/funnel.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Funnel represents a funnel chart.
|
||||||
|
type Funnel struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Funnel) Type() string { return types.ChartFunnel }
|
||||||
|
|
||||||
|
// NewFunnel creates a new funnel chart.
|
||||||
|
func NewFunnel() *Funnel {
|
||||||
|
c := &Funnel{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Funnel) AddSeries(name string, data []opts.FunnelData, options ...SeriesOpts) *Funnel {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartFunnel, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Funnel instance.
|
||||||
|
func (c *Funnel) SetGlobalOptions(options ...GlobalOpts) *Funnel {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Gauge instance.
|
||||||
|
func (c *Funnel) SetDispatchActions(actions ...GlobalActions) *Funnel {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Funnel) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/gauge.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/gauge.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gauge represents a gauge chart.
|
||||||
|
type Gauge struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Gauge) Type() string { return types.ChartGauge }
|
||||||
|
|
||||||
|
// NewGauge creates a new gauge chart.
|
||||||
|
func NewGauge() *Gauge {
|
||||||
|
c := &Gauge{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Gauge) AddSeries(name string, data []opts.GaugeData, options ...SeriesOpts) *Gauge {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartGauge, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Gauge instance.
|
||||||
|
func (c *Gauge) SetGlobalOptions(options ...GlobalOpts) *Gauge {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Gauge instance.
|
||||||
|
func (c *Gauge) SetDispatchActions(actions ...GlobalActions) *Gauge {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Gauge) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
75
vendor/github.com/go-echarts/go-echarts/v2/charts/geo.go
generated
vendored
Normal file
75
vendor/github.com/go-echarts/go-echarts/v2/charts/geo.go
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-echarts/go-echarts/v2/datasets"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Geo represents a geo chart.
|
||||||
|
type Geo struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Geo) Type() string { return types.ChartGeo }
|
||||||
|
|
||||||
|
var geoFormatter = `function (params) {
|
||||||
|
return params.name + ' : ' + params.value[2];
|
||||||
|
}`
|
||||||
|
|
||||||
|
// NewGeo creates a new geo chart.
|
||||||
|
func NewGeo() *Geo {
|
||||||
|
c := &Geo{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasGeo = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
// geoType options:
|
||||||
|
// * types.ChartScatter
|
||||||
|
// * types.ChartEffectScatter
|
||||||
|
// * types.ChartHeatMap
|
||||||
|
func (c *Geo) AddSeries(name, geoType string, data []opts.GeoData, options ...SeriesOpts) *Geo {
|
||||||
|
series := SingleSeries{Name: name, Type: geoType, Data: data, CoordSystem: types.ChartGeo}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Geo) extendValue(region string, v float32) []float32 {
|
||||||
|
res := make([]float32, 0)
|
||||||
|
tv := datasets.Coordinates[region]
|
||||||
|
if tv == [2]float32{0, 0} {
|
||||||
|
log.Printf("goecharts: No coordinate is specified for %s\n", region)
|
||||||
|
} else {
|
||||||
|
res = append(tv[:], v)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Geo instance.
|
||||||
|
func (c *Geo) SetGlobalOptions(options ...GlobalOpts) *Geo {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Geo instance.
|
||||||
|
func (c *Geo) SetDispatchActions(actions ...GlobalActions) *Geo {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Geo) Validate() {
|
||||||
|
if c.Tooltip.Formatter == "" {
|
||||||
|
c.Tooltip.Formatter = opts.FuncOpts(geoFormatter)
|
||||||
|
}
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
55
vendor/github.com/go-echarts/go-echarts/v2/charts/graph.go
generated
vendored
Normal file
55
vendor/github.com/go-echarts/go-echarts/v2/charts/graph.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Graph represents a graph chart.
|
||||||
|
type Graph struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Graph) Type() string { return types.ChartGraph }
|
||||||
|
|
||||||
|
// NewGraph creates a new graph chart.
|
||||||
|
func NewGraph() *Graph {
|
||||||
|
chart := new(Graph)
|
||||||
|
chart.initBaseConfiguration()
|
||||||
|
chart.Renderer = render.NewChartRender(chart, chart.Validate)
|
||||||
|
return chart
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Graph) AddSeries(name string, nodes []opts.GraphNode, links []opts.GraphLink, options ...SeriesOpts) *Graph {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartGraph, Links: links, Data: nodes}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Graph instance.
|
||||||
|
func (c *Graph) SetGlobalOptions(options ...GlobalOpts) *Graph {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Graph instance.
|
||||||
|
func (c *Graph) SetDispatchActions(actions ...GlobalActions) *Graph {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Graph) Validate() {
|
||||||
|
// If there is no layout setting, default layout is set to "force".
|
||||||
|
for i := 0; i < len(c.MultiSeries); i++ {
|
||||||
|
if c.MultiSeries[i].Layout == "" {
|
||||||
|
c.MultiSeries[i].Layout = "force"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
43
vendor/github.com/go-echarts/go-echarts/v2/charts/heatmap.go
generated
vendored
Normal file
43
vendor/github.com/go-echarts/go-echarts/v2/charts/heatmap.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HeatMap represents a heatmap chart.
|
||||||
|
type HeatMap struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*HeatMap) Type() string { return types.ChartHeatMap }
|
||||||
|
|
||||||
|
// NewHeatMap creates a new heatmap chart.
|
||||||
|
func NewHeatMap() *HeatMap {
|
||||||
|
c := &HeatMap{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *HeatMap) SetXAxis(x interface{}) *HeatMap {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *HeatMap) AddSeries(name string, data []opts.HeatMapData, options ...SeriesOpts) *HeatMap {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartHeatMap, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *HeatMap) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
44
vendor/github.com/go-echarts/go-echarts/v2/charts/kline.go
generated
vendored
Normal file
44
vendor/github.com/go-echarts/go-echarts/v2/charts/kline.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kline represents a kline chart.
|
||||||
|
type Kline struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Kline) Type() string { return types.ChartKline }
|
||||||
|
|
||||||
|
// NewKLine creates a new kline chart.
|
||||||
|
func NewKLine() *Kline {
|
||||||
|
c := &Kline{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *Kline) SetXAxis(xAxis interface{}) *Kline {
|
||||||
|
c.xAxisData = xAxis
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Kline) AddSeries(name string, data []opts.KlineData, options ...SeriesOpts) *Kline {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartKline, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Kline) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
45
vendor/github.com/go-echarts/go-echarts/v2/charts/line.go
generated
vendored
Normal file
45
vendor/github.com/go-echarts/go-echarts/v2/charts/line.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Line represents a line chart.
|
||||||
|
type Line struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Line) Type() string { return types.ChartLine }
|
||||||
|
|
||||||
|
// NewLine creates a new line chart.
|
||||||
|
func NewLine() *Line {
|
||||||
|
c := &Line{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *Line) SetXAxis(x interface{}) *Line {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Line) AddSeries(name string, data []opts.LineData, options ...SeriesOpts) *Line {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartLine, Data: data}
|
||||||
|
series.InitSeriesDefaultOpts(c.BaseConfiguration)
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Line) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
30
vendor/github.com/go-echarts/go-echarts/v2/charts/line3d.go
generated
vendored
Normal file
30
vendor/github.com/go-echarts/go-echarts/v2/charts/line3d.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Line3D represents a 3D line chart.
|
||||||
|
type Line3D struct {
|
||||||
|
Chart3D
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Line3D) Type() string { return types.ChartLine3D }
|
||||||
|
|
||||||
|
// NewLine3D creates a new 3D line chart.
|
||||||
|
func NewLine3D() *Line3D {
|
||||||
|
c := &Line3D{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.initChart3D()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Line3D) AddSeries(name string, data []opts.Chart3DData, options ...SeriesOpts) *Line3D {
|
||||||
|
c.addSeries(types.ChartLine3D, name, data, options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
51
vendor/github.com/go-echarts/go-echarts/v2/charts/liquid.go
generated
vendored
Normal file
51
vendor/github.com/go-echarts/go-echarts/v2/charts/liquid.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Liquid represents a liquid chart.
|
||||||
|
type Liquid struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Liquid) Type() string { return types.ChartLiquid }
|
||||||
|
|
||||||
|
// NewLiquid creates a new liquid chart.
|
||||||
|
func NewLiquid() *Liquid {
|
||||||
|
c := &Liquid{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.JSAssets.Add(opts.CompatibleEchartsJS)
|
||||||
|
c.JSAssets.Add("echarts-liquidfill.min.js")
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Liquid) AddSeries(name string, data []opts.LiquidData, options ...SeriesOpts) *Liquid {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartLiquid, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Liquid instance.
|
||||||
|
func (c *Liquid) SetGlobalOptions(options ...GlobalOpts) *Liquid {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Liquid instance.
|
||||||
|
func (c *Liquid) SetDispatchActions(actions ...GlobalActions) *Liquid {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Liquid) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
58
vendor/github.com/go-echarts/go-echarts/v2/charts/map.go
generated
vendored
Normal file
58
vendor/github.com/go-echarts/go-echarts/v2/charts/map.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/datasets"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map represents a map chart.
|
||||||
|
type Map struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
|
||||||
|
mapType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Map) Type() string { return types.ChartMap }
|
||||||
|
|
||||||
|
// NewMap creates a new map chart.
|
||||||
|
func NewMap() *Map {
|
||||||
|
c := &Map{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterMapType registers the given mapType.
|
||||||
|
func (c *Map) RegisterMapType(mapType string) {
|
||||||
|
c.mapType = mapType
|
||||||
|
c.JSAssets.Add("maps/" + datasets.MapFileNames[mapType] + ".js")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Map) AddSeries(name string, data []opts.MapData, options ...SeriesOpts) *Map {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartMap, MapType: c.mapType, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Map instance.
|
||||||
|
func (c *Map) SetGlobalOptions(options ...GlobalOpts) *Map {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Radar instance.
|
||||||
|
func (c *Map) SetDispatchActions(actions ...GlobalActions) *Map {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Map) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
50
vendor/github.com/go-echarts/go-echarts/v2/charts/parallel.go
generated
vendored
Normal file
50
vendor/github.com/go-echarts/go-echarts/v2/charts/parallel.go
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parallel represents a parallel axis.
|
||||||
|
type Parallel struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Parallel) Type() string { return types.ChartParallel }
|
||||||
|
|
||||||
|
// NewParallel creates a new parallel instance.
|
||||||
|
func NewParallel() *Parallel {
|
||||||
|
c := &Parallel{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasParallel = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Parallel) AddSeries(name string, data []opts.ParallelData, options ...SeriesOpts) *Parallel {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartParallel, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Parallel instance.
|
||||||
|
func (c *Parallel) SetGlobalOptions(options ...GlobalOpts) *Parallel {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Radar instance.
|
||||||
|
func (c *Parallel) SetDispatchActions(actions ...GlobalActions) *Parallel {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Parallel) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/pie.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/pie.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pie represents a pie chart.
|
||||||
|
type Pie struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Pie) Type() string { return types.ChartPie }
|
||||||
|
|
||||||
|
// NewPie creates a new pie chart.
|
||||||
|
func NewPie() *Pie {
|
||||||
|
c := &Pie{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Pie) AddSeries(name string, data []opts.PieData, options ...SeriesOpts) *Pie {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartPie, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Pie instance.
|
||||||
|
func (c *Pie) SetGlobalOptions(options ...GlobalOpts) *Pie {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Pie instance.
|
||||||
|
func (c *Pie) SetDispatchActions(actions ...GlobalActions) *Pie {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Pie) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
55
vendor/github.com/go-echarts/go-echarts/v2/charts/radar.go
generated
vendored
Normal file
55
vendor/github.com/go-echarts/go-echarts/v2/charts/radar.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Radar represents a radar chart.
|
||||||
|
type Radar struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
|
||||||
|
// SymbolKeepAspect is whether to keep aspect for symbols in the form of path://.
|
||||||
|
SymbolKeepAspect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Radar) Type() string { return types.ChartRadar }
|
||||||
|
|
||||||
|
// NewRadar creates a new radar chart.
|
||||||
|
func NewRadar() *Radar {
|
||||||
|
c := &Radar{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasRadar = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Radar) AddSeries(name string, data []opts.RadarData, options ...SeriesOpts) *Radar {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartRadar, Data: data, SymbolKeepAspect: c.SymbolKeepAspect}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
c.legends = append(c.legends, name)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Radar instance.
|
||||||
|
func (c *Radar) SetGlobalOptions(options ...GlobalOpts) *Radar {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Radar instance.
|
||||||
|
func (c *Radar) SetDispatchActions(actions ...GlobalActions) *Radar {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Radar) Validate() {
|
||||||
|
c.Legend.Data = c.legends
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
111
vendor/github.com/go-echarts/go-echarts/v2/charts/rectangle.go
generated
vendored
Normal file
111
vendor/github.com/go-echarts/go-echarts/v2/charts/rectangle.go
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Overlaper interface {
|
||||||
|
overlap() MultiSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
// XYAxis represent the X and Y axis in the rectangular coordinates.
|
||||||
|
type XYAxis struct {
|
||||||
|
XAxisList []opts.XAxis `json:"xaxis"`
|
||||||
|
YAxisList []opts.YAxis `json:"yaxis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xy *XYAxis) initXYAxis() {
|
||||||
|
xy.XAxisList = append(xy.XAxisList, opts.XAxis{})
|
||||||
|
xy.YAxisList = append(xy.YAxisList, opts.YAxis{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendXAxis adds new X axes.
|
||||||
|
func (xy *XYAxis) ExtendXAxis(xAxis ...opts.XAxis) {
|
||||||
|
xy.XAxisList = append(xy.XAxisList, xAxis...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendYAxis adds new Y axes.
|
||||||
|
func (xy *XYAxis) ExtendYAxis(yAxis ...opts.YAxis) {
|
||||||
|
xy.YAxisList = append(xy.YAxisList, yAxis...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXAxisOpts sets the X axis.
|
||||||
|
func WithXAxisOpts(opt opts.XAxis, index ...int) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
if len(index) == 0 {
|
||||||
|
index = []int{0}
|
||||||
|
}
|
||||||
|
for i := 0; i < len(index); i++ {
|
||||||
|
bc.XYAxis.XAxisList[index[i]] = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithYAxisOpts sets the Y axis.
|
||||||
|
func WithYAxisOpts(opt opts.YAxis, index ...int) GlobalOpts {
|
||||||
|
return func(bc *BaseConfiguration) {
|
||||||
|
if len(index) == 0 {
|
||||||
|
index = []int{0}
|
||||||
|
}
|
||||||
|
for i := 0; i < len(index); i++ {
|
||||||
|
bc.XYAxis.YAxisList[index[i]] = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RectConfiguration contains options for the rectangular coordinates.
|
||||||
|
type RectConfiguration struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rect *RectConfiguration) setRectGlobalOptions(options ...GlobalOpts) {
|
||||||
|
rect.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rect *RectConfiguration) setRectGlobalActions(options ...GlobalActions) {
|
||||||
|
rect.BaseActions.setBaseGlobalActions(options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RectChart is a chart in RectChart coordinate.
|
||||||
|
type RectChart struct {
|
||||||
|
RectConfiguration
|
||||||
|
|
||||||
|
xAxisData interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RectChart) overlap() MultiSeries {
|
||||||
|
return rc.MultiSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the RectChart instance.
|
||||||
|
func (rc *RectChart) SetGlobalOptions(options ...GlobalOpts) *RectChart {
|
||||||
|
rc.RectConfiguration.setRectGlobalOptions(options...)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the RectChart instance.
|
||||||
|
func (rc *RectChart) SetDispatchActions(options ...GlobalActions) *RectChart {
|
||||||
|
rc.RectConfiguration.setRectGlobalActions(options...)
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlap composes multiple charts into one single canvas.
|
||||||
|
// It is only suited for some of the charts which are in rectangular coordinate.
|
||||||
|
// Supported charts: Bar/BoxPlot/Line/Scatter/EffectScatter/Kline/HeatMap
|
||||||
|
func (rc *RectChart) Overlap(a ...Overlaper) {
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
rc.MultiSeries = append(rc.MultiSeries, a[i].overlap()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (rc *RectChart) Validate() {
|
||||||
|
// Make sure that the data of X axis won't be cleaned for XAxisOpts
|
||||||
|
rc.XAxisList[0].Data = rc.xAxisData
|
||||||
|
// Make sure that the labels of Y axis show correctly
|
||||||
|
for i := 0; i < len(rc.YAxisList); i++ {
|
||||||
|
rc.YAxisList[i].AxisLabel.Show = true
|
||||||
|
}
|
||||||
|
rc.Assets.Validate(rc.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/sankey.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/sankey.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sankey represents a sankey chart.
|
||||||
|
type Sankey struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Sankey) Type() string { return types.ChartSankey }
|
||||||
|
|
||||||
|
// NewSankey creates a new sankey chart.
|
||||||
|
func NewSankey() *Sankey {
|
||||||
|
c := &Sankey{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Sankey) AddSeries(name string, nodes []opts.SankeyNode, links []opts.SankeyLink, options ...SeriesOpts) *Sankey {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartSankey, Data: nodes, Links: links}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Sankey instance.
|
||||||
|
func (c *Sankey) SetGlobalOptions(options ...GlobalOpts) *Sankey {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Sankey instance.
|
||||||
|
func (c *Sankey) SetDispatchActions(actions ...GlobalActions) *Sankey {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Sankey) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
44
vendor/github.com/go-echarts/go-echarts/v2/charts/scatter.go
generated
vendored
Normal file
44
vendor/github.com/go-echarts/go-echarts/v2/charts/scatter.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scatter represents a scatter chart.
|
||||||
|
type Scatter struct {
|
||||||
|
RectChart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Scatter) Type() string { return types.ChartScatter }
|
||||||
|
|
||||||
|
// NewScatter creates a new scatter chart.
|
||||||
|
func NewScatter() *Scatter {
|
||||||
|
c := &Scatter{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasXYAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAxis adds the X axis.
|
||||||
|
func (c *Scatter) SetXAxis(x interface{}) *Scatter {
|
||||||
|
c.xAxisData = x
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Scatter) AddSeries(name string, data []opts.ScatterData, options ...SeriesOpts) *Scatter {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartScatter, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Scatter) Validate() {
|
||||||
|
c.XAxisList[0].Data = c.xAxisData
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
30
vendor/github.com/go-echarts/go-echarts/v2/charts/scatter3d.go
generated
vendored
Normal file
30
vendor/github.com/go-echarts/go-echarts/v2/charts/scatter3d.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scatter3D represents a 3D scatter chart.
|
||||||
|
type Scatter3D struct {
|
||||||
|
Chart3D
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Scatter3D) Type() string { return types.ChartScatter3D }
|
||||||
|
|
||||||
|
// NewScatter3D creates a new 3D scatter chart.
|
||||||
|
func NewScatter3D() *Scatter3D {
|
||||||
|
c := &Scatter3D{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.initChart3D()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Scatter3D) AddSeries(name string, data []opts.Chart3DData, options ...SeriesOpts) *Scatter3D {
|
||||||
|
c.addSeries(types.ChartScatter3D, name, data, options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
549
vendor/github.com/go-echarts/go-echarts/v2/charts/series.go
generated
vendored
Normal file
549
vendor/github.com/go-echarts/go-echarts/v2/charts/series.go
generated
vendored
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import "github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
|
||||||
|
type SingleSeries struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Rectangular charts
|
||||||
|
Stack string `json:"stack,omitempty"`
|
||||||
|
XAxisIndex int `json:"xAxisIndex,omitempty"`
|
||||||
|
YAxisIndex int `json:"yAxisIndex,omitempty"`
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
BarGap string `json:"barGap,omitempty"`
|
||||||
|
BarCategoryGap string `json:"barCategoryGap,omitempty"`
|
||||||
|
ShowBackground bool `json:"showBackground,omitempty"`
|
||||||
|
RoundCap bool `json:"roundCap,omitempty"`
|
||||||
|
|
||||||
|
// Bar3D
|
||||||
|
Shading string `json:"shading,omitempty"`
|
||||||
|
|
||||||
|
// Graph
|
||||||
|
Links interface{} `json:"links,omitempty"`
|
||||||
|
Layout string `json:"layout,omitempty"`
|
||||||
|
Force interface{} `json:"force,omitempty"`
|
||||||
|
Categories interface{} `json:"categories,omitempty"`
|
||||||
|
Roam bool `json:"roam,omitempty"`
|
||||||
|
EdgeSymbol interface{} `json:"edgeSymbol,omitempty"`
|
||||||
|
EdgeSymbolSize interface{} `json:"edgeSymbolSize,omitempty"`
|
||||||
|
EdgeLabel interface{} `json:"edgeLabel,omitempty"`
|
||||||
|
Draggable bool `json:"draggable,omitempty"`
|
||||||
|
FocusNodeAdjacency bool `json:"focusNodeAdjacency,omitempty"`
|
||||||
|
SymbolKeepAspect bool `json:"symbolKeepAspect,omitempty"`
|
||||||
|
|
||||||
|
// KLine
|
||||||
|
BarWidth string `json:"barWidth,omitempty"`
|
||||||
|
BarMinWidth string `json:"barMinWidth,omitempty"`
|
||||||
|
BarMaxWidth string `json:"barMaxWidth,omitempty"`
|
||||||
|
|
||||||
|
// Line
|
||||||
|
Step interface{} `json:"step,omitempty"`
|
||||||
|
Smooth bool `json:"smooth"`
|
||||||
|
ConnectNulls bool `json:"connectNulls"`
|
||||||
|
ShowSymbol bool `json:"showSymbol"`
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// Liquid
|
||||||
|
IsLiquidOutline bool `json:"outline,omitempty"`
|
||||||
|
IsWaveAnimation bool `json:"waveAnimation"`
|
||||||
|
|
||||||
|
// Map
|
||||||
|
MapType string `json:"map,omitempty"`
|
||||||
|
CoordSystem string `json:"coordinateSystem,omitempty"`
|
||||||
|
|
||||||
|
// Pie
|
||||||
|
RoseType interface{} `json:"roseType,omitempty"`
|
||||||
|
Center interface{} `json:"center,omitempty"`
|
||||||
|
Radius interface{} `json:"radius,omitempty"`
|
||||||
|
|
||||||
|
// Scatter
|
||||||
|
SymbolSize interface{} `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// Tree
|
||||||
|
Orient string `json:"orient,omitempty"`
|
||||||
|
ExpandAndCollapse bool `json:"expandAndCollapse,omitempty"`
|
||||||
|
InitialTreeDepth int `json:"initialTreeDepth,omitempty"`
|
||||||
|
Leaves interface{} `json:"leaves,omitempty"`
|
||||||
|
Left string `json:"left,omitempty"`
|
||||||
|
Right string `json:"right,omitempty"`
|
||||||
|
Top string `json:"top,omitempty"`
|
||||||
|
Bottom string `json:"bottom,omitempty"`
|
||||||
|
|
||||||
|
// TreeMap
|
||||||
|
LeafDepth int `json:"leafDepth,omitempty"`
|
||||||
|
Levels interface{} `json:"levels,omitempty"`
|
||||||
|
UpperLabel interface{} `json:"upperLabel,omitempty"`
|
||||||
|
|
||||||
|
// WordCloud
|
||||||
|
Shape string `json:"shape,omitempty"`
|
||||||
|
SizeRange []float32 `json:"sizeRange,omitempty"`
|
||||||
|
RotationRange []float32 `json:"rotationRange,omitempty"`
|
||||||
|
|
||||||
|
// Sunburst
|
||||||
|
NodeClick string `json:"nodeClick,omitempty"`
|
||||||
|
Sort string `json:"sort,omitempty"`
|
||||||
|
RenderLabelForZeroData bool `json:"renderLabelForZeroData"`
|
||||||
|
SelectedMode bool `json:"selectedMode"`
|
||||||
|
Animation bool `json:"animation" default:"true"`
|
||||||
|
AnimationThreshold int `json:"animationThreshold,omitempty"`
|
||||||
|
AnimationDuration int `json:"animationDuration,omitempty"`
|
||||||
|
AnimationEasing string `json:"animationEasing,omitempty"`
|
||||||
|
AnimationDelay int `json:"animationDelay,omitempty"`
|
||||||
|
AnimationDurationUpdate int `json:"animationDurationUpdate,omitempty"`
|
||||||
|
AnimationEasingUpdate string `json:"animationEasingUpdate,omitempty"`
|
||||||
|
AnimationDelayUpdate int `json:"animationDelayUpdate,omitempty"`
|
||||||
|
|
||||||
|
// series data
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
DatasetIndex int `json:"datasetIndex,omitempty"`
|
||||||
|
|
||||||
|
// series options
|
||||||
|
*opts.Encode `json:"encode,omitempty"`
|
||||||
|
*opts.ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
*opts.Label `json:"label,omitempty"`
|
||||||
|
*opts.LabelLine `json:"labelLine,omitempty"`
|
||||||
|
*opts.Emphasis `json:"emphasis,omitempty"`
|
||||||
|
*opts.MarkLines `json:"markLine,omitempty"`
|
||||||
|
*opts.MarkAreas `json:"markArea,omitempty"`
|
||||||
|
*opts.MarkPoints `json:"markPoint,omitempty"`
|
||||||
|
*opts.RippleEffect `json:"rippleEffect,omitempty"`
|
||||||
|
*opts.LineStyle `json:"lineStyle,omitempty"`
|
||||||
|
*opts.AreaStyle `json:"areaStyle,omitempty"`
|
||||||
|
*opts.TextStyle `json:"textStyle,omitempty"`
|
||||||
|
*opts.CircularStyle `json:"circular,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesOpts func(s *SingleSeries)
|
||||||
|
|
||||||
|
func WithSeriesAnimation(enable bool) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Animation = enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLabelOpts sets the label.
|
||||||
|
func WithLabelOpts(opt opts.Label) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Label = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEmphasisOpts sets the emphasis.
|
||||||
|
func WithEmphasisOpts(opt opts.Emphasis) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Emphasis = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAreaStyleOpts sets the area style.
|
||||||
|
func WithAreaStyleOpts(opt opts.AreaStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.AreaStyle = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithItemStyleOpts sets the item style.
|
||||||
|
func WithItemStyleOpts(opt opts.ItemStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.ItemStyle = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRippleEffectOpts sets the ripple effect.
|
||||||
|
func WithRippleEffectOpts(opt opts.RippleEffect) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.RippleEffect = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLineStyleOpts sets the line style.
|
||||||
|
func WithLineStyleOpts(opt opts.LineStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.LineStyle = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With CircularStyle Opts
|
||||||
|
func WithCircularStyleOpts(opt opts.CircularStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.CircularStyle = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Options */
|
||||||
|
|
||||||
|
// WithBarChartOpts sets the BarChart option.
|
||||||
|
func WithBarChartOpts(opt opts.BarChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Stack = opt.Stack
|
||||||
|
s.BarGap = opt.BarGap
|
||||||
|
s.BarCategoryGap = opt.BarCategoryGap
|
||||||
|
s.XAxisIndex = opt.XAxisIndex
|
||||||
|
s.YAxisIndex = opt.YAxisIndex
|
||||||
|
s.ShowBackground = opt.ShowBackground
|
||||||
|
s.RoundCap = opt.RoundCap
|
||||||
|
s.CoordSystem = opt.CoordSystem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSunburstOpts sets the SunburstChart option.
|
||||||
|
func WithSunburstOpts(opt opts.SunburstChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.NodeClick = opt.NodeClick
|
||||||
|
s.Sort = opt.Sort
|
||||||
|
s.RenderLabelForZeroData = opt.RenderLabelForZeroData
|
||||||
|
s.SelectedMode = opt.SelectedMode
|
||||||
|
s.Animation = opt.Animation
|
||||||
|
s.AnimationThreshold = opt.AnimationThreshold
|
||||||
|
s.AnimationDuration = opt.AnimationDuration
|
||||||
|
s.AnimationEasing = opt.AnimationEasing
|
||||||
|
s.AnimationDelay = opt.AnimationDelay
|
||||||
|
s.AnimationDurationUpdate = opt.AnimationDurationUpdate
|
||||||
|
s.AnimationEasingUpdate = opt.AnimationEasingUpdate
|
||||||
|
s.AnimationDelayUpdate = opt.AnimationDelayUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGraphChartOpts sets the GraphChart option.
|
||||||
|
func WithGraphChartOpts(opt opts.GraphChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Layout = opt.Layout
|
||||||
|
s.Force = opt.Force
|
||||||
|
s.Roam = opt.Roam
|
||||||
|
s.EdgeSymbol = opt.EdgeSymbol
|
||||||
|
s.EdgeSymbolSize = opt.EdgeSymbolSize
|
||||||
|
s.Draggable = opt.Draggable
|
||||||
|
s.FocusNodeAdjacency = opt.FocusNodeAdjacency
|
||||||
|
s.Categories = opt.Categories
|
||||||
|
s.EdgeLabel = opt.EdgeLabel
|
||||||
|
s.SymbolKeepAspect = opt.SymbolKeepAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeatMapChartOpts sets the HeatMapChart option.
|
||||||
|
func WithHeatMapChartOpts(opt opts.HeatMapChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.XAxisIndex = opt.XAxisIndex
|
||||||
|
s.YAxisIndex = opt.YAxisIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLineChartOpts sets the LineChart option.
|
||||||
|
func WithLineChartOpts(opt opts.LineChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.YAxisIndex = opt.YAxisIndex
|
||||||
|
s.Stack = opt.Stack
|
||||||
|
s.Smooth = opt.Smooth
|
||||||
|
s.ShowSymbol = opt.ShowSymbol
|
||||||
|
s.Symbol = opt.Symbol
|
||||||
|
s.SymbolSize = opt.SymbolSize
|
||||||
|
s.Step = opt.Step
|
||||||
|
s.XAxisIndex = opt.XAxisIndex
|
||||||
|
s.YAxisIndex = opt.YAxisIndex
|
||||||
|
s.ConnectNulls = opt.ConnectNulls
|
||||||
|
s.Color = opt.Color
|
||||||
|
s.SymbolKeepAspect = opt.SymbolKeepAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLineChartOpts sets the LineChart option.
|
||||||
|
func WithKlineChartOpts(opt opts.KlineChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.BarWidth = opt.BarWidth
|
||||||
|
s.BarMinWidth = opt.BarMinWidth
|
||||||
|
s.BarMaxWidth = opt.BarMaxWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPieChartOpts sets the PieChart option.
|
||||||
|
func WithPieChartOpts(opt opts.PieChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.RoseType = opt.RoseType
|
||||||
|
s.Center = opt.Center
|
||||||
|
s.Radius = opt.Radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithScatterChartOpts sets the ScatterChart option.
|
||||||
|
func WithScatterChartOpts(opt opts.ScatterChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.XAxisIndex = opt.XAxisIndex
|
||||||
|
s.YAxisIndex = opt.YAxisIndex
|
||||||
|
s.SymbolKeepAspect = opt.SymbolKeepAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLiquidChartOpts sets the LiquidChart option.
|
||||||
|
func WithLiquidChartOpts(opt opts.LiquidChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Shape = opt.Shape
|
||||||
|
s.IsLiquidOutline = opt.IsShowOutline
|
||||||
|
s.IsWaveAnimation = opt.IsWaveAnimation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBar3DChartOpts sets the Bar3DChart option.
|
||||||
|
func WithBar3DChartOpts(opt opts.Bar3DChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Shading = opt.Shading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTreeOpts sets the TreeChart option.
|
||||||
|
func WithTreeOpts(opt opts.TreeChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Layout = opt.Layout
|
||||||
|
s.Orient = opt.Orient
|
||||||
|
s.ExpandAndCollapse = opt.ExpandAndCollapse
|
||||||
|
s.InitialTreeDepth = opt.InitialTreeDepth
|
||||||
|
s.Roam = opt.Roam
|
||||||
|
s.Label = opt.Label
|
||||||
|
s.Leaves = opt.Leaves
|
||||||
|
s.Right = opt.Right
|
||||||
|
s.Left = opt.Left
|
||||||
|
s.Top = opt.Top
|
||||||
|
s.Bottom = opt.Bottom
|
||||||
|
s.SymbolKeepAspect = opt.SymbolKeepAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTreeMapOpts sets the TreeMapChart options.
|
||||||
|
func WithTreeMapOpts(opt opts.TreeMapChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Animation = opt.Animation
|
||||||
|
s.LeafDepth = opt.LeafDepth
|
||||||
|
s.Roam = opt.Roam
|
||||||
|
s.Levels = opt.Levels
|
||||||
|
s.UpperLabel = opt.UpperLabel
|
||||||
|
s.Right = opt.Right
|
||||||
|
s.Left = opt.Left
|
||||||
|
s.Top = opt.Top
|
||||||
|
s.Bottom = opt.Bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWorldCloudChartOpts sets the WorldCloudChart option.
|
||||||
|
func WithWorldCloudChartOpts(opt opts.WordCloudChart) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Shape = opt.Shape
|
||||||
|
s.SizeRange = opt.SizeRange
|
||||||
|
s.RotationRange = opt.RotationRange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkLineNameTypeItemOpts sets the type of the MarkLine.
|
||||||
|
func WithMarkLineNameTypeItemOpts(opt ...opts.MarkLineNameTypeItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkLines == nil {
|
||||||
|
s.MarkLines = &opts.MarkLines{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkLines.Data = append(s.MarkLines.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkLineStyleOpts sets the style of the MarkLine.
|
||||||
|
func WithMarkLineStyleOpts(opt opts.MarkLineStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkLines == nil {
|
||||||
|
s.MarkLines = &opts.MarkLines{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.MarkLines.MarkLineStyle = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkLineNameCoordItemOpts sets the coordinates of the MarkLine.
|
||||||
|
func WithMarkLineNameCoordItemOpts(opt ...opts.MarkLineNameCoordItem) SeriesOpts {
|
||||||
|
type MLNameCoord struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Coord []interface{} `json:"coord"`
|
||||||
|
}
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkLines == nil {
|
||||||
|
s.MarkLines = &opts.MarkLines{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkLines.Data = append(
|
||||||
|
s.MarkLines.Data,
|
||||||
|
[]MLNameCoord{{Name: o.Name, Coord: o.Coordinate0}, {Coord: o.Coordinate1}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkLineNameXAxisItemOpts sets the X axis of the MarkLine.
|
||||||
|
func WithMarkLineNameXAxisItemOpts(opt ...opts.MarkLineNameXAxisItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkLines == nil {
|
||||||
|
s.MarkLines = &opts.MarkLines{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkLines.Data = append(s.MarkLines.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkLineNameYAxisItemOpts sets the Y axis of the MarkLine.
|
||||||
|
func WithMarkLineNameYAxisItemOpts(opt ...opts.MarkLineNameYAxisItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkLines == nil {
|
||||||
|
s.MarkLines = &opts.MarkLines{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkLines.Data = append(s.MarkLines.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkAreaNameTypeItemOpts sets the type of the MarkArea.
|
||||||
|
func WithMarkAreaNameTypeItemOpts(opt ...opts.MarkAreaNameTypeItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkAreas == nil {
|
||||||
|
s.MarkAreas = &opts.MarkAreas{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkAreas.Data = append(s.MarkAreas.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkAreaStyleOpts sets the style of the MarkArea.
|
||||||
|
func WithMarkAreaStyleOpts(opt opts.MarkAreaStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkAreas == nil {
|
||||||
|
s.MarkAreas = &opts.MarkAreas{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.MarkAreas.MarkAreaStyle = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkAreaNameCoordItemOpts sets the coordinates of the MarkLine.
|
||||||
|
func WithMarkAreaNameCoordItemOpts(opt ...opts.MarkAreaNameCoordItem) SeriesOpts {
|
||||||
|
type MANameCoord struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ItemStyle *opts.ItemStyle `json:"itemStyle"`
|
||||||
|
Coord []interface{} `json:"coord"`
|
||||||
|
}
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkAreas == nil {
|
||||||
|
s.MarkAreas = &opts.MarkAreas{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkAreas.Data = append(
|
||||||
|
s.MarkAreas.Data,
|
||||||
|
[]MANameCoord{
|
||||||
|
{Name: o.Name, ItemStyle: o.ItemStyle, Coord: o.Coordinate0},
|
||||||
|
{Coord: o.Coordinate1},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkAreaNameXAxisItemOpts sets the X axis of the MarkLine.
|
||||||
|
func WithMarkAreaNameXAxisItemOpts(opt ...opts.MarkAreaNameXAxisItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkAreas == nil {
|
||||||
|
s.MarkAreas = &opts.MarkAreas{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkAreas.Data = append(s.MarkAreas.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkAreaNameYAxisItemOpts sets the Y axis of the MarkLine.
|
||||||
|
func WithMarkAreaNameYAxisItemOpts(opt ...opts.MarkAreaNameYAxisItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkAreas == nil {
|
||||||
|
s.MarkAreas = &opts.MarkAreas{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkAreas.Data = append(s.MarkAreas.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkPointNameTypeItemOpts sets the type of the MarkPoint.
|
||||||
|
func WithMarkPointNameTypeItemOpts(opt ...opts.MarkPointNameTypeItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkPoints == nil {
|
||||||
|
s.MarkPoints = &opts.MarkPoints{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkPoints.Data = append(s.MarkPoints.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkPointStyleOpts sets the style of the MarkPoint.
|
||||||
|
func WithMarkPointStyleOpts(opt opts.MarkPointStyle) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkPoints == nil {
|
||||||
|
s.MarkPoints = &opts.MarkPoints{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.MarkPoints.MarkPointStyle = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMarkPointNameCoordItemOpts sets the coordinated of the MarkPoint.
|
||||||
|
func WithMarkPointNameCoordItemOpts(opt ...opts.MarkPointNameCoordItem) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
if s.MarkPoints == nil {
|
||||||
|
s.MarkPoints = &opts.MarkPoints{}
|
||||||
|
}
|
||||||
|
for _, o := range opt {
|
||||||
|
s.MarkPoints.Data = append(s.MarkPoints.Data, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SingleSeries) InitSeriesDefaultOpts(c BaseConfiguration) {
|
||||||
|
opts.SetDefaultValue(s)
|
||||||
|
// some special inherited options from BaseConfiguration
|
||||||
|
s.Animation = c.Animation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SingleSeries) ConfigureSeriesOpts(options ...SeriesOpts) {
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSeries represents multiple series.
|
||||||
|
type MultiSeries []SingleSeries
|
||||||
|
|
||||||
|
// SetSeriesOptions sets options for all the series.
|
||||||
|
// Previous options will be overwrote every time hence setting them on the `AddSeries` if you want
|
||||||
|
// to customize each series individually
|
||||||
|
//
|
||||||
|
// here -> ↓ <-
|
||||||
|
//
|
||||||
|
// func (c *Bar) AddSeries(name string, data []opts.BarData, options ...SeriesOpts)
|
||||||
|
func (ms *MultiSeries) SetSeriesOptions(opts ...SeriesOpts) {
|
||||||
|
s := *ms
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
s[i].ConfigureSeriesOpts(opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncodeOpts Set encodes for dataSets
|
||||||
|
func WithEncodeOpts(opt opts.Encode) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.Encode = &opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDatasetIndex sets the datasetIndex option.
|
||||||
|
func WithDatasetIndex(index int) SeriesOpts {
|
||||||
|
return func(s *SingleSeries) {
|
||||||
|
s.DatasetIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/sunburst.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/sunburst.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sunburst represents a sunburst chart.
|
||||||
|
type Sunburst struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Sunburst) Type() string { return types.ChartSunburst }
|
||||||
|
|
||||||
|
// NewSunburst creates a new sunburst chart instance.
|
||||||
|
func NewSunburst() *Sunburst {
|
||||||
|
c := &Sunburst{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Sunburst) AddSeries(name string, data []opts.SunBurstData, options ...SeriesOpts) *Sunburst {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartSunburst, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Pie instance.
|
||||||
|
func (c *Sunburst) SetGlobalOptions(options ...GlobalOpts) *Sunburst {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Sunburst instance.
|
||||||
|
func (c *Sunburst) SetDispatchActions(actions ...GlobalActions) *Sunburst {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Sunburst) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
30
vendor/github.com/go-echarts/go-echarts/v2/charts/surface3d.go
generated
vendored
Normal file
30
vendor/github.com/go-echarts/go-echarts/v2/charts/surface3d.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Surface3D represents a 3D surface chart.
|
||||||
|
type Surface3D struct {
|
||||||
|
Chart3D
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Surface3D) Type() string { return types.ChartSurface3D }
|
||||||
|
|
||||||
|
// NewSurface3D creates a new 3d surface chart.
|
||||||
|
func NewSurface3D() *Surface3D {
|
||||||
|
c := &Surface3D{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.initChart3D()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds the new series.
|
||||||
|
func (c *Surface3D) AddSeries(name string, data []opts.Chart3DData, options ...SeriesOpts) *Surface3D {
|
||||||
|
c.addSeries(types.ChartScatter3D, name, data, options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
54
vendor/github.com/go-echarts/go-echarts/v2/charts/themeriver.go
generated
vendored
Normal file
54
vendor/github.com/go-echarts/go-echarts/v2/charts/themeriver.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThemeRiver represents a theme river chart.
|
||||||
|
type ThemeRiver struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*ThemeRiver) Type() string { return types.ChartThemeRiver }
|
||||||
|
|
||||||
|
// NewThemeRiver creates a new theme river chart.
|
||||||
|
func NewThemeRiver() *ThemeRiver {
|
||||||
|
c := &ThemeRiver{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.hasSingleAxis = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *ThemeRiver) AddSeries(name string, data []opts.ThemeRiverData, options ...SeriesOpts) *ThemeRiver {
|
||||||
|
cd := make([][3]interface{}, len(data))
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
cd[i] = data[i].ToList()
|
||||||
|
}
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartThemeRiver, Data: cd}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the ThemeRiver instance.
|
||||||
|
func (c *ThemeRiver) SetGlobalOptions(options ...GlobalOpts) *ThemeRiver {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the ThemeRiver instance.
|
||||||
|
func (c *ThemeRiver) SetDispatchActions(actions ...GlobalActions) *ThemeRiver {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *ThemeRiver) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/tree.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/tree.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree represents a Tree chart.
|
||||||
|
type Tree struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*Tree) Type() string { return types.ChartTree }
|
||||||
|
|
||||||
|
// NewTree creates a new Tree chart instance.
|
||||||
|
func NewTree() *Tree {
|
||||||
|
c := &Tree{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *Tree) AddSeries(name string, data []opts.TreeData, options ...SeriesOpts) *Tree {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartTree, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the Tree instance.
|
||||||
|
func (c *Tree) SetGlobalOptions(options ...GlobalOpts) *Tree {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the Tree instance.
|
||||||
|
func (c *Tree) SetDispatchActions(actions ...GlobalActions) *Tree {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *Tree) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
49
vendor/github.com/go-echarts/go-echarts/v2/charts/treemap.go
generated
vendored
Normal file
49
vendor/github.com/go-echarts/go-echarts/v2/charts/treemap.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TreeMap represents a TreeMap chart.
|
||||||
|
type TreeMap struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*TreeMap) Type() string { return types.ChartTreeMap }
|
||||||
|
|
||||||
|
// NewTreeMap creates a new TreeMap chart instance.
|
||||||
|
func NewTreeMap() *TreeMap {
|
||||||
|
c := &TreeMap{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *TreeMap) AddSeries(name string, data []opts.TreeMapNode, options ...SeriesOpts) *TreeMap {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartTreeMap, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the TreeMap instance.
|
||||||
|
func (c *TreeMap) SetGlobalOptions(options ...GlobalOpts) *TreeMap {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the TreeMap instance.
|
||||||
|
func (c *TreeMap) SetDispatchActions(actions ...GlobalActions) *TreeMap {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *TreeMap) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
67
vendor/github.com/go-echarts/go-echarts/v2/charts/wordcloud.go
generated
vendored
Normal file
67
vendor/github.com/go-echarts/go-echarts/v2/charts/wordcloud.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/render"
|
||||||
|
"github.com/go-echarts/go-echarts/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WordCloud represents a word cloud chart.
|
||||||
|
type WordCloud struct {
|
||||||
|
BaseConfiguration
|
||||||
|
BaseActions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the chart type.
|
||||||
|
func (*WordCloud) Type() string { return types.ChartWordCloud }
|
||||||
|
|
||||||
|
var wcTextColor = `function () {
|
||||||
|
return 'rgb(' + [
|
||||||
|
Math.round(Math.random() * 160),
|
||||||
|
Math.round(Math.random() * 160),
|
||||||
|
Math.round(Math.random() * 160)].join(',') + ')';
|
||||||
|
}`
|
||||||
|
|
||||||
|
// NewWordCloud creates a new word cloud chart.
|
||||||
|
func NewWordCloud() *WordCloud {
|
||||||
|
c := &WordCloud{}
|
||||||
|
c.initBaseConfiguration()
|
||||||
|
c.Renderer = render.NewChartRender(c, c.Validate)
|
||||||
|
c.JSAssets.Add(opts.CompatibleEchartsJS)
|
||||||
|
c.JSAssets.Add("echarts-wordcloud.min.js")
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSeries adds new data sets.
|
||||||
|
func (c *WordCloud) AddSeries(name string, data []opts.WordCloudData, options ...SeriesOpts) *WordCloud {
|
||||||
|
series := SingleSeries{Name: name, Type: types.ChartWordCloud, Data: data}
|
||||||
|
series.ConfigureSeriesOpts(options...)
|
||||||
|
|
||||||
|
// set default random color for WordCloud chart
|
||||||
|
if series.TextStyle == nil {
|
||||||
|
series.TextStyle = &opts.TextStyle{Normal: &opts.TextStyle{}}
|
||||||
|
}
|
||||||
|
if series.TextStyle.Normal.Color == "" {
|
||||||
|
series.TextStyle.Normal.Color = opts.FuncOpts(wcTextColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MultiSeries = append(c.MultiSeries, series)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalOptions sets options for the WordCloud instance.
|
||||||
|
func (c *WordCloud) SetGlobalOptions(options ...GlobalOpts) *WordCloud {
|
||||||
|
c.BaseConfiguration.setBaseGlobalOptions(options...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDispatchActions sets actions for the WordCloud instance.
|
||||||
|
func (c *WordCloud) SetDispatchActions(actions ...GlobalActions) *WordCloud {
|
||||||
|
c.BaseActions.setBaseGlobalActions(actions...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given configuration.
|
||||||
|
func (c *WordCloud) Validate() {
|
||||||
|
c.Assets.Validate(c.AssetsHost)
|
||||||
|
}
|
||||||
3755
vendor/github.com/go-echarts/go-echarts/v2/datasets/coordinates.go
generated
vendored
Normal file
3755
vendor/github.com/go-echarts/go-echarts/v2/datasets/coordinates.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
404
vendor/github.com/go-echarts/go-echarts/v2/datasets/mapfiles.go
generated
vendored
Normal file
404
vendor/github.com/go-echarts/go-echarts/v2/datasets/mapfiles.go
generated
vendored
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
package datasets
|
||||||
|
|
||||||
|
var MapFileNames = map[string]string{
|
||||||
|
"china": "china",
|
||||||
|
"world": "world",
|
||||||
|
"广东": "guangdong",
|
||||||
|
"安徽": "anhui",
|
||||||
|
"福建": "fujian",
|
||||||
|
"甘肃": "gansu",
|
||||||
|
"广西": "guangxi",
|
||||||
|
"贵州": "guizhou",
|
||||||
|
"海南": "hainan",
|
||||||
|
"河北": "hebei",
|
||||||
|
"黑龙江": "heilongjiang",
|
||||||
|
"河南": "henan",
|
||||||
|
"湖北": "hubei",
|
||||||
|
"湖南": "hunan",
|
||||||
|
"江苏": "jiangsu",
|
||||||
|
"江西": "jiangxi",
|
||||||
|
"吉林": "jilin",
|
||||||
|
"辽宁": "liaoning",
|
||||||
|
"内蒙古": "neimenggu",
|
||||||
|
"宁夏": "ningxia",
|
||||||
|
"青海": "qinghai",
|
||||||
|
"山东": "shandong",
|
||||||
|
"山西": "shanxi",
|
||||||
|
"陕西": "shanxi1",
|
||||||
|
"四川": "sichuan",
|
||||||
|
"台湾": "taiwan",
|
||||||
|
"新疆": "xinjiang",
|
||||||
|
"西藏": "xizang",
|
||||||
|
"云南": "yunnan",
|
||||||
|
"浙江": "zhejiang",
|
||||||
|
"七台河": "hei1_long2_jiang1_qi1_tai2_he2",
|
||||||
|
"万宁": "hai3_nan2_wan4_ning2",
|
||||||
|
"三亚": "hai3_nan2_san1_ya4",
|
||||||
|
"三明": "fu2_jian4_san1_ming2",
|
||||||
|
"三沙": "hai3_nan2_san1_sha1",
|
||||||
|
"三门峡": "he2_nan2_san1_men2_xia2",
|
||||||
|
"上海": "shanghai",
|
||||||
|
"上饶": "jiang1_xi1_shang4_rao2",
|
||||||
|
"东方": "hai3_nan2_dong1_fang1",
|
||||||
|
"东沙群岛": "guang3_dong1_dong1_sha1_qun2_dao3",
|
||||||
|
"东莞": "guang3_dong1_dong1_guan1",
|
||||||
|
"东营": "shan1_dong1_dong1_ying2",
|
||||||
|
"中卫": "ning2_xia4_zhong1_wei4",
|
||||||
|
"中山": "guang3_dong1_zhong1_shan1",
|
||||||
|
"临夏回族自治州": "gan1_su4_lin2_xia4_hui2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"临汾": "shan1_xi1_lin2_fen2",
|
||||||
|
"临沂": "shan1_dong1_lin2_yi2",
|
||||||
|
"临沧": "yun2_nan2_lin2_cang1",
|
||||||
|
"临高县": "hai3_nan2_lin2_gao1_xian4",
|
||||||
|
"丹东": "liao2_ning2_dan1_dong1",
|
||||||
|
"丽水": "zhe4_jiang1_li4_shui3",
|
||||||
|
"丽江": "yun2_nan2_li4_jiang1",
|
||||||
|
"乌兰察布": "nei4_meng2_gu3_wu1_lan2_cha2_bu4",
|
||||||
|
"乌海": "nei4_meng2_gu3_wu1_hai3",
|
||||||
|
"乌鲁木齐": "xin1_jiang1_wu1_lu3_mu4_qi2",
|
||||||
|
"乐东黎族自治县": "hai3_nan2_le4_dong1_li2_zu2_zi4_zhi4_xian4",
|
||||||
|
"乐山": "si4_chuan1_le4_shan1",
|
||||||
|
"九江": "jiang1_xi1_jiu3_jiang1",
|
||||||
|
"云浮": "guang3_dong1_yun2_fu2",
|
||||||
|
"五家渠": "xin1_jiang1_wu3_jia1_qu2",
|
||||||
|
"五指山": "hai3_nan2_wu3_zhi3_shan1",
|
||||||
|
"亳州": "an1_hui1_bo2_zhou1",
|
||||||
|
"仙桃": "hu2_bei3_xian1_tao2",
|
||||||
|
"伊春": "hei1_long2_jiang1_yi1_chun1",
|
||||||
|
"伊犁哈萨克自治州": "xin1_jiang1_yi1_li2_ha1_sa4_ke4_zi4_zhi4_zhou1",
|
||||||
|
"佛山": "guang3_dong1_fo2_shan1",
|
||||||
|
"佳木斯": "hei1_long2_jiang1_jia1_mu4_si1",
|
||||||
|
"保亭黎族苗族自治县": "hai3_nan2_bao3_ting2_li2_zu2_miao2_zu2_zi4_zhi4_xian4",
|
||||||
|
"保定": "he2_bei3_bao3_ding4",
|
||||||
|
"保山": "yun2_nan2_bao3_shan1",
|
||||||
|
"信阳": "he2_nan2_xin4_yang2",
|
||||||
|
"儋州": "hai3_nan2_dan1_zhou1",
|
||||||
|
"克孜勒苏柯尔克孜自治州": "xin1_jiang1_ke4_zi1_le4_su1_ke1_er3_ke4_zi1_zi4_zhi4_zhou1",
|
||||||
|
"克拉玛依": "xin1_jiang1_ke4_la1_ma3_yi1",
|
||||||
|
"六安": "an1_hui1_liu4_an1",
|
||||||
|
"六盘水": "gui4_zhou1_liu4_pan2_shui3",
|
||||||
|
"兰州": "gan1_su4_lan2_zhou1",
|
||||||
|
"兴安盟": "nei4_meng2_gu3_xing1_an1_meng2",
|
||||||
|
"内江": "si4_chuan1_nei4_jiang1",
|
||||||
|
"凉山彝族自治州": "si4_chuan1_liang2_shan1_yi2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"包头": "nei4_meng2_gu3_bao1_tou2",
|
||||||
|
"北京": "beijing",
|
||||||
|
"北屯": "xin1_jiang1_bei3_tun2",
|
||||||
|
"北海": "guang3_xi1_bei3_hai3",
|
||||||
|
"十堰": "hu2_bei3_shi2_yan4",
|
||||||
|
"南京": "jiang1_su1_nan2_jing1",
|
||||||
|
"南充": "si4_chuan1_nan2_chong1",
|
||||||
|
"南宁": "guang3_xi1_nan2_ning2",
|
||||||
|
"南平": "fu2_jian4_nan2_ping2",
|
||||||
|
"南昌": "jiang1_xi1_nan2_chang1",
|
||||||
|
"南通": "jiang1_su1_nan2_tong1",
|
||||||
|
"南阳": "he2_nan2_nan2_yang2",
|
||||||
|
"博尔塔拉蒙古自治州": "xin1_jiang1_bo2_er3_ta3_la1_meng2_gu3_zi4_zhi4_zhou1",
|
||||||
|
"厦门": "fu2_jian4_sha4_men2",
|
||||||
|
"双河": "xin1_jiang1_shuang1_he2",
|
||||||
|
"双鸭山": "hei1_long2_jiang1_shuang1_ya1_shan1",
|
||||||
|
"可克达拉": "xin1_jiang1_ke3_ke4_da2_la1",
|
||||||
|
"台州": "zhe4_jiang1_tai2_zhou1",
|
||||||
|
"合肥": "an1_hui1_he2_fei2",
|
||||||
|
"吉安": "jiang1_xi1_ji2_an1",
|
||||||
|
"吉林市": "ji2_lin2_ji2_lin2",
|
||||||
|
"吐鲁番": "xin1_jiang1_tu3_lu3_fan1",
|
||||||
|
"吕梁": "shan1_xi1_lv3_liang2",
|
||||||
|
"吴忠": "ning2_xia4_wu2_zhong1",
|
||||||
|
"周口": "he2_nan2_zhou1_kou3",
|
||||||
|
"呼伦贝尔": "nei4_meng2_gu3_hu1_lun2_bei4_er3",
|
||||||
|
"呼和浩特": "nei4_meng2_gu3_hu1_he2_hao4_te4",
|
||||||
|
"和田地区": "xin1_jiang1_he2_tian2_di4_qu1",
|
||||||
|
"咸宁": "hu2_bei3_xian2_ning2",
|
||||||
|
"咸阳": "shan3_xi1_xian2_yang2",
|
||||||
|
"哈密": "xin1_jiang1_ha1_mi4",
|
||||||
|
"哈尔滨": "hei1_long2_jiang1_ha1_er3_bin1",
|
||||||
|
"唐山": "he2_bei3_tang2_shan1",
|
||||||
|
"商丘": "he2_nan2_shang1_qiu1",
|
||||||
|
"商洛": "shan3_xi1_shang1_luo4",
|
||||||
|
"喀什地区": "xin1_jiang1_ka1_shi2_di4_qu1",
|
||||||
|
"嘉兴": "zhe4_jiang1_jia1_xing1",
|
||||||
|
"嘉峪关": "gan1_su4_jia1_yu4_guan1",
|
||||||
|
"四平": "ji2_lin2_si4_ping2",
|
||||||
|
"固原": "ning2_xia4_gu4_yuan2",
|
||||||
|
"图木舒克": "xin1_jiang1_tu2_mu4_shu1_ke4",
|
||||||
|
"塔城地区": "xin1_jiang1_ta3_cheng2_di4_qu1",
|
||||||
|
"大兴安岭地区": "hei1_long2_jiang1_da4_xing1_an1_ling2_di4_qu1",
|
||||||
|
"大同": "shan1_xi1_da4_tong2",
|
||||||
|
"大庆": "hei1_long2_jiang1_da4_qing4",
|
||||||
|
"大理白族自治州": "yun2_nan2_da4_li3_bai2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"大连": "liao2_ning2_da4_lian2",
|
||||||
|
"天水": "gan1_su4_tian1_shui3",
|
||||||
|
"天津": "tianjin",
|
||||||
|
"天门": "hu2_bei3_tian1_men2",
|
||||||
|
"太原": "shan1_xi1_tai4_yuan2",
|
||||||
|
"威海": "shan1_dong1_wei1_hai3",
|
||||||
|
"娄底": "hu2_nan2_lou2_di3",
|
||||||
|
"孝感": "hu2_bei3_xiao4_gan3",
|
||||||
|
"宁德": "fu2_jian4_ning2_de2",
|
||||||
|
"宁波": "zhe4_jiang1_ning2_bo1",
|
||||||
|
"安庆": "an1_hui1_an1_qing4",
|
||||||
|
"安康": "shan3_xi1_an1_kang1",
|
||||||
|
"安阳": "he2_nan2_an1_yang2",
|
||||||
|
"安顺": "gui4_zhou1_an1_shun4",
|
||||||
|
"定安县": "hai3_nan2_ding4_an1_xian4",
|
||||||
|
"定西": "gan1_su4_ding4_xi1",
|
||||||
|
"宜宾": "si4_chuan1_yi2_bin1",
|
||||||
|
"宜昌": "hu2_bei3_yi2_chang1",
|
||||||
|
"宜春": "jiang1_xi1_yi2_chun1",
|
||||||
|
"宝鸡": "shan3_xi1_bao3_ji1",
|
||||||
|
"宣城": "an1_hui1_xuan1_cheng2",
|
||||||
|
"宿州": "an1_hui1_su4_zhou1",
|
||||||
|
"宿迁": "jiang1_su1_su4_qian1",
|
||||||
|
"屯昌县": "hai3_nan2_tun2_chang1_xian4",
|
||||||
|
"山南": "xi1_cang2_shan1_nan2",
|
||||||
|
"岳阳": "hu2_nan2_yue4_yang2",
|
||||||
|
"崇左": "guang3_xi1_chong2_zuo3",
|
||||||
|
"巴中": "si4_chuan1_ba1_zhong1",
|
||||||
|
"巴彦淖尔": "nei4_meng2_gu3_ba1_yan4_nao4_er3",
|
||||||
|
"巴音郭楞蒙古自治州": "xin1_jiang1_ba1_yin1_guo1_leng2_meng2_gu3_zi4_zhi4_zhou1",
|
||||||
|
"常州": "jiang1_su1_chang2_zhou1",
|
||||||
|
"常德": "hu2_nan2_chang2_de2",
|
||||||
|
"平凉": "gan1_su4_ping2_liang2",
|
||||||
|
"平顶山": "he2_nan2_ping2_ding3_shan1",
|
||||||
|
"广元": "si4_chuan1_guang3_yuan2",
|
||||||
|
"广安": "si4_chuan1_guang3_an1",
|
||||||
|
"广州": "guang3_dong1_guang3_zhou1",
|
||||||
|
"庆阳": "gan1_su4_qing4_yang2",
|
||||||
|
"廊坊": "he2_bei3_lang2_fang1",
|
||||||
|
"延安": "shan3_xi1_yan2_an1",
|
||||||
|
"延边朝鲜族自治州": "ji2_lin2_yan2_bian1_zhao1_xian1_zu2_zi4_zhi4_zhou1",
|
||||||
|
"开封": "he2_nan2_kai1_feng1",
|
||||||
|
"张家口": "he2_bei3_zhang1_jia1_kou3",
|
||||||
|
"张家界": "hu2_nan2_zhang1_jia1_jie4",
|
||||||
|
"张掖": "gan1_su4_zhang1_ye4",
|
||||||
|
"徐州": "jiang1_su1_xu2_zhou1",
|
||||||
|
"德宏傣族景颇族自治州": "yun2_nan2_de2_hong2_dai3_zu2_jing3_po3_zu2_zi4_zhi4_zhou1",
|
||||||
|
"德州": "shan1_dong1_de2_zhou1",
|
||||||
|
"德阳": "si4_chuan1_de2_yang2",
|
||||||
|
"忻州": "shan1_xi1_xin1_zhou1",
|
||||||
|
"怀化": "hu2_nan2_huai2_hua4",
|
||||||
|
"怒江傈僳族自治州": "yun2_nan2_nu4_jiang1_li4_su4_zu2_zi4_zhi4_zhou1",
|
||||||
|
"恩施土家族苗族自治州": "hu2_bei3_en1_shi1_tu3_jia1_zu2_miao2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"惠州": "guang3_dong1_hui4_zhou1",
|
||||||
|
"成都": "si4_chuan1_cheng2_du1",
|
||||||
|
"扬州": "jiang1_su1_yang2_zhou1",
|
||||||
|
"承德": "he2_bei3_cheng2_de2",
|
||||||
|
"抚州": "jiang1_xi1_fu3_zhou1",
|
||||||
|
"抚顺": "liao2_ning2_fu3_shun4",
|
||||||
|
"拉萨": "xi1_cang2_la1_sa4",
|
||||||
|
"揭阳": "guang3_dong1_jie1_yang2",
|
||||||
|
"攀枝花": "si4_chuan1_pan1_zhi1_hua1",
|
||||||
|
"文山壮族苗族自治州": "yun2_nan2_wen2_shan1_zhuang4_zu2_miao2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"文昌": "hai3_nan2_wen2_chang1",
|
||||||
|
"新乡": "he2_nan2_xin1_xiang1",
|
||||||
|
"新余": "jiang1_xi1_xin1_yu2",
|
||||||
|
"无锡": "jiang1_su1_wu2_xi2",
|
||||||
|
"日喀则": "xi1_cang2_ri4_ka1_ze2",
|
||||||
|
"日照": "shan1_dong1_ri4_zhao4",
|
||||||
|
"昆明": "yun2_nan2_kun1_ming2",
|
||||||
|
"昆玉": "xin1_jiang1_kun1_yu4",
|
||||||
|
"昌吉回族自治州": "xin1_jiang1_chang1_ji2_hui2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"昌江黎族自治县": "hai3_nan2_chang1_jiang1_li2_zu2_zi4_zhi4_xian4",
|
||||||
|
"昌都": "xi1_cang2_chang1_du1",
|
||||||
|
"昭通": "yun2_nan2_zhao1_tong1",
|
||||||
|
"晋中": "shan1_xi1_jin4_zhong1",
|
||||||
|
"晋城": "shan1_xi1_jin4_cheng2",
|
||||||
|
"普洱": "yun2_nan2_pu3_er3",
|
||||||
|
"景德镇": "jiang1_xi1_jing3_de2_zhen4",
|
||||||
|
"曲靖": "yun2_nan2_qu1_jing4",
|
||||||
|
"朔州": "shan1_xi1_shuo4_zhou1",
|
||||||
|
"朝阳": "liao2_ning2_zhao1_yang2",
|
||||||
|
"本溪": "liao2_ning2_ben3_xi1",
|
||||||
|
"来宾": "guang3_xi1_lai2_bin1",
|
||||||
|
"杭州": "zhe4_jiang1_hang2_zhou1",
|
||||||
|
"松原": "ji2_lin2_song1_yuan2",
|
||||||
|
"林芝": "xi1_cang2_lin2_zhi1",
|
||||||
|
"果洛藏族自治州": "qing1_hai3_guo3_luo4_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"枣庄": "shan1_dong1_zao3_zhuang1",
|
||||||
|
"柳州": "guang3_xi1_liu3_zhou1",
|
||||||
|
"株洲": "hu2_nan2_zhu1_zhou1",
|
||||||
|
"桂林": "guang3_xi1_gui4_lin2",
|
||||||
|
"梅州": "guang3_dong1_mei2_zhou1",
|
||||||
|
"梧州": "guang3_xi1_wu2_zhou1",
|
||||||
|
"楚雄彝族自治州": "yun2_nan2_chu3_xiong2_yi2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"榆林": "shan3_xi1_yu2_lin2",
|
||||||
|
"武威": "gan1_su4_wu3_wei1",
|
||||||
|
"武汉": "hu2_bei3_wu3_han4",
|
||||||
|
"毕节": "gui4_zhou1_bi4_jie2",
|
||||||
|
"永州": "hu2_nan2_yong3_zhou1",
|
||||||
|
"汉中": "shan3_xi1_han4_zhong1",
|
||||||
|
"汕头": "guang3_dong1_shan4_tou2",
|
||||||
|
"汕尾": "guang3_dong1_shan4_wei3",
|
||||||
|
"江门": "guang3_dong1_jiang1_men2",
|
||||||
|
"池州": "an1_hui1_chi2_zhou1",
|
||||||
|
"沈阳": "liao2_ning2_shen3_yang2",
|
||||||
|
"沧州": "he2_nan2_cang1_zhou1",
|
||||||
|
"河池": "guang3_xi1_he2_chi2",
|
||||||
|
"河源": "guang3_dong1_he2_yuan2",
|
||||||
|
"泉州": "fu2_jian4_quan2_zhou1",
|
||||||
|
"泰安": "shan1_dong1_tai4_an1",
|
||||||
|
"泰州": "jiang1_su1_tai4_zhou1",
|
||||||
|
"泸州": "si4_chuan1_lu2_zhou1",
|
||||||
|
"洛阳": "he2_nan2_luo4_yang2",
|
||||||
|
"济南": "shan1_dong1_ji4_nan2",
|
||||||
|
"济宁": "shan1_dong1_ji4_ning2",
|
||||||
|
"济源": "he2_nan2_ji4_yuan2",
|
||||||
|
"海东": "qing1_hai3_hai3_dong1",
|
||||||
|
"海北藏族自治州": "qing1_hai3_hai3_bei3_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"海南藏族自治州": "qing1_hai3_hai3_nan2_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"海口": "hai3_nan2_hai3_kou3",
|
||||||
|
"海西蒙古族藏族自治州": "qing1_hai3_hai3_xi1_meng2_gu3_zu2_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"淄博": "shan1_dong1_zi1_bo2",
|
||||||
|
"淮北": "an1_hui1_huai2_bei3",
|
||||||
|
"淮南": "an1_hui1_huai2_nan2",
|
||||||
|
"淮安": "jiang1_su1_huai2_an1",
|
||||||
|
"深圳": "guang3_dong1_shen1_zhen4",
|
||||||
|
"清远": "guang3_dong1_qing1_yuan3",
|
||||||
|
"温州": "zhe4_jiang1_wen1_zhou1",
|
||||||
|
"渭南": "shan3_xi1_wei4_nan2",
|
||||||
|
"湖州": "zhe4_jiang1_hu2_zhou1",
|
||||||
|
"湘潭": "hu2_nan2_xiang1_tan2",
|
||||||
|
"湘西土家族苗族自治州": "hu2_nan2_xiang1_xi1_tu3_jia1_zu2_miao2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"湛江": "guang3_dong1_zhan4_jiang1",
|
||||||
|
"滁州": "an1_hui1_chu2_zhou1",
|
||||||
|
"滨州": "shan1_dong1_bin1_zhou1",
|
||||||
|
"漯河": "he2_nan2_ta4_he2",
|
||||||
|
"漳州": "fu2_jian4_zhang1_zhou1",
|
||||||
|
"潍坊": "shan1_dong1_wei2_fang1",
|
||||||
|
"潜江": "hu2_bei3_qian2_jiang1",
|
||||||
|
"潮州": "guang3_dong1_chao2_zhou1",
|
||||||
|
"澄迈县": "hai3_nan2_cheng2_mai4_xian4",
|
||||||
|
"澳门": "aomen",
|
||||||
|
"濮阳": "he2_nan2_pu2_yang2",
|
||||||
|
"烟台": "shan1_dong1_yan1_tai2",
|
||||||
|
"焦作": "he2_nan2_jiao1_zuo4",
|
||||||
|
"牡丹江": "hei1_long2_jiang1_mu3_dan1_jiang1",
|
||||||
|
"玉林": "guang3_xi1_yu4_lin2",
|
||||||
|
"玉树藏族自治州": "qing1_hai3_yu4_shu4_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"玉溪": "yun2_nan2_yu4_xi1",
|
||||||
|
"珠海": "guang3_dong1_zhu1_hai3",
|
||||||
|
"琼中黎族苗族自治县": "hai3_nan2_qiong2_zhong1_li2_zu2_miao2_zu2_zi4_zhi4_xian4",
|
||||||
|
"琼海": "hai3_nan2_qiong2_hai3",
|
||||||
|
"甘南藏族自治州": "gan1_su4_gan1_nan2_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"甘孜藏族自治州": "si4_chuan1_gan1_zi1_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"白城": "ji2_lin2_bai2_cheng2",
|
||||||
|
"白山": "ji2_lin2_bai2_shan1",
|
||||||
|
"白沙黎族自治县": "hai3_nan2_bai2_sha1_li2_zu2_zi4_zhi4_xian4",
|
||||||
|
"白银": "gan1_su4_bai2_yin2",
|
||||||
|
"百色": "guang3_xi1_bai3_se4",
|
||||||
|
"益阳": "hu2_nan2_yi4_yang2",
|
||||||
|
"盐城": "jiang1_su1_yan2_cheng2",
|
||||||
|
"盘锦": "liao2_ning2_pan2_jin3",
|
||||||
|
"眉山": "si4_chuan1_mei2_shan1",
|
||||||
|
"石嘴山": "ning2_xia4_shi2_zui3_shan1",
|
||||||
|
"石家庄": "he2_bei3_shi2_jia1_zhuang1",
|
||||||
|
"石河子": "xin1_jiang1_shi2_he2_zi3",
|
||||||
|
"神农架林区": "hu2_bei3_shen2_nong2_jia4_lin2_qu1",
|
||||||
|
"福州": "fu2_jian4_fu2_zhou1",
|
||||||
|
"秦皇岛": "he2_bei3_qin2_huang2_dao3",
|
||||||
|
"红河哈尼族彝族自治州": "yun2_nan2_hong2_he2_ha1_ni2_zu2_yi2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"绍兴": "zhe4_jiang1_shao4_xing1",
|
||||||
|
"绥化": "hei1_long2_jiang1_sui1_hua4",
|
||||||
|
"绵阳": "si4_chuan1_mian2_yang2",
|
||||||
|
"聊城": "shan1_dong1_liao2_cheng2",
|
||||||
|
"肇庆": "guang3_dong1_zhao4_qing4",
|
||||||
|
"自贡": "si4_chuan1_zi4_gong4",
|
||||||
|
"舟山": "zhe4_jiang1_zhou1_shan1",
|
||||||
|
"芜湖": "an1_hui1_wu2_hu2",
|
||||||
|
"苏州": "jiang1_su1_su1_zhou1",
|
||||||
|
"茂名": "guang3_dong1_mao4_ming2",
|
||||||
|
"荆州": "hu2_bei3_jing1_zhou1",
|
||||||
|
"荆门": "hu2_bei3_jing1_men2",
|
||||||
|
"莆田": "fu2_jian4_fu3_tian2",
|
||||||
|
"莱芜": "shan1_dong1_lai2_wu2",
|
||||||
|
"菏泽": "shan1_dong1_he2_ze2",
|
||||||
|
"萍乡": "jiang1_xi1_ping2_xiang1",
|
||||||
|
"营口": "liao2_ning2_ying2_kou3",
|
||||||
|
"葫芦岛": "liao2_ning2_hu2_lu2_dao3",
|
||||||
|
"蚌埠": "an1_hui1_bang4_bu4",
|
||||||
|
"衡水": "he2_bei3_heng2_shui3",
|
||||||
|
"衡阳": "hu2_nan2_heng2_yang2",
|
||||||
|
"衢州": "zhe4_jiang1_qu2_zhou1",
|
||||||
|
"襄阳": "hu2_bei3_xiang1_yang2",
|
||||||
|
"西双版纳傣族自治州": "yun2_nan2_xi1_shuang1_ban3_na4_dai3_zu2_zi4_zhi4_zhou1",
|
||||||
|
"西宁": "qing1_hai3_xi1_ning2",
|
||||||
|
"西安": "shan3_xi1_xi1_an1",
|
||||||
|
"许昌": "he2_nan2_xu3_chang1",
|
||||||
|
"贵港": "guang3_xi1_gui4_gang3",
|
||||||
|
"贵阳": "gui4_zhou1_gui4_yang2",
|
||||||
|
"贺州": "guang3_xi1_he4_zhou1",
|
||||||
|
"资阳": "si4_chuan1_zi1_yang2",
|
||||||
|
"赣州": "jiang1_xi1_gan4_zhou1",
|
||||||
|
"赤峰": "nei4_meng2_gu3_chi4_feng1",
|
||||||
|
"辽源": "ji2_lin2_liao2_yuan2",
|
||||||
|
"辽阳": "liao2_ning2_liao2_yang2",
|
||||||
|
"达州": "si4_chuan1_da2_zhou1",
|
||||||
|
"运城": "shan1_xi1_yun4_cheng2",
|
||||||
|
"连云港": "jiang1_su1_lian2_yun2_gang3",
|
||||||
|
"迪庆藏族自治州": "yun2_nan2_di2_qing4_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"通化": "ji2_lin2_tong1_hua4",
|
||||||
|
"通辽": "nei4_meng2_gu3_tong1_liao2",
|
||||||
|
"遂宁": "si4_chuan1_sui4_ning2",
|
||||||
|
"遵义": "gui4_zhou1_zun1_yi4",
|
||||||
|
"邢台": "he2_bei3_xing2_tai2",
|
||||||
|
"那曲地区": "xi1_cang2_na4_qu1_di4_qu1",
|
||||||
|
"邯郸": "he2_bei3_han2_dan1",
|
||||||
|
"邵阳": "hu2_nan2_shao4_yang2",
|
||||||
|
"郑州": "he2_nan2_zheng4_zhou1",
|
||||||
|
"郴州": "hu2_nan2_chen1_zhou1",
|
||||||
|
"鄂尔多斯": "nei4_meng2_gu3_e4_er3_duo1_si1",
|
||||||
|
"鄂州": "hu2_bei3_e4_zhou1",
|
||||||
|
"酒泉": "gan1_su4_jiu3_quan2",
|
||||||
|
"重庆": "chongqing",
|
||||||
|
"金华": "zhe4_jiang1_jin1_hua2",
|
||||||
|
"金昌": "gan1_su4_jin1_chang1",
|
||||||
|
"钦州": "guang3_xi1_qin1_zhou1",
|
||||||
|
"铁岭": "liao2_ning2_tie3_ling2",
|
||||||
|
"铁门关": "xin1_jiang1_tie3_men2_guan1",
|
||||||
|
"铜仁": "gui4_zhou1_tong2_ren2",
|
||||||
|
"铜川": "shan3_xi1_tong2_chuan1",
|
||||||
|
"铜陵": "an1_hui1_tong2_ling2",
|
||||||
|
"银川": "ning2_xia4_yin2_chuan1",
|
||||||
|
"锡林郭勒盟": "nei4_meng2_gu3_xi2_lin2_guo1_le4_meng2",
|
||||||
|
"锦州": "liao2_ning2_jin3_zhou1",
|
||||||
|
"镇江": "jiang1_su1_zhen4_jiang1",
|
||||||
|
"长春": "ji2_lin2_chang2_chun1",
|
||||||
|
"长沙": "hu2_nan2_chang2_sha1",
|
||||||
|
"长治": "shan1_xi1_chang2_zhi4",
|
||||||
|
"阜新": "liao2_ning2_fu4_xin1",
|
||||||
|
"阜阳": "an1_hui1_fu4_yang2",
|
||||||
|
"防城港": "guang3_xi1_fang2_cheng2_gang3",
|
||||||
|
"阳江": "guang3_dong1_yang2_jiang1",
|
||||||
|
"阳泉": "shan1_xi1_yang2_quan2",
|
||||||
|
"阿克苏地区": "xin1_jiang1_a1_ke4_su1_di4_qu1",
|
||||||
|
"阿勒泰地区": "xin1_jiang1_a1_le4_tai4_di4_qu1",
|
||||||
|
"阿坝藏族羌族自治州": "si4_chuan1_a1_ba4_cang2_zu2_qiang1_zu2_zi4_zhi4_zhou1",
|
||||||
|
"阿拉善盟": "nei4_meng2_gu3_a1_la1_shan4_meng2",
|
||||||
|
"阿拉尔": "xin1_jiang1_a1_la1_er3",
|
||||||
|
"阿里地区": "xi1_cang2_a1_li3_di4_qu1",
|
||||||
|
"陇南": "gan1_su4_long3_nan2",
|
||||||
|
"陵水黎族自治县": "hai3_nan2_ling2_shui3_li2_zu2_zi4_zhi4_xian4",
|
||||||
|
"随州": "hu2_bei3_sui2_zhou1",
|
||||||
|
"雅安": "si4_chuan1_ya3_an1",
|
||||||
|
"青岛": "shan1_dong1_qing1_dao3",
|
||||||
|
"鞍山": "liao2_ning2_an1_shan1",
|
||||||
|
"韶关": "guang3_dong1_shao2_guan1",
|
||||||
|
"香港": "xianggang",
|
||||||
|
"马鞍山": "an1_hui1_ma3_an1_shan1",
|
||||||
|
"驻马店": "he2_nan2_zhu4_ma3_dian4",
|
||||||
|
"鸡西": "hei1_long2_jiang1_ji1_xi1",
|
||||||
|
"鹤壁": "he2_nan2_he4_bi4",
|
||||||
|
"鹤岗": "hei1_long2_jiang1_he4_gang3",
|
||||||
|
"鹰潭": "jiang1_xi1_ying1_tan2",
|
||||||
|
"黄冈": "hu2_bei3_huang2_gang1",
|
||||||
|
"黄南藏族自治州": "qing1_hai3_huang2_nan2_cang2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"黄山": "an1_hui1_huang2_shan1",
|
||||||
|
"黄石": "hu2_bei3_huang2_shi2",
|
||||||
|
"黑河": "hei1_long2_jiang1_hei1_he2",
|
||||||
|
"黔东南苗族侗族自治州": "gui4_zhou1_qian2_dong1_nan2_miao2_zu2_tong1_zu2_zi4_zhi4_zhou1",
|
||||||
|
"黔南布依族苗族自治州": "gui4_zhou1_qian2_nan2_bu4_yi1_zu2_miao2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"黔西南布依族苗族自治州": "gui4_zhou1_qian2_xi1_nan2_bu4_yi1_zu2_miao2_zu2_zi4_zhi4_zhou1",
|
||||||
|
"齐齐哈尔": "hei1_long2_jiang1_qi2_qi2_ha1_er3",
|
||||||
|
"龙岩": "fu2_jian4_long2_yan2",
|
||||||
|
}
|
||||||
738
vendor/github.com/go-echarts/go-echarts/v2/opts/charts.go
generated
vendored
Normal file
738
vendor/github.com/go-echarts/go-echarts/v2/opts/charts.go
generated
vendored
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
package opts
|
||||||
|
|
||||||
|
// BarChart
|
||||||
|
// https://echarts.apache.org/en/option.html#series-bar
|
||||||
|
type BarChart struct {
|
||||||
|
Type string
|
||||||
|
// Name of stack. On the same category axis, the series with the
|
||||||
|
// same stack name would be put on top of each other.
|
||||||
|
Stack string
|
||||||
|
|
||||||
|
// The gap between bars between different series, is a percent value like '30%',
|
||||||
|
// which means 30% of the bar width.
|
||||||
|
// Set barGap as '-100%' can overlap bars that belong to different series,
|
||||||
|
// which is useful when putting a series of bar as background.
|
||||||
|
// In a single coordinate system, this attribute is shared by multiple 'bar' series.
|
||||||
|
// This attribute should be set on the last 'bar' series in the coordinate system,
|
||||||
|
// then it will be adopted by all 'bar' series in the coordinate system.
|
||||||
|
BarGap string
|
||||||
|
|
||||||
|
// The bar gap of a single series, defaults to be 20% of the category gap,
|
||||||
|
// can be set as a fixed value.
|
||||||
|
// In a single coordinate system, this attribute is shared by multiple 'bar' series.
|
||||||
|
// This attribute should be set on the last 'bar' series in the coordinate system,
|
||||||
|
// then it will be adopted by all 'bar' series in the coordinate system.
|
||||||
|
BarCategoryGap string
|
||||||
|
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int
|
||||||
|
|
||||||
|
// Index of y axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int
|
||||||
|
|
||||||
|
ShowBackground bool
|
||||||
|
RoundCap bool
|
||||||
|
CoordSystem string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SunburstChart
|
||||||
|
// https://echarts.apache.org/en/option.html#series-sunburst
|
||||||
|
type SunburstChart struct {
|
||||||
|
// The action of clicking a sector
|
||||||
|
NodeClick string `json:"nodeClick,omitempty"`
|
||||||
|
// Sorting method that sectors use based on value
|
||||||
|
Sort string `json:"sort,omitempty"`
|
||||||
|
// If there is no name, whether need to render it.
|
||||||
|
RenderLabelForZeroData bool `json:"renderLabelForZeroData"`
|
||||||
|
// Selected mode
|
||||||
|
SelectedMode bool `json:"selectedMode"`
|
||||||
|
// Whether to enable animation.
|
||||||
|
Animation bool `json:"animation"`
|
||||||
|
// Whether to set graphic number threshold to animation
|
||||||
|
AnimationThreshold int `json:"animationThreshold,omitempty"`
|
||||||
|
// Duration of the first animation
|
||||||
|
AnimationDuration int `json:"animationDuration,omitempty"`
|
||||||
|
// Easing method used for the first animation
|
||||||
|
AnimationEasing string `json:"animationEasing,omitempty"`
|
||||||
|
// Delay before updating the first animation
|
||||||
|
AnimationDelay int `json:"animationDelay,omitempty"`
|
||||||
|
// Time for animation to complete
|
||||||
|
AnimationDurationUpdate int `json:"animationDurationUpdate,omitempty"`
|
||||||
|
// Easing method used for animation.
|
||||||
|
AnimationEasingUpdate string `json:"animationEasingUpdate,omitempty"`
|
||||||
|
// Delay before updating animation
|
||||||
|
AnimationDelayUpdate int `json:"animationDelayUpdate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-bar.data
|
||||||
|
type BarData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// The style setting of the text label in a single bar.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// Tooltip settings in this series data.
|
||||||
|
Tooltip *Tooltip `json:"tooltip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar3DChart is the option set for a 3D bar chart.
|
||||||
|
type Bar3DChart struct {
|
||||||
|
// Shading is the coloring effect of 3D graphics in 3D Bar.
|
||||||
|
// The following three coloring methods are supported in echarts-gl:
|
||||||
|
// Options:
|
||||||
|
//
|
||||||
|
// * "color": Only display colors, not affected by other factors such as lighting.
|
||||||
|
// * "lambert": Through the classic [lambert] coloring, can express the light and dark that the light shows.
|
||||||
|
// * "realistic": Realistic rendering, combined with light.ambientCubemap and postEffect,
|
||||||
|
// can improve the quality and texture of the display.
|
||||||
|
// [Physical Based Rendering (PBR)] (https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/)
|
||||||
|
// is used in ECharts GL to represent realistic materials.
|
||||||
|
Shading string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoxPlotData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-boxplot.data
|
||||||
|
type BoxPlotData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// The style setting of the text label in a single bar.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// Emphasis settings in this series data.
|
||||||
|
Emphasis *Emphasis `json:"emphasis,omitempty"`
|
||||||
|
|
||||||
|
// Tooltip settings in this series data.
|
||||||
|
Tooltip *Tooltip `json:"tooltip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectScatterData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-effectScatter.data
|
||||||
|
type EffectScatterData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FunnelData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-funnel.data
|
||||||
|
type FunnelData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoData
|
||||||
|
type GeoData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GaugeData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-gauge.data
|
||||||
|
type GaugeData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphChart is the option set for graph chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-graph
|
||||||
|
type GraphChart struct {
|
||||||
|
// Graph layout.
|
||||||
|
// * 'none' No layout, use x, y provided in node as the position of node.
|
||||||
|
// * 'circular' Adopt circular layout, see the example Les Miserables.
|
||||||
|
// * 'force' Adopt force-directed layout, see the example Force, the
|
||||||
|
// detail about layout configurations are in graph.force
|
||||||
|
Layout string
|
||||||
|
|
||||||
|
// Force is the option set for graph force layout.
|
||||||
|
Force *GraphForce
|
||||||
|
|
||||||
|
// Whether to enable mouse zooming and translating. false by default.
|
||||||
|
// If either zooming or translating is wanted, it can be set to 'scale' or 'move'.
|
||||||
|
// Otherwise, set it to be true to enable both.
|
||||||
|
Roam bool
|
||||||
|
|
||||||
|
// EdgeSymbol is the symbols of two ends of edge line.
|
||||||
|
// * 'circle'
|
||||||
|
// * 'arrow'
|
||||||
|
// * 'none'
|
||||||
|
// example: ["circle", "arrow"] or "circle"
|
||||||
|
EdgeSymbol interface{}
|
||||||
|
|
||||||
|
// EdgeSymbolSize is size of symbol of two ends of edge line. Can be an array or a single number
|
||||||
|
// example: [5,10] or 5
|
||||||
|
EdgeSymbolSize interface{}
|
||||||
|
|
||||||
|
// Draggable allows you to move the nodes with the mouse if they are not fixed.
|
||||||
|
Draggable bool
|
||||||
|
|
||||||
|
// Whether to focus/highlight the hover node and it's adjacencies.
|
||||||
|
FocusNodeAdjacency bool
|
||||||
|
|
||||||
|
// The categories of node, which is optional. If there is a classification of nodes,
|
||||||
|
// the category of each node can be assigned through data[i].category.
|
||||||
|
// And the style of category will also be applied to the style of nodes. categories can also be used in legend.
|
||||||
|
Categories []*GraphCategory
|
||||||
|
|
||||||
|
// EdgeLabel is the properties of an label of edge.
|
||||||
|
EdgeLabel *EdgeLabel `json:"edgeLabel"`
|
||||||
|
|
||||||
|
// SymbolKeepAspect is whether to keep aspect for symbols in the form of path://.
|
||||||
|
SymbolKeepAspect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNode represents a data node in graph chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-graph.data
|
||||||
|
type GraphNode struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// x value of node position.
|
||||||
|
X float32 `json:"x,omitempty"`
|
||||||
|
|
||||||
|
// y value of node position.
|
||||||
|
Y float32 `json:"y,omitempty"`
|
||||||
|
|
||||||
|
// Value of data item.
|
||||||
|
Value float32 `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// If node are fixed when doing force directed layout.
|
||||||
|
Fixed bool `json:"fixed,omitempty"`
|
||||||
|
|
||||||
|
// Index of category which the data item belongs to.
|
||||||
|
Category interface{} `json:"category,omitempty"`
|
||||||
|
|
||||||
|
// Symbol of node of this category.
|
||||||
|
// Icon types provided by ECharts includes
|
||||||
|
// 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
// It can be set to an image with 'image://url' , in which URL is the link to an image, or dataURI of an image.
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// node of this category symbol size. It can be set to single numbers like 10,
|
||||||
|
// or use an array to represent width and height. For example, [20, 10] means symbol width is 20, and height is10.
|
||||||
|
SymbolSize interface{} `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// The style of this node.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// The tooltip of this node.
|
||||||
|
Tooltip *Tooltip `json:"tooltip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphLink represents relationship between two data nodes.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-graph.links
|
||||||
|
type GraphLink struct {
|
||||||
|
// A string representing the name of source node on edge. Can also be a number representing the node index.
|
||||||
|
Source interface{} `json:"source,omitempty"`
|
||||||
|
|
||||||
|
// A string representing the name of target node on edge. Can also be a number representing node index.
|
||||||
|
Target interface{} `json:"target,omitempty"`
|
||||||
|
|
||||||
|
// value of edge, can be mapped to edge length in force graph.
|
||||||
|
Value float32 `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Label for this link.
|
||||||
|
Label *EdgeLabel `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphCategory represents a category for data nodes.
|
||||||
|
// The categories of node, which is optional. If there is a classification of nodes,
|
||||||
|
// the category of each node can be assigned through data[i].category.
|
||||||
|
// And the style of category will also be applied to the style of nodes. categories can also be used in legend.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-graph.categories
|
||||||
|
type GraphCategory struct {
|
||||||
|
// Name of category, which is used to correspond with legend and the content of tooltip.
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// The label style of node in this category.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeatMapChart is the option set for a heatmap chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-heatmap
|
||||||
|
type HeatMapChart struct {
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int
|
||||||
|
|
||||||
|
// Index of y axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeatMapData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-heatmap.data
|
||||||
|
type HeatMapData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KlineData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-candlestick.data
|
||||||
|
type KlineData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineChart is the options set for a line chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-line
|
||||||
|
type LineChart struct {
|
||||||
|
// If stack the value. On the same category axis, the series with the same stack name would be put on top of each other.
|
||||||
|
// The effect of the below example could be seen through stack switching of toolbox on the top right corner:
|
||||||
|
Stack string
|
||||||
|
|
||||||
|
// Whether to show as smooth curve.
|
||||||
|
// If is typed in boolean, then it means whether to enable smoothing. If is
|
||||||
|
// typed in number, valued from 0 to 1, then it means smoothness. A smaller value makes it less smooth.
|
||||||
|
Smooth bool
|
||||||
|
|
||||||
|
// Whether to show as a step line. It can be true, false. Or 'start', 'middle', 'end'.
|
||||||
|
// Which will configure the turn point of step line.
|
||||||
|
Step interface{}
|
||||||
|
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int
|
||||||
|
|
||||||
|
// Index of y axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int
|
||||||
|
|
||||||
|
// Whether to connect the line across null points.
|
||||||
|
ConnectNulls bool
|
||||||
|
|
||||||
|
// Whether to show symbol. It would be shown during tooltip hover.
|
||||||
|
ShowSymbol bool
|
||||||
|
|
||||||
|
// Icon types provided by ECharts includes
|
||||||
|
// 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
// Full documentation: https://echarts.apache.org/en/option.html#series-line.symbol
|
||||||
|
Symbol string
|
||||||
|
|
||||||
|
// symbol size. It can be set to single numbers like 10, or use an array to represent width and height. For example, [20, 10] means symbol width is 20, and height is10.
|
||||||
|
// Full documentation: https://echarts.apache.org/en/option.html#series-line.symbolSize
|
||||||
|
SymbolSize interface{}
|
||||||
|
|
||||||
|
// color for Line series. it affects Line series including symbols, unlike LineStyle.Color
|
||||||
|
Color string
|
||||||
|
|
||||||
|
// SymbolKeepAspect is whether to keep aspect for symbols in the form of path://.
|
||||||
|
SymbolKeepAspect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineChart is the options set for a chandlestick chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-candlestick
|
||||||
|
type KlineChart struct {
|
||||||
|
// Specify bar width. Absolute value (like 10) or percentage (like '20%', according to band width) can be used. Auto adapt by default.
|
||||||
|
BarWidth string
|
||||||
|
|
||||||
|
// Specify bar min width. Absolute value (like 10) or percentage (like '20%', according to band width) can be used. Auto adapt by default.
|
||||||
|
BarMinWidth string
|
||||||
|
|
||||||
|
// Specify bar max width. Absolute value (like 10) or percentage (like '20%', according to band width) can be used. Auto adapt by default.
|
||||||
|
BarMaxWidth string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-line.data
|
||||||
|
type LineData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Symbol of single data.
|
||||||
|
// Icon types provided by ECharts includes 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
// It can be set to an image with 'image://url' , in which URL is the link to an image, or dataURI of an image.
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// single data symbol size. It can be set to single numbers like 10, or
|
||||||
|
// use an array to represent width and height. For example, [20, 10] means symbol width is 20, and height is10
|
||||||
|
SymbolSize int `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int
|
||||||
|
|
||||||
|
// Index of y axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// LiquidChart
|
||||||
|
// reference https://github.com/ecomfe/echarts-liquidfill
|
||||||
|
type LiquidChart struct {
|
||||||
|
// Shape of single data.
|
||||||
|
// Icon types provided by ECharts includes 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
// It can be set to an image with 'image://url' , in which URL is the link to an image, or dataURI of an image.
|
||||||
|
Shape string
|
||||||
|
|
||||||
|
// Whether to show outline
|
||||||
|
IsShowOutline bool
|
||||||
|
|
||||||
|
// Whether to stop animation
|
||||||
|
IsWaveAnimation bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LiquidData
|
||||||
|
// reference https://github.com/ecomfe/echarts-liquidfill
|
||||||
|
type LiquidData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-map.data
|
||||||
|
type MapData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParallelData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-parallel.data
|
||||||
|
type ParallelData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PieChart is the option set for a pie chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-pie
|
||||||
|
type PieChart struct {
|
||||||
|
// Whether to show as Nightingale chart, which distinguishes data through radius. There are 2 optional modes:
|
||||||
|
// * 'radius' Use central angle to show the percentage of data, radius to show data size.
|
||||||
|
// * 'area' All the sectors will share the same central angle, the data size is shown only through radiuses.
|
||||||
|
RoseType string
|
||||||
|
|
||||||
|
// Center position of Pie chart, the first of which is the horizontal position, and the second is the vertical position.
|
||||||
|
// Percentage is supported. When set in percentage, the item is relative to the container width,
|
||||||
|
// and the second item to the height.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// Set to absolute pixel values ->> center: [400, 300]
|
||||||
|
// Set to relative percent ->> center: ['50%', '50%']
|
||||||
|
Center interface{}
|
||||||
|
|
||||||
|
// Radius of Pie chart. Value can be:
|
||||||
|
// * number: Specify outside radius directly.
|
||||||
|
// * string: For example, '20%', means that the outside radius is 20% of the viewport
|
||||||
|
// size (the little one between width and height of the chart container).
|
||||||
|
//
|
||||||
|
// Array.<number|string>: The first item specifies the inside radius, and the
|
||||||
|
// second item specifies the outside radius. Each item follows the definitions above.
|
||||||
|
Radius interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PieData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-pie.data
|
||||||
|
type PieData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Whether the data item is selected.
|
||||||
|
Selected bool `json:"selected,omitempty"`
|
||||||
|
|
||||||
|
// The label configuration of a single sector.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// Graphic style of , emphasis is the style when it is highlighted, like being hovered by mouse, or highlighted via legend connect.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// tooltip settings in this series data.
|
||||||
|
Tooltip *Tooltip `json:"tooltip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadarData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-radar
|
||||||
|
type RadarData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SankeyLink represents relationship between two data nodes.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-sankey.links
|
||||||
|
type SankeyLink struct {
|
||||||
|
// The name of source node of edge
|
||||||
|
Source interface{} `json:"source,omitempty"`
|
||||||
|
|
||||||
|
// The name of target node of edge
|
||||||
|
Target interface{} `json:"target,omitempty"`
|
||||||
|
|
||||||
|
// The value of edge, which decides the width of edge.
|
||||||
|
Value float32 `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SankeyNode represents a data node.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-sankey.nodes
|
||||||
|
type SankeyNode struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Depth of the node within the chart
|
||||||
|
Depth *int `json:"depth,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScatterChart is the option set for a scatter chart.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-scatter
|
||||||
|
type ScatterChart struct {
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int
|
||||||
|
|
||||||
|
// Index of x axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int
|
||||||
|
|
||||||
|
// SymbolKeepAspect is whether to keep aspect for symbols in the form of path://.
|
||||||
|
SymbolKeepAspect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScatterData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-scatter.data
|
||||||
|
type ScatterData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Symbol
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// SymbolSize
|
||||||
|
SymbolSize int `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// SymbolRotate
|
||||||
|
SymbolRotate int `json:"symbolRotate,omitempty"`
|
||||||
|
|
||||||
|
// Index of x axis to combine with, which is useful for multiple x axes in one chart.
|
||||||
|
XAxisIndex int `json:"xAxisIndex,omitempty"`
|
||||||
|
|
||||||
|
// Index of y axis to combine with, which is useful for multiple y axes in one chart.
|
||||||
|
YAxisIndex int `json:"yAxisIndex,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThemeRiverData
|
||||||
|
// https://echarts.apache.org/en/option.html#series-themeRiver
|
||||||
|
type ThemeRiverData struct {
|
||||||
|
// the time attribute of time and theme.
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
|
|
||||||
|
// the value of an event or theme at a time point.
|
||||||
|
Value float64 `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// the name of an event or theme.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToList converts the themeriver data to a list
|
||||||
|
func (trd ThemeRiverData) ToList() [3]interface{} {
|
||||||
|
return [3]interface{}{trd.Date, trd.Value, trd.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordCloudChart is the option set for a word cloud chart.
|
||||||
|
type WordCloudChart struct {
|
||||||
|
// Shape of WordCloud
|
||||||
|
// Optional: "circle", "rect", "roundRect", "triangle", "diamond", "pin", "arrow"
|
||||||
|
Shape string
|
||||||
|
|
||||||
|
// range of font size
|
||||||
|
SizeRange []float32
|
||||||
|
|
||||||
|
// range of font rotation angle
|
||||||
|
RotationRange []float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordCloudData
|
||||||
|
type WordCloudData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of a single data item.
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chart3DData struct {
|
||||||
|
// Name of the data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of the data item.
|
||||||
|
// []interface{}{1, 2, 3}
|
||||||
|
Value []interface{} `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// The style setting of the text label in a single bar.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeChart struct {
|
||||||
|
// The layout of the tree, which can be orthogonal and radial.
|
||||||
|
// * 'orthogonal' refer to the horizontal and vertical direction.
|
||||||
|
// * 'radial' refers to the view that the root node as the center and each layer of nodes as the ring.
|
||||||
|
Layout string
|
||||||
|
|
||||||
|
// The direction of the orthogonal layout in the tree diagram.
|
||||||
|
// * 'from left to right' or 'LR'
|
||||||
|
// * 'from right to left' or 'RL'
|
||||||
|
// * 'from top to bottom' or 'TB'
|
||||||
|
// * 'from bottom to top' or 'BT'
|
||||||
|
Orient string `json:"orient,omitempty"`
|
||||||
|
|
||||||
|
// Whether to enable mouse zooming and translating. false by default.
|
||||||
|
// If either zooming or translating is wanted, it can be set to 'scale' or 'move'.
|
||||||
|
// Otherwise, set it to be true to enable both.
|
||||||
|
Roam bool `json:"roam"`
|
||||||
|
|
||||||
|
// Subtree collapses and expands interaction, default true.
|
||||||
|
ExpandAndCollapse bool `json:"expandAndCollapse,omitempty"`
|
||||||
|
|
||||||
|
// The initial level (depth) of the tree. The root node is the 0th layer, then the first layer, the second layer, ... , until the leaf node.
|
||||||
|
// This configuration item is primarily used in conjunction with collapsing and expansion interactions.
|
||||||
|
// The purpose is to prevent the nodes from obscuring each other. If set as -1 or null or undefined, all nodes are expanded.
|
||||||
|
InitialTreeDepth int `json:"initialTreeDepth,omitempty"`
|
||||||
|
|
||||||
|
// The style setting of the text label in a single bar.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// Leaf node special configuration, the leaf node and non-leaf node label location is different.
|
||||||
|
Leaves *TreeLeaves `json:"leaves,omitempty"`
|
||||||
|
|
||||||
|
// Distance between tree component and the sides of the container.
|
||||||
|
// value can be instant pixel value like 20;
|
||||||
|
// It can also be a percentage value relative to container width like '20%';
|
||||||
|
Left string `json:"left,omitempty"`
|
||||||
|
Right string `json:"right,omitempty"`
|
||||||
|
Top string `json:"top,omitempty"`
|
||||||
|
Bottom string `json:"bottom,omitempty"`
|
||||||
|
|
||||||
|
// SymbolKeepAspect is whether to keep aspect for symbols in the form of path://.
|
||||||
|
SymbolKeepAspect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeData struct {
|
||||||
|
// Name of the data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Value of the data item.
|
||||||
|
Value int `json:"value,omitempty"`
|
||||||
|
|
||||||
|
Children []*TreeData `json:"children,omitempty"`
|
||||||
|
|
||||||
|
// Symbol of node of this category.
|
||||||
|
// Icon types provided by ECharts includes
|
||||||
|
// 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
// It can be set to an image with 'image://url' , in which URL is the link to an image, or dataURI of an image.
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// node of this category symbol size. It can be set to single numbers like 10,
|
||||||
|
// or use an array to represent width and height. For example, [20, 10] means symbol width is 20, and height is10.
|
||||||
|
SymbolSize interface{} `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// If set as `true`, the node is collapsed in the initialization.
|
||||||
|
Collapsed bool `json:"collapsed,omitempty"`
|
||||||
|
|
||||||
|
// LineStyle settings in this series data.
|
||||||
|
LineStyle *LineStyle `json:"lineStyle,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeMapChart struct {
|
||||||
|
// Whether to enable animation.
|
||||||
|
Animation bool `json:"animation"`
|
||||||
|
|
||||||
|
// leafDepth represents how many levels are shown at most. For example, when leafDepth is set to 1, only one level will be shown.
|
||||||
|
// leafDepth is null/undefined by default, which means that "drill down" is disabled.
|
||||||
|
LeafDepth int `json:"leafDeapth,omitempty"`
|
||||||
|
|
||||||
|
// Roam describes whether to enable mouse zooming and translating. false by default.
|
||||||
|
Roam bool `json:"roam"`
|
||||||
|
|
||||||
|
// Label decribes the style of the label in each node.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// UpperLabel is used to specify whether show label when the treemap node has children.
|
||||||
|
UpperLabel *UpperLabel `json:"upperLabel,omitempty"`
|
||||||
|
|
||||||
|
// ColorMappingBy specifies the rule according to which each node obtain color from color list.
|
||||||
|
ColorMappingBy string `json:"colorMappingBy,omitempty"`
|
||||||
|
|
||||||
|
// Levels provide configration for each node level
|
||||||
|
Levels *[]TreeMapLevel `json:"levels,omitempty"`
|
||||||
|
|
||||||
|
// Distance between treemap component and the sides of the container.
|
||||||
|
// value can be instant pixel value like 20;
|
||||||
|
// It can also be a percentage value relative to container width like '20%';
|
||||||
|
Left string `json:"left,omitempty"`
|
||||||
|
Right string `json:"right,omitempty"`
|
||||||
|
Top string `json:"top,omitempty"`
|
||||||
|
Bottom string `json:"bottom,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeMapNode struct {
|
||||||
|
// Name of the tree node item.
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Value of the tree node item.
|
||||||
|
Value int `json:"value,omitempty"`
|
||||||
|
|
||||||
|
Children []TreeMapNode `json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SunBurstData data
|
||||||
|
type SunBurstData struct {
|
||||||
|
// Name of data item.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// Value of data item.
|
||||||
|
Value float64 `json:"value,omitempty"`
|
||||||
|
// sub item of data item
|
||||||
|
Children []*SunBurstData `json:"children,omitempty"`
|
||||||
|
}
|
||||||
1521
vendor/github.com/go-echarts/go-echarts/v2/opts/global.go
generated
vendored
Normal file
1521
vendor/github.com/go-echarts/go-echarts/v2/opts/global.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
699
vendor/github.com/go-echarts/go-echarts/v2/opts/series.go
generated
vendored
Normal file
699
vendor/github.com/go-echarts/go-echarts/v2/opts/series.go
generated
vendored
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
package opts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Label contains options for a label text.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-line.label
|
||||||
|
type Label struct {
|
||||||
|
// Whether to show label.
|
||||||
|
Show bool `json:"show"`
|
||||||
|
|
||||||
|
// Color is the text color.
|
||||||
|
// If set as "auto", the color will assigned as visual color, such as series color.
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// font style.
|
||||||
|
// Options are: 'normal', 'italic', 'oblique'
|
||||||
|
FontStyle string `json:"fontStyle,omitempty"`
|
||||||
|
|
||||||
|
// font thick weight.
|
||||||
|
// Options are: 'normal', 'bold', 'bolder', 'lighter', 100 | 200 | 300 | 400...
|
||||||
|
FontWeight string `json:"fontWeight,omitempty"`
|
||||||
|
|
||||||
|
// font family.
|
||||||
|
// Can also be 'serif' , 'monospace', ...
|
||||||
|
FontFamily string `json:"fontFamily,omitempty"`
|
||||||
|
|
||||||
|
// font size.
|
||||||
|
FontSize float32 `json:"fontSize,omitempty"`
|
||||||
|
|
||||||
|
// Horizontal alignment of text, automatic by default.
|
||||||
|
// Options are: 'left', 'center', 'right'
|
||||||
|
Align string `json:"align,omitempty"`
|
||||||
|
|
||||||
|
// Vertical alignment of text, automatic by default.
|
||||||
|
// Options are: 'top', 'middle', 'bottom'
|
||||||
|
VerticalAlign string `json:"verticalAlign,omitempty"`
|
||||||
|
|
||||||
|
// Line height of the text fragment.
|
||||||
|
LineHeight float32 `json:"lineHeight,omitempty"`
|
||||||
|
|
||||||
|
// Background color of the text fragment.
|
||||||
|
BackgroundColor string `json:"backgroundColor,omitempty"`
|
||||||
|
|
||||||
|
// Border color of the text fragment.
|
||||||
|
BorderColor string `json:"borderColor,omitempty"`
|
||||||
|
|
||||||
|
// Border width of the text fragment.
|
||||||
|
BorderWidth float32 `json:"borderWidth,omitempty"`
|
||||||
|
|
||||||
|
// the text fragment border type.
|
||||||
|
// Possible values are: 'solid', 'dashed', 'dotted'
|
||||||
|
BorderType string `json:"borderType,omitempty"`
|
||||||
|
|
||||||
|
// To set the line dash offset. With borderType , we can make the line style more flexible.
|
||||||
|
BorderDashOffset float32 `json:"borderDashOffset,omitempty"`
|
||||||
|
|
||||||
|
// Border radius of the text fragment.
|
||||||
|
BorderRadius float32 `json:"borderRadius,omitempty"`
|
||||||
|
|
||||||
|
// Padding of the text fragment, for example:
|
||||||
|
// padding: [3, 4, 5, 6]: represents padding of [top, right, bottom, left].
|
||||||
|
// padding: 4: represents padding: [4, 4, 4, 4].
|
||||||
|
// padding: [3, 4]: represents padding: [3, 4, 3, 4].
|
||||||
|
// Notice, width and height specifies the width and height of the content, without padding.
|
||||||
|
Padding string `json:"padding,omitempty"`
|
||||||
|
|
||||||
|
// Label position. Followings are the options:
|
||||||
|
//
|
||||||
|
// [x, y]
|
||||||
|
// Use relative percentage, or absolute pixel values to represent position of label
|
||||||
|
// relative to top-left corner of bounding box. For example:
|
||||||
|
//
|
||||||
|
// Absolute pixel values: position: [10, 10],
|
||||||
|
// Relative percentage: position: ["50%", "50%"]
|
||||||
|
//
|
||||||
|
// "top"
|
||||||
|
// "left"
|
||||||
|
// "right"
|
||||||
|
// "bottom"
|
||||||
|
// "inside"
|
||||||
|
// "insideLeft"
|
||||||
|
// "insideRight"
|
||||||
|
// "insideTop"
|
||||||
|
// "insideBottom"
|
||||||
|
// "insideTopLeft"
|
||||||
|
// "insideBottomLeft"
|
||||||
|
// "insideTopRight"
|
||||||
|
// "insideBottomRight"
|
||||||
|
Position string `json:"position,omitempty"`
|
||||||
|
|
||||||
|
// Data label formatter, which supports string template and callback function.
|
||||||
|
// In either form, \n is supported to represent a new line.
|
||||||
|
// String template, Model variation includes:
|
||||||
|
//
|
||||||
|
// {a}: series name.
|
||||||
|
// {b}: the name of a data item.
|
||||||
|
// {c}: the value of a data item.
|
||||||
|
// {@xxx}: the value of a dimension named"xxx", for example,{@product}refers the value of"product"` dimension.
|
||||||
|
// {@[n]}: the value of a dimension at the index ofn, for example,{@[3]}` refers the value at dimensions[3].
|
||||||
|
Formatter string `json:"formatter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelLine Configuration of label guide line.
|
||||||
|
type LabelLine struct {
|
||||||
|
// Whether to show the label guide line.
|
||||||
|
Show bool `json:"show"`
|
||||||
|
// Whether to show the label guide line above the corresponding element.
|
||||||
|
ShowAbove bool `json:"showAbove"`
|
||||||
|
// The length of the second segment of guide line.
|
||||||
|
Length2 float64 `json:"length2,omitempty"`
|
||||||
|
// smoothness of guide line.
|
||||||
|
Smooth bool `json:"smooth"`
|
||||||
|
// Minimum turn angle between two segments of guide line
|
||||||
|
MinTurnAngle float64 `json:"minTurnAngle,omitempty"`
|
||||||
|
// The style of label line
|
||||||
|
LineStyle *LineStyle `json:"lineStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emphasis is the style when it is highlighted, like being hovered by mouse, or highlighted via legend connect.
|
||||||
|
type Emphasis struct {
|
||||||
|
// the emphasis style of label
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// the emphasis style of item
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemStyle represents a style of an item.
|
||||||
|
type ItemStyle struct {
|
||||||
|
// Color of chart
|
||||||
|
// Kline Up candle color
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// Kline Down candle color
|
||||||
|
Color0 string `json:"color0,omitempty"`
|
||||||
|
|
||||||
|
// BorderColor is the hart border color
|
||||||
|
// Kline Up candle border color
|
||||||
|
BorderColor string `json:"borderColor,omitempty"`
|
||||||
|
|
||||||
|
// Kline Down candle border color
|
||||||
|
BorderColor0 string `json:"borderColor0,omitempty"`
|
||||||
|
|
||||||
|
// Color saturation of a border or gap.
|
||||||
|
BorderColorSaturation float32 `json:"borderColorSaturation,omitempty"`
|
||||||
|
|
||||||
|
// Border width of a node
|
||||||
|
BorderWidth float32 `json:"borderWidth,omitempty"`
|
||||||
|
|
||||||
|
// Gaps between child nodes.
|
||||||
|
GapWidth float32 `json:"gapWidth,omitempty"`
|
||||||
|
|
||||||
|
// Opacity of the component. Supports value from 0 to 1, and the component will not be drawn when set to 0.
|
||||||
|
Opacity float32 `json:"opacity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLines represents a series of marklines.
|
||||||
|
type MarkLines struct {
|
||||||
|
Data []interface{} `json:"data,omitempty"`
|
||||||
|
MarkLineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLineStyle contains styling options for a MarkLine.
|
||||||
|
type MarkLineStyle struct {
|
||||||
|
// Symbol type at the two ends of the mark line. It can be an array for two ends, or assigned separately.
|
||||||
|
// Options: "circle", "rect", "roundRect", "triangle", "diamond", "pin", "arrow", "none"
|
||||||
|
Symbol []string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// Symbol size.
|
||||||
|
SymbolSize float32 `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// Mark line text options.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircularStyle contains styling options for circular layout.
|
||||||
|
type CircularStyle struct {
|
||||||
|
RotateLabel bool `json:"rotateLabel,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLineNameTypeItem represents type for a MarkLine.
|
||||||
|
type MarkLineNameTypeItem struct {
|
||||||
|
// Mark line name.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark line type, options: "average", "min", "max".
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLineNameYAxisItem defines a MarkLine on a Y axis.
|
||||||
|
type MarkLineNameYAxisItem struct {
|
||||||
|
// Mark line name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Y axis data
|
||||||
|
YAxis interface{} `json:"yAxis,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLineNameXAxisItem defines a MarkLine on a X axis.
|
||||||
|
type MarkLineNameXAxisItem struct {
|
||||||
|
// Mark line name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// X axis data
|
||||||
|
XAxis interface{} `json:"xAxis,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLineNameCoordItem represents coordinates for a MarkLine.
|
||||||
|
type MarkLineNameCoordItem struct {
|
||||||
|
// Mark line name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark line start coordinate
|
||||||
|
Coordinate0 []interface{}
|
||||||
|
|
||||||
|
// Mark line end coordinate
|
||||||
|
Coordinate1 []interface{}
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreas represents a series of markareas.
|
||||||
|
type MarkAreas struct {
|
||||||
|
Data []interface{} `json:"data,omitempty"`
|
||||||
|
MarkAreaStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreaStyle contains styling options for a MarkArea.
|
||||||
|
type MarkAreaStyle struct {
|
||||||
|
// Mark area text options.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreaNameTypeItem represents type for a MarkArea.
|
||||||
|
type MarkAreaNameTypeItem struct {
|
||||||
|
// Mark area name.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark area type, options: "average", "min", "max".
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreaNameYAxisItem defines a MarkArea on a Y axis.
|
||||||
|
type MarkAreaNameYAxisItem struct {
|
||||||
|
// Mark area name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Y axis data
|
||||||
|
YAxis interface{} `json:"yAxis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreaNameXAxisItem defines a MarkArea on a X axis.
|
||||||
|
type MarkAreaNameXAxisItem struct {
|
||||||
|
// Mark area name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// X axis data
|
||||||
|
XAxis interface{} `json:"xAxis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAreaNameCoordItem represents coordinates for a MarkArea.
|
||||||
|
type MarkAreaNameCoordItem struct {
|
||||||
|
// Mark area name
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark area start coordinate
|
||||||
|
Coordinate0 []interface{}
|
||||||
|
|
||||||
|
// Mark area end coordinate
|
||||||
|
Coordinate1 []interface{}
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
|
||||||
|
// Mark point text options.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkPoints represents a series of markpoints.
|
||||||
|
type MarkPoints struct {
|
||||||
|
Data []interface{} `json:"data,omitempty"`
|
||||||
|
MarkPointStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkPointStyle contains styling options for a MarkPoint.
|
||||||
|
type MarkPointStyle struct {
|
||||||
|
// Symbol type at the two ends of the mark line. It can be an array for two ends, or assigned separately.
|
||||||
|
// Options: "circle", "rect", "roundRect", "triangle", "diamond", "pin", "arrow", "none"
|
||||||
|
Symbol []string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// Symbol size.
|
||||||
|
SymbolSize float32 `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// Symbol rotate.
|
||||||
|
SymbolRotate float32 `json:"symbolRotate,omitempty"`
|
||||||
|
|
||||||
|
// Mark point text options.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkPointNameTypeItem represents type for a MarkPoint.
|
||||||
|
type MarkPointNameTypeItem struct {
|
||||||
|
// Name of markpoint
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark point type, options: "average", "min", "max".
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkPointNameCoordItem represents coordinates for a MarkPoint.
|
||||||
|
type MarkPointNameCoordItem struct {
|
||||||
|
// Name of markpoint
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Mark point coordinate
|
||||||
|
Coordinate []interface{} `json:"coord,omitempty"`
|
||||||
|
|
||||||
|
// Value in mark point
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Works only when type is assigned.
|
||||||
|
// It is used to state the dimension used to calculate maximum value or minimum value.
|
||||||
|
// It may be the direct name of a dimension, like x,
|
||||||
|
// or angle for line charts, or open, or close for candlestick charts.
|
||||||
|
ValueDim string `json:"valueDim,omitempty"`
|
||||||
|
|
||||||
|
// Mark point text options.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// Symbol type
|
||||||
|
// Options: "circle", "rect", "roundRect", "triangle", "diamond", "pin", "arrow", "none"
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
|
||||||
|
// Symbol size.
|
||||||
|
SymbolSize float32 `json:"symbolSize,omitempty"`
|
||||||
|
|
||||||
|
// Symbol rotate.
|
||||||
|
SymbolRotate float32 `json:"symbolRotate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RippleEffect is the option set for the ripple effect.
|
||||||
|
type RippleEffect struct {
|
||||||
|
// The period duration of animation, in seconds.
|
||||||
|
// default 4(s)
|
||||||
|
Period float32 `json:"period,omitempty"`
|
||||||
|
|
||||||
|
// The maximum zooming scale of ripples in animation.
|
||||||
|
// default 2.5
|
||||||
|
Scale float32 `json:"scale,omitempty"`
|
||||||
|
|
||||||
|
// The brush type for ripples. options: "stroke" and "fill".
|
||||||
|
// default "fill"
|
||||||
|
BrushType string `json:"brushType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineStyle is the option set for a link style component.
|
||||||
|
type LineStyle struct {
|
||||||
|
// Line color
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// Width of line. default 1
|
||||||
|
Width float32 `json:"width,omitempty"`
|
||||||
|
|
||||||
|
// Type of line,options: "solid", "dashed", "dotted". default "solid"
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Opacity of the component. Supports value from 0 to 1, and the component will not be drawn when set to 0.
|
||||||
|
Opacity float32 `json:"opacity,omitempty"`
|
||||||
|
|
||||||
|
// Curveness of edge. The values from 0 to 1 could be set.
|
||||||
|
// it would be larger as the the value becomes larger. default 0
|
||||||
|
Curveness float32 `json:"curveness,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreaStyle is the option set for an area style component.
|
||||||
|
type AreaStyle struct {
|
||||||
|
// Fill area color.
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// Opacity of the component. Supports value from 0 to 1, and the component will not be drawn when set to 0.
|
||||||
|
Opacity float32 `json:"opacity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration items about force-directed layout. Force-directed layout simulates
|
||||||
|
// spring/charge model, which will add a repulsion between 2 nodes and add a attraction
|
||||||
|
// between 2 nodes of each edge. In each iteration nodes will move under the effect
|
||||||
|
// of repulsion and attraction. After several iterations, the nodes will be static in a
|
||||||
|
// balanced position. As a result, the energy local minimum of this whole model will be realized.
|
||||||
|
// The result of force-directed layout has a good symmetries and clustering, which is also aesthetically pleasing.
|
||||||
|
type GraphForce struct {
|
||||||
|
// The initial layout before force-directed layout, which will influence on the result of force-directed layout.
|
||||||
|
// It defaults not to do any layout and use x, y provided in node as the position of node.
|
||||||
|
// If it doesn't exist, the position will be generated randomly.
|
||||||
|
// You can also use circular layout "circular".
|
||||||
|
InitLayout string `json:"initLayout,omitempty"`
|
||||||
|
|
||||||
|
// The repulsion factor between nodes. The repulsion will be stronger and the distance
|
||||||
|
// between 2 nodes becomes further as this value becomes larger.
|
||||||
|
// It can be an array to represent the range of repulsion. In this case larger value have larger
|
||||||
|
// repulsion and smaller value will have smaller repulsion.
|
||||||
|
Repulsion float32 `json:"repulsion,omitempty"`
|
||||||
|
|
||||||
|
// The gravity factor enforcing nodes approach to the center. The nodes will be
|
||||||
|
// closer to the center as the value becomes larger. default 0.1
|
||||||
|
Gravity float32 `json:"gravity,omitempty"`
|
||||||
|
|
||||||
|
// The distance between 2 nodes on edge. This distance is also affected by repulsion.
|
||||||
|
// It can be an array to represent the range of edge length. In this case edge with larger
|
||||||
|
// value will be shorter, which means two nodes are closer. And edge with smaller value will be longer.
|
||||||
|
// default 30
|
||||||
|
EdgeLength float32 `json:"edgeLength,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaf node special configuration, the leaf node and non-leaf node label location is different.
|
||||||
|
type TreeLeaves struct {
|
||||||
|
// The style setting of the text label in a single bar.
|
||||||
|
Label *Label `json:"label,omitempty"`
|
||||||
|
|
||||||
|
// LineStyle settings in this series data.
|
||||||
|
LineStyle *LineStyle `json:"lineStyle,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// Emphasis settings in this series data.
|
||||||
|
Emphasis *Emphasis `json:"emphasis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeMapLevel is level specific configuration.
|
||||||
|
type TreeMapLevel struct {
|
||||||
|
// Color defines a list for a node level, if empty, retreived from global color list.
|
||||||
|
Color []string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// ColorAlpha indicates the range of tranparent rate (color alpha) for nodes in a level.
|
||||||
|
ColorAlpha []float32 `json:"colorAlpha,omitempty"`
|
||||||
|
|
||||||
|
// ColorSaturation indicates the range of saturation (color alpha) for nodes in a level.
|
||||||
|
ColorSaturation []float32 `json:"colorSaturation,omitempty"`
|
||||||
|
|
||||||
|
// ColorMappingBy specifies the rule according to which each node obtain color from color list.
|
||||||
|
ColorMappingBy string `json:"colorMappingBy,omitempty"`
|
||||||
|
|
||||||
|
// UpperLabel is used to specify whether show label when the treemap node has children.
|
||||||
|
UpperLabel *UpperLabel `json:"upperLabel,omitempty"`
|
||||||
|
|
||||||
|
// ItemStyle settings in this series data.
|
||||||
|
ItemStyle *ItemStyle `json:"itemStyle,omitempty"`
|
||||||
|
|
||||||
|
// Emphasis settings in this series data.
|
||||||
|
Emphasis *Emphasis `json:"emphasis,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpperLabel is used to specify whether show label when the treemap node has children.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-treemap.upperLabel
|
||||||
|
type UpperLabel struct {
|
||||||
|
// Show is true to show upper label.
|
||||||
|
Show bool `json:"show,omitempty"`
|
||||||
|
|
||||||
|
// Position is the label's position.
|
||||||
|
// * top
|
||||||
|
// * left
|
||||||
|
// * right
|
||||||
|
// * bottom
|
||||||
|
// * inside
|
||||||
|
// * insideLeft
|
||||||
|
// * insideRight
|
||||||
|
// * insideTop
|
||||||
|
// * insideBottom
|
||||||
|
// * insideTopLeft
|
||||||
|
// * insideBottomLeft
|
||||||
|
// * insideTopRight
|
||||||
|
// * insideBottomRight
|
||||||
|
Position string `json:"position,omitempty"`
|
||||||
|
|
||||||
|
// Distance to the host graphic element.
|
||||||
|
// It is valid only when position is string value (like 'top', 'insideRight').
|
||||||
|
Distance float32 `json:"distance,omitempty"`
|
||||||
|
|
||||||
|
// Rotate label, from -90 degree to 90, positive value represents rotate anti-clockwise.
|
||||||
|
Rotate float32 `json:"rotate,omitempty"`
|
||||||
|
|
||||||
|
// Whether to move text slightly. For example: [30, 40] means move 30 horizontally and move 40 vertically.
|
||||||
|
Offset []float32 `json:"offset,omitempty"`
|
||||||
|
|
||||||
|
// Color is the text color
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// FontStyle
|
||||||
|
// * "normal"
|
||||||
|
// * "italic"
|
||||||
|
// * "oblique"
|
||||||
|
FontStyle string `json:"fontStyle,omitempty"`
|
||||||
|
|
||||||
|
// FontWeight can be the string or a number
|
||||||
|
// * "normal"
|
||||||
|
// * "bold"
|
||||||
|
// * "bolder"
|
||||||
|
// * "lighter"
|
||||||
|
// 100 | 200 | 300| 400 ...
|
||||||
|
FontWeight interface{} `json:"fontWeight,omitempty"`
|
||||||
|
|
||||||
|
// FontSize
|
||||||
|
FontSize float32 `json:"fontSize,omitempty"`
|
||||||
|
|
||||||
|
// Align is a horizontal alignment of text, automatic by default.
|
||||||
|
// * "left"
|
||||||
|
// * "center"
|
||||||
|
// * "right"
|
||||||
|
Align string `json:"align,omitempty"`
|
||||||
|
|
||||||
|
// Align is a horizontal alignment of text, automatic by default.
|
||||||
|
// * "top"
|
||||||
|
// * "middle"
|
||||||
|
// * "bottom"
|
||||||
|
VerticalAlign string `json:"verticalAlign,omitempty"`
|
||||||
|
|
||||||
|
// Padding of the text fragment, for example:
|
||||||
|
// Padding: [3, 4, 5, 6]: represents padding of [top, right, bottom, left].
|
||||||
|
// Padding: 4: represents padding: [4, 4, 4, 4].
|
||||||
|
// Padding: [3, 4]: represents padding: [3, 4, 3, 4].
|
||||||
|
Padding interface{} `json:"padding,omitempty"`
|
||||||
|
|
||||||
|
// Width of text block
|
||||||
|
Width float32 `json:"width,omitempty"`
|
||||||
|
|
||||||
|
// Height of text block
|
||||||
|
Height float32 `json:"height,omitempty"`
|
||||||
|
|
||||||
|
// Upper label formatter, which supports string template and callback function.
|
||||||
|
// In either form, \n is supported to represent a new line.
|
||||||
|
// String template, Model variation includes:
|
||||||
|
//
|
||||||
|
// {a}: series name.
|
||||||
|
// {b}: the name of a data item.
|
||||||
|
// {c}: the value of a data item.
|
||||||
|
// {@xxx}: the value of a dimension named"xxx", for example,{@product}refers the value of"product"` dimension.
|
||||||
|
// {@[n]}: the value of a dimension at the index ofn, for example,{@[3]}` refers the value at dimensions[3].
|
||||||
|
Formatter string `json:"formatter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBColor returns the color with RGB format
|
||||||
|
func RGBColor(r, g, b uint16) string {
|
||||||
|
return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBAColor returns the color with RGBA format
|
||||||
|
func RGBAColor(r, g, b uint16, a float32) string {
|
||||||
|
return fmt.Sprintf("rgba(%d,%d,%d,%f)", r, g, b, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSLColor returns the color with HSL format
|
||||||
|
func HSLColor(h, s, l float32) string {
|
||||||
|
return fmt.Sprintf("hsl(%f,%f%%,%f%%)", h, s, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSLAColor returns the color with HSLA format
|
||||||
|
func HSLAColor(h, s, l, a float32) string {
|
||||||
|
return fmt.Sprintf("hsla(%f,%f%%,%f%%,%f)", h, s, l, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeLabel is the properties of an label of edge.
|
||||||
|
// https://echarts.apache.org/en/option.html#series-graph.edgeLabel
|
||||||
|
type EdgeLabel struct {
|
||||||
|
// Show is true to show label on edge.
|
||||||
|
Show bool `json:"show,omitempty"`
|
||||||
|
|
||||||
|
// Position is the label's position in line of edge.
|
||||||
|
// * "start"
|
||||||
|
// * "middle"
|
||||||
|
// * "end"
|
||||||
|
Position string `json:"position,omitempty"`
|
||||||
|
|
||||||
|
// Color is the text color
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// FontStyle
|
||||||
|
// * "normal"
|
||||||
|
// * "italic"
|
||||||
|
// * "oblique"
|
||||||
|
FontStyle string `json:"fontStyle,omitempty"`
|
||||||
|
|
||||||
|
// FontWeight can be the string or a number
|
||||||
|
// * "normal"
|
||||||
|
// * "bold"
|
||||||
|
// * "bolder"
|
||||||
|
// * "lighter"
|
||||||
|
// 100 | 200 | 300| 400 ...
|
||||||
|
FontWeight interface{} `json:"fontWeight,omitempty"`
|
||||||
|
|
||||||
|
// FontSize
|
||||||
|
FontSize float32 `json:"fontSize,omitempty"`
|
||||||
|
|
||||||
|
// Align is a horizontal alignment of text, automatic by default.
|
||||||
|
// * "left"
|
||||||
|
// * "center"
|
||||||
|
// * "right"
|
||||||
|
Align string `json:"align,omitempty"`
|
||||||
|
|
||||||
|
// Align is a horizontal alignment of text, automatic by default.
|
||||||
|
// * "top"
|
||||||
|
// * "middle"
|
||||||
|
// * "bottom"
|
||||||
|
VerticalAlign string `json:"verticalAlign,omitempty"`
|
||||||
|
|
||||||
|
// Padding of the text fragment, for example:
|
||||||
|
// Padding: [3, 4, 5, 6]: represents padding of [top, right, bottom, left].
|
||||||
|
// Padding: 4: represents padding: [4, 4, 4, 4].
|
||||||
|
// Padding: [3, 4]: represents padding: [3, 4, 3, 4].
|
||||||
|
Padding interface{} `json:"padding,omitempty"`
|
||||||
|
|
||||||
|
// Width of text block
|
||||||
|
Width float32 `json:"width,omitempty"`
|
||||||
|
|
||||||
|
// Height of text block
|
||||||
|
Height float32 `json:"height,omitempty"`
|
||||||
|
|
||||||
|
// Edge label formatter, which supports string template and callback function.
|
||||||
|
// In either form, \n is supported to represent a new line.
|
||||||
|
// String template, Model variation includes:
|
||||||
|
//
|
||||||
|
// {a}: series name.
|
||||||
|
// {b}: the name of a data item.
|
||||||
|
// {c}: the value of a data item.
|
||||||
|
// {@xxx}: the value of a dimension named"xxx", for example,{@product}refers the value of"product"` dimension.
|
||||||
|
// {@[n]}: the value of a dimension at the index ofn, for example,{@[3]}` refers the value at dimensions[3].
|
||||||
|
Formatter string `json:"formatter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define what is encoded to for each dimension of data
|
||||||
|
// https://echarts.apache.org/en/option.html#series-candlestick.encode
|
||||||
|
type Encode struct {
|
||||||
|
X interface{} `json:"x"`
|
||||||
|
|
||||||
|
Y interface{} `json:"y"`
|
||||||
|
|
||||||
|
Tooltip interface{} `json:"tooltip,omitempty"`
|
||||||
|
|
||||||
|
SeriesName interface{} `json:"seriesName,omitempty"`
|
||||||
|
|
||||||
|
ItemID interface{} `json:"itemId,omitempty"`
|
||||||
|
|
||||||
|
ItemName interface{} `json:"itemName,omitempty"`
|
||||||
|
|
||||||
|
ItemGroupID interface{} `json:"itemGroupId,omitempty"`
|
||||||
|
}
|
||||||
118
vendor/github.com/go-echarts/go-echarts/v2/render/engine.go
generated
vendored
Normal file
118
vendor/github.com/go-echarts/go-echarts/v2/render/engine.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
tpls "github.com/go-echarts/go-echarts/v2/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Renderer
|
||||||
|
// Any kinds of charts have their render implementation and
|
||||||
|
// you can define your own render logic easily.
|
||||||
|
type Renderer interface {
|
||||||
|
Render(w io.Writer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModChart = "chart"
|
||||||
|
ModPage = "page"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pat = regexp.MustCompile(`(__f__")|("__f__)|(__f__)`)
|
||||||
|
|
||||||
|
type pageRender struct {
|
||||||
|
c interface{}
|
||||||
|
before []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPageRender returns a render implementation for Page.
|
||||||
|
func NewPageRender(c interface{}, before ...func()) Renderer {
|
||||||
|
return &pageRender{c: c, before: before}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the page into the given io.Writer.
|
||||||
|
func (r *pageRender) Render(w io.Writer) error {
|
||||||
|
for _, fn := range r.before {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
contents := []string{tpls.HeaderTpl, tpls.BaseTpl, tpls.PageTpl}
|
||||||
|
tpl := MustTemplate(ModPage, contents)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tpl.ExecuteTemplate(&buf, ModPage, r.c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
content := pat.ReplaceAll(buf.Bytes(), []byte(""))
|
||||||
|
|
||||||
|
_, err := w.Write(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type chartRender struct {
|
||||||
|
c interface{}
|
||||||
|
before []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChartRender returns a render implementation for Chart.
|
||||||
|
func NewChartRender(c interface{}, before ...func()) Renderer {
|
||||||
|
return &chartRender{c: c, before: before}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart into the given io.Writer.
|
||||||
|
func (r *chartRender) Render(w io.Writer) error {
|
||||||
|
for _, fn := range r.before {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
contents := []string{tpls.HeaderTpl, tpls.BaseTpl, tpls.ChartTpl}
|
||||||
|
tpl := MustTemplate(ModChart, contents)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tpl.ExecuteTemplate(&buf, ModChart, r.c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
content := pat.ReplaceAll(buf.Bytes(), []byte(""))
|
||||||
|
|
||||||
|
_, err := w.Write(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSet check if the field exist in the chart instance
|
||||||
|
// Shamed copy from https://stackoverflow.com/questions/44675087/golang-template-variable-isset
|
||||||
|
func isSet(name string, data interface{}) bool {
|
||||||
|
v := reflect.ValueOf(data)
|
||||||
|
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.FieldByName(name).IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustTemplate creates a new template with the given name and parsed contents.
|
||||||
|
func MustTemplate(name string, contents []string) *template.Template {
|
||||||
|
tpl := template.New(name).Funcs(template.FuncMap{
|
||||||
|
"safeJS": func(s interface{}) template.JS {
|
||||||
|
return template.JS(fmt.Sprint(s))
|
||||||
|
},
|
||||||
|
"isSet": isSet,
|
||||||
|
})
|
||||||
|
tpl = template.Must(tpl.Parse(contents[0]))
|
||||||
|
|
||||||
|
for _, cont := range contents[1:] {
|
||||||
|
tpl = template.Must(tpl.Parse(cont))
|
||||||
|
}
|
||||||
|
return tpl
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user