Histogram.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import React from 'react';
  2. import uPlot, { AlignedData } from 'uplot';
  3. import {
  4. DataFrame,
  5. formattedValueToString,
  6. getFieldColorModeForField,
  7. getFieldSeriesColor,
  8. GrafanaTheme2,
  9. } from '@grafana/data';
  10. import {
  11. histogramBucketSizes,
  12. histogramFrameBucketMaxFieldName,
  13. } from '@grafana/data/src/transformations/transformers/histogram';
  14. import {
  15. VizLegendOptions,
  16. LegendDisplayMode,
  17. ScaleDistribution,
  18. AxisPlacement,
  19. ScaleDirection,
  20. ScaleOrientation,
  21. } from '@grafana/schema';
  22. import {
  23. Themeable2,
  24. UPlotConfigBuilder,
  25. UPlotChart,
  26. VizLayout,
  27. PlotLegend,
  28. measureText,
  29. UPLOT_AXIS_FONT_SIZE,
  30. } from '@grafana/ui';
  31. import { PanelOptions } from './models.gen';
  32. function incrRoundDn(num: number, incr: number) {
  33. return Math.floor(num / incr) * incr;
  34. }
  35. function incrRoundUp(num: number, incr: number) {
  36. return Math.ceil(num / incr) * incr;
  37. }
  38. export interface HistogramProps extends Themeable2 {
  39. options: PanelOptions; // used for diff
  40. alignedFrame: DataFrame; // This could take HistogramFields
  41. bucketSize: number;
  42. width: number;
  43. height: number;
  44. structureRev?: number; // a number that will change when the frames[] structure changes
  45. legend: VizLegendOptions;
  46. children?: (builder: UPlotConfigBuilder, frame: DataFrame) => React.ReactNode;
  47. }
  48. export function getBucketSize(frame: DataFrame) {
  49. // assumes BucketMin is fields[0] and BucktMax is fields[1]
  50. return frame.fields[1].values.get(0) - frame.fields[0].values.get(0);
  51. }
  52. const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
  53. // todo: scan all values in BucketMin and BucketMax fields to assert if uniform bucketSize
  54. let builder = new UPlotConfigBuilder();
  55. // assumes BucketMin is fields[0] and BucktMax is fields[1]
  56. let bucketSize = getBucketSize(frame);
  57. // splits shifter, to ensure splits always start at first bucket
  58. let xSplits: uPlot.Axis.Splits = (u, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
  59. /** @ts-ignore */
  60. let minSpace = u.axes[axisIdx]._space;
  61. let bucketWidth = u.valToPos(u.data[0][0] + bucketSize, 'x') - u.valToPos(u.data[0][0], 'x');
  62. let firstSplit = u.data[0][0];
  63. let lastSplit = u.data[0][u.data[0].length - 1] + bucketSize;
  64. let splits = [];
  65. let skip = Math.ceil(minSpace / bucketWidth);
  66. for (let i = 0, s = firstSplit; s <= lastSplit; i++, s += bucketSize) {
  67. !(i % skip) && splits.push(s);
  68. }
  69. return splits;
  70. };
  71. builder.addScale({
  72. scaleKey: 'x', // bukkits
  73. isTime: false,
  74. distribution: ScaleDistribution.Linear,
  75. orientation: ScaleOrientation.Horizontal,
  76. direction: ScaleDirection.Right,
  77. range: (u, wantedMin, wantedMax) => {
  78. let fullRangeMin = u.data[0][0];
  79. let fullRangeMax = u.data[0][u.data[0].length - 1];
  80. // snap to bucket divisors...
  81. if (wantedMax === fullRangeMax) {
  82. wantedMax += bucketSize;
  83. } else {
  84. wantedMax = incrRoundUp(wantedMax, bucketSize);
  85. }
  86. if (wantedMin > fullRangeMin) {
  87. wantedMin = incrRoundDn(wantedMin, bucketSize);
  88. }
  89. return [wantedMin, wantedMax];
  90. },
  91. });
  92. builder.addScale({
  93. scaleKey: 'y', // counts
  94. isTime: false,
  95. distribution: ScaleDistribution.Linear,
  96. orientation: ScaleOrientation.Vertical,
  97. direction: ScaleDirection.Up,
  98. });
  99. const fmt = frame.fields[0].display!;
  100. const xAxisFormatter = (v: number) => {
  101. return formattedValueToString(fmt(v));
  102. };
  103. builder.addAxis({
  104. scaleKey: 'x',
  105. isTime: false,
  106. placement: AxisPlacement.Bottom,
  107. incrs: histogramBucketSizes,
  108. splits: xSplits,
  109. values: (u: uPlot, splits: any[]) => {
  110. const tickLabels = splits.map(xAxisFormatter);
  111. const maxWidth = tickLabels.reduce(
  112. (curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax),
  113. 0
  114. );
  115. const labelSpacing = 10;
  116. const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio);
  117. const keepMod = Math.ceil(tickLabels.length / maxCount);
  118. return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null));
  119. },
  120. //incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize),
  121. //splits: config.xSplits,
  122. //values: config.xValues,
  123. //grid: false,
  124. //ticks: false,
  125. //gap: 15,
  126. theme,
  127. });
  128. builder.addAxis({
  129. scaleKey: 'y',
  130. isTime: false,
  131. placement: AxisPlacement.Left,
  132. //splits: config.xSplits,
  133. //values: config.xValues,
  134. //grid: false,
  135. //ticks: false,
  136. //gap: 15,
  137. theme,
  138. });
  139. builder.setCursor({
  140. points: { show: false },
  141. drag: {
  142. x: true,
  143. y: false,
  144. setScale: true,
  145. },
  146. });
  147. let pathBuilder = uPlot.paths.bars!({ align: 1, size: [1, Infinity] });
  148. let seriesIndex = 0;
  149. // assumes BucketMax is [1]
  150. for (let i = 2; i < frame.fields.length; i++) {
  151. const field = frame.fields[i];
  152. field.state = field.state ?? {};
  153. field.state.seriesIndex = seriesIndex++;
  154. const customConfig = { ...field.config.custom };
  155. const scaleKey = 'y';
  156. const colorMode = getFieldColorModeForField(field);
  157. const scaleColor = getFieldSeriesColor(field, theme);
  158. const seriesColor = scaleColor.color;
  159. builder.addSeries({
  160. scaleKey,
  161. lineWidth: customConfig.lineWidth,
  162. lineColor: seriesColor,
  163. //lineStyle: customConfig.lineStyle,
  164. fillOpacity: customConfig.fillOpacity,
  165. theme,
  166. colorMode,
  167. pathBuilder,
  168. //pointsBuilder: config.drawPoints,
  169. show: !customConfig.hideFrom?.vis,
  170. gradientMode: customConfig.gradientMode,
  171. thresholds: field.config.thresholds,
  172. hardMin: field.config.min,
  173. hardMax: field.config.max,
  174. softMin: customConfig.axisSoftMin,
  175. softMax: customConfig.axisSoftMax,
  176. // The following properties are not used in the uPlot config, but are utilized as transport for legend config
  177. dataFrameFieldIndex: {
  178. fieldIndex: i,
  179. frameIndex: 0,
  180. },
  181. });
  182. }
  183. return builder;
  184. };
  185. const preparePlotData = (frame: DataFrame) => {
  186. let data = [];
  187. for (const field of frame.fields) {
  188. if (field.name !== histogramFrameBucketMaxFieldName) {
  189. data.push(field.values.toArray());
  190. }
  191. }
  192. // uPlot's bars pathBuilder will draw rects even if 0 (to distinguish them from nulls)
  193. // but for histograms we want to omit them, so remap 0s -> nulls
  194. for (let i = 1; i < data.length; i++) {
  195. let counts = data[i];
  196. for (let j = 0; j < counts.length; j++) {
  197. if (counts[j] === 0) {
  198. counts[j] = null;
  199. }
  200. }
  201. }
  202. return data as AlignedData;
  203. };
  204. interface State {
  205. alignedData: AlignedData;
  206. config?: UPlotConfigBuilder;
  207. }
  208. export class Histogram extends React.Component<HistogramProps, State> {
  209. constructor(props: HistogramProps) {
  210. super(props);
  211. this.state = this.prepState(props);
  212. }
  213. prepState(props: HistogramProps, withConfig = true) {
  214. let state: State = null as any;
  215. const { alignedFrame } = props;
  216. if (alignedFrame) {
  217. state = {
  218. alignedData: preparePlotData(alignedFrame),
  219. };
  220. if (withConfig) {
  221. state.config = prepConfig(alignedFrame, this.props.theme);
  222. }
  223. }
  224. return state;
  225. }
  226. renderLegend(config: UPlotConfigBuilder) {
  227. const { legend } = this.props;
  228. if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
  229. return null;
  230. }
  231. return <PlotLegend data={[this.props.alignedFrame]} config={config} maxHeight="35%" maxWidth="60%" {...legend} />;
  232. }
  233. componentDidUpdate(prevProps: HistogramProps) {
  234. const { structureRev, alignedFrame, bucketSize } = this.props;
  235. if (alignedFrame !== prevProps.alignedFrame) {
  236. let newState = this.prepState(this.props, false);
  237. if (newState) {
  238. const shouldReconfig =
  239. bucketSize !== prevProps.bucketSize ||
  240. this.props.options !== prevProps.options ||
  241. this.state.config === undefined ||
  242. structureRev !== prevProps.structureRev ||
  243. !structureRev;
  244. if (shouldReconfig) {
  245. newState.config = prepConfig(alignedFrame, this.props.theme);
  246. }
  247. }
  248. newState && this.setState(newState);
  249. }
  250. }
  251. render() {
  252. const { width, height, children, alignedFrame } = this.props;
  253. const { config } = this.state;
  254. if (!config) {
  255. return null;
  256. }
  257. return (
  258. <VizLayout width={width} height={height} legend={this.renderLegend(config)}>
  259. {(vizWidth: number, vizHeight: number) => (
  260. <UPlotChart
  261. config={this.state.config!}
  262. data={this.state.alignedData}
  263. width={vizWidth}
  264. height={vizHeight}
  265. timeRange={null as any}
  266. >
  267. {children ? children(config, alignedFrame) : null}
  268. </UPlotChart>
  269. )}
  270. </VizLayout>
  271. );
  272. }
  273. }