Skip to content

Commit

Permalink
compound/entity tab with grid
Browse files Browse the repository at this point in the history
  • Loading branch information
kristinlindquist committed Dec 18, 2023
1 parent f5d63f6 commit 3d9b8db
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 6 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@mui/icons-material": "^5.14.18",
"@mui/joy": "^5.0.0-beta.15",
"@mui/material": "^5.14.18",
"@mui/x-charts": "^6.18.3",
"@mui/x-data-grid": "^6.18.1",
"@mui/x-data-grid-pro": "^6.18.1",
"@mui/x-license-pro": "^6.10.2",
Expand Down
26 changes: 26 additions & 0 deletions src/app/core/patents/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cache } from 'react';
import { z } from 'zod';

import {
ENTITY_SEARCH_API_URL,
PATENT_AUTOCOMPLETE_API_URL,
PATENT_SEARCH_API_URL,
TERM_DESCRIPTION_API_URL,
Expand All @@ -22,6 +23,11 @@ import {
TrialResponseSchema,
TrialSearchArgs,
} from '@/types/trials';
import {
EntityResponse,
EntityResponseSchema,
EntitySearchArgs,
} from '@/types/entities';

const AutocompleteResponse = z.array(
z.object({
Expand Down Expand Up @@ -69,6 +75,26 @@ export const fetchDescription = cache(
}
);

/**
* Fetch entities from the API. Cached.
* @param args
* @returns patents promise
*/
export const fetchEntities = cache(
async (args: EntitySearchArgs): Promise<EntityResponse> => {
if (args.terms?.length === 0) {
return [];
}
const queryArgs = getQueryArgs(args, true);
const res = await doFetch(
`${ENTITY_SEARCH_API_URL}?${queryArgs}`,
EntityResponseSchema
);

return res;
}
);

/**
* Fetch patents from the API. Cached.
* @param args
Expand Down
98 changes: 98 additions & 0 deletions src/app/core/patents/compound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use server';

import Box from '@mui/joy/Box';
import { GridColDef } from '@mui/x-data-grid/models/colDef';
import Alert from '@mui/joy/Alert';
import Typography from '@mui/joy/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import 'server-only';

import {
DataGrid,
renderChip,
renderCompoundCountChip,
renderSparkline,
} from '@/components/data/grid';
import { EntitySearchArgs } from '@/types/entities';

import { fetchEntities } from './actions';

const getCompoundColumns = (): GridColDef[] => [
{
field: 'name',
headerName: 'Entity',
width: 250,
},
{
field: 'trial_count',
headerName: 'Trials',
width: 125,
renderCell: renderCompoundCountChip,
},
{
field: 'patent_count',
headerName: 'Patents',
width: 125,
renderCell: renderCompoundCountChip,
},
{
field: 'activity',
headerName: 'Activity',
width: 125,
renderCell: renderSparkline,
},
{
field: 'last_priority_year',
headerName: 'Latest Priority Date',
width: 125,
},
{
field: 'max_phase',
headerName: 'Max Phase',
width: 125,
renderCell: renderChip,
},
{
field: 'last_status',
headerName: 'Last Status',
width: 125,
renderCell: renderChip,
},
{
field: 'last_updated',
headerName: 'Last Update',
width: 125,
},
];

export const CompoundList = async (args: EntitySearchArgs) => {
const columns = getCompoundColumns();
try {
const entities = await fetchEntities(args);
return (
<Box height="100vh">
<DataGrid
columns={columns}
// detailComponent={PatentDetail<Entity>}
rows={entities.map((entity) => ({
...entity,
id: entity.name,
}))}
/>
</Box>
);
} catch (e) {
return (
<Alert
startDecorator={<WarningIcon />}
variant="soft"
color="warning"
>
<Typography level="h4">Failed to fetch patents</Typography>
<Typography>
{e instanceof Error ? e.message : JSON.stringify(e)}
</Typography>
</Alert>
);
}
};
9 changes: 9 additions & 0 deletions src/app/core/patents/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Tabs } from '@/components/layout/tabs';
import { PatentSearchArgs } from '@/types/patents';

import { getStyles } from './client';
import { CompoundList } from './compound';
import { PatentList } from './patent';
import { PatentGraph } from './graph';
import { OverTime } from './over-time';
Expand All @@ -20,6 +21,14 @@ import { TrialList } from './trials';
export const Content = (args: PatentSearchArgs) => {
try {
const tabs = [
{
label: 'Compounds',
panel: (
<Suspense fallback={<Skeleton />}>
<CompoundList {...args} />
</Suspense>
),
},
{
label: 'Patents',
panel: (
Expand Down
50 changes: 44 additions & 6 deletions src/components/data/grid/formatters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@mui/x-data-grid/models/params/gridCellParams';
import unescape from 'lodash/fp/unescape';
import { format } from 'date-fns';
import { SparkLineChart } from '@mui/x-charts/SparkLineChart';

import { Chip, ChipProps, formatChips } from '@/components/data/chip';
import { formatLabel, formatPercent, title } from '@/utils/string';
Expand Down Expand Up @@ -152,18 +153,55 @@ export const renderList = (
* Render string array as chips
*/
export const getRenderChip =
(color: ChipProps['color']) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(params: GridRenderCellParams<any, string>): ReactNode => {
const { value } = params;
if (typeof value !== 'string') {
<T extends Record<string, unknown>>(
color: ChipProps['color'],
getUrl: (row: T) => string | undefined = () => undefined
) =>
(params: GridRenderCellParams<T, string | number>): ReactNode => {
const { value, row } = params;
if (typeof value !== 'string' && typeof value !== 'number') {
return <>{JSON.stringify(value)}</>;
}
return <Chip color={color}>{formatLabel(value || '')}</Chip>;

const href = getUrl(row);

if (!value) {
return <span />;
}

return (
<Chip color={color} href={href}>
{formatLabel(value || '')}
</Chip>
);
};

export const renderPrimaryChip = getRenderChip('primary');
export const renderChip = getRenderChip('neutral');
export const renderCompoundCountChip = getRenderChip(
'primary',
(row: { name: string }) => `/core/patents?terms=${row.name}`
);

export const getRenderSparkline =
<T extends Record<string, unknown>>() =>
(params: GridRenderCellParams<T, number[]>): ReactNode => {
const { value } = params;
if (!value) {
return <span />;
}
return (
<SparkLineChart
plotType="line"
colors={['blue']}
data={value}
height={50}
margin={{ top: 10, right: 0, bottom: 10, left: 0 }}
/>
);
};

export const renderSparkline = getRenderSparkline();

/**
* Format label
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const API_URL = 'http://localhost:3001/dev'; // 'https://api.biosymbolics.ai';
export const API_KEY = 'Ahu8ef3VoNzBqn'; // /biosymbolics/pipeline/api/free-key

export const ENTITY_SEARCH_API_URL = `${API_URL}/entities/search`;
export const PATENT_SEARCH_API_URL = `${API_URL}/patents/search`;
export const PATENT_GRAPH_API_URL = `${API_URL}/patents/reports/graph`;
export const PATENT_SUMMARY_API_URL = `${API_URL}/patents/reports/summarize`;
Expand Down
29 changes: 29 additions & 0 deletions src/types/entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { z } from 'zod';

import { PatentSchema } from './patents';
import { TrialSchema } from './trials';

export const EntitySchema = z.object({
activity: z.array(z.number()),
last_status: z.union([z.string(), z.null()]),
last_updated: z.union([z.string(), z.null()]),
name: z.string(),
max_phase: z.union([z.string(), z.null()]),
last_priority_year: z.union([z.number(), z.null()]),
patents: z.array(PatentSchema),
patent_count: z.number(),
record_count: z.number(),
trials: z.array(TrialSchema),
trial_count: z.number(),
});

export const EntityResponseSchema = z.array(EntitySchema);

export type Entity = z.infer<typeof EntitySchema>;
export type EntityResponse = z.infer<typeof EntityResponseSchema>;

export type EntitySearchArgs = {
queryType: string | null;
terms: string[] | null;
};
Loading

0 comments on commit 3d9b8db

Please sign in to comment.