first commit
This commit is contained in:
16
go.mod
Normal file
16
go.mod
Normal file
@@ -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
|
||||
)
|
29
go.sum
Normal file
29
go.sum
Normal file
@@ -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=
|
352
main.go
Normal file
352
main.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user