Add charts showing historic test scores

This commit is contained in:
2021-09-11 17:40:59 +01:00
parent f73f6d8986
commit c1b04b0736
3 changed files with 284 additions and 89 deletions

View File

@@ -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
View 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"
/>
</>
)
}

View File

@@ -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