@@ -8,8 +8,9 @@ import { Badge } from "@/components/ui/badge"
88import { assignmentScoresQueryOptions } from "@/utils/queryOptions"
99
1010import { ChartContainer , type ChartConfig } from "@/components/ui/chart"
11- import { Bar , BarChart } from "recharts"
11+ import { Bar , BarChart , CartesianGrid } from "recharts"
1212import { useSuspenseQuery } from "@tanstack/react-query"
13+ import { cn } from "@/lib/utils"
1314
1415export function SectionList ( { sections } : { sections : Section [ ] } ) {
1516 return (
@@ -164,7 +165,11 @@ function AssignmentListElement({ assignment }: { assignment: Assignment }) {
164165 </ p >
165166 </ div >
166167 { assignment . submission_count > 0 && (
167- < AssignmentScoresHistogram assignmentId = { assignment . id } />
168+ < AssignmentScoresHistogram
169+ assignmentId = { assignment . id }
170+ className = "h-[40px] w-1/2 mx-auto"
171+ maxGrade = { assignment . max_score }
172+ />
168173 ) }
169174 < div className = "ml-auto font-medium mr-4 flex items-center gap-1" >
170175 { assignment . submission_count > 0 && (
@@ -202,26 +207,111 @@ const chartConfig = {
202207 } ,
203208} satisfies ChartConfig
204209
205- function AssignmentScoresHistogram ( { assignmentId } : { assignmentId : number } ) {
210+ export function AssignmentScoresHistogram ( {
211+ assignmentId,
212+ className,
213+ showXTicks = false , // existing optional prop
214+ showYTicks = false , // new optional prop
215+ filled = true ,
216+ maxGrade,
217+ } : {
218+ assignmentId : number
219+ className ?: string
220+ showXTicks ?: boolean
221+ showYTicks ?: boolean
222+ filled ?: boolean
223+ maxGrade ?: number
224+ } ) {
206225 const { data : scores } = useSuspenseQuery (
207226 assignmentScoresQueryOptions ( assignmentId )
208227 )
209- if ( ! scores ) {
210- return null
228+ // if scores is null or undefined, return null
229+ if ( ! scores ) return null
230+
231+ const scoresEmpty = scores . length === 0
232+ let scoreCountsData : { total : number ; score : number } [ ]
233+ let yTicks : number [ ] = [ ]
234+
235+ if ( scoresEmpty ) {
236+ // When there are no grades, use the provided maxGrade or a fallback of 10.
237+ const effectiveMaxScore = maxGrade ?? 10
238+ const bins = effectiveMaxScore + 1
239+ scoreCountsData = Array . from ( { length : bins } , ( _ , index ) => ( {
240+ total : 0 ,
241+ score : index ,
242+ } ) )
243+ // No yTicks when there are no grades
244+ yTicks = [ ]
245+ } else {
246+ const maxScore = Math . max ( ...scores )
247+ const bins = Math . min ( maxScore + 1 , 10 )
248+ const scoreCounts = Array . from ( { length : bins } , ( ) => 0 )
249+ scores . forEach ( ( score ) => {
250+ const bin = Math . floor ( ( score / maxScore ) * bins )
251+ scoreCounts [ bin ] = ( scoreCounts [ bin ] || 0 ) + 1
252+ } )
253+ scoreCountsData = scoreCounts . map ( ( total , index ) => ( {
254+ total,
255+ score : index ,
256+ } ) )
257+ // Compute Y ticks if enabled - using 5 tick marks (from max to 0)
258+ const maxCount = Math . max ( ...scoreCounts )
259+ const tickCount = 5
260+ yTicks = Array . from ( { length : tickCount } , ( _ , i ) =>
261+ Math . round ( ( maxCount * ( tickCount - i - 1 ) ) / ( tickCount - 1 ) )
262+ )
211263 }
212- // should be replaced with assignment.max_score
213- const maxScore = Math . max ( ...scores )
214- // should be replaced with (a max of) ten bins
215- const scoreCounts = Array . from ( { length : maxScore + 1 } , ( ) => 0 )
216- scores . forEach ( ( score ) => {
217- scoreCounts [ score ] += 1
218- } )
219- const scoreCountsData = scoreCounts . map ( ( total , score ) => ( { total, score } ) )
264+
220265 return (
221- < ChartContainer config = { chartConfig } className = "h-[40px] w-1/4 ml-auto" >
222- < BarChart accessibilityLayer data = { scoreCountsData } >
223- < Bar dataKey = "total" fill = "var(--color-total)" radius = { 4 } />
224- </ BarChart >
225- </ ChartContainer >
266+ < div className = { className } >
267+ { showYTicks && ! scoresEmpty ? (
268+ < div className = "flex h-full" >
269+ < div className = "flex flex-col justify-between pr-2" >
270+ { yTicks . map ( ( tick , i ) => (
271+ < span key = { i } className = "text-xs" >
272+ { tick }
273+ </ span >
274+ ) ) }
275+ </ div >
276+ < ChartContainer
277+ config = { chartConfig }
278+ className = { cn ( "h-[40px]" , className ) }
279+ >
280+ < BarChart accessibilityLayer data = { scoreCountsData } >
281+ < CartesianGrid vertical = { false } />
282+ < Bar
283+ dataKey = "total"
284+ fill = { chartConfig . total . color }
285+ opacity = { filled ? 1 : 0.5 }
286+ radius = { 4 }
287+ />
288+ </ BarChart >
289+ </ ChartContainer >
290+ </ div >
291+ ) : (
292+ < ChartContainer
293+ config = { chartConfig }
294+ className = { cn ( "h-[40px]" , className ) }
295+ >
296+ < BarChart accessibilityLayer data = { scoreCountsData } >
297+ { /* Show grid only if there are grades */ }
298+ { ! scoresEmpty && < CartesianGrid vertical = { false } /> }
299+ < Bar
300+ dataKey = "total"
301+ fill = { chartConfig . total . color }
302+ opacity = { filled ? 1 : 0.4 }
303+ radius = { 4 }
304+ />
305+ </ BarChart >
306+ </ ChartContainer >
307+ ) }
308+ { showXTicks && (
309+ < div className = "flex justify-between text-xs mt-2" >
310+ { scoreCountsData . map ( ( data ) => (
311+ < span key = { data . score } > { data . score } </ span >
312+ ) ) }
313+ </ div >
314+ ) }
315+ </ div >
226316 )
227317}
0 commit comments