Skip to content

Commit 9c98b16

Browse files
Add equivalent variable names feature to search results
- When search matches a variable name, find other variables sharing the same concept_code - Display equivalent names grouped by cohort, with matched names listed last - Hidden behind 'Show equivalent variable names' toggle button - Increase search results font size from text-sm to text-base
1 parent 007ac7c commit 9c98b16

File tree

1 file changed

+120
-1
lines changed

1 file changed

+120
-1
lines changed

frontend/src/pages/cohorts.tsx

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,119 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
163163

164164
SearchResultsDisplay.displayName = 'SearchResultsDisplay';
165165

166+
// Component to show equivalent variable names based on shared concept_codes
167+
const EquivalentVariableNames = React.memo(({cohortsData, searchTerms, searchMode, searchScope}: {
168+
cohortsData: Record<string, Cohort>,
169+
searchTerms: string[],
170+
searchMode: 'or' | 'and' | 'exact',
171+
searchScope: 'cohorts' | 'variables' | 'all'
172+
}) => {
173+
const [expanded, setExpanded] = useState(false);
174+
175+
const equivalentNames = useMemo(() => {
176+
if (searchTerms.length === 0) return null;
177+
// Only relevant when searching variables or all
178+
if (searchScope === 'cohorts') return null;
179+
180+
// Step 1: Find variables whose var_name matches the query and collect their concept_codes
181+
const matchedConceptCodes = new Map<string, Set<string>>(); // concept_code -> set of matched var_names
182+
183+
Object.entries(cohortsData).forEach(([_cohortId, cohortData]) => {
184+
Object.entries(cohortData.variables || {}).forEach(([varName, varData]) => {
185+
const nameMatches = matchesSearchTerms(varName, searchTerms, searchMode);
186+
if (nameMatches && varData.concept_code) {
187+
const code = varData.concept_code.trim();
188+
if (code) {
189+
if (!matchedConceptCodes.has(code)) {
190+
matchedConceptCodes.set(code, new Set());
191+
}
192+
matchedConceptCodes.get(code)!.add(varName);
193+
}
194+
}
195+
});
196+
});
197+
198+
if (matchedConceptCodes.size === 0) return null;
199+
200+
// Step 2: For each concept_code, find ALL variable names across all cohorts, grouped by cohort
201+
const result: {conceptCode: string; conceptName: string | null; namesByCohort: {cohortId: string; names: string[]; isMatched: boolean}[]}[] = [];
202+
203+
matchedConceptCodes.forEach((matchedVarNames, conceptCode) => {
204+
let conceptName: string | null = null;
205+
const cohortEntries: {cohortId: string; names: string[]; isMatched: boolean}[] = [];
206+
207+
Object.entries(cohortsData).forEach(([cohortId, cohortData]) => {
208+
const namesInCohort: string[] = [];
209+
Object.entries(cohortData.variables || {}).forEach(([varName, varData]) => {
210+
if (varData.concept_code && varData.concept_code.trim() === conceptCode) {
211+
namesInCohort.push(varName);
212+
if (!conceptName && varData.concept_name) {
213+
conceptName = varData.concept_name;
214+
}
215+
}
216+
});
217+
if (namesInCohort.length > 0) {
218+
// A cohort is "matched" if any of its names were the ones that triggered the search hit
219+
const hasMatchedName = namesInCohort.some(n => matchedVarNames.has(n));
220+
cohortEntries.push({ cohortId, names: namesInCohort.sort(), isMatched: hasMatchedName });
221+
}
222+
});
223+
224+
// Sort: cohorts with non-matched (equivalent) names first, matched cohorts last
225+
cohortEntries.sort((a, b) => {
226+
if (a.isMatched === b.isMatched) return a.cohortId.localeCompare(b.cohortId);
227+
return a.isMatched ? 1 : -1;
228+
});
229+
230+
// Only show if there's at least one name beyond the matched ones
231+
const hasEquivalent = cohortEntries.some(e =>
232+
e.names.some(n => !matchedVarNames.has(n))
233+
);
234+
if (hasEquivalent) {
235+
result.push({ conceptCode, conceptName, namesByCohort: cohortEntries });
236+
}
237+
});
238+
239+
return result.length > 0 ? result : null;
240+
}, [cohortsData, searchTerms, searchMode, searchScope]);
241+
242+
if (!equivalentNames) return null;
243+
244+
return (
245+
<div className="mt-2">
246+
<button
247+
onClick={() => setExpanded(!expanded)}
248+
className="btn btn-xs btn-outline btn-info"
249+
>
250+
{expanded ? '▾ Hide' : '▸ Show'} equivalent variable names
251+
</button>
252+
{expanded && (
253+
<div className="mt-2 space-y-2">
254+
{equivalentNames.map(({conceptCode, conceptName, namesByCohort}) => (
255+
<div key={conceptCode} className="p-2 bg-base-100 rounded-lg border border-base-300 text-sm">
256+
<div className="font-semibold text-gray-700 dark:text-gray-300 mb-1">
257+
Standard code: <span className="text-primary">{conceptCode}</span>
258+
{conceptName && <span className="ml-1 text-gray-500">({conceptName})</span>}
259+
</div>
260+
<div className="space-y-1">
261+
{namesByCohort.map(({cohortId, names, isMatched}) => (
262+
<div key={cohortId} className={isMatched ? 'text-gray-400 dark:text-gray-500' : ''}>
263+
<span className="font-medium">{cohortId}:</span>{' '}
264+
{names.join(', ')}
265+
{isMatched && <span className="ml-1 text-xs italic">(matched)</span>}
266+
</div>
267+
))}
268+
</div>
269+
</div>
270+
))}
271+
</div>
272+
)}
273+
</div>
274+
);
275+
});
276+
277+
EquivalentVariableNames.displayName = 'EquivalentVariableNames';
278+
166279
// Helper function to format participants value for display in tags
167280
const formatParticipantsForTag = (value: string | number | null | undefined): string => {
168281
if (!value) return '';
@@ -650,7 +763,7 @@ export default function CohortsList() {
650763

651764
{/* Search Results Display */}
652765
{searchInput.trim() && (
653-
<div className="mt-2 p-2 bg-base-200 rounded-lg text-sm">
766+
<div className="mt-2 p-2 bg-base-200 rounded-lg text-base">
654767
<div className="flex items-start gap-3">
655768
<span className="text-gray-600 dark:text-gray-400 mt-0.5">🔍</span>
656769
<div className="flex-1">
@@ -660,6 +773,12 @@ export default function CohortsList() {
660773
searchMode={searchMode}
661774
searchScope={searchScope}
662775
/>
776+
<EquivalentVariableNames
777+
cohortsData={cohortsData as Record<string, Cohort>}
778+
searchTerms={searchTerms}
779+
searchMode={searchMode}
780+
searchScope={searchScope}
781+
/>
663782
</div>
664783
<button
665784
onClick={() => {

0 commit comments

Comments
 (0)