feat(bandChart): add feature flag and update options for Band Chart

pull/19324/head
Deniz Kusefoglu 2020-08-17 00:47:57 -07:00 committed by Timmy Luong
parent 894aa99d37
commit 8ce6fe6a17
13 changed files with 547 additions and 2 deletions

View File

@ -120,6 +120,14 @@
contact: Query Team
lifetime: temporary
- name: Band Plot Type
description: Enables the creation of a band plot in Dashboards
key: bandPlotType
default: false
contact: Monitoring Team
expose: true
lifetime: temporary
- name: Mosaic Graph Type
description: Enables the creation of a mosaic graph in Dashboards
key: mosaicGraphType

View File

@ -212,6 +212,20 @@ func MergedFiltersRule() BoolFlag {
return mergeFiltersRule
}
var bandPlotType = MakeBoolFlag(
"Band Plot Type",
"bandPlotType",
"Monitoring Team",
false,
Temporary,
true,
)
// BandPlotType - Enables the creation of a band plot in Dashboards
func BandPlotType() BoolFlag {
return bandPlotType
}
var mosaicGraphType = MakeBoolFlag(
"Mosaic Graph Type",
"mosaicGraphType",
@ -284,6 +298,7 @@ var all = []Flag{
simpleTaskOptionsExtraction,
useUserPermission,
mergeFiltersRule,
bandPlotType,
mosaicGraphType,
notebooks,
pushDownGroupAggregateMinMax,
@ -306,6 +321,7 @@ var byKey = map[string]Flag{
"simpleTaskOptionsExtraction": simpleTaskOptionsExtraction,
"useUserPermission": useUserPermission,
"mergeFiltersRule": mergeFiltersRule,
"bandPlotType": bandPlotType,
"mosaicGraphType": mosaicGraphType,
"notebooks": notebooks,
"pushDownGroupAggregateMinMax": pushDownGroupAggregateMinMax,

View File

@ -0,0 +1,185 @@
// Libraries
import React, {FC, useMemo} from 'react'
import {Config, Table} from '@influxdata/giraffe'
// Components
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
// Utils
import {
useVisXDomainSettings,
useVisYDomainSettings,
} from 'src/shared/utils/useVisDomainSettings'
import {
getFormatter,
geomToInterpolation,
filterNoisyColumns,
parseXBounds,
parseYBounds,
defaultXColumn,
defaultYColumn,
} from 'src/shared/utils/vis'
// Constants
import {
BAND_LINE_OPACITY,
BAND_LINE_WIDTH,
BAND_SHADE_OPACITY,
VIS_THEME,
VIS_THEME_LIGHT,
} from 'src/shared/constants'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {INVALID_DATA_COPY} from 'src/shared/copy/cell'
// Types
import {BandViewProperties, TimeZone, TimeRange, Theme} from 'src/types'
interface Props {
children: (config: Config) => JSX.Element
fluxGroupKeyUnion: string[]
timeRange?: TimeRange | null
table: Table
timeZone: TimeZone
viewProperties: BandViewProperties
theme: Theme
}
const BandPlot: FC<Props> = ({
children,
fluxGroupKeyUnion,
timeRange,
table,
timeZone,
viewProperties: {
geom,
colors,
xColumn: storedXColumn,
yColumn: storedYColumn,
upperColumn: upperColumnName,
lowerColumn: lowerColumnName,
hoverDimension,
axes: {
x: {
label: xAxisLabel,
prefix: xTickPrefix,
suffix: xTickSuffix,
base: xTickBase,
bounds: xBounds,
},
y: {
label: yAxisLabel,
prefix: yTickPrefix,
suffix: yTickSuffix,
bounds: yBounds,
base: yTickBase,
},
},
timeFormat,
},
theme,
}) => {
const storedXDomain = useMemo(() => parseXBounds(xBounds), [xBounds])
const storedYDomain = useMemo(() => parseYBounds(yBounds), [yBounds])
const xColumn = storedXColumn || defaultXColumn(table, '_time')
const yColumn =
(table.columnKeys.includes(storedYColumn) && storedYColumn) ||
defaultYColumn(table)
const columnKeys = table.columnKeys
const isValidView =
xColumn &&
columnKeys.includes(xColumn) &&
yColumn &&
columnKeys.includes(yColumn)
const colorHexes =
colors && colors.length
? colors.map(c => c.hex)
: DEFAULT_LINE_COLORS.map(c => c.hex)
const interpolation = geomToInterpolation(geom)
const groupKey = [...fluxGroupKeyUnion, 'result']
const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings(
storedXDomain,
table.getColumn(xColumn, 'number'),
timeRange
)
const memoizedYColumnData = useMemo(
() => table.getColumn(yColumn, 'number'),
[table, yColumn, xColumn, colorHexes, groupKey]
)
const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings(
storedYDomain,
memoizedYColumnData
)
const legendColumns = filterNoisyColumns(
[...groupKey, xColumn, yColumn],
table
)
const xFormatter = getFormatter(table.getColumnType(xColumn), {
prefix: xTickPrefix,
suffix: xTickSuffix,
base: xTickBase,
timeZone,
timeFormat,
})
const yFormatter = getFormatter(table.getColumnType(yColumn), {
prefix: yTickPrefix,
suffix: yTickSuffix,
base: yTickBase,
timeZone,
timeFormat,
})
const currentTheme = theme === 'light' ? VIS_THEME_LIGHT : VIS_THEME
const config: Config = {
...currentTheme,
table,
xAxisLabel,
yAxisLabel,
xDomain,
onSetXDomain,
onResetXDomain,
yDomain,
onSetYDomain,
onResetYDomain,
legendColumns,
valueFormatters: {
[xColumn]: xFormatter,
[yColumn]: yFormatter,
},
layers: [
{
type: 'band',
x: xColumn,
y: yColumn,
fill: groupKey,
interpolation,
colors: colorHexes,
lineWidth: BAND_LINE_WIDTH,
lineOpacity: BAND_LINE_OPACITY,
shadeOpacity: BAND_SHADE_OPACITY,
hoverDimension,
upperColumnName,
lowerColumnName,
},
],
}
if (!isValidView) {
return <EmptyGraphMessage message={INVALID_DATA_COPY} />
}
return children(config)
}
export default BandPlot

View File

@ -14,6 +14,7 @@ import XYPlot from 'src/shared/components/XYPlot'
import ScatterPlot from 'src/shared/components/ScatterPlot'
import LatestValueTransform from 'src/shared/components/LatestValueTransform'
import CheckPlot from 'src/shared/components/CheckPlot'
import BandPlot from 'src/shared/components/BandPlot'
// Types
import {
@ -105,6 +106,19 @@ const ViewSwitcher: FunctionComponent<Props> = ({
{config => <Plot config={config} />}
</XYPlot>
)
case 'band':
return (
<BandPlot
timeRange={timeRange}
fluxGroupKeyUnion={fluxGroupKeyUnion}
table={table}
timeZone={timeZone}
viewProperties={properties}
theme={theme}
>
{config => <Plot config={config} />}
</BandPlot>
)
case 'line-plus-single-stat':
const xyProperties = {

View File

@ -107,3 +107,7 @@ export const GIRAFFE_COLOR_SCHEMES = [
{name: 'Ectoplasm', colors: ECTOPLASM},
{name: 'T-MAX 400 Film', colors: T_MAX_400_FILM},
]
export const BAND_LINE_OPACITY = 0.7
export const BAND_LINE_WIDTH = 3
export const BAND_SHADE_OPACITY = 0.3

View File

@ -0,0 +1,250 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
// Components
import {Grid, Form, Dropdown} from '@influxdata/clockface'
import Geom from 'src/timeMachine/components/view_options/Geom'
import YAxisTitle from 'src/timeMachine/components/view_options/YAxisTitle'
import AxisAffixes from 'src/timeMachine/components/view_options/AxisAffixes'
import ColorSelector from 'src/timeMachine/components/view_options/ColorSelector'
import AutoDomainInput from 'src/shared/components/AutoDomainInput'
import YAxisBase from 'src/timeMachine/components/view_options/YAxisBase'
import ColumnSelector from 'src/shared/components/ColumnSelector'
import Checkbox from 'src/shared/components/Checkbox'
import TimeFormat from 'src/timeMachine/components/view_options/TimeFormat'
// Actions
import {
setColors,
setYAxisLabel,
setAxisPrefix,
setAxisSuffix,
setYAxisBounds,
setYAxisBase,
setGeom,
setXColumn,
setYColumn,
setShadeBelow,
setLinePosition,
setTimeFormat,
SetHoverDimension,
} from 'src/timeMachine/actions'
// Utils
import {parseYBounds} from 'src/shared/utils/vis'
import {
getXColumnSelection,
getYColumnSelection,
getNumericColumns,
getActiveTimeMachine,
} from 'src/timeMachine/selectors'
// Types
import {
AppState,
XYGeom,
Axes,
Color,
NewView,
XYViewProperties,
ViewType,
} from 'src/types'
interface OwnProps {
type: ViewType
axes: Axes
geom?: XYGeom
colors: Color[]
shadeBelow?: boolean
hoverDimension?: 'auto' | 'x' | 'y' | 'xy'
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps & ReduxProps
class BandOptions extends PureComponent<Props> {
public render() {
const {
axes: {
y: {label, prefix, suffix, base},
},
colors,
geom,
shadeBelow,
onUpdateColors,
onUpdateYAxisLabel,
onUpdateAxisPrefix,
onUpdateAxisSuffix,
onUpdateYAxisBase,
onSetShadeBelow,
onSetGeom,
onSetYColumn,
yColumn,
onSetXColumn,
xColumn,
numericColumns,
onSetTimeFormat,
timeFormat,
hoverDimension = 'auto',
onSetHoverDimension,
} = this.props
return (
<>
<Grid.Column>
<h4 className="view-options--header">Customize Band Plot</h4>
<h5 className="view-options--header">Data</h5>
<ColumnSelector
selectedColumn={xColumn}
onSelectColumn={onSetXColumn}
availableColumns={numericColumns}
axisName="x"
/>
<ColumnSelector
selectedColumn={yColumn}
onSelectColumn={onSetYColumn}
availableColumns={numericColumns}
axisName="y"
/>
<Form.Element label="Time Format">
<TimeFormat
timeFormat={timeFormat}
onTimeFormatChange={onSetTimeFormat}
/>
</Form.Element>
<h5 className="view-options--header">Options</h5>
</Grid.Column>
{geom && <Geom geom={geom} onSetGeom={onSetGeom} />}
<ColorSelector
colors={colors.filter(c => c.type === 'scale')}
onUpdateColors={onUpdateColors}
/>
<Grid.Column>
<Checkbox
label="Shade Area Below Lines"
checked={!!shadeBelow}
onSetChecked={onSetShadeBelow}
/>
</Grid.Column>
<Grid.Column>
<br />
<Form.Element label="Hover Dimension">
<Dropdown
button={(active, onClick) => (
<Dropdown.Button active={active} onClick={onClick}>
{hoverDimension}
</Dropdown.Button>
)}
menu={onCollapse => (
<Dropdown.Menu onCollapse={onCollapse}>
<Dropdown.Item
id="auto"
value="auto"
onClick={onSetHoverDimension}
selected={hoverDimension === 'auto'}
>
Auto
</Dropdown.Item>
<Dropdown.Item
id="x"
value="x"
onClick={onSetHoverDimension}
selected={hoverDimension === 'x'}
>
X Axis
</Dropdown.Item>
<Dropdown.Item
id="y"
value="y"
onClick={onSetHoverDimension}
selected={hoverDimension === 'y'}
>
Y Axis
</Dropdown.Item>
<Dropdown.Item
id="xy"
value="xy"
onClick={onSetHoverDimension}
selected={hoverDimension === 'xy'}
>
X & Y Axis
</Dropdown.Item>
</Dropdown.Menu>
)}
/>
</Form.Element>
</Grid.Column>
<Grid.Column>
<h5 className="view-options--header">Y Axis</h5>
</Grid.Column>
<YAxisTitle label={label} onUpdateYAxisLabel={onUpdateYAxisLabel} />
<YAxisBase base={base} onUpdateYAxisBase={onUpdateYAxisBase} />
<AxisAffixes
prefix={prefix}
suffix={suffix}
axisName="y"
onUpdateAxisPrefix={prefix => onUpdateAxisPrefix(prefix, 'y')}
onUpdateAxisSuffix={suffix => onUpdateAxisSuffix(suffix, 'y')}
/>
<Grid.Column>
<AutoDomainInput
domain={this.yDomain}
onSetDomain={this.handleSetYDomain}
label="Y Axis Domain"
/>
</Grid.Column>
</>
)
}
private get yDomain(): [number, number] {
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 | null, string | null]
if (yDomain) {
const [min, max] = yDomain
bounds = [this.setBoundValues(min), this.setBoundValues(max)]
} else {
bounds = [null, null]
}
this.props.onUpdateYAxisBounds(bounds)
}
}
const mstp = (state: AppState) => {
const xColumn = getXColumnSelection(state)
const yColumn = getYColumnSelection(state)
const numericColumns = getNumericColumns(state)
const view = getActiveTimeMachine(state).view as NewView<XYViewProperties>
const {timeFormat} = view.properties
return {xColumn, yColumn, numericColumns, timeFormat}
}
const mdtp = {
onUpdateYAxisLabel: setYAxisLabel,
onUpdateAxisPrefix: setAxisPrefix,
onUpdateAxisSuffix: setAxisSuffix,
onUpdateYAxisBounds: setYAxisBounds,
onUpdateYAxisBase: setYAxisBase,
onSetXColumn: setXColumn,
onSetYColumn: setYColumn,
onSetShadeBelow: setShadeBelow,
onUpdateColors: setColors,
onSetGeom: setGeom,
onSetPosition: setLinePosition,
onSetTimeFormat: setTimeFormat,
onSetHoverDimension: SetHoverDimension,
}
const connector = connect(mstp, mdtp)
export default connector(BandOptions)

View File

@ -2,6 +2,7 @@
import React, {PureComponent} from 'react'
// Components
import BandOptions from 'src/timeMachine/components/view_options/BandOptions'
import LineOptions from 'src/timeMachine/components/view_options/LineOptions'
import GaugeOptions from 'src/timeMachine/components/view_options/GaugeOptions'
import SingleStatOptions from 'src/timeMachine/components/view_options/SingleStatOptions'
@ -32,6 +33,8 @@ class OptionsSwitcher extends PureComponent<Props> {
)
case 'xy':
return <LineOptions {...view.properties} />
case 'band':
return <BandOptions {...view.properties} />
case 'gauge':
return <GaugeOptions {...view.properties} />
case 'single-stat':

View File

@ -1,7 +1,8 @@
@import "src/style/modules";
@import 'src/style/modules';
.view-type-dropdown {
.cf-dropdown-item--children, .cf-dropdown--selected {
.cf-dropdown-item--children,
.cf-dropdown--selected {
display: flex;
align-items: center;
}
@ -50,6 +51,11 @@
stroke: $g13-mist;
}
.vis-graphic--line-e {
stroke: $c-honeydew;
stroke-width: 4px;
}
.vis-graphic--fill-a {
fill: $g11-sidewalk;
}
@ -95,6 +101,11 @@
fill: $c-honeydew;
}
.vis-graphic--fill-e {
fill: $c-honeydew;
opacity: 0.3;
}
.vis-graphic--fill-a,
.vis-graphic--fill-b,
.vis-graphic--fill-c {

View File

@ -58,6 +58,9 @@ class ViewTypeDropdown extends PureComponent<Props> {
if (g.type === 'mosaic' && !isFlagEnabled('mosaicGraphType')) {
return false
}
if (g.type === 'band' && !isFlagEnabled('bandPlotType')) {
return false
}
return true
}).map(g => {
return (

View File

@ -6,6 +6,10 @@ interface VisType {
}
export const VIS_TYPES: VisType[] = [
{
type: 'band',
name: 'Band Plot',
},
{
type: 'xy',
name: 'Graph',

View File

@ -470,6 +470,37 @@ const GRAPHIC_SVGS = {
</svg>
</div>
),
band: (
<div className="vis-graphic" data-testid="vis-graphic--band-chart">
<svg
id="Band Chart"
x="0px"
y="0px"
width="100%"
height="100%"
viewBox="0 0 150 150"
>
<path
className="vis-graphic--fill vis-graphic--fill-e"
d="M10,127V52.6v-8c0-1.8,0.8-3.5,2.2-4.6l25-18.3c1.6-1.2,3.7-0.9,5,0.5l24.6,26.9c1.8,1.9,4.5,2.1,6.5,0.5
L104,23c1.8-1.5,7.1-1.5,8.3,0.6l16.5,27c0.7,1.2,2,1.9,3.3,1.9h7.9v5.5V127H10z"
/>
<path
className="vis-graphic--line vis-graphic--line-c"
d="M39.5,22c0.8,0,1.5,0.3,2.1,1l24.6,26.9c1.1,1.2,2.7,1.9,4.3,1.9c1.3,0,2.6-0.5,3.7-1.3l30.5-26.7
c0.6-0.5,1.9-0.8,3.3-0.8c1.7,0,3.1,0.5,3.5,1.2l16.5,27c0.9,1.5,2.5,2.4,4.2,2.4h6.9v4.5V126H11V52.6v-8c0-1.5,0.7-2.9,1.8-3.7
l25-18.3C38.3,22.2,38.9,22,39.5,22 M39.5,21c-0.8,0-1.5,0.2-2.2,0.7L12.2,40c-1.4,1-2.2,2.7-2.2,4.6v8V127h130V58.1v-5.5h-7.9
c-1.3,0-2.6-0.7-3.3-1.9l-16.5-27c-0.7-1.1-2.5-1.7-4.4-1.7c-1.6,0-3.1,0.4-4,1.1L73.5,49.7c-0.9,0.7-1.9,1.1-3,1.1
c-1.3,0-2.6-0.5-3.5-1.6L42.3,22.3C41.5,21.4,40.5,21,39.5,21L39.5,21z"
/>
<path
className="vis-graphic--line vis-graphic--line-e"
d="M10,91h0.2c1.2,0,2.4-0.4,3.3-1.2L35.6,71c2-1.7,5-1.6,6.9,0.3L66.6,96c2.1,2.1,5.6,2,7.5-0.3l29.2-34.4
c2.2-2.6,6.4-2.3,8.2,0.6l15,24.6c0.9,1.5,2.5,2.4,4.3,2.4h9.3"
/>
</svg>
</div>
),
xy: (
<div className="vis-graphic" data-testid="vis-graphic--xy">
<svg

View File

@ -474,6 +474,7 @@ export const timeMachineReducer = (
return setViewProperties(state, {prefix})
case 'check':
case 'xy':
case 'band':
return setYAxis(state, {prefix})
default:
return state
@ -490,6 +491,7 @@ export const timeMachineReducer = (
return setViewProperties(state, {tickPrefix})
case 'check':
case 'xy':
case 'band':
return setYAxis(state, {tickPrefix})
default:
return state
@ -506,6 +508,7 @@ export const timeMachineReducer = (
return setViewProperties(state, {suffix})
case 'check':
case 'xy':
case 'band':
return setYAxis(state, {suffix})
default:
return state
@ -522,6 +525,7 @@ export const timeMachineReducer = (
return setViewProperties(state, {tickSuffix})
case 'check':
case 'xy':
case 'band':
return setYAxis(state, {tickSuffix})
default:
return state
@ -538,6 +542,7 @@ export const timeMachineReducer = (
case 'scatter':
case 'check':
case 'xy':
case 'band':
case 'histogram':
return setViewProperties(state, {colors})
case 'line-plus-single-stat':

View File

@ -355,6 +355,17 @@ export const getSaveableView = (state: AppState): QueryView & {id?: string} => {
}
}
if (saveableView.properties.type === 'band') {
saveableView = {
...saveableView,
properties: {
...saveableView.properties,
xColumn: getXColumnSelection(state),
yColumn: getYColumnSelection(state),
},
}
}
if (saveableView.properties.type === 'line-plus-single-stat') {
saveableView = {
...saveableView,