Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tooltip): custom tooltip header context #1989

Merged
merged 12 commits into from
Apr 3, 2023
29 changes: 18 additions & 11 deletions packages/charts/api/charts.api.md
nickofthyme marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2487,7 +2487,7 @@ export const Settings: (props: SFProps<SettingsSpec, keyof (typeof settingsBuild
// Warning: (ae-forgotten-export) The symbol "BuildProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "debug" | "rotation" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "legendPosition" | "flatLegend" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin", "tooltip" | "ariaLabel" | "theme" | "xDomain" | "ariaDescription" | "ariaDescribedBy" | "ariaLabelledBy" | "ariaTableCaption" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "customLegend" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "legendSort", never>;
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "debug" | "rotation" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "legendPosition" | "flatLegend" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin", "tooltip" | "ariaLabel" | "xDomain" | "ariaDescription" | "ariaDescribedBy" | "ariaLabelledBy" | "ariaTableCaption" | "theme" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "customLegend" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "legendSort", never>;

// @public (undocumented)
export type SettingsProps = ComponentProps<typeof Settings>;
Expand Down Expand Up @@ -2846,7 +2846,7 @@ export function toEntries<T extends Record<string, string>, S>(array: T[], acces
export type ToggleSelectedTooltipItemCallback = (item: TooltipValue<any, SeriesIdentifier>) => any;

// @public
export const Tooltip: <D extends BaseDatum = any, SI extends SeriesIdentifier = SeriesIdentifier>(props: SFProps<TooltipSpec<D, SI>, "id" | "chartType" | "specType", "body" | "footer" | "header" | "type" | "actions" | "selectionPrompt" | "actionsLoading" | "noActionsLoaded" | "snap" | "showNullValues" | "actionPrompt" | "pinningPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "headerFormatter" | "unit" | "customTooltip" | "stickTo", never>) => null;
export const Tooltip: <D extends BaseDatum = any, SI extends SeriesIdentifier = SeriesIdentifier>(props: SFProps<TooltipSpec<D, SI>, "id" | "chartType" | "specType", "body" | "footer" | "header" | "type" | "snap" | "showNullValues" | "actions" | "actionsLoading" | "noActionsLoaded" | "actionPrompt" | "pinningPrompt" | "selectionPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "headerFormatter" | "unit" | "customTooltip" | "stickTo", never>) => null;

// @public
export type TooltipAction<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> = {
Expand All @@ -2857,7 +2857,7 @@ export type TooltipAction<D extends BaseDatum = Datum, SI extends SeriesIdentifi
};

// @public
export const tooltipBuildProps: BuildProps<TooltipSpec<any, SeriesIdentifier>, "id" | "chartType" | "specType", "body" | "footer" | "header" | "type" | "actions" | "selectionPrompt" | "actionsLoading" | "noActionsLoaded" | "snap" | "showNullValues" | "actionPrompt" | "pinningPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "headerFormatter" | "unit" | "customTooltip" | "stickTo", never>;
export const tooltipBuildProps: BuildProps<TooltipSpec<any, SeriesIdentifier>, "id" | "chartType" | "specType", "body" | "footer" | "header" | "type" | "snap" | "showNullValues" | "actions" | "actionsLoading" | "noActionsLoaded" | "actionPrompt" | "pinningPrompt" | "selectionPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "headerFormatter" | "unit" | "customTooltip" | "stickTo", never>;

// @public
export type TooltipCellStyle = Pick<CSSProperties, 'maxHeight' | 'textAlign' | 'padding' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'>;
Expand Down Expand Up @@ -2905,14 +2905,21 @@ export const TooltipDivider: ({ margin }: TooltipDividerProps) => JSX.Element;
// @public (undocumented)
export const TooltipFooter: ({ children }: TooltipFooterProps) => JSX.Element;

// Warning: (ae-forgotten-export) The symbol "TooltipHeaderProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const TooltipHeader: <D extends BaseDatum = any, SI extends SeriesIdentifier = SeriesIdentifier>(props: TooltipHeaderProps<D, SI>) => JSX.Element | null;
export const TooltipHeader: <D extends BaseDatum = any>(props: TooltipHeaderProps<D>) => JSX.Element | null;

// @public
export type TooltipHeaderFormatter<D extends BaseDatum = Datum> = (data: PointerValue<D>) => JSX.Element | string;

// @public (undocumented)
export type TooltipHeaderProps<D extends BaseDatum = Datum> = PropsOrChildrenWithProps<{
header: PointerValue<D> | null;
formatter?: TooltipHeaderFormatter<D>;
}>;

// @public
export interface TooltipInfo<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> {
header: TooltipValue<D, SI> | null;
header: PointerValue<D> | null;
values: TooltipValue<D, SI>[];
}

Expand Down Expand Up @@ -2945,18 +2952,18 @@ export interface TooltipSpec<D extends BaseDatum = Datum, SI extends SeriesIdent
}>;
body: 'default' | 'none' | ComponentType<{
items: TooltipValue<D, SI>[];
header: TooltipValue<D, SI> | null;
header: PointerValue<D> | null;
}>;
customTooltip?: CustomTooltip<D, SI>;
footer: 'default' | 'none' | ComponentType<{
items: TooltipValue<D, SI>[];
header: TooltipValue<D, SI> | null;
header: PointerValue<D> | null;
}>;
header: 'default' | 'none' | ComponentType<{
items: TooltipValue<D, SI>[];
header: TooltipValue<D, SI> | null;
header: PointerValue<D> | null;
}>;
headerFormatter?: TooltipValueFormatter<D, SI>;
headerFormatter?: TooltipHeaderFormatter<D>;
maxTooltipItems: number;
maxVisibleTooltipItems: number;
noActionsLoaded: string | ComponentType<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import { getChartRotationSelector } from '../../../../state/selectors/get_chart_
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { getTooltipInteractionState } from '../../../../state/selectors/get_tooltip_interaction_state';
import { getTooltipSpecSelector } from '../../../../state/selectors/get_tooltip_spec';
import { PointerValue } from '../../../../state/types';
import { isNil, Rotation } from '../../../../utils/common';
import { isValidPointerOverEvent } from '../../../../utils/events';
import { IndexedGeometry } from '../../../../utils/geometry';
import { Point } from '../../../../utils/point';
import { getTooltipCompareFn } from '../../../../utils/series_sort';
import { isPointOnGeometry } from '../../rendering/utils';
import { formatTooltip } from '../../tooltip/tooltip';
import { formatTooltipHeader, formatTooltipValue } from '../../tooltip/tooltip';
import { defaultXYLegendSeriesSort } from '../../utils/default_series_sort_fn';
import { DataSeries } from '../../utils/series';
import { BasicSeriesSpec, AxisSpec } from '../../utils/specs';
Expand Down Expand Up @@ -124,7 +125,7 @@ function getTooltipAndHighlightFromValue(
}

// build the tooltip value list
let header: TooltipValue | null = null;
let header: PointerValue | null = null;
const highlightedGeometries: IndexedGeometry[] = [];
const xValues = new Set<any>();
const hideNullValues = !tooltip.showNullValues;
Expand Down Expand Up @@ -160,13 +161,13 @@ function getTooltipAndHighlightFromValue(
}

// format the tooltip values
const formattedTooltip = formatTooltip(indexedGeometry, spec, false, isHighlighted, hasSingleSeries, yAxis);
const formattedTooltip = formatTooltipValue(indexedGeometry, spec, isHighlighted, hasSingleSeries, yAxis);

// format only one time the x value
if (!header) {
// if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter
const formatterAxis = tooltip.headerFormatter ? undefined : xAxis;
header = formatTooltip(indexedGeometry, spec, true, false, hasSingleSeries, formatterAxis);
header = formatTooltipHeader(indexedGeometry, spec, formatterAxis);
}

xValues.add(indexedGeometry.value.x);
Expand All @@ -188,6 +189,7 @@ function getTooltipAndHighlightFromValue(
const sortedTooltipValues = values.sort((a, b) => {
return tooltipSortFn(a.seriesIdentifier, b.seriesIdentifier);
});

return {
tooltip: {
header,
Expand All @@ -206,7 +208,7 @@ export const getHighlightedTooltipTooltipValuesSelector = createCustomCachedSele
const highlightedValues = values.tooltip.values.filter((v) => v.isHighlighted);
const hasTooltipContent = values.tooltip.values.length > tooltip.maxTooltipItems && highlightedValues.length > 0;

if (!pinned && (isFollowTooltipType(tooltipType) || hasTooltipContent)) {
if (!pinned && !tooltip.customTooltip && (isFollowTooltipType(tooltipType) || hasTooltipContent)) {
return {
...values,
tooltip: {
Expand Down
68 changes: 28 additions & 40 deletions packages/charts/src/chart_types/xy_chart/tooltip/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { formatTooltip } from './tooltip';
import { formatTooltipHeader, formatTooltipValue } from './tooltip';
import { ChartType } from '../..';
import { MockBarGeometry } from '../../../mocks';
import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs';
Expand Down Expand Up @@ -117,7 +117,7 @@ describe('Tooltip formatting', () => {
});

test('format simple tooltip', () => {
const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue).toBeDefined();
expect(tooltipValue.valueAccessor).toBe('y1');
expect(tooltipValue.label).toBe('bar_1');
Expand All @@ -130,41 +130,39 @@ describe('Tooltip formatting', () => {
});
it('should set name as spec name when provided', () => {
const name = 'test - spec';
const tooltipValue = formatTooltip(indexedBandedGeometry, { ...SPEC_1, name }, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(indexedBandedGeometry, { ...SPEC_1, name }, false, false, YAXIS_SPEC);
expect(tooltipValue.label).toBe(name);
});
it('should set name as spec id when name is not provided', () => {
const tooltipValue = formatTooltip(indexedBandedGeometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(indexedBandedGeometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue.label).toBe(SPEC_1.id);
});
test('format banded tooltip - upper', () => {
const tooltipValue = formatTooltip(indexedBandedGeometry, bandedSpec, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(indexedBandedGeometry, bandedSpec, false, false, YAXIS_SPEC);
expect(tooltipValue.label).toBe('bar_1 - upper');
});
test('format banded tooltip - y1AccessorFormat', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
indexedBandedGeometry,
{ ...bandedSpec, y1AccessorFormat: ' [max]' },
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.label).toBe('bar_1 [max]');
});
test('format banded tooltip - y1AccessorFormat as function', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
indexedBandedGeometry,
{ ...bandedSpec, y1AccessorFormat: (label) => `[max] ${label}` },
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.label).toBe('[max] bar_1');
});
test('format banded tooltip - lower', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
{
...indexedBandedGeometry,
value: {
Expand All @@ -175,13 +173,12 @@ describe('Tooltip formatting', () => {
bandedSpec,
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.label).toBe('bar_1 - lower');
});
test('format banded tooltip - y0AccessorFormat', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
{
...indexedBandedGeometry,
value: {
Expand All @@ -192,13 +189,12 @@ describe('Tooltip formatting', () => {
{ ...bandedSpec, y0AccessorFormat: ' [min]' },
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.label).toBe('bar_1 [min]');
});
test('format banded tooltip - y0AccessorFormat as function', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
{
...indexedBandedGeometry,
value: {
Expand All @@ -209,7 +205,6 @@ describe('Tooltip formatting', () => {
{ ...bandedSpec, y0AccessorFormat: (label) => `[min] ${label}` },
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue.label).toBe('[min] bar_1');
Expand All @@ -226,7 +221,7 @@ describe('Tooltip formatting', () => {
seriesKeys: ['y1'],
},
};
const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(geometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue).toBeDefined();
expect(tooltipValue.valueAccessor).toBe('y1');
expect(tooltipValue.label).toBe('bar_1');
Expand All @@ -243,7 +238,7 @@ describe('Tooltip formatting', () => {
accessor: 'y0',
},
};
const tooltipValue = formatTooltip(geometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(geometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue).toBeDefined();
expect(tooltipValue.valueAccessor).toBe('y0');
expect(tooltipValue.label).toBe('bar_1');
Expand All @@ -260,17 +255,11 @@ describe('Tooltip formatting', () => {
accessor: 'y0',
},
};
let tooltipValue = formatTooltip(geometry, SPEC_1, true, false, false, YAXIS_SPEC);
expect(tooltipValue).toBeDefined();
expect(tooltipValue.valueAccessor).toBe('y0');
expect(tooltipValue.label).toBe('bar_1');
expect(tooltipValue.isHighlighted).toBe(false);
expect(tooltipValue.color).toBe('blue');
expect(tooltipValue.value).toBe(1);
expect(tooltipValue.formattedValue).toBe('1');
// disable any highlight on x value
tooltipValue = formatTooltip(geometry, SPEC_1, true, true, false, YAXIS_SPEC);
expect(tooltipValue.isHighlighted).toBe(false);
const tooltipHeader = formatTooltipHeader(geometry, SPEC_1, YAXIS_SPEC);
expect(tooltipHeader).toBeDefined();
expect(tooltipHeader.valueAccessor).toBeUndefined();
expect(tooltipHeader.value).toBe(1);
expect(tooltipHeader.formattedValue).toBe('1');
});

it('should format ticks with custom formatter from spec', () => {
Expand All @@ -284,7 +273,7 @@ describe('Tooltip formatting', () => {
...SPEC_1,
tickFormat: tickFormatter,
};
const tooltipValue = formatTooltip(indexedGeometry, spec, false, false, false, axisSpec);
const tooltipValue = formatTooltipValue(indexedGeometry, spec, false, false, axisSpec);
expect(tooltipValue.value).toBe(10);
expect(tooltipValue.formattedValue).toBe('10 spec');
});
Expand All @@ -295,13 +284,13 @@ describe('Tooltip formatting', () => {
...YAXIS_SPEC,
tickFormat: axisTickFormatter,
};
const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, axisSpec);
const tooltipValue = formatTooltipValue(indexedGeometry, SPEC_1, false, false, axisSpec);
expect(tooltipValue.value).toBe(10);
expect(tooltipValue.formattedValue).toBe('10 axis');
});

it('should format ticks with default formatter', () => {
const tooltipValue = formatTooltip(indexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(indexedGeometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue.value).toBe(10);
expect(tooltipValue.formattedValue).toBe('10');
});
Expand All @@ -317,9 +306,9 @@ describe('Tooltip formatting', () => {
...SPEC_1,
tickFormat: tickFormatter,
};
const tooltipValue = formatTooltip(indexedGeometry, spec, true, false, false, axisSpec);
expect(tooltipValue.value).toBe(1);
expect(tooltipValue.formattedValue).toBe('1 axis');
const tooltipHeader = formatTooltipHeader(indexedGeometry, spec, axisSpec);
expect(tooltipHeader.value).toBe(1);
expect(tooltipHeader.formattedValue).toBe('1 axis');
});

it('should format header with default formatter from axis', () => {
Expand All @@ -328,9 +317,9 @@ describe('Tooltip formatting', () => {
...SPEC_1,
tickFormat: tickFormatter,
};
const tooltipValue = formatTooltip(indexedGeometry, spec, true, false, false, YAXIS_SPEC);
expect(tooltipValue.value).toBe(1);
expect(tooltipValue.formattedValue).toBe('1');
const tooltipHeader = formatTooltipHeader(indexedGeometry, spec, YAXIS_SPEC);
expect(tooltipHeader.value).toBe(1);
expect(tooltipHeader.formattedValue).toBe('1');
});

describe('markFormat', () => {
Expand All @@ -347,15 +336,14 @@ describe('Tooltip formatting', () => {
};

it('should format mark value with markFormat', () => {
const tooltipValue = formatTooltip(
const tooltipValue = formatTooltipValue(
markIndexedGeometry,
{
...SPEC_1,
markFormat,
},
false,
false,
false,
YAXIS_SPEC,
);
expect(tooltipValue).toBeDefined();
Expand All @@ -365,7 +353,7 @@ describe('Tooltip formatting', () => {
});

it('should format mark value with defaultTickFormatter', () => {
const tooltipValue = formatTooltip(markIndexedGeometry, SPEC_1, false, false, false, YAXIS_SPEC);
const tooltipValue = formatTooltipValue(markIndexedGeometry, SPEC_1, false, false, YAXIS_SPEC);
expect(tooltipValue).toBeDefined();
expect(tooltipValue.markValue).toBe(10);
expect(tooltipValue.formattedMarkValue).toBe('10');
Expand Down
Loading