Skip to content

Commit

Permalink
Data Diagrams split between accepted, rejected, notAssessed (#170)
Browse files Browse the repository at this point in the history
* adding tabs selector to switch between all applications and accepted applications

* fixing diagram

* set corner radius

* redesigning to stacked diagrams and code cleaning
  • Loading branch information
niclasheun authored Feb 14, 2025
1 parent 95d35ae commit 40aa412
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 190 deletions.
Original file line number Diff line number Diff line change
@@ -1,77 +1,51 @@
import { CalendarX, MailWarningIcon } from 'lucide-react'
import { MissingConfig, MissingConfigItem } from '@/components/MissingConfig'
import { MissingConfig } from '@/components/MissingConfig'
import { getIsApplicationConfigured } from '../../utils/getApplicationIsConfigured'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { ApplicationMetaData } from '../../interfaces/applicationMetaData'
import { ManagementPageHeader } from '@/components/ManagementPageHeader'
import { useParseApplicationMetaData } from '../../hooks/useParseApplicationMetaData'
import { useApplicationStore } from '../../zustand/useApplicationStore'
import { useLocation } from 'react-router-dom'
import { AssessmentDiagram } from './diagrams/AssessmentDiagram'
import { useMissingConfigs } from './hooks/useMissingConfig'
import { useHideMailingWarning } from './hooks/useHideMailingWarning'
import { ApplicationStatusCard } from './diagrams/ApplicationStatusCard'
import { AssessmentDiagram } from './diagrams/AssessmentDiagram'
import { ApplicationGenderDiagram } from './diagrams/ApplicationGenderDiagram'
import { ApplicationStudyBackgroundDiagram } from './diagrams/ApplicationStudyBackgroundDiagram'
import { ApplicationStudySemesterDiagram } from './diagrams/ApplicationStudySemesterDiagram'
import { ManagementPageHeader } from '@/components/ManagementPageHeader'
import { useParseApplicationMetaData } from '../../hooks/useParseApplicationMetaData'
import { useApplicationStore } from '../../zustand/useApplicationStore'
import { useHideMailingWarning } from './hooks/useHideMailingWarning'

export const ApplicationLandingPage = (): JSX.Element => {
const [applicationMetaData, setApplicationMetaData] = useState<ApplicationMetaData | null>(null)
const path = useLocation().pathname
const { pathname } = useLocation()
const { coursePhase, participations } = useApplicationStore()

useParseApplicationMetaData(coursePhase, setApplicationMetaData)
const { hideMailingWarning } = useHideMailingWarning()

const missingConfigs: MissingConfigItem[] = useMemo(() => {
const missingConfigItems: MissingConfigItem[] = []
if (!getIsApplicationConfigured(applicationMetaData)) {
missingConfigItems.push({
title: 'Application Phase Deadlines',
icon: CalendarX,
link: `${path}/configuration`,
})
}
if (
coursePhase?.restrictedData?.mailingSettings === undefined &&
!coursePhase?.restrictedData?.hideMailingWarning
) {
missingConfigItems.push({
title: 'Application Mailing Settings',
description: `This application phase has no mailing settings configured.
If you do not want to send mails, you can hide this warning.`,
icon: MailWarningIcon,
link: `${path}/mailing`,
hide: hideMailingWarning,
})
}
return missingConfigItems
}, [
const missingConfigs = useMissingConfigs(
applicationMetaData,
coursePhase?.restrictedData?.hideMailingWarning,
coursePhase?.restrictedData?.mailingSettings,
coursePhase,
pathname,
hideMailingWarning,
path,
])
)

useParseApplicationMetaData(coursePhase, setApplicationMetaData)

const isApplicationConfigured = getIsApplicationConfigured(applicationMetaData)

return (
<div>
<ManagementPageHeader>Application Administration</ManagementPageHeader>

<>
<MissingConfig elements={missingConfigs} />
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-6'>
<ApplicationStatusCard
applicationMetaData={applicationMetaData}
applicationPhaseIsConfigured={getIsApplicationConfigured(applicationMetaData)}
/>
<AssessmentDiagram applications={participations ?? []} />
<ApplicationGenderDiagram applications={participations ?? []} />
</div>
<div className='grid gap-6 md:grid-cols-1 lg:grid-cols-2 mb-6'>
<ApplicationStudyBackgroundDiagram applications={participations ?? []} />
<ApplicationStudySemesterDiagram applications={participations ?? []} />
</div>
</>
<MissingConfig elements={missingConfigs} />
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-6'>
<ApplicationStatusCard
applicationMetaData={applicationMetaData}
applicationPhaseIsConfigured={isApplicationConfigured}
/>
<AssessmentDiagram applications={participations} />
<ApplicationGenderDiagram applications={participations} />
</div>
<div className='grid gap-6 md:grid-cols-1 lg:grid-cols-2 mb-6'>
<ApplicationStudyBackgroundDiagram applications={participations} />
<ApplicationStudySemesterDiagram applications={participations} />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
import { Bar, BarChart, LabelList, XAxis, YAxis } from 'recharts'
import { useMemo } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { ApplicationParticipation } from '../../../interfaces/applicationParticipation'
import { Gender } from '@tumaet/prompt-shared-state'
import { useMemo } from 'react'

const chartConfig = {
female: {
label: 'Female',
color: 'hsl(var(--primary))',
},
male: {
label: 'Male',
color: 'hsl(var(--primary))',
},
diverse: {
label: 'Diverse',
color: 'hsl(var(--primary))',
},
prefer_not_to_say: {
label: 'Unknown',
color: 'hsl(var(--primary))',
},
} satisfies ChartConfig
import { Gender, getGenderString } from '@tumaet/prompt-shared-state'
import { StackedBarChartWithPassStatus } from './StackedBarChartWithPassStatus'

interface GenderDistributionCardProps {
applications: ApplicationParticipation[]
Expand All @@ -36,23 +11,39 @@ interface GenderDistributionCardProps {
export const ApplicationGenderDiagram = ({
applications,
}: GenderDistributionCardProps): JSX.Element => {
const { genderData } = useMemo(() => {
const genderCounts = applications.reduce(
(acc, app) => {
const gender = app.student.gender || 'prefer_not_to_say'
acc[gender] = (acc[gender] || 0) + 1
return acc
},
{} as Record<Gender, number>,
)
const genderData = useMemo(() => {
// Initialize counts for each gender
const initialCounts: Record<Gender, { passed: number; failed: number; not_assessed: number }> =
{
[Gender.MALE]: { passed: 0, failed: 0, not_assessed: 0 },
[Gender.FEMALE]: { passed: 0, failed: 0, not_assessed: 0 },
[Gender.DIVERSE]: { passed: 0, failed: 0, not_assessed: 0 },
[Gender.PREFER_NOT_TO_SAY]: { passed: 0, failed: 0, not_assessed: 0 },
}

// Aggregate counts by gender and passStatus
const countsByGender = applications.reduce((acc, app) => {
const gender: Gender = app.student.gender || Gender.PREFER_NOT_TO_SAY
acc[gender][app.passStatus] += 1
return acc
}, initialCounts)

// Map the counts to diagram data for the chart
const diagramData = (Object.values(Gender) as Gender[]).map((gender) => {
const accepted = countsByGender[gender]?.passed ?? 0
const rejected = countsByGender[gender]?.failed ?? 0
const notAssessed = countsByGender[gender]?.not_assessed ?? 0

const data = Object.entries(chartConfig).map(([gender, config]) => ({
gender: config.label,
Students: genderCounts[gender as Gender] || 0,
fill: config.color,
}))
return {
gender: gender !== Gender.PREFER_NOT_TO_SAY ? getGenderString(gender) : 'Unknown',
accepted,
rejected,
notAssessed,
total: accepted + rejected + notAssessed,
}
})

return { genderData: data }
return diagramData
}, [applications])

return (
Expand All @@ -62,27 +53,7 @@ export const ApplicationGenderDiagram = ({
<CardDescription>Breakdown of student genders</CardDescription>
</CardHeader>
<CardContent className='flex-1 flex flex-col justify-end pb-0'>
<ChartContainer config={chartConfig} className='mx-auto w-full h-[280px]'>
<BarChart data={genderData} margin={{ top: 30, right: 10, bottom: 0, left: 10 }}>
<XAxis
dataKey='gender'
axisLine={false}
tickLine={false}
tick={{ fontSize: 12 }}
interval={0}
height={50}
tickFormatter={(value) => value.split(' ').join('\n')}
/>
<YAxis hide />
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Bar dataKey='Students' radius={[4, 4, 0, 0]}>
{genderData.map((entry, index) => (
<Bar key={`bar-${index}`} dataKey='Students' fill={entry.fill} />
))}
<LabelList position='top' offset={10} className='fill-foreground' fontSize={12} />
</Bar>
</BarChart>
</ChartContainer>
<StackedBarChartWithPassStatus data={genderData} dataKey='gender' />
</CardContent>
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { Bar, BarChart, LabelList, XAxis, YAxis } from 'recharts'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { ChartConfig, ChartContainer, ChartTooltip } from '@/components/ui/chart'
import { ApplicationParticipation } from '../../../interfaces/applicationParticipation'
import { useMemo } from 'react'
import translations from '@/lib/translations.json'
import { StackedBarChartWithPassStatus } from './StackedBarChartWithPassStatus'

const programsWithOther = translations.university.studyPrograms.concat('Other')
const studyProgramsConfig = programsWithOther.reduce((acc, program) => {
acc[program] = {
label: translations.university.studyProgramShortNames[program] || program,
color: 'hsl(var(--primary))',
}
return acc
}, {} as ChartConfig)

interface StudyBackgroundCardProps {
applications: ApplicationParticipation[]
Expand All @@ -21,28 +13,40 @@ interface StudyBackgroundCardProps {
export const ApplicationStudyBackgroundDiagram = ({
applications,
}: StudyBackgroundCardProps): JSX.Element => {
const { studyData } = useMemo(() => {
const studyPrograms = translations.university.studyPrograms
const studyData = useMemo(() => {
// Use the complete list of programs including 'Other'
const allPrograms = programsWithOther

const programCounts = applications.reduce(
// Count each pass status per program, initializing the count object when needed.
const countsByProgram = applications.reduce(
(acc, app) => {
const program = studyPrograms.includes(app.student.studyProgram || '')
? app.student.studyProgram
: 'Other'
acc[program!] = (acc[program!] || 0) + 1
const program = app.student.studyProgram || 'Other'
if (!acc[program]) {
acc[program] = { passed: 0, failed: 0, not_assessed: 0 }
}
acc[program][app.passStatus] = (acc[program][app.passStatus] || 0) + 1
return acc
},
{} as Record<string, number>,
{} as Record<string, { passed: number; failed: number; not_assessed: number }>,
)

const data = Object.entries(studyProgramsConfig).map(([program, config]) => ({
program: config.label,
fullName: program,
Students: programCounts[program] || 0,
fill: config.color,
}))

return { studyData: data }
// Map over all programs (including "Other") to create the data for the chart.
const data = allPrograms.map((program) => {
const accepted = countsByProgram[program]?.passed ?? 0
const rejected = countsByProgram[program]?.failed ?? 0
const notAssessed = countsByProgram[program]?.not_assessed ?? 0

return {
program,
shortTitle: translations.university.studyProgramShortNames[program] || program,
accepted,
rejected,
notAssessed,
total: accepted + rejected + notAssessed,
}
})

return data
}, [applications])

return (
Expand All @@ -52,52 +56,8 @@ export const ApplicationStudyBackgroundDiagram = ({
<CardDescription>Breakdown of student study programs</CardDescription>
</CardHeader>
<CardContent className='flex-1 flex flex-col justify-end pb-0'>
<ChartContainer config={studyProgramsConfig} className='mx-auto w-full h-[280px]'>
<BarChart data={studyData} margin={{ top: 30, right: 10, bottom: 0, left: 10 }}>
<XAxis
dataKey='program'
axisLine={false}
tickLine={false}
tick={{ fontSize: 12 }}
interval={0}
height={50}
tickFormatter={(value) => value.split(' ').join('\n')}
/>
<YAxis hide />
<ChartTooltip cursor={false} content={<CustomTooltipContent />} />
<Bar dataKey='Students' radius={[4, 4, 0, 0]}>
{studyData.map((entry, index) => (
<Bar key={`bar-${index}`} dataKey='Students' fill={entry.fill} />
))}
<LabelList position='top' offset={10} className='fill-foreground' fontSize={12} />
</Bar>
</BarChart>
</ChartContainer>
<StackedBarChartWithPassStatus data={studyData} dataKey='shortTitle' />
</CardContent>
</Card>
)
}

interface CustomTooltipContentProps {
active?: boolean
payload?: Array<{
value: number
payload: {
fullName: string
Students: number
}
}>
}

const CustomTooltipContent = ({ active, payload }: CustomTooltipContentProps) => {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className='bg-background border border-border rounded-lg shadow-md p-2'>
<p className='font-semibold'>{data.fullName}</p>
<p>Students: {data.Students}</p>
</div>
)
}
return null
}
Loading

0 comments on commit 40aa412

Please sign in to comment.