puzzle dashboard WIP

pull/7901/head
Thibault Duplessis 2021-01-12 18:07:50 +01:00
parent 292dbcc9eb
commit 47a68eddbd
12 changed files with 174 additions and 18 deletions

View File

@ -3,10 +3,12 @@ package html.puzzle
import controllers.routes
import play.api.i18n.Lang
import play.api.libs.json.Json
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.safeJsonValue
import lila.puzzle.PuzzleDashboard
import lila.puzzle.PuzzleTheme
import lila.user.User
@ -27,15 +29,55 @@ object dashboard {
if (ctx is user) "Puzzle dashboard"
else s"${user.username} puzzle dashboard",
subtitle = "Let's see how good you've been doing",
dashOpt = dashOpt
dashOpt = dashOpt,
moreJs = dashOpt ?? { dash =>
val mostPlayed = dash.mostPlayed.sortBy { case (key, _) =>
PuzzleTheme(key).name.txt()
}
frag(
jsModule("puzzle.dashboard"),
embedJsUnsafeLoadThen(s"""LichessPuzzleDashboard.renderRadar(${safeJsonValue(
Json
.obj(
"radar" -> Json.obj(
"labels" -> mostPlayed.map { case (key, _) =>
PuzzleTheme(key).name.txt()
},
"datasets" -> Json.arr(
// Json.obj(
// "label" -> "# of puzzles",
// "data" -> mostPlayed.map { case (_, results) =>
// results.nb
// }
// ),
Json.obj(
"label" -> "Performance",
"data" -> mostPlayed.map { case (_, results) =>
results.performance
}
)
)
)
)
)})""")
)
}
) { dash =>
frag(
div(cls := s"${baseClass}__global")(
metricsOf(days, PuzzleTheme.mix.key, dash.global)
metricsOf(days, PuzzleTheme.mix.key, dash.global),
canvas(cls := s"${baseClass}__radar")
)
)
}
// data: {
// labels: ['Running', 'Swimming', 'Eating', 'Cycling'],
// datasets: [{
// data: [20, 10, 4, 2]
// }]
// }
def improvementAreas(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
dashboardLayout(
user = user,
@ -70,13 +112,15 @@ object dashboard {
path: String,
title: String,
subtitle: String,
dashOpt: Option[PuzzleDashboard]
dashOpt: Option[PuzzleDashboard],
moreJs: Frag = emptyFrag
)(
body: PuzzleDashboard => Frag
)(implicit ctx: Context) =
views.html.base.layout(
title = title,
moreCss = cssTag("puzzle.dashboard")
moreCss = cssTag("puzzle.dashboard"),
moreJs = moreJs
)(
main(cls := "page-menu")(
bits.pageMenu("dashboard"),

View File

@ -27,7 +27,7 @@ object history {
div(cls := "puzzle-history")(
div(cls := "infinite-scroll")(
pager.currentPageResults map renderSession,
pagerNext(pager, np => routes.Puzzle.history(np).url)
pagerNext(pager, np => s"${routes.Puzzle.history(np).url}${!ctx.is(user) ?? s"&u=${user.id}"}")
)
)
)

View File

@ -33,6 +33,8 @@ case class PuzzleDashboard(
.takeRight(topThemesNb)
(weaks, strong)
}
def mostPlayed = byTheme.toList.sortBy(-_._2.nb).take(9)
}
object PuzzleDashboard {

View File

@ -1,5 +1,6 @@
package lila.puzzle
import cats.data.NonEmptyList
import org.joda.time.DateTime
import reactivemongo.api.bson.BSONNull
import reactivemongo.api.ReadPreference
@ -7,22 +8,29 @@ import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
import scala.util.chaining._
import lila.common.config.MaxPerPage
import lila.common.paginator.AdapterLike
import lila.common.paginator.Paginator
import lila.db.dsl._
import lila.memo.CacheApi
import lila.user.User
import lila.common.paginator.Paginator
import lila.common.config.MaxPerPage
import cats.data.NonEmptyList
object PuzzleHistory {
val maxPerPage = MaxPerPage(100)
case class SessionRound(round: PuzzleRound, puzzle: Puzzle, theme: PuzzleTheme.Key)
case class PuzzleSession(
theme: PuzzleTheme.Key,
puzzles: NonEmptyList[SessionRound] // chronological order, oldest first
)
) {
// val nb = puzzles.size
// val firstWins = puzzles.toList.count(_.round.firstWin)
// val fails = nb - firstWins
// def puzzleRatingAvg = puzzles.toList.foldLeft(0)(_ + _.puzzle.glicko.intRating)
// def performance = puzzleRatingAvg - 500 + math.round(1000 * (firstWins.toFloat / nb))
}
final class HistoryAdapter(user: User, colls: PuzzleColls)(implicit ec: ExecutionContext)
extends AdapterLike[PuzzleSession] {
@ -60,13 +68,13 @@ object PuzzleHistory {
rounds
.foldLeft(List.empty[PuzzleSession]) {
case (Nil, round) => List(PuzzleSession(round.theme, NonEmptyList(round, Nil)))
case (last :: sessions, round) =>
case (last :: sessions, r) =>
if (
last.puzzles.head.theme == round.theme &&
last.puzzles.head.round.date.isAfter(round.round.date minusMinutes 15)
last.puzzles.head.theme == r.theme &&
r.round.date.isAfter(last.puzzles.head.round.date minusHours 1)
)
last.copy(puzzles = round :: last.puzzles) :: sessions
else PuzzleSession(round.theme, NonEmptyList(round, Nil)) :: last :: sessions
last.copy(puzzles = r :: last.puzzles) :: sessions
else PuzzleSession(r.theme, NonEmptyList(r, Nil)) :: last :: sessions
}
.reverse
}
@ -82,7 +90,7 @@ final class PuzzleHistoryApi(
Paginator[PuzzleSession](
new HistoryAdapter(user, colls),
currentPage = page,
maxPerPage = MaxPerPage(50)
maxPerPage = maxPerPage
)
}

View File

@ -40,6 +40,8 @@ case class PuzzleRound(
win = win,
fixedAt = fixedAt orElse win.option(DateTime.now)
)
def firstWin = win && fixedAt.isEmpty
}
object PuzzleRound {

View File

@ -19,6 +19,7 @@ apps3="site swiss msg chat cli challenge notify learn insight editor puzzle roun
site_plugins="tvEmbed puzzleEmbed analyseEmbed user modUser clas coordinate captcha expandText team forum account coachShow coachForm challengePage checkout login passwordComplexity tourForm teamBattleForm gameSearch userComplete infiniteScroll flatpickr teamAdmin"
round_plugins="nvui keyboardMove"
analyse_plugins="nvui studyTopicForm"
puzzle_plugins="dashboard"
if [ $mode == "upgrade" ]; then
yarn upgrade --non-interactive
@ -66,11 +67,13 @@ else
parallel --gnu $P_OPTS build_plugin site ::: $site_plugins
parallel --gnu $P_OPTS build_plugin round ::: $round_plugins
parallel --gnu $P_OPTS build_plugin analyse ::: $analyse_plugins
parallel --gnu $P_OPTS build_plugin analyse ::: $puzzle_plugins
else # sequential execution
echo "For faster builds, install GNU parallel."
for app in $apps1 $apps2 $apps3; do (build $app); done
for plugin in $site_plugins; do (build_plugin site $plugin); done
for plugin in $round_plugins; do (build_plugin round $plugin); done
for plugin in $analyse_plugins; do (build_plugin analyse $plugin); done
for plugin in $puzzle_plugins; do (build_plugin puzzle $plugin); done
fi
fi

View File

@ -40,6 +40,12 @@
text-align: center;
}
&__radar {
width: 100%;
height: 70vh;
margin-top: 4em;
}
&__metrics {
display: flex;
}

View File

@ -1,9 +1,16 @@
.puzzle-history {
&__session {
margin-bottom: 5em;
&__title {
@extend %flex-between;
align-items: flex-end;
margin-bottom: 1rem;
time {
font-size: 1.5rem;
margin-right: 1ch;
}
}
&__rounds {
display: grid;
@ -13,7 +20,7 @@
}
&__round {
padding: .3em;
padding: .4em;
color: $c-font;
&__meta {

View File

@ -17,7 +17,9 @@
"@types/lichess": "2.0.0"
},
"dependencies": {
"@types/chart.js": "^2.9.29",
"ceval": "2.0.0",
"chart.js": "^2.9.4",
"chessground": "^7.9.2",
"chessops": "^0.8.1",
"common": "2.0.0",
@ -26,6 +28,8 @@
},
"scripts": {
"dev": "rollup --config",
"prod": "rollup --config --config-prod"
"prod": "rollup --config --config-prod",
"plugin-dev": "rollup --config --config-plugin",
"plugin-prod": "rollup --config --config-prod --config-plugin"
}
}

View File

@ -6,4 +6,9 @@ export default rollupProject({
input: 'src/main.ts',
output: 'puzzle',
},
dashboard: {
name: 'LichessPuzzleDashboard',
input: 'src/dashboard.ts',
output: 'puzzle.dashboard',
}
});

View File

@ -0,0 +1,35 @@
import Chart from 'chart.js';
export function renderRadar(data: any) {
$('.puzzle-dashboard__radar').each(function() {
const d = data.radar;
d.datasets[0] = {
...d.datasets[0],
...{
backgroundColor: 'rgba(189,130,35,.1)',
borderColor: 'rgba(189,130,35,1)'
}
};
const chart = new Chart(this, {
type: 'radar',
data: d,
options: {
scales: {
y: {
beginAtZero: false
}
},
plugins: {
legend: {
labels: {
font: {
size: 140
}
}
}
}
}
});
});
}

View File

@ -123,6 +123,13 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@types/chart.js@^2.9.29":
version "2.9.29"
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.29.tgz#73bf7f02387402943f29946012492f10bde7ed43"
integrity sha512-WOZMitUU3gHDM0oQsCsVivX+oDsIki93szcTmmUPBm39cCvAELBjokjSDVOoA3xiIEbb+jp17z/3S2tIqruwOQ==
dependencies:
moment "^2.10.2"
"@types/defer-promise@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/defer-promise/-/defer-promise-1.0.0.tgz#9c5054b7ef63fd0f0497d17b0ea1a849f9c6323b"
@ -719,6 +726,29 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chart.js@^2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
"chess.js@github:ornicar/chess.js#learn":
version "0.10.2"
resolved "https://codeload.github.com/ornicar/chess.js/tar.gz/ad0709c0b07773d9d0da8e4605a9a2a28f00d249"
@ -846,7 +876,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0:
color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -858,6 +888,11 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
@ -3018,6 +3053,11 @@ mixin-deep@^1.2.0:
dependencies:
minimist "^1.2.5"
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"