diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 63b3cbe..b825992 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -128,16 +128,17 @@ import * as Chart from "$lib/components/ui/chart/index.js"; import * as Card from "$lib/components/ui/card/index.js"; import * as Select from "$lib/components/ui/select/index.js"; - import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; - import { scaleUtc } from "d3-scale"; - import { Area, AreaChart, ChartClipPath } from "layerchart"; - import { curveNatural } from "d3-shape"; - import ChartContainer from "$lib/components/ui/chart/chart-container.svelte"; - import { cubicInOut } from "svelte/easing"; - import { ChevronDownIcon, Columns2 } from "@lucide/svelte"; let chartData = $state([]); + let newChartData = $derived( + chartData.filter((item: object) => { + if (item.sensorId === selectedESP) { + return { ...item }; + } + }), + ); + let timeRange = $state("90d"); const selectedLabel = $derived.by(() => { @@ -154,8 +155,8 @@ }); const filteredData = $derived( - chartData.filter((item) => { - const referenceDate = new Date("2024-06-30"); + newChartData.filter((item) => { + const now = new Date(); let daysToSubtract = 90; if (timeRange === "30d") { daysToSubtract = 30; @@ -163,75 +164,132 @@ daysToSubtract = 7; } - referenceDate.setDate(referenceDate.getDate() - daysToSubtract); - return item.date >= referenceDate; + const cutoffDate = new Date(now.getTime() - daysToSubtract * 24 * 60 * 60 * 1000); + return item.date >= cutoffDate; }), ); const chartConfig = { - temperature: { label: "Temperature", color: "var(--chart-1)" }, - humidity: { label: "Humidity", color: "var(--chart-2)" }, - altitude: { label: "Altitude", color: "var(--chart-1)" }, - pressure: { label: "Pressure", color: "var(--chart-2)" }, + temperature: { label: "Temperature (°C)", color: "var(--chart-1)" }, + humidity: { label: "Humidity (%)", color: "var(--chart-2)" }, + altitude: { label: "Altitude (m)", color: "var(--chart-3)" }, + pressure: { label: "Pressure (kPa)", color: "var(--chart-4)" }, } satisfies Chart.ChartConfig; - const table = $state([ + let selectedSensor = $state("temperature"); + + const sensorLabel = $derived.by(() => { + switch (selectedSensor) { + case "altitude": + return "Altitude (m)"; + case "humidity": + return "Humidity (%)"; + case "pressure": + return "Pressure (kPa)"; + case "temperature": + default: + return "Temperature (°C)"; + } + }); + + const sensorOptions = [ { - visible: true, - key: "altitude", - label: chartConfig.altitude.label, - color: chartConfig.altitude.color, + key: "temperature", + label: chartConfig.temperature.label, + color: chartConfig.temperature.color, }, { - visible: true, key: "humidity", label: chartConfig.humidity.label, color: chartConfig.humidity.color, }, { - visible: true, key: "pressure", label: chartConfig.pressure.label, color: chartConfig.pressure.color, }, { - visible: true, - key: "temperature", - label: chartConfig.temperature.label, - color: chartConfig.temperature.color, + key: "altitude", + label: chartConfig.altitude.label, + color: chartConfig.altitude.color, }, - ]); + ]; - const tables = $derived( - table - .filter((item) => { - if (item.visible) return true; - return false; - }) - .map((item) => { - return { key: item.key, label: item.label, color: item.color }; - }), + const selectedSensorConfig = $derived( + sensorOptions.find((s) => s.key === selectedSensor) || sensorOptions[0], ); - let onOpenChange = $state(async (open: boolean) => { - if (open) { - const response = await fetch("/" + data.slug + "/stats"); - const number = await response.json(); - console.log(number); + let esps = $state([]); + let selectedESP = $state("Select ESP sensor"); - chartData = number.map((obj: any) => { - return { ...obj, date: new Date(obj.date) }; - }); + let isLoading = $state(false); + let hasError = $state(false); + + let onOpenChange = $state(async (open: boolean) => { + console.log("onOpenChange called with:", open); + if (open) { + console.log("Starting to load statistics..."); + isLoading = true; + hasError = false; + try { + console.log(`Fetching stats for floor: ${data.slug}`); + const response = await fetch(`/${data.slug}/stats`); + console.log("Stats response status:", response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const rawData = await response.json(); + console.log("Raw stats data:", rawData); + const allesps = rawData.map((i: any) => { + return i.sensorId; + }); + esps = allesps.filter((v: any, i: any, a: any) => a.indexOf(v) === i); + + if (!rawData || rawData.length === 0) { + console.log("No data available for statistics"); + chartData = []; + } else { + chartData = rawData.map((obj: any) => { + return { ...obj, date: new Date(obj.date) }; + }); + console.log("Processed chart data:", chartData); + } + } catch (error) { + console.error("Error loading statistics:", error); + hasError = true; + chartData = []; + } finally { + isLoading = false; + } } }); + + function exportCSV(): void { + let csv = "data:text/csv;charset=utf-8,"; + + const headers = "time," + selectedSensorConfig.key; + csv += headers + "\n"; + + newChartData.forEach((obj: any) => { + csv += obj.date.toISOString() + "," + obj[selectedSensorConfig.key] + "\n"; + }); + + const encodedUri = encodeURI(csv); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", selectedESP + "_" + selectedSensorConfig.key + "_data.csv"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }
-

Floor {data.slug}

{#if data.hasConfig && data.floorConfig} -
{#if data.floorConfig.image}
@@ -308,7 +366,6 @@ {/if}
{:else} -
{mqttMessage} @@ -316,7 +373,7 @@ {/if} Statistics - +
Statistics for floor {data.slug} @@ -325,28 +382,31 @@
- - - {#snippet child({ props })} - - {/snippet} - - - {#each table as column} - - {column.key} - + + + {selectedESP} + + + {#each esps as esp} + {esp} {/each} - - + + + + + + {sensorLabel} + + + Altitude (m) + Humidity (%) + Pressure (kPa) + Temperature (°C) + + - + {selectedLabel} @@ -356,77 +416,221 @@ -
- - { - return v.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }, - }, + {#if isLoading} +
+
+
+

Loading statistics...

+
+
+ {:else if hasError} +
+
+

Error loading statistics

+

Please try again later

+
+
+ {:else if newChartData.length === 0} +
+
+

No data available

+

+ Statistics will appear once sensor data is collected +

+
+
+ {:else} + + + {selectedSensorConfig.label} + + +
+ {#if filteredData.length > 0} + +
+
+
Latest
+
+ {filteredData[filteredData.length - 1]?.[selectedSensor]?.toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
+
Average
+
+ {( + filteredData.reduce((sum, item) => sum + item[selectedSensor], 0) / + filteredData.length + ).toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
+
Range
+
+ {Math.min(...filteredData.map((d) => d[selectedSensor])).toFixed(1)} - {Math.max( + ...filteredData.map((d) => d[selectedSensor]), + ).toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
- yAxis: { format: () => "" }, - }} - > - {#snippet marks({ series, getAreaProps })} - - - - - - - - - - - - {#each series as s, i (s.key)} - - {/each} - - {/snippet} - {#snippet tooltip()} - { - return v.toLocaleDateString("en-US", { - month: "long", - }); - }} - indicator="line" - /> - {/snippet} - - + +
+ + + {#if filteredData.length > 1} + {@const minValue = Math.min(...filteredData.map((d) => d[selectedSensor]))} + {@const maxValue = Math.max(...filteredData.map((d) => d[selectedSensor]))} + {@const valueRange = maxValue - minValue || 1} + {@const padding = 30} + {@const chartWidth = 800 - padding * 2} + {@const chartHeight = 400 - padding * 2} + + + + + + + + + + + {maxValue.toFixed(1)} + {((minValue + maxValue) / 2).toFixed(1)} + {minValue.toFixed(1)} + + + {#if filteredData.length > 0} + {@const firstDate = filteredData[0].date} + {@const lastDate = filteredData[filteredData.length - 1].date} + {@const midIndex = Math.floor(filteredData.length / 2)} + {@const midDate = filteredData[midIndex].date} + + + {@const formatDate = (date) => { + if (timeRange === "7d") { + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + } else if (timeRange === "30d") { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + } else { + return date.toLocaleDateString("en-US", { month: "short" }); + } + }} + + {formatDate(firstDate)} + {formatDate(midDate)} + {formatDate(lastDate)} + {/if} + + + { + const x = padding + (i / (filteredData.length - 1)) * chartWidth; + const y = + padding + + chartHeight - + ((d[selectedSensor] - minValue) / valueRange) * chartHeight; + return `${x},${y}`; + }) + .join(" ")} + /> + {/if} + +
+ +
+ Data from {filteredData[0]?.date?.toLocaleDateString()} to {filteredData[ + filteredData.length - 1 + ]?.date?.toLocaleDateString()} +
+ {:else} +
+
+

No data available

+

+ No data found for the selected time period +

+
+
+ {/if} +
+
+
+ {/if} diff --git a/src/routes/(app)/[slug]/stats/+server.ts b/src/routes/(app)/[slug]/stats/+server.ts index 32fa2db..226f421 100644 --- a/src/routes/(app)/[slug]/stats/+server.ts +++ b/src/routes/(app)/[slug]/stats/+server.ts @@ -1,9 +1,28 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema" +import { eq } from "drizzle-orm"; -export const GET = async () => { - const data = await db.select({ altitude: table.sensorData.altitude, humidity: table.sensorData.humidity, pressure: table.sensorData.pressure, temperature: table.sensorData.temperature, date: table.sensorData.time, }).from(table.sensorData); - console.log(data); +export const GET = async (event) => { + // Get all sensor data (like the original working version) + console.log(event.locals.session.userId) + const rawData = await db.select({ + altitude: table.sensorData.altitude, + humidity: table.sensorData.humidity, + pressure: table.sensorData.pressure, + temperature: table.sensorData.temperature, + date: table.sensorData.time, + sensorId: table.sensorData.sensor, + user: table.sensors.user, + }).from(table.sensorData).innerJoin(table.sensors, eq(table.sensors.id, table.sensorData.sensor)).where(eq(table.sensors.user, event.locals.session.userId)); + + // Scale pressure values to be in a similar range as other sensors + // Divide by 1000 to convert from Pa to kPa (more reasonable scale) + const data = rawData.map(item => ({ + ...item, + pressure: Math.round((item.pressure / 1000) * 10) / 10 // Convert to kPa with 1 decimal place + })); + + console.log(`Returning ${data.length} data points`); return new Response(JSON.stringify(data), { headers: {