001package com.studentgui.apppages; 002 003import java.awt.BorderLayout; 004import java.awt.Font; 005import java.awt.GridBagConstraints; 006import java.awt.GridBagLayout; 007import java.awt.Insets; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.sql.SQLException; 011import java.time.LocalDate; 012 013import javax.swing.JButton; 014import javax.swing.JLabel; 015import javax.swing.JOptionPane; 016import javax.swing.JPanel; 017import javax.swing.JScrollPane; 018import javax.swing.SwingUtilities; 019 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023/** 024 * Abacus computational skills assessment page. 025 * 026 * <p>Provides a structured interface for evaluating student proficiency with the Cranmer 027 * Abacus across 22 standardized skills organized into 8 progressive competency phases:</p> 028 * 029 * <ul> 030 * <li><b>Phase 1 (P1_1–P1_4):</b> Foundational bead manipulation (setting, clearing, place value, vocabulary)</li> 031 * <li><b>Phase 2 (P2_1–P2_3):</b> Single-digit addition (direct and indirect methods)</li> 032 * <li><b>Phase 3 (P3_1–P3_3):</b> Single-digit subtraction (direct and indirect methods)</li> 033 * <li><b>Phase 4 (P4_1–P4_2):</b> Multiplication with multi-digit operands</li> 034 * <li><b>Phase 5 (P5_1–P5_2):</b> Division with multi-digit operands</li> 035 * <li><b>Phase 6 (P6_1–P6_4):</b> Decimal arithmetic (all four operations)</li> 036 * <li><b>Phase 7 (P7_1–P7_4):</b> Fraction arithmetic (all four operations)</li> 037 * <li><b>Phase 8 (P8_1–P8_2):</b> Advanced operations (percentages, square roots)</li> 038 * </ul> 039 * 040 * <p><b>Data Persistence and Export:</b></p> 041 * <ul> 042 * <li>Skill scores are captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)</li> 043 * <li>Submit button persists values to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li> 044 * <li>Session data exported to timestamped JSON in {@code StudentDataFiles/<student>/Sessions/Abacus/}</li> 045 * <li>Per-phase time-series plots generated and saved to {@code plots/} directory</li> 046 * <li>Comprehensive Markdown and HTML reports generated with embedded phase plots and color-coded legends</li> 047 * </ul> 048 * 049 * <p><b>Report Artifacts:</b></p> 050 * <ul> 051 * <li><b>JSON export:</b> {@code Abacus-<sessionId>-<timestamp>.json} with session envelope</li> 052 * <li><b>Phase group plots:</b> {@code Abacus-<sessionId>-<date>-P<N>.png} (8 PNG images)</li> 053 * <li><b>Markdown report:</b> {@code reports/Abacus-<sessionId>-<date>.md} with relative image links</li> 054 * <li><b>HTML report:</b> {@code reports/Abacus-<sessionId>-<date>.html} with inline styles and legends</li> 055 * </ul> 056 * 057 * <p>The shared {@link JLineGraph} visualizes recent session trends, grouping skills by phase prefix 058 * to maintain chart readability. Implements {@link com.studentgui.app.DateChangeListener} and 059 * {@link com.studentgui.app.StudentChangeListener} for dynamic updates when global selections change.</p> 060 * 061 * @see com.studentgui.apphelpers.Database 062 * @see JLineGraph 063 * @see com.studentgui.uicomp.PhaseScoreField 064 */ 065public class Abacus extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { 066 private static final Logger LOG = LoggerFactory.getLogger(Abacus.class); 067 068 /** Array of input components for each skill. */ 069 private final com.studentgui.uicomp.PhaseScoreField[] skillFields; 070 /** Canonical list of abacus assessment parts: code and display label. */ 071 private final String[][] parts; 072 /** Shared graph component used to visualize recent results. */ 073 private final JLineGraph lineGraph; // Reference to the JLineGraph instance 074 /** Selected student display name (may be null). */ 075 private String studentNameParam; 076 /** Session date associated with persisted progress. */ 077 private LocalDate dateParam; 078 /** 079 * Title label shown at the top of the page. 080 */ 081 private JLabel titleLabel; 082 /** 083 * Base title text used when rendering the page header (date suffixes are appended). 084 */ 085 private final String baseTitle = "Abacus Skills Progression"; 086 087 /** 088 * Construct the Abacus page for the given student and session date. 089 * 090 * @param studentName the selected student's display name (may be null before selection) 091 * @param date the date to associate with created progress sessions 092 * @param lineGraph the shared graph component used to visualize results 093 */ 094 public Abacus(final String studentName, final LocalDate date, final JLineGraph lineGraph) { 095 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 096 this.dateParam = date; 097 this.lineGraph = lineGraph; // Use the passed in graph instance 098 setLayout(new BorderLayout()); 099 100 // Initialize skills array and layout using canonical abacus parts 101 this.parts = new String[][]{ 102 {"P1_1","1.1 Setting Numbers"},{"P1_2","1.2 Clearing Beads"},{"P1_3","1.3 Place Value"},{"P1_4","1.4 Vocabulary"}, 103 {"P2_1","2.1 Addition of Single Digit Numbers"},{"P2_2","2.2 Direct Addition"},{"P2_3","2.3 Indirect Addition"}, 104 {"P3_1","3.1 Subtraction of Single Digit Numbers"},{"P3_2","3.2 Direct Subtraction"},{"P3_3","3.3 Indirect Subtraction"}, 105 {"P4_1","4.1 Multiplication – 2+ Digit Multiplicand 1-Digit Multiplier"},{"P4_2","4.2 Multiplication – 2+ Digit Multiplicand AND Multiplier"}, 106 {"P5_1","5.1 Division – 2+ Digit Dividend 1-Digit Divisor"},{"P5_2","5.2 Division – 2+ Digit Dividend AND 1 Digit Divisor"}, 107 {"P6_1","6.1 Addition of Decimals"},{"P6_2","6.2 Subtraction of Decimals"},{"P6_3","6.3 Multiplication of Decimals"},{"P6_4","6.4 Division of Decimals"}, 108 {"P7_1","7.1 Addition of Fractions"},{"P7_2","7.2 Subtraction of Fractions"},{"P7_3","7.3 Multiplication of Fractions"},{"P7_4","7.4 Division of Fractions"}, 109 {"P8_1","8.1 Percent"},{"P8_2","8.2 Square Root"} 110 }; 111 112 // Panel for data entry 113 JPanel dataEntryPanel = new JPanel(); 114 dataEntryPanel.setLayout(new GridBagLayout()); 115 JPanel view = new JPanel(new BorderLayout()); 116 view.add(dataEntryPanel, BorderLayout.NORTH); 117 view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); 118 JScrollPane dataEntryScrollPane = new JScrollPane(view); 119 dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 120 dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 121 dataEntryScrollPane.getAccessibleContext().setAccessibleName("Abacus data entry scroll pane"); 122 123 GridBagConstraints gbc = new GridBagConstraints(); 124 gbc.insets = new Insets(2, 2, 2, 2); 125 gbc.fill = GridBagConstraints.HORIZONTAL; 126 gbc.weightx = 1.0; 127 gbc.weighty = 0.0; 128 129 this.titleLabel = new JLabel(baseTitle); 130 this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 28f)); 131 gbc.gridx = 0; 132 gbc.gridy = 0; 133 gbc.gridwidth = GridBagConstraints.REMAINDER; 134 dataEntryPanel.add(titleLabel, gbc); 135 136 gbc.gridy = 1; 137 gbc.gridwidth = GridBagConstraints.REMAINDER; 138 gbc.ipady = 20; 139 dataEntryPanel.add(new JPanel(), gbc); 140 141 // visual spacing controlled by PhaseScoreField and layout 142 143 String[] labels = java.util.Arrays.stream(this.parts).map(x->x[1]).toArray(String[]::new); 144 int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels); 145 com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); 146 skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; 147 for (int i = 0; i < this.parts.length; i++) { 148 gbc.gridy = i + 2; 149 gbc.gridx = 0; 150 gbc.gridwidth = 1; 151 com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); 152 field.setName("abacus_" + this.parts[i][0]); 153 field.getAccessibleContext().setAccessibleName(this.parts[i][1]); 154 field.setToolTipText("Enter a numeric score for " + this.parts[i][1]); 155 gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); 156 dataEntryPanel.add(field, gbc); 157 skillFields[i] = field; 158 gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(2, 0, 2, 2); 159 dataEntryPanel.add(new JPanel(), gbc); 160 } 161 162 gbc.gridy = this.parts.length + 3; 163 gbc.gridx = 0; 164 gbc.gridwidth = GridBagConstraints.REMAINDER; 165 gbc.weighty = 1.0; 166 dataEntryPanel.add(new JPanel(), gbc); 167 168 // Place Submit and Open Latest side-by-side with IOS-like height 169 gbc.gridy = this.parts.length + 4; 170 gbc.weighty = 0.0; 171 gbc.gridx = 0; 172 gbc.gridwidth = 1; 173 JButton submitDataButton = new JButton("Submit Data"); 174 submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); 175 submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); 176 submitDataButton.setMnemonic(KeyEvent.VK_S); 177 submitDataButton.setToolTipText("Save Abacus scores for the selected student (Alt+S)"); 178 submitDataButton.getAccessibleContext().setAccessibleName("Submit Abacus Data"); 179 dataEntryPanel.add(submitDataButton, gbc); 180 181 gbc.gridx = 1; 182 JButton openLatestBtn = new JButton("Open Latest Plot"); 183 openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); 184 openLatestBtn.addActionListener((ActionEvent e) -> { 185 java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Abacus"); 186 if (p == null) { 187 com.studentgui.apphelpers.UiNotifier.show("No Abacus plot found for student"); 188 } else { 189 try { 190 java.awt.Desktop.getDesktop().open(p.toFile()); 191 } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { 192 com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); 193 } 194 } 195 }); 196 dataEntryPanel.add(openLatestBtn, gbc); 197 198 gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; 199 dataEntryPanel.add(new JPanel(), gbc); 200 201 add(dataEntryScrollPane, BorderLayout.CENTER); 202 203 // Add existing graph reference 204 add(lineGraph, BorderLayout.SOUTH); 205 206 SwingUtilities.invokeLater(() -> { 207 dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); 208 updateTitleDate(); 209 revalidate(); 210 }); 211 212 // Ensure application folders and DB schema exist before DB operations 213 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 214 initDatabase(); 215 refreshGraph(); 216 } 217 218 /** 219 * Ensure the canonical progress-type and assessment parts for Abacus exist 220 * in the normalized database schema. Safe to call multiple times. 221 */ 222 private void initDatabase() { 223 try { 224 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); 225 // Use the canonical part codes declared on this page so parts are created 226 // with the expected codes like "P1_1", "P1_2", ... 227 String[] codes = new String[this.parts.length]; 228 for (int i = 0; i < this.parts.length; i++) { 229 codes[i] = this.parts[i][0]; 230 } 231 com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); 232 try { 233 com.studentgui.apphelpers.Database.cleanupAssessmentParts(ptId, codes); 234 } catch (SQLException se) { 235 LOG.warn("Could not cleanup legacy parts for Abacus", se); 236 } 237 } catch (SQLException e) { 238 LOG.error("SQL error initializing Abacus parts", e); 239 } 240 } 241 242 /** 243 * Read input fields, validate numeric input, and persist the values as a 244 * new progress session for the selected student. 245 */ 246 private void submitData() { 247 if (studentNameParam == null || studentNameParam.trim().isEmpty()) { 248 JOptionPane.showMessageDialog(this, "Please select a student before submitting Abacus data.", "Missing student", JOptionPane.WARNING_MESSAGE); 249 return; 250 } 251 252 try { 253 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); 254 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); 255 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); 256 257 String[] codes = new String[this.parts.length]; 258 int[] scores = new int[this.parts.length]; 259 for (int i = 0; i < this.parts.length; i++) { 260 codes[i] = this.parts[i][0]; 261 scores[i] = skillFields[i].getValue(); 262 } 263 com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); 264 LOG.info("Data submitted successfully via normalized schema."); 265 com.studentgui.apphelpers.UiNotifier.show("Abacus data saved."); 266 // Also persist this session as a JSON file in the student's folder (timestamped per-session) 267 com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); 268 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Abacus", payload, sessionId); 269 if (jsonOut == null) { 270 LOG.warn("Unable to save Abacus session JSON for sessionId={}", sessionId); 271 } 272 // Generate per-phase PNGs (time-series) and a markdown report for this session 273 try { 274 java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); 275 java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); 276 java.nio.file.Files.createDirectories(plotsOut); 277 java.nio.file.Files.createDirectories(reportsOut); 278 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 279 String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); 280 String baseName = "Abacus-" + sessionId + "-" + dateStr; 281 282 // Fetch recent dated sessions (oldest first) to build time-series plots. 283 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Abacus", Integer.MAX_VALUE); 284 285 java.util.Map<String, java.nio.file.Path> groups = null; 286 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 287 // Build human-friendly labels from this.parts and render time-series grouped charts 288 String[] labels = new String[this.parts.length]; 289 for (int i = 0; i < this.parts.length; i++) { 290 labels[i] = this.parts[i][1]; 291 } 292 lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); 293 // Persist each group as a PNG (time-series image) 294 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 295 // Use the most-recent session date for the report header if available 296 java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); 297 dateStr = headerDate.format(df); 298 } else { 299 // Fallback: render only the latest session snapshot 300 java.util.List<java.util.List<Integer>> rows = new java.util.ArrayList<>(); 301 java.util.List<Integer> latest = new java.util.ArrayList<>(); 302 for (int v : scores) { 303 latest.add(v); 304 } 305 rows.add(latest); 306 lineGraph.updateWithGroupedData(rows, codes); 307 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 308 } 309 310 // Generate markdown report 311 if (groups == null) { 312 groups = new java.util.LinkedHashMap<>(); 313 } 314 StringBuilder md = new StringBuilder(); 315 md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); 316 for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) { 317 md.append("## ").append(e.getKey()).append("\n\n"); 318 md.append(".append(e.getValue().getFileName().toString()).append(")\n\n"); 319 } 320 java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); 321 // images live in ../plots relative to reports 322 String mdText = md.toString().replace("; 323 java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); 324 LOG.info("Wrote Abacus session report {} with {} group images", mdFile, groups.size()); 325 // Also produce a simple HTML report that embeds the PNGs and 326 // shows a scrollable legend under each plot. 327 try { 328 String[] palette = new String[] {"#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"}; 329 330 // Build a map of group -> list of part indexes to recreate legend order 331 java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>(); 332 for (int i = 0; i < codes.length; i++) { 333 String code = codes[i]; 334 String grp = code != null && code.contains("_") ? code.split("_")[0] : code; 335 groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); 336 } 337 338 StringBuilder html = new StringBuilder(); 339 html.append("<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>"); 340 html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr); 341 html.append("</title>"); 342 html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>"); 343 html.append("</head><body>"); 344 html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>"); 345 346 for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) { 347 String grp = e2.getKey(); 348 String imgName = e2.getValue().getFileName().toString(); 349 html.append("<h2>").append(grp).append("</h2>"); 350 html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>"); 351 352 // legend for this group 353 java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); 354 html.append("<div class=\"legend\">"); 355 for (int s = 0; s < idxs.size(); s++) { 356 int idx = idxs.get(s); 357 String code = codes[idx]; 358 String human = this.parts[idx][1]; 359 String seriesName = code + " - " + human; 360 String color = palette[s % palette.length]; 361 html.append("<div class=\"legend-item\"><span class=\"swatch\" style=\"background:" + color + ";\"></span>"); 362 html.append("<div>").append(seriesName).append("</div></div>"); 363 } 364 html.append("</div>"); 365 } 366 367 html.append("</body></html>"); 368 java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); 369 // adjust image src to point to ../plots 370 String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); 371 java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); 372 LOG.info("Wrote Abacus HTML session report {}", htmlFile); 373 } catch (java.io.IOException ioex) { 374 LOG.warn("Unable to write HTML report: {}", ioex.toString()); 375 } 376 } catch (java.io.IOException | SQLException ex) { 377 LOG.warn("Unable to save Abacus per-phase plots or markdown report: {}", ex.toString()); 378 } 379 } catch (SQLException e) { 380 LOG.error("SQL error in submitData", e); 381 JOptionPane.showMessageDialog(this, "Database error saving Abacus data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); 382 } 383 } 384 385 /** 386 * Load recent assessment sessions for the selected student and update the 387 * shared {@link JLineGraph} with the returned metric series. 388 */ 389 private void refreshGraph() { 390 try { 391 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(studentNameParam, "Abacus", Integer.MAX_VALUE); 392 String[] codes = new String[this.parts.length]; 393 for (int i = 0; i < this.parts.length; i++) { 394 codes[i] = this.parts[i][0]; 395 } 396 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 397 // Use the date-aware grouped plotter so X axis is dates and each 398 // skill within a phase is a separate line series. 399 String[] labels = new String[this.parts.length]; 400 for (int i = 0; i < this.parts.length; i++) { 401 labels[i] = this.parts[i][1]; 402 } 403 lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); 404 LOG.debug("Graph updated with {} dated sessions", rwd.rows.size()); 405 } else { 406 LOG.info("No data to plot; showing grouped placeholders."); 407 lineGraph.showEmptyGrouped(codes); 408 } 409 } catch (SQLException e) { 410 LOG.error("SQL error refreshing graph", e); 411 } 412 } 413 @Override 414 /** 415 * Update the displayed date for the Abacus page and refresh content. 416 * 417 * Records `dateParam` and schedules a UI refresh on the Swing EDT so the 418 * graph and title display the selected date. Note: some pages refresh 419 * recent sessions independent of the selected date; this call keeps the 420 * saved date in sync for subsequent actions. 421 * 422 * @param newDate the date to display (may be null to use current date) 423 */ 424 425 public void dateChanged(final LocalDate newDate) { 426 this.dateParam = newDate; 427 // When the global date changes, update the graph to reflect any 428 // date-related logic (most refreshGraph implementations load 429 // recent sessions independent of the selected session date, but 430 // updating here keeps the saved date in sync for future submits). 431 SwingUtilities.invokeLater(this::refreshGraph); 432 } 433 434 @Override 435 /** 436 * Update the selected student for the Abacus page and refresh content. 437 * 438 * Sets `studentNameParam` and posts a UI refresh on the Swing EDT to 439 * reload data and update the page title. 440 * 441 * @param newStudent student identifier (name or id) to display; may be null 442 */ 443 444 public void studentChanged(final String newStudent) { 445 this.studentNameParam = newStudent; 446 SwingUtilities.invokeLater(() -> { 447 refreshGraph(); 448 updateTitleDate(); 449 }); 450 } 451 452 private void updateTitleDate() { 453 try { 454 String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); 455 this.titleLabel.setText(baseTitle + " - " + dateStr); 456 } catch (Exception ex) { 457 this.titleLabel.setText(baseTitle); 458 } 459 } 460 461 462}