Skip to content

Commit

Permalink
Parameter list filtering component (#404)
Browse files Browse the repository at this point in the history
Add component for filter a list of Parameter objects. To shorten the list of parameters based on wanted property values/flags
  • Loading branch information
jorgenherje authored Oct 10, 2023
1 parent 44881ab commit 09407b3
Show file tree
Hide file tree
Showing 8 changed files with 492 additions and 22 deletions.
2 changes: 1 addition & 1 deletion frontend/src/framework/EnsembleParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class ParameterIdent {
}
if (parts.length === 2) {
return new ParameterIdent(parts[0], parts[1]);
}
}

throw new Error(`Invalid parameter ident string: ${paramIdentString}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ParameterListFilter } from "./parameterListFilter";
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from "react";

import { Parameter } from "@framework/EnsembleParameters";
import { SmartNodeSelector, SmartNodeSelectorSelection } from "@lib/components/SmartNodeSelector";
import { TreeDataNode } from "@lib/components/SmartNodeSelector";
import { resolveClassNames } from "@lib/utils/resolveClassNames";

import { isEqual } from "lodash";

// Icons placed here due to limitation of jest for testing utils (cannot import svg)
import checkIcon from "./private-assets/check.svg";
import segmentIcon from "./private-assets/segment.svg";
import {
ParameterParentNodeNames,
createTreeDataNodeListFromParameters,
getParametersMatchingSelectedNodes,
} from "./private-utils/smartNodeSelectorUtils";

export type InitialParameterFilter = Extract<
(typeof ParameterParentNodeNames)[keyof typeof ParameterParentNodeNames],
"Continuous" | "Discrete" | "Constant" | "Nonconstant"
>;

export type ParameterListFilterProps = {
parameters: Parameter[];
initialFilters?: InitialParameterFilter[];
showTitle?: boolean;
onChange?: (filteredParameters: Parameter[]) => void;
};

export const ParameterListFilter: React.FC<ParameterListFilterProps> = (props: ParameterListFilterProps) => {
const smartNodeSelectorId = React.useId();
const [selectedTags, setSelectedTags] = React.useState<string[]>(
props.initialFilters ?? [ParameterParentNodeNames.IS_NONCONSTANT]
);
const [selectedNodes, setSelectedNodes] = React.useState<string[]>([]);
const [numberOfMatchingParameters, setNumberOfMatchingParameters] = React.useState<number>(0);
const [parameters, setParameters] = React.useState<Parameter[] | null>(null);
const [previousTreeDataNodeList, setPreviousTreeDataNodeList] = React.useState<TreeDataNode[]>([]);
const smartNodeSelectorDelimiter = ":";

let newTreeDataNodeList: TreeDataNode[] | null = null;
if (parameters === null || !isEqual(props.parameters, parameters)) {
newTreeDataNodeList = createTreeDataNodeListFromParameters(props.parameters, checkIcon, segmentIcon);
setParameters(props.parameters);
setPreviousTreeDataNodeList(newTreeDataNodeList);
}
const treeDataNodeList = newTreeDataNodeList ? newTreeDataNodeList : previousTreeDataNodeList;

// Utilizing useEffect to prevent re-render of parent component during rendering of this component
React.useEffect(
function createFilterParameters() {
if (parameters === null || parameters.length === 0) {
setNumberOfMatchingParameters(0);
if (props.onChange) {
props.onChange([]);
}
return;
}

const filteredParameters = getParametersMatchingSelectedNodes(
parameters,
selectedNodes,
smartNodeSelectorDelimiter
);
setNumberOfMatchingParameters(filteredParameters.length);
if (props.onChange) {
props.onChange(filteredParameters);
}
},
[selectedNodes, parameters, props.onChange]
);

function handleSmartNodeSelectorChange(selection: SmartNodeSelectorSelection) {
setSelectedTags(selection.selectedTags);
setSelectedNodes(selection.selectedNodes);
}

return (
<div className={props.showTitle ? "mb-2 mt-2" : ""}>
<>
<SmartNodeSelector
id={smartNodeSelectorId}
delimiter={smartNodeSelectorDelimiter}
data={treeDataNodeList}
selectedTags={selectedTags}
label={props.showTitle ? "Parameter filtering" : undefined}
onChange={handleSmartNodeSelectorChange}
placeholder="Add new filter..."
/>
<div className={resolveClassNames("text-right relative w-full mt-2 text-slate-600 text-sm")}>
Number of matches: {numberOfMatchingParameters}
</div>
</>
</div>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Parameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters";
import { TreeDataNode } from "@lib/components/SmartNodeSelector";

export const ParameterParentNodeNames = {
NAME: "Name",
GROUP: "Group",
CONTINUOUS: "Continuous", // For Parameter.type === ParameterType.CONTINUOUS
DISCRETE: "Discrete", // For Parameter.type === ParameterType.DISCRETE
IS_CONSTANT: "Constant", // For Parameter.isConstant === true
IS_NONCONSTANT: "Nonconstant", // For Parameter.isConstant === false
IS_LOGARITHMIC: "Logarithmic", // For Parameter.isLogarithmic === true
IS_LINEAR: "Linear", // For Parameter.isLogarithmic === false
} as const;

export function createAndAddNode(treeNodeDataList: TreeDataNode[], nodeName: string, icon?: string): TreeDataNode {
const newNode: TreeDataNode = { name: nodeName, description: "", icon: icon };
treeNodeDataList.push(newNode);
return newNode;
}

export function createTreeDataNodeListFromParameters(
parameters: Parameter[],
checkIcon?: string,
parentIcon?: string
): TreeDataNode[] {
if (parameters.length === 0) {
return [];
}

const treeDataNodeList: TreeDataNode[] = [];

const hasContinuousParameter = parameters.some((parameter) => parameter.type === ParameterType.CONTINUOUS);

// Node for boolean/state properties on top level
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.CONTINUOUS, checkIcon);
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.DISCRETE, checkIcon);
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_CONSTANT, checkIcon);
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_NONCONSTANT, checkIcon);
if (hasContinuousParameter) {
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_LOGARITHMIC, checkIcon);
createAndAddNode(treeDataNodeList, ParameterParentNodeNames.IS_LINEAR, checkIcon);
}

// Add parameter name and check for group name
const parameterNameSet = new Set<string>();
const groupNameSet = new Set<string>();
for (const parameter of parameters) {
parameterNameSet.add(parameter.name);

if (parameter.groupName) {
groupNameSet.add(parameter.groupName);
}
}

// Add parameter names
if (parameterNameSet.size !== 0) {
const nameParentNode = createAndAddNode(treeDataNodeList, ParameterParentNodeNames.NAME, parentIcon);
nameParentNode.children = [];
for (const parameterName of parameterNameSet) {
createAndAddNode(nameParentNode.children, parameterName);
}
}

// Add parameter groups
if (groupNameSet.size !== 0) {
const groupParentNode = createAndAddNode(treeDataNodeList, ParameterParentNodeNames.GROUP, parentIcon);
groupParentNode.children = [];
for (const groupName of groupNameSet) {
createAndAddNode(groupParentNode.children, groupName);
}
}

return treeDataNodeList;
}

export function getChildNodeNamesFromParentNodeName(
nodes: string[],
parentNodeName: string,
delimiter = ":"
): string[] {
return nodes
.filter((node) => node.split(delimiter, 1)[0] === parentNodeName)
.map((node) => node.split(delimiter, 2)[1]);
}

export function getParametersMatchingSelectedNodes(
parameters: Parameter[],
selectedNodes: string[],
delimiter = ":"
): Parameter[] {
// No selection implies no filtering
if (selectedNodes.length === 0) {
return parameters;
}

const isContinuousSelected = selectedNodes.includes(ParameterParentNodeNames.CONTINUOUS);
const isDiscreteSelected = selectedNodes.includes(ParameterParentNodeNames.DISCRETE);
const isConstantSelected = selectedNodes.includes(ParameterParentNodeNames.IS_CONSTANT);
const isNonConstantSelected = selectedNodes.includes(ParameterParentNodeNames.IS_NONCONSTANT);
const isLogarithmicSelected = selectedNodes.includes(ParameterParentNodeNames.IS_LOGARITHMIC);
const isLinearSelected = selectedNodes.includes(ParameterParentNodeNames.IS_LINEAR);

// Intersection filtering, i.e. parameter cannot be both continuous and discrete, constant and non-constant, logarithmic and linear
if (isContinuousSelected && isDiscreteSelected) return [];
if (isConstantSelected && isNonConstantSelected) return [];
if (isLogarithmicSelected && isLinearSelected) return [];

const selectedParameterNames = getChildNodeNamesFromParentNodeName(
selectedNodes,
ParameterParentNodeNames.NAME,
delimiter
);
const selectedParameterGroups = getChildNodeNamesFromParentNodeName(
selectedNodes,
ParameterParentNodeNames.GROUP,
delimiter
);

const isNoParameterPropertyAmongSelectedNodes =
!isContinuousSelected &&
!isDiscreteSelected &&
!isConstantSelected &&
!isNonConstantSelected &&
!isLogarithmicSelected &&
!isLinearSelected &&
selectedParameterNames.length === 0 &&
selectedParameterGroups.length === 0;
if (isNoParameterPropertyAmongSelectedNodes) {
return [];
}

const selectedEnsembleParameters: Parameter[] = [];
for (const parameter of parameters) {
// Filter by parameter name
if (selectedParameterNames.length !== 0 && !selectedParameterNames.includes(parameter.name)) {
continue;
}

// Filter by parameter group
if (selectedParameterGroups.length !== 0 && parameter.groupName === null) {
continue;
}
if (
selectedParameterGroups.length !== 0 &&
parameter.groupName !== null &&
!selectedParameterGroups.includes(parameter.groupName)
) {
continue;
}

// Intersection filter by parameter type (continuous/discrete)
if (isContinuousSelected && parameter.type !== ParameterType.CONTINUOUS) continue;
if (isDiscreteSelected && parameter.type !== ParameterType.DISCRETE) continue;

// Intersection filter by parameter is constant/non-constant
if (isConstantSelected && !parameter.isConstant) continue;
if (isNonConstantSelected && parameter.isConstant) continue;

// Filter by parameter is logarithmic/linear (only for continuous parameters)
if (isLogarithmicSelected && parameter.type === ParameterType.CONTINUOUS && !parameter.isLogarithmic) continue;
if (isLinearSelected && parameter.type === ParameterType.CONTINUOUS && parameter.isLogarithmic) continue;

// Prevent duplicates
const parameterIdent = ParameterIdent.fromNameAndGroup(parameter.name, parameter.groupName);
if (
selectedEnsembleParameters.some((elm) =>
ParameterIdent.fromNameAndGroup(elm.name, elm.groupName).equals(parameterIdent)
)
) {
continue;
}

selectedEnsembleParameters.push(parameter);
}

return selectedEnsembleParameters;
}
Loading

0 comments on commit 09407b3

Please sign in to comment.