Commit 566cd2c6 by Torkel Ödegaard Committed by GitHub

ColorSchemes: Adds more color schemes and text colors that depend on the background (#28305)

* Adding more color modes and text colors that depend on the background color

* Updates

* Updated

* Another big value fix

* Fixing unit tests

* Updated

* Updated test

* Update

* Updated

* Updated

* Updated

* Updated

* Added new demo dashboard

* Updated

* updated

* Updated

* Updateed

* added beta notice

* Fixed e2e test
parent 95a19934
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"custom": {
"align": "center",
"displayMode": "color-background",
"filterable": false
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 20
},
{
"color": "orange",
"value": 60
},
{
"color": "red",
"value": 70
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Field"
},
"properties": [
{
"id": "custom.displayMode"
}
]
}
]
},
"gridPos": {
"h": 16,
"w": 19,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Last"
}
]
},
"pluginVersion": "7.4.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 15,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Gradient color schemes",
"transformations": [
{
"id": "reduce",
"options": {
"reducers": ["max", "mean", "last", "min"]
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Field": false
},
"indexByName": {},
"renameByName": {}
}
}
],
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-blues"
},
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 20
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 26,
"w": 5,
"x": 19,
"y": 0
},
"id": 2,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["mean"],
"fields": "",
"values": false
},
"textMode": "value"
},
"pluginVersion": "7.4.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"labels": "",
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 30,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Stats",
"type": "stat"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"align": "center",
"displayMode": "color-background",
"filterable": false
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 20
},
{
"color": "orange",
"value": 60
},
{
"color": "red",
"value": 70
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Field"
},
"properties": [
{
"id": "custom.displayMode"
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 19,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"displayMode": "lcd",
"orientation": "auto",
"reduceOptions": {
"calcs": ["mean"],
"fields": "",
"values": false
},
"showUnfilled": true
},
"pluginVersion": "7.4.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 15,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Bar Gauge LCD",
"transformations": [],
"type": "bargauge"
}
],
"schemaVersion": 26,
"style": "dark",
"tags": ["gdev", "demo"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Gradient Color modes",
"uid": "inxsweKGz",
"version": 17
}
......@@ -29,7 +29,7 @@ e2e.scenario({
e2e.components.DashboardLinks.link()
.should('be.visible')
.and(links => {
expect(links).to.have.length(13);
expect(links).to.have.length.greaterThan(13);
for (let index = 0; index < links.length; index++) {
expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`);
......
......@@ -3,13 +3,14 @@ import _ from 'lodash';
// Types
import { Field, FieldType } from '../types/dataFrame';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
import { GrafanaTheme } from '../types/theme';
import { DecimalCount, DecimalInfo, DisplayProcessor, DisplayValue } from '../types/displayValue';
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { dateTime } from '../datetime';
import { KeyValue, TimeZone } from '../types';
import { getScaleCalculator } from './scale';
import { getTestTheme } from '../utils/testdata/testTheme';
interface DisplayProcessorOptions {
field: Partial<Field>;
......@@ -41,7 +42,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
const config = field.config ?? {};
// Theme should be required or we need access to default theme instance from here
const theme = options.theme ?? ({ type: GrafanaThemeType.Dark } as GrafanaTheme);
const theme = options.theme ?? getTestTheme();
let unit = config.unit;
let hasDateUnit = unit && (timeFormats[unit] || unit.startsWith('time:'));
......
import { Field, GrafanaThemeType, GrafanaTheme, FieldColorModeId } from '../types';
import { Field, FieldColorModeId } from '../types';
import { getTestTheme } from '../utils/testdata/testTheme';
import { fieldColorModeRegistry, FieldValueColorCalculator } from './fieldColor';
describe('fieldColorModeRegistry', () => {
......@@ -9,10 +10,7 @@ describe('fieldColorModeRegistry', () => {
function getCalculator(options: GetCalcOptions): FieldValueColorCalculator {
const mode = fieldColorModeRegistry.get(options.mode);
return mode.getCalculator(
{ state: { seriesIndex: options.seriesIndex } } as Field,
{ type: GrafanaThemeType.Dark } as GrafanaTheme
);
return mode.getCalculator({ state: { seriesIndex: options.seriesIndex } } as Field, getTestTheme());
}
it('Schemes should interpolate', () => {
......
......@@ -54,20 +54,81 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
// }),
new FieldColorSchemeMode({
id: FieldColorModeId.PaletteClassic,
name: 'By series / Classic palette',
//description: 'Assigns color based on series or field index',
name: 'Classic palette',
isContinuous: false,
isByValue: false,
colors: classicColors,
}),
new FieldColorSchemeMode({
id: 'continuous-GrYlRd',
name: 'By value / Green Yellow Red (gradient)',
//description: 'Interpolated colors based value, min and max',
name: 'Green-Yellow-Red',
isContinuous: true,
isByValue: true,
colors: ['green', 'yellow', 'red'],
}),
new FieldColorSchemeMode({
id: 'continuous-BlYlRd',
name: 'Blue-Yellow-Red',
isContinuous: true,
isByValue: true,
colors: ['dark-blue', 'super-light-yellow', 'dark-red'],
}),
new FieldColorSchemeMode({
id: 'continuous-RdYlBl',
name: 'Red-Yellow-Blue',
isContinuous: true,
isByValue: true,
colors: ['dark-red', 'super-light-yellow', 'dark-blue'],
}),
new FieldColorSchemeMode({
id: 'continuous-YlRd',
name: 'Yellow-Red',
isContinuous: true,
isByValue: true,
colors: ['super-light-yellow', 'dark-red'],
}),
new FieldColorSchemeMode({
id: 'continuous-BlPu',
name: 'Blue-Purple',
isContinuous: true,
isByValue: true,
colors: ['blue', 'purple'],
}),
new FieldColorSchemeMode({
id: 'continuous-YlBl',
name: 'Yellow-Blue',
isContinuous: true,
isByValue: true,
colors: ['super-light-yellow', 'dark-blue'],
}),
new FieldColorSchemeMode({
id: 'continuous-blues',
name: 'Blues',
isContinuous: true,
isByValue: true,
colors: ['panel-bg', 'dark-blue'],
}),
new FieldColorSchemeMode({
id: 'continuous-reds',
name: 'Reds',
isContinuous: true,
isByValue: true,
colors: ['panel-bg', 'dark-red'],
}),
new FieldColorSchemeMode({
id: 'continuous-greens',
name: 'Greens',
isContinuous: true,
isByValue: true,
colors: ['panel-bg', 'dark-green'],
}),
new FieldColorSchemeMode({
id: 'continuous-purples',
name: 'Purples',
isContinuous: true,
isByValue: true,
colors: ['panel-bg', 'dark-purple'],
}),
];
});
......
......@@ -2,9 +2,9 @@ import merge from 'lodash/merge';
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from '../transformations/fieldReducer';
import { GrafanaTheme } from '../types/theme';
import { MappingType } from '../types';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { getTestTheme } from '../utils/testdata/testTheme';
describe('FieldDisplay', () => {
beforeAll(() => {
......@@ -241,7 +241,7 @@ function createDisplayOptions(extend: Partial<GetFieldDisplayValuesOptions> = {}
overrides: [],
defaults: {},
},
theme: {} as GrafanaTheme,
theme: getTestTheme(),
};
return merge<GetFieldDisplayValuesOptions, any>(options, extend);
......
......@@ -15,7 +15,6 @@ import {
FieldConfigPropertyItem,
FieldConfigSource,
FieldType,
GrafanaTheme,
InterpolateFunction,
ThresholdsMode,
FieldColorModeId,
......@@ -28,6 +27,7 @@ import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getFieldDisplayName } from './fieldState';
import { ArrayVector } from '../vector';
import { getDisplayProcessor } from './displayProcessor';
import { getTestTheme } from '../utils/testdata/testTheme';
const property1: any = {
id: 'custom.property1', // Match field properties
......@@ -136,7 +136,7 @@ describe('applyFieldOverrides', () => {
},
replaceVariables: (value: any) => value,
getDataSourceSettingsByUid: undefined as any,
theme: {} as GrafanaTheme,
theme: getTestTheme(),
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
});
......@@ -199,7 +199,7 @@ describe('applyFieldOverrides', () => {
fieldConfigRegistry: customFieldRegistry,
getDataSourceSettingsByUid: undefined as any,
replaceVariables: v => v,
theme: {} as GrafanaTheme,
theme: getTestTheme(),
})[0];
const outField = processed.fields[0];
......@@ -216,7 +216,7 @@ describe('applyFieldOverrides', () => {
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: (undefined as any) as GrafanaTheme,
theme: getTestTheme(),
fieldConfigRegistry: customFieldRegistry,
})[0];
const valueColumn = data.fields[1];
......@@ -244,7 +244,7 @@ describe('applyFieldOverrides', () => {
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: (undefined as any) as GrafanaTheme,
theme: getTestTheme(),
autoMinMax: true,
})[0];
const valueColumn = data.fields[1];
......@@ -268,7 +268,7 @@ describe('applyFieldOverrides', () => {
return value;
}) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: (undefined as any) as GrafanaTheme,
theme: getTestTheme(),
autoMinMax: true,
fieldConfigRegistry: customFieldRegistry,
})[0];
......@@ -521,7 +521,7 @@ describe('getLinksSupplier', () => {
// this is used only for internal links so isn't needed here
() => ({} as any),
{
theme: {} as GrafanaTheme,
theme: getTestTheme(),
}
);
supplier({});
......@@ -568,7 +568,7 @@ describe('getLinksSupplier', () => {
// We do not need to interpolate anything for this test
(value, vars, format) => value,
uid => ({ name: 'testDS' } as any),
{ theme: {} as GrafanaTheme }
{ theme: getTestTheme() }
);
const links = supplier({ valueRowIndex: 0 });
expect(links.length).toBe(1);
......
......@@ -2,6 +2,7 @@ import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
import { applyFieldOverrides } from './fieldOverrides';
import { toDataFrame } from '../dataframe';
import { GrafanaTheme } from '../types';
import { getTestTheme } from '../utils/testdata/testTheme';
describe('getFieldDisplayValuesProxy', () => {
const data = applyFieldOverrides({
......@@ -30,7 +31,7 @@ describe('getFieldDisplayValuesProxy', () => {
replaceVariables: (val: string) => val,
getDataSourceSettingsByUid: (val: string) => ({} as any),
timeZone: 'utc',
theme: {} as GrafanaTheme,
theme: getTestTheme(),
autoMinMax: true,
})[0];
......
import { ThresholdsMode, Field, FieldType, GrafanaThemeType, GrafanaTheme } from '../types';
import { ThresholdsMode, Field, FieldType } from '../types';
import { sortThresholds } from './thresholds';
import { ArrayVector } from '../vector/ArrayVector';
import { getScaleCalculator } from './scale';
import { getTestTheme } from '../utils/testdata/testTheme';
describe('getScaleCalculator', () => {
it('should return percent, threshold and color', () => {
......@@ -18,7 +19,7 @@ describe('getScaleCalculator', () => {
values: new ArrayVector([0, 50, 100]),
};
const calc = getScaleCalculator(field, { type: GrafanaThemeType.Dark } as GrafanaTheme);
const calc = getScaleCalculator(field, getTestTheme());
expect(calc(70)).toEqual({
percent: 0.7,
threshold: thresholds[1],
......
......@@ -34,7 +34,8 @@ export type Color =
| 'dark-purple'
| 'semi-dark-purple'
| 'light-purple'
| 'super-light-purple';
| 'super-light-purple'
| 'panel-bg';
type ThemeVariants = {
dark: string;
......@@ -82,6 +83,8 @@ export function buildColorsMapForTheme(theme: GrafanaTheme): Record<Color, strin
}
}
colorsMap['panel-bg'] = theme.colors.panelBg;
return colorsMap;
}
......@@ -118,7 +121,25 @@ export function getColorForTheme(color: string, theme: GrafanaTheme): string {
export function getColorFromHexRgbOrName(color: string, type?: GrafanaThemeType): string {
const themeType = type ?? GrafanaThemeType.Dark;
return getColorForTheme(color, ({ type: themeType } as unknown) as GrafanaTheme);
if (themeType === GrafanaThemeType.Dark) {
const darkTheme = ({
type: themeType,
colors: {
panelBg: '#141619',
},
} as unknown) as GrafanaTheme;
return getColorForTheme(color, darkTheme);
}
const lightTheme = ({
type: themeType,
colors: {
panelBg: '#000000',
},
} as unknown) as GrafanaTheme;
return getColorForTheme(color, lightTheme);
}
const buildNamedColorsPalette = () => {
......
import { GrafanaTheme, GrafanaThemeType } from '../../types/theme';
export function getTestTheme(type: GrafanaThemeType = GrafanaThemeType.Dark): GrafanaTheme {
return ({
type,
isDark: type === GrafanaThemeType.Dark,
isLight: type === GrafanaThemeType.Light,
colors: {
panelBg: 'white',
},
} as unknown) as GrafanaTheme;
}
......@@ -9,6 +9,7 @@ import { calculateFontSize } from '../../utils/measureText';
// Types
import { BigValueColorMode, Props, BigValueJustifyMode, BigValueTextMode } from './BigValue';
import { getTextColorForBackground } from '../../utils';
const LINE_HEIGHT = 1.2;
const MAX_TITLE_SIZE = 30;
......@@ -51,7 +52,7 @@ export abstract class BigValueLayout {
};
if (this.props.colorMode === BigValueColorMode.Background) {
styles.color = 'white';
styles.color = getTextColorForBackground(this.valueColor);
}
return styles;
......@@ -69,7 +70,7 @@ export abstract class BigValueLayout {
styles.color = this.valueColor;
break;
case BigValueColorMode.Background:
styles.color = 'white';
styles.color = getTextColorForBackground(this.valueColor);
}
return styles;
......
......@@ -30,7 +30,7 @@ exports[`BigValue Render with basic options should render 1`] = `
<FormattedDisplayValue
style={
Object {
"color": "white",
"color": "#202226",
"fontSize": 230,
"fontWeight": 500,
"lineHeight": 1.2,
......
......@@ -23,9 +23,11 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde
const styles = useStyles(getStyles);
const options = fieldColorModeRegistry.list().map(mode => {
let suffix = mode.isByValue ? ' (by value)' : '';
return {
value: mode.id,
label: mode.name,
label: `${mode.name}${suffix}`,
description: mode.description,
isContinuous: mode.isContinuous,
isByValue: mode.isByValue,
......
......@@ -5,6 +5,7 @@ import { TableCellDisplayMode, TableCellProps } from './types';
import tinycolor from 'tinycolor2';
import { TableStyles } from './styles';
import { FilterActions } from './FilterActions';
import { getTextColorForBackground } from '../../utils';
export const DefaultCell: FC<TableCellProps> = props => {
const { field, cell, tableStyles, row, cellProps } = props;
......@@ -65,7 +66,12 @@ function getCellStyle(tableStyles: TableStyles, field: Field, displayValue: Disp
.spin(5)
.toRgbString();
return tableStyles.buildCellContainerStyle('white', `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`);
const textColor = getTextColorForBackground(displayValue.color!);
return tableStyles.buildCellContainerStyle(
textColor,
`linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`
);
}
return tableStyles.cellContainer;
......
......@@ -4,6 +4,8 @@ import flattenDeep from 'lodash/flattenDeep';
import chunk from 'lodash/chunk';
import zip from 'lodash/zip';
import tinycolor from 'tinycolor2';
import lightTheme from '../themes/light';
import darkTheme from '../themes/dark';
export const PALETTE_ROWS = 4;
export const PALETTE_COLUMNS = 14;
......@@ -93,4 +95,9 @@ function hslToHex(color: any) {
return tinycolor(color).toHexString();
}
export function getTextColorForBackground(color: string) {
const b = tinycolor(color).getBrightness();
return b > 150 ? lightTheme.colors.textStrong : darkTheme.colors.textStrong;
}
export let sortedColors = sortColorsByHue(colors);
let canvas: HTMLCanvasElement | null = null;
const cache: Record<string, TextMetrics> = {};
/**
* @beta
*/
export function measureText(text: string, fontSize: number): TextMetrics {
const fontStyle = `${fontSize}px 'Roboto'`;
const cacheKey = text + fontStyle;
......@@ -26,6 +29,9 @@ export function measureText(text: string, fontSize: number): TextMetrics {
return metrics;
}
/**
* @beta
*/
export function calculateFontSize(text: string, width: number, height: number, lineHeight: number, maxSize?: number) {
// calculate width in 14px
const textSize = measureText(text, 14);
......
import { getFieldLinksSupplier } from './linkSuppliers';
import { applyFieldOverrides, DataFrameView, dateTime, FieldDisplay, GrafanaTheme, toDataFrame } from '@grafana/data';
import { applyFieldOverrides, DataFrameView, dateTime, FieldDisplay, toDataFrame } from '@grafana/data';
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
import { TemplateSrv } from '../../templating/template_srv';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { getTheme } from '@grafana/ui';
describe('getFieldLinksSupplier', () => {
let originalLinkSrv: LinkService;
......@@ -91,7 +92,7 @@ describe('getFieldLinksSupplier', () => {
replaceVariables: (val: string) => val,
getDataSourceSettingsByUid: (val: string) => ({} as any),
timeZone: 'utc',
theme: {} as GrafanaTheme,
theme: getTheme(),
autoMinMax: true,
})[0];
......
......@@ -3,7 +3,7 @@ import TableModel from 'app/core/table_model';
import { TableRenderer } from '../renderer';
import { ScopedVars, TimeZone } from '@grafana/data';
import { ColumnRender } from '../types';
import { config } from 'app/core/config';
import { getTheme } from '@grafana/ui';
const utc: TimeZone = 'utc';
......@@ -211,7 +211,7 @@ describe('when rendering table', () => {
};
//@ts-ignore
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, config.theme);
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
it('time column should be formatted', () => {
const html = renderer.renderCell(0, 0, 1388556366666);
......@@ -467,7 +467,7 @@ describe('when rendering table with different patterns', () => {
};
//@ts-ignore
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv);
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
const html = renderer.renderCell(1, 0, 1230);
expect(html).toBe(expected);
......@@ -537,7 +537,7 @@ describe('when rendering cells with different alignment options', () => {
};
//@ts-ignore
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv);
const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv, getTheme());
const html = renderer.renderCell(1, 0, 42);
expect(html).toBe(expected);
......
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