mirror of
https://github.com/vee1e/bitsat_predictor.git
synced 2026-09-01 10:59:02 +00:00
merge: Update graph plot (#8)
feat: updating graphing tool to compute for B.E. M.Sc. & Phoenix for better UI/UX
This commit is contained in:
parent
7d89d7717c
commit
c54957a389
1 changed files with 276 additions and 33 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import DynamicDropdownForm from "./Dropdown";
|
import DynamicDropdownForm from "./Dropdown";
|
||||||
import Plotly from "plotly.js-basic-dist";
|
import Plotly from "plotly.js-basic-dist";
|
||||||
import createPlotlyComponent from "react-plotly.js/factory";
|
import createPlotlyComponent from "react-plotly.js/factory";
|
||||||
|
|
@ -22,11 +22,7 @@ interface Trace {
|
||||||
mode: "lines+markers";
|
mode: "lines+markers";
|
||||||
line?: { color?: string; width?: number };
|
line?: { color?: string; width?: number };
|
||||||
marker?: { size?: number; line?: { width?: number; color?: string } };
|
marker?: { size?: number; line?: { width?: number; color?: string } };
|
||||||
}
|
visible?: boolean;
|
||||||
|
|
||||||
interface PlotParams {
|
|
||||||
data?: Trace[];
|
|
||||||
layout?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plot = createPlotlyComponent(Plotly);
|
const Plot = createPlotlyComponent(Plotly);
|
||||||
|
|
@ -35,6 +31,49 @@ const PILANI = 0,
|
||||||
GOA = 1,
|
GOA = 1,
|
||||||
HYDERABAD = 2;
|
HYDERABAD = 2;
|
||||||
|
|
||||||
|
// Phoenix branch keywords (flexible matching)
|
||||||
|
const PHOENIX_KEYWORDS = [
|
||||||
|
"computer science",
|
||||||
|
"electrical",
|
||||||
|
"electronics",
|
||||||
|
"math",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if a branch is phoenix (electrical/cse related)
|
||||||
|
function isPhoenixBranch(branchName: string): boolean {
|
||||||
|
const lower = branchName.toLowerCase();
|
||||||
|
return PHOENIX_KEYWORDS.some((keyword) => lower.includes(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check branch type
|
||||||
|
function getBranchType(branchName: string): "be" | "msc" | "other" {
|
||||||
|
const lower = branchName.toLowerCase();
|
||||||
|
|
||||||
|
// M.Sc branches
|
||||||
|
if (
|
||||||
|
lower.includes("m.sc") ||
|
||||||
|
lower.includes("biology") ||
|
||||||
|
lower.includes("chemistry") ||
|
||||||
|
lower.includes("economics") ||
|
||||||
|
lower.includes("mathematics") ||
|
||||||
|
lower.includes("physics") ||
|
||||||
|
lower.includes("general studies")
|
||||||
|
) {
|
||||||
|
return "msc";
|
||||||
|
}
|
||||||
|
|
||||||
|
// B.E. branches
|
||||||
|
if (
|
||||||
|
lower.includes("engineering") ||
|
||||||
|
lower.includes("b.e") ||
|
||||||
|
lower.includes("computer science")
|
||||||
|
) {
|
||||||
|
return "be";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
const formConfig = [
|
const formConfig = [
|
||||||
{
|
{
|
||||||
key: "campus",
|
key: "campus",
|
||||||
|
|
@ -49,7 +88,10 @@ const formConfig = [
|
||||||
|
|
||||||
function GraphPlot() {
|
function GraphPlot() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [graph, setGraph] = useState<PlotParams>({});
|
const [allBranches, setAllBranches] = useState<BranchData[]>([]);
|
||||||
|
const [visibleTraces, setVisibleTraces] = useState<Map<string, Trace>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||||
const [formData, setForm] = useState<{ campus: number; [key: string]: any }>({
|
const [formData, setForm] = useState<{ campus: number; [key: string]: any }>({
|
||||||
campus: PILANI,
|
campus: PILANI,
|
||||||
|
|
@ -58,6 +100,25 @@ function GraphPlot() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
// Queues for adding and removing branches progressively
|
||||||
|
const [renderQueue, setRenderQueue] = useState<string[]>([]);
|
||||||
|
const [removeQueue, setRemoveQueue] = useState<string[]>([]);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [showBE, setShowBE] = useState(true);
|
||||||
|
const [showMSc, setShowMSc] = useState(true);
|
||||||
|
const [showPhoenixOnly, setShowPhoenixOnly] = useState(false);
|
||||||
|
|
||||||
|
// Refs for tracking state - used to avoid dependency cycles
|
||||||
|
const visibleTracesRef = useRef<Map<string, Trace>>(new Map());
|
||||||
|
const allBranchesRef = useRef<BranchData[]>([]);
|
||||||
|
|
||||||
|
// Keep ref in sync with state
|
||||||
|
useEffect(() => {
|
||||||
|
visibleTracesRef.current = visibleTraces;
|
||||||
|
}, [visibleTraces]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const check = () => setIsMobile(window.innerWidth < 640);
|
const check = () => setIsMobile(window.innerWidth < 640);
|
||||||
check();
|
check();
|
||||||
|
|
@ -65,10 +126,156 @@ function GraphPlot() {
|
||||||
return () => window.removeEventListener("resize", check);
|
return () => window.removeEventListener("resize", check);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Determine if a branch should be visible based on current filters
|
||||||
|
const shouldShowBranch = useCallback(
|
||||||
|
(branch: BranchData): boolean => {
|
||||||
|
const type = getBranchType(branch.name);
|
||||||
|
const isPhoenix = isPhoenixBranch(branch.name);
|
||||||
|
|
||||||
|
// Phoenix only filter takes precedence - if enabled, only show phoenix branches
|
||||||
|
if (showPhoenixOnly) {
|
||||||
|
return (
|
||||||
|
isPhoenix &&
|
||||||
|
((type === "be" && showBE) ||
|
||||||
|
(type === "msc" && showMSc) ||
|
||||||
|
type === "other")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise apply BE/MSc filters
|
||||||
|
if (type === "be" && !showBE) return false;
|
||||||
|
if (type === "msc" && !showMSc) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[showBE, showMSc, showPhoenixOnly],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all branches that should be visible
|
||||||
|
const getVisibleBranches = useCallback(
|
||||||
|
(branches: BranchData[]): BranchData[] => {
|
||||||
|
return branches.filter(shouldShowBranch);
|
||||||
|
},
|
||||||
|
[shouldShowBranch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Progressive processing effect - handles both adding and removing branches
|
||||||
|
useEffect(() => {
|
||||||
|
// Stop processing if both queues are empty
|
||||||
|
if (
|
||||||
|
(renderQueue.length === 0 && removeQueue.length === 0) ||
|
||||||
|
!isProcessing
|
||||||
|
) {
|
||||||
|
setIsProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
// Process remove queue first (remove one branch)
|
||||||
|
if (removeQueue.length > 0) {
|
||||||
|
const branchName = removeQueue[0];
|
||||||
|
setVisibleTraces((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(branchName);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setRemoveQueue((prev) => prev.slice(1));
|
||||||
|
}
|
||||||
|
// Then process render queue (add one branch)
|
||||||
|
else if (renderQueue.length > 0) {
|
||||||
|
const branchName = renderQueue[0];
|
||||||
|
const branch = allBranchesRef.current.find(
|
||||||
|
(b) => b.name === branchName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
const newTrace: Trace = {
|
||||||
|
x: branch.years,
|
||||||
|
y: branch.marks,
|
||||||
|
name: branch.name,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
};
|
||||||
|
|
||||||
|
setVisibleTraces((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(branch.name, newTrace);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setRenderQueue((prev) => prev.slice(1));
|
||||||
|
}
|
||||||
|
}, 0); // 0ms delay - render instantly
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [renderQueue, removeQueue, isProcessing]);
|
||||||
|
|
||||||
|
// Smart filter change handler - only adds/removes what changed
|
||||||
|
// This effect runs when filters change, using refs to avoid dependency on visibleTraces
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoaded || allBranches.length === 0) return;
|
||||||
|
|
||||||
|
// Use setTimeout to access the latest ref values without adding dependencies
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Calculate which branches should be visible now
|
||||||
|
const shouldBeVisible = new Set(
|
||||||
|
getVisibleBranches(allBranches).map((b) => b.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate which branches are currently visible (from ref to avoid dependency)
|
||||||
|
const currentlyVisible = new Set(visibleTracesRef.current.keys());
|
||||||
|
|
||||||
|
// Branches to add (should be visible but aren't)
|
||||||
|
const toAdd: string[] = [];
|
||||||
|
shouldBeVisible.forEach((name) => {
|
||||||
|
if (!currentlyVisible.has(name)) {
|
||||||
|
toAdd.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Branches to remove (are visible but shouldn't be)
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
currentlyVisible.forEach((name) => {
|
||||||
|
if (!shouldBeVisible.has(name)) {
|
||||||
|
toRemove.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up queues and start processing
|
||||||
|
allBranchesRef.current = allBranches;
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
setRemoveQueue(toRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
setRenderQueue(toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRemove.length > 0 || toAdd.length > 0) {
|
||||||
|
setIsProcessing(true);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [
|
||||||
|
showBE,
|
||||||
|
showMSc,
|
||||||
|
showPhoenixOnly,
|
||||||
|
allBranches,
|
||||||
|
isLoaded,
|
||||||
|
getVisibleBranches,
|
||||||
|
]);
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
|
setAllBranches([]);
|
||||||
|
setVisibleTraces(new Map());
|
||||||
|
setRenderQueue([]);
|
||||||
|
setRemoveQueue([]);
|
||||||
|
setIsProcessing(false);
|
||||||
|
|
||||||
const url = `${import.meta.env.VITE_API_URL}/graph?campus=${formData.campus}`;
|
const url = `${import.meta.env.VITE_API_URL}/graph?campus=${formData.campus}`;
|
||||||
try {
|
try {
|
||||||
|
|
@ -76,26 +283,19 @@ function GraphPlot() {
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const response: GraphResponse = await res.json();
|
const response: GraphResponse = await res.json();
|
||||||
|
|
||||||
const traces: Trace[] = response.branches.map((branch) => ({
|
if (!response.branches || !Array.isArray(response.branches)) {
|
||||||
x: branch.years,
|
throw new Error("Invalid response: branches data missing");
|
||||||
y: branch.marks,
|
}
|
||||||
name: branch.name,
|
|
||||||
type: "scatter",
|
|
||||||
mode: "lines+markers",
|
|
||||||
}));
|
|
||||||
|
|
||||||
setGraph({
|
setAllBranches(response.branches);
|
||||||
data: traces,
|
allBranchesRef.current = response.branches;
|
||||||
layout: {
|
|
||||||
title: {
|
// Calculate initial visible branches
|
||||||
text: "Cutoff Trends",
|
const visible = getVisibleBranches(response.branches);
|
||||||
font: {
|
|
||||||
family: '"JetBrains Mono", monospace',
|
// Start progressive rendering
|
||||||
size: isMobile ? 14 : 18,
|
setRenderQueue(visible.map((b) => b.name));
|
||||||
},
|
setIsProcessing(true);
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load Plot. Error: ", err);
|
console.error("Failed to load Plot. Error: ", err);
|
||||||
|
|
@ -131,6 +331,8 @@ function GraphPlot() {
|
||||||
? { l: 40, r: 8, t: 20, b: 80 }
|
? { l: 40, r: 8, t: 20, b: 80 }
|
||||||
: { l: 50, r: 10, t: 30, b: 50 };
|
: { l: 50, r: 10, t: 30, b: 50 };
|
||||||
|
|
||||||
|
const tracesArray = Array.from(visibleTraces.values());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-4xl mx-auto">
|
<div className="w-full max-w-4xl mx-auto">
|
||||||
{/* SELECTION PANEL */}
|
{/* SELECTION PANEL */}
|
||||||
|
|
@ -147,6 +349,43 @@ function GraphPlot() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FILTERS */}
|
||||||
|
{isLoaded && (
|
||||||
|
<div className="mt-6 flex flex-wrap justify-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBE(!showBE)}
|
||||||
|
className={`px-4 py-2 font-bold text-sm uppercase border-2 transition-all ${
|
||||||
|
showBE
|
||||||
|
? "bg-[var(--brutal-accent)] text-white border-[var(--brutal-accent)]"
|
||||||
|
: "bg-transparent text-[var(--brutal-text)] border-[var(--brutal-border)] hover:border-[var(--brutal-accent)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
B.E.
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowMSc(!showMSc)}
|
||||||
|
className={`px-4 py-2 font-bold text-sm uppercase border-2 transition-all ${
|
||||||
|
showMSc
|
||||||
|
? "bg-[var(--brutal-accent)] text-white border-[var(--brutal-accent)]"
|
||||||
|
: "bg-transparent text-[var(--brutal-text)] border-[var(--brutal-border)] hover:border-[var(--brutal-accent)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
M.Sc
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPhoenixOnly(!showPhoenixOnly)}
|
||||||
|
className={`px-4 py-2 font-bold text-sm uppercase border-2 transition-all ${
|
||||||
|
showPhoenixOnly
|
||||||
|
? "bg-orange-500 text-white border-orange-500"
|
||||||
|
: "bg-transparent text-[var(--brutal-text)] border-[var(--brutal-border)] hover:border-orange-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Phoenix Only
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<p className="mt-4 font-bold animate-pulse">GENERATING PLOT...</p>
|
<p className="mt-4 font-bold animate-pulse">GENERATING PLOT...</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -155,12 +394,12 @@ function GraphPlot() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PLOT CONTAINER */}
|
{/* PLOT CONTAINER - Always visible once loaded */}
|
||||||
{isLoaded && graph.data && (
|
{isLoaded && (
|
||||||
<div className="brutal-box p-2 sm:p-4 bg-[var(--brutal-bg)] overflow-hidden">
|
<div className="brutal-box p-2 sm:p-4 bg-[var(--brutal-bg)] overflow-hidden">
|
||||||
<div className="w-full h-[350px] sm:h-[400px] md:h-[500px] relative">
|
<div className="w-full h-[350px] sm:h-[400px] md:h-[500px] relative">
|
||||||
<Plot
|
<Plot
|
||||||
data={graph.data.map((trace: any) => ({
|
data={tracesArray.map((trace: any) => ({
|
||||||
...trace,
|
...trace,
|
||||||
line: { ...trace.line, width: isMobile ? 2 : 3 },
|
line: { ...trace.line, width: isMobile ? 2 : 3 },
|
||||||
marker: {
|
marker: {
|
||||||
|
|
@ -170,7 +409,13 @@ function GraphPlot() {
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
layout={{
|
layout={{
|
||||||
...graph.layout,
|
title: {
|
||||||
|
text: "Cutoff Trends",
|
||||||
|
font: {
|
||||||
|
family: '"JetBrains Mono", monospace',
|
||||||
|
size: isMobile ? 14 : 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
dragmode: false,
|
dragmode: false,
|
||||||
plot_bgcolor: bgColor,
|
plot_bgcolor: bgColor,
|
||||||
paper_bgcolor: bgColor,
|
paper_bgcolor: bgColor,
|
||||||
|
|
@ -180,7 +425,6 @@ function GraphPlot() {
|
||||||
size: isMobile ? 10 : 12,
|
size: isMobile ? 10 : 12,
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
...graph.layout?.xaxis,
|
|
||||||
gridcolor: gridColor,
|
gridcolor: gridColor,
|
||||||
zerolinecolor: gridColor,
|
zerolinecolor: gridColor,
|
||||||
tickfont: {
|
tickfont: {
|
||||||
|
|
@ -198,7 +442,6 @@ function GraphPlot() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
...graph.layout?.yaxis,
|
|
||||||
gridcolor: gridColor,
|
gridcolor: gridColor,
|
||||||
zerolinecolor: gridColor,
|
zerolinecolor: gridColor,
|
||||||
tickfont: {
|
tickfont: {
|
||||||
|
|
@ -216,11 +459,11 @@ function GraphPlot() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...graph.layout?.legend,
|
|
||||||
...legendConfig,
|
...legendConfig,
|
||||||
},
|
},
|
||||||
margin: margins,
|
margin: margins,
|
||||||
autosize: true,
|
autosize: true,
|
||||||
|
showlegend: tracesArray.length > 0,
|
||||||
}}
|
}}
|
||||||
config={{
|
config={{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue