Commit 93a59561 by Torkel Ödegaard Committed by GitHub

GraphNG: Color series from by value scheme & change to fillGradient to gradientMode (#29893)

parent f004655b
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0.5, "gradientMode": "opacity",
"fillOpacity": 40, "fillOpacity": 40,
"lineInterpolation": "linear", "lineInterpolation": "linear",
"lineWidth": 2, "lineWidth": 2,
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "bars", "drawStyle": "bars",
"fillGradient": 0.4, "gradientNode": "opacity",
"fillOpacity": 53, "fillOpacity": 53,
"lineInterpolation": "linear", "lineInterpolation": "linear",
"lineWidth": 1, "lineWidth": 1,
...@@ -211,7 +211,7 @@ ...@@ -211,7 +211,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "bars", "drawStyle": "bars",
"fillGradient": "opacity", "gradientMode": "opacity",
"fillOpacity": 100, "fillOpacity": 100,
"lineInterpolation": "linear", "lineInterpolation": "linear",
"lineWidth": 0, "lineWidth": 0,
...@@ -300,7 +300,7 @@ ...@@ -300,7 +300,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0.5, "gradientMode": "opacity",
"fillOpacity": 20, "fillOpacity": 20,
"lineInterpolation": "linear", "lineInterpolation": "linear",
"lineWidth": 1, "lineWidth": 1,
...@@ -388,7 +388,7 @@ ...@@ -388,7 +388,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": "hue", "gradientMode": "hue",
"fillOpacity": 62, "fillOpacity": 62,
"lineInterpolation": "smooth", "lineInterpolation": "smooth",
"lineWidth": 2, "lineWidth": 2,
...@@ -477,7 +477,7 @@ ...@@ -477,7 +477,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "bars", "drawStyle": "bars",
"fillGradient": "hue", "gradientMode": "hue",
"fillOpacity": 100, "fillOpacity": 100,
"lineInterpolation": "smooth", "lineInterpolation": "smooth",
"lineWidth": 0, "lineWidth": 0,
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -245,7 +245,7 @@ ...@@ -245,7 +245,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -374,7 +374,7 @@ ...@@ -374,7 +374,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -475,7 +475,7 @@ ...@@ -475,7 +475,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -585,7 +585,7 @@ ...@@ -585,7 +585,7 @@
"axisLabel": "", "axisLabel": "",
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": 0, "fillGradient": "none",
"fillOpacity": 0, "fillOpacity": 0,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -726,9 +726,7 @@ ...@@ -726,9 +726,7 @@
"fill": { "fill": {
"alpha": 0 "alpha": 0
}, },
"fillGradient": { "fillGradient": "none",
"label": "None"
},
"fillOpacity": 10, "fillOpacity": 10,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
...@@ -889,9 +887,7 @@ ...@@ -889,9 +887,7 @@
"fill": { "fill": {
"alpha": 0 "alpha": 0
}, },
"fillGradient": { "fillGradient": "none",
"label": "None"
},
"fillOpacity": 10, "fillOpacity": 10,
"hideFrom": { "hideFrom": {
"graph": false, "graph": false,
......
import { Field, FieldColorModeId } from '../types'; import { Field, FieldColorModeId, FieldType } from '../types';
import { getTestTheme } from '../utils/testdata/testTheme'; import { getTestTheme } from '../utils/testdata/testTheme';
import { fieldColorModeRegistry, FieldValueColorCalculator } from './fieldColor'; import { ArrayVector } from '../vector/ArrayVector';
import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor';
describe('fieldColorModeRegistry', () => { function getTestField(mode: string): Field {
interface GetCalcOptions { return {
mode: string; name: 'name',
seriesIndex?: number; type: FieldType.number,
} values: new ArrayVector(),
config: {
color: {
mode: mode,
} as any,
},
state: {},
};
}
interface GetCalcOptions {
mode: string;
seriesIndex?: number;
}
function getCalculator(options: GetCalcOptions): FieldValueColorCalculator { function getCalculator(options: GetCalcOptions): FieldValueColorCalculator {
const mode = fieldColorModeRegistry.get(options.mode); const field = getTestField(options.mode);
return mode.getCalculator({ state: { seriesIndex: options.seriesIndex } } as Field, getTestTheme()); const mode = fieldColorModeRegistry.get(options.mode);
} field.state!.seriesIndex = options.seriesIndex;
return mode.getCalculator(field, getTestTheme());
}
describe('fieldColorModeRegistry', () => {
it('Schemes should interpolate', () => { it('Schemes should interpolate', () => {
const calcFn = getCalculator({ mode: 'continuous-GrYlRd' }); const calcFn = getCalculator({ mode: 'continuous-GrYlRd' });
expect(calcFn(70, 0.5, undefined)).toEqual('rgb(226, 192, 61)'); expect(calcFn(70, 0.5, undefined)).toEqual('rgb(226, 192, 61)');
...@@ -27,4 +44,48 @@ describe('fieldColorModeRegistry', () => { ...@@ -27,4 +44,48 @@ describe('fieldColorModeRegistry', () => {
const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 1 }); const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 1 });
expect(calcFn(70, 0, undefined)).toEqual('#EAB839'); expect(calcFn(70, 0, undefined)).toEqual('#EAB839');
}); });
it('When color.seriesBy is set to last use that instead of v', () => {
const field = getTestField('continuous-GrYlRd');
field.config.color!.seriesBy = 'last';
// min = -10, max = 10, last = 5
// last percent 75%
field.values = new ArrayVector([0, -10, 5, 10, 2, 5]);
const color = getFieldSeriesColor(field, getTestTheme());
const calcFn = getCalculator({ mode: 'continuous-GrYlRd' });
expect(color.color).toEqual(calcFn(4, 0.75));
});
});
describe('getFieldSeriesColor', () => {
const field = getTestField('continuous-GrYlRd');
field.values = new ArrayVector([0, -10, 5, 10, 2, 5]);
it('When color.seriesBy is last use that to calc series color', () => {
field.config.color!.seriesBy = 'last';
const color = getFieldSeriesColor(field, getTestTheme());
const calcFn = getCalculator({ mode: 'continuous-GrYlRd' });
// the 4 can be anything, 0.75 comes from 5 being 75% in the range -10 to 10 (see data above)
expect(color.color).toEqual(calcFn(4, 0.75));
});
it('When color.seriesBy is max use that to calc series color', () => {
field.config.color!.seriesBy = 'max';
const color = getFieldSeriesColor(field, getTestTheme());
const calcFn = getCalculator({ mode: 'continuous-GrYlRd' });
expect(color.color).toEqual(calcFn(10, 1));
});
it('When color.seriesBy is min use that to calc series color', () => {
field.config.color!.seriesBy = 'min';
const color = getFieldSeriesColor(field, getTestTheme());
const calcFn = getCalculator({ mode: 'continuous-GrYlRd' });
expect(color.color).toEqual(calcFn(-10, 0));
});
}); });
...@@ -3,6 +3,8 @@ import { classicColors, getColorForTheme, RegistryItem } from '../utils'; ...@@ -3,6 +3,8 @@ import { classicColors, getColorForTheme, RegistryItem } from '../utils';
import { Registry } from '../utils/Registry'; import { Registry } from '../utils/Registry';
import { interpolateRgbBasis } from 'd3-interpolate'; import { interpolateRgbBasis } from 'd3-interpolate';
import { fallBackTreshold } from './thresholds'; import { fallBackTreshold } from './thresholds';
import { getScaleCalculator, ColorScaleValue } from './scale';
import { reduceField } from '../transformations/fieldReducer';
export type FieldValueColorCalculator = (value: number, percent: number, Threshold?: Threshold) => string; export type FieldValueColorCalculator = (value: number, percent: number, Threshold?: Threshold) => string;
...@@ -209,6 +211,30 @@ export function getFieldColorMode(mode?: FieldColorModeId): FieldColorMode { ...@@ -209,6 +211,30 @@ export function getFieldColorMode(mode?: FieldColorModeId): FieldColorMode {
return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds); return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds);
} }
/**
* @alpha
* Function that will return a series color for any given color mode. If the color mode is a by value color
* mode it will use the field.config.color.seriesBy property to figure out which value to use
*/
export function getFieldSeriesColor(field: Field, theme: GrafanaTheme): ColorScaleValue {
const mode = getFieldColorModeForField(field);
if (!mode.isByValue) {
return {
color: mode.getCalculator(field, theme)(0, 0),
threshold: fallBackTreshold,
percent: 1,
};
}
const scale = getScaleCalculator(field, theme);
const stat = field.config.color?.seriesBy ?? 'last';
const calcs = reduceField({ field, reducers: [stat] });
const value = calcs[stat] ?? 0;
return scale(value);
}
function getFixedColor(field: Field, theme: GrafanaTheme) { function getFixedColor(field: Field, theme: GrafanaTheme) {
return () => { return () => {
return getColorForTheme(field.config.color?.fixedColor ?? FALLBACK_COLOR, theme); return getColorForTheme(field.config.color?.fixedColor ?? FALLBACK_COLOR, theme);
......
...@@ -3,7 +3,13 @@ export * from './displayProcessor'; ...@@ -3,7 +3,13 @@ export * from './displayProcessor';
export * from './standardFieldConfigEditorRegistry'; export * from './standardFieldConfigEditorRegistry';
export * from './overrides/processors'; export * from './overrides/processors';
export { getFieldColorModeForField, getFieldColorMode, fieldColorModeRegistry, FieldColorMode } from './fieldColor'; export {
getFieldColorModeForField,
getFieldColorMode,
fieldColorModeRegistry,
FieldColorMode,
getFieldSeriesColor,
} from './fieldColor';
export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { sortThresholds, getActiveThreshold } from './thresholds'; export { sortThresholds, getActiveThreshold } from './thresholds';
export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides } from './fieldOverrides'; export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides } from './fieldOverrides';
......
...@@ -131,6 +131,11 @@ export interface FieldColorConfigSettings { ...@@ -131,6 +131,11 @@ export interface FieldColorConfigSettings {
* to from thresholds if it was set to a by series palette * to from thresholds if it was set to a by series palette
*/ */
preferThresholdsMode?: boolean; preferThresholdsMode?: boolean;
/**
* Set to true if the visualization supports both by value and by series
* This will enable the Color by series UI option that sets the `color.seriesBy` option.
*/
bySeriesSupport?: boolean;
} }
export interface StatsPickerConfigSettings { export interface StatsPickerConfigSettings {
......
...@@ -4,13 +4,13 @@ import { Field, FieldConfig, FieldType, GrafanaTheme, NumericRange, Threshold } ...@@ -4,13 +4,13 @@ import { Field, FieldConfig, FieldType, GrafanaTheme, NumericRange, Threshold }
import { getFieldColorModeForField } from './fieldColor'; import { getFieldColorModeForField } from './fieldColor';
import { getActiveThresholdForValue } from './thresholds'; import { getActiveThresholdForValue } from './thresholds';
export interface ScaledValue { export interface ColorScaleValue {
percent: number; // 0-1 percent: number; // 0-1
threshold: Threshold; threshold: Threshold;
color: string; color: string;
} }
export type ScaleCalculator = (value: number) => ScaledValue; export type ScaleCalculator = (value: number) => ColorScaleValue;
export function getScaleCalculator(field: Field, theme: GrafanaTheme): ScaleCalculator { export function getScaleCalculator(field: Field, theme: GrafanaTheme): ScaleCalculator {
const mode = getFieldColorModeForField(field); const mode = getFieldColorModeForField(field);
......
...@@ -18,5 +18,5 @@ export { ...@@ -18,5 +18,5 @@ export {
BasicValueMatcherOptions, BasicValueMatcherOptions,
RangeValueMatcherOptions, RangeValueMatcherOptions,
} from './transformations/matchers/valueMatchers/types'; } from './transformations/matchers/valueMatchers/types';
export { PanelPlugin } from './panel/PanelPlugin'; export { PanelPlugin, SetFieldConfigOptionsArgs } from './panel/PanelPlugin';
export { createFieldConfigRegistry } from './panel/registryFactories'; export { createFieldConfigRegistry } from './panel/registryFactories';
...@@ -21,6 +21,7 @@ type StandardOptionConfig = { ...@@ -21,6 +21,7 @@ type StandardOptionConfig = {
settings?: any; settings?: any;
}; };
/** @beta */
export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> { export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
/** /**
* Configuration object of the standard field config properites * Configuration object of the standard field config properites
......
/**
* @public
*/
export enum FieldColorModeId { export enum FieldColorModeId {
Thresholds = 'thresholds', Thresholds = 'thresholds',
PaletteClassic = 'palette-classic', PaletteClassic = 'palette-classic',
...@@ -5,9 +8,21 @@ export enum FieldColorModeId { ...@@ -5,9 +8,21 @@ export enum FieldColorModeId {
Fixed = 'fixed', Fixed = 'fixed',
} }
/**
* @public
*/
export interface FieldColor { export interface FieldColor {
/** The main color scheme mode */
mode: FieldColorModeId; mode: FieldColorModeId;
/** Stores the fixed color value if mode is fixed */
fixedColor?: string; fixedColor?: string;
/** Some visualizations need to know how to assign a series color from by value color schemes */
seriesBy?: FieldColorSeriesByMode;
} }
/**
* @beta
*/
export type FieldColorSeriesByMode = 'min' | 'max' | 'last';
export const FALLBACK_COLOR = 'gray'; export const FALLBACK_COLOR = 'gray';
...@@ -8,7 +8,6 @@ import { ...@@ -8,7 +8,6 @@ import {
fieldReducers, fieldReducers,
FieldType, FieldType,
formattedValueToString, formattedValueToString,
getFieldColorModeForField,
getFieldDisplayName, getFieldDisplayName,
reduceField, reduceField,
TimeRange, TimeRange,
...@@ -23,6 +22,7 @@ import { LegendDisplayMode, VizLegendItem, VizLegendOptions } from '../VizLegend ...@@ -23,6 +22,7 @@ import { LegendDisplayMode, VizLegendItem, VizLegendOptions } from '../VizLegend
import { VizLegend } from '../VizLegend/VizLegend'; import { VizLegend } from '../VizLegend/VizLegend';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { useRevision } from '../uPlot/hooks'; import { useRevision } from '../uPlot/hooks';
import { getFieldColorModeForField, getFieldSeriesColor } from '@grafana/data';
import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types'; import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
...@@ -109,6 +109,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -109,6 +109,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
// X is the first field in the aligned frame // X is the first field in the aligned frame
const xField = alignedFrame.fields[0]; const xField = alignedFrame.fields[0];
if (xField.type === FieldType.time) { if (xField.type === FieldType.time) {
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: 'x',
...@@ -118,6 +119,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -118,6 +119,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
return [r.from.valueOf(), r.to.valueOf()]; return [r.from.valueOf(), r.to.valueOf()];
}, },
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: 'x',
isTime: true, isTime: true,
...@@ -130,6 +132,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -130,6 +132,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: 'x',
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: 'x',
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
...@@ -153,7 +156,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -153,7 +156,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
const fmt = field.display ?? defaultFormatter; const fmt = field.display ?? defaultFormatter;
const scaleKey = config.unit || FIXED_UNIT; const scaleKey = config.unit || FIXED_UNIT;
const colorMode = getFieldColorModeForField(field); const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0); const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
// The builder will manage unique scaleKeys and combine where appropriate // The builder will manage unique scaleKeys and combine where appropriate
...@@ -200,18 +204,21 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -200,18 +204,21 @@ export const GraphNG: React.FC<GraphNGProps> = ({
builder.addSeries({ builder.addSeries({
scaleKey, scaleKey,
showPoints,
colorMode,
fillOpacity,
theme,
drawStyle: customConfig.drawStyle!, drawStyle: customConfig.drawStyle!,
lineColor: customConfig.lineColor ?? seriesColor, lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth, lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation, lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle, lineStyle: customConfig.lineStyle,
showPoints,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor, pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity,
spanNulls: customConfig.spanNulls || false, spanNulls: customConfig.spanNulls || false,
show: !customConfig.hideFrom?.graph, show: !customConfig.hideFrom?.graph,
fillGradient: customConfig.fillGradient, gradientMode: customConfig.gradientMode,
thresholds: config.thresholds,
// The following properties are not used in the uPlot config, but are utilized as transport for legend config // The following properties are not used in the uPlot config, but are utilized as transport for legend config
dataFrameFieldIndex, dataFrameFieldIndex,
......
...@@ -9,11 +9,15 @@ import { ...@@ -9,11 +9,15 @@ import {
GrafanaTheme, GrafanaTheme,
getColorForTheme, getColorForTheme,
FieldColorConfigSettings, FieldColorConfigSettings,
FieldColorSeriesByMode,
getFieldColorMode,
} from '@grafana/data'; } from '@grafana/data';
import { Select } from '../Select/Select'; import { Select } from '../Select/Select';
import { ColorValueEditor } from './color'; import { ColorValueEditor } from './color';
import { useStyles, useTheme } from '../../themes/ThemeContext'; import { useStyles, useTheme } from '../../themes/ThemeContext';
import { css } from 'emotion'; import { css } from 'emotion';
import { Field } from '../Forms/Field';
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | undefined, FieldColorConfigSettings>> = ({ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | undefined, FieldColorConfigSettings>> = ({
value, value,
...@@ -23,6 +27,7 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde ...@@ -23,6 +27,7 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde
const theme = useTheme(); const theme = useTheme();
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const colorMode = getFieldColorMode(value?.mode);
const availableOptions = item.settings?.byValueSupport const availableOptions = item.settings?.byValueSupport
? fieldColorModeRegistry.list() ? fieldColorModeRegistry.list()
: fieldColorModeRegistry.list().filter(m => !m.isByValue); : fieldColorModeRegistry.list().filter(m => !m.isByValue);
...@@ -44,17 +49,27 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde ...@@ -44,17 +49,27 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde
const onModeChange = (newMode: SelectableValue<string>) => { const onModeChange = (newMode: SelectableValue<string>) => {
onChange({ onChange({
...value,
mode: newMode.value! as FieldColorModeId, mode: newMode.value! as FieldColorModeId,
}); });
}; };
const onColorChange = (color?: string) => { const onColorChange = (color?: string) => {
onChange({ onChange({
...value,
mode, mode,
fixedColor: color, fixedColor: color,
}); });
}; };
const onSeriesModeChange = (seriesBy?: FieldColorSeriesByMode) => {
onChange({
...value,
mode,
seriesBy,
});
};
const mode = value?.mode ?? FieldColorModeId.Thresholds; const mode = value?.mode ?? FieldColorModeId.Thresholds;
if (mode === FieldColorModeId.Fixed) { if (mode === FieldColorModeId.Fixed) {
...@@ -66,6 +81,25 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde ...@@ -66,6 +81,25 @@ export const FieldColorEditor: React.FC<FieldConfigEditorProps<FieldColor | unde
); );
} }
if (item.settings?.bySeriesSupport && colorMode.isByValue) {
const seriesModes: Array<SelectableValue<FieldColorSeriesByMode>> = [
{ label: 'Last', value: 'last' },
{ label: 'Min', value: 'min' },
{ label: 'Max', value: 'max' },
];
return (
<>
<div style={{ marginBottom: theme.spacing.formInputMargin }}>
<Select minMenuHeight={200} options={options} value={mode} onChange={onModeChange} />
</div>
<Field label="Color series by">
<RadioButtonGroup value={value?.seriesBy ?? 'last'} options={seriesModes} onChange={onSeriesModeChange} />
</Field>
</>
);
}
return <Select minMenuHeight={200} options={options} value={mode} onChange={onModeChange} />; return <Select minMenuHeight={200} options={options} value={mode} onChange={onModeChange} />;
}; };
......
...@@ -137,6 +137,7 @@ export class Sparkline extends PureComponent<Props, State> { ...@@ -137,6 +137,7 @@ export class Sparkline extends PureComponent<Props, State> {
builder.addSeries({ builder.addSeries({
scaleKey, scaleKey,
theme,
fieldName: getFieldDisplayName(field, data), fieldName: getFieldDisplayName(field, data),
drawStyle: customConfig.drawStyle!, drawStyle: customConfig.drawStyle!,
lineColor: customConfig.lineColor ?? seriesColor, lineColor: customConfig.lineColor ?? seriesColor,
......
...@@ -73,17 +73,17 @@ export interface LineConfig { ...@@ -73,17 +73,17 @@ export interface LineConfig {
export interface FillConfig { export interface FillConfig {
fillColor?: string; fillColor?: string;
fillOpacity?: number; fillOpacity?: number;
fillGradient?: FillGradientMode;
fillBelowTo?: string; // name of the field fillBelowTo?: string; // name of the field
} }
/** /**
* @alpha * @alpha
*/ */
export enum FillGradientMode { export enum GraphGradientMode {
None = 'none', None = 'none',
Opacity = 'opacity', Opacity = 'opacity',
Hue = 'hue', Hue = 'hue',
Scheme = 'scheme',
} }
/** /**
...@@ -131,6 +131,7 @@ export interface HideSeriesConfig { ...@@ -131,6 +131,7 @@ export interface HideSeriesConfig {
*/ */
export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig { export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig {
drawStyle?: DrawStyle; drawStyle?: DrawStyle;
gradientMode?: GraphGradientMode;
hideFrom?: HideSeriesConfig; hideFrom?: HideSeriesConfig;
} }
...@@ -165,8 +166,9 @@ export const graphFieldOptions = { ...@@ -165,8 +166,9 @@ export const graphFieldOptions = {
] as Array<SelectableValue<AxisPlacement>>, ] as Array<SelectableValue<AxisPlacement>>,
fillGradient: [ fillGradient: [
{ label: 'None', value: FillGradientMode.None }, { label: 'None', value: GraphGradientMode.None },
{ label: 'Opacity', value: FillGradientMode.Opacity }, { label: 'Opacity', value: GraphGradientMode.Opacity },
{ label: 'Hue', value: FillGradientMode.Hue }, { label: 'Hue', value: GraphGradientMode.Hue },
] as Array<SelectableValue<FillGradientMode>>, // { label: 'Color scheme', value: GraphGradientMode.Scheme },
] as Array<SelectableValue<GraphGradientMode>>,
}; };
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
import { UPlotConfigBuilder } from './UPlotConfigBuilder'; import { UPlotConfigBuilder } from './UPlotConfigBuilder';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { expect } from '../../../../../../public/test/lib/common'; import { GraphGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
import { FillGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
import darkTheme from '../../../themes/dark'; import darkTheme from '../../../themes/dark';
describe('UPlotConfigBuilder', () => { describe('UPlotConfigBuilder', () => {
...@@ -327,6 +326,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -327,6 +326,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x', scaleKey: 'scale-x',
fieldName: 'A-series', fieldName: 'A-series',
lineColor: '#0000ff', lineColor: '#0000ff',
theme: darkTheme,
}); });
expect(builder.getConfig().series[1].fill).toBe(undefined); expect(builder.getConfig().series[1].fill).toBe(undefined);
...@@ -340,6 +340,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -340,6 +340,7 @@ describe('UPlotConfigBuilder', () => {
fieldName: 'A-series', fieldName: 'A-series',
lineColor: '#FFAABB', lineColor: '#FFAABB',
fillOpacity: 50, fillOpacity: 50,
theme: darkTheme,
}); });
expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)'); expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)');
...@@ -354,6 +355,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -354,6 +355,7 @@ describe('UPlotConfigBuilder', () => {
lineColor: '#FFAABB', lineColor: '#FFAABB',
fillOpacity: 50, fillOpacity: 50,
fillColor: '#FF0000', fillColor: '#FF0000',
theme: darkTheme,
}); });
expect(builder.getConfig().series[1].fill).toBe('#FF0000'); expect(builder.getConfig().series[1].fill).toBe('#FF0000');
...@@ -367,7 +369,8 @@ describe('UPlotConfigBuilder', () => { ...@@ -367,7 +369,8 @@ describe('UPlotConfigBuilder', () => {
fieldName: 'A-series', fieldName: 'A-series',
lineColor: '#FFAABB', lineColor: '#FFAABB',
fillOpacity: 50, fillOpacity: 50,
fillGradient: FillGradientMode.Opacity, gradientMode: GraphGradientMode.Opacity,
theme: darkTheme,
}); });
expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function); expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function);
...@@ -380,13 +383,14 @@ describe('UPlotConfigBuilder', () => { ...@@ -380,13 +383,14 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x', scaleKey: 'scale-x',
fieldName: 'A-series', fieldName: 'A-series',
fillOpacity: 50, fillOpacity: 50,
fillGradient: FillGradientMode.Opacity, gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto, showPoints: PointVisibility.Auto,
pointSize: 5, pointSize: 5,
pointColor: '#00ff00', pointColor: '#00ff00',
lineColor: '#0000ff', lineColor: '#0000ff',
lineWidth: 1, lineWidth: 1,
spanNulls: false, spanNulls: false,
theme: darkTheme,
}); });
expect(builder.getConfig()).toMatchInlineSnapshot(` expect(builder.getConfig()).toMatchInlineSnapshot(`
......
import { FALLBACK_COLOR, FieldColorMode, GrafanaTheme, ThresholdsConfig } from '@grafana/data';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import uPlot, { Series } from 'uplot'; import uPlot, { Series } from 'uplot';
import { getCanvasContext } from '../../../utils/measureText';
import { import {
DrawStyle, DrawStyle,
LineConfig, LineConfig,
...@@ -8,18 +8,25 @@ import { ...@@ -8,18 +8,25 @@ import {
PointsConfig, PointsConfig,
PointVisibility, PointVisibility,
LineInterpolation, LineInterpolation,
FillGradientMode, GraphGradientMode,
} from '../config'; } from '../config';
import { PlotConfigBuilder } from '../types'; import { PlotConfigBuilder } from '../types';
import { DataFrameFieldIndex } from '@grafana/data'; import { DataFrameFieldIndex } from '@grafana/data';
import { getScaleGradientFn, getOpacityGradientFn, getHueGradientFn } from './gradientFills';
export interface SeriesProps extends LineConfig, FillConfig, PointsConfig { export interface SeriesProps extends LineConfig, FillConfig, PointsConfig {
scaleKey: string; scaleKey: string;
gradientMode?: GraphGradientMode;
/** Used when gradientMode is set to Scheme */
thresholds?: ThresholdsConfig;
/** Used when gradientMode is set to Scheme */
colorMode?: FieldColorMode;
fieldName: string; fieldName: string;
drawStyle: DrawStyle; drawStyle: DrawStyle;
show?: boolean; show?: boolean;
dataFrameFieldIndex?: DataFrameFieldIndex; dataFrameFieldIndex?: DataFrameFieldIndex;
hideInLegend?: boolean; hideInLegend?: boolean;
theme: GrafanaTheme;
} }
export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
...@@ -27,7 +34,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -27,7 +34,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
const { const {
drawStyle, drawStyle,
lineInterpolation, lineInterpolation,
lineColor,
lineWidth, lineWidth,
lineStyle, lineStyle,
showPoints, showPoints,
...@@ -43,7 +49,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -43,7 +49,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
if (drawStyle === DrawStyle.Points) { if (drawStyle === DrawStyle.Points) {
lineConfig.paths = () => null; lineConfig.paths = () => null;
} else { } else {
lineConfig.stroke = lineColor; lineConfig.stroke = this.getLineColor();
lineConfig.width = lineWidth; lineConfig.width = lineWidth;
if (lineStyle && lineStyle.fill !== 'solid') { if (lineStyle && lineStyle.fill !== 'solid') {
if (lineStyle.fill === 'dot') { if (lineStyle.fill === 'dot') {
...@@ -90,28 +96,39 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -90,28 +96,39 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
}; };
} }
getFill(): Series.Fill | undefined { private getLineColor(): Series.Stroke {
const { lineColor, fillColor, fillGradient, fillOpacity } = this.props; const { lineColor, gradientMode, colorMode, thresholds } = this.props;
if (fillColor) { if (gradientMode === GraphGradientMode.Scheme) {
return fillColor; return getScaleGradientFn(1, colorMode, thresholds);
} }
const mode = fillGradient ?? FillGradientMode.None; return lineColor ?? FALLBACK_COLOR;
let fillOpacityNumber = fillOpacity ?? 0; }
if (mode !== FillGradientMode.None) { private getFill(): Series.Fill | undefined {
return getCanvasGradient({ const { lineColor, fillColor, gradientMode, fillOpacity, colorMode, thresholds, theme } = this.props;
color: (fillColor ?? lineColor)!,
opacity: fillOpacityNumber / 100, if (fillColor) {
mode, return fillColor;
});
} }
if (fillOpacityNumber > 0) { const mode = gradientMode ?? GraphGradientMode.None;
return tinycolor(lineColor) const opacityPercent = (fillOpacity ?? 0) / 100;
.setAlpha(fillOpacityNumber / 100)
.toString(); switch (mode) {
case GraphGradientMode.Opacity:
return getOpacityGradientFn((fillColor ?? lineColor)!, opacityPercent);
case GraphGradientMode.Hue:
return getHueGradientFn((fillColor ?? lineColor)!, opacityPercent, theme);
case GraphGradientMode.Scheme:
return getScaleGradientFn(opacityPercent, colorMode, thresholds);
default:
if (opacityPercent > 0) {
return tinycolor(lineColor)
.setAlpha(opacityPercent)
.toString();
}
} }
return undefined; return undefined;
...@@ -165,50 +182,3 @@ function mapDrawStyleToPathBuilder( ...@@ -165,50 +182,3 @@ function mapDrawStyleToPathBuilder(
return builders.linear; // the default return builders.linear; // the default
} }
interface AreaGradientOptions {
color: string;
mode: FillGradientMode;
opacity: number;
}
function getCanvasGradient(opts: AreaGradientOptions): (self: uPlot, seriesIdx: number) => CanvasGradient {
return (plot: uPlot, seriesIdx: number) => {
const { color, mode, opacity } = opts;
const ctx = getCanvasContext();
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
switch (mode) {
case FillGradientMode.Hue:
const color1 = tinycolor(color)
.spin(-25)
.darken(30)
.setAlpha(opacity)
.toRgbString();
const color2 = tinycolor(color)
.spin(25)
.lighten(35)
.setAlpha(opacity)
.toRgbString();
gradient.addColorStop(0, color2);
gradient.addColorStop(1, color1);
case FillGradientMode.Opacity:
default:
gradient.addColorStop(
0,
tinycolor(color)
.setAlpha(opacity)
.toRgbString()
);
gradient.addColorStop(
1,
tinycolor(color)
.setAlpha(0)
.toRgbString()
);
return gradient;
}
};
}
import { FieldColorMode, getColorForTheme, GrafanaTheme, ThresholdsConfig } from '@grafana/data';
import tinycolor from 'tinycolor2';
import uPlot from 'uplot';
import darkTheme from '../../../themes/dark';
import { getCanvasContext } from '../../../utils/measureText';
export function getOpacityGradientFn(
color: string,
opacity: number
): (self: uPlot, seriesIdx: number) => CanvasGradient {
return (plot: uPlot, seriesIdx: number) => {
const ctx = getCanvasContext();
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
gradient.addColorStop(
0,
tinycolor(color)
.setAlpha(opacity)
.toRgbString()
);
gradient.addColorStop(
1,
tinycolor(color)
.setAlpha(0)
.toRgbString()
);
return gradient;
};
}
export function getHueGradientFn(
color: string,
opacity: number,
theme: GrafanaTheme
): (self: uPlot, seriesIdx: number) => CanvasGradient {
return (plot: uPlot, seriesIdx: number) => {
const ctx = getCanvasContext();
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
const color1 = tinycolor(color).spin(-15);
const color2 = tinycolor(color).spin(15);
if (theme.isDark) {
gradient.addColorStop(
0,
color2
.lighten(10)
.setAlpha(opacity)
.toString()
);
gradient.addColorStop(
1,
color1
.darken(10)
.setAlpha(opacity)
.toString()
);
} else {
gradient.addColorStop(
0,
color2
.lighten(10)
.setAlpha(opacity)
.toString()
);
gradient.addColorStop(1, color1.setAlpha(opacity).toString());
}
return gradient;
};
}
/**
* Experimental & quick and dirty test
* Not being used
*/
export function getScaleGradientFn(
opacity: number,
colorMode?: FieldColorMode,
thresholds?: ThresholdsConfig
): (self: uPlot, seriesIdx: number) => CanvasGradient {
if (!colorMode) {
throw Error('Missing colorMode required for color scheme gradients');
}
if (!thresholds) {
throw Error('Missing thresholds required for color scheme gradients');
}
return (plot: uPlot, seriesIdx: number) => {
const ctx = getCanvasContext();
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
const series = plot.series[seriesIdx];
const scale = plot.scales[series.scale!];
const range = plot.bbox.height;
console.log('scale', scale);
console.log('series.min', series.min);
console.log('series.max', series.max);
const getColorWithAlpha = (color: string) => {
return tinycolor(getColorForTheme(color, darkTheme))
.setAlpha(opacity)
.toString();
};
const addColorStop = (value: number, color: string) => {
const pos = plot.valToPos(value, series.scale!);
const percent = pos / range;
console.log(`addColorStop(value = ${value}, xPos=${pos})`);
gradient.addColorStop(Math.min(percent, 1), getColorWithAlpha(color));
};
for (let idx = 0; idx < thresholds.steps.length; idx++) {
const step = thresholds.steps[idx];
const value = step.value === -Infinity ? 0 : step.value;
addColorStop(value, step.color);
// to make the gradient discrete
if (thresholds.steps.length > idx + 1) {
addColorStop(thresholds.steps[idx + 1].value - 0.0000001, step.color);
}
}
return gradient;
};
}
...@@ -89,8 +89,8 @@ Object { ...@@ -89,8 +89,8 @@ Object {
"custom": Object { "custom": Object {
"axisPlacement": "hidden", "axisPlacement": "hidden",
"drawStyle": "line", "drawStyle": "line",
"fillGradient": "opacity",
"fillOpacity": 60, "fillOpacity": 60,
"gradientMode": "opacity",
"lineInterpolation": "stepAfter", "lineInterpolation": "stepAfter",
"lineWidth": 1, "lineWidth": 1,
"pointSize": 6, "pointSize": 6,
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
FieldConfigProperty, FieldConfigProperty,
FieldType, FieldType,
identityOverrideProcessor, identityOverrideProcessor,
SetFieldConfigOptionsArgs,
stringOverrideProcessor, stringOverrideProcessor,
} from '@grafana/data'; } from '@grafana/data';
import { import {
...@@ -15,12 +16,11 @@ import { ...@@ -15,12 +16,11 @@ import {
PointVisibility, PointVisibility,
ScaleDistribution, ScaleDistribution,
ScaleDistributionConfig, ScaleDistributionConfig,
GraphGradientMode,
} from '@grafana/ui'; } from '@grafana/ui';
import { SeriesConfigEditor } from './HideSeriesConfigEditor'; import { SeriesConfigEditor } from './HideSeriesConfigEditor';
import { ScaleDistributionEditor } from './ScaleDistributionEditor'; import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor'; import { LineStyleEditor } from './LineStyleEditor';
import { SetFieldConfigOptionsArgs } from '@grafana/data/src/panel/PanelPlugin';
import { FillGradientMode } from '@grafana/ui/src/components/uPlot/config';
import { FillBellowToEditor } from './FillBelowToEditor'; import { FillBellowToEditor } from './FillBelowToEditor';
export const defaultGraphConfig: GraphFieldConfig = { export const defaultGraphConfig: GraphFieldConfig = {
...@@ -28,7 +28,7 @@ export const defaultGraphConfig: GraphFieldConfig = { ...@@ -28,7 +28,7 @@ export const defaultGraphConfig: GraphFieldConfig = {
lineInterpolation: LineInterpolation.Linear, lineInterpolation: LineInterpolation.Linear,
lineWidth: 1, lineWidth: 1,
fillOpacity: 0, fillOpacity: 0,
fillGradient: FillGradientMode.None, gradientMode: GraphGradientMode.None,
}; };
export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> { export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> {
...@@ -37,6 +37,8 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption ...@@ -37,6 +37,8 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
[FieldConfigProperty.Color]: { [FieldConfigProperty.Color]: {
settings: { settings: {
byValueSupport: false, byValueSupport: false,
bySeriesSupport: true,
preferThresholdsMode: false,
}, },
defaultValue: { defaultValue: {
mode: FieldColorModeId.PaletteClassic, mode: FieldColorModeId.PaletteClassic,
...@@ -85,13 +87,13 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption ...@@ -85,13 +87,13 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
showIf: c => c.drawStyle !== DrawStyle.Points, showIf: c => c.drawStyle !== DrawStyle.Points,
}) })
.addRadio({ .addRadio({
path: 'fillGradient', path: 'gradientMode',
name: 'Fill gradient', name: 'Gradient mode',
defaultValue: graphFieldOptions.fillGradient[0].value, defaultValue: graphFieldOptions.fillGradient[0],
settings: { settings: {
options: graphFieldOptions.fillGradient, options: graphFieldOptions.fillGradient,
}, },
showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0), showIf: c => c.drawStyle !== DrawStyle.Points,
}) })
.addCustomEditor({ .addCustomEditor({
id: 'fillBelowTo', id: 'fillBelowTo',
......
...@@ -12,9 +12,9 @@ import { ...@@ -12,9 +12,9 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui'; import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import { import {
GraphGradientMode,
AxisPlacement, AxisPlacement,
DrawStyle, DrawStyle,
FillGradientMode,
LineInterpolation, LineInterpolation,
LineStyle, LineStyle,
PointVisibility, PointVisibility,
...@@ -251,7 +251,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -251,7 +251,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
} }
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) { if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
graph.fillGradient = FillGradientMode.Opacity; graph.gradientMode = GraphGradientMode.Opacity;
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10 graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
} }
......
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