mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-04 17:30:34 +01:00
Compare commits
1 Commits
feat/dashb
...
feat/toolt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca473e4c65 |
@@ -10,12 +10,31 @@
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.crossPanelSync {
|
||||
.crossPanelSyncGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
& + & {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.crossPanelSyncInfo {
|
||||
|
||||
@@ -3,8 +3,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
@@ -24,6 +28,9 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
dashboardData?.id,
|
||||
);
|
||||
|
||||
const [syncTooltipFilterMode, setSyncTooltipFilterMode] =
|
||||
useSyncTooltipFilterMode(dashboardData?.id);
|
||||
|
||||
const selectedData = dashboardData?.data;
|
||||
|
||||
const {
|
||||
@@ -162,27 +169,56 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col className={`${styles.overviewSettings} ${styles.crossPanelSync}`}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</Typography.Text>
|
||||
<Col className={`${styles.overviewSettings} ${styles.crossPanelSyncGroup}`}>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Sync Mode
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={cursorSyncMode}
|
||||
onChange={(e): void => {
|
||||
setCursorSyncMode(e.target.value as DashboardCursorSync);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={DashboardCursorSync.None}>No Sync</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Crosshair}>
|
||||
Crosshair
|
||||
</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Tooltip}>Tooltip</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={cursorSyncMode}
|
||||
onChange={(e): void => {
|
||||
setCursorSyncMode(e.target.value as DashboardCursorSync);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={DashboardCursorSync.None}>No Sync</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Crosshair}>
|
||||
Crosshair
|
||||
</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Tooltip}>Tooltip</Radio.Button>
|
||||
</Radio.Group>
|
||||
{cursorSyncMode === DashboardCursorSync.Tooltip && (
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Synced Tooltip Series
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Show only series that intersect on group-by, or every series with the
|
||||
matching ones highlighted
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={syncTooltipFilterMode}
|
||||
onChange={(e): void => {
|
||||
setSyncTooltipFilterMode(e.target.value as SyncTooltipFilterMode);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={SyncTooltipFilterMode.Filtered}>
|
||||
Filtered
|
||||
</Radio.Button>
|
||||
<Radio.Button value={SyncTooltipFilterMode.All}>All</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
{numberOfUnsavedChanges > 0 && (
|
||||
<div className={styles.overviewSettingsFooter}>
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function ChartWrapper({
|
||||
onClick,
|
||||
syncMode,
|
||||
syncKey,
|
||||
syncFilterMode,
|
||||
onDestroy = noop,
|
||||
children,
|
||||
layoutChildren,
|
||||
@@ -70,8 +71,9 @@ export default function ChartWrapper({
|
||||
() => ({
|
||||
yAxisUnit,
|
||||
groupBy,
|
||||
filterMode: syncFilterMode,
|
||||
}),
|
||||
[yAxisUnit, groupBy],
|
||||
[yAxisUnit, groupBy, syncFilterMode],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
TooltipClickData,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
@@ -30,6 +31,7 @@ interface UPlotBasedChartProps {
|
||||
legendConfig: LegendConfig;
|
||||
syncMode?: DashboardCursorSync;
|
||||
syncKey?: string;
|
||||
syncFilterMode?: SyncTooltipFilterMode;
|
||||
plotRef?: (plot: uPlot | null) => void;
|
||||
onDestroy?: (plot: uPlot) => void;
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
@@ -38,6 +39,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardData?.id);
|
||||
const [syncMode] = useDashboardCursorSyncMode(dashboardId, panelMode);
|
||||
const [syncFilterMode] = useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
@@ -132,7 +134,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<BarChart
|
||||
key={syncMode}
|
||||
key={`${syncMode}-${syncFilterMode}`}
|
||||
config={config}
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
@@ -150,6 +152,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
syncMode={syncMode}
|
||||
syncFilterMode={syncFilterMode}
|
||||
>
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
|
||||
@@ -4,6 +4,7 @@ import ChartManager from 'container/DashboardContainer/visualization/components/
|
||||
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
@@ -37,6 +38,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardData?.id);
|
||||
const [syncMode] = useDashboardCursorSyncMode(dashboardId, panelMode);
|
||||
const [syncFilterMode] = useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
@@ -123,7 +125,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<TimeSeries
|
||||
key={syncMode}
|
||||
key={`${syncMode}-${syncFilterMode}`}
|
||||
config={config}
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
@@ -137,6 +139,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
syncMode={syncMode}
|
||||
syncFilterMode={syncFilterMode}
|
||||
layoutChildren={layoutChildren}
|
||||
>
|
||||
<ContextMenu
|
||||
|
||||
@@ -90,6 +90,31 @@ describe('useDashboardCursorSyncMode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('without a panelMode (e.g. dashboard settings call site)', () => {
|
||||
it('reads the stored value just like DASHBOARD_VIEW does', () => {
|
||||
useDashboardPreferencesStore.setState({
|
||||
preferences: { 'dash-1': { cursorSyncMode: DashboardCursorSync.Tooltip } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDashboardCursorSyncMode('dash-1'));
|
||||
|
||||
expect(result.current[0]).toBe(DashboardCursorSync.Tooltip);
|
||||
});
|
||||
|
||||
it('writes through the setter to the store', () => {
|
||||
const { result } = renderHook(() => useDashboardCursorSyncMode('dash-1'));
|
||||
|
||||
act(() => {
|
||||
result.current[1](DashboardCursorSync.None);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(DashboardCursorSync.None);
|
||||
expect(useDashboardPreferencesStore.getState().preferences).toStrictEqual({
|
||||
'dash-1': { cursorSyncMode: DashboardCursorSync.None },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([[PanelMode.DASHBOARD_EDIT], [PanelMode.STANDALONE_VIEW]])(
|
||||
'in %s mode (cursor sync disabled)',
|
||||
(panelMode) => {
|
||||
|
||||
@@ -9,14 +9,18 @@ export function useDashboardCursorSyncMode(
|
||||
dashboardId: string | undefined,
|
||||
panelMode?: PanelMode,
|
||||
): [DashboardCursorSync, (value: DashboardCursorSync) => void] {
|
||||
if (panelMode !== PanelMode.DASHBOARD_VIEW) {
|
||||
// Only allow cursor sync modes to be set in the dashboard view mode, as the other modes are used for editing and standalone panel views where cursor sync is not applicable.
|
||||
return [DEFAULT_CURSOR_SYNC_MODE, () => {}];
|
||||
}
|
||||
|
||||
return useDashboardPreference(
|
||||
const [value, setValue] = useDashboardPreference(
|
||||
dashboardId,
|
||||
'cursorSyncMode',
|
||||
DEFAULT_CURSOR_SYNC_MODE,
|
||||
);
|
||||
|
||||
// Chart panels in edit / standalone modes don't participate in cross-panel
|
||||
// sync, so return the default with a no-op setter for them. Callers without
|
||||
// a panelMode (e.g. dashboard settings) read/write the preference normally.
|
||||
if (panelMode && panelMode !== PanelMode.DASHBOARD_VIEW) {
|
||||
return [DEFAULT_CURSOR_SYNC_MODE, (): void => {}];
|
||||
}
|
||||
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ import { useCallback } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
|
||||
// Per-dashboard preferences persisted in localStorage. Add new preference
|
||||
// fields here as they are introduced.
|
||||
export type DashboardPreferences = {
|
||||
cursorSyncMode?: DashboardCursorSync;
|
||||
syncTooltipFilterMode?: SyncTooltipFilterMode;
|
||||
};
|
||||
|
||||
interface DashboardPreferencesState {
|
||||
|
||||
15
frontend/src/hooks/dashboard/useSyncTooltipFilterMode.ts
Normal file
15
frontend/src/hooks/dashboard/useSyncTooltipFilterMode.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { SyncTooltipFilterMode } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
|
||||
import { useDashboardPreference } from './useDashboardPreference';
|
||||
|
||||
const DEFAULT_SYNC_TOOLTIP_FILTER_MODE = SyncTooltipFilterMode.Filtered;
|
||||
|
||||
export function useSyncTooltipFilterMode(
|
||||
dashboardId: string | undefined,
|
||||
): [SyncTooltipFilterMode, (value: SyncTooltipFilterMode) => void] {
|
||||
return useDashboardPreference(
|
||||
dashboardId,
|
||||
'syncTooltipFilterMode',
|
||||
DEFAULT_SYNC_TOOLTIP_FILTER_MODE,
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export default function BarChartTooltip(props: BarTooltipProps): JSX.Element {
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
isStackedBarChart: props.isStackedBarChart,
|
||||
syncedSeriesIndexes: props.syncedSeriesIndexes,
|
||||
syncFilterMode: props.syncFilterMode,
|
||||
}),
|
||||
[
|
||||
props.uPlotInstance,
|
||||
@@ -26,6 +27,7 @@ export default function BarChartTooltip(props: BarTooltipProps): JSX.Element {
|
||||
props.decimalPrecision,
|
||||
props.isStackedBarChart,
|
||||
props.syncedSeriesIndexes,
|
||||
props.syncFilterMode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function HistogramTooltip(
|
||||
yAxisUnit: props.yAxisUnit ?? '',
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
syncedSeriesIndexes: props.syncedSeriesIndexes,
|
||||
syncFilterMode: props.syncFilterMode,
|
||||
}),
|
||||
[
|
||||
props.uPlotInstance,
|
||||
@@ -26,6 +27,7 @@ export default function HistogramTooltip(
|
||||
props.yAxisUnit,
|
||||
props.decimalPrecision,
|
||||
props.syncedSeriesIndexes,
|
||||
props.syncFilterMode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function TimeSeriesTooltip(
|
||||
yAxisUnit: props.yAxisUnit ?? '',
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
syncedSeriesIndexes: props.syncedSeriesIndexes,
|
||||
syncFilterMode: props.syncFilterMode,
|
||||
}),
|
||||
[
|
||||
props.uPlotInstance,
|
||||
@@ -26,6 +27,7 @@ export default function TimeSeriesTooltip(
|
||||
props.yAxisUnit,
|
||||
props.decimalPrecision,
|
||||
props.syncedSeriesIndexes,
|
||||
props.syncFilterMode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function TooltipList({
|
||||
style={{ height }}
|
||||
totalListHeightChanged={setTotalListHeight}
|
||||
itemContent={(_, item): JSX.Element => (
|
||||
<TooltipItem item={item} isItemActive={false} />
|
||||
<TooltipItem item={item} isItemActive={item.isHighlighted === true} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PrecisionOption } from 'components/Graph/types';
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import uPlot, { AlignedData, Series } from 'uplot';
|
||||
|
||||
import { SyncTooltipFilterMode } from '../../plugins/TooltipPlugin/types';
|
||||
import { TooltipContentItem } from '../types';
|
||||
|
||||
export const FALLBACK_SERIES_COLOR = '#000000';
|
||||
@@ -63,6 +64,7 @@ export function buildTooltipContent({
|
||||
decimalPrecision,
|
||||
isStackedBarChart,
|
||||
syncedSeriesIndexes,
|
||||
syncFilterMode,
|
||||
}: {
|
||||
data: AlignedData;
|
||||
series: Series[];
|
||||
@@ -73,10 +75,16 @@ export function buildTooltipContent({
|
||||
decimalPrecision?: PrecisionOption;
|
||||
isStackedBarChart?: boolean;
|
||||
syncedSeriesIndexes?: number[] | null;
|
||||
syncFilterMode?: SyncTooltipFilterMode;
|
||||
}): TooltipContentItem[] {
|
||||
const items: TooltipContentItem[] = [];
|
||||
const allowedIndexes =
|
||||
const matchedIndexes =
|
||||
syncedSeriesIndexes != null ? new Set(syncedSeriesIndexes) : null;
|
||||
const filterMode = syncFilterMode ?? SyncTooltipFilterMode.Filtered;
|
||||
// In Filtered mode the matched indexes act as a whitelist; in All mode every
|
||||
// series renders and matched indexes only drive row highlighting.
|
||||
const allowedIndexes =
|
||||
filterMode === SyncTooltipFilterMode.All ? null : matchedIndexes;
|
||||
|
||||
for (let seriesIndex = 1; seriesIndex < series.length; seriesIndex += 1) {
|
||||
const seriesItem = series[seriesIndex];
|
||||
@@ -89,6 +97,7 @@ export function buildTooltipContent({
|
||||
|
||||
const dataIndex = dataIndexes[seriesIndex];
|
||||
const isSync = allowedIndexes != null;
|
||||
const isHighlighted = matchedIndexes?.has(seriesIndex) ?? false;
|
||||
|
||||
if (dataIndex === null) {
|
||||
if (isSync) {
|
||||
@@ -98,6 +107,7 @@ export function buildTooltipContent({
|
||||
tooltipValue: 'No Data',
|
||||
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
|
||||
isActive: false,
|
||||
isHighlighted,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
@@ -118,6 +128,7 @@ export function buildTooltipContent({
|
||||
tooltipValue: getToolTipValue(baseValue, yAxisUnit, decimalPrecision),
|
||||
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
|
||||
isActive: seriesIndex === activeSeriesIndex,
|
||||
isHighlighted,
|
||||
});
|
||||
} else if (isSync) {
|
||||
items.push({
|
||||
@@ -126,6 +137,7 @@ export function buildTooltipContent({
|
||||
tooltipValue: 'No Data',
|
||||
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
|
||||
isActive: false,
|
||||
isHighlighted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrecisionOption } from 'components/Graph/types';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
||||
import { SyncTooltipFilterMode } from '../plugins/TooltipPlugin/types';
|
||||
|
||||
/**
|
||||
* Props for the Plot component
|
||||
@@ -58,9 +59,15 @@ export interface TooltipRenderArgs {
|
||||
isPinned: boolean;
|
||||
dismiss: () => void;
|
||||
viaSync: boolean;
|
||||
/** In Tooltip sync mode, limits which series are rendered in the receiver tooltip.
|
||||
* null = no filtering; [] = no matches (tooltip hidden upstream); [...] = allowed indexes */
|
||||
/** In Tooltip sync mode, identifies receiver series that match the source's
|
||||
* focused series on the shared groupBy keys.
|
||||
* Filtered mode: limits which series are rendered (null = no filter,
|
||||
* [] = no matches/tooltip hidden upstream, [...] = allowed indexes).
|
||||
* All mode: same indexes are interpreted as a highlight set; non-matching
|
||||
* series still render. */
|
||||
syncedSeriesIndexes?: number[] | null;
|
||||
/** Receiver-side filter mode for the synced tooltip. Defaults to Filtered. */
|
||||
syncFilterMode?: SyncTooltipFilterMode;
|
||||
}
|
||||
|
||||
export interface BaseTooltipProps {
|
||||
@@ -106,4 +113,9 @@ export interface TooltipContentItem {
|
||||
tooltipValue: string;
|
||||
color: string;
|
||||
isActive: boolean;
|
||||
/** Synced receiver series whose metric matches the source's focused series
|
||||
* on the shared groupBy keys, in 'all' filter mode. List rendering uses this
|
||||
* to apply the active highlight to matching rows while non-matching rows
|
||||
* stay dimmed. */
|
||||
isHighlighted?: boolean;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
DEFAULT_PIN_TOOLTIP_KEY,
|
||||
SyncTooltipFilterMode,
|
||||
TooltipControllerContext,
|
||||
TooltipControllerState,
|
||||
TooltipLayoutInfo,
|
||||
@@ -199,10 +200,14 @@ export default function TooltipPlugin({
|
||||
if (!controller.hoverActive || !plot) {
|
||||
return null;
|
||||
}
|
||||
// In Tooltip sync mode, suppress the receiver tooltip entirely when
|
||||
// no receiver series match the source panel's focused series.
|
||||
const filterMode =
|
||||
syncMetadata?.filterMode ?? SyncTooltipFilterMode.Filtered;
|
||||
// In Filtered Tooltip sync mode, suppress the receiver tooltip entirely
|
||||
// when no receiver series match the source panel's focused series. In
|
||||
// All mode the tooltip still renders with every series visible.
|
||||
if (
|
||||
syncTooltipWithDashboard &&
|
||||
filterMode === SyncTooltipFilterMode.Filtered &&
|
||||
controller.cursorDrivenBySync &&
|
||||
Array.isArray(controller.syncedSeriesIndexes) &&
|
||||
controller.syncedSeriesIndexes.length === 0
|
||||
@@ -217,6 +222,7 @@ export default function TooltipPlugin({
|
||||
dismiss: dismissTooltip,
|
||||
viaSync: controller.cursorDrivenBySync,
|
||||
syncedSeriesIndexes: controller.syncedSeriesIndexes,
|
||||
syncFilterMode: filterMode,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import uPlot from 'uplot';
|
||||
|
||||
import type { ExtendedSeries } from '../../config/types';
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import type { TooltipControllerState, TooltipSyncMetadata } from './types';
|
||||
import {
|
||||
SyncTooltipFilterMode,
|
||||
type TooltipControllerState,
|
||||
type TooltipSyncMetadata,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Returns the dimension keys present in both groupBy arrays.
|
||||
@@ -79,10 +83,15 @@ function applySourceSync({
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns:
|
||||
* null – no groupBy filtering configured or cursor off-chart (no-op for tooltip)
|
||||
* [] – groupBy configured but no receiver series match the source (hide synced tooltip)
|
||||
* number[] – 1-based indexes of matching receiver series (show only these)
|
||||
* Computes receiver-side series filtering / highlighting for Tooltip sync.
|
||||
*
|
||||
* Returns the indexes that the tooltip render path should treat per
|
||||
* `syncMetadata.filterMode`:
|
||||
* - Filtered (default): null = no filter, [] = no matches (suppress tooltip),
|
||||
* number[] = allowed indexes (show only these).
|
||||
* - All: null = no highlight (show all), number[] = highlight set (show all,
|
||||
* emphasize matching rows). Never returns [] in this mode so the synced
|
||||
* tooltip is not suppressed when matches are missing.
|
||||
*/
|
||||
function applyReceiverSync({
|
||||
uPlotInstance,
|
||||
@@ -102,6 +111,10 @@ function applyReceiverSync({
|
||||
yCrosshairEl.style.display =
|
||||
sourceMetadata?.yAxisUnit === syncMetadata?.yAxisUnit ? '' : 'none';
|
||||
|
||||
const filterMode = syncMetadata?.filterMode ?? SyncTooltipFilterMode.Filtered;
|
||||
const noMatchResult: number[] | null =
|
||||
filterMode === SyncTooltipFilterMode.All ? null : [];
|
||||
|
||||
if (commonKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -114,7 +127,7 @@ function applyReceiverSync({
|
||||
const sourceSeriesMetric = syncCursorRegistry.getActiveSeriesMetric(syncKey);
|
||||
if (sourceSeriesMetric == null) {
|
||||
uPlotInstance.setSeries(null, { focus: false });
|
||||
return [];
|
||||
return noMatchResult;
|
||||
}
|
||||
|
||||
const matchingIdxs = findMatchingSeriesIndexes(
|
||||
@@ -125,7 +138,7 @@ function applyReceiverSync({
|
||||
|
||||
if (matchingIdxs.length === 0) {
|
||||
uPlotInstance.setSeries(null, { focus: false });
|
||||
return [];
|
||||
return noMatchResult;
|
||||
}
|
||||
|
||||
uPlotInstance.setSeries(matchingIdxs[0], { focus: true });
|
||||
|
||||
@@ -16,9 +16,18 @@ export const TOOLTIP_OFFSET = 10;
|
||||
export const DEFAULT_PIN_TOOLTIP_KEY = 'p';
|
||||
|
||||
export enum DashboardCursorSync {
|
||||
Crosshair,
|
||||
None,
|
||||
Tooltip,
|
||||
Crosshair = 'crosshair',
|
||||
None = 'none',
|
||||
Tooltip = 'tooltip',
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls whether a synced tooltip filters series by groupBy intersection
|
||||
* or shows every series with the matching ones highlighted.
|
||||
*/
|
||||
export enum SyncTooltipFilterMode {
|
||||
Filtered = 'filtered',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export interface TooltipViewState {
|
||||
@@ -41,6 +50,7 @@ export interface TooltipLayoutInfo {
|
||||
export interface TooltipSyncMetadata {
|
||||
yAxisUnit?: string;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
filterMode?: SyncTooltipFilterMode;
|
||||
}
|
||||
|
||||
export interface TooltipPluginProps {
|
||||
|
||||
Reference in New Issue
Block a user