Skip to content

Commit

Permalink
feature: UI for loading CSVs (CSV pt. 3) (#213)
Browse files Browse the repository at this point in the history
* feat: Simple CSV parsing, reading of dataset values from CSV

* refactor: Distinguish between numeric and discrete features, implemented more functions (greyscreen)

* refactor: Store feature data, map categories to indices

* fix: Fix crash caused by values being overwritten with NaN

* fix: Initial channel settings, volume and thumbnail URLs

* fix: Index cells by row index, not by cell Id label name

* refactor: Store feature defs and data together in the same internal map

* fix: Fixed bug where counts would not appear for categories

* feat: Added artificial row feature when no discrete feature definition is given

* refactor: Moved imageDataset into metadata state

* feat: Added action to replace image dataset

* feat: Added button to upload CSV data

* feat: Use categorical color palette

* refactor: Code cleanup

* refactor: Code cleanup

* feat: Add compatibility with BFF CSV data

* refactor: CSV Dataset cleanup

* feat: Added CSV parser as a mock image dataset

* feat: Added unit tests, handled NaN values

* doc: Added additional TODOs for CSV unit tests

* refactor: Renamed constants

* refactor: Renamed methods

* feat: Check for BFF keys

* refactor: Simplified default groupby feature behavior

* fix: Added handling for spaces in headers and values

* feat: Added additional unit tests for data validation

* feat: Added unit tests for BFF parsing validation

* refactor: Code cleanup

* feat: Unit tests, updates to csv dataset loader

* fix: Handled empty string in cell ID

* refactor: Renamed components, actions, constants

* refactor: Moved image dataset to its own state branch

* refactor: Changed CSV load action to be a logic

* fix: Linting

* refactor: Code cleanup

* doc: Added comment on webpack config

* refactor: Renamed methods for clarity

* refactor: Added remapping helper function

* refactor: Moved types, const assignment, docstring updates

* refactor: Hide CsvInput area
  • Loading branch information
ShrimpCryptid authored Dec 2, 2024
1 parent b126f16 commit 22e316d
Show file tree
Hide file tree
Showing 27 changed files with 275 additions and 57 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions src/components/LandingPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Col, Descriptions, Layout, Row } from "antd";
import React from "react";
import { Row, Col, Layout, Descriptions } from "antd";
const { Content } = Layout;

import MegasetCard from "../../components/MegasetCard";
// import CsvInput from "../../containers/CsvInput";
import { Megaset } from "../../state/image-dataset/types";
import downloadData, { DownloadInfo } from "./download-data";

Expand Down Expand Up @@ -44,6 +45,10 @@ const LandingPage = ({ handleSelectDataset, megasets }: LandingPageProps) => (
))}
</Row>
</div>

