diff --git a/src/App.tsx b/src/App.tsx index 6ea0b10..67dccb7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,12 +2,15 @@ import { createEffect, createResource, createSignal, + ErrorBoundary, For, Show, } from "solid-js"; import { openDatabase } from "./data"; import Graph from "./Graph"; +type Unit = "kg" | "lb"; + export default function App() { const [database] = createResource(openDatabase); @@ -52,15 +55,9 @@ export default function App() { refetch(); } - const [unit, setUnit] = createSignal<"kg" | "lb">("kg"); - // function handleChange(even: Event & { currentTarget: HTMLInputElement }) { - // const value = even.currentTarget.value; - // if (value !== "kg" && value !== "lb") throw new Error("Expected kg or lb"); - - // setUnit(value); - // } + const [unit, setUnit] = createSignal("kg"); - function format(weight: number, unit: "kg" | "lb") { + function format(weight: number, unit: Unit) { if (unit === "kg") return weight; // If 1lb = 0.45359237kg // Then divide both sides by 45359237 to get 1/45359237lb and 0.00000001kg @@ -70,18 +67,11 @@ export default function App() { } let weightInput: HTMLInputElement | undefined; - // const value = () => { - // const currentUnit = unit(); - // if (!weightInput) return; - - // return format(weightInput.valueAsNumber, currentUnit); - // }; createEffect(() => { - // weightInput?.value + // Need to access unit here to register it as a effect dependency const currentUnit = unit(); if (!weightInput?.valueAsNumber) return; - console.debug("Updating weight input", weightInput); weightInput.valueAsNumber = currentUnit === "kg" ? weightInput.valueAsNumber / 2.2046226218 @@ -91,7 +81,21 @@ export default function App() { return (

Waight

- + ( +
+

Error creating graph

+
+ Error +
{error.message}
+
{error}
+
+ +
+ )} + > + entries() ?? []} /> +
    {(entry) => ( diff --git a/src/Graph.tsx b/src/Graph.tsx index 32bda1e..ea51bf7 100644 --- a/src/Graph.tsx +++ b/src/Graph.tsx @@ -1,65 +1,147 @@ -import { For } from "solid-js"; +import { createEffect, For, from, type Accessor, type JSX } from "solid-js"; +import type { Entry } from "./data"; function* repeat(times: number) { - for (let i = 0; i < times; i++) { - yield i; + for (let index = 0; index < times; index++) { + yield index; } } -export default function Graph() { - const maxX = 100; - const maxY = 100; +function daysInMonth(year: number, month: number) { + // You can go negative on days. So 0 is the last day of the previous month + // Momth is 0 based index e.g. 11 is December + // Sou we would have to -1 to get the current month with the momth number + // But we want the next one so it would be +1 which cancels out the -1 + return new Date(year, month, 0).getDate(); +} - const numberOfPoints = Math.floor(100 * Math.random()); +function startOfMonth(year: number, month: number) { + return new Date(year, month, 1); +} - const sampleData = { - x: repeat(numberOfPoints) - .map(() => Math.random() * maxX) - .toArray(), - y: repeat(numberOfPoints) - .map(() => Math.random() * maxY) - .toArray(), +type Properties = { entries: Accessor }; + +type TrendLine = Pick< + JSX.LineSVGAttributes, + "x1" | "x2" | "y1" | "y2" +>; + +type Max = { + x: number; + y: number; +}; + +type Range = { + from: number; + to: number; +}; + +type Aggregation = { + sums: { + xSquared: number; + xTimesY: number; + x: number; + y: number; + }; + ranges: { + x: Range; + y: Range; + }; +}; + +function aggregate(entries: Entry[], xScope: Range): Aggregation { + const sums = { + xSquared: 0, + xTimesY: 0, + x: 0, + y: 0, }; - if (sampleData.x.length !== sampleData.y.length) - throw new Error("Expected x and y to be the same length"); + const ranges = { + x: { + from: Number.NEGATIVE_INFINITY, + to: Number.POSITIVE_INFINITY, + }, + y: { + from: Number.NEGATIVE_INFINITY, + to: Number.POSITIVE_INFINITY, + }, + }; - const n = sampleData.x.length; + for (const entry of entries) { + const x = entry.timestamp.getTime() - xScope.from; + // If the timestamp is before the start of the month + if (x < 0) continue; - const xSquared = () => sampleData.x.values().map((x) => x * x); + sums.xSquared += x * x; + sums.xTimesY += x * entry.weight; + sums.x += x; + sums.y += entry.weight; - const xTimesY = () => - sampleData.x.values().map((x, i) => x * sampleData.y[i]); + ranges.x.to = Math.max(ranges.x.to, x); + ranges.y.to = Math.max(ranges.y.to, entry.weight); + ranges.x.from = Math.min(ranges.x.from, x); + ranges.y.from = Math.min(ranges.y.from, entry.weight); + } - // Σx² - const xSquaredSum = xSquared().reduce((sum, x) => sum + x, 0); + return { sums, ranges }; +} - // Σxy - const xTimesYSum = xTimesY().reduce((sum, product) => sum + product, 0); +function useTrendLine( + entries: Accessor, + xRange: Range +): { + trendLine: Accessor; + aggregation: Accessor; +} { + const n = () => entries().length; + const aggregation = () => aggregate(entries(), xRange); + + const slope = () => { + const { sums } = aggregation(); + const dividend = n() * sums.xTimesY - sums.x * sums.y; + const divisor = n() * sums.xSquared - sums.x * sums.x; + return dividend / divisor; + }; + + const xAverage = () => aggregation().sums.x / n(); + const yAverage = () => aggregation().sums.y / n(); + + const yIntercept = () => yAverage() - slope() * xAverage(); - const xSum = sampleData.x.reduce((sum, x) => sum + x, 0); - const ySum = sampleData.y.reduce((sum, y) => sum + y, 0); + const y = (x: number) => slope() * x + yIntercept(); - // Calculate slope m - // n(∑xy)−(∑x)(∑y)​ - const dividend = n * xTimesYSum - xSum * ySum; - const divisor = n * xSquaredSum - xSum * xSum; - const slope = dividend / divisor; + const trendLine = (): TrendLine => { + const { ranges } = aggregation(); + return { + x1: 0, + x2: ranges.x.to, + y1: ranges.y.to - yIntercept(), + y2: ranges.y.to - y(ranges.x.to), + }; + }; - // Calculate y intercept b - // b=yˉ​−mxˉ - // Find the averages - // yˉ​ - const yAverage = ySum / n; + return { trendLine, aggregation }; +} - // xˉ - const xAverage = xSum / n; +export default function Graph({ entries }: Properties) { + //TODO make range configurable + // Need to curb time stamp or we get overflow + const now = new Date(); + const xRange = { + from: startOfMonth(now.getFullYear(), now.getMonth()).getTime(), + to: Number.POSITIVE_INFINITY, + }; + const { trendLine, aggregation } = useTrendLine(entries, xRange); - // Calculate b - const yIntercept = yAverage - slope * xAverage; + createEffect(() => { + console.debug("trendLine", trendLine()); + }); + const maxX = () => aggregation().ranges.x.to; + const maxY = () => aggregation().ranges.y.to; return ( - + {/* Y markers */} {(y) => ( @@ -79,24 +161,28 @@ export default function Graph() { )} - - {(x, index) => { - const y = sampleData.y[index()]; - return ; + + {(entry) => { + return ( + // TODO filter out values out of range + + ); }}