diff --git a/changelog_entry.yaml b/changelog_entry.yaml
index e69de29..69f9b7f 100644
--- a/changelog_entry.yaml
+++ b/changelog_entry.yaml
@@ -0,0 +1,5 @@
+- bump: minor
+ changes:
+ added:
+ - Adding column to sort by calibration difference to dashboard comparisons page.
+ - Ensure pagination so all repo branches are visible in the github loading.
diff --git a/microcalibration-dashboard/src/components/ComparisonDataTable.tsx b/microcalibration-dashboard/src/components/ComparisonDataTable.tsx
index 64e6a36..8694774 100644
--- a/microcalibration-dashboard/src/components/ComparisonDataTable.tsx
+++ b/microcalibration-dashboard/src/components/ComparisonDataTable.tsx
@@ -18,7 +18,7 @@ interface ComparisonRow {
second?: CalibrationDataPoint;
}
-type SortField = keyof CalibrationDataPoint | 'random';
+type SortField = keyof CalibrationDataPoint | 'random' | 'difference';
type SortDataset = 'first' | 'second' | null;
type SortDirection = 'asc' | 'desc';
@@ -97,6 +97,29 @@ export default function ComparisonDataTable({
return sortDirection === 'asc' ? result : -result;
}
+ if (sortField === 'difference') {
+ // Calculate differences for sorting
+ const aDiff = (a.first?.rel_abs_error !== undefined && a.second?.rel_abs_error !== undefined)
+ ? a.second.rel_abs_error - a.first.rel_abs_error
+ : null;
+ const bDiff = (b.first?.rel_abs_error !== undefined && b.second?.rel_abs_error !== undefined)
+ ? b.second.rel_abs_error - b.first.rel_abs_error
+ : null;
+
+ // Handle undefined/null values - put them at the end regardless of sort direction
+ if (aDiff === null && bDiff === null) {
+ return 0;
+ }
+ if (aDiff === null) {
+ return 1;
+ }
+ if (bDiff === null) {
+ return -1;
+ }
+
+ return sortDirection === 'asc' ? aDiff - bDiff : bDiff - aDiff;
+ }
+
// Sort by values based on selected dataset
let aVal, bVal;
if (sortDataset === 'first') {
@@ -150,15 +173,15 @@ export default function ComparisonDataTable({
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
- const handleSort = (field: keyof CalibrationDataPoint, dataset?: 'first' | 'second') => {
- // For target_name, don't use dataset-specific sorting
- if (field === 'target_name') {
+ const handleSort = (field: keyof CalibrationDataPoint | 'difference', dataset?: 'first' | 'second') => {
+ // For target_name and difference, don't use dataset-specific sorting
+ if (field === 'target_name' || field === 'difference') {
if (sortField === field && sortDataset === null) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDataset(null);
- setSortDirection('asc');
+ setSortDirection(field === 'difference' ? 'desc' : 'asc'); // Start with desc for difference to show biggest improvements first
}
return;
}
@@ -176,11 +199,11 @@ export default function ComparisonDataTable({
};
const SortButton = ({ field, children, dataset }: {
- field: keyof CalibrationDataPoint,
+ field: keyof CalibrationDataPoint | 'difference',
children: React.ReactNode,
dataset?: 'first' | 'second'
}) => {
- const isActive = field === 'target_name'
+ const isActive = (field === 'target_name' || field === 'difference')
? (sortField === field && sortDataset === null)
: (sortField === field && sortDataset === dataset);
@@ -283,6 +306,28 @@ export default function ComparisonDataTable({
);
};
+ const renderDifferenceCell = (firstValue: number | undefined | null, secondValue: number | undefined | null) => {
+ if (firstValue === undefined || firstValue === null || secondValue === undefined || secondValue === null) {
+ return —;
+ }
+
+ const difference = secondValue - firstValue;
+ const percentageDiff = (difference * 100).toFixed(2);
+ const sign = difference > 0 ? '+' : '';
+
+ // Color coding: negative difference (B better than A) = green, positive (B worse than A) = red
+ let colorClass = 'text-gray-600';
+ if (Math.abs(difference) > 0.001) { // Only color if difference is meaningful
+ colorClass = difference < 0 ? 'text-green-600' : 'text-red-600';
+ }
+
+ return (
+
+ {sign}{percentageDiff}%
+
+ );
+ };
+
return (
@@ -347,6 +392,7 @@ export default function ComparisonDataTable({
+
@@ -368,6 +414,9 @@ export default function ComparisonDataTable({
|
Rel abs error %
|
+
+ Difference
+ |
| A |
@@ -388,6 +437,9 @@ export default function ComparisonDataTable({
B
+
+ (B-A)
+ |
@@ -460,6 +512,11 @@ export default function ComparisonDataTable({
{renderDataCell(row.second?.rel_abs_error, false, true)}
|
+
+ {/* Difference column */}
+
+ {renderDifferenceCell(row.first?.rel_abs_error, row.second?.rel_abs_error)}
+ |
))}
diff --git a/microcalibration-dashboard/src/components/ComparisonQualitySummary.tsx b/microcalibration-dashboard/src/components/ComparisonQualitySummary.tsx
index 2b0e213..4851111 100644
--- a/microcalibration-dashboard/src/components/ComparisonQualitySummary.tsx
+++ b/microcalibration-dashboard/src/components/ComparisonQualitySummary.tsx
@@ -120,11 +120,11 @@ export default function ComparisonQualitySummary({
let qualityIcon: React.ReactNode;
if (overallQualityChange > 0.1) {
- qualityAssessment = "Significantly Better";
+ qualityAssessment = "Improved";
qualityColor = "green";
qualityIcon =
;
} else if (overallQualityChange < -0.1) {
- qualityAssessment = "Significantly worse";
+ qualityAssessment = "Worsened";
qualityColor = "red";
qualityIcon =
;
} else {
@@ -145,7 +145,7 @@ export default function ComparisonQualitySummary({
{/* Overall Quality Assessment */}
-
+
{qualityIcon}
diff --git a/microcalibration-dashboard/src/components/FileUpload.tsx b/microcalibration-dashboard/src/components/FileUpload.tsx
index 3d5cab8..17ece15 100644
--- a/microcalibration-dashboard/src/components/FileUpload.tsx
+++ b/microcalibration-dashboard/src/components/FileUpload.tsx
@@ -568,28 +568,56 @@ export default function FileUpload({
setError('');
try {
- // Use authenticated requests to avoid rate limiting
- const response = await fetch(`https://api.github.com/repos/${githubRepo}/branches`, {
- headers: {
- 'Authorization': `Bearer ${githubToken}`,
- 'Accept': 'application/vnd.github.v3+json',
- 'User-Agent': 'PolicyEngine-Dashboard/1.0'
+ // Fetch all branches with pagination support
+ const allBranches: GitHubBranch[] = [];
+ let page = 1;
+ const perPage = 100; // Maximum allowed by GitHub API
+
+ while (true) {
+ const response = await fetch(`https://api.github.com/repos/${githubRepo}/branches?per_page=${perPage}&page=${page}`, {
+ headers: {
+ 'Authorization': `Bearer ${githubToken}`,
+ 'Accept': 'application/vnd.github.v3+json',
+ 'User-Agent': 'PolicyEngine-Dashboard/1.0'
+ }
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ throw new Error('Repository not found. Please check the repository name and ensure it is accessible.');
+ } else if (response.status === 403) {
+ throw new Error('Access forbidden. Please check your GitHub token permissions or repository access.');
+ }
+ throw new Error(`Failed to fetch branches: ${response.status} ${response.statusText}`);
}
- });
- if (!response.ok) {
- if (response.status === 404) {
- throw new Error('Repository not found. Please check the repository name and ensure it is accessible.');
- } else if (response.status === 403) {
- throw new Error('Access forbidden. Please check your GitHub token permissions or repository access.');
+
+ const branches: GitHubBranch[] = await response.json();
+
+ if (branches.length === 0) {
+ // No more branches to fetch
+ break;
+ }
+
+ allBranches.push(...branches);
+
+ // If we got fewer branches than requested, we've reached the end
+ if (branches.length < perPage) {
+ break;
+ }
+
+ page++;
+
+ // Safety check to prevent infinite loops (GitHub repos rarely have more than 1000 branches)
+ if (page > 10) {
+ console.warn('Stopped fetching branches after 10 pages (1000 branches) to prevent excessive API calls');
+ break;
}
- throw new Error(`Failed to fetch branches: ${response.status} ${response.statusText}`);
}
- const branches: GitHubBranch[] = await response.json();
- setGithubBranches(branches);
+ setGithubBranches(allBranches);
// Auto-select main/master branch if available
- const defaultBranch = branches.find(b => b.name === 'main' || b.name === 'master');
+ const defaultBranch = allBranches.find(b => b.name === 'main' || b.name === 'master');
if (defaultBranch) {
setSelectedBranch(defaultBranch.name);
await fetchGithubCommits(defaultBranch.name);