{/* <div className={styles.sectionContent}>
<CsvInput />
</div> */}
</Row>
<Row className={styles.lightSection}>
<Col className={styles.sectionContent}>
Expand Down Expand Up @@ -103,10 +108,13 @@ const LandingPage = ({ handleSelectDataset, megasets }: LandingPageProps) => (
>
<Descriptions.Item label="RELEASE DATE">DATASET</Descriptions.Item>

{downloadData.map((downloadInfo: DownloadInfo) => {
{downloadData.map((downloadInfo: DownloadInfo, index) => {
return (
<>
<Descriptions.Item label={downloadInfo.date}>
<Descriptions.Item
label={downloadInfo.date}
key={index}
>
{" "}
<a
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/components/PopoverCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface PopoverCardProps {
src?: string;
}

const PopoverCard: React.SFC<PopoverCardProps> = (props) => {
const PopoverCard: React.FC<PopoverCardProps> = (props) => {
return (
<Card
className={styles.container}
Expand Down
5 changes: 4 additions & 1 deletion src/containers/Cfe/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ export const getPropsForVolumeViewer = createSelector(
convertFullFieldIdToDownloadId(fileInfo ? fileInfo.FOVId : "")
);
}
if (!dataRoot.endsWith("/")) {

// Discard data root if it's an empty string so it doesn't add a "/" to
// the front of all HTTP(S) URLs. (ex: "/http://example.com" is invalid)
if (dataRoot !== "" && !dataRoot.endsWith("/")) {
dataRoot = dataRoot + "/";
}
return {
Expand Down
4 changes: 1 addition & 3 deletions src/containers/Cfe/test/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ const fileInfo: FileInfo[] = [
];

const stateWithSelections: State = {
metadata: {
...mockState.metadata,
},
...mockState,
selection: {
...mockState.selection,
cellSelectedFor3D: "1",
Expand Down
45 changes: 45 additions & 0 deletions src/containers/CsvInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PlusOutlined } from "@ant-design/icons";
import { Flex } from "antd";
import { RcFile } from "antd/es/upload";
import Dragger from "antd/es/upload/Dragger";
import React, { ReactElement } from "react";
import { ActionCreator, connect } from "react-redux";

import { State } from "../../state";
import imageDatasetStateBranch from "../../state/image-dataset";
import { LoadCsvDatasetAction } from "../../state/image-dataset/types";

type DispatchProps = {
loadCsvDataset: ActionCreator<LoadCsvDatasetAction>;
};

type CsvInputProps = DispatchProps;

/**
* An input area for CSV files. When CSV data is provided, replaces the current image dataset
* with a new `CsvRequest` image dataset and triggers the loading of the CSV data.
*/
function CsvInput(props: CsvInputProps): ReactElement {
const action = async (file: RcFile): Promise<string> => {
const fileText = await file.text();
props.loadCsvDataset(fileText);
return Promise.resolve("");
};

return (
<Dragger action={action} accept=".csv" multiple={false} style={{ width: "50vw" }}>
<Flex vertical={true} align={"center"}>
<p style={{ fontSize: "30px" }}>
<PlusOutlined />
</p>
<p className="ant-upload-text">Click or drag a CSV file here to load.</p>
</Flex>
</Dragger>
);
}

const dispatchToPropsMap: DispatchProps = {
loadCsvDataset: imageDatasetStateBranch.actions.loadCsvDataset,
};

export default connect<any, DispatchProps, any, State>(null, dispatchToPropsMap)(CsvInput);
7 changes: 6 additions & 1 deletion src/containers/MainPlotContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getYTickConversion,
} from "./selectors";
import { getFeatureDefTooltip } from "../../state/selection/selectors";
import { formatThumbnailSrc } from "../../state/util";

import styles from "./style.css";

Expand Down Expand Up @@ -173,13 +174,17 @@ class MainPlotContainer extends React.Component<MainPlotContainerProps> {

public renderPopover() {
const { hoveredPointData, galleryCollapsed, thumbnailRoot } = this.props;
const thumbnailSrc = formatThumbnailSrc(
thumbnailRoot,
hoveredPointData?.thumbnailPath || ""
);
return (
hoveredPointData &&
galleryCollapsed && (
<PopoverCard
title={hoveredPointData[GROUP_BY_KEY] || ""}
description={hoveredPointData[CELL_ID_KEY].toString()}
src={`${thumbnailRoot}/${hoveredPointData.thumbnailPath}`}
src={thumbnailSrc}
/>
)
);
Expand Down
13 changes: 8 additions & 5 deletions src/containers/ThumbnailGallery/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ export const getThumbnails = createSelector(
const cellID = fileInfoForCell[CELL_ID_KEY];
let cellIndex = -1;
if (fileInfoForCell.index !== undefined && fileInfoForCell.index > 0) {
cellIndex = fileInfoForCell.index
cellIndex = fileInfoForCell.index;
} else {
cellIndex = perCellPlotData.labels[ARRAY_OF_CELL_IDS_KEY].indexOf(cellID);
}
}
if (cellIndex < 0) {
return {} as Thumbnail;
}
Expand Down Expand Up @@ -120,9 +120,12 @@ export const getThumbnails = createSelector(
);
}

const thumbnailSrc = formatThumbnailSrc(thumbnailRoot, fileInfoForCell);
const lookupKey = groupByValues[cellIndex];
const category = getCategoryString(groupByFeatureDef, lookupKey ? lookupKey.toString() : "");
const thumbnailSrc = formatThumbnailSrc(thumbnailRoot, fileInfoForCell.thumbnailPath);
const lookupKey = groupByValues[cellIndex];
const category = getCategoryString(
groupByFeatureDef,
lookupKey ? lookupKey.toString() : ""
);
return {
cellID,
downloadHref,
Expand Down
7 changes: 3 additions & 4 deletions src/state/configure-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { merge } from "lodash";
import { applyMiddleware, combineReducers, createStore } from "redux";
import { createLogicMiddleware } from "redux-logic";

import { enableBatching, initialState, metadata, selection, State } from "./";
import RequestClassToUse from "./image-dataset";
import { enableBatching, initialState, imageDataset, metadata, selection, State } from "./";

const reducers = {
metadata: metadata.reducer,
selection: selection.reducer,
imageDataset: imageDataset.reducer,
};

const logics = [...metadata.logics, ...selection.logics];
const logics = [...metadata.logics, ...selection.logics, ...imageDataset.logics];

const reduxLogicDependencies = {
httpClient: axios,
imageDataSet: RequestClassToUse(),
};

export default function createReduxStore(preloadedState?: Partial<State>) {
Expand Down
14 changes: 14 additions & 0 deletions src/state/image-dataset/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeImageDatasetTypeAction, LoadCsvDatasetAction } from "./types";
import { CHANGE_IMAGE_DATASET_TYPE, LOAD_CSV_DATASET } from "./constants";
import { ImageDataset } from "./types";

export function changeImageDatasetType(payload: ImageDataset): ChangeImageDatasetTypeAction {
return { payload, type: CHANGE_IMAGE_DATASET_TYPE };
}

export function loadCsvDataset(payload: string): LoadCsvDatasetAction {
return {
payload,
type: LOAD_CSV_DATASET,
};
}
7 changes: 7 additions & 0 deletions src/state/image-dataset/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makeConstant } from "../util";

const makeImageDatasetConstant = (constant: string) => makeConstant("image-dataset", constant);

export const LOAD_CSV_DATASET = makeImageDatasetConstant("load-csv-dataset");

export const CHANGE_IMAGE_DATASET_TYPE = makeImageDatasetConstant("change-image-dataset-type");
20 changes: 12 additions & 8 deletions src/state/image-dataset/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import FirebaseRequest from "./firebase";
import JsonRequest from "./json-dataset";
import { ImageDataset } from "./types";
import * as actions from "./actions";
import logics from "./logics";
import reducer from "./reducer";
import * as selectors from "./selectors";
import * as types from "./types";

// by default will use Firebase for dataset, can be switched to JSON dataset using ENV
// variable
export default function RequestClassToUse(): ImageDataset {
return process.env.USE_JSON_DATASET ? new JsonRequest() : new FirebaseRequest();
}
export default {
actions,
logics,
reducer,
selectors,
types,
};
28 changes: 28 additions & 0 deletions src/state/image-dataset/logics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createLogic } from "redux-logic";

import { ReduxLogicDeps } from "..";
import { receiveAvailableDatasets } from "../metadata/actions";
import { changeDataset } from "../selection/actions";
import { changeImageDatasetType } from "./actions";
import { LOAD_CSV_DATASET } from "./constants";
import CsvRequest, { DEFAULT_CSV_DATASET_KEY } from "./csv-dataset";

/**
* Parses a CSV file and opens it as a new image dataset.
*/
const loadCsvDataset = createLogic({
type: LOAD_CSV_DATASET,
async process(deps: ReduxLogicDeps, dispatch: any, done: any) {
const { action } = deps;
const fileContents = action.payload as string;
const dataset = new CsvRequest(fileContents);
dispatch(changeImageDatasetType(dataset));

const megasets = await dataset.getAvailableDatasets();
dispatch(receiveAvailableDatasets(megasets));
dispatch(changeDataset(DEFAULT_CSV_DATASET_KEY));
done();
},
});

export default [loadCsvDataset];
32 changes: 32 additions & 0 deletions src/state/image-dataset/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TypeToDescriptionMap } from "..";
import { AnyAction } from "redux";
import { CHANGE_IMAGE_DATASET_TYPE } from "./constants";
import { ImageDataset, ImageDatasetStateBranch, ReceiveImageDatasetAction } from "./types";
import { makeReducer } from "../util";
import FirebaseRequest from "./firebase";
import JsonRequest from "./json-dataset";

// by default will use Firebase for dataset, can be switched to JSON dataset using ENV
// variable
function getImageDatasetInstance(): ImageDataset {
return process.env.USE_JSON_DATASET ? new JsonRequest() : new FirebaseRequest();
}

export const initialState: ImageDatasetStateBranch = {
imageDataset: getImageDatasetInstance(),
};

const actionToConfigMap: TypeToDescriptionMap = {
[CHANGE_IMAGE_DATASET_TYPE]: {
accepts: (action: AnyAction): action is ReceiveImageDatasetAction =>
action.type === CHANGE_IMAGE_DATASET_TYPE,
perform: (state: ImageDatasetStateBranch, action: ReceiveImageDatasetAction) => {
return {
...state,
imageDataset: action.payload,
};
},
},
};

export default makeReducer<ImageDatasetStateBranch>(actionToConfigMap, initialState);
4 changes: 4 additions & 0 deletions src/state/image-dataset/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { State } from "..";
import { ImageDataset } from "./types";

export const getImageDataset = (state: State): ImageDataset => state.imageDataset.imageDataset;
21 changes: 21 additions & 0 deletions src/state/image-dataset/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import { ViewerChannelSettings } from "@aics/web-3d-viewer/type-declarations";
import { DataForPlot, FileInfo, MeasuredFeatureDef } from "../metadata/types";
import { Album } from "../types";

export interface ImageDatasetStateBranch {
imageDataset: ImageDataset;
}

export interface ReceiveImageDatasetAction {
payload: ImageDataset;
type: string;
}

export interface LoadCsvDatasetAction {
payload: string; // CSV contents
type: string;
}

export interface InitialDatasetSelections {
defaultXAxis: string;
defaultYAxis: string;
Expand Down Expand Up @@ -65,3 +79,10 @@ export interface ImageDataset {
getFileInfoByCellId(id: string): Promise<FileInfo | undefined>;
getFileInfoByArrayOfCellIds(ids: string[]): Promise<(FileInfo | undefined)[]>;
}

// ACTIONS

export interface ChangeImageDatasetTypeAction {
payload: ImageDataset;
type: string;
}
3 changes: 3 additions & 0 deletions src/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { initialState as imageDatasetInitialState } from "./image-dataset/reducer";
import { initialState as metadataInitialState } from "./metadata/reducer";
import { initialState as selectionInitialState } from "./selection/reducer";
import { State } from "./types";

export { default as imageDataset } from "./image-dataset";
export { default as metadata } from "./metadata";
export { default as selection } from "./selection";

Expand All @@ -14,4 +16,5 @@ export * from "./types";
export const initialState: State = Object.freeze({
metadata: metadataInitialState,
selection: selectionInitialState,
imageDataset: imageDatasetInitialState,
});
5 changes: 2 additions & 3 deletions src/state/metadata/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import {
export function requestAvailableDatasets() {
return { type: REQUEST_AVAILABLE_DATASETS };
}
export function receiveAvailableDatasets(
payload: Megaset[]
): ReceiveAvailableDatasetsAction {

export function receiveAvailableDatasets(payload: Megaset[]): ReceiveAvailableDatasetsAction {
return {
payload,
type: RECEIVE_AVAILABLE_DATASETS,
Expand Down
Loading

0 comments on commit 22e316d

Please sign in to comment.