Skip to content

Commit

Permalink
feat: Dynamic inputs for grade distribution comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
mathhulk committed Nov 27, 2024
1 parent dc41ac3 commit 40af3f2
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 71 deletions.
205 changes: 134 additions & 71 deletions apps/frontend/src/app/GradeDistributions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";

import { useApolloClient } from "@apollo/client";
import { useSearchParams } from "react-router-dom";
import {
Bar,
BarChart,
Expand Down Expand Up @@ -64,93 +65,142 @@ import styles from "./GradeDistributions.module.scss";
// },
// ];

interface Input {
subject: string;
courseNumber: string;
year?: number;
semester?: Semester;
givenName?: string;
familyName?: string;
}

interface Output {
color: string;
gradeDistribution: GradeDistribution;
input: {
subject: string;
courseNumber: string;
number?: string;
year?: number;
semester?: Semester;
givenName?: string;
familyName?: string;
};
input: Input;
}

const input = [
{
subject: "COMPSCI",
courseNumber: "61B",
},
{
subject: "COMPSCI",
courseNumber: "61B",
number: "001",
year: 2024,
semester: "Spring",
},
{
subject: "COMPSCI",
courseNumber: "61B",
year: 2024,
semester: "Spring",
},
{
subject: "COMPSCI",
courseNumber: "61A",
year: 2024,
semester: "Spring",
givenName: "John",
familyName: "DeNero",
},
// {
// subject: "COMPSCI",
// courseNumber: "61A",
// givenName: "Joshua",
// familyName: "Hug",
// },
];
// const input = [
// {
// subject: "COMPSCI",
// courseNumber: "61B",
// },
// {
// subject: "COMPSCI",
// courseNumber: "61B",
// year: 2024,
// semester: "Spring",
// },
// {
// subject: "COMPSCI",
// courseNumber: "61B",
// year: 2024,
// semester: "Spring",
// },
// {
// subject: "COMPSCI",
// courseNumber: "61A",
// year: 2024,
// semester: "Spring",
// givenName: "John",
// familyName: "DeNero",
// },
// ];

export default function GradeDistributions() {
const client = useApolloClient();
const [searchParams] = useSearchParams();

const [loading, setLoading] = useState(false);
const [outputs, setOutputs] = useState<Output[] | null>(null);

const inputs = useMemo(
() =>
searchParams.getAll("input").reduce((acc, input) => {
const output = input.split(";");

const [output, setOutput] = useState<Output[] | null>(null);
// Filter out invalid inputs
if (output.length < 2) return acc;

// COMPSCI;61B
if (output.length < 4) {
const parsedInput: Input = {
subject: output[0],
courseNumber: output[1],
};

return acc.concat(parsedInput);
}

// Filter out invalid inputs
if (!["T", "P"].includes(output[2])) return acc;

// COMPSCI;61B;T;2024:Spring;John:DeNero, COMPSCI;61B;T;2024:Spring
const term = output[output[2] === "T" ? 3 : 4]?.split(":");

// COMPSCI;61B;P;John:DeNero;2024:Spring, COMPSCI;61B;P;John:DeNero
const professor = output[output[2] === "T" ? 4 : 3]?.split(":");

const parsedInput: Input = {
subject: output[0],
courseNumber: output[1],
year: parseInt(term?.[0]),
semester: term?.[1] as Semester,
familyName: professor?.[0],
givenName: professor?.[1],
};

return acc.concat(parsedInput);
}, [] as Input[]),
[searchParams]
);

const initialize = useCallback(async () => {
setLoading(true);

// TODO: Fetch course data

const responses = await Promise.all(
input.map((variables) =>
client.query<ReadGradeDistributionResponse>({
query: READ_GRADE_DISTRIBUTION,
variables,
})
)
inputs.map(async (variables) => {
try {
const response = await client.query<ReadGradeDistributionResponse>({
query: READ_GRADE_DISTRIBUTION,
variables,
});

return response;
} catch (error) {
// TODO: Handle errors

return null;
}
})
);

const output = responses.map(
(response, index) =>
({
color: colors[Math.floor(Math.random() * colors.length)],
gradeDistribution: response.data.grade,
input: input[index],
}) as Output
const output = responses.reduce(
(acc, response, index) =>
response
? acc.concat({
color: colors[Math.floor(Math.random() * colors.length)],
gradeDistribution: response!.data.grade,
input: inputs[index],
})
: acc,
[] as Output[]
);

setOutput(output);
setOutputs(output);

setLoading(false);
}, []);
}, [inputs]);

useEffect(() => {
initialize();
}, [initialize]);

const data = useMemo(
() =>
output?.reduce(
outputs?.reduce(
(acc, output, index) => {
output.gradeDistribution.distribution.forEach((grade) => {
const column = acc.find((item) => item.letter === grade.letter);
Expand All @@ -172,11 +222,9 @@ export default function GradeDistributions() {
[key: number]: number;
}[]
),
[output]
[outputs]
);

console.log(data);

return (
<div className={styles.root}>
<div className={styles.panel}></div>
Expand All @@ -187,7 +235,12 @@ export default function GradeDistributions() {
) : (
<div className={styles.view}>
<ResponsiveContainer width="100%" height={256}>
<BarChart width={730} height={250} data={data}>
<BarChart
syncId="grade-distributions"
width={730}
height={250}
data={data}
>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
Expand All @@ -201,13 +254,18 @@ export default function GradeDistributions() {
<YAxis />
<Legend />
<Tooltip />
{output?.map((_, index) => (
<Bar dataKey={index} fill={output[index].color} key={index} />
{outputs?.map((_, index) => (
<Bar dataKey={index} fill={outputs[index].color} key={index} />
))}
</BarChart>
</ResponsiveContainer>
<ResponsiveContainer width="100%" height={256}>
<LineChart width={730} height={250} data={data}>
<LineChart
syncId="grade-distributions"
width={730}
height={250}
data={data}
>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
Expand All @@ -221,10 +279,10 @@ export default function GradeDistributions() {
<YAxis />
<Legend />
<Tooltip />
{output?.map((_, index) => (
{outputs?.map((_, index) => (
<Line
dataKey={index}
stroke={output[index].color}
stroke={outputs[index].color}
key={index}
type="natural"
dot={false}
Expand All @@ -233,9 +291,14 @@ export default function GradeDistributions() {
</LineChart>
</ResponsiveContainer>
<div className={styles.grid}>
{output?.map((_, index) => (
{outputs?.map((_, index) => (
<ResponsiveContainer width="100%" height={256} key={index}>
<BarChart width={730} height={250} data={data}>
<BarChart
syncId="grade-distributions"
width={730}
height={250}
data={data}
>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
Expand All @@ -249,7 +312,7 @@ export default function GradeDistributions() {
<YAxis />
<Legend />
<Tooltip cursor={{ fill: "var(--backdrop-color)" }} />
<Bar dataKey={index} fill={output[index].color} />
<Bar dataKey={index} fill={outputs[index].color} />
</BarChart>
</ResponsiveContainer>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
}
}

body[data-theme="dark"] .overlay {
background: linear-gradient(to left, rgb(0 0 0 / 50%) 384px, transparent);
}

@media (prefers-color-scheme: dark) {
body:not([data-theme]) .overlay {
background: linear-gradient(to left, rgb(0 0 0 / 50%) 384px, transparent);
}
}

.content {
width: 384px;
border-left: 1px solid var(--border-color);
Expand Down
46 changes: 46 additions & 0 deletions apps/frontend/src/hooks/useDynamicYAxisWidth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCallback, useState } from "react";

const RECHART_CERTESIAN_AXIS_TICK_VALUE_SELECTOR = `.recharts-cartesian-axis-tick-value[orientation="left"],
.recharts-cartesian-axis-tick-value[orientation="right"]`;

const useDynamicYAxisWidth = (tickMargin = 12) => {
const [width, setWidth] = useState<number>();

const handleRef = useCallback(
(ref: unknown | null) => {
// @ts-expect-error - LineChart cannot be typed
if (!ref?.container) return;

// @ts-expect-error - LineChart cannot be typed
const tickValueElements = ref.container.querySelectorAll(
RECHART_CERTESIAN_AXIS_TICK_VALUE_SELECTOR
);

const highestWidth = [...tickValueElements]
.map((el) => {
const boundingRect = el.getBoundingClientRect();

if (!boundingRect?.width) return 0;

return boundingRect.width;
})
.reduce((accumulator, value) => {
if (accumulator < value) {
return value;
}

return accumulator;
}, 0);

setWidth(highestWidth + tickMargin);
},
[tickMargin]
);

return {
width,
handleRef,
};
};

export default useDynamicYAxisWidth;

0 comments on commit 40af3f2

Please sign in to comment.