From c54957a389ace5954c983259feb433b41cf4107a Mon Sep 17 00:00:00 2001 From: vee1e <51952975+vee1e@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:40:10 +0530 Subject: [PATCH] merge: Update graph plot (#8) feat: updating graphing tool to compute for B.E. M.Sc. & Phoenix for better UI/UX --- frontend/src/components/ui/GraphPlot.tsx | 309 ++++++++++++++++++++--- 1 file changed, 276 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/ui/GraphPlot.tsx b/frontend/src/components/ui/GraphPlot.tsx index 29900fd..df03502 100644 --- a/frontend/src/components/ui/GraphPlot.tsx +++ b/frontend/src/components/ui/GraphPlot.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import DynamicDropdownForm from "./Dropdown"; import Plotly from "plotly.js-basic-dist"; import createPlotlyComponent from "react-plotly.js/factory"; @@ -22,11 +22,7 @@ interface Trace { mode: "lines+markers"; line?: { color?: string; width?: number }; marker?: { size?: number; line?: { width?: number; color?: string } }; -} - -interface PlotParams { - data?: Trace[]; - layout?: any; + visible?: boolean; } const Plot = createPlotlyComponent(Plotly); @@ -35,6 +31,49 @@ const PILANI = 0, GOA = 1, 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 = [ { key: "campus", @@ -49,7 +88,10 @@ const formConfig = [ function GraphPlot() { const { theme } = useTheme(); - const [graph, setGraph] = useState({}); + const [allBranches, setAllBranches] = useState([]); + const [visibleTraces, setVisibleTraces] = useState>( + new Map(), + ); const [isLoaded, setIsLoaded] = useState(false); const [formData, setForm] = useState<{ campus: number; [key: string]: any }>({ campus: PILANI, @@ -58,6 +100,25 @@ function GraphPlot() { const [error, setError] = useState(null); const [isMobile, setIsMobile] = useState(false); + // Queues for adding and removing branches progressively + const [renderQueue, setRenderQueue] = useState([]); + const [removeQueue, setRemoveQueue] = useState([]); + 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>(new Map()); + const allBranchesRef = useRef([]); + + // Keep ref in sync with state + useEffect(() => { + visibleTracesRef.current = visibleTraces; + }, [visibleTraces]); + useEffect(() => { const check = () => setIsMobile(window.innerWidth < 640); check(); @@ -65,10 +126,156 @@ function GraphPlot() { 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() { setLoading(true); setError(null); setIsLoaded(false); + setAllBranches([]); + setVisibleTraces(new Map()); + setRenderQueue([]); + setRemoveQueue([]); + setIsProcessing(false); const url = `${import.meta.env.VITE_API_URL}/graph?campus=${formData.campus}`; try { @@ -76,26 +283,19 @@ function GraphPlot() { if (!res.ok) throw new Error(`HTTP ${res.status}`); const response: GraphResponse = await res.json(); - const traces: Trace[] = response.branches.map((branch) => ({ - x: branch.years, - y: branch.marks, - name: branch.name, - type: "scatter", - mode: "lines+markers", - })); + if (!response.branches || !Array.isArray(response.branches)) { + throw new Error("Invalid response: branches data missing"); + } - setGraph({ - data: traces, - layout: { - title: { - text: "Cutoff Trends", - font: { - family: '"JetBrains Mono", monospace', - size: isMobile ? 14 : 18, - }, - }, - }, - }); + setAllBranches(response.branches); + allBranchesRef.current = response.branches; + + // Calculate initial visible branches + const visible = getVisibleBranches(response.branches); + + // Start progressive rendering + setRenderQueue(visible.map((b) => b.name)); + setIsProcessing(true); setIsLoaded(true); } catch (err) { console.error("Failed to load Plot. Error: ", err); @@ -131,6 +331,8 @@ function GraphPlot() { ? { l: 40, r: 8, t: 20, b: 80 } : { l: 50, r: 10, t: 30, b: 50 }; + const tracesArray = Array.from(visibleTraces.values()); + return (
{/* SELECTION PANEL */} @@ -147,6 +349,43 @@ function GraphPlot() { />
+ + {/* FILTERS */} + {isLoaded && ( +
+ + + +
+ )} + {loading && (

GENERATING PLOT...

)} @@ -155,12 +394,12 @@ function GraphPlot() { )} - {/* PLOT CONTAINER */} - {isLoaded && graph.data && ( + {/* PLOT CONTAINER - Always visible once loaded */} + {isLoaded && (
({ + data={tracesArray.map((trace: any) => ({ ...trace, line: { ...trace.line, width: isMobile ? 2 : 3 }, marker: { @@ -170,7 +409,13 @@ function GraphPlot() { }, }))} layout={{ - ...graph.layout, + title: { + text: "Cutoff Trends", + font: { + family: '"JetBrains Mono", monospace', + size: isMobile ? 14 : 18, + }, + }, dragmode: false, plot_bgcolor: bgColor, paper_bgcolor: bgColor, @@ -180,7 +425,6 @@ function GraphPlot() { size: isMobile ? 10 : 12, }, xaxis: { - ...graph.layout?.xaxis, gridcolor: gridColor, zerolinecolor: gridColor, tickfont: { @@ -198,7 +442,6 @@ function GraphPlot() { }, }, yaxis: { - ...graph.layout?.yaxis, gridcolor: gridColor, zerolinecolor: gridColor, tickfont: { @@ -216,11 +459,11 @@ function GraphPlot() { }, }, legend: { - ...graph.layout?.legend, ...legendConfig, }, margin: margins, autosize: true, + showlegend: tracesArray.length > 0, }} config={{ responsive: true,