Skip to content

Commit

Permalink
Merge pull request #262 from flatironinstitute/console-output-tab
Browse files Browse the repository at this point in the history
Add console text output tab
  • Loading branch information
WardBrian authored Jan 24, 2025
2 parents e7dedeb + 8fdc796 commit 7232838
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 220 deletions.
17 changes: 17 additions & 0 deletions gui/src/app/SamplerOutputView/ConsoleOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Box from "@mui/material/Box";

import { FunctionComponent } from "react";

type ConsoleOutputProps = {
text: string;
};

const ConsoleOutput: FunctionComponent<ConsoleOutputProps> = ({ text }) => {
return (
<Box className="stdout" color="info.dark">
<pre>{text}</pre>
</Box>
);
};

export default ConsoleOutput;
198 changes: 198 additions & 0 deletions gui/src/app/SamplerOutputView/DrawsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Download } from "@mui/icons-material";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import {
SuccessBorderedTableRow,
SuccessColoredTableHead,
} from "@SpComponents/StyledTables";
import { SamplingOpts } from "@SpCore/ProjectDataModel";
import { triggerDownload } from "@SpUtil/triggerDownload";
import JSZip from "jszip";
import { FunctionComponent, useCallback, useMemo, useState } from "react";

type DrawsViewProps = {
draws: number[][];
paramNames: string[];
drawChainIds: number[];
drawNumbers: number[];
samplingOpts: SamplingOpts; // for including in exported zip
};

const DrawsView: FunctionComponent<DrawsViewProps> = ({
draws,
paramNames,
drawChainIds,
drawNumbers,
samplingOpts,
}) => {
const [abbreviatedToNumRows, setAbbreviatedToNumRows] = useState<
number | undefined
>(300);
const formattedDraws = useMemo(() => {
if (abbreviatedToNumRows === undefined) return draws;
return draws.map((draw) =>
formatDraws(draw.slice(0, abbreviatedToNumRows)),
);
}, [draws, abbreviatedToNumRows]);
const handleExportToCsv = useCallback(() => {
const csvText = prepareCsvText(
draws,
paramNames,
drawChainIds,
drawNumbers,
);
downloadTextFile(csvText, "draws.csv");
}, [draws, paramNames, drawChainIds, drawNumbers]);
const handleExportToMultipleCsvs = useCallback(async () => {
const uniqueChainIds = Array.from(new Set(drawChainIds));
const csvTexts = prepareMultipleCsvsText(
draws,
paramNames,
drawChainIds,
uniqueChainIds,
);
const blob = await createZipBlobForMultipleCsvs(
csvTexts,
uniqueChainIds,
samplingOpts,
);
const fileName = "SP-draws.zip";
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}, [draws, paramNames, drawChainIds, samplingOpts]);
return (
<>
<div>
<IconButton size="small" title="Download" onClick={handleExportToCsv}>
<Download fontSize="inherit" />
&nbsp;Export to single .csv
</IconButton>
&nbsp;
<IconButton
size="small"
title="Download"
onClick={handleExportToMultipleCsvs}
>
<Download fontSize="inherit" />
&nbsp;Export to multiple .csv
</IconButton>
</div>
<br />
<TableContainer>
<Table size="small" padding="none">
<SuccessColoredTableHead>
<SuccessBorderedTableRow>
<TableCell key="chain">Chain</TableCell>
<TableCell key="draw">Draw</TableCell>
{paramNames.map((name, i) => (
<TableCell key={i}>{name}</TableCell>
))}
</SuccessBorderedTableRow>
</SuccessColoredTableHead>
<TableBody>
{formattedDraws[0].map((_, i) => (
<SuccessBorderedTableRow key={i} hover>
<TableCell>{drawChainIds[i]}</TableCell>
<TableCell>{drawNumbers[i]}</TableCell>
{formattedDraws.map((draw, j) => (
<TableCell key={j}>{draw[i]}</TableCell>
))}
</SuccessBorderedTableRow>
))}
</TableBody>
</Table>
{abbreviatedToNumRows !== undefined &&
abbreviatedToNumRows < draws[0].length && (
<div className="DrawAbbreviationToggle">
<Button
onClick={() => {
setAbbreviatedToNumRows((x) => (x || 0) + 300);
}}
>
Show more
</Button>
</div>
)}
</TableContainer>
</>
);
};

const formatDraws = (draws: number[]) => {
if (draws.every((x) => Number.isInteger(x))) return draws;
return draws.map((x) => x.toPrecision(6));
};

const prepareCsvText = (
draws: number[][],
paramNames: string[],
drawChainIds: number[],
drawNumbers: number[],
) => {
// draws: Each element of draws is a column corresponding to a parameter, across all chains
// paramNames: The paramNames array contains the names of the parameters in the same order that they appear in the draws array
// drawChainIds: The drawChainIds array contains the chain id for each row in the draws array
// uniqueChainIds: The uniqueChainIds array contains the unique chain ids
const lines = draws[0].map((_, i) => {
return [
`${drawChainIds[i]}`,
`${drawNumbers[i]}`,
...paramNames.map((_, j) => draws[j][i]),
].join(",");
});
return [["Chain", "Draw", ...paramNames].join(","), ...lines].join("\n");
};

const prepareMultipleCsvsText = (
draws: number[][],
paramNames: string[],
drawChainIds: number[],
uniqueChainIds: number[],
) => {
// See the comments in prepareCsvText for the meaning of the arguments.
// Whereas prepareCsvText returns a CSV that represents a long-form table,
// this function returns multiple CSVs, one for each chain.
return uniqueChainIds.map((chainId) => {
const drawIndicesForChain = drawChainIds
.map((id, i) => (id === chainId ? i : -1))
.filter((i) => i >= 0);
const lines = drawIndicesForChain.map((i) => {
return paramNames.map((_, j) => draws[j][i]).join(",");
});

return [paramNames.join(","), ...lines].join("\n");
});
};

const createZipBlobForMultipleCsvs = async (
csvTexts: string[],
uniqueChainIds: number[],
samplingOpts: SamplingOpts,
) => {
const zip = new JSZip();
// put them all in a folder called 'draws'
const folder = zip.folder("draws");
if (!folder) throw new Error("Failed to create folder");
csvTexts.forEach((text, i) => {
folder.file(`chain_${uniqueChainIds[i]}.csv`, text);
});
const samplingOptsText = JSON.stringify(samplingOpts, null, 2);
folder.file("sampling_opts.json", samplingOptsText);
const blob = await zip.generateAsync({ type: "blob" });
return blob;
};

const downloadTextFile = (text: string, filename: string) => {
const blob = new Blob([text], { type: "text/plain" });
triggerDownload(blob, filename, () => {});
};

export default DrawsView;
Loading

0 comments on commit 7232838

Please sign in to comment.