Skip to content

Commit 33cea66

Browse files
authored
Merge pull request #4948 from UniversityOfHelsinkiCS/progress-chart-graduated-category
Progress chart graduated category
2 parents f0fc96f + fc75dfd commit 33cea66

File tree

13 files changed

+138
-52
lines changed

13 files changed

+138
-52
lines changed

cypress/e2e/Degree_programme_overview.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -398,30 +398,32 @@ describe('Degree programme overview', () => {
398398
it('Student progress data is shown correctly', () => {
399399
const years = getEmptyYears(true)
400400
const tableContents = [
401-
// [Year, All, < 30 credits, 30–60 credits, 60–90 credits, 90–120 credits, 120–150 credits, 150–180 credits, ≥ 180 credits]
402-
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
403-
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0],
404-
['2022 - 2023', 26, 9, 9, 4, 3, 0, 1, 0],
405-
['2021 - 2022', 38, 8, 6, 11, 8, 5, 0, 0],
406-
['2020 - 2021', 30, 2, 1, 3, 7, 4, 4, 9],
407-
['2019 - 2020', 35, 1, 0, 2, 1, 0, 4, 27],
408-
['2018 - 2019', 45, 0, 1, 1, 2, 0, 3, 38],
409-
['2017 - 2018', 47, 0, 1, 3, 0, 1, 2, 40],
410-
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
401+
// [Year, All, < 30 credits, 30–60 credits, 60–90 credits, 90–120 credits, 120–150 credits, 150–180 credits, ≥ 180 credits, Graduated]
402+
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
403+
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0, 0],
404+
['2022 - 2023', 26, 9, 8, 4, 3, 0, 0, 0, 2],
405+
['2021 - 2022', 38, 8, 4, 9, 8, 4, 0, 0, 5],
406+
['2020 - 2021', 30, 2, 1, 2, 5, 0, 0, 1, 19],
407+
['2019 - 2020', 35, 1, 0, 2, 1, 0, 0, 4, 27],
408+
['2018 - 2019', 45, 0, 1, 1, 2, 0, 0, 2, 39],
409+
['2017 - 2018', 47, 0, 1, 3, 0, 0, 1, 0, 42],
410+
['Total', 229, 28, 15, 21, 19, 4, 1, 7, 134],
411411
]
412412

413413
cy.checkTableStats(tableContents, 'study-programme-progress')
414414
})
415415

416416
it('Progress section', () => {
417+
cy.cs('programme-progress-bar-chart-section').scrollIntoView()
418+
417419
cy.cs('programme-progress-bar-chart-section')
418420
.should('contain', 'Less than 30 credits')
419421
.should('contain', '30–60 credits')
420422
.should('contain', 'At least 180 credits')
421-
.should('contain', '49.8%') // The percentage for total, at least 180 credits, to check that the graph renders
423+
.should('contain', '58.5%') // The percentage for total graduated, to check that the graph renders
422424

423-
cy.cs('programme-progress-bar-chart-section').contains('49.8%').trigger('mouseover', { force: true })
424-
cy.contains('At least 180 credits: 114')
425+
cy.cs('programme-progress-bar-chart-section').contains('58.5%').trigger('mouseover', { force: true })
426+
cy.contains('Graduated: 134')
425427
})
426428

