Commit 8250b59d by Ryan McKinley Committed by GitHub

GraphNG: support fill gradient (#29765)

parent 0c849840
...@@ -165,6 +165,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -165,6 +165,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
pointColor: customConfig.pointColor ?? seriesColor, pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity: customConfig.fillOpacity, fillOpacity: customConfig.fillOpacity,
spanNulls: customConfig.spanNulls || false, spanNulls: customConfig.spanNulls || false,
fillGradient: customConfig.fillGradient,
}); });
if (hasLegend.current) { if (hasLegend.current) {
......
...@@ -133,6 +133,7 @@ export class Sparkline extends PureComponent<Props, State> { ...@@ -133,6 +133,7 @@ export class Sparkline extends PureComponent<Props, State> {
const colorMode = getFieldColorModeForField(field); const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0); const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
const pointsMode = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; const pointsMode = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
builder.addSeries({ builder.addSeries({
scaleKey, scaleKey,
drawStyle: customConfig.drawStyle!, drawStyle: customConfig.drawStyle!,
......
...@@ -8,9 +8,12 @@ import { DataFrame } from '@grafana/data'; ...@@ -8,9 +8,12 @@ import { DataFrame } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
import usePrevious from 'react-use/lib/usePrevious'; import usePrevious from 'react-use/lib/usePrevious';
// uPlot abstraction responsible for plot initialisation, setup and refresh /**
// Receives a data frame that is x-axis aligned, as of https://github.com/leeoniya/uPlot/tree/master/docs#data-format * @internal
// Exposes contexts for plugins registration and uPlot instance access * uPlot abstraction responsible for plot initialisation, setup and refresh
* Receives a data frame that is x-axis aligned, as of https://github.com/leeoniya/uPlot/tree/master/docs#data-format
* Exposes contexts for plugins registration and uPlot instance access
*/
export const UPlotChart: React.FC<PlotProps> = props => { export const UPlotChart: React.FC<PlotProps> = props => {
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);
const plotInstance = useRef<uPlot>(); const plotInstance = useRef<uPlot>();
......
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
/**
* @alpha
*/
export enum AxisPlacement { export enum AxisPlacement {
Auto = 'auto', // First axis on the left, the rest on the right Auto = 'auto', // First axis on the left, the rest on the right
Top = 'top', Top = 'top',
...@@ -9,18 +12,27 @@ export enum AxisPlacement { ...@@ -9,18 +12,27 @@ export enum AxisPlacement {
Hidden = 'hidden', Hidden = 'hidden',
} }
/**
* @alpha
*/
export enum PointVisibility { export enum PointVisibility {
Auto = 'auto', // will show points when the density is low or line is hidden Auto = 'auto', // will show points when the density is low or line is hidden
Never = 'never', Never = 'never',
Always = 'always', Always = 'always',
} }
/**
* @alpha
*/
export enum DrawStyle { export enum DrawStyle {
Line = 'line', // default Line = 'line', // default
Bars = 'bars', // will also have a gap percent Bars = 'bars', // will also have a gap percent
Points = 'points', // Only show points Points = 'points', // Only show points
} }
/**
* @alpha
*/
export enum LineInterpolation { export enum LineInterpolation {
Linear = 'linear', Linear = 'linear',
Smooth = 'smooth', Smooth = 'smooth',
...@@ -28,6 +40,9 @@ export enum LineInterpolation { ...@@ -28,6 +40,9 @@ export enum LineInterpolation {
StepAfter = 'stepAfter', StepAfter = 'stepAfter',
} }
/**
* @alpha
*/
export enum ScaleDistribution { export enum ScaleDistribution {
Linear = 'linear', Linear = 'linear',
Logarithmic = 'log', Logarithmic = 'log',
...@@ -40,6 +55,7 @@ export interface LineConfig { ...@@ -40,6 +55,7 @@ export interface LineConfig {
lineColor?: string; lineColor?: string;
lineWidth?: number; lineWidth?: number;
lineInterpolation?: LineInterpolation; lineInterpolation?: LineInterpolation;
lineDash?: number[];
spanNulls?: boolean; spanNulls?: boolean;
} }
...@@ -49,6 +65,16 @@ export interface LineConfig { ...@@ -49,6 +65,16 @@ export interface LineConfig {
export interface AreaConfig { export interface AreaConfig {
fillColor?: string; fillColor?: string;
fillOpacity?: number; fillOpacity?: number;
fillGradient?: AreaGradientMode;
}
/**
* @alpha
*/
export enum AreaGradientMode {
None = 'none',
Opacity = 'opacity',
Hue = 'hue',
} }
/** /**
...@@ -61,11 +87,18 @@ export interface PointsConfig { ...@@ -61,11 +87,18 @@ export interface PointsConfig {
pointSymbol?: string; // eventually dot,star, etc pointSymbol?: string; // eventually dot,star, etc
} }
/**
* @alpha
*/
export interface ScaleDistributionConfig { export interface ScaleDistributionConfig {
type: ScaleDistribution; type: ScaleDistribution;
log?: number; log?: number;
} }
// Axis is actually unique based on the unit... not each field!
/**
* @alpha
* Axis is actually unique based on the unit... not each field!
*/
export interface AxisConfig { export interface AxisConfig {
axisPlacement?: AxisPlacement; axisPlacement?: AxisPlacement;
axisLabel?: string; axisLabel?: string;
...@@ -80,6 +113,9 @@ export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, ...@@ -80,6 +113,9 @@ export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig,
drawStyle?: DrawStyle; drawStyle?: DrawStyle;
} }
/**
* @alpha
*/
export const graphFieldOptions = { export const graphFieldOptions = {
drawStyle: [ drawStyle: [
{ label: 'Lines', value: DrawStyle.Line }, { label: 'Lines', value: DrawStyle.Line },
...@@ -106,4 +142,10 @@ export const graphFieldOptions = { ...@@ -106,4 +142,10 @@ export const graphFieldOptions = {
{ label: 'Right', value: AxisPlacement.Right }, { label: 'Right', value: AxisPlacement.Right },
{ label: 'Hidden', value: AxisPlacement.Hidden }, { label: 'Hidden', value: AxisPlacement.Hidden },
] as Array<SelectableValue<AxisPlacement>>, ] as Array<SelectableValue<AxisPlacement>>,
fillGradient: [
{ label: 'None', value: undefined },
{ label: 'Opacity', value: AreaGradientMode.Opacity },
{ label: 'Hue', value: AreaGradientMode.Hue },
] as Array<SelectableValue<AreaGradientMode>>,
}; };
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,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 { expect } from '../../../../../../public/test/lib/common';
import { AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config'; import { AreaGradientMode, AxisPlacement, DrawStyle, PointVisibility, ScaleDistribution } from '../config';
describe('UPlotConfigBuilder', () => { describe('UPlotConfigBuilder', () => {
describe('scales config', () => { describe('scales config', () => {
...@@ -266,13 +266,62 @@ describe('UPlotConfigBuilder', () => { ...@@ -266,13 +266,62 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getAxisPlacement('y2')).toBe(AxisPlacement.Right); expect(builder.getAxisPlacement('y2')).toBe(AxisPlacement.Right);
}); });
it('When fillColor is not set fill', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
lineColor: '#0000ff',
});
expect(builder.getConfig().series[1].fill).toBe(undefined);
});
it('When fillOpacity is set', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
lineColor: '#FFAABB',
fillOpacity: 50,
});
expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)');
});
it('When fillColor is set ignore fillOpacity', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
lineColor: '#FFAABB',
fillOpacity: 50,
fillColor: '#FF0000',
});
expect(builder.getConfig().series[1].fill).toBe('#FF0000');
});
it('When fillGradient mode is opacity', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
lineColor: '#FFAABB',
fillOpacity: 50,
fillGradient: AreaGradientMode.Opacity,
});
expect(builder.getConfig().series[1].fill).toBeInstanceOf(Function);
});
it('allows series configuration', () => { it('allows series configuration', () => {
const builder = new UPlotConfigBuilder(); const builder = new UPlotConfigBuilder();
builder.addSeries({ builder.addSeries({
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
scaleKey: 'scale-x', scaleKey: 'scale-x',
fillColor: '#ff0000',
fillOpacity: 50, fillOpacity: 50,
fillGradient: AreaGradientMode.Opacity,
showPoints: PointVisibility.Auto, showPoints: PointVisibility.Auto,
pointSize: 5, pointSize: 5,
pointColor: '#00ff00', pointColor: '#00ff00',
...@@ -299,7 +348,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -299,7 +348,7 @@ describe('UPlotConfigBuilder', () => {
"series": Array [ "series": Array [
Object {}, Object {},
Object { Object {
"fill": "rgba(255, 0, 0, 0.5)", "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#00ff00", "fill": "#00ff00",
......
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import uPlot, { Series } from 'uplot'; import uPlot, { Series } from 'uplot';
import { DrawStyle, LineConfig, AreaConfig, PointsConfig, PointVisibility, LineInterpolation } from '../config'; import { getCanvasContext } from '../../../utils/measureText';
import {
DrawStyle,
LineConfig,
AreaConfig,
PointsConfig,
PointVisibility,
LineInterpolation,
AreaGradientMode,
} from '../config';
import { PlotConfigBuilder } from '../types'; import { PlotConfigBuilder } from '../types';
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig { export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
...@@ -18,8 +27,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -18,8 +27,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
showPoints, showPoints,
pointColor, pointColor,
pointSize, pointSize,
fillColor,
fillOpacity,
scaleKey, scaleKey,
spanNulls, spanNulls,
} = this.props; } = this.props;
...@@ -56,31 +63,41 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -56,31 +63,41 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
pointsConfig.points!.show = true; pointsConfig.points!.show = true;
} }
let fillConfig: any | undefined;
let fillOpacityNumber = fillOpacity ?? 0;
if (fillColor) {
fillConfig = {
fill: fillColor,
};
}
if (fillOpacityNumber !== 0) {
fillConfig = {
fill: tinycolor(fillColor ?? lineColor)
.setAlpha(fillOpacityNumber / 100)
.toRgbString(),
};
}
return { return {
scale: scaleKey, scale: scaleKey,
spanGaps: spanNulls, spanGaps: spanNulls,
fill: this.getFill(),
...lineConfig, ...lineConfig,
...pointsConfig, ...pointsConfig,
...fillConfig,
}; };
} }
getFill(): Series.Fill | undefined {
const { lineColor, fillColor, fillGradient, fillOpacity } = this.props;
if (fillColor) {
return fillColor;
}
const mode = fillGradient ?? AreaGradientMode.None;
let fillOpacityNumber = fillOpacity ?? 0;
if (mode !== AreaGradientMode.None) {
return getCanvasGradient({
color: (fillColor ?? lineColor)!,
opacity: fillOpacityNumber / 100,
mode,
});
}
if (fillOpacityNumber > 0) {
return tinycolor(lineColor)
.setAlpha(fillOpacityNumber / 100)
.toString();
}
return undefined;
}
} }
interface PathBuilders { interface PathBuilders {
...@@ -130,3 +147,50 @@ function mapDrawStyleToPathBuilder( ...@@ -130,3 +147,50 @@ function mapDrawStyleToPathBuilder(
return builders.linear; // the default return builders.linear; // the default
} }
interface AreaGradientOptions {
color: string;
mode: AreaGradientMode;
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 AreaGradientMode.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 AreaGradientMode.Opacity:
default:
gradient.addColorStop(
0,
tinycolor(color)
.setAlpha(opacity)
.toRgbString()
);
gradient.addColorStop(
1,
tinycolor(color)
.setAlpha(0)
.toRgbString()
);
return gradient;
}
};
}
...@@ -2,6 +2,22 @@ let canvas: HTMLCanvasElement | null = null; ...@@ -2,6 +2,22 @@ let canvas: HTMLCanvasElement | null = null;
const cache: Record<string, TextMetrics> = {}; const cache: Record<string, TextMetrics> = {};
/** /**
* @internal
*/
export function getCanvasContext() {
if (canvas === null) {
canvas = document.createElement('canvas');
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create context');
}
return context;
}
/**
* @beta * @beta
*/ */
export function measureText(text: string, fontSize: number): TextMetrics { export function measureText(text: string, fontSize: number): TextMetrics {
...@@ -13,14 +29,7 @@ export function measureText(text: string, fontSize: number): TextMetrics { ...@@ -13,14 +29,7 @@ export function measureText(text: string, fontSize: number): TextMetrics {
return fromCache; return fromCache;
} }
if (canvas === null) { const context = getCanvasContext();
canvas = document.createElement('canvas');
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create context');
}
context.font = fontStyle; context.font = fontStyle;
const metrics = context.measureText(text); const metrics = context.measureText(text);
......
...@@ -33,7 +33,8 @@ Object { ...@@ -33,7 +33,8 @@ Object {
"custom": Object { "custom": Object {
"axisPlacement": "hidden", "axisPlacement": "hidden",
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 50, "fillGradient": "opacity",
"fillOpacity": 60,
"lineInterpolation": "stepAfter", "lineInterpolation": "stepAfter",
"lineWidth": 1, "lineWidth": 1,
"pointSize": 6, "pointSize": 6,
......
...@@ -11,7 +11,13 @@ import { ...@@ -11,7 +11,13 @@ import {
FieldColorModeId, FieldColorModeId,
} from '@grafana/data'; } from '@grafana/data';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui'; import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import { AxisPlacement, DrawStyle, LineInterpolation, PointVisibility } from '@grafana/ui/src/components/uPlot/config'; import {
AreaGradientMode,
AxisPlacement,
DrawStyle,
LineInterpolation,
PointVisibility,
} from '@grafana/ui/src/components/uPlot/config';
import { Options } from './types'; import { Options } from './types';
import omitBy from 'lodash/omitBy'; import omitBy from 'lodash/omitBy';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
...@@ -112,6 +118,18 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -112,6 +118,18 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
value: v * 10, // was 0-10, new graph is 0 - 100 value: v * 10, // was 0-10, new graph is 0 - 100
}); });
break; break;
case 'fillGradient':
if (v) {
rule.properties.push({
id: 'custom.fillGradient',
value: 'opacity', // was 0-10
});
rule.properties.push({
id: 'custom.fillOpacity',
value: v * 10, // was 0-10, new graph is 0 - 100
});
}
break;
case 'points': case 'points':
rule.properties.push({ rule.properties.push({
id: 'custom.showPoints', id: 'custom.showPoints',
...@@ -159,6 +177,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -159,6 +177,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
const graph = y1.custom ?? ({} as GraphFieldConfig); const graph = y1.custom ?? ({} as GraphFieldConfig);
graph.drawStyle = angular.bars ? DrawStyle.Bars : angular.lines ? DrawStyle.Line : DrawStyle.Points; graph.drawStyle = angular.bars ? DrawStyle.Bars : angular.lines ? DrawStyle.Line : DrawStyle.Points;
if (angular.points) { if (angular.points) {
graph.showPoints = PointVisibility.Always; graph.showPoints = PointVisibility.Always;
} else if (graph.drawStyle !== DrawStyle.Points) { } else if (graph.drawStyle !== DrawStyle.Points) {
...@@ -166,19 +185,30 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -166,19 +185,30 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
} }
graph.lineWidth = angular.linewidth; graph.lineWidth = angular.linewidth;
if (isNumber(angular.pointradius)) { if (isNumber(angular.pointradius)) {
graph.pointSize = 2 + angular.pointradius * 2; graph.pointSize = 2 + angular.pointradius * 2;
} }
if (isNumber(angular.fill)) { if (isNumber(angular.fill)) {
graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100 graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100
} }
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
graph.fillGradient = AreaGradientMode.Opacity;
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
}
graph.spanNulls = angular.nullPointMode === NullValueMode.Null; graph.spanNulls = angular.nullPointMode === NullValueMode.Null;
if (angular.steppedLine) { if (angular.steppedLine) {
graph.lineInterpolation = LineInterpolation.StepAfter; graph.lineInterpolation = LineInterpolation.StepAfter;
} }
if (graph.drawStyle === DrawStyle.Bars) { if (graph.drawStyle === DrawStyle.Bars) {
graph.fillOpacity = 1.0; // bars were always graph.fillOpacity = 1.0; // bars were always
} }
y1.custom = omitBy(graph, isNil); y1.custom = omitBy(graph, isNil);
y1.nullValueMode = angular.nullPointMode as NullValueMode; y1.nullValueMode = angular.nullPointMode as NullValueMode;
......
...@@ -75,6 +75,15 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel) ...@@ -75,6 +75,15 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
showIf: c => c.drawStyle !== DrawStyle.Points, showIf: c => c.drawStyle !== DrawStyle.Points,
}) })
.addRadio({ .addRadio({
path: 'fillGradient',
name: 'Fill gradient',
defaultValue: graphFieldOptions.fillGradient[0],
settings: {
options: graphFieldOptions.fillGradient,
},
showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0),
})
.addRadio({
path: 'spanNulls', path: 'spanNulls',
name: 'Null values', name: 'Null values',
defaultValue: false, defaultValue: false,
......
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