Ashoka74's picture
Deploy: SCU_v1 default, Sankey, filter reconciliation, nested-expander fixes, deps
0e62cc9
Raw
History Blame Contribute Delete
13.2 kB
import { useState } from 'react';
import { Play, AlertTriangle, CheckCircle2, Layers, BarChart3, Grid3x3, Settings2, Network } from 'lucide-react';
import { api } from '../../api/client';
import { useStore } from '../../store/useStore';
import { Panel } from '../common/Panel';
import { LoadingSpinner } from '../common/LoadingSpinner';
import { ClusterVisualization } from './ClusterVisualization';
import { CorrelationHeatmap } from './CorrelationHeatmap';
import { XGBoostResults } from './XGBoostResults';
import { DistributionChart } from './DistributionChart';
import { CramersVExplorer } from './CramersVExplorer';
import type { AnalysisResponse, XGBoostResult } from '../../types';
type TabId = 'clusters' | 'correlation' | 'xgboost' | 'distribution' | 'association';
export function AnalysisPage() {
const { data, dataLoaded, analysisResults, setAnalysisResults, analysisRunning, setAnalysisRunning, setPage } = useStore();
const [selected, setSelected] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<TabId>('clusters');
// XGBoost feature importance handed over from the Cramér's V explorer.
const [assocXgboost, setAssocXgboost] = useState<Record<string, XGBoostResult> | null>(null);
const handleAssocXgboost = (r: Record<string, XGBoostResult>) => {
setAssocXgboost(r);
setActiveTab('xgboost');
};
// Cluster pipeline tuning (mirrors analyzing.py controls)
const [showParams, setShowParams] = useState(false);
const [enableTfidf, setEnableTfidf] = useState(false);
const [minClusterSize, setMinClusterSize] = useState(15);
const [nNeighbors, setNNeighbors] = useState(15);
const [minDist, setMinDist] = useState(0.1);
const columns = data?.columns ?? [];
const results: AnalysisResponse | null = analysisResults;
const toggleColumn = (col: string) => {
setSelected((prev) =>
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
);
};
const runAnalysis = async () => {
if (selected.length === 0) return;
setError(null);
setAnalysisRunning(true);
try {
const res = await api.runAnalysis(selected, {
enable_tfidf: enableTfidf,
min_cluster_size: minClusterSize,
n_neighbors: nNeighbors,
min_dist: minDist,
});
setAnalysisResults(res);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Analysis failed');
setAnalysisRunning(false);
}
};
if (!dataLoaded) {
return (
<Panel title="No Data Loaded">
<p className="text-sm text-text-muted">
Load a dataset first from the{' '}
<button onClick={() => setPage('dashboard')} className="text-accent hover:underline">
Dashboard
</button>{' '}
or{' '}
<button onClick={() => setPage('data')} className="text-accent hover:underline">
Data Explorer
</button>.
</p>
</Panel>
);
}
const tabs: { id: TabId; label: string; icon: typeof Layers; needsResults: boolean }[] = [
{ id: 'clusters', label: 'Clusters', icon: Layers, needsResults: true },
{ id: 'correlation', label: 'Correlation', icon: Grid3x3, needsResults: true },
{ id: 'xgboost', label: 'Feature Importance', icon: BarChart3, needsResults: true },
{ id: 'distribution', label: 'Distribution', icon: BarChart3, needsResults: true },
{ id: 'association', label: "Cramér's V Explorer", icon: Network, needsResults: false },
];
return (
<div className="space-y-4">
{/* Column selector */}
<Panel
title="Select Columns for Analysis"
subtitle="Choose columns to run through the clustering and ML pipeline"
actions={
<div className="flex items-center gap-2">
<button
onClick={() => setShowParams((s) => !s)}
className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs transition-colors ${
showParams ? 'border-accent text-accent' : 'border-border text-text-secondary hover:text-text-primary'
}`}
>
<Settings2 className="h-3.5 w-3.5" /> Params
</button>
<button
onClick={runAnalysis}
disabled={selected.length === 0 || analysisRunning}
className="flex items-center gap-2 rounded-md bg-accent-dim px-4 py-1.5 text-xs font-medium text-white transition-colors hover:bg-accent disabled:opacity-50"
>
<Play className="h-3.5 w-3.5" />
{analysisRunning ? 'Running...' : 'Run Analysis'}
</button>
</div>
}
>
<div className="flex flex-wrap gap-2">
{columns.map((col) => (
<button
key={col}
onClick={() => toggleColumn(col)}
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
selected.includes(col)
? 'border-accent bg-accent-dim/30 text-accent-bright'
: 'border-border bg-raised text-text-secondary hover:border-border-bright'
}`}
>
{col}
</button>
))}
</div>
{selected.length > 0 && (
<p className="mt-2 text-xs text-text-muted">
{selected.length} column{selected.length > 1 ? 's' : ''} selected
</p>
)}
{showParams && (
<div className="mt-3 grid grid-cols-1 gap-4 rounded-md border border-border/50 bg-raised/50 p-3 sm:grid-cols-2 lg:grid-cols-4">
<label className="flex items-center gap-2 text-xs text-text-secondary">
<input
type="checkbox"
checked={enableTfidf}
onChange={(e) => setEnableTfidf(e.target.checked)}
className="accent-accent"
/>
TF-IDF cluster naming + merging
</label>
<div>
<label className="mb-1 block text-[11px] text-text-muted">
Min cluster size: {minClusterSize}
</label>
<input
type="range" min={2} max={50} value={minClusterSize}
onChange={(e) => setMinClusterSize(Number(e.target.value))}
className="w-full accent-accent"
/>
</div>
<div>
<label className="mb-1 block text-[11px] text-text-muted">
UMAP neighbors: {nNeighbors}
</label>
<input
type="range" min={2} max={100} value={nNeighbors}
onChange={(e) => setNNeighbors(Number(e.target.value))}
className="w-full accent-accent"
/>
</div>
<div>
<label className="mb-1 block text-[11px] text-text-muted">
UMAP min dist: {minDist.toFixed(2)}
</label>
<input
type="range" min={0} max={1} step={0.05} value={minDist}
onChange={(e) => setMinDist(Number(e.target.value))}
className="w-full accent-accent"
/>
</div>
</div>
)}
</Panel>
{analysisRunning && <LoadingSpinner text="Running analysis pipeline..." />}
{error && (
<div className="flex items-center gap-2 rounded-md border border-danger/30 bg-danger/10 px-4 py-2.5 text-sm text-danger">
<AlertTriangle className="h-4 w-4" /> {error}
</div>
)}
{/* Success message */}
{results && (
<>
<div className="flex items-center gap-2 rounded-md border border-success/30 bg-success/10 px-4 py-2.5 text-sm text-success">
<CheckCircle2 className="h-4 w-4" />
Analysis complete for {Object.keys(results.results).length} column(s)
</div>
{results.mock_mode && (
<div className="flex items-center gap-2 rounded-md border border-warning/30 bg-warning/10 px-4 py-2.5 text-sm text-warning">
<AlertTriangle className="h-4 w-4" />
Mock analysis mode is enabled. Outputs are for demo/debugging, not scientific conclusions.
</div>
)}
</>
)}
{/* Tab bar — always available; the Cramér's V explorer needs no analysis run */}
<div className="flex flex-wrap gap-1 border-b border-border">
{tabs.map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setActiveTab(id)}
className={`flex items-center gap-1.5 border-b-2 px-4 py-2.5 text-xs font-medium transition-colors ${
activeTab === id
? 'border-accent text-accent'
: 'border-transparent text-text-muted hover:text-text-secondary'
}`}
>
<Icon className="h-3.5 w-3.5" />
{label}
</button>
))}
</div>
{/* Association explorer — independent of the cluster pipeline. Its XGBoost
run is handed to the Feature Importance tab via handleAssocXgboost. */}
{activeTab === 'association' && (
<CramersVExplorer source="dataset" onXgboost={handleAssocXgboost} />
)}
{/* Feature importance — independent of the cluster pipeline: it shows the
explorer-driven results when present, otherwise the pipeline's. */}
{activeTab === 'xgboost' && (() => {
const xgb =
assocXgboost && Object.keys(assocXgboost).length > 0
? assocXgboost
: results?.xgboost ?? null;
if (xgb && Object.keys(xgb).length > 0) {
return (
<div className="space-y-3">
{assocXgboost && Object.keys(assocXgboost).length > 0 && (
<div className="flex items-center gap-2 rounded-md border border-purple/30 bg-purple/10 px-4 py-2.5 text-xs text-text-secondary">
<Network className="h-4 w-4 text-purple" />
Computed directly from your Cramér's V column selection — each column predicted
from the others.
</div>
)}
<XGBoostResults results={xgb} />
</div>
);
}
return (
<Panel title="Feature Importance">
<p className="text-sm text-text-muted">
No feature-importance results yet. Run the cluster pipeline above, or open the{' '}
<button onClick={() => setActiveTab('association')} className="text-accent hover:underline">
Cramér's V Explorer
</button>
, select columns, and click <span className="text-accent">Feature Importance →</span>.
</p>
</Panel>
);
})()}
{/* Result-dependent tabs */}
{activeTab !== 'association' && activeTab !== 'xgboost' && !results && (
<Panel title="Run the analysis pipeline">
<p className="text-sm text-text-muted">
Select columns above and click <span className="text-accent">Run Analysis</span> to
populate clustering, correlation, feature-importance and distribution views. The{' '}
<button onClick={() => setActiveTab('association')} className="text-accent hover:underline">
Cramér's V Explorer
</button>{' '}
works directly on raw columns without running the pipeline.
</p>
</Panel>
)}
{results && (
<>
{/* Cluster visualizations */}
{activeTab === 'clusters' && (
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
{Object.entries(results.cluster_viz).map(([col, viz]) => (
<Panel key={col} title={viz.title}>
<ClusterVisualization viz={viz} />
</Panel>
))}
</div>
)}
{/* Correlation heatmap */}
{activeTab === 'correlation' && results.cramers_v && (
<Panel title="Cramer's V Correlation Matrix">
<CorrelationHeatmap data={results.cramers_v} height={500} />
</Panel>
)}
{activeTab === 'correlation' && !results.cramers_v && (
<Panel title="Correlation">
<p className="text-sm text-text-muted">
Select at least 2 columns to compute correlation analysis.
</p>
</Panel>
)}
{/* Distribution */}
{activeTab === 'distribution' && (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
{Object.entries(results.results).map(([col, analysis]) => (
<Panel key={col} title={`${col} (${analysis.cluster_count} clusters)`}>
<DistributionChart
data={analysis.distribution}
title={col}
height={280}
/>
</Panel>
))}
</div>
)}
</>
)}
</div>
);
}