Skip to content

Commit

Permalink
feat(JAQPOT-184): export csv results (#38)
Browse files Browse the repository at this point in the history
* feat: export csv results

* fix: add bottom margin to dataset result
  • Loading branch information
alarv authored Jul 24, 2024
1 parent 690d32e commit 3d2a403
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 57 deletions.
48 changes: 48 additions & 0 deletions src/app/api/datasets/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DatasetDto, FeatureDto, ModelDto } from '@/app/api.types';
import { generateResultTableRow } from '@/app/util/dataset';

function generateCSVFromData(
independentFeatures: FeatureDto[],
dependentFeatures: FeatureDto[],
dataset: DatasetDto,
) {
const headerRow = [...independentFeatures, ...dependentFeatures]
.map((feature) => {
return feature.name;
})
.join(',');

const resultRows =
dataset.result?.map((result: any, resultIndex: number) => {
return generateResultTableRow(
independentFeatures,
dependentFeatures,
dataset,
resultIndex,
result,
).join(',');
}) ?? [];

return [headerRow, ...resultRows].join('\n');
}

export async function POST(request: Request) {
const { independentFeatures, dependentFeatures, dataset } =
await request.json();
const csv = generateCSVFromData(
independentFeatures,
dependentFeatures,
dataset,
);

const headers = new Headers();
headers.append('Content-Disposition', 'attachment; filename="results.csv"');
headers.append('Content-Type', 'application/csv');

return new Response(csv, {
headers: {
'Content-Disposition': 'attachment; filename="results.csv"',
'Content-Type': 'application/csv',
},
});
}
2 changes: 1 addition & 1 deletion src/app/api/models/[modelId]/predict/sample-csv/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FeatureDto, ModelDto } from '@/app/api.types';
import { FeatureDto } from '@/app/api.types';

