feat(y_axis_domain): updated y-axis domain settings to allow min OR max values to be set. (#18040)
parent
2fb92a138a
commit
aad4eea933
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue