Skip to content

Commit

Permalink
Merge pull request #1189 from hexlet-codebattle/user-charts
Browse files Browse the repository at this point in the history
user charts
  • Loading branch information
vtm9 committed Dec 6, 2022
2 parents 474416d + 159bf98 commit b90e32e
Show file tree
Hide file tree
Showing 7 changed files with 2,201 additions and 1,918 deletions.
6 changes: 3 additions & 3 deletions services/app/assets/js/widgets/components/User/UserStats.jsx
Expand Up @@ -45,15 +45,15 @@ const UserStats = ({ data }) => {
<div className="col d-flex justify-content-between">
<div>
<span>Won:</span>
<b className="text-success">{stats.won}</b>
<b className="text-success">{stats.games.won}</b>
</div>
<div className="ml-1">
<span>Lost:</span>
<b className="text-danger">{stats.lost}</b>
<b className="text-danger">{stats.games.lost}</b>
</div>
<div className="ml-1">
<span>GaveUp:</span>
<b className="text-warning">{stats.gaveUp}</b>
<b className="text-warning">{stats.games.gaveUp}</b>
</div>
</div>
</div>
Expand Down
183 changes: 144 additions & 39 deletions services/app/assets/js/widgets/containers/UserProfile.jsx
@@ -1,6 +1,21 @@
import { camelizeKeys } from 'humps';
import { useDispatch, useSelector } from 'react-redux';
import _ from 'lodash';
import React, { useState, useEffect } from 'react';

import {
Radar,
RadarChart,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Tooltip,
Legend,
} from 'recharts';
import axios from 'axios';
// import classnames from 'classnames';

Expand All @@ -13,7 +28,9 @@ import * as selectors from '../selectors';

const UserProfile = () => {
const [stats, setStats] = useState(null);
const { completedGames, totalGames } = useSelector(selectors.completedGamesData);
const { completedGames, totalGames } = useSelector(
selectors.completedGamesData,
);

const dispatch = useDispatch();

Expand All @@ -34,7 +51,11 @@ const UserProfile = () => {
dispatch(fetchCompletedGames());
}, [dispatch]);

const dateParse = date => new Date(date).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
const dateParse = date => new Date(date).toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});

