Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/0.4.2 #31

Merged
merged 9 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,3 @@ dev-dist
*.njsproj
*.sln
*.sw?
jsconfig.json
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.4.2] - 2023-10-30

### Added

### Changed
- Converted codebase to Typescript

### Fixed


## [0.4.1] - 2023-10-22

### Added
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
</head>
<body class="bg-gray-200">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
457 changes: 318 additions & 139 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "healthrecord",
"private": true,
"version": "0.4.1",
"version": "0.4.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down Expand Up @@ -33,16 +33,24 @@
"yjs": "^13.6.8"
},
"devDependencies": {
"@types/file-saver": "^2.0.6",
"@types/number-to-words": "^1.2.2",
"@types/pluralize": "^0.0.32",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/cli-plugin-router": "~5.0.0",
"autoprefixer": "^10.4.15",
"postcss": "^8.4.29",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.5",
"vite-plugin-pwa": "^0.16.5"
"vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.20"
},
"prettier": {
"singleQuote": true,
"printWidth": 100
"printWidth": 100,
"arrowParens": "avoid",
"bracketSameLine": true,
"htmlWhitespaceSensitivity": "ignore"
}
}
147 changes: 80 additions & 67 deletions src/classes/insight.js → src/classes/insight.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,66 @@
import { record } from "../store/record";
import pluralize from "pluralize";
import { record } from '@stores/record';
import pluralize from 'pluralize';
import n2w from 'number-to-words';

class Insight {

/**
* The analyzed object
*/
#object;

/**
* The analyzed person
*/
person;
person: Person;

/**
* @param {import("../typedefs").Vital | import("../typedefs").Measurement} object Object to analyze
* @param {import("../typedefs").Person} person Person to analyze
*/
constructor(object, person) {
this.#object = object
this.person = person
constructor(person: Person) {
this.person = person;
}

/**
* Get the text insight for this object
*
* @returns {string}
*/
get description() {
return '';
}
}

export class VitalInsight extends Insight {

/**
* The analyzed vital
*/
vital;
vital: Vital;

/**
* @param {import("../typedefs").Vital} object Vital to be analyzed
* @param {import("../typedefs").Person} person Person to analyze
*/
constructor(object, person) {
super(object, person);
constructor(object: Vital, person: Person) {
super(person);
this.vital = object;
}

/**
* Get person's measurements for this vital
*
* @returns {import("../typedefs").Measurement[]}
*/
get #measurements() {
if (!record.value) return [];
if (!this.vital.low && !this.vital.high) return [];
return record.value.measurements
.filter(measurement => measurement.personId === this.person.id && measurement.vitalId === this.vital.id)
.filter(
measurement =>
measurement.personId === this.person.id && measurement.vitalId === this.vital.id
)
.toSorted((a, b) => b.date - a.date);
}

/**
* Measurements whose recent values are below the low value for this vital
*
* @returns {import("../typedefs").Measurement[]}
*/
get #lowMeasurements() {
return this.#measurements.slice(0, 5).filter(measurement => measurement.value < this.vital.low);
if (!this.vital.low) return [];
const low = this.vital.low;
return this.#measurements.slice(0, 5).filter(measurement => measurement.value < low);
}

/**
* Measurements whose recent values are above the high value for this vital
*
* @returns {import("../typedefs").Measurement[]}
*/
get #highMeasurements() {
return this.#measurements.slice(0, 5).filter(measurement => measurement.value > this.vital.high);
if (!this.vital.high) return [];
const high = this.vital.high;
return this.#measurements.slice(0, 5).filter(measurement => measurement.value > high);
}

