Skip to content

Commit

Permalink
Add method to convert the file explorer url to pandas
Browse files Browse the repository at this point in the history
  • Loading branch information
aswallace committed May 13, 2024
1 parent 90c8336 commit 4032c48
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,39 @@
.code {
cursor: text;
user-select: text;
}
}

.button-menu, .button-menu button {
background-color: var(--secondary-background-color);
color: var(--secondary-text-color);
}

.button-menu i, .button-menu li > div {
color: var(--secondary-text-color);
}

.button-menu :is(a, button):hover, .button-menu button:hover i {
background-color: var(--highlight-background-color);
color: var(--highlight-text-color) !important;
}

.code-actions {
max-width: 100%;
padding: 0 var(--padding) 0 var(--padding);

/* flex parent */
display: flex;
}

.action-button {
background-color: var(--primary-background-color);
border: none;
border-radius: 0;
color: var(--primary-text-color);
/* height: 30px;*/
width: 150px;
}

.action-button i, .action-button:hover i {
color: var(--primary-text-color) !important;
}
30 changes: 29 additions & 1 deletion packages/core/components/Modal/CodeSnippet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IconButton, TooltipHost } from "@fluentui/react";
import { ActionButton, IContextualMenuItem, IconButton, TooltipHost } from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
import { useSelector } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
Expand All @@ -18,9 +19,19 @@ export default function CodeSnippet({ onDismiss }: ModalProps) {
const pythonSnippet = useSelector(interaction.selectors.getPythonSnippet);
const code = pythonSnippet?.code;
const setup = pythonSnippet?.setup;
const languageOptions: IContextualMenuItem[] = [
{
key: "python",
text: "Python (pandas)",
onClick() {
setLanguage("Python (pandas)");
},
},
];

const [isSetupCopied, setSetupCopied] = React.useState(false);
const [isCodeCopied, setCodeCopied] = React.useState(false);
const [language, setLanguage] = React.useState(languageOptions[0].text);

const onCopySetup = () => {
setup && navigator.clipboard.writeText(setup);
Expand All @@ -44,6 +55,19 @@ export default function CodeSnippet({ onDismiss }: ModalProps) {

const body = (
<>
<div className={styles.header}>
<h4>Language</h4>
<div className={styles.codeActions}>
<ActionButton
className={classNames(styles.actionButton, styles.copyButton)}
menuProps={{
className: styles.buttonMenu,
items: languageOptions,
}}
text={language}
/>
</div>
</div>
<div className={styles.header}>
<h4>Setup</h4>
<TooltipHost content={isSetupCopied ? "Copied to clipboard!" : undefined}>
Expand Down Expand Up @@ -75,6 +99,10 @@ export default function CodeSnippet({ onDismiss }: ModalProps) {
</div>
<SyntaxHighlighter
className={styles.code}
lineProps={{ style: { wordBreak: "break-all", whiteSpace: "pre-wrap" } }}
wrapLines
showLineNumbers={false}
showInlineLineNumbers={false}
language="python"
onMouseDown={stopPropagationHandler}
onMouseMove={stopPropagationHandler}
Expand Down
75 changes: 75 additions & 0 deletions packages/core/entity/FileExplorerURL/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,79 @@ export default class FileExplorerURL {
sortColumn,
};
}

public static convertToPython(urlComponents: Partial<FileExplorerURLComponents>) {
const collectionString = this.convertCollectionToPython(urlComponents?.collection);

const groupByQueryString =
urlComponents.hierarchy
?.map((annotation) => this.convertGroupByToPython(annotation))
.join("") || "";

// Group filters by name and use OR to concatenate same filter values
const filterGroups = new Map();
urlComponents.filters?.forEach((filter) => {
const pythonQueryString = filterGroups.get(filter.name);
if (!pythonQueryString) {
filterGroups.set(filter.name, this.convertFilterToPython(filter));
} else {
filterGroups.set(
filter.name,
pythonQueryString.concat(` | ${this.convertFilterToPython(filter)}`)
);
}
});

// Chain the filters together
let filterQueryString = "";
filterGroups.forEach((value) => {
filterQueryString = filterQueryString.concat(`.query('${value}')`);
});

const sortQueryString = urlComponents.sortColumn
? this.convertSortToPython(urlComponents.sortColumn)
: "";
// const fuzzy = [] // TO DO: support fuzzy filtering

const hasQueryElements = groupByQueryString || filterQueryString || sortQueryString;
const imports = "import pandas\n";
const comment = hasQueryElements ? "#Query on dataframe df" : "#No options selected";
const fullQueryString = `${comment}${
hasQueryElements && `\ndf${groupByQueryString}${filterQueryString}${sortQueryString}`
}`;
return `${imports}${collectionString}${fullQueryString}`;
}

private static convertSortToPython(sortColumn: FileSort) {
return `.sort_values(by='${sortColumn.annotationName}', ascending=${
sortColumn.order == "ASC" ? "True" : "False"
})`;
}

private static convertGroupByToPython(annotation: string) {
return `.groupby('${annotation}', group_keys=True).apply(lambda x: x)`;
}

private static convertFilterToPython(filter: FileFilter) {
// TO DO: Support querying non-string types
if (filter.value.includes("RANGE")) {
return;
// let begin, end;
// return `\`${filter.name}\`>="${begin}"&\`${filter.name}\`<"${end}"`
}
return `\`${filter.name}\`=="${filter.value}"`;
}

private static convertCollectionToPython(collection: Collection | undefined) {
if (collection?.uri) {
const comment = "#Convert current datasource file to a pandas dataframe";
const extension = collection.uri.substring(collection.uri.lastIndexOf(".") + 1);
// Currently suggest setting all fields to strings; otherwise pandas assumes type conversions
// TO DO: Address different non-string type conversions
const code = `df = pandas.read_${extension}('${collection.uri}').astype('str')`;
// This only works if we assume that the file types will only be csv, parquet or json
return `${comment}\n${code}\n\n`;
}
return "";
}
}
9 changes: 4 additions & 5 deletions packages/core/state/interaction/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createSelector } from "reselect";

import { State } from "../";
import { getCollection } from "../selection/selectors";
import { getCollection, getPythonConversion } from "../selection/selectors";
import { AnnotationService, FileService, HttpServiceBase } from "../../services";
import DatasetService, { PythonicDataAccessSnippet } from "../../services/DatasetService";
import DatabaseAnnotationService from "../../services/AnnotationService/DatabaseAnnotationService";
Expand Down Expand Up @@ -32,12 +32,11 @@ export const getUserSelectedApplications = (state: State) =>
export const getVisibleModal = (state: State) => state.interaction.visibleModal;

// COMPOSED SELECTORS
// TODO: Implement PythonicDataAccessSnippet
export const getPythonSnippet = createSelector(
[],
(): PythonicDataAccessSnippet => {
[getPythonConversion],
(pythonQuery): PythonicDataAccessSnippet => {
const setup = "pip install pandas";
const code = "TODO";
const code = `${pythonQuery}`;

return { setup, code };
}
Expand Down
19 changes: 19 additions & 0 deletions packages/core/state/selection/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ export const getEncodedFileExplorerUrl = createSelector(
}
);

export const getPythonConversion = createSelector(
[getAnnotationHierarchy, getFileFilters, getOpenFileFolders, getSortColumn, getCollection],
(
hierarchy: string[],
filters: FileFilter[],
openFolders: FileFolder[],
sortColumn?: FileSort,
collection?: Dataset
) => {
return FileExplorerURL.convertToPython({
hierarchy,
filters,
openFolders,
sortColumn,
collection,
});
}
);

export const getGroupedByFilterName = createSelector(
[getFileFilters, getAnnotations],
(globalFilters: FileFilter[], annotations: Annotation[]) => {
Expand Down

0 comments on commit 4032c48

Please sign in to comment.