From 557df68c75f91814aa193ceb2370031965799410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Sun, 30 Jun 2024 09:53:13 +0200 Subject: [PATCH] Rework graphs to widget --- server/src/app/routes/dow.ts | 186 +++++++------------------ server/src/lib/dow/widgets/barGraph.ts | 129 +++++++++++++++++ 2 files changed, 176 insertions(+), 139 deletions(-) create mode 100644 server/src/lib/dow/widgets/barGraph.ts diff --git a/server/src/app/routes/dow.ts b/server/src/app/routes/dow.ts index 8ace527..9cb2751 100644 --- a/server/src/app/routes/dow.ts +++ b/server/src/app/routes/dow.ts @@ -8,6 +8,7 @@ import { ALIGN } from '../../lib/dow/constants' import { DowWidget } from '../../lib/dow/constants' import { rectangleWithTitle } from '../../lib/dow/widgets/rectangleWithTitle' import { text } from '../../lib/dow/primitives/text' +import { barGraph } from '../../lib/dow/widgets/barGraph' const weekdaysInCs = [ 'Pondeli', @@ -167,56 +168,17 @@ export const dowRoutes = router((router, ctx) => { const next12Hours = Object.entries(weather.hours).slice(1, 22) - const minTemp = - Math.min(...next12Hours.map(([, h]) => h.temperature as number)) - 5 - - const minTempIndex = Object.entries(weather.hours).reduce( - (acc, [i, h]) => { - if (typeof h.temperature !== 'number') { - return acc - } - - if (!acc || h.temperature < acc.temp) { - return { temp: h.temperature, index: i } - } - - return acc - }, - {} as { temp: number; index: string } | undefined, - )?.index - - const maxTemp = Math.max( - ...next12Hours.map(([, h]) => h.temperature as number), - ) - - const maxTempIndex = Object.entries(weather.hours).reduce( - (acc, [i, h]) => { - if (typeof h.temperature !== 'number') { - return acc - } - - if (!acc || h.temperature > acc.temp) { - return { temp: h.temperature, index: i } - } - - return acc - }, - {} as { temp: number; index: string } | undefined, - )?.index - - const tempRange = maxTemp - minTemp - - const tempY = 250 + const tempY = 230 const tempX = 30 const tempW = 280 const tempH = 120 - const tempGraphP = 10 - const tempGraphW = tempW - tempGraphP * 2 + const tempGraphP = 5 + const tempGraphW = tempW - tempGraphP * 4 widgets.push( ...rectangleWithTitle({ x: tempX, - y: tempY - 40, + y: tempY - 20, width: tempW, height: tempH, title: 'Teplota', @@ -225,54 +187,26 @@ export const dowRoutes = router((router, ctx) => { }), ) - for (const [offset, hour] of next12Hours) { - const offsetInt = +offset - - const x = Math.floor( - tempX + - tempGraphP + - (offsetInt - 1) * (tempGraphW / next12Hours.length), - ) - - const y = tempY - const temperature = hour.temperature as number - const ratio = (temperature - minTemp) / tempRange - const height = Math.floor(50 * ratio) - const t = now.plus({ hours: offsetInt }).hour - - widgets.push({ - x: x + 7, - y: y + 5 + 50 - height, - x2: 6, - y2: height, - t: TYPES.RECT_FILL, - }) - - if (offset === minTempIndex || offset === maxTempIndex) { - widgets.push({ - x: x + 10, - y: y, - c: Math.floor(hour.temperature as number).toString(), - t: TYPES.TEXT, - va: ALIGN.END, - ha: ALIGN.CENTER, - f: 3, - }) - } - - if ((offsetInt - 1) % 2 === 0) { - widgets.push({ - x: x + 10, - y: y + 60, - c: t.toString().padStart(2, '0'), - t: TYPES.TEXT, - va: ALIGN.START, - ha: ALIGN.CENTER, - f: 3, - }) - } - } + widgets.push( + ...barGraph({ + x: tempX + tempGraphP * 2, + y: tempY + tempGraphP, + width: tempGraphW, + height: tempH - 20 - tempGraphP * 2, + data: next12Hours.map(([offset, h]) => ({ + x: now.plus({ hours: +offset }).hour.toString().padStart(2, '0'), + yFormatted: Math.floor(h.temperature as number).toString(), + y: h.temperature as number, + })), + xAxisFont: 3, + yAxisFont: 3, + adjustMinValue: (min) => Math.min(0, min - 5), + showMinValue: true, + showMaxValue: true, + }), + ) + /* const minPre = Math.min( ...next12Hours.map(([, h]) => h.precipitationAmount as number), ) @@ -298,18 +232,19 @@ export const dowRoutes = router((router, ctx) => { const displayedMaxPre = Math.max(maxPre, 1) const preRange = displayedMaxPre - minPre + */ - const preY = 250 + 120 + 20 + const preY = 250 + 120 const preX = 30 const preW = 280 const preH = 120 - const preGraphP = 10 - const preGraphW = preW - preGraphP * 2 + const preGraphP = 5 + const preGraphW = preW - preGraphP * 4 widgets.push( ...rectangleWithTitle({ x: preX, - y: preY - 40, + y: preY - 20, width: preW, height: preH, title: 'Srazky', @@ -318,51 +253,24 @@ export const dowRoutes = router((router, ctx) => { }), ) - for (const [offset, hour] of next12Hours) { - const offsetInt = +offset - - const x = Math.floor( - preX + preGraphP + (offsetInt - 1) * (preGraphW / next12Hours.length), - ) - - const y = preY - const pre = hour.precipitationAmount as number - const ratio = (pre - minPre) / preRange - const height = Math.floor(50 * ratio) - const t = now.plus({ hours: offsetInt }).hour - - widgets.push({ - x: x + 7, - y: y + 5 + 50 - height, - x2: 6, - y2: height, - t: TYPES.RECT_FILL, - }) - - if (offset === maxPreIndex && maxPre > 0.01) { - widgets.push({ - x: x + 10, - y: y, - c: (hour.precipitationAmount as number).toFixed(1) + 'mm', - t: TYPES.TEXT, - va: ALIGN.END, - ha: ALIGN.CENTER, - f: 3, - }) - } - - if ((offsetInt - 1) % 2 === 0) { - widgets.push({ - x: x + 10, - y: y + 60, - c: t.toString().padStart(2, '0'), - t: TYPES.TEXT, - va: ALIGN.START, - ha: ALIGN.CENTER, - f: 3, - }) - } - } + widgets.push( + ...barGraph({ + x: preX + preGraphP * 2, + y: preY + preGraphP, + width: preGraphW, + height: preH - 20 - preGraphP * 2, + data: next12Hours.map(([offset, h]) => ({ + x: now.plus({ hours: +offset }).hour.toString().padStart(2, '0'), + yFormatted: (h.precipitationAmount as number).toFixed(1) + 'mm', + y: h.precipitationAmount as number, + })), + xAxisFont: 3, + yAxisFont: 3, + adjustMinValue: () => 0, + adjustMaxValue: (max) => Math.max(1, max), + showMaxValue: true, + }), + ) res.json(widgets) }), diff --git a/server/src/lib/dow/widgets/barGraph.ts b/server/src/lib/dow/widgets/barGraph.ts new file mode 100644 index 0000000..cf1b849 --- /dev/null +++ b/server/src/lib/dow/widgets/barGraph.ts @@ -0,0 +1,129 @@ +import { ALIGN, DowWidget } from '../constants' +import { defineWidget } from '../defineWidget' +import { rectangle } from '../primitives/rectangle' +import { text } from '../primitives/text' + +type Params = { + data: { + x: string + y: number + yFormatted?: string + }[] + showMaxValue?: boolean + showMinValue?: boolean + adjustMaxValue?: (max: number) => number + adjustMinValue?: (min: number) => number + x: number + y: number + width: number + height: number + xAxisFont: number + yAxisFont: number +} + +export const barGraph = defineWidget( + ({ + data, + showMaxValue, + showMinValue, + adjustMaxValue, + adjustMinValue, + x, + y, + width, + height, + xAxisFont, + yAxisFont, + }: Params) => { + if (data.length === 0) { + return [] + } + + const minValue = data.reduce( + (acc, { y }, index) => + !acc || y < acc?.value ? { value: y, index } : acc, + undefined as { value: number; index: number } | undefined, + ) + + const maxValue = data.reduce( + (acc, { y }, index) => + !acc || y > acc?.value ? { value: y, index } : acc, + undefined as { value: number; index: number } | undefined, + ) + + if (!minValue || !maxValue) { + return [] + } + + const adjustedMinValue = adjustMinValue + ? adjustMinValue(minValue.value) + : minValue.value + + const adjustedMaxValue = adjustMaxValue + ? adjustMaxValue(maxValue.value) + : maxValue.value + + const valueRange = adjustedMaxValue - adjustedMinValue + + const widgets = [] as DowWidget[] + + // DEBUG: widgets.push(rectangle({ x, y, width, height, filled: false })) + + const xAxisHeight = 25 + const yAxisHeight = 20 + + const barsHeight = height - xAxisHeight - yAxisHeight + const barSpaceWidth = width / data.length + const barsWidth = 6 + + for (const [index, dataPoint] of data.entries()) { + const barX = Math.floor(x + index * barSpaceWidth) + const barY = y + yAxisHeight + + const value = dataPoint.y + const ratio = (value - adjustedMinValue) / valueRange + const barHeight = Math.floor(barsHeight * ratio) + + widgets.push( + rectangle({ + x: Math.floor(barX + barSpaceWidth / 2 - barsWidth / 2), + y: barY + 5 + barsHeight - barHeight, + width: barsWidth, + height: barHeight, + filled: true, + }), + ) + + if ( + (index === minValue.index && showMinValue) || + (index === maxValue.index && showMaxValue) + ) { + widgets.push( + text({ + x: Math.floor(barX + barSpaceWidth / 2), + y: barY, + text: dataPoint.yFormatted ?? dataPoint.y.toString(), + verticalAlign: ALIGN.END, + horizontalAlign: ALIGN.CENTER, + font: yAxisFont, + }), + ) + } + + if (index % 2 === 0) { + widgets.push( + text({ + x: Math.floor(barX + barSpaceWidth / 2), + y: barY + barsHeight + 10, + text: dataPoint.x, + verticalAlign: ALIGN.START, + horizontalAlign: ALIGN.CENTER, + font: xAxisFont, + }), + ) + } + } + + return widgets + }, +)