diff --git a/apps/web/components/dashboard/analytics/AnalyticsView.tsx b/apps/web/components/dashboard/analytics/AnalyticsView.tsx
index ded9ce8..8e274ae 100644
--- a/apps/web/components/dashboard/analytics/AnalyticsView.tsx
+++ b/apps/web/components/dashboard/analytics/AnalyticsView.tsx
@@ -13,6 +13,7 @@ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
import PieChart from "./PieChart";
import { useTimezone } from "@/lib/userLocalSettings/client";
+import TimeseriesChart from "./Timeseries";
const parseDateRangeFromQuery = (): Date[] | undefined => {
const searchParams = useSearchParams();
@@ -146,7 +147,7 @@ export default function AnalyticsView() {
-
+
@@ -187,19 +188,7 @@ export default function AnalyticsView() {
{
!weightData ?
:
-
- {weightData.map((measurement) => (
- -
- {measurement.value} {measurement.unit}
- ,
- {
- spacetime(measurement.datetime)
- .goto(useTimezone())
- .format("{iso-short} at {hour} {ampm}")
- }
-
- ))}
-
+
}
diff --git a/apps/web/components/dashboard/analytics/Timeseries.tsx b/apps/web/components/dashboard/analytics/Timeseries.tsx
new file mode 100644
index 0000000..ecd9bea
--- /dev/null
+++ b/apps/web/components/dashboard/analytics/Timeseries.tsx
@@ -0,0 +1,54 @@
+'use client';
+import { LineChart, Line, YAxis, XAxis, CartesianGrid, Tooltip, Area, Label } from 'recharts';
+import spacetime from 'spacetime';
+import regression from 'regression';
+
+function polynomialTrendline(data, degree) {
+ // Map your data to the form required by regression-js: [x, y]
+ // Here we convert datetime to a numeric value (e.g., milliseconds since epoch)
+ const regressionData = data.map((dp, i) => [i, dp.value]);
+
+ const result = regression.polynomial(regressionData, { order: degree });
+ // result.equation contains the polynomial coefficients,
+ // and result.predict(x) returns the predicted y for a given x value.
+ return result;
+}
+
+function formatXAxis(tickItem) {
+ return spacetime(tickItem).format('{date}');
+}
+
+export default function TimeseriesChart({ data }) {
+ const unit = data[0] ? data[0].unit : '';
+
+ const polyResult = polynomialTrendline(data, 1)// 2 for quadratic
+ const dataWithFit = (data.map((dp, i) => {
+ return {
+ datetime: dp.datetime,
+ value: dp.value,
+ smoothed: polyResult.predict(i)[1],
+ };
+ }));
+ console.log(polyResult);
+
+ return (
+
+
+ Trend: {7 * polyResult.equation[0]} {unit}/week
+
+
+
+ `${spacetime(d).format("{month-short} {date}")}`}
+ labelStyle={{ color: 'black' }}
+ />
+
+
+
+
+ 0 ? "red" : "green"} dot={false} />
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/package.json b/apps/web/package.json
index 6597518..383b8e8 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -92,6 +92,8 @@
"react-select": "^5.8.0",
"react-syntax-highlighter": "^15.5.0",
"react-timezone-select": "^3.2.8",
+ "recharts": "^2.15.1",
+ "regression": "^2.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"request-ip": "^3.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 26ae817..ce07116 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -385,6 +385,12 @@ importers:
react-timezone-select:
specifier: ^3.2.8
version: 3.2.8(react-dom@18.3.1(react@18.3.1))(react-select@5.8.3(@types/react@18.2.61)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ recharts:
+ specifier: ^2.15.1
+ version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ regression:
+ specifier: ^2.0.1
+ version: 2.0.1
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
@@ -4260,6 +4266,9 @@ packages:
'@types/d3-delaunay@6.0.1':
resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==}
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
'@types/d3-format@3.0.1':
resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==}
@@ -4281,12 +4290,18 @@ packages:
'@types/d3-shape@1.3.12':
resolution: {integrity: sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==}
+ '@types/d3-shape@3.1.7':
+ resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
+
'@types/d3-time-format@2.1.0':
resolution: {integrity: sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==}
'@types/d3-time@3.0.0':
resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==}
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -6039,6 +6054,10 @@ packages:
resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==}
engines: {node: '>=12'}
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
@@ -6054,6 +6073,10 @@ packages:
d3-path@1.0.9:
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
d3-random@2.2.2:
resolution: {integrity: sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==}
@@ -6064,6 +6087,10 @@ packages:
d3-shape@1.3.7:
resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
@@ -6072,6 +6099,10 @@ packages:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
dag-map@1.0.2:
resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==}
@@ -6141,6 +6172,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@@ -6994,6 +7028,10 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-equals@5.2.2:
+ resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==}
+ engines: {node: '>=6.0.0'}
+
fast-glob@3.3.1:
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
engines: {node: '>=8.6.0'}
@@ -10556,6 +10594,12 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-smooth@4.0.4:
+ resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@@ -10658,6 +10702,16 @@ packages:
resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==}
engines: {node: '>= 4'}
+ recharts-scale@0.4.5:
+ resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+ recharts@2.15.1:
+ resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
rechoir@0.6.2:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
@@ -10740,6 +10794,9 @@ packages:
resolution: {integrity: sha512-vTbzVAjQDzwQdKuvj7qEq6OlAprCjE656khuGQ4QaBLg7abQ9I9ISpmLuc6inWe7zP75AECjqUa4g4sdQvOXhg==}
hasBin: true
+ regression@2.0.1:
+ resolution: {integrity: sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ==}
+
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
@@ -12217,6 +12274,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@36.9.2:
+ resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
vite-node@1.6.0:
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -17962,6 +18022,8 @@ snapshots:
'@types/d3-delaunay@6.0.1': {}
+ '@types/d3-ease@3.0.2': {}
+
'@types/d3-format@3.0.1': {}
'@types/d3-geo@3.1.0':
@@ -17984,10 +18046,16 @@ snapshots:
dependencies:
'@types/d3-path': 1.0.11
+ '@types/d3-shape@3.1.7':
+ dependencies:
+ '@types/d3-path': 1.0.11
+
'@types/d3-time-format@2.1.0': {}
'@types/d3-time@3.0.0': {}
+ '@types/d3-timer@3.0.2': {}
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@@ -20302,6 +20370,8 @@ snapshots:
dependencies:
delaunator: 5.0.1
+ d3-ease@3.0.1: {}
+
d3-format@3.1.0: {}
d3-geo@3.1.0:
@@ -20314,6 +20384,8 @@ snapshots:
d3-path@1.0.9: {}
+ d3-path@3.1.0: {}
+
d3-random@2.2.2: {}
d3-scale@4.0.2:
@@ -20328,6 +20400,10 @@ snapshots:
dependencies:
d3-path: 1.0.9
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
d3-time-format@4.1.0:
dependencies:
d3-time: 3.1.0
@@ -20336,6 +20412,8 @@ snapshots:
dependencies:
d3-array: 3.2.1
+ d3-timer@3.0.1: {}
+
dag-map@1.0.2:
optional: true
@@ -20390,6 +20468,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
decode-named-character-reference@1.0.2:
dependencies:
character-entities: 2.0.2
@@ -21036,7 +21116,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+ eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.33.2(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -21090,7 +21170,7 @@ snapshots:
enhanced-resolve: 5.15.0
eslint: 8.57.0
eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+ eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.1
get-tsconfig: 4.8.1
is-core-module: 2.13.1
@@ -21129,7 +21209,7 @@ snapshots:
eslint: 8.57.0
ignore: 5.3.1
- eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
+ eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
dependencies:
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.3
@@ -21599,6 +21679,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
+ fast-equals@5.2.2: {}
+
fast-glob@3.3.1:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -26058,6 +26140,14 @@ snapshots:
- '@types/react'
- supports-color
+ react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ fast-equals: 5.2.2
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
react-style-singleton@2.2.1(@types/react@18.2.61)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
@@ -26184,6 +26274,23 @@ snapshots:
source-map: 0.6.1
tslib: 2.8.1
+ recharts-scale@0.4.5:
+ dependencies:
+ decimal.js-light: 2.5.1
+
+ recharts@2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 2.1.1
+ eventemitter3: 4.0.7
+ lodash: 4.17.21
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+ react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ recharts-scale: 0.4.5
+ tiny-invariant: 1.3.3
+ victory-vendor: 36.9.2
+
rechoir@0.6.2:
dependencies:
resolve: 1.22.8
@@ -26285,6 +26392,8 @@ snapshots:
dependencies:
jsesc: 3.0.2
+ regression@2.0.1: {}
+
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -28040,6 +28149,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
+ victory-vendor@36.9.2:
+ dependencies:
+ '@types/d3-array': 3.0.3
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.1
+ '@types/d3-scale': 4.0.2
+ '@types/d3-shape': 3.1.7
+ '@types/d3-time': 3.0.0
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
vite-node@1.6.0(@types/node@20.11.24)(lightningcss@1.28.1)(terser@5.34.1):
dependencies:
cac: 6.7.14