Akshit Shubham Qwen-Coder commited on
Commit
e5a5320
·
1 Parent(s): f07037a

Add stacked chart with topic ratios and subject filtering

Browse files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

database.py CHANGED
@@ -395,6 +395,30 @@ def setup_database():
395
  except sqlite3.OperationalError:
396
  cursor.execute("ALTER TABLE users ADD COLUMN pdf_viewer TEXT DEFAULT 'adobe'")
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  conn.commit()
399
  conn.close()
400
 
 
395
  except sqlite3.OperationalError:
396
  cursor.execute("ALTER TABLE users ADD COLUMN pdf_viewer TEXT DEFAULT 'adobe'")
397
 
398
+ # Migration to normalize subject names (e.g., "CHEMISTRY" to "Chemistry", "PHYSICS" to "Physics", etc.)
399
+ try:
400
+ # Define subject mappings
401
+ subject_mappings = {
402
+ 'CHEMISTRY': 'Chemistry',
403
+ 'PHYSICS': 'Physics',
404
+ 'BIOLOGY': 'Biology',
405
+ 'MATHEMATICS': 'Mathematics',
406
+ 'ZOOLOGY': 'Zoology',
407
+ 'BOTANY': 'Botany'
408
+ }
409
+
410
+ for old_subject, new_subject in subject_mappings.items():
411
+ # Check if there are any questions with the old subject name
412
+ cursor.execute("SELECT COUNT(*) FROM questions WHERE subject = ?", (old_subject,))
413
+ count = cursor.fetchone()[0]
414
+
415
+ if count > 0:
416
+ # Update all old subject names to new standardized names
417
+ cursor.execute("UPDATE questions SET subject = ? WHERE subject = ?", (new_subject, old_subject))
418
+ print(f"Updated {cursor.rowcount} questions from '{old_subject}' to '{new_subject}'")
419
+ except Exception as e:
420
+ print(f"Error during subject normalization migration: {e}")
421
+
422
  conn.commit()
423
  conn.close()
424
 
routes/general.py CHANGED
@@ -99,4 +99,301 @@ def chart():
99
  total_questions = conn.execute("SELECT COUNT(q.id) FROM questions q JOIN sessions s ON q.session_id = s.id WHERE s.user_id = ?", (current_user.id,)).fetchone()[0]
100
  total_classified = conn.execute("SELECT COUNT(q.id) FROM questions q JOIN sessions s ON q.session_id = s.id WHERE s.user_id = ? AND q.subject IS NOT NULL AND q.chapter IS NOT NULL", (current_user.id,)).fetchone()[0]
101
  conn.close()
102
- return render_template('chart.html', total_sessions=total_sessions, total_pdfs=total_pdfs, total_questions=total_questions, total_classified_questions=total_classified)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  total_questions = conn.execute("SELECT COUNT(q.id) FROM questions q JOIN sessions s ON q.session_id = s.id WHERE s.user_id = ?", (current_user.id,)).fetchone()[0]
100
  total_classified = conn.execute("SELECT COUNT(q.id) FROM questions q JOIN sessions s ON q.session_id = s.id WHERE s.user_id = ? AND q.subject IS NOT NULL AND q.chapter IS NOT NULL", (current_user.id,)).fetchone()[0]
101
  conn.close()
