diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
index fca1fb372e4a..8c8defeb18ee 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
@@ -51,7 +51,7 @@ const formMock: FormInstance = {
const filterMock: Filter = {
cascadeParentIds: [],
defaultDataMask: {},
- id: 'mock',
+ id: 'filterMockId',
name: 'mock',
scope: {
rootPath: [],
@@ -69,7 +69,7 @@ const createProps: () => ControlItemsProps = () => ({
disabled: false,
forceUpdate: jest.fn(),
form: formMock,
- filterId: 'filterId',
+ filterId: 'filterMockId',
filterToEdit: filterMock,
filterType: 'filterType',
});
@@ -78,8 +78,25 @@ const createControlItems = () => [
null,
false,
{},
- { name: 'name_1', config: { renderTrigger: true, resetConfig: true } },
+ {
+ name: 'name_1',
+ config: { type: 'CheckboxControl', renderTrigger: true, resetConfig: true },
+ },
{ name: 'groupby', config: { multiple: true, required: false } },
+ {
+ name: 'select_1',
+ config: {
+ type: 'SelectControl',
+ renderTrigger: true,
+ freeForm: true,
+ default: 1,
+ choices: [
+ [0, 'choice_0'],
+ [1, 'choice_1'],
+ [2, 'choice_2'],
+ ],
+ },
+ },
];
beforeEach(() => {
@@ -146,7 +163,10 @@ test('Should render render ControlItems', () => {
const controlItems = [
...createControlItems(),
- { name: 'name_2', config: { renderTrigger: true } },
+ {
+ name: 'name_2',
+ config: { type: 'CheckboxControl', renderTrigger: true },
+ },
];
(getControlItems as jest.Mock).mockReturnValue(controlItems);
const controlItemsMap = getControlItemsMap(props);
@@ -169,7 +189,14 @@ test('Clickin on checkbox', () => {
test('Clickin on checkbox when resetConfig:flase', () => {
const props = createProps();
(getControlItems as jest.Mock).mockReturnValue([
- { name: 'name_1', config: { renderTrigger: true, resetConfig: false } },
+ {
+ name: 'name_1',
+ config: {
+ type: 'CheckboxControl',
+ renderTrigger: true,
+ resetConfig: false,
+ },
+ },
]);
const controlItemsMap = getControlItemsMap(props);
renderControlItems(controlItemsMap);
@@ -179,3 +206,26 @@ test('Clickin on checkbox when resetConfig:flase', () => {
expect(props.forceUpdate).toBeCalled();
expect(setNativeFilterFieldValues).not.toBeCalled();
});
+
+test('Should render SelectControl ControlItems', () => {
+ const props = createProps();
+ const controlItems = [...createControlItems()];
+ (getControlItems as jest.Mock).mockReturnValue(controlItems);
+ const controlItemsMap = getControlItemsMap(props);
+ renderControlItems(controlItemsMap);
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
+});
+
+test('Should change selection for SelectControl', () => {
+ const props = createProps();
+ const controlItems = [...createControlItems()];
+ (getControlItems as jest.Mock).mockReturnValue(controlItems);
+ const controlItemsMap = getControlItemsMap(props);
+ renderControlItems(controlItemsMap);
+ userEvent.click(screen.getByRole('combobox'));
+ userEvent.click(screen.getByTitle('choice_2'));
+ expect(setNativeFilterFieldValues).toBeCalledWith(formMock, filterMock.id, {
+ select_1: 2,
+ defaultDataMask: null,
+ });
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
index 22078c26a926..4e804d2f4cd9 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -19,6 +19,7 @@
import {
CustomControlItem,
InfoTooltipWithTrigger,
+ ControlValueValidator,
} from '@superset-ui/chart-controls';
import React from 'react';
import { AntdCheckbox, FormInstance } from 'src/components';
@@ -30,6 +31,8 @@ import {
} from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { FormItem } from 'src/components/Form';
+import SelectControl from 'src/explore/components/controls/SelectControl';
+import { RuleObject } from 'antd/lib/form';
import {
doesColumnMatchFilterType,
getControlItems,
@@ -59,6 +62,25 @@ const CleanFormItem = styled(FormItem)`
margin-bottom: 0;
`;
+// converts the controlPanel validators to an rc-field-form rule
+const getControlItemRules = (
+ validators: ControlValueValidator[] | undefined,
+) => {
+ if (validators) {
+ const v = validators.map(validator => ({
+ validator: (_rule: RuleObject, value: unknown) => {
+ const errorMessage = validator(value);
+ if (errorMessage !== false && errorMessage !== true) {
+ return Promise.reject(new Error(errorMessage));
+ }
+ return Promise.resolve();
+ },
+ }));
+ return v;
+ }
+ return undefined;
+};
+
export default function getControlItemsMap({
datasetId,
disabled,
@@ -149,6 +171,7 @@ export default function getControlItemsMap({
(controlItem: CustomControlItem) =>
controlItem?.config?.renderTrigger &&
controlItem.name !== 'sortAscending' &&
+ controlItem?.config?.type === 'CheckboxControl' &&
controlItem.name !== 'enableSingleValue',
)
.forEach(controlItem => {
@@ -214,6 +237,68 @@ export default function getControlItemsMap({
);
mapControlItems[controlItem.name] = { element, checked: initialValue };
});
+ controlItems
+ .filter(
+ (controlItem: CustomControlItem) =>
+ controlItem?.config?.renderTrigger &&
+ controlItem?.config?.type === 'SelectControl',
+ )
+ .forEach(controlItem => {
+ const initialValue =
+ filterToEdit?.controlValues?.[controlItem.name] ??
+ controlItem?.config?.default;
+ const element = (
+ <>
+
+
+
+ {t(`${controlItem.config?.label}`) || t('Select')}
+
+ }
+ rules={getControlItemRules(controlItem.config.validators)}
+ >
+ {
+ setNativeFilterFieldValues(form, filterId, {
+ [controlItem.name]: value,
+ defaultDataMask: null,
+ });
+ forceUpdate();
+ }}
+ value={controlItem.config.value || initialValue}
+ choices={controlItem.config.choices}
+ />
+
+
+ >
+ );
+ mapControlItems[controlItem.name] = { element, checked: false };
+ });
return {
controlItems: mapControlItems,
mainControlItems: mapMainControlItems,
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
index 7dac323d6b13..6990c2185551 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
@@ -22,6 +22,11 @@ import { render } from 'spec/helpers/testing-library';
import RangeFilterPlugin from './RangeFilterPlugin';
import { SingleValueType } from './SingleValueType';
import transformProps from './transformProps';
+import {
+ PluginFilterRangeScalingFunctions,
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION,
+} from './types';
+import { roundDecimals } from '../../utils';
const rangeProps = {
formData: {
@@ -62,6 +67,7 @@ const rangeProps = {
urlParams: {},
vizType: 'filter_range',
inputRef: { current: null },
+ scaling: PluginFilterRangeScalingFunctions.LINEAR,
},
height: 20,
hooks: {},
@@ -82,9 +88,28 @@ const rangeProps = {
appSection: AppSection.DASHBOARD,
};
+describe('ScalingFunctions', () => {
+ Object.keys(PluginFilterRangeScalingFunctions).forEach(scaling => {
+ [0, 1e-5, 1e-2, 1, 1e4, 1e6, 1e8].forEach(val =>
+ it(`inverse should undo transform (using ${scaling}, ${val})`, () =>
+ expect(
+ // make sure it's good to 6 decimal places
+ roundDecimals(
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].inverseScale(
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale(
+ val,
+ ),
+ ),
+ 6,
+ ),
+ ).toEqual(val)),
+ );
+ });
+});
+
describe('RangeFilterPlugin', () => {
const setDataMask = jest.fn();
- const getWrapper = (props = {}) =>
+ const getWrapper = (props = {}, filterState = {}) =>
render(
// @ts-ignore
{
{...transformProps({
...rangeProps,
formData: { ...rangeProps.formData, ...props },
+ filterState,
})}
setDataMask={setDataMask}
/>,
@@ -101,22 +127,43 @@ describe('RangeFilterPlugin', () => {
jest.clearAllMocks();
});
- it('should call setDataMask with correct filter', () => {
- getWrapper();
- expect(setDataMask).toHaveBeenCalledWith({
- extraFormData: {
- filters: [
- {
- col: 'SP_POP_TOTL',
- op: '<=',
- val: 70,
- },
- ],
- },
- filterState: {
- label: 'x ≤ 70',
- value: [10, 70],
- },
+ Object.keys(PluginFilterRangeScalingFunctions).forEach(scaling => {
+ it(`should call setDataMask with correct filter (using ${scaling})`, () => {
+ getWrapper(
+ { scaling },
+ {
+ value: [
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale(
+ 10,
+ ),
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale(
+ 70,
+ ),
+ ],
+ },
+ );
+ expect(setDataMask).toHaveBeenCalledWith({
+ extraFormData: {
+ filters: [
+ {
+ col: 'SP_POP_TOTL',
+ op: '<=',
+ val: 70,
+ },
+ ],
+ },
+ filterState: {
+ label: 'x ≤ 70',
+ value: [
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale(
+ 10,
+ ),
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale(
+ 70,
+ ),
+ ],
+ },
+ });
});
});
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
index d2cc9a417132..542b3d4a8868 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
@@ -27,9 +27,17 @@ import {
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { AntdSlider } from 'src/components';
import { rgba } from 'emotion-rgba';
-import { PluginFilterRangeProps } from './types';
+
+// could add in scalePow and others
+// import { scaleLog, scaleLinear } from 'd3-scale';
+import {
+ PluginFilterRangeProps,
+ PluginFilterRangeScalingFunctions,
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION,
+} from './types';
+
import { StatusMessage, StyledFormItem, FilterPluginStyle } from '../common';
-import { getRangeExtraFormData } from '../../utils';
+import { getRangeExtraFormData, roundDecimals } from '../../utils';
import { SingleValueType } from './SingleValueType';
const LIGHT_BLUE = '#99e7f0';
@@ -109,40 +117,6 @@ const Wrapper = styled.div<{ validateStatus?: 'error' | 'warning' | 'info' }>`
`}
`;
-const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
-
-const tipFormatter = (value: number) => numberFormatter(value);
-
-const getLabel = (lower: number | null, upper: number | null): string => {
- if (lower !== null && upper !== null && lower === upper) {
- return `x = ${numberFormatter(lower)}`;
- }
- if (lower !== null && upper !== null) {
- return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`;
- }
- if (lower !== null) {
- return `x ≥ ${numberFormatter(lower)}`;
- }
- if (upper !== null) {
- return `x ≤ ${numberFormatter(upper)}`;
- }
- return '';
-};
-
-const getMarks = (
- lower: number | null,
- upper: number | null,
-): { [key: number]: string } => {
- const newMarks: { [key: number]: string } = {};
- if (lower !== null) {
- newMarks[lower] = numberFormatter(lower);
- }
- if (upper !== null) {
- newMarks[upper] = numberFormatter(upper);
- }
- return newMarks;
-};
-
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
const {
data,
@@ -156,25 +130,121 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
filterState,
inputRef,
} = props;
+ const {
+ groupby,
+ defaultValue,
+ stepSize = 1,
+ scaling = PluginFilterRangeScalingFunctions.LINEAR,
+ enableSingleValue,
+ } = formData;
+
+ const transformScale = useCallback(
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].transformScale,
+ [scaling],
+ );
+ const inverseScale = useCallback(
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[scaling].inverseScale,
+ [scaling],
+ );
+ // # digits of precision in the step size? Doesn't work for multi digit
+ const stepSizeDecimals = Math.abs(Math.floor(Math.log10(stepSize)));
const [row] = data;
// @ts-ignore
- const { min, max }: { min: number; max: number } = row;
- const { groupby, defaultValue, enableSingleValue } = formData;
+ const { min, max } = useMemo(() => {
+ // @ts-ignore
+ const { min: tempMin, max: tempMax }: { min: number; max: number } = row;
+ return {
+ min: tempMin,
+ // this is ugly but it bumps the max val up to an integer multiple of
+ // stepSize greater than the min value
+ max: inverseScale(
+ roundDecimals(
+ transformScale(
+ tempMin +
+ inverseScale(
+ roundDecimals(
+ // limits must be integer multiples of the stepSize
+ Math.ceil(transformScale(tempMax - tempMin) / stepSize) *
+ stepSize,
+ stepSizeDecimals,
+ ),
+ ),
+ ),
+ stepSizeDecimals,
+ ),
+ ),
+ };
+ }, [row, stepSize]);
+ const minTransformed = transformScale(min);
+ const maxTransformed = transformScale(max);
const enableSingleMinValue = enableSingleValue === SingleValueType.Minimum;
const enableSingleMaxValue = enableSingleValue === SingleValueType.Maximum;
const enableSingleExactValue = enableSingleValue === SingleValueType.Exact;
- const rangeValue = enableSingleValue === undefined;
+ // value can be false for a dashboard filter
+ const rangeValue =
+ enableSingleValue === undefined || enableSingleValue === false;
const [col = ''] = ensureIsArray(groupby).map(getColumnLabel);
+
+ const numberFormatter = useMemo(() => {
+ if (stepSize < 1) {
+ // need to make sure displayed numbers show the correct sig figs
+ return getNumberFormatter(`.${stepSizeDecimals}f`);
+ }
+ return getNumberFormatter(NumberFormats.SMART_NUMBER);
+ }, [stepSize, stepSizeDecimals]);
+
+ // lower and upper are NOT transformed!!!!
+ const getLabel = (lower: number | null, upper: number | null): string => {
+ if (lower !== null && upper !== null && lower === upper) {
+ return `x = ${numberFormatter(lower)}`;
+ }
+ if (lower !== null && upper !== null) {
+ return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`;
+ }
+ if (lower !== null) {
+ return `x ≥ ${numberFormatter(lower)}`;
+ }
+ if (upper !== null) {
+ return `x ≤ ${numberFormatter(upper)}`;
+ }
+ return '';
+ };
+
const [value, setValue] = useState<[number, number]>(
- defaultValue ?? [min, enableSingleExactValue ? min : max],
+ (defaultValue ?? [min, enableSingleExactValue ? min : max]).map(
+ transformScale,
+ ),
);
const [marks, setMarks] = useState<{ [key: number]: string }>({});
const minIndex = 0;
const maxIndex = 1;
- const minMax = value ?? [min, max];
+ // minMax uses transformed values
+ const minMax = useMemo(
+ () => value ?? [minTransformed ?? 0, maxTransformed],
+ [maxTransformed, minTransformed, value],
+ );
+
+ const tipFormatter = (value: number) =>
+ numberFormatter(inverseScale(Number(value)));
+ // lower & upper are transformed
+ const getMarks = useCallback(
+ (lower: number | null, upper: number | null): { [key: number]: string } => {
+ const newMarks: { [key: number]: string } = {};
+ if (lower !== null) {
+ newMarks[lower] = numberFormatter(inverseScale(lower));
+ }
+ if (upper !== null) {
+ newMarks[upper] = numberFormatter(inverseScale(upper));
+ }
+ return newMarks;
+ },
+ [inverseScale, value],
+ );
+
+ // value is transformed
const getBounds = useCallback(
(
value: [number, number],
@@ -186,41 +256,75 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
}
return {
- lower: lowerRaw > min ? lowerRaw : null,
- upper: upperRaw < max ? upperRaw : null,
+ lower: lowerRaw > Number(minTransformed) ? lowerRaw : null,
+ upper: upperRaw < Number(maxTransformed) ? upperRaw : null,
};
},
- [max, min, enableSingleExactValue],
+ [maxTransformed, minTransformed, value, enableSingleExactValue],
);
const handleAfterChange = useCallback(
(value: [number, number]): void => {
- setValue(value);
- const { lower, upper } = getBounds(value);
+ // value is transformed, min and max are NOT transformed
+ let val = value;
+ if (value[0] === min && value[1] === max) {
+ // after a filter value reset, make sure it's a transformed value
+ val = [transformScale(value[0]), transformScale(value[1])];
+ }
+ // FIXME: this doesn't work properly. Eg, stepSize = 0.35 for [0, 1]
+ // antd apparently uses the floor value, not the rounded value...?
+ // which causes issues like log(123) = 2.0899
+ if (val[1] >= roundDecimals(maxTransformed, stepSizeDecimals)) {
+ val = [val[0], maxTransformed];
+ }
+ if (val[0] <= roundDecimals(minTransformed, stepSizeDecimals)) {
+ val = [minTransformed, val[1]];
+ }
+ // value is transformed
+ setValue(val);
+ // lower & upper are transformed
+ const { lower, upper } = getBounds(val);
setMarks(getMarks(lower, upper));
-
+ // removed Number
setDataMask({
- extraFormData: getRangeExtraFormData(col, lower, upper),
+ extraFormData: getRangeExtraFormData(
+ col,
+ inverseScale(lower),
+ inverseScale(upper),
+ ),
filterState: {
- value: lower !== null || upper !== null ? value : null,
- label: getLabel(lower, upper),
+ value: lower !== null || upper !== null ? val : null,
+ label: getLabel(inverseScale(lower), inverseScale(upper)),
},
});
},
- [col, getBounds, setDataMask],
+ [
+ col,
+ getBounds,
+ setDataMask,
+ getMarks,
+ inverseScale,
+ transformScale,
+ stepSizeDecimals,
+ ],
);
+ // value is transformed
const handleChange = useCallback((value: [number, number]) => {
setValue(value);
}, []);
+ // value is transformed
useEffect(() => {
// when switch filter type and queriesData still not updated we need ignore this case (in FilterBar)
if (row?.min === undefined && row?.max === undefined) {
return;
}
- let filterStateValue = filterState.value ?? [min, max];
+ let filterStateValue = filterState.value ?? [
+ minTransformed,
+ maxTransformed,
+ ];
if (enableSingleMaxValue) {
const filterStateMax =
filterStateValue[maxIndex] <= minMax[maxIndex]
@@ -296,49 +400,53 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
>
{enableSingleMaxValue && (
handleAfterChange([min, value])}
onChange={value => handleChange([min, value])}
+ step={stepSize}
/>
)}
{enableSingleMinValue && (
handleAfterChange([value, max])}
onChange={value => handleChange([value, max])}
+ step={stepSize}
/>
)}
{enableSingleExactValue && (
handleAfterChange([value, value])}
onChange={value => handleChange([value, value])}
+ step={stepSize}
/>
)}
{rangeValue && (
)}
diff --git a/superset-frontend/src/filters/components/Range/controlPanel.ts b/superset-frontend/src/filters/components/Range/controlPanel.ts
index 076d5a44aa0d..3f76ec18220e 100644
--- a/superset-frontend/src/filters/components/Range/controlPanel.ts
+++ b/superset-frontend/src/filters/components/Range/controlPanel.ts
@@ -16,13 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { t } from '@superset-ui/core';
+import { t, validateNonEmpty, validateNumber } from '@superset-ui/core';
import {
ControlPanelConfig,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { SingleValueType } from './SingleValueType';
+import {
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION,
+ PluginFilterRangeScalingFunctions,
+} from './types';
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -71,6 +75,49 @@ const config: ControlPanelConfig = {
},
},
],
+ [
+ {
+ name: 'scaling',
+ config: {
+ type: 'SelectControl',
+ label: t('Scaling Function'),
+ default: PluginFilterRangeScalingFunctions.LINEAR,
+ renderTrigger: true,
+ freeForm: false,
+ choices: Object.keys(
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION,
+ ).map(key => [
+ key,
+ SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION[key].display,
+ ]),
+ description: t('Choose a scaling function for the slider.'),
+ },
+ },
+ {
+ name: 'stepSize',
+ config: {
+ type: 'SelectControl',
+ label: t('Step Size'),
+ default: 1,
+ renderTrigger: true,
+ validators: [validateNonEmpty, validateNumber],
+ freeForm: true,
+ choices: [
+ [0.001, 0.001],
+ [0.01, 0.01],
+ [0.1, 0.1],
+ [1, 1],
+ [2, 2],
+ [10, 10],
+ [25, 25],
+ [100, 100],
+ ],
+ description: t(
+ 'Set the slider step size. Note, the step size is in increments of the transformed scale, not the original value scale.',
+ ),
+ },
+ },
+ ],
],
},
],
diff --git a/superset-frontend/src/filters/components/Range/types.ts b/superset-frontend/src/filters/components/Range/types.ts
index 69ad4d53da80..0e72671b6aa6 100644
--- a/superset-frontend/src/filters/components/Range/types.ts
+++ b/superset-frontend/src/filters/components/Range/types.ts
@@ -25,14 +25,17 @@ import {
import { RefObject } from 'react';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
-interface PluginFilterSelectCustomizeProps {
+interface PluginFilterRangeCustomizeProps {
max?: number;
min?: number;
+ stepSize: number;
+ logScale?: boolean;
+ scaling: string;
}
export type PluginFilterRangeQueryFormData = QueryFormData &
PluginFilterStylesProps &
- PluginFilterSelectCustomizeProps;
+ PluginFilterRangeCustomizeProps;
export type PluginFilterRangeProps = PluginFilterStylesProps & {
data: DataRecord[];
@@ -41,3 +44,58 @@ export type PluginFilterRangeProps = PluginFilterStylesProps & {
behaviors: Behavior[];
inputRef: RefObject;
} & PluginFilterHooks;
+
+export enum PluginFilterRangeScalingFunctions {
+ LINEAR = 'LINEAR',
+ LOG = 'LOG',
+ SQRT = 'SQRT',
+ CBRT = 'CBRT',
+ QDRT = 'QDRT',
+ SQUARED = 'SQUARED',
+}
+
+export interface PluginFilterRangeScalingFunction {
+ display: string;
+ transformScale: (val: number | null) => number | null;
+ inverseScale: (val: number | null) => number | null;
+}
+
+export const SCALING_FUNCTION_ENUM_TO_SCALING_FUNCTION: {
+ [key in PluginFilterRangeScalingFunctions]: PluginFilterRangeScalingFunction;
+} = {
+ [PluginFilterRangeScalingFunctions.LINEAR]: {
+ display: 'Linear',
+ transformScale: (val: number | null) => val,
+ inverseScale: (val: number | null) => val,
+ },
+ [PluginFilterRangeScalingFunctions.LOG]: {
+ display: 'Log Base 10',
+ transformScale: (val: number | null) =>
+ val ? (val > 0 ? Math.log10(val + 1) : 0) : val,
+ inverseScale: (val: number | null) => (val ? Math.pow(10, val) - 1 : val),
+ },
+ [PluginFilterRangeScalingFunctions.SQRT]: {
+ display: 'Square Root',
+ transformScale: (val: number | null) =>
+ val ? (val > 0 ? Math.sqrt(val) : 0) : val,
+ inverseScale: (val: number | null) => (val ? Math.pow(val, 2) : val),
+ },
+ [PluginFilterRangeScalingFunctions.CBRT]: {
+ display: 'Cube Root',
+ transformScale: (val: number | null) =>
+ val ? (val > 0 ? Math.cbrt(val) : 0) : val,
+ inverseScale: (val: number | null) => (val ? Math.pow(val, 3) : val),
+ },
+ [PluginFilterRangeScalingFunctions.QDRT]: {
+ display: 'Quad Root',
+ transformScale: (val: number | null) =>
+ val ? (val > 0 ? Math.pow(val, 1 / 4) : 0) : val,
+ inverseScale: (val: number | null) => (val ? Math.pow(val, 4) : val),
+ },
+ [PluginFilterRangeScalingFunctions.SQUARED]: {
+ display: 'Squared',
+ transformScale: (val: number | null) => (val ? Math.pow(val, 2) : val),
+ inverseScale: (val: number | null) =>
+ val ? (val > 0 ? Math.sqrt(val) : 0) : val,
+ },
+};
diff --git a/superset-frontend/src/filters/utils.ts b/superset-frontend/src/filters/utils.ts
index 4908f1a2893c..14cc52754aca 100644
--- a/superset-frontend/src/filters/utils.ts
+++ b/superset-frontend/src/filters/utils.ts
@@ -26,6 +26,10 @@ import {
} from '@superset-ui/core';
import { FALSE_STRING, NULL_STRING, TRUE_STRING } from 'src/utils/common';
+// round a float to this many decimal values
+export const roundDecimals = (num: number, decimals: number) =>
+ Math.round((num + Number.EPSILON) * 10 ** decimals) / 10 ** decimals;
+
export const getSelectExtraFormData = (
col: string,
value?: null | (string | number | boolean | null)[],