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> = ({
pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity: customConfig.fillOpacity,
spanNulls: customConfig.spanNulls || false,
fillGradient: customConfig.fillGradient,
});
if (hasLegend.current) {
......
......@@ -133,6 +133,7 @@ export class Sparkline extends PureComponent<Props, State> {
const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
const pointsMode = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
builder.addSeries({
scaleKey,
drawStyle: customConfig.drawStyle!,
......
......@@ -8,9 +8,12 @@ import { DataFrame } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
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
// Exposes contexts for plugins registration and uPlot instance access
/**
* @internal
* 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 => {
const canvasRef = useRef<HTMLDivElement>(null);
const plotInstance = useRef<uPlot>();
......
import { SelectableValue } from '@grafana/data';
/**
* @alpha
*/
export enum AxisPlacement {
Auto = 'auto', // First axis on the left, the rest on the right
Top = 'top',
......@@ -9,18 +12,27 @@ export enum AxisPlacement {
Hidden = 'hidden',
}
/**
* @alpha
*/
export enum PointVisibility {
Auto = 'auto', // will show points when the density is low or line is hidden
Never = 'never',
Always = 'always',
}
/**
* @alpha
*/
export enum DrawStyle {
Line = 'line', // default
Bars = 'bars', // will also have a gap percent
Points = 'points', // Only show points
}
/**
* @alpha
*/
export enum LineInterpolation {
Linear = 'linear',
Smooth = 'smooth',
......@@ -28,6 +40,9 @@ export enum LineInterpolation {
StepAfter = 'stepAfter',
}
/**
* @alpha
*/
export enum ScaleDistribution {
Linear = 'linear',
Logarithmic = 'log',
......@@ -40,6 +55,7 @@ export interface LineConfig {
lineColor?: string;
lineWidth?: number;
lineInterpolation?: LineInterpolation;
lineDash?: number[];
spanNulls?: boolean;
}
......@@ -49,6 +65,16 @@ export interface LineConfig {
export interface AreaConfig {
fillColor?: string;
fillOpacity?: number;
fillGradient?: AreaGradientMode;
}
/**
* @alpha
*/
export enum AreaGradientMode {
None = 'none',
Opacity = 'opacity',
Hue = 'hue',
}
/**
......@@ -61,11 +87,18 @@ export interface PointsConfig {
pointSymbol?: string; // eventually dot,star, etc
}
/**
* @alpha
*/
export interface ScaleDistributionConfig {
type: ScaleDistribution;
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 {
axisPlacement?: AxisPlacement;
axisLabel?: string;
......@@ -80,6 +113,9 @@ export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig,
drawStyle?: DrawStyle;
}
/**
* @alpha
*/
export const graphFieldOptions = {
drawStyle: [
{ label: 'Lines', value: DrawStyle.Line },
......@@ -106,4 +142,10 @@ export const graphFieldOptions = {
{ label: 'Right', value: AxisPlacement.Right },
{ label: 'Hidden', value: AxisPlacement.Hidden },
] 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 @@
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
import { GrafanaTheme } from '@grafana/data';
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('scales config', () => {
......@@ -266,13 +266,62 @@ describe('UPlotConfigBuilder', () => {
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', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
fillColor: '#ff0000',
fillOpacity: 50,
fillGradient: AreaGradientMode.Opacity,
showPoints: PointVisibility.Auto,
pointSize: 5,
pointColor: '#00ff00',
......@@ -299,7 +348,7 @@ describe('UPlotConfigBuilder', () => {
"series": Array [
Object {},
Object {
"fill": "rgba(255, 0, 0, 0.5)",
"fill": [Function],
"paths": [Function],
"points": Object {
"fill": "#00ff00",
......
import tinycolor from 'tinycolor2';
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';
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
......@@ -18,8 +27,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
showPoints,
pointColor,
pointSize,
fillColor,
fillOpacity,
scaleKey,
spanNulls,
} = this.props;
......@@ -56,30 +63,40 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
pointsConfig.points!.show = true;
}
let fillConfig: any | undefined;
let fillOpacityNumber = fillOpacity ?? 0;
return {
scale: scaleKey,
spanGaps: spanNulls,
fill: this.getFill(),
...lineConfig,
...pointsConfig,
};
}
getFill(): Series.Fill | undefined {
const { lineColor, fillColor, fillGradient, fillOpacity } = this.props;
if (fillColor) {
fillConfig = {
fill: 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) {
fillConfig = {
fill: tinycolor(fillColor ?? lineColor)
if (fillOpacityNumber > 0) {
return tinycolor(lineColor)
.setAlpha(fillOpacityNumber / 100)
.toRgbString(),
};
.toString();
}
return {
scale: scaleKey,
spanGaps: spanNulls,
...lineConfig,
...pointsConfig,
...fillConfig,
};
return undefined;
}
}
......@@ -130,3 +147,50 @@ function mapDrawStyleToPathBuilder(
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;
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
*/
export function measureText(text: string, fontSize: number): TextMetrics {
......@@ -13,14 +29,7 @@ export function measureText(text: string, fontSize: number): TextMetrics {
return fromCache;
}
if (canvas === null) {
canvas = document.createElement('canvas');
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create context');
}
const context = getCanvasContext();
context.font = fontStyle;
const metrics = context.measureText(text);
......
......@@ -33,7 +33,8 @@ Object {
"custom": Object {
"axisPlacement": "hidden",
"drawStyle": "line",
"fillOpacity": 50,
"fillGradient": "opacity",
"fillOpacity": 60,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 6,
......
......@@ -11,7 +11,13 @@ import {
FieldColorModeId,
} from '@grafana/data';
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 omitBy from 'lodash/omitBy';
import isNil from 'lodash/isNil';
......@@ -112,6 +118,18 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
value: v * 10, // was 0-10, new graph is 0 - 100
});
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':
rule.properties.push({
id: 'custom.showPoints',
......@@ -159,6 +177,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
const graph = y1.custom ?? ({} as GraphFieldConfig);
graph.drawStyle = angular.bars ? DrawStyle.Bars : angular.lines ? DrawStyle.Line : DrawStyle.Points;
if (angular.points) {
graph.showPoints = PointVisibility.Always;
} else if (graph.drawStyle !== DrawStyle.Points) {
......@@ -166,19 +185,30 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}
graph.lineWidth = angular.linewidth;
if (isNumber(angular.pointradius)) {
graph.pointSize = 2 + angular.pointradius * 2;
}
if (isNumber(angular.fill)) {
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;
if (angular.steppedLine) {
graph.lineInterpolation = LineInterpolation.StepAfter;
}
if (graph.drawStyle === DrawStyle.Bars) {
graph.fillOpacity = 1.0; // bars were always
}
y1.custom = omitBy(graph, isNil);
y1.nullValueMode = angular.nullPointMode as NullValueMode;
......
......@@ -75,6 +75,15 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
showIf: c => c.drawStyle !== DrawStyle.Points,
})
.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',
name: 'Null values',
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