Skip to content

Commit cf0adeb

Browse files
committed
fix(LearningCurves): learning curve final draft
1 parent 1351af5 commit cf0adeb

File tree

5 files changed

+131
-77
lines changed

5 files changed

+131
-77
lines changed

components/LearningCurveDescriptor.tsx

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { DEFAULT_MARGINS, DEFAULT_WIDTH } from "@/utils/visualization-helpers";
66
import { ArrowRight, ArrowLeft } from "react-bootstrap-icons";
77
import LearningObjectiveQuestionsAligned from "./LearningObjectiveQuestionsAligned";
88
import VisualizationLoading from "./VisualizationLoading";
9+
import { format, parseISO } from "date-fns";
910

1011
const MARGIN = {
1112
left: 40,
1213
right: 35,
1314
top: 30,
14-
bottom: 35,
15+
bottom: 40,
1516
};
1617
const DEFAULT_HEIGHT = 350;
1718

@@ -41,40 +42,28 @@ const LearningCurveDescriptor: React.FC<LearningCurveDescriptorProps> = ({
4142

4243
useEffect(() => {
4344
if (!data || !width || !height) return;
44-
drawChart(); // Need to redraw the main chart when no longer viewing subobjectives
45+
drawChart();
4546
}, [data, width]);
4647

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

52-
const cleaned = data.score_data.filter((d) => !isNaN(d.score));
53-
const maxScore = () => {
54-
const DEFAULT_MAX = 1;
55-
if (cleaned.length === 0) return DEFAULT_MAX;
56-
const calculated = Math.max(...cleaned.map((d) => d.score));
57-
return calculated > DEFAULT_MAX ? calculated : DEFAULT_MAX;
58-
};
59-
60-
const maxAttempts = () => {
61-
const DEFAULT_MAX = 15;
62-
if (cleaned.length === 0) return DEFAULT_MAX;
63-
const calculated = Math.max(...cleaned.map((d) => d.num_attempts));
64-
return calculated > DEFAULT_MAX ? calculated : DEFAULT_MAX;
65-
};
66-
67-
const xMax = maxAttempts();
68-
const yMax = maxScore();
53+
const cleaned = data.score_data.filter((d) => !isNaN(d.avg_percent));
54+
const domain = data.score_data.map((d) =>
55+
format(d.submission_date, "MM/dd/yyyy")
56+
);
6957

7058
const x = d3
71-
.scaleLinear()
72-
.domain([1, xMax])
73-
.range([MARGIN.left, width - MARGIN.right]);
59+
.scaleBand()
60+
.domain(domain)
61+
.range([MARGIN.left, width - MARGIN.right])
62+
.padding(0.1);
7463

7564
const y = d3
7665
.scaleLinear()
77-
.domain([0, yMax])
66+
.domain([0, 100])
7867
.range([height - MARGIN.bottom, MARGIN.top]);
7968

8069
svg
@@ -83,26 +72,30 @@ const LearningCurveDescriptor: React.FC<LearningCurveDescriptorProps> = ({
8372
.data(cleaned)
8473
.enter()
8574
.append("circle")
86-
.attr("cx", (d) => x(d.num_attempts))
87-
.attr("cy", (d) => y(d.score))
88-
.attr("r", 3)
75+
.attr("cx", (d) => x(format(d.submission_date, "MM/dd/yyyy")) ?? 0)
76+
.attr("cy", (d) => y(d.avg_percent))
77+
.attr("r", 4)
8978
.style("fill", "#69b3a2");
9079

91-
// Add x-axis
80+
// Add x-axis, only show 8 ticks
9281
svg
9382
.append("g")
9483
.attr("transform", `translate(0, ${height - MARGIN.bottom})`)
95-
.call(d3.axisBottom(x));
84+
.call(
85+
d3.axisBottom(x).tickValues(
86+
x.domain().filter((d, i) => i % Math.ceil(domain.length / 8) === 0)
87+
)
88+
);
9689

9790
// Add x-axis label
9891
svg
9992
.append("text")
10093
.attr("text-anchor", "middle")
10194
.attr("x", width / 2)
102-
.attr("y", height - 10)
95+
.attr("y", height - 5)
10396
.attr("font-size", "12px")
10497
.attr("font-weight", "semibold")
105-
.text("Number of Submission Attempts");
98+
.text("Submission Date")
10699

107100
// Add y-axis
108101
svg
@@ -117,32 +110,7 @@ const LearningCurveDescriptor: React.FC<LearningCurveDescriptorProps> = ({
117110
.attr("transform", `translate(10, ${height / 2}) rotate(-90)`)
118111
.attr("font-size", "12px")
119112
.attr("font-weight", "semibold")
120-
.text("Final Question Score");
121-
122-
// Calculate the slope and intercept for the line of best fit starting at x = 1
123-
const filteredData = cleaned.filter((d) => d.num_attempts >= 1);
124-
const n = filteredData.length;
125-
const sumX = d3.sum(filteredData, (d) => d.num_attempts);
126-
const sumY = d3.sum(filteredData, (d) => d.score);
127-
const sumXY = d3.sum(filteredData, (d) => d.num_attempts * d.score);
128-
const sumXX = d3.sum(filteredData, (d) => d.num_attempts * d.num_attempts);
129-
130-
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
131-
const intercept = (sumY - slope * sumX) / n;
132-
133-
// Determine the y-value of the line at x = 1 and clamp it to the x-axis
134-
let startY = intercept + slope * 1;
135-
if (startY < 0) startY = 0;
136-
137-
// Create the line of best fit starting at x = 1
138-
svg
139-
.append("line")
140-
.attr("x1", x(1))
141-
.attr("y1", y(intercept + slope))
142-
.attr("x2", x(xMax))
143-
.attr("y2", y(Math.max(0, intercept + slope * xMax)))
144-
.attr("stroke", "blue")
145-
.attr("stroke-width", 1);
113+
.text("Avg Percent Correct");
146114

147115
setLoading(false);
148116
}

components/LearningCurves.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const LearningCurves: React.FC<LearningCurvesProps> = ({
5151
if (!globalState.courseID) return;
5252

5353
const _data = await getData(globalState.courseID);
54+
console.log(_data)
5455
setData(_data);
5556
} catch (err) {
5657
console.error(err);

lib/Analytics.ts

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
IDWithText,
1212
LOCData,
1313
LearningCurveData,
14+
LearningCurveRawData,
15+
LearningCurveRawDataWPercent,
16+
LearningCurveRawScoreDataWPercent,
1417
PerformancePerAssignment,
1518
Student,
1619
SubmissionTimeline,
@@ -1018,7 +1021,8 @@ class Analytics {
10181021
) as ICalcADAPTStudentActivity_Raw | undefined;
10191022

10201023
const submitted = activity?.seen.length ?? 0;
1021-
const unsubmitted = allCourseQuestions[0].questions.length - submitted ?? 0;
1024+
const unsubmitted =
1025+
allCourseQuestions[0].questions.length - submitted ?? 0;
10221026

10231027
return {
10241028
actor_id: d.actor_id,
@@ -1489,7 +1493,7 @@ class Analytics {
14891493
"descriptors.id": { $in: Array.from(uniqueDescriptorIDs) },
14901494
})) as IFrameworkLevel_Raw[];
14911495

1492-
const out: LearningCurveData[] = [];
1496+
const out: LearningCurveRawData[] = [];
14931497
for (const a of alignment) {
14941498
for (const d of a.framework_descriptors) {
14951499
const levelWDescriptor = frameworkLevelData.find((fd) =>
@@ -1532,7 +1536,7 @@ class Analytics {
15321536

15331537
// out is now a list of all descriptors
15341538
for (const o of out) {
1535-
const _scoreData: LearningCurveData["score_data"] = [];
1539+
const _scoreData: LearningCurveRawData["score_data"] = [];
15361540
const descriptorQuestionIds = alignment
15371541
.filter((a) =>
15381542
a.framework_descriptors.find(
@@ -1548,23 +1552,80 @@ class Analytics {
15481552
);
15491553

15501554
descriptorScoreData.forEach((d) => {
1551-
d.questions.forEach((q) => {
1555+
for (const q of d.questions) {
1556+
if (!q.last_submitted_at) continue;
1557+
15521558
_scoreData.push({
15531559
question_id: q.question_id,
15541560
score: parseFloat(q.score),
1555-
num_attempts: q.submission_count,
1561+
max_score: parseFloat(q.max_score),
1562+
last_submitted_at: q.last_submitted_at,
15561563
});
1557-
});
1564+
}
15581565
});
15591566

15601567
// Set the score data for this descriptor
15611568
o.score_data = _scoreData;
15621569
}
15631570

1571+
// Calculate percent correct for each question
1572+
const withPercentCorrect: LearningCurveRawDataWPercent[] = out.map(
1573+
(d) => {
1574+
d.score_data = d.score_data.map((s) => {
1575+
return {
1576+
...s,
1577+
percent_correct: parseFloat(
1578+
((s.score / s.max_score) * 100).toPrecision(2)
1579+
),
1580+
};
1581+
}) as LearningCurveRawScoreDataWPercent[];
1582+
return d as LearningCurveRawDataWPercent;
1583+
}
1584+
);
1585+
1586+
// Group the questions by last_submitted_at and calculate the average percent correct for each day
1587+
const groupedByDate: LearningCurveData[] = withPercentCorrect.map((d) => {
1588+
const grouped = d.score_data.reduce((acc, curr) => {
1589+
const date = new Date(curr.last_submitted_at).toLocaleDateString();
1590+
const existing = acc.find((a) => a.date === date);
1591+
if (existing) {
1592+
existing.percent_correct.push(curr.percent_correct);
1593+
} else {
1594+
acc.push({
1595+
date: date,
1596+
percent_correct: [curr.percent_correct],
1597+
});
1598+
}
1599+
return acc;
1600+
}, [] as { date: string; percent_correct: number[] }[]);
1601+
1602+
const mappedScoreData = grouped.map((g) => ({
1603+
submission_date: g.date,
1604+
avg_percent: parseFloat(
1605+
(
1606+
g.percent_correct.reduce((acc, curr) => acc + curr, 0) /
1607+
g.percent_correct.length
1608+
).toPrecision(2)
1609+
),
1610+
}));
1611+
1612+
const dateSorted = mappedScoreData.sort((a, b) => {
1613+
return (
1614+
new Date(a.submission_date).getTime() -
1615+
new Date(b.submission_date).getTime()
1616+
);
1617+
});
1618+
1619+
return {
1620+
descriptor: d.descriptor,
1621+
score_data: dateSorted,
1622+
};
1623+
});
1624+
15641625
// Filter out exclusions
15651626
const frameworkExclusions = await this._getFrameworkExclusions();
15661627
const exclusionIDs = frameworkExclusions.map((d) => d.id);
1567-
const finalFiltered = out.filter((d) => {
1628+
const finalFiltered = groupedByDate.filter((d) => {
15681629
return !exclusionIDs.includes(d.descriptor.id);
15691630
});
15701631

@@ -1627,9 +1688,12 @@ class Analytics {
16271688
}
16281689
}
16291690

1630-
private _learningObjectivesParseScoreData(
1631-
data: IAssignmentScoresRaw[]
1632-
): { question_id: string; score: number; max_score: number }[] {
1691+
private _learningObjectivesParseScoreData(data: IAssignmentScoresRaw[]): {
1692+
question_id: string;
1693+
score: number;
1694+
max_score: number;
1695+
last_submitted_at: string | null;
1696+
}[] {
16331697
const results = [];
16341698
for (const d of data) {
16351699
for (const q of d.questions) {
@@ -1638,6 +1702,7 @@ class Analytics {
16381702
question_id: q.question_id,
16391703
score: parseFloat(q.score),
16401704
max_score: parseFloat(q.max_score),
1705+
last_submitted_at: q.last_submitted_at,
16411706
});
16421707
}
16431708
}

lib/AnalyticsDataCollector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class AnalyticsDataCollector {
4444
//await this.collectAllAssignments();
4545
//await this.collectEnrollments();
4646
await this.collectAssignmentScores();
47-
//await this.collectSubmissionTimestamps(); // this should only run after collectAssignmentScores
47+
await this.collectSubmissionTimestamps(); // this should only run after collectAssignmentScores
4848
//await this.collectFrameworkData();
4949
//await this.collectQuestionFrameworkAlignment();
5050
//await this.collectReviewTimeData();

lib/types/analytics.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,36 @@ export type LOCData = {
107107
}[];
108108
};
109109

110-
export type LearningCurveData = {
111-
descriptor: {
112-
id: string;
113-
text: string;
114-
questions: string[];
115-
question_count: number;
116-
};
110+
export type LearningCurveDescriptor = {
111+
id: string;
112+
text: string;
113+
questions: string[];
114+
question_count: number;
115+
}
116+
117+
export type LearningCurveRawScoreData = {
118+
question_id: string;
119+
score: number;
120+
max_score: number;
121+
last_submitted_at: string;
122+
};
123+
124+
export type LearningCurveRawScoreDataWPercent = LearningCurveRawScoreData & {
125+
percent_correct: number;
126+
};
127+
128+
export type LearningCurveRawData = {
129+
descriptor: LearningCurveDescriptor;
130+
score_data: LearningCurveRawScoreData[];
131+
};
132+
133+
export type LearningCurveRawDataWPercent = Pick<LearningCurveRawData, "descriptor"> & {
134+
score_data: LearningCurveRawScoreDataWPercent[];
135+
};
136+
137+
export type LearningCurveData = Pick<LearningCurveRawData, "descriptor"> & {
117138
score_data: {
118-
question_id: string;
119-
score: number;
120-
num_attempts: number;
139+
submission_date: string;
140+
avg_percent: number;
121141
}[];
122142
};

0 commit comments

Comments
 (0)