const renderAchivement = achievement => {
if (achievement.includes('win_games_with')) {
Expand Down Expand Up @@ -68,10 +89,96 @@ const UserProfile = () => {
/>
);
};

if (!stats) {
return <Loading />;
}

const renderCustomPieChart = () => {
if (!stats.stats) {
return <Loading />;
}

const colors = [
'#E40303',
'#008026',
'#732982',
'#FF8C00',
'#24408E',
'#FFED00',
];

const groups = _.groupBy(stats.stats.all, 'lang');
const reducedByLangStats = _.mapValues(groups, group => group.reduce((total, item) => total + item.count, 0));
const dataForPie = Object.entries(reducedByLangStats).map(
([lang, count]) => ({ name: lang, value: count }),
);

const fullMark = Math.max(...Object.values(stats.stats.games));
const resultDataForRadar = Object.keys(stats.stats.games).map(
subject => ({
subject,
A: stats.stats.games[subject],
fullMark,
}),
);
const sortedDataForRadar = resultDataForRadar.sort((a, b) => {
if (a.subject === 'won') return -1;
if (b.subject === 'won') return 1;
if (a.subject < b.subject) return -1;
if (a.subject > b.subject) return 1;
return 0;
});

return (
<>
<div className="col-6">
<ResponsiveContainer width="100%" height="100%">
<RadarChart
cx="50%"
cy="50%"
outerRadius="80%"
data={sortedDataForRadar}
>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis />
<Radar
name="Nikita"
dataKey="A"
stroke="#8884d8"
fill="#8884d8"
fillOpacity={0.6}
/>
</RadarChart>
</ResponsiveContainer>
</div>
<div className="col-6">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
dataKey="value"
data={dataForPie}
labelLine={false}
label
position="inside"
>
{dataForPie.map(({ name }, index) => (
<Cell
key={`cell-${name}`}
fill={colors[index % colors.length]}
/>
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
</>
);
};

const renderStatistics = () => (
<>
<div className="row my-4 justify-content-center">
Expand All @@ -86,23 +193,17 @@ const UserProfile = () => {
<p className="lead">elo_rating</p>
</div>
<div className="col-md-3 col-5 text-center">
<div className="h1 cb-stats-number">{stats.stats.won + stats.stats.lost + stats.stats.gaveUp}</div>
<div className="h1 cb-stats-number">
{Object.values(stats.stats.games).reduce((a, b) => a + b, 0)}
</div>
<p className="lead">games_played</p>
</div>
</div>
<div className="row my-4 justify-content-center">
<div className="col-3 col-lg-2 text-center">
<div className="h1 cb-stats-number">{stats.stats.won}</div>
<p className="lead">won</p>
</div>
<div className="col-3 col-lg-2 text-center border-left border-right">
<div className="h1 cb-stats-number">{stats.stats.lost}</div>
<p className="lead">lost</p>
</div>
<div className="col-3 col-lg-2 text-center">
<div className="h1 cb-stats-number">{stats.stats.gaveUp}</div>
<p className="lead">gave up</p>
</div>
<div
className="row my-4 justify-content-center"
style={{ width: '100%', height: 400 }}
>
{renderCustomPieChart()}
</div>
<div className="row my-4 justify-content-center">
<div className="col-10 col-lg-8 cb-heatmap">
Expand All @@ -117,26 +218,28 @@ const UserProfile = () => {
<div className="col-12">
<div className="text-left">
{completedGames && completedGames.length > 0 && (
<>
<CompletedGames
games={completedGames}
loadNextPage={loadNextPage}
totalGames={totalGames}
/>
</>
)}
<>
<CompletedGames
games={completedGames}
loadNextPage={loadNextPage}
totalGames={totalGames}
/>
</>
)}
{completedGames && completedGames.length === 0 && (
<>
<div style={{ height: 498 }} className="d-flex align-items-center justify-content-center border text-muted">
No completed games
</div>
</>
<>
<div
style={{ height: 498 }}
className="d-flex align-items-center justify-content-center border text-muted"
>
No completed games
</div>
</>
)}
</div>
</div>
</div>
);

const statContainers = () => (
<div>
<nav>
Expand Down Expand Up @@ -216,14 +319,14 @@ const UserProfile = () => {
</p>
<h1 className="my-2">
{stats.user.githubName && (
<a
title="Github account"
className="text-muted"
href={`https://github.com/${stats.user.githubName}`}
>
<span className="fab fa-github pr-3" />
</a>
)}
<a
title="Github account"
className="text-muted"
href={`https://github.com/${stats.user.githubName}`}
>
<span className="fab fa-github pr-3" />
</a>
)}
</h1>
<div className="my-2">
{stats.user.achievements.length > 0 && (
Expand All @@ -232,7 +335,9 @@ const UserProfile = () => {
<h5 className="text-break cb-heading">Achievements</h5>
<div className="col d-flex flex-wrap justify-content-start cb-profile mt-3 pl-0">
{stats.user.achievements.map(achievement => (
<div key={achievement}>{renderAchivement(achievement)}</div>
<div key={achievement}>
{renderAchivement(achievement)}
</div>
))}
</div>
</>
Expand Down
47 changes: 15 additions & 32 deletions services/app/lib/codebattle/user/stats.ex
Expand Up @@ -3,46 +3,29 @@ defmodule Codebattle.User.Stats do
Find user game statistic
"""

alias Codebattle.{Repo, UserGame, Game}
alias Codebattle.Repo
alias Codebattle.UserGame

import Ecto.Query, warn: false
import Ecto.Query

@default_game_stats %{"won" => 0, "lost" => 0, "gave_up" => 0}

def get_game_stats(user_id) do
query =
user_games_stats =
from(ug in UserGame,
select: {
ug.result,
count(ug.id)
},
select: %{result: ug.result, lang: ug.lang, count: count(ug.id)},
where: ug.user_id == ^user_id,
where: ug.result in ["won", "lost", "gave_up"],
group_by: ug.result
)

stats = Repo.all(query)

Map.merge(%{"won" => 0, "lost" => 0, "gave_up" => 0}, Enum.into(stats, %{}))
end

def get_completed_games(user_id, params) do
page_number = params |> Map.get("page", "1") |> String.to_integer()
page_size = params |> Map.get("page_size", "9") |> String.to_integer()

query =
from(
g in Game,
order_by: [desc_nulls_last: g.finishes_at],
inner_join: ug in assoc(g, :user_games),
inner_join: u in assoc(ug, :user),
where: g.state == "game_over" and ug.user_id == ^user_id,
preload: [:users, :user_games]
group_by: [ug.result, ug.lang]
)
|> Repo.all()

page = Repo.paginate(query, %{page: page_number, page_size: page_size})
games_stats =
user_games_stats
|> Enum.group_by(& &1.result, & &1.count)
|> Map.new(fn {k, v} -> {k, Enum.sum(v)} end)
|> Map.merge(@default_game_stats, fn _k, v1, _v2 -> v1 end)

%{
games: page.entries,
page_info: Map.take(page, [:page_number, :page_size, :total_entries, :total_pages])
}
%{games: games_stats, all: user_games_stats}
end
end
3 changes: 3 additions & 0 deletions services/app/package.json
Expand Up @@ -35,6 +35,7 @@
"axios": "^0.21.2",
"bootstrap": "^4.5.3",
"calcite-react": "^0.56.2",
"chart.js": "^4.0.1",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.3.1",
"core-js": "3.8.0",
Expand Down Expand Up @@ -62,6 +63,7 @@
"react": "^16.13.1",
"react-bootstrap": "^1.4.0",
"react-calendar-heatmap": "^1.8.1",
"react-chartjs-2": "^5.0.1",
"react-dom": "^16.13.1",
"react-feather": "^2.0.9",
"react-hotkeys": "^2.0.0",
Expand All @@ -78,6 +80,7 @@
"react-stay-scrolled": "^7.1.0",
"react-toastify": "^6.1.0",
"react-transition-group": "^4.4.1",
"recharts": "^2.1.16",
"redux-persist": "^6.0.0",
"rollbar": "^2.19.4",
"rollbar-redux-middleware": "^0.2.0",
Expand Down

0 comments on commit b90e32e

Please sign in to comment.