102
+ return render_template('chart.html', total_sessions=total_sessions, total_pdfs=total_pdfs, total_questions=total_questions, total_classified_questions=total_classified)
103
+
104
+
105
+ @main_bp.route('/stacked_chart')
106
+ @login_required
107
+ def stacked_chart():
108
+ conn = get_db_connection()
109
+
110
+ # Get classified questions grouped by session_id, subject, and topic (chapter)
111
+ # Filter for only specific subjects (P, C, B, Z, M) as requested
112
+ # Also normalize subject names to ensure consistency
113
+ classified_data = conn.execute("""
114
+ SELECT s.id as session_id, s.name as session_name,
115
+ CASE
116
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
117
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
118
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
119
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
120
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
121
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
122
+ ELSE q.subject
123
+ END as subject,
124
+ q.chapter, COUNT(*) as question_count
125
+ FROM questions q
126
+ JOIN sessions s ON q.session_id = s.id
127
+ WHERE s.user_id = ?
128
+ AND q.subject IS NOT NULL
129
+ AND q.chapter IS NOT NULL
130
+ AND UPPER(SUBSTR(TRIM(
131
+ CASE
132
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
133
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
134
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
135
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
136
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
137
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
138
+ ELSE q.subject
139
+ END
140
+ ), 1, 1)) IN ('P', 'C', 'B', 'Z', 'M')
141
+ GROUP BY s.id, s.name,
142
+ CASE
143
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
144
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
145
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
146
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
147
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
148
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
149
+ ELSE q.subject
150
+ END,
151
+ q.chapter
152
+ ORDER BY s.created_at DESC, s.id,
153
+ CASE
154
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
155
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
156
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
157
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
158
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
159
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
160
+ ELSE q.subject
161
+ END,
162
+ q.chapter
163
+ """, (current_user.id,)).fetchall()
164
+
165
+ # Group the data by session_id for the chart
166
+ sessions_data = {}
167
+ for row in classified_data:
168
+ session_id = row['session_id']
169
+ session_name = row['session_name'] or session_id # Use session_id if name is None
170
+ subject = row['subject']
171
+ topic = row['chapter']
172
+ count = row['question_count']
173
+
174
+ if session_id not in sessions_data:
175
+ sessions_data[session_id] = {
176
+ 'session_name': session_name,
177
+ 'subjects': {}
178
+ }
179
+
180
+ if subject not in sessions_data[session_id]['subjects']:
181
+ sessions_data[session_id]['subjects'][subject] = {}
182
+
183
+ sessions_data[session_id]['subjects'][subject][topic] = count
184
+
185
+ conn.close()
186
+
187
+ return render_template('stacked_chart.html', sessions_data=sessions_data)
188
+
189
+
190
+ @main_bp.route('/api/stacked_chart_data')
191
+ @login_required
192
+ def api_stacked_chart_data():
193
+ conn = get_db_connection()
194
+
195
+ # Get classified questions grouped by session_id, subject, and topic (chapter)
196
+ # Filter for only specific subjects (P, C, B, Z, M) as requested
197
+ # Also normalize subject names to ensure consistency
198
+ classified_data = conn.execute("""
199
+ SELECT s.id as session_id, s.name as session_name, s.original_filename,
200
+ CASE
201
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
202
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
203
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
204
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
205
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
206
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
207
+ ELSE q.subject
208
+ END as subject,
209
+ q.chapter, COUNT(*) as question_count
210
+ FROM questions q
211
+ JOIN sessions s ON q.session_id = s.id
212
+ WHERE s.user_id = ?
213
+ AND q.subject IS NOT NULL
214
+ AND q.chapter IS NOT NULL
215
+ AND UPPER(SUBSTR(TRIM(
216
+ CASE
217
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
218
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
219
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
220
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
221
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
222
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
223
+ ELSE q.subject
224
+ END
225
+ ), 1, 1)) IN ('P', 'C', 'B', 'Z', 'M')
226
+ GROUP BY s.id, s.name, s.original_filename,
227
+ CASE
228
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
229
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
230
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
231
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
232
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
233
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
234
+ ELSE q.subject
235
+ END,
236
+ q.chapter
237
+ ORDER BY s.created_at DESC, s.id,
238
+ CASE
239
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
240
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
241
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
242
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
243
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
244
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
245
+ ELSE q.subject
246
+ END,
247
+ q.chapter
248
+ """, (current_user.id,)).fetchall()
249
+
250
+ # Get topic occurrence statistics - total occurrences and number of sessions for each topic
251
+ topic_stats = conn.execute("""
252
+ SELECT
253
+ CASE
254
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
255
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
256
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
257
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
258
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
259
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
260
+ ELSE q.subject
261
+ END as subject,
262
+ q.chapter,
263
+ COUNT(*) as total_occurrences,
264
+ COUNT(DISTINCT q.session_id) as total_sessions_with_topic
265
+ FROM questions q
266
+ JOIN sessions s ON q.session_id = s.id
267
+ WHERE s.user_id = ?
268
+ AND q.subject IS NOT NULL
269
+ AND q.chapter IS NOT NULL
270
+ AND UPPER(SUBSTR(TRIM(
271
+ CASE
272
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
273
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
274
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
275
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
276
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
277
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
278
+ ELSE q.subject
279
+ END
280
+ ), 1, 1)) IN ('P', 'C', 'B', 'Z', 'M')
281
+ GROUP BY
282
+ CASE
283
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
284
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
285
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
286
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
287
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
288
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
289
+ ELSE q.subject
290
+ END,
291
+ q.chapter
292
+ """, (current_user.id,)).fetchall()
293
+
294
+ # Get wrong ratios for topics - aggregated by topic across all sessions
295
+ wrong_ratios = conn.execute("""
296
+ SELECT
297
+ CASE
298
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
299
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
300
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
301
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
302
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
303
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
304
+ ELSE q.subject
305
+ END as subject,
306
+ q.chapter,
307
+ SUM(CASE WHEN q.status = 'wrong' THEN 1 ELSE 0 END) as total_wrongs,
308
+ COUNT(DISTINCT q.session_id) as total_sessions_with_wrongs
309
+ FROM questions q
310
+ JOIN sessions s ON q.session_id = s.id
311
+ WHERE s.user_id = ?
312
+ AND q.subject IS NOT NULL
313
+ AND q.chapter IS NOT NULL
314
+ AND UPPER(SUBSTR(TRIM(
315
+ CASE
316
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
317
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
318
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
319
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
320
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
321
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
322
+ ELSE q.subject
323
+ END
324
+ ), 1, 1)) IN ('P', 'C', 'B', 'Z', 'M')
325
+ GROUP BY
326
+ CASE
327
+ WHEN q.subject = 'CHEMISTRY' THEN 'Chemistry'
328
+ WHEN q.subject = 'PHYSICS' THEN 'Physics'
329
+ WHEN q.subject = 'BIOLOGY' THEN 'Biology'
330
+ WHEN q.subject = 'MATHEMATICS' THEN 'Mathematics'
331
+ WHEN q.subject = 'ZOOLOGY' THEN 'Zoology'
332
+ WHEN q.subject = 'BOTANY' THEN 'Botany'
333
+ ELSE q.subject
334
+ END,
335
+ q.chapter
336
+ """, (current_user.id,)).fetchall()
337
+
338
+ # Group the data by session_id for the chart
339
+ sessions_data = {}
340
+ for row in classified_data:
341
+ session_id = row['session_id']
342
+ session_name = row['session_name'] or session_id # Use session_id if name is None
343
+
344
+ # Use original_filename if session_name is the same as session_id
345
+ if session_name == session_id and row['original_filename']:
346
+ session_name = row['original_filename']
347
+
348
+ subject = row['subject']
349
+ topic = row['chapter']
350
+ count = row['question_count']
351
+
352
+ if session_id not in sessions_data:
353
+ sessions_data[session_id] = {
354
+ 'session_name': session_name,
355
+ 'subjects': {}
356
+ }
357
+
358
+ if subject not in sessions_data[session_id]['subjects']:
359
+ sessions_data[session_id]['subjects'][subject] = {}
360
+
361
+ sessions_data[session_id]['subjects'][subject][topic] = count
362
+
363
+ # Create mapping for topic stats (total occurrences and number of sessions with topic)
364
+ topic_stats_map = {}
365
+ for row in topic_stats:
366
+ subject = row['subject']
367
+ topic = row['chapter']
368
+ total_occurrences = row['total_occurrences']
369
+ total_sessions_with_topic = row['total_sessions_with_topic']
370
+
371
+ topic_stats_map[f"{subject}|{topic}"] = {
372
+ 'total_occurrences': total_occurrences,
373
+ 'total_sessions_with_topic': total_sessions_with_topic
374
+ }
375
+
376
+ # Add topic occurrence ratios to the data for each session
377
+ for session_id in sessions_data:
378
+ for subject in sessions_data[session_id]['subjects']:
379
+ for topic in sessions_data[session_id]['subjects'][subject]:
380
+ # Store topic occurrence ratio data
381
+ if 'wrong_ratios' not in sessions_data[session_id]:
382
+ sessions_data[session_id]['wrong_ratios'] = {}
383
+ if subject not in sessions_data[session_id]['wrong_ratios']:
384
+ sessions_data[session_id]['wrong_ratios'][subject] = {}
385
+
386
+ key = f"{subject}|{topic}"
387
+
388
+ # Get topic stats (total occurrences and number of sessions with topic)
389
+ topic_stat = topic_stats_map.get(key, {'total_occurrences': 0, 'total_sessions_with_topic': 0})
390
+
391
+ # Store topic occurrence ratio: total occurrences / number of sessions with topic
392
+ sessions_data[session_id]['wrong_ratios'][subject][topic] = {
393
+ 'total_occurrences': topic_stat['total_occurrences'],
394
+ 'total_sessions_with_topic': topic_stat['total_sessions_with_topic']
395
+ }
396
+
397
+ conn.close()
398
+
399
+ return jsonify(sessions_data)
templates/_nav_links.html CHANGED
@@ -12,6 +12,9 @@
12
  <a class="nav-link" href="/chart">