function generateCSVFromModel(independentFeatures: FeatureDto[]) {
return [...independentFeatures].map((feature) => feature.name).join(',');
Expand Down
1 change: 0 additions & 1 deletion src/app/api/user/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export async function GET(
}

const searchParams = generatePaginationAndSortingSearchParams(request);
console.log('ohhai', searchParams.toString());

const res = await fetch(
`${process.env.API_URL}/v1/user/models?${searchParams.toString()}`,
Expand Down
88 changes: 57 additions & 31 deletions src/app/dashboard/models/[modelId]/components/DatasetResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import SWRClientFetchError from '@/app/components/SWRClientFetchError';
import { Skeleton } from '@nextui-org/skeleton';
import { Link } from '@nextui-org/link';
import { Chip } from '@nextui-org/chip';
import { getDatasetStatusNode } from '@/app/util/datasets';
import {
getDatasetStatusNode,
generateResultTableRow,
} from '@/app/util/dataset';
import { Button } from '@nextui-org/button';
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid';

interface PredictionResultProps {
datasetId: string;
Expand Down Expand Up @@ -64,6 +69,31 @@ export default function DatasetResults({
}
}, [dataset]);

function downloadResultsCSV(model: ModelDto) {
fetch(`/api/datasets/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
independentFeatures: model.independentFeatures,
dependentFeatures: model.dependentFeatures,
dataset,
}),
})
.then((response) => response.blob())
.then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `result-${dataset!.id}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
})
.catch((error) => console.error('Error:', error));
}

if (error) return <SWRClientFetchError error={error} />;

const isLoaded =
Expand All @@ -82,34 +112,18 @@ export default function DatasetResults({
}

return dataset?.result.map((result: any, resultIndex: number) => {
const independentFeatureCellValues: string[] =
model.independentFeatures.map((feature, independentFeatureIndex) => {
const input = dataset.input[resultIndex] as any;
if (!input || !input[feature.key]) {
return 'N/A';
}
if (feature.featureType === 'CATEGORICAL') {
return (
feature.possibleValues?.find(
(possibleValue) => possibleValue.key === input[feature.key],
)?.value ?? input[feature.key]
);
}
return input[feature.key];
});

const dependentFeatureCellValues = model.dependentFeatures.map(
(feature, index) => {
return result[feature.key];
},
const resultTableData = generateResultTableRow(
model.independentFeatures,
model.dependentFeatures,
dataset,
resultIndex,
result,
);
return (
<TableRow key={resultIndex}>
{[...independentFeatureCellValues, ...dependentFeatureCellValues].map(
(value, index) => (
<TableCell key={index}>{value}</TableCell>
),
)}
{resultTableData.map((value, index) => (
<TableCell key={index}>{value}</TableCell>
))}
</TableRow>
);
});
Expand All @@ -118,7 +132,7 @@ export default function DatasetResults({
const tableRows = generateTableRows();

return (
<div className="mt-5 flex flex-col gap-4">
<div className="mb-20 mt-5 flex flex-col gap-4">
<h2 className="text-2xl font-bold leading-7 sm:truncate sm:text-3xl sm:tracking-tight">
Result
</h2>
Expand All @@ -142,10 +156,22 @@ export default function DatasetResults({
</div>
)}
{isLoaded && dataset?.status === 'SUCCESS' && (
<Table aria-label="Prediction table">
<TableHeader>{tableHeaders}</TableHeader>
<TableBody loadingState={loadingState}>{tableRows}</TableBody>
</Table>
<>
<div>
<Button
color="primary"
startContent={<ArrowDownTrayIcon className="size-6" />}
className="mb-2"
onPress={() => downloadResultsCSV(model)}
>
Export CSV
</Button>
</div>
<Table aria-label="Prediction table" className="mb-6">
<TableHeader>{tableHeaders}</TableHeader>
<TableBody loadingState={loadingState}>{tableRows}</TableBody>
</Table>
</>
)}
</div>
);
Expand Down
9 changes: 9 additions & 0 deletions src/app/dashboard/models/components/ModelsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CustomError } from '@/app/types/CustomError';
import { ApiResponse } from '@/app/util/response';
import { SortDescriptor } from '@react-types/shared/src/collections';
import { convertSortDirection, SORT_DELIMITER } from '@/app/util/sort';
import JaqpotTimeAgo from '@/app/dashboard/models/[modelId]/components/TimeAgo';

const fetcher: Fetcher<ApiResponse<ModelsResponseDto>, string> = async (
url,
Expand Down Expand Up @@ -125,6 +126,9 @@ export default function ModelsTable({ modelsEndpoint }: ModelsTableProps) {
<TableColumn key="visibility" allowsSorting>
Visibility
</TableColumn>
<TableColumn key="createdAt" allowsSorting>
Created at
</TableColumn>
</TableHeader>
<TableBody
items={data?.content ?? []}
Expand All @@ -148,6 +152,11 @@ export default function ModelsTable({ modelsEndpoint }: ModelsTableProps) {
<TableCell>{item.independentFeatures.length}</TableCell>
<TableCell>{item.dependentFeatures.length}</TableCell>
<TableCell>{item.visibility}</TableCell>
<TableCell>
<JaqpotTimeAgo
date={new Date(item.createdAt as unknown as string)}
/>
</TableCell>
</TableRow>
)}
</TableBody>
Expand Down
2 changes: 1 addition & 1 deletion src/app/dashboard/results/components/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { CustomError } from '@/app/types/CustomError';
import { ApiResponse } from '@/app/util/response';
import { Link } from '@nextui-org/link';
import JaqpotTimeAgo from '@/app/dashboard/models/[modelId]/components/TimeAgo';
import { getDatasetStatusNode } from '@/app/util/datasets';
import { getDatasetStatusNode } from '@/app/util/dataset';
import { SortDescriptor } from '@react-types/shared/src/collections';
import { convertSortDirection, SORT_DELIMITER } from '@/app/util/sort';

Expand Down
53 changes: 53 additions & 0 deletions src/app/util/dataset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Chip } from '@nextui-org/chip';
import React from 'react';
import { DatasetDto, FeatureDto, ModelDto } from '@/app/api.types';

export function getDatasetStatusNode(dataset: DatasetDto | null | undefined) {
if (!dataset) {
return <></>;
} else if (dataset?.status === 'SUCCESS') {
return (
<Chip color="success" variant="flat">
Success
</Chip>
);
} else if (dataset?.status === 'FAILURE') {
return (
<Chip color="danger" variant="flat">
Failed
</Chip>
);
} else {
return <Chip color="primary">In progress</Chip>;
}
}

export function generateResultTableRow(
independentFeatures: FeatureDto[],
dependentFeatures: FeatureDto[],
dataset: DatasetDto,
resultIndex: number,
result: any,
): string[] {
const independentFeatureCellValues: string[] = independentFeatures.map(
(feature, independentFeatureIndex) => {
const input = dataset.input[resultIndex] as any;
if (!input || !input[feature.key]) {
return 'N/A';
}
if (feature.featureType === 'CATEGORICAL') {
return (
feature.possibleValues?.find(
(possibleValue) => possibleValue.key === input[feature.key],
)?.value ?? input[feature.key]
);
}
return input[feature.key];
},
);

const dependentFeatureCellValues = dependentFeatures.map((feature, index) => {
return result[feature.key];
});
return [...independentFeatureCellValues, ...dependentFeatureCellValues];
}
23 changes: 0 additions & 23 deletions src/app/util/datasets.tsx

This file was deleted.

0 comments on commit 3d2a403

Please sign in to comment.