package main
import (
"context"
"encoding/csv"
"fmt"
"log"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/chromedp/chromedp"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/google/go-github/v66/github"
"github.com/joho/godotenv"
"golang.org/x/oauth2"
)
const (
csvFilename = "data/repo_stats.csv"
owner = "pancsta"
repo = "asyncmachine-go"
)
var token = ""
type DailyStat struct {
Date string
Views int
UniqueViews int
Clones int
UniqueClones int
ReleaseDownloads int
}
func init() {
godotenv .Load ()
token = os .Getenv ("GITHUB_TOKEN_STATS" )
}
func main() {
ctx := context .Background ()
getStats (ctx )
genCharts (ctx )
render ("light" , "white" )
render ("dark" , "#100c2a" )
}
func getStats(ctx context .Context ) {
if token == "YOUR_TOKEN" {
log .Fatal ("Please update the token, owner, and repo constants in the code." )
}
ts := oauth2 .StaticTokenSource (&oauth2 .Token {AccessToken : token })
tc := oauth2 .NewClient (ctx , ts )
client := github .NewClient (tc )
statsMap := loadExistingCSV (csvFilename )
fmt .Printf ("Loaded %d historical records.\n" , len (statsMap ))
fmt .Println ("Fetching Views..." )
views , _ , err := client .Repositories .ListTrafficViews (ctx , owner , repo , nil )
if err != nil {
log .Printf ("Error fetching views: %v" , err )
} else {
for _ , v := range views .Views {
dateStr := v .GetTimestamp ().Format ("2006-01-02" )
entry := getOrCreate (statsMap , dateStr )
entry .Views = v .GetCount ()
entry .UniqueViews = v .GetUniques ()
}
}
fmt .Println ("Fetching Clones..." )
clones , _ , err := client .Repositories .ListTrafficClones (ctx , owner , repo , nil )
if err != nil {
log .Printf ("Error fetching clones: %v" , err )
} else {
for _ , c := range clones .Clones {
dateStr := c .GetTimestamp ().Format ("2006-01-02" )
entry := getOrCreate (statsMap , dateStr )
entry .Clones = c .GetCount ()
entry .UniqueClones = c .GetUniques ()
}
}
totalDownloads := 0
todayStr := time .Now ().Format ("2006-01-02" )
todayEntry := getOrCreate (statsMap , todayStr )
todayEntry .ReleaseDownloads = totalDownloads
if err := saveCSV (csvFilename , statsMap ); err != nil {
log .Fatalf ("Failed to save CSV: %v" , err )
}
fmt .Printf ("Done! Stats merged and saved to %s\n" , csvFilename )
}
func getOrCreate(m map [string ]*DailyStat , date string ) *DailyStat {
if val , ok := m [date ]; ok {
return val
}
newItem := &DailyStat {Date : date }
m [date ] = newItem
return newItem
}
func loadExistingCSV(csvPath string ) map [string ]*DailyStat {
data := make (map [string ]*DailyStat )
if path .Dir (csvPath ) != "" {
err := os .MkdirAll (path .Dir (csvPath ), 0755 )
if err != nil {
panic ( err )
}
}
f , err := os .Open (csvPath )
if os .IsNotExist (err ) {
return data
}
if err != nil {
log .Printf ("Could not open existing CSV: %v" , err )
return data
}
defer f .Close ()
reader := csv .NewReader (f )
records , err := reader .ReadAll ()
if err != nil {
return data
}
for i , row := range records {
if i == 0 || len (row ) < 6 {
continue
}
views , _ := strconv .Atoi (row [1 ])
uViews , _ := strconv .Atoi (row [2 ])
clones , _ := strconv .Atoi (row [3 ])
uClones , _ := strconv .Atoi (row [4 ])
dl , _ := strconv .Atoi (row [5 ])
data [row [0 ]] = &DailyStat {
Date : row [0 ],
Views : views ,
UniqueViews : uViews ,
Clones : clones ,
UniqueClones : uClones ,
ReleaseDownloads : dl ,
}
}
return data
}
func saveCSV(filename string , data map [string ]*DailyStat ) error {
var stats []*DailyStat
for _ , v := range data {
stats = append (stats , v )
}
sort .Slice (stats , func (i , j int ) bool {
return stats [i ].Date < stats [j ].Date
})
f , err := os .Create (filename )
if err != nil {
return err
}
defer f .Close ()
writer := csv .NewWriter (f )
defer writer .Flush ()
header := []string {"Date" , "Views" , "Unique_Views" , "Clones" , "Unique_Clones" , "Total_Release_Downloads" }
if err := writer .Write (header ); err != nil {
return err
}
for _ , s := range stats {
record := []string {
s .Date ,
strconv .Itoa (s .Views ),
strconv .Itoa (s .UniqueViews ),
strconv .Itoa (s .Clones ),
strconv .Itoa (s .UniqueClones ),
strconv .Itoa (s .ReleaseDownloads ),
}
if err := writer .Write (record ); err != nil {
return err
}
}
return nil
}
func genCharts(ctx context .Context ) {
f , err := os .Open (csvFilename )
if err != nil {
log .Fatalf ("Unable to read input file: %v" , err )
}
defer f .Close ()
csvReader := csv .NewReader (f )
records , err := csvReader .ReadAll ()
if err != nil {
log .Fatalf ("Unable to parse file as CSV: %v" , err )
}
var dates []string
var uniqueClones []opts .LineData
for i , record := range records {
if i == 0 {
continue
}
if len (record ) < 5 {
continue
}
dateStr := record [0 ]
if dateStr == "" {
continue
}
val , err := strconv .Atoi (record [4 ])
if err != nil {
log .Printf ("Skipping row %d due to invalid number: %v" , i , err )
continue
}
dates = append (dates , dateStr )
uniqueClones = append (uniqueClones , opts .LineData {Value : val })
}
themes := []string {"light" , "dark" }
for _ , theme := range themes {
line := charts .NewLine ()
line .SetGlobalOptions (
charts .WithInitializationOpts (opts .Initialization {
Theme : theme ,
Width : "800px" ,
}),
charts .WithTooltipOpts (opts .Tooltip {
Show : opts .Bool (true ),
Trigger : "axis" ,
}),
charts .WithXAxisOpts (opts .XAxis {
Name : "Date" ,
}),
charts .WithYAxisOpts (opts .YAxis {
Name : "Count" ,
}),
)
line .SetXAxis (dates ).
AddSeries ("Unique Clones" , uniqueClones ).
SetSeriesOptions (
charts .WithLineChartOpts (opts .LineChart {
Smooth : opts .Bool (true ),
}),
charts .WithAnimationOpts (opts .Animation {
Animation : opts .Bool (false ),
}),
charts .WithLabelOpts (opts .Label {
Show : opts .Bool (true ),
}),
)
outputFile , _ := os .Create ("data/stats." + theme + ".html" )
err = line .Render (outputFile )
if err != nil {
log .Println (err )
}
outputFile .Close ()
}
}
func render(theme , bg string ) {
inputHTML := "data/stats." + theme + ".html"
outputImage := "data/stats." + theme + ".png"
wd , err := os .Getwd ()
if err != nil {
log .Fatal (err )
}
fileURL := "file://" + filepath .Join (wd , inputHTML )
ctx , cancel := chromedp .NewContext (context .Background ())
defer cancel ()
var imageBuf []byte
fmt .Println ("Capturing screenshot... (this may take a few seconds)" )
err = chromedp .Run (ctx ,
chromedp .Navigate (fileURL ),
chromedp .WaitVisible ("canvas" , chromedp .ByQuery ),
chromedp .Evaluate ("document.body.style.backgroundColor = '" +bg +"'" , nil ),
chromedp .Sleep (2 *time .Second ),
chromedp .FullScreenshot (&imageBuf , 90 ),
)
if err != nil {
log .Fatalf ("Error taking screenshot: %v" , err )
}
if err := os .WriteFile (outputImage , imageBuf , 0644 ); err != nil {
log .Fatal (err )
}
fmt .Printf ("Success! Saved image to %s\n" , outputImage )
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .