From dd7861c2e4308bbb51dfbbd0ef409f7fbe523a18 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Thu, 26 Sep 2024 07:10:25 +0200 Subject: [PATCH 1/6] Fixing import error --- .../aiDashGraphs/HourlyPredictionGraph.tsx | 89 ++++++++ .../src/pages/aiDashboard/AiDashboard.tsx | 196 +++++++++++------- 2 files changed, 215 insertions(+), 70 deletions(-) create mode 100644 frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx diff --git a/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx new file mode 100644 index 00000000..65788d2b --- /dev/null +++ b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; + +// Define the interface for the hourly prediction data +interface HourlyPrediction { + Hour: number; + Predicted_Attendance_Level: string; + Predicted_Class: number; +} + +interface ResponseData { + Date: string; + Day_of_Week: number; + Hourly_Predictions: HourlyPrediction[]; + Is_Weekend: boolean; + Special_Event: boolean; +} + +const HourlyPredictionGraph = () => { + const [predictedData, setPredictedData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const levelColors: { [key: number]: string } = { + 1: "#4CAF50", // Green + 2: "#8BC34A", // Light Green + 3: "#CDDC39", // Yellow Green + 4: "#FFC107", // Yellow + 5: "#FF9800", // Orange + 6: "#FF5722", // Deep Orange + 7: "#F44336", // Red + }; + + useEffect(() => { + const fetchData = async () => { + try { + const response = await axios.get( + "https://ai.occupi.tech/predict_day" + ); + setPredictedData(response.data); + setLoading(false); + } catch (err) { + setError(err as Error); + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + if (!predictedData) { + return
No data available
; + } + + return ( +
+

+ Predictions for {predictedData.Date} +

+
+ {predictedData.Hourly_Predictions.map((prediction, index) => ( +
+
+ Hour: {prediction.Hour}:00 +
+
+
+ Predicted Attendance: {prediction.Predicted_Attendance_Level} +
+
Level: {prediction.Predicted_Class}
+
+ ))} +
+
+ ); +}; + +export default HourlyPredictionGraph; diff --git a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx index 9b26aeeb..fe549216 100644 --- a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx +++ b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx @@ -1,80 +1,123 @@ import React, { useState, useEffect } from "react"; -import { TopNav, AiDashCard, PredictedCapacityGraph, CapacityComparisonGraph } from "@components/index"; -import { FaUsers, FaBed, FaClipboardList, FaCalendarCheck, FaUndo, FaPlus } from "react-icons/fa"; -import { Responsive, WidthProvider, Layout, Layouts } from 'react-grid-layout'; -import 'react-grid-layout/css/styles.css'; -import 'react-resizable/css/styles.css'; +import { + TopNav, + AiDashCard, + PredictedCapacityGraph, + CapacityComparisonGraph, +} from "@components/index"; +import { + FaUsers, + FaBed, + FaClipboardList, + FaCalendarCheck, + FaUndo, + FaPlus, +} from "react-icons/fa"; +import { Responsive, WidthProvider, Layout, Layouts } from "react-grid-layout"; +import "react-grid-layout/css/styles.css"; +import "react-resizable/css/styles.css"; +import { HourlyPredictionGraph } from "@components/index"; const ResponsiveGridLayout = WidthProvider(Responsive); const defaultLayouts: Layouts = { lg: [ - { i: 'card1', x: 0, y: 0, w: 3, h: 2 }, - { i: 'card2', x: 3, y: 0, w: 3, h: 2 }, - { i: 'card3', x: 6, y: 0, w: 3, h: 2 }, - { i: 'card4', x: 9, y: 0, w: 3, h: 2 }, - { i: 'graph1', x: 0, y: 2, w: 6, h: 4 }, - { i: 'graph2', x: 6, y: 2, w: 6, h: 4 }, - ] + { i: "card1", x: 0, y: 0, w: 3, h: 2 }, + { i: "card2", x: 3, y: 0, w: 3, h: 2 }, + { i: "card3", x: 6, y: 0, w: 3, h: 2 }, + { i: "card4", x: 9, y: 0, w: 3, h: 2 }, + { i: "graph1", x: 0, y: 2, w: 6, h: 4 }, + { i: "graph2", x: 6, y: 2, w: 6, h: 4 }, + { i: "hourlyPrediction", x: 0, y: 6, w: 12, h: 3 }, + ], }; const cardData = [ - { id: 'card1', title: "Office Occupancy", icon: , stat: "65%", trend: 3.46 }, - { id: 'card2', title: "Available Desks", icon: , stat: "89", trend: -2.1 }, - { id: 'card3', title: "Reservations", icon: , stat: "45", trend: 8.7 }, - { id: 'card4', title: "Check-ins Today", icon: , stat: "23", trend: 3.4 }, + { + id: "card1", + title: "Office Occupancy", + icon: , + stat: "65%", + trend: 3.46, + }, + { + id: "card2", + title: "Available Desks", + icon: , + stat: "89", + trend: -2.1, + }, + { + id: "card3", + title: "Reservations", + icon: , + stat: "45", + trend: 8.7, + }, + { + id: "card4", + title: "Check-ins Today", + icon: , + stat: "23", + trend: 3.4, + }, ]; const originalCardLayouts: { [key: string]: Layout } = { - card1: { i: 'card1', x: 0, y: 0, w: 3, h: 2 }, - card2: { i: 'card2', x: 3, y: 0, w: 3, h: 2 }, - card3: { i: 'card3', x: 6, y: 0, w: 3, h: 2 }, - card4: { i: 'card4', x: 9, y: 0, w: 3, h: 2 }, + card1: { i: "card1", x: 0, y: 0, w: 3, h: 2 }, + card2: { i: "card2", x: 3, y: 0, w: 3, h: 2 }, + card3: { i: "card3", x: 6, y: 0, w: 3, h: 2 }, + card4: { i: "card4", x: 9, y: 0, w: 3, h: 2 }, }; const AiDashboard: React.FC = () => { const [layouts, setLayouts] = useState(() => { - const savedLayouts = localStorage.getItem('dashboardLayouts'); + const savedLayouts = localStorage.getItem("dashboardLayouts"); if (savedLayouts) { try { return JSON.parse(savedLayouts); } catch (error) { - console.error('Error parsing saved layouts:', error); + console.error("Error parsing saved layouts:", error); } } return defaultLayouts; }); const [visibleCards, setVisibleCards] = useState(() => { - const savedVisibleCards = localStorage.getItem('visibleCards'); + const savedVisibleCards = localStorage.getItem("visibleCards"); if (savedVisibleCards) { try { return JSON.parse(savedVisibleCards); } catch (error) { - console.error('Error parsing saved visible cards:', error); + console.error("Error parsing saved visible cards:", error); } } - return cardData.map(card => card.id); + return cardData.map((card) => card.id); }); const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { - localStorage.setItem('dashboardLayouts', JSON.stringify(layouts)); - localStorage.setItem('visibleCards', JSON.stringify(visibleCards)); + localStorage.setItem("dashboardLayouts", JSON.stringify(layouts)); + localStorage.setItem("visibleCards", JSON.stringify(visibleCards)); }, [layouts, visibleCards]); const onLayoutChange = (currentLayout: Layout[], allLayouts: Layouts) => { setLayouts(allLayouts); - const newVisibleCards = currentLayout.map((item: Layout) => item.i).filter((id: string) => cardData.some(card => card.id === id)); + const newVisibleCards = currentLayout + .map((item: Layout) => item.i) + .filter((id: string) => cardData.some((card) => card.id === id)); setVisibleCards(newVisibleCards); }; - + const resetToDefaultLayout = () => { setLayouts(defaultLayouts); - setVisibleCards(cardData.map(card => card.id)); - localStorage.setItem('dashboardLayouts', JSON.stringify(defaultLayouts)); - localStorage.setItem('visibleCards', JSON.stringify(cardData.map(card => card.id))); + setVisibleCards(cardData.map((card) => card.id)); + localStorage.setItem("dashboardLayouts", JSON.stringify(defaultLayouts)); + localStorage.setItem( + "visibleCards", + JSON.stringify(cardData.map((card) => card.id)) + ); }; const handleInputChange = (e: React.ChangeEvent) => { @@ -82,24 +125,33 @@ const AiDashboard: React.FC = () => { }; const handleRemoveCard = (cardId: string) => { - setVisibleCards(prev => prev.filter(id => id !== cardId)); - + setVisibleCards((prev) => prev.filter((id) => id !== cardId)); + const newLayouts = { ...layouts }; - Object.keys(newLayouts).forEach(breakpoint => { - newLayouts[breakpoint] = newLayouts[breakpoint].filter(item => item.i !== cardId); + Object.keys(newLayouts).forEach((breakpoint) => { + newLayouts[breakpoint] = newLayouts[breakpoint].filter( + (item) => item.i !== cardId + ); }); setLayouts(newLayouts); }; const handleAddCard = (cardId: string) => { if (!visibleCards.includes(cardId)) { - setVisibleCards(prev => [...prev, cardId]); - + setVisibleCards((prev) => [...prev, cardId]); + const newLayouts = { ...layouts }; - Object.keys(newLayouts).forEach(breakpoint => { + Object.keys(newLayouts).forEach((breakpoint) => { newLayouts[breakpoint] = [ ...newLayouts[breakpoint], - originalCardLayouts[cardId] || defaultLayouts.lg.find((item: Layout) => item.i === cardId) || { i: cardId, x: 0, y: 0, w: 3, h: 2 } + originalCardLayouts[cardId] || + defaultLayouts.lg.find((item: Layout) => item.i === cardId) || { + i: cardId, + x: 0, + y: 0, + w: 3, + h: 2, + }, ]; }); setLayouts(newLayouts); @@ -125,22 +177,21 @@ const AiDashboard: React.FC = () => {
- {cardData.map(card => ( - !visibleCards.includes(card.id) && ( - - ) - ))} + {cardData.map( + (card) => + !visibleCards.includes(card.id) && ( + + ) + )}
@@ -155,31 +206,36 @@ const AiDashboard: React.FC = () => { isResizable={true} compactType="vertical" preventCollision={false} - margin={[20, 20]} - > - {cardData.map(card => ( - visibleCards.includes(card.id) && ( -
- handleRemoveCard(card.id)} - /> -
- ) - ))} + margin={[20, 20]}> + {cardData.map( + (card) => + visibleCards.includes(card.id) && ( +
+ handleRemoveCard(card.id)} + /> +
+ ) + )}
- +
+
+ +
); }; -export default AiDashboard; \ No newline at end of file +export default AiDashboard; From cfb5a0477840b42d19c587b349d7736983bcd6f9 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Thu, 26 Sep 2024 07:16:25 +0200 Subject: [PATCH 2/6] Import fixed --- .../aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx | 2 +- frontend/occupi-web/src/components/index.ts | 4 +++- frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx index 65788d2b..9a6be929 100644 --- a/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx +++ b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx @@ -16,7 +16,7 @@ interface ResponseData { Special_Event: boolean; } -const HourlyPredictionGraph = () => { +const HourlyPredictionGraph: React.FC = () => { const [predictedData, setPredictedData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/frontend/occupi-web/src/components/index.ts b/frontend/occupi-web/src/components/index.ts index 4485e088..d3fa15ae 100644 --- a/frontend/occupi-web/src/components/index.ts +++ b/frontend/occupi-web/src/components/index.ts @@ -41,6 +41,7 @@ import AboutComponent from "./aboutComponent/AboutComponent"; import Security from "./securityComponent/Security"; import EditRoomModal from "./editRoomModal/EditRoomModal"; import AddRoomModal from "./addRoomModal/AddRoomModal"; +import HourlyPredictionGraph from "./aiDashboard/aiDashGraphs/HourlyPredictionGraph"; @@ -87,5 +88,6 @@ export { AboutComponent, Security, EditRoomModal, - AddRoomModal + AddRoomModal, + HourlyPredictionGraph } \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx index fe549216..5f38d7f4 100644 --- a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx +++ b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx @@ -4,6 +4,7 @@ import { AiDashCard, PredictedCapacityGraph, CapacityComparisonGraph, + HourlyPredictionGraph, } from "@components/index"; import { FaUsers, @@ -16,7 +17,6 @@ import { import { Responsive, WidthProvider, Layout, Layouts } from "react-grid-layout"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; -import { HourlyPredictionGraph } from "@components/index"; const ResponsiveGridLayout = WidthProvider(Responsive); From faa67f2216e100c5f9e04169486652da57310999 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Thu, 26 Sep 2024 08:20:35 +0200 Subject: [PATCH 3/6] Integrating hourly predictions --- .../aiDashGraphs/HourlyPredictionGraph.tsx | 144 +++++++++++++++--- python-code/app.py | 17 ++- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx index 9a6be929..cf99ece1 100644 --- a/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx +++ b/frontend/occupi-web/src/components/aiDashboard/aiDashGraphs/HourlyPredictionGraph.tsx @@ -1,4 +1,14 @@ import { useEffect, useState } from "react"; +import { + ResponsiveContainer, + BarChart, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Bar, + Cell, +} from "recharts"; import axios from "axios"; // Define the interface for the hourly prediction data @@ -16,6 +26,68 @@ interface ResponseData { Special_Event: boolean; } +// Define attendance levels for each day of the week +const attendance_levels_by_day: { [key: string]: string[] } = { + Monday: [ + "0-50", + "50-100", + "100-150", + "150-200", + "200-250", + "250-300", + "300+", + ], + Tuesday: [ + "0-300", + "300-600", + "600-900", + "900-1200", + "1200-1500", + "1500-1800", + "1800+", + ], + Wednesday: [ + "0-50", + "50-100", + "100-150", + "150-200", + "200-250", + "250-300", + "300+", + ], + Thursday: [ + "0-300", + "300-600", + "600-900", + "900-1200", + "1200-1500", + "1500-1800", + "1800+", + ], + Friday: [ + "0-50", + "50-100", + "100-150", + "150-200", + "200-250", + "250-300", + "300+", + ], + Saturday: ["0-25", "25-50", "50-75", "75-100", "100-125", "125-150", "150+"], + Sunday: ["0-10", "10-20", "20-30", "30-40", "40-50", "50-60", "60+"], +}; + +// Mapping of day numbers to day names +const dayNames = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +]; + const HourlyPredictionGraph: React.FC = () => { const [predictedData, setPredictedData] = useState(null); const [loading, setLoading] = useState(true); @@ -35,7 +107,7 @@ const HourlyPredictionGraph: React.FC = () => { const fetchData = async () => { try { const response = await axios.get( - "https://ai.occupi.tech/predict_day" + "https://ai.occupi.tech/predict_day?date=2024-09-26&start_hour=6&end_hour=17" ); setPredictedData(response.data); setLoading(false); @@ -60,25 +132,63 @@ const HourlyPredictionGraph: React.FC = () => { return
No data available
; } + // Get the correct day of the week and corresponding attendance levels + const dayOfWeek = dayNames[predictedData.Day_of_Week]; // Get day name + const attendanceLevels = attendance_levels_by_day[dayOfWeek] || []; + + // Format the data for the BarChart (map hourly predictions to required format) + const formattedData = predictedData.Hourly_Predictions.map((prediction) => ({ + hour: `${prediction.Hour}:00`, // Label the hour with a string + level: prediction.Predicted_Class, // Class number for the y-axis + })); + return ( -
+

- Predictions for {predictedData.Date} + Hourly Predicted Capacity Levels for {dayOfWeek}

-
- {predictedData.Hourly_Predictions.map((prediction, index) => ( -
-
- Hour: {prediction.Hour}:00 -
-
-
- Predicted Attendance: {prediction.Predicted_Attendance_Level} -
-
Level: {prediction.Predicted_Class}
+ + + + + i)} // Dynamically set ticks for levels + tickFormatter={(tick) => tick} // Display the level numbers instead of attendance ranges + /> + + + {formattedData.map((entry, index) => ( + + ))} + + + +
+ {attendanceLevels.map((range, index) => ( +
+
+ {range}
))}
diff --git a/python-code/app.py b/python-code/app.py index de932d06..b52c88c1 100644 --- a/python-code/app.py +++ b/python-code/app.py @@ -241,14 +241,15 @@ def predict_hourly(): @app.route('/predict_day', methods=['GET']) def predict_range(): try: - # Get the date, start_hour, and end_hour from query parameters - date_str = request.args.get('date') - start_hour = request.args.get('start_hour', type=int) - end_hour = request.args.get('end_hour', type=int) - - # Check if date, start_hour, and end_hour are provided - if not date_str or start_hour is None or end_hour is None: - return jsonify({"error": "Both 'date', 'start_hour', and 'end_hour' parameters are required"}), 400 + # Set defaults for date, start_hour, and end_hour + today = datetime.today().strftime('%Y-%m-%d') # Default to today's date + default_start_hour = 6 # Default start time + default_end_hour = 17 # Default end time + + # Get the date, start_hour, and end_hour from query parameters (use defaults if not provided) + date_str = request.args.get('date', default=today) # Default to today's date + start_hour = request.args.get('start_hour', default=default_start_hour, type=int) # Default start hour + end_hour = request.args.get('end_hour', default=default_end_hour, type=int) # Default end hour # Validate the hours (must be between 0 and 23) if not (0 <= start_hour <= 23) or not (0 <= end_hour <= 23): From 8c33bd318e425e999ddd200e53c6888b18f98911 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Thu, 26 Sep 2024 08:51:49 +0200 Subject: [PATCH 4/6] Added recommendations button --- .../occupi-web/src/pages/aiDashboard/AiDashboard.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx index 5f38d7f4..87d4a7dd 100644 --- a/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx +++ b/frontend/occupi-web/src/pages/aiDashboard/AiDashboard.tsx @@ -28,7 +28,7 @@ const defaultLayouts: Layouts = { { i: "card4", x: 9, y: 0, w: 3, h: 2 }, { i: "graph1", x: 0, y: 2, w: 6, h: 4 }, { i: "graph2", x: 6, y: 2, w: 6, h: 4 }, - { i: "hourlyPrediction", x: 0, y: 6, w: 12, h: 3 }, + { i: "hourlyPrediction", x: 0, y: 6, w: 12, h: 5 }, ], }; @@ -173,6 +173,16 @@ const AiDashboard: React.FC = () => { onChange={handleInputChange} /> + {/* Add Recommendations Button */} +
+ +
+
- +
+
+ +