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

feat: add highlightBy functionality to ChartTooltip #255

Merged
merged 2 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/ChartTooltip/ChartTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { FC } from 'react';
import { ChartTooltipProps } from '../../types';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ChartTooltip: FC<ChartTooltipProps> = ({ children, excludeDataKeys: excludeDataKey }) => {
const ChartTooltip: FC<ChartTooltipProps> = ({ children, excludeDataKeys, highlightBy = 'item' }) => {
return null;
};

Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const STACK_ID = 'rscStackId';

// signal names
export const HIGHLIGHTED_ITEM = 'highlightedItem'; // data point
export const HIGHLIGHTED_GROUP = 'highlightedGroup'; // data point
export const HIGHLIGHTED_SERIES = 'highlightedSeries'; // series
export const SELECTED_ITEM = 'selectedItem'; // data point
export const SELECTED_SERIES = 'selectedSeries'; // series
Expand Down
24 changes: 14 additions & 10 deletions src/specBuilder/area/areaSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const defaultAreaProps: AreaSpecProps = {
color: DEFAULT_COLOR,
dimension: DEFAULT_TIME_DIMENSION,
index: 0,
markType: 'area',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a property markType to bar, area, line, scatter and donut. This is helpful for chart tooltip since it could be added to any of these. With the markType prop, chart tooltip can be setup correctly for the correct mark type.

metric: DEFAULT_METRIC,
name: 'area0',
opacity: 0.8,
Expand Down Expand Up @@ -197,22 +198,25 @@ describe('areaSpecBuilder', () => {
test('children: should add signals', () => {
const tooltip = createElement(ChartTooltip);
const signals = addSignals(defaultSignals, { ...defaultAreaProps, children: [tooltip] });
expect(signals).toHaveLength(5);
expect(signals).toHaveLength(defaultSignals.length + 1);
expect(signals[0]).toHaveProperty('name', HIGHLIGHTED_ITEM);
expect(signals[1]).toHaveProperty('name', HIGHLIGHTED_SERIES);
expect(signals[1].on).toHaveLength(2);
expect(signals[2]).toHaveProperty('name', SELECTED_ITEM);
expect(signals[3]).toHaveProperty('name', SELECTED_SERIES);
expect(signals[4]).toHaveProperty('name', 'area0_controlledHoveredId');
expect(signals[2]).toHaveProperty('name', HIGHLIGHTED_SERIES);
expect(signals[2].on).toHaveLength(2);
expect(signals[3]).toHaveProperty('name', SELECTED_ITEM);
expect(signals[4]).toHaveProperty('name', SELECTED_SERIES);
expect(signals[5]).toHaveProperty('name', 'area0_controlledHoveredId');
});

test('should exclude data with key from update if tooltip has excludeDataKey', () => {
const tooltip = createElement(ChartTooltip, { excludeDataKeys: ['excludeFromTooltip'] });
const signals = addSignals(defaultSignals, { ...defaultAreaProps, children: [tooltip] });
expect(signals).toHaveLength(5);
expect(signals[1]).toHaveProperty('name', HIGHLIGHTED_SERIES);
expect(signals[1].on?.[0]).toHaveProperty('events', '@area0:mouseover');
expect(signals[1].on?.[0]).toHaveProperty('update', '(datum.excludeFromTooltip) ? null : datum.rscSeriesId');
expect(signals).toHaveLength(defaultSignals.length + 1);
expect(signals[2]).toHaveProperty('name', HIGHLIGHTED_SERIES);
expect(signals[2].on?.[0]).toHaveProperty('events', '@area0:mouseover');
expect(signals[2].on?.[0]).toHaveProperty(
'update',
'(datum.excludeFromTooltip) ? null : datum.rscSeriesId'
);
});
});

Expand Down
1 change: 1 addition & 0 deletions src/specBuilder/area/areaSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const addArea = produce<Spec, [AreaProps & { colorScheme?: ColorScheme; i
colorScheme,
dimension,
index,
markType: 'area',
metric,
name: toCamelCase(name || `area${index}`),
scaleType,
Expand Down
7 changes: 5 additions & 2 deletions src/specBuilder/bar/barSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ describe('barSpecBuilder', () => {
describe('addSignals()', () => {
test('should add padding signal', () => {
const signals = addSignals(defaultSignals, defaultBarProps);
expect(signals).toHaveLength(5);
expect(signals).toHaveLength(defaultSignals.length + 1);
expect(signals.at(-1)).toHaveProperty('name', 'paddingInner');
});
test('should add hover events if tooltip is present', () => {
Expand All @@ -242,7 +242,10 @@ describe('barSpecBuilder', () => {
expect(signals[0].on?.[0]).toHaveProperty('events', '@bar0:mouseover');
});
test('should exclude data with key from update if tooltip has excludeDataKey', () => {
const signals = addSignals(defaultSignals, { ...defaultBarProps, children: [createElement(ChartTooltip, { excludeDataKeys: ['excludeFromTooltip'] })] });
const signals = addSignals(defaultSignals, {
...defaultBarProps,
children: [createElement(ChartTooltip, { excludeDataKeys: ['excludeFromTooltip'] })],
});
expect(signals[0]).toHaveProperty('on');
expect(signals[0].on).toHaveLength(2);
expect(signals[0].on?.[0]).toHaveProperty('events', '@bar0:mouseover');
Expand Down
1 change: 1 addition & 0 deletions src/specBuilder/bar/barSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const addBar = produce<Spec, [BarProps & { colorScheme?: ColorScheme; ind
index,
lineType,
lineWidth,
markType: 'bar',
metric,
name: toCamelCase(name || `bar${index}`),
opacity,
Expand Down
5 changes: 3 additions & 2 deletions src/specBuilder/bar/barTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,20 @@ import { NumericValueRef, ProductionRule, RectEncodeEntry } from 'vega';
export const defaultBarProps: BarSpecProps = {
children: [],
color: DEFAULT_COLOR,
colorScheme: DEFAULT_COLOR_SCHEME,
dimension: DEFAULT_CATEGORICAL_DIMENSION,
index: 0,
lineType: { value: 'solid' },
lineWidth: 0,
markType: 'bar',
metric: DEFAULT_METRIC,
name: 'bar0',
orientation: 'vertical',
opacity: { value: 1 },
paddingRatio: PADDING_RATIO,
colorScheme: DEFAULT_COLOR_SCHEME,
trellisOrientation: 'horizontal',
trellisPadding: TRELLIS_PADDING,
type: 'stacked',
orientation: 'vertical',
};

export const defaultBarPropsWithSecondayColor: BarSpecProps = {
Expand Down
2 changes: 2 additions & 0 deletions src/specBuilder/chartSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DEFAULT_COLOR_SCHEME,
DEFAULT_LINE_TYPES,
FILTERED_TABLE,
HIGHLIGHTED_GROUP,
HIGHLIGHTED_ITEM,
HIGHLIGHTED_SERIES,
LINEAR_COLOR_SCALE,
Expand Down Expand Up @@ -224,6 +225,7 @@ export const getDefaultSignals = (
getGenericSignal('opacities', getTwoDimensionalOpacities(opacities)),
getGenericSignal('hiddenSeries', hiddenSeries ?? []),
getGenericSignal(HIGHLIGHTED_ITEM),
getGenericSignal(HIGHLIGHTED_GROUP),
getGenericSignal(HIGHLIGHTED_SERIES, highlightedSeries),
getGenericSignal(SELECTED_ITEM),
getGenericSignal(SELECTED_SERIES),
Expand Down
145 changes: 145 additions & 0 deletions src/specBuilder/chartTooltip/chartTooltipUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { createElement } from 'react';

import { ChartPopover } from '@components/ChartPopover';
import { ChartTooltip } from '@components/ChartTooltip';
import { HIGHLIGHTED_GROUP } from '@constants';
import { defaultBarProps } from '@specBuilder/bar/barTestUtils';
import { defaultScatterProps } from '@specBuilder/scatter/scatterTestUtils';
import { defaultSignals } from '@specBuilder/specTestUtils';
import { baseData } from '@specBuilder/specUtils';
import { BarSpecProps, ChartTooltipProps } from 'types';
import { Data, Signal } from 'vega';

import {
addTooltipData,
addTooltipMarkOpacityRules,
addTooltipSignals,
applyTooltipPropDefaults,
getTooltips,
isHighlightedByGroup,
} from './chartTooltipUtils';

const getDefautltMarkProps = (tooltipProps: ChartTooltipProps = {}): BarSpecProps => ({
...defaultBarProps,
children: [createElement(ChartTooltip, tooltipProps)],
});

describe('getTooltips()', () => {
test('should get all the tooltips from props', () => {
const markProps = { ...defaultBarProps, children: [createElement(ChartTooltip), createElement(ChartPopover)] };
const tooltips = getTooltips(markProps);
expect(tooltips.length).toBe(1);
});
});

describe('applyTooltipPropDefaults()', () => {
test('should apply all defaults to ChartTooltipProps', () => {
const chartTooltipProps: ChartTooltipProps = {};
const markName = 'bar0';
const tooltipSpecProps = applyTooltipPropDefaults(chartTooltipProps, markName);
expect(tooltipSpecProps).toHaveProperty('highlightBy', 'item');
expect(tooltipSpecProps).toHaveProperty('markName', markName);
});
});

describe('addTooltipData()', () => {
let data: Data[];
beforeEach(() => {
data = JSON.parse(JSON.stringify(baseData));
});
test('if highlightBy is `item` or undefined, no data should be added', () => {
const markProps = getDefautltMarkProps();
addTooltipData(data, markProps);
expect(data).toEqual(baseData);
addTooltipData(data, getDefautltMarkProps({ highlightBy: 'item' }));
expect(data).toEqual(baseData);
});
test('should add the group id transform if highlightBy is `dimension`', () => {
const markProps = getDefautltMarkProps({ highlightBy: 'dimension' });
addTooltipData(data, markProps);
expect(data[1].transform?.length).toBe(1);
expect(data[1].transform?.[0]).toHaveProperty('as', 'bar0_groupId');
});
test('should add the group id transform if highlightBy is `series`', () => {
const markProps = getDefautltMarkProps({ highlightBy: 'series' });
addTooltipData(data, markProps);
expect(data[1].transform?.length).toBe(1);
expect(data[1].transform?.[0]).toHaveProperty('as', 'bar0_groupId');
});
test('should add the group id transform if highlightBy is a key array', () => {
const markProps = getDefautltMarkProps({ highlightBy: ['operatingSystem'] });
addTooltipData(data, markProps);
expect(data[1].transform?.length).toBe(1);
expect(data[1].transform?.[0]).toHaveProperty('as', 'bar0_groupId');
});
});

describe('isHighlightedByGroup()', () => {
test('should return true if highlightBy is `dimension` or `series`', () => {
expect(isHighlightedByGroup(getDefautltMarkProps({ highlightBy: 'dimension' }))).toBe(true);
expect(isHighlightedByGroup(getDefautltMarkProps({ highlightBy: 'series' }))).toBe(true);
});
test('should return true if highlightBy is an array', () => {
expect(isHighlightedByGroup(getDefautltMarkProps({ highlightBy: ['operatingSystem'] }))).toBe(true);
});
test('should return false if highlightBy is `item` or undefined', () => {
expect(isHighlightedByGroup(getDefautltMarkProps({ highlightBy: 'item' }))).toBe(false);
expect(isHighlightedByGroup(getDefautltMarkProps())).toBe(false);
});
});

describe('addTooltipSignals()', () => {
let signals: Signal[] = [];
let highlightedGroupSignal: Signal;
beforeEach(() => {
signals = JSON.parse(JSON.stringify(defaultSignals));
highlightedGroupSignal = signals.find((signal) => signal.name === HIGHLIGHTED_GROUP) as Signal;
});

test('if mark is not highlighted by group id, should not add any signals', () => {
addTooltipSignals(signals, getDefautltMarkProps());
expect(highlightedGroupSignal).not.toHaveProperty('on');
addTooltipSignals(signals, getDefautltMarkProps({ highlightBy: 'item' }));
expect(highlightedGroupSignal).not.toHaveProperty('on');
});

test('should add on events if highlightBy is `series`', () => {
addTooltipSignals(signals, getDefautltMarkProps({ highlightBy: 'series' }));
expect(highlightedGroupSignal).toHaveProperty('on');
expect(highlightedGroupSignal.on).toHaveLength(2);
});

test('should add on events if highlightBy is `dimension`', () => {
addTooltipSignals(signals, getDefautltMarkProps({ highlightBy: 'dimension' }));
expect(highlightedGroupSignal).toHaveProperty('on');
expect(highlightedGroupSignal.on).toHaveLength(2);
});

test('should add on events if highlightBy is a key array', () => {
addTooltipSignals(signals, getDefautltMarkProps({ highlightBy: ['operatingSystem'] }));
expect(highlightedGroupSignal).toHaveProperty('on');
expect(highlightedGroupSignal.on).toHaveLength(2);
});

test('should include voronoi in the mark name if the markprops are for scatter or line', () => {
addTooltipSignals(signals, {
...defaultScatterProps,
children: [createElement(ChartTooltip, { highlightBy: 'series' })],
});
expect(highlightedGroupSignal.on?.[0].events.toString().includes('_voronoi')).toBeTruthy();
});
});

describe('addTooltipMarkOpacityRules()', () => {
test('should only add a simple item rule if not highlighted by group', () => {
const opacityRules = [];
addTooltipMarkOpacityRules(opacityRules, getDefautltMarkProps());
expect(opacityRules).toHaveLength(1);
});

test('shold add highlight group rule if highlighted by group', () => {
const opacityRules = [];
addTooltipMarkOpacityRules(opacityRules, getDefautltMarkProps({ highlightBy: 'series' }));
expect(opacityRules).toHaveLength(2);
});
});
Loading
Loading