Merge pull request #2598 from influxdata/single-stat-colors-polish

Single Stat Colors Polish
pull/10616/head
Alex Paxton 2017-12-22 12:48:50 -08:00 committed by GitHub
commit da2d8a4fbd
14 changed files with 419 additions and 118 deletions

View File

@ -28,6 +28,9 @@
## v1.4.0.0-beta2 [2017-12-14]
### UI Improvements
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
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

View File

@ -23,7 +23,7 @@ const (
ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method")
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")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")

View File

@ -116,7 +116,7 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error {
// HasCorrectColors verifies that the format of each color is correct
func HasCorrectColors(c *chronograf.DashboardCell) error {
for _, color := range c.CellColors {
if !oneOf(color.Type, "max", "min", "threshold") {
if !oneOf(color.Type, "max", "min", "threshold", "text", "background") {
return chronograf.ErrInvalidColorType
}
if len(color.Hex) != 7 {

View File

@ -25,10 +25,11 @@ import {AUTO_GROUP_BY} from 'shared/constants'
import {
COLOR_TYPE_THRESHOLD,
MAX_THRESHOLDS,
DEFAULT_COLORS,
DEFAULT_VALUE_MIN,
DEFAULT_VALUE_MAX,
GAUGE_COLORS,
COLOR_TYPE_MIN,
COLOR_TYPE_MAX,
SINGLE_STAT_TEXT,
SINGLE_STAT_BG,
validateColors,
} from 'src/dashboards/constants/gaugeColors'
@ -48,6 +49,7 @@ class CellEditorOverlay extends Component {
source,
}))
)
const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT})
this.state = {
cellWorkingName: name,
@ -56,7 +58,8 @@ class CellEditorOverlay extends Component {
activeQueryIndex: 0,
isDisplayOptionsTabActive: false,
axes,
colors: validateColors(colors) ? colors : DEFAULT_COLORS,
colorSingleStatText: colorsTypeContainsText,
colors: validateColors(colors, type, colorsTypeContainsText),
}
}
@ -74,17 +77,20 @@ class CellEditorOverlay extends Component {
}
handleAddThreshold = () => {
const {colors} = this.state
const {colors, cellWorkingType} = this.state
const sortedColors = _.sortBy(colors, color => Number(color.value))
if (colors.length <= MAX_THRESHOLDS) {
const randomColor = _.random(0, GAUGE_COLORS.length)
if (sortedColors.length <= MAX_THRESHOLDS) {
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
const maxValue = Number(
colors.find(color => color.type === COLOR_TYPE_MAX).value
)
const minValue = Number(
colors.find(color => color.type === COLOR_TYPE_MIN).value
)
const maxValue =
cellWorkingType === 'gauge'
? Number(sortedColors[sortedColors.length - 1].value)
: DEFAULT_VALUE_MAX
const minValue =
cellWorkingType === 'gauge'
? Number(sortedColors[0].value)
: DEFAULT_VALUE_MIN
const colorsValues = _.mapValues(colors, 'value')
let randomValue
@ -135,31 +141,32 @@ class CellEditorOverlay extends Component {
}
handleValidateColorValue = (threshold, e) => {
const {colors} = this.state
const {colors, cellWorkingType} = this.state
const sortedColors = _.sortBy(colors, color => Number(color.value))
const thresholdValue = Number(threshold.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
// If type === min, make sure it is less than the next threshold
if (threshold.type === COLOR_TYPE_MIN) {
const nextValue = Number(sortedColors[1].value)
allowedToUpdate = targetValueNumber < nextValue && targetValueNumber >= 0
if (cellWorkingType === 'single-stat') {
// If type is single-stat then value only has to be unique
return !sortedColors.some(color => color.value === e.target.value)
}
// 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)
allowedToUpdate = previousValue < targetValueNumber
}
// If type === threshold, make sure new value is greater than min, less than max, and unique
if (threshold.type === COLOR_TYPE_THRESHOLD) {
// If not min or max, make sure new value is greater than min, less than max, and unique
if (thresholdValue !== minValue && thresholdValue !== maxValue) {
const greaterThanMin = targetValueNumber > minValue
const lessThanMax = targetValueNumber < maxValue
@ -178,6 +185,33 @@ class CellEditorOverlay extends Component {
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) => {
const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find(q => q.id === queryID)
@ -287,7 +321,13 @@ class CellEditorOverlay extends Component {
}
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 => () => {
@ -419,6 +459,7 @@ class CellEditorOverlay extends Component {
cellWorkingType,
isDisplayOptionsTabActive,
queriesWorkingDraft,
colorSingleStatText,
} = this.state
const queryActions = {
@ -472,12 +513,15 @@ class CellEditorOverlay extends Component {
onUpdateColorValue={this.handleUpdateColorValue}
onAddThreshold={this.handleAddThreshold}
onDeleteThreshold={this.handleDeleteThreshold}
onToggleSingleStatText={this.handleToggleSingleStatText}
colorSingleStatText={colorSingleStatText}
onSetBase={this.handleSetBase}
onSetLabel={this.handleSetLabel}
onSetScale={this.handleSetScale}
queryConfigs={queriesWorkingDraft}
selectedGraphType={cellWorkingType}
onSetPrefixSuffix={this.handleSetPrefixSuffix}
onSetSuffix={this.handleSetSuffix}
onSelectGraphType={this.handleSelectGraphType}
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}

View File

@ -2,6 +2,7 @@ import React, {Component, PropTypes} from 'react'
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
import SingleStatOptions from 'src/dashboards/components/SingleStatOptions'
import AxesOptions from 'src/dashboards/components/AxesOptions'
import {buildDefaultYLabel} from 'shared/presenters'
@ -32,14 +33,13 @@ class DisplayOptions extends Component {
: axes
}
render() {
renderOptions = () => {
const {
colors,
onSetBase,
onSetScale,
onSetLabel,
selectedGraphType,
onSelectGraphType,
onSetPrefixSuffix,
onSetYAxisBoundMin,
onSetYAxisBoundMax,
@ -48,19 +48,16 @@ class DisplayOptions extends Component {
onChooseColor,
onValidateColorValue,
onUpdateColorValue,
colorSingleStatText,
onToggleSingleStatText,
onSetSuffix,
} = this.props
const {axes} = this.state
const isGauge = selectedGraphType === 'gauge'
const {axes, axes: {y: {suffix}}} = this.state
switch (selectedGraphType) {
case 'gauge':
return (
<div className="display-options">
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
{isGauge
? <GaugeOptions
<GaugeOptions
colors={colors}
onChooseColor={onChooseColor}
onValidateColorValue={onValidateColorValue}
@ -68,7 +65,25 @@ class DisplayOptions extends Component {
onAddThreshold={onAddThreshold}
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}
axes={axes}
onSetBase={onSetBase}
@ -77,12 +92,26 @@ class DisplayOptions extends Component {
onSetPrefixSuffix={onSetPrefixSuffix}
onSetYAxisBoundMin={onSetYAxisBoundMin}
onSetYAxisBoundMax={onSetYAxisBoundMax}
/>}
/>
)
}
}
render() {
const {selectedGraphType, onSelectGraphType} = this.props
return (
<div className="display-options">
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
{this.renderOptions()}
</div>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
const {arrayOf, bool, func, shape, string} = PropTypes
DisplayOptions.propTypes = {
onAddThreshold: func.isRequired,
@ -93,6 +122,7 @@ DisplayOptions.propTypes = {
selectedGraphType: string.isRequired,
onSelectGraphType: func.isRequired,
onSetPrefixSuffix: func.isRequired,
onSetSuffix: func.isRequired,
onSetYAxisBoundMin: func.isRequired,
onSetYAxisBoundMax: func.isRequired,
onSetScale: func.isRequired,
@ -109,6 +139,8 @@ DisplayOptions.propTypes = {
}).isRequired
),
queryConfigs: arrayOf(shape()).isRequired,
colorSingleStatText: bool.isRequired,
onToggleSingleStatText: func.isRequired,
}
export default DisplayOptions

View File

@ -2,12 +2,11 @@ import React, {PropTypes} from 'react'
import _ from 'lodash'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import GaugeThreshold from 'src/dashboards/components/GaugeThreshold'
import Threshold from 'src/dashboards/components/Threshold'
import {
MAX_THRESHOLDS,
MIN_THRESHOLDS,
DEFAULT_COLORS,
} from 'src/dashboards/constants/gaugeColors'
const GaugeOptions = ({
@ -19,9 +18,7 @@ const GaugeOptions = ({
onUpdateColorValue,
}) => {
const disableMaxColor = colors.length > MIN_THRESHOLDS
const disableAddThreshold = colors.length > MAX_THRESHOLDS
const sortedColors = _.sortBy(colors, color => Number(color.value))
return (
@ -32,8 +29,20 @@ const GaugeOptions = ({
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Gauge 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 =>
<GaugeThreshold
<Threshold
isMin={color.value === sortedColors[0].value}
isMax={
color.value === sortedColors[sortedColors.length - 1].value
}
visualizationType="gauge"
threshold={color}
key={color.id}
disableMaxColor={disableMaxColor}
@ -43,13 +52,6 @@ const GaugeOptions = ({
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>
</FancyScrollbar>
@ -58,10 +60,6 @@ const GaugeOptions = ({
const {arrayOf, func, shape, string} = PropTypes
GaugeOptions.defaultProps = {
colors: DEFAULT_COLORS,
}
GaugeOptions.propTypes = {
colors: arrayOf(
shape({

View File

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

View File

@ -2,13 +2,9 @@ import React, {Component, PropTypes} from 'react'
import ColorDropdown from 'shared/components/ColorDropdown'
import {
COLOR_TYPE_MIN,
COLOR_TYPE_MAX,
GAUGE_COLORS,
} from 'src/dashboards/constants/gaugeColors'
import {GAUGE_COLORS} from 'src/dashboards/constants/gaugeColors'
class GaugeThreshold extends Component {
class Threshold extends Component {
constructor(props) {
super(props)
@ -36,27 +32,34 @@ class GaugeThreshold extends Component {
render() {
const {
visualizationType,
threshold,
threshold: {type, hex, name},
threshold: {hex, name},
disableMaxColor,
onChooseColor,
onDeleteThreshold,
isMin,
isMax,
} = this.props
const {workingValue, valid} = this.state
const selectedColor = {hex, name}
const labelClass =
type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX
let label = 'Threshold'
let labelClass = 'gauge-controls--label-editable'
let canBeDeleted = true
if (visualizationType === 'gauge') {
labelClass =
isMin || isMax
? 'gauge-controls--label'
: 'gauge-controls--label-editable'
canBeDeleted = !(isMin || isMax)
}
const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX)
let label = 'Threshold'
if (type === COLOR_TYPE_MIN) {
if (isMin && visualizationType === 'gauge') {
label = 'Minimum'
}
if (type === COLOR_TYPE_MAX) {
if (isMax && visualizationType === 'gauge') {
label = 'Maximum'
}
@ -83,13 +86,12 @@ class GaugeThreshold extends Component {
type="number"
onChange={this.handleChangeWorkingValue}
onBlur={this.handleBlur}
min={0}
/>
<ColorDropdown
colors={GAUGE_COLORS}
selected={selectedColor}
onChoose={onChooseColor(threshold)}
disabled={type === COLOR_TYPE_MAX && disableMaxColor}
disabled={isMax && disableMaxColor}
/>
</div>
)
@ -98,7 +100,8 @@ class GaugeThreshold extends Component {
const {bool, func, shape, string} = PropTypes
GaugeThreshold.propTypes = {
Threshold.propTypes = {
visualizationType: string.isRequired,
threshold: shape({
type: string.isRequired,
hex: string.isRequired,
@ -111,6 +114,8 @@ GaugeThreshold.propTypes = {
onValidateColorValue: func.isRequired,
onUpdateColorValue: func.isRequired,
onDeleteThreshold: func.isRequired,
isMin: bool,
isMax: bool,
}
export default GaugeThreshold
export default Threshold

View File

@ -1,3 +1,5 @@
import _ from 'lodash'
export const MAX_THRESHOLDS = 5
export const MIN_THRESHOLDS = 2
@ -7,6 +9,9 @@ export const COLOR_TYPE_MAX = 'max'
export const DEFAULT_VALUE_MAX = '100'
export const COLOR_TYPE_THRESHOLD = 'threshold'
export const SINGLE_STAT_TEXT = 'text'
export const SINGLE_STAT_BG = 'background'
export const GAUGE_COLORS = [
{
hex: '#BF3D5E',
@ -95,12 +100,27 @@ export const DEFAULT_COLORS = [
},
]
export const validateColors = colors => {
if (!colors) {
return false
export const validateColors = (colors, type, colorSingleStatText) => {
if (type === 'single-stat') {
// 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
}

View File

@ -39,13 +39,16 @@ const RefreshingGraph = ({
}
if (type === 'single-stat') {
const suffix = axes.y.suffix || ''
return (
<RefreshingSingleStat
colors={colors}
key={manualRefresh}
queries={[queries[0]]}
templates={templates}
autoRefresh={autoRefresh}
cellHeight={cellHeight}
suffix={suffix}
/>
)
}

View File

@ -1,17 +1,18 @@
import React, {PropTypes, Component} from 'react'
import React, {PropTypes, PureComponent} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import shallowCompare from 'react-addons-shallow-compare'
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 {
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState)
}
const darkText = '#292933'
const lightText = '#ffffff'
class SingleStat extends PureComponent {
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 (isFetchingInitially) {
@ -26,27 +27,65 @@ class SingleStat extends Component {
const precision = 100.0
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 (
<div className="single-stat">
<div
className={className}
style={{backgroundColor: bgColor, color: textColor}}
>
<span
className={classnames('single-stat--value', {
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
})}
>
{roundedValue}
{suffix}
</span>
</div>
)
}
}
const {arrayOf, bool, number, shape} = PropTypes
const {arrayOf, bool, number, shape, string} = PropTypes
SingleStat.propTypes = {
data: arrayOf(shape()).isRequired,
isFetchingInitially: bool,
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

View File

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

View File

@ -1,6 +1,6 @@
/*
Cell Editor Overlay - Display Options
------------------------------------------------------
------------------------------------------------------------------------------
*/
$graph-type--gutter: 4px;
@ -200,7 +200,7 @@ $graph-type--gutter: 4px;
/*
Cell Editor Overlay - Gauge Controls
------------------------------------------------------
------------------------------------------------------------------------------
*/
.gauge-controls {
width: 100%;
@ -212,7 +212,7 @@ $graph-type--gutter: 4px;
flex-wrap: nowrap;
align-items: center;
height: 30px;
margin-bottom: 8px;
margin-top: 8px;
}
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
width: 100%;
@ -244,3 +244,18 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
flex: 1 0 0;
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;
}
}

View File

@ -75,10 +75,13 @@
/* Single Stat Cells */
.single-stat {
position: absolute;
width: 100%;
height: 100%;
left: 2px;
width: calc(100% - 4px);
height: calc(100% - 2px);
pointer-events: none;
border-radius: 3px;
@include no-user-select();
color: $c-laser;
&.graph-single-stat {
top: 0;
@ -89,19 +92,20 @@
height: 100% !important;
}
}
.single-stat.single-stat--colored {
transition: background-color 0.25s ease, color 0.25s ease;
}
.single-stat--value {
position: absolute;
top: calc(50% - 15px);
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: calc(100% - 32px);
// overflow: hidden;
text-align: center;
// text-overflow: ellipsis;
font-size: 54px;
line-height: 54px;
font-weight: 300;
color: $c-laser;
color: inherit;
z-index: 1;
&.single-stat--small {
@ -130,6 +134,7 @@
}
/*
Legend Styles
------------------------------------------------------------------------------