/**
Expand Down Expand Up @@ -104,11 +88,11 @@ export class VitalInsight extends Insight {

/**
* Get the text description of this vital's Insight
*
*
* @returns {string}
*/
get description() {
if (!this.level) return null;
if (!this.level) return '';

let text = `Recent ${this.vital.name} measurements`;

Expand All @@ -124,9 +108,15 @@ export class VitalInsight extends Insight {
break;
}

if ( (this.level === 'low' && this.trend === 1) || (this.level === 'high' && this.trend === -1) ) {
if (
(this.level === 'low' && this.trend === 1) ||
(this.level === 'high' && this.trend === -1)
) {
text += ', but are';
} else if ( (this.level === 'low' && this.trend === -1) || (this.level === 'high' && this.trend === 1) ) {
} else if (
(this.level === 'low' && this.trend === -1) ||
(this.level === 'high' && this.trend === 1)
) {
text += ' and are';
} else if (this.trend !== 0) {
text += ' and are';
Expand All @@ -146,21 +136,23 @@ export class VitalInsight extends Insight {
}

export class VitalInsightsSummary {
person: Person;

/**
* @param {import("../typedefs").Person} person Person whose vitals are being analyzed
*/
constructor(person) {
this.person = person
constructor(person: Person) {
this.person = person;
}

get #measurements() {
if (!record.value) return [];
return record.value.measurements.filter(measurement => measurement.personId === this.person.id);
}

get #vitals() {
if (!record.value) return [];

let vitalIds = this.#measurements.map(measurement => measurement.vitalId);
vitalIds = [...new Set(vitalIds)];

return record.value.vitals.filter(vital => vitalIds.includes(vital.id));
}

Expand Down Expand Up @@ -195,15 +187,30 @@ export class VitalInsightsSummary {
const vitalLevelsDescriptions = [];

if (this.#normalLevelVitals.length > 0) {
vitalLevelsDescriptions.push(`${n2w.toWords(this.#normalLevelVitals.length)} ${pluralize('vital', this.#normalLevelVitals.length)} ${pluralize('has', this.#normalLevelVitals.length)} normal levels`);
vitalLevelsDescriptions.push(
`${n2w.toWords(this.#normalLevelVitals.length)} ${pluralize(
'vital',
this.#normalLevelVitals.length
)} ${pluralize('has', this.#normalLevelVitals.length)} normal levels`
);
}

if (this.#lowLevelVitals.length > 0) {
vitalLevelsDescriptions.push(`${n2w.toWords(this.#lowLevelVitals.length)} ${pluralize('vital', this.#lowLevelVitals.length)} ${pluralize('has', this.#lowLevelVitals.length)} low levels`);
vitalLevelsDescriptions.push(
`${n2w.toWords(this.#lowLevelVitals.length)} ${pluralize(
'vital',
this.#lowLevelVitals.length
)} ${pluralize('has', this.#lowLevelVitals.length)} low levels`
);
}

if (this.#highLevelVitals.length > 0) {
vitalLevelsDescriptions.push(`${n2w.toWords(this.#highLevelVitals.length)} ${pluralize('vital', this.#highLevelVitals.length)} ${pluralize('has', this.#highLevelVitals.length)} high levels`);
vitalLevelsDescriptions.push(
`${n2w.toWords(this.#highLevelVitals.length)} ${pluralize(
'vital',
this.#highLevelVitals.length
)} ${pluralize('has', this.#highLevelVitals.length)} high levels`
);
}

// Trends
Expand All @@ -213,11 +220,21 @@ export class VitalInsightsSummary {
const vitalTrendsDescriptions = [];

if (this.#upwardTrendVitals.length > 0) {
vitalTrendsDescriptions.push(`${n2w.toWords(this.#upwardTrendVitals.length)} ${pluralize('vital', this.#upwardTrendVitals.length)} ${pluralize('is', this.#upwardTrendVitals.length)} trending upward`);
vitalTrendsDescriptions.push(
`${n2w.toWords(this.#upwardTrendVitals.length)} ${pluralize(
'vital',
this.#upwardTrendVitals.length
)} ${pluralize('is', this.#upwardTrendVitals.length)} trending upward`
);
}

if (this.#downwardTrendVitals.length > 0) {
vitalTrendsDescriptions.push(`${n2w.toWords(this.#downwardTrendVitals.length)} ${pluralize('vital', this.#downwardTrendVitals.length)} ${pluralize('is', this.#downwardTrendVitals.length)} trending downward`);
vitalTrendsDescriptions.push(
`${n2w.toWords(this.#downwardTrendVitals.length)} ${pluralize(
'vital',
this.#downwardTrendVitals.length
)} ${pluralize('is', this.#downwardTrendVitals.length)} trending downward`
);
}

const trendsDescription = vitalTrendsDescriptions.join(', ');
Expand All @@ -230,38 +247,34 @@ export class VitalInsightsSummary {

/**
* Calculate non-parametric regression
*
* @param {Array<Number>} values Values to consider
* @param {Number} bandwidth Bandwidth
* @returns {Number[]}
*/
const nonParametricRegression = (values, bandwidth = 0.5) => {
const nonParametricRegression = (values: number[], bandwidth = 0.5) => {
// Create a kernel function.
const kernelFunction = (x) => Math.exp(-(Math.pow(x, 2)) / (2 * Math.pow(bandwidth, 2)));
const kernelFunction = (x: number) => Math.exp(-Math.pow(x, 2) / (2 * Math.pow(bandwidth, 2)));

// Calculate the weighted average of the data points.
const predictedValues = values.map((yi, i) => {
const weights = values.map((yj, j) => kernelFunction((yi - yj) / bandwidth));
return values.reduce((acc, yj, j) => acc + weights[j] * yj, 0) / weights.reduce((acc, w) => acc + w, 0);
const predictedValues = values.map(yi => {
const weights = values.map(yj => kernelFunction((yi - yj) / bandwidth));
return (
values.reduce((acc, yj, j) => acc + weights[j] * yj, 0) /
weights.reduce((acc, w) => acc + w, 0)
);
});

// Return the predicted values.
return predictedValues;
}
};

/**
* Calculate trend from values
*
* @param {Array<Number>} values Values to consider
* @param {Number} bandwidth Bandwidth
* @returns {1 | 0 | -1}
*/
const nonParametricRegressionTrend = (values, bandwidth = 0.5) => {
const nonParametricRegressionTrend = (values: number[], bandwidth = 0.5) => {
// Calculate the predicted values using the nonParametricRegression function.
const predictedValues = nonParametricRegression(values, bandwidth);

// Calculate the slope of the predicted values.
const slope = (predictedValues[predictedValues.length - 1] - predictedValues[0]) / predictedValues.length;
const slope =
(predictedValues[predictedValues.length - 1] - predictedValues[0]) / predictedValues.length;

// Return 1 if the trend is up, 0 if the trend is flat, and -1 if the trend is down.
if (slope > 0) {
Expand All @@ -271,4 +284,4 @@ const nonParametricRegressionTrend = (values, bandwidth = 0.5) => {
} else {
return 0;
}
}
};
Loading