mirror of
https://github.com/KevinMidboe/brewPi.git
synced 2025-10-29 16:50:12 +00:00
Merge pull request #2 from KevinMidboe/refactor/graph-api-data
Refactor: Graph api data
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^4.3.0",
|
||||||
"chartjs-plugin-zoom": "^1.2.1",
|
"chartjs-plugin-zoom": "^1.2.1",
|
||||||
"d3": "^7.8.4",
|
"d3": "^7.8.4",
|
||||||
"d3-selection": "^3.0.0",
|
"d3-selection": "^3.0.0",
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
background-size: 150px 150px;
|
background-size: 150px 150px;
|
||||||
animation: move-it 12s linear infinite;
|
animation: move-it 8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move-it {
|
@keyframes move-it {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
PointElement,
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
Title,
|
Title,
|
||||||
Legend
|
Legend
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
|
import { getRelativePosition } from 'chart.js/helpers';
|
||||||
|
|
||||||
import type { ChartDataset } from 'chart.js';
|
import type { ChartDataset } from 'chart.js';
|
||||||
import type IChartFrame from '../interfaces/IChartFrame';
|
import type IChartFrame from '../interfaces/IChartFrame';
|
||||||
@@ -20,123 +22,112 @@
|
|||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
PointElement,
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
Title,
|
Title,
|
||||||
Legend
|
Legend
|
||||||
);
|
);
|
||||||
|
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let dataFrames: IChartFrame[];
|
export let dataFrames: IChartFrame[];
|
||||||
export let beginAtZero: boolean = true;
|
export let hideTitle: boolean;
|
||||||
let chartCanvas: HTMLCanvasElement;
|
let chartCanvas: HTMLCanvasElement;
|
||||||
let chart: Chart;
|
let chart: Chart;
|
||||||
let prevData: any = {};
|
|
||||||
|
|
||||||
interface IDataset {
|
onMount(() => renderChart());
|
||||||
labels: string[];
|
afterUpdate(() => {
|
||||||
data?: ChartDataset<'line', number[]>;
|
chart.destroy();
|
||||||
}
|
renderChart();
|
||||||
|
});
|
||||||
|
|
||||||
interface ITemperatureDataset extends IDataset {
|
// Converts Date to format suitable for the current range displayed
|
||||||
inside: ChartDataset<'line', number[]>;
|
function dateLabelsFormatedBasedOnResolution(dataFrames: IChartFrame[]): string[] {
|
||||||
outside?: ChartDataset<'line', number[]>;
|
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 {}
|
if (deltaSeconds < 3600) {
|
||||||
interface IPressureDataset extends IDataset {}
|
dateOptions = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
|
||||||
|
} else if (deltaSeconds <= 86400) {
|
||||||
function pad(num) {
|
dateOptions = { hour: 'numeric', minute: 'numeric' };
|
||||||
if (num < 10) {
|
} else if (deltaSeconds <= 2592000) {
|
||||||
return `0${num}`;
|
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) {
|
// set dataset label & colors matching the name sent as prop
|
||||||
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${pad(date.getYear() - 100)}`;
|
function setDataColorAndName(data: ChartDataset) {
|
||||||
}
|
if (name === 'Pressure') {
|
||||||
|
Object.assign(data, {
|
||||||
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: {
|
|
||||||
label: 'Bar of pressure',
|
label: 'Bar of pressure',
|
||||||
borderColor: '#ef5878',
|
borderColor: '#ef5878',
|
||||||
backgroundColor: '#fbd7de',
|
backgroundColor: '#fbd7de'
|
||||||
lineTension: 0.5,
|
});
|
||||||
borderWidth: 3,
|
} else if (name === 'Humidity') {
|
||||||
data
|
Object.assign(data, {
|
||||||
}
|
label: '% humidity',
|
||||||
};
|
borderColor: '#57d2fb',
|
||||||
|
backgroundColor: '#d4f2fe'
|
||||||
|
});
|
||||||
|
} else if (name === 'Temperature') {
|
||||||
|
Object.assign(data, {
|
||||||
|
label: '℃ inside',
|
||||||
|
borderColor: '#10e783',
|
||||||
|
backgroundColor: '#c8f9df'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChart() {
|
function renderChart() {
|
||||||
const context: CanvasRenderingContext2D = chartCanvas.getContext('2d');
|
const context: CanvasRenderingContext2D | null = chartCanvas.getContext('2d');
|
||||||
|
if (!context) return
|
||||||
|
|
||||||
let dataset: IDataset | ITemperatureDataset | IHumidityDataset | IPressureDataset;
|
// create labels and singular dataset (data)
|
||||||
if (name === 'Temperature') dataset = computeTemperatureDataset();
|
const labels: string[] = dateLabelsFormatedBasedOnResolution(dataFrames);
|
||||||
else if (name === 'Humidity') dataset = computeHumidityDataset();
|
const data: ChartDataset = {
|
||||||
else if (name === 'Pressure') dataset = computePressureDataset();
|
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, {
|
chart = new Chart(context, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: dataset.labels,
|
labels: labels,
|
||||||
datasets: [dataset?.inside || dataset.data]
|
datasets: [data]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
elements: {
|
elements: {
|
||||||
point: {
|
point: {
|
||||||
radius: 1
|
radius: 2
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
tension: 0.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: !hideTitle,
|
||||||
position: 'left',
|
position: 'left',
|
||||||
|
position: 'top',
|
||||||
text: `${name} over time`,
|
text: `${name} over time`,
|
||||||
font: {
|
font: {
|
||||||
size: 20
|
size: 20
|
||||||
@@ -163,8 +154,21 @@
|
|||||||
// },
|
// },
|
||||||
mode: 'xy'
|
mode: 'xy'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
titleFont: {
|
||||||
|
size: 14
|
||||||
|
},
|
||||||
|
bodyFont: {
|
||||||
|
size: 14
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: false,
|
beginAtZero: false,
|
||||||
@@ -187,14 +191,6 @@
|
|||||||
|
|
||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => renderChart());
|
|
||||||
afterUpdate(() => {
|
|
||||||
console.log('after update run');
|
|
||||||
chart.destroy();
|
|
||||||
renderChart();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<canvas class="card" id="{name}" bind:this="{chartCanvas}" width="400" height="400"></canvas>
|
<canvas id="{name}" bind:this="{chartCanvas}" width="400" height="400"></canvas>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export const title: string = "Temperature"
|
|
||||||
|
|
||||||
let temp = 23
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<header>{ title }</header>
|
|
||||||
|
|
||||||
<div class="body">
|
|
||||||
<p>Inside temperature: {23}℃</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss" module="scoped">
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -83,8 +83,8 @@ function buildQuery(field: String, from: Date, to: Date, interval: String) {
|
|||||||
{
|
{
|
||||||
range: {
|
range: {
|
||||||
'@timestamp': {
|
'@timestamp': {
|
||||||
gte: toDateString,
|
gte: fromDateString,
|
||||||
lte: fromDateString,
|
lte: toDateString,
|
||||||
format: 'strict_date_optional_time'
|
format: 'strict_date_optional_time'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ function calculateInterval(from, to, interval, size) {
|
|||||||
if (interval !== 'auto') {
|
if (interval !== 'auto') {
|
||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
const dateMathInterval = roundInterval((from - to) / size);
|
const dateMathInterval = roundInterval((to - from) / size);
|
||||||
// const dateMathIntervalMs = toMS(dateMathInterval);
|
// const dateMathIntervalMs = toMS(dateMathInterval);
|
||||||
// const minMs = toMS(min);
|
// const minMs = toMS(min);
|
||||||
// if (dateMathIntervalMs !== undefined && minMs !== undefined && dateMathIntervalMs < minMs) {
|
// if (dateMathIntervalMs !== undefined && minMs !== undefined && dateMathIntervalMs < minMs) {
|
||||||
@@ -148,6 +148,7 @@ function calculateInterval(from, to, interval, size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseTempResponse(data: IESTelemetry): IChartFrame[] {
|
function parseTempResponse(data: IESTelemetry): IChartFrame[] {
|
||||||
|
console.log('got temp response:', data);
|
||||||
return data?.aggregations?.data?.buckets.map((bucket) => {
|
return data?.aggregations?.data?.buckets.map((bucket) => {
|
||||||
return {
|
return {
|
||||||
value: bucket?.maxValue?.value,
|
value: bucket?.maxValue?.value,
|
||||||
@@ -161,12 +162,7 @@ function parseLatestResponse(data: IESTelemetry) {
|
|||||||
return data?.hits?.hits[0]?._source;
|
return data?.hits?.hits[0]?._source;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchTemperature(
|
export function fetchTemperature(from: Date, to: Date, size: number = 50): Promise<IChartFrame[]> {
|
||||||
from: Date,
|
|
||||||
to: Date,
|
|
||||||
size: number = 50,
|
|
||||||
fetch: Function
|
|
||||||
): Promise<IChartFrame[]> {
|
|
||||||
const fromMS = from.getTime();
|
const fromMS = from.getTime();
|
||||||
const toMS = to.getTime();
|
const toMS = to.getTime();
|
||||||
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
||||||
@@ -181,18 +177,14 @@ export function fetchTemperature(
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(esSearchQuery)
|
body: JSON.stringify(esSearchQuery)
|
||||||
};
|
};
|
||||||
|
console.log('temp options:', options);
|
||||||
|
|
||||||
return fetch(TELEMETRY_ENDPOINT, options)
|
return fetch(TELEMETRY_ENDPOINT, options)
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
.then(parseTempResponse);
|
.then(parseTempResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchHumidity(
|
export function fetchHumidity(from: Date, to: Date, size: number = 50): Promise<IChartFrame[]> {
|
||||||
from: Date,
|
|
||||||
to: Date,
|
|
||||||
size: number = 50,
|
|
||||||
fetch: Function
|
|
||||||
): Promise<IChartFrame[]> {
|
|
||||||
const fromMS = from.getTime();
|
const fromMS = from.getTime();
|
||||||
const toMS = to.getTime();
|
const toMS = to.getTime();
|
||||||
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
||||||
@@ -213,12 +205,7 @@ export function fetchHumidity(
|
|||||||
.then(parseTempResponse);
|
.then(parseTempResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchPressure(
|
export function fetchPressure(from: Date, to: Date, size: number = 50): Promise<IChartFrame[]> {
|
||||||
from: Date,
|
|
||||||
to: Date,
|
|
||||||
size: number = 50,
|
|
||||||
fetch: Function
|
|
||||||
): Promise<IChartFrame[]> {
|
|
||||||
const fromMS = from.getTime();
|
const fromMS = from.getTime();
|
||||||
const toMS = to.getTime();
|
const toMS = to.getTime();
|
||||||
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
const interval = calculateInterval(fromMS, toMS, 'auto', size);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HeaderComponent from '$lib/components/Header.svelte';
|
import HeaderComponent from '../lib/components/Header.svelte';
|
||||||
// import Darkmode from '$lib/components/Darkmode.svelte'
|
// import Darkmode from '$lib/components/Darkmode.svelte'
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { getLatestInsideReadings, getLatestOutsideReadings } from '$lib/graphQueryGenerator';
|
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
let DEFAULT_MINUTES = 14400;
|
|
||||||
const host = 'http://brewpi.schleppe:5000';
|
const host = 'http://brewpi.schleppe:5000';
|
||||||
const sensorsUrl = `${host}/api/sensors`;
|
const sensorsUrl = `${host}/api/sensors`;
|
||||||
const relaysUrl = `${host}/api/relays`;
|
const relaysUrl = `${host}/api/relays`;
|
||||||
@@ -24,8 +22,6 @@ async function getRelays() {
|
|||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
const [sensors, relays] = await Promise.all([getSensors(), getRelays()]);
|
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 inside = sensors.find((sensor) => sensor.location === 'inside');
|
||||||
const outside = sensors.find((sensor) => sensor.location === 'outside');
|
const outside = sensors.find((sensor) => sensor.location === 'outside');
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
import PageHeader from '../lib/components/PageHeader.svelte';
|
||||||
import Display from '$lib/components/display.svelte'
|
import VerticalSensorDisplay from '../lib/components/VerticalSensorDisplay.svelte';
|
||||||
import VerticalSensorDisplay from '$lib/components/VerticalSensorDisplay.svelte'
|
// import Livestream from '$lib/components/Livestream.svelte'
|
||||||
import Livestream from '$lib/components/Livestream.svelte'
|
import BrewProgress from '../lib/components/BrewProgress.svelte';
|
||||||
import BrewProgress from '$lib/components/BrewProgress.svelte';
|
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import RelayControls from '../lib/components/RelayControls.svelte';
|
import RelayControls from '../lib/components/RelayControls.svelte';
|
||||||
|
|
||||||
export let data: PageData
|
export let data: PageData;
|
||||||
const { inside, outside, relays } = data;
|
const { inside, outside, relays } = data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -16,9 +15,9 @@
|
|||||||
<div class="vertical-grid">
|
<div class="vertical-grid">
|
||||||
<BrewProgress />
|
<BrewProgress />
|
||||||
|
|
||||||
<VerticalSensorDisplay {inside} {outside} />
|
<VerticalSensorDisplay inside="{inside}" outside="{outside}" />
|
||||||
|
|
||||||
<RelayControls {relays} />
|
<RelayControls relays="{relays}" />
|
||||||
|
|
||||||
<!-- <Livestream /> -->
|
<!-- <Livestream /> -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
39
src/routes/api/graph/[unit]/+server.ts
Normal file
39
src/routes/api/graph/[unit]/+server.ts
Normal file
@@ -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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import brews from '../../brews.json'
|
import brews from '../../brews.json';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
const path = (date: string) => '/brews/' + String(date);
|
const path = (date: string) => '/brews/' + String(date);
|
||||||
|
|
||||||
const dateFormat = { year: 'numeric', month: 'short', day: 'numeric' };
|
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);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import brews from '../../../brews.json';
|
import brews from '../../../brews.json';
|
||||||
|
import { fetchHumidity, fetchTemperature } from '../../../lib/server/graphQueryGenerator';
|
||||||
import type { PageLoad } from './$types';
|
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 { date } = params;
|
||||||
const brew = brews.find((b) => b?.date === date);
|
const brew = brews.find((b) => b?.date === date);
|
||||||
|
|
||||||
@@ -10,5 +27,7 @@ export const load = (({ params }) => {
|
|||||||
throw error(404, 'Brew not found');
|
throw error(404, 'Brew not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { brew };
|
const graphData = await fetchGraphData(brew);
|
||||||
|
|
||||||
|
return { brew, graphData };
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad;
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Graph from '../../../lib/components/Graph.svelte';
|
import Graph from '../../../lib/components/Graph.svelte';
|
||||||
import { fetchTemperature, fetchHumidity } from '../../../lib/graphQueryGenerator';
|
|
||||||
import IChartFrame from '../../../lib/interfaces/IChartFrame';
|
import IChartFrame from '../../../lib/interfaces/IChartFrame';
|
||||||
let height: number;
|
let height: number;
|
||||||
|
|
||||||
// let brew = {
|
|
||||||
// recipe: 'https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ',
|
|
||||||
// bryggselv: 'https://www.bryggselv.no/finest/105932/kinn-kveldsbris-allgrain-ølsett-25-liter',
|
|
||||||
// untapped: 'https://untappd.com/b/kinn-bryggeri-kveldsbris/695024'
|
|
||||||
// }
|
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let brew = data.brew;
|
let brew = data.brew;
|
||||||
let temperatureData: IChartFrame[];
|
let temperatureData: IChartFrame[] = data.graphData.temperature;
|
||||||
let humidityData: IChartFrame[];
|
let humidityData: IChartFrame[] = data.graphData.humidity;
|
||||||
|
|
||||||
const from: Date = new Date();
|
const dateFormat: Intl.DateTimeFormatOptions = {
|
||||||
const to = new Date(1684872000000);
|
weekday: 'long',
|
||||||
const size = 40;
|
year: 'numeric',
|
||||||
fetchTemperature(from, to, size, fetch).then((resp) => (temperatureData = resp));
|
month: 'short',
|
||||||
fetchHumidity(from, to, size, fetch).then((resp) => (humidityData = resp));
|
day: 'numeric'
|
||||||
|
};
|
||||||
const dateFormat = { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric' };
|
|
||||||
const dateString = new Date(Number(brew.date * 1000)).toLocaleDateString('no-NB', dateFormat);
|
const dateString = new Date(Number(brew.date * 1000)).toLocaleDateString('no-NB', dateFormat);
|
||||||
|
|
||||||
const wizards = brew.by.join(', ');
|
const wizards = brew.by.join(', ');
|
||||||
@@ -75,19 +67,21 @@
|
|||||||
ble Tuborg Bryggeri en del av Carlsberg.
|
ble Tuborg Bryggeri en del av Carlsberg.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if temperatureData}
|
<div class="graph-container">
|
||||||
<div class="graph">
|
{#if temperatureData}
|
||||||
<h3>Temperature during fermentation</h3>
|
<div class="graph">
|
||||||
<Graph dataFrames="{temperatureData}" name="Temperature" />
|
<h3>Temperature during fermentation</h3>
|
||||||
</div>
|
<Graph dataFrames="{temperatureData}" name="Temperature" hideTitle="{true}" />
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if humidityData}
|
{#if humidityData}
|
||||||
<div class="graph">
|
<div class="graph">
|
||||||
<h3>Humidity during carbonation</h3>
|
<h3>Humidity during carbonation</h3>
|
||||||
<Graph dataFrames="{humidityData}" name="Humidity" />
|
<Graph dataFrames="{humidityData}" name="Humidity" hideTitle="{true}" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Smak</h3>
|
<h3>Smak</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -222,6 +216,12 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { fetchTemperature, fetchHumidity, fetchPressure } from '$lib/graphQueryGenerator';
|
import {
|
||||||
|
fetchTemperature,
|
||||||
|
fetchHumidity,
|
||||||
|
fetchPressure
|
||||||
|
} from '../../lib/server/graphQueryGenerator';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import type IChartFrame from '$lib/interfaces/IChartFrame';
|
import type IChartFrame from '../../lib/interfaces/IChartFrame';
|
||||||
|
|
||||||
let DEFAULT_MINUTES = 10080;
|
const DEFAULT_MINUTES = 10080;
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch }) => {
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
const temperatureData: IChartFrame[] = await getTemp(DEFAULT_MINUTES, fetch);
|
const to = new Date();
|
||||||
const humidityData: IChartFrame[] = await getHumidity(DEFAULT_MINUTES, fetch);
|
const from = new Date(to.getTime() - DEFAULT_MINUTES * 60 * 1000);
|
||||||
const pressureData: IChartFrame[] = await getPressure(DEFAULT_MINUTES, fetch);
|
const size = 40;
|
||||||
|
|
||||||
|
const temperatureData: IChartFrame[] = await fetchTemperature(from, to, size);
|
||||||
|
const humidityData: IChartFrame[] = await fetchHumidity(from, to, size);
|
||||||
|
const pressureData: IChartFrame[] = await fetchPressure(from, to, size);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
temperatureData,
|
temperatureData,
|
||||||
@@ -16,23 +24,3 @@ export const load: PageServerLoad = async ({ fetch }) => {
|
|||||||
DEFAULT_MINUTES
|
DEFAULT_MINUTES
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSensor(func: Function, minutes: number, fetch: Function) {
|
|
||||||
const from: Date = new Date();
|
|
||||||
const to = new Date(from.getTime() - minutes * 60 * 1000);
|
|
||||||
const size = 40;
|
|
||||||
|
|
||||||
return func(from, to, size, fetch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTemp(minutes: number, fetch: Function): IChartFrame[] {
|
|
||||||
return getSensor(fetchTemperature, minutes, fetch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHumidity(minutes: number, fetch: Function): IChartFrame[] {
|
|
||||||
return getSensor(fetchHumidity, minutes, fetch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPressure(minutes: number, fetch: Function): IChartFrame[] {
|
|
||||||
return getSensor(fetchPressure, minutes, fetch);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,80 +1,89 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fetchTemperature, fetchHumidity, fetchPressure } from '$lib/graphQueryGenerator';
|
import Graph from '../../lib/components/Graph.svelte';
|
||||||
import Graph from '$lib/components/Graph.svelte';
|
import type IChartFrame from '../../lib/interfaces/IChartFrame';
|
||||||
import type IChartFrame from '$lib/interfaces/IChartFrame';
|
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData
|
export let data: PageData;
|
||||||
|
|
||||||
let temperatureData: IChartFrame[] = data?.temperatureData;
|
let temperatureData: IChartFrame[] = data?.temperatureData;
|
||||||
let humidityData: IChartFrame[] = data?.temperatureData;
|
let humidityData: IChartFrame[] = data?.humidityData;
|
||||||
let pressureData: IChartFrame[] = data?.temperatureData;
|
let pressureData: IChartFrame[] = data?.pressureData;
|
||||||
let DEFAULT_MINUTES: number = data?.DEFAULT_MINUTES
|
let DEFAULT_MINUTES: number = data?.DEFAULT_MINUTES;
|
||||||
let minutes: number = DEFAULT_MINUTES;
|
let minutes: number = DEFAULT_MINUTES;
|
||||||
|
|
||||||
const buttonMinutes = [{
|
async function fetchData(unit: string, from: Date, to: Date, size: number) {
|
||||||
value: 15,
|
const options = {
|
||||||
name: 'Last 15 minutes'
|
method: 'POST',
|
||||||
}, {
|
body: JSON.stringify({ from, to, size })
|
||||||
value: 60,
|
};
|
||||||
name: 'Last hour'
|
|
||||||
}, {
|
return fetch(`/api/graph/${unit}`, options).then((resp) => resp.json());
|
||||||
value: 1440,
|
}
|
||||||
name: 'Last day'
|
|
||||||
}, {
|
const buttonMinutes = [
|
||||||
value: 10080,
|
{ value: 15, name: 'Last 15 minutes' },
|
||||||
name: 'Last week'
|
{ value: 60, name: 'Last hour' },
|
||||||
}, {
|
{ value: 360, name: 'Last 6 hours' },
|
||||||
value: 43200,
|
{ value: 1440, name: 'Last day' },
|
||||||
name: 'Last month'
|
{ value: 10080, name: 'Last week' },
|
||||||
}, {
|
{ value: 43200, name: 'Last month' },
|
||||||
value: 129600,
|
{ value: 129600, name: 'Last 3 months' },
|
||||||
name: 'Last 3 months'
|
{ value: 259200, name: 'Last 6 months' },
|
||||||
}, {
|
{ value: 518400, name: 'Last year' }
|
||||||
value: 259200,
|
];
|
||||||
name: 'Last 6 months'
|
|
||||||
}, {
|
|
||||||
value: 518400,
|
|
||||||
name: 'Last year',
|
|
||||||
}]
|
|
||||||
|
|
||||||
function reload(mins: number) {
|
function reload(mins: number) {
|
||||||
minutes = mins
|
minutes = mins;
|
||||||
const from: Date = new Date();
|
const to: Date = new Date();
|
||||||
const to = new Date(from.getTime() - minutes * 60 * 1000);
|
const from = new Date(to.getTime() - minutes * 60 * 1000);
|
||||||
const size = 40;
|
const size = 40;
|
||||||
|
|
||||||
fetchTemperature(from, to, size, window.fetch).then((resp) => (temperatureData = resp));
|
fetchData('temperature', from, to, size).then((resp) => (temperatureData = resp?.data));
|
||||||
fetchHumidity(from, to, size, window.fetch).then((resp) => (humidityData = resp));
|
fetchData('humidity', from, to, size).then((resp) => (humidityData = resp?.data));
|
||||||
fetchPressure(from, to, size, window.fetch).then((resp) => (pressureData = resp));
|
fetchData('pressure', from, to, size).then((resp) => (pressureData = resp?.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollSelectedButtonIntoView() {
|
||||||
|
const container = document.getElementsByClassName('button-wrapper')[0];
|
||||||
|
const selected = document.getElementsByClassName('selected')[0] as HTMLElement;
|
||||||
|
const containerWidth = container.getBoundingClientRect().width;
|
||||||
|
const selectedWidth = selected.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
// if the container is in-view, return
|
||||||
|
if (containerWidth > selected.offsetLeft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.scrollLeft = selected.offsetLeft - selectedWidth / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(scrollSelectedButtonIntoView);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <h1>Server: {emoji.emoji}</h1> -->
|
|
||||||
|
|
||||||
<input type="number" bind:value={minutes} on:input={() => reload(minutes)} />
|
|
||||||
|
|
||||||
<div class="button-wrapper">
|
<div class="button-wrapper">
|
||||||
{#each buttonMinutes as button}
|
{#each buttonMinutes as button}
|
||||||
<button on:click={() => reload(button.value)} class="{button.value === minutes ? 'selected' : ''}">{ button.name }</button>
|
<button
|
||||||
|
on:click="{() => reload(button.value)}"
|
||||||
|
class="{button.value === minutes ? 'selected' : ''}">{button.name}</button
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="graphs">
|
<section class="graphs">
|
||||||
{#if temperatureData}
|
{#if temperatureData}
|
||||||
<div class="graphWrapper">
|
<div class="card">
|
||||||
<Graph dataFrames={temperatureData} name="Temperature" />
|
<Graph dataFrames="{temperatureData}" name="Temperature" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if humidityData}
|
{#if humidityData}
|
||||||
<div class="graphWrapper">
|
<div class="card">
|
||||||
<Graph dataFrames={humidityData} name="Humidity" beginAtZero={false} />
|
<Graph dataFrames="{humidityData}" name="Humidity" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if pressureData}
|
{#if pressureData}
|
||||||
<div class="graphWrapper">
|
<div class="card">
|
||||||
<Graph dataFrames={pressureData} name="Pressure" beginAtZero={false} />
|
<Graph dataFrames="{pressureData}" name="Pressure" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
@@ -89,17 +98,21 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
grid-template-columns: 1fr;
|
display: block;
|
||||||
|
> *:not(*:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.graphWrapper {
|
.card {
|
||||||
max-width: 100vw;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper {
|
.button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: min-content;
|
margin: 1rem 0;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ li {
|
|||||||
font-style: normal;
|
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-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
src: url('/fonts/Roboto-Light.ttf') format('truetype');
|
src: url('/fonts/Roboto-Light.ttf') format('truetype');
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@@ -164,6 +164,11 @@
|
|||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@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":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
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"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
chart.js@^3.8.0:
|
chart.js@^4.3.0:
|
||||||
version "3.8.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.8.0.tgz#c6c14c457b9dc3ce7f1514a59e9b262afd6f1a94"
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1"
|
||||||
integrity sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==
|
integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==
|
||||||
|
dependencies:
|
||||||
|
"@kurkle/color" "^0.3.0"
|
||||||
|
|
||||||
chartjs-plugin-zoom@^1.2.1:
|
chartjs-plugin-zoom@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user