Merge pull request #2598 from influxdata/single-stat-colors-polish
Single Stat Colors Polishpull/10616/head
commit
da2d8a4fbd
|
@ -28,6 +28,9 @@
|
||||||
## v1.4.0.0-beta2 [2017-12-14]
|
## v1.4.0.0-beta2 [2017-12-14]
|
||||||
### UI Improvements
|
### UI Improvements
|
||||||
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
|
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
|
||||||
|
1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow appendage of a suffix to single stat visualizations
|
||||||
|
1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow optional colorization of text instead of background on single stat visualizations
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
|
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
|
||||||
1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
|
1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
|
||||||
|
|
|
@ -23,7 +23,7 @@ const (
|
||||||
ErrAuthentication = Error("user not authenticated")
|
ErrAuthentication = Error("user not authenticated")
|
||||||
ErrUninitialized = Error("client uninitialized. Call Open() method")
|
ErrUninitialized = Error("client uninitialized. Call Open() method")
|
||||||
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
|
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
|
||||||
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'")
|
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'")
|
||||||
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
||||||
ErrUserAlreadyExists = Error("user already exists")
|
ErrUserAlreadyExists = Error("user already exists")
|
||||||
ErrOrganizationNotFound = Error("organization not found")
|
ErrOrganizationNotFound = Error("organization not found")
|
||||||
|
|
|
@ -116,7 +116,7 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error {
|
||||||
// HasCorrectColors verifies that the format of each color is correct
|
// HasCorrectColors verifies that the format of each color is correct
|
||||||
func HasCorrectColors(c *chronograf.DashboardCell) error {
|
func HasCorrectColors(c *chronograf.DashboardCell) error {
|
||||||
for _, color := range c.CellColors {
|
for _, color := range c.CellColors {
|
||||||
if !oneOf(color.Type, "max", "min", "threshold") {
|
if !oneOf(color.Type, "max", "min", "threshold", "text", "background") {
|
||||||
return chronograf.ErrInvalidColorType
|
return chronograf.ErrInvalidColorType
|
||||||
}
|
}
|
||||||
if len(color.Hex) != 7 {
|
if len(color.Hex) != 7 {
|
||||||
|
|
|
@ -25,10 +25,11 @@ import {AUTO_GROUP_BY} from 'shared/constants'
|
||||||
import {
|
import {
|
||||||
COLOR_TYPE_THRESHOLD,
|
COLOR_TYPE_THRESHOLD,
|
||||||
MAX_THRESHOLDS,
|
MAX_THRESHOLDS,
|
||||||
DEFAULT_COLORS,
|
DEFAULT_VALUE_MIN,
|
||||||
|
DEFAULT_VALUE_MAX,
|
||||||
GAUGE_COLORS,
|
GAUGE_COLORS,
|
||||||
COLOR_TYPE_MIN,
|
SINGLE_STAT_TEXT,
|
||||||
COLOR_TYPE_MAX,
|
SINGLE_STAT_BG,
|
||||||
validateColors,
|
validateColors,
|
||||||
} from 'src/dashboards/constants/gaugeColors'
|
} from 'src/dashboards/constants/gaugeColors'
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ class CellEditorOverlay extends Component {
|
||||||
source,
|
source,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT})
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
cellWorkingName: name,
|
cellWorkingName: name,
|
||||||
|
@ -56,7 +58,8 @@ class CellEditorOverlay extends Component {
|
||||||
activeQueryIndex: 0,
|
activeQueryIndex: 0,
|
||||||
isDisplayOptionsTabActive: false,
|
isDisplayOptionsTabActive: false,
|
||||||
axes,
|
axes,
|
||||||
colors: validateColors(colors) ? colors : DEFAULT_COLORS,
|
colorSingleStatText: colorsTypeContainsText,
|
||||||
|
colors: validateColors(colors, type, colorsTypeContainsText),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,17 +77,20 @@ class CellEditorOverlay extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddThreshold = () => {
|
handleAddThreshold = () => {
|
||||||
const {colors} = this.state
|
const {colors, cellWorkingType} = this.state
|
||||||
|
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||||
|
|
||||||
if (colors.length <= MAX_THRESHOLDS) {
|
if (sortedColors.length <= MAX_THRESHOLDS) {
|
||||||
const randomColor = _.random(0, GAUGE_COLORS.length)
|
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
|
||||||
|
|
||||||
const maxValue = Number(
|
const maxValue =
|
||||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
cellWorkingType === 'gauge'
|
||||||
)
|
? Number(sortedColors[sortedColors.length - 1].value)
|
||||||
const minValue = Number(
|
: DEFAULT_VALUE_MAX
|
||||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
const minValue =
|
||||||
)
|
cellWorkingType === 'gauge'
|
||||||
|
? Number(sortedColors[0].value)
|
||||||
|
: DEFAULT_VALUE_MIN
|
||||||
|
|
||||||
const colorsValues = _.mapValues(colors, 'value')
|
const colorsValues = _.mapValues(colors, 'value')
|
||||||
let randomValue
|
let randomValue
|
||||||
|
@ -135,31 +141,32 @@ class CellEditorOverlay extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleValidateColorValue = (threshold, e) => {
|
handleValidateColorValue = (threshold, e) => {
|
||||||
const {colors} = this.state
|
const {colors, cellWorkingType} = this.state
|
||||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||||
|
const thresholdValue = Number(threshold.value)
|
||||||
const targetValueNumber = Number(e.target.value)
|
const targetValueNumber = Number(e.target.value)
|
||||||
|
|
||||||
const maxValue = Number(
|
|
||||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
|
||||||
)
|
|
||||||
const minValue = Number(
|
|
||||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
|
||||||
)
|
|
||||||
|
|
||||||
let allowedToUpdate = false
|
let allowedToUpdate = false
|
||||||
|
|
||||||
// If type === min, make sure it is less than the next threshold
|
if (cellWorkingType === 'single-stat') {
|
||||||
if (threshold.type === COLOR_TYPE_MIN) {
|
// If type is single-stat then value only has to be unique
|
||||||
const nextValue = Number(sortedColors[1].value)
|
return !sortedColors.some(color => color.value === e.target.value)
|
||||||
allowedToUpdate = targetValueNumber < nextValue && targetValueNumber >= 0
|
|
||||||
}
|
}
|
||||||
// If type === max, make sure it is greater than the previous threshold
|
|
||||||
if (threshold.type === COLOR_TYPE_MAX) {
|
const minValue = Number(sortedColors[0].value)
|
||||||
|
const maxValue = Number(sortedColors[sortedColors.length - 1].value)
|
||||||
|
|
||||||
|
// If lowest value, make sure it is less than the next threshold
|
||||||
|
if (thresholdValue === minValue) {
|
||||||
|
const nextValue = Number(sortedColors[1].value)
|
||||||
|
allowedToUpdate = targetValueNumber < nextValue
|
||||||
|
}
|
||||||
|
// If highest value, make sure it is greater than the previous threshold
|
||||||
|
if (thresholdValue === maxValue) {
|
||||||
const previousValue = Number(sortedColors[sortedColors.length - 2].value)
|
const previousValue = Number(sortedColors[sortedColors.length - 2].value)
|
||||||
allowedToUpdate = previousValue < targetValueNumber
|
allowedToUpdate = previousValue < targetValueNumber
|
||||||
}
|
}
|
||||||
// If type === threshold, make sure new value is greater than min, less than max, and unique
|
// If not min or max, make sure new value is greater than min, less than max, and unique
|
||||||
if (threshold.type === COLOR_TYPE_THRESHOLD) {
|
if (thresholdValue !== minValue && thresholdValue !== maxValue) {
|
||||||
const greaterThanMin = targetValueNumber > minValue
|
const greaterThanMin = targetValueNumber > minValue
|
||||||
const lessThanMax = targetValueNumber < maxValue
|
const lessThanMax = targetValueNumber < maxValue
|
||||||
|
|
||||||
|
@ -178,6 +185,33 @@ class CellEditorOverlay extends Component {
|
||||||
return allowedToUpdate
|
return allowedToUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleToggleSingleStatText = () => {
|
||||||
|
const {colors, colorSingleStatText} = this.state
|
||||||
|
const formattedColors = colors.map(color => ({
|
||||||
|
...color,
|
||||||
|
type: colorSingleStatText ? SINGLE_STAT_BG : SINGLE_STAT_TEXT,
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
colorSingleStatText: !colorSingleStatText,
|
||||||
|
colors: formattedColors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetSuffix = e => {
|
||||||
|
const {axes} = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
axes: {
|
||||||
|
...axes,
|
||||||
|
y: {
|
||||||
|
...axes.y,
|
||||||
|
suffix: e.target.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
queryStateReducer = queryModifier => (queryID, ...payload) => {
|
queryStateReducer = queryModifier => (queryID, ...payload) => {
|
||||||
const {queriesWorkingDraft} = this.state
|
const {queriesWorkingDraft} = this.state
|
||||||
const query = queriesWorkingDraft.find(q => q.id === queryID)
|
const query = queriesWorkingDraft.find(q => q.id === queryID)
|
||||||
|
@ -287,7 +321,13 @@ class CellEditorOverlay extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectGraphType = graphType => () => {
|
handleSelectGraphType = graphType => () => {
|
||||||
this.setState({cellWorkingType: graphType})
|
const {colors, colorSingleStatText} = this.state
|
||||||
|
const validatedColors = validateColors(
|
||||||
|
colors,
|
||||||
|
graphType,
|
||||||
|
colorSingleStatText
|
||||||
|
)
|
||||||
|
this.setState({cellWorkingType: graphType, colors: validatedColors})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
|
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
|
||||||
|
@ -419,6 +459,7 @@ class CellEditorOverlay extends Component {
|
||||||
cellWorkingType,
|
cellWorkingType,
|
||||||
isDisplayOptionsTabActive,
|
isDisplayOptionsTabActive,
|
||||||
queriesWorkingDraft,
|
queriesWorkingDraft,
|
||||||
|
colorSingleStatText,
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
const queryActions = {
|
const queryActions = {
|
||||||
|
@ -472,12 +513,15 @@ class CellEditorOverlay extends Component {
|
||||||
onUpdateColorValue={this.handleUpdateColorValue}
|
onUpdateColorValue={this.handleUpdateColorValue}
|
||||||
onAddThreshold={this.handleAddThreshold}
|
onAddThreshold={this.handleAddThreshold}
|
||||||
onDeleteThreshold={this.handleDeleteThreshold}
|
onDeleteThreshold={this.handleDeleteThreshold}
|
||||||
|
onToggleSingleStatText={this.handleToggleSingleStatText}
|
||||||
|
colorSingleStatText={colorSingleStatText}
|
||||||
onSetBase={this.handleSetBase}
|
onSetBase={this.handleSetBase}
|
||||||
onSetLabel={this.handleSetLabel}
|
onSetLabel={this.handleSetLabel}
|
||||||
onSetScale={this.handleSetScale}
|
onSetScale={this.handleSetScale}
|
||||||
queryConfigs={queriesWorkingDraft}
|
queryConfigs={queriesWorkingDraft}
|
||||||
selectedGraphType={cellWorkingType}
|
selectedGraphType={cellWorkingType}
|
||||||
onSetPrefixSuffix={this.handleSetPrefixSuffix}
|
onSetPrefixSuffix={this.handleSetPrefixSuffix}
|
||||||
|
onSetSuffix={this.handleSetSuffix}
|
||||||
onSelectGraphType={this.handleSelectGraphType}
|
onSelectGraphType={this.handleSelectGraphType}
|
||||||
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
|
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
|
||||||
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
|
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, {Component, PropTypes} from 'react'
|
||||||
|
|
||||||
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
|
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
|
||||||
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
|
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
|
||||||
|
import SingleStatOptions from 'src/dashboards/components/SingleStatOptions'
|
||||||
import AxesOptions from 'src/dashboards/components/AxesOptions'
|
import AxesOptions from 'src/dashboards/components/AxesOptions'
|
||||||
|
|
||||||
import {buildDefaultYLabel} from 'shared/presenters'
|
import {buildDefaultYLabel} from 'shared/presenters'
|
||||||
|
@ -32,14 +33,13 @@ class DisplayOptions extends Component {
|
||||||
: axes
|
: axes
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderOptions = () => {
|
||||||
const {
|
const {
|
||||||
colors,
|
colors,
|
||||||
onSetBase,
|
onSetBase,
|
||||||
onSetScale,
|
onSetScale,
|
||||||
onSetLabel,
|
onSetLabel,
|
||||||
selectedGraphType,
|
selectedGraphType,
|
||||||
onSelectGraphType,
|
|
||||||
onSetPrefixSuffix,
|
onSetPrefixSuffix,
|
||||||
onSetYAxisBoundMin,
|
onSetYAxisBoundMin,
|
||||||
onSetYAxisBoundMax,
|
onSetYAxisBoundMax,
|
||||||
|
@ -48,19 +48,16 @@ class DisplayOptions extends Component {
|
||||||
onChooseColor,
|
onChooseColor,
|
||||||
onValidateColorValue,
|
onValidateColorValue,
|
||||||
onUpdateColorValue,
|
onUpdateColorValue,
|
||||||
|
colorSingleStatText,
|
||||||
|
onToggleSingleStatText,
|
||||||
|
onSetSuffix,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {axes} = this.state
|
const {axes, axes: {y: {suffix}}} = this.state
|
||||||
|
|
||||||
const isGauge = selectedGraphType === 'gauge'
|
|
||||||
|
|
||||||
|
switch (selectedGraphType) {
|
||||||
|
case 'gauge':
|
||||||
return (
|
return (
|
||||||
<div className="display-options">
|
<GaugeOptions
|
||||||
<GraphTypeSelector
|
|
||||||
selectedGraphType={selectedGraphType}
|
|
||||||
onSelectGraphType={onSelectGraphType}
|
|
||||||
/>
|
|
||||||
{isGauge
|
|
||||||
? <GaugeOptions
|
|
||||||
colors={colors}
|
colors={colors}
|
||||||
onChooseColor={onChooseColor}
|
onChooseColor={onChooseColor}
|
||||||
onValidateColorValue={onValidateColorValue}
|
onValidateColorValue={onValidateColorValue}
|
||||||
|
@ -68,7 +65,25 @@ class DisplayOptions extends Component {
|
||||||
onAddThreshold={onAddThreshold}
|
onAddThreshold={onAddThreshold}
|
||||||
onDeleteThreshold={onDeleteThreshold}
|
onDeleteThreshold={onDeleteThreshold}
|
||||||
/>
|
/>
|
||||||
: <AxesOptions
|
)
|
||||||
|
case 'single-stat':
|
||||||
|
return (
|
||||||
|
<SingleStatOptions
|
||||||
|
colors={colors}
|
||||||
|
suffix={suffix}
|
||||||
|
onSetSuffix={onSetSuffix}
|
||||||
|
onChooseColor={onChooseColor}
|
||||||
|
onValidateColorValue={onValidateColorValue}
|
||||||
|
onUpdateColorValue={onUpdateColorValue}
|
||||||
|
onAddThreshold={onAddThreshold}
|
||||||
|
onDeleteThreshold={onDeleteThreshold}
|
||||||
|
colorSingleStatText={colorSingleStatText}
|
||||||
|
onToggleSingleStatText={onToggleSingleStatText}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<AxesOptions
|
||||||
selectedGraphType={selectedGraphType}
|
selectedGraphType={selectedGraphType}
|
||||||
axes={axes}
|
axes={axes}
|
||||||
onSetBase={onSetBase}
|
onSetBase={onSetBase}
|
||||||
|
@ -77,12 +92,26 @@ class DisplayOptions extends Component {
|
||||||
onSetPrefixSuffix={onSetPrefixSuffix}
|
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||||
/>}
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {selectedGraphType, onSelectGraphType} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="display-options">
|
||||||
|
<GraphTypeSelector
|
||||||
|
selectedGraphType={selectedGraphType}
|
||||||
|
onSelectGraphType={onSelectGraphType}
|
||||||
|
/>
|
||||||
|
{this.renderOptions()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const {arrayOf, func, shape, string} = PropTypes
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
DisplayOptions.propTypes = {
|
DisplayOptions.propTypes = {
|
||||||
onAddThreshold: func.isRequired,
|
onAddThreshold: func.isRequired,
|
||||||
|
@ -93,6 +122,7 @@ DisplayOptions.propTypes = {
|
||||||
selectedGraphType: string.isRequired,
|
selectedGraphType: string.isRequired,
|
||||||
onSelectGraphType: func.isRequired,
|
onSelectGraphType: func.isRequired,
|
||||||
onSetPrefixSuffix: func.isRequired,
|
onSetPrefixSuffix: func.isRequired,
|
||||||
|
onSetSuffix: func.isRequired,
|
||||||
onSetYAxisBoundMin: func.isRequired,
|
onSetYAxisBoundMin: func.isRequired,
|
||||||
onSetYAxisBoundMax: func.isRequired,
|
onSetYAxisBoundMax: func.isRequired,
|
||||||
onSetScale: func.isRequired,
|
onSetScale: func.isRequired,
|
||||||
|
@ -109,6 +139,8 @@ DisplayOptions.propTypes = {
|
||||||
}).isRequired
|
}).isRequired
|
||||||
),
|
),
|
||||||
queryConfigs: arrayOf(shape()).isRequired,
|
queryConfigs: arrayOf(shape()).isRequired,
|
||||||
|
colorSingleStatText: bool.isRequired,
|
||||||
|
onToggleSingleStatText: func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DisplayOptions
|
export default DisplayOptions
|
||||||
|
|
|
@ -2,12 +2,11 @@ import React, {PropTypes} from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||||
import GaugeThreshold from 'src/dashboards/components/GaugeThreshold'
|
import Threshold from 'src/dashboards/components/Threshold'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MAX_THRESHOLDS,
|
MAX_THRESHOLDS,
|
||||||
MIN_THRESHOLDS,
|
MIN_THRESHOLDS,
|
||||||
DEFAULT_COLORS,
|
|
||||||
} from 'src/dashboards/constants/gaugeColors'
|
} from 'src/dashboards/constants/gaugeColors'
|
||||||
|
|
||||||
const GaugeOptions = ({
|
const GaugeOptions = ({
|
||||||
|
@ -19,9 +18,7 @@ const GaugeOptions = ({
|
||||||
onUpdateColorValue,
|
onUpdateColorValue,
|
||||||
}) => {
|
}) => {
|
||||||
const disableMaxColor = colors.length > MIN_THRESHOLDS
|
const disableMaxColor = colors.length > MIN_THRESHOLDS
|
||||||
|
|
||||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||||
|
|
||||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -32,8 +29,20 @@ const GaugeOptions = ({
|
||||||
<div className="display-options--cell-wrapper">
|
<div className="display-options--cell-wrapper">
|
||||||
<h5 className="display-options--header">Gauge Controls</h5>
|
<h5 className="display-options--header">Gauge Controls</h5>
|
||||||
<div className="gauge-controls">
|
<div className="gauge-controls">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||||
|
onClick={onAddThreshold}
|
||||||
|
disabled={disableAddThreshold}
|
||||||
|
>
|
||||||
|
<span className="icon plus" /> Add Threshold
|
||||||
|
</button>
|
||||||
{sortedColors.map(color =>
|
{sortedColors.map(color =>
|
||||||
<GaugeThreshold
|
<Threshold
|
||||||
|
isMin={color.value === sortedColors[0].value}
|
||||||
|
isMax={
|
||||||
|
color.value === sortedColors[sortedColors.length - 1].value
|
||||||
|
}
|
||||||
|
visualizationType="gauge"
|
||||||
threshold={color}
|
threshold={color}
|
||||||
key={color.id}
|
key={color.id}
|
||||||
disableMaxColor={disableMaxColor}
|
disableMaxColor={disableMaxColor}
|
||||||
|
@ -43,13 +52,6 @@ const GaugeOptions = ({
|
||||||
onDeleteThreshold={onDeleteThreshold}
|
onDeleteThreshold={onDeleteThreshold}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
|
||||||
onClick={onAddThreshold}
|
|
||||||
disabled={disableAddThreshold}
|
|
||||||
>
|
|
||||||
<span className="icon plus" /> Add Threshold
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FancyScrollbar>
|
</FancyScrollbar>
|
||||||
|
@ -58,10 +60,6 @@ const GaugeOptions = ({
|
||||||
|
|
||||||
const {arrayOf, func, shape, string} = PropTypes
|
const {arrayOf, func, shape, string} = PropTypes
|
||||||
|
|
||||||
GaugeOptions.defaultProps = {
|
|
||||||
colors: DEFAULT_COLORS,
|
|
||||||
}
|
|
||||||
|
|
||||||
GaugeOptions.propTypes = {
|
GaugeOptions.propTypes = {
|
||||||
colors: arrayOf(
|
colors: arrayOf(
|
||||||
shape({
|
shape({
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||||
|
import Threshold from 'src/dashboards/components/Threshold'
|
||||||
|
|
||||||
|
import {MAX_THRESHOLDS} from 'src/dashboards/constants/gaugeColors'
|
||||||
|
|
||||||
|
const SingleStatOptions = ({
|
||||||
|
suffix,
|
||||||
|
onSetSuffix,
|
||||||
|
colors,
|
||||||
|
onAddThreshold,
|
||||||
|
onDeleteThreshold,
|
||||||
|
onChooseColor,
|
||||||
|
onValidateColorValue,
|
||||||
|
onUpdateColorValue,
|
||||||
|
colorSingleStatText,
|
||||||
|
onToggleSingleStatText,
|
||||||
|
}) => {
|
||||||
|
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||||
|
|
||||||
|
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FancyScrollbar
|
||||||
|
className="display-options--cell y-axis-controls"
|
||||||
|
autoHide={false}
|
||||||
|
>
|
||||||
|
<div className="display-options--cell-wrapper">
|
||||||
|
<h5 className="display-options--header">Single Stat Controls</h5>
|
||||||
|
<div className="gauge-controls">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||||
|
onClick={onAddThreshold}
|
||||||
|
disabled={disableAddThreshold}
|
||||||
|
>
|
||||||
|
<span className="icon plus" /> Add Threshold
|
||||||
|
</button>
|
||||||
|
{sortedColors.map(color =>
|
||||||
|
<Threshold
|
||||||
|
visualizationType="single-stat"
|
||||||
|
threshold={color}
|
||||||
|
key={color.id}
|
||||||
|
onChooseColor={onChooseColor}
|
||||||
|
onValidateColorValue={onValidateColorValue}
|
||||||
|
onUpdateColorValue={onUpdateColorValue}
|
||||||
|
onDeleteThreshold={onDeleteThreshold}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="single-stat-controls">
|
||||||
|
<div className="form-group col-xs-6">
|
||||||
|
<label>Coloring</label>
|
||||||
|
<ul className="nav nav-tablist nav-tablist-sm">
|
||||||
|
<li
|
||||||
|
className={colorSingleStatText ? null : 'active'}
|
||||||
|
onClick={onToggleSingleStatText}
|
||||||
|
>
|
||||||
|
Background
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className={colorSingleStatText ? 'active' : null}
|
||||||
|
onClick={onToggleSingleStatText}
|
||||||
|
>
|
||||||
|
Text
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="form-group col-xs-6">
|
||||||
|
<label>Suffix</label>
|
||||||
|
<input
|
||||||
|
className="form-control input-sm"
|
||||||
|
placeholder="%, MPH, etc."
|
||||||
|
defaultValue={suffix}
|
||||||
|
onChange={onSetSuffix}
|
||||||
|
maxLength="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FancyScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
SingleStatOptions.defaultProps = {
|
||||||
|
colors: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleStatOptions.propTypes = {
|
||||||
|
colors: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
hex: string.isRequired,
|
||||||
|
id: string.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
value: string.isRequired,
|
||||||
|
}).isRequired
|
||||||
|
),
|
||||||
|
onAddThreshold: func.isRequired,
|
||||||
|
onDeleteThreshold: func.isRequired,
|
||||||
|
onChooseColor: func.isRequired,
|
||||||
|
onValidateColorValue: func.isRequired,
|
||||||
|
onUpdateColorValue: func.isRequired,
|
||||||
|
colorSingleStatText: bool.isRequired,
|
||||||
|
onToggleSingleStatText: func.isRequired,
|
||||||
|
onSetSuffix: func.isRequired,
|
||||||
|
suffix: string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SingleStatOptions
|
|
@ -2,13 +2,9 @@ import React, {Component, PropTypes} from 'react'
|
||||||
|
|
||||||
import ColorDropdown from 'shared/components/ColorDropdown'
|
import ColorDropdown from 'shared/components/ColorDropdown'
|
||||||
|
|
||||||
import {
|
import {GAUGE_COLORS} from 'src/dashboards/constants/gaugeColors'
|
||||||
COLOR_TYPE_MIN,
|
|
||||||
COLOR_TYPE_MAX,
|
|
||||||
GAUGE_COLORS,
|
|
||||||
} from 'src/dashboards/constants/gaugeColors'
|
|
||||||
|
|
||||||
class GaugeThreshold extends Component {
|
class Threshold extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
@ -36,27 +32,34 @@ class GaugeThreshold extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
visualizationType,
|
||||||
threshold,
|
threshold,
|
||||||
threshold: {type, hex, name},
|
threshold: {hex, name},
|
||||||
disableMaxColor,
|
disableMaxColor,
|
||||||
onChooseColor,
|
onChooseColor,
|
||||||
onDeleteThreshold,
|
onDeleteThreshold,
|
||||||
|
isMin,
|
||||||
|
isMax,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {workingValue, valid} = this.state
|
const {workingValue, valid} = this.state
|
||||||
const selectedColor = {hex, name}
|
const selectedColor = {hex, name}
|
||||||
|
|
||||||
const labelClass =
|
let label = 'Threshold'
|
||||||
type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX
|
let labelClass = 'gauge-controls--label-editable'
|
||||||
|
let canBeDeleted = true
|
||||||
|
|
||||||
|
if (visualizationType === 'gauge') {
|
||||||
|
labelClass =
|
||||||
|
isMin || isMax
|
||||||
? 'gauge-controls--label'
|
? 'gauge-controls--label'
|
||||||
: 'gauge-controls--label-editable'
|
: 'gauge-controls--label-editable'
|
||||||
|
canBeDeleted = !(isMin || isMax)
|
||||||
|
}
|
||||||
|
|
||||||
const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX)
|
if (isMin && visualizationType === 'gauge') {
|
||||||
|
|
||||||
let label = 'Threshold'
|
|
||||||
if (type === COLOR_TYPE_MIN) {
|
|
||||||
label = 'Minimum'
|
label = 'Minimum'
|
||||||
}
|
}
|
||||||
if (type === COLOR_TYPE_MAX) {
|
if (isMax && visualizationType === 'gauge') {
|
||||||
label = 'Maximum'
|
label = 'Maximum'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,13 +86,12 @@ class GaugeThreshold extends Component {
|
||||||
type="number"
|
type="number"
|
||||||
onChange={this.handleChangeWorkingValue}
|
onChange={this.handleChangeWorkingValue}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
min={0}
|
|
||||||
/>
|
/>
|
||||||
<ColorDropdown
|
<ColorDropdown
|
||||||
colors={GAUGE_COLORS}
|
colors={GAUGE_COLORS}
|
||||||
selected={selectedColor}
|
selected={selectedColor}
|
||||||
onChoose={onChooseColor(threshold)}
|
onChoose={onChooseColor(threshold)}
|
||||||
disabled={type === COLOR_TYPE_MAX && disableMaxColor}
|
disabled={isMax && disableMaxColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -98,7 +100,8 @@ class GaugeThreshold extends Component {
|
||||||
|
|
||||||
const {bool, func, shape, string} = PropTypes
|
const {bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
GaugeThreshold.propTypes = {
|
Threshold.propTypes = {
|
||||||
|
visualizationType: string.isRequired,
|
||||||
threshold: shape({
|
threshold: shape({
|
||||||
type: string.isRequired,
|
type: string.isRequired,
|
||||||
hex: string.isRequired,
|
hex: string.isRequired,
|
||||||
|
@ -111,6 +114,8 @@ GaugeThreshold.propTypes = {
|
||||||
onValidateColorValue: func.isRequired,
|
onValidateColorValue: func.isRequired,
|
||||||
onUpdateColorValue: func.isRequired,
|
onUpdateColorValue: func.isRequired,
|
||||||
onDeleteThreshold: func.isRequired,
|
onDeleteThreshold: func.isRequired,
|
||||||
|
isMin: bool,
|
||||||
|
isMax: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GaugeThreshold
|
export default Threshold
|
|
@ -1,3 +1,5 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
export const MAX_THRESHOLDS = 5
|
export const MAX_THRESHOLDS = 5
|
||||||
export const MIN_THRESHOLDS = 2
|
export const MIN_THRESHOLDS = 2
|
||||||
|
|
||||||
|
@ -7,6 +9,9 @@ export const COLOR_TYPE_MAX = 'max'
|
||||||
export const DEFAULT_VALUE_MAX = '100'
|
export const DEFAULT_VALUE_MAX = '100'
|
||||||
export const COLOR_TYPE_THRESHOLD = 'threshold'
|
export const COLOR_TYPE_THRESHOLD = 'threshold'
|
||||||
|
|
||||||
|
export const SINGLE_STAT_TEXT = 'text'
|
||||||
|
export const SINGLE_STAT_BG = 'background'
|
||||||
|
|
||||||
export const GAUGE_COLORS = [
|
export const GAUGE_COLORS = [
|
||||||
{
|
{
|
||||||
hex: '#BF3D5E',
|
hex: '#BF3D5E',
|
||||||
|
@ -95,12 +100,27 @@ export const DEFAULT_COLORS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const validateColors = colors => {
|
export const validateColors = (colors, type, colorSingleStatText) => {
|
||||||
if (!colors) {
|
if (type === 'single-stat') {
|
||||||
return false
|
// Single stat colors should all have type of 'text' or 'background'
|
||||||
|
const colorType = colorSingleStatText ? SINGLE_STAT_TEXT : SINGLE_STAT_BG
|
||||||
|
return colors ? colors.map(color => ({...color, type: colorType})) : null
|
||||||
|
}
|
||||||
|
if (!colors || colors.length === 0) {
|
||||||
|
return DEFAULT_COLORS
|
||||||
|
}
|
||||||
|
if (type === 'gauge') {
|
||||||
|
// Gauge colors should have a type of min, any number of thresholds, and a max
|
||||||
|
const formatttedColors = _.sortBy(colors, color =>
|
||||||
|
Number(color.value)
|
||||||
|
).map(c => ({
|
||||||
|
...c,
|
||||||
|
type: COLOR_TYPE_THRESHOLD,
|
||||||
|
}))
|
||||||
|
formatttedColors[0].type = COLOR_TYPE_MIN
|
||||||
|
formatttedColors[formatttedColors.length - 1].type = COLOR_TYPE_MAX
|
||||||
|
return formatttedColors
|
||||||
}
|
}
|
||||||
const hasMin = colors.some(color => color.type === COLOR_TYPE_MIN)
|
|
||||||
const hasMax = colors.some(color => color.type === COLOR_TYPE_MAX)
|
|
||||||
|
|
||||||
return hasMin && hasMax
|
return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,13 +39,16 @@ const RefreshingGraph = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'single-stat') {
|
if (type === 'single-stat') {
|
||||||
|
const suffix = axes.y.suffix || ''
|
||||||
return (
|
return (
|
||||||
<RefreshingSingleStat
|
<RefreshingSingleStat
|
||||||
|
colors={colors}
|
||||||
key={manualRefresh}
|
key={manualRefresh}
|
||||||
queries={[queries[0]]}
|
queries={[queries[0]]}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
cellHeight={cellHeight}
|
cellHeight={cellHeight}
|
||||||
|
suffix={suffix}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React, {PropTypes, Component} from 'react'
|
import React, {PropTypes, PureComponent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import shallowCompare from 'react-addons-shallow-compare'
|
|
||||||
import lastValues from 'shared/parsing/lastValues'
|
import lastValues from 'shared/parsing/lastValues'
|
||||||
|
|
||||||
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
|
import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers'
|
||||||
|
import {SINGLE_STAT_TEXT} from 'src/dashboards/constants/gaugeColors'
|
||||||
|
import {isBackgroundLight} from 'shared/constants/colorOperations'
|
||||||
|
|
||||||
class SingleStat extends Component {
|
const darkText = '#292933'
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
const lightText = '#ffffff'
|
||||||
return shallowCompare(this, nextProps, nextState)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class SingleStat extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const {data, cellHeight, isFetchingInitially} = this.props
|
const {data, cellHeight, isFetchingInitially, colors, suffix} = this.props
|
||||||
|
|
||||||
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
||||||
if (isFetchingInitially) {
|
if (isFetchingInitially) {
|
||||||
|
@ -26,27 +27,65 @@ class SingleStat extends Component {
|
||||||
|
|
||||||
const precision = 100.0
|
const precision = 100.0
|
||||||
const roundedValue = Math.round(+lastValue * precision) / precision
|
const roundedValue = Math.round(+lastValue * precision) / precision
|
||||||
|
let bgColor = null
|
||||||
|
let textColor = null
|
||||||
|
let className = 'single-stat'
|
||||||
|
|
||||||
|
if (colors && colors.length > 0) {
|
||||||
|
className = 'single-stat single-stat--colored'
|
||||||
|
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||||
|
const nearestCrossedThreshold = sortedColors
|
||||||
|
.filter(color => lastValue > color.value)
|
||||||
|
.pop()
|
||||||
|
|
||||||
|
const colorizeText = _.some(colors, {type: SINGLE_STAT_TEXT})
|
||||||
|
|
||||||
|
if (colorizeText) {
|
||||||
|
textColor = nearestCrossedThreshold
|
||||||
|
? nearestCrossedThreshold.hex
|
||||||
|
: '#292933'
|
||||||
|
} else {
|
||||||
|
bgColor = nearestCrossedThreshold
|
||||||
|
? nearestCrossedThreshold.hex
|
||||||
|
: '#292933'
|
||||||
|
textColor = isBackgroundLight(bgColor) ? darkText : lightText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="single-stat">
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{backgroundColor: bgColor, color: textColor}}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className={classnames('single-stat--value', {
|
className={classnames('single-stat--value', {
|
||||||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{roundedValue}
|
{roundedValue}
|
||||||
|
{suffix}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {arrayOf, bool, number, shape} = PropTypes
|
const {arrayOf, bool, number, shape, string} = PropTypes
|
||||||
|
|
||||||
SingleStat.propTypes = {
|
SingleStat.propTypes = {
|
||||||
data: arrayOf(shape()).isRequired,
|
data: arrayOf(shape()).isRequired,
|
||||||
isFetchingInitially: bool,
|
isFetchingInitially: bool,
|
||||||
cellHeight: number,
|
cellHeight: number,
|
||||||
|
colors: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
hex: string.isRequired,
|
||||||
|
id: string.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
value: string.isRequired,
|
||||||
|
}).isRequired
|
||||||
|
),
|
||||||
|
suffix: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SingleStat
|
export default SingleStat
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
const hexToRgb = hex => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const averageRgbValues = valuesObject => {
|
||||||
|
const {r, g, b} = valuesObject
|
||||||
|
return (r + g + b) / 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const trueNeutralGrey = 128
|
||||||
|
|
||||||
|
export const isBackgroundLight = backgroundColor => {
|
||||||
|
const averageBackground = averageRgbValues(hexToRgb(backgroundColor))
|
||||||
|
const isLight = averageBackground > trueNeutralGrey
|
||||||
|
|
||||||
|
return isLight
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Cell Editor Overlay - Display Options
|
Cell Editor Overlay - Display Options
|
||||||
------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$graph-type--gutter: 4px;
|
$graph-type--gutter: 4px;
|
||||||
|
@ -200,7 +200,7 @@ $graph-type--gutter: 4px;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Cell Editor Overlay - Gauge Controls
|
Cell Editor Overlay - Gauge Controls
|
||||||
------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
.gauge-controls {
|
.gauge-controls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -212,7 +212,7 @@ $graph-type--gutter: 4px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-bottom: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -244,3 +244,18 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cell Editor Overlay - Single-Stat Controls
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
.single-stat-controls {
|
||||||
|
display: inline-block;
|
||||||
|
width: calc(100% + 12px);
|
||||||
|
margin: 30px -6px 0 -6px;
|
||||||
|
|
||||||
|
> div.form-group {
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -75,10 +75,13 @@
|
||||||
/* Single Stat Cells */
|
/* Single Stat Cells */
|
||||||
.single-stat {
|
.single-stat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
left: 2px;
|
||||||
height: 100%;
|
width: calc(100% - 4px);
|
||||||
|
height: calc(100% - 2px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
border-radius: 3px;
|
||||||
@include no-user-select();
|
@include no-user-select();
|
||||||
|
color: $c-laser;
|
||||||
|
|
||||||
&.graph-single-stat {
|
&.graph-single-stat {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -89,19 +92,20 @@
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.single-stat.single-stat--colored {
|
||||||
|
transition: background-color 0.25s ease, color 0.25s ease;
|
||||||
|
}
|
||||||
.single-stat--value {
|
.single-stat--value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 15px);
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%,-50%);
|
transform: translate(-50%,-50%);
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
// overflow: hidden;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
// text-overflow: ellipsis;
|
|
||||||
font-size: 54px;
|
font-size: 54px;
|
||||||
line-height: 54px;
|
line-height: 54px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: $c-laser;
|
color: inherit;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
&.single-stat--small {
|
&.single-stat--small {
|
||||||
|
@ -130,6 +134,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Legend Styles
|
Legend Styles
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue