Add precision to gauge and fix single stat bugs
parent
b5a07a5854
commit
44a24e4634
|
@ -1,31 +1,124 @@
|
||||||
import React, {Component} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {bindActionCreators} from 'redux'
|
|
||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import uuid from 'uuid'
|
import uuid from 'uuid'
|
||||||
|
|
||||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||||
import Threshold from 'src/dashboards/components/Threshold'
|
import Threshold from 'src/dashboards/components/Threshold'
|
||||||
|
import GraphOptionsDecimalPlaces from 'src/dashboards/components/GraphOptionsDecimalPlaces'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_TYPE_THRESHOLD,
|
COLOR_TYPE_THRESHOLD,
|
||||||
THRESHOLD_COLORS,
|
THRESHOLD_COLORS,
|
||||||
MAX_THRESHOLDS,
|
MAX_THRESHOLDS,
|
||||||
MIN_THRESHOLDS,
|
MIN_THRESHOLDS,
|
||||||
} from 'shared/constants/thresholds'
|
} from 'src/shared/constants/thresholds'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
changeDecimalPlaces,
|
||||||
updateGaugeColors,
|
updateGaugeColors,
|
||||||
updateAxes,
|
updateAxes,
|
||||||
} from 'src/dashboards/actions/cellEditorOverlay'
|
} from 'src/dashboards/actions/cellEditorOverlay'
|
||||||
import {colorsNumberSchema} from 'shared/schemas'
|
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
import {Axes} from 'src/types'
|
||||||
|
import {DecimalPlaces} from 'src/types/dashboards'
|
||||||
|
import {ColorNumber} from 'src/types/colors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
axes: Axes
|
||||||
|
gaugeColors: ColorNumber[]
|
||||||
|
decimalPlaces: DecimalPlaces
|
||||||
|
onResetFocus: () => void
|
||||||
|
handleUpdateAxes: (a: Axes) => void
|
||||||
|
onUpdateDecimalPlaces: (d: DecimalPlaces) => void
|
||||||
|
handleUpdateGaugeColors: (d: ColorNumber[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
class GaugeOptions extends Component {
|
class GaugeOptions extends PureComponent<Props> {
|
||||||
handleAddThreshold = () => {
|
public render() {
|
||||||
|
const {gaugeColors, axes, decimalPlaces} = this.props
|
||||||
|
const {y} = axes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FancyScrollbar
|
||||||
|
className="display-options--cell y-axis-controls"
|
||||||
|
autoHide={false}
|
||||||
|
>
|
||||||
|
<div className="display-options--cell-wrapper">
|
||||||
|
<h5 className="display-options--header">Gauge Controls</h5>
|
||||||
|
<div className="thresholds-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={this.handleAddThreshold}
|
||||||
|
disabled={this.disableAddThreshold}
|
||||||
|
>
|
||||||
|
<span className="icon plus" /> Add Threshold
|
||||||
|
</button>
|
||||||
|
{this.sortedGaugeColors.map((color, index) => (
|
||||||
|
<Threshold
|
||||||
|
isMin={index === 0}
|
||||||
|
isMax={index === gaugeColors.length - 1}
|
||||||
|
visualizationType="gauge"
|
||||||
|
threshold={color}
|
||||||
|
key={uuid.v4()}
|
||||||
|
disableMaxColor={this.disableMaxColor}
|
||||||
|
onChooseColor={this.handleChooseColor}
|
||||||
|
onValidateColorValue={this.handleValidateColorValue}
|
||||||
|
onUpdateColorValue={this.handleUpdateColorValue}
|
||||||
|
onDeleteThreshold={this.handleDeleteThreshold}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="graph-options-group form-group-wrapper">
|
||||||
|
<div className="form-group col-xs-6">
|
||||||
|
<label>Prefix</label>
|
||||||
|
<input
|
||||||
|
className="form-control input-sm"
|
||||||
|
placeholder="%, MPH, etc."
|
||||||
|
defaultValue={y.prefix}
|
||||||
|
onChange={this.handleUpdatePrefix}
|
||||||
|
maxLength={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group col-xs-6">
|
||||||
|
<label>Suffix</label>
|
||||||
|
<input
|
||||||
|
className="form-control input-sm"
|
||||||
|
placeholder="%, MPH, etc."
|
||||||
|
defaultValue={y.suffix}
|
||||||
|
onChange={this.handleUpdateSuffix}
|
||||||
|
maxLength={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GraphOptionsDecimalPlaces
|
||||||
|
digits={decimalPlaces.digits}
|
||||||
|
isEnforced={decimalPlaces.isEnforced}
|
||||||
|
onDecimalPlacesChange={this.handleDecimalPlacesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FancyScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get disableMaxColor(): boolean {
|
||||||
|
const {gaugeColors} = this.props
|
||||||
|
return gaugeColors.length > MIN_THRESHOLDS
|
||||||
|
}
|
||||||
|
|
||||||
|
private get disableAddThreshold(): boolean {
|
||||||
|
const {gaugeColors} = this.props
|
||||||
|
return gaugeColors.length > MAX_THRESHOLDS
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDecimalPlacesChange = (decimalPlaces: DecimalPlaces) => {
|
||||||
|
const {onUpdateDecimalPlaces} = this.props
|
||||||
|
onUpdateDecimalPlaces(decimalPlaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAddThreshold = () => {
|
||||||
const {gaugeColors, handleUpdateGaugeColors, onResetFocus} = this.props
|
const {gaugeColors, handleUpdateGaugeColors, onResetFocus} = this.props
|
||||||
const sortedColors = _.sortBy(gaugeColors, color => color.value)
|
const sortedColors = _.sortBy(gaugeColors, color => color.value)
|
||||||
|
|
||||||
|
@ -50,7 +143,7 @@ class GaugeOptions extends Component {
|
||||||
name: THRESHOLD_COLORS[randomColor].name,
|
name: THRESHOLD_COLORS[randomColor].name,
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedColors = _.sortBy(
|
const updatedColors: ColorNumber[] = _.sortBy<ColorNumber>(
|
||||||
[...gaugeColors, newThreshold],
|
[...gaugeColors, newThreshold],
|
||||||
color => color.value
|
color => color.value
|
||||||
)
|
)
|
||||||
|
@ -61,7 +154,7 @@ class GaugeOptions extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteThreshold = threshold => {
|
private handleDeleteThreshold = threshold => {
|
||||||
const {handleUpdateGaugeColors, onResetFocus} = this.props
|
const {handleUpdateGaugeColors, onResetFocus} = this.props
|
||||||
const gaugeColors = this.props.gaugeColors.filter(
|
const gaugeColors = this.props.gaugeColors.filter(
|
||||||
color => color.id !== threshold.id
|
color => color.id !== threshold.id
|
||||||
|
@ -72,7 +165,7 @@ class GaugeOptions extends Component {
|
||||||
onResetFocus()
|
onResetFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChooseColor = threshold => {
|
private handleChooseColor = threshold => {
|
||||||
const {handleUpdateGaugeColors} = this.props
|
const {handleUpdateGaugeColors} = this.props
|
||||||
const gaugeColors = this.props.gaugeColors.map(
|
const gaugeColors = this.props.gaugeColors.map(
|
||||||
color =>
|
color =>
|
||||||
|
@ -84,7 +177,7 @@ class GaugeOptions extends Component {
|
||||||
handleUpdateGaugeColors(gaugeColors)
|
handleUpdateGaugeColors(gaugeColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateColorValue = (threshold, value) => {
|
private handleUpdateColorValue = (threshold, value) => {
|
||||||
const {handleUpdateGaugeColors} = this.props
|
const {handleUpdateGaugeColors} = this.props
|
||||||
const gaugeColors = this.props.gaugeColors.map(
|
const gaugeColors = this.props.gaugeColors.map(
|
||||||
color => (color.id === threshold.id ? {...color, value} : color)
|
color => (color.id === threshold.id ? {...color, value} : color)
|
||||||
|
@ -93,7 +186,7 @@ class GaugeOptions extends Component {
|
||||||
handleUpdateGaugeColors(gaugeColors)
|
handleUpdateGaugeColors(gaugeColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleValidateColorValue = (threshold, targetValue) => {
|
private handleValidateColorValue = (threshold, targetValue) => {
|
||||||
const {gaugeColors} = this.props
|
const {gaugeColors} = this.props
|
||||||
|
|
||||||
const thresholdValue = threshold.value
|
const thresholdValue = threshold.value
|
||||||
|
@ -134,14 +227,14 @@ class GaugeOptions extends Component {
|
||||||
return allowedToUpdate
|
return allowedToUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdatePrefix = e => {
|
private handleUpdatePrefix = e => {
|
||||||
const {handleUpdateAxes, axes} = this.props
|
const {handleUpdateAxes, axes} = this.props
|
||||||
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
|
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
|
||||||
|
|
||||||
handleUpdateAxes(newAxes)
|
handleUpdateAxes(newAxes)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateSuffix = e => {
|
private handleUpdateSuffix = e => {
|
||||||
const {handleUpdateAxes, axes} = this.props
|
const {handleUpdateAxes, axes} = this.props
|
||||||
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
|
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
|
||||||
|
|
||||||
|
@ -154,98 +247,23 @@ class GaugeOptions extends Component {
|
||||||
|
|
||||||
return sortedColors
|
return sortedColors
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
gaugeColors,
|
|
||||||
axes: {
|
|
||||||
y: {prefix, suffix},
|
|
||||||
},
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
const disableMaxColor = gaugeColors.length > MIN_THRESHOLDS
|
|
||||||
const disableAddThreshold = gaugeColors.length > MAX_THRESHOLDS
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FancyScrollbar
|
|
||||||
className="display-options--cell y-axis-controls"
|
|
||||||
autoHide={false}
|
|
||||||
>
|
|
||||||
<div className="display-options--cell-wrapper">
|
|
||||||
<h5 className="display-options--header">Gauge Controls</h5>
|
|
||||||
<div className="thresholds-list">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
onClick={this.handleAddThreshold}
|
|
||||||
disabled={disableAddThreshold}
|
|
||||||
>
|
|
||||||
<span className="icon plus" /> Add Threshold
|
|
||||||
</button>
|
|
||||||
{this.sortedGaugeColors.map((color, index) => (
|
|
||||||
<Threshold
|
|
||||||
isMin={index === 0}
|
|
||||||
isMax={index === gaugeColors.length - 1}
|
|
||||||
visualizationType="gauge"
|
|
||||||
threshold={color}
|
|
||||||
key={uuid.v4()}
|
|
||||||
disableMaxColor={disableMaxColor}
|
|
||||||
onChooseColor={this.handleChooseColor}
|
|
||||||
onValidateColorValue={this.handleValidateColorValue}
|
|
||||||
onUpdateColorValue={this.handleUpdateColorValue}
|
|
||||||
onDeleteThreshold={this.handleDeleteThreshold}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="graph-options-group form-group-wrapper">
|
|
||||||
<div className="form-group col-xs-6">
|
|
||||||
<label>Prefix</label>
|
|
||||||
<input
|
|
||||||
className="form-control input-sm"
|
|
||||||
placeholder="%, MPH, etc."
|
|
||||||
defaultValue={prefix}
|
|
||||||
onChange={this.handleUpdatePrefix}
|
|
||||||
maxLength="5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group col-xs-6">
|
|
||||||
<label>Suffix</label>
|
|
||||||
<input
|
|
||||||
className="form-control input-sm"
|
|
||||||
placeholder="%, MPH, etc."
|
|
||||||
defaultValue={suffix}
|
|
||||||
onChange={this.handleUpdateSuffix}
|
|
||||||
maxLength="5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FancyScrollbar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {func, shape} = PropTypes
|
|
||||||
|
|
||||||
GaugeOptions.propTypes = {
|
|
||||||
gaugeColors: colorsNumberSchema,
|
|
||||||
handleUpdateGaugeColors: func.isRequired,
|
|
||||||
handleUpdateAxes: func.isRequired,
|
|
||||||
axes: shape({}).isRequired,
|
|
||||||
onResetFocus: func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = ({
|
const mapStateToProps = ({
|
||||||
cellEditorOverlay: {
|
cellEditorOverlay: {
|
||||||
gaugeColors,
|
gaugeColors,
|
||||||
cell: {axes},
|
cell: {axes, decimalPlaces},
|
||||||
},
|
},
|
||||||
}) => ({
|
}) => ({
|
||||||
|
decimalPlaces,
|
||||||
gaugeColors,
|
gaugeColors,
|
||||||
axes,
|
axes,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = {
|
||||||
handleUpdateGaugeColors: bindActionCreators(updateGaugeColors, dispatch),
|
handleUpdateGaugeColors: updateGaugeColors,
|
||||||
handleUpdateAxes: bindActionCreators(updateAxes, dispatch),
|
handleUpdateAxes: updateAxes,
|
||||||
})
|
onUpdateDecimalPlaces: changeDecimalPlaces,
|
||||||
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(GaugeOptions)
|
export default connect(mapStateToProps, mapDispatchToProps)(GaugeOptions)
|
|
@ -283,6 +283,7 @@ class Gauge extends Component {
|
||||||
drawGaugeValue = (ctx, radius, labelValueFontSize) => {
|
drawGaugeValue = (ctx, radius, labelValueFontSize) => {
|
||||||
const {gaugePosition, prefix, suffix} = this.props
|
const {gaugePosition, prefix, suffix} = this.props
|
||||||
const {valueColor} = GAUGE_SPECS
|
const {valueColor} = GAUGE_SPECS
|
||||||
|
const maximumFractionDigits = 20
|
||||||
|
|
||||||
ctx.font = `${labelValueFontSize}px Roboto`
|
ctx.font = `${labelValueFontSize}px Roboto`
|
||||||
ctx.fillStyle = valueColor
|
ctx.fillStyle = valueColor
|
||||||
|
@ -290,7 +291,9 @@ class Gauge extends Component {
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
|
|
||||||
const textY = radius
|
const textY = radius
|
||||||
const textContent = `${prefix}${gaugePosition.toLocaleString()}${suffix}`
|
const textContent = `${prefix}${gaugePosition.toLocaleString(undefined, {
|
||||||
|
maximumFractionDigits,
|
||||||
|
})}${suffix}`
|
||||||
ctx.fillText(textContent, 0, textY)
|
ctx.fillText(textContent, 0, textY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
|
||||||
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
|
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
|
||||||
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants'
|
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants'
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
import {DecimalPlaces} from 'src/types/dashboards'
|
||||||
|
|
||||||
interface Color {
|
interface Color {
|
||||||
type: string
|
type: string
|
||||||
|
@ -19,6 +20,7 @@ interface Color {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: TimeSeriesResponse[]
|
data: TimeSeriesResponse[]
|
||||||
|
decimalPlaces: DecimalPlaces
|
||||||
isFetchingInitially: boolean
|
isFetchingInitially: boolean
|
||||||
cellID: string
|
cellID: string
|
||||||
cellHeight?: number
|
cellHeight?: number
|
||||||
|
@ -76,11 +78,19 @@ class GaugeChart extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get lastValueForGauge(): number {
|
private get lastValueForGauge(): number {
|
||||||
const {data} = this.props
|
const {data, decimalPlaces} = this.props
|
||||||
const {lastValues} = getLastValues(data)
|
const {lastValues} = getLastValues(data)
|
||||||
const precision = 100.0
|
let lastValue = _.get(lastValues, 0, 0)
|
||||||
|
|
||||||
return Math.round(_.get(lastValues, 0, 0) * precision) / precision
|
if (lastValue === null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimalPlaces.isEnforced) {
|
||||||
|
lastValue = +lastValue.toFixed(decimalPlaces.digits)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,20 +83,21 @@ const RefreshingGraph = ({
|
||||||
if (type === 'gauge') {
|
if (type === 'gauge') {
|
||||||
return (
|
return (
|
||||||
<RefreshingGaugeChart
|
<RefreshingGaugeChart
|
||||||
source={source}
|
|
||||||
type={type}
|
type={type}
|
||||||
|
source={source}
|
||||||
|
cellID={cellID}
|
||||||
|
prefix={prefix}
|
||||||
|
suffix={suffix}
|
||||||
|
inView={inView}
|
||||||
colors={colors}
|
colors={colors}
|
||||||
key={manualRefresh}
|
key={manualRefresh}
|
||||||
queries={[queries[0]]}
|
queries={[queries[0]]}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
cellHeight={cellHeight}
|
cellHeight={cellHeight}
|
||||||
|
decimalPlaces={decimalPlaces}
|
||||||
resizerTopHeight={resizerTopHeight}
|
resizerTopHeight={resizerTopHeight}
|
||||||
editQueryStatus={editQueryStatus}
|
editQueryStatus={editQueryStatus}
|
||||||
cellID={cellID}
|
|
||||||
prefix={prefix}
|
|
||||||
suffix={suffix}
|
|
||||||
inView={inView}
|
|
||||||
onSetResolution={onSetResolution}
|
onSetResolution={onSetResolution}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -75,6 +75,11 @@ class SingleStat extends PureComponent<Props> {
|
||||||
|
|
||||||
private get roundedLastValue(): string {
|
private get roundedLastValue(): string {
|
||||||
const {decimalPlaces} = this.props
|
const {decimalPlaces} = this.props
|
||||||
|
|
||||||
|
if (this.lastValue === null) {
|
||||||
|
return `${0}`
|
||||||
|
}
|
||||||
|
|
||||||
let roundedValue = `${this.lastValue}`
|
let roundedValue = `${this.lastValue}`
|
||||||
|
|
||||||
if (decimalPlaces.isEnforced) {
|
if (decimalPlaces.isEnforced) {
|
||||||
|
@ -85,7 +90,8 @@ class SingleStat extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatToLocale(n: number): string {
|
private formatToLocale(n: number): string {
|
||||||
return n.toLocaleString()
|
const maximumFractionDigits = 20
|
||||||
|
return n.toLocaleString(undefined, {maximumFractionDigits})
|
||||||
}
|
}
|
||||||
|
|
||||||
private get containerStyle(): CSSProperties {
|
private get containerStyle(): CSSProperties {
|
||||||
|
|
|
@ -26,6 +26,10 @@ const defaultProps = {
|
||||||
cellID: '',
|
cellID: '',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
|
decimalPlaces: {
|
||||||
|
digits: 10,
|
||||||
|
isEnforced: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup = (overrides = {}) => {
|
const setup = (overrides = {}) => {
|
||||||
|
|
Loading…
Reference in New Issue