427429
it('Average graduation times section', () => {

services/backend/src/routes/university.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ router.get('/allprogressstats', async (req: GetProgressStatsRequest, res: Respon
7171
master: {} as Record<string, number[]>,
7272
doctor: {} as Record<string, number[]>,
7373
},
74+
graduatedCount: {
75+
bachelor: {} as Record<string, number>,
76+
bachelorMaster: {} as Record<string, number>,
77+
master: {} as Record<string, number>,
78+
doctor: {} as Record<string, number>,
79+
},
7480
}
7581

7682
const unifyProgressStats = (progStats: number[][][]) => {
@@ -96,6 +102,14 @@ router.get('/allprogressstats', async (req: GetProgressStatsRequest, res: Respon
96102
if (!universityData.creditCounts[fieldName][year]) universityData.creditCounts[fieldName][year] = []
97103
universityData.creditCounts[fieldName][year].push(...facultyData.creditCounts[fieldName][year])
98104
}
105+
106+
for (const fieldName of degreeNames) {
107+
if (!facultyData.graduatedCount[fieldName] || Object.keys(facultyData.graduatedCount[fieldName]).length === 0)
108+
continue
109+
if (!universityData.graduatedCount[fieldName][year]) universityData.graduatedCount[fieldName][year] = 0
110+
universityData.graduatedCount[fieldName][year] += facultyData.graduatedCount[fieldName][year]
111+
}
112+
99113
const progStats = ['bachelorsProgStats', 'bcMsProgStats', 'mastersProgStats', 'doctoralProgStats'] as const
100114
for (const fieldName of progStats) {
101115
if (Object.keys(facultyData[fieldName] || {}).length === 0) continue

services/backend/src/services/faculty/facultyStudentProgress.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const combineFacultyStudentProgress = async (
107107
const statsOfProgrammes: Array<Awaited<ReturnType<typeof getStudyTrackStatsForStudyProgramme>>> = []
108108
const allDegreeProgrammes = await getDegreeProgrammesOfOrganization(rootOrgId, false)
109109
const creditCounts: Record<string, Record<string, number[]>> = {}
110+
const graduatedCount: Record<string, Record<string, number>> = {}
110111
const progressStats: Record<string, Record<string, number[][]>> = {}
111112
const yearlyTitles: Record<string, string[][]> = {}
112113

@@ -137,12 +138,14 @@ export const combineFacultyStudentProgress = async (
137138
}
138139
}
139140

140-
const updateCreditCounts = (
141+
const updateCreditAndGraduatedCount = (
141142
level: 'bachelor' | 'bcMs' | 'master' | 'doctor',
142-
creditCountsOfProgramme: Record<string, number[]>
143+
creditCountsOfProgramme: Record<string, number[]>,
144+
graduatedCountOfProgramme: Record<string, number>
143145
) => {
144146
if (!(level in creditCounts)) {
145147
creditCounts[level] = cloneDeep(creditCountsOfProgramme)
148+
graduatedCount[level] = cloneDeep(graduatedCountOfProgramme)
146149
} else {
147150
for (const year of Object.keys(creditCountsOfProgramme)) {
148151
if (!(year in creditCounts[level])) {
@@ -151,6 +154,10 @@ export const combineFacultyStudentProgress = async (
151154
creditCounts[level][year].push(...creditCountsOfProgramme[year])
152155
}
153156
}
157+
158+
for (const year of Object.keys(graduatedCountOfProgramme)) {
159+
graduatedCount[level][year] = cloneDeep(graduatedCountOfProgramme[year])
160+
}
154161
}
155162
}
156163

@@ -163,11 +170,11 @@ export const combineFacultyStudentProgress = async (
163170
const { degreeProgrammeType, progId } = programmeInfo
164171
const level = programmeTypes[degreeProgrammeType]
165172

166-
updateCreditCounts(level, stats.creditCounts)
173+
updateCreditAndGraduatedCount(level, stats.creditCounts, stats.graduatedCount)
167174
calculateProgressStats(level, stats.creditCounts, yearlyTitles, progressStats, progId)
168175

169176
if (level === 'master' && Object.keys(stats.creditCountsCombo).length > 0) {
170-
updateCreditCounts('bcMs', stats.creditCountsCombo)
177+
updateCreditAndGraduatedCount('bcMs', stats.creditCountsCombo, stats.graduatedCount)
171178
calculateProgressStats('bcMs', stats.creditCountsCombo, yearlyTitles, progressStats, progId)
172179
}
173180
}
@@ -180,6 +187,7 @@ export const combineFacultyStudentProgress = async (
180187
bcMsProgStats: progressStats.bcMs,
181188
doctoralProgStats: progressStats.doctor,
182189
creditCounts: {} as Record<string, Record<string, number[]>>,
190+
graduatedCount: {} as Record<string, Record<string, number>>,
183191
yearlyBachelorTitles: yearlyTitles.bachelor,
184192
yearlyBcMsTitles: yearlyTitles.bcMs,
185193
yearlyMasterTitles: yearlyTitles.master,
@@ -202,6 +210,14 @@ export const combineFacultyStudentProgress = async (
202210
}
203211
result.creditCounts[levelName][year] = creditCounts[level][year]
204212
}
213+
214+
for (const year of Object.keys(graduatedCount[level]).toSorted(sortByYear)) {
215+
const levelName = level === 'bcMs' ? 'bachelorMaster' : level
216+
if (!(levelName in result.graduatedCount)) {
217+
result.graduatedCount[levelName] = {}
218+
}
219+
result.graduatedCount[levelName][year] = graduatedCount[level][year]
220+
}
205221
}
206222

207223
return result

services/backend/src/services/studyProgramme/studyTrackStats.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,6 @@ const getMainStatsByTrackAndYear = async (
338338
if (!studyRightElement) {
339339
continue
340340
}
341-
if (!includeGraduated && studyRightElement.graduated) {
342-
continue
343-
}
344341

345342
const [hasTransferredFromProgramme, hasTransferredToProgramme] = hasTransferredFromOrToProgramme(
346343
studyRight,
@@ -378,15 +375,18 @@ const getMainStatsByTrackAndYear = async (
378375
}
379376

380377
const countAsBachelorMaster = doCombo && studyRight.extentCode === ExtentCode.BACHELOR_AND_MASTER
381-
if (countAsBachelorMaster) {
382-
const startedInBachelor = getStudyRightElementsWithPhase(studyRight, 1)[0]?.startDate
383-
if (startedInBachelor && years.includes(defineYear(startedInBachelor, true))) {
384-
creditCountsCombo[defineYear(startedInBachelor, true)].push(
385-
getCreditCount(studyRight.student.credits, startedInBachelor)
386-
)
378+
// TODO: FIXME: This could be written better
379+
if (!studyRightElement.graduated) {
380+
if (countAsBachelorMaster) {
381+
const startedInBachelor = getStudyRightElementsWithPhase(studyRight, 1)[0]?.startDate
382+
if (startedInBachelor && years.includes(defineYear(startedInBachelor, true))) {
383+
creditCountsCombo[defineYear(startedInBachelor, true)].push(
384+
getCreditCount(studyRight.student.credits, startedInBachelor)
385+
)
386+
}
387+
} else {
388+
creditCounts[startYear].push(getCreditCount(studyRight.student.credits, startedInProgramme))
387389
}
388-
} else {
389-
creditCounts[startYear].push(getCreditCount(studyRight.student.credits, startedInProgramme))
390390
}
391391
}
392392

@@ -410,6 +410,15 @@ const getMainStatsByTrackAndYear = async (
410410
}
411411
}
412412

413+
const graduatedCount: Record<string, number> = {}
414+
years.forEach(year => {
415+
if (!yearlyStats[year]) {
416+
graduatedCount[year] = 0
417+
} else {
418+
graduatedCount[year] = yearlyStats[year][studyProgramme].graduated
419+
}
420+
})
421+
413422
const { mainStatsByYear, mainStatsByTrack, otherCountriesCount } = combineStats(
414423
years,
415424
yearlyStats,
@@ -438,6 +447,7 @@ const getMainStatsByTrackAndYear = async (
438447
otherCountriesCount,
439448
creditCounts,
440449
creditCountsCombo,
450+
graduatedCount,
441451
graduationTimes: finalGraduationTimes,
442452
graduationTimesSecondProg: finalCombinedGraduationTimes,
443453
}
@@ -484,6 +494,7 @@ export const getStudyTrackStatsForStudyProgramme = async ({
484494
creditCounts: stats.creditCounts,
485495
creditCountsCombo: stats.creditCountsCombo,
486496
doCombo,
497+
graduatedCount: stats.graduatedCount,
487498
graduationTimes: stats.graduationTimes,
488499
graduationTimesSecondProg: stats.graduationTimesSecondProg,
489500
id: combinedProgramme ? `${studyProgramme}-${combinedProgramme}` : studyProgramme,

services/frontend/src/common/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,12 @@ export const getCreditCategories = (
316316
const limitBreaks = cumulative
317317
? creditCategoryArray.map(num => Math.round(minCredits + (num * (maxCredits - minCredits)) / creditCategoryAmount))
318318
: [15, 30, 45, 60].map(limit => limit * (timeDivision === TimeDivision.SEMESTER ? 0.5 : 1))
319-
return range(0, limitBreaks.length + 1).map(i => [limitBreaks[i - 1], limitBreaks[i]])
319+
const limits: (number[] | 'Graduated')[] = range(0, limitBreaks.length + 1).map(i => [
320+
limitBreaks[i - 1],
321+
limitBreaks[i],
322+
])
323+
limits.push('Graduated')
324+
return limits
320325
}
321326

322327
export const validateInputLength = (input: string, minLength: number) => input?.trim().length >= minLength

services/frontend/src/components/Faculties/FacultyProgress.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ export const FacultyProgress = ({
1919
progressStats: GetAllProgressStatsResponse | undefined
2020
}) => {
2121
const creditCounts = progressStats?.creditCounts
22+
const graduatedCount = progressStats?.graduatedCount
2223

23-
const bachelorStats = calculateStats(creditCounts?.bachelor, 180)
24-
const bachelorMasterStats = calculateStats(creditCounts?.bachelorMaster, faculty === 'H90' ? 360 : 300, 180, 7)
25-
const masterStats = calculateStats(creditCounts?.master, 120)
26-
const doctorStats = calculateStats(creditCounts?.doctor, 40, 0, 5)
24+
const bachelorStats = calculateStats(creditCounts?.bachelor, graduatedCount?.bachelor, 180)
25+
const bachelorMasterStats = calculateStats(
26+
creditCounts?.bachelorMaster,
27+
graduatedCount?.bachelorMaster,
28+
faculty === 'H90' ? 360 : 300,
29+
180,
30+
7
31+
)
32+
const masterStats = calculateStats(creditCounts?.master, graduatedCount?.master, 120)
33+
const doctorStats = calculateStats(creditCounts?.doctor, graduatedCount?.doctor, 40, 0, 5)
2734

2835
const sortKeys = (stats: Record<string, number[][]>) => {
2936
return sortProgrammeKeys(

services/frontend/src/components/PopulationDetails/CreditGainStats/CreditDistributionDevelopment.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ const hasGraduatedBeforeDate = (student, programme, date) => {
7575
return studyRightElement.graduated && date.isAfter(studyRightElement.endDate, 'day')
7676
}
7777

78-
const GRADUATED = Symbol('GRADUATED')
79-
8078
const getChartData = (
8179
students: any[],
8280
timeSlots: any[],
@@ -87,16 +85,14 @@ const getChartData = (
8785
) => {
8886
const programmeCredits = getTargetCreditsForProgramme(programme) + (combinedProgramme ? 180 : 0)
8987

90-
const limits: Array<number[] | typeof GRADUATED> = getCreditCategories(
88+
const limits: Array<number[] | 'Graduated'> = getCreditCategories(
9189
cumulative,
9290
timeDivision,
9391
programmeCredits,
9492
timeSlots.length,
9593
6
9694
)
97-
const colors = generateGradientColors(limits.length)
98-
99-
limits.push(GRADUATED)
95+
const colors = generateGradientColors(limits.length - 1)
10096
colors.push('#ddd') // Color for graduated (grey)
10197

10298
const data: { y: number; custom: { students: number[] } }[][] = limits.map(() =>
@@ -116,9 +112,9 @@ const getChartData = (
116112
const credits = studentCredits[studentIndex][timeSlotIndex]
117113

118114
const rangeIndex = hasGraduated
119-
? limits.indexOf(GRADUATED)
115+
? limits.indexOf('Graduated')
120116
: limits.findIndex(limit => {
121-
if (limit === GRADUATED) {
117+
if (limit === 'Graduated') {
122118
return false
123119
}
124120

@@ -144,7 +140,7 @@ const getChartData = (
144140
const limit = limits[limitN]
145141
let name
146142

147-
if (limit === GRADUATED) {
143+
if (limit === 'Graduated') {
148144
name = 'Graduated'
149145
} else {
150146
const [min, max] = limit

services/frontend/src/components/common/ProgressBarChart.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export const ProgressBarChart = ({
2929
return null
3030
}
3131

32-
const colors = generateGradientColors(Object.keys(data.stats).length)
32+
const colors = generateGradientColors(Object.keys(data.stats).length - 1)
33+
colors.push('#ddd') // For graduated students
3334
const dataWithColors: SeriesColumnOptions[] = Object.values(data.stats).map((series, index) => ({
3435
...series,
3536
color: colors[index],

services/frontend/src/pages/Faculties/StudentsByStartingYearTab/ProgressSection.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,30 @@ export const ProgressSection = ({
3939
)
4040
}
4141

42-
const bachelorStats = calculateStats(progressStats?.data?.creditCounts?.bachelor, 180)
42+
const bachelorStats = calculateStats(
43+
progressStats?.data?.creditCounts?.bachelor,
44+
progressStats?.data?.graduatedCount?.bachelor,
45+
180
46+
)
4347
const bachelorMasterStats = calculateStats(
4448
progressStats?.data?.creditCounts?.bachelorMaster,
49+
progressStats?.data?.graduatedCount?.bachelorMaster,
4550
faculty.code === 'H90' ? 360 : 300,
4651
180,
4752
7
4853
)
49-
const masterStats = calculateStats(progressStats?.data?.creditCounts?.master, 120)
50-
const doctorStats = calculateStats(progressStats?.data?.creditCounts?.doctor, 40, 0, 5)
54+
const masterStats = calculateStats(
55+
progressStats?.data?.creditCounts?.master,
56+
progressStats?.data?.graduatedCount?.master,
57+
120
58+
)
59+
const doctorStats = calculateStats(
60+
progressStats?.data?.creditCounts?.doctor,
61+
progressStats?.data?.graduatedCount?.doctor,
62+
40,
63+
0,
64+
5
65+
)
5166

5267
const hasNonZeroStats = stats => {
5368
const allValuesZero = values => values.every(value => parseFloat(value) === 0)

services/frontend/src/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ export const StudyTracksAndClassStatisticsTab = ({
8585

8686
const programmeCode = combinedProgramme ? `${studyProgramme}-${combinedProgramme}` : studyProgramme
8787

88-
const progressStats = calculateStats(studyTrackStats?.creditCounts, getTargetCreditsForProgramme(programmeCode))
88+
const progressStats = calculateStats(
89+
studyTrackStats?.creditCounts,
90+
studyTrackStats?.graduatedCount,
91+
getTargetCreditsForProgramme(programmeCode)
92+
)
8993
if (progressStats?.chartStats) {
9094
progressStats.chartStats.forEach(creditCategory => {
9195
const [total, ...years] = creditCategory.data
@@ -95,7 +99,11 @@ export const StudyTracksAndClassStatisticsTab = ({
9599

96100
const progressComboStats =
97101
Object.keys(studyTrackStats?.creditCountsCombo ?? {}).length > 0
98-
? calculateStats(studyTrackStats?.creditCountsCombo, getTargetCreditsForProgramme(programmeCode) + 180)
102+
? calculateStats(
103+
studyTrackStats?.creditCountsCombo,
104+
studyTrackStats?.graduatedCount,
105+
getTargetCreditsForProgramme(programmeCode) + 180
106+
)
99107
: null
100108

101109
if (progressComboStats?.chartStats) {

0 commit comments

Comments
 (0)