diff --git a/src/src/components/GroupingItem/GroupingItem.tsx b/src/src/components/GroupingItem/GroupingItem.tsx
index 3a5b17b3..fd9952e2 100644
--- a/src/src/components/GroupingItem/GroupingItem.tsx
+++ b/src/src/components/GroupingItem/GroupingItem.tsx
@@ -9,6 +9,8 @@ import { Icon } from 'components/kit';
import { IconName } from 'components/kit/Icon';
import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
+import { GroupNameEnum } from 'config/grouping/GroupingPopovers';
+
import { IGroupingItemProps } from 'types/pages/components/GroupingItem/GroupingItem';
import './GroupingItem.scss';
@@ -50,7 +52,9 @@ function GroupingItem({
' = '>',
+ '<' = '<',
+ '>=' = '>=',
+ '<=' = '<=',
+}
+
function ColorPopoverAdvanced({
onPersistenceChange,
onGroupingPaletteChange,
onShuffleChange,
+ onGroupingConditionsChange,
+ groupingSelectOptions,
persistence,
paletteIndex,
groupingData,
}: IGroupingPopoverAdvancedProps): React.FunctionComponentElement {
+ const [inputValue, setInputValue] = useState('');
+ const [selectedField, setSelectedField] =
+ useState(null);
+ const [selectedOperator, setSelectedOperator] = useState(
+ IOperator['=='],
+ );
+ const [selectedValue, setSelectedValue] = useState('');
+ const [conditions, setConditions] = useState(
+ groupingData?.conditions?.color || [],
+ );
+
+ const onAddCondition = () => {
+ const condition: IGroupingCondition = {
+ fieldName: selectedField?.label || '',
+ operator: selectedOperator ?? IOperator['=='],
+ value: selectedValue,
+ };
+ const conditionIndex = conditions.findIndex(
+ (c) => c.fieldName === condition.fieldName,
+ );
+ const newConditions =
+ conditionIndex === -1
+ ? [...conditions, condition]
+ : conditions.map((c, index) =>
+ index === conditionIndex ? condition : c,
+ );
+ setConditions(newConditions);
+ onGroupingConditionsChange?.(newConditions, GroupNameEnum.COLOR);
+ };
+
+ const onChangeField = (e: any, value: IGroupingSelectOption | null): void => {
+ if (!e || e.code !== 'Backspace' || inputValue.length === 0)
+ handleSelectField(value);
+ };
+
+ const onChangeOperator = (e: any, value: IOperator): void => {
+ handleSelectOperator(value || IOperator['==']);
+ };
+
+ const handleSelectField = (value: IGroupingSelectOption | null) => {
+ const newSelectedField =
+ selectedField?.value === value?.value ? null : value;
+ setInputValue(newSelectedField?.label || '');
+ setSelectedField(newSelectedField);
+ };
+
+ const handleSelectOperator = (value?: IOperator) => {
+ setSelectedOperator(value ?? IOperator['==']);
+ };
+
+ const handleSelectValue = (value: string) => {
+ setSelectedValue(value);
+ };
+
+ const options = useMemo(() => {
+ const filteredOptions = groupingSelectOptions?.filter((item) =>
+ item.label.toLowerCase().includes(inputValue.toLowerCase()),
+ );
+ return (
+ filteredOptions?.sort(
+ (a, b) =>
+ a.label.toLowerCase().indexOf(inputValue.toLowerCase()) -
+ b.label.toLowerCase().indexOf(inputValue.toLowerCase()),
+ ) || []
+ );
+ }, [groupingSelectOptions, inputValue]);
+
function onPaletteChange(e: React.ChangeEvent) {
let { value } = e.target;
if (onGroupingPaletteChange) {
@@ -110,6 +198,144 @@ function ColorPopoverAdvanced({
))}
+
+
+ group by condition
+
+
+ Group charts by conditions such as{' '}
+
+ run.epochs > 30
+
+ .
+
+
+ {/* Add textbox to allow grouping by condition */}
+
option.group}
+ getOptionLabel={(option) => option.label}
+ getOptionSelected={(option, value) =>
+ option.value === selectedField?.value
+ }
+ renderInput={(params: any) => (
+ {
+ setInputValue(e.target?.value);
+ },
+ }}
+ className='TextField__OutLined__Small'
+ variant='outlined'
+ placeholder='Select fields'
+ />
+ )}
+ renderTags={() => null} // No tags for single selection
+ renderOption={(option, { selected }) => (
+ onChangeField(null, option)}
+ >
+ }
+ checkedIcon={}
+ style={{ marginRight: 4 }}
+ checked={selected}
+ />
+
+ {option.label}
+
+
+ )}
+ />
+ {/* Dropdown for operator */}
+ (
+
+ )}
+ />
+ {/* Textbox for the condition value */}
+ handleSelectValue(e.target.value)}
+ />
+
+
+
+ {conditions.map((condition, index) => (
+
+ {/* Show condition and button in same line */}
+
+ {condition.fieldName} {condition.operator} {condition.value}
+
+
+
+ ))}
+
+
);
diff --git a/src/src/services/models/explorer/createAppModel.ts b/src/src/services/models/explorer/createAppModel.ts
index cf28c1f2..a4d231ce 100644
--- a/src/src/services/models/explorer/createAppModel.ts
+++ b/src/src/services/models/explorer/createAppModel.ts
@@ -212,7 +212,8 @@ import getLegendsData from 'utils/app/getLegendsData';
import onLegendsChange from 'utils/app/onLegendsChange';
import { getSelectedExperiments } from 'utils/app/getSelectedExperiments';
import { removeOldSelectedMetrics } from 'utils/app/removeOldSelectedMetrics';
-import evaluateCondition from 'utils/app/evaluateCondition';
+import { generateGroupValues } from 'utils/app/generateGroupValues';
+import { getConditionStrings } from 'utils/app/getConditionStrings';
import { AppDataTypeEnum, AppNameEnum } from './index';
@@ -1466,30 +1467,21 @@ function createAppModel(appConfig: IAppInitialConfig) {
const grouping = configData!.grouping;
const { paletteIndex = 0 } = grouping || {};
- const chartConditions: IGroupingCondition[] =
- grouping.conditions?.chart || [];
- const chartConditionStrings = chartConditions.map(
- (condition) =>
- `${condition.fieldName} ${condition.operator} ${condition.value}`,
- );
-
- const strokeConditions: IGroupingCondition[] =
- grouping.conditions?.stroke || [];
- const strokeConditionStrings = strokeConditions.map(
- (condition) =>
- `${condition.fieldName} ${condition.operator} ${condition.value}`,
- );
-
- const allConditions = chartConditions.concat(strokeConditions);
- const allConditionStrings = allConditions.map(
- (condition) =>
- `${condition.fieldName} ${condition.operator} ${condition.value}`,
+ const colorConditions = grouping.conditions?.color || [];
+ const colorConditionStrings = getConditionStrings(colorConditions);
+ const strokeConditions = grouping.conditions?.stroke || [];
+ const strokeConditionStrings = getConditionStrings(strokeConditions);
+ const chartConditions = grouping.conditions?.chart || [];
+ const chartConditionStrings = getConditionStrings(chartConditions);
+ const allConditions = colorConditions.concat(
+ strokeConditions,
+ chartConditions,
);
const groupByColor = getFilteredGroupingOptions({
groupName: GroupNameEnum.COLOR,
model,
- });
+ }).concat(colorConditionStrings);
const groupByStroke = getFilteredGroupingOptions({
groupName: GroupNameEnum.STROKE,
model,
@@ -1515,63 +1507,13 @@ function createAppModel(appConfig: IAppInitialConfig) {
]);
}
- const groupValues: {
- [key: string]: IMetricsCollection;
- } = {};
-
const groupingFields = _.uniq(
groupByColor.concat(groupByStroke).concat(groupByChart),
);
- for (let i = 0; i < data.length; i++) {
- const groupValue: { [key: string]: any } = {};
- groupingFields.forEach((field) => {
- groupValue[field] = getValue(data[i], field);
- });
-
- // Evaluate the conditions and update the row
- allConditionStrings.forEach((conditionString, j) => {
- // Evaluate the condition
- const condition = allConditions[j];
-
- // Get everything after the first dot in the field name
- const fieldTypeAndName = condition.fieldName.split('.');
- const fieldType = fieldTypeAndName[0];
- const fieldName = fieldTypeAndName.slice(1).join('.');
-
- // Flatten default run attributes and store them in a single object
- const runAttributes = {
- ...data[i].run.params,
- ...data[i].run.props,
- hash: data[i].run.hash,
- name:
- fieldType === 'metric' ? data[i].name : data[i].run.props.name,
- tags: data[i].run.params.tags,
- experiment: data[i].run.props.experiment?.name,
- };
-
- // Get the relevant attribute's value
- const attributeValue = getValue(runAttributes, fieldName);
- groupValue[conditionString] = evaluateCondition(
- attributeValue,
- condition,
- );
- });
-
- const groupKey = encode(groupValue);
- if (groupValues.hasOwnProperty(groupKey)) {
- groupValues[groupKey].data.push(data[i]);
- } else {
- groupValues[groupKey] = {
- key: groupKey,
- config: groupValue,
- color: null,
- dasharray: null,
- chartIndex: 0,
- data: [data[i]],
- };
- }
- }
+ const groupValues: {
+ [key: string]: IMetricsCollection;
+ } = generateGroupValues(data, allConditions, groupingFields);
let colorIndex = 0;
let dasharrayIndex = 0;
diff --git a/src/src/utils/app/generateGroupValues.ts b/src/src/utils/app/generateGroupValues.ts
new file mode 100644
index 00000000..361599f5
--- /dev/null
+++ b/src/src/utils/app/generateGroupValues.ts
@@ -0,0 +1,74 @@
+import {
+ IGroupingCondition,
+ IMetricsCollection,
+} from 'types/services/models/metrics/metricsAppModel';
+import { IMetric } from 'types/services/models/metrics/metricModel';
+
+import { getValue } from 'utils/helper';
+import { encode } from 'utils/encoder/encoder';
+
+import evaluateCondition from './evaluateCondition';
+import { getConditionStrings } from './getConditionStrings';
+
+export function generateGroupValues(
+ data: IMetric[],
+ allConditions: IGroupingCondition[],
+ groupingFields: string[],
+) {
+ const groupValues: {
+ [key: string]: IMetricsCollection;
+ } = {};
+
+ const allConditionStrings = getConditionStrings(allConditions);
+
+ for (let i = 0; i < data.length; i++) {
+ const groupValue: { [key: string]: any } = {};
+ groupingFields.forEach((field) => {
+ groupValue[field] = getValue(data[i], field);
+ });
+
+ // Evaluate the conditions and update the row
+ allConditionStrings.forEach((conditionString, j) => {
+ // Evaluate the condition
+ const condition = allConditions[j];
+
+ // Get everything after the first dot in the field name
+ const fieldTypeAndName = condition.fieldName.split('.');
+ const fieldType = fieldTypeAndName[0];
+ const fieldName = fieldTypeAndName.slice(1).join('.');
+
+ // Flatten default run attributes and store them in a single object
+ const runAttributes = {
+ ...data[i].run.params,
+ ...data[i].run.props,
+ hash: data[i].run.hash,
+ name: fieldType === 'metric' ? data[i].name : data[i].run.props.name,
+ tags: data[i].run.params.tags,
+ experiment: data[i].run.props.experiment?.name,
+ };
+
+ // Get the relevant attribute's value
+ const attributeValue = getValue(runAttributes, fieldName);
+ groupValue[conditionString] = evaluateCondition(
+ attributeValue,
+ condition,
+ );
+ });
+
+ const groupKey = encode(groupValue);
+ if (groupValues.hasOwnProperty(groupKey)) {
+ groupValues[groupKey].data.push(data[i]);
+ } else {
+ groupValues[groupKey] = {
+ key: groupKey,
+ config: groupValue,
+ color: null,
+ dasharray: null,
+ chartIndex: 0,
+ data: [data[i]],
+ };
+ }
+ }
+
+ return groupValues;
+}
diff --git a/src/src/utils/app/getConditionStrings.ts b/src/src/utils/app/getConditionStrings.ts
new file mode 100644
index 00000000..0c51e685
--- /dev/null
+++ b/src/src/utils/app/getConditionStrings.ts
@@ -0,0 +1,12 @@
+import { IGroupingCondition } from 'types/services/models/metrics/metricsAppModel';
+
+/**
+ * Get the list of conditions as strings
+ * @param conditions the list of IGroupingCondition
+ * @returns the list of conditions as strings
+ */
+export function getConditionStrings(conditions: IGroupingCondition[]) {
+ return conditions.map((condition) => {
+ return `${condition.fieldName} ${condition.operator} ${condition.value}`;
+ });
+}
diff --git a/src/src/utils/app/getLegendsData.tsx b/src/src/utils/app/getLegendsData.tsx
index e0e2d001..2ab836c9 100644
--- a/src/src/utils/app/getLegendsData.tsx
+++ b/src/src/utils/app/getLegendsData.tsx
@@ -39,7 +39,7 @@ function getLegendsData(
const groupConfig = groupingConfig[groupName];
const groupedItemPropKeys =
- groupName === GroupNameEnum.ROW || groupName === GroupNameEnum.COLOR
+ groupName === GroupNameEnum.ROW
? groupConfig || []
: groupConfig?.concat(
groupingConfig.conditions?.[groupName].map(