diff --git a/packages/web/components/ProfileViewer.tsx b/packages/web/components/ProfileViewer.tsx index 697e01c..5f6f803 100644 --- a/packages/web/components/ProfileViewer.tsx +++ b/packages/web/components/ProfileViewer.tsx @@ -1,7 +1,6 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useState, useCallback } from "react"; import { BarChart, Bar, @@ -24,21 +23,14 @@ import { type ProfileStats, } from "@/lib/api"; -interface ProfileData { - solar: Record; // { locationId: [8760 values] } - wind: Record; - stats: Record; -} - const LOCATIONS = [ { id: "GJ", name: "Gujarat" }, { id: "KA", name: "Karnataka" }, { id: "RJ", name: "Rajasthan" }, ]; -const SOLAR_COLOR = "#f97316"; // orange-500 -const WIND_COLOR = "#3b82f6"; // blue-500 -const UPLOAD_COLOR = "#8b5cf6"; // violet-500 +const SOLAR_COLOR = "#f97316"; +const WIND_COLOR = "#3b82f6"; interface StatsCardProps { label: string; @@ -57,7 +49,6 @@ function StatsCard({ label, value, color }: StatsCardProps) { ); } -// Custom tooltip for charts function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: { value: number; name: string }[]; label?: string }) { if (!active || !payload?.length) return null; return ( @@ -91,31 +82,28 @@ export function ProfileViewer() { name: string; path: string; data: string[][]; - headers: string[]; } | null>(null); const [isLoadingFile, setIsLoadingFile] = useState(false); - const API_BASE = typeof window !== 'undefined' - ? ((window as any).NEXT_PUBLIC_API_URL || 'http://localhost:8000') - : 'http://localhost:8000'; + const API_BASE = typeof window !== "undefined" + ? ((window as unknown as { NEXT_PUBLIC_API_URL?: string }).NEXT_PUBLIC_API_URL || "http://localhost:8000") + : "http://localhost:8000"; async function fetchAndDisplayFile(path: string, name: string) { setIsLoadingFile(true); try { const res = await fetch(`${API_BASE}${path}`); const text = await res.text(); - const lines = text.trim().split('\n'); - const headers = lines[0].split(','); - const data = lines.slice(1).map(line => line.split(',')); - setSelectedFile({ name, path, data, headers }); + const lines = text.trim().split("\n"); + const data = lines.slice(1).map((line) => line.split(",")); + setSelectedFile({ name, path, data }); } catch (err) { - console.error('Failed to fetch file:', err); + console.error("Failed to fetch file:", err); } finally { setIsLoadingFile(false); } } - // Calculate stats for a profile const calculateStats = (data: number[]) => { const sorted = [...data].sort((a, b) => a - b); const sum = data.reduce((a, b) => a + b, 0); @@ -127,7 +115,6 @@ export function ProfileViewer() { }; }; - // Handle file upload const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; @@ -143,12 +130,10 @@ export function ProfileViewer() { throw new Error("File must have at least a header row and one data row"); } - // Try to detect format const header = lines[0].toLowerCase(); let parsedData: number[] = []; if (header.includes("%_generation")) { - // New format: find the %_generation column const cols = header.split(","); const colIdx = cols.indexOf("%_generation"); if (colIdx === -1) throw new Error("Could not find %_generation column"); @@ -157,22 +142,19 @@ export function ProfileViewer() { const vals = line.split(","); let val = parseFloat(vals[colIdx]); if (isNaN(val)) return 0; - if (val > 1) val = val / 100; // Convert % to fraction + if (val > 1) val = val / 100; return val; }); } else if (header.includes("irradiance") || header.includes("wind_speed") || header.includes("e_grid")) { - // Old format: usually second column parsedData = lines.slice(1).map((line) => { const vals = line.split(","); let val = parseFloat(vals[1]); if (isNaN(val)) return 0; - // Handle different scales - if (val > 1) val = val / 1000; // Convert kW to MW - if (val > 1) val = val / 100; // Convert % to fraction + if (val > 1) val = val / 1000; + if (val > 1) val = val / 100; return val; }); } else { - // Generic: just take all numeric values from first column parsedData = lines.slice(1).map((line) => { const vals = line.split(","); let val = parseFloat(vals[0]); @@ -181,14 +163,11 @@ export function ProfileViewer() { }); } - // Validate 8760 values if (parsedData.length !== 8760) { throw new Error(`Expected 8760 values, got ${parsedData.length}`); } - // Determine if solar or wind based on average value (solar typically higher during day) const isSolar = parsedData.reduce((a, b, i) => { - // Solar has high values during midday hours (indices 6-18 of each day) const hourOfDay = i % 24; if (hourOfDay >= 6 && hourOfDay <= 18) return a + b; return a; @@ -206,8 +185,7 @@ export function ProfileViewer() { } }, []); - // Generate monthly averages for chart - const getMonthlyAverages = (data: number[], isSolar: boolean) => { + const getMonthlyAverages = (data: number[]) => { const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const monthNames = ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar"]; const monthPositions = [4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3]; @@ -222,33 +200,13 @@ export function ProfileViewer() { }); }; - // Generate hourly average profile for a typical day - const getHourlyAverage = (data: number[]) => { - const hourly = new Array(24).fill(0); - for (let i = 0; i < 8760; i++) { - hourly[i % 24] += data[i]; - } - return hourly.map((v) => v / 365); - }; - return ( <> - {/* Button to open the modal */} - {/* Modal */} {isOpen && (
- {/* Backdrop */} -
setIsOpen(false)} - /> +
setIsOpen(false)} /> - {/* Modal content */}
- {/* Header */}

