1class AGREEMENT_DATA{ 2 constructor(status_id){ 3 this.status_id = status_id; 4 this.label_array = []; 5 this.selected_label_array = []; 6 this.paper_id_array = []; 7 this.raters_identifiers = []; 8 this.data_array = []; 9 10 // Overall Metrics. 11 this.agreement_percentage = []; 12 this.fleiss_kappa_stats = []; 13 this.fleiss_kappa_not_calculable_reason = null; 14 15 // Agreement ofr individual docs. 16 this.individual_agreement_array = []; 17 this.individual_agreement_colours = []; 18 19 // Raters correlation. 20 this.corr_matrix_stats = []; 21 this.t_matrix_stats = []; 22 this.p_matrix_stats = []; 23 this.matrix_colours = []; 24 25 // Miscellaneous. 26 this.list_of_colours = [ 27 "#4CAF50", // Green 28 "#42A5F5", // Blue 29 "#FF5252", // Red 30 "#FFD740", // Yellow 31 "#FF4081", // Pink 32 "#8BC34A", // Light Green 33 "#FF9800", // Orange 34 "#9C27B0", // Purple 35 "#795548", // Brown 36 "#00BCD4", // Cyan 37 "#FF5722", // Deep Orange 38 "#3F51B5", // Indigo 39 "#9E9E9E", // Grey 40 "#607D8B", // Blue Grey 41 "#F44336", // Dark Red 42 "#2196F3", // Dark Blue 43 "#00ACC1", // Dark Cyan 44 "#9C27B0" // Dark Purple 45 ]; 46 47 //Resolution labelling. 48 this.currentIndex = 0; 49 this.indexes_to_label = []; 50 this.view_state = {}; 51 this._save_meta = null; 52 53 } 54 55 _normalize_binary_label(value) { 56 if (value === 0 || value === "0" || value === false || value === "false") return 0; 57 if (value === 1 || value === "1" || value === true || value === "true") return 1; 58 return null; 59 } 60 61 _normalize_selected_flag(value, fallback = true) { 62 if (value == null || value === "") return fallback; 63 if (value === true || value === 1 || value === "1") return true; 64 if (value === false || value === 0 || value === "0") return false; 65 66 const normalized = String(value).trim().toLowerCase(); 67 if (["true", "yes", "y", "selected"].includes(normalized)) return true; 68 if (["false", "no", "n", "unselected"].includes(normalized)) return false; 69 return fallback; 70 } 71 72 _normalize_rater_identifier(identifier, fallbackIndex = 0) { 73 const normalized = String(identifier ?? "").trim(); 74 return normalized || `Rater ${fallbackIndex + 1}`; 75 } 76 77 _normalize_paper_id_key(paperId, fallback = "") { 78 const utils = globalThis?.AIMReviewIdentifierUtils; 79 const normalized = utils?.getPaperIdentifierMatchKey 80 ? utils.getPaperIdentifierMatchKey(paperId) 81 : String(paperId ?? "").trim(); 82 return normalized || fallback; 83 } 84 85 _calculate_agreement_for_labels(labels) { 86 const validLabels = (Array.isArray(labels) ? labels : []) 87 .map(label => this._normalize_binary_label(label)) 88 .filter(label => label === 0 || label === 1); 89 90 if (validLabels.length < 2) return null; 91 92 const labelCounts = {};
93 validLabels.forEach(label => { 94 labelCounts[label] = (labelCounts[label] || 0) + 1; 95 }); 96 97 const agreementCount = Math.max(...Object.values(labelCounts)); 98 const agreement = 100 * agreementCount / validLabels.length; 99 return Number.isFinite(agreement) ? agreement : null; 100 } 101 102 _get_individual_agreement_for_entry_index(index) { 103 const row = Array.isArray(this.label_array?.[index]) ? this.label_array[index] : []; 104 return this._calculate_agreement_for_labels(row); 105 } 106 107 get_individual_agreement_for_position(position) { 108 const normalizedPosition = Math.max(0, Math.floor(Number(position) || 0)); 109 const entryIndex = Array.isArray(this.indexes_to_label) 110 ? this.indexes_to_label[normalizedPosition] 111 : null; 112 return this._get_individual_agreement_for_entry_index(entryIndex); 113 } 114 115 _is_agreement_results_metadata_header(header) { 116 const normalized = String(header ?? "").trim().toLowerCase(); 117 const compact = normalized.replace(/[^a-z0-9%]+/g, ""); 118 return [ 119 "", 120 "paperid", 121 "sheet", 122 "sourcefile", 123 "filename", 124 "file", 125 "individualagreement", 126 "individualagreement%" 127 ].includes(compact); 128 } 129 130 _get_previous_agreement_rater_headers(labelData = []) { 131 const headers = Object.keys(labelData?.[0] || {}); 132 const agreementColumnIndex = headers.findIndex(header => 133 String(header ?? "").trim().toLowerCase() === "individual agreement (%)" 134 ); 135 const orderedHeaders = agreementColumnIndex >= 0 136 ? headers.slice(0, agreementColumnIndex) 137 : headers; 138 139 const candidateHeaders = orderedHeaders.filter(header => 140 !this._is_agreement_results_metadata_header(header) 141 ); 142 const headersWithLabels = candidateHeaders.filter(header => 143 labelData.some(row => { 144 const label = this._normalize_binary_label(row?.[header]); 145 return label === 0 || label === 1; 146 }) 147 ); 148 149 return headersWithLabels.length >= 2 ? headersWithLabels : candidateHeaders; 150 } 151 152 _register_rater_identifier(identifier) { 153 const normalized = this._normalize_rater_identifier(identifier, this.raters_identifiers.length); 154 const existingIndex = this.raters_identifiers.indexOf(normalized); 155 if (existingIndex !== -1) { 156 return existingIndex; 157 } 158 159 this.raters_identifiers.push(normalized); 160 this.label_array = (Array.isArray(this.label_array) ? this.label_array : []).map(row => { 161 const padded = Array.isArray(row) ? [...row] : []; 162 while (padded.length < this.raters_identifiers.length) { 163 padded.push(null); 164 } 165 return padded; 166 }); 167 168 return this.raters_identifiers.length - 1; 169 } 170 171 _pad_label_rows() { 172 const width = Array.isArray(this.raters_identifiers) ? this.raters_identifiers.length : 0; 173 this.label_array = (Array.isArray(this.label_array) ? this.label_array : []).map(row => { 174 const padded = Array.isArray(row) ? [...row] : []; 175 while (padded.length < width) { 176 padded.push(null); 177 } 178 return padded.slice(0, width); 179 }); 180 } 181 182 _get_valid_labels_for_index(index) { 183 const row = Array.isArray(this.label_array?.[index]) ? this.label_array[index] : []; 184 return row.filter(label => label === 0 || label === 1); 185 } 186 187 _get_comparable_indexes() { 188 return (Array.isArray(this.data_array) ? this.data_array : []) 189 .map((entry, index) => this._get_valid_labels_for_index(index).length >= 2 ? index : -1) 190 .filter(index => index !== -1); 191 } 192 193 _build_pairwise_arrays(firstRaterIndex, secondRaterIndex) { 194 const first = []; 195 const second = []; 196 197 (Array.isArray(this.indexes_to_label) ? this.indexes_to_label : []).forEach(index => { 198 const row = Array.isArray(this.label_array?.[index]) ? this.label_array[index] : []; 199 const firstLabel = this._normalize_binary_label(row[firstRaterIndex]); 200 const secondLabel = this._normalize_binary_label(row[secondRaterIndex]); 201 if ((firstLabel === 0 || firstLabel === 1) && (secondLabel === 0 || secondLabel === 1)) { 202 first.push(firstLabel); 203 second.push(secondLabel); 204 } 205 });
206 207 return [first, second]; 208 } 209 210 _sanitize_metric_tuple(values, length = 3) { 211 const tuple = Array.isArray(values) ? values.slice(0, length) : []; 212 while (tuple.length < length) { 213 tuple.push(null); 214 } 215 return tuple.map(value => { 216 const number = Number(value); 217 return Number.isFinite(number) ? number : null; 218 }); 219 } 220 221 _get_paper_id_array() { 222 return (Array.isArray(this.data_array) ? this.data_array : []).map(entry => entry?.paper_id ?? null); 223 } 224 225 _get_selected_label_array() { 226 return (Array.isArray(this.indexes_to_label) ? this.indexes_to_label : []) 227 .map(index => this._get_valid_labels_for_index(index)) 228 .filter(row => Array.isArray(row) && row.length >= 2); 229 } 230 231 _has_missing_labels_for_fleiss() { 232 const raterCount = Array.isArray(this.raters_identifiers) ? this.raters_identifiers.length : 0; 233 if (raterCount < 2) return true; 234 235 return (Array.isArray(this.label_array) ? this.label_array : []).some(row => { 236 const labels = Array.isArray(row) ? row.slice(0, raterCount) : []; 237 while (labels.length < raterCount) { 238 labels.push(null); 239 } 240 const validCount = labels.filter(label => label === 0 || label === 1).length; 241 return validCount > 0 && validCount < raterCount; 242 }); 243 } 244 245 _sync_derived_arrays() { 246 this.paper_id_array = this._get_paper_id_array(); 247 this._pad_label_rows(); 248 this.indexes_to_label = this._get_comparable_indexes(); 249 this.selected_label_array = this._get_selected_label_array(); 250 } 251 252 import_data_from_storage_json(json_data) { 253 if (!json_data || !Array.isArray(json_data.data_array) || !Array.isArray(json_data.label_array)) { 254 throw new Error("Invalid agreement progress data."); 255 } 256 257 if (!Array.isArray(json_data.raters_identifiers) || json_data.raters_identifiers.length < 2) { 258 throw new Error("Agreement progress must include at least two raters."); 259 } 260 261 if (json_data.label_array.length !== json_data.data_array.length) { 262 throw new Error("Agreement progress entries and labels do not match."); 263 } 264 265 const raters = json_data.raters_identifiers.map((identifier, index) => 266 this._normalize_rater_identifier(identifier, index) 267 ); 268 269 const label_array = json_data.label_array.map(row => { 270 if (!Array.isArray(row)) { 271 throw new Error("Agreement progress label matrix is corrupted."); 272 } 273 274 const normalizedRow = row.map(label => this._normalize_binary_label(label)); 275 while (normalizedRow.length < raters.length) { 276 normalizedRow.push(null); 277 } 278 return normalizedRow.slice(0, raters.length); 279 }); 280 281 const data_array = json_data.data_array.map(entry => ({ ...entry })); 282 283 this.status_id = json_data.status_id || this.status_id; 284 this.data_array = data_array; 285 this.raters_identifiers = raters; 286 this.label_array = label_array; 287 this._sync_derived_arrays(); 288 this.currentIndex = Math.min( 289 Math.max(Number.isInteger(Number(json_data.currentIndex)) ? Number(json_data.currentIndex) : 0, 0), 290 Math.max(this.indexes_to_label.length - 1, 0) 291 ); 292 293 this.agreement_percentage = []; 294 this.fleiss_kappa_stats = []; 295 this.fleiss_kappa_not_calculable_reason = null; 296 this.individual_agreement_array = []; 297 this.individual_agreement_colours = []; 298 this.corr_matrix_stats = []; 299 this.t_matrix_stats = []; 300 this.p_matrix_stats = []; 301 this.matrix_colours = []; 302 this.view_state = json_data.view_state && typeof json_data.view_state === "object" 303 ? JSON.parse(JSON.stringify(json_data.view_state)) 304 : {}; 305 this._save_meta = json_data._save_meta && typeof json_data._save_meta === "object" 306 ? JSON.parse(JSON.stringify(json_data._save_meta)) 307 : null; 308 } 309 310 export_snapshot_state() { 311 return { 312 label_array: this.label_array, 313 selected_label_array: [], 314 paper_id_array: [], 315 raters_identifiers: this.raters_identifiers, 316 data_array: this.data_array, 317 agreement_percentage: this.agreement_percentage, 318 fleiss_kappa_stats: this.fleiss_kappa_stats, 319 fleiss_kappa_not_calculable_reason: this.fleiss_kappa_not_calculable_reason, 320 individual_agreement_array: this.individual_agreement_array,
321 individual_agreement_colours: this.individual_agreement_colours, 322 corr_matrix_stats: this.corr_matrix_stats, 323 t_matrix_stats: this.t_matrix_stats, 324 p_matrix_stats: this.p_matrix_stats, 325 matrix_colours: this.matrix_colours, 326 currentIndex: this.currentIndex, 327 indexes_to_label: this.indexes_to_label, 328 view_state: this.view_state, 329 _save_meta: this._save_meta 330 }; 331 } 332 333 import_snapshot_state(state) { 334 this.import_data_from_storage_json(state); 335 336 this.selected_label_array = Array.isArray(state.selected_label_array) && 337 state.selected_label_array.length === this.indexes_to_label.length 338 ? state.selected_label_array 339 : this._get_selected_label_array(); 340 this.agreement_percentage = Array.isArray(state.agreement_percentage) ? state.agreement_percentage : []; 341 this.fleiss_kappa_stats = Array.isArray(state.fleiss_kappa_stats) ? state.fleiss_kappa_stats : []; 342 this.fleiss_kappa_not_calculable_reason = typeof state.fleiss_kappa_not_calculable_reason === "string" 343 ? state.fleiss_kappa_not_calculable_reason 344 : null; 345 this.individual_agreement_array = Array.isArray(state.individual_agreement_array) ? state.individual_agreement_array : []; 346 this.individual_agreement_colours = Array.isArray(state.individual_agreement_colours) ? state.individual_agreement_colours : []; 347 this.corr_matrix_stats = Array.isArray(state.corr_matrix_stats) ? state.corr_matrix_stats : []; 348 this.t_matrix_stats = Array.isArray(state.t_matrix_stats) ? state.t_matrix_stats : []; 349 this.p_matrix_stats = Array.isArray(state.p_matrix_stats) ? state.p_matrix_stats : []; 350 this.matrix_colours = Array.isArray(state.matrix_colours) ? state.matrix_colours : []; 351 } 352 353 // Function to extract labels for each paper_id 354 add_data(data, raterIdentifier = null) { 355 const paperIdToIndex = new Map(this.paper_id_array.map((paperId, index) => [ 356 this._normalize_paper_id_key(paperId, `__missing_existing_${index}`), 357 index 358 ])); 359 360 for (var i = 0; i < data.length; i++) { 361 const {paper_id, label, selected_for_training} = data[i]; 362 const paperIdKey = this._normalize_paper_id_key(paper_id, `__missing_import_${i}`); 363 const normalizedLabel = this._normalize_binary_label(label); 364 const resolvedRaterIdentifier = raterIdentifier || data[i]?.rater_identifier || data[i]?.source_file || data[i]?._sheet; 365 const raterIndex = this._register_rater_identifier(resolvedRaterIdentifier); 366 367 if (!paperIdToIndex.has(paperIdKey)) { 368 this.data_array.push({...data[i], label: null}); 369 this.paper_id_array.push(paper_id); 370 paperIdToIndex.set(paperIdKey, this.paper_id_array.length - 1); 371 const labelRow = new Array(this.raters_identifiers.length).fill(null); 372 labelRow[raterIndex] = normalizedLabel; 373 this.label_array.push(labelRow); 374 375 } else { 376 const index = paperIdToIndex.get(paperIdKey); 377 const labelRow = Array.isArray(this.label_array[index]) 378 ? this.label_array[index] 379 : new Array(this.raters_identifiers.length).fill(null); 380 381 while (labelRow.length < this.raters_identifiers.length) { 382 labelRow.push(null); 383 } 384 385 const existingLabel = this._normalize_binary_label(labelRow[raterIndex]); 386 if (existingLabel !== null && normalizedLabel !== null && existingLabel !== normalizedLabel) { 387 throw new Error(`Duplicate labels were found for ${this.raters_identifiers[raterIndex]} on ${paper_id || "an imported record"}.`); 388 } 389 390 labelRow[raterIndex] = normalizedLabel; 391 this.label_array[index] = labelRow; 392 393 const existingEntry = this.data_array[index] || {}; 394 this.data_array[index] = { 395 ...data[i], 396 ...existingEntry, 397 label: existingEntry.label ?? null, 398 selected_for_training: existingEntry.selected_for_training ?? selected_for_training, 399 notes: existingEntry.notes ?? data[i]?.notes ?? "" 400 }; 401 }; 402 } 403 this._sync_derived_arrays(); 404 }; 405 406 add_raters_indentifiers(identifiers){
407 (Array.isArray(identifiers) ? identifiers : []).forEach(identifier => { 408 this._register_rater_identifier(identifier); 409 }); 410 this._sync_derived_arrays(); 411 } 412 413 414 // Check if number of labels are all the same for each publication. 415 check_dimensions(){ 416 const width = Array.isArray(this.raters_identifiers) ? this.raters_identifiers.length : 0; 417 return (Array.isArray(this.label_array) ? this.label_array : []).some(row => !Array.isArray(row) || row.length !== width); 418 } 419 420 calculate_metrics(type, correction, alpha){ 421 this._sync_derived_arrays(); 422 const selectedLabels = this.selected_label_array; 423 424 if (!Array.isArray(selectedLabels) || selectedLabels.length === 0) { 425 throw new Error("No comparable agreement entries with at least two valid labels are available to analyse."); 426 } 427 428 this.individual_agreement_array = []; 429 this.individual_agreement_colours = []; 430 this.corr_matrix_stats = []; 431 this.t_matrix_stats = []; 432 this.p_matrix_stats = []; 433 this.agreement_percentage = percentageAgreement(selectedLabels); 434 if (this._has_missing_labels_for_fleiss()) { 435 this.fleiss_kappa_stats = [null, null, null, [null, null], null, null]; 436 this.fleiss_kappa_not_calculable_reason = "Not calculable due to splitting of entries."; 437 } else { 438 this.fleiss_kappa_stats = fleiss_kappa(selectedLabels, alpha); 439 this.fleiss_kappa_not_calculable_reason = null; 440 } 441 this.set_individual_agreement_array(); 442 this.set_document_agreement_colours(); 443 this.calculate_matrix_metric(type, correction); 444 } 445 446 prepare_agreement_export(type = 1, correction = 1, alpha = 0.05) { 447 this._sync_derived_arrays(); 448 const selectedLabels = this.selected_label_array; 449 450 this.agreement_percentage = Array.isArray(selectedLabels) && selectedLabels.length > 0 451 ? percentageAgreement(selectedLabels) 452 : null; 453 454 if (!Array.isArray(selectedLabels) || selectedLabels.length === 0 || this._has_missing_labels_for_fleiss()) { 455 this.fleiss_kappa_stats = [null, null, null, [null, null], null, null]; 456 this.fleiss_kappa_not_calculable_reason = "Not calculable due to splitting of entries."; 457 } else { 458 this.fleiss_kappa_stats = fleiss_kappa(selectedLabels, alpha); 459 this.fleiss_kappa_not_calculable_reason = null; 460 } 461 462 this.set_individual_agreement_array(); 463 this.set_document_agreement_colours(); 464 this.calculate_matrix_metric(type, correction); 465 } 466 467 set_individual_agreement_array(){ 468 this._sync_derived_arrays(); 469 this.individual_agreement_array = (Array.isArray(this.indexes_to_label) ? this.indexes_to_label : []) 470 .map(index => this._get_individual_agreement_for_entry_index(index)); 471 }; 472 473 474 set_document_agreement_colours(){ 475 this.individual_agreement_colours = []; 476 this.individual_agreement_array.forEach(agreement => { 477 const numericAgreement = Number(agreement); 478 if (!Number.isFinite(numericAgreement)) { 479 this.individual_agreement_colours.push("#9E9E9E"); 480 } else if (numericAgreement >= 80) { 481 this.individual_agreement_colours.push("#4CAF50"); 482 } else if (numericAgreement >= 60) { 483 this.individual_agreement_colours.push("#FFA500"); 484 } else { 485 this.individual_agreement_colours.push("#FF5252"); 486 } 487 }); 488 }; 489 490 491 calculate_matrix_metric(type, correction) { 492 let calc_function; 493 let correction_function; 494 495 // Selection of metric. 496 if (type == 1) { 497 calc_function = spearmanCoeff; 498 } else if (type == 2) { 499 calc_function = kendalltau; 500 } else if (type == 3) { 501 calc_function = cohen_kappa; 502 } 503 if (typeof calc_function !== "function") { 504 calc_function = spearmanCoeff; 505 } 506 507 // Selection of correction. 508 if (correction == 1) { 509 correction_function = fdrCorrection; 510 } else if (correction == 2) { 511 correction_function = bonferroniCorrection; 512 } 513 if (typeof correction_function !== "function") { 514 correction_function = pValue => pValue; 515 } 516 517 this._sync_derived_arrays(); 518 const numRaters = Array.isArray(this.raters_identifiers) ? this.raters_identifiers.length : 0; 519 this.corr_matrix_stats = Array.from({ length: numRaters }, () => Array(numRaters).fill(null)); 520 this.t_matrix_stats = Array.from({ length: numRaters }, () => Array(numRaters).fill(null)); 521 this.p_matrix_stats = Array.from({ length: numRaters }, () => Array(numRaters).fill(null)); 522 523 for (let i = 0; i < numRaters; i++) { 524 for (let j = i; j < numRaters; j++) { 525 if (i === j) { 526 this.corr_matrix_stats[i][j] = 1; // Diagonal elements are 1 527 this.t_matrix_stats[i][j] = 0; 528 this.p_matrix_stats[i][j] = 1; // p-value for correlation with itself is 1 529 } else { 530 const [firstLabels, secondLabels] = this._build_pairwise_arrays(i, j); 531 if (firstLabels.length < 2 || secondLabels.length < 2) { 532 continue; 533 } 534 535 const [correlation, t, pValue] = this._sanitize_metric_tuple(calc_function(firstLabels, secondLabels)); 536 this.corr_matrix_stats[i][j] = correlation; 537 this.corr_matrix_stats[j][i] = correlation; // Since the matrix is symmetric 538 this.t_matrix_stats[i][j] = t; 539 this.t_matrix_stats[j][i] = t; 540 const correctedPValue = pValue == null ? null : correction_function(pValue, numRaters, j); 541 this.p_matrix_stats[i][j] = correctedPValue; 542 this.p_matrix_stats[j][i] = correctedPValue; 543 } 544 } 545 } 546 } 547 548 549 550 _build_agreement_results_aoa() { 551 this._sync_derived_arrays(); 552 const headings = ["paper_id", ...deep_copy_array(this.raters_identifiers), "Individual agreement (%)"]; 553 const paperIds = this._get_paper_id_array(); 554 const rows = (Array.isArray(this.label_array) ? this.label_array : []).map((row, entryIndex) => { 555 const labels = (Array.isArray(this.raters_identifiers) ? this.raters_identifiers : []).map((_, raterIndex) => { 556 const normalizedLabel = this._normalize_binary_label(Array.isArray(row) ? row[raterIndex] : null); 557 return normalizedLabel === 0 || normalizedLabel === 1 ? normalizedLabel : null; 558 }); 559 const agreement = this._calculate_agreement_for_labels(labels); 560 return [ 561 paperIds[entryIndex] ?? "", 562 ...labels, 563 agreement == null ? null : agreement 564 ]; 565 });
566 567 return [headings, ...rows]; 568 } 569 570 save_agreement_results(filename, type = 1, correction = 1, alpha = 0.05) { 571 try { 572 this.prepare_agreement_export(type, correction, alpha); 573 const wb = XLSX.utils.book_new(); 574 575 // ===== Sheet 1: Resolved Entries (ordered columns) ===== 576 const orderedData = this.data_array.map(entry => ({ 577 title: entry.title ?? "", 578 abstract: entry.abstract ?? "", 579 paper_id: entry.paper_id ?? "", 580 year: entry.year ?? "", 581 authors: entry.authors ?? "", 582 journal: entry.journal ?? "", 583 keywords: entry.keywords ?? "", 584 label: entry.label ?? "", 585 notes: entry.notes ?? "", 586 selected_for_training: entry.selected_for_training ?? "" 587 })); 588 589 const data_array_sheet = XLSX.utils.json_to_sheet(orderedData); 590 XLSX.utils.book_append_sheet(wb, data_array_sheet, 'Resolved Entries'); 591 592 // ===== Sheet 2: Agreement Results ===== 593 const ws = XLSX.utils.aoa_to_sheet([]); 594 const aoa = this._build_agreement_results_aoa(); 595 596 XLSX.utils.sheet_add_aoa(ws, aoa); 597 XLSX.utils.book_append_sheet(wb, ws, 'Agreement Results'); 598 599 // ===== Sheet 3: Agreement overall stats ===== 600 const ws2 = XLSX.utils.aoa_to_sheet([ 601 ['Fleiss k', 'Fleiss k SE', 'CI', 'z', 'p', 'Total agreement (%)'], 602 [ 603 this.fleiss_kappa_stats[1], 604 this.fleiss_kappa_stats[2], 605 this.fleiss_kappa_stats[3], 606 this.fleiss_kappa_stats[4], 607 this.fleiss_kappa_stats[5], 608 this.agreement_percentage 609 ] 610 ]); 611 XLSX.utils.book_append_sheet(wb, ws2, 'Agreement overall stats'); 612 613 // ===== Sheet 4â6 ===== 614 XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(this.corr_matrix_stats), 'Correlation Stats'); 615 XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(this.t_matrix_stats), 'T Values'); 616 XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(this.p_matrix_stats), 'p Values'); 617 618 XLSX.writeFile(wb, filename); 619 620 } catch (error) { 621 alert('â ï¸ Length of text is longer than maximum possible in XLSX, falling back to CSV.'); 622 623 const saveCsv = (content, name) => { 624 const blob = new Blob(['\uFEFF' + content], { type: 'text/csv;charset=utf-8;' }); 625 const link = document.createElement('a'); 626 const downloadUrl = URL.createObjectURL(blob); 627 link.href = downloadUrl;
628 link.download = name; 629 document.body.appendChild(link); 630 link.click(); 631 document.body.removeChild(link); 632 setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); 633 }; 634 635 // ===== CSV fallback: Resolved Entries (same order) ===== 636 const headers = [ 637 'title','abstract','paper_id','year','authors', 638 'journal','keywords','label','notes','selected_for_training' 639 ]; 640 641 const resolvedCSV = [ 642 headers.join(','), 643 ...this.data_array.map(e => 644 headers.map(h => `"${(e[h] ?? '').toString().replace(/"/g, '""')}"`).join(',') 645 ) 646 ].join('\n'); 647 648 saveCsv(resolvedCSV, 'Resolved_Entries.csv'); 649 650 // Remaining CSV exports unchanged 651 const aoa = this._build_agreement_results_aoa(); 652 653 saveCsv(aoa.map(r => r.join(',')).join('\n'), 'Agreement_Results.csv'); 654 655 saveCsv( 656 [ 657 ['Fleiss k','Fleiss k SE','CI','z','p','Total agreement (%)'].join(','), 658 [ 659 this.fleiss_kappa_stats[1], 660 this.fleiss_kappa_stats[2], 661 this.fleiss_kappa_stats[3], 662 this.fleiss_kappa_stats[4], 663 this.fleiss_kappa_stats[5], 664 this.agreement_percentage 665 ].join(',') 666 ].join('\n'), 667 'Agreement_overall_stats.csv' 668 ); 669 670 saveCsv(this.corr_matrix_stats.map(r => r.join(',')).join('\n'), 'Correlation_Stats.csv'); 671 saveCsv(this.t_matrix_stats.map(r => r.join(',')).join('\n'), 'T_Values.csv'); 672 saveCsv(this.p_matrix_stats.map(r => r.join(',')).join('\n'), 'P_Values.csv'); 673 } 674 } 675 676 677 678 add_previous_agreement(data) { 679 const resolved_data = Array.isArray(data?.[0]) ? data[0] : []; // Sheet 1: Resolved Entries 680 const label_data = Array.isArray(data?.[1]) ? data[1] : []; // Sheet 2: Agreement Results 681 682 if (resolved_data.length === 0 || label_data.length === 0) { 683 throw new Error("Agreement spreadsheet files must include Resolved Entries and Agreement Results sheets."); 684 } 685 686 // --- 1. Add raters identifiers --- 687 // The loader adds bookkeeping fields such as _sheet/source_file; keep only real rater columns. 688 this.raters_identifiers = this._get_previous_agreement_rater_headers(label_data); 689 if (!Array.isArray(this.raters_identifiers) || this.raters_identifiers.length < 2) { 690 throw new Error("Agreement Results must include at least two rater label columns."); 691 } 692 693 // --- 2. Add paper ID array --- 694 this.paper_id_array = resolved_data.map(x => x.paper_id); 695 696 // --- 3. Add data array --- 697 // Copy all fields except loader bookkeeping metadata 698 this.data_array = resolved_data.map(({ _sheet, source_file, ...rest }) => ({ ...rest })); 699 700 // --- 4. Load label_array (rater labels only) --- 701 const labelsByPaperId = new Map(label_data.map((item, index) => [ 702 this._normalize_paper_id_key(item?.paper_id, `__missing_${index}`), 703 item 704 ])); 705 this.label_array = this.paper_id_array.map((pid, index) => { 706 const entry = labelsByPaperId.get(this._normalize_paper_id_key(pid, `__missing_${index}`)); 707 if (entry) { 708 // map rater columns to values 709 return this.raters_identifiers.map(rater => { 710 const val = entry[rater]; 711 return val === undefined || val === null || val === '' ? null : this._normalize_binary_label(val); 712 }); 713 } else { 714 return new Array(this.raters_identifiers.length).fill(null); 715 } 716 }); 717 718 this._sync_derived_arrays(); 719 }; 720 721 722 723 doc_index_agreement_click(event, array, window){ 724 let doc_data, percentage, rater_labels; 725 //Check if click is in one of the documents. 726 727 if (array.length != 0){ 728 if (array[0]._datasetIndex == 0){ 729 $('#'+window).modal('show'); 730 //Change the index. 731 this.currentIndex = array[0]._index; 732 } 733 } 734 }; 735 736 get_agreed_labels(preserveCurrentIndex = false) { 737 // Set labels where agreement is total, but only if label is missing 738 this.data_array.forEach((doc, index) => { 739 if (doc.label == null) { // only update if label is null or undefined 740 const labels = this._get_valid_labels_for_index(index); 741 if (labels.length < 2) return; 742 const allAgree = labels.every(label => label === labels[0]); 743 doc.label = allAgree ? labels[0] : null; 744 } 745 }); 746 747 if (preserveCurrentIndex) { 748 this.currentIndex = Math.min(this.currentIndex, Math.max(this.indexes_to_label.length - 1, 0)); 749 return; 750 } 751 752 // Find the first index in indexes_to_label where label is null 753 const firstNullIndex = this.indexes_to_label 754 .map(i => this.data_array[i]) 755 .findIndex(doc => doc.label == null); 756 757 // If a null label is found, update currentIndex; otherwise, keep its original value 758 if (firstNullIndex !== -1) { 759 this.currentIndex = firstNullIndex; 760 } 761 } 762 763 set_index_to_next_mismatch() { 764 // Store the original currentIndex 765 const currentIndex = this.currentIndex; 766 767 // Find the first null label after the current index 768 const nextNullIndex = this.indexes_to_label 769 .map(i => this.data_array[i]) 770 .findIndex((doc, index) => index > currentIndex && doc.label === null); 771 772 // If a null label is found, update currentIndex; otherwise, keep its original value. 773 if (nextNullIndex !== -1) { 774 this.currentIndex = nextNullIndex; 775 } 776 } 777 778apply_majority(threshold) { 779 this._sync_derived_arrays(); 780 const selectedLabels = this.selected_label_array; 781 this.indexes_to_label.map(i =>
781 this.data_array[i]).forEach((doc, index) => { 782 // Only process documents with a null label 783 if (doc.label === null) { 784 const labels = selectedLabels[index]; 785 if (!Array.isArray(labels) || labels.length < 2) return; 786 const labelCount = {}; 787 788 // Calculate the percentages of accepted and rejected labels 789 const percentage_of_accepted = 100*countOccurrences(labels, 1) / labels.length; 790 const percentage_of_rejected = 100*countOccurrences(labels, 0) / labels.length; 791 792 if (percentage_of_accepted > threshold) { 793 doc.label = 1; 794 } else if (percentage_of_rejected > threshold) { 795 doc.label = 0; 796 } 797 } 798 }); 799} 800 801 export_ris(filename, option = "relevant") { 802 // Filter entries based on option 803 let entries; 804 if (option === "relevant") { 805 entries = this.data_array.filter(entry => entry.label === 1); 806 } else if (option === "irrelevant") { 807 entries = this.data_array.filter(entry => entry.label === 0); 808 } else if (option === "all") { 809 entries = this.data_array; 810 } else { 811 return; 812 } 813 814 if (!entries.length) return; 815 816 const escapeRIS = (text) => 817 text ? text.replace(/\r?\n/g, ' ').replace(/\t/g, ' ').trim() : ''; 818 819 let risContent = entries.map(entry => { 820 let risEntry = ''; 821 822 // 1ï¸â£ Type 823 risEntry += `TY - JOUR\n`; 824 825 // 2ï¸â£ Title 826 if (entry.title) { 827 risEntry += `TI - ${escapeRIS(entry.title)}\n`; 828 } 829 830 // 3ï¸â£ Authors 831 if (entry.authors) { 832 entry.authors 833 .split(';') 834 .map(a => a.trim()) 835 .filter(Boolean) 836 .forEach(author => { 837 risEntry += `AU - ${escapeRIS(author)}\n`; 838 }); 839 } 840 841 // 4ï¸â£ Year 842 if (entry.year) { 843 risEntry += `PY - ${escapeRIS(entry.year)}\n`; 844 } 845 846 // 5ï¸â£ Journal 847 if (entry.journal) { 848 risEntry += `T2 - ${escapeRIS(entry.journal)}\n`; 849 } 850 851 // 6ï¸â£ Abstract 852 if (entry.abstract) { 853 risEntry += `AB - ${escapeRIS(entry.abstract)}\n`; 854 } 855 856 // 7ï¸â£ Keywords 857 if (entry.keywords) { 858 entry.keywords 859 .split(';') 860 .map(k => k.trim()) 861 .filter(Boolean) 862 .forEach(keyword => { 863 risEntry += `KW - ${escapeRIS(keyword)}\n`; 864 }); 865 } 866 867 // 8ï¸â£ DOI or ID 868 if (entry.paper_id != null) { 869 const paperIdStr = String(entry.paper_id); 870 if (paperIdStr.includes('/')) { 871 risEntry += `DO - ${escapeRIS(paperIdStr)}\n`; 872 } else { 873 risEntry += `ID - ${escapeRIS(paperIdStr)}\n`; 874 } 875 } 876 877 // 9ï¸â£ Notes + label 878 let noteText = entry.notes ? escapeRIS(entry.notes) + ' ' : ''; 879 noteText += `Label: ${entry.label === 1 ? 'included' : 'excluded'}`; 880 risEntry += `N1 - ${noteText}\n`; 881 882 // ð End record 883 risEntry += `ER - \n`; 884 885 return risEntry; 886 }).join('\n'); 887 888 // Download RIS file 889 const blob = new Blob([risContent], { 890 type: "application/x-research-info-systems" 891 }); 892 const link = document.createElement("a"); 893 const downloadUrl = URL.createObjectURL(blob); 894 link.href = downloadUrl; 895 link.download = filename.endsWith('.ris') ? filename : filename + '.ris'; 896 document.body.appendChild(link); 897 link.click(); 898 document.body.removeChild(link); 899 setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); 900 } 901 902 903 904cleanup(){ 905 906 // Reset arrays holding label and paper ID data 907 this.label_array = []; 908 this.selected_label_array = []; 909 this.paper_id_array = []; 910 this.raters_identifiers = []; 911 this.data_array = []; 912 this.indexes_to_label = []; 913 this.currentIndex = 0; 914 915 // Clear overall metrics 916 this.agreement_percentage = []; 917 this.fleiss_kappa_stats = []; 918 this.fleiss_kappa_not_calculable_reason = null; 919 920 // Reset individual document agreement data 921 this.individual_agreement_array = [];
922 this.individual_agreement_colours = []; 923 924 // Clear raters' correlation metrics 925 this.corr_matrix_stats = []; 926 this.t_matrix_stats = []; 927 this.p_matrix_stats = []; 928 this.matrix_colours = []; 929 this.view_state = {}; 930 this._save_meta = null; 931} 932 933 934 935}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.