feat: add histogram visualisation support (#4858)

* refactor: added panel type histogram and basic setup done

* feat: histogram for one query

* refactor: multiple query support histogram

* chore: typecorrection

* refactor: done with legend part

* refactor: legend change fix

* refactor: fix the tooltip value for histogram

* refactor: disable drag for histogram

* fix: tsc

* refactor: handled panel type change in edit mode

* refactor: full view done (#4881)

Co-authored-by: Rajat-Dabade <rajat@signoz.io>

* [Feat]: Full view histogram (#4867)

* refactor: added panel type histogram and basic setup done

* feat: histogram for one query

* refactor: multiple query support histogram

* chore: typecorrection

* refactor: done with legend part

* refactor: legend change fix

* refactor: fix the tooltip value for histogram

* refactor: disable drag for histogram

* fix: tsc

* refactor: handled panel type change in edit mode

* refactor: full view done

---------

Co-authored-by: Rajat-Dabade <rajat@signoz.io>
Co-authored-by: Yunus M <myounis.ar@live.com>

* feat: histogram customisations (#5133)

* feat: added bucket size and bucket width enhancements for histogram

* fix: added handling for bucket size bucket count and combine into one series

* fix: added bidirectional sync

* fix: remove extra props from interfaces

* fix: hide legends when merging all queries

* fix: minor legend fixes

* fix: build issues

---------

Co-authored-by: Rajat-Dabade <rajat@signoz.io>
Co-authored-by: Yunus M <myounis.ar@live.com>
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
This commit is contained in:
Rajat Dabade 2024-06-05 07:07:49 +05:30 committed by GitHub
parent b39f703919
commit 7e9bf2d48d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 924 additions and 18 deletions

View File

@ -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,
};

View File

@ -286,6 +286,7 @@ export enum PANEL_TYPES {
TRACE = 'trace',
BAR = 'bar',
PIE = 'pie',
HISTOGRAM = 'histogram',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}

View File

@ -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,
}),
],
}),
[

View File

@ -17,6 +17,10 @@
border-radius: 3px;
}
.height-widget {
height: calc(100% - 40px);
}
.list-graph-container {
height: calc(100% - 40px);
overflow-y: auto;

View File

@ -27,5 +27,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
TRACE: false,
BAR: true,
PIE: false,
HISTOGRAM: false,
EMPTY_WIDGET: false,
};

View File

@ -204,6 +204,7 @@ function FullView({
<div
className={cx('graph-container', {
disabled: isDashboardLocked,
'height-widget': widget?.mergeAllActiveQueries,
'list-graph-container': isListView,
})}
ref={fullViewRef}

View File

@ -49,6 +49,7 @@ const GridPanelSwitch = forwardRef<
options,
ref,
},
[PANEL_TYPES.HISTOGRAM]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@ -43,5 +43,6 @@ export type PropsTypePropsMap = {
[PANEL_TYPES.BAR]: UplotProps & {
ref: ForwardedRef<ToggleGraphProps | undefined>;
};
[PANEL_TYPES.HISTOGRAM]: null;
[PANEL_TYPES.EMPTY_WIDGET]: null;
};

View File

@ -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,
};

View File

@ -40,6 +40,11 @@ const Items: ItemsProps[] = [
icon: <PieChart size={16} color={Color.BG_ROBIN_400} />,
display: 'Pie',
},
{
name: PANEL_TYPES.HISTOGRAM,
icon: <BarChart3 size={16} color={Color.BG_ROBIN_400} />,
display: 'Histogram',
},
];
export interface ItemsProps {

View File

@ -428,9 +428,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{!isEmpty(description) && (
<section className="dashboard-description-section">{description}</section>
)}
<section className="dashboard-variables">
<DashboardVariableSelection />
</section>
{!isEmpty(selectedData.variables) && (
<section className="dashboard-variables">
<DashboardVariableSelection />
</section>
)}
<DashboardGraphSlider />
<Modal

View File

@ -240,6 +240,62 @@
}
}
}
.bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.bucket-size-label {
margin-top: 8px;
}
.bucket-input {
display: flex;
width: 100%;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
align-self: stretch;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.ant-input {
background: var(--bg-ink-300);
}
}
.combine-hist {
display: flex;
justify-content: space-between;
margin-top: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
}
}
.alerts {

View File

@ -28,6 +28,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -39,6 +40,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -50,6 +52,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -61,6 +64,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -72,6 +76,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -83,6 +88,19 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsBucketConfig: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: false,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -96,6 +114,7 @@ export const panelTypeVsPanelTimePreferences: {
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: true,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
@ -110,5 +129,6 @@ export const panelTypeVsColumnUnitPreferences: {
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@ -23,6 +23,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
panelTypeVsCreateAlert,
panelTypeVsFillSpan,
@ -45,12 +46,18 @@ function RightContainer({
setTitle,
title,
selectedGraph,
bucketCount,
bucketWidth,
setBucketCount,
setBucketWidth,
setSelectedTime,
selectedTime,
yAxisUnit,
setYAxisUnit,
setGraphHandler,
thresholds,
combineHistogram,
setCombineHistogram,
setThresholds,
selectedWidget,
isFillSpans,
@ -79,6 +86,7 @@ function RightContainer({
const allowFillSpans = panelTypeVsFillSpan[selectedGraph];
const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph];
const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph];
const allowBucketConfig = panelTypeVsBucketConfig[selectedGraph];
const allowPanelTimePreference =
panelTypeVsPanelTimePreferences[selectedGraph];
@ -222,6 +230,47 @@ function RightContainer({
</section>
</section>
)}
{allowBucketConfig && (
<section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section>
)}
</section>
{allowCreateAlerts && (
@ -263,6 +312,12 @@ interface RightContainerProps {
setSelectedTime: Dispatch<SetStateAction<timePreferance>>;
selectedTime: timePreferance;
yAxisUnit: string;
bucketWidth: number;
bucketCount: number;
combineHistogram: boolean;
setCombineHistogram: Dispatch<SetStateAction<boolean>>;
setBucketWidth: Dispatch<SetStateAction<number>>;
setBucketCount: Dispatch<SetStateAction<number>>;
setYAxisUnit: Dispatch<SetStateAction<string>>;
setGraphHandler: (type: PANEL_TYPES) => void;
thresholds: ThresholdProps[];

View File

@ -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<number>(
selectedWidget?.bucketWidth || 0,
);
const [bucketCount, setBucketCount] = useState<number>(
selectedWidget?.bucketCount || DEFAULT_BUCKET_COUNT,
);
const [combineHistogram, setCombineHistogram] = useState<boolean>(
selectedWidget?.mergeAllActiveQueries || false,
);
const [softMin, setSoftMin] = useState<number | null>(
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}

View File

@ -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: {

View File

@ -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<HTMLDivElement>(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<ToggleGraphProps>();
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 (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={histogramOptions} data={histogramData} ref={lineChartRef} />
{isFullViewMode && setGraphVisibility && !widget.mergeAllActiveQueries && (
<GraphManager
data={histogramData}
name={widget.id}
options={histogramOptions}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphVisibility}
graphsVisibilityStates={graphVisibility}
lineChartRef={lineChartRef}
/>
)}
</div>
);
}
export default HistogramPanelWrapper;

View File

@ -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;

View File

@ -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<number | null>,
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<number>;
// 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;
};

View File

@ -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,
}),

View File

@ -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<SetStateAction<boolean[]>>;
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);

View File

@ -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 });

View File

@ -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

View File

@ -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;

View File

@ -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) {