Skip to content

Commit

Permalink
feat: learning objective completion
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Jun 21, 2024
1 parent 34d72a0 commit 33f6ba0
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 215 deletions.
2 changes: 1 addition & 1 deletion app/(authorized)/learning-objectives/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PageHeader from "@/components/PageHeader";
import GenericPageContainer from "@/components/GenericPageContainer";
import LearningObjectiveCompletion from "@/components/Visualizations/LearningObjectiveCompletion";
import LearningObjectiveCompletion from "@/components/LearningObjectiveCompletion";
import { getLearningObjectiveCompletion } from "@/lib/analytics-functions";

export default async function LearningObjectives() {
Expand Down
8 changes: 5 additions & 3 deletions components/CourseSettings/AssignmentExclusions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ const AssignmentExclusions: React.FC<AssignmentExclusionsProps> = ({
const [showSuccess, setShowSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const [availableItems, setAvailableItems] = useState<IDWithName[]>([]);
const [selectedItems, setSelectedItems] = useState<IDWithName[]>(
globalState.assignmentExclusions || []
);
const [selectedItems, setSelectedItems] = useState<IDWithName[]>([]);

useEffect(() => {
if (!globalState.courseID) return;
fetchAssignments();
}, [globalState.courseID]);

useEffect(() => {
setSelectedItems(globalState.assignmentExclusions || []);
}, [globalState.assignmentExclusions]);

async function fetchAssignments() {
try {
const data = await getAssignments(globalState.courseID, true);
Expand Down
8 changes: 5 additions & 3 deletions components/CourseSettings/FrameworkExclusions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ const FrameworkExclusions: React.FC<FrameworkExclusionsProps> = ({
const [showSuccess, setShowSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const [availableItems, setAvailableItems] = useState<IDWithText[]>([]);
const [selectedItems, setSelectedItems] = useState<IDWithText[]>(
globalState.frameworkExclusions || []
);
const [selectedItems, setSelectedItems] = useState<IDWithText[]>([]);

useEffect(() => {
if (!globalState.courseID) return;
fetchFrameworkDescriptors();
}, [globalState.courseID]);

useEffect(() => {
setSelectedItems(globalState.frameworkExclusions || []);
}, [globalState.frameworkExclusions]);

async function fetchFrameworkDescriptors() {
try {
const data = await getCourseFrameworkData(globalState.courseID);
Expand Down
2 changes: 1 addition & 1 deletion components/InstructorDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import ADAPTPerformance from "./Visualizations/ADAPTPerformance";
import InstructorDashboardControls from "./InstructorDashboardControls";
import GradeDistribution from "./Visualizations/GradeDistribution";
import ActivityAccessed from "./Visualizations/StudentActivity";
import LearningObjectiveCompletion from "./Visualizations/LearningObjectiveCompletion";
import LearningObjectiveCompletion from "./LearningObjectiveCompletion";
import TimeInReview from "./Visualizations/TimeInReview";
import TimeOnTask from "./Visualizations/TimeOnTask";

Expand Down
74 changes: 74 additions & 0 deletions components/LearningObjectiveCompletion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";
import {
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { LOCData, VisualizationBaseProps } from "@/lib/types";
import { useGlobalContext } from "@/state/globalContext";
import LearningObjectiveLevel from "./LearningObjectiveLevel";

type LOCProps = VisualizationBaseProps & {
getData: (course_id: string) => Promise<LOCData[]>;
};

const LearningObjectiveCompletion: React.FC<LOCProps> = ({
getData,
innerRef,
}) => {
useImperativeHandle(innerRef, () => ({
getSVG: () => svgRef.current,
}));

const [globalState] = useGlobalContext();
const containerRef = useRef<HTMLDivElement>(null);
const chartsRef = useRef<HTMLDivElement>(null);
const svgRef = useRef(null);
const [data, setData] = useState<LOCData[]>([]);
const [loading, setLoading] = useState(false);

const noFrameworkAlignment = useMemo(() => {
if (data.length === 0) return true;
return false;
}, [data]);

useEffect(() => {
if (!globalState.courseID) return;
handleGetData();
}, [globalState.courseID]);

async function handleGetData() {
try {
setLoading(true);
if (!globalState.courseID) return;

const _data = await getData(globalState.courseID);
setData(_data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}

return (
<div ref={containerRef}>
{noFrameworkAlignment && (
<div className="tw-flex tw-flex-row tw-justify-center">
<p>No questions have been aligned to frameworks in this course.</p>
</div>
)}
{!noFrameworkAlignment && (
<div className="tw-flex tw-flex-col">
{data.map((d, index) => (
<LearningObjectiveLevel key={crypto.randomUUID()} data={d} />
))}
</div>
)}
</div>
);
};

export default LearningObjectiveCompletion;
196 changes: 196 additions & 0 deletions components/LearningObjectiveLevel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { LOCData } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import { Accordion, Card } from "react-bootstrap";
import * as d3 from "d3";
import { DEFAULT_MARGINS, DEFAULT_WIDTH } from "@/utils/visualization-helpers";
import { ChevronRight, ChevronDown } from "react-bootstrap-icons";

const MARGIN = DEFAULT_MARGINS;
const DEFAULT_HEIGHT = 150;

interface LearningObjectiveLevelProps {
data: LOCData;
}

const LearningObjectiveLevel: React.FC<LearningObjectiveLevelProps> = ({
data,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const chartsRef = useRef<HTMLDivElement>(null);
const svgRef = useRef(null);
const [loading, setLoading] = useState(false);
const [width, setWidth] = useState(DEFAULT_WIDTH);
const [height, setHeight] = useState(DEFAULT_HEIGHT);
const [subObjectivesOpen, setSubObjectivesOpen] = useState(false);

useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setWidth(entry.contentRect.width ?? DEFAULT_WIDTH);
// setHeight(entry.contentRect.height ?? DEFAULT_HEIGHT);
}
});
observer.observe(containerRef.current as Element);
}, [containerRef.current]);

useEffect(() => {
if (!data || !width || !height) return;
drawChart();
drawSubCharts();
}, [data, width]);

function drawChart() {
setLoading(true);
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();

const x = d3
.scaleLinear()
.domain([0, 100])
.range([10, width - MARGIN.right]);

const y = d3
.scaleBand()
.domain([data.framework_level.text])
.range([0, height - MARGIN.bottom - MARGIN.top])
.padding(0.1);

svg
.append("g")
.selectAll("rect")
.data([data.framework_level])
.enter()
.append("rect")
.attr("x", x(0))
.attr("y", (y(data.framework_level.text) as number) + MARGIN.top)
.attr("height", y.bandwidth())
.attr("width", (d) => x(d.avg_performance))
.attr("fill", "#22c55e");

// Add x-axis
svg
.append("g")
.attr("transform", `translate(0, ${height - MARGIN.bottom})`)
.call(d3.axisBottom(x).ticks(2))
.selectAll("text")
.text((d) => `${d as string}%`);

setLoading(false);
}

function drawSubCharts() {
const SUB_DATA = data.framework_descriptors;
if (!SUB_DATA || SUB_DATA.length === 0) return;

setLoading(true);
const container = d3.select(chartsRef.current);
container.selectAll("svg").remove();

const chartHeight =
height / SUB_DATA.length > 200 ? 200 : height / SUB_DATA.length; // set max height of 20px
const chartWidth = width - MARGIN.left - MARGIN.right;
const chartInnerHeight = chartHeight - MARGIN.top - MARGIN.bottom;

SUB_DATA.forEach((d, index) => {
container
.append("svg")
.attr("width", width)
.attr("height", chartHeight)
.attr("id", `chart-${index}`)
.style("margin-bottom", "20px");

const svg = d3.select(`#chart-${index}`);

const group = svg
.append("g")
.attr("transform", `translate(${MARGIN.left}, ${MARGIN.top})`);

const x = d3.scaleLinear().domain([0, 100]).range([0, chartWidth]);

const y = d3
.scaleBand()
.domain([d.text])
.range([0, chartInnerHeight])
.padding(0.1);

// add title to top left of chart
group
.append("text")
.attr("x", 0)
.attr("y", index * chartHeight - 10)
.attr("text-anchor", "start")
.style("font-size", "14px")
.text(d.text);

group
.append("g")
.selectAll("rect")
.data([d])
.enter()
.append("rect")
.attr("x", 0)
.attr("y", y(d.text) || 0)
.attr("width", x(d.avg_performance))
.attr("height", y.bandwidth())
.attr("fill", "steelblue");

group
.append("g")
.call(d3.axisBottom(x).ticks(5))
.attr("transform", `translate(0, ${chartInnerHeight})`);

group.append("g").call(d3.axisLeft(y));
});

setLoading(false);
}

return (
<Card
className="tw-mt-4 tw-rounded-lg tw-shadow-sm tw-px-4 tw-pt-4 tw-pb-2 tw-max-w-[96%]"
ref={containerRef}
>
<div className="tw-flex tw-flex-row tw-justify-between">
<div className="tw-flex tw-flex-col">
<div className="tw-flex tw-flex-row tw-mb-0 tw-items-center">
<h3 className="tw-text-2xl tw-font-semibold">
{data.framework_level.text}
</h3>
</div>
<p className="tw-text-xs tw-text-gray-500 tw-mt-0">
Average performance across associated questions (
{data.framework_level.question_count} questions aligned)
</p>
</div>
</div>
<div className="tw-rounded-md tw-min-h-36">
<svg ref={svgRef} width={width} height={height}></svg>
</div>
<div className="tw-border-[0.75px] tw-border-solid tw-border-slate-300 tw-shadow-sm tw-rounded-md tw-flex tw-flex-col">
<div
className="tw-flex tw-items-center tw-p-2 tw-cursor-pointer"
onClick={() => setSubObjectivesOpen(!subObjectivesOpen)}
>
{subObjectivesOpen ? <ChevronDown /> : <ChevronRight />}
<p className="!tw-my-0 !tw-py-0 tw-ml-2">
View Sub-Objective Completion
</p>
</div>
{subObjectivesOpen && (
<>
{data.framework_descriptors.length === 0 ? (
<div className="tw-p-2">
<p>No sub-objectives aligned to this learning objective.</p>
</div>
) : (
<div ref={chartsRef} className="tw-p-2"></div>
)}
</>
)}
</div>
</Card>
);
};

export default LearningObjectiveLevel;
2 changes: 0 additions & 2 deletions components/TransferList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ import {

function not<T>(a: T[], b: T[]) {
if (!a || !b) return [];
if (typeof a === "string" || typeof b === "string") return [];
if (!Array.isArray(a) || !Array.isArray(b)) return [];

return a.filter((value) => b.indexOf(value) === -1);
}

function intersection<T>(a: T[], b: T[]) {
if (!a || !b) return [];
if (typeof a === "string" || typeof b === "string") return [];
if (!Array.isArray(a) || !Array.isArray(b)) return [];

return a.filter((value) => b.indexOf(value) !== -1);
Expand Down
Loading

0 comments on commit 33f6ba0

Please sign in to comment.