diff --git a/package.json b/package.json
index 4169762..37033c0 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
},
"type": "module",
"dependencies": {
- "chart.js": "^3.8.0",
+ "chart.js": "^4.3.0",
"chartjs-plugin-zoom": "^1.2.1",
"d3": "^7.8.4",
"d3-selection": "^3.0.0",
diff --git a/src/lib/components/BrewProgress.svelte b/src/lib/components/BrewProgress.svelte
index c63abf4..b2108de 100644
--- a/src/lib/components/BrewProgress.svelte
+++ b/src/lib/components/BrewProgress.svelte
@@ -120,7 +120,7 @@
);
background-size: 150px 150px;
- animation: move-it 12s linear infinite;
+ animation: move-it 8s linear infinite;
}
@keyframes move-it {
diff --git a/src/lib/components/Graph.svelte b/src/lib/components/Graph.svelte
index ca6a80c..5d604de 100644
--- a/src/lib/components/Graph.svelte
+++ b/src/lib/components/Graph.svelte
@@ -7,9 +7,11 @@
CategoryScale,
LinearScale,
PointElement,
+ Tooltip,
Title,
Legend
} from 'chart.js';
+ import { getRelativePosition } from 'chart.js/helpers';
import type { ChartDataset } from 'chart.js';
import type IChartFrame from '../interfaces/IChartFrame';
@@ -20,123 +22,112 @@
CategoryScale,
LinearScale,
PointElement,
+ Tooltip,
Title,
Legend
);
export let name: string;
export let dataFrames: IChartFrame[];
- export let beginAtZero: boolean = true;
+ export let hideTitle: boolean;
let chartCanvas: HTMLCanvasElement;
let chart: Chart;
- let prevData: any = {};
- interface IDataset {
- labels: string[];
- data?: ChartDataset<'line', number[]>;
- }
+ onMount(() => renderChart());
+ afterUpdate(() => {
+ chart.destroy();
+ renderChart();
+ });
- interface ITemperatureDataset extends IDataset {
- inside: ChartDataset<'line', number[]>;
- outside?: ChartDataset<'line', number[]>;
- }
+ // Converts Date to format suitable for the current range displayed
+ function dateLabelsFormatedBasedOnResolution(dataFrames: IChartFrame[]): string[] {
+ const firstFrame = dataFrames[0];
+ const lastFrame = dataFrames[dataFrames.length - 1];
+ const deltaSeconds =
+ (new Date(lastFrame.key).getTime() - new Date(firstFrame.key).getTime()) / 1000;
+ let dateOptions: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric'
+ };
- interface IHumidityDataset extends IDataset {}
- interface IPressureDataset extends IDataset {}
-
- function pad(num) {
- if (num < 10) {
- return `0${num}`;
+ if (deltaSeconds < 3600) {
+ dateOptions = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
+ } else if (deltaSeconds <= 86400) {
+ dateOptions = { hour: 'numeric', minute: 'numeric' };
+ } else if (deltaSeconds <= 2592000) {
+ dateOptions = {
+ day: 'numeric',
+ month: 'numeric',
+ year: '2-digit',
+ hour: 'numeric',
+ minute: 'numeric'
+ };
}
- return num;
+
+ const scaledDate = new Intl.DateTimeFormat('no-NB', dateOptions);
+ return dataFrames.map((frame) => scaledDate.format(frame.key));
}
- function prettierDateString(date) {
- return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${pad(date.getYear() - 100)}`;
- }
-
- function computeTemperatureDataset(): ITemperatureDataset {
- const labels: string[] = dataFrames.map(
- (frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
- );
- const data: number[] = dataFrames.map((frame) => frame.value);
-
- return {
- labels,
- inside: {
- label: '℃ inside',
- borderColor: '#10e783',
- backgroundColor: '#c8f9df',
- lineTension: 0.5,
- borderWidth: 3,
- data
- }
- };
- }
-
- function computeHumidityDataset(): IHumidityDataset {
- const labels: string[] = dataFrames.map(
- (frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
- );
- const data: number[] = dataFrames.map((frame) => frame.value);
-
- return {
- labels,
- data: {
- label: '% humidity',
- borderColor: '#57d2fb',
- backgroundColor: '#d4f2fe',
- lineTension: 0.5,
- borderWidth: 3,
- data
- }
- };
- }
-
- function computePressureDataset(): IPressureDataset {
- const labels: string[] = dataFrames.map(
- (frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
- );
- const data: number[] = dataFrames.map((frame) => frame.value);
-
- return {
- labels,
- data: {
+ // set dataset label & colors matching the name sent as prop
+ function setDataColorAndName(data: ChartDataset) {
+ if (name === 'Pressure') {
+ Object.assign(data, {
label: 'Bar of pressure',
borderColor: '#ef5878',
- backgroundColor: '#fbd7de',
- lineTension: 0.5,
- borderWidth: 3,
- data
- }
- };
+ backgroundColor: '#fbd7de'
+ });
+ } else if (name === 'Humidity') {
+ Object.assign(data, {
+ label: '% humidity',
+ borderColor: '#57d2fb',
+ backgroundColor: '#d4f2fe'
+ });
+ } else if (name === 'Temperature') {
+ Object.assign(data, {
+ label: '℃ inside',
+ borderColor: '#10e783',
+ backgroundColor: '#c8f9df'
+ });
+ }
}
function renderChart() {
- const context: CanvasRenderingContext2D = chartCanvas.getContext('2d');
+ const context: CanvasRenderingContext2D | null = chartCanvas.getContext('2d');
+ if (!context) return
- let dataset: IDataset | ITemperatureDataset | IHumidityDataset | IPressureDataset;
- if (name === 'Temperature') dataset = computeTemperatureDataset();
- else if (name === 'Humidity') dataset = computeHumidityDataset();
- else if (name === 'Pressure') dataset = computePressureDataset();
+ // create labels and singular dataset (data)
+ const labels: string[] = dateLabelsFormatedBasedOnResolution(dataFrames);
+ const data: ChartDataset = {
+ data: dataFrames.map((frame) => frame.value),
+ borderWidth: 3,
+ };
+ // based on name, add label and color options to dataset
+ setDataColorAndName(data)
+
+ // create chart instance, most here is chart options
chart = new Chart(context, {
type: 'line',
data: {
- labels: dataset.labels,
- datasets: [dataset?.inside || dataset.data]
+ labels: labels,
+ datasets: [data]
},
options: {
elements: {
point: {
- radius: 1
+ radius: 2
+ },
+ line: {
+ tension: 0.5
}
},
maintainAspectRatio: false,
plugins: {
title: {
- display: true,
+ display: !hideTitle,
position: 'left',
+ position: 'top',
text: `${name} over time`,
font: {
size: 20
@@ -163,8 +154,21 @@
// },
mode: 'xy'
}
+ },
+ tooltip: {
+ titleFont: {
+ size: 14
+ },
+ bodyFont: {
+ size: 14
+ },
+ enabled: true
}
},
+ interaction: {
+ intersect: false,
+ mode: 'index'
+ },
scales: {
y: {
beginAtZero: false,
@@ -187,14 +191,6 @@
chart.update();
}
-
- onMount(() => renderChart());
- afterUpdate(() => {
- console.log('after update run');
- chart.destroy();
- renderChart();
- });
-
-
+
diff --git a/src/lib/components/display.svelte b/src/lib/components/display.svelte
deleted file mode 100644
index fceabbe..0000000
--- a/src/lib/components/display.svelte
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
Inside temperature: {23}℃
-
-
-
-
\ No newline at end of file
diff --git a/src/lib/graphQueryGenerator.ts b/src/lib/server/graphQueryGenerator.ts
similarity index 91%
rename from src/lib/graphQueryGenerator.ts
rename to src/lib/server/graphQueryGenerator.ts
index f3cc22e..427d6d9 100644
--- a/src/lib/graphQueryGenerator.ts
+++ b/src/lib/server/graphQueryGenerator.ts
@@ -83,8 +83,8 @@ function buildQuery(field: String, from: Date, to: Date, interval: String) {
{
range: {
'@timestamp': {
- gte: toDateString,
- lte: fromDateString,
+ gte: fromDateString,
+ lte: toDateString,
format: 'strict_date_optional_time'
}
}
@@ -138,7 +138,7 @@ function calculateInterval(from, to, interval, size) {
if (interval !== 'auto') {
return interval;
}
- const dateMathInterval = roundInterval((from - to) / size);
+ const dateMathInterval = roundInterval((to - from) / size);
// const dateMathIntervalMs = toMS(dateMathInterval);
// const minMs = toMS(min);
// if (dateMathIntervalMs !== undefined && minMs !== undefined && dateMathIntervalMs < minMs) {
@@ -148,6 +148,7 @@ function calculateInterval(from, to, interval, size) {
}
function parseTempResponse(data: IESTelemetry): IChartFrame[] {
+ console.log('got temp response:', data);
return data?.aggregations?.data?.buckets.map((bucket) => {
return {
value: bucket?.maxValue?.value,
@@ -161,12 +162,7 @@ function parseLatestResponse(data: IESTelemetry) {
return data?.hits?.hits[0]?._source;
}
-export function fetchTemperature(
- from: Date,
- to: Date,
- size: number = 50,
- fetch: Function
-): Promise {
+export function fetchTemperature(from: Date, to: Date, size: number = 50): Promise {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
@@ -181,18 +177,14 @@ export function fetchTemperature(
},
body: JSON.stringify(esSearchQuery)
};
+ console.log('temp options:', options);
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseTempResponse);
}
-export function fetchHumidity(
- from: Date,
- to: Date,
- size: number = 50,
- fetch: Function
-): Promise {
+export function fetchHumidity(from: Date, to: Date, size: number = 50): Promise {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
@@ -213,12 +205,7 @@ export function fetchHumidity(
.then(parseTempResponse);
}
-export function fetchPressure(
- from: Date,
- to: Date,
- size: number = 50,
- fetch: Function
-): Promise {
+export function fetchPressure(from: Date, to: Date, size: number = 50): Promise {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 71e56d0..ca0d026 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,5 +1,5 @@
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
index f96bc99..6cb6adf 100644
--- a/src/routes/+page.server.ts
+++ b/src/routes/+page.server.ts
@@ -1,7 +1,5 @@
-import { getLatestInsideReadings, getLatestOutsideReadings } from '$lib/graphQueryGenerator';
import type { PageServerLoad } from './$types';
-let DEFAULT_MINUTES = 14400;
const host = 'http://brewpi.schleppe:5000';
const sensorsUrl = `${host}/api/sensors`;
const relaysUrl = `${host}/api/relays`;
@@ -24,8 +22,6 @@ async function getRelays() {
export const load: PageServerLoad = async () => {
const [sensors, relays] = await Promise.all([getSensors(), getRelays()]);
- console.log('got sensors and relays');
- console.log(sensors, relays);
const inside = sensors.find((sensor) => sensor.location === 'inside');
const outside = sensors.find((sensor) => sensor.location === 'outside');
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 2db7b72..54d0922 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,13 +1,12 @@
@@ -16,9 +15,9 @@
-
+
-
+
diff --git a/src/routes/api/graph/[unit]/+server.ts b/src/routes/api/graph/[unit]/+server.ts
new file mode 100644
index 0000000..346142a
--- /dev/null
+++ b/src/routes/api/graph/[unit]/+server.ts
@@ -0,0 +1,39 @@
+import { json, RequestEvent } from '@sveltejs/kit';
+import {
+ fetchTemperature,
+ fetchHumidity,
+ fetchPressure
+} from '../../../../lib/server/graphQueryGenerator';
+import type { RequestHandler } from './$types';
+
+const UNITS = ['temperature', 'humidity', 'pressure'];
+const UNITS_STRING = UNITS.join(', ');
+
+export const POST = (async (event: RequestEvent) => {
+ const { request, params } = event;
+
+ const { unit } = params;
+ if (!unit || UNITS.indexOf(unit) == -1) {
+ return json({
+ success: false,
+ message: `Unit ${unit} not found. Choose from: ${UNITS_STRING}`
+ });
+ }
+
+ const bodyData = await request.json();
+ let data;
+ let { from, to } = bodyData;
+ const { size } = bodyData;
+ from = new Date(from);
+ to = new Date(to);
+
+ if (unit === 'temperature') {
+ data = await fetchTemperature(from, to, size);
+ } else if (unit === 'humidity') {
+ data = await fetchHumidity(from, to, size);
+ } else if (unit === 'pressure') {
+ data = await fetchPressure(from, to, size);
+ }
+
+ return json({ success: true, data });
+}) satisfies RequestHandler;
diff --git a/src/routes/brews/+page.server.ts b/src/routes/brews/+page.server.ts
index 6d2aa15..bf1de29 100644
--- a/src/routes/brews/+page.server.ts
+++ b/src/routes/brews/+page.server.ts
@@ -1,4 +1,4 @@
-import brews from '../../brews.json'
+import brews from '../../brews.json';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
diff --git a/src/routes/brews/+page.svelte b/src/routes/brews/+page.svelte
index 1a546de..a8af991 100644
--- a/src/routes/brews/+page.svelte
+++ b/src/routes/brews/+page.svelte
@@ -5,7 +5,7 @@
const path = (date: string) => '/brews/' + String(date);
const dateFormat = { year: 'numeric', month: 'short', day: 'numeric' };
- const dateString = (date) => new Date(date * 1000).toLocaleDateString('no-NB', dateFormat);
+ const dateString = (date: number) => new Date(date * 1000).toLocaleDateString('no-NB', dateFormat);
diff --git a/src/routes/brews/[date]/+page.server.ts b/src/routes/brews/[date]/+page.server.ts
index 318eab9..495d6de 100644
--- a/src/routes/brews/[date]/+page.server.ts
+++ b/src/routes/brews/[date]/+page.server.ts
@@ -1,8 +1,25 @@
import { error } from '@sveltejs/kit';
import brews from '../../../brews.json';
+import { fetchHumidity, fetchTemperature } from '../../../lib/server/graphQueryGenerator';
import type { PageLoad } from './$types';
-export const load = (({ params }) => {
+async function fetchGraphData(brew) {
+ const start = new Date(brew.date * 1000 - 86400000);
+ const end = new Date(brew.date * 1000 + 4838400000);
+ const size = 200;
+
+ const [temperature, humidity] = await Promise.all([
+ fetchTemperature(start, end, size),
+ fetchHumidity(start, end, size)
+ ]);
+
+ return {
+ temperature,
+ humidity
+ };
+}
+
+export const load = (async ({ params }) => {
const { date } = params;
const brew = brews.find((b) => b?.date === date);
@@ -10,5 +27,7 @@ export const load = (({ params }) => {
throw error(404, 'Brew not found');
}
- return { brew };
+ const graphData = await fetchGraphData(brew);
+
+ return { brew, graphData };
}) satisfies PageLoad;
diff --git a/src/routes/brews/[date]/+page.svelte b/src/routes/brews/[date]/+page.svelte
index e4c736c..7bff593 100644
--- a/src/routes/brews/[date]/+page.svelte
+++ b/src/routes/brews/[date]/+page.svelte
@@ -1,27 +1,19 @@
-
-
- reload(minutes)} />
-
{#each buttonMinutes as button}
-
+
{/each}
{#if temperatureData}
-
-
+
+
{/if}
{#if humidityData}
-
-
+
+
{/if}
{#if pressureData}
-
-
+
+
{/if}
@@ -89,17 +98,21 @@
width: 100%;
@include mobile {
- grid-template-columns: 1fr;
+ display: block;
+ > *:not(*:first-child) {
+ margin-top: 1rem;
+ }
}
- .graphWrapper {
- max-width: 100vw;
+ .card {
+ padding: 1rem;
}
}
.button-wrapper {
display: flex;
- width: min-content;
+ margin: 1rem 0;
+ overflow-y: scroll;
}
button {
diff --git a/src/styles/global.css b/src/styles/global.css
index 4048b83..7c95241 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -96,6 +96,14 @@ li {
font-style: normal;
}
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/Roboto-Bold.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/Roboto-Bold.woff') format('woff'), url('/fonts/Roboto-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: bold;
+}
+
@font-face {
font-family: 'Roboto';
src: url('/fonts/Roboto-Light.ttf') format('truetype');
diff --git a/yarn.lock b/yarn.lock
index 97c900b..b2252e6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -164,6 +164,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
+"@kurkle/color@^0.3.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
+ integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -513,10 +518,12 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chart.js@^3.8.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.8.0.tgz#c6c14c457b9dc3ce7f1514a59e9b262afd6f1a94"
- integrity sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==
+chart.js@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1"
+ integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==
+ dependencies:
+ "@kurkle/color" "^0.3.0"
chartjs-plugin-zoom@^1.2.1:
version "1.2.1"