Files
signoz/frontend/src/lib/uPlotLib/getUplotChartOptions.ts
2024-09-16 11:11:33 +05:30

386 lines
9.8 KiB
TypeScript

/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
/* eslint-disable sonarjs/cognitive-complexity */
import './uPlotLib.styles.scss';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { FullViewProps } from 'container/GridCardLayout/GridCard/FullView/types';
import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { Dimensions } from 'hooks/useDimensions';
import { convertValue } from 'lib/getConvertedValue';
import { cloneDeep, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
import uPlot from 'uplot';
import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
import tooltipPlugin from './plugins/tooltipPlugin';
import getAxes from './utils/getAxes';
import getSeries from './utils/getSeriesData';
import { getXAxisScale } from './utils/getXAxisScale';
import { getYAxisScale } from './utils/getYAxisScale';
export interface GetUPlotChartOptions {
id?: string;
apiResponse?: MetricRangePayloadProps;
dimensions: Dimensions;
isDarkMode: boolean;
panelType?: PANEL_TYPES;
onDragSelect?: (startTime: number, endTime: number) => void;
yAxisUnit?: string;
onClickHandler?: OnClickPluginOpts['onClick'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates?: FullViewProps['setGraphsVisibilityStates'];
thresholds?: ThresholdProps[];
thresholdValue?: number;
thresholdText?: string;
fillSpans?: boolean;
minTimeScale?: number;
maxTimeScale?: number;
softMin: number | null;
softMax: number | null;
currentQuery?: Query;
stackBarChart?: boolean;
hiddenGraph?: {
[key: string]: boolean;
};
setHiddenGraph?: Dispatch<
SetStateAction<{
[key: string]: boolean;
}>
>;
customTooltipElement?: HTMLDivElement;
verticalLineTimestamp?: number;
}
/** the function converts series A , series B , series C to
* series A , series A + series B , series A + series B + series C
* which helps us to always ensure the bar in the front is always
* of the smallest value.
*/
function getStackedSeries(apiResponse: QueryData[]): QueryData[] {
const series = cloneDeep(apiResponse);
if (!series) {
return series;
}
for (let i = series.length - 2; i >= 0; i--) {
const { values } = series[i];
for (let j = 0; j < values.length; j++) {
values[j][1] = String(
parseFloat(values[j]?.[1] || '0') +
parseFloat(series[i + 1].values[j]?.[1] || '0'),
);
}
series[i].values = values;
}
return series;
}
/** this does the exact same operations as the function above for a different
* response format.
*/
function getStackedSeriesQueryFormat(apiResponse: QueryData[]): QueryData[] {
const series = cloneDeep(apiResponse);
if (!series) {
return apiResponse;
}
for (let i = series.length - 2; i >= 0; i--) {
const { values } = series[i];
for (let j = 0; j < values.length; j++) {
values[j].value = String(
parseFloat(values[j]?.value || '0') +
parseFloat(series[i + 1].values[j]?.value || '0'),
);
}
series[i].values = values;
}
return series;
}
function getStackedSeriesYAxis(apiResponse: QueryDataV3[]): QueryDataV3[] {
const series = cloneDeep(apiResponse);
if (!series) {
return apiResponse;
}
for (let i = 0; i < series.length; i++) {
series[i].series = getStackedSeriesQueryFormat(series[i].series || []);
}
return series;
}
/**
* here we define the different series bands which should get highlighted based
* on cursor hover. basically the to and the from destination of a particular band.
*/
function getBands(series): any[] {
const bands = [];
for (let i = 0; i < series.length; i++) {
bands.push({
series: [i === 0 ? -1 : i, i + 1],
});
}
return bands;
}
export const getUPlotChartOptions = ({
id,
dimensions,
isDarkMode,
apiResponse,
onDragSelect,
yAxisUnit,
minTimeScale,
maxTimeScale,
onClickHandler = _noop,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds,
softMax,
softMin,
panelType,
currentQuery,
stackBarChart: stackChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
verticalLineTimestamp,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
const stackBarChart = stackChart && isUndefined(hiddenGraph);
const series = getStackedSeries(apiResponse?.data?.result || []);
const bands = stackBarChart ? getBands(series) : null;
return {
id,
width: dimensions.width,
height: dimensions.height - 30,
legend: {
show: true,
live: false,
isolate: true,
},
focus: {
alpha: 0.3,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
points: {
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 3,
width: (u, seriesIdx, size): number => size / 4,
stroke: (u, seriesIdx): string =>
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
fill: (): string => '#fff',
},
},
padding: [16, 16, 8, 8],
bands,
scales: {
x: {
spanGaps: true,
...timeScaleProps,
},
y: {
...getYAxisScale({
thresholds,
series: stackBarChart
? getStackedSeriesYAxis(apiResponse?.data?.newResult?.data?.result || [])
: apiResponse?.data?.newResult?.data?.result || [],
yAxisUnit,
softMax,
softMin,
}),
},
},
plugins: [
tooltipPlugin({
apiResponse,
yAxisUnit,
stackBarChart,
isDarkMode,
customTooltipElement,
}),
onClickPlugin({
onClick: onClickHandler,
apiResponse,
}),
{
hooks: {
draw: [
(u): void => {
if (verticalLineTimestamp) {
const { ctx } = u;
ctx.save();
ctx.setLineDash([4, 2]);
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
const x = u.valToPos(verticalLineTimestamp, 'x', true);
ctx.beginPath();
ctx.moveTo(x, u.bbox.top);
ctx.lineTo(x, u.bbox.top + u.bbox.height);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
}
},
],
},
},
],
hooks: {
draw: [
(u): void => {
thresholds?.forEach((threshold) => {
if (threshold.thresholdValue !== undefined) {
const { ctx } = u;
ctx.save();
const yPos = u.valToPos(
convertValue(
threshold.thresholdValue,
threshold.thresholdUnit,
yAxisUnit,
),
'y',
true,
);
ctx.strokeStyle = threshold.thresholdColor || 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.stroke();
// Text configuration
if (threshold.thresholdLabel) {
const text = threshold.thresholdLabel;
const textX = plotRight - ctx.measureText(text).width - 20;
const canvasHeight = ctx.canvas.height;
const yposHeight = canvasHeight - yPos;
const isHeightGreaterThan90Percent = canvasHeight * 0.9 < yposHeight;
// Adjust textY based on the condition
let textY;
if (isHeightGreaterThan90Percent) {
textY = yPos + 15; // Below the threshold line
} else {
textY = yPos - 15; // Above the threshold line
}
ctx.fillStyle = threshold.thresholdColor || 'red';
ctx.fillText(text, textX, textY);
}
ctx.restore();
}
});
},
],
setSelect: [
(self): void => {
const selection = self.select;
if (selection) {
const startTime = self.posToVal(selection.left, 'x');
const endTime = self.posToVal(selection.left + selection.width, 'x');
const diff = endTime - startTime;
if (typeof onDragSelect === 'function' && diff > 0) {
onDragSelect(startTime * 1000, endTime * 1000);
}
}
},
],
ready: [
(self): void => {
const legend = self.root.querySelector('.u-legend');
if (legend) {
const seriesEls = legend.querySelectorAll('.u-series');
const seriesArray = Array.from(seriesEls);
seriesArray.forEach((seriesEl, index) => {
seriesEl.addEventListener('click', () => {
if (stackChart) {
setHiddenGraph((prev) => {
if (isUndefined(prev)) {
return { [index]: true };
}
if (prev[index] === true) {
return undefined;
}
return { [index]: true };
});
}
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
if (
newGraphVisibilityStates[index + 1] &&
newGraphVisibilityStates.every((value, i) =>
i === index + 1 ? value : !value,
)
) {
newGraphVisibilityStates.fill(true);
} else {
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
}
});
});
}
},
],
},
series: getSeries({
series:
stackBarChart && isUndefined(hiddenGraph)
? series
: apiResponse?.data?.result,
widgetMetaData: apiResponse?.data.result,
graphsVisibilityStates,
panelType,
currentQuery,
stackBarChart,
hiddenGraph,
isDarkMode,
}),
axes: getAxes(isDarkMode, yAxisUnit),
};
};