2
import React, { PureComponent } from 'react';
3
import ReactResizeDetector from 'react-resize-detector';
5
import { Legend } from './Legend';
6
import { ExemplarData, Histogram, Metric, QueryParams } from '../../types/types';
7
import { isPresent } from '../../utils';
8
import { getOptions, normalizeData, toHoverColor } from './GraphHelpers';
9
import { Button } from 'reactstrap';
10
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
import { faTimes } from '@fortawesome/free-solid-svg-icons';
12
import { GraphDisplayMode } from './Panel';
14
require('../../vendor/flot/jquery.flot');
15
require('../../vendor/flot/jquery.flot.stack');
16
require('../../vendor/flot/jquery.flot.time');
17
require('../../vendor/flot/jquery.flot.crosshair');
18
require('../../vendor/flot/jquery.flot.selection');
19
require('../../vendor/flot/jquery.flot.heatmap');
20
require('jquery.flot.tooltip');
22
export interface GraphProps {
25
result: Array<{ metric: Metric; values?: [number, string][]; histograms?: [number, Histogram][] }>;
27
exemplars: ExemplarData;
28
displayMode: GraphDisplayMode;
29
useLocalTime: boolean;
30
showExemplars: boolean;
31
handleTimeRangeSelection: (startTime: number, endTime: number) => void;
32
queryParams: QueryParams | null;
36
export interface GraphSeries {
37
labels: { [key: string]: string };
39
data: (number | null)[][]; // [x,y][]
43
export interface GraphExemplar {
44
seriesLabels: { [key: string]: string };
45
labels: { [key: string]: string };
47
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48
points: any; // This is used to specify the symbol.
52
export interface GraphData {
53
series: GraphSeries[];
54
exemplars: GraphExemplar[];
59
selectedExemplarLabels: { exemplar: { [key: string]: string }; series: { [key: string]: string } };
62
class Graph extends PureComponent<GraphProps, GraphState> {
63
private chartRef = React.createRef<HTMLDivElement>();
64
private $chart?: jquery.flot.plot;
66
private selectedSeriesIndexes: number[] = [];
69
chartData: normalizeData(this.props),
70
selectedExemplarLabels: { exemplar: {}, series: {} },
73
componentDidUpdate(prevProps: GraphProps): void {
74
const { data, displayMode, useLocalTime, showExemplars } = this.props;
75
if (prevProps.data !== data) {
76
this.selectedSeriesIndexes = [];
77
this.setState({ chartData: normalizeData(this.props) }, this.plot);
78
} else if (prevProps.displayMode !== displayMode) {
79
this.setState({ chartData: normalizeData(this.props) }, () => {
80
if (this.selectedSeriesIndexes.length === 0) {
84
...this.state.chartData.series.filter((_, i) => this.selectedSeriesIndexes.includes(i)),
85
...this.state.chartData.exemplars,
91
if (prevProps.useLocalTime !== useLocalTime) {
95
if (prevProps.showExemplars !== showExemplars && !showExemplars) {
98
chartData: { series: this.state.chartData.series, exemplars: [] },
99
selectedExemplarLabels: { exemplar: {}, series: {} },
108
componentDidMount(): void {
111
$(`.graph-${this.props.id}`).bind('plotclick', (event, pos, item) => {
112
// If an item has the series label property that means it's an exemplar.
113
if (item && 'seriesLabels' in item.series) {
115
selectedExemplarLabels: { exemplar: item.series.labels, series: item.series.seriesLabels },
116
chartData: this.state.chartData,
120
chartData: this.state.chartData,
121
selectedExemplarLabels: { exemplar: {}, series: {} },
126
$(`.graph-${this.props.id}`).bind('plotselected', (_, ranges) => {
127
if (isPresent(this.$chart)) {
128
// eslint-disable-next-line
129
// @ts-ignore Typescript doesn't think this method exists although it actually does.
130
this.$chart.clearSelection();
131
this.props.handleTimeRangeSelection(ranges.xaxis.from, ranges.xaxis.to);
136
componentWillUnmount(): void {
141
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
143
if (!this.chartRef.current) {
148
const options = getOptions(this.props.displayMode === GraphDisplayMode.Stacked, this.props.useLocalTime);
149
const isHeatmap = this.props.displayMode === GraphDisplayMode.Heatmap;
150
options.series.heatmap = isHeatmap;
152
if (options.yaxis && isHeatmap) {
153
options.yaxis.ticks = () => new Array(data.length + 1).fill(0).map((_el, i) => i);
154
options.yaxis.tickFormatter = (val) => `${val ? data[val - 1].labels.le : ''}`;
155
options.yaxis.min = 0;
156
options.yaxis.max = data.length;
157
options.series.lines = { show: false };
159
this.$chart = $.plot($(this.chartRef.current), data, options);
162
destroyPlot = (): void => {
163
if (isPresent(this.$chart)) {
164
this.$chart.destroy();
169
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
171
if (isPresent(this.$chart)) {
172
this.$chart.setData(data);
177
handleSeriesSelect = (selected: number[], selectedIndex: number): void => {
178
const { chartData } = this.state;
180
this.selectedSeriesIndexes.length === 1 && this.selectedSeriesIndexes.includes(selectedIndex)
182
...chartData.series.map(toHoverColor(selectedIndex, this.props.displayMode === GraphDisplayMode.Stacked)),
183
...chartData.exemplars,
186
...chartData.series.filter((_, i) => selected.includes(i)),
187
...chartData.exemplars.filter((exemplar) => {
188
series: for (const i in selected) {
189
for (const name in chartData.series[selected[i]].labels) {
190
if (exemplar.seriesLabels[name] !== chartData.series[selected[i]].labels[name]) {
198
] // draw only selected
200
this.selectedSeriesIndexes = selected;
203
handleSeriesHover = (index: number) => (): void => {
205
cancelAnimationFrame(this.rafID);
207
this.rafID = requestAnimationFrame(() => {
208
this.plotSetAndDraw([
209
...this.state.chartData.series.map(toHoverColor(index, this.props.displayMode === GraphDisplayMode.Stacked)),
210
...this.state.chartData.exemplars,
215
handleLegendMouseOut = (): void => {
216
cancelAnimationFrame(this.rafID);
217
this.plotSetAndDraw();
220
handleResize = (): void => {
221
if (isPresent(this.$chart)) {
222
this.plot(this.$chart.getData() as (GraphSeries | GraphExemplar)[]);
226
render(): JSX.Element {
227
const { chartData, selectedExemplarLabels } = this.state;
228
const selectedLabels = selectedExemplarLabels as {
229
exemplar: { [key: string]: string };
230
series: { [key: string]: string };
233
<div className={`graph-${this.props.id}`}>
234
<ReactResizeDetector handleWidth onResize={this.handleResize} skipOnMount />
235
<div className="graph-chart" ref={this.chartRef} />
236
{Object.keys(selectedLabels.exemplar).length > 0 ? (
237
<div className="graph-selected-exemplar">
238
<div className="font-weight-bold">Selected exemplar labels:</div>
239
<div className="labels mt-1 ml-3">
240
{Object.keys(selectedLabels.exemplar).map((k, i) => (
242
<strong>{k}</strong>: {selectedLabels.exemplar[k]}
246
<div className="font-weight-bold mt-3">Associated series labels:</div>
247
<div className="labels mt-1 ml-3">
248
{Object.keys(selectedLabels.series).map((k, i) => (
250
<strong>{k}</strong>: {selectedLabels.series[k]}
257
style={{ position: 'absolute', top: 5, right: 5 }}
258
title="Hide selected exemplar details"
261
chartData: this.state.chartData,
262
selectedExemplarLabels: { exemplar: {}, series: {} },
266
<FontAwesomeIcon icon={faTimes} />
270
{this.props.displayMode !== GraphDisplayMode.Heatmap && (
272
shouldReset={this.selectedSeriesIndexes.length === 0}
273
chartData={chartData.series}
274
onHover={this.handleSeriesHover}
275
onLegendMouseOut={this.handleLegendMouseOut}
276
onSeriesToggle={this.handleSeriesSelect}
279
{/* This is to make sure the graph box expands when the selected exemplar info pops up. */}
280
<br style={{ clear: 'both' }} />