commit 73eac1d9ea85cb492144d006b7fbdb9f716f9a24 Author: Nathan Coad Date: Fri Oct 4 14:27:16 2024 +1000 first commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3b6ef4a --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module excel-chart + +go 1.23.2 + +require github.com/xuri/excelize/v2 v2.8.1 + +require ( + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect + github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..144577c --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ= +github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2404496 --- /dev/null +++ b/main.go @@ -0,0 +1,352 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + "reflect" + "runtime" + "slices" + "sort" + "strconv" + "time" + + "github.com/xuri/excelize/v2" +) + +type Input struct { + Tracking struct { + AvgCpu struct { + Tin Record `json:"Tin"` + Bronze Record `json:"Bronze"` + Silver Record `json:"Silver"` + Gold Record `json:"Gold"` + } `json:"AVG vCPUs"` + AvgRam Record `json:"Avg RAM (GB)"` + ProVmCount Record `json:"ProRated VM Count"` + VmCount Record `json:"VM Name"` + } `json:"Tracking"` +} + +type Record struct { + Ryde json.RawMessage `json:"Ryde Computer Centre"` + Wsdc json.RawMessage `json:"Western Sydney Data Centre"` +} + +func GenerateAvgVcpusCharts(f *excelize.File, data any, name string) { + var err error + rydeAvgCpu := make(map[string]interface{}) + wsdcAvgCpu := make(map[string]interface{}) + var avgCpuColumns []string + + // Access AvgCpu using reflection + avgCpuVal := reflect.ValueOf(data) + avgCpuType := reflect.TypeOf(data) + + // Iterate over each field in AvgCpu + for i := 0; i < avgCpuVal.NumField(); i++ { + field := avgCpuVal.Field(i) + fieldName := avgCpuType.Field(i).Name + + // Create variables to hold unmarshaled data + var rydeData interface{} + var wsdcData interface{} + + // Unmarshal Ryde and Wsdc fields into interface{} if they contain valid JSON + ryde := field.FieldByName("Ryde") + wsdc := field.FieldByName("Wsdc") + + if len(ryde.Interface().(json.RawMessage)) > 0 { + err = json.Unmarshal(ryde.Interface().(json.RawMessage), &rydeData) + if err != nil { + fmt.Printf("Error unmarshaling Ryde for %s: %v\n", fieldName, err) + } + } + + if len(wsdc.Interface().(json.RawMessage)) > 0 { + err = json.Unmarshal(wsdc.Interface().(json.RawMessage), &wsdcData) + if err != nil { + fmt.Printf("Error unmarshaling Wsdc for %s: %v\n", fieldName, err) + } + } + + rydeAvgCpu[fieldName] = rydeData + wsdcAvgCpu[fieldName] = wsdcData + avgCpuColumns = append(avgCpuColumns, fieldName) + } + + // Generate RCC worksheet and graph + AvgChart(f, name+" RCC", "A1", avgCpuColumns, rydeAvgCpu) + // Generate WSDC worksheet and graph + AvgChart(f, name+" WSDC", "N1", avgCpuColumns, wsdcAvgCpu) +} + +func GenerateCharts(f *excelize.File, data any, name string, location string) { + var err error + parsedData := make(map[string]interface{}) + var dataColumns = []string{"RCC", "WSDC"} + + // Create interfaces to hold unmarshaled data + var rcc interface{} + var wsdc interface{} + + // Access specific json objects using reflection + values := reflect.ValueOf(data) + rccField := values.FieldByName("Ryde") + wsdcField := values.FieldByName("Wsdc") + + // unmarshal raw json into interfaces + if len(rccField.Interface().(json.RawMessage)) > 0 { + err = json.Unmarshal(rccField.Interface().(json.RawMessage), &rcc) + if err != nil { + fmt.Printf("Error unmarshaling Ryde: %v\n", err) + } + } + if len(wsdcField.Interface().(json.RawMessage)) > 0 { + err = json.Unmarshal(wsdcField.Interface().(json.RawMessage), &wsdc) + if err != nil { + fmt.Printf("Error unmarshaling Wsdc: %v\n", err) + } + } + + // store the data together + parsedData["RCC"] = rcc + parsedData["WSDC"] = wsdc + //prettyPrint(parsedData) + + // Generate worksheet and graph + AvgChart(f, name, location, dataColumns, parsedData) +} + +func AvgChart(f *excelize.File, worksheetName string, location string, avgCpuColumns []string, data map[string]interface{}) { + var err error + var chartSeries []excelize.ChartSeries + var dataDates []string + var col int + var row int + + _, err = f.NewSheet(worksheetName) + if err != nil { + log.Fatal(err) + } + + // Create column headers dynamically + f.SetCellValue(worksheetName, "A1", "Date") + for i := 0; i < len(avgCpuColumns); i++ { + cell := string(rune('A'+i+1)) + "1" // A1, B1, C1, etc. + f.SetCellValue(worksheetName, cell, avgCpuColumns[i]) + } + + // Get the values for the dates column + for _, v := range data { + for date := range v.(map[string]interface{}) { + dataDates = append(dataDates, date) + } + break + } + + // Sort dates using the custom function + err = sortDates(dataDates) + if err != nil { + fmt.Println("Failed to sort dates:", err) + return + } + + // set the values for the dates column + for i := 0; i < len(dataDates); i++ { + cell := string(rune('A')) + strconv.Itoa(i+2) // A2, A3 etc + f.SetCellValue(worksheetName, cell, dataDates[i]) + } + + for pool, v := range data { // iterate each resource pool type + // Find the column that matches, add one to account for the date column + col = slices.Index(avgCpuColumns, pool) + 1 + //fmt.Printf("Pool: %s, column: %d\n", pool, col) + + for date, val := range v.(map[string]interface{}) { // use type assertion to loop over map[string]interface{} + //fmt.Printf("val: %v\n", val) + + // Find the correct row, add one to account for sheet heading + row = slices.Index(dataDates, date) + 1 + + cell := string(rune('A'+col)) + strconv.Itoa(row+1) + //fmt.Printf("Adding value %f (%s) to %s]\n", val, date, cell) + f.SetCellValue(worksheetName, cell, val) + } + + // Create the chartseries for this resource pool + thisChartSeries := excelize.ChartSeries{ + Name: "'" + worksheetName + "'!$" + string(rune('A'+col)) + "$1", // Reference the cell containing the resource pool name, eg Tin in $B$1 + Categories: "'" + worksheetName + "'!$A$2:$A$" + strconv.Itoa(len(dataDates)+1), // Reference the dates in the first column eg $A$2:$A$5 + Values: "'" + worksheetName + "'!$" + string(rune('A'+col)) + "$2:$" + string(rune('A'+col)) + "$" + strconv.Itoa(len(dataDates)+1), // Reference the values in the column matching the resource pool name, eg Tin in $B$2:$B$5 + Line: excelize.ChartLine{ + Smooth: true, + }, + } + //prettyPrint(thisChartSeries) + chartSeries = append(chartSeries, thisChartSeries) + } + + if err := f.AddChart("Report", location, &excelize.Chart{ + Type: excelize.Line, + Series: chartSeries, + Format: excelize.GraphicOptions{ + OffsetX: 5, + OffsetY: 5, + }, + Legend: excelize.ChartLegend{ + Position: "right", + }, + Title: []excelize.RichTextRun{ + { + Text: worksheetName, + }, + }, + PlotArea: excelize.ChartPlotArea{ + ShowCatName: false, + ShowLeaderLines: false, + ShowPercent: true, + ShowSerName: false, + ShowVal: false, + }, + ShowBlanksAs: "zero", + XAxis: excelize.ChartAxis{ + MajorGridLines: true, + MinorGridLines: true, + Font: excelize.Font{ + Color: "000000", + }, + }, + YAxis: excelize.ChartAxis{ + MajorGridLines: true, + MinorGridLines: true, + Font: excelize.Font{ + Color: "000000", + }, + }, + Dimension: excelize.ChartDimension{ + Height: 500, + Width: 800, + }, + }); err != nil { + fmt.Println(err) + return + } +} + +func main() { + var err error + + // Create the workbook + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + err = f.SetSheetName("Sheet1", "Report") + if err != nil { + log.Fatal(err) + } + + // Load the JSON data from file + file, err := os.Open("input.json") + if err != nil { + log.Fatalf("Failed to open input.json: %v", err) + } + defer file.Close() + + byteValue, _ := io.ReadAll(file) + var data Input + if err := json.Unmarshal(byteValue, &data); err != nil { + log.Fatal(err) + } + + // Generate charts into workbook + GenerateAvgVcpusCharts(f, data.Tracking.AvgCpu, "Average vCPUs") + GenerateCharts(f, data.Tracking.AvgRam, "Average RAM(GB)", "A30") + GenerateCharts(f, data.Tracking.ProVmCount, "ProRated VM Count", "A60") + GenerateCharts(f, data.Tracking.VmCount, "VM Count", "N60") + + // Save workbook + if err := f.SaveAs("Book1.xlsx"); err != nil { + fmt.Println(err) + } +} + +// parseDate parses a date string like "January-2006" into a time.Time object +func parseDate(dateStr string) (time.Time, error) { + layout := "January-2006" + return time.Parse(layout, dateStr) +} + +// sortDates sorts a slice of date strings in the format "Month-Year" +func sortDates(dates []string) error { + // Custom sort logic + sort.Slice(dates, func(i, j int) bool { + date1, err1 := parseDate(dates[i]) + date2, err2 := parseDate(dates[j]) + + if err1 != nil || err2 != nil { + fmt.Println("Error parsing dates:", err1, err2) + return false + } + + // Compare parsed dates + return date1.Before(date2) + }) + return nil +} + +func fetchValue(value interface{}) { + switch value.(type) { + case string: + fmt.Printf("%v is an interface \n ", value) + case bool: + fmt.Printf("%v is bool \n ", value) + case float64: + fmt.Printf("%v is float64 \n ", value) + case []interface{}: + fmt.Printf("%v is a slice of interface \n ", value) + for _, v := range value.([]interface{}) { // use type assertion to loop over []interface{} + fetchValue(v) + } + case map[string]interface{}: + fmt.Printf("%v is a map \n ", value) + for _, v := range value.(map[string]interface{}) { // use type assertion to loop over map[string]interface{} + fetchValue(v) + } + default: + fmt.Printf("%v is unknown \n ", value) + } +} + +// prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c +func prettyPrint(args ...interface{}) { + var caller string + + timeNow := time.Now().Format("01-02-2006 15:04:05") + prefix := fmt.Sprintf("[%s] %s -- ", "PrettyPrint", timeNow) + _, fileName, fileLine, ok := runtime.Caller(1) + + if ok { + caller = fmt.Sprintf("%s:%d", fileName, fileLine) + } else { + caller = "" + } + + fmt.Printf("\n%s%s\n", prefix, caller) + + if len(args) == 2 { + label := args[0] + value := args[1] + + s, _ := json.MarshalIndent(value, "", "\t") + fmt.Printf("%s%s: %s\n", prefix, label, string(s)) + } else { + s, _ := json.MarshalIndent(args, "", "\t") + fmt.Printf("%s%s\n", prefix, string(s)) + } +}