Add charts showing historic test scores
This commit is contained in:
@@ -3,8 +3,10 @@ import { HomeRounded as HomeRoundedIcon, QuestionAnswerRounded as QuestionAnswer
|
|||||||
import NavBar from "./NavBar";
|
import NavBar from "./NavBar";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
import LineChart from "./LineChart";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import "./css/History.css";
|
import "./css/History.css";
|
||||||
|
import "./css/Chart.css";
|
||||||
|
|
||||||
export default class History extends Component {
|
export default class History extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -38,13 +40,25 @@ export default class History extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = "History | Parandum";
|
document.title = "History | Parandum";
|
||||||
|
|
||||||
|
const userSets = this.state.db
|
||||||
|
.collection("sets")
|
||||||
|
.where("owner", "==", this.state.user.uid)
|
||||||
|
.orderBy("title", "asc")
|
||||||
|
.get();
|
||||||
|
|
||||||
this.state.db.collection("progress")
|
this.state.db.collection("progress")
|
||||||
.where("uid", "==", this.state.user.uid)
|
.where("uid", "==", this.state.user.uid)
|
||||||
.orderBy("start_time", "desc")
|
.orderBy("start_time", "desc")
|
||||||
.get()
|
.get()
|
||||||
.then((querySnapshot) => {
|
.then(async (querySnapshot) => {
|
||||||
let complete = [];
|
let complete = [];
|
||||||
let incomplete = [];
|
let incomplete = [];
|
||||||
|
let totalCorrect = 0;
|
||||||
|
let totalIncorrect = 0;
|
||||||
|
let totalMarks = 0;
|
||||||
|
let totalTime = 0;
|
||||||
|
let totalPercentage = 0;
|
||||||
|
let userMarkHistory = [];
|
||||||
|
|
||||||
querySnapshot.docs.map((doc) => {
|
querySnapshot.docs.map((doc) => {
|
||||||
const data = doc.data();
|
const data = doc.data();
|
||||||
@@ -52,14 +66,24 @@ export default class History extends Component {
|
|||||||
id: doc.id,
|
id: doc.id,
|
||||||
setTitle: data.set_title,
|
setTitle: data.set_title,
|
||||||
switchLanguage: data.switch_language,
|
switchLanguage: data.switch_language,
|
||||||
percentage: (data.progress / data.questions.length * 100).toFixed(2),
|
percentageProgress: (data.progress / data.questions.length * 100).toFixed(2),
|
||||||
mark: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
|
grade: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
correct: data.correct.length,
|
correct: data.correct.length,
|
||||||
progress: data.progress,
|
progress: data.progress,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
totalCorrect += data.correct.length;
|
||||||
|
totalIncorrect += data.incorrect.length;
|
||||||
|
totalMarks += data.progress;
|
||||||
|
|
||||||
if (data.duration !== null) {
|
if (data.duration !== null) {
|
||||||
|
totalPercentage += (data.correct.length / data.questions.length * 100);
|
||||||
|
totalTime += data.duration;
|
||||||
|
userMarkHistory.push({
|
||||||
|
x: new Date(data.start_time),
|
||||||
|
y: (data.correct.length / data.questions.length * 100),
|
||||||
|
});
|
||||||
return complete.push(pushData);
|
return complete.push(pushData);
|
||||||
} else {
|
} else {
|
||||||
return incomplete.push(pushData);
|
return incomplete.push(pushData);
|
||||||
@@ -69,6 +93,14 @@ export default class History extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
progressHistoryComplete: complete,
|
progressHistoryComplete: complete,
|
||||||
progressHistoryIncomplete: incomplete,
|
progressHistoryIncomplete: incomplete,
|
||||||
|
totalCorrect: totalCorrect,
|
||||||
|
totalIncorrect: totalIncorrect,
|
||||||
|
totalMarks: totalMarks,
|
||||||
|
totalTime: totalTime,
|
||||||
|
totalPercentage: totalPercentage,
|
||||||
|
totalCompleteTests: complete.length,
|
||||||
|
userMarkHistory: userMarkHistory,
|
||||||
|
personalSetsCount: (await userSets).docs.length,
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log(`Couldn't retrieve progress history: ${error}`);
|
console.log(`Couldn't retrieve progress history: ${error}`);
|
||||||
@@ -93,6 +125,18 @@ export default class History extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msToTime = (time) => {
|
||||||
|
const localeData = {
|
||||||
|
minimumIntegerDigits: 2,
|
||||||
|
useGrouping: false,
|
||||||
|
};
|
||||||
|
const seconds = Math.floor((time / 1000) % 60).toLocaleString("en-GB", localeData);
|
||||||
|
const minutes = Math.floor((time / 1000 / 60) % 60).toLocaleString("en-GB", localeData);
|
||||||
|
const hours = Math.floor(time / 1000 / 60 / 60).toLocaleString("en-GB", localeData);
|
||||||
|
|
||||||
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -100,10 +144,42 @@ export default class History extends Component {
|
|||||||
<main>
|
<main>
|
||||||
<h1>History</h1>
|
<h1>History</h1>
|
||||||
|
|
||||||
|
<div className="history-sections">
|
||||||
|
<div className="historical-user-stats-container">
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{this.state.totalCorrect}</h1>
|
||||||
|
<p>correct</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{this.state.totalIncorrect}</h1>
|
||||||
|
<p>incorrect</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{`${(this.state.totalPercentage / this.state.totalCompleteTests).toFixed(2)}%`}</h1>
|
||||||
|
<p>average</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{this.msToTime(this.state.totalTime)}</h1>
|
||||||
|
<p>total time</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{this.state.totalCompleteTests}</h1>
|
||||||
|
<p>tests completed</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-row stat-row--inline">
|
||||||
|
<h1>{this.state.personalSetsCount}</h1>
|
||||||
|
<p>personal set{ this.state.personalSetsCount > 1 && "s"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ this.state.userMarkHistory && this.state.userMarkHistory.length > 1 &&
|
||||||
|
<LineChart data={this.state.userMarkHistory} />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
this.state.progressHistoryComplete.length > 0 || this.state.progressHistoryIncomplete.length > 0
|
this.state.progressHistoryComplete.length > 0 || this.state.progressHistoryIncomplete.length > 0
|
||||||
?
|
?
|
||||||
<>
|
<div>
|
||||||
{
|
{
|
||||||
this.state.progressHistoryIncomplete.length > 0 &&
|
this.state.progressHistoryIncomplete.length > 0 &&
|
||||||
<div className="progress-history-container">
|
<div className="progress-history-container">
|
||||||
@@ -127,9 +203,9 @@ export default class History extends Component {
|
|||||||
<SwapHorizRoundedIcon />
|
<SwapHorizRoundedIcon />
|
||||||
}
|
}
|
||||||
</Link>
|
</Link>
|
||||||
<p>{progressItem.percentage}%</p>
|
<p>{progressItem.percentageProgress}%</p>
|
||||||
<p>{progressItem.correct}/{progressItem.progress}</p>
|
<p>{progressItem.correct}/{progressItem.progress}</p>
|
||||||
<p>{progressItem.mark}%</p>
|
<p>{progressItem.grade}%</p>
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
progressItem.mode === "questions"
|
progressItem.mode === "questions"
|
||||||
@@ -172,9 +248,9 @@ export default class History extends Component {
|
|||||||
<SwapHorizRoundedIcon />
|
<SwapHorizRoundedIcon />
|
||||||
}
|
}
|
||||||
</Link>
|
</Link>
|
||||||
<p>{progressItem.percentage}%</p>
|
<p>{progressItem.percentageProgress}%</p>
|
||||||
<p>{progressItem.correct}/{progressItem.progress}</p>
|
<p>{progressItem.correct}/{progressItem.progress}</p>
|
||||||
<p>{progressItem.mark}%</p>
|
<p>{progressItem.grade}%</p>
|
||||||
{
|
{
|
||||||
progressItem.mode === "questions"
|
progressItem.mode === "questions"
|
||||||
?
|
?
|
||||||
@@ -187,10 +263,13 @@ export default class History extends Component {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</div>
|
||||||
:
|
:
|
||||||
|
<div>
|
||||||
<p>You haven't done any tests yet.</p>
|
<p>You haven't done any tests yet.</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
91
src/LineChart.js
Normal file
91
src/LineChart.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
|
||||||
|
export default function LineChart (props) {
|
||||||
|
const options = {
|
||||||
|
xaxis: {
|
||||||
|
type: "datetime",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
labels: {
|
||||||
|
formatter: (value) => `${value.toFixed(0)}%`
|
||||||
|
},
|
||||||
|
tickAmount: 5,
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
foreColor:
|
||||||
|
getComputedStyle(
|
||||||
|
document.querySelector("#root > div")
|
||||||
|
).getPropertyValue("--text-color")
|
||||||
|
.trim(),
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
fontFamily: "Hind, sans-serif",
|
||||||
|
offsetX: -15,
|
||||||
|
zoom: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: [
|
||||||
|
getComputedStyle(
|
||||||
|
document.querySelector("#root > div")
|
||||||
|
).getPropertyValue("--primary-color")
|
||||||
|
.trim()
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
theme: "dark",
|
||||||
|
x: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: 3,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: getComputedStyle(
|
||||||
|
document.querySelector("#root > div")
|
||||||
|
).getPropertyValue("--overlay-color")
|
||||||
|
.trim(),
|
||||||
|
xaxis: {
|
||||||
|
lines: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
markers: {
|
||||||
|
size: 1
|
||||||
|
},
|
||||||
|
responsive: [{
|
||||||
|
breakpoint: 600,
|
||||||
|
options: {
|
||||||
|
chart: {
|
||||||
|
height: "200px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
data: props.data,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Chart
|
||||||
|
options={options}
|
||||||
|
series={series}
|
||||||
|
type="line"
|
||||||
|
height="250px"
|
||||||
|
className="chart"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ import LinkButton from "./LinkButton";
|
|||||||
import Error404 from "./Error404";
|
import Error404 from "./Error404";
|
||||||
import SettingsContent from "./SettingsContent";
|
import SettingsContent from "./SettingsContent";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
import LineChart from './LineChart';
|
||||||
|
|
||||||
import "./css/PopUp.css";
|
import "./css/PopUp.css";
|
||||||
import "./css/Progress.css";
|
import "./css/Progress.css";
|
||||||
|
import "./css/Chart.css";
|
||||||
|
|
||||||
export default withRouter(class Progress extends React.Component {
|
export default withRouter(class Progress extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -60,6 +62,7 @@ export default withRouter(class Progress extends React.Component {
|
|||||||
themeInput: this.props.theme,
|
themeInput: this.props.theme,
|
||||||
setIds: [],
|
setIds: [],
|
||||||
attemptNumber: 1,
|
attemptNumber: 1,
|
||||||
|
attemptHistory: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -145,6 +148,12 @@ export default withRouter(class Progress extends React.Component {
|
|||||||
.get()
|
.get()
|
||||||
.then((querySnapshot) => {
|
.then((querySnapshot) => {
|
||||||
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
||||||
|
if (newState.attemptNumber > 1)
|
||||||
|
newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null)
|
||||||
|
.map((doc) => ({
|
||||||
|
x: new Date(doc.data().start_time),
|
||||||
|
y: (doc.data().correct.length / doc.data().questions.length * 100),
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (incorrectAnswers.length > 0) {
|
if (incorrectAnswers.length > 0) {
|
||||||
@@ -182,6 +191,11 @@ export default withRouter(class Progress extends React.Component {
|
|||||||
this.setState(newState, () => {
|
this.setState(newState, () => {
|
||||||
if (!setDone) this.answerInput.focus()
|
if (!setDone) this.answerInput.focus()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.props.logEvent("select_content", {
|
||||||
|
content_type: "progress",
|
||||||
|
item_id: this.props.match.params.progressId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -310,8 +324,13 @@ export default withRouter(class Progress extends React.Component {
|
|||||||
.orderBy("start_time")
|
.orderBy("start_time")
|
||||||
.get()
|
.get()
|
||||||
.then((querySnapshot) => {
|
.then((querySnapshot) => {
|
||||||
console.log(querySnapshot);
|
|
||||||
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
||||||
|
if (newState.attemptNumber > 1)
|
||||||
|
newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null)
|
||||||
|
.map((doc) => ({
|
||||||
|
x: new Date(doc.data().start_time),
|
||||||
|
y: (doc.data().correct.length / doc.data().questions.length * 100),
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +536,13 @@ export default withRouter(class Progress extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{/* TODO: provide the attempt number -- .get() where array-contains-all array of setIds from original sets? would mean a new field in db and adjusting cloud fns again */}
|
|
||||||
|
{this.state.attemptNumber > 1 &&
|
||||||
|
<>
|
||||||
|
<h2 className="chart-title">History</h2>
|
||||||
|
<LineChart data={this.state.attemptHistory} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="progress-end-button-container">
|
<div className="progress-end-button-container">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
|||||||
Reference in New Issue
Block a user