From 41cd43f7dafcb402fcd275da5f6516548c9793e5 Mon Sep 17 00:00:00 2001 From: Greg Albert Date: Mon, 5 Oct 2026 21:23:34 -0400 Subject: [PATCH] Polish React analysis layout and tables ## Summary - Move the collapsible Analysis tools panel below the page content and footer so it no longer crowds the main logo and title. - Size interactive tables to their visible columns, estimate readable default widths from their contents, and preserve horizontal scrolling when a table exceeds its available space. - Render the heatmap guidance as a properly formatted, styled list instead of a raw Markdown code block. ## Verification - Reviewed the updated layout in the running React preview, including the clear header and redesigned heatmap legend. - `git diff --check` completed without errors. --- .../backend/app/services/reference_views.py | 8 +++++- fastapi_react/frontend/src/App.jsx | 2 +- .../frontend/src/components/Presentation.jsx | 2 +- .../frontend/src/components/ViewTable.jsx | 28 +++++++++++++++---- fastapi_react/frontend/src/parity.css | 12 ++++++-- raceAnalysis.py | 14 ++++------ 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/fastapi_react/backend/app/services/reference_views.py b/fastapi_react/backend/app/services/reference_views.py index f8498035..9a541af4 100644 --- a/fastapi_react/backend/app/services/reference_views.py +++ b/fastapi_react/backend/app/services/reference_views.py @@ -3124,7 +3124,13 @@ def _is_number(x): ui.image(str(mae_img), width=1000) except Exception: pass - ui.info('\n **Color scale**: darker/warmer colors indicate larger average absolute error.\n\n **Missing cells**: blank or neutral color means insufficient data (no races for that pair).\n\n **Sample size**: confidence intervals are empirical percentiles computed only when a group has at least 5 residuals.\n \n **Interpretation**: cells with darker colors indicate that the model has higher prediction errors for that driver/constructor at that circuit, suggesting potential areas for model improvement or unique performance characteristics.\n ') + ui.info( + '**How to read these heatmaps**\n\n' + '- **Color:** Darker, warmer cells indicate higher average absolute error.\n' + '- **Blank cells:** There were not enough races for that driver or constructor at that circuit.\n' + '- **Sample size:** Each cell averages the available races; confidence intervals are shown only for groups with at least 5 residuals.\n' + '- **Interpretation:** Treat higher-error cells as leads for further investigation. They can reflect model weaknesses or unusual race conditions.' + ) for img_name, title in [('heatmap_driver_by_circuit.png', 'Driver x Circuit heatmap'), ('heatmap_constructor_by_circuit.png', 'Constructor x Circuit heatmap')]: img_path = OUT_DIR / img_name if img_path.exists(): diff --git a/fastapi_react/frontend/src/App.jsx b/fastapi_react/frontend/src/App.jsx index eee61cec..81eed0b3 100644 --- a/fastapi_react/frontend/src/App.jsx +++ b/fastapi_react/frontend/src/App.jsx @@ -138,7 +138,6 @@ export default function App() { {sidebar && }
- {FEATURES_ENABLED &&
Analysis tools
}
Gridlocked {shell.length ?
:

F1 Races from 2016 to {new Date().getFullYear()}

} @@ -154,6 +153,7 @@ export default function App() { {busy && !FEATURES_ENABLED && Loading analysis…} + {FEATURES_ENABLED &&
Analysis tools
}
; } diff --git a/fastapi_react/frontend/src/components/Presentation.jsx b/fastapi_react/frontend/src/components/Presentation.jsx index dcb0dae6..1b9a31eb 100644 --- a/fastapi_react/frontend/src/components/Presentation.jsx +++ b/fastapi_react/frontend/src/components/Presentation.jsx @@ -222,7 +222,7 @@ export function ViewNodes({ nodes = [], values = {}, change = (_key, _value) => case 'json': return
{JSON.stringify(node.value, null, 2)}
; case 'notice': if (node.text === 'Research controls are disabled in hosted mode. Enable F1_RESEARCH_MODE=1 only for a trusted local/admin session; precomputed analyses remain available below.') return null; - return
{node.icon && {node.icon}}{node.text}
; + return
{node.icon && {node.icon}}
{node.text}
; case 'metric': return
{node.label}{node.value}{node.delta != null && {node.delta}}
; case 'divider': return
; case 'image': return {node.alt; diff --git a/fastapi_react/frontend/src/components/ViewTable.jsx b/fastapi_react/frontend/src/components/ViewTable.jsx index 7efa3ad1..7aa243a0 100644 --- a/fastapi_react/frontend/src/components/ViewTable.jsx +++ b/fastapi_react/frontend/src/components/ViewTable.jsx @@ -5,6 +5,12 @@ import {displayCell} from './Presentation'; import {useTheme} from './useTheme'; const empty=[]; +const DEFAULT_COLUMN_WIDTH=150; +const estimateColumnWidth=(column,index,rows)=>{ + const longest=rows.reduce((max,row)=>Math.max(max,String(row[index]??'').length),String(column.label??'').length); + const minimum=column.kind==='NumberColumn'?72:96; + return Math.min(320,Math.max(minimum,Math.ceil(longest*6+24))); +}; const quote=value=>{const text=value==null?'':String(value);return /[,"\r\n]/.test(text)?`"${text.replaceAll('"','""')}"`:text;}; function csvValue(value,column){ if(value==null)return ''; @@ -32,6 +38,16 @@ export function ViewTable({node}) { const [pinned,setPinned]=useState([]); const [formats,setFormats]=useState(/** @type {Record} */ ({})); const outer=useRef(null); + const [availableWidth,setAvailableWidth]=useState(0); + useEffect(()=>{ + const parent=outer.current?.parentElement; + if(!parent)return; + const measure=()=>setAvailableWidth(parent.clientWidth); + measure(); + const observer=new ResizeObserver(measure); + observer.observe(parent); + return()=>observer.disconnect(); + },[]); useEffect(()=>{ if(!menu && !showColumns)return; const close=event=>{if(event.type==='keydown' && event.key==='Escape' || event.type==='pointerdown' && !outer.current?.contains(event.target)){setMenu(null);setShowColumns(false);}}; @@ -50,10 +66,12 @@ export function ViewTable({node}) { },[rows,sort]); const visible=useMemo(()=>columns.map((column,index)=>({column,index})).filter(c=>!hidden.includes(c.index)).sort((a,b)=>Number(pinned.includes(b.index))-Number(pinned.includes(a.index))),[columns,hidden,pinned]); const gridColumns=useMemo(()=>{ - const result=visible.map(({column,index})=>({id:String(index),title:column.label+(sort?.column===index?(sort.desc?' ↓':' ↑'):''),hasMenu:true,width:widths[index] || (typeof column.width==='number'?column.width:undefined)})); - if(!node.hide_index)result.unshift({id:'index',title:node.index_name || '',width:widths.index}); + const result=visible.map(({column,index})=>({id:String(index),title:column.label+(sort?.column===index?(sort.desc?' ↓':' ↑'):''),hasMenu:true,width:widths[index] || Math.max(typeof column.width==='number'?column.width:0,estimateColumnWidth(column,index,rows))})); + if(!node.hide_index)result.unshift({id:'index',title:node.index_name || '',width:widths.index || 80}); return result; - },[visible,widths,sort,node.hide_index,node.index_name]); + },[visible,widths,sort,node.hide_index,node.index_name,rows]); + const contentWidth=gridColumns.reduce((total,column)=>total+(widths[column.id] || column.width || DEFAULT_COLUMN_WIDTH),0); + const tableWidth=Math.min(contentWidth,availableWidth || Infinity,typeof node.width==='number'?node.width:Infinity); const dark=useTheme()==='dark'; const bg=dark?'#0e1117':'#fff',text=dark?'#fafafa':'#31333f'; /** @type {(cell: import('@glideapps/glide-data-grid').Item) => import('@glideapps/glide-data-grid').GridCell} */ @@ -86,7 +104,7 @@ export function ViewTable({node}) { const height=node.height || Math.min(400,(rows.length+1)*35+3); const menuColumn=menu?columns[menu.index]:null; const numeric=menuColumn?.kind==='NumberColumn'; - return
+ return
@@ -95,6 +113,6 @@ export function ViewTable({node}) {
{showColumns &&
{columns.map((col,i)=>)}
} {menuColumn &&
{menuColumn.label}{numeric && }{rows.length.toLocaleString()} rows · {rows.filter(r=>r[menu.index]==null).length.toLocaleString()} missing · {new Set(rows.map(r=>r[menu.index])).size.toLocaleString()} unique
} - setShowSearch(false)} onHeaderMenuClick={(col,bounds)=>{const selected=visible[col-(node.hide_index?0:1)];if(selected)setMenu({index:selected.index,left:Math.max(0,Math.min(bounds.x-(outer.current?.getBoundingClientRect().x || 0),(outer.current?.clientWidth || 240)-240))});}} onColumnResize={(column,width)=>setWidths(old=>({...old,[column.id]:width}))} onColumnResizeEnd={(column,width)=>setWidths(old=>({...old,[column.id]:width}))} onHeaderClicked={col=>{const selected=visible[col-(node.hide_index?0:1)];if(selected)setSort(s=>s?.column===selected.index?(s.desc?null:{...s,desc:true}):{column:selected.index,desc:false});}} theme={{fontFamily:'Source Sans',baseFontStyle:'13px',headerFontStyle:'13px',cellHorizontalPadding:8,cellVerticalPadding:3,bgCell:bg,bgHeader:dark?'#262730':'#f7f9fc',bgHeaderHovered:dark?'#3a3d46':'#eff1f6',bgHeaderHasFocus:dark?'#3a3d46':'#eff1f6',textDark:text,textHeader:dark?'#bfc2ce':'#808495',textMedium:text,textLight:'#808495',borderColor:dark?'#3a3d46':'#e6e7eb',accentColor:'#ff4b4b',accentLight:dark?'#ff4b4b33':'#ff4b4b1a',accentFg:'#fff',headerBottomBorderColor:dark?'#3a3d46':'#d6d8df',roundingRadius:0}} /> + setShowSearch(false)} onHeaderMenuClick={(col,bounds)=>{const selected=visible[col-(node.hide_index?0:1)];if(selected)setMenu({index:selected.index,left:Math.max(0,Math.min(bounds.x-(outer.current?.getBoundingClientRect().x || 0),(outer.current?.clientWidth || 240)-240))});}} onColumnResize={(column,width)=>setWidths(old=>({...old,[column.id]:width}))} onColumnResizeEnd={(column,width)=>setWidths(old=>({...old,[column.id]:width}))} onHeaderClicked={col=>{const selected=visible[col-(node.hide_index?0:1)];if(selected)setSort(s=>s?.column===selected.index?(s.desc?null:{...s,desc:true}):{column:selected.index,desc:false});}} theme={{fontFamily:'Source Sans',baseFontStyle:'13px',headerFontStyle:'13px',cellHorizontalPadding:8,cellVerticalPadding:3,bgCell:bg,bgHeader:dark?'#262730':'#f7f9fc',bgHeaderHovered:dark?'#3a3d46':'#eff1f6',bgHeaderHasFocus:dark?'#3a3d46':'#eff1f6',textDark:text,textHeader:dark?'#bfc2ce':'#555965',textMedium:text,textLight:'#808495',borderColor:dark?'#3a3d46':'#e6e7eb',accentColor:'#ff4b4b',accentLight:dark?'#ff4b4b33':'#ff4b4b1a',accentFg:'#fff',headerBottomBorderColor:dark?'#3a3d46':'#d6d8df',roundingRadius:0}} />
; } diff --git a/fastapi_react/frontend/src/parity.css b/fastapi_react/frontend/src/parity.css index ca1e1c49..b72597ee 100644 --- a/fastapi_react/frontend/src/parity.css +++ b/fastapi_react/frontend/src/parity.css @@ -22,6 +22,8 @@ main:focus{outline:none} .shell-copy{margin-top:16px} .view-flow{display:flex;flex-direction:column;gap:16px;min-width:0} .view-flow:empty{display:none} +.analysis-tools{margin-top:24px;padding-top:12px;border-top:1px solid var(--border)} +.analysis-tools summary{cursor:pointer;color:var(--muted);font-size:14px;padding:6px 0} .view-heading{font-weight:600;letter-spacing:0;line-height:1.2;margin:0 0 -16px;padding:16px 0} h1.view-heading,.shell-title{font-size:44px;font-weight:700;padding:20px 0 16px;line-height:1.2} h2.view-heading{font-size:36px} @@ -54,8 +56,12 @@ h3.view-heading{font-size:28px} .number-input button{width:32px;flex-shrink:0;padding:0!important;border:0!important;border-radius:0;font-size:22px;background:transparent!important} .number-input button:hover{background:#e4e6ec!important} .view-notice{display:flex;gap:16px;padding:16px;border-radius:8px;background:#e8f2fc;color:#17517d;font-size:16px;line-height:25.6px} -.view-notice p{margin:0} -.view-notice.info{background:#e8f2fc;color:#17517d} +.view-notice-content{flex:1;min-width:0} +.view-notice-content p{margin:0 0 8px} +.view-notice-content p:last-child{margin-bottom:0} +.view-notice-content ul,.view-notice-content ol{margin:4px 0 0;padding-left:22px} +.view-notice-content li+li{margin-top:4px} +.view-notice.info{align-items:flex-start;border:1px solid #c8ddf0;border-left:4px solid #5799ce;background:#f2f7fc;color:#17436a;padding:12px 16px} .view-notice.warning{background:#fff8e1;color:#715000} .view-notice.error{background:#ffeded;color:#922222} .view-notice.success{background:#e5f7ed;color:#17683b} @@ -148,7 +154,7 @@ h3.view-heading{font-size:28px} .view-help{float:right;border:1px solid #808495;border-radius:50%;font-size:10px;font-weight:600;width:14px;height:14px;line-height:12px;text-align:center;margin-top:4px;color:#808495}.upload-file{padding:8px 16px;display:flex;justify-content:space-between}.upload-file button,.multiselect-box button{border:0;font-size:18px;line-height:1}.multiselect-field{position:relative}.multiselect-box{display:flex;flex-wrap:wrap;gap:8px;background:#f0f2f6;border-radius:8px;padding:8px 12px;min-height:40px}.select-tag{display:flex;align-items:center;gap:6px;background:#ff4b4b;color:white;padding:0 6px;border-radius:4px;font-size:14px;line-height:24px}.select-tag button{color:white}.multiselect-box input{min-width:40px;flex:1;width:40px;background:transparent;border:0;outline:none}.multiselect-options{position:absolute;top:100%;left:0;right:0;background:var(--page-bg);border:1px solid var(--border);box-shadow:0 4px 16px #0002;border-radius:8px;padding:8px;z-index:20;max-height:300px;overflow:auto}.multiselect-options button{display:block;width:100%;border:0;text-align:left;padding:8px;border-radius:4px}.multiselect-options button:hover{background:#f0f2f6}:root[data-theme=dark] .multiselect-box{background:#262730} .parity-nav,.tab-strip{position:relative}.tab-scroll{position:absolute!important;top:0!important;bottom:2px!important;width:20px!important;height:38px!important;z-index:4!important;border:0!important;background:var(--page-bg)!important;color:#808495!important;font-size:24px!important;line-height:38px!important;padding:0!important}.tab-scroll.left{left:0}.tab-scroll.right{right:0}.view-checkbox>span{font-size:14px;line-height:21px}.view-field>label,.view-field>span{display:flex;align-items:center;justify-content:space-between;min-height:24px}.view-help{margin-top:0}h3.view-heading{padding-top:12px} -.view-caption{color:inherit;opacity:.6}.chart-shell{position:relative;min-width:0}.chart-shell:hover>.table-toolbar,.chart-shell:focus-within>.table-toolbar{opacity:1}.chart-shell>.table-toolbar{top:0}.chart-shell:fullscreen{padding:40px;background:var(--page-bg)}.chart-shell:fullscreen .view-chart{height:calc(100vh - 80px)} +.view-caption{color:inherit}.chart-shell{position:relative;min-width:0}.chart-shell:hover>.table-toolbar,.chart-shell:focus-within>.table-toolbar{opacity:1}.chart-shell>.table-toolbar{top:0}.chart-shell:fullscreen{padding:40px;background:var(--page-bg)}.chart-shell:fullscreen .view-chart{height:calc(100vh - 80px)} .view-slider>label{position:relative;top:1px}.canvas-table .column-picker{top:30px} .grid-column-menu{position:absolute;top:35px;z-index:10;width:240px;padding:8px;border:1px solid var(--border);border-radius:8px;background:var(--page-bg);box-shadow:0 4px 16px #0002;font-size:14px;display:flex;flex-direction:column;gap:4px}.grid-column-menu>button{text-align:left;padding:8px;border:0;border-radius:4px;font-size:14px}.grid-column-menu>button:hover{background:#f0f2f6}.grid-column-menu strong,.grid-column-menu small{padding:4px 8px}.grid-column-menu label{display:flex;flex-direction:column;padding:8px;gap:4px}.grid-column-menu select{padding:6px;border:1px solid var(--border);border-radius:4px} .table-toolbar{pointer-events:none}.view-table:hover>.table-toolbar,.view-table:focus-within>.table-toolbar,.chart-shell:hover>.table-toolbar,.chart-shell:focus-within>.table-toolbar{pointer-events:auto}.chart-shell>.table-toolbar{z-index:6} diff --git a/raceAnalysis.py b/raceAnalysis.py index aa459f0c..1b6a2c39 100644 --- a/raceAnalysis.py +++ b/raceAnalysis.py @@ -6426,15 +6426,11 @@ def _is_number(x): # Notes (render in a shaded info box) st.info( - """ - **Color scale**: darker/warmer colors indicate larger average absolute error. - - **Missing cells**: blank or neutral color means insufficient data (no races for that pair). - - **Sample size**: confidence intervals are empirical percentiles computed only when a group has at least 5 residuals. - - **Interpretation**: cells with darker colors indicate that the model has higher prediction errors for that driver/constructor at that circuit, suggesting potential areas for model improvement or unique performance characteristics. - """ + "**How to read these heatmaps**\n\n" + "- **Color:** Darker, warmer cells indicate higher average absolute error.\n" + "- **Blank cells:** There were not enough races for that driver or constructor at that circuit.\n" + "- **Sample size:** Each cell averages the available races; confidence intervals are shown only for groups with at least 5 residuals.\n" + "- **Interpretation:** Treat higher-error cells as leads for further investigation. They can reflect model weaknesses or unusual race conditions." ) # Heatmaps