From aae09020e68c8263f7bced7a8e2cba553fa89656 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Tue, 2 Jun 2026 12:17:28 -0600 Subject: [PATCH] Sort metrics numerically on the CCP Metrics page Add a natural-sort comparator for metric IDs (e.g. 2.3.6i, 5.2.6, 10.1.1) and apply it to the metric breakdown cards, the vertical detail table, and the forecast burndown metric dropdown. Metrics now appear in ascending numerical order instead of arbitrary API response order. Closes #24 --- .../src/components/pages/CCPMetricsPage.js | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/CCPMetricsPage.js b/frontend/src/components/pages/CCPMetricsPage.js index ae2705e..5687dc7 100644 --- a/frontend/src/components/pages/CCPMetricsPage.js +++ b/frontend/src/components/pages/CCPMetricsPage.js @@ -8,6 +8,26 @@ const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; const PURPLE = '#A78BFA'; +// Natural sort comparator for metric IDs like "2.3.6i", "5.2.6", "10.1.1". +// Splits on "." and compares each segment numerically (trailing letters after digits sort after pure numbers). +function compareMetricIds(a, b) { + const partsA = a.split('.'); + const partsB = b.split('.'); + const len = Math.max(partsA.length, partsB.length); + for (let i = 0; i < len; i++) { + const segA = partsA[i] || ''; + const segB = partsB[i] || ''; + const numA = parseFloat(segA) || 0; + const numB = parseFloat(segB) || 0; + if (numA !== numB) return numA - numB; + // Same numeric prefix — compare the suffix (e.g. "6i" vs "6") + const suffA = segA.replace(/^[\d.]+/, ''); + const suffB = segB.replace(/^[\d.]+/, ''); + if (suffA !== suffB) return suffA.localeCompare(suffB); + } + return 0; +} + // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- @@ -106,7 +126,8 @@ function MetricBreakdownPanel({ metrics }) { if (!metrics || metrics.length === 0) return null; // Only show metrics with non_compliant > 0 - const ncMetrics = metrics.filter(m => m.non_compliant > 0); + const ncMetrics = metrics.filter(m => m.non_compliant > 0) + .sort((a, b) => compareMetricIds(a.metric_id, b.metric_id)); if (ncMetrics.length === 0) return null; const TOP_COUNT = 8; @@ -628,9 +649,10 @@ function VerticalDetailView({ vertical, onBack, onSelectMetric }) { if (loading) return
Loading...
; // Filter metrics by team if a team filter is active - const displayMetrics = teamFilter + const displayMetrics = (teamFilter ? metrics.filter(m => m.sub_teams && m.sub_teams.some(st => st.team === teamFilter)) - : metrics; + : metrics + ).slice().sort((a, b) => compareMetricIds(a.metric_id, b.metric_id)); return (
@@ -1282,7 +1304,7 @@ function MetricSelector({ onMetricSelect, selectedMetric }) { minWidth: '200px', }} > - {metrics.map(m => ( + {metrics.slice().sort((a, b) => compareMetricIds(a.metric_id, b.metric_id)).map(m => (