mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 07:28:59 +08:00
feat: allow custom color pallete in panel for legends (#8063)
This commit is contained in:
parent
aaeffae1bd
commit
8990fb7a73
@ -6,7 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@ -27,6 +27,7 @@ function LeftContainer({
|
||||
requestData,
|
||||
setRequestData,
|
||||
isLoadingPanelData,
|
||||
setQueryResponse,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
@ -49,6 +50,13 @@ function LeftContainer({
|
||||
},
|
||||
);
|
||||
|
||||
// Update parent component with query response for legend colors
|
||||
useEffect(() => {
|
||||
if (setQueryResponse) {
|
||||
setQueryResponse(queryResponse);
|
||||
}
|
||||
}, [queryResponse, setQueryResponse]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetGraph
|
||||
|
@ -0,0 +1,169 @@
|
||||
.legend-colors-container {
|
||||
.legend-colors-collapse {
|
||||
.ant-collapse-header {
|
||||
padding: 8px 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 0 12px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-colors-content {
|
||||
.legend-colors-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding-top: 4px;
|
||||
|
||||
/* Webkit scrollbar styling */
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-400);
|
||||
border-radius: 0.5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styling */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--bg-slate-400) transparent;
|
||||
}
|
||||
|
||||
.legend-item-wrapper {
|
||||
.ant-color-picker-trigger {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-400);
|
||||
border-color: var(--bg-slate-500);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.legend-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.legend-marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-label-text {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.reset-link {
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend-colors-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.legend-colors-container {
|
||||
.legend-colors-content {
|
||||
.legend-items {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-vanilla-500);
|
||||
}
|
||||
|
||||
scrollbar-color: var(--bg-vanilla-400) transparent;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-slate-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
import './LegendColors.styles.scss';
|
||||
|
||||
import { Button, Collapse, ColorPicker, Tooltip, Typography } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { Palette } from 'lucide-react';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
// Component for legend text with conditional tooltip
|
||||
function LegendText({ label }: { label: string }): JSX.Element {
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = (): void => {
|
||||
if (textRef.current) {
|
||||
const isTextOverflowing =
|
||||
textRef.current.scrollWidth > textRef.current.clientWidth;
|
||||
setIsOverflowing(isTextOverflowing);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
// Check on window resize
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
return (): void => window.removeEventListener('resize', checkOverflow);
|
||||
}, [label]);
|
||||
|
||||
return (
|
||||
<Tooltip title={label} open={isOverflowing ? undefined : false}>
|
||||
<span ref={textRef} className="legend-label-text">
|
||||
{label}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
interface LegendColorsProps {
|
||||
customLegendColors: Record<string, string>;
|
||||
setCustomLegendColors: Dispatch<SetStateAction<Record<string, string>>>;
|
||||
queryResponse?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
}
|
||||
|
||||
function LegendColors({
|
||||
customLegendColors,
|
||||
setCustomLegendColors,
|
||||
queryResponse = null as any,
|
||||
}: LegendColorsProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// Get legend labels from query response or current query
|
||||
const legendLabels = useMemo(() => {
|
||||
if (queryResponse?.data?.payload?.data?.result) {
|
||||
return queryResponse.data.payload.data.result.map((item: any) =>
|
||||
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to query data if no response available
|
||||
return currentQuery.builder.queryData.map((query) =>
|
||||
getLabelName({}, query.queryName || '', query.legend || ''),
|
||||
);
|
||||
}, [queryResponse, currentQuery]);
|
||||
|
||||
// Get current or default color for a legend
|
||||
const getColorForLegend = (label: string): string => {
|
||||
if (customLegendColors[label]) {
|
||||
return customLegendColors[label];
|
||||
}
|
||||
return generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
};
|
||||
|
||||
// Handle color change
|
||||
const handleColorChange = (label: string, color: string): void => {
|
||||
setCustomLegendColors((prev) => ({
|
||||
...prev,
|
||||
[label]: color,
|
||||
}));
|
||||
};
|
||||
|
||||
// Reset to default color
|
||||
const resetToDefault = (label: string): void => {
|
||||
setCustomLegendColors((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[label];
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Reset all colors to default
|
||||
const resetAllColors = (): void => {
|
||||
setCustomLegendColors({});
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'legend-colors',
|
||||
label: (
|
||||
<section className="legend-colors-header">
|
||||
<Palette size={16} />
|
||||
<Typography.Text className="typography">Legend Colors</Typography.Text>
|
||||
</section>
|
||||
),
|
||||
children: (
|
||||
<div className="legend-colors-content">
|
||||
{legendLabels.length === 0 ? (
|
||||
<Typography.Text type="secondary">
|
||||
No legends available. Run a query to see legend options.
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<>
|
||||
<div className="legend-colors-header">
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={resetAllColors}
|
||||
disabled={Object.keys(customLegendColors).length === 0}
|
||||
>
|
||||
Reset All
|
||||
</Button>
|
||||
</div>
|
||||
<div className="legend-items">
|
||||
{legendLabels.map((label: string) => (
|
||||
<div key={label} className="legend-item-wrapper">
|
||||
<ColorPicker
|
||||
value={getColorForLegend(label)}
|
||||
onChange={(color): void =>
|
||||
handleColorChange(label, color.toHexString())
|
||||
}
|
||||
size="small"
|
||||
showText={false}
|
||||
trigger="click"
|
||||
>
|
||||
<div className="legend-item">
|
||||
<div className="legend-info">
|
||||
<div
|
||||
className="legend-marker"
|
||||
style={{ backgroundColor: getColorForLegend(label) }}
|
||||
/>
|
||||
<LegendText label={label} />
|
||||
</div>
|
||||
{customLegendColors[label] && (
|
||||
<div className="legend-actions">
|
||||
<Typography.Link
|
||||
className="reset-link"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
resetToDefault(label);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Typography.Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ColorPicker>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="legend-colors-container">
|
||||
<Collapse
|
||||
items={items}
|
||||
ghost
|
||||
size="small"
|
||||
expandIconPosition="end"
|
||||
className="legend-colors-collapse"
|
||||
accordion
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LegendColors.defaultProps = {
|
||||
queryResponse: null,
|
||||
};
|
||||
|
||||
export default LegendColors;
|
@ -174,6 +174,10 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-colors {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.panel-time-text {
|
||||
margin-top: 16px;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
@ -164,3 +164,17 @@ export const panelTypeVsLegendPosition: {
|
||||
[PANEL_TYPES.HISTOGRAM]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsLegendColors: {
|
||||
[key in PANEL_TYPES]: boolean;
|
||||
} = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: true,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.HISTOGRAM]: true,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
@ -30,11 +30,14 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ColumnUnit,
|
||||
LegendPosition,
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
@ -44,6 +47,7 @@ import {
|
||||
panelTypeVsColumnUnitPreferences,
|
||||
panelTypeVsCreateAlert,
|
||||
panelTypeVsFillSpan,
|
||||
panelTypeVsLegendColors,
|
||||
panelTypeVsLegendPosition,
|
||||
panelTypeVsLogScale,
|
||||
panelTypeVsPanelTimePreferences,
|
||||
@ -52,6 +56,7 @@ import {
|
||||
panelTypeVsThreshold,
|
||||
panelTypeVsYAxisUnit,
|
||||
} from './constants';
|
||||
import LegendColors from './LegendColors/LegendColors';
|
||||
import ThresholdSelector from './Threshold/ThresholdSelector';
|
||||
import { ThresholdProps } from './Threshold/types';
|
||||
import { timePreferance } from './timeItems';
|
||||
@ -105,6 +110,9 @@ function RightContainer({
|
||||
setIsLogScale,
|
||||
legendPosition,
|
||||
setLegendPosition,
|
||||
customLegendColors,
|
||||
setCustomLegendColors,
|
||||
queryResponse,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const [inputValue, setInputValue] = useState(title);
|
||||
@ -136,6 +144,7 @@ function RightContainer({
|
||||
const allowPanelTimePreference =
|
||||
panelTypeVsPanelTimePreferences[selectedGraph];
|
||||
const allowLegendPosition = panelTypeVsLegendPosition[selectedGraph];
|
||||
const allowLegendColors = panelTypeVsLegendColors[selectedGraph];
|
||||
|
||||
const allowPanelColumnPreference =
|
||||
panelTypeVsColumnUnitPreferences[selectedGraph];
|
||||
@ -462,6 +471,16 @@ function RightContainer({
|
||||
</Select>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{allowLegendColors && (
|
||||
<section className="legend-colors">
|
||||
<LegendColors
|
||||
customLegendColors={customLegendColors}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={queryResponse}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{allowCreateAlerts && (
|
||||
@ -529,10 +548,17 @@ interface RightContainerProps {
|
||||
setIsLogScale: Dispatch<SetStateAction<boolean>>;
|
||||
legendPosition: LegendPosition;
|
||||
setLegendPosition: Dispatch<SetStateAction<LegendPosition>>;
|
||||
customLegendColors: Record<string, string>;
|
||||
setCustomLegendColors: Dispatch<SetStateAction<Record<string, string>>>;
|
||||
queryResponse?: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
}
|
||||
|
||||
RightContainer.defaultProps = {
|
||||
selectedWidget: undefined,
|
||||
queryResponse: null,
|
||||
};
|
||||
|
||||
export default RightContainer;
|
||||
|
@ -34,9 +34,11 @@ import {
|
||||
} from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ColumnUnit,
|
||||
Dashboard,
|
||||
@ -44,6 +46,7 @@ import {
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@ -191,6 +194,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
|
||||
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||
);
|
||||
const [customLegendColors, setCustomLegendColors] = useState<
|
||||
Record<string, string>
|
||||
>(selectedWidget?.customLegendColors || {});
|
||||
|
||||
const [saveModal, setSaveModal] = useState(false);
|
||||
const [discardModal, setDiscardModal] = useState(false);
|
||||
|
||||
@ -257,6 +264,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedTracesFields,
|
||||
isLogScale,
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||
};
|
||||
});
|
||||
@ -282,6 +290,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
stackedBarChart,
|
||||
isLogScale,
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths,
|
||||
]);
|
||||
|
||||
@ -340,6 +349,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
// hence while changing the query contains the older value and the processing logic fails
|
||||
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
||||
|
||||
// State to hold query response for sharing between left and right containers
|
||||
const [queryResponse, setQueryResponse] = useState<
|
||||
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||
>(null as any);
|
||||
|
||||
// request data should be handled by the parent and the child components should consume the same
|
||||
// this has been moved here from the left container
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
@ -482,6 +496,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||
},
|
||||
]
|
||||
: [
|
||||
@ -510,6 +525,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||
},
|
||||
...afterWidgets,
|
||||
],
|
||||
@ -723,6 +739,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
requestData={requestData}
|
||||
setRequestData={setRequestData}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
setQueryResponse={setQueryResponse}
|
||||
/>
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
@ -766,6 +783,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
setIsLogScale={setIsLogScale}
|
||||
legendPosition={legendPosition}
|
||||
setLegendPosition={setLegendPosition}
|
||||
customLegendColors={customLegendColors}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={queryResponse}
|
||||
softMin={softMin}
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
|
@ -27,6 +27,11 @@ export interface WidgetGraphProps {
|
||||
requestData: GetQueryResultsProps;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
isLoadingPanelData: boolean;
|
||||
setQueryResponse?: Dispatch<
|
||||
SetStateAction<
|
||||
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export type WidgetGraphContainerProps = {
|
||||
|
@ -50,14 +50,19 @@ function PiePanelWrapper({
|
||||
color: string;
|
||||
}[] = [].concat(
|
||||
...(panelData
|
||||
.map((d) => ({
|
||||
label: getLabelName(d.metric, d.queryName || '', d.legend || ''),
|
||||
value: d.values?.[0]?.[1],
|
||||
color: generateColor(
|
||||
getLabelName(d.metric, d.queryName || '', d.legend || ''),
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
),
|
||||
}))
|
||||
.map((d) => {
|
||||
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
|
||||
return {
|
||||
label,
|
||||
value: d.values?.[0]?.[1],
|
||||
color:
|
||||
widget?.customLegendColors?.[label] ||
|
||||
generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((d) => d !== undefined) as never[]),
|
||||
);
|
||||
|
||||
|
@ -138,6 +138,7 @@ function UplotPanelWrapper({
|
||||
timezone: timezone.value,
|
||||
customSeries,
|
||||
isLogScale: widget?.isLogScale,
|
||||
colorMapping: widget?.customLegendColors,
|
||||
enhancedLegend: true, // Enable enhanced legend
|
||||
legendPosition: widget?.legendPosition,
|
||||
}),
|
||||
@ -166,6 +167,7 @@ function UplotPanelWrapper({
|
||||
customSeries,
|
||||
widget?.isLogScale,
|
||||
widget?.legendPosition,
|
||||
widget?.customLegendColors,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -34,6 +34,7 @@ const getSeries = ({
|
||||
panelType,
|
||||
hiddenGraph,
|
||||
isDarkMode,
|
||||
colorMapping,
|
||||
}: GetSeriesProps): uPlot.Options['series'] => {
|
||||
const configurations: uPlot.Series[] = [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
@ -52,10 +53,12 @@ const getSeries = ({
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const color = generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
const color =
|
||||
colorMapping?.[label] ||
|
||||
generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
|
||||
const pointSize = seriesList[i].values.length > 1 ? 5 : 10;
|
||||
const showPoints = !(seriesList[i].values.length > 1);
|
||||
@ -105,6 +108,7 @@ export type GetSeriesProps = {
|
||||
hiddenGraph?: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
colorMapping?: Record<string, string>;
|
||||
};
|
||||
|
||||
export default getSeries;
|
||||
|
@ -117,6 +117,7 @@ export interface IBaseWidget {
|
||||
isLogScale?: boolean;
|
||||
columnWidths?: Record<string, number>;
|
||||
legendPosition?: LegendPosition;
|
||||
customLegendColors?: Record<string, string>;
|
||||
}
|
||||
export interface Widgets extends IBaseWidget {
|
||||
query: Query;
|
||||
|
Loading…
x
Reference in New Issue
Block a user