13
  <i class="bi bi-bar-chart-line me-1"></i> Charts
14
  </a>
 
 
 
15
  <a class="nav-link" href="{{ url_for('subjective.list_questions') }}">
16
  <i class="bi bi-magic me-1"></i> Subjective Gen
17
  </a>
 
12
  <a class="nav-link" href="/chart">
13
  <i class="bi bi-bar-chart-line me-1"></i> Charts
14
  </a>
15
+ <a class="nav-link" href="/stacked_chart">
16
+ <i class="bi bi-bar-chart-steps me-1"></i> Stacked Chart
17
+ </a>
18
  <a class="nav-link" href="{{ url_for('subjective.list_questions') }}">
19
  <i class="bi bi-magic me-1"></i> Subjective Gen
20
  </a>
templates/stacked_chart.html ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Stacked Chart - Classified Questions{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container mt-5">
7
+ <div class="card bg-dark text-white">
8
+ <div class="card-header text-center">
9
+ <h1 class="h3 mb-0">Classified Questions Stacked Chart</h1>
10
+ <p class="mb-0">Session -> Subjects -> Topics</p>
11
+ </div>
12
+ <div class="card-body">
13
+ <div class="row">
14
+ <div class="col-12">
15
+ <div class="d-flex flex-wrap justify-content-center mb-3" id="mainChartSubjectCheckboxes">
16
+ <!-- Main chart subject checkboxes will be populated by JavaScript -->
17
+ </div>
18
+ <div class="col-12" style="overflow-x: auto;">
19
+ <div id="chartWrapper" style="min-width: 800px; height: 450px;">
20
+ <canvas id="stackedChart"></canvas>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row mt-4">
27
+ <div class="col-12">
28
+ <h4 class="text-center">Topic-wise Aggregate</h4>
29
+ <div class="d-flex flex-wrap justify-content-center mb-3" id="subjectCheckboxes">
30
+ <!-- Subject checkboxes will be populated by JavaScript -->
31
+ </div>
32
+ <div class="d-flex justify-content-center mb-3">
33
+ <div class="form-check form-switch">
34
+ <input class="form-check-input" type="checkbox" id="sortByWrongRatio">
35
+ <label class="form-check-label" for="sortByWrongRatio">Sort by Wrong Ratio</label>
36
+ </div>
37
+ </div>
38
+ <div class="row mb-3">
39
+ <div class="col-md-6">
40
+ <div class="card bg-secondary text-white text-center p-3">
41
+ <h5>Total Questions</h5>
42
+ <p class="fs-2 fw-bold" id="totalQuestionsCount">0</p>
43
+ </div>
44
+ </div>
45
+ <div class="col-md-6">
46
+ <div class="card bg-warning text-dark text-center p-3">
47
+ <h5>Tests with Wrongings</h5>
48
+ <p class="fs-2 fw-bold" id="wrongingsCount">0</p>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <canvas id="topicChart" width="400" height="300"></canvas>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="row mt-4">
57
+ <div class="col-12">
58
+ <div class="table-responsive">
59
+ <table class="table table-dark table-striped">
60
+ <thead>
61
+ <tr>
62
+ <th>Session ID</th>
63
+ <th>Session Name</th>
64
+ <th>Subject</th>
65
+ <th>Topic</th>
66
+ <th>Question Count</th>
67
+ <th>Wrong Ratio</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody id="dataTableBody">
71
+ <!-- Data will be populated by JavaScript -->
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
82
+ <script>
83
+ // Global variables to store chart instances and data
84
+ let stackedChartInstance = null;
85
+ let topicChartInstance = null;
86
+ let originalData = null;
87
+ let selectedSubjects = new Set(); // Track selected subjects
88
+
89
+ document.addEventListener('DOMContentLoaded', function() {
90
+ fetch('/api/stacked_chart_data')
91
+ .then(response => response.json())
92
+ .then(data => {
93
+ originalData = data;
94
+
95
+ // Extract all unique subjects from the data
96
+ const allSubjects = new Set();
97
+ Object.keys(data).forEach(sessionId => {
98
+ Object.keys(data[sessionId].subjects).forEach(subject => {
99
+ allSubjects.add(subject);
100
+ });
101
+ });
102
+
103
+ // Create checkboxes for each subject for both charts
104
+ createSubjectCheckboxes(Array.from(allSubjects));
105
+ createMainChartSubjectCheckboxes(Array.from(allSubjects)); // For the main chart
106
+
107
+ // Initialize with all subjects selected
108
+ allSubjects.forEach(subject => selectedSubjects.add(subject));
109
+
110
+ createStackedChart(data);
111
+ createTopicChart(data);
112
+ updateAdditionalData(data);
113
+ populateDataTable(data);
114
+
115
+ // Add event listener for the sort by wrong ratio checkbox
116
+ document.getElementById('sortByWrongRatio').addEventListener('change', function() {
117
+ sortByWrongRatio = this.checked;
118
+ createTopicChart(data);
119
+ });
120
+ })
121
+ .catch(error => console.error('Error fetching chart data:', error));
122
+ });
123
+
124
+ function createSubjectCheckboxes(subjects) {
125
+ const container = document.getElementById('subjectCheckboxes');
126
+ container.innerHTML = ''; // Clear existing checkboxes
127
+
128
+ subjects.forEach(subject => {
129
+ const div = document.createElement('div');
130
+ div.className = 'form-check mx-2';
131
+
132
+ const input = document.createElement('input');
133
+ input.type = 'checkbox';
134
+ input.className = 'form-check-input';
135
+ input.id = `subject-${subject}`;
136
+ input.value = subject;
137
+ input.checked = true; // Initially all are checked
138
+ input.dataset.subject = subject;
139
+
140
+ input.addEventListener('change', function() {
141
+ if (this.checked) {
142
+ selectedSubjects.add(subject);
143
+ } else {
144
+ selectedSubjects.delete(subject);
145
+ }
146
+
147
+ // Apply the filter to both charts
148
+ applySubjectFilter();
149
+ });
150
+
151
+ const label = document.createElement('label');
152
+ label.className = 'form-check-label';
153
+ label.htmlFor = `subject-${subject}`;
154
+ label.textContent = subject;
155
+
156
+ div.appendChild(input);
157
+ div.appendChild(label);
158
+ container.appendChild(div);
159
+ });
160
+ }
161
+
162
+ function createMainChartSubjectCheckboxes(subjects) {
163
+ const container = document.getElementById('mainChartSubjectCheckboxes');
164
+ container.innerHTML = ''; // Clear existing checkboxes
165
+
166
+ // Only show checkboxes for P, C, B, Z, M subjects
167
+ const pcbzmSubjects = subjects.filter(subject => {
168
+ const firstLetter = subject.charAt(0).toUpperCase();
169
+ return ['P', 'C', 'B', 'Z', 'M'].includes(firstLetter);
170
+ });
171
+
172
+ pcbzmSubjects.forEach(subject => {
173
+ const div = document.createElement('div');
174
+ div.className = 'form-check form-check-inline mx-2';
175
+
176
+ const input = document.createElement('input');
177
+ input.type = 'checkbox';
178
+ input.className = 'form-check-input';
179
+ input.id = `main-subject-${subject}`;
180
+ input.value = subject;
181
+ input.checked = true; // Initially all are checked
182
+ input.dataset.subject = subject;
183
+
184
+ input.addEventListener('change', function() {
185
+ if (this.checked) {
186
+ selectedSubjects.add(subject);
187
+ } else {
188
+ selectedSubjects.delete(subject);
189
+ }
190
+
191
+ // Apply the filter to both charts
192
+ applySubjectFilter();
193
+ });
194
+
195
+ const label = document.createElement('label');
196
+ label.className = 'form-check-label';
197
+ label.htmlFor = `main-subject-${subject}`;
198
+ // Use only the first letter as label
199
+ label.textContent = subject.charAt(0).toUpperCase();
200
+
201
+ div.appendChild(input);
202
+ div.appendChild(label);
203
+ container.appendChild(div);
204
+ });
205
+ }
206
+
207
+ function updateAdditionalData(data) {
208
+ // Calculate total number of questions
209
+ let totalQuestions = 0;
210
+ let sessionsWithWrongings = new Set(); // Track sessions that have wrongings
211
+
212
+ Object.keys(data).forEach(sessionId => {
213
+ Object.keys(data[sessionId].subjects).forEach(subject => {
214
+ Object.keys(data[sessionId].subjects[subject]).forEach(topic => {
215
+ totalQuestions += data[sessionId].subjects[subject][topic];
216
+
217
+ // For now, assuming all questions count toward wrongings
218
+ // In a real implementation, you would check for actual wrongings
219
+ sessionsWithWrongings.add(sessionId);
220
+ });
221
+ });
222
+ });
223
+
224
+ // Update the displayed counts
225
+ document.getElementById('totalQuestionsCount').textContent = totalQuestions;
226
+ document.getElementById('wrongingsCount').textContent = sessionsWithWrongings.size;
227
+ }
228
+
229
+ function applySubjectFilter() {
230
+ if (selectedSubjects.size === 0) {
231
+ // If no subjects are selected, show empty data
232
+ createStackedChart({});
233
+ createTopicChart({});
234
+ updateAdditionalData({});
235
+ return;
236
+ }
237
+
238
+ // Filter data based on selected subjects
239
+ const filteredData = {};
240
+ Object.keys(originalData).forEach(sessionId => {
241
+ filteredData[sessionId] = {
242
+ session_name: originalData[sessionId].session_name,
243
+ subjects: {},
244
+ wrong_ratios: originalData[sessionId].wrong_ratios // Keep the original wrong_ratios data
245
+ };
246
+
247
+ Object.keys(originalData[sessionId].subjects).forEach(subject => {
248
+ if (selectedSubjects.has(subject)) {
249
+ filteredData[sessionId].subjects[subject] = originalData[sessionId].subjects[subject];
250
+ }
251
+ });
252
+ });
253
+
254
+ createStackedChart(filteredData);
255
+ createTopicChart(filteredData);
256
+ updateAdditionalData(filteredData);
257
+ }
258
+
259
+ function createStackedChart(data) {
260
+ const ctx = document.getElementById('stackedChart').getContext('2d');
261
+ const sessionIds = Object.keys(data);
262
+ const sessionNames = sessionIds.map(id => data[id].session_name || id);
263
+
264
+ // Collect all unique subject-topic combinations
265
+ const datasetMap = {}; // key: "subject|topic", value: {subject, topic, counts[], wrong_ratios[]}
266
+
267
+ sessionIds.forEach((sessionId, sessionIndex) => {
268
+ const sessionData = data[sessionId];
269
+ Object.keys(sessionData.subjects).forEach(subject => {
270
+ Object.keys(sessionData.subjects[subject]).forEach(topic => {
271
+ const key = `${subject}|${topic}`;
272
+ if (!datasetMap[key]) {
273
+ datasetMap[key] = {
274
+ subject: subject,
275
+ topic: topic,
276
+ counts: new Array(sessionIds.length).fill(0),
277
+ wrong_ratios: new Array(sessionIds.length).fill(null)
278
+ };
279
+ }
280
+ datasetMap[key].counts[sessionIndex] = sessionData.subjects[subject][topic];
281
+
282
+ // Get topic occurrence ratio for this topic (total occurrences / number of sessions with topic)
283
+ if (sessionData.wrong_ratios &&
284
+ sessionData.wrong_ratios[subject] &&
285
+ sessionData.wrong_ratios[subject][topic]) {
286
+ const topicData = sessionData.wrong_ratios[subject][topic];
287
+ datasetMap[key].wrong_ratios[sessionIndex] = `${topicData.total_occurrences}/${topicData.total_sessions_with_topic}`;
288
+ } else {
289
+ datasetMap[key].wrong_ratios[sessionIndex] = '0/0';
290
+ }
291
+ });
292
+ });
293
+ });
294
+
295
+ const datasets = Object.values(datasetMap).map(item => ({
296
+ label: `${item.subject} - ${item.topic}`,
297
+ backgroundColor: getRandomColor(),
298
+ data: item.counts,
299
+ stack: item.subject, // Each subject gets its own stacked bar
300
+ // Store wrong ratios for tooltip
301
+ wrong_ratios: item.wrong_ratios
302
+ }));
303
+
304
+ // Dynamically set the min-width based on session count
305
+ const chartWrapper = document.getElementById('chartWrapper');
306
+ chartWrapper.style.minWidth = Math.max(800, sessionIds.length * 200) + 'px';
307
+
308
+ // Destroy previous chart instance if exists
309
+ if (stackedChartInstance) {
310
+ stackedChartInstance.destroy();
311
+ }
312
+
313
+ stackedChartInstance = new Chart(ctx, {
314
+ type: 'bar',
315
+ data: { labels: sessionNames, datasets: datasets },
316
+ options: {
317
+ responsive: true,
318
+ maintainAspectRatio: false,
319
+ plugins: {
320
+ title: { display: true, text: 'Questions by Session → Subject → Topic' },
321
+ legend: { display: true, position: 'right', labels: { boxWidth: 12, font: { size: 10 } } },
322
+ tooltip: {
323
+ callbacks: {
324
+ label: function(context) {
325
+ const label = context.dataset.label || '';
326
+ const value = context.parsed.y;
327
+ const wrongRatio = context.dataset.wrong_ratios[context.dataIndex] || '0/0';
328
+
329
+ return `${label}: ${value} questions (Wrong: ${wrongRatio})`;
330
+ }
331
+ }
332
+ }
333
+ },
334
+ scales: {
335
+ x: { stacked: true, title: { display: true, text: 'Sessions' } },
336
+ y: { stacked: true, title: { display: true, text: 'Question Count' } }
337
+ }
338
+ }
339
+ });
340
+ }
341
+
342
+ function createTopicChart(data) {
343
+ const topicCounts = {};
344
+ const topicWrongRatios = {}; // Store wrong ratios for each topic
345
+
346
+ Object.keys(data).forEach(sessionId => {
347
+ const subjects = data[sessionId].subjects;
348
+ Object.keys(subjects).forEach(subject => {
349
+ Object.keys(subjects[subject]).forEach(topic => {
350
+ const key = `${subject} - ${topic}`;
351
+ topicCounts[key] = (topicCounts[key] || 0) + subjects[subject][topic];
352
+
353
+ // Store topic occurrence ratio for this topic (total occurrences / number of sessions with topic)
354
+ if (data[sessionId].wrong_ratios &&
355
+ data[sessionId].wrong_ratios[subject] &&
356
+ data[sessionId].wrong_ratios[subject][topic]) {
357
+ const topicData = data[sessionId].wrong_ratios[subject][topic];
358
+ topicWrongRatios[key] = `${topicData.total_occurrences}/${topicData.total_sessions_with_topic}`;
359
+ } else if (!topicWrongRatios[key]) {
360
+ // Initialize with default if not set
361
+ topicWrongRatios[key] = '0/0';
362
+ }
363
+ });
364
+ });
365
+ });
366
+
367
+ // Convert to array and sort by count (highest first)
368
+ const sortedEntries = Object.entries(topicCounts).sort((a, b) => b[1] - a[1]);
369
+ const labels = sortedEntries.map(entry => entry[0]);
370
+ const counts = sortedEntries.map(entry => entry[1]);
371
+ const colors = labels.map(() => getRandomColor());
372
+
373
+ // Get wrong ratios in the same order as labels
374
+ const wrongRatios = labels.map(label => topicWrongRatios[label] || '0/0');
375
+
376
+ const ctx = document.getElementById('topicChart').getContext('2d');
377
+
378
+ // Destroy previous chart instance if exists
379
+ if (topicChartInstance) {
380
+ topicChartInstance.destroy();
381
+ }
382
+
383
+ topicChartInstance = new Chart(ctx, {
384
+ type: 'bar',
385
+ data: {
386
+ labels: labels,
387
+ datasets: [{
388
+ label: 'Total Questions',
389
+ data: counts,
390
+ backgroundColor: colors,
391
+ // Store wrong ratios for tooltip
392
+ wrong_ratios: wrongRatios
393
+ }]
394
+ },
395
+ options: {
396
+ indexAxis: 'y',
397
+ responsive: true,
398
+ plugins: {
399
+ legend: { display: false },
400
+ title: { display: true, text: 'Topic-wise Question Distribution' },
401
+ tooltip: {
402
+ callbacks: {
403
+ label: function(context) {
404
+ const label = context.dataset.label || '';
405
+ const value = context.parsed.x;
406
+ const wrongRatio = context.dataset.wrong_ratios[context.dataIndex] || '0/0';
407
+
408
+ return `${label}: ${value} questions (Wrong: ${wrongRatio})`;
409
+ }
410
+ }
411
+ }
412
+ },
413
+ scales: { x: { title: { display: true, text: 'Count' } } }
414
+ }
415
+ });
416
+ }
417
+
418
+ function populateDataTable(data) {
419
+ const tableBody = document.getElementById('dataTableBody');
420
+ tableBody.innerHTML = ''; // Clear existing data
421
+
422
+ const sessionIds = Object.keys(data);
423
+
424
+ sessionIds.forEach(sessionId => {
425
+ const sessionData = data[sessionId];
426
+ const subjects = Object.keys(sessionData.subjects);
427
+
428
+ // Calculate total number of topic rows across all subjects in this session
429
+ let totalTopicsCount = 0;
430
+ subjects.forEach(subject => {
431
+ const topics = Object.keys(sessionData.subjects[subject]);
432
+ totalTopicsCount += topics.length;
433
+ });
434
+
435
+ let isFirstRowOfSession = true; // Flag to determine if this is the first row of the session
436
+
437
+ subjects.forEach(subject => {
438
+ const topics = Object.keys(sessionData.subjects[subject]);
439
+
440
+ topics.forEach((topic, index) => {
441
+ const row = document.createElement('tr');
442
+
443
+ // Add session ID and session name only on the very first row of each session
444
+ if (isFirstRowOfSession) {
445
+ // Session ID cell - spans across all topic rows in the session
446
+ const sessionIdCell = document.createElement('td');
447
+ sessionIdCell.rowSpan = totalTopicsCount;
448
+ sessionIdCell.textContent = sessionId;
449
+ row.appendChild(sessionIdCell);
450
+
451
+ // Session name cell - spans across all topic rows in the session
452
+ const sessionNameCell = document.createElement('td');
453
+ sessionNameCell.rowSpan = totalTopicsCount;
454
+ sessionNameCell.textContent = sessionData.session_name || sessionId;
455
+ row.appendChild(sessionNameCell);
456
+
457
+ isFirstRowOfSession = false; // Set flag to false after first row
458
+ }
459
+
460
+ // Add subject cell only for the first topic of each subject (with rowSpan for topics in this subject)
461
+ if (index === 0) {
462
+ const subjectTopicsCount = topics.length;
463
+
464
+ // Subject cell - spans across all topics within this subject only
465
+ const subjectCell = document.createElement('td');
466
+ subjectCell.rowSpan = subjectTopicsCount;
467
+ subjectCell.textContent = subject;
468
+ row.appendChild(subjectCell);
469
+ }
470
+
471
+ // Topic cell - always added
472
+ const topicCell = document.createElement('td');
473
+ topicCell.textContent = topic;
474
+ row.appendChild(topicCell);
475
+
476
+ // Question count cell - always added
477
+ const countCell = document.createElement('td');
478
+ countCell.textContent = sessionData.subjects[subject][topic];
479
+ row.appendChild(countCell);
480
+
481
+ // Wrong ratio cell - always added
482
+ const wrongRatioCell = document.createElement('td');
483
+ if (sessionData.wrong_ratios &&
484
+ sessionData.wrong_ratios[subject] &&
485
+ sessionData.wrong_ratios[subject][topic]) {
486
+ const wrongData = sessionData.wrong_ratios[subject][topic];
487
+ wrongRatioCell.textContent = `${wrongData.wrong_count}/${wrongData.wrong_sessions}`;
488
+ } else {
489
+ wrongRatioCell.textContent = '0/0'; // Default if no wrong data
490
+ }
491
+ row.appendChild(wrongRatioCell);
492
+
493
+ tableBody.appendChild(row);
494
+ });
495
+ });
496
+ });
497
+ }
498
+
499
+ // Helper function to generate random colors for chart
500
+ function getRandomColor() {
501
+ const letters = '0123456789ABCDEF';
502
+ let color = '#';
503
+ for (let i = 0; i < 6; i++) {
504
+ color += letters[Math.floor(Math.random() * 16)];
505
+ }
506
+ return color;
507
+ }
508
+ </script>
509
+ {% endblock %}