Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add Zoo #437

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7293a55
feat: Multi-image selection in Python and AI plugins
SilviaZeta May 20, 2022
11514b1
refactor: Separate filter functionality (WIP)
SilviaZeta Jun 22, 2022
43832d6
refactor: Separate filter functionality
SilviaZeta Jun 23, 2022
7e93906
fix: Module export
SilviaZeta Jun 24, 2022
f4bfef1
fix: Tests
SilviaZeta Jun 24, 2022
4cd22f7
chore: Export getLabelsFromKeys
SilviaZeta Jun 28, 2022
6ce63d3
style: Set "outlined" variant for all MuiCards
SilviaZeta Jun 28, 2022
6e67bae
feat: Add ZooDialog (and remove metadataKeys)
SilviaZeta Jun 28, 2022
9a9e288
style: SearchBar
SilviaZeta Jun 28, 2022
0e70d03
style: SearchBar
SilviaZeta Jun 28, 2022
fdf7120
fix: Lint
SilviaZeta Jun 28, 2022
f3864eb
style: SearchFilterCard
SilviaZeta Jun 28, 2022
25a22d4
style: Add margin above annotation section
SilviaZeta Jun 28, 2022
67e5793
style: SearchFilterCard
SilviaZeta Jun 28, 2022
26e1e44
fix: FilterDataItem interface
SilviaZeta Jun 29, 2022
9a4c0a8
feat: Add convertToFilterData (and fix filterData)
SilviaZeta Jul 11, 2022
dfbc34f
feat: Make group-by functionality in SortPopover optional
SilviaZeta Jul 11, 2022
167ae9d
chore: Delete convertToFilterData
SilviaZeta Jul 12, 2022
020a04e
fix: FilterDataItem interface
SilviaZeta Jul 12, 2022
38b4c7d
Merge branch 'main' into moveSearchSort
SilviaZeta Jul 15, 2022
0cafd9b
fix: FilterDataItem interface (again)
SilviaZeta Jul 15, 2022
2576406
Merge remote-tracking branch 'origin/main' into moveSearchSort
SilviaZeta Jul 29, 2022
9a64049
Merge remote-tracking branch 'origin/refactorPlugins' into moveSearch…
SilviaZeta Jul 29, 2022
144d33a
Merge branch 'main' into moveSearchSort
ChasNelson1990 Aug 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/MetadataDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import makeStyles from "@mui/styles/makeStyles";
import { theme, HtmlTooltip, icons, Typography } from "@gliff-ai/style";
import SVG from "react-inlinesvg";
import { MetaItem } from "@/interfaces";
import { getLabelsFromKeys, MetadataLabel } from "@/search/SearchBar";

type MetadataNameMap = { [index: string]: string };
import { getLabelsFromKeys } from "@/helpers";

