Commit 3ce93050 by Ryan McKinley Committed by GitHub

GraphNG: replace bizcharts with uPlot for sparklines (#29632)

parent 0e0ab8c9
......@@ -6,7 +6,7 @@ import { DataFrame } from '../types/dataFrame';
*
* To compare multiple frames use:
* ```
* areArraysEqual(a, b, framesHaveSameStructure);
* compareArrayValues(a, b, framesHaveSameStructure);
* ```
* NOTE: this does a shallow check on the FieldConfig properties, when using the query
* editor, this should be sufficient, however if applicaitons are mutating properties
......
......@@ -2,7 +2,6 @@ import toString from 'lodash/toString';
import isEmpty from 'lodash/isEmpty';
import { getDisplayProcessor } from './displayProcessor';
import { getFlotPairs } from '../utils/flotPairs';
import {
DataFrame,
DisplayValue,
......@@ -13,10 +12,10 @@ import {
FieldType,
InterpolateFunction,
LinkModel,
TimeRange,
TimeZone,
} from '../types';
import { DataFrameView } from '../dataframe/DataFrameView';
import { GraphSeriesValue } from '../types/graph';
import { GrafanaTheme } from '../types/theme';
import { reduceField, ReducerID } from '../transformations/fieldReducer';
import { ScopedVars } from '../types/ScopedVars';
......@@ -56,11 +55,18 @@ function getTitleTemplate(stats: string[]): string {
return parts.join(' ');
}
export interface FieldSparkline {
y: Field; // Y values
x?: Field; // if this does not exist, use the index
timeRange?: TimeRange; // Optionally force an absolute time
highlightIndex?: number;
}
export interface FieldDisplay {
name: string; // The field name (title is in display)
field: FieldConfig;
display: DisplayValue;
sparkline?: GraphSeriesValue[][];
sparkline?: FieldSparkline;
// Expose to the original values for delayed inspection (DataLinks etc)
view?: DataFrameView;
......@@ -185,15 +191,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
field,
reducers: calcs, // The stats to calculate
});
let sparkline: GraphSeriesValue[][] | undefined = undefined;
// Single sparkline for every reducer
if (options.sparkline && timeField) {
sparkline = getFlotPairs({
xField: timeField,
yField: series.fields[i],
});
}
for (const calc of calcs) {
scopedVars[VAR_CALC] = { value: calc, text: calc };
......@@ -203,6 +200,19 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
...scopedVars,
});
let sparkline: FieldSparkline | undefined = undefined;
if (options.sparkline) {
sparkline = {
y: series.fields[i],
x: timeField,
};
if (calc === ReducerID.last) {
sparkline.highlightIndex = sparkline.y.values.length - 1;
} else if (calc === ReducerID.first) {
sparkline.highlightIndex = 0;
}
}
values.push({
name: calc,
field: config,
......
import { Field, FieldType } from '../types';
import { FunctionalVector } from './FunctionalVector';
/**
* IndexVector is a simple vector implementation that returns the index value
* for each element in the vector. It is functionally equivolant a vector backed
* by an array with values: `[0,1,2,...,length-1]`
*/
export class IndexVector extends FunctionalVector<number> {
constructor(private len: number) {
super();
}
get length() {
return this.len;
}
get(index: number): number {
return index;
}
/**
* Returns a field representing the range [0 ... length-1]
*/
static newField(len: number): Field<number> {
return {
name: '',
values: new IndexVector(len),
type: FieldType.number,
config: {
min: 0,
max: len - 1,
},
};
}
}
......@@ -5,5 +5,6 @@ export * from './ConstantVector';
export * from './BinaryOperationVector';
export * from './SortedVector';
export * from './FormattedVector';
export * from './IndexVector';
export { vectorator } from './FunctionalVector';
......@@ -117,7 +117,7 @@ module.exports = {
minimize: isProductionBuild,
minimizer: isProductionBuild
? [
new TerserPlugin({ cache: false, parallel: false, sourceMap: false, exclude: /monaco|bizcharts/ }),
new TerserPlugin({ cache: false, parallel: false, sourceMap: false, exclude: /monaco/ }),
new OptimizeCSSAssetsPlugin({}),
]
: [],
......
......@@ -41,7 +41,6 @@
"@types/react-table": "7.0.12",
"@types/slate": "0.47.1",
"@types/slate-react": "0.22.5",
"bizcharts": "^3.5.8",
"classnames": "2.2.6",
"d3": "5.15.0",
"emotion": "10.0.27",
......
......@@ -4,6 +4,7 @@ import { BigValue, BigValueColorMode, BigValueGraphMode, BigValueJustifyMode, Bi
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './BigValue.mdx';
import { useTheme } from '../../themes';
import { ArrayVector, FieldSparkline, FieldType } from '@grafana/data';
const getKnobs = () => {
return {
......@@ -37,17 +38,13 @@ export default {
export const Basic = () => {
const { value, title, colorMode, graphMode, height, width, color, textMode, justifyMode } = getKnobs();
const theme = useTheme();
const sparkline = {
xMin: 0,
xMax: 5,
data: [
[0, 10],
[1, 20],
[2, 15],
[3, 25],
[4, 5],
[5, 10],
],
const sparkline: FieldSparkline = {
y: {
name: '',
values: new ArrayVector([1, 2, 3, 4, 3]),
type: FieldType.number,
config: {},
},
};
return (
......
// Library
import React, { PureComponent } from 'react';
import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors, TextDisplayOptions } from '@grafana/data';
import { DisplayValue, DisplayValueAlignmentFactors, FieldSparkline, TextDisplayOptions } from '@grafana/data';
// Types
import { Themeable } from '../../types';
import { buildLayout } from './BigValueLayout';
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
export interface BigValueSparkline {
data: GraphSeriesValue[][];
xMin?: number | null;
xMax?: number | null;
yMin?: number | null;
yMax?: number | null;
highlightIndex?: number;
}
export enum BigValueColorMode {
Value = 'value',
Background = 'background',
......@@ -51,7 +42,7 @@ export interface Props extends Themeable {
/** Value displayed as Big Value */
value: DisplayValue;
/** Sparkline values for showing a graph under/behind the value */
sparkline?: BigValueSparkline;
sparkline?: FieldSparkline;
/** onClick handler for the value */
onClick?: React.MouseEventHandler<HTMLElement>;
/** Custom styling */
......
import { Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { buildLayout, StackedWithChartLayout, WideWithChartLayout } from './BigValueLayout';
import { getTheme } from '../../themes';
import { ArrayVector, FieldType } from '@grafana/data';
function getProps(propOverrides?: Partial<Props>): Props {
const props: Props = {
......@@ -13,12 +14,12 @@ function getProps(propOverrides?: Partial<Props>): Props {
numeric: 25,
},
sparkline: {
data: [
[10, 10],
[10, 10],
],
xMin: 0,
xMax: 100,
y: {
name: '',
values: new ArrayVector([1, 2, 3, 4, 3]),
type: FieldType.number,
config: {},
},
},
theme: getTheme(),
};
......
// Libraries
import React, { CSSProperties } from 'react';
import tinycolor from 'tinycolor2';
import { Chart, Geom } from 'bizcharts';
// Utils
import { formattedValueToString, DisplayValue, getColorForTheme } from '@grafana/data';
import { formattedValueToString, DisplayValue, getColorForTheme, FieldConfig } from '@grafana/data';
import { calculateFontSize } from '../../utils/measureText';
// Types
import { BigValueColorMode, Props, BigValueJustifyMode, BigValueTextMode } from './BigValue';
import { getTextColorForBackground } from '../../utils';
import { DrawStyle, GraphFieldConfig } from '../uPlot/config';
import { Sparkline } from '../Sparkline/Sparkline';
const LINE_HEIGHT = 1.2;
const MAX_TITLE_SIZE = 30;
......@@ -148,65 +149,12 @@ export abstract class BigValueLayout {
}
renderChart(): JSX.Element | null {
const { sparkline } = this.props;
const { sparkline, colorMode } = this.props;
if (!sparkline || sparkline.data.length === 0) {
if (!sparkline || !sparkline.y) {
return null;
}
const data = sparkline.data.map(values => {
return { time: values[0], value: values[1], name: 'A' };
});
const scales = {
time: {
type: 'time',
min: sparkline.xMin,
max: sparkline.xMax,
},
value: {
min: sparkline.yMin,
max: sparkline.yMax,
},
};
if (sparkline.xMax && sparkline.xMin) {
// Having the last data point align with the edge of the panel looks good
// So if it's close adjust time.max to the last data point time
const timeDelta = sparkline.xMax - sparkline.xMin;
const lastDataPointTime = data[data.length - 1].time || 0;
const lastTimeDiffFromMax = Math.abs(sparkline.xMax - lastDataPointTime);
// if last data point is just 5% or lower from the edge adjust it
if (lastTimeDiffFromMax / timeDelta < 0.05) {
scales.time.max = lastDataPointTime;
}
}
return (
<Chart
height={this.chartHeight}
width={this.chartWidth}
data={data}
animate={false}
padding={[4, 0, 0, 0]}
scale={scales}
style={this.getChartStyles()}
>
{this.renderGeom()}
</Chart>
);
}
renderGeom(): JSX.Element {
const { colorMode } = this.props;
const lineStyle: any = {
opacity: 1,
fillOpacity: 1,
lineWidth: 2,
};
let fillColor: string;
let lineColor: string;
......@@ -224,16 +172,28 @@ export abstract class BigValueLayout {
.toRgbString();
}
lineStyle.stroke = lineColor;
// The graph field configuration applied to Y values
const config: FieldConfig<GraphFieldConfig> = {
custom: {
drawStyle: DrawStyle.Line,
lineWidth: 1,
fillColor,
lineColor,
},
};
return (
<>
<Geom type="area" position="time*value" size={0} color={fillColor} style={lineStyle} shape="smooth" />
<Geom type="line" position="time*value" size={1} color={lineColor} style={lineStyle} shape="smooth" />
</>
<div style={this.getChartStyles()}>
<Sparkline
height={this.chartHeight}
width={this.chartWidth}
sparkline={sparkline}
config={config}
theme={this.props.theme}
/>
</div>
);
}
getChartStyles(): CSSProperties {
return {
position: 'absolute',
......
import React, { PureComponent } from 'react';
import {
compareDataFrameStructures,
DefaultTimeZone,
FieldSparkline,
IndexVector,
DataFrame,
FieldType,
getFieldColorModeForField,
FieldConfig,
} from '@grafana/data';
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { UPlotChart } from '../uPlot/Plot';
import { Themeable } from '../../types';
export interface Props extends Themeable {
width: number;
height: number;
config?: FieldConfig<GraphFieldConfig>;
sparkline: FieldSparkline;
}
interface State {
data: DataFrame;
configBuilder: UPlotConfigBuilder;
}
const defaultConfig: GraphFieldConfig = {
drawStyle: DrawStyle.Line,
showPoints: PointVisibility.Auto,
axisPlacement: AxisPlacement.Hidden,
};
export class Sparkline extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const data = this.prepareData(props);
this.state = {
data,
configBuilder: this.prepareConfig(data, props),
};
}
componentDidUpdate(oldProps: Props) {
if (oldProps.sparkline !== this.props.sparkline) {
const data = this.prepareData(this.props);
if (!compareDataFrameStructures(this.state.data, data)) {
const configBuilder = this.prepareConfig(data, this.props);
this.setState({ data, configBuilder });
} else {
this.setState({ data });
}
}
}
prepareData(props: Props): DataFrame {
const { sparkline } = props;
const length = sparkline.y.values.length;
const yFieldConfig = {
...sparkline.y.config,
...this.props.config,
};
return {
refId: 'sparkline',
fields: [
sparkline.x ?? IndexVector.newField(length),
{
...sparkline.y,
config: yFieldConfig,
},
],
length,
};
}
prepareConfig(data: DataFrame, props: Props) {
const { theme } = this.props;
const builder = new UPlotConfigBuilder();
builder.setCursor({
show: true,
x: false, // no crosshairs
y: false,
});
// X is the first field in the alligned frame
const xField = data.fields[0];
builder.addScale({
scaleKey: 'x',
isTime: false, //xField.type === FieldType.time,
range: () => {
const { sparkline } = this.props;
if (sparkline.x) {
if (sparkline.timeRange && sparkline.x.type === FieldType.time) {
return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()];
}
const vals = sparkline.x.values;
return [vals.get(0), vals.get(vals.length - 1)];
}
return [0, sparkline.y.values.length - 1];
},
});
builder.addAxis({
scaleKey: 'x',
theme,
placement: AxisPlacement.Hidden,
});
for (let i = 0; i < data.fields.length; i++) {
const field = data.fields[i];
const config = field.config as FieldConfig<GraphFieldConfig>;
const customConfig: GraphFieldConfig = {
...defaultConfig,
...config.custom,
};
if (field === xField || field.type !== FieldType.number) {
continue;
}
const scaleKey = config.unit || '__fixed';
builder.addScale({ scaleKey, min: field.config.min, max: field.config.max });
builder.addAxis({
scaleKey,
theme,
placement: AxisPlacement.Hidden,
});
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!,
lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
showPoints: pointsMode,
pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity: customConfig.fillOpacity,
fillColor: customConfig.fillColor ?? seriesColor,
});
}
return builder;
}
render() {
const { data, configBuilder } = this.state;
const { width, height, sparkline } = this.props;
return (
<UPlotChart
data={{
frame: data,
isGap: () => true, // any null is a gap
}}
config={configBuilder}
width={width}
height={height}
timeRange={sparkline.timeRange!}
timeZone={DefaultTimeZone}
/>
);
}
}
......@@ -63,7 +63,6 @@ export { Counter } from './Tabs/Counter';
export {
BigValue,
BigValueColorMode,
BigValueSparkline,
BigValueGraphMode,
BigValueJustifyMode,
BigValueTextMode,
......
......@@ -3,11 +3,13 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { AxisPlacement } from '../config';
import { Cursor } from 'uplot';
export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = [];
private axes: Record<string, UPlotAxisBuilder> = {};
private scales: UPlotScaleBuilder[] = [];
private cursor: Cursor | undefined;
hasLeftAxis = false;
......@@ -28,6 +30,11 @@ export class UPlotConfigBuilder {
this.hasLeftAxis = true;
}
if (props.placement === AxisPlacement.Hidden) {
props.show = false;
props.size = 0;
}
this.axes[props.scaleKey] = new UPlotAxisBuilder(props);
}
......@@ -36,6 +43,10 @@ export class UPlotConfigBuilder {
return axis?.props.placement! ?? AxisPlacement.Left;
}
setCursor(cursor?: Cursor) {
this.cursor = cursor;
}
addSeries(props: SeriesProps) {
this.series.push(new UPlotSeriesBuilder(props));
}
......@@ -57,7 +68,9 @@ export class UPlotConfigBuilder {
config.scales = this.scales.reduce((acc, s) => {
return { ...acc, ...s.getConfig() };
}, {});
if (this.cursor) {
config.cursor = this.cursor;
}
return config;
}
}
......@@ -77,21 +77,23 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
pointsConfig.points!.show = true;
}
const areaConfig =
fillOpacity !== undefined
? {
fill: tinycolor(fillColor)
let fillConfig: any | undefined;
if (fillColor && fillOpacity !== 0) {
fillConfig = {
fill: fillOpacity
? tinycolor(fillColor)
.setAlpha(fillOpacity)
.toRgbString(),
}
: { fill: undefined };
.toRgbString()
: fillColor,
};
}
return {
scale: scaleKey,
spanGaps: spanNulls,
...lineConfig,
...pointsConfig,
...areaConfig,
...fillConfig,
};
}
}
......@@ -3,7 +3,7 @@ import uPlot, { Options, Series, Hooks } from 'uplot';
import { DataFrame, TimeRange, TimeZone } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes'>;
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor'>;
export type PlotPlugin = {
id: string;
/** can mutate provided opts as necessary */
......
......@@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
import {
BigValue,
BigValueGraphMode,
BigValueSparkline,
DataLinksContextMenu,
VizRepeater,
VizRepeaterRenderValueProps,
......@@ -14,7 +13,6 @@ import {
getDisplayValueAlignmentFactors,
getFieldDisplayValues,
PanelProps,
ReducerID,
} from '@grafana/data';
import { config } from 'app/core/config';
......@@ -29,21 +27,9 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
const { timeRange, options } = this.props;
const { value, alignmentFactors, width, height, count } = valueProps;
const { openMenu, targetClassName } = menuProps;
let sparkline: BigValueSparkline | undefined;
if (value.sparkline) {
sparkline = {
data: value.sparkline,
xMin: timeRange.from.valueOf(),
xMax: timeRange.to.valueOf(),
yMin: value.field.min,
yMax: value.field.max,
};
const calc = options.reduceOptions.calcs[0];
if (calc === ReducerID.last) {
sparkline.highlightIndex = sparkline.data.length - 1;
}
let sparkline = value.sparkline;
if (sparkline) {
sparkline.timeRange = timeRange;
}
return (
......
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