From aad4eea933b2bb266a74be29eb22016a29ab3325 Mon Sep 17 00:00:00 2001 From: Ariel Salem Date: Tue, 12 May 2020 14:36:23 -0700 Subject: [PATCH] feat(y_axis_domain): updated y-axis domain settings to allow min OR max values to be set. (#18040) --- CHANGELOG.md | 1 + ui/cypress/e2e/explorer.test.ts | 28 +++++++++++ ui/src/shared/components/AutoDomainInput.tsx | 39 ++++++---------- ui/src/shared/components/HeatmapPlot.tsx | 9 ++-- ui/src/shared/components/HistogramPlot.tsx | 4 +- ui/src/shared/components/ScatterPlot.tsx | 9 ++-- ui/src/shared/components/XYPlot.tsx | 17 ++++--- .../shared/utils/useVisDomainSettings.test.ts | 46 ++++++++++++++++++- ui/src/shared/utils/useVisDomainSettings.ts | 43 ++++++++++++++++- ui/src/shared/utils/vis.test.ts | 18 ++++++++ ui/src/shared/utils/vis.ts | 14 +++++- .../components/view_options/LineOptions.tsx | 13 ++++-- 12 files changed, 191 insertions(+), 50 deletions(-) create mode 100644 ui/src/shared/utils/vis.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d068ba2b0a..6ca860dae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features 1. [18011](https://github.com/influxdata/influxdb/pull/18011): Integrate UTC dropdown when making custom time range query +1. [18040](https://github.com/influxdata/influxdb/pull/18040): Allow for min OR max y-axis visualization settings rather than min AND max 1. [17764](https://github.com/influxdata/influxdb/pull/17764): Add CSV to line protocol conversion library ### Bug Fixes diff --git a/ui/cypress/e2e/explorer.test.ts b/ui/cypress/e2e/explorer.test.ts index caf77d74e5..53e2001108 100644 --- a/ui/cypress/e2e/explorer.test.ts +++ b/ui/cypress/e2e/explorer.test.ts @@ -681,6 +681,34 @@ describe('DataExplorer', () => { cy.getByTestID('raw-data--toggle').click() }) + it('can set min or max y-axis values', () => { + // build the query to return data from beforeEach + cy.getByTestID(`selector-list m`).click() + cy.getByTestID('selector-list v').click() + cy.getByTestID(`selector-list tv1`).click() + + cy.getByTestID('time-machine-submit-button').click() + cy.getByTestID('cog-cell--button').click() + cy.getByTestID('select-group--option') + .contains('Custom') + .click() + cy.getByTestID('auto-domain--min') + .type('-100') + .blur() + + cy.getByTestID('form--element-error').should('not.exist') + // find no errors + cy.getByTestID('auto-domain--max') + .type('450') + .blur() + // find no errors + cy.getByTestID('form--element-error').should('not.exist') + cy.getByTestID('auto-domain--min') + .clear() + .blur() + cy.getByTestID('form--element-error').should('not.exist') + }) + it('can view table data & sort values numerically', () => { // build the query to return data from beforeEach cy.getByTestID(`selector-list m`).click() diff --git a/ui/src/shared/components/AutoDomainInput.tsx b/ui/src/shared/components/AutoDomainInput.tsx index a9266bb60a..7cfeff3838 100644 --- a/ui/src/shared/components/AutoDomainInput.tsx +++ b/ui/src/shared/components/AutoDomainInput.tsx @@ -14,35 +14,25 @@ interface MinMaxInputsProps { initialMin: string initialMax: string onSetMinMax: (minMax: [number, number]) => void - onSetErrorMessage: (errorMessage: string) => void } const MinMaxInputs: SFC = ({ initialMin, initialMax, onSetMinMax, - onSetErrorMessage, }) => { const [minInput, setMinInput] = useOneWayState(initialMin) const [maxInput, setMaxInput] = useOneWayState(initialMax) const emitIfValid = () => { - const newMin = parseFloat(minInput) - const newMax = parseFloat(maxInput) - + let newMin = parseFloat(minInput) + let newMax = parseFloat(maxInput) if (isNaN(newMin)) { - onSetErrorMessage('Must supply a valid minimum value') - return + newMin = null } if (isNaN(newMax)) { - onSetErrorMessage('Must supply a valid maximum value') - return - } - - if (newMin >= newMax) { - onSetErrorMessage('Minium value must be less than maximum') - return + newMax = null } if (initialMin === minInput && initialMax === maxInput) { @@ -50,7 +40,6 @@ const MinMaxInputs: SFC = ({ return } - onSetErrorMessage('') onSetMinMax([newMin, newMax]) } @@ -69,6 +58,7 @@ const MinMaxInputs: SFC = ({ onChange={e => setMinInput(e.target.value)} onBlur={emitIfValid} onKeyPress={handleKeyPress} + testID="auto-domain--min" /> @@ -79,6 +69,7 @@ const MinMaxInputs: SFC = ({ onChange={e => setMaxInput(e.target.value)} onBlur={emitIfValid} onKeyPress={handleKeyPress} + testID="auto-domain--max" /> @@ -86,6 +77,10 @@ const MinMaxInputs: SFC = ({ ) } +const formatDomainValue = (value: number | null): string => { + return value === null ? '' : String(value) +} + interface AutoDomainInputProps { domain: [number, number] onSetDomain: (domain: [number, number]) => void @@ -98,28 +93,21 @@ const AutoDomainInput: SFC = ({ label = 'Set Domain', }) => { const [showInputs, setShowInputs] = useState(!!domain) - const [errorMessage, setErrorMessage] = useState('') const handleChooseAuto = () => { setShowInputs(false) - setErrorMessage('') onSetDomain(null) } const handleChooseCustom = () => { setShowInputs(true) - setErrorMessage('') } - const initialMin = domain ? String(domain[0]) : '' - const initialMax = domain ? String(domain[1]) : '' + const initialMin = Array.isArray(domain) ? formatDomainValue(domain[0]) : '' + const initialMax = Array.isArray(domain) ? formatDomainValue(domain[1]) : '' return ( - + @@ -153,7 +141,6 @@ const AutoDomainInput: SFC = ({ initialMin={initialMin} initialMax={initialMax} onSetMinMax={onSetDomain} - onSetErrorMessage={setErrorMessage} /> )} diff --git a/ui/src/shared/components/HeatmapPlot.tsx b/ui/src/shared/components/HeatmapPlot.tsx index 76122852ad..997a4dae11 100644 --- a/ui/src/shared/components/HeatmapPlot.tsx +++ b/ui/src/shared/components/HeatmapPlot.tsx @@ -7,7 +7,10 @@ import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage' import GraphLoadingDots from 'src/shared/components/GraphLoadingDots' // Utils -import {useVisDomainSettings} from 'src/shared/utils/useVisDomainSettings' +import { + useVisXDomainSettings, + useVisYDomainSettings, +} from 'src/shared/utils/useVisDomainSettings' import {getFormatter} from 'src/shared/utils/vis' // Constants @@ -59,13 +62,13 @@ const HeatmapPlot: FunctionComponent = ({ }) => { const columnKeys = table.columnKeys - const [xDomain, onSetXDomain, onResetXDomain] = useVisDomainSettings( + const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings( storedXDomain, table.getColumn(xColumn, 'number'), timeRange ) - const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings( + const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings( storedYDomain, table.getColumn(yColumn, 'number') ) diff --git a/ui/src/shared/components/HistogramPlot.tsx b/ui/src/shared/components/HistogramPlot.tsx index 896301953d..8a8e8b8e80 100644 --- a/ui/src/shared/components/HistogramPlot.tsx +++ b/ui/src/shared/components/HistogramPlot.tsx @@ -7,7 +7,7 @@ import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage' import GraphLoadingDots from 'src/shared/components/GraphLoadingDots' // Utils -import {useVisDomainSettings} from 'src/shared/utils/useVisDomainSettings' +import {useVisXDomainSettings} from 'src/shared/utils/useVisDomainSettings' import {getFormatter} from 'src/shared/utils/vis' // Constants @@ -50,7 +50,7 @@ const HistogramPlot: FunctionComponent = ({ }) => { const columnKeys = table.columnKeys - const [xDomain, onSetXDomain, onResetXDomain] = useVisDomainSettings( + const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings( storedXDomain, table.getColumn(xColumn, 'number') ) diff --git a/ui/src/shared/components/ScatterPlot.tsx b/ui/src/shared/components/ScatterPlot.tsx index a127658108..930727ab7a 100644 --- a/ui/src/shared/components/ScatterPlot.tsx +++ b/ui/src/shared/components/ScatterPlot.tsx @@ -7,7 +7,10 @@ import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage' import GraphLoadingDots from 'src/shared/components/GraphLoadingDots' // Utils -import {useVisDomainSettings} from 'src/shared/utils/useVisDomainSettings' +import { + useVisXDomainSettings, + useVisYDomainSettings, +} from 'src/shared/utils/useVisDomainSettings' import { getFormatter, defaultXColumn, @@ -71,13 +74,13 @@ const ScatterPlot: FunctionComponent = ({ const columnKeys = table.columnKeys - const [xDomain, onSetXDomain, onResetXDomain] = useVisDomainSettings( + const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings( storedXDomain, table.getColumn(xColumn, 'number'), timeRange ) - const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings( + const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings( storedYDomain, table.getColumn(yColumn, 'number') ) diff --git a/ui/src/shared/components/XYPlot.tsx b/ui/src/shared/components/XYPlot.tsx index 664611fe44..831187cf91 100644 --- a/ui/src/shared/components/XYPlot.tsx +++ b/ui/src/shared/components/XYPlot.tsx @@ -13,12 +13,16 @@ import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage' import GraphLoadingDots from 'src/shared/components/GraphLoadingDots' // Utils -import {useVisDomainSettings} from 'src/shared/utils/useVisDomainSettings' +import { + useVisXDomainSettings, + useVisYDomainSettings, +} from 'src/shared/utils/useVisDomainSettings' import { getFormatter, geomToInterpolation, filterNoisyColumns, - parseBounds, + parseXBounds, + parseYBounds, defaultXColumn, defaultYColumn, } from 'src/shared/utils/vis' @@ -82,9 +86,8 @@ const XYPlot: FunctionComponent = ({ }, theme, }) => { - const storedXDomain = useMemo(() => parseBounds(xBounds), [xBounds]) - const storedYDomain = useMemo(() => parseBounds(yBounds), [yBounds]) - + const storedXDomain = useMemo(() => parseXBounds(xBounds), [xBounds]) + const storedYDomain = useMemo(() => parseYBounds(yBounds), [yBounds]) const xColumn = storedXColumn || defaultXColumn(table) const yColumn = storedYColumn || defaultYColumn(table) @@ -109,7 +112,7 @@ const XYPlot: FunctionComponent = ({ const groupKey = [...fluxGroupKeyUnion, 'result'] - const [xDomain, onSetXDomain, onResetXDomain] = useVisDomainSettings( + const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings( storedXDomain, table.getColumn(xColumn, 'number'), timeRange @@ -130,7 +133,7 @@ const XYPlot: FunctionComponent = ({ return table.getColumn(yColumn, 'number') }, [table, yColumn, position]) - const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings( + const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings( storedYDomain, memoizedYColumnData ) diff --git a/ui/src/shared/utils/useVisDomainSettings.test.ts b/ui/src/shared/utils/useVisDomainSettings.test.ts index a1c175df2b..742273aa3f 100644 --- a/ui/src/shared/utils/useVisDomainSettings.test.ts +++ b/ui/src/shared/utils/useVisDomainSettings.test.ts @@ -1,5 +1,8 @@ // Funcs -import {getValidRange} from 'src/shared/utils/useVisDomainSettings' +import { + getValidRange, + getRemainingRange, +} from 'src/shared/utils/useVisDomainSettings' // Types import {numericColumnData as data} from 'mocks/dummyData' @@ -47,7 +50,46 @@ describe('getValidRange', () => { const newRange = getValidRange(data, timeRange) expect(newRange[1]).toEqual(data[data.length - 1]) }) - it('should return the the start and end times based on the data array if no start / endTime are passed', () => { + it('should return the start and end times based on the data array if no start / endTime are passed', () => { expect(getValidRange(data, null)).toEqual([data[0], data[data.length - 1]]) }) }) + +describe('getRemainingRange', () => { + // const startTime: string = 'Nov 07 2019 02:46:51 GMT-0800' + const startTime: string = '2019-11-07T02:46:51Z' + const unixStart: number = 1573094811000 + const endTime: string = '2019-11-28T14:46:51Z' + const unixEnd: number = 1574952411000 + it('should return null when no parameters are input', () => { + expect(getRemainingRange(undefined, undefined, undefined)).toEqual(null) + }) + it('should return null when no data is passed', () => { + const timeRange: CustomTimeRange = { + type: 'custom', + lower: startTime, + upper: endTime, + } + expect(getRemainingRange([], timeRange, [null, null])).toEqual(null) + }) + it("should return the min y-axis if it's set", () => { + const timeRange: CustomTimeRange = { + type: 'custom', + lower: startTime, + upper: endTime, + } + const setMin = unixStart - 10 + const [start] = getRemainingRange(data, timeRange, [setMin, null]) + expect(start).toEqual(setMin) + }) + it("should return the max y-axis if it's set", () => { + const timeRange: CustomTimeRange = { + type: 'custom', + lower: startTime, + upper: endTime, + } + const setMax = unixEnd + 10 + const range = getRemainingRange(data, timeRange, [null, setMax]) + expect(range[1]).toEqual(setMax) + }) +}) diff --git a/ui/src/shared/utils/useVisDomainSettings.ts b/ui/src/shared/utils/useVisDomainSettings.ts index 032ecd19af..18f4cf83b1 100644 --- a/ui/src/shared/utils/useVisDomainSettings.ts +++ b/ui/src/shared/utils/useVisDomainSettings.ts @@ -22,7 +22,7 @@ export const getValidRange = ( data: NumericColumnData = [], timeRange: TimeRange | null ) => { - const range = extent((data as number[]) || []) + const range = extent(data as number[]) if (isNull(timeRange)) { return range } @@ -36,7 +36,7 @@ export const getValidRange = ( return range } -export const useVisDomainSettings = ( +export const useVisXDomainSettings = ( storedDomain: number[], data: NumericColumnData, timeRange: TimeRange | null = null @@ -54,3 +54,42 @@ export const useVisDomainSettings = ( return [domain, setDomain, resetDomain] } + +export const getRemainingRange = ( + data: NumericColumnData = [], + timeRange: TimeRange | null, + storedDomain: number[] +) => { + const range = extent(data as number[]) + if (Array.isArray(range) && range.length >= 2) { + const startTime = getStartTime(timeRange) + const endTime = getEndTime(timeRange) + const start = storedDomain[0] + ? storedDomain[0] + : Math.min(startTime, range[0]) + const end = storedDomain[1] ? storedDomain[1] : Math.max(endTime, range[1]) + return [start, end] + } + return range +} + +export const useVisYDomainSettings = ( + storedDomain: number[], + data: NumericColumnData, + timeRange: TimeRange | null = null +) => { + const initialDomain = useMemo(() => { + if (storedDomain === null || storedDomain.every(val => val === null)) { + return getValidRange(data, timeRange) + } + if (storedDomain.includes(null)) { + return getRemainingRange(data, timeRange, storedDomain) + } + return storedDomain + }, [storedDomain, data]) + + const [domain, setDomain] = useOneWayState(initialDomain) + const resetDomain = () => setDomain(initialDomain) + + return [domain, setDomain, resetDomain] +} diff --git a/ui/src/shared/utils/vis.test.ts b/ui/src/shared/utils/vis.test.ts new file mode 100644 index 0000000000..de2cd3f8db --- /dev/null +++ b/ui/src/shared/utils/vis.test.ts @@ -0,0 +1,18 @@ +// Funcs +import {parseYBounds} from 'src/shared/utils/vis' + +describe('parseYBounds', () => { + it('should return null when bounds is null', () => { + expect(parseYBounds(null)).toEqual(null) + expect(parseYBounds([null, null])).toEqual(null) + }) + it('should return [0, 100] when the bounds are ["0", "100"]', () => { + expect(parseYBounds(['0', '100'])).toEqual([0, 100]) + }) + it('should return [null, 100] when the bounds are [null, "100"]', () => { + expect(parseYBounds([null, '100'])).toEqual([null, 100]) + }) + it('should return [-10, null] when the bounds are ["-10", null]', () => { + expect(parseYBounds(['-10', null])).toEqual([-10, null]) + }) +}) diff --git a/ui/src/shared/utils/vis.ts b/ui/src/shared/utils/vis.ts index 92856fab27..ef3ba7131c 100644 --- a/ui/src/shared/utils/vis.ts +++ b/ui/src/shared/utils/vis.ts @@ -113,7 +113,7 @@ export const filterNoisyColumns = (columns: string[], table: Table): string[] => return false }) -export const parseBounds = ( +export const parseXBounds = ( bounds: Axis['bounds'] ): [number, number] | null => { if ( @@ -129,6 +129,18 @@ export const parseBounds = ( return [+bounds[0], +bounds[1]] } +export const parseYBounds = ( + bounds: Axis['bounds'] +): [number | null, number | null] | null => { + if (!bounds || (!bounds[0] && !bounds[1])) { + return null + } + + const min = isNaN(parseInt(bounds[0])) ? null : parseInt(bounds[0]) + const max = isNaN(parseInt(bounds[1])) ? null : parseInt(bounds[1]) + return [min, max] +} + export const extent = (xs: number[]): [number, number] | null => { if (!xs || !xs.length) { return null diff --git a/ui/src/timeMachine/components/view_options/LineOptions.tsx b/ui/src/timeMachine/components/view_options/LineOptions.tsx index 923a921337..d24b18dcd2 100644 --- a/ui/src/timeMachine/components/view_options/LineOptions.tsx +++ b/ui/src/timeMachine/components/view_options/LineOptions.tsx @@ -32,7 +32,7 @@ import { } from 'src/timeMachine/actions' // Utils -import {parseBounds} from 'src/shared/utils/vis' +import {parseYBounds} from 'src/shared/utils/vis' import { getXColumnSelection, getYColumnSelection, @@ -202,14 +202,19 @@ class LineOptions extends PureComponent { } private get yDomain(): [number, number] { - return parseBounds(this.props.axes.y.bounds) + return parseYBounds(this.props.axes.y.bounds) + } + + private setBoundValues = (value: number | null): string | null => { + return value === null ? null : String(value) } private handleSetYDomain = (yDomain: [number, number]): void => { - let bounds: [string, string] | [null, null] + let bounds: [string | null, string | null] if (yDomain) { - bounds = [String(yDomain[0]), String(yDomain[1])] + const [min, max] = yDomain + bounds = [this.setBoundValues(min), this.setBoundValues(max)] } else { bounds = [null, null] }