Skip to content

Commit

Permalink
Merge pull request #5 from Oskarowski/add-charts-generation-and-display
Browse files Browse the repository at this point in the history
Add charts generation under /charts & total refactor of home page
  • Loading branch information
Oskarowski authored Oct 1, 2024
2 parents 3f44ca6 + aa641e7 commit ada09c3
Show file tree
Hide file tree
Showing 20 changed files with 690 additions and 103 deletions.
32 changes: 31 additions & 1 deletion db/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,40 @@ FROM
WHERE
uuid IN (sqlc.slice('uuids'));

-- name: GetGamesInfo :one
SELECT
COUNT(*) AS total_games,
COUNT(*) FILTER (WHERE game_won = TRUE) AS won_games,
COUNT(*) FILTER (WHERE game_failed = TRUE AND game_won = FALSE) AS lost_games,
COUNT(*) FILTER (WHERE game_failed = FALSE AND game_won = FALSE) AS not_finished_games
FROM
games;

-- name: GetMovesByGameId :many
SELECT
*
FROM
moves
WHERE
game_id = ?
game_id = ?;

-- name: GetGamesByMonthYearGroupedByDay :many
SELECT
strftime('%d', created_at) AS day,
COUNT(*) AS games_played
FROM games
WHERE created_at >= ? AND created_at < ?
GROUP BY day
ORDER BY day;

-- name: GetGamesPlayedPerGridSize :many
SELECT grid_size, COUNT(*) AS games_played
FROM games
GROUP BY grid_size
ORDER BY grid_size;

-- name: GetMinesPopularity :many
SELECT mines_amount, COUNT(*) AS mines_count
FROM games
GROUP BY mines_amount
ORDER BY mines_amount;
1 change: 1 addition & 0 deletions db/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ sql:
schema: "./migrations"
gen:
go:
initialisms: []
package: "db"
out: "../internal/db"
Binary file added dist/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions dist/load-spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions dist/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
background-color: rgba(0, 0, 0, 0.088);
transition: background-color 0.2s ease;
cursor: pointer;
font-weight: bold;
}

.cell-revealed {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.1

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-echarts/go-echarts/v2 v2.4.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.3.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-echarts/go-echarts/v2 v2.4.2 h1:1FC3tGzsLSgdeO4Ltc3OAtcIiRomfEKxKX9oocIL68g=
github.com/go-echarts/go-echarts/v2 v2.4.2/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
Expand Down
296 changes: 296 additions & 0 deletions internal/api_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package internal

import (
"bytes"
"database/sql"
"fmt"
"html/template"
"minesweeper/internal/db"
"net/http"
"strconv"
"strings"
"time"

"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/gorilla/sessions"

chartrender "github.com/go-echarts/go-echarts/v2/render"
)

type ApiHandler struct {
Templates *template.Template
Store *sessions.CookieStore
Queries *db.Queries
}

func NewApiHandler(templates *template.Template, store *sessions.CookieStore, queries *db.Queries) *ApiHandler {
return &ApiHandler{templates, store, queries}
}

// renderToHtml renders a chart as a template.HTML value.
//
// The argument should be a go-echarts chart that implements the Renderer interface.
// The rendered chart is returned as a template.HTML value.
//
// If the chart fails to render, an error is returned.
func renderToHtml(c interface{}) (template.HTML, error) {
r, ok := c.(chartrender.Renderer)
if !ok {
return "", fmt.Errorf("provided chart does not implement the Renderer interface")
}

var buf bytes.Buffer

err := r.Render(&buf)
if err != nil {
return "", fmt.Errorf("failed to render chart: %v", err)

}

htmlContent := buf.String()

// Remove the <head>, <title>, <script> tags from the rendered chart
// TODO use net/html package with RemoveChild to remove the tags
htmlContent = strings.ReplaceAll(htmlContent, "<head>", "")
htmlContent = strings.ReplaceAll(htmlContent, "</head>", "")
htmlContent = strings.ReplaceAll(htmlContent, "<title>Awesome go-echarts</title>", "")
htmlContent = strings.ReplaceAll(htmlContent, "<script src=\"https://go-echarts.github.io/go-echarts-assets/assets/echarts.min.js\"></script>", "")

return template.HTML(htmlContent), nil
}

func (h *ApiHandler) PieWinsLossesIncompleteChart(w http.ResponseWriter, r *http.Request) {
rawData, err := h.Queries.GetGamesInfo(r.Context())

if err != nil {
http.Error(w, fmt.Sprintf("Error fetching DB information: %v", err), http.StatusInternalServerError)
return
}

parsedData := make([]opts.PieData, 0)
var colors = []string{"#28a745", "#dc3545", "#ffc107"}

parsedData = append(parsedData, opts.PieData{Name: "Wins", Value: rawData.WonGames, ItemStyle: &opts.ItemStyle{Color: colors[0]}})
parsedData = append(parsedData, opts.PieData{Name: "Losses", Value: rawData.LostGames, ItemStyle: &opts.ItemStyle{Color: colors[1]}})
parsedData = append(parsedData, opts.PieData{Name: "Incomplete", Value: rawData.NotFinishedGames, ItemStyle: &opts.ItemStyle{Color: colors[2]}})

pie := charts.NewPie()

pie.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
Title: "Wins vs Losses vs Incomplete",
Subtitle: fmt.Sprintf("Total games: %v", rawData.TotalGames),
}))

