#!/usr/bin/env python # encoding: utf-8 import dash import dash_html_components as html import dash_core_components as dcc import dash_table from dash.exceptions import PreventUpdate import plotly.graph_objects as go from pathlib import Path from datetime import datetime import pandas as pd import numpy as np from ... import flat_df_students, pp_q_scores from ...config import NO_ST_COLUMNS from ...scripts.getconfig import config from ..app import app COLORS = { ".": "black", 0: "#E7472B", 1: "#FF712B", 2: "#F2EC4C", 3: "#68D42F", } layout = html.Div( children=[ html.Header( children=[ html.H1("Analyse des notes"), html.P("Dernière sauvegarde", id="lastsave"), ], ), html.Main( [ html.Section( [ html.Div( [ "Classe: ", dcc.Dropdown( id="tribe", options=[ {"label": t["name"], "value": t["name"]} for t in config["tribes"] ], value=config["tribes"][0]["name"], ), ], style={ "display": "flex", "flex-flow": "column", }, ), html.Div( [ "Evaluation: ", dcc.Dropdown(id="csv"), ], style={ "display": "flex", "flex-flow": "column", }, ), ], id="select", style={ "display": "flex", "flex-flow": "row wrap", }, ), html.Div( [ html.Div( dash_table.DataTable( id="final_score_table", columns=[ {"id": "Eleve", "name": "Élève"}, {"id": "Note", "name": "Note"}, {"id": "Bareme", "name": "Barème"}, ], data=[], style_data_conditional=[ { "if": {"row_index": "odd"}, "backgroundColor": "rgb(248, 248, 248)", } ], style_data={ "width": "100px", "maxWidth": "100px", "minWidth": "100px", }, ), id="final_score_table_container", ), html.Div( [ dash_table.DataTable( id="final_score_describe", columns=[ {"id": "count", "name": "count"}, {"id": "mean", "name": "mean"}, {"id": "std", "name": "std"}, {"id": "min", "name": "min"}, {"id": "25%", "name": "25%"}, {"id": "50%", "name": "50%"}, {"id": "75%", "name": "75%"}, {"id": "max", "name": "max"}, ], ), dcc.Graph( id="fig_assessment_hist", ), dcc.Graph(id="fig_competences"), ], id="desc_plots", ), ], id="analysis", ), html.Div( [ dash_table.DataTable( id="scores_table", columns=[ {"id": "id", "name": "Question"}, { "id": "competence", "name": "Competence", }, {"id": "theme", "name": "Domaine"}, {"id": "comment", "name": "Commentaire"}, {"id": "score_rate", "name": "Bareme"}, {"id": "is_leveled", "name": "Est_nivele"}, ], style_cell={ "whiteSpace": "normal", "height": "auto", }, fixed_columns={"headers": True, "data": 7}, style_table={"minWidth": "100%"}, style_data_conditional=[], editable=True, ), html.Button("Ajouter un élément", id="btn_add_element"), ], id="big_table", ), dcc.Store(id="final_score"), ], className="content", style={ "width": "95vw", "margin": "auto", }, ), ], ) @app.callback( [ dash.dependencies.Output("csv", "options"), dash.dependencies.Output("csv", "value"), ], [dash.dependencies.Input("tribe", "value")], ) def update_csvs(value): if not value: raise PreventUpdate p = Path(value) csvs = list(p.glob("*.csv")) try: return [{"label": str(c), "value": str(c)} for c in csvs], str(csvs[0]) except IndexError: return [] @app.callback( [ dash.dependencies.Output("final_score", "data"), ], [dash.dependencies.Input("scores_table", "data")], ) def update_final_scores(data): if not data: raise PreventUpdate scores = pd.DataFrame.from_records(data) try: if scores.iloc[0]["Commentaire"] == "commentaire": scores.drop([0], inplace=True) except KeyError: pass scores = flat_df_students(scores).dropna(subset=["Score"]) if scores.empty: return [{}] scores = pp_q_scores(scores) assessment_scores = scores.groupby(["Eleve"]).agg({"Note": "sum", "Bareme": "sum"}) return [assessment_scores.reset_index().to_dict("records")] @app.callback( [ dash.dependencies.Output("final_score_table", "data"), ], [dash.dependencies.Input("final_score", "data")], ) def update_final_scores_table(data): assessment_scores = pd.DataFrame.from_records(data) return [assessment_scores.to_dict("records")] @app.callback( [ dash.dependencies.Output("final_score_describe", "data"), ], [dash.dependencies.Input("final_score", "data")], ) def update_final_scores_descr(data): scores = pd.DataFrame.from_records(data) if scores.empty: return [[{}]] desc = scores["Note"].describe().T.round(2) return [[desc.to_dict()]] @app.callback( [ dash.dependencies.Output("fig_assessment_hist", "figure"), ], [dash.dependencies.Input("final_score", "data")], ) def update_final_scores_hist(data): assessment_scores = pd.DataFrame.from_records(data) if assessment_scores.empty: return [go.Figure(data=[go.Scatter(x=[], y=[])])] ranges = np.linspace( -0.5, assessment_scores.Bareme.max(), int(assessment_scores.Bareme.max() * 2 + 2), ) bins = pd.cut(assessment_scores["Note"], ranges) assessment_scores["Bin"] = bins assessment_grouped = ( assessment_scores.reset_index() .groupby("Bin") .agg({"Bareme": "count", "Eleve": lambda x: "\n".join(x)}) ) assessment_grouped.index = assessment_grouped.index.map(lambda i: i.right) fig = go.Figure() fig.add_bar( x=assessment_grouped.index, y=assessment_grouped.Bareme, text=assessment_grouped.Eleve, textposition="auto", hovertemplate="", marker_color="#4E89DE", ) fig.update_layout( height=300, margin=dict(l=5, r=5, b=5, t=5), ) return [fig] @app.callback( [ dash.dependencies.Output("fig_competences", "figure"), ], [dash.dependencies.Input("scores_table", "data")], ) def update_competence_fig(data): scores = pd.DataFrame.from_records(data) try: if scores.iloc[0]["Commentaire"] == "commentaire": scores.drop([0], inplace=True) except KeyError: pass scores = flat_df_students(scores).dropna(subset=["Score"]) if scores.empty: return [go.Figure(data=[go.Scatter(x=[], y=[])])] scores = pp_q_scores(scores) pt = pd.pivot_table( scores, index=["Exercice", "Question", "Commentaire"], columns="Score", aggfunc="size", fill_value=0, ) for i in {i for i in pt.index.get_level_values(0)}: pt.loc[(str(i), "", ""), :] = "" pt.sort_index(inplace=True) index = ( pt.index.get_level_values(0).map(str) + ":" + pt.index.get_level_values(1).map(str) + " " + pt.index.get_level_values(2).map(str) ) fig = go.Figure() bars = [ {"score": -1, "name": "Pas de réponse", "color": COLORS["."]}, {"score": 0, "name": "Faux", "color": COLORS[0]}, {"score": 1, "name": "Peu juste", "color": COLORS[1]}, {"score": 2, "name": "Presque juste", "color": COLORS[2]}, {"score": 3, "name": "Juste", "color": COLORS[3]}, ] for b in bars: try: fig.add_bar( x=index, y=pt[b["score"]], name=b["name"], marker_color=b["color"] ) except KeyError: pass fig.update_layout(barmode="relative") fig.update_layout( height=500, margin=dict(l=5, r=5, b=5, t=5), ) return [fig] @app.callback( [ dash.dependencies.Output("lastsave", "children"), ], [ dash.dependencies.Input("scores_table", "data"), dash.dependencies.State("csv", "value"), ], ) def save_scores(data, csv): try: scores = pd.DataFrame.from_records(data) scores.to_csv(csv, index=False) except: return [f"Soucis pour sauvegarder à {datetime.today()} dans {csv}"] else: return [f"Dernière sauvegarde {datetime.today()} dans {csv}"] def highlight_value(df): """ Cells style """ hight = [] for v, color in COLORS.items(): hight += [ { "if": {"filter_query": "{{{}}} = {}".format(col, v), "column_id": col}, "backgroundColor": color, "color": "white", } for col in df.columns if col not in NO_ST_COLUMNS.values() ] return hight @app.callback( [ dash.dependencies.Output("scores_table", "columns"), dash.dependencies.Output("scores_table", "data"), dash.dependencies.Output("scores_table", "style_data_conditional"), ], [ dash.dependencies.Input("csv", "value"), dash.dependencies.Input("btn_add_element", "n_clicks"), dash.dependencies.State("scores_table", "data"), ], ) def update_scores_table(csv, add_element, data): ctx = dash.callback_context if ctx.triggered[0]["prop_id"] == "csv.value": stack = pd.read_csv(csv, encoding="UTF8") elif ctx.triggered[0]["prop_id"] == "btn_add_element.n_clicks": stack = pd.DataFrame.from_records(data) infos = pd.DataFrame.from_records( [{k: stack.iloc[-1][k] for k in NO_ST_COLUMNS.values()}] ) stack = stack.append(infos) return ( [ {"id": c, "name": c} for c in stack.columns if c not in ["Trimestre", "Nom", "Date"] ], stack.to_dict("records"), highlight_value(stack), )