Skip to content

Commit

Permalink
feat(stats): add most watched movies and ratings chart
Browse files Browse the repository at this point in the history
  • Loading branch information
believer committed Sep 29, 2023
1 parent 1a607bd commit a70d290
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 7 deletions.
31 changes: 31 additions & 0 deletions db/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,34 @@ FROM
WHERE
p.id = $1;

-- name: stats-most-watched-movies
SELECT
COUNT(*) AS count,
m.title,
m.id
FROM
seen AS s
INNER JOIN movie AS m ON m.id = s.movie_id
WHERE
user_id = 1
GROUP BY
m.id
HAVING
COUNT(*) > 1
ORDER BY
count DESC
LIMIT 20;

-- name: stats-ratings
SELECT
COUNT(*) AS count,
rating
FROM
rating
WHERE
user_id = 1
GROUP BY
rating
ORDER BY
rating;

104 changes: 104 additions & 0 deletions handlers/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package handlers
import (
"believer/movies/db"
"believer/movies/utils"
"cmp"
"strconv"

"slices"

"github.com/gofiber/fiber/v2"
)
Expand Down Expand Up @@ -35,3 +39,103 @@ WHERE
"FormattedTotalRuntime": utils.FormatRuntime(stats.TotalRuntime),
})
}

func HandleGetMostWatchedMovies(c *fiber.Ctx) error {
var movies []struct {
Title string `db:"title"`
ID string `db:"id"`
Count int `db:"count"`
}

err := db.Dot.Select(db.Client, &movies, "stats-most-watched-movies")

if err != nil {
return err
}

return c.Render("partials/stats/most-watched-movies", fiber.Map{
"Data": movies,
})
}

type Rating struct {
Rating int `db:"rating"`
Count int `db:"count"`
}

func HandleGetRatings(c *fiber.Ctx) error {
var ratings []Rating

err := db.Dot.Select(db.Client, &ratings, "stats-ratings")

if err != nil {
return err
}

type Bar struct {
Label int
Value int
BarHeight int
BarWidth int
BarX int
BarY int
LabelX float64
LabelY int
ValueX float64
ValueY int
}

var graphData []Bar

maxBarHeight := 200
graphWidth := 536
maxCountInRatings := slices.MaxFunc(ratings, func(a, b Rating) int {
return cmp.Compare(a.Count, b.Count)
})

// The data is used for a bar chart, so we need to convert the data
for i, rating := range ratings {
// Calcualte the bar Height
// Subtract 20 from the maxBarHeight to make room for the text
barHeight := int(float64(rating.Count) / float64(maxCountInRatings.Count) * float64(maxBarHeight-20))
barWidth := int(float64(graphWidth)/float64(len(ratings))) - 5

// Space the bars evenly across the graph
barX := (graphWidth / len(ratings)) * i

// Subtract the barHeight from the maxBarHeight to position the bar at the bottom.
// This is because the SVG coordinate system starts at the top left corner.
barY := maxBarHeight - barHeight

// Position centered on the bar. Subtract 3.4 which is half the width of the text.
charWidth := 7.56 // Uses tabular nums so all characters are the same width
numberOfCharsInCount := len(strconv.Itoa(rating.Count))
halfWidthOfCount := charWidth * float64(numberOfCharsInCount) / 2
valueX := float64(barX+(barWidth/2)) - halfWidthOfCount
labelX := float64(barX+(barWidth/2)) - halfWidthOfCount

// Subtract 8 to put some space between the text and the bar
valueY := barY - 8
labelY := maxBarHeight + 15

// Add the data to the graphData slice
graphData = append(graphData, Bar{
Label: rating.Rating,
Value: rating.Count,
BarHeight: barHeight,
BarWidth: barWidth,
BarX: barX,
BarY: barY,
ValueX: valueX,
ValueY: valueY,
LabelX: labelX,
LabelY: labelY,
})
}

return c.Render("partials/stats/ratings", fiber.Map{
"Data": graphData,
"Width": graphWidth,
"Height": maxBarHeight,
})
}
72 changes: 72 additions & 0 deletions public/styles.1c4cb1.css → public/styles.ba1eff.css
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,18 @@ video {
background-color: transparent;
}

.fill-neutral-100 {
fill: #f5f5f5;
}

.fill-neutral-500 {
fill: #737373;
}

.stroke-neutral-400 {
stroke: #a3a3a3;
}

.px-4 {
padding-left: 1rem;
padding-right: 1rem;
Expand Down Expand Up @@ -802,11 +814,26 @@ video {
--tw-ring-offset-color: #fff;
}

.before\:absolute::before {
content: var(--tw-content);
position: absolute;
}

.before\:relative::before {
content: var(--tw-content);
position: relative;
}

.before\:-left-6::before {
content: var(--tw-content);
left: -1.5rem;
}

.before\:top-1\/2::before {
content: var(--tw-content);
top: 50%;
}

.before\:mb-4::before {
content: var(--tw-content);
margin-bottom: 1rem;
Expand All @@ -817,12 +844,40 @@ video {
display: block;
}

.before\:w-\[2ch\]::before {
content: var(--tw-content);
width: 2ch;
}

.before\:-translate-y-1\/2::before {
content: var(--tw-content);
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}

.before\:text-right::before {
content: var(--tw-content);
text-align: right;
}

.before\:text-4xl::before {
content: var(--tw-content);
font-size: 2.25rem;
line-height: 2.5rem;
}

.before\:text-xs::before {
content: var(--tw-content);
font-size: 0.75rem;
line-height: 1rem;
}

.before\:tabular-nums::before {
content: var(--tw-content);
--tw-numeric-spacing: tabular-nums;
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);
}

.before\:text-transparent::before {
content: var(--tw-content);
color: transparent;
Expand All @@ -833,6 +888,11 @@ video {
opacity: 0.1;
}

.before\:content-\[attr\(data-position\)\]::before {
--tw-content: attr(data-position);
content: var(--tw-content);
}

.before\:content-\[attr\(data-year\)\]::before {
--tw-content: attr(data-year);
content: var(--tw-content);
Expand Down Expand Up @@ -930,6 +990,18 @@ video {
background-color: rgb(23 23 23 / var(--tw-bg-opacity));
}

.dark\:fill-neutral-400 {
fill: #a3a3a3;
}

.dark\:fill-neutral-800 {
fill: #262626;
}

.dark\:stroke-neutral-600 {
stroke: #525252;
}

.dark\:text-neutral-200 {
--tw-text-opacity: 1;
color: rgb(229 229 229 / var(--tw-text-opacity));
Expand Down
1 change: 1 addition & 0 deletions public/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ func SetupRoutes(app *fiber.App) {

// Stats
// --------------------------
app.Get("/stats", handlers.HandleGetStats)
statsGroup := app.Group("/stats")

statsGroup.Get("/", handlers.HandleGetStats)
statsGroup.Get("/most-watched-movies", handlers.HandleGetMostWatchedMovies)
statsGroup.Get("/ratings", handlers.HandleGetRatings)
}
2 changes: 1 addition & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ func FormatRuntime(runtime int) string {
hours := runtime / 60 % 24
minutes := runtime % 60

return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
return fmt.Sprintf("%d days %d hours %d minutes", days, hours, minutes)
}
2 changes: 1 addition & 1 deletion views/layouts/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<title>Movies</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/public/styles.1c4cb1.css" rel="stylesheet" />
<link href="/public/styles.ba1eff.css" rel="stylesheet" />
<script
src="https://unpkg.com/htmx.org@1.9.6"
integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"
Expand Down
Loading

0 comments on commit a70d290

Please sign in to comment.