Source code for class_factory.quiz_maker.quiz_viz

"""
quiz_viz.py
-----------

This module provides visualization utilities for quiz results, including:
- An interactive dashboard (using Dash and Plotly) to display summary statistics and per-question analysis.
- Generation of static HTML reports with embedded plots and summary tables for quiz assessments.

Key Functions:
- `generate_dashboard`: Launches a Dash web app for interactive quiz result exploration.
- `generate_html_report`: Creates a static HTML report with summary statistics and per-question plots.
- `create_question_figure`: Generates Plotly figures for individual quiz questions.

Dependencies: pandas, plotly, dash, jinja2 (for HTML reports).
"""

from pathlib import Path

import pandas as pd
# plot setup
import plotly.express as px
import plotly.graph_objects as go
from dash import Dash, dash_table, dcc, html


[docs] def generate_dashboard(df: pd.DataFrame, summary: pd.DataFrame, test_mode: bool = False) -> None: """ Launch an interactive dashboard using Dash to display quiz summary statistics and per-question plots. Args: df (pd.DataFrame): DataFrame containing quiz responses. test_mode (bool): If True, does not launch the server (for testing purposes). summary (pd.DataFrame): DataFrame containing summary statistics. """ app = Dash(__name__) # Create a list of plotly figures for each question figures = [] for question in df['question'].unique(): fig = create_question_figure(df, question) figures.append({'question': question, 'figure': fig}) # Create a list of dcc.Graph components graphs = [] for item in figures: graph = html.Div([ html.H3(item['question'], style={'textAlign': 'center'}), dcc.Graph(figure=item['figure'], style={'height': '350px'}) ], style={ 'width': '23%', # Adjust width for 3-4 plots per row # Removed 'display: inline-block' to avoid stacking issues 'verticalAlign': 'top', 'margin': '1%', 'boxSizing': 'border-box', 'height': '420px', # Fixed height for each graph container 'overflow': 'hidden', 'display': 'flex', 'flexDirection': 'column', 'justifyContent': 'flex-start' }) graphs.append(graph) # Layout of the dashboard app.layout = html.Div([ html.H1('Quiz Assessment Dashboard', style={'textAlign': 'center'}), html.H2('Summary Statistics'), dash_table.DataTable( data=summary.to_dict('records'), columns=[{'name': i, 'id': i} for i in summary.columns], style_table={'overflowX': 'auto'}, style_cell={'textAlign': 'left'}, ), html.H2('Question Analysis'), html.Div(children=graphs, style={ 'display': 'flex', 'flexWrap': 'wrap', 'justifyContent': 'space-around', 'alignItems': 'flex-start', 'width': '100%' }) ]) # Run the Dash app if not test_mode: app.run(debug=False) print("Access server at\nhttp://127.0.0.1:8050")
[docs] def generate_html_report(df: pd.DataFrame, summary: pd.DataFrame, output_dir: Path, quiz_df: pd.DataFrame = None) -> None: """ Generate a static HTML report with summary statistics and per-question plots for quiz results. Args: df (pd.DataFrame): DataFrame containing quiz responses. summary (pd.DataFrame): DataFrame containing summary statistics. output_dir (Path): Directory where the report will be saved. quiz_df (pd.DataFrame, optional): DataFrame containing the quiz questions and options (for option text in plots). """ from jinja2 import Environment, FileSystemLoader # Prepare plots plots = [] for question in df['question'].unique(): fig = create_question_figure(df, question, quiz_df) # Convert the figure to HTML div string plot_html = fig.to_html(full_html=False, include_plotlyjs=False) plots.append({'question': question, 'plot_html': plot_html}) # Set up Jinja2 environment env = Environment(loader=FileSystemLoader('.')) template = env.from_string(''' <!DOCTYPE html> <html> <head> <title>Quiz Assessment Report</title> <meta charset="utf-8"> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <style> body { font-family: Arial, sans-serif; margin: 20px; } .plot-container { display: flex; flex-wrap: wrap; justify-content: space-around; } .plot-item { width: 23%; margin: 1%; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; } th { background-color: #f2f2f2; } h2 { margin-top: 40px; } @media screen and (max-width: 768px) { .plot-item { width: 45%; } } @media screen and (max-width: 480px) { .plot-item { width: 100%; } } </style> </head> <body> <h1 style="text-align: center;">Quiz Assessment Report</h1> <h2>Summary Statistics</h2> {{ summary_table }} <h2>Question Analysis</h2> <div class="plot-container"> {% for item in plots %} <div class="plot-item"> <h3 style="text-align: center;">{{ item.question }}</h3> {{ item.plot_html | safe }} </div> {% endfor %} </div> </body> </html> ''') # Convert summary DataFrame to HTML table with styling summary_table = summary.to_html(index=False, classes='summary-table') # Render the template html_content = template.render(summary_table=summary_table, plots=plots) # Save the report to an HTML file report_path = output_dir / 'quiz_report.html' with open(report_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f"Report saved to '{report_path}'")
[docs] def create_question_figure(df: pd.DataFrame, question_text: str, quiz_df: pd.DataFrame = None) -> go.Figure: """ Create a Plotly bar chart for a specific quiz question, showing answer distribution and correctness. Args: df (pd.DataFrame): DataFrame containing quiz responses. question_text (str): The question text to plot. quiz_df (pd.DataFrame, optional): DataFrame containing the quiz questions and options (for answer text mapping). Returns: go.Figure: Plotly figure object visualizing answer distribution for the question. """ # Standardize answer keys in user_answer and correct_answer to be 'A', 'B', 'C', 'D' def standardize_key(val): if isinstance(val, str): v = val.strip() if v.endswith(')') and len(v) == 2 and v[0] in 'ABCD': return v[0] if v in 'ABCD': return v return val question_df = df[df['question'] == question_text].copy() question_df['user_answer'] = question_df['user_answer'].apply(standardize_key) correct_answer = standardize_key(question_df['correct_answer'].iloc[0]) # Determine all possible options if quiz_df is not None: quiz_question = quiz_df[quiz_df['question'] == question_text] if not quiz_question.empty: option_columns = ['A)', 'B)', 'C)', 'D)'] options_list = [] option_labels = [] for col in option_columns: if col in quiz_question.columns: option_text = str(quiz_question.iloc[0][col]) if pd.notna(option_text) and option_text.strip() != '': options_list.append(option_text.strip()) option_labels.append(col[0]) # 'A)' -> 'A' else: options_list = None option_labels = None else: options_list = None option_labels = None # Determine possible options if option_labels is not None: possible_options = option_labels else: question_df['user_answer'] = question_df['user_answer'].fillna('No Answer') possible_options = sorted(set(question_df['user_answer'].unique()).union(set([correct_answer]))) options_df = pd.DataFrame({'user_answer': possible_options}) answer_counts = question_df.groupby('user_answer').size().reset_index(name='counts') answer_counts = pd.merge(options_df, answer_counts, on='user_answer', how='left').fillna(0) answer_counts['counts'] = answer_counts['counts'].astype(int) # Determine if each answer is correct answer_counts['is_correct'] = answer_counts['user_answer'] == correct_answer # Map user_answer to option_text if available if options_list is not None and option_labels is not None: option_map = dict(zip(option_labels, options_list)) answer_counts['option_text'] = answer_counts['user_answer'].map(option_map) else: answer_counts['option_text'] = answer_counts['user_answer'] # Create the bar chart fig = px.bar( answer_counts, x='user_answer', y='counts', color='is_correct', color_discrete_map={True: 'green', False: 'red'}, labels={'user_answer': 'Answer Option', 'counts': 'Number of Responses'}, category_orders={'user_answer': possible_options} ) # Update hover data to include option_text fig.update_traces( hovertemplate='Option: %{x}<br>Option Text: %{customdata[0]}<br>Responses: %{y}', customdata=answer_counts[['option_text']].values ) fig.update_layout( xaxis_title='Answer Options', yaxis_title='Number of Responses', title_x=0.5, legend_title_text='Correct Answer', margin=dict(l=20, r=20, t=40, b=20), height=400 # Fix the height to prevent dashboard from growing ) # Update legend labels fig.for_each_trace(lambda t: t.update(name='Correct' if t.name == 'True' else 'Incorrect')) return fig