-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
style (frontend): Restyle grade analyzer component
- Loading branch information
Showing
8 changed files
with
529 additions
and
224 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
IguideME.Web/Frontend/src/components/crystals/comparison-view/comparison-view.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
101 changes: 101 additions & 0 deletions
101
IguideME.Web/Frontend/src/components/crystals/comparison-view/grade-correlation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
27 changes: 27 additions & 0 deletions
27
IguideME.Web/Frontend/src/components/crystals/comparison-view/graphs/correlation-graph.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
40 changes: 40 additions & 0 deletions
40
IguideME.Web/Frontend/src/components/crystals/comparison-view/graphs/pass-rate-bar-graph.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
66 changes: 66 additions & 0 deletions
66
IguideME.Web/Frontend/src/components/crystals/comparison-view/pass-rate.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.