Commit 8894e285 by Torkel Ödegaard Committed by GitHub

StatPanel: ColorMode, GraphMode & JustifyMode changes (#20680)

* StatPanel: Options rethink

* Changed options to string based

* -Fixed tests

* Refactoring moving files

* Refactoring alignment factors

* Added alignment factors

* Added basic test

* Added unit test for layout

* Font size handling

* Font sizing works

* Progress on sizing

* Updated

* Minor update

* Updated

* Updated

* Removed line option

* updated

* Updated

* Updated

* Updated

* Highlight last point

* Fixed tests

* Code refactoring and cleanup

* updated

* Updated snapshot
parent fcde26e2
......@@ -20,103 +20,15 @@
{
"datasource": null,
"gridPos": {
"h": 7,
"w": 20,
"h": 2,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 2,
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"mappings": [],
"max": 100,
"min": 0,
"thresholds": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "purple",
"value": 20
},
{
"color": "orange",
"value": 40
},
{
"color": "red",
"value": 80
}
],
"unit": "percent"
},
"override": {},
"values": false
},
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"type": "stat"
},
{
"datasource": null,
"gridPos": {
"h": 20,
"w": 4,
"x": 20,
"y": 0
},
"id": 8,
"id": 6,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 2,
"colorMode": "background",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -150,60 +62,64 @@
"override": {},
"values": false
},
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"alias": "A longer title",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "AB",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "CPU",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Google",
"labels": "",
"refId": "E",
"scenarioId": "random_walk"
},
{
"alias": "Even longer road title",
"refId": "F",
"scenarioId": "random_walk"
},
{
"alias": "Why does it have to",
"refId": "G",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"title": "",
"type": "stat"
},
{
"datasource": null,
"gridPos": {
"h": 3,
"w": 20,
"h": 5,
"w": 24,
"x": 0,
"y": 7
"y": 2
},
"id": 6,
"id": 10,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 2,
"colorMode": "background",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -232,43 +148,48 @@
"value": 80
}
],
"unit": "percent"
"unit": "areaM2"
},
"override": {},
"values": false
},
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"alias": "AB",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "CPU",
"labels": "",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Longer title",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Even longer title",
"refId": "E",
"scenarioId": "random_walk"
},
{
"alias": "Outside",
"refId": "F",
"scenarioId": "random_walk"
},
{
"alias": "Inside",
"refId": "G",
"scenarioId": "random_walk"
}
......@@ -281,16 +202,15 @@
{
"datasource": null,
"gridPos": {
"h": 6,
"w": 20,
"h": 5,
"w": 24,
"x": 0,
"y": 10
"y": 7
},
"id": 3,
"id": 11,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 3,
"colorMode": "value",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -324,22 +244,20 @@
"override": {},
"values": false
},
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
......@@ -363,21 +281,21 @@
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"transparent": true,
"type": "stat"
},
{
"datasource": null,
"gridPos": {
"h": 6,
"w": 20,
"h": 14,
"w": 6,
"x": 0,
"y": 16
"y": 12
},
"id": 4,
"id": 13,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 0,
"colorMode": "background",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -411,12 +329,14 @@
"override": {},
"values": false
},
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"refId": "A",
......@@ -455,16 +375,15 @@
{
"datasource": null,
"gridPos": {
"h": 21,
"h": 11,
"w": 4,
"x": 20,
"y": 20
"x": 6,
"y": 12
},
"id": 9,
"id": 8,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 0,
"colorMode": "background",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -498,12 +417,14 @@
"override": {},
"values": false
},
"graphMode": "line",
"justifyMode": "auto",
"orientation": "auto",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"refId": "A",
......@@ -542,16 +463,15 @@
{
"datasource": null,
"gridPos": {
"h": 6,
"w": 20,
"x": 0,
"y": 22
"h": 9,
"w": 8,
"x": 10,
"y": 12
},
"id": 5,
"id": 12,
"interval": "10m",
"options": {
"colorMode": 0,
"displayMode": 1,
"colorMode": "background",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
......@@ -585,12 +505,14 @@
"override": {},
"values": false
},
"orientation": "auto",
"graphMode": "line",
"justifyMode": "auto",
"orientation": "horizontal",
"sparkline": {
"show": true
}
},
"pluginVersion": "6.5.0-pre",
"pluginVersion": "6.6.0-pre",
"targets": [
{
"refId": "A",
......@@ -627,7 +549,7 @@
"type": "stat"
}
],
"schemaVersion": 20,
"schemaVersion": 21,
"style": "dark",
"tags": ["gdev", "panel-tests"],
"templating": {
......@@ -643,5 +565,5 @@
"timezone": "",
"title": "Panel Tests - Stat",
"uid": "jWWHNJpWz",
"version": 6
"version": 25
}
......@@ -7,7 +7,7 @@ import { FieldConfig, DataFrame, FieldType } from '../types/dataFrame';
import { InterpolateFunction } from '../types/panel';
import { DataFrameView } from '../dataframe/DataFrameView';
import { GraphSeriesValue } from '../types/graph';
import { DisplayValue } from '../types/displayValue';
import { DisplayValue, DisplayValueAlignmentFactors } from '../types/displayValue';
import { GrafanaTheme } from '../types/theme';
import { ReducerID, reduceField } from '../transformations/fieldReducer';
import { ScopedVars } from '../types/ScopedVars';
......@@ -21,6 +21,7 @@ export interface FieldDisplayOptions {
defaults: FieldConfig; // Use these values unless otherwise stated
override: FieldConfig; // Set these values regardless of the source
}
// TODO: use built in variables, same as for data links?
export const VAR_SERIES_NAME = '__series.name';
export const VAR_FIELD_NAME = '__field.name';
......@@ -278,3 +279,22 @@ export function getFieldProperties(...props: FieldConfig[]): FieldConfig {
}
return field;
}
export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): DisplayValueAlignmentFactors {
const info: DisplayValueAlignmentFactors = {
title: '',
text: '',
};
for (let i = 0; i < values.length; i++) {
const v = values[i].display;
if (v.text && v.text.length > info.text.length) {
info.text = v.text;
}
if (v.title && v.title.length > info.title.length) {
info.title = v.title;
}
}
return info;
}
......@@ -8,6 +8,15 @@ export interface DisplayValue {
fontSize?: string;
}
/**
* These represents the displau value with the longest title and text.
* Used to align widths and heights when displaying multiple DisplayValues
*/
export interface DisplayValueAlignmentFactors {
title: string;
text: string;
}
export type DecimalCount = number | null | undefined;
export interface DecimalInfo {
......
import { DisplayValue } from './displayValue';
import { Field } from './dataFrame';
export interface YAxis {
index: number;
min?: number;
......
// Library
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
import { Threshold, TimeSeriesValue, getActiveThreshold, DisplayValue } from '@grafana/data';
import {
Threshold,
TimeSeriesValue,
getActiveThreshold,
DisplayValue,
DisplayValueAlignmentFactors,
} from '@grafana/data';
// Utils
import { getColorFromHexRgbOrName } from '@grafana/data';
import { measureText } from '../../utils/measureText';
import { measureText, calculateFontSize } from '../../utils/measureText';
// Types
import { VizOrientation } from '@grafana/data';
......@@ -19,18 +25,6 @@ const TITLE_LINE_HEIGHT = 1.5;
const VALUE_LINE_HEIGHT = 1;
const VALUE_LEFT_PADDING = 10;
/**
* These values calculate the internal font sizes and
* placement. For consistent behavior across repeating
* panels, we can optionally pass in the maximum values.
*
* If performace becomes a problem, we can cache the results
*/
export interface BarGaugeAlignmentFactors {
title: string;
text: string;
}
export interface Props extends Themeable {
height: number;
width: number;
......@@ -43,7 +37,7 @@ export interface Props extends Themeable {
displayMode: 'basic' | 'lcd' | 'gradient';
onClick?: React.MouseEventHandler<HTMLElement>;
className?: string;
alignmentFactors?: BarGaugeAlignmentFactors;
alignmentFactors?: DisplayValueAlignmentFactors;
}
export class BarGauge extends PureComponent<Props> {
......@@ -537,14 +531,6 @@ function getValueStyles(
textWidth -= VALUE_LEFT_PADDING;
}
// calculate width in 14px
const textSize = measureText(value, 14);
// how much bigger than 14px can we make it while staying within our width constraints
const fontSizeBasedOnWidth = (textWidth / (textSize.width + 2)) * 14;
const fontSizeBasedOnHeight = height / VALUE_LINE_HEIGHT;
// final fontSize
valueStyles.fontSize = Math.min(fontSizeBasedOnHeight, fontSizeBasedOnWidth).toFixed(4) + 'px';
valueStyles.fontSize = calculateFontSize(value, textWidth, height, VALUE_LINE_HEIGHT) + 'px';
return valueStyles;
}
......@@ -27,7 +27,7 @@ exports[`BarGauge Render with basic options should render 1`] = `
"alignItems": "center",
"color": "#73BF69",
"display": "flex",
"fontSize": "175.0000px",
"fontSize": "175px",
"height": "300px",
"justifyContent": "flex-start",
"lineHeight": 1,
......
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import { BigValue, BigValueDisplayMode } from './BigValue';
import { BigValue, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
......@@ -16,20 +16,22 @@ const BigValueStories = storiesOf('UI/BigValue', module);
BigValueStories.addDecorator(withCenteredStory);
interface StoryOptions {
mode: BigValueDisplayMode;
colorMode: BigValueColorMode;
graphMode: BigValueGraphMode;
width?: number;
height?: number;
noSparkline?: boolean;
}
function addStoryForMode(options: StoryOptions) {
BigValueStories.add(`Mode: ${BigValueDisplayMode[options.mode]}`, () => {
BigValueStories.add(`Color: ${options.colorMode}`, () => {
const { value, title } = getKnobs();
return renderComponentWithTheme(BigValue, {
width: options.width || 400,
height: options.height || 300,
displayMode: options.mode,
colorMode: options.colorMode,
graphMode: options.graphMode,
value: {
text: value,
numeric: 5022,
......@@ -52,7 +54,5 @@ function addStoryForMode(options: StoryOptions) {
});
}
addStoryForMode({ mode: BigValueDisplayMode.Classic });
addStoryForMode({ mode: BigValueDisplayMode.Classic2 });
addStoryForMode({ mode: BigValueDisplayMode.Vibrant });
addStoryForMode({ mode: BigValueDisplayMode.Vibrant2 });
addStoryForMode({ colorMode: BigValueColorMode.Value, graphMode: BigValueGraphMode.Area });
addStoryForMode({ colorMode: BigValueColorMode.Background, graphMode: BigValueGraphMode.Line });
import React from 'react';
import { shallow } from 'enzyme';
import { BigValue, Props, BigValueDisplayMode } from './BigValue';
import { getTheme } from '../../themes/index';
import { BigValue, Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { getTheme } from '../../themes';
jest.mock('jquery', () => ({
plot: jest.fn(),
}));
const setup = (propOverrides?: object) => {
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
colorMode: BigValueColorMode.Background,
graphMode: BigValueGraphMode.Line,
height: 300,
width: 300,
displayMode: BigValueDisplayMode.Classic,
value: {
text: '25',
numeric: 25,
......@@ -20,7 +17,11 @@ const setup = (propOverrides?: object) => {
};
Object.assign(props, propOverrides);
return props;
}
const setup = (propOverrides?: object) => {
const props = getProps(propOverrides);
const wrapper = shallow(<BigValue {...props} />);
const instance = wrapper.instance() as BigValue;
......@@ -30,10 +31,11 @@ const setup = (propOverrides?: object) => {
};
};
describe('Render SingleStat with basic options', () => {
describe('BigValue', () => {
describe('Render with basic options', () => {
it('should render', () => {
const { wrapper } = setup();
expect(wrapper).toBeDefined();
// expect(wrapper).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BigValue Render with basic options should render 1`] = `
<div
style={
Object {
"alignItems": "center",
"background": "linear-gradient(120deg, rgb(66, 154, 67), rgb(111, 183, 87))",
"borderRadius": "3px",
"display": "flex",
"flexDirection": "row",
"height": "300px",
"padding": "12px",
"position": "relative",
"width": "300px",
}
}
>
<div
style={
Object {
"alignItems": "center",
"display": "flex",
"flexDirection": "column",
"flexGrow": 1,
"justifyContent": "center",
}
}
>
<div
style={
Object {
"color": "#EEE",
"fontSize": "230px",
"fontWeight": 500,
"lineHeight": 1.2,
"textShadow": "#333 0px 0px 1px",
}
}
>
25
</div>
</div>
</div>
`;
import React, { CSSProperties } from 'react';
import tinycolor from 'tinycolor2';
import { Chart, Geom, Guide } from 'bizcharts';
import { LayoutResult, LayoutType } from './styles';
import { BigValueSparkline, BigValueColorMode } from './BigValue';
const { DataMarker } = Guide;
export function renderGraph(layout: LayoutResult, sparkline?: BigValueSparkline) {
if (!sparkline || layout.type === LayoutType.WideNoChart || layout.type === LayoutType.StackedNoChart) {
return null;
}
const data = sparkline.data.map(values => {
return { time: values[0], value: values[1], name: 'A' };
});
const scales = {
time: {
type: 'time',
},
};
const chartStyles: CSSProperties = {
position: 'absolute',
};
// default to line graph
const geomRender = getGraphGeom(layout.colorMode);
if (layout.type === LayoutType.Wide) {
// Area chart
chartStyles.bottom = 0;
chartStyles.right = 0;
} else {
// need some top padding
chartStyles.width = `${layout.chartWidth}px`;
chartStyles.height = `${layout.chartHeight}px`;
chartStyles.bottom = 0;
chartStyles.right = 0;
chartStyles.left = 0;
chartStyles.right = 0;
}
return (
<Chart
height={layout.chartHeight}
width={layout.chartWidth}
data={data}
animate={false}
padding={[4, 0, 0, 0]}
scale={scales}
style={chartStyles}
>
{geomRender(layout, sparkline)}
</Chart>
);
}
function getGraphGeom(colorMode: BigValueColorMode) {
// background color mode
if (colorMode === BigValueColorMode.Background) {
return renderAreaGeomOnColoredBackground;
}
return renderClassicAreaGeom;
}
function renderAreaGeomOnColoredBackground(layout: LayoutResult, sparkline: BigValueSparkline) {
const lineColor = tinycolor(layout.valueColor)
.brighten(40)
.toRgbString();
const lineStyle: any = {
stroke: lineColor,
lineWidth: 2,
};
return (
<>
<Geom type="area" position="time*value" size={0} color="rgba(255,255,255,0.4)" style={lineStyle} shape="smooth" />
<Geom type="line" position="time*value" size={1} color={lineColor} style={lineStyle} shape="smooth" />
{highlightPoint(lineColor, sparkline)}
</>
);
}
function highlightPoint(lineColor: string, sparkline: BigValueSparkline) {
if (!sparkline.highlightIndex) {
return null;
}
const pointPos = sparkline.data[sparkline.highlightIndex];
return (
<Guide>
<DataMarker
top
position={pointPos}
lineLength={0}
display={{ point: true }}
style={{
point: {
color: lineColor,
stroke: lineColor,
r: 2,
},
}}
/>
</Guide>
);
}
function renderClassicAreaGeom(layout: LayoutResult, sparkline: BigValueSparkline) {
const lineStyle: any = {
opacity: 1,
fillOpacity: 1,
};
const fillColor = tinycolor(layout.valueColor)
.setAlpha(0.2)
.toRgbString();
lineStyle.stroke = layout.valueColor;
return (
<>
<Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />
<Geom type="line" position="time*value" size={1} color={layout.valueColor} style={lineStyle} shape="smooth" />
{highlightPoint('#EEE', sparkline)}
</>
);
}
/* function renderAreaGeom(layout: LayoutResult) { */
/* const lineStyle: any = { */
/* opacity: 1, */
/* fillOpacity: 1, */
/* }; */
/* */
/* const color1 = tinycolor(layout.valueColor) */
/* .darken(0) */
/* .spin(20) */
/* .toRgbString(); */
/* const color2 = tinycolor(layout.valueColor) */
/* .lighten(0) */
/* .spin(-20) */
/* .toRgbString(); */
/* */
/* const fillColor = `l (0) 0:${color1} 1:${color2}`; */
/* */
/* return <Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />; */
/* } */
import { Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { calculateLayout, LayoutType } from './styles';
import { getTheme } from '../../themes';
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
colorMode: BigValueColorMode.Background,
graphMode: BigValueGraphMode.Area,
height: 300,
width: 300,
value: {
text: '25',
numeric: 25,
},
sparkline: {
data: [
[10, 10],
[10, 10],
],
minX: 0,
maxX: 100,
},
theme: getTheme(),
};
Object.assign(props, propOverrides);
return props;
}
describe('BigValue styles', () => {
describe('calculateLayout', () => {
it('should auto select to stacked layout', () => {
const layout = calculateLayout(
getProps({
width: 300,
height: 300,
})
);
expect(layout.type).toBe(LayoutType.Stacked);
});
it('should auto select to wide layout', () => {
const layout = calculateLayout(
getProps({
width: 300,
height: 100,
})
);
expect(layout.type).toBe(LayoutType.Wide);
});
});
});
// Libraries
import { CSSProperties } from 'react';
import tinycolor from 'tinycolor2';
// Utils
import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data';
import { calculateFontSize } from '../../utils/measureText';
// Types
import { BigValueColorMode, BigValueGraphMode, Props, BigValueJustifyMode } from './BigValue';
const LINE_HEIGHT = 1.2;
export interface LayoutResult {
titleFontSize: number;
valueFontSize: number;
chartHeight: number;
chartWidth: number;
type: LayoutType;
width: number;
height: number;
colorMode: BigValueColorMode;
graphMode: BigValueGraphMode;
theme: GrafanaTheme;
valueColor: string;
panelPadding: number;
justifyCenter: boolean;
}
export enum LayoutType {
Stacked,
StackedNoChart,
Wide,
WideNoChart,
}
export function shouldJustifyCenter(props: Props) {
const { value, justifyMode } = props;
if (justifyMode === BigValueJustifyMode.Center) {
return true;
}
return (value.title ?? '').length === 0;
}
export function calculateLayout(props: Props): LayoutResult {
const { width, height, sparkline, colorMode, theme, value, graphMode, alignmentFactors } = props;
const useWideLayout = width / height > 2.5;
const valueColor = getColorFromHexRgbOrName(value.color || 'green', theme.type);
const justifyCenter = shouldJustifyCenter(props);
const panelPadding = height > 100 ? 12 : 8;
const titleToAlignTo = alignmentFactors ? alignmentFactors.title : value.title;
const valueToAlignTo = alignmentFactors ? alignmentFactors.text : value.text;
const maxTitleFontSize = 30;
const maxTextWidth = width - panelPadding * 2;
const maxTextHeight = height - panelPadding * 2;
let layoutType = LayoutType.Stacked;
let chartHeight = 0;
let chartWidth = 0;
let titleHeight = 0;
let titleFontSize = 0;
let valueFontSize = 14;
if (useWideLayout) {
// Detect auto wide layout type
layoutType = height > 50 && !!sparkline ? LayoutType.Wide : LayoutType.WideNoChart;
// Wide no chart mode
if (layoutType === LayoutType.WideNoChart) {
const valueWidthPercent = 0.3;
if (titleToAlignTo && titleToAlignTo.length > 0) {
// initial value size
valueFontSize = calculateFontSize(valueToAlignTo, maxTextWidth * valueWidthPercent, maxTextHeight, LINE_HEIGHT);
// How big can we make the title and still have it fit
titleFontSize = calculateFontSize(
titleToAlignTo,
maxTextWidth * 0.6,
maxTextHeight,
LINE_HEIGHT,
maxTitleFontSize
);
// make sure it's a bit smaller than valueFontSize
titleFontSize = Math.min(valueFontSize * 0.7, titleFontSize);
titleHeight = titleFontSize * LINE_HEIGHT;
} else {
// if no title wide
valueFontSize = calculateFontSize(valueToAlignTo, maxTextWidth, maxTextHeight, LINE_HEIGHT);
}
} else {
// wide with chart
const chartHeightPercent = 0.5;
const titleWidthPercent = 0.6;
const valueWidthPercent = 1 - titleWidthPercent;
const textHeightPercent = 0.4;
chartWidth = width;
chartHeight = height * chartHeightPercent;
if (titleToAlignTo && titleToAlignTo.length > 0) {
titleFontSize = calculateFontSize(
titleToAlignTo,
maxTextWidth * titleWidthPercent,
maxTextHeight * textHeightPercent,
LINE_HEIGHT,
maxTitleFontSize
);
titleHeight = titleFontSize * LINE_HEIGHT;
}
valueFontSize = calculateFontSize(
valueToAlignTo,
maxTextWidth * valueWidthPercent,
maxTextHeight * chartHeightPercent,
LINE_HEIGHT
);
}
} else {
// Stacked layout (title, value, chart)
const titleHeightPercent = 0.15;
const chartHeightPercent = 0.25;
// Does a chart fit or exist?
if (height < 100 || !sparkline) {
layoutType = LayoutType.StackedNoChart;
} else {
// we have chart
chartHeight = height * chartHeightPercent;
chartWidth = width;
}
if (titleToAlignTo && titleToAlignTo.length > 0) {
titleFontSize = calculateFontSize(
titleToAlignTo,
maxTextWidth,
height * titleHeightPercent,
LINE_HEIGHT,
maxTitleFontSize
);
titleHeight = titleFontSize * LINE_HEIGHT;
}
valueFontSize = calculateFontSize(
valueToAlignTo,
maxTextWidth,
maxTextHeight - chartHeight - titleHeight,
LINE_HEIGHT
);
// make title fontsize it's a bit smaller than valueFontSize
titleFontSize = Math.min(valueFontSize * 0.7, titleFontSize);
}
return {
valueFontSize,
titleFontSize,
chartHeight,
chartWidth,
type: layoutType,
width,
height,
colorMode,
graphMode,
theme,
valueColor,
justifyCenter,
panelPadding,
};
}
export function getTitleStyles(layout: LayoutResult) {
const styles: CSSProperties = {
fontSize: `${layout.titleFontSize}px`,
textShadow: '#333 0px 0px 1px',
color: '#EEE',
lineHeight: LINE_HEIGHT,
};
if (layout.theme.isLight) {
styles.color = 'white';
}
return styles;
}
export function getValueStyles(layout: LayoutResult) {
const styles: CSSProperties = {
fontSize: `${layout.valueFontSize}px`,
color: '#EEE',
textShadow: '#333 0px 0px 1px',
fontWeight: 500,
lineHeight: LINE_HEIGHT,
};
switch (layout.colorMode) {
case BigValueColorMode.Value:
styles.color = layout.valueColor;
}
return styles;
}
export function getValueAndTitleContainerStyles(layout: LayoutResult): CSSProperties {
const styles: CSSProperties = {
display: 'flex',
};
switch (layout.type) {
case LayoutType.Wide:
styles.flexDirection = 'row';
styles.justifyContent = 'space-between';
styles.flexGrow = 1;
break;
case LayoutType.WideNoChart:
styles.flexDirection = 'row';
styles.justifyContent = 'space-between';
styles.alignItems = 'center';
styles.flexGrow = 1;
break;
case LayoutType.StackedNoChart:
styles.flexDirection = 'column';
styles.flexGrow = 1;
break;
case LayoutType.Stacked:
default:
styles.flexDirection = 'column';
styles.justifyContent = 'center';
}
if (layout.justifyCenter) {
styles.alignItems = 'center';
styles.justifyContent = 'center';
styles.flexGrow = 1;
}
return styles;
}
export function getPanelStyles(layout: LayoutResult) {
const panelStyles: CSSProperties = {
width: `${layout.width}px`,
height: `${layout.height}px`,
padding: `${layout.panelPadding}px`,
borderRadius: '3px',
position: 'relative',
display: 'flex',
};
const themeFactor = layout.theme.isDark ? 1 : -0.7;
switch (layout.colorMode) {
case BigValueColorMode.Background:
const bgColor2 = tinycolor(layout.valueColor)
.darken(15 * themeFactor)
.spin(8)
.toRgbString();
const bgColor3 = tinycolor(layout.valueColor)
.darken(5 * themeFactor)
.spin(-8)
.toRgbString();
panelStyles.background = `linear-gradient(120deg, ${bgColor2}, ${bgColor3})`;
break;
case BigValueColorMode.Value:
panelStyles.background = `${layout.theme.colors.dark4}`;
break;
}
switch (layout.type) {
case LayoutType.Stacked:
panelStyles.flexDirection = 'column';
break;
case LayoutType.StackedNoChart:
panelStyles.alignItems = 'center';
break;
case LayoutType.Wide:
panelStyles.flexDirection = 'row';
panelStyles.justifyContent = 'space-between';
break;
case LayoutType.WideNoChart:
panelStyles.alignItems = 'center';
break;
}
if (layout.justifyCenter) {
panelStyles.alignItems = 'center';
panelStyles.flexDirection = 'row';
}
return panelStyles;
}
......@@ -48,14 +48,22 @@ export { Table } from './Table/Table';
export { TableInputCSV } from './Table/TableInputCSV';
// Visualizations
export { BigValue, BigValueDisplayMode, BigValueSparkline } from './BigValue/BigValue';
export {
BigValue,
BigValueColorMode,
BigValueSparkline,
BigValueGraphMode,
BigValueJustifyMode,
} from './BigValue/BigValue';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { GraphLegend } from './Graph/GraphLegend';
export { GraphWithLegend } from './Graph/GraphWithLegend';
export { BarGauge, BarGaugeAlignmentFactors } from './BarGauge/BarGauge';
export { BarGauge } from './BarGauge/BarGauge';
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
export { VizRepeater } from './VizRepeater/VizRepeater';
export {
LegendOptions,
LegendBasicOptions,
......@@ -66,6 +74,7 @@ export {
LegendPlacement,
LegendDisplayMode,
} from './Legend/Legend';
export { Alert, AlertVariant } from './Alert/Alert';
export { GraphSeriesToggler, GraphSeriesTogglerAPI } from './Graph/GraphSeriesToggler';
export { Collapse } from './Collapse/Collapse';
......
......@@ -25,3 +25,15 @@ export function measureText(text: string, fontSize: number): TextMetrics {
cache[cacheKey] = metrics;
return metrics;
}
export function calculateFontSize(text: string, width: number, height: number, lineHeight: number, maxSize?: number) {
// calculate width in 14px
const textSize = measureText(text, 14);
// how much bigger than 14px can we make it while staying within our width constraints
const fontSizeBasedOnWidth = (width / (textSize.width + 2)) * 14;
const fontSizeBasedOnHeight = height / lineHeight;
// final fontSize
const optimialSize = Math.min(fontSizeBasedOnHeight, fontSizeBasedOnWidth);
return Math.min(optimialSize, maxSize ?? optimialSize);
}
......@@ -4,36 +4,23 @@ import React, { PureComponent } from 'react';
// Services & Utils
import { config } from 'app/core/config';
import { BarGauge, BarGaugeAlignmentFactors, VizRepeater, DataLinksContextMenu } from '@grafana/ui';
import { BarGauge, VizRepeater, DataLinksContextMenu } from '@grafana/ui';
import { BarGaugeOptions } from './types';
import { getFieldDisplayValues, FieldDisplay, PanelProps } from '@grafana/data';
import {
getFieldDisplayValues,
FieldDisplay,
PanelProps,
getDisplayValueAlignmentFactors,
DisplayValueAlignmentFactors,
} from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
findMaximumInput = (values: FieldDisplay[], width: number, height: number): BarGaugeAlignmentFactors => {
const info: BarGaugeAlignmentFactors = {
title: '',
text: '',
};
for (let i = 0; i < values.length; i++) {
const v = values[i].display;
if (v.text && v.text.length > info.text.length) {
info.text = v.text;
}
if (v.title && v.title.length > info.title.length) {
info.title = v.title;
}
}
return info;
};
renderValue = (
value: FieldDisplay,
width: number,
height: number,
alignmentFactors: BarGaugeAlignmentFactors
alignmentFactors: DisplayValueAlignmentFactors
): JSX.Element => {
const { options } = this.props;
const { field, display } = value;
......@@ -87,7 +74,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
return (
<VizRepeater
source={data}
getAlignmentFactors={this.findMaximumInput}
getAlignmentFactors={getDisplayValueAlignmentFactors}
getValues={this.getValues}
renderValue={this.renderValue}
renderCounter={renderCounter}
......
......@@ -15,7 +15,8 @@ import {
import { FieldDisplayOptions, FieldConfig, DataLink, PanelEditorProps } from '@grafana/data';
import { Threshold, ValueMapping } from '@grafana/data';
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
import { BarGaugeOptions, displayModes } from './types';
import { orientationOptions } from '../gauge/types';
import {
getDataLinksVariableSuggestions,
getCalculationValueDataLinksVariableSuggestions,
......
......@@ -12,11 +12,6 @@ export const displayModes: Array<SelectableValue<string>> = [
{ value: 'basic', label: 'Basic' },
];
export const orientationOptions: Array<SelectableValue<VizOrientation>> = [
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
{ value: VizOrientation.Vertical, label: 'Vertical' },
];
export const defaults: BarGaugeOptions = {
displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
......
import { VizOrientation, FieldDisplayOptions } from '@grafana/data';
import { VizOrientation, FieldDisplayOptions, SelectableValue } from '@grafana/data';
import { SingleStatBaseOptions } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { standardFieldDisplayOptions } from '../stat/types';
......@@ -11,6 +11,12 @@ export const standardGaugeFieldOptions: FieldDisplayOptions = {
...standardFieldDisplayOptions,
};
export const orientationOptions: Array<SelectableValue<VizOrientation>> = [
{ value: VizOrientation.Auto, label: 'Auto' },
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
{ value: VizOrientation.Vertical, label: 'Vertical' },
];
export const defaults: GaugeOptions = {
showThresholdMarkers: true,
showThresholdLabels: false,
......
// Libraries
import React, { PureComponent } from 'react';
// Components
import { Switch } from '@grafana/ui';
// Types
import { SparklineOptions } from './types';
interface Props {
options: SparklineOptions;
onChange: (options: SparklineOptions) => void;
}
export class SparklineEditor extends PureComponent<Props> {
onToggleShow = () => this.props.onChange({ ...this.props.options, show: !this.props.options.show });
render() {
const { show } = this.props.options;
return <Switch label="Graph" labelClass="width-8" checked={show} onChange={this.onToggleShow} />;
}
}
......@@ -6,22 +6,41 @@ import { config } from 'app/core/config';
// Types
import { StatPanelOptions } from './types';
import { VizRepeater, BigValue, DataLinksContextMenu, BigValueSparkline } from '@grafana/ui';
import { PanelProps, getFieldDisplayValues, FieldDisplay } from '@grafana/data';
import { VizRepeater, BigValue, DataLinksContextMenu, BigValueSparkline, BigValueGraphMode } from '@grafana/ui';
import {
PanelProps,
getFieldDisplayValues,
FieldDisplay,
ReducerID,
getDisplayValueAlignmentFactors,
DisplayValueAlignmentFactors,
VizOrientation,
} from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
renderValue = (
value: FieldDisplay,
width: number,
height: number,
alignmentFactors: DisplayValueAlignmentFactors
): JSX.Element => {
const { timeRange, options } = this.props;
let sparkline: BigValueSparkline | undefined;
if (value.sparkline) {
sparkline = {
...options.sparkline,
data: value.sparkline,
minX: timeRange.from.valueOf(),
maxX: timeRange.to.valueOf(),
};
const calc = options.fieldOptions.calcs[0];
if (calc === ReducerID.last) {
sparkline.highlightIndex = sparkline.data.length - 1;
}
}
return (
......@@ -31,7 +50,10 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
<BigValue
value={value.display}
sparkline={sparkline}
displayMode={options.displayMode}
colorMode={options.colorMode}
graphMode={options.graphMode}
justifyMode={options.justifyMode}
alignmentFactors={alignmentFactors}
width={width}
height={height}
theme={config.theme}
......@@ -52,22 +74,39 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
replaceVariables,
theme: config.theme,
data: data.series,
sparkline: options.sparkline.show,
sparkline: options.graphMode !== BigValueGraphMode.None,
});
};
render() {
const { height, width, options, data, renderCounter } = this.props;
const { height, options, width, data, renderCounter } = this.props;
return (
<VizRepeater
getValues={this.getValues}
getAlignmentFactors={getDisplayValueAlignmentFactors}
renderValue={this.renderValue}
width={width}
height={height}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
orientation={getOrientation(width, height, options.orientation)}
/>
);
}
}
/**
* Stat panel custom auto orientation
*/
function getOrientation(width: number, height: number, orientation: VizOrientation): VizOrientation {
if (orientation !== VizOrientation.Auto) {
return orientation;
}
if (width / height > 2) {
return VizOrientation.Vertical;
} else {
return VizOrientation.Horizontal;
}
}
......@@ -15,8 +15,9 @@ import {
import { Threshold, ValueMapping, FieldConfig, DataLink, PanelEditorProps, FieldDisplayOptions } from '@grafana/data';
import { StatPanelOptions, SparklineOptions, displayModes } from './types';
import { SparklineEditor } from './SparklineEditor';
import { StatPanelOptions, colorModes, graphModes, justifyModes } from './types';
import { orientationOptions } from '../gauge/types';
import {
getDataLinksVariableSuggestions,
getCalculationValueDataLinksVariableSuggestions,
......@@ -45,13 +46,10 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
fieldOptions,
});
onSparklineChanged = (sparkline: SparklineOptions) =>
this.props.onOptionsChange({
...this.props.options,
sparkline,
});
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
onColorModeChanged = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, colorMode: value });
onGraphModeChanged = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, graphMode: value });
onJustifyModeChanged = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, justifyMode: value });
onOrientationChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
onDefaultsChange = (field: FieldConfig) => {
this.onDisplayOptionsChanged({
......@@ -81,16 +79,45 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
<PanelOptionsGroup title="Display">
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} labelWidth={8} />
<div className="form-field">
<FormLabel width={8}>Display mode</FormLabel>
<FormLabel width={8}>Orientation</FormLabel>
<Select
width={12}
options={orientationOptions}
defaultValue={orientationOptions[0]}
onChange={this.onOrientationChange}
value={orientationOptions.find(item => item.value === options.orientation)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Color</FormLabel>
<Select
width={12}
options={colorModes}
defaultValue={colorModes[0]}
onChange={this.onColorModeChanged}
value={colorModes.find(item => item.value === options.colorMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Graph</FormLabel>
<Select
width={12}
options={graphModes}
defaultValue={graphModes[0]}
onChange={this.onGraphModeChanged}
value={graphModes.find(item => item.value === options.graphMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Justify</FormLabel>
<Select
width={12}
options={displayModes}
defaultValue={displayModes[0]}
onChange={this.onDisplayModeChange}
value={displayModes.find(item => item.value === options.displayMode)}
options={justifyModes}
defaultValue={justifyModes[0]}
onChange={this.onJustifyModeChanged}
value={justifyModes.find(item => item.value === options.justifyMode)}
/>
</div>
<SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
......
import { SingleStatBaseOptions, BigValueDisplayMode } from '@grafana/ui';
import { SingleStatBaseOptions, BigValueColorMode, BigValueGraphMode, BigValueJustifyMode } from '@grafana/ui';
import { VizOrientation, ReducerID, FieldDisplayOptions, SelectableValue } from '@grafana/data';
export interface SparklineOptions {
show: boolean;
}
// Structure copied from angular
export interface StatPanelOptions extends SingleStatBaseOptions {
sparkline: SparklineOptions;
displayMode: BigValueDisplayMode;
graphMode: BigValueGraphMode;
colorMode: BigValueColorMode;
justifyMode: BigValueJustifyMode;
}
export const displayModes: Array<SelectableValue<BigValueDisplayMode>> = [
{ value: BigValueDisplayMode.Classic, label: 'Classic' },
{ value: BigValueDisplayMode.Vibrant, label: 'Vibrant' },
{ value: BigValueDisplayMode.Vibrant2, label: 'Vibrant 2' },
export const colorModes: Array<SelectableValue<BigValueColorMode>> = [
{ value: BigValueColorMode.Value, label: 'Value' },
{ value: BigValueColorMode.Background, label: 'Background' },
];
export const graphModes: Array<SelectableValue<BigValueGraphMode>> = [
{ value: BigValueGraphMode.None, label: 'None' },
{ value: BigValueGraphMode.Area, label: 'Area graph' },
];
export const justifyModes: Array<SelectableValue<BigValueJustifyMode>> = [
{ value: BigValueJustifyMode.Auto, label: 'Auto' },
{ value: BigValueJustifyMode.Center, label: 'Center' },
];
export const standardFieldDisplayOptions: FieldDisplayOptions = {
......@@ -33,10 +39,9 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
};
export const defaults: StatPanelOptions = {
sparkline: {
show: true,
},
displayMode: BigValueDisplayMode.Vibrant,
graphMode: BigValueGraphMode.Area,
colorMode: BigValueColorMode.Value,
justifyMode: BigValueJustifyMode.Auto,
fieldOptions: standardFieldDisplayOptions,
orientation: VizOrientation.Auto,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment