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) && (
)}
-
+
+ {!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) {