1import React, { PureComponent } from 'react'; 2import { AlignedData, Range } from 'uplot'; 3import { 4 compareDataFrameStructures, 5 DataFrame, 6 Field, 7 FieldConfig, 8 FieldSparkline, 9 FieldType, 10 getFieldColorModeForField, 11} from '@grafana/data'; 12import { 13 AxisPlacement, 14 GraphDrawStyle, 15 GraphFieldConfig, 16 VisibilityMode, 17 ScaleDirection, 18 ScaleOrientation, 19} from '@grafana/schema'; 20import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; 21import { UPlotChart } from '../uPlot/Plot'; 22import { Themeable2 } from '../../types'; 23import { preparePlotData } from '../uPlot/utils'; 24import { preparePlotFrame } from './utils'; 25import { isEqual } from 'lodash'; 26 27export interface SparklineProps extends Themeable2 { 28 width: number; 29 height: number; 30 config?: FieldConfig<GraphFieldConfig>; 31 sparkline: FieldSparkline; 32} 33 34interface State { 35 data: AlignedData; 36 alignedDataFrame: DataFrame; 37 configBuilder: UPlotConfigBuilder; 38} 39 40const defaultConfig: GraphFieldConfig = { 41 drawStyle: GraphDrawStyle.Line, 42 showPoints: VisibilityMode.Auto, 43 axisPlacement: AxisPlacement.Hidden, 44}; 45 46export class Sparkline extends PureComponent<SparklineProps, State> { 47 constructor(props: SparklineProps) { 48 super(props); 49 50 const alignedDataFrame = preparePlotFrame(props.sparkline, props.config); 51 52 this.state = { 53 data: preparePlotData([alignedDataFrame]), 54 alignedDataFrame, 55 configBuilder: this.prepareConfig(alignedDataFrame), 56 }; 57 } 58 59 static getDerivedStateFromProps(props: SparklineProps, state: State) { 60 const frame = preparePlotFrame(props.sparkline, props.config); 61 if (!frame) { 62 return { ...state }; 63 } 64 65 return { 66 ...state, 67 data: preparePlotData([frame]), 68 alignedDataFrame: frame, 69 }; 70 } 71 72 componentDidUpdate(prevProps: SparklineProps, prevState: State) { 73 const { alignedDataFrame } = this.state; 74 75 if (!alignedDataFrame) { 76 return; 77 } 78 79 let rebuildConfig = false; 80 81 if (prevProps.sparkline !== this.props.sparkline) { 82 rebuildConfig = !compareDataFrameStructures(this.state.alignedDataFrame, prevState.alignedDataFrame); 83 } else { 84 rebuildConfig = !isEqual(prevProps.config, this.props.config); 85 } 86 87 if (rebuildConfig) { 88 this.setState({ configBuilder: this.prepareConfig(alignedDataFrame) }); 89 } 90 } 91 92 getYRange(field: Field) { 93 let { min, max } = this.state.alignedDataFrame.fields[1].state?.range!; 94 95 if (min === max) { 96 if (min === 0) { 97 max = 100; 98 } else { 99 min = 0; 100 max! *= 2; 101 } 102 } 103 104 return [ 105 Math.max(min!, field.config.min ?? -Infinity), 106 Math.min(max!, field.config.max ?? Infinity), 107 ] as Range.MinMax; 108 } 109 110 prepareConfig(data: DataFrame) { 111 const { theme } = this.props; 112 const builder = new UPlotConfigBuilder(); 113 114 builder.setCursor({ 115 show: false, 116 x: false, // no crosshairs 117 y: false, 118 }); 119 120 // X is the first field in the alligned frame 121 const xField = data.fields[0]; 122 builder.addScale({ 123 scaleKey: 'x', 124 orientation: ScaleOrientation.Horizontal, 125 direction: ScaleDirection.Right, 126 isTime: false, //xField.type === FieldType.time, 127 range: () => { 128 const { sparkline } = this.props; 129 if (sparkline.x) { 130 if (sparkline.timeRange && sparkline.x.type === FieldType.time) { 131 return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()]; 132 } 133 const vals = sparkline.x.values; 134 return [vals.get(0), vals.get(vals.length - 1)]; 135 } 136 return [0, sparkline.y.values.length - 1]; 137 }, 138 }); 139 140 builder.addAxis({ 141 scaleKey: 'x', 142 theme, 143 placement: AxisPlacement.Hidden, 144 }); 145 146 for (let i = 0; i < data.fields.length; i++) { 147 const field = data.fields[i]; 148 const config = field.config as FieldConfig<GraphFieldConfig>; 149 const customConfig: GraphFieldConfig = { 150 ...defaultConfig, 151 ...config.custom, 152 }; 153 154 if (field === xField || field.type !== FieldType.number) { 155 continue; 156 } 157 158 const scaleKey = config.unit || '__fixed'; 159 builder.addScale({ 160 scaleKey, 161 orientation: ScaleOrientation.Vertical, 162 direction: ScaleDirection.Up, 163 range: () => this.getYRange(field), 164 }); 165 166 builder.addAxis({ 167 scaleKey, 168 theme, 169 placement: AxisPlacement.Hidden, 170 }); 171 172 const colorMode = getFieldColorModeForField(field); 173 const seriesColor = colorMode.getCalculator(field, theme)(0, 0); 174 const pointsMode = 175 customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; 176 177 builder.addSeries({ 178 pxAlign: false, 179 scaleKey, 180 theme, 181 drawStyle: customConfig.drawStyle!, 182 lineColor: customConfig.lineColor ?? seriesColor, 183 lineWidth: customConfig.lineWidth, 184 lineInterpolation: customConfig.lineInterpolation, 185 showPoints: pointsMode, 186 pointSize: customConfig.pointSize, 187 fillOpacity: customConfig.fillOpacity, 188 fillColor: customConfig.fillColor ?? seriesColor, 189 }); 190 } 191 192 return builder; 193 } 194 195 render() { 196 const { data, configBuilder } = this.state; 197 const { width, height, sparkline } = this.props; 198 return ( 199 <UPlotChart data={data} config={configBuilder} width={width} height={height} timeRange={sparkline.timeRange!} /> 200 ); 201 } 202} 203