const useStyles = makeStyles({
paperHeader: {
Expand Down Expand Up @@ -85,7 +83,7 @@ const useStyles = makeStyles({
svgSmall: { width: "12px", height: "100%" },
});

export const metadataNameMap: MetadataNameMap = {
export const dataNameMap: { [index: string]: string } = {
imageName: "Name",
size: "Size",
dateCreated: "Created",
Expand All @@ -103,6 +101,8 @@ export const metadataNameMap: MetadataNameMap = {
assignees: "Assignees",
};

type DataKeyLabel = { key: string; label: string };

interface Props {
metadata: MetaItem;
close: () => void;
Expand All @@ -111,13 +111,13 @@ interface Props {
export default function MetadataDrawer(props: Props): ReactElement {
const classes = useStyles();
const [hover, sethover] = useState(false);
const [metaKeys, setMetaKeys] = useState<MetadataLabel[]>([]);
const [metaKeys, setMetaKeys] = useState<DataKeyLabel[]>([]);

useEffect(() => {
setMetaKeys(
Object.keys(props.metadata).reduce(
getLabelsFromKeys,
[] as MetadataLabel[]
getLabelsFromKeys(dataNameMap)(),
[] as DataKeyLabel[]
)
);
}, [props.metadata]);
Expand Down Expand Up @@ -178,8 +178,8 @@ export default function MetadataDrawer(props: Props): ReactElement {
<ListItemText
primaryTypographyProps={{ variant: "h6" }}
className={classes.metaKey}
title={`${metadataNameMap[key] || key}`}
primary={`${metadataNameMap[key] || key}:`}
title={`${dataNameMap[key] || key}`}
primary={`${dataNameMap[key] || key}:`}
classes={{
primary: classes.metaKey,
root: classes.metaRoot,
Expand Down
8 changes: 0 additions & 8 deletions src/components/Tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ const tooltips: Tooltips = {
name: "Download Dataset",
icon: icons.download,
},
sort: {
name: "Sort",
icon: icons.searchFilter,
},
search: {
name: "Search",
icon: icons.search,
},
addLabels: {
name: "Update Image Labels",
icon: icons.annotationLabel,
Expand Down
2 changes: 1 addition & 1 deletion src/components/plugins/PluginsAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const PluginsAccordion = ({
} else {
data = {
collectionUid,
imageUid: selectedImagesUid[0] || undefined,
imageUids: selectedImagesUid,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface PluginElement {

type PluginInput = Partial<{
collectionUid: string;
imageUid: string;
imageUids: string[];
metadata: Metadata;
}>;

Expand Down
204 changes: 204 additions & 0 deletions src/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import type { Filter, FilterData, FilterDataItem } from "./interfaces";

export class Filters {
activeFilters: Filter[];

isGrouped: boolean;

sortedBy: string | null;

constructor() {
this.activeFilters = [];
this.isGrouped = false;
this.sortedBy = null;
}

private compare = (
a: string | Date | number,
b: string | Date | number,
ascending: boolean
): number => {
// Compare two values. Undefined values are always at the end.
if (a === undefined) {
return 1;
}
if (b === undefined) {
return -1;
}
if (a < b) {
return ascending ? -1 : 1;
}
if (a > b) {
return ascending ? 1 : -1;
}
return 0;
};

getDataKeys = (item: FilterDataItem): string[] =>
Object.keys(item).filter((k) => k !== "selected");

hasAnyFilters = (): boolean => this.activeFilters.length > 0;

private hasFilter = (filter: Filter): boolean =>
this.activeFilters.some(
(filt) => filt.key === filter.key && filt.value === filter.value
);

resetFilters = (data: FilterData): FilterData => {
this.activeFilters = [];
return this.selectAll(data);
};

selectAll = (data: FilterData): FilterData =>
data.map((i) => ({ ...i, filterShow: true }));

isSelectAll = (filter: Filter): boolean => {
const { value, key } = filter;
return value === "All values" || key === "" || value === "";
};

toggleFilter = (filter: Filter): void => {
if (this.hasFilter(filter)) {
this.activeFilters.splice(this.activeFilters.indexOf(filter), 1);
} else {
this.activeFilters.push(filter);
}
};

addFilter = (filter: Filter): void => {
if (!this.hasFilter(filter)) {
this.activeFilters.push(filter);
}
};

applyFilter = (data: FilterData, filter: Filter): FilterData => {
if (this.isSelectAll(filter)) {
return this.resetFilters(data);
}
this.addFilter(filter);
return this.filterData(data);
};

private getKeyType = (data: FilterData, key: string): string => {
if (key?.toLowerCase().includes("date")) return "date";
for (const mitem of data) {
const someType = typeof mitem[key];
if (someType !== "undefined") {
return someType;
}
}
return "undefined";
};

sortData = (
data: FilterData,
key: string,
ascending = true
): FilterData | null => {
const dataCopy = [...data];
const dataType = this.getKeyType(data, key);

function toDate(value: string): Date {
return value !== undefined ? new Date(value) : undefined;
}

this.sortedBy = key;

switch (dataType) {
case "number":
dataCopy.sort((a: FilterDataItem, b: FilterDataItem): number =>
this.compare(a[key] as number, b[key] as number, ascending)
);
return dataCopy;
case "date":
dataCopy.sort((a, b): number =>
this.compare(
toDate(a[key] as string),
toDate(b[key] as string),
ascending
)
);
return dataCopy;

case "string":
dataCopy.sort((a: FilterDataItem, b: FilterDataItem): number =>
this.compare(a[key] as number, b[key] as number, ascending)
);
return dataCopy;

default:
console.warn(`Cannot sort values with type "${dataType}".`);
this.sortedBy = null;
return null;
}
};

filterData = (data: FilterData): FilterData => {
const dataCopy = [...data];
if (this.activeFilters.length > 0) {
dataCopy.forEach((item) => {
this.activeFilters.forEach((filter, fi) => {
const value = item[filter.key];

// current filter selection
const currentSel = Number(
Array.isArray(value)
? value.some((v) => v.includes(filter.value))
: String(value).includes(filter.value)
);

// selection for all filters up to current
const prevSel = fi === 0 ? 1 : Number(item.filterShow);

// update 'filterShow' field
item.filterShow = Boolean(prevSel * currentSel);
});
});
return dataCopy;
}
// select all items
return this.selectAll(dataCopy);
};

private getMonthAndYear = (date: string): string =>
date !== undefined
? new Date(date).toLocaleDateString("en-GB", {
month: "short",
year: "numeric",
})
: "";

toggleIsGrouped = (): void => {
this.isGrouped = !this.isGrouped;
};

resetSort = (): void => {
this.sortedBy = null;
};

groupByValue = (data: FilterData): FilterData => {
// Assign the newGroup field to all items, based on the same key used for sort
if (!this.sortedBy || !this.isGrouped) return data;

const areValuesEqual = this.sortedBy?.toLowerCase().includes("date")
? (value: unknown, previousValue: unknown) =>
this.getMonthAndYear(value as string) !==
this.getMonthAndYear(previousValue as string)
: (value: unknown, previousValue: unknown) => value !== previousValue;

let prevValue: unknown = null;
data.forEach((item) => {
if (!item.filterShow) return;
// Number.MAX_VALUE added to handle missing values
const value = (item[this.sortedBy] as string) || Number.MAX_VALUE;
if (!prevValue || areValuesEqual(value, prevValue)) {
item.newGroup = true;
} else {
item.newGroup = false;
}
prevValue = value;
});
return data;
};
}
export type { Filter, FilterData, FilterDataItem };
14 changes: 14 additions & 0 deletions src/filter/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type Filter = {
key: string;
value: string;
};

interface FilterDataItem {
[key: string]: string | number | boolean | string[] | undefined | null;
filterShow: boolean;
newGroup: boolean;
}

type FilterData = FilterDataItem[];

export type { Filter, FilterDataItem, FilterData };
18 changes: 12 additions & 6 deletions src/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Filter, MetaItem } from "@/interfaces";
import { sortMetadata, filterMetadata } from "./helpers";
import { MetaItem } from "@/interfaces";
import { Filter, FilterData, Filters } from "./filter";

type TestMetaData = Partial<MetaItem>[];
const filters = new Filters();
const metadata: Partial<MetaItem>[] = [
{
string: "through",
Expand Down Expand Up @@ -31,8 +32,12 @@ describe("sort metadata with missing values", () => {
test.each(testSample)(
`sort values of type %s`,
(key: string, output: any[]) => {
const metadataAsc = sortMetadata(cloneMetadata(), key);
const metadataDes = sortMetadata(cloneMetadata(), key, false);
const metadataAsc = filters.sortData(cloneMetadata() as FilterData, key);
const metadataDes = filters.sortData(
cloneMetadata() as FilterData,
key,
false
);
const arrayUndefined = Array.from(
metadata.filter((mitem) => !Object.keys(mitem).includes(key))
).fill(undefined);
Expand All @@ -48,8 +53,9 @@ describe("sort metadata with missing values", () => {
);
});

const testFilter = (filters: Filter[], outcome: TestMetaData): void => {
const newMetadata = filterMetadata(cloneMetadata(), filters);
const testFilter = (activeFilters: Filter[], outcome: TestMetaData): void => {
filters.activeFilters = activeFilters;
const newMetadata = filters.filterData(cloneMetadata() as FilterData);
expect(
newMetadata
.filter(({ filterShow }) => filterShow)
Expand Down
Loading