first step to show graph of sensors
This commit is contained in:
parent
91ca53880b
commit
fae128bc1b
6 changed files with 224 additions and 1 deletions
|
@ -68,6 +68,8 @@
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"mqtt": "^5.13.1",
|
"mqtt": "^5.13.1",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -17,6 +17,12 @@ importers:
|
||||||
'@oslojs/encoding':
|
'@oslojs/encoding':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
|
d3-scale:
|
||||||
|
specifier: ^4.0.2
|
||||||
|
version: 4.0.2
|
||||||
|
d3-shape:
|
||||||
|
specifier: ^3.2.0
|
||||||
|
version: 3.2.0
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.43.1
|
specifier: ^0.43.1
|
||||||
version: 0.43.1(postgres@3.4.5)
|
version: 0.43.1(postgres@3.4.5)
|
||||||
|
|
|
@ -42,4 +42,5 @@ export const sensorData = pgTable("sensor_data", {
|
||||||
humidity: real().notNull(),
|
humidity: real().notNull(),
|
||||||
pressure: real().notNull(),
|
pressure: real().notNull(),
|
||||||
altitude: real().notNull(),
|
altitude: real().notNull(),
|
||||||
|
time: timestamp().notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { Thermometer, Droplets, Gauge, Mountain, Cpu } from "@lucide/svelte";
|
import { Thermometer, Droplets, Gauge, Mountain, Cpu } from "@lucide/svelte";
|
||||||
|
import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
|
@ -120,9 +124,83 @@
|
||||||
drawFloorplan(data.floor[0].json);
|
drawFloorplan(data.floor[0].json);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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([
|
||||||
|
{ date: new Date("2024-04-01"), desktop: 222, mobile: 150 },
|
||||||
|
{ date: new Date("2024-04-02"), desktop: 97, mobile: 180 },
|
||||||
|
{ date: new Date("2024-04-03"), desktop: 167, mobile: 120 },
|
||||||
|
{ date: new Date("2024-04-04"), desktop: 242, mobile: 260 },
|
||||||
|
{ date: new Date("2024-06-27"), desktop: 448, mobile: 490 },
|
||||||
|
{ date: new Date("2024-06-28"), desktop: 149, mobile: 200 },
|
||||||
|
{ date: new Date("2024-06-29"), desktop: 103, mobile: 160 },
|
||||||
|
{ date: new Date("2024-06-30"), desktop: 446, mobile: 400 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
let timeRange = $state("90d");
|
||||||
|
|
||||||
|
const selectedLabel = $derived.by(() => {
|
||||||
|
switch (timeRange) {
|
||||||
|
case "90d":
|
||||||
|
return "Last 3 months";
|
||||||
|
case "30d":
|
||||||
|
return "Last 30 days";
|
||||||
|
case "7d":
|
||||||
|
return "Last 7 days";
|
||||||
|
default:
|
||||||
|
return "Last 3 months";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredData = $derived(
|
||||||
|
chartData.filter((item) => {
|
||||||
|
const referenceDate = new Date("2024-06-30");
|
||||||
|
let daysToSubtract = 90;
|
||||||
|
if (timeRange === "30d") {
|
||||||
|
daysToSubtract = 30;
|
||||||
|
} else if (timeRange === "7d") {
|
||||||
|
daysToSubtract = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceDate.setDate(referenceDate.getDate() - daysToSubtract);
|
||||||
|
return item.date >= referenceDate;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
desktop: { label: "Desktop", color: "var(--chart-1)" },
|
||||||
|
mobile: { label: "Mobile", color: "var(--chart-2)" },
|
||||||
|
} satisfies Chart.ChartConfig;
|
||||||
|
|
||||||
|
const table = $state([
|
||||||
|
{ visible: true, id: "temperature" },
|
||||||
|
{ visible: true, id: "altitude" },
|
||||||
|
{ visible: true, id: "humidity" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
let onOpenChange = $state(async (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
const response = await fetch("/" + data.slug + "/stats");
|
||||||
|
const number = await response.json();
|
||||||
|
console.log(number);
|
||||||
|
|
||||||
|
chartData = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
</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}
|
||||||
|
@ -209,4 +287,127 @@
|
||||||
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
|
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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.Header>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" size="sm" {...props}>
|
||||||
|
<Columns2 />
|
||||||
|
<span class="hidden lg:inline">Customize Columns</span>
|
||||||
|
<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"
|
||||||
|
checked={column.visible}
|
||||||
|
onCheckedChange={(value) => (column.visible = !value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
<div class="grid flex-1 gap-1 text-center sm:text-left">
|
||||||
|
<Card.Title>Data for floor {data.slug}</Card.Title>
|
||||||
|
<Card.Description>Showing total data for the last 3 months</Card.Description>
|
||||||
|
</div>
|
||||||
|
<Select.Root type="single" bind:value={timeRange}>
|
||||||
|
<Select.Trigger class="w-[160px] rounded-lg sm:ml-auto" aria-label="Select a value">
|
||||||
|
{selectedLabel}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content class="rounded-xl">
|
||||||
|
<Select.Item value="90d" class="rounded-lg">Last 3 months</Select.Item>
|
||||||
|
<Select.Item value="30d" class="rounded-lg">Last 30 days</Select.Item>
|
||||||
|
<Select.Item value="7d" class="rounded-lg">Last 7 days</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<ChartContainer config={chartConfig} class="aspect-auto h-[250px] w-full">
|
||||||
|
<AreaChart
|
||||||
|
legend
|
||||||
|
data={filteredData}
|
||||||
|
x="date"
|
||||||
|
xScale={scaleUtc()}
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
key: "mobile",
|
||||||
|
label: "Mobile",
|
||||||
|
color: chartConfig.mobile.color,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "desktop",
|
||||||
|
label: "Desktop",
|
||||||
|
color: chartConfig.desktop.color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
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", {
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
|
||||||
|
<Dialog.Footer></Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
13
src/routes/(app)/[slug]/stats/+server.ts
Normal file
13
src/routes/(app)/[slug]/stats/+server.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { db } from "$lib/server/db";
|
||||||
|
import * as table from "$lib/server/db/schema"
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const data = await db.select({ sensor: table.sensorData.sensor }).from(table.sensorData);
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// src/routes/mqtt/+server.js
|
// src/routes/mqtt/+server.js
|
||||||
import { connectedDevices, deviceSensorData, getCurrentDevices } from "$lib/server/mqtt-devices.js";
|
import { connectedDevices, deviceSensorData, getCurrentDevices } from "$lib/server/mqtt-devices.js";
|
||||||
import * as mqtt from "mqtt";
|
import mqtt from "mqtt";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
// A Svelte store to hold the latest MQTT message.
|
// A Svelte store to hold the latest MQTT message.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue