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

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"