pie.AddSeries("Game Status", parsedData).
SetSeriesOptions(
charts.WithLabelOpts(opts.Label{
Formatter: "{b}: {d}%", // Label formatter to show percentage
}),
)

htmlPieSnipper, err := renderToHtml(pie)

if err != nil {
http.Error(w, fmt.Sprintf("Error rendering chart: %v", err), http.StatusInternalServerError)
return
}

w.Write([]byte(htmlPieSnipper))
}

func (h *ApiHandler) GridSizeBar(w http.ResponseWriter, r *http.Request) {

rawData, err := h.Queries.GetGamesPlayedPerGridSize(r.Context())

if err != nil {
http.Error(w, fmt.Sprintf("Error fetching grid size data: %v", err), http.StatusInternalServerError)
return
}

gridSizes := make([]string, len(rawData))
parsedBarData := make([]opts.BarData, len(rawData))

// find the most popular grid size
maxGamesPlayed := int64(0)
for _, dbData := range rawData {
if dbData.GamesPlayed > maxGamesPlayed {
maxGamesPlayed = dbData.GamesPlayed
}
}

for i, dbData := range rawData {
gridSizes[i] = fmt.Sprintf("%vx%v", dbData.GridSize, dbData.GridSize)

if dbData.GamesPlayed == maxGamesPlayed {
parsedBarData[i] = opts.BarData{Value: dbData.GamesPlayed, ItemStyle: &opts.ItemStyle{Color: "#ffa500"}}
} else {
parsedBarData[i] = opts.BarData{Value: dbData.GamesPlayed}
}
}

bar := charts.NewBar()
bar.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{
Title: "Grid Size Popularity",
Subtitle: "Games played per grid size",
}), charts.WithDataZoomOpts(opts.DataZoom{
Type: "slider",
Start: 0,
End: 100,
}),
charts.WithLegendOpts(opts.Legend{
Show: opts.Bool(false),
}),
)

bar.SetXAxis(gridSizes).
AddSeries("Games Played", parsedBarData).
SetSeriesOptions(
charts.WithLabelOpts(opts.Label{Show: opts.Bool(true)}),
)

htmlBarSnippet, err := renderToHtml(bar)

if err != nil {
http.Error(w, fmt.Sprintf("Error rendering chart: %v", err), http.StatusInternalServerError)
return
}

w.Write([]byte(htmlBarSnippet))
}

