Compare commits
No commits in common. "b57d9b787559aa6cd9bca8645043e7ff305cc65e" and "1841746f6a40abe70dfe908dd6357c1fc2af258b" have entirely different histories.
b57d9b7875
...
1841746f6a
2 changed files with 133 additions and 356 deletions
|
@ -128,17 +128,16 @@
|
||||||
import * as Chart from "$lib/components/ui/chart/index.js";
|
import * as Chart from "$lib/components/ui/chart/index.js";
|
||||||
import * as Card from "$lib/components/ui/card/index.js";
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
import * as Select from "$lib/components/ui/select/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 chartData = $state([]);
|
||||||
|
|
||||||
let newChartData = $derived(
|
|
||||||
chartData.filter((item: object) => {
|
|
||||||
if (item.sensorId === selectedESP) {
|
|
||||||
return { ...item };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let timeRange = $state("90d");
|
let timeRange = $state("90d");
|
||||||
|
|
||||||
const selectedLabel = $derived.by(() => {
|
const selectedLabel = $derived.by(() => {
|
||||||
|
@ -155,8 +154,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredData = $derived(
|
const filteredData = $derived(
|
||||||
newChartData.filter((item) => {
|
chartData.filter((item) => {
|
||||||
const now = new Date();
|
const referenceDate = new Date("2024-06-30");
|
||||||
let daysToSubtract = 90;
|
let daysToSubtract = 90;
|
||||||
if (timeRange === "30d") {
|
if (timeRange === "30d") {
|
||||||
daysToSubtract = 30;
|
daysToSubtract = 30;
|
||||||
|
@ -164,132 +163,75 @@
|
||||||
daysToSubtract = 7;
|
daysToSubtract = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutoffDate = new Date(now.getTime() - daysToSubtract * 24 * 60 * 60 * 1000);
|
referenceDate.setDate(referenceDate.getDate() - daysToSubtract);
|
||||||
return item.date >= cutoffDate;
|
return item.date >= referenceDate;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
temperature: { label: "Temperature (°C)", color: "var(--chart-1)" },
|
temperature: { label: "Temperature", color: "var(--chart-1)" },
|
||||||
humidity: { label: "Humidity (%)", color: "var(--chart-2)" },
|
humidity: { label: "Humidity", color: "var(--chart-2)" },
|
||||||
altitude: { label: "Altitude (m)", color: "var(--chart-3)" },
|
altitude: { label: "Altitude", color: "var(--chart-1)" },
|
||||||
pressure: { label: "Pressure (kPa)", color: "var(--chart-4)" },
|
pressure: { label: "Pressure", color: "var(--chart-2)" },
|
||||||
} satisfies Chart.ChartConfig;
|
} satisfies Chart.ChartConfig;
|
||||||
|
|
||||||
let selectedSensor = $state("temperature");
|
const table = $state([
|
||||||
|
|
||||||
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 = [
|
|
||||||
{
|
{
|
||||||
key: "temperature",
|
visible: true,
|
||||||
label: chartConfig.temperature.label,
|
key: "altitude",
|
||||||
color: chartConfig.temperature.color,
|
label: chartConfig.altitude.label,
|
||||||
|
color: chartConfig.altitude.color,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
visible: true,
|
||||||
key: "humidity",
|
key: "humidity",
|
||||||
label: chartConfig.humidity.label,
|
label: chartConfig.humidity.label,
|
||||||
color: chartConfig.humidity.color,
|
color: chartConfig.humidity.color,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
visible: true,
|
||||||
key: "pressure",
|
key: "pressure",
|
||||||
label: chartConfig.pressure.label,
|
label: chartConfig.pressure.label,
|
||||||
color: chartConfig.pressure.color,
|
color: chartConfig.pressure.color,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "altitude",
|
visible: true,
|
||||||
label: chartConfig.altitude.label,
|
key: "temperature",
|
||||||
color: chartConfig.altitude.color,
|
label: chartConfig.temperature.label,
|
||||||
|
color: chartConfig.temperature.color,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
const selectedSensorConfig = $derived(
|
const tables = $derived(
|
||||||
sensorOptions.find((s) => s.key === selectedSensor) || sensorOptions[0],
|
table
|
||||||
|
.filter((item) => {
|
||||||
|
if (item.visible) return true;
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((item) => {
|
||||||
|
return { key: item.key, label: item.label, color: item.color };
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let esps = $state([]);
|
|
||||||
let selectedESP = $state("Select ESP sensor");
|
|
||||||
|
|
||||||
let isLoading = $state(false);
|
|
||||||
let hasError = $state(false);
|
|
||||||
|
|
||||||
let onOpenChange = $state(async (open: boolean) => {
|
let onOpenChange = $state(async (open: boolean) => {
|
||||||
console.log("onOpenChange called with:", open);
|
|
||||||
if (open) {
|
if (open) {
|
||||||
console.log("Starting to load statistics...");
|
const response = await fetch("/" + data.slug + "/stats");
|
||||||
isLoading = true;
|
const number = await response.json();
|
||||||
hasError = false;
|
console.log(number);
|
||||||
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) {
|
chartData = number.map((obj: any) => {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
return { ...obj, date: new Date(obj.date) };
|
||||||
}
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
|
<!-- <div style="display: flex; justify-content: center; align-items: center; height: 100%;"> -->
|
||||||
<h1 class="mb-6 text-center text-4xl font-bold">Floor {data.slug}</h1>
|
<h1 class="mb-6 text-center text-4xl font-bold">Floor {data.slug}</h1>
|
||||||
|
|
||||||
{#if data.hasConfig && data.floorConfig}
|
{#if data.hasConfig && data.floorConfig}
|
||||||
|
<!-- Display saved floor plan configuration -->
|
||||||
<div class="relative mx-auto max-w-4xl">
|
<div class="relative mx-auto max-w-4xl">
|
||||||
{#if data.floorConfig.image}
|
{#if data.floorConfig.image}
|
||||||
<div class="relative overflow-hidden rounded-lg border bg-gray-50">
|
<div class="relative overflow-hidden rounded-lg border bg-gray-50">
|
||||||
|
@ -366,6 +308,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<!-- Fallback to canvas drawing -->
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span class="mb-4">{mqttMessage}</span>
|
<span class="mb-4">{mqttMessage}</span>
|
||||||
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
|
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
|
||||||
|
@ -373,7 +316,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<Dialog.Root bind:onOpenChange>
|
<Dialog.Root bind:onOpenChange>
|
||||||
<Dialog.Trigger class={buttonVariants({ variant: "outline" })}>Statistics</Dialog.Trigger>
|
<Dialog.Trigger class={buttonVariants({ variant: "outline" })}>Statistics</Dialog.Trigger>
|
||||||
<Dialog.Content class="sm:max-h-[90%] sm:max-w-[90%]">
|
<Dialog.Content class="sm:max-h-2/3 sm:max-w-2/3">
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<div class="grid flex-1 gap-1 text-center sm:text-left">
|
<div class="grid flex-1 gap-1 text-center sm:text-left">
|
||||||
<Card.Title>Statistics for floor {data.slug}</Card.Title>
|
<Card.Title>Statistics for floor {data.slug}</Card.Title>
|
||||||
|
@ -382,31 +325,28 @@
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Select.Root type="single" bind:value={selectedESP}>
|
<DropdownMenu.Root>
|
||||||
<Select.Trigger class="w-[160px] rounded-lg" aria-label="Select a sensor">
|
<DropdownMenu.Trigger class="rounded-lg sm:ml-auto">
|
||||||
{selectedESP}
|
{#snippet child({ props })}
|
||||||
</Select.Trigger>
|
<Button variant="outline" size="sm" {...props}>
|
||||||
<Select.Content class="rounded-xl">
|
<Columns2 />
|
||||||
{#each esps as esp}
|
<span class="hidden lg:inline">Customize Data</span>
|
||||||
<Select.Item value={esp} class="rounded-lg">{esp}</Select.Item>
|
<span class="lg:hidden">Columns</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>
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</DropdownMenu.Content>
|
||||||
</Select.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<Select.Root type="single" bind:value={selectedSensor}>
|
|
||||||
<Select.Trigger class="w-[160px] rounded-lg" aria-label="Select a metric">
|
|
||||||
{sensorLabel}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content class="rounded-xl">
|
|
||||||
<Select.Item value="altitude" class="rounded-lg">Altitude (m)</Select.Item>
|
|
||||||
<Select.Item value="humidity" class="rounded-lg">Humidity (%)</Select.Item>
|
|
||||||
<Select.Item value="pressure" class="rounded-lg">Pressure (kPa)</Select.Item>
|
|
||||||
<Select.Item value="temperature" class="rounded-lg">Temperature (°C)</Select.Item>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
|
|
||||||
<Select.Root type="single" bind:value={timeRange}>
|
<Select.Root type="single" bind:value={timeRange}>
|
||||||
<Select.Trigger class="w-[160px] rounded-lg" aria-label="Select a time range">
|
<Select.Trigger class="w-[160px] rounded-lg sm:ml-auto" aria-label="Select a value">
|
||||||
{selectedLabel}
|
{selectedLabel}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="rounded-xl">
|
<Select.Content class="rounded-xl">
|
||||||
|
@ -416,221 +356,77 @@
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onclick={() => exportCSV()}>
|
<Button variant="outline" size="sm">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
<span class="hidden lg:inline">Export Data</span>
|
<span class="hidden lg:inline">Export Data</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isLoading}
|
<ChartContainer config={chartConfig} class="aspect-auto h-[800px] w-full">
|
||||||
<div class="flex h-[400px] items-center justify-center">
|
<AreaChart
|
||||||
<div class="text-center">
|
legend
|
||||||
<div
|
data={filteredData}
|
||||||
class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600"
|
x="date"
|
||||||
></div>
|
xScale={scaleUtc()}
|
||||||
<p class="text-muted-foreground">Loading statistics...</p>
|
series={tables}
|
||||||
</div>
|
seriesLayout="stack"
|
||||||
</div>
|
props={{
|
||||||
{:else if hasError}
|
area: {
|
||||||
<div class="flex h-[400px] items-center justify-center">
|
curve: curveNatural,
|
||||||
<div class="text-center">
|
"fill-opacity": 0.4,
|
||||||
<p class="mb-2 text-red-600">Error loading statistics</p>
|
line: { class: "stroke-1" },
|
||||||
<p class="text-muted-foreground text-sm">Please try again later</p>
|
motion: "tween",
|
||||||
</div>
|
},
|
||||||
</div>
|
xAxis: {
|
||||||
{:else if newChartData.length === 0}
|
ticks: timeRange === "7d" ? 7 : undefined,
|
||||||
<div class="flex h-[400px] items-center justify-center">
|
format: (v) => {
|
||||||
<div class="text-center">
|
return v.toLocaleDateString("en-US", {
|
||||||
<p class="mb-2 text-lg font-semibold">No data available</p>
|
month: "short",
|
||||||
<p class="text-muted-foreground text-sm">
|
day: "numeric",
|
||||||
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 -->
|
yAxis: { format: () => "" },
|
||||||
<div class="rounded border bg-white p-4">
|
}}
|
||||||
<svg viewBox="0 0 800 400" class="h-80 w-full">
|
>
|
||||||
<!-- Chart area -->
|
{#snippet marks({ series, getAreaProps })}
|
||||||
{#if filteredData.length > 1}
|
<defs>
|
||||||
{@const minValue = Math.min(...filteredData.map((d) => d[selectedSensor]))}
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||||
{@const maxValue = Math.max(...filteredData.map((d) => d[selectedSensor]))}
|
<stop offset="5%" stop-color="var(--color-desktop)" stop-opacity={1.0} />
|
||||||
{@const valueRange = maxValue - minValue || 1}
|
<stop offset="95%" stop-color="var(--color-desktop)" stop-opacity={0.1} />
|
||||||
{@const padding = 30}
|
</linearGradient>
|
||||||
{@const chartWidth = 800 - padding * 2}
|
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||||
{@const chartHeight = 400 - padding * 2}
|
<stop offset="5%" stop-color="var(--color-mobile)" stop-opacity={0.8} />
|
||||||
|
<stop offset="95%" stop-color="var(--color-mobile)" stop-opacity={0.1} />
|
||||||
<!-- Background grid -->
|
</linearGradient>
|
||||||
<defs>
|
</defs>
|
||||||
<pattern id="grid" width="50" height="40" patternUnits="userSpaceOnUse">
|
<ChartClipPath
|
||||||
<path
|
initialWidth={0}
|
||||||
d="M 50 0 L 0 0 0 40"
|
motion={{
|
||||||
fill="none"
|
width: { type: "tween", duration: 1000, easing: cubicInOut },
|
||||||
stroke="#f0f0f0"
|
}}
|
||||||
stroke-width="1"
|
>
|
||||||
/>
|
{#each series as s, i (s.key)}
|
||||||
</pattern>
|
<Area
|
||||||
</defs>
|
{...getAreaProps(s, i)}
|
||||||
<rect
|
fill={s.key === "desktop" ? "url(#fillDesktop)" : "url(#fillMobile)"}
|
||||||
x={padding}
|
/>
|
||||||
y={padding}
|
{/each}
|
||||||
width={chartWidth}
|
</ChartClipPath>
|
||||||
height={chartHeight}
|
{/snippet}
|
||||||
fill="url(#grid)"
|
{#snippet tooltip()}
|
||||||
/>
|
<Chart.Tooltip
|
||||||
|
labelFormatter={(v: Date) => {
|
||||||
<!-- Y-axis labels -->
|
return v.toLocaleDateString("en-US", {
|
||||||
<text x="25" y={padding + 5} class="fill-gray-600 text-xs" text-anchor="end"
|
month: "long",
|
||||||
>{maxValue.toFixed(1)}</text
|
});
|
||||||
>
|
}}
|
||||||
<text
|
indicator="line"
|
||||||
x="25"
|
/>
|
||||||
y={padding + chartHeight / 2 + 5}
|
{/snippet}
|
||||||
class="fill-gray-600 text-xs"
|
</AreaChart>
|
||||||
text-anchor="end">{((minValue + maxValue) / 2).toFixed(1)}</text
|
</ChartContainer>
|
||||||
>
|
|
||||||
<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",
|
|
||||||
});
|
|
||||||
} else if (timeRange === "30d") {
|
|
||||||
return date.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return date.toLocaleDateString("en-US", { month: "short" });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
<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(" ")}
|
|
||||||
/>
|
|
||||||
{/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.Footer></Dialog.Footer>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
|
|
@ -1,28 +1,9 @@
|
||||||
import { db } from "$lib/server/db";
|
import { db } from "$lib/server/db";
|
||||||
import * as table from "$lib/server/db/schema"
|
import * as table from "$lib/server/db/schema"
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const GET = async (event) => {
|
export const GET = async () => {
|
||||||
// Get all sensor data (like the original working version)
|
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(event.locals.session.userId)
|
console.log(data);
|
||||||
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), {
|
return new Response(JSON.stringify(data), {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue