update the data and chart

This commit is contained in:
Yan Zhou 2025-06-13 21:14:27 +02:00
parent 1841746f6a
commit ca805be243
2 changed files with 292 additions and 112 deletions

View file

@ -155,7 +155,7 @@
const filteredData = $derived(
chartData.filter((item) => {
const referenceDate = new Date("2024-06-30");
const now = new Date();
let daysToSubtract = 90;
if (timeRange === "30d") {
daysToSubtract = 30;
@ -163,65 +163,84 @@
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 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 isLoading = $state(false);
let hasError = $state(false);
chartData = number.map((obj: any) => {
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);
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;
}
}
});
</script>
@ -316,7 +335,7 @@
{/if}
<Dialog.Root bind:onOpenChange>
<Dialog.Trigger class={buttonVariants({ variant: "outline" })}>Statistics</Dialog.Trigger>
<Dialog.Content class="sm:max-h-2/3 sm:max-w-2/3">
<Dialog.Content class="sm:max-h-[90%] sm:max-w-[90%]">
<Dialog.Header>
<div class="grid flex-1 gap-1 text-center sm:text-left">
<Card.Title>Statistics for floor {data.slug}</Card.Title>
@ -326,27 +345,29 @@
<div class="flex items-center gap-2">
<DropdownMenu.Root>
<DropdownMenu.Trigger class="rounded-lg sm:ml-auto">
<DropdownMenu.Trigger class="rounded-lg">
{#snippet child({ props })}
<Button variant="outline" size="sm" {...props}>
<Columns2 />
<span class="hidden lg:inline">Customize Data</span>
<span class="lg:hidden">Columns</span>
<span class="hidden lg:inline">{selectedSensorConfig.label}</span>
<span class="lg:hidden">{selectedSensorConfig.label}</span>
<ChevronDownIcon />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="w-56">
{#each table as column}
<DropdownMenu.CheckboxItem class="capitalize" bind:checked={column.visible}>
{column.key}
</DropdownMenu.CheckboxItem>
<DropdownMenu.RadioGroup bind:value={selectedSensor}>
{#each sensorOptions as sensor}
<DropdownMenu.RadioItem value={sensor.key} class="capitalize">
{sensor.label}
</DropdownMenu.RadioItem>
{/each}
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Select.Root type="single" bind:value={timeRange}>
<Select.Trigger class="w-[160px] rounded-lg sm:ml-auto" aria-label="Select a value">
<Select.Trigger class="w-[160px] rounded-lg" aria-label="Select a value">
{selectedLabel}
</Select.Trigger>
<Select.Content class="rounded-xl">
@ -362,71 +383,215 @@
</Button>
</div>
<ChartContainer config={chartConfig} class="aspect-auto h-[800px] w-full">
<AreaChart
legend
data={filteredData}
x="date"
xScale={scaleUtc()}
series={tables}
seriesLayout="stack"
props={{
area: {
curve: curveNatural,
"fill-opacity": 0.4,
line: { class: "stroke-1" },
motion: "tween",
},
xAxis: {
ticks: timeRange === "7d" ? 7 : undefined,
format: (v) => {
return v.toLocaleDateString("en-US", {
{#if isLoading}
<div class="flex h-[400px] items-center justify-center">
<div class="text-center">
<div
class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600"
></div>
<p class="text-muted-foreground">Loading statistics...</p>
</div>
</div>
{:else if hasError}
<div class="flex h-[400px] items-center justify-center">
<div class="text-center">
<p class="mb-2 text-red-600">Error loading statistics</p>
<p class="text-muted-foreground text-sm">Please try again later</p>
</div>
</div>
{:else if chartData.length === 0}
<div class="flex h-[400px] items-center justify-center">
<div class="text-center">
<p class="mb-2 text-lg font-semibold">No data available</p>
<p class="text-muted-foreground text-sm">
Statistics will appear once sensor data is collected
</p>
</div>
</div>
{:else}
<Card.Root>
<Card.Header>
<Card.Title class="text-xl">{selectedSensorConfig.label}</Card.Title>
</Card.Header>
<Card.Content>
<div class="space-y-4">
{#if filteredData.length > 0}
<!-- Statistics Summary -->
<div class="grid grid-cols-3 gap-4 rounded border p-4 text-center">
<div>
<div class="text-muted-foreground text-sm font-medium">Latest</div>
<div class="text-lg font-semibold">
{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}
</div>
</div>
<div>
<div class="text-muted-foreground text-sm font-medium">Average</div>
<div class="text-lg font-semibold">
{(
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}
</div>
</div>
<div>
<div class="text-muted-foreground text-sm font-medium">Range</div>
<div class="text-lg font-semibold">
{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}
</div>
</div>
</div>
<!-- Simple SVG Line Chart -->
<div class="rounded border bg-white p-4">
<svg viewBox="0 0 800 400" class="h-80 w-full">
<!-- Chart area -->
{#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}
<!-- Background grid -->
<defs>
<pattern id="grid" width="50" height="40" patternUnits="userSpaceOnUse">
<path
d="M 50 0 L 0 0 0 40"
fill="none"
stroke="#f0f0f0"
stroke-width="1"
/>
</pattern>
</defs>
<rect
x={padding}
y={padding}
width={chartWidth}
height={chartHeight}
fill="url(#grid)"
/>
<!-- Y-axis labels -->
<text x="25" y={padding + 5} class="fill-gray-600 text-xs" text-anchor="end"
>{maxValue.toFixed(1)}</text
>
<text
x="25"
y={padding + chartHeight / 2 + 5}
class="fill-gray-600 text-xs"
text-anchor="end">{((minValue + maxValue) / 2).toFixed(1)}</text
>
<text
x="25"
y={padding + chartHeight + 5}
class="fill-gray-600 text-xs"
text-anchor="end">{minValue.toFixed(1)}</text
>
<!-- X-axis labels -->
{#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}
<!-- Format dates based on time range -->
{@const formatDate = (date) => {
if (timeRange === "7d") {
return date.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
},
},
yAxis: { format: () => "" },
}}
>
{#snippet marks({ series, getAreaProps })}
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stop-color="var(--color-desktop)" stop-opacity={1.0} />
<stop offset="95%" stop-color="var(--color-desktop)" stop-opacity={0.1} />
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stop-color="var(--color-mobile)" stop-opacity={0.8} />
<stop offset="95%" stop-color="var(--color-mobile)" stop-opacity={0.1} />
</linearGradient>
</defs>
<ChartClipPath
initialWidth={0}
motion={{
width: { type: "tween", duration: 1000, easing: cubicInOut },
}}
>
{#each series as s, i (s.key)}
<Area
{...getAreaProps(s, i)}
fill={s.key === "desktop" ? "url(#fillDesktop)" : "url(#fillMobile)"}
/>
{/each}
</ChartClipPath>
{/snippet}
{#snippet tooltip()}
<Chart.Tooltip
labelFormatter={(v: Date) => {
return v.toLocaleDateString("en-US", {
month: "long",
} else if (timeRange === "30d") {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
} else {
return date.toLocaleDateString("en-US", { month: "short" });
}
}}
indicator="line"
<text
x={padding}
y={padding + chartHeight + 20}
class="fill-gray-600 text-xs"
text-anchor="start">{formatDate(firstDate)}</text
>
<text
x={padding + chartWidth / 2}
y={padding + chartHeight + 20}
class="fill-gray-600 text-xs"
text-anchor="middle">{formatDate(midDate)}</text
>
<text
x={padding + chartWidth}
y={padding + chartHeight + 20}
class="fill-gray-600 text-xs"
text-anchor="end">{formatDate(lastDate)}</text
>
{/if}
<!-- Data line -->
<polyline
fill="none"
stroke="#3b82f6"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
points={filteredData
.map((d, i) => {
const x = padding + (i / (filteredData.length - 1)) * chartWidth;
const y =
padding +
chartHeight -
((d[selectedSensor] - minValue) / valueRange) * chartHeight;
return `${x},${y}`;
})
.join(" ")}
/>
{/snippet}
</AreaChart>
</ChartContainer>
{/if}
</svg>
</div>
<div class="text-muted-foreground text-center text-xs">
Data from {filteredData[0]?.date?.toLocaleDateString()} to {filteredData[
filteredData.length - 1
]?.date?.toLocaleDateString()}
</div>
{:else}
<div class="flex h-[400px] items-center justify-center">
<div class="text-center">
<p class="mb-2 text-lg font-semibold">No data available</p>
<p class="text-muted-foreground text-sm">
No data found for the selected time period
</p>
</div>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>
{/if}
<Dialog.Footer></Dialog.Footer>
</Dialog.Content>

View file

@ -2,8 +2,23 @@ import { db } from "$lib/server/db";
import * as table from "$lib/server/db/schema"
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);
// Get all sensor data (like the original working version)
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,
}).from(table.sensorData);
// 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: {