func (h *ApiHandler) MinesAmountBarChart(w http.ResponseWriter, r *http.Request) {
rawDbData, err := h.Queries.GetMinesPopularity(r.Context())

if err != nil {
http.Error(w, fmt.Sprintf("Error fetching mines popularity data: %v", err), http.StatusInternalServerError)
return
}

// find the most popular amount of mines
maxAmount := int64(0)
for _, dbData := range rawDbData {
if dbData.MinesCount > maxAmount {
maxAmount = dbData.MinesCount
}
}

minesPopularity := make([]string, len(rawDbData))
parsedBarData := make([]opts.BarData, len(rawDbData))

for i, dbData := range rawDbData {
minesPopularity[i] = strconv.FormatInt(dbData.MinesAmount, 10)

if dbData.MinesCount == maxAmount {
parsedBarData[i] = opts.BarData{Value: dbData.MinesCount, ItemStyle: &opts.ItemStyle{Color: "#ffa500"}}
} else {
parsedBarData[i] = opts.BarData{Value: dbData.MinesCount}
}
}

bar := charts.NewBar()
bar.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{
Title: "Amount of Mines Popularity",
Subtitle: "Games with particular amount of mines",
}),
charts.WithDataZoomOpts(opts.DataZoom{
Type: "slider",
Start: 10,
End: 75,
}),
charts.WithLegendOpts(opts.Legend{
Show: opts.Bool(false),
}),
)

bar.SetXAxis(minesPopularity).
AddSeries("Mines Amount", parsedBarData).
SetSeriesOptions(
charts.WithLabelOpts(opts.Label{Show: opts.Bool(true)}),
)

htmlBarSnippet, err := renderToHtml(bar)

if err != nil {
http.Error(w, fmt.Sprintf("Error rendering chart: %v", err), http.StatusInternalServerError)
return
}

w.Write([]byte(htmlBarSnippet))
}

func (h *ApiHandler) PlayedGamesInMonthBarChart(w http.ResponseWriter, r *http.Request) {
pickedDate := r.URL.Query().Get("picked-date-range")

if pickedDate == "" {
now := time.Now()
pickedDate = now.Format("2006-01")
}

parsedDate, err := time.Parse("2006-01", pickedDate)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid date format: %v", err), http.StatusBadRequest)
return
}

startOfMonth := parsedDate
endOfMonth := startOfMonth.AddDate(0, 1, 0)

// Create sql.NullTime for start and end of the month
startTime := sql.NullTime{
Time: startOfMonth,
Valid: true,
}

endTime := sql.NullTime{
Time: endOfMonth,
Valid: true,
}

gamesPerDay, err := h.Queries.GetGamesByMonthYearGroupedByDay(r.Context(), db.GetGamesByMonthYearGroupedByDayParams{
CreatedAt: startTime,
CreatedAt_2: endTime,
})
if err != nil {
http.Error(w, fmt.Sprintf("Error fetching games: %v", err), http.StatusInternalServerError)
return
}

days := []string{}
gamesPlayed := []opts.BarData{}

for _, gameDay := range gamesPerDay {
days = append(days, fmt.Sprintf("%v", gameDay.Day))
gamesPlayed = append(gamesPlayed, opts.BarData{Value: gameDay.GamesPlayed})
}

bar := charts.NewBar()
bar.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{
Title: fmt.Sprintf("Games Per Day in %v", parsedDate.Format("January 2006")),
Subtitle: "Amount of games played per day",
}),
charts.WithDataZoomOpts(opts.DataZoom{
Type: "slider",
Start: 0,
End: 100,
}),
charts.WithLegendOpts(opts.Legend{
Show: opts.Bool(false),
}),
)

bar.SetXAxis(days).
AddSeries("Games Played", gamesPlayed).
SetSeriesOptions(charts.WithLabelOpts(opts.Label{Show: opts.Bool(true)}))

htmlBarSnippet, err := renderToHtml(bar)
if err != nil {
http.Error(w, fmt.Sprintf("Error rendering chart: %v", err), http.StatusInternalServerError)
return
}

w.Write([]byte(htmlBarSnippet))
}
Loading

0 comments on commit ada09c3

Please sign in to comment.