You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
3.8 KiB
147 lines
3.8 KiB
document.getElementById('fileInput').addEventListener('change', handleFiles);
|
|
|
|
async function handleFiles(event) {
|
|
const files = event.target.files;
|
|
document.getElementById('fileCount').textContent = `Число файлов: ${files.length}`;
|
|
let processed = 0;
|
|
|
|
const questionErrors = {};
|
|
const topicStats = {};
|
|
const studentScores = [];
|
|
|
|
for (const file of files) {
|
|
const text = await file.text();
|
|
const json = JSON.parse(text);
|
|
const student = json.student;
|
|
const questions = json.questions;
|
|
let correct = 0;
|
|
|
|
for (const q of questions) {
|
|
const topic = q.topic || "Без темы";
|
|
const questionText = q.question;
|
|
const isCorrect = q.is_correct;
|
|
|
|
if (isCorrect) correct++;
|
|
else {
|
|
if (!questionErrors[questionText]) questionErrors[questionText] = [];
|
|
questionErrors[questionText].push({
|
|
student,
|
|
given: q.student_answer,
|
|
correct: q.correct_answer,
|
|
topic
|
|
});
|
|
}
|
|
|
|
if (!topicStats[topic]) topicStats[topic] = 0;
|
|
topicStats[topic]++;
|
|
}
|
|
|
|
studentScores.push({ student, correct });
|
|
processed++;
|
|
document.getElementById('processedCount').textContent = `Обработано файлов: ${processed}`;
|
|
}
|
|
|
|
drawCharts(studentScores, questionErrors, topicStats);
|
|
}
|
|
|
|
function truncate(text, len = 35) {
|
|
return text.length > len ? text.slice(0, len) + "…" : text;
|
|
}
|
|
|
|
function surname(fullname) {
|
|
return fullname.split(" ")[0];
|
|
}
|
|
|
|
// Wrap long hover text
|
|
function wrapText(str, width = 60) {
|
|
return str.replace(new RegExp(`(.{${width}})`, "g"), "$1<br>");
|
|
}
|
|
|
|
// Generate stable color from topic name
|
|
function hashColor(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
let color = "#";
|
|
for (let i = 0; i < 3; i++) {
|
|
const value = (hash >> (i * 8)) & 0xff;
|
|
color += ("00" + value.toString(16)).slice(-2);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
function drawCharts(scores, errors, topics) {
|
|
// === Chart 1 ===
|
|
scores.sort((a, b) => b.correct - a.correct);
|
|
Plotly.newPlot("chart1", [{
|
|
x: scores.map(x => x.student),
|
|
y: scores.map(x => x.correct),
|
|
type: "bar",
|
|
marker: { color: "lightblue" }
|
|
}], { title: "Рейтинг студентов" });
|
|
|
|
// === Chart 2 ===
|
|
const sortedErrors = Object.entries(errors)
|
|
.sort((a, b) => b[1].length - a[1].length)
|
|
.slice(0, 50);
|
|
|
|
const fullQuestions = sortedErrors.map(x => x[0]);
|
|
const shortLabels = fullQuestions.map(q => truncate(q));
|
|
|
|
const xvals = fullQuestions.map((_, i) => i);
|
|
|
|
const colors2 = sortedErrors.map(([qText, entries]) => {
|
|
const topic = entries[0].topic || "Без темы";
|
|
return hashColor(topic);
|
|
});
|
|
|
|
const customdata = sortedErrors.map(([qText, entries]) => {
|
|
return {
|
|
full: wrapText(qText, 60),
|
|
lines: entries.map(d => `${surname(d.student)}: "${d.given}"`).join("<br>")
|
|
};
|
|
});
|
|
|
|
Plotly.newPlot(
|
|
"chart2",
|
|
[
|
|
{
|
|
x: xvals,
|
|
y: sortedErrors.map(x => x[1].length),
|
|
type: "bar",
|
|
marker: { color: colors2 },
|
|
customdata: customdata,
|
|
hovertemplate:
|
|
"<b>%{customdata.full}</b><br>%{customdata.lines}<extra></extra>"
|
|
}
|
|
],
|
|
{
|
|
title: "Наиболее частые ошибки",
|
|
xaxis: {
|
|
tickmode: "array",
|
|
tickvals: xvals,
|
|
ticktext: shortLabels
|
|
},
|
|
hoverlabel: {
|
|
namelength: -1,
|
|
bgcolor: "rgba(255,255,255,0.95)",
|
|
bordercolor: "#333",
|
|
font: { size: 14 }
|
|
}
|
|
}
|
|
);
|
|
|
|
// === Chart 3 ===
|
|
const topicsList = Object.keys(topics);
|
|
const topicsValues = Object.values(topics);
|
|
const colors3 = topicsList.map(t => hashColor(t));
|
|
|
|
Plotly.newPlot("chart3", [{
|
|
x: topicsList,
|
|
y: topicsValues,
|
|
type: "bar",
|
|
marker: { color: colors3 }
|
|
}], { title: "Анализ по темам" });
|
|
}
|