Skip to content

Commit

Permalink
style (frontend): Restyle grade analyzer component
Browse files Browse the repository at this point in the history
  • Loading branch information
TijnvdK committed Oct 19, 2024
1 parent d07a778 commit 3a3e39a
Show file tree
Hide file tree
Showing 8 changed files with 529 additions and 224 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ReactElement } from 'react';

import { GradeCorrelation } from './grade-correlation';
import { PassRate } from './pass-rate';
import { useQuery } from '@tanstack/react-query';
import type { CompareParams, CompareTitles } from '@/components/pages/admin/analyzer/analyzer';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { getCompareGrades } from '@/api/grades';
import { Tabs, type TabsProps } from 'antd';
import QueryLoading from '@/components/particles/QueryLoading';

export default function ComparisonView({
compareParams,
compareTitles,
}: {
compareParams: CompareParams;
compareTitles: CompareTitles;
}): ReactElement {
const {
data: gradesA,
isError: isErrorA,
isLoading: isLoadingA,
} = useQuery({
queryKey: ['g:' + Number(compareParams.tileAId) + compareParams.tileAType],
queryFn: async () => await getCompareGrades(Number(compareParams.tileAId), compareParams.tileAType),
enabled: Number(compareParams.tileAId) !== -1,
refetchOnWindowFocus: false,
});

const {
data: gradesB,
isError: isErrorB,
isLoading: isLoadingB,
} = useQuery({
queryKey: ['g:' + compareParams.tileBId + compareParams.tileBType],
queryFn: async () => await getCompareGrades(Number(compareParams.tileBId), compareParams.tileBType),
enabled: Number(compareParams.tileBId) !== -1,
refetchOnWindowFocus: false,
});

const items: TabsProps['items'] = [
{
key: 'correlation',
label: 'Grade correlation',
children:
isErrorA || isErrorB ?
<div className='flex flex-col items-center justify-center gap-2'>
<ExclamationCircleOutlined className='h-12 w-12 text-failure' />
<i className='text-base text-failure'>Error: Failed to retrieve the grades.</i>
</div>
: <GradeCorrelation gradesA={gradesA ?? []} gradesB={gradesB ?? []} compareTitles={compareTitles} />,
},
{
key: 'passRate',
label: 'Pass rate',
children:
isErrorA || isErrorB ?
<div className='flex flex-col items-center justify-center gap-2'>
<ExclamationCircleOutlined className='h-12 w-12 text-failure' />
<i className='text-base text-failure'>Error: Failed to retrieve the grades.</i>
</div>
: <PassRate gradesA={gradesA ?? []} gradesB={gradesB ?? []} compareTitles={compareTitles} />,
},
];

return (
<QueryLoading isLoading={isLoadingA || isLoadingB}>
<Tabs className='course-selection-tabs' defaultActiveKey='correlation' items={items} />
</QueryLoading>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { ReactElement } from 'react';

import { CorrelationGraph } from './graphs/correlation-graph';
import { varFixed, type UserGrade } from '@/types/grades';
import type { CompareTitles } from '@/components/pages/admin/analyzer/analyzer';
import { Card } from 'antd';

function CorrelationStatistics({ gradeData }: { gradeData: GradeData[] }): ReactElement {
let sumX = 0;
let sumY = 0;
let sumXY = 0;
let sumXX = 0;
let sumYY = 0;
let minX = 100;
let minY = 100;
let maxX = 0;
let maxY = 0;

const dataCount = gradeData.length;

gradeData.forEach(({ x, y }) => {
sumX += x;
sumY += y;
sumXY += x * y;
sumXX += x * x;
sumYY += y * y;
minX = minX > x ? x : minX;
minY = minY > y ? y : minY;
maxX = maxX < x ? x : maxX;
maxY = maxY < y ? y : maxY;
});

const correlationCoefficient =
(dataCount * sumXY - sumX * sumY) /
(Math.sqrt(dataCount * sumXX - sumX * sumX) * Math.sqrt(dataCount * sumYY - sumY * sumY));
const averageX = sumX / dataCount;
const averageY = sumY / dataCount;
const stdDevX = Math.sqrt(
gradeData.reduce((sum, { x }) => sum + (x - averageX) * (x - averageX), 0) / (dataCount - 1),
);
const stdDevY = Math.sqrt(
gradeData.reduce((sum, { y }) => sum + (y - averageY) * (y - averageY), 0) / (dataCount - 1),
);

return (
<Card size='small' title={<h3 className='text-base text-text'>Correlation statistics</h3>}>
<div className='[&>p]:text-sm'>
<p>Correlation Coefficient: {correlationCoefficient.toFixed(5)}</p>
<p>Average X: {varFixed(averageX)}</p>
<p>Average Y: {varFixed(averageY)}</p>
<p>Data Count (size): {dataCount}</p>
<p>Standard Deviation X: {varFixed(stdDevX)}</p>
<p>Standard Deviation Y: {varFixed(stdDevY)}</p>
<p>Minimum X: {varFixed(minX)}</p>
<p>Minimum Y: {varFixed(minY)}</p>
<p>Maximum X: {varFixed(maxX)}</p>
<p>Maximum Y: {varFixed(maxY)}</p>
</div>
</Card>
);
}

interface GradeCorrelationProps {
gradesA: UserGrade[];
gradesB: UserGrade[];
compareTitles: CompareTitles;
}

interface GradeData {
x: number;
y: number;
}

function GradeCorrelation({ gradesA, gradesB, compareTitles }: GradeCorrelationProps): ReactElement {
const gradeData: GradeData[] = gradesA
.map((gradeA) => {
const correspondingGradeB = gradesB.find((gradeB) => gradeB.userID === gradeA.userID);
return correspondingGradeB ? { x: gradeA.grade, y: correspondingGradeB.grade } : undefined;
})
.filter((gradePair) => gradePair !== undefined);

return (
<div className='flex flex-col justify-between gap-8 lg:flex-row'>
<div className='w-fit shrink-0'>
{gradeData.length === 0 ?
<Card size='small' title={<h3 className='text-base'>Correlation statistics</h3>} className='flex-1'>
<p className='text-destructive max-w-60 text-sm'>
No comparison possible. This might be because one of the two tiles or entries have no grades.
</p>
</Card>
: <CorrelationStatistics gradeData={gradeData} />}
</div>
<div className='flex-1'>
<CorrelationGraph gradeData={gradeData} compareTitles={compareTitles} />
</div>
</div>
);
}

export type { GradeCorrelationProps, GradeData };
export { GradeCorrelation };
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ReactElement } from 'react';
import { CartesianGrid, Label, ResponsiveContainer, Scatter, ScatterChart, XAxis, YAxis } from 'recharts';
import type { GradeData } from '../grade-correlation';
import type { CompareTitles } from '@/components/pages/admin/analyzer/analyzer';

export function CorrelationGraph({
gradeData,
compareTitles,
}: {
gradeData: GradeData[];
compareTitles: CompareTitles;
}): ReactElement {
return (
<ResponsiveContainer className='h-80 w-full min-w-80'>
<ScatterChart margin={{ top: 10, right: 0, bottom: 20, left: 20 }}>
<CartesianGrid />
<XAxis type='number' dataKey='x' name={compareTitles.a}>
<Label value={compareTitles.a} position='bottom' offset={0} />
</XAxis>
<YAxis type='number' dataKey='y' name={compareTitles.b}>
<Label value={compareTitles.b} angle={270} position='left' offset={0} />
</YAxis>
<Scatter data={gradeData} fill='hsl(var(--primary))' />
</ScatterChart>
</ResponsiveContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { UserGrade } from '@/types/grades';
import type { ReactElement } from 'react';
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis } from 'recharts';

export function PassRateBarGraph({ grades }: { grades: UserGrade[] }): ReactElement {
const chartData = Array.from({ length: 11 }, (_, i) => ({
grade: i,
failed: 0,
passed: 0,
}));

grades.forEach(({ grade }) => {
const viewingGrade = Math.round(grade / 10);
const existingGrade = chartData.find((data) => data.grade === Math.round(viewingGrade));
if (existingGrade) {
if (viewingGrade < 5.5) {
existingGrade.failed += 1;
} else {
existingGrade.passed += 1;
}
} else {
chartData.push({
grade: viewingGrade,
failed: viewingGrade < 5.5 ? 1 : 0,
passed: viewingGrade >= 5.5 ? 1 : 0,
});
}
});

return (
<ResponsiveContainer className='min-h-44 flex-1 flex-grow'>
<BarChart accessibilityLayer data={chartData} margin={{ top: 10, right: 0, bottom: 0, left: 0 }}>
<CartesianGrid vertical={false} />
<XAxis axisLine dataKey='grade' interval={0} tickLine tickMargin={10} />
<Bar dataKey='failed' stackId='a' fill='hsl(var(--failure))' radius={[0, 0, 4, 4]} />
<Bar dataKey='passed' stackId='a' fill='hsl(var(--success))' radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ReactElement } from 'react';

import { PassRateBarGraph } from './graphs/pass-rate-bar-graph';
import type { GradeCorrelationProps } from './grade-correlation';
import type { UserGrade } from '@/types/grades';
import { Card } from 'antd';

function Statistics({ grades }: { grades: UserGrade[] }): ReactElement {
const sortedGrades = grades.sort((a, b) => a.grade - b.grade);
const averageGrade = sortedGrades.reduce((acc, grade) => acc + grade.grade, 0) / sortedGrades.length;
const passRate = (grades.filter((grade) => grade.grade >= 55).length / grades.length) * 100;

return (
<Card size='small' title={<h3 className='text-base text-text'>Pass rate statistics</h3>}>
{grades.length === 0 ?
<p className='text-muted-foreground text-sm'>No grades available</p>
: <div className='[&>p]:text-sm'>
<p>Minium grade: {sortedGrades[0]?.grade.toFixed(1)}</p>
<p>Average grade: {averageGrade.toFixed(1)}</p>
<p>Maximum grade: {sortedGrades[sortedGrades.length - 1]?.grade.toFixed(1)}</p>
<p>Pass rate: {passRate.toFixed(1)}%</p>
</div>
}
</Card>
);
}

export function PassRate({ gradesA, gradesB, compareTitles }: GradeCorrelationProps): ReactElement {
return (
<div className='flex w-full flex-col gap-8 lg:flex-row'>
<Card
size='small'
title={
<h2 className='text-lg'>
{compareTitles.a.length === 0 ?
<span className='text-destructive'>No title found</span>
: compareTitles.a}
</h2>
}
className='flex-1'
>
<div className='flex w-full flex-wrap gap-8'>
<Statistics grades={gradesA} />
<PassRateBarGraph grades={gradesA} />
</div>
</Card>

<Card
size='small'
title={
<h2 className='text-lg'>
{compareTitles.b.length === 0 ?
<span className='text-destructive'>No title found</span>
: compareTitles.b}
</h2>
}
className='flex-1'
>
<div className='flex w-full flex-wrap gap-8'>
<Statistics grades={gradesB} />
<PassRateBarGraph grades={gradesB} />
</div>
</Card>
</div>
);
}
Loading

0 comments on commit 3a3e39a

Please sign in to comment.