From a34d8e1455e05c36842302c16a39212658cbacb5 Mon Sep 17 00:00:00 2001 From: Daniel Fitzgibbon Alves Pereira Date: Sat, 9 Nov 2024 23:04:51 +0000 Subject: [PATCH] Add visualisation filtering for boolean values --- apps/api/src/python/visualizations.ts | 5 + .../visualization/FilterSelector.tsx | 95 +++++++++++++++++-- packages/types/src/index.ts | 26 +++++ 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/apps/api/src/python/visualizations.ts b/apps/api/src/python/visualizations.ts index ad7dc51f..672f2349 100644 --- a/apps/api/src/python/visualizations.ts +++ b/apps/api/src/python/visualizations.ts @@ -604,6 +604,11 @@ def _briefer_create_visualization( df = df[df[column_name].isnull()] elif operator == 'isNotNull': df = df[df[column_name].notnull()] + elif pd.api.types.is_bool_dtype(df[column_name]): + if operator == 'isTrue': + df = df[df[column_name]] + elif operator == 'isFalse': + df = df[~df[column_name]] elif pd.api.types.is_datetime64_any_dtype(df[column_name]): # Convert both DataFrame column and value to UTC safely df_column_utc, value_utc = _briefer_convert_to_utc_safe(df[column_name], pd.to_datetime(value)) diff --git a/apps/web/src/components/v2Editor/customBlocks/visualization/FilterSelector.tsx b/apps/web/src/components/v2Editor/customBlocks/visualization/FilterSelector.tsx index f766e7ca..e808b2e6 100644 --- a/apps/web/src/components/v2Editor/customBlocks/visualization/FilterSelector.tsx +++ b/apps/web/src/components/v2Editor/customBlocks/visualization/FilterSelector.tsx @@ -11,12 +11,15 @@ import { VisualizationDateFilterOperator, VisualizationNumberFilterOperator, VisualizationStringFilterOperator, + VisualizationBooleanFilterOperator, numberFilterOperators, stringFilterOperators, dateFilterOperators, + booleanFilterOperators, VisualizationNumberFilter, VisualizationStringFilter, VisualizationDateFilter, + VisualizationBooleanFilter, toDate, DataFrame, VisualizationFilter, @@ -49,6 +52,7 @@ function isNumberOperator( | VisualizationNumberFilterOperator | VisualizationStringFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator ): operator is VisualizationNumberFilterOperator { return VisualizationNumberFilterOperator.safeParse(operator).success } @@ -58,15 +62,27 @@ function isStringOperator( | VisualizationNumberFilterOperator | VisualizationStringFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator ): operator is VisualizationStringFilterOperator { return VisualizationStringFilterOperator.safeParse(operator).success } +function isBooleanOperator( + operator: + | VisualizationNumberFilterOperator + | VisualizationStringFilterOperator + | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator +): operator is VisualizationBooleanFilterOperator { + return VisualizationBooleanFilterOperator.safeParse(operator).success +} + function isDateOperator( operator: | VisualizationNumberFilterOperator | VisualizationStringFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator ): operator is VisualizationDateFilterOperator { return VisualizationDateFilterOperator.safeParse(operator).success } @@ -169,6 +185,27 @@ function stringOperatorLabel( } } +function booleanOperatorSymbol( + operator: VisualizationBooleanFilterOperator +): string { + switch (operator) { + case 'isTrue': + return 'is true' + case 'isFalse': + return 'is false' + } +} +function booleanOperatorLabel( + operator: VisualizationBooleanFilterOperator +): string { + switch (operator) { + case 'isTrue': + return 'Is True' + case 'isFalse': + return 'Is False' + } +} + function dateOperatorSymbol(operator: VisualizationDateFilterOperator): string { switch (operator) { case 'eq': @@ -215,6 +252,7 @@ function getOperatorLabel( | VisualizationStringFilterOperator | VisualizationNumberFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator ): string { if (isNumberOperator(operator)) { return numberOperatorLabel(operator) @@ -224,6 +262,10 @@ function getOperatorLabel( return stringOperatorLabel(operator) } + if (isBooleanOperator(operator)) { + return booleanOperatorLabel(operator) + } + return dateOperatorLabel(operator) } @@ -232,6 +274,7 @@ function searchOperator< | VisualizationNumberFilterOperator | VisualizationStringFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator >(options: T[], query: string): T[] { return options.filter((c) => { if (isNumberOperator(c)) { @@ -248,6 +291,13 @@ function searchOperator< ) } + if (isBooleanOperator(c)) { + return ( + booleanOperatorLabel(c).toLowerCase().includes(query.toLowerCase()) || + booleanOperatorSymbol(c).toLowerCase().includes(query.toLowerCase()) + ) + } + return ( dateOperatorLabel(c).toLowerCase().includes(query.toLowerCase()) || dateOperatorSymbol(c).toLowerCase().includes(query.toLowerCase()) @@ -268,9 +318,8 @@ function getOperatorOptions(columnType: DataFrameColumn['type']) { return dateFilterOperators } - // TODO: add filtering capabilities for boolean types if (NumpyBoolTypes.safeParse(columnType).success) { - return [] + return booleanFilterOperators } // TODO: this should never happen, we should be alerted @@ -281,6 +330,7 @@ type Operator = | VisualizationStringFilterOperator | VisualizationNumberFilterOperator | VisualizationDateFilterOperator + | VisualizationBooleanFilterOperator interface Props { dataframe: Pick @@ -364,6 +414,13 @@ function FilterSelector(props: Props) { return } + if (NumpyBoolTypes.safeParse(column.type).success) { + if (!isBooleanOperator(operator)) { + setOperator('isTrue') + } + return + } + if (NumpyDateTypes.safeParse(column.type).success) { if (!isDateOperator(operator)) { setOperator('eq') @@ -418,6 +475,23 @@ function FilterSelector(props: Props) { } } + if ( + NumpyBoolTypes.safeParse(column.type).success + ) { + if (isBooleanOperator(operator)) { + const filter = VisualizationBooleanFilter.safeParse({ + id: props.filter.id, + column, + operator, + value, + }) + if (filter.success) { + props.onChange(filter.data) + return + } + } + } + if (NumpyDateTypes.safeParse(column.type).success) { if (isDateOperator(operator)) { const filter = VisualizationDateFilter.safeParse({ @@ -514,7 +588,7 @@ function FilterSelector(props: Props) { } } - if (column && (newOp === 'isNull' || newOp === 'isNotNull')) { + if (column && (newOp === 'isNull' || newOp === 'isNotNull' || newOp === 'isTrue' || newOp === 'isFalse')) { if ( NumpyNumberTypes.or(NumpyTimeDeltaTypes).safeParse(column.type) .success @@ -528,6 +602,12 @@ function FilterSelector(props: Props) { setValue('filter') } + if ( + NumpyBoolTypes.safeParse(column.type).success + ) { + setValue('filter') // FIXME: Improve value handling for boolean filtering + } + if (NumpyDateTypes.safeParse(column.type).success) { setValue(new Date().toISOString()) } @@ -636,7 +716,7 @@ function FilterSelector(props: Props) { {column?.name ?? 'New filter'} - {operator !== 'isNull' && operator !== 'isNotNull' ? ( + {operator !== 'isNull' && operator !== 'isNotNull' && operator !== 'isTrue' && operator !== 'isFalse' ? ( <> {renderedValue ? ( @@ -719,6 +801,7 @@ function FilterSelector(props: Props) { | VisualizationNumberFilterOperator | VisualizationDateFilterOperator | VisualizationStringFilterOperator + | VisualizationBooleanFilterOperator > icon={() => null} label="Operator" @@ -730,7 +813,7 @@ function FilterSelector(props: Props) { placeholder="Operator" disabled={props.disabled} /> - {operator !== 'isNull' && operator !== 'isNotNull' && ( + {operator !== 'isNull' && operator !== 'isNotNull' && operator !== 'isTrue' && operator !== 'isFalse' && (
{VisualizationStringFilterMultiValuesOperator.safeParse( operator diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1194c37f..fa5ba612 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -397,6 +397,8 @@ export type VisualizationStringFilterSingleValueOperator = z.infer< export const VisualizationOperatorWithoutValue = z.union([ z.literal('isNull'), z.literal('isNotNull'), + z.literal('isTrue'), + z.literal('isFalse'), ]) export type VisualizationOperatorWithoutValue = z.infer< typeof VisualizationOperatorWithoutValue @@ -523,10 +525,33 @@ export const VisualizationDateFilter = z.object({ }) export type VisualizationDateFilter = z.infer +export const VisualizationBooleanFilterOperator = z.union([ + z.literal('isTrue'), + z.literal('isFalse'), +]) +export type VisualizationBooleanFilterOperator = z.infer< + typeof VisualizationBooleanFilterOperator +> +export const booleanFilterOperators: VisualizationBooleanFilterOperator[] = [ + 'isTrue', + 'isFalse', +] + +export const VisualizationBooleanFilter = z.object({ + id: uuidSchema, + column: DataFrameBooleanColumn, + operator: VisualizationBooleanFilterOperator, + value: z.string().optional(), + renderError: PythonErrorOutput.optional(), + renderedValue: z.string().optional(), +}) +export type VisualizationBooleanFilter = z.infer + const VisualizationFilterOperator = z.union([ VisualizationNumberFilterOperator, VisualizationStringFilterOperator, VisualizationDateFilterOperator, + VisualizationBooleanFilterOperator, ]) export const UnfinishedVisualizationFilter = z.object({ @@ -544,6 +569,7 @@ export const VisualizationFilter = z.union([ VisualizationStringFilter, VisualizationNumberFilter, VisualizationDateFilter, + VisualizationBooleanFilter, UnfinishedVisualizationFilter, ]) export type VisualizationFilter = z.infer