From 2a450476b18d707022f0380bce6f4451c9ff9e1a Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 17 Nov 2020 11:51:58 +0100 Subject: [PATCH 01/15] feat: gauge mini --- giraffe/src/components/GaugeMini.tsx | 545 ++++++++++++++++++++++ giraffe/src/components/GaugeMiniLayer.tsx | 42 ++ giraffe/src/components/Plot.tsx | 3 +- giraffe/src/components/SizedTable.tsx | 17 + giraffe/src/constants/gaugeMiniStyles.ts | 78 ++++ giraffe/src/index.ts | 1 + giraffe/src/types/index.ts | 36 ++ giraffe/src/utils/PlotEnv.ts | 1 + stories/src/gaugeMini.stories.tsx | 116 +++++ 9 files changed, 838 insertions(+), 1 deletion(-) create mode 100644 giraffe/src/components/GaugeMini.tsx create mode 100644 giraffe/src/components/GaugeMiniLayer.tsx create mode 100644 giraffe/src/constants/gaugeMiniStyles.ts create mode 100644 stories/src/gaugeMini.stories.tsx diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx new file mode 100644 index 00000000..df90d6cb --- /dev/null +++ b/giraffe/src/components/GaugeMini.tsx @@ -0,0 +1,545 @@ +// Libraries +import React, {FunctionComponent, useRef, useEffect, useState} from 'react' +import {color as d3Color} from 'd3-color' +import {scaleLinear} from 'd3-scale' +import {range} from 'd3-array' +import {Color, GaugeMiniLayerConfig} from '../types' + +// todo: remove before minigauge release +export const t = (x: number, y: number) => ({ + transform: `translate(${x},${y})`, +}) + +const throwReturn = (msg: string): T => { + throw new Error(msg) +} + +interface IProps { + width: number + height: number + value: number | {_field: string; value: number}[] + theme: Required +} + +//#region colors + +export type Colors = { + min: Color + max: Color + secondary: string + thresholds: Color[] + // targets: Color[], +} + +export const getColors = (theme: Required): Colors => { + const {colorSecondary: secondary, gaugeColors: colorsAndTargets} = theme + + colorsAndTargets.forEach( + ({hex, name}) => + d3Color(hex) ?? + throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) + ) + + const min: Color = + colorsAndTargets.find(x => x.type === 'min') ?? + throwReturn('color of type min must be defined') + const max: Color = + colorsAndTargets.find(x => x.type === 'max') ?? + throwReturn('color of type max must be defined') + + const thresholds = colorsAndTargets + .filter(({type}) => type === 'threshold') + .sort(({value: a}, {value: b}) => a - b) + // const targets = colorsAndTargets.filter(({ type }) => type === "target").sort(({ value: a }, { value: b }) => a - b); + + return {max, min, secondary, /* targets, */ thresholds} +} + +//#endregion colors + +//#region svg helpers + +type TSvgTextRectProps = { + onRectChanged?: (rect: DOMRect) => void +} & React.SVGProps + +/** + * Helper component that returns rect when children changes. Usefull for calculating text box size. + */ +export const SvgTextRect: React.FC = props => { + const {onRectChanged = () => {}} = props + + const textRef = useRef(null) + + useEffect(() => { + const rect = textRef.current?.getBBox() + if (!rect) return + + onRectChanged(rect) + }, [props.children]) + + return ( + <> + + + ) +} + +const AutoCenterGroup: FunctionComponent<{ + enabled?: boolean + refreshToken?: number | string +} & React.SVGProps> = props => { + const {children, enabled = true, refreshToken = 0} = props + const ref = useRef(null) + + const [x, setX] = useState(0) + const [y, setY] = useState(0) + + useEffect(() => { + if (!enabled) { + setX(0) + setY(0) + return + } + + const g = ref.current + // we use this function because we want to know how much we are in negative direction + const box = g?.getBBox() + // we use this function because we want to have fixed parent width/height + const boxParent = ((g?.parentElement as any) as + | SVGGraphicsElement + | undefined)?.getBoundingClientRect() + + if (!box || !boxParent) return + + setX((boxParent.width - box.width) / 2 - box.x) + setY((boxParent.height - box.height) / 2 - box.y) + }, [refreshToken]) + + return ( + + {children} + + ) +} + +//#endregion svg helpers + +const barCssClass = 'gauge-mini-bar' + +const BarBackground: FunctionComponent<{ + theme: Required + colors: Colors + barWidth: number + getFrac: (x: number) => number + barCenter: number +}> = ({ + theme, + colors: {max, min, secondary, thresholds}, + barWidth, + getFrac, + barCenter, +}) => { + const {gaugeHeight, mode, gaugeRounding} = theme + + const colors: {start: number; end: number; col: string}[] = [] + if (mode === 'progress') { + colors.push({start: 0, end: 1, col: secondary}) + } else { + const all = [min, ...thresholds, max] + let start = 0 + for (let i = 0; i + 1 < all.length; i++) { + const {hex: col} = all[i] + const {value} = all[i + 1] + + const end = getFrac(value) + + colors.push({start, end, col}) + start = end + } + } + + const y = barCenter - gaugeHeight / 2 + + // todo: invalid HTML -> multiple same ID attribute possible + // todo: move to svg root + const roundingDefId = `rounded-bar-${barWidth}-${gaugeHeight}` + const gradientDefId = `gradient-${min.hex}-${max.hex}` + + return ( + <> + + + + + + + + + + {thresholds.length === 0 && mode === 'bullet' ? ( + + ) : ( + colors.map(({col, end, start}) => ( + + )) + )} + + ) +} + +const BarValue: FunctionComponent<{ + theme: Required + barValueWidth: number + colors: Colors + value: number + valueFracFixed: number + barCenter: number +}> = ({colors, barValueWidth, value, theme, valueFracFixed, barCenter}) => { + const {valueHeight, gaugeHeight, mode, valueRounding} = theme + const colorModeGradient = colors.thresholds.length === 0 + + const colorValue = + mode === 'bullet' + ? colors.secondary + : d3Color( + (() => { + if (colorModeGradient) { + return scaleLinear() + .range([colors.min.hex, colors.max.hex] as any) + .domain([colors.min.value, colors.max.value])(value) as any + } else { + const sortedColors = [ + colors.min, + ...colors.thresholds, + colors.max, + ] + let i = 0 + while ( + i < sortedColors.length && + value >= sortedColors[i].value + ) { + i++ + } + return sortedColors[ + Math.max(Math.min(i - 1, sortedColors.length - 1), 0) + ].hex + } + })() + ) + ?.brighter(1) + .hex() + + const y = barCenter - valueHeight / 2 + const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 + + const className = 'value-rect' + + // todo: move styling out -> styling is now multiple times inserted + return ( + <> + + + + ) +} + +const Bar: FunctionComponent<{ + value: number + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +}> = ({value, theme, y, barWidth, getFrac}) => { + const {gaugeHeight, valueHeight} = theme + + const colors = getColors(theme) + + const oveflowFrac = 0.03 + // fixes fraction into -oveflowFrac <-> 1+oveflowFrac + const getFixedFrac = (val: number) => + Math.max(-oveflowFrac, Math.min(oveflowFrac + 1, getFrac(val))) + const valueFracFixed = getFixedFrac(value) + + const barY = y + const barValueWidth = barWidth * valueFracFixed + const maxBarHeight = Math.max(gaugeHeight, valueHeight) + const barCenter = maxBarHeight / 2 + + return ( + + + + + + + + + + + ) +} + +const Text: FunctionComponent<{ + theme: Required + barValueWidth: number + colors: Colors + value: number +}> = ({value, barValueWidth, theme}) => { + const { + valueFontColorInside, + valueFontColorOutside, + textMode, + valueFormater, + valueFontSize, + } = theme + const textValue = valueFormater(value) + + const [textBBox, setTextBBox] = useState(null) + const padding = 5 + + const textInside = + (textBBox?.width ? textBBox?.width + padding * 2 : 0) < barValueWidth + + const textAnchor = textInside && textMode === 'follow' ? 'end' : 'start' + + const textColor = textInside ? valueFontColorInside : valueFontColorOutside + + const x = + textMode === 'follow' + ? Math.max(barValueWidth + (textInside ? -padding : padding), padding) + : padding + + return ( + <> + + {textValue} + + + ) +} + +const Axes: FunctionComponent<{ + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +}> = ({theme, barWidth, y, getFrac}) => { + const {axesSteps, axesFormater, axesFontColor, axesFontSize} = theme + + if (axesSteps === undefined || axesSteps === null) return <> + + const colors = getColors(theme) + + const colorLen = colors.max.value - colors.min.value + + const axesLineStyle = {stroke: axesFontColor, strokeWidth: 2} + + const axesValuesArray = Array.isArray(axesSteps) + ? axesSteps + : axesSteps === 'thresholds' + ? colors.thresholds.map(x => x.value) + : Number.isInteger(axesSteps) + ? range(axesSteps).map( + x => ((x + 1) * colorLen) / (axesSteps + 1) + colors.min.value + ) + : throwReturn( + `${JSON.stringify( + axesSteps + )} axesSteps must be number | "thresholds" | number[] | undefined.` + ) + + const points: { + anchor: string + value: number + lineLength: number + }[] = axesValuesArray + .map(value => ({ + anchor: 'middle', + value, + lineLength: 5, + })) + .concat([ + { + anchor: 'start', + lineLength: 3, + value: colors.min.value, + }, + { + anchor: 'end', + lineLength: 3, + value: colors.max.value, + }, + ]) + + return ( + <> + + + {points.map(({anchor, lineLength, value}, i) => { + const posX = getFrac(value) * barWidth + const text = axesFormater(value) + return ( + <> + + + + {text} + + + + ) + })} + + + ) +} + +export const GaugeMini: FunctionComponent = ({ + value, + theme, + width, + height, +}) => { + const { + gaugeHeight, + sidePaddings: gaugePaddingSides, + valueHeight, + barPaddings, + labelMain, + labelBars, + labelMainFontSize, + labelMainFontColor, + labelBarsFontColor, + labelBarsFontSize, + } = theme + const [barLabelsWidth] = useState([]) + + const valueArray = Array.isArray(value) + ? value + : [{_field: '_default', value}] + + const colors = getColors(theme) + const colorLen = colors.max.value - colors.min.value + const centerY = height / 2 + + const barLabelWidth = Math.max(...barLabelsWidth) || 0 + + const barWidth = width - gaugePaddingSides * 2 - barLabelWidth + + const maxBarHeight = Math.max(gaugeHeight, valueHeight) + + const allBarsHeight = valueArray.length * (maxBarHeight + barPaddings) + + const [autocenterToken, setAutocenterToken] = useState(0) + useEffect(() => { + setAutocenterToken(autocenterToken + 1) + }, [barLabelsWidth, gaugePaddingSides, valueHeight, width, height]) + + /** return value as fraction 0->min 1->max */ + const getFrac = (val: number): number => (val - colors.min.value) / colorLen + + return ( + + + {labelMain && ( + + {labelMain} + + )} + {valueArray.map(({_field, value}, i) => { + const y = 0 + i * (maxBarHeight + barPaddings) + const label = labelBars?.find(({_field: f}) => f === _field)?.label + + const textCenter = y + maxBarHeight / 2 + + // todo: no rerender ? + const onRectChanged = (r: DOMRect) => { + barLabelsWidth[i] = r.width + } + + return ( + <> + + + {label} + + + ) + })} + + + + ) +} diff --git a/giraffe/src/components/GaugeMiniLayer.tsx b/giraffe/src/components/GaugeMiniLayer.tsx new file mode 100644 index 00000000..a432fbbe --- /dev/null +++ b/giraffe/src/components/GaugeMiniLayer.tsx @@ -0,0 +1,42 @@ +// Libraries +import React, {FunctionComponent} from 'react' +import AutoSizer from 'react-virtualized-auto-sizer' + +// Types +import {GaugeMiniLayerConfig} from '../types' +import {GaugeMini} from './GaugeMini' + +import {GAUGE_MINI_THEME_BULLET_DARK} from '../constants/gaugeMiniStyles' + +interface Props { + value: number | string + theme: GaugeMiniLayerConfig +} + +export const GaugeMiniLayer: FunctionComponent = (props: Props) => { + const {theme, value} = props + const themeOrDefault: Required = { + ...GAUGE_MINI_THEME_BULLET_DARK, + ...theme, + } + + const valueNumber = typeof value === 'string' ? parseFloat(value) : value + + return ( + + {({width, height}) => ( +
+ +
+ )} +
+ ) +} diff --git a/giraffe/src/components/Plot.tsx b/giraffe/src/components/Plot.tsx index 51b5e5c2..747a65f9 100644 --- a/giraffe/src/components/Plot.tsx +++ b/giraffe/src/components/Plot.tsx @@ -43,7 +43,8 @@ export const Plot: FunctionComponent = ({ if ( graphType === LayerTypes.Table || graphType === LayerTypes.RawFluxDataTable || - graphType === LayerTypes.Gauge + graphType === LayerTypes.Gauge || + graphType === LayerTypes.GaugeMini ) { return ( diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index 6b1f9dd6..de17b83a 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -2,12 +2,14 @@ import React, {FunctionComponent, CSSProperties} from 'react' import { GaugeLayerConfig, + GaugeMiniLayerConfig, SizedConfig, TableGraphLayerConfig, LayerTypes, } from '../types' import {GaugeLayer} from './GaugeLayer' +import {GaugeMiniLayer} from './GaugeMiniLayer' import {LatestValueTransform} from './LatestValueTransform' import {newTableFromConfig} from '../utils/newTable' import {RawFluxDataTable} from './RawFluxDataTable' @@ -79,6 +81,21 @@ export const SizedTable: FunctionComponent = ({ )} ) + case LayerTypes.GaugeMini: + return ( + + {latestValue => ( + } + /> + )} + + ) case LayerTypes.RawFluxDataTable: return ( = { + type: 'gauge mini', + mode: 'bullet', + textMode: 'follow', + + valueHeight: 18, + gaugeHeight: 25, + valueRounding: 2, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, + {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBars: [], + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valueFontSize: 12, + valueFontColorOutside: InfluxColors.Raven, + valueFontColorInside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: 'thresholds', + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), +} + +export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { + type: 'gauge mini', + mode: 'progress', + textMode: 'follow', + + valueHeight: 20, + gaugeHeight: 20, + valueRounding: 3, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBars: [], + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valueFontSize: 18, + valueFontColorInside: InfluxColors.Raven, + valueFontColorOutside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: undefined as any, + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), +} diff --git a/giraffe/src/index.ts b/giraffe/src/index.ts index fd2f378f..3d6d9a99 100644 --- a/giraffe/src/index.ts +++ b/giraffe/src/index.ts @@ -36,6 +36,7 @@ export { Formatter, GaugeLayerConfig, GaugeTheme, + GaugeMiniLayerConfig, GetColumn, HistogramLayerConfig, HistogramPosition, diff --git a/giraffe/src/types/index.ts b/giraffe/src/types/index.ts index 7403cbe6..1d80a8a4 100644 --- a/giraffe/src/types/index.ts +++ b/giraffe/src/types/index.ts @@ -175,6 +175,7 @@ export type FluxDataType = export enum LayerTypes { RawFluxDataTable = 'flux data table', Gauge = 'gauge', + GaugeMini = 'gauge mini', Custom = 'custom', Annotation = 'annotation', SingleStat = 'single stat', @@ -193,6 +194,7 @@ export type LayerConfig = | AnnotationLayerConfig | RawFluxDataTableLayerConfig | GaugeLayerConfig + | GaugeMiniLayerConfig | SingleStatLayerConfig | HeatmapLayerConfig | HistogramLayerConfig @@ -276,6 +278,40 @@ export interface GaugeTheme { overflowDelta: number } +export interface GaugeMiniLayerConfig { + type: 'gauge mini' + mode?: 'progress' | 'bullet' + textMode?: 'follow' | 'left' + + valueHeight?: number + gaugeHeight?: number + valueRounding?: number + gaugeRounding?: number + barPaddings?: number + sidePaddings?: number + + gaugeColors?: Color[] + colorSecondary?: string + + labelMain?: string + labelMainFontSize?: number + labelMainFontColor?: string + + labelBars?: {_field: string; label: string}[] + labelBarsFontSize?: number + labelBarsFontColor?: string + + valueFontSize?: number + valueFontColorInside?: string + valueFontColorOutside?: string + valueFormater?: (value: number) => string + + axesSteps?: number | 'thresholds' | undefined | number[] + axesFontSize?: number + axesFontColor?: string + axesFormater: (value: number) => string +} + export interface SingleStatLayerConfig { type: 'single stat' // do not refactor or restrict to LayerTypes.SingleStat prefix: string diff --git a/giraffe/src/utils/PlotEnv.ts b/giraffe/src/utils/PlotEnv.ts index a04bf549..30e9aaff 100644 --- a/giraffe/src/utils/PlotEnv.ts +++ b/giraffe/src/utils/PlotEnv.ts @@ -375,6 +375,7 @@ export class PlotEnv { case LayerTypes.RawFluxDataTable: case LayerTypes.Gauge: + case LayerTypes.GaugeMini: case LayerTypes.Geo: case LayerTypes.Custom: case LayerTypes.SingleStat: diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx new file mode 100644 index 00000000..20b65d61 --- /dev/null +++ b/stories/src/gaugeMini.stories.tsx @@ -0,0 +1,116 @@ +import * as React from 'react' +import {storiesOf} from '@storybook/react' +import {withKnobs, number, select, text} from '@storybook/addon-knobs' +import { + Config, + GaugeMiniLayerConfig, + InfluxColors, + Plot, +} from '../../giraffe/src' + +import {PlotContainer} from './helpers' +import {gaugeTable} from './data/gaugeLayer' +import { + GAUGE_MINI_THEME_BULLET_DARK, + GAUGE_MINI_THEME_PROGRESS_DARK, +} from '../../giraffe/src/constants/gaugeMiniStyles' + +type Theme = Required + +const color = (() => { + const colors = (() => { + const obj = {} + Object.keys(InfluxColors).forEach(x => (obj[x] = InfluxColors[x])) + return obj + })() + + // todo: type definitions + return (label: string, ...rest: any[]) => select(label, colors, ...rest) +})() + +const editableLayer = (theme: Theme): Theme => ({ + type: theme.type, + mode: select( + 'Mode', + { + bullet: 'bullet', + progress: 'progress', + }, + theme.mode + ), + textMode: select( + 'textMode', + { + follow: 'follow', + left: 'left', + }, + theme.textMode + ), + + valueHeight: number('valueHeight', theme.valueHeight), + gaugeHeight: number('gaugeHeight', theme.gaugeHeight), + valueRounding: number('valueRounding', theme.valueRounding), + gaugeRounding: number('gaugeRounding', theme.gaugeRounding), + barPaddings: number('barPaddings', theme.barPaddings), + sidePaddings: number('gaugePaddingSides', theme.sidePaddings), + + // todo: add knobs + gaugeColors: theme.gaugeColors, + colorSecondary: color('colorSecondary', theme.colorSecondary), + + labelMain: text('labelMain', 'Gauge-mini example'), + labelMainFontSize: number('labelMainFontSize', theme.labelMainFontSize), + labelMainFontColor: color('labelMainFontColor', theme.labelMainFontColor), + + // todo: add knobs + labelBars: theme.labelBars, + labelBarsFontSize: number('labelBarsFontSize', theme.labelBarsFontSize), + labelBarsFontColor: color('labelBarsFontColor', theme.labelBarsFontColor), + + valueFontSize: number('valueFontSize', theme.valueFontSize), + valueFontColorInside: color( + 'valueFontColorInside', + theme.valueFontColorInside + ), + valueFontColorOutside: color( + 'valueFontColorOutside', + theme.valueFontColorOutside + ), + // todo: add knobs + valueFormater: theme.valueFormater, + + axesSteps: select( + 'axesSteps', + { + thresholds: 'thresholds', + 0: 0, + 1: 1, + 2: 2, + undefined: null, + }, + theme.axesSteps + ), + axesFontSize: number('axesFontSize', theme.axesFontSize), + axesFontColor: color('axesFontColor', theme.axesFontColor), + // todo: add knobs + axesFormater: theme.axesFormater, +}) + +const createStory = (theme: Theme) => () => { + const config: Config = { + table: gaugeTable(0, 100), + layers: [editableLayer(theme)], + } + return ( + <> + + + + + ) +} + +storiesOf('Gauge mini', module) + .addDecorator(withKnobs) + .add('Bullet', createStory(GAUGE_MINI_THEME_BULLET_DARK)) + .add('Progress', createStory(GAUGE_MINI_THEME_PROGRESS_DARK)) From a79e231ba583042ad5268e6b824c9cb88fe45961 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 24 Nov 2020 11:40:07 +0100 Subject: [PATCH 02/15] docs: gauge mini docs created --- giraffe/README.md | 172 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/giraffe/README.md b/giraffe/README.md index 81c1df1b..9b530bed 100644 --- a/giraffe/README.md +++ b/giraffe/README.md @@ -698,6 +698,178 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **overflowDelta**: _number. Optional. Defaults to 0.03 when excluded._ This constant expresses how far past the gauge min or gauge max the needle should be drawn if the value for the needle is less than gauge min or greater than the gauge max. It is expressed as a fraction of the circumference of a circle, e.g. 0.5 means draw halfway around the gauge from the min or max value. + +- **GaugeLayerConfig**: _Object._ Maximum one per ``. Properties are: + + All values (excluding **type**) are Optional and their defaults is defined in precreated theme `GAUGE_MINI_THEME_BULLET_DARK` + + - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. + + - **mode**: _'progress' | 'bullet'._ + - `'bullet'` backgroud bar is colored and value bar has always secondary color + - `'progress'` value bar is colored and backgroud bar has always secondary color + + - **textMode** _'follow' | 'left'_ + - `'left'` text value will stay on _left_ side of bar + - `'follow'` text value will _follow_ width of value bar + + - **valueHeight** _number_ height of value bar + + - **gaugeHeight** _number_ hegiht of backgroud bar + + - **valueRounding** _number_ rounding of value bar corners + + - **gaugeRounding** _number_ rounding of backgroud bar corners + + - **barPaddings** _number_ vertical distance between bars, axes and label + + - **sidePaddings** _number_ empty space on left/right of the bar + + - **oveflowFraction** _number_ fraction number defining how much can value bar go outside of background bar. e.g. with `oveflowFraction: .1` will value bar have max length 110% of background bar for max value and will have 10% length to the left from background bar for minimal value. + + - **gaugeColors** _Color[]_ An array of objects that defines the colors of the Gauge. Each object has the following properties. + + - **id**: _string. **Required**._ The id for this color. Should be unique within the **gaugeColors** array. + + - **type**: _'min' | 'max' | 'threshold'. **Required**._ The type of value associated with this color. _'min'_ type comes first, _'max'_ type comes last, and _'threshold'_ types are in between _'min'_ and _'max'_. **gaugeColors** must contain at least one _'min'_ type and one _'max'_ type for the Gauge-mini to have color. Only the first _'min'_ and first _'max'_ in the array are recognized for each of their respective types. The color will change as a gradient if the **gaugeColors** array contains no 'threshold' that means gradient background bar for 'bullet' mode and continuous value bar color change for 'progress' mode. The color will be segmented if any _'threshold'_ types are included in the **gaugeColors** array that means step color change background bar for 'bullet' mode and fixed color of value bar based on value change for 'progress' mode. + + - **hex**: _string. **Required**._ The [_color hex_](https://www.color-hex.com/) string for this color. + + - **name**: _string. **Required**._ For descriptive purposes only. The name given to this color. + + - **value**: _number. **Required**._ The starting gauge value associated with this color. + + + + + - **colorSecondary** _string_ Secondary color used for value bar in 'bullet' mode or for background bar in 'progress' mode + + Main label + - **labelMain** _string_ Main label text. + + - **labelMainFontSize** _number_ Main label font size. + + - **labelMainFontColor** _string_ Main label color. + + Bar labels + - **labelBars** _{\_field: string; label: string}[]_ Labels for individual fields/bars. + + - **labelBarsFontSize** _number_ Bar labels font size + + - **labelBarsFontColor** _string_ Bar labels font color + + Text value + - **valuePadding** _number_ Padding on sides of text (distance from value bar start/end) + + - **valueFontSize** _number_ Text value font size + + - **valueFontColorInside** _string_ Text value color when value bar is behind the text + + - **valueFontColorOutside** _string_ Text value color when value bar is not behind the text + + - **valueFormater** _(value: number) => string_ Function that defines how will be text value shown based on current value. e.g. ```valueFormater: (num: number) => `${num.toFixed(0)}%` ``` for _value=23.213_ will show text value _23%_. + + Axes + - **axesSteps** _number | 'thresholds' | undefined | number[]_ Defines where to show axes: + - _number_ number of how many evenly distributed axes values will be shown between min and max value. Only min and max value will be shown when `axesSteps: 0` + - _'thresholds'_ axes values will be shown at threshold values. + - _undefined_ axes will not show. + - _number[]_ shows axes at given fixed values + + - **axesFontSize** _number_ Axes values font size + + - **axesFontColor** _string_ Color of axes values and axes lines + + - **axesFormater** _(value: number) => string_ Same as **valueFormater** for axes values + + **Precreated themes** + - `GAUGE_MINI_THEME_BULLET_DARK` + +``` +{ + type: 'gauge mini', + mode: 'bullet', + textMode: 'follow', + + valueHeight: 18, + gaugeHeight: 25, + valueRounding: 2, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, + {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBars: [], + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 12, + valueFontColorOutside: InfluxColors.Raven, + valueFontColorInside: InfluxColors.Cloud, + valueFormater: (num: number) => num.toFixed(0), + + axesSteps: 'thresholds', + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (num: number) => num.toFixed(0), +} +``` + - `GAUGE_MINI_THEME_PROGRESS_DARK` +``` +{ + type: 'gauge mini', + mode: 'progress', + textMode: 'follow', + + valueHeight: 20, + gaugeHeight: 20, + valueRounding: 3, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBars: [], + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 18, + valueFontColorInside: InfluxColors.Raven, + valueFontColorOutside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: undefined as any, + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), +} +``` + + - **SingleStatLayerConfig**: _Object._ No limit but generally one per ``. Using more than one requires additional styling through configuration and is not recommended.
A Single Stat layer is a pre-defined custom layer that displays a single value on top of any other plot type, or by itself, but usually displayed on top of (single) line graphs. The displayed value is the latest value by timestamp. If more than one value has the latest timestamp, then the first value in the [table](#data-properties) with the latest timestamp will be displayed. Currently, there is no guarantee which value will be considered the first value when there are multiple values with the same timestamp. From c0fb5f41e8b491ee0dd510a034aaf059b007c979 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Fri, 27 Nov 2020 11:06:18 +0100 Subject: [PATCH 03/15] feat: mini gauge integration --- chronograf-ui | 1 + giraffe/README.md | 184 ++++---- giraffe/src/components/GaugeMini.tsx | 412 ++++++++++-------- giraffe/src/components/GaugeMiniLayer.tsx | 13 +- .../LatestMultipleValueTransform.tsx | 46 ++ giraffe/src/components/SizedTable.tsx | 9 +- giraffe/src/constants/gaugeMiniStyles.ts | 8 +- giraffe/src/types/index.ts | 6 +- stories/src/data/gaugeMiniLayer.ts | 37 ++ stories/src/gaugeMini.stories.tsx | 17 +- 10 files changed, 437 insertions(+), 296 deletions(-) create mode 160000 chronograf-ui create mode 100644 giraffe/src/components/LatestMultipleValueTransform.tsx create mode 100644 stories/src/data/gaugeMiniLayer.ts diff --git a/chronograf-ui b/chronograf-ui new file mode 160000 index 00000000..e1de5936 --- /dev/null +++ b/chronograf-ui @@ -0,0 +1 @@ +Subproject commit e1de5936e5ba9f1c347b5f8d5c59e6d31d41e617 diff --git a/giraffe/README.md b/giraffe/README.md index 9b530bed..acff5413 100644 --- a/giraffe/README.md +++ b/giraffe/README.md @@ -701,10 +701,21 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **GaugeLayerConfig**: _Object._ Maximum one per ``. Properties are: - All values (excluding **type**) are Optional and their defaults is defined in precreated theme `GAUGE_MINI_THEME_BULLET_DARK` + For multiple bars values has to be passed as columns (not fields). That can be done in flux by + ``` + import "influxdata/influxdb/v1" + + |> v1.fieldsAsCols() + ``` - - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. + All values (excluding **type**) are Optional and their defaults is defined by precreated theme `GAUGE_MINI_THEME_BULLET_DARK` + - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. + + - **bars** _{\_field: string; label?: string}[]_ per bar settings: + - __field_ giraffe table column name (that contains value), can be set to `'_value'` when there is only one field present + - _label_ optional label for bar + - **mode**: _'progress' | 'bullet'._ - `'bullet'` backgroud bar is colored and value bar has always secondary color - `'progress'` value bar is colored and backgroud bar has always secondary color @@ -752,8 +763,6 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **labelMainFontColor** _string_ Main label color. Bar labels - - **labelBars** _{\_field: string; label: string}[]_ Labels for individual fields/bars. - - **labelBarsFontSize** _number_ Bar labels font size - **labelBarsFontColor** _string_ Bar labels font color @@ -784,90 +793,89 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data **Precreated themes** - `GAUGE_MINI_THEME_BULLET_DARK` - -``` -{ - type: 'gauge mini', - mode: 'bullet', - textMode: 'follow', - - valueHeight: 18, - gaugeHeight: 25, - valueRounding: 2, - gaugeRounding: 3, - barPaddings: 5, - sidePaddings: 20, - oveflowFraction: 0.03, - - gaugeColors: [ - {value: 0, type: 'min', hex: InfluxColors.Krypton}, - {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, - {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, - {value: 100, type: 'max', hex: InfluxColors.Topaz}, - ] as Color[], - colorSecondary: InfluxColors.Kevlar, - - labelMain: '', - labelMainFontSize: 13, - labelMainFontColor: InfluxColors.Ghost, - - labelBars: [], - labelBarsFontSize: 11, - labelBarsFontColor: InfluxColors.Forge, - - valuePadding: 5, - valueFontSize: 12, - valueFontColorOutside: InfluxColors.Raven, - valueFontColorInside: InfluxColors.Cloud, - valueFormater: (num: number) => num.toFixed(0), - - axesSteps: 'thresholds', - axesFontSize: 11, - axesFontColor: InfluxColors.Forge, - axesFormater: (num: number) => num.toFixed(0), -} -``` - - `GAUGE_MINI_THEME_PROGRESS_DARK` -``` -{ - type: 'gauge mini', - mode: 'progress', - textMode: 'follow', - - valueHeight: 20, - gaugeHeight: 20, - valueRounding: 3, - gaugeRounding: 3, - barPaddings: 5, - sidePaddings: 20, - oveflowFraction: 0.03, - - gaugeColors: [ - {value: 0, type: 'min', hex: InfluxColors.Krypton}, - {value: 100, type: 'max', hex: InfluxColors.Topaz}, - ] as Color[], - colorSecondary: InfluxColors.Kevlar, - - labelMain: '', - labelMainFontSize: 13, - labelMainFontColor: InfluxColors.Ghost, - - labelBars: [], - labelBarsFontSize: 11, - labelBarsFontColor: InfluxColors.Forge, - - valuePadding: 5, - valueFontSize: 18, - valueFontColorInside: InfluxColors.Raven, - valueFontColorOutside: InfluxColors.Cloud, - valueFormater: (val: number) => val.toFixed(0), - - axesSteps: undefined as any, - axesFontSize: 11, - axesFontColor: InfluxColors.Forge, - axesFormater: (val: number) => val.toFixed(0), -} -``` + ``` + { + type: 'gauge mini', + mode: 'bullet', + textMode: 'follow', + bars: [], + + valueHeight: 18, + gaugeHeight: 25, + valueRounding: 2, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, + {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 12, + valueFontColorOutside: InfluxColors.Raven, + valueFontColorInside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: 'thresholds', + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), + } + ``` + - `GAUGE_MINI_THEME_PROGRESS_DARK` + ``` + { + type: 'gauge mini', + mode: 'progress', + textMode: 'follow', + bars: [], + + valueHeight: 20, + gaugeHeight: 20, + valueRounding: 3, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 18, + valueFontColorInside: InfluxColors.Raven, + valueFontColorOutside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: undefined as any, + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), + } + ``` - **SingleStatLayerConfig**: _Object._ No limit but generally one per ``. Using more than one requires additional styling through configuration and is not recommended. diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index df90d6cb..58198eab 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -5,22 +5,19 @@ import {scaleLinear} from 'd3-scale' import {range} from 'd3-array' import {Color, GaugeMiniLayerConfig} from '../types' -// todo: remove before minigauge release -export const t = (x: number, y: number) => ({ - transform: `translate(${x},${y})`, -}) - const throwReturn = (msg: string): T => { throw new Error(msg) } -interface IProps { +interface Props { width: number height: number value: number | {_field: string; value: number}[] theme: Required } +const barCssClass = 'gauge-mini-bar' + //#region colors export type Colors = { @@ -28,31 +25,29 @@ export type Colors = { max: Color secondary: string thresholds: Color[] - // targets: Color[], } export const getColors = (theme: Required): Colors => { - const {colorSecondary: secondary, gaugeColors: colorsAndTargets} = theme + const {colorSecondary: secondary, gaugeColors} = theme - colorsAndTargets.forEach( + gaugeColors.forEach( ({hex, name}) => d3Color(hex) ?? throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) ) - const min: Color = - colorsAndTargets.find(x => x.type === 'min') ?? - throwReturn('color of type min must be defined') - const max: Color = - colorsAndTargets.find(x => x.type === 'max') ?? - throwReturn('color of type max must be defined') - - const thresholds = colorsAndTargets - .filter(({type}) => type === 'threshold') - .sort(({value: a}, {value: b}) => a - b) - // const targets = colorsAndTargets.filter(({ type }) => type === "target").sort(({ value: a }, { value: b }) => a - b); - - return {max, min, secondary, /* targets, */ thresholds} + return { + min: + gaugeColors.find(x => x.type === 'min') ?? + throwReturn('color of type min must be defined'), + max: + gaugeColors.find(x => x.type === 'max') ?? + throwReturn('color of type max must be defined'), + thresholds: gaugeColors + .filter(({type}) => type === 'threshold') + .sort(({value: a}, {value: b}) => a - b), + secondary, + } } //#endregion colors @@ -65,6 +60,7 @@ type TSvgTextRectProps = { /** * Helper component that returns rect when children changes. Usefull for calculating text box size. + * !onRectChanged called only when children changes! */ export const SvgTextRect: React.FC = props => { const {onRectChanged = () => {}} = props @@ -73,10 +69,12 @@ export const SvgTextRect: React.FC = props => { useEffect(() => { const rect = textRef.current?.getBBox() - if (!rect) return + if (!rect) { + return + } onRectChanged(rect) - }, [props.children]) + }, [props.children, onRectChanged]) return ( <> @@ -85,6 +83,10 @@ export const SvgTextRect: React.FC = props => { ) } +/** + * Helper component for centering content. + * !Doesn't react on content size changed. Recententering is done manualy by changing refreshToken! + */ const AutoCenterGroup: FunctionComponent<{ enabled?: boolean refreshToken?: number | string @@ -110,14 +112,16 @@ const AutoCenterGroup: FunctionComponent<{ | SVGGraphicsElement | undefined)?.getBoundingClientRect() - if (!box || !boxParent) return + if (!box || !boxParent) { + return + } setX((boxParent.width - box.width) / 2 - box.x) setY((boxParent.height - box.height) / 2 - box.y) - }, [refreshToken]) + }, [refreshToken, enabled]) return ( - + {children} ) @@ -125,15 +129,52 @@ const AutoCenterGroup: FunctionComponent<{ //#endregion svg helpers -const barCssClass = 'gauge-mini-bar' +//#region subcomponents + +//#region types -const BarBackground: FunctionComponent<{ +type BarBackgroundProps = { theme: Required colors: Colors barWidth: number getFrac: (x: number) => number barCenter: number -}> = ({ +} + +type BarValueProps = { + theme: Required + barValueWidth: number + colors: Colors + value: number + valueFracFixed: number + barCenter: number +} + +type TextProps = { + theme: Required + barValueWidth: number + colors: Colors + value: number +} + +type BarProps = { + value: number + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +} + +type AxesProps = { + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +} + +//#endregion types + +const BarBackground: FunctionComponent = ({ theme, colors: {max, min, secondary, thresholds}, barWidth, @@ -142,30 +183,32 @@ const BarBackground: FunctionComponent<{ }) => { const {gaugeHeight, mode, gaugeRounding} = theme - const colors: {start: number; end: number; col: string}[] = [] - if (mode === 'progress') { - colors.push({start: 0, end: 1, col: secondary}) - } else { - const all = [min, ...thresholds, max] - let start = 0 - for (let i = 0; i + 1 < all.length; i++) { - const {hex: col} = all[i] - const {value} = all[i + 1] - - const end = getFrac(value) - - colors.push({start, end, col}) - start = end - } - } - const y = barCenter - gaugeHeight / 2 - // todo: invalid HTML -> multiple same ID attribute possible - // todo: move to svg root const roundingDefId = `rounded-bar-${barWidth}-${gaugeHeight}` const gradientDefId = `gradient-${min.hex}-${max.hex}` + type Segment = {start: number; end: number; hex: string} + const segments: Segment[] = [] + if (mode === 'bullet') { + // thresholds are already sorted by getColors + const allColors = [min, ...thresholds, max] + + for ( + let i = 0, start = 0, end = 0; + i + 1 < allColors.length; + i++, start = end + ) { + const {hex} = allColors[i] + const next = allColors[i + 1].value + + end = getFrac(next) + segments.push({start, end, hex}) + } + } else { + segments.push({start: 0, end: 1, hex: secondary}) + } + return ( <> @@ -191,32 +234,40 @@ const BarBackground: FunctionComponent<{ y={y} /> ) : ( - colors.map(({col, end, start}) => ( - - )) + segments + .reverse() + .map(({hex: col, end, start}, i) => ( + + )) )} ) } -const BarValue: FunctionComponent<{ - theme: Required - barValueWidth: number - colors: Colors - value: number - valueFracFixed: number - barCenter: number -}> = ({colors, barValueWidth, value, theme, valueFracFixed, barCenter}) => { - const {valueHeight, gaugeHeight, mode, valueRounding} = theme +const BarValue: FunctionComponent = ({ + colors, + barValueWidth, + value, + theme, + valueFracFixed, + barCenter, +}) => { + const {valueHeight, mode, valueRounding} = theme const colorModeGradient = colors.thresholds.length === 0 + const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 + const y = barCenter - valueHeight / 2 + + const className = 'value-rect' + const colorValue = mode === 'bullet' ? colors.secondary @@ -248,11 +299,6 @@ const BarValue: FunctionComponent<{ ?.brighter(1) .hex() - const y = barCenter - valueHeight / 2 - const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 - - const className = 'value-rect' - // todo: move styling out -> styling is now multiple times inserted return ( <> @@ -278,83 +324,41 @@ const BarValue: FunctionComponent<{ ) } -const Bar: FunctionComponent<{ - value: number - theme: Required - barWidth: number - y: number - getFrac: (x: number) => number -}> = ({value, theme, y, barWidth, getFrac}) => { - const {gaugeHeight, valueHeight} = theme - - const colors = getColors(theme) - - const oveflowFrac = 0.03 - // fixes fraction into -oveflowFrac <-> 1+oveflowFrac - const getFixedFrac = (val: number) => - Math.max(-oveflowFrac, Math.min(oveflowFrac + 1, getFrac(val))) - const valueFracFixed = getFixedFrac(value) - - const barY = y - const barValueWidth = barWidth * valueFracFixed - const maxBarHeight = Math.max(gaugeHeight, valueHeight) - const barCenter = maxBarHeight / 2 - - return ( - - - - - - - - - - - ) -} - -const Text: FunctionComponent<{ - theme: Required - barValueWidth: number - colors: Colors - value: number -}> = ({value, barValueWidth, theme}) => { +const Text: FunctionComponent = ({value, barValueWidth, theme}) => { const { valueFontColorInside, valueFontColorOutside, textMode, valueFormater, - valueFontSize, + valueFontSize: fontSize, + valuePadding, } = theme const textValue = valueFormater(value) + const follow = textMode === 'follow' const [textBBox, setTextBBox] = useState(null) - const padding = 5 - const textInside = - (textBBox?.width ? textBBox?.width + padding * 2 : 0) < barValueWidth - - const textAnchor = textInside && textMode === 'follow' ? 'end' : 'start' + const textWidth = textBBox?.width ? textBBox?.width + valuePadding * 2 : 0 + const textInside = textWidth < barValueWidth const textColor = textInside ? valueFontColorInside : valueFontColorOutside - const x = - textMode === 'follow' - ? Math.max(barValueWidth + (textInside ? -padding : padding), padding) - : padding + const textAnchor = textInside && follow ? 'end' : 'start' + + const x = follow + ? Math.max( + barValueWidth + (textInside ? -1 : +1) * valuePadding, + valuePadding + ) + : valuePadding return ( <> {textValue} @@ -362,21 +366,56 @@ const Text: FunctionComponent<{ ) } -const Axes: FunctionComponent<{ - theme: Required - barWidth: number - y: number - getFrac: (x: number) => number -}> = ({theme, barWidth, y, getFrac}) => { +const Bar: FunctionComponent = ({ + value, + theme, + y, + barWidth, + getFrac, +}) => { + const {gaugeHeight, valueHeight, oveflowFraction} = theme + + const colors = getColors(theme) + + const valueFracFixed = Math.max( + -oveflowFraction, + Math.min(oveflowFraction + 1, getFrac(value)) + ) + + const barValueWidth = barWidth * valueFracFixed + const maxBarHeight = Math.max(gaugeHeight, valueHeight) + const barCenter = maxBarHeight / 2 + + return ( + + + + + + + + + + + ) +} + +const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { const {axesSteps, axesFormater, axesFontColor, axesFontSize} = theme - if (axesSteps === undefined || axesSteps === null) return <> + if (axesSteps === undefined || axesSteps === null) { + return <> + } const colors = getColors(theme) - const colorLen = colors.max.value - colors.min.value - - const axesLineStyle = {stroke: axesFontColor, strokeWidth: 2} + const axesLineStyle: React.CSSProperties = { + stroke: axesFontColor, + strokeWidth: 2, + strokeLinecap: 'round', + } const axesValuesArray = Array.isArray(axesSteps) ? axesSteps @@ -396,59 +435,58 @@ const Axes: FunctionComponent<{ anchor: string value: number lineLength: number + text: string + posX: number }[] = axesValuesArray .map(value => ({ - anchor: 'middle', value, + anchor: 'middle', lineLength: 5, })) .concat([ { + value: colors.min.value, anchor: 'start', lineLength: 3, - value: colors.min.value, }, { + value: colors.max.value, anchor: 'end', lineLength: 3, - value: colors.max.value, }, ]) + .map(x => ({ + ...x, + posX: getFrac(x.value) * barWidth, + text: axesFormater(x.value), + })) return ( <> - - - {points.map(({anchor, lineLength, value}, i) => { - const posX = getFrac(value) * barWidth - const text = axesFormater(value) - return ( - <> - - - - {text} - - - - ) - })} + + + {points.map(({posX, lineLength, anchor, text}, i) => ( + + + + {text} + + + ))} ) } -export const GaugeMini: FunctionComponent = ({ +//#endregion subcomponents + +export const GaugeMini: FunctionComponent = ({ value, theme, width, @@ -456,11 +494,11 @@ export const GaugeMini: FunctionComponent = ({ }) => { const { gaugeHeight, - sidePaddings: gaugePaddingSides, + sidePaddings, valueHeight, + bars, barPaddings, labelMain, - labelBars, labelMainFontSize, labelMainFontColor, labelBarsFontColor, @@ -468,37 +506,29 @@ export const GaugeMini: FunctionComponent = ({ } = theme const [barLabelsWidth] = useState([]) - const valueArray = Array.isArray(value) - ? value - : [{_field: '_default', value}] - + const valueArray = Array.isArray(value) ? value : [{_field: '', value}] const colors = getColors(theme) const colorLen = colors.max.value - colors.min.value - const centerY = height / 2 - const barLabelWidth = Math.max(...barLabelsWidth) || 0 - - const barWidth = width - gaugePaddingSides * 2 - barLabelWidth - + const barWidth = width - sidePaddings * 2 - barLabelWidth const maxBarHeight = Math.max(gaugeHeight, valueHeight) - const allBarsHeight = valueArray.length * (maxBarHeight + barPaddings) const [autocenterToken, setAutocenterToken] = useState(0) useEffect(() => { - setAutocenterToken(autocenterToken + 1) - }, [barLabelsWidth, gaugePaddingSides, valueHeight, width, height]) + setAutocenterToken(x => x + 1) + }, [barLabelWidth, sidePaddings, valueHeight, width, height]) /** return value as fraction 0->min 1->max */ const getFrac = (val: number): number => (val - colors.min.value) / colorLen return ( - - + + {labelMain && ( = ({ )} {valueArray.map(({_field, value}, i) => { const y = 0 + i * (maxBarHeight + barPaddings) - const label = labelBars?.find(({_field: f}) => f === _field)?.label + const label = bars?.find(({_field: f}) => f === _field)?.label const textCenter = y + maxBarHeight / 2 diff --git a/giraffe/src/components/GaugeMiniLayer.tsx b/giraffe/src/components/GaugeMiniLayer.tsx index a432fbbe..f1f2e92b 100644 --- a/giraffe/src/components/GaugeMiniLayer.tsx +++ b/giraffe/src/components/GaugeMiniLayer.tsx @@ -9,18 +9,23 @@ import {GaugeMini} from './GaugeMini' import {GAUGE_MINI_THEME_BULLET_DARK} from '../constants/gaugeMiniStyles' interface Props { - value: number | string + values: {[field: string]: number} theme: GaugeMiniLayerConfig } export const GaugeMiniLayer: FunctionComponent = (props: Props) => { - const {theme, value} = props + const {theme, values: valueObj} = props const themeOrDefault: Required = { ...GAUGE_MINI_THEME_BULLET_DARK, ...theme, } - const valueNumber = typeof value === 'string' ? parseFloat(value) : value + const values = Object.keys(valueObj) + .map(key => ({_field: key, value: valueObj[key]})) + .map(({_field, value}) => ({ + _field, + value: typeof value === 'string' ? parseFloat(value) : value, + })) return ( @@ -32,7 +37,7 @@ export const GaugeMiniLayer: FunctionComponent = (props: Props) => { diff --git a/giraffe/src/components/LatestMultipleValueTransform.tsx b/giraffe/src/components/LatestMultipleValueTransform.tsx new file mode 100644 index 00000000..b85d5532 --- /dev/null +++ b/giraffe/src/components/LatestMultipleValueTransform.tsx @@ -0,0 +1,46 @@ +// Libraries +import React, {useMemo, FunctionComponent} from 'react' +import {Table} from '../types' + +// Utils +import {getLatestValues} from '../utils/getLatestValues' + +interface Props { + table: Table + children: (latestValue: {[field: string]: number}) => JSX.Element + allowString: boolean + // If `quiet` is set and a latest value can't be found, this component will + // display nothing instead of an empty graph error message + quiet?: boolean +} + +// todo: can return string ? +export const LatestMultipleValueTransform: FunctionComponent = ({ + table, + quiet = false, + children, +}) => { + const latestValues = useMemo(() => getLatestValues(table), [table]) + + if (latestValues.length === 0 && quiet) { + return null + } + + if (latestValues.length === 0) { + return ( +
+

No latest value found

+
+ ) + } + + const entries = {} + + for (let i = latestValues.length / 2; i--; ) { + const value = latestValues[i * 2] + const field = latestValues[i * 2 + 1] + entries[field] = value + } + + return children(entries) +} diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index de17b83a..9b49f14c 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -17,6 +17,7 @@ import {FluxTablesTransform} from './FluxTablesTransform' import {TableGraphLayer} from './TableGraphLayer' import {usePlotEnv} from '../utils/usePlotEnv' +import {LatestMultipleValueTransform} from './LatestMultipleValueTransform' interface Props { config: SizedConfig @@ -83,18 +84,18 @@ export const SizedTable: FunctionComponent = ({ ) case LayerTypes.GaugeMini: return ( - - {latestValue => ( + {latestValues => ( } /> )} - + ) case LayerTypes.RawFluxDataTable: return ( diff --git a/giraffe/src/constants/gaugeMiniStyles.ts b/giraffe/src/constants/gaugeMiniStyles.ts index 68cc06a6..d4aad5ab 100644 --- a/giraffe/src/constants/gaugeMiniStyles.ts +++ b/giraffe/src/constants/gaugeMiniStyles.ts @@ -5,6 +5,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { type: 'gauge mini', mode: 'bullet', textMode: 'follow', + bars: [], valueHeight: 18, gaugeHeight: 25, @@ -12,6 +13,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { gaugeRounding: 3, barPaddings: 5, sidePaddings: 20, + oveflowFraction: 0.03, gaugeColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, @@ -25,10 +27,10 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, - labelBars: [], labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, + valuePadding: 5, valueFontSize: 12, valueFontColorOutside: InfluxColors.Raven, valueFontColorInside: InfluxColors.Cloud, @@ -44,6 +46,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { type: 'gauge mini', mode: 'progress', textMode: 'follow', + bars: [], valueHeight: 20, gaugeHeight: 20, @@ -51,6 +54,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { gaugeRounding: 3, barPaddings: 5, sidePaddings: 20, + oveflowFraction: 0.03, gaugeColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, @@ -62,10 +66,10 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, - labelBars: [], labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, + valuePadding: 5, valueFontSize: 18, valueFontColorInside: InfluxColors.Raven, valueFontColorOutside: InfluxColors.Cloud, diff --git a/giraffe/src/types/index.ts b/giraffe/src/types/index.ts index 1d80a8a4..06c442df 100644 --- a/giraffe/src/types/index.ts +++ b/giraffe/src/types/index.ts @@ -280,6 +280,7 @@ export interface GaugeTheme { export interface GaugeMiniLayerConfig { type: 'gauge mini' + bars?: {_field: string; label?: string}[] mode?: 'progress' | 'bullet' textMode?: 'follow' | 'left' @@ -289,6 +290,7 @@ export interface GaugeMiniLayerConfig { gaugeRounding?: number barPaddings?: number sidePaddings?: number + oveflowFraction?: number gaugeColors?: Color[] colorSecondary?: string @@ -297,10 +299,10 @@ export interface GaugeMiniLayerConfig { labelMainFontSize?: number labelMainFontColor?: string - labelBars?: {_field: string; label: string}[] labelBarsFontSize?: number labelBarsFontColor?: string + valuePadding?: number valueFontSize?: number valueFontColorInside?: string valueFontColorOutside?: string @@ -309,7 +311,7 @@ export interface GaugeMiniLayerConfig { axesSteps?: number | 'thresholds' | undefined | number[] axesFontSize?: number axesFontColor?: string - axesFormater: (value: number) => string + axesFormater?: (value: number) => string } export interface SingleStatLayerConfig { diff --git a/stories/src/data/gaugeMiniLayer.ts b/stories/src/data/gaugeMiniLayer.ts new file mode 100644 index 00000000..6ed66132 --- /dev/null +++ b/stories/src/data/gaugeMiniLayer.ts @@ -0,0 +1,37 @@ +import {newTable, Table} from '../../../giraffe/src' +import memoizeOne from 'memoize-one' + +const now = Date.now() +const numberOfRecords = 20 +const recordsPerLine = 20 + +let TIME_COL: Array +let VALUE_COL: Array +let FIELD_COL: Array + +function getRandomNumber(min: number, max: number) { + return Math.random() * (max - min) + min +} +const createColumns = (minValue: number, maxValue: number, fields: number) => { + TIME_COL = [] + VALUE_COL = [] + FIELD_COL = [] + for (let i = 0; i < numberOfRecords * fields; i += 1) { + VALUE_COL.push(getRandomNumber(minValue, maxValue)) + TIME_COL.push(now + ((i % recordsPerLine) % fields) * 1000 * 60) + FIELD_COL.push(gaugeMiniTableGetField(Math.floor(i / numberOfRecords))) + } +} + +/** return field name for given index */ +export const gaugeMiniTableGetField = (i: number) => `_field_${i}` + +export const gaugeMiniTable = memoizeOne( + (minValue: number, maxValue: number, fields: number): Table => { + createColumns(minValue, maxValue, fields) + return newTable(numberOfRecords * fields) + .addColumn('_time', 'dateTime:RFC3339', 'time', TIME_COL) + .addColumn('_value', 'system', 'number', VALUE_COL) + .addColumn('_field', 'string', 'string', FIELD_COL) + } +) diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index 20b65d61..05d53d3c 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -9,11 +9,13 @@ import { } from '../../giraffe/src' import {PlotContainer} from './helpers' -import {gaugeTable} from './data/gaugeLayer' import { GAUGE_MINI_THEME_BULLET_DARK, GAUGE_MINI_THEME_PROGRESS_DARK, } from '../../giraffe/src/constants/gaugeMiniStyles' +// import {gaugeTable as gaugeMiniTable} from './data/gaugeLayer' +import {gaugeMiniTable, gaugeMiniTableGetField} from './data/gaugeMiniLayer' +import {range} from 'd3-array' type Theme = Required @@ -46,6 +48,9 @@ const editableLayer = (theme: Theme): Theme => ({ }, theme.textMode ), + bars: range(number('number of bars', 1)).map(x => ({ + _field: gaugeMiniTableGetField(x), + })), valueHeight: number('valueHeight', theme.valueHeight), gaugeHeight: number('gaugeHeight', theme.gaugeHeight), @@ -53,6 +58,7 @@ const editableLayer = (theme: Theme): Theme => ({ gaugeRounding: number('gaugeRounding', theme.gaugeRounding), barPaddings: number('barPaddings', theme.barPaddings), sidePaddings: number('gaugePaddingSides', theme.sidePaddings), + oveflowFraction: number('oveflowFraction', theme.oveflowFraction), // todo: add knobs gaugeColors: theme.gaugeColors, @@ -62,11 +68,10 @@ const editableLayer = (theme: Theme): Theme => ({ labelMainFontSize: number('labelMainFontSize', theme.labelMainFontSize), labelMainFontColor: color('labelMainFontColor', theme.labelMainFontColor), - // todo: add knobs - labelBars: theme.labelBars, labelBarsFontSize: number('labelBarsFontSize', theme.labelBarsFontSize), labelBarsFontColor: color('labelBarsFontColor', theme.labelBarsFontColor), + valuePadding: number('valuePadding', theme.valuePadding), valueFontSize: number('valueFontSize', theme.valueFontSize), valueFontColorInside: color( 'valueFontColorInside', @@ -97,9 +102,11 @@ const editableLayer = (theme: Theme): Theme => ({ }) const createStory = (theme: Theme) => () => { + const layer = editableLayer(theme) + const config: Config = { - table: gaugeTable(0, 100), - layers: [editableLayer(theme)], + table: gaugeMiniTable(0, 100, layer.bars.length), + layers: [layer], } return ( <> From 95cffff988f35b5b39eeb078b5f910da85c1f172 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Sun, 29 Nov 2020 16:24:33 +0100 Subject: [PATCH 04/15] feat: group by selected columns --- giraffe/src/components/GaugeMini.tsx | 47 +++++++-- giraffe/src/components/GaugeMiniLayer.tsx | 13 +-- .../LatestMultipleValueTransform.tsx | 95 ++++++++++++++++--- giraffe/src/components/SizedTable.tsx | 5 +- giraffe/src/constants/gaugeMiniStyles.ts | 8 +- giraffe/src/types/index.ts | 11 ++- stories/src/gaugeMini.stories.tsx | 11 ++- 7 files changed, 146 insertions(+), 44 deletions(-) diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index 58198eab..c8181358 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -12,10 +12,25 @@ const throwReturn = (msg: string): T => { interface Props { width: number height: number - value: number | {_field: string; value: number}[] + values: {colsMString: string; value: number}[] theme: Required } +/** create merged string for given column string values. String is same for all columns with same values and unique for different ones */ +export const createColsMString = ( + groupedBy: T, + col: {[key in keyof T]: string} +): string => { + const columns = Object.keys(groupedBy).sort() + const columnValues = columns.map(x => col[x]) + /** + * replacing - with -- will ensures that rows + * { a: '0-1', b: '2' } and { a: '0', b: '1-2' } + * will not have same string (0-1-2 instead they will be 0--1-2 and 0-1--2) + */ + return columnValues.map(x => x.split('-').join('--')).join('-') +} + const barCssClass = 'gauge-mini-bar' //#region colors @@ -185,7 +200,7 @@ const BarBackground: FunctionComponent = ({ const y = barCenter - gaugeHeight / 2 // todo: invalid HTML -> multiple same ID attribute possible - const roundingDefId = `rounded-bar-${barWidth}-${gaugeHeight}` + const roundingDefId = `rounded-bar-w-${barWidth}-h-${gaugeHeight}-r-${gaugeRounding}` const gradientDefId = `gradient-${min.hex}-${max.hex}` type Segment = {start: number; end: number; hex: string} @@ -209,6 +224,7 @@ const BarBackground: FunctionComponent = ({ segments.push({start: 0, end: 1, hex: secondary}) } + // todo: dont't render def linear gradient when is not used return ( <> @@ -487,7 +503,7 @@ const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { //#endregion subcomponents export const GaugeMini: FunctionComponent = ({ - value, + values, theme, width, height, @@ -496,7 +512,7 @@ export const GaugeMini: FunctionComponent = ({ gaugeHeight, sidePaddings, valueHeight, - bars, + barsDefinitions, barPaddings, labelMain, labelMainFontSize, @@ -506,13 +522,20 @@ export const GaugeMini: FunctionComponent = ({ } = theme const [barLabelsWidth] = useState([]) - const valueArray = Array.isArray(value) ? value : [{_field: '', value}] const colors = getColors(theme) const colorLen = colors.max.value - colors.min.value const barLabelWidth = Math.max(...barLabelsWidth) || 0 const barWidth = width - sidePaddings * 2 - barLabelWidth const maxBarHeight = Math.max(gaugeHeight, valueHeight) - const allBarsHeight = valueArray.length * (maxBarHeight + barPaddings) + const allBarsHeight = values.length * (maxBarHeight + barPaddings) + + const {groupByColumns} = barsDefinitions + const labelMapping: any = {} + barsDefinitions?.bars?.forEach(x => { + if (!x.label) return + const mstring = createColsMString(groupByColumns, x.barDef) + labelMapping[mstring] = x.label + }) const [autocenterToken, setAutocenterToken] = useState(0) useEffect(() => { @@ -538,9 +561,9 @@ export const GaugeMini: FunctionComponent = ({ {labelMain}
)} - {valueArray.map(({_field, value}, i) => { + {values.map(({colsMString, value}, i) => { const y = 0 + i * (maxBarHeight + barPaddings) - const label = bars?.find(({_field: f}) => f === _field)?.label + const label = labelMapping?.[colsMString] const textCenter = y + maxBarHeight / 2 @@ -567,7 +590,13 @@ export const GaugeMini: FunctionComponent = ({ ) })} diff --git a/giraffe/src/components/GaugeMiniLayer.tsx b/giraffe/src/components/GaugeMiniLayer.tsx index f1f2e92b..a6eb7bc7 100644 --- a/giraffe/src/components/GaugeMiniLayer.tsx +++ b/giraffe/src/components/GaugeMiniLayer.tsx @@ -9,24 +9,17 @@ import {GaugeMini} from './GaugeMini' import {GAUGE_MINI_THEME_BULLET_DARK} from '../constants/gaugeMiniStyles' interface Props { - values: {[field: string]: number} + values: {colsMString: string; value: number}[] theme: GaugeMiniLayerConfig } export const GaugeMiniLayer: FunctionComponent = (props: Props) => { - const {theme, values: valueObj} = props + const {theme, values} = props const themeOrDefault: Required = { ...GAUGE_MINI_THEME_BULLET_DARK, ...theme, } - const values = Object.keys(valueObj) - .map(key => ({_field: key, value: valueObj[key]})) - .map(({_field, value}) => ({ - _field, - value: typeof value === 'string' ? parseFloat(value) : value, - })) - return ( {({width, height}) => ( @@ -37,7 +30,7 @@ export const GaugeMiniLayer: FunctionComponent = (props: Props) => { diff --git a/giraffe/src/components/LatestMultipleValueTransform.tsx b/giraffe/src/components/LatestMultipleValueTransform.tsx index b85d5532..fc36890d 100644 --- a/giraffe/src/components/LatestMultipleValueTransform.tsx +++ b/giraffe/src/components/LatestMultipleValueTransform.tsx @@ -1,26 +1,96 @@ // Libraries import React, {useMemo, FunctionComponent} from 'react' import {Table} from '../types' +import {createColsMString} from './GaugeMini' -// Utils -import {getLatestValues} from '../utils/getLatestValues' +interface SelectedColumns { + [key: string]: true +} + +export const getLatestValuesGrouped = ( + table: Table, + columnsObj: SelectedColumns +) => { + const columns = Object.keys(columnsObj).sort() + + columns.forEach(x => { + if (table.getColumnType(x) !== 'string') { + throw new Error( + `Data can be grouped only by string columns. But column ${x} is typeof ${table.getColumnType( + x + )}` + ) + } + }) + + const valueColumn = table.getColumn('_value', 'number') as number[] + + if (!valueColumn.length) { + return [] + } + + // Fallback to `_stop` column if `_time` column missing otherwise return empty array. + let timeColData: number[] = [] + + if (table.columnKeys.includes('_time')) { + timeColData = table.getColumn('_time', 'number') as number[] + } else if (table.columnKeys.includes('_stop')) { + timeColData = table.getColumn('_stop', 'number') as number[] + } + if (!timeColData && table.length !== 1) { + return [] + } + + const groupColsData = columns.map(k => + table.getColumn(k, 'string') + ) as string[][] + + const result: {[key: string]: number} = {} + + timeColData + // merge time with it's index + .map((time, index) => ({time, index})) + // remove entries without time + .filter(({time}) => time) + // todo: sort time complexity too high ... replace with another solution + // from low time to high time (last is last) + .sort(({time: t1}, {time: t2}) => t1 - t2) + // get relevant data from index (we don't need time anymore) + .map(({index}) => ({ + value: valueColumn[index], + groupRow: groupColsData.map(x => x[index]), + })) + // remove invalid data + .filter(({value}) => Number.isFinite(value) && typeof value === 'number') + // create result + .forEach(({value, groupRow}) => { + const grupObj = {} + groupRow.forEach((x, i) => (grupObj[columns[i]] = x)) + const strKey = createColsMString(columnsObj, grupObj) + // data is inserted from last to first so latest data is first + result[strKey] = value + }) + + return result +} interface Props { table: Table - children: (latestValue: {[field: string]: number}) => JSX.Element - allowString: boolean - // If `quiet` is set and a latest value can't be found, this component will - // display nothing instead of an empty graph error message + columns: SelectedColumns + children: (latestValue: {colsMString: string; value: number}[]) => JSX.Element quiet?: boolean } // todo: can return string ? export const LatestMultipleValueTransform: FunctionComponent = ({ table, + columns, quiet = false, children, }) => { - const latestValues = useMemo(() => getLatestValues(table), [table]) + const latestValues = useMemo(() => getLatestValuesGrouped(table, columns), [ + table, + ]) if (latestValues.length === 0 && quiet) { return null @@ -34,13 +104,10 @@ export const LatestMultipleValueTransform: FunctionComponent = ({ ) } - const entries = {} - - for (let i = latestValues.length / 2; i--; ) { - const value = latestValues[i * 2] - const field = latestValues[i * 2 + 1] - entries[field] = value - } + const entries = Object.keys(latestValues).map(x => ({ + colsMString: x, + value: latestValues[x], + })) return children(entries) } diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index 9b49f14c..fbe45829 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -87,7 +87,10 @@ export const SizedTable: FunctionComponent = ({ ) + .barsDefinitions.groupByColumns + } > {latestValues => ( = { type: 'gauge mini', mode: 'bullet', textMode: 'follow', - bars: [], + barsDefinitions: {groupByColumns: {_label: true}}, valueHeight: 18, gaugeHeight: 25, @@ -34,19 +34,19 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { valueFontSize: 12, valueFontColorOutside: InfluxColors.Raven, valueFontColorInside: InfluxColors.Cloud, - valueFormater: (val: number) => val.toFixed(0), + valueFormater: (num: number) => num.toFixed(0), axesSteps: 'thresholds', axesFontSize: 11, axesFontColor: InfluxColors.Forge, - axesFormater: (val: number) => val.toFixed(0), + axesFormater: (num: number) => num.toFixed(0), } export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { type: 'gauge mini', mode: 'progress', textMode: 'follow', - bars: [], + barsDefinitions: {groupByColumns: {_label: true}}, valueHeight: 20, gaugeHeight: 20, diff --git a/giraffe/src/types/index.ts b/giraffe/src/types/index.ts index 06c442df..28eaed3c 100644 --- a/giraffe/src/types/index.ts +++ b/giraffe/src/types/index.ts @@ -278,9 +278,18 @@ export interface GaugeTheme { overflowDelta: number } +export interface GaugeMiniBarsDefinitions { + /** Defines which columns choose as unique bar indentificator. */ + groupByColumns: T + // todo: allow regex ? + /** Give label for given unique column values */ + bars?: {barDef: {[key in keyof T]: string}; label?: string}[] +} + export interface GaugeMiniLayerConfig { type: 'gauge mini' - bars?: {_field: string; label?: string}[] + /** Defines which columns choose as unique bar indentificator. Also bar labels can be defined here. */ + barsDefinitions: GaugeMiniBarsDefinitions mode?: 'progress' | 'bullet' textMode?: 'follow' | 'left' diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index 05d53d3c..20fd78b7 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -30,7 +30,7 @@ const color = (() => { return (label: string, ...rest: any[]) => select(label, colors, ...rest) })() -const editableLayer = (theme: Theme): Theme => ({ +const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ type: theme.type, mode: select( 'Mode', @@ -48,9 +48,10 @@ const editableLayer = (theme: Theme): Theme => ({ }, theme.textMode ), - bars: range(number('number of bars', 1)).map(x => ({ - _field: gaugeMiniTableGetField(x), - })), + numberOfBars: number('number of bars', 1), + barsDefinitions: { + groupByColumns: {_field: true}, + }, valueHeight: number('valueHeight', theme.valueHeight), gaugeHeight: number('gaugeHeight', theme.gaugeHeight), @@ -105,7 +106,7 @@ const createStory = (theme: Theme) => () => { const layer = editableLayer(theme) const config: Config = { - table: gaugeMiniTable(0, 100, layer.bars.length), + table: gaugeMiniTable(0, 100, layer.numberOfBars), layers: [layer], } return ( From 715fc23ed091cdd25905565e60f2358a8d523a0d Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Sun, 29 Nov 2020 17:28:24 +0100 Subject: [PATCH 05/15] fix: _label renamed to _field --- giraffe/src/components/GaugeMini.tsx | 4 +++- giraffe/src/constants/gaugeMiniStyles.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index c8181358..1a36fa25 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -532,7 +532,9 @@ export const GaugeMini: FunctionComponent = ({ const {groupByColumns} = barsDefinitions const labelMapping: any = {} barsDefinitions?.bars?.forEach(x => { - if (!x.label) return + if (!x.label) { + return + } const mstring = createColsMString(groupByColumns, x.barDef) labelMapping[mstring] = x.label }) diff --git a/giraffe/src/constants/gaugeMiniStyles.ts b/giraffe/src/constants/gaugeMiniStyles.ts index f434b94e..33de7391 100644 --- a/giraffe/src/constants/gaugeMiniStyles.ts +++ b/giraffe/src/constants/gaugeMiniStyles.ts @@ -5,7 +5,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { type: 'gauge mini', mode: 'bullet', textMode: 'follow', - barsDefinitions: {groupByColumns: {_label: true}}, + barsDefinitions: {groupByColumns: {_field: true}}, valueHeight: 18, gaugeHeight: 25, @@ -46,7 +46,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { type: 'gauge mini', mode: 'progress', textMode: 'follow', - barsDefinitions: {groupByColumns: {_label: true}}, + barsDefinitions: {groupByColumns: {_field: true}}, valueHeight: 20, gaugeHeight: 20, From 565a0707cdfe6c83e10295b8822e543e8de8d136 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 23 Dec 2020 03:47:17 +0100 Subject: [PATCH 06/15] fix: false columns --- giraffe/src/components/GaugeMini.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index 1a36fa25..6c49fa95 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -3,6 +3,8 @@ import React, {FunctionComponent, useRef, useEffect, useState} from 'react' import {color as d3Color} from 'd3-color' import {scaleLinear} from 'd3-scale' import {range} from 'd3-array' + +// Types import {Color, GaugeMiniLayerConfig} from '../types' const throwReturn = (msg: string): T => { @@ -21,7 +23,9 @@ export const createColsMString = ( groupedBy: T, col: {[key in keyof T]: string} ): string => { - const columns = Object.keys(groupedBy).sort() + const columns = Object.keys(groupedBy) + .filter(x => groupedBy[x]) + .sort() const columnValues = columns.map(x => col[x]) /** * replacing - with -- will ensures that rows From 4f985f5fc8dd7f2240a3d19d77f824d7fa3fda9c Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Mon, 28 Dec 2020 19:22:58 +0100 Subject: [PATCH 07/15] feat: gauge mini simpler types --- giraffe/src/components/GaugeMini.tsx | 139 ++++++----------- giraffe/src/components/SizedTable.tsx | 7 +- giraffe/src/constants/gaugeMiniStyles.ts | 16 +- giraffe/src/types/index.ts | 27 +++- giraffe/src/utils/formatStatValue.ts | 2 +- giraffe/src/utils/gaugeMiniThemeNormalize.ts | 153 +++++++++++++++++++ stories/src/gaugeMini.stories.tsx | 2 +- 7 files changed, 238 insertions(+), 108 deletions(-) create mode 100644 giraffe/src/utils/gaugeMiniThemeNormalize.ts diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index 6c49fa95..c3f2dc2f 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -2,14 +2,13 @@ import React, {FunctionComponent, useRef, useEffect, useState} from 'react' import {color as d3Color} from 'd3-color' import {scaleLinear} from 'd3-scale' -import {range} from 'd3-array' // Types -import {Color, GaugeMiniLayerConfig} from '../types' - -const throwReturn = (msg: string): T => { - throw new Error(msg) -} +import {GaugeMiniColors, GaugeMiniLayerConfig} from '../types' +import { + gaugeMiniNormalizeTheme, + GaugeMiniThemeNormalized, +} from '../utils/gaugeMiniThemeNormalize' interface Props { width: number @@ -18,6 +17,7 @@ interface Props { theme: Required } +// todo: move into gauge utils /** create merged string for given column string values. String is same for all columns with same values and unique for different ones */ export const createColsMString = ( groupedBy: T, @@ -37,40 +37,6 @@ export const createColsMString = ( const barCssClass = 'gauge-mini-bar' -//#region colors - -export type Colors = { - min: Color - max: Color - secondary: string - thresholds: Color[] -} - -export const getColors = (theme: Required): Colors => { - const {colorSecondary: secondary, gaugeColors} = theme - - gaugeColors.forEach( - ({hex, name}) => - d3Color(hex) ?? - throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) - ) - - return { - min: - gaugeColors.find(x => x.type === 'min') ?? - throwReturn('color of type min must be defined'), - max: - gaugeColors.find(x => x.type === 'max') ?? - throwReturn('color of type max must be defined'), - thresholds: gaugeColors - .filter(({type}) => type === 'threshold') - .sort(({value: a}, {value: b}) => a - b), - secondary, - } -} - -//#endregion colors - //#region svg helpers type TSvgTextRectProps = { @@ -153,62 +119,67 @@ const AutoCenterGroup: FunctionComponent<{ //#region types type BarBackgroundProps = { - theme: Required - colors: Colors + theme: Required + colors: GaugeMiniColors barWidth: number getFrac: (x: number) => number barCenter: number } type BarValueProps = { - theme: Required + theme: Required barValueWidth: number - colors: Colors + colors: GaugeMiniColors value: number valueFracFixed: number barCenter: number } type TextProps = { - theme: Required + theme: Required barValueWidth: number - colors: Colors + colors: GaugeMiniColors value: number } type BarProps = { value: number - theme: Required + theme: Required barWidth: number y: number getFrac: (x: number) => number } type AxesProps = { - theme: Required + theme: Required barWidth: number y: number getFrac: (x: number) => number } +type BarSegment = { + start: number + end: number + hex: string +} + //#endregion types const BarBackground: FunctionComponent = ({ theme, - colors: {max, min, secondary, thresholds}, barWidth, getFrac, barCenter, }) => { - const {gaugeHeight, mode, gaugeRounding} = theme + const {gaugeHeight, mode, gaugeRounding, colors, colorSecondary} = theme + const {max, min, thresholds = []} = colors const y = barCenter - gaugeHeight / 2 // todo: invalid HTML -> multiple same ID attribute possible const roundingDefId = `rounded-bar-w-${barWidth}-h-${gaugeHeight}-r-${gaugeRounding}` const gradientDefId = `gradient-${min.hex}-${max.hex}` - type Segment = {start: number; end: number; hex: string} - const segments: Segment[] = [] + const segments: BarSegment[] = [] if (mode === 'bullet') { // thresholds are already sorted by getColors const allColors = [min, ...thresholds, max] @@ -225,7 +196,7 @@ const BarBackground: FunctionComponent = ({ segments.push({start, end, hex}) } } else { - segments.push({start: 0, end: 1, hex: secondary}) + segments.push({start: 0, end: 1, hex: colorSecondary}) } // todo: dont't render def linear gradient when is not used @@ -280,8 +251,9 @@ const BarValue: FunctionComponent = ({ valueFracFixed, barCenter, }) => { - const {valueHeight, mode, valueRounding} = theme - const colorModeGradient = colors.thresholds.length === 0 + const {valueHeight, mode, valueRounding, colorSecondary} = theme + const {min, max, thresholds = []} = colors + const colorModeGradient = thresholds.length === 0 const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 const y = barCenter - valueHeight / 2 @@ -290,19 +262,15 @@ const BarValue: FunctionComponent = ({ const colorValue = mode === 'bullet' - ? colors.secondary + ? colorSecondary : d3Color( (() => { if (colorModeGradient) { return scaleLinear() - .range([colors.min.hex, colors.max.hex] as any) - .domain([colors.min.value, colors.max.value])(value) as any + .range([min.hex, max.hex] as any) + .domain([min.value, max.value])(value) as any } else { - const sortedColors = [ - colors.min, - ...colors.thresholds, - colors.max, - ] + const sortedColors = [min, ...thresholds, max] let i = 0 while ( i < sortedColors.length && @@ -393,9 +361,7 @@ const Bar: FunctionComponent = ({ barWidth, getFrac, }) => { - const {gaugeHeight, valueHeight, oveflowFraction} = theme - - const colors = getColors(theme) + const {gaugeHeight, valueHeight, oveflowFraction, colors} = theme const valueFracFixed = Math.max( -oveflowFraction, @@ -423,41 +389,26 @@ const Bar: FunctionComponent = ({ } const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { - const {axesSteps, axesFormater, axesFontColor, axesFontSize} = theme + const {axesSteps, axesFormater, axesFontColor, axesFontSize, colors} = theme - if (axesSteps === undefined || axesSteps === null) { + if (axesSteps === undefined) { return <> } - const colors = getColors(theme) - const colorLen = colors.max.value - colors.min.value + const {min, max} = colors const axesLineStyle: React.CSSProperties = { stroke: axesFontColor, strokeWidth: 2, strokeLinecap: 'round', } - const axesValuesArray = Array.isArray(axesSteps) - ? axesSteps - : axesSteps === 'thresholds' - ? colors.thresholds.map(x => x.value) - : Number.isInteger(axesSteps) - ? range(axesSteps).map( - x => ((x + 1) * colorLen) / (axesSteps + 1) + colors.min.value - ) - : throwReturn( - `${JSON.stringify( - axesSteps - )} axesSteps must be number | "thresholds" | number[] | undefined.` - ) - const points: { anchor: string value: number lineLength: number text: string posX: number - }[] = axesValuesArray + }[] = axesSteps .map(value => ({ value, anchor: 'middle', @@ -465,12 +416,12 @@ const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { })) .concat([ { - value: colors.min.value, + value: min.value, anchor: 'start', lineLength: 3, }, { - value: colors.max.value, + value: max.value, anchor: 'end', lineLength: 3, }, @@ -508,38 +459,42 @@ const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { export const GaugeMini: FunctionComponent = ({ values, - theme, + theme: _theme, width, height, }) => { + const theme = gaugeMiniNormalizeTheme(_theme) + const { gaugeHeight, sidePaddings, valueHeight, - barsDefinitions, barPaddings, labelMain, labelMainFontSize, labelMainFontColor, labelBarsFontColor, labelBarsFontSize, + colors, } = theme const [barLabelsWidth] = useState([]) - const colors = getColors(theme) const colorLen = colors.max.value - colors.min.value const barLabelWidth = Math.max(...barLabelsWidth) || 0 const barWidth = width - sidePaddings * 2 - barLabelWidth const maxBarHeight = Math.max(gaugeHeight, valueHeight) const allBarsHeight = values.length * (maxBarHeight + barPaddings) - const {groupByColumns} = barsDefinitions + const barsDefinitions = theme.barsDefinitions + + // create unified barsDefinition + const labelMapping: any = {} barsDefinitions?.bars?.forEach(x => { if (!x.label) { return } - const mstring = createColsMString(groupByColumns, x.barDef) + const mstring = createColsMString(barsDefinitions.groupByColumns, x.barDef) labelMapping[mstring] = x.label }) diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index fbe45829..10fb09bd 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -18,6 +18,7 @@ import {TableGraphLayer} from './TableGraphLayer' import {usePlotEnv} from '../utils/usePlotEnv' import {LatestMultipleValueTransform} from './LatestMultipleValueTransform' +import {getGaugeMiniBarsDefinitions} from '../utils/gaugeMiniThemeNormalize' interface Props { config: SizedConfig @@ -88,8 +89,10 @@ export const SizedTable: FunctionComponent = ({ key={layerIndex} table={newTableFromConfig(config)} columns={ - (layerConfig as Required) - .barsDefinitions.groupByColumns + getGaugeMiniBarsDefinitions( + (layerConfig as Required) + .barsDefinitions + ).groupByColumns } > {latestValues => ( diff --git a/giraffe/src/constants/gaugeMiniStyles.ts b/giraffe/src/constants/gaugeMiniStyles.ts index 33de7391..5013ab05 100644 --- a/giraffe/src/constants/gaugeMiniStyles.ts +++ b/giraffe/src/constants/gaugeMiniStyles.ts @@ -5,7 +5,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { type: 'gauge mini', mode: 'bullet', textMode: 'follow', - barsDefinitions: {groupByColumns: {_field: true}}, + barsDefinitions: {groupByColumns: ['_field']}, valueHeight: 18, gaugeHeight: 25, @@ -15,7 +15,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { sidePaddings: 20, oveflowFraction: 0.03, - gaugeColors: [ + gaugeMiniColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, @@ -34,19 +34,19 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { valueFontSize: 12, valueFontColorOutside: InfluxColors.Raven, valueFontColorInside: InfluxColors.Cloud, - valueFormater: (num: number) => num.toFixed(0), + valueFormater: {}, axesSteps: 'thresholds', axesFontSize: 11, axesFontColor: InfluxColors.Forge, - axesFormater: (num: number) => num.toFixed(0), + axesFormater: {}, } export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { type: 'gauge mini', mode: 'progress', textMode: 'follow', - barsDefinitions: {groupByColumns: {_field: true}}, + barsDefinitions: {groupByColumns: ['_field']}, valueHeight: 20, gaugeHeight: 20, @@ -56,7 +56,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { sidePaddings: 20, oveflowFraction: 0.03, - gaugeColors: [ + gaugeMiniColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, {value: 100, type: 'max', hex: InfluxColors.Topaz}, ] as Color[], @@ -73,10 +73,10 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { valueFontSize: 18, valueFontColorInside: InfluxColors.Raven, valueFontColorOutside: InfluxColors.Cloud, - valueFormater: (val: number) => val.toFixed(0), + valueFormater: {}, axesSteps: undefined as any, axesFontSize: 11, axesFontColor: InfluxColors.Forge, - axesFormater: (val: number) => val.toFixed(0), + axesFormater: {}, } diff --git a/giraffe/src/types/index.ts b/giraffe/src/types/index.ts index 28eaed3c..7faf123f 100644 --- a/giraffe/src/types/index.ts +++ b/giraffe/src/types/index.ts @@ -2,6 +2,7 @@ import CSS from 'csstype' import {CSSProperties, ReactNode} from 'react' import {TimeZone} from './timeZones' import {GeoLayerConfig} from './geo' +import {FormatStatValueOptions} from '../utils/formatStatValue' export type SizedConfig = Config & {width: number; height: number} export interface Config { @@ -278,6 +279,17 @@ export interface GaugeTheme { overflowDelta: number } +export type ColorHexValue = { + value: number + hex: string +} + +export type GaugeMiniColors = { + min: ColorHexValue + max: ColorHexValue + thresholds?: ColorHexValue[] +} + export interface GaugeMiniBarsDefinitions { /** Defines which columns choose as unique bar indentificator. */ groupByColumns: T @@ -286,10 +298,17 @@ export interface GaugeMiniBarsDefinitions { bars?: {barDef: {[key in keyof T]: string}; label?: string}[] } +export interface GaugeMiniBarsDefinitionsArr { + groupByColumns: string[] + bars?: {barDef: string[]; label?: string}[] +} + export interface GaugeMiniLayerConfig { type: 'gauge mini' /** Defines which columns choose as unique bar indentificator. Also bar labels can be defined here. */ - barsDefinitions: GaugeMiniBarsDefinitions + barsDefinitions: + | GaugeMiniBarsDefinitionsArr + | GaugeMiniBarsDefinitions<{[key: string]: true}> mode?: 'progress' | 'bullet' textMode?: 'follow' | 'left' @@ -301,7 +320,7 @@ export interface GaugeMiniLayerConfig { sidePaddings?: number oveflowFraction?: number - gaugeColors?: Color[] + gaugeMiniColors?: Color[] | GaugeMiniColors colorSecondary?: string labelMain?: string @@ -315,12 +334,12 @@ export interface GaugeMiniLayerConfig { valueFontSize?: number valueFontColorInside?: string valueFontColorOutside?: string - valueFormater?: (value: number) => string + valueFormater?: ((value: number) => string) | FormatStatValueOptions axesSteps?: number | 'thresholds' | undefined | number[] axesFontSize?: number axesFontColor?: string - axesFormater?: (value: number) => string + axesFormater?: ((value: number) => string) | FormatStatValueOptions } export interface SingleStatLayerConfig { diff --git a/giraffe/src/utils/formatStatValue.ts b/giraffe/src/utils/formatStatValue.ts index bc56768e..32fda6d8 100644 --- a/giraffe/src/utils/formatStatValue.ts +++ b/giraffe/src/utils/formatStatValue.ts @@ -6,7 +6,7 @@ import {DecimalPlaces} from '../types' export const MAX_DECIMAL_PLACES = 10 -interface FormatStatValueOptions { +export interface FormatStatValueOptions { decimalPlaces?: DecimalPlaces prefix?: string suffix?: string diff --git a/giraffe/src/utils/gaugeMiniThemeNormalize.ts b/giraffe/src/utils/gaugeMiniThemeNormalize.ts new file mode 100644 index 00000000..1bc5a806 --- /dev/null +++ b/giraffe/src/utils/gaugeMiniThemeNormalize.ts @@ -0,0 +1,153 @@ +import {FormatStatValueOptions, formatStatValue} from './formatStatValue' +import { + GaugeMiniBarsDefinitions, + GaugeMiniColors, + GaugeMiniBarsDefinitionsArr, +} from '../types' +import {GaugeMiniLayerConfig} from '..' +import {color as d3Color} from 'd3-color' +import {range} from 'd3-array' + +export const throwReturn = (msg: string): T => { + throw new Error(msg) +} + +type TBarsDefinitions = GaugeMiniBarsDefinitions<{[key: string]: true}> + +type RestrictedTypesProperties = { + barsDefinitions: TBarsDefinitions + colors?: GaugeMiniColors + valueFormater?: (value: number) => string + + axesSteps: undefined | number[] + axesFormater?: (value: number) => string +} + +export type GaugeMiniThemeNormalized = Omit< + GaugeMiniLayerConfig, + keyof RestrictedTypesProperties +> & + RestrictedTypesProperties + +const getFormater = ( + formater: ((value: number) => string) | FormatStatValueOptions +): ((value: number) => string) => + typeof formater === 'function' + ? formater + : (value: number) => formatStatValue(value, formater) + +const isBarsDefinitionsArrayStyle = ( + barsDefinitions: GaugeMiniLayerConfig['barsDefinitions'] +): barsDefinitions is GaugeMiniBarsDefinitionsArr => { + return Array.isArray(barsDefinitions.groupByColumns) +} + +const getBarsDefinitions = ( + barsDefinitions: GaugeMiniLayerConfig['barsDefinitions'] +): TBarsDefinitions => { + if (!isBarsDefinitionsArrayStyle(barsDefinitions)) { + return barsDefinitions + } + + const {groupByColumns, bars} = barsDefinitions + + return { + groupByColumns: groupByColumns.reduce( + (obj, prop) => ((obj[prop] = true), obj), + {} as {[key: string]: true} + ), + bars: bars?.map(x => ({ + barDef: x.barDef.reduce( + (obj, prop, i) => ((obj[prop] = groupByColumns[i]), obj), + {} as Required['bars'][number]['barDef'] + ), + label: x.label, + })), + } +} + +const getAxesSteps = ( + axesSteps: number | 'thresholds' | undefined | number[], + colors: GaugeMiniColors +): number[] | undefined => { + if (axesSteps === undefined || axesSteps === null) { + return undefined + } + const { + max: {value: max}, + min: {value: min}, + } = colors + + if (Array.isArray(axesSteps)) { + if (axesSteps.some(x => x < min || x > max)) { + throw new Error(`All axes values must be inside range of colors!`) + } + return axesSteps + } + + if (axesSteps === 'thresholds') { + return (colors.thresholds ?? []).map(x => x.value) + } + + if (Number.isInteger(axesSteps)) { + const colorLen = max - min + + return range(axesSteps).map( + x => ((x + 1) * colorLen) / (axesSteps + 1) + min + ) + } + + throw new Error( + `AxesSteps must be number | "thresholds" | number[]` + + ` | undefined. But it's value is ${JSON.stringify(axesSteps)}` + ) +} + +const getColors = (theme: Required): GaugeMiniColors => { + const {gaugeMiniColors: colors} = theme + + if (!Array.isArray(colors)) { + return { + ...colors, + thresholds: (colors.thresholds ?? []).sort( + ({value: a}, {value: b}) => a - b + ), + } + } + + colors.forEach( + ({hex, name}) => + d3Color(hex) ?? + throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) + ) + + return { + min: + colors.find(x => x.type === 'min') ?? + throwReturn('color of type min must be defined'), + max: + colors.find(x => x.type === 'max') ?? + throwReturn('color of type max must be defined'), + thresholds: colors + .filter(({type}) => type === 'threshold') + .sort(({value: a}, {value: b}) => a - b), + } +} + +// todo: more validations +export const gaugeMiniNormalizeTheme = ( + theme: Required +): Required => { + const colors = getColors(theme) + return { + ...theme, + barsDefinitions: getBarsDefinitions(theme.barsDefinitions), + colors, + valueFormater: getFormater(theme.valueFormater), + + axesSteps: getAxesSteps(theme.axesSteps, colors), + axesFormater: getFormater(theme.axesFormater), + } +} + +export const getGaugeMiniBarsDefinitions = getBarsDefinitions diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index 20fd78b7..b1bf0937 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -62,7 +62,7 @@ const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ oveflowFraction: number('oveflowFraction', theme.oveflowFraction), // todo: add knobs - gaugeColors: theme.gaugeColors, + gaugeMiniColors: theme.gaugeMiniColors, colorSecondary: color('colorSecondary', theme.colorSecondary), labelMain: text('labelMain', 'Gauge-mini example'), From 6632abaf6355d19bac38a9313267034c4c12aed0 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Dec 2020 04:23:00 +0100 Subject: [PATCH 08/15] fix: don't throw error on axesSteps out of bounds --- giraffe/src/utils/gaugeMiniThemeNormalize.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/giraffe/src/utils/gaugeMiniThemeNormalize.ts b/giraffe/src/utils/gaugeMiniThemeNormalize.ts index 1bc5a806..6bca02f1 100644 --- a/giraffe/src/utils/gaugeMiniThemeNormalize.ts +++ b/giraffe/src/utils/gaugeMiniThemeNormalize.ts @@ -79,10 +79,11 @@ const getAxesSteps = ( } = colors if (Array.isArray(axesSteps)) { - if (axesSteps.some(x => x < min || x > max)) { - throw new Error(`All axes values must be inside range of colors!`) + const steps = axesSteps.filter(x => x > min || x < max) + if (axesSteps.length !== steps.length) { + console.error(`All axes values must be inside range of colors!`) } - return axesSteps + return steps } if (axesSteps === 'thresholds') { From afbd78dbf9a1b4d215b726f6338b7f1d48d5b439 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Dec 2020 23:23:43 +0100 Subject: [PATCH 09/15] docs: gauge mini --- giraffe/README.md | 89 ++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/giraffe/README.md b/giraffe/README.md index acff5413..ffc0b9cb 100644 --- a/giraffe/README.md +++ b/giraffe/README.md @@ -699,22 +699,19 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **overflowDelta**: _number. Optional. Defaults to 0.03 when excluded._ This constant expresses how far past the gauge min or gauge max the needle should be drawn if the value for the needle is less than gauge min or greater than the gauge max. It is expressed as a fraction of the circumference of a circle, e.g. 0.5 means draw halfway around the gauge from the min or max value. -- **GaugeLayerConfig**: _Object._ Maximum one per ``. Properties are: +- **GaugeMiniLayerConfig**: _Object._ Maximum one per ``. Properties are: - For multiple bars values has to be passed as columns (not fields). That can be done in flux by - ``` - import "influxdata/influxdb/v1" - - |> v1.fieldsAsCols() - ``` + All values (excluding **type**) are Optional and their defaults is defined by theme `GAUGE_MINI_THEME_BULLET_DARK` - All values (excluding **type**) are Optional and their defaults is defined by precreated theme `GAUGE_MINI_THEME_BULLET_DARK` - - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. - - **bars** _{\_field: string; label?: string}[]_ per bar settings: - - __field_ giraffe table column name (that contains value), can be set to `'_value'` when there is only one field present - - _label_ optional label for bar + - **barsDefinitions** _{groupByColumns; bars;}_ _(types of properties based on selected style)_ + - Object style: + - **groupByColumns** _{ [key: string]: true }_ bar for each unique combination of given columns values. _(example: `{cpu: true, _field: true}`)_ + - **bars** _{ barDef: { [key in keyof T]: string }, label?: string }[]_ where _barDef_ contains values for specific bar columns and label for this bar. + - _or_ Array style: + - **groupByColumns** _string[]_ bar for each unique combination of given columns values. _(example: `['cpu', '_field', ]`)_ + - **bars** _{ barDef: string[], label?: string }[]_ where _barDef_ contains values for specific bar columns and label for this bar. Each barDef value belongs to key grom groupByColumns with same index. - **mode**: _'progress' | 'bullet'._ - `'bullet'` backgroud bar is colored and value bar has always secondary color @@ -738,20 +735,23 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **oveflowFraction** _number_ fraction number defining how much can value bar go outside of background bar. e.g. with `oveflowFraction: .1` will value bar have max length 110% of background bar for max value and will have 10% length to the left from background bar for minimal value. - - **gaugeColors** _Color[]_ An array of objects that defines the colors of the Gauge. Each object has the following properties. - - - **id**: _string. **Required**._ The id for this color. Should be unique within the **gaugeColors** array. - - - **type**: _'min' | 'max' | 'threshold'. **Required**._ The type of value associated with this color. _'min'_ type comes first, _'max'_ type comes last, and _'threshold'_ types are in between _'min'_ and _'max'_. **gaugeColors** must contain at least one _'min'_ type and one _'max'_ type for the Gauge-mini to have color. Only the first _'min'_ and first _'max'_ in the array are recognized for each of their respective types. The color will change as a gradient if the **gaugeColors** array contains no 'threshold' that means gradient background bar for 'bullet' mode and continuous value bar color change for 'progress' mode. The color will be segmented if any _'threshold'_ types are included in the **gaugeColors** array that means step color change background bar for 'bullet' mode and fixed color of value bar based on value change for 'progress' mode. + - **gaugeColors** _types based on used style_ + - giraffe style _Color[]_ An array of objects that defines the colors of the Gauge. Each object has the following properties. - - **hex**: _string. **Required**._ The [_color hex_](https://www.color-hex.com/) string for this color. + - **id**: _string. **Required**._ The id for this color. Should be unique within the **gaugeColors** array. - - **name**: _string. **Required**._ For descriptive purposes only. The name given to this color. - - - **value**: _number. **Required**._ The starting gauge value associated with this color. + - **type**: _'min' | 'max' | 'threshold'. **Required**._ The type of value associated with this color. _'min'_ type comes first, _'max'_ type comes last, and _'threshold'_ types are in between _'min'_ and _'max'_. **gaugeColors** must contain at least one _'min'_ type and one _'max'_ type for the Gauge-mini to have color. Only the first _'min'_ and first _'max'_ in the array are recognized for each of their respective types. The color will change as a gradient if the **gaugeColors** array contains no 'threshold' that means gradient background bar for 'bullet' mode and continuous value bar color change for 'progress' mode. The color will be segmented if any _'threshold'_ types are included in the **gaugeColors** array that means step color change background bar for 'bullet' mode and fixed color of value bar based on value change for 'progress' mode. + - **hex**: _string. **Required**._ The [_color hex_](https://www.color-hex.com/) string for this color. + - **name**: _string. **Required**._ For descriptive purposes only. The name given to this color. + - **value**: _number. **Required**._ The starting gauge value associated with this color. + - mini gauge style: _{min; max; thresholds; }_ + - _ColorHexValue_ is _{ value: number; hex: string; }_ where hex is [_color hex_](https://www.color-hex.com/) adn value is where color is applied. + - **min** _ColorHexValue_ **Required** is minimaln value of gauge + - **max** _ColorHexValue_ **Required** is maximaln value of gauge + - **thresholds** _ColorHexValue[]_ is thresholds of gauge. The color will change as a gradient if no thresholds present, that means gradient background bar for 'bullet' mode and continuous value bar color change for 'progress' mode. The color will be segmented one or more thresholds present, that means step color change background bar for 'bullet' mode and fixed color of value bar based on value change for 'progress' mode. - **colorSecondary** _string_ Secondary color used for value bar in 'bullet' mode or for background bar in 'progress' mode @@ -776,8 +776,6 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **valueFontColorOutside** _string_ Text value color when value bar is not behind the text - - **valueFormater** _(value: number) => string_ Function that defines how will be text value shown based on current value. e.g. ```valueFormater: (num: number) => `${num.toFixed(0)}%` ``` for _value=23.213_ will show text value _23%_. - Axes - **axesSteps** _number | 'thresholds' | undefined | number[]_ Defines where to show axes: - _number_ number of how many evenly distributed axes values will be shown between min and max value. Only min and max value will be shown when `axesSteps: 0` @@ -789,7 +787,18 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **axesFontColor** _string_ Color of axes values and axes lines - - **axesFormater** _(value: number) => string_ Same as **valueFormater** for axes values + - Formaters **valueFormater** and **axesFormater** _(value: number) => string_ or _FormatStatValueOptions_ + - How will be text value on bar and axes values formated. + - _FormatStatValueOptions_ + - **prefix**: _string. Optional._ The text that appears before the gauge value. Use an empty string if no text is preferred. + + - **suffix**: _string. Optional._ The text that appears after the gauge value. Use an empty string if no text is preferred. + + - **decimalPlaces**: _Object. Optional._ + + - **isEnforced**: _boolean. Optional. Defaults to false when not included._ Indicates whether the number of decimal places ("**digits**") will be enforced. When **isEnforced** is falsy or omitted, **digits** will be locked to 2 for stat values with a decimal and 0 for stat values that are integers, and the **digits** option will be ignored. + - **digits**: _number. Optional. Defaults to 0 when not included. Maximum 10._ When **digits** is a non-integer number, the decimal portion is ignored. Represents the number of decimal places to display in the stat value. Displayed stat value is subject to rounding. + - example ```valueFormater: (num: number) => `${num.toFixed(0)}%` ``` for _value=23.213_ will show text value _23%_. **Precreated themes** - `GAUGE_MINI_THEME_BULLET_DARK` @@ -798,7 +807,7 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data type: 'gauge mini', mode: 'bullet', textMode: 'follow', - bars: [], + barsDefinitions: {groupByColumns: ["_field"]}, valueHeight: 18, gaugeHeight: 25, @@ -806,9 +815,9 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data gaugeRounding: 3, barPaddings: 5, sidePaddings: 20, - oveflowFraction: 0.03, + oveflowFraction: .03, - gaugeColors: [ + gaugeMiniColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, @@ -827,12 +836,12 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data valueFontSize: 12, valueFontColorOutside: InfluxColors.Raven, valueFontColorInside: InfluxColors.Cloud, - valueFormater: (val: number) => val.toFixed(0), + valueFormater: {}, axesSteps: 'thresholds', axesFontSize: 11, axesFontColor: InfluxColors.Forge, - axesFormater: (val: number) => val.toFixed(0), + axesFormater: {}, } ``` - `GAUGE_MINI_THEME_PROGRESS_DARK` @@ -841,39 +850,39 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data type: 'gauge mini', mode: 'progress', textMode: 'follow', - bars: [], - + barsDefinitions: {groupByColumns: ['_field']}, + valueHeight: 20, gaugeHeight: 20, valueRounding: 3, gaugeRounding: 3, barPaddings: 5, sidePaddings: 20, - oveflowFraction: 0.03, - - gaugeColors: [ + oveflowFraction: .03, + + gaugeMiniColors: [ {value: 0, type: 'min', hex: InfluxColors.Krypton}, {value: 100, type: 'max', hex: InfluxColors.Topaz}, ] as Color[], colorSecondary: InfluxColors.Kevlar, - + labelMain: '', labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, - + labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, - + valuePadding: 5, valueFontSize: 18, valueFontColorInside: InfluxColors.Raven, valueFontColorOutside: InfluxColors.Cloud, - valueFormater: (val: number) => val.toFixed(0), - + valueFormater: {}, + axesSteps: undefined as any, axesFontSize: 11, axesFontColor: InfluxColors.Forge, - axesFormater: (val: number) => val.toFixed(0), + axesFormater: {}, } ``` From 09675d137118736e19e1c75b0ca05e323b9086b8 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Dec 2020 23:48:34 +0100 Subject: [PATCH 10/15] fix: autocenter fixed --- giraffe/src/components/GaugeMini.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index c3f2dc2f..2c9d7559 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -476,6 +476,8 @@ export const GaugeMini: FunctionComponent = ({ labelBarsFontColor, labelBarsFontSize, colors, + axesSteps, + axesFontSize, } = theme const [barLabelsWidth] = useState([]) @@ -501,7 +503,19 @@ export const GaugeMini: FunctionComponent = ({ const [autocenterToken, setAutocenterToken] = useState(0) useEffect(() => { setAutocenterToken(x => x + 1) - }, [barLabelWidth, sidePaddings, valueHeight, width, height]) + }, [ + width, + height, + barLabelWidth, + barsDefinitions, + valueHeight, + gaugeHeight, + barPaddings, + sidePaddings, + labelMainFontSize, + axesSteps, + axesFontSize, + ]) /** return value as fraction 0->min 1->max */ const getFrac = (val: number): number => (val - colors.min.value) / colorLen From be8ed845a73adc26cc58cacae7fab0978d678772 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 30 Dec 2020 00:18:53 +0100 Subject: [PATCH 11/15] style: types fixed --- giraffe/src/components/SizedTable.tsx | 9 +++------ giraffe/src/constants/gaugeMiniStyles.ts | 2 +- stories/src/gaugeMini.stories.tsx | 4 +--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index 10fb09bd..e422192a 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -2,7 +2,6 @@ import React, {FunctionComponent, CSSProperties} from 'react' import { GaugeLayerConfig, - GaugeMiniLayerConfig, SizedConfig, TableGraphLayerConfig, LayerTypes, @@ -89,16 +88,14 @@ export const SizedTable: FunctionComponent = ({ key={layerIndex} table={newTableFromConfig(config)} columns={ - getGaugeMiniBarsDefinitions( - (layerConfig as Required) - .barsDefinitions - ).groupByColumns + getGaugeMiniBarsDefinitions(layerConfig.barsDefinitions) + .groupByColumns } > {latestValues => ( } + theme={layerConfig} /> )} diff --git a/giraffe/src/constants/gaugeMiniStyles.ts b/giraffe/src/constants/gaugeMiniStyles.ts index 5013ab05..90d22373 100644 --- a/giraffe/src/constants/gaugeMiniStyles.ts +++ b/giraffe/src/constants/gaugeMiniStyles.ts @@ -75,7 +75,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { valueFontColorOutside: InfluxColors.Cloud, valueFormater: {}, - axesSteps: undefined as any, + axesSteps: undefined, axesFontSize: 11, axesFontColor: InfluxColors.Forge, axesFormater: {}, diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index b1bf0937..45820dd8 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -13,9 +13,7 @@ import { GAUGE_MINI_THEME_BULLET_DARK, GAUGE_MINI_THEME_PROGRESS_DARK, } from '../../giraffe/src/constants/gaugeMiniStyles' -// import {gaugeTable as gaugeMiniTable} from './data/gaugeLayer' -import {gaugeMiniTable, gaugeMiniTableGetField} from './data/gaugeMiniLayer' -import {range} from 'd3-array' +import {gaugeMiniTable} from './data/gaugeMiniLayer' type Theme = Required From 9deb5b45aa3a2e6760f67f94f8d978d3e8fddeb3 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 19 Jan 2021 12:57:21 +0100 Subject: [PATCH 12/15] fix: autocenter cyclic refresh fixed --- giraffe/src/components/GaugeMini.tsx | 8 +++--- giraffe/src/utils/gaugeMiniThemeNormalize.ts | 28 +++++++++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index 2c9d7559..f9d43db6 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -6,7 +6,7 @@ import {scaleLinear} from 'd3-scale' // Types import {GaugeMiniColors, GaugeMiniLayerConfig} from '../types' import { - gaugeMiniNormalizeTheme, + gaugeMiniNormalizeThemeMemoized, GaugeMiniThemeNormalized, } from '../utils/gaugeMiniThemeNormalize' @@ -463,7 +463,7 @@ export const GaugeMini: FunctionComponent = ({ width, height, }) => { - const theme = gaugeMiniNormalizeTheme(_theme) + const theme = gaugeMiniNormalizeThemeMemoized(_theme) const { gaugeHeight, @@ -500,9 +500,9 @@ export const GaugeMini: FunctionComponent = ({ labelMapping[mstring] = x.label }) - const [autocenterToken, setAutocenterToken] = useState(0) + const [autocenterToken, setAutocenterToken] = useState(Date.now()) useEffect(() => { - setAutocenterToken(x => x + 1) + setAutocenterToken(Date.now()) }, [ width, height, diff --git a/giraffe/src/utils/gaugeMiniThemeNormalize.ts b/giraffe/src/utils/gaugeMiniThemeNormalize.ts index 6bca02f1..3fcca275 100644 --- a/giraffe/src/utils/gaugeMiniThemeNormalize.ts +++ b/giraffe/src/utils/gaugeMiniThemeNormalize.ts @@ -7,6 +7,7 @@ import { import {GaugeMiniLayerConfig} from '..' import {color as d3Color} from 'd3-color' import {range} from 'd3-array' +import {useMemo} from 'react' export const throwReturn = (msg: string): T => { throw new Error(msg) @@ -104,9 +105,9 @@ const getAxesSteps = ( ) } -const getColors = (theme: Required): GaugeMiniColors => { - const {gaugeMiniColors: colors} = theme - +const getColors = ( + colors: Required['gaugeMiniColors'] +): GaugeMiniColors => { if (!Array.isArray(colors)) { return { ...colors, @@ -136,17 +137,30 @@ const getColors = (theme: Required): GaugeMiniColors => { } // todo: more validations -export const gaugeMiniNormalizeTheme = ( +export const gaugeMiniNormalizeThemeMemoized = ( theme: Required ): Required => { - const colors = getColors(theme) + const barsDefinitions = useMemo( + () => getBarsDefinitions(theme.barsDefinitions), + [theme.barsDefinitions] + ) + + const colors = useMemo(() => getColors(theme.gaugeMiniColors), [ + theme.gaugeMiniColors, + ]) + + const axesSteps = useMemo(() => getAxesSteps(theme.axesSteps, colors), [ + theme.axesSteps, + colors, + ]) + return { ...theme, - barsDefinitions: getBarsDefinitions(theme.barsDefinitions), + barsDefinitions, colors, valueFormater: getFormater(theme.valueFormater), - axesSteps: getAxesSteps(theme.axesSteps, colors), + axesSteps, axesFormater: getFormater(theme.axesFormater), } } From 70c89080bdcbe4559fd3001d269deb18f168868b Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 20 Jan 2021 08:09:54 +0100 Subject: [PATCH 13/15] feat: stories missing knobs added --- giraffe/README.md | 2 +- giraffe/src/utils/gaugeMiniThemeNormalize.ts | 3 +- stories/src/gaugeMini.stories.tsx | 254 ++++++++++++++++--- 3 files changed, 221 insertions(+), 38 deletions(-) diff --git a/giraffe/README.md b/giraffe/README.md index ffc0b9cb..55d068a5 100644 --- a/giraffe/README.md +++ b/giraffe/README.md @@ -713,7 +713,7 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **groupByColumns** _string[]_ bar for each unique combination of given columns values. _(example: `['cpu', '_field', ]`)_ - **bars** _{ barDef: string[], label?: string }[]_ where _barDef_ contains values for specific bar columns and label for this bar. Each barDef value belongs to key grom groupByColumns with same index. - - **mode**: _'progress' | 'bullet'._ + - **mode** _'progress' | 'bullet'._ - `'bullet'` backgroud bar is colored and value bar has always secondary color - `'progress'` value bar is colored and backgroud bar has always secondary color diff --git a/giraffe/src/utils/gaugeMiniThemeNormalize.ts b/giraffe/src/utils/gaugeMiniThemeNormalize.ts index 3fcca275..5a1dd663 100644 --- a/giraffe/src/utils/gaugeMiniThemeNormalize.ts +++ b/giraffe/src/utils/gaugeMiniThemeNormalize.ts @@ -18,6 +18,7 @@ type TBarsDefinitions = GaugeMiniBarsDefinitions<{[key: string]: true}> type RestrictedTypesProperties = { barsDefinitions: TBarsDefinitions colors?: GaugeMiniColors + gaugeMiniColors?: GaugeMiniColors valueFormater?: (value: number) => string axesSteps: undefined | number[] @@ -136,7 +137,6 @@ const getColors = ( } } -// todo: more validations export const gaugeMiniNormalizeThemeMemoized = ( theme: Required ): Required => { @@ -158,6 +158,7 @@ export const gaugeMiniNormalizeThemeMemoized = ( ...theme, barsDefinitions, colors, + gaugeMiniColors: colors, valueFormater: getFormater(theme.valueFormater), axesSteps, diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index 45820dd8..f44d6b46 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import {storiesOf} from '@storybook/react' -import {withKnobs, number, select, text} from '@storybook/addon-knobs' +import {withKnobs, number, select, boolean, text} from '@storybook/addon-knobs' import { Config, GaugeMiniLayerConfig, @@ -14,8 +14,16 @@ import { GAUGE_MINI_THEME_PROGRESS_DARK, } from '../../giraffe/src/constants/gaugeMiniStyles' import {gaugeMiniTable} from './data/gaugeMiniLayer' +import { + gaugeMiniNormalizeThemeMemoized, + GaugeMiniThemeNormalized, +} from '../../giraffe/src/utils/gaugeMiniThemeNormalize' +import {range} from 'd3-array' +import {FormatStatValueOptions} from '../../giraffe/src/utils/formatStatValue' type Theme = Required +type ThemeNormalized = Required +type Color = ThemeNormalized['gaugeMiniColors']['min'] const color = (() => { const colors = (() => { @@ -24,11 +32,74 @@ const color = (() => { return obj })() - // todo: type definitions return (label: string, ...rest: any[]) => select(label, colors, ...rest) })() -const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ +const randInt: { + (max: number): number + (min: number, max: number): number +} = (min: number, max?: number) => { + if (max === undefined) return Math.floor(Math.random() * min) + return min + Math.floor(Math.random() * (max - min)) +} + +const colorRandom = () => { + const colors = Object.values(InfluxColors) + return colors[randInt(colors.length)] +} + +const colorHexValue = ( + label: string, + colorDefault: Color, + groupID: string, + numberOptions: any = {} +): Color => { + return { + hex: color(label + ' - hex', colorDefault?.hex ?? '#888888', groupID), + value: number( + label + ' - value', + colorDefault?.value ?? 50, + numberOptions, + groupID + ), + } +} + +const formatStatValueOptions = ( + label: string, + formDefault: FormatStatValueOptions, + groupID: string +): FormatStatValueOptions => { + const l = (sublabel: string) => label + ' - ' + sublabel + return { + decimalPlaces: { + isEnforced: boolean( + l('decimalPlaces - isEnforced'), + formDefault?.decimalPlaces?.isEnforced ?? false, + groupID + ), + digits: number( + l('decimalPlaces - digits'), + formDefault?.decimalPlaces?.digits ?? 2, + {}, + groupID + ), + }, + prefix: text(l('prefix'), formDefault?.prefix, groupID), + suffix: text(l('suffix'), formDefault?.suffix, groupID), + } +} + +const knobGroups = { + basics: 'basics', + sizing: 'sizing', + colors: 'colors', + labels: 'labels', +} + +const editableLayer = ( + theme: ThemeNormalized +): Theme & {numberOfBars: number} => ({ type: theme.type, mode: select( 'Mode', @@ -36,7 +107,8 @@ const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ bullet: 'bullet', progress: 'progress', }, - theme.mode + theme.mode, + knobGroups.basics ), textMode: select( 'textMode', @@ -44,44 +116,144 @@ const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ follow: 'follow', left: 'left', }, - theme.textMode + theme.textMode, + knobGroups.basics ), - numberOfBars: number('number of bars', 1), + numberOfBars: number('number of bars', 1, {}, knobGroups.basics), barsDefinitions: { groupByColumns: {_field: true}, }, - valueHeight: number('valueHeight', theme.valueHeight), - gaugeHeight: number('gaugeHeight', theme.gaugeHeight), - valueRounding: number('valueRounding', theme.valueRounding), - gaugeRounding: number('gaugeRounding', theme.gaugeRounding), - barPaddings: number('barPaddings', theme.barPaddings), - sidePaddings: number('gaugePaddingSides', theme.sidePaddings), - oveflowFraction: number('oveflowFraction', theme.oveflowFraction), + valueHeight: number('valueHeight', theme.valueHeight, {}, knobGroups.sizing), + gaugeHeight: number('gaugeHeight', theme.gaugeHeight, {}, knobGroups.sizing), + valueRounding: number( + 'valueRounding', + theme.valueRounding, + {}, + knobGroups.sizing + ), + gaugeRounding: number( + 'gaugeRounding', + theme.gaugeRounding, + {}, + knobGroups.sizing + ), + barPaddings: number('barPaddings', theme.barPaddings, {}, knobGroups.sizing), + sidePaddings: number( + 'gaugePaddingSides', + theme.sidePaddings, + {}, + knobGroups.sizing + ), + oveflowFraction: number( + 'oveflowFraction', + theme.oveflowFraction, + {}, + knobGroups.sizing + ), + + colorSecondary: color( + 'colorSecondary', + theme.colorSecondary, + knobGroups.colors + ), + gaugeMiniColors: (() => { + const {thresholds} = theme.gaugeMiniColors + const min = colorHexValue( + 'colorMin', + theme.gaugeMiniColors.min, + knobGroups.colors + ) + const max = colorHexValue( + 'colorMax', + theme.gaugeMiniColors.max, + knobGroups.colors + ) - // todo: add knobs - gaugeMiniColors: theme.gaugeMiniColors, - colorSecondary: color('colorSecondary', theme.colorSecondary), + const colors: Theme['gaugeMiniColors'] = { + min, + max, + thresholds: (() => { + const length = number( + 'thresholds number', + thresholds.length, + {}, + knobGroups.colors + ) - labelMain: text('labelMain', 'Gauge-mini example'), - labelMainFontSize: number('labelMainFontSize', theme.labelMainFontSize), - labelMainFontColor: color('labelMainFontColor', theme.labelMainFontColor), + return range(length).map(x => + colorHexValue( + `threshold ${x}`, + thresholds[x] || { + hex: colorRandom(), + value: randInt(min.value + 1, max.value), + }, + knobGroups.colors, + { + range: true, + min: min.value, + max: max.value, + step: 1, + } + ) + ) + })(), + } + return colors + })(), - labelBarsFontSize: number('labelBarsFontSize', theme.labelBarsFontSize), - labelBarsFontColor: color('labelBarsFontColor', theme.labelBarsFontColor), + labelMain: text('labelMain', 'Gauge-mini example', knobGroups.labels), + labelMainFontSize: number( + 'labelMainFontSize', + theme.labelMainFontSize, + {}, + knobGroups.labels + ), + labelMainFontColor: color( + 'labelMainFontColor', + theme.labelMainFontColor, + knobGroups.labels + ), - valuePadding: number('valuePadding', theme.valuePadding), - valueFontSize: number('valueFontSize', theme.valueFontSize), + labelBarsFontSize: number( + 'labelBarsFontSize', + theme.labelBarsFontSize, + {}, + knobGroups.labels + ), + labelBarsFontColor: color( + 'labelBarsFontColor', + theme.labelBarsFontColor, + knobGroups.labels + ), + + valuePadding: number( + 'valuePadding', + theme.valuePadding, + {}, + knobGroups.labels + ), + valueFontSize: number( + 'valueFontSize', + theme.valueFontSize, + {}, + knobGroups.labels + ), valueFontColorInside: color( 'valueFontColorInside', - theme.valueFontColorInside + theme.valueFontColorInside, + knobGroups.labels ), valueFontColorOutside: color( 'valueFontColorOutside', - theme.valueFontColorOutside + theme.valueFontColorOutside, + knobGroups.labels + ), + valueFormater: formatStatValueOptions( + 'valueFormater', + theme.valueFormater as any, + knobGroups.labels ), - // todo: add knobs - valueFormater: theme.valueFormater, axesSteps: select( 'axesSteps', @@ -92,16 +264,26 @@ const editableLayer = (theme: Theme): Theme & {numberOfBars: number} => ({ 2: 2, undefined: null, }, - theme.axesSteps + 'thresholds', + knobGroups.basics + ), + axesFontSize: number( + 'axesFontSize', + theme.axesFontSize, + {}, + knobGroups.labels + ), + axesFontColor: color('axesFontColor', theme.axesFontColor, knobGroups.labels), + axesFormater: formatStatValueOptions( + 'axesFormater', + theme.axesFormater as any, + knobGroups.labels ), - axesFontSize: number('axesFontSize', theme.axesFontSize), - axesFontColor: color('axesFontColor', theme.axesFontColor), - // todo: add knobs - axesFormater: theme.axesFormater, }) -const createStory = (theme: Theme) => () => { - const layer = editableLayer(theme) +const Story: React.FC<{theme: Theme}> = ({theme}) => { + const normalized = gaugeMiniNormalizeThemeMemoized(theme) + const layer = editableLayer(normalized) const config: Config = { table: gaugeMiniTable(0, 100, layer.numberOfBars), @@ -118,5 +300,5 @@ const createStory = (theme: Theme) => () => { storiesOf('Gauge mini', module) .addDecorator(withKnobs) - .add('Bullet', createStory(GAUGE_MINI_THEME_BULLET_DARK)) - .add('Progress', createStory(GAUGE_MINI_THEME_PROGRESS_DARK)) + .add('Bullet', () => ) + .add('Progress', () => ) From 4f1a9270bb875912e2b7d4370edcd2a151840042 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Mon, 25 Jan 2021 12:02:10 +0100 Subject: [PATCH 14/15] fix: removed bar grouping feature --- giraffe/README.md | 18 ++- giraffe/src/components/GaugeMini.tsx | 43 ++------ giraffe/src/components/GaugeMiniLayer.tsx | 4 +- .../LatestMultipleValueTransform.tsx | 104 ++++++------------ giraffe/src/components/SizedTable.tsx | 5 - giraffe/src/constants/gaugeMiniStyles.ts | 4 +- giraffe/src/types/index.ts | 18 +-- giraffe/src/utils/gaugeMiniThemeNormalize.ts | 49 +-------- stories/src/gaugeMini.stories.tsx | 8 +- 9 files changed, 62 insertions(+), 191 deletions(-) diff --git a/giraffe/README.md b/giraffe/README.md index 55d068a5..e29bd8ff 100644 --- a/giraffe/README.md +++ b/giraffe/README.md @@ -703,15 +703,9 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data All values (excluding **type**) are Optional and their defaults is defined by theme `GAUGE_MINI_THEME_BULLET_DARK` - - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. + gauge mini creates one bar per unique __field_ value - - **barsDefinitions** _{groupByColumns; bars;}_ _(types of properties based on selected style)_ - - Object style: - - **groupByColumns** _{ [key: string]: true }_ bar for each unique combination of given columns values. _(example: `{cpu: true, _field: true}`)_ - - **bars** _{ barDef: { [key in keyof T]: string }, label?: string }[]_ where _barDef_ contains values for specific bar columns and label for this bar. - - _or_ Array style: - - **groupByColumns** _string[]_ bar for each unique combination of given columns values. _(example: `['cpu', '_field', ]`)_ - - **bars** _{ barDef: string[], label?: string }[]_ where _barDef_ contains values for specific bar columns and label for this bar. Each barDef value belongs to key grom groupByColumns with same index. + - **type**: _'gauge mini'. **Required**._ Specifies that this LayerConfig is a gauge mini layer. - **mode** _'progress' | 'bullet'._ - `'bullet'` backgroud bar is colored and value bar has always secondary color @@ -763,6 +757,8 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **labelMainFontColor** _string_ Main label color. Bar labels + - **labelBarsEnabled** _boolean_ Bar labels shown if true + - **labelBarsFontSize** _number_ Bar labels font size - **labelBarsFontColor** _string_ Bar labels font color @@ -798,7 +794,7 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data - **isEnforced**: _boolean. Optional. Defaults to false when not included._ Indicates whether the number of decimal places ("**digits**") will be enforced. When **isEnforced** is falsy or omitted, **digits** will be locked to 2 for stat values with a decimal and 0 for stat values that are integers, and the **digits** option will be ignored. - **digits**: _number. Optional. Defaults to 0 when not included. Maximum 10._ When **digits** is a non-integer number, the decimal portion is ignored. Represents the number of decimal places to display in the stat value. Displayed stat value is subject to rounding. - - example ```valueFormater: (num: number) => `${num.toFixed(0)}%` ``` for _value=23.213_ will show text value _23%_. + - example ```valueFormater: (num: number) => `${((num || 0) * 100).toFixed(0)}%` ``` for _value=0.23213_ will show text value _23%_. **Precreated themes** - `GAUGE_MINI_THEME_BULLET_DARK` @@ -807,7 +803,6 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data type: 'gauge mini', mode: 'bullet', textMode: 'follow', - barsDefinitions: {groupByColumns: ["_field"]}, valueHeight: 18, gaugeHeight: 25, @@ -829,6 +824,7 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, + labelBarsEnabled: false, labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, @@ -850,7 +846,6 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data type: 'gauge mini', mode: 'progress', textMode: 'follow', - barsDefinitions: {groupByColumns: ['_field']}, valueHeight: 20, gaugeHeight: 20, @@ -870,6 +865,7 @@ TableGraphLayerConfig uses the `fluxResponse` property from `config` as the data labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, + labelBarsEnabled: false, labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, diff --git a/giraffe/src/components/GaugeMini.tsx b/giraffe/src/components/GaugeMini.tsx index f9d43db6..843d9cc5 100644 --- a/giraffe/src/components/GaugeMini.tsx +++ b/giraffe/src/components/GaugeMini.tsx @@ -5,6 +5,8 @@ import {scaleLinear} from 'd3-scale' // Types import {GaugeMiniColors, GaugeMiniLayerConfig} from '../types' +import {GroupedData} from './LatestMultipleValueTransform' + import { gaugeMiniNormalizeThemeMemoized, GaugeMiniThemeNormalized, @@ -13,28 +15,10 @@ import { interface Props { width: number height: number - values: {colsMString: string; value: number}[] + values: GroupedData theme: Required } -// todo: move into gauge utils -/** create merged string for given column string values. String is same for all columns with same values and unique for different ones */ -export const createColsMString = ( - groupedBy: T, - col: {[key in keyof T]: string} -): string => { - const columns = Object.keys(groupedBy) - .filter(x => groupedBy[x]) - .sort() - const columnValues = columns.map(x => col[x]) - /** - * replacing - with -- will ensures that rows - * { a: '0-1', b: '2' } and { a: '0', b: '1-2' } - * will not have same string (0-1-2 instead they will be 0--1-2 and 0-1--2) - */ - return columnValues.map(x => x.split('-').join('--')).join('-') -} - const barCssClass = 'gauge-mini-bar' //#region svg helpers @@ -473,6 +457,7 @@ export const GaugeMini: FunctionComponent = ({ labelMain, labelMainFontSize, labelMainFontColor, + labelBarsEnabled, labelBarsFontColor, labelBarsFontSize, colors, @@ -485,21 +470,11 @@ export const GaugeMini: FunctionComponent = ({ const barLabelWidth = Math.max(...barLabelsWidth) || 0 const barWidth = width - sidePaddings * 2 - barLabelWidth const maxBarHeight = Math.max(gaugeHeight, valueHeight) - const allBarsHeight = values.length * (maxBarHeight + barPaddings) - - const barsDefinitions = theme.barsDefinitions + const allBarsHeight = + Object.keys(values).length * (maxBarHeight + barPaddings) // create unified barsDefinition - const labelMapping: any = {} - barsDefinitions?.bars?.forEach(x => { - if (!x.label) { - return - } - const mstring = createColsMString(barsDefinitions.groupByColumns, x.barDef) - labelMapping[mstring] = x.label - }) - const [autocenterToken, setAutocenterToken] = useState(Date.now()) useEffect(() => { setAutocenterToken(Date.now()) @@ -507,7 +482,6 @@ export const GaugeMini: FunctionComponent = ({ width, height, barLabelWidth, - barsDefinitions, valueHeight, gaugeHeight, barPaddings, @@ -536,11 +510,10 @@ export const GaugeMini: FunctionComponent = ({ {labelMain} )} - {values.map(({colsMString, value}, i) => { + {Object.entries(values).map(([group, value], i) => { const y = 0 + i * (maxBarHeight + barPaddings) - const label = labelMapping?.[colsMString] - const textCenter = y + maxBarHeight / 2 + const label = labelBarsEnabled ? group : '' // todo: no rerender ? const onRectChanged = (r: DOMRect) => { diff --git a/giraffe/src/components/GaugeMiniLayer.tsx b/giraffe/src/components/GaugeMiniLayer.tsx index a6eb7bc7..d88f9d47 100644 --- a/giraffe/src/components/GaugeMiniLayer.tsx +++ b/giraffe/src/components/GaugeMiniLayer.tsx @@ -7,12 +7,14 @@ import {GaugeMiniLayerConfig} from '../types' import {GaugeMini} from './GaugeMini' import {GAUGE_MINI_THEME_BULLET_DARK} from '../constants/gaugeMiniStyles' +import {GroupedData} from './LatestMultipleValueTransform' interface Props { - values: {colsMString: string; value: number}[] + values: GroupedData theme: GaugeMiniLayerConfig } +// todo: move gauge mini here export const GaugeMiniLayer: FunctionComponent = (props: Props) => { const {theme, values} = props const themeOrDefault: Required = { diff --git a/giraffe/src/components/LatestMultipleValueTransform.tsx b/giraffe/src/components/LatestMultipleValueTransform.tsx index fc36890d..a5ce197a 100644 --- a/giraffe/src/components/LatestMultipleValueTransform.tsx +++ b/giraffe/src/components/LatestMultipleValueTransform.tsx @@ -1,34 +1,10 @@ // Libraries import React, {useMemo, FunctionComponent} from 'react' import {Table} from '../types' -import {createColsMString} from './GaugeMini' -interface SelectedColumns { - [key: string]: true -} - -export const getLatestValuesGrouped = ( - table: Table, - columnsObj: SelectedColumns -) => { - const columns = Object.keys(columnsObj).sort() - - columns.forEach(x => { - if (table.getColumnType(x) !== 'string') { - throw new Error( - `Data can be grouped only by string columns. But column ${x} is typeof ${table.getColumnType( - x - )}` - ) - } - }) - - const valueColumn = table.getColumn('_value', 'number') as number[] - - if (!valueColumn.length) { - return [] - } +export type GroupedData = {[key: string]: number} +const getTimeColumn = (table: Table) => { // Fallback to `_stop` column if `_time` column missing otherwise return empty array. let timeColData: number[] = [] @@ -41,73 +17,61 @@ export const getLatestValuesGrouped = ( return [] } - const groupColsData = columns.map(k => - table.getColumn(k, 'string') - ) as string[][] + return timeColData +} - const result: {[key: string]: number} = {} +export const getLatestValuesGrouped = ( + table: Table, + groupColumnKey: string +): GroupedData => { + const valueCol = table.getColumn('_value', 'number') + const groupCol = table.getColumn(groupColumnKey) - timeColData - // merge time with it's index - .map((time, index) => ({time, index})) - // remove entries without time - .filter(({time}) => time) - // todo: sort time complexity too high ... replace with another solution - // from low time to high time (last is last) - .sort(({time: t1}, {time: t2}) => t1 - t2) - // get relevant data from index (we don't need time anymore) - .map(({index}) => ({ - value: valueColumn[index], - groupRow: groupColsData.map(x => x[index]), - })) - // remove invalid data - .filter(({value}) => Number.isFinite(value) && typeof value === 'number') - // create result - .forEach(({value, groupRow}) => { - const grupObj = {} - groupRow.forEach((x, i) => (grupObj[columns[i]] = x)) - const strKey = createColsMString(columnsObj, grupObj) - // data is inserted from last to first so latest data is first - result[strKey] = value - }) + if (!valueCol.length) { + return {} + } - return result + return Object.fromEntries( + getTimeColumn(table) + // merge time with it's index + .map((time, index) => ({time, index})) + // remove entries without time + .filter(({time}) => time) + // todo: sort time complexity too high ... replace with linear solution + // from low time to high time (last is last) + .sort(({time: t1}, {time: t2}) => t1 - t2) + // get relevant data from index (we don't need time anymore) + .map(({index}) => [groupCol?.[index] ?? '', valueCol[index]] as const) + // remove invalid data + .filter( + ([_, value]) => typeof value === 'number' && Number.isFinite(value) + ) + ) } interface Props { table: Table - columns: SelectedColumns - children: (latestValue: {colsMString: string; value: number}[]) => JSX.Element + children: (latestValue: GroupedData) => JSX.Element quiet?: boolean } // todo: can return string ? export const LatestMultipleValueTransform: FunctionComponent = ({ table, - columns, quiet = false, children, }) => { - const latestValues = useMemo(() => getLatestValuesGrouped(table, columns), [ + const latestValues = useMemo(() => getLatestValuesGrouped(table, '_field'), [ table, ]) - if (latestValues.length === 0 && quiet) { - return null - } - - if (latestValues.length === 0) { - return ( + if (Object.keys(latestValues).length === 0) { + return quiet ? null : (

No latest value found

) } - const entries = Object.keys(latestValues).map(x => ({ - colsMString: x, - value: latestValues[x], - })) - - return children(entries) + return children(latestValues) } diff --git a/giraffe/src/components/SizedTable.tsx b/giraffe/src/components/SizedTable.tsx index e422192a..7beddfd8 100644 --- a/giraffe/src/components/SizedTable.tsx +++ b/giraffe/src/components/SizedTable.tsx @@ -17,7 +17,6 @@ import {TableGraphLayer} from './TableGraphLayer' import {usePlotEnv} from '../utils/usePlotEnv' import {LatestMultipleValueTransform} from './LatestMultipleValueTransform' -import {getGaugeMiniBarsDefinitions} from '../utils/gaugeMiniThemeNormalize' interface Props { config: SizedConfig @@ -87,10 +86,6 @@ export const SizedTable: FunctionComponent = ({ {latestValues => ( = { type: 'gauge mini', mode: 'bullet', textMode: 'follow', - barsDefinitions: {groupByColumns: ['_field']}, valueHeight: 18, gaugeHeight: 25, @@ -27,6 +26,7 @@ export const GAUGE_MINI_THEME_BULLET_DARK: Required = { labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, + labelBarsEnabled: false, labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, @@ -46,7 +46,6 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { type: 'gauge mini', mode: 'progress', textMode: 'follow', - barsDefinitions: {groupByColumns: ['_field']}, valueHeight: 20, gaugeHeight: 20, @@ -66,6 +65,7 @@ export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { labelMainFontSize: 13, labelMainFontColor: InfluxColors.Ghost, + labelBarsEnabled: false, labelBarsFontSize: 11, labelBarsFontColor: InfluxColors.Forge, diff --git a/giraffe/src/types/index.ts b/giraffe/src/types/index.ts index 7faf123f..4142aa51 100644 --- a/giraffe/src/types/index.ts +++ b/giraffe/src/types/index.ts @@ -290,25 +290,8 @@ export type GaugeMiniColors = { thresholds?: ColorHexValue[] } -export interface GaugeMiniBarsDefinitions { - /** Defines which columns choose as unique bar indentificator. */ - groupByColumns: T - // todo: allow regex ? - /** Give label for given unique column values */ - bars?: {barDef: {[key in keyof T]: string}; label?: string}[] -} - -export interface GaugeMiniBarsDefinitionsArr { - groupByColumns: string[] - bars?: {barDef: string[]; label?: string}[] -} - export interface GaugeMiniLayerConfig { type: 'gauge mini' - /** Defines which columns choose as unique bar indentificator. Also bar labels can be defined here. */ - barsDefinitions: - | GaugeMiniBarsDefinitionsArr - | GaugeMiniBarsDefinitions<{[key: string]: true}> mode?: 'progress' | 'bullet' textMode?: 'follow' | 'left' @@ -327,6 +310,7 @@ export interface GaugeMiniLayerConfig { labelMainFontSize?: number labelMainFontColor?: string + labelBarsEnabled?: boolean labelBarsFontSize?: number labelBarsFontColor?: string diff --git a/giraffe/src/utils/gaugeMiniThemeNormalize.ts b/giraffe/src/utils/gaugeMiniThemeNormalize.ts index 5a1dd663..3b6572cb 100644 --- a/giraffe/src/utils/gaugeMiniThemeNormalize.ts +++ b/giraffe/src/utils/gaugeMiniThemeNormalize.ts @@ -1,22 +1,15 @@ +import {useMemo} from 'react' import {FormatStatValueOptions, formatStatValue} from './formatStatValue' -import { - GaugeMiniBarsDefinitions, - GaugeMiniColors, - GaugeMiniBarsDefinitionsArr, -} from '../types' +import {GaugeMiniColors} from '../types' import {GaugeMiniLayerConfig} from '..' import {color as d3Color} from 'd3-color' import {range} from 'd3-array' -import {useMemo} from 'react' export const throwReturn = (msg: string): T => { throw new Error(msg) } -type TBarsDefinitions = GaugeMiniBarsDefinitions<{[key: string]: true}> - type RestrictedTypesProperties = { - barsDefinitions: TBarsDefinitions colors?: GaugeMiniColors gaugeMiniColors?: GaugeMiniColors valueFormater?: (value: number) => string @@ -38,36 +31,6 @@ const getFormater = ( ? formater : (value: number) => formatStatValue(value, formater) -const isBarsDefinitionsArrayStyle = ( - barsDefinitions: GaugeMiniLayerConfig['barsDefinitions'] -): barsDefinitions is GaugeMiniBarsDefinitionsArr => { - return Array.isArray(barsDefinitions.groupByColumns) -} - -const getBarsDefinitions = ( - barsDefinitions: GaugeMiniLayerConfig['barsDefinitions'] -): TBarsDefinitions => { - if (!isBarsDefinitionsArrayStyle(barsDefinitions)) { - return barsDefinitions - } - - const {groupByColumns, bars} = barsDefinitions - - return { - groupByColumns: groupByColumns.reduce( - (obj, prop) => ((obj[prop] = true), obj), - {} as {[key: string]: true} - ), - bars: bars?.map(x => ({ - barDef: x.barDef.reduce( - (obj, prop, i) => ((obj[prop] = groupByColumns[i]), obj), - {} as Required['bars'][number]['barDef'] - ), - label: x.label, - })), - } -} - const getAxesSteps = ( axesSteps: number | 'thresholds' | undefined | number[], colors: GaugeMiniColors @@ -140,11 +103,6 @@ const getColors = ( export const gaugeMiniNormalizeThemeMemoized = ( theme: Required ): Required => { - const barsDefinitions = useMemo( - () => getBarsDefinitions(theme.barsDefinitions), - [theme.barsDefinitions] - ) - const colors = useMemo(() => getColors(theme.gaugeMiniColors), [ theme.gaugeMiniColors, ]) @@ -156,7 +114,6 @@ export const gaugeMiniNormalizeThemeMemoized = ( return { ...theme, - barsDefinitions, colors, gaugeMiniColors: colors, valueFormater: getFormater(theme.valueFormater), @@ -165,5 +122,3 @@ export const gaugeMiniNormalizeThemeMemoized = ( axesFormater: getFormater(theme.axesFormater), } } - -export const getGaugeMiniBarsDefinitions = getBarsDefinitions diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index f44d6b46..fd34be73 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -120,9 +120,6 @@ const editableLayer = ( knobGroups.basics ), numberOfBars: number('number of bars', 1, {}, knobGroups.basics), - barsDefinitions: { - groupByColumns: {_field: true}, - }, valueHeight: number('valueHeight', theme.valueHeight, {}, knobGroups.sizing), gaugeHeight: number('gaugeHeight', theme.gaugeHeight, {}, knobGroups.sizing), @@ -215,6 +212,11 @@ const editableLayer = ( knobGroups.labels ), + labelBarsEnabled: boolean( + 'labelBarsEnabled', + theme.labelBarsEnabled, + knobGroups.labels + ), labelBarsFontSize: number( 'labelBarsFontSize', theme.labelBarsFontSize, From 536a63bf7ba7abb25dbe884e615b566cea30ea56 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Mon, 1 Mar 2021 11:31:26 +0100 Subject: [PATCH 15/15] style: prettier fix --- stories/src/gaugeMini.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stories/src/gaugeMini.stories.tsx b/stories/src/gaugeMini.stories.tsx index fd34be73..d81e284d 100644 --- a/stories/src/gaugeMini.stories.tsx +++ b/stories/src/gaugeMini.stories.tsx @@ -39,7 +39,9 @@ const randInt: { (max: number): number (min: number, max: number): number } = (min: number, max?: number) => { - if (max === undefined) return Math.floor(Math.random() * min) + if (max === undefined) { + return Math.floor(Math.random() * min) + } return min + Math.floor(Math.random() * (max - min)) }