File Reference Library

-
- {/* Tabs */}
- - -
- {/* Upload section */}
- {uploadError && ( - {uploadError} - )} + {uploadError && {uploadError}}
- {/* Content */}
- {/* File preview section - ALWAYS SHOWS when file is selected */} {selectedFile ? ( -
+
- {selectedFile.name.includes('Solar') ? '☀️' : '💨'} + {selectedFile.name.includes("Solar") ? "☀️" : "💨"}

{selectedFile.name}

{selectedFile.data.length} rows of data

- + Download CSV -
-
+
@@ -422,206 +313,144 @@ export function ProfileViewer() { ) : ( -
- {/* Bundled profiles section - shown when no file selected */} -
-

📂 Click on any file below to view its contents:

-
- {[ - { name: "Solar - Gujarat", path: "/api/profiles/solar/GJ", type: "solar", id: "solar_gj" }, - { name: "Solar - Karnataka", path: "/api/profiles/solar/KA", type: "solar", id: "solar_ka" }, - { name: "Solar - Rajasthan", path: "/api/profiles/solar/RJ", type: "solar", id: "solar_rj" }, - { name: "Wind - Gujarat", path: "/api/profiles/wind/GJ", type: "wind", id: "wind_gj" }, - { name: "Wind - Karnataka", path: "/api/profiles/wind/KA", type: "wind", id: "wind_ka" }, - { name: "Wind - Rajasthan", path: "/api/profiles/wind/RJ", type: "wind", id: "wind_rj" }, - ].map((profile) => ( - - ))} -
-
- - {/* Charts section - hidden when file is selected */} - {!selectedFile && activeTab === "solar" && ( -
-
-

- Monthly Average Solar Irradiance -

-
- - { - // Mock data for demonstration - in real app this would come from API - const mockData = Array(8760).fill(0).map((_, i) => { - const hour = i % 24; - const dayOfYear = Math.floor(i / 24); - // Simple sinusoidal approximation - const solarNoon = Math.sin((hour - 6) * Math.PI / 12); - const seasonFactor = Math.sin((dayOfYear - 80) * 2 * Math.PI / 365); - return Math.max(0, solarNoon * (0.8 + 0.2 * seasonFactor)); - }); - const monthly = getMonthlyAverages(mockData, true); - const idx = ["GJ", "KA", "RJ"].indexOf(loc.id); - return { - name: loc.name, - Jan: monthly[9]?.avg || 0, - Feb: monthly[10]?.avg || 0, - Mar: monthly[11]?.avg || 0, - Apr: monthly[0]?.avg || 0, - May: monthly[1]?.avg || 0, - Jun: monthly[2]?.avg || 0, - Jul: monthly[3]?.avg || 0, - Aug: monthly[4]?.avg || 0, - Sep: monthly[5]?.avg || 0, - Oct: monthly[6]?.avg || 0, - Nov: monthly[7]?.avg || 0, - Dec: monthly[8]?.avg || 0, - [idx]: 0, - }; - })}> - - - } /> - - - - - - + <> +
+

📂 Click on any file below to view its contents:

+
+ {[ + { name: "Solar - Gujarat", path: "/api/profiles/solar/GJ", type: "solar" }, + { name: "Solar - Karnataka", path: "/api/profiles/solar/KA", type: "solar" }, + { name: "Solar - Rajasthan", path: "/api/profiles/solar/RJ", type: "solar" }, + { name: "Wind - Gujarat", path: "/api/profiles/wind/GJ", type: "wind" }, + { name: "Wind - Karnataka", path: "/api/profiles/wind/KA", type: "wind" }, + { name: "Wind - Rajasthan", path: "/api/profiles/wind/RJ", type: "wind" }, + ].map((profile) => ( + + ))}
- {uploadedProfiles.solar && ( -
-

- Your Uploaded Solar Profile -

-
- {(() => { - const stats = calculateStats(uploadedProfiles.solar!); - return ( - <> - - - - - - ); - })()} + {activeTab === "solar" && ( +
+
+

Monthly Average Solar Irradiance

+
+ + + + + } /> + + + + +
+
+ {uploadedProfiles.solar && ( +
+

Your Uploaded Solar Profile

+
+ {(() => { + const stats = calculateStats(uploadedProfiles.solar); + return ( + <> + + + + + + ); + })()} +
+
+ )} +
+ )} + + {activeTab === "wind" && ( +
+
+

Monthly Average Wind Speed (m/s)

+
+ + + + + + + + +
+
+ {uploadedProfiles.wind && ( +
+

Your Uploaded Wind Profile

+
+ {(() => { + const stats = calculateStats(uploadedProfiles.wind); + return ( + <> + + + + + + ); + })()} +
+
+ )} +
+ )} + + {activeTab === "compare" && ( +
+
+

Solar vs Wind CUF (Typical Daily Pattern)

+
+ + + + + `${(v * 100).toFixed(0)}%`} /> + } /> + + + + + +
)} -
- )} - - {activeTab === "wind" && ( -
-
-

- Monthly Average Wind Speed (m/s) -

-
- - - - - - - - -
-
- - {uploadedProfiles.wind && ( -
-

- Your Uploaded Wind Profile -

-
- {(() => { - const stats = calculateStats(uploadedProfiles.wind!); - return ( - <> - - - - - - ); - })()} -
-
- )} -
- )} - - {!selectedFile && activeTab === "compare" && ( -
-
-

- Solar vs Wind CUF (Typical Daily Pattern) -

-
- - { - const hour = i % 24; - const solarValue = Math.max(0, Math.sin((hour - 6) * Math.PI / 12)); - const windValue = 0.3 + 0.2 * Math.sin(i * Math.PI / 12) + 0.1 * Math.random(); - return (solarValue + windValue) / 2; - }) - ).map((v, i) => ({ hour: `${String(i).padStart(2, '0')}:00`, Solar: v, Wind: 0.3 + 0.1 * Math.sin(i * Math.PI / 12) }))}> - - - `${(v * 100).toFixed(0)}%`} /> - } /> - - - - - -
-
- - {uploadedProfiles.solar && uploadedProfiles.wind && ( -
-

- Your Uploaded Profiles Comparison -

-
- {(() => { - const solarStats = calculateStats(uploadedProfiles.solar!); - const windStats = calculateStats(uploadedProfiles.wind!); - return ( - <> - - - - - - ); - })()} -
-
- )} -
+ )}
@@ -629,4 +458,4 @@ export function ProfileViewer() { )} ); -} +} \ No newline at end of file diff --git a/packages/web/components/ProfileViewer_backup.tsx b/packages/web/components/ProfileViewer_backup.tsx new file mode 100644 index 0000000..697e01c --- /dev/null +++ b/packages/web/components/ProfileViewer_backup.tsx @@ -0,0 +1,632 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + ReferenceLine, + LineChart, + Line, + CartesianGrid, + Legend, +} from "recharts"; +import { + getAvailableProfiles, + getProfileStats, + getSolarProfile, + getWindProfile, + profileCsvUrl, + type ProfileStats, +} from "@/lib/api"; + +interface ProfileData { + solar: Record; // { locationId: [8760 values] } + wind: Record; + stats: Record; +} + +const LOCATIONS = [ + { id: "GJ", name: "Gujarat" }, + { id: "KA", name: "Karnataka" }, + { id: "RJ", name: "Rajasthan" }, +]; + +const SOLAR_COLOR = "#f97316"; // orange-500 +const WIND_COLOR = "#3b82f6"; // blue-500 +const UPLOAD_COLOR = "#8b5cf6"; // violet-500 + +interface StatsCardProps { + label: string; + value: string; + color?: string; +} + +function StatsCard({ label, value, color }: StatsCardProps) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +// Custom tooltip for charts +function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: { value: number; name: string }[]; label?: string }) { + if (!active || !payload?.length) return null; + return ( +
+

{label}

+ {payload.map((entry, i) => ( +
+ + {entry.name}: + {(entry.value * 100).toFixed(2)}% +
+ ))} +
+ ); +} + +export function ProfileViewer() { + const [isOpen, setIsOpen] = useState(false); + const [activeTab, setActiveTab] = useState<"solar" | "wind" | "compare">("solar"); + const [uploadedProfiles, setUploadedProfiles] = useState<{ + solar?: number[]; + wind?: number[]; + filename?: string; + }>({}); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [selectedFile, setSelectedFile] = useState<{ + name: string; + path: string; + data: string[][]; + headers: string[]; + } | null>(null); + const [isLoadingFile, setIsLoadingFile] = useState(false); + + const API_BASE = typeof window !== 'undefined' + ? ((window as any).NEXT_PUBLIC_API_URL || 'http://localhost:8000') + : 'http://localhost:8000'; + + async function fetchAndDisplayFile(path: string, name: string) { + setIsLoadingFile(true); + try { + const res = await fetch(`${API_BASE}${path}`); + const text = await res.text(); + const lines = text.trim().split('\n'); + const headers = lines[0].split(','); + const data = lines.slice(1).map(line => line.split(',')); + setSelectedFile({ name, path, data, headers }); + } catch (err) { + console.error('Failed to fetch file:', err); + } finally { + setIsLoadingFile(false); + } + } + + // Calculate stats for a profile + const calculateStats = (data: number[]) => { + const sorted = [...data].sort((a, b) => a - b); + const sum = data.reduce((a, b) => a + b, 0); + return { + avg: sum / data.length, + max: Math.max(...data), + min: Math.min(...data), + median: sorted[Math.floor(sorted.length / 2)], + }; + }; + + // Handle file upload + const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setIsUploading(true); + setUploadError(null); + + try { + const text = await file.text(); + const lines = text.trim().split("\n"); + + if (lines.length < 2) { + throw new Error("File must have at least a header row and one data row"); + } + + // Try to detect format + const header = lines[0].toLowerCase(); + let parsedData: number[] = []; + + if (header.includes("%_generation")) { + // New format: find the %_generation column + const cols = header.split(","); + const colIdx = cols.indexOf("%_generation"); + if (colIdx === -1) throw new Error("Could not find %_generation column"); + + parsedData = lines.slice(1).map((line) => { + const vals = line.split(","); + let val = parseFloat(vals[colIdx]); + if (isNaN(val)) return 0; + if (val > 1) val = val / 100; // Convert % to fraction + return val; + }); + } else if (header.includes("irradiance") || header.includes("wind_speed") || header.includes("e_grid")) { + // Old format: usually second column + parsedData = lines.slice(1).map((line) => { + const vals = line.split(","); + let val = parseFloat(vals[1]); + if (isNaN(val)) return 0; + // Handle different scales + if (val > 1) val = val / 1000; // Convert kW to MW + if (val > 1) val = val / 100; // Convert % to fraction + return val; + }); + } else { + // Generic: just take all numeric values from first column + parsedData = lines.slice(1).map((line) => { + const vals = line.split(","); + let val = parseFloat(vals[0]); + if (isNaN(val)) return 0; + return val; + }); + } + + // Validate 8760 values + if (parsedData.length !== 8760) { + throw new Error(`Expected 8760 values, got ${parsedData.length}`); + } + + // Determine if solar or wind based on average value (solar typically higher during day) + const isSolar = parsedData.reduce((a, b, i) => { + // Solar has high values during midday hours (indices 6-18 of each day) + const hourOfDay = i % 24; + if (hourOfDay >= 6 && hourOfDay <= 18) return a + b; + return a; + }, 0) / parsedData.reduce((a, b) => a + b, 0) > 0.7; + + setUploadedProfiles({ + solar: isSolar ? parsedData : undefined, + wind: !isSolar ? parsedData : undefined, + filename: file.name, + }); + } catch (err) { + setUploadError(err instanceof Error ? err.message : "Failed to parse file"); + } finally { + setIsUploading(false); + } + }, []); + + // Generate monthly averages for chart + const getMonthlyAverages = (data: number[], isSolar: boolean) => { + const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const monthNames = ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar"]; + const monthPositions = [4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3]; + + return monthNames.map((name, i) => { + const month = monthPositions[i]; + const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; + const days = MONTH_DAYS[month - 1]; + const monthData = data.slice(start, start + days * 24); + const avg = monthData.reduce((a, b) => a + b, 0) / monthData.length; + return { name, avg: avg * 100 }; + }); + }; + + // Generate hourly average profile for a typical day + const getHourlyAverage = (data: number[]) => { + const hourly = new Array(24).fill(0); + for (let i = 0; i < 8760; i++) { + hourly[i % 24] += data[i]; + } + return hourly.map((v) => v / 365); + }; + + return ( + <> + {/* Button to open the modal */} + + + {/* Modal */} + {isOpen && ( +
+ {/* Backdrop */} +
setIsOpen(false)} + /> + + {/* Modal content */} +
+ {/* Header */} +
+

