omnidiag / frontend /src /utils /featureCategorizer.js
yahyoha's picture
feat(utils): add schema parsing, Zod validation, and feature categorization
9e5ff04
Raw
History Blame Contribute Delete
5.22 kB
/**
* featureCategorizer.js
* =====================
* Categorizes dynamically fetched fields into logical groups (Vitals,
* Lifestyle, Demographics, Medical History, etc.) for visual hierarchy.
*
* Uses keyword matching on field names and descriptions. Categories
* are returned as an ordered Map so they render in a consistent layout.
*
* Category definitions:
* β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
* β”‚ Category β”‚ Matched Keywords β”‚
* β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
* β”‚ Vitals & Signs β”‚ Age, BP, HR, BMI, Cholesterol, Chest, ST, β”‚
* β”‚ β”‚ Oldpeak, MaxHR, Pulse, Temp, Resp, O2 β”‚
* β”‚ Lifestyle β”‚ Smoke, PhysActivity, Fruits, Veggies, Alc, β”‚
* β”‚ β”‚ Exercise, Diet, Activity β”‚
* β”‚ Demographics β”‚ Sex, Age, Education, Income, Race, Region β”‚
* β”‚ Medical History β”‚ HighBP, HighChol, Stroke, Heart, Diabetes, β”‚
* β”‚ β”‚ RestingECG, FastingBS, CholCheck, DiffWalk β”‚
* β”‚ Healthcare β”‚ AnyHealthcare, NoDocbcCost, Insurance β”‚
* β”‚ Mental Health β”‚ MentHlth, PhysHlth, GenHlth, DiffWalk β”‚
* β”‚ General β”‚ (fallback for uncategorized fields) β”‚
* β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
*/
/**
* @typedef {import('./schemaFieldParser').FieldMetadata} FieldMetadata
*/
// ── Category definitions ──────────────────────────────────────────────
const CATEGORIES = {
'Vitals & Signs': {
icon: 'Activity',
keywords: [
'age', 'restingbp', 'maxhr', 'cholesterol', 'oldpeak',
'chestpain', 'st_slope', 'fastingbs', 'bmi', 'heartrate',
'pulse', 'temperature', 'respiratory', 'o2', 'bp', 'blood pressure',
],
},
Lifestyle: {
icon: 'Heart',
keywords: [
'smoker', 'physactivity', 'fruits', 'veggies',
'hvyalcoholconsump', 'exercise', 'diet', 'activity',
'alcohol', 'smoking',
],
},
Demographics: {
icon: 'User',
keywords: [
'sex', 'education', 'income', 'race', 'region',
'geography', 'marital',
],
},
'Medical History': {
icon: 'Stethoscope',
keywords: [
'highbp', 'highchol', 'stroke', 'heartdiseaseorattack',
'restingecg', 'cholcheck', 'diffwalk', 'diabetes',
'anyhealthcare', 'nodocbccost', 'kidney', 'cancer',
'condition', 'diagnosis',
],
},
'Healthcare Access': {
icon: 'Pill',
keywords: [
'anyhealthcare', 'nodocbccost', 'insurance', 'doctor',
'checkup', 'medication',
],
},
'Mental Health': {
icon: 'Wind',
keywords: [
'menthlth', 'physhlth', 'genhlth', 'depression',
'anxiety', 'mental', 'wellbeing',
],
},
};
const CATEGORY_ORDER = [
'Vitals & Signs',
'Medical History',
'Lifestyle',
'Demographics',
'Mental Health',
'Healthcare Access',
'General',
];
// ── Categorization logic ──────────────────────────────────────────────
/**
* Determine the category for a single field based on its name and description.
*/
function categorizeField(field) {
const searchText = `${field.name} ${field.description}`.toLowerCase();
for (const [category, def] of Object.entries(CATEGORIES)) {
for (const keyword of def.keywords) {
// Match whole field names (case-insensitive) or partial in description
if (
field.name.toLowerCase() === keyword ||
searchText.includes(keyword)
) {
return category;
}
}
}
return 'General';
}
/**
* Categorize an array of FieldMetadata into an ordered Map.
*
* @param {FieldMetadata[]} fields - Parsed field metadata
* @returns {Map<string, FieldMetadata[]>} Ordered map: category β†’ fields[]
*/
export function categorizeFields(fields) {
const grouped = new Map();
// Initialize all categories in order
for (const cat of CATEGORY_ORDER) {
grouped.set(cat, []);
}
// Assign each field to a category
for (const field of fields) {
const category = categorizeField(field);
field.category = category;
if (!grouped.has(category)) {
grouped.set(category, []);
}
grouped.get(category).push(field);
}
// Remove empty categories (preserving order)
for (const [cat, fieldsInCat] of grouped) {
if (fieldsInCat.length === 0) {
grouped.delete(cat);
}
}
return grouped;
}
/**
* Get the icon name for a category.
*/
export function getCategoryIcon(category) {
return CATEGORIES[category]?.icon || 'ClipboardList';
}