feat(y_axis_domain): updated y-axis domain settings to allow min OR max values to be set. (#18040)

pull/18069/head
Ariel Salem 2020-05-12 14:36:23 -07:00 committed by GitHub
parent 2fb92a138a
commit aad4eea933
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 191 additions and 50 deletions

View File

@ -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

View File

@ -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()

View File

@ -14,35 +14,25 @@ interface MinMaxInputsProps {
initialMin: string
initialMax: string
onSetMinMax: (minMax: [number, number]) => void
onSetErrorMessage: (errorMessage: string) => void
}
const MinMaxInputs: SFC<MinMaxInputsProps> = ({
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<MinMaxInputsProps> = ({
return
}
onSetErrorMessage('')
onSetMinMax([newMin, newMax])
}
@ -69,6 +58,7 @@ const MinMaxInputs: SFC<MinMaxInputsProps> = ({
onChange={e => setMinInput(e.target.value)}
onBlur={emitIfValid}
onKeyPress={handleKeyPress}
testID="auto-domain--min"
/>
</Form.Element>
</Grid.Column>
@ -79,6 +69,7 @@ const MinMaxInputs: SFC<MinMaxInputsProps> = ({
onChange={e => setMaxInput(e.target.value)}
onBlur={emitIfValid}
onKeyPress={handleKeyPress}
testID="auto-domain--max"
/>
</Form.Element>
</Grid.Column>
@ -86,6 +77,10 @@ const MinMaxInputs: SFC<MinMaxInputsProps> = ({
)
}
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<AutoDomainInputProps> = ({
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 (
<Form.Element
label={label}
errorMessage={errorMessage}
className="auto-domain-input"
>
<Form.Element label={label} className="auto-domain-input">
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve}>
@ -153,7 +141,6 @@ const AutoDomainInput: SFC<AutoDomainInputProps> = ({
initialMin={initialMin}
initialMax={initialMax}
onSetMinMax={onSetDomain}
onSetErrorMessage={setErrorMessage}
/>
</Grid.Row>
)}

View File

@ -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<Props> = ({
}) => {
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')
)

View File

@ -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<Props> = ({
}) => {
const columnKeys = table.columnKeys
const [xDomain, onSetXDomain, onResetXDomain] = useVisDomainSettings(
const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings(
storedXDomain,
table.getColumn(xColumn, 'number')
)

View File

@ -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<Props> = ({
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')
)

View File

@ -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<Props> = ({
},
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<Props> = ({
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<Props> = ({
return table.getColumn(yColumn, 'number')
}, [table, yColumn, position])
const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings(
const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings(
storedYDomain,
memoizedYColumnData
)

View File

@ -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)
})
})

View File

@ -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]
}

View File

@ -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])
})
})

View File

@ -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

View File

@ -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<Props> {
}
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]
}