Merge pull request #2 from KevinMidboe/refactor/graph-api-data

Refactor: Graph api data
This commit is contained in:
2023-05-30 17:39:01 +02:00
committed by GitHub
17 changed files with 295 additions and 263 deletions

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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');

View File

@@ -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>

View 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;

View File

@@ -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 () => {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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"