mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 00:39:06 +08:00
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:
parent
b39f703919
commit
7e9bf2d48d
@ -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,
|
||||
};
|
||||
|
||||
|
@ -286,6 +286,7 @@ export enum PANEL_TYPES {
|
||||
TRACE = 'trace',
|
||||
BAR = 'bar',
|
||||
PIE = 'pie',
|
||||
HISTOGRAM = 'histogram',
|
||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
[
|
||||
|
@ -17,6 +17,10 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.height-widget {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.list-graph-container {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
|
@ -27,5 +27,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
|
||||
TRACE: false,
|
||||
BAR: true,
|
||||
PIE: false,
|
||||
HISTOGRAM: false,
|
||||
EMPTY_WIDGET: false,
|
||||
};
|
||||
|
@ -204,6 +204,7 @@ function FullView({
|
||||
<div
|
||||
className={cx('graph-container', {
|
||||
disabled: isDashboardLocked,
|
||||
'height-widget': widget?.mergeAllActiveQueries,
|
||||
'list-graph-container': isListView,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
|
@ -49,6 +49,7 @@ const GridPanelSwitch = forwardRef<
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.HISTOGRAM]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
|
@ -43,5 +43,6 @@ export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.BAR]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.HISTOGRAM]: null;
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
|
@ -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}
|
||||
|
@ -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: {
|
||||
|
103
frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx
Normal file
103
frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx
Normal 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;
|
@ -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;
|
||||
|
276
frontend/src/container/PanelWrapper/histogram.ts
Normal file
276
frontend/src/container/PanelWrapper/histogram.ts
Normal 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;
|
||||
};
|
@ -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,
|
||||
}),
|
||||
|
199
frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts
Normal file
199
frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts
Normal 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);
|
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user