File Reference Library

+ +
+ + {/* Tabs */} +
+ + + +
+ + {/* Upload section */} +
+
+ + {uploadError && ( + {uploadError} + )} +
+
+ + {/* Content */} +
+ {/* File preview section - ALWAYS SHOWS when file is selected */} + {selectedFile ? ( +
+
+
+ {selectedFile.name.includes('Solar') ? '☀️' : '💨'} +
+

{selectedFile.name}

+

{selectedFile.data.length} rows of data

+
+
+
+ + Download CSV + + +
+
+
+
+ + + + + + + + + + + + {selectedFile.data.map((row: string[], i: number) => ( + + + + + + + + + ))} + +
HourDateMonthDayHour of Day% Generation
{row[0]}{row[1]}{row[2]}{row[3]}{row[4]}{row[5]}
+
+
+ ) : ( +
+ {/* Bundled profiles section - shown when no file selected */} +
+

📂 Click on any file below to view its contents:

+
+ {[ + { name: "Solar - Gujarat", path: "/api/profiles/solar/GJ", type: "solar", id: "solar_gj" }, + { name: "Solar - Karnataka", path: "/api/profiles/solar/KA", type: "solar", id: "solar_ka" }, + { name: "Solar - Rajasthan", path: "/api/profiles/solar/RJ", type: "solar", id: "solar_rj" }, + { name: "Wind - Gujarat", path: "/api/profiles/wind/GJ", type: "wind", id: "wind_gj" }, + { name: "Wind - Karnataka", path: "/api/profiles/wind/KA", type: "wind", id: "wind_ka" }, + { name: "Wind - Rajasthan", path: "/api/profiles/wind/RJ", type: "wind", id: "wind_rj" }, + ].map((profile) => ( + + ))} +
+
+ + {/* Charts section - hidden when file is selected */} + {!selectedFile && activeTab === "solar" && ( +
+
+

+ Monthly Average Solar Irradiance +

+
+ + { + // Mock data for demonstration - in real app this would come from API + const mockData = Array(8760).fill(0).map((_, i) => { + const hour = i % 24; + const dayOfYear = Math.floor(i / 24); + // Simple sinusoidal approximation + const solarNoon = Math.sin((hour - 6) * Math.PI / 12); + const seasonFactor = Math.sin((dayOfYear - 80) * 2 * Math.PI / 365); + return Math.max(0, solarNoon * (0.8 + 0.2 * seasonFactor)); + }); + const monthly = getMonthlyAverages(mockData, true); + const idx = ["GJ", "KA", "RJ"].indexOf(loc.id); + return { + name: loc.name, + Jan: monthly[9]?.avg || 0, + Feb: monthly[10]?.avg || 0, + Mar: monthly[11]?.avg || 0, + Apr: monthly[0]?.avg || 0, + May: monthly[1]?.avg || 0, + Jun: monthly[2]?.avg || 0, + Jul: monthly[3]?.avg || 0, + Aug: monthly[4]?.avg || 0, + Sep: monthly[5]?.avg || 0, + Oct: monthly[6]?.avg || 0, + Nov: monthly[7]?.avg || 0, + Dec: monthly[8]?.avg || 0, + [idx]: 0, + }; + })}> + + + } /> + + + + + + +
+
+ + {uploadedProfiles.solar && ( +
+

+ Your Uploaded Solar Profile +

+
+ {(() => { + const stats = calculateStats(uploadedProfiles.solar!); + return ( + <> + + + + + + ); + })()} +
+
+ )} +
+ )} + + {activeTab === "wind" && ( +
+
+

+ Monthly Average Wind Speed (m/s) +

+
+ + + + + + + + +
+
+ + {uploadedProfiles.wind && ( +
+

+ Your Uploaded Wind Profile +

+
+ {(() => { + const stats = calculateStats(uploadedProfiles.wind!); + return ( + <> + + + + + + ); + })()} +
+
+ )} +
+ )} + + {!selectedFile && activeTab === "compare" && ( +
+
+

+ Solar vs Wind CUF (Typical Daily Pattern) +

+
+ + { + const hour = i % 24; + const solarValue = Math.max(0, Math.sin((hour - 6) * Math.PI / 12)); + const windValue = 0.3 + 0.2 * Math.sin(i * Math.PI / 12) + 0.1 * Math.random(); + return (solarValue + windValue) / 2; + }) + ).map((v, i) => ({ hour: `${String(i).padStart(2, '0')}:00`, Solar: v, Wind: 0.3 + 0.1 * Math.sin(i * Math.PI / 12) }))}> + + + `${(v * 100).toFixed(0)}%`} /> + } /> + + + + + +
+
+ + {uploadedProfiles.solar && uploadedProfiles.wind && ( +
+

+ Your Uploaded Profiles Comparison +

+
+ {(() => { + const solarStats = calculateStats(uploadedProfiles.solar!); + const windStats = calculateStats(uploadedProfiles.wind!); + return ( + <> + + + + + + ); + })()} +
+
+ )} +
+ )} +
+
+
+ )} + + ); +}