diff --git a/frontend/src/constants/panelTypes.ts b/frontend/src/constants/panelTypes.ts index c76380fff1..7476b20783 100644 --- a/frontend/src/constants/panelTypes.ts +++ b/frontend/src/constants/panelTypes.ts @@ -30,6 +30,7 @@ export const getComponentForPanelType = ( dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent, [PANEL_TYPES.BAR]: Uplot, [PANEL_TYPES.PIE]: null, + [PANEL_TYPES.HISTOGRAM]: Uplot, [PANEL_TYPES.EMPTY_WIDGET]: null, }; diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index b5feadc9f2..9d09cd37ed 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -286,6 +286,7 @@ export enum PANEL_TYPES { TRACE = 'trace', BAR = 'bar', PIE = 'pie', + HISTOGRAM = 'histogram', EMPTY_WIDGET = 'EMPTY_WIDGET', } diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx index 92f2744612..0aecc5d102 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx @@ -159,11 +159,14 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { }, padding: [32, 32, 16, 16], plugins: [ - tooltipPlugin( - fillMissingValuesForQuantities(graphCompatibleData, chartData[0]), - '', - true, - ), + tooltipPlugin({ + apiResponse: fillMissingValuesForQuantities( + graphCompatibleData, + chartData[0], + ), + yAxisUnit: '', + isBillingUsageGraphs: true, + }), ], }), [ diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index 9efb621385..29d578f096 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -17,6 +17,10 @@ border-radius: 3px; } + .height-widget { + height: calc(100% - 40px); + } + .list-graph-container { height: calc(100% - 40px); overflow-y: auto; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts index d8bf328b4d..698d9e6ffa 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts @@ -27,5 +27,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP TRACE: false, BAR: true, PIE: false, + HISTOGRAM: false, EMPTY_WIDGET: false, }; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index b457c4014b..184c34e77b 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -204,6 +204,7 @@ function FullView({
; }; + [PANEL_TYPES.HISTOGRAM]: null; [PANEL_TYPES.EMPTY_WIDGET]: null; }; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts index 467595702a..14ce24cfef 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts @@ -11,6 +11,7 @@ export const PANEL_TYPES_INITIAL_QUERY = { [PANEL_TYPES.TRACE]: initialQueriesMap.traces, [PANEL_TYPES.BAR]: initialQueriesMap.metrics, [PANEL_TYPES.PIE]: initialQueriesMap.metrics, + [PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics, [PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics, }; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx index d25d8a3e8a..33977aa778 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx @@ -40,6 +40,11 @@ const Items: ItemsProps[] = [ icon: , display: 'Pie', }, + { + name: PANEL_TYPES.HISTOGRAM, + icon: , + display: 'Histogram', + }, ]; export interface ItemsProps { diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 50831b02d7..0af57abdc7 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -428,9 +428,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { {!isEmpty(description) && (
{description}
)} -
- -
+ + {!isEmpty(selectedData.variables) && ( +
+ +
+ )} )} + + {allowBucketConfig && ( +
+ Number of buckets + { + setBucketCount(val || 0); + }} + /> + + Bucket width + + { + setBucketWidth(val || 0); + }} + /> +
+ + Merge all series into one + + setCombineHistogram(checked)} + /> +
+
+ )} {allowCreateAlerts && ( @@ -263,6 +312,12 @@ interface RightContainerProps { setSelectedTime: Dispatch>; selectedTime: timePreferance; yAxisUnit: string; + bucketWidth: number; + bucketCount: number; + combineHistogram: boolean; + setCombineHistogram: Dispatch>; + setBucketWidth: Dispatch>; + setBucketCount: Dispatch>; setYAxisUnit: Dispatch>; setGraphHandler: (type: PANEL_TYPES) => void; thresholds: ThresholdProps[]; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 7ffa2c969f..bc7f5bf1fd 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -10,6 +10,7 @@ import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts'; +import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -138,6 +139,18 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { const [saveModal, setSaveModal] = useState(false); const [discardModal, setDiscardModal] = useState(false); + const [bucketWidth, setBucketWidth] = useState( + selectedWidget?.bucketWidth || 0, + ); + + const [bucketCount, setBucketCount] = useState( + selectedWidget?.bucketCount || DEFAULT_BUCKET_COUNT, + ); + + const [combineHistogram, setCombineHistogram] = useState( + selectedWidget?.mergeAllActiveQueries || false, + ); + const [softMin, setSoftMin] = useState( selectedWidget?.softMin === null || selectedWidget?.softMin === undefined ? null @@ -181,6 +194,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { softMax, fillSpans: isFillSpans, columnUnits, + bucketCount, + bucketWidth, + mergeAllActiveQueries: combineHistogram, selectedLogFields, selectedTracesFields, }; @@ -200,6 +216,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { thresholds, title, yAxisUnit, + bucketWidth, + bucketCount, + combineHistogram, ]); const closeModal = (): void => { @@ -296,6 +315,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { softMin: selectedWidget?.softMin || 0, softMax: selectedWidget?.softMax || 0, fillSpans: selectedWidget?.fillSpans, + bucketWidth: selectedWidget?.bucketWidth || 0, + bucketCount: selectedWidget?.bucketCount || 0, + mergeAllActiveQueries: selectedWidget?.mergeAllActiveQueries || false, selectedLogFields: selectedWidget?.selectedLogFields || [], selectedTracesFields: selectedWidget?.selectedTracesFields || [], }, @@ -318,6 +340,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { softMin: selectedWidget?.softMin || 0, softMax: selectedWidget?.softMax || 0, fillSpans: selectedWidget?.fillSpans, + bucketWidth: selectedWidget?.bucketWidth || 0, + bucketCount: selectedWidget?.bucketCount || 0, + mergeAllActiveQueries: selectedWidget?.mergeAllActiveQueries || false, selectedLogFields: selectedWidget?.selectedLogFields || [], selectedTracesFields: selectedWidget?.selectedTracesFields || [], }, @@ -511,6 +536,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { yAxisUnit={yAxisUnit} columnUnits={columnUnits} setColumnUnits={setColumnUnits} + bucketCount={bucketCount} + bucketWidth={bucketWidth} + combineHistogram={combineHistogram} + setCombineHistogram={setCombineHistogram} + setBucketWidth={setBucketWidth} + setBucketCount={setBucketCount} setOpacity={setOpacity} selectedNullZeroValue={selectedNullZeroValue} setSelectedNullZeroValue={setSelectedNullZeroValue} diff --git a/frontend/src/container/NewWidget/utils.ts b/frontend/src/container/NewWidget/utils.ts index caf2604639..a8ac095d1e 100644 --- a/frontend/src/container/NewWidget/utils.ts +++ b/frontend/src/container/NewWidget/utils.ts @@ -32,6 +32,7 @@ export type PartialPanelTypes = { [PANEL_TYPES.TIME_SERIES]: 'graph'; [PANEL_TYPES.VALUE]: 'value'; [PANEL_TYPES.PIE]: 'pie'; + [PANEL_TYPES.HISTOGRAM]: 'histogram'; }; export const panelTypeDataSourceFormValuesMap: Record< @@ -144,6 +145,94 @@ export const panelTypeDataSourceFormValuesMap: Record< }, }, }, + [PANEL_TYPES.HISTOGRAM]: { + [DataSource.LOGS]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + 'functions', + ], + }, + }, + [DataSource.METRICS]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + 'functions', + 'spaceAggregation', + ], + }, + }, + [DataSource.TRACES]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + ], + }, + }, + }, + [PANEL_TYPES.HISTOGRAM]: { + [DataSource.LOGS]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + 'functions', + ], + }, + }, + [DataSource.METRICS]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + 'functions', + 'spaceAggregation', + ], + }, + }, + [DataSource.TRACES]: { + builder: { + queryData: [ + 'filters', + 'aggregateOperator', + 'aggregateAttribute', + 'groupBy', + 'limit', + 'having', + 'orderBy', + ], + }, + }, + }, [PANEL_TYPES.TABLE]: { [DataSource.LOGS]: { builder: { diff --git a/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx b/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx new file mode 100644 index 0000000000..a6c80b01bd --- /dev/null +++ b/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx @@ -0,0 +1,103 @@ +import { ToggleGraphProps } from 'components/Graph/types'; +import Uplot from 'components/Uplot'; +import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager'; +import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useResizeObserver } from 'hooks/useDimensions'; +import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useEffect, useMemo, useRef } from 'react'; + +import { buildHistogramData } from './histogram'; +import { PanelWrapperProps } from './panelWrapper.types'; + +function HistogramPanelWrapper({ + queryResponse, + widget, + setGraphVisibility, + graphVisibility, + isFullViewMode, + onToggleModelHandler, +}: PanelWrapperProps): JSX.Element { + const graphRef = useRef(null); + const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); + const isDarkMode = useIsDarkMode(); + const containerDimensions = useResizeObserver(graphRef); + + const histogramData = buildHistogramData( + queryResponse.data?.payload.data.result, + widget?.bucketWidth, + widget?.bucketCount, + widget?.mergeAllActiveQueries, + ); + + useEffect(() => { + if (toScrollWidgetId === widget.id) { + graphRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + graphRef.current?.focus(); + setToScrollWidgetId(''); + } + }, [toScrollWidgetId, setToScrollWidgetId, widget.id]); + const lineChartRef = useRef(); + + useEffect(() => { + const { + graphVisibilityStates: localStoredVisibilityState, + } = getLocalStorageGraphVisibilityState({ + apiResponse: queryResponse.data?.payload.data.result || [], + name: widget.id, + }); + if (setGraphVisibility) { + setGraphVisibility(localStoredVisibilityState); + } + }, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]); + + const histogramOptions = useMemo( + () => + getUplotHistogramChartOptions({ + id: widget.id, + dimensions: containerDimensions, + isDarkMode, + apiResponse: queryResponse.data?.payload, + histogramData, + panelType: widget.panelTypes, + setGraphsVisibilityStates: setGraphVisibility, + graphsVisibilityStates: graphVisibility, + mergeAllQueries: widget.mergeAllActiveQueries, + }), + [ + containerDimensions, + graphVisibility, + histogramData, + isDarkMode, + queryResponse.data?.payload, + setGraphVisibility, + widget.id, + widget.mergeAllActiveQueries, + widget.panelTypes, + ], + ); + + return ( +
+ + {isFullViewMode && setGraphVisibility && !widget.mergeAllActiveQueries && ( + + )} +
+ ); +} + +export default HistogramPanelWrapper; diff --git a/frontend/src/container/PanelWrapper/constants.ts b/frontend/src/container/PanelWrapper/constants.ts index a8c456d469..868f44ab5b 100644 --- a/frontend/src/container/PanelWrapper/constants.ts +++ b/frontend/src/container/PanelWrapper/constants.ts @@ -1,5 +1,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import HistogramPanelWrapper from './HistogramPanelWrapper'; import ListPanelWrapper from './ListPanelWrapper'; import PiePanelWrapper from './PiePanelWrapper'; import TablePanelWrapper from './TablePanelWrapper'; @@ -15,4 +16,34 @@ export const PanelTypeVsPanelWrapper = { [PANEL_TYPES.EMPTY_WIDGET]: null, [PANEL_TYPES.PIE]: PiePanelWrapper, [PANEL_TYPES.BAR]: UplotPanelWrapper, + [PANEL_TYPES.HISTOGRAM]: HistogramPanelWrapper, }; + +export const DEFAULT_BUCKET_COUNT = 30; + +// prettier-ignore +export const histogramBucketSizes = [ + 1e-9, 2e-9, 2.5e-9, 4e-9, 5e-9, + 1e-8, 2e-8, 2.5e-8, 4e-8, 5e-8, + 1e-7, 2e-7, 2.5e-7, 4e-7, 5e-7, + 1e-6, 2e-6, 2.5e-6, 4e-6, 5e-6, + 1e-5, 2e-5, 2.5e-5, 4e-5, 5e-5, + 1e-4, 2e-4, 2.5e-4, 4e-4, 5e-4, + 1e-3, 2e-3, 2.5e-3, 4e-3, 5e-3, + 1e-2, 2e-2, 2.5e-2, 4e-2, 5e-2, + 1e-1, 2e-1, 2.5e-1, 4e-1, 5e-1, + 1, 2, 4, 5, + 1e+1, 2e+1, 2.5e+1, 4e+1, 5e+1, + 1e+2, 2e+2, 2.5e+2, 4e+2, 5e+2, + 1e+3, 2e+3, 2.5e+3, 4e+3, 5e+3, + 1e+4, 2e+4, 2.5e+4, 4e+4, 5e+4, + 1e+5, 2e+5, 2.5e+5, 4e+5, 5e+5, + 1e+6, 2e+6, 2.5e+6, 4e+6, 5e+6, + 1e+7, 2e+7, 2.5e+7, 4e+7, 5e+7, + 1e+8, 2e+8, 2.5e+8, 4e+8, 5e+8, + 1e+9, 2e+9, 2.5e+9, 4e+9, 5e+9, + ]; + +export const NULL_REMOVE = 0; +export const NULL_RETAIN = 1; +export const NULL_EXPAND = 2; diff --git a/frontend/src/container/PanelWrapper/histogram.ts b/frontend/src/container/PanelWrapper/histogram.ts new file mode 100644 index 0000000000..ca11a9f49a --- /dev/null +++ b/frontend/src/container/PanelWrapper/histogram.ts @@ -0,0 +1,276 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable no-param-reassign */ +import { histogramBucketSizes } from '@grafana/data'; +import { QueryData } from 'types/api/widgets/getQuery'; +import uPlot, { AlignedData } from 'uplot'; + +import { + DEFAULT_BUCKET_COUNT, + NULL_EXPAND, + NULL_REMOVE, + NULL_RETAIN, +} from './constants'; + +export function incrRoundDn(num: number, incr: number): number { + return Math.floor(num / incr) * incr; +} + +const histSort = (a: number, b: number): number => a - b; + +export function roundDecimals(val: number, dec = 0): number { + if (Number.isInteger(val)) { + return val; + } + + const p = 10 ** dec; + const n = val * p * (1 + Number.EPSILON); + return Math.round(n) / p; +} + +function nullExpand( + yVals: Array, + nullIdxs: number[], + alignedLen: number, +): void { + for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) { + const nullIdx = nullIdxs[i]; + + if (nullIdx > lastNullIdx) { + xi = nullIdx - 1; + while (xi >= 0 && yVals[xi] == null) { + yVals[xi--] = null; + } + + xi = nullIdx + 1; + while (xi < alignedLen && yVals[xi] == null) { + yVals[(lastNullIdx = xi++)] = null; + } + } + } +} + +export function join( + tables: AlignedData[], + nullModes?: number[][], +): AlignedData[] { + let xVals: Set; + + // eslint-disable-next-line prefer-const + xVals = new Set(); + + for (let ti = 0; ti < tables.length; ti++) { + const t = tables[ti]; + const xs = t[0]; + const len = xs.length; + + for (let i = 0; i < len; i++) { + xVals.add(xs[i]); + } + } + + const data = [Array.from(xVals).sort((a, b) => a - b)]; + + const alignedLen = data[0].length; + + const xIdxs = new Map(); + + for (let i = 0; i < alignedLen; i++) { + xIdxs.set(data[0][i], i); + } + + for (let ti = 0; ti < tables.length; ti++) { + const t = tables[ti]; + const xs = t[0]; + + for (let si = 1; si < t.length; si++) { + const ys = t[si]; + + const yVals = Array(alignedLen).fill(undefined); + + const nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN; + + const nullIdxs = []; + + for (let i = 0; i < ys.length; i++) { + const yVal = ys[i]; + const alignedIdx = xIdxs.get(xs[i]); + + if (yVal === null) { + if (nullMode !== NULL_REMOVE) { + yVals[alignedIdx] = yVal; + + if (nullMode === NULL_EXPAND) { + nullIdxs.push(alignedIdx); + } + } + } else { + yVals[alignedIdx] = yVal; + } + } + + nullExpand(yVals, nullIdxs, alignedLen); + + data.push(yVals); + } + } + + return (data as unknown) as AlignedData[]; +} + +export function histogram( + vals: number[], + getBucket: (v: number) => number, + sort?: ((a: number, b: number) => number) | null, +): AlignedData { + const hist = new Map(); + + for (let i = 0; i < vals.length; i++) { + let v = vals[i]; + + if (v != null) { + v = getBucket(v); + } + + const entry = hist.get(v); + + if (entry) { + entry.count++; + } else { + hist.set(v, { value: v, count: 1 }); + } + } + + const bins = [...hist.values()]; + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + sort && bins.sort((a, b) => sort(a.value, b.value)); + + const values = Array(bins.length); + const counts = Array(bins.length); + + for (let i = 0; i < bins.length; i++) { + values[i] = bins[i].value; + counts[i] = bins[i].count; + } + + return [values, counts]; +} + +function replaceUndefinedWithNull(arrays: (number | null)[][]): AlignedData[] { + for (let i = 0; i < arrays.length; i++) { + for (let j = 0; j < arrays[i].length; j++) { + if (arrays[i][j] === undefined) { + arrays[i][j] = null; + } + } + } + return (arrays as unknown) as AlignedData[]; +} + +function addNullToFirstHistogram( + histograms: (number | null)[][], + bucketSize: number, +): void { + if ( + histograms.length > 0 && + histograms[0].length > 0 && + histograms[0][0] !== null + ) { + histograms[0].unshift(histograms[0][0] - bucketSize); + for (let i = 1; i < histograms.length; i++) { + histograms[i].unshift(null); + } + } +} + +export const buildHistogramData = ( + data: QueryData[] | undefined, + widgetBucketSize?: number, + widgteBucketCount?: number, + widgetMergeAllActiveQueries?: boolean, +): uPlot.AlignedData => { + let bucketSize = 0; + const bucketCount = widgteBucketCount || DEFAULT_BUCKET_COUNT; + const bucketOffset = 0; + + const seriesValues: number[] = []; + data?.forEach((item) => { + item.values.forEach((value) => { + seriesValues.push(parseFloat(value[1]) || 0); + }); + }); + + seriesValues.sort((a, b) => a - b); + + let smallestDelta = Infinity; + if (seriesValues.length === 1) { + smallestDelta = 0; + } else { + for (let i = 1; i < seriesValues.length; i++) { + const delta = seriesValues[i] - seriesValues[i - 1]; + if (delta !== 0) { + smallestDelta = Math.min(smallestDelta, delta); + } + } + } + + const min = seriesValues[0]; + const max = seriesValues[seriesValues.length - 1]; + + const range = max - min; + const targetSize = range / bucketCount; + + for (let i = 0; i < histogramBucketSizes.length; i++) { + const newBucketSize = histogramBucketSizes[i]; + + if (targetSize < newBucketSize && newBucketSize >= smallestDelta) { + bucketSize = newBucketSize; + break; + } + } + + if (widgetBucketSize) { + bucketSize = widgetBucketSize; + } + + const getBucket = (v: number): number => + roundDecimals(incrRoundDn(v - bucketOffset, bucketSize) + bucketOffset, 9); + + const frames: number[][] = []; + + data?.forEach((item) => { + const newFrame: number[] = []; + item.values.forEach((value) => { + newFrame.push(parseFloat(value[1]) || 0); + }); + frames.push(newFrame); + }); + + if (widgetMergeAllActiveQueries) { + for (let i = 1; i < frames.length; i++) { + frames[i].forEach((val) => { + frames[0].push(val); + }); + frames[i] = []; + } + } + + const histograms: AlignedData[] = []; + + frames.forEach((frame) => { + const fieldHist = histogram(frame, getBucket, histSort); + histograms.push(fieldHist); + }); + + const joinHistogram = replaceUndefinedWithNull( + (join(histograms) as unknown) as (number | null)[][], + ); + + addNullToFirstHistogram( + (joinHistogram as unknown) as (number | null)[][], + bucketSize, + ); + + return (joinHistogram as unknown) as AlignedData; +}; diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 2e8d6137a5..2247ec90bd 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -57,7 +57,6 @@ export const getUPlotChartOptions = ({ graphsVisibilityStates, setGraphsVisibilityStates, thresholds, - fillSpans, softMax, softMin, panelType, @@ -108,7 +107,7 @@ export const getUPlotChartOptions = ({ }, }, plugins: [ - tooltipPlugin(apiResponse, yAxisUnit, fillSpans), + tooltipPlugin({ apiResponse, yAxisUnit }), onClickPlugin({ onClick: onClickHandler, }), diff --git a/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts new file mode 100644 index 0000000000..15bea36cba --- /dev/null +++ b/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts @@ -0,0 +1,199 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { themeColors } from 'constants/theme'; +import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils'; +import { Dimensions } from 'hooks/useDimensions'; +import getLabelName from 'lib/getLabelName'; +import { Dispatch, SetStateAction } from 'react'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryData } from 'types/api/widgets/getQuery'; +import uPlot from 'uplot'; + +import tooltipPlugin from './plugins/tooltipPlugin'; +import { drawStyles } from './utils/constants'; +import { generateColor } from './utils/generateColor'; +import getAxes from './utils/getAxes'; + +type GetUplotHistogramChartOptionsProps = { + id?: string; + apiResponse?: MetricRangePayloadProps; + histogramData: uPlot.AlignedData; + dimensions: Dimensions; + isDarkMode: boolean; + panelType?: PANEL_TYPES; + onDragSelect?: (startTime: number, endTime: number) => void; + currentQuery?: Query; + graphsVisibilityStates?: boolean[]; + setGraphsVisibilityStates?: Dispatch>; + mergeAllQueries?: boolean; +}; + +type GetHistogramSeriesProps = { + apiResponse?: MetricRangePayloadProps; + currentQuery?: Query; + widgetMetaData?: QueryData[]; + graphsVisibilityStates?: boolean[]; + isMergedSeries?: boolean; +}; + +const { bars } = uPlot.paths; + +const paths = ( + u: uPlot, + seriesIdx: number, + idx0: number, + idx1: number, +): uPlot.Series.Paths | null | undefined => { + const renderer = bars && bars({ size: [1], align: -1 }); + + return renderer && renderer(u, seriesIdx, idx0, idx1); +}; + +const getHistogramSeries = ({ + apiResponse, + currentQuery, + widgetMetaData, + graphsVisibilityStates, + isMergedSeries, +}: GetHistogramSeriesProps): uPlot.Options['series'] => { + const configurations: uPlot.Series[] = [ + { label: 'Timestamp', stroke: 'purple' }, + ]; + const seriesList = apiResponse?.data.result || []; + + const newGraphVisibilityStates = graphsVisibilityStates?.slice(1); + + for (let i = 0; i < seriesList?.length; i += 1) { + const { metric = {}, queryName = '', legend: lgd } = + (widgetMetaData && widgetMetaData[i]) || {}; + + const newLegend = + currentQuery?.builder.queryData.find((item) => item.queryName === queryName) + ?.legend || ''; + + const legend = newLegend || lgd || ''; + + const label = isMergedSeries + ? 'merged_series' + : getLabelName(metric, queryName || '', legend); + + const color = generateColor(label, themeColors.chartcolors); + + const pointSize = seriesList[i].values.length > 1 ? 5 : 10; + const showPoints = !(seriesList[i].values.length > 1); + + const seriesObj: uPlot.Series = { + paths, + drawStyle: drawStyles.bars, + lineInterpolation: null, + show: newGraphVisibilityStates ? newGraphVisibilityStates[i] : true, + label, + fill: `${color}40`, + stroke: color, + width: 2, + points: { + size: pointSize, + show: showPoints, + stroke: color, + }, + } as uPlot.Series; + + configurations.push(seriesObj); + } + + return configurations; +}; + +export const getUplotHistogramChartOptions = ({ + id, + dimensions, + isDarkMode, + apiResponse, + currentQuery, + graphsVisibilityStates, + setGraphsVisibilityStates, + mergeAllQueries, +}: GetUplotHistogramChartOptionsProps): uPlot.Options => + ({ + id, + width: dimensions.width, + height: dimensions.height - 30, + legend: { + show: !mergeAllQueries, + live: false, + isolate: true, + }, + focus: { + alpha: 0.3, + }, + padding: [16, 16, 8, 8], + plugins: [ + tooltipPlugin({ + apiResponse, + isHistogramGraphs: true, + isMergedSeries: mergeAllQueries, + }), + ], + scales: { + x: { + time: false, + auto: true, + }, + y: { + auto: true, + }, + }, + cursor: { + drag: { + x: false, + y: false, + setScale: true, + }, + }, + series: getHistogramSeries({ + apiResponse, + widgetMetaData: apiResponse?.data.result, + currentQuery, + graphsVisibilityStates, + isMergedSeries: mergeAllQueries, + }), + hooks: { + 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 (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; + }); + } + }); + }); + } + }, + ], + }, + axes: getAxes(isDarkMode), + } as uPlot.Options); diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts index b06e5bff63..35a10bf217 100644 --- a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts +++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts @@ -29,6 +29,8 @@ const generateTooltipContent = ( yAxisUnit?: string, series?: uPlot.Options['series'], isBillingUsageGraphs?: boolean, + isHistogramGraphs?: boolean, + isMergedSeries?: boolean, // eslint-disable-next-line sonarjs/cognitive-complexity ): HTMLElement => { const container = document.createElement('div'); @@ -49,7 +51,9 @@ const generateTooltipContent = ( } if (Array.isArray(series) && series.length > 0) { + console.log(series); series.forEach((item, index) => { + console.log(item, index); if (index === 0) { if (isBillingUsageGraphs) { tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY'); @@ -67,7 +71,9 @@ const generateTooltipContent = ( const value = data[index][idx]; const dataIngested = quantity[idx]; - const label = getLabelName(metric, queryName || '', legend || ''); + const label = isMergedSeries + ? 'merged_series' + : getLabelName(metric, queryName || '', legend || ''); let color = generateColor(label, themeColors.chartcolors); @@ -146,7 +152,7 @@ const generateTooltipContent = ( const div = document.createElement('div'); div.classList.add('tooltip-content-row'); - div.textContent = tooltipTitle; + div.textContent = isHistogramGraphs ? '' : tooltipTitle; div.classList.add('tooltip-content-header'); container.appendChild(div); @@ -191,11 +197,21 @@ const generateTooltipContent = ( return container; }; -const tooltipPlugin = ( - apiResponse: MetricRangePayloadProps | undefined, - yAxisUnit?: string, - isBillingUsageGraphs?: boolean, -): any => { +type ToolTipPluginProps = { + apiResponse: MetricRangePayloadProps | undefined; + yAxisUnit?: string; + isBillingUsageGraphs?: boolean; + isHistogramGraphs?: boolean; + isMergedSeries?: boolean; +}; + +const tooltipPlugin = ({ + apiResponse, + yAxisUnit, + isBillingUsageGraphs, + isHistogramGraphs, + isMergedSeries, +}: ToolTipPluginProps): any => { let over: HTMLElement; let bound: HTMLElement; let bLeft: any; @@ -256,6 +272,8 @@ const tooltipPlugin = ( yAxisUnit, u.series, isBillingUsageGraphs, + isHistogramGraphs, + isMergedSeries, ); overlay.appendChild(content); placement(overlay, anchor, 'right', 'start', { bound }); diff --git a/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts b/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts index 42860ea8c8..9581862c70 100644 --- a/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts +++ b/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts @@ -124,6 +124,10 @@ GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => { // Situation: thresholds are absent if (!thresholds || thresholds.length === 0) { + if (softMax === softMin) { + return { auto: true }; + } + // Situation: No thresholds data but series data is present if (series && !areAllSeriesEmpty(series)) { // Situation: softMin and softMax are null diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 0fa9f4caf7..af254e032e 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -97,6 +97,9 @@ export interface IBaseWidget { timePreferance: timePreferenceType; stepSize?: number; yAxisUnit?: string; + bucketCount?: number; + bucketWidth?: number; + mergeAllActiveQueries?: boolean; thresholds?: ThresholdProps[]; softMin: number | null; softMax: number | null; diff --git a/frontend/src/utils/getGraphType.ts b/frontend/src/utils/getGraphType.ts index 8f1ba43da3..9c3ded97e8 100644 --- a/frontend/src/utils/getGraphType.ts +++ b/frontend/src/utils/getGraphType.ts @@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; export const getGraphType = (panelType: PANEL_TYPES): PANEL_TYPES => { // backend don't support graphType as bar, as we consume time series data, sending graphType as time_series whenever we use bar as panel_type - if (panelType === PANEL_TYPES.BAR) { + if (panelType === PANEL_TYPES.BAR || panelType === PANEL_TYPES.HISTOGRAM) { return PANEL_TYPES.TIME_SERIES; } if (panelType === PANEL_TYPES.PIE) {