Skip to content

Commit

Permalink
Merge pull request #258 from adobe/an-334305/highlightBy
Browse files Browse the repository at this point in the history
An 334305/highlight by to bar
  • Loading branch information
marshallpete committed Apr 22, 2024
2 parents 9881a14 + 2a283bf commit 19b9a23
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 41 deletions.
24 changes: 13 additions & 11 deletions src/specBuilder/bar/barSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
STACK_ID,
TRELLIS_PADDING,
} from '@constants';
import { addTooltipData, addTooltipSignals } from '@specBuilder/chartTooltip/chartTooltipUtils';
import { getTransformSort } from '@specBuilder/data/dataUtils';
import { getTooltipProps } from '@specBuilder/marks/markUtils';
import {
addDomainFields,
addFieldToFacetScaleDomain,
Expand All @@ -42,7 +44,6 @@ import { getBarPadding, getScaleValues, isDodgedAndStacked } from './barUtils';
import { getDodgedMark } from './dodgedBarUtils';
import { getDodgedAndStackedBarMark, getStackedBarMarks } from './stackedBarUtils';
import { addTrellisScale, getTrellisGroupMark, isTrellised } from './trellisedBarUtils';
import { getTooltipProps } from '@specBuilder/marks/markUtils';

export const addBar = produce<Spec, [BarProps & { colorScheme?: ColorScheme; index?: number }]>(
(
Expand Down Expand Up @@ -94,18 +95,18 @@ export const addBar = produce<Spec, [BarProps & { colorScheme?: ColorScheme; ind
}
);

export const addSignals = produce<Signal[], [BarSpecProps]>(
(signals, { children, name, paddingRatio, paddingOuter: barPaddingOuter }) => {
// We use this value to calculate ReferenceLine positions.
const { paddingInner } = getBarPadding(paddingRatio, barPaddingOuter);
signals.push(getGenericSignal('paddingInner', paddingInner));
export const addSignals = produce<Signal[], [BarSpecProps]>((signals, props) => {
const { children, name, paddingRatio, paddingOuter: barPaddingOuter } = props;
// We use this value to calculate ReferenceLine positions.
const { paddingInner } = getBarPadding(paddingRatio, barPaddingOuter);
signals.push(getGenericSignal('paddingInner', paddingInner));

if (!children.length) {
return;
}
addHighlightedItemSignalEvents(signals, name, 1, getTooltipProps(children)?.excludeDataKeys);
if (!children.length) {
return;
}
);
addHighlightedItemSignalEvents(signals, name, 1, getTooltipProps(children)?.excludeDataKeys);
addTooltipSignals(signals, props);
});

export const addData = produce<Data[], [BarSpecProps]>((data, props) => {
const { metric, order, type } = props;
Expand All @@ -126,6 +127,7 @@ export const addData = produce<Data[], [BarSpecProps]>((data, props) => {
if (type === 'dodged' || isDodgedAndStacked(props)) {
data[index].transform?.push(getDodgeGroupTransform(props));
}
addTooltipData(data, props);
});

/**
Expand Down
5 changes: 1 addition & 4 deletions src/specBuilder/bar/barTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,9 @@ export const defaultDodgedCornerRadiusEncodings: RectEncodeEntry = {
export const defaultBarFillOpacity: ProductionRule<NumericValueRef> = [{ value: 1 }];

export const defaultBarPopoverOpacity: ProductionRule<NumericValueRef> = [
{
test: `!${SELECTED_ITEM} && ${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${MARK_ID}`,
value: 1 / HIGHLIGHT_CONTRAST_RATIO,
},
{ test: `${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${MARK_ID}`, value: 1 / HIGHLIGHT_CONTRAST_RATIO },
{ test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}`, value: 1 },
{ test: `${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${MARK_ID}`, value: 1 / HIGHLIGHT_CONTRAST_RATIO },
DEFAULT_OPACITY_RULE,
];

Expand Down
28 changes: 11 additions & 17 deletions src/specBuilder/bar/barUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import {
DEFAULT_OPACITY_RULE,
DISCRETE_PADDING,
FILTERED_TABLE,
HIGHLIGHTED_ITEM,
HIGHLIGHT_CONTRAST_RATIO,
MARK_ID,
SELECTED_ITEM,
STACK_ID,
} from '@constants';
import { addTooltipMarkOpacityRules } from '@specBuilder/chartTooltip/chartTooltipUtils';
import {
getColorProductionRule,
getCursor,
getHighlightOpacityValue,
getOpacityProductionRule,
getStrokeDashProductionRule,
getTooltip,
Expand Down Expand Up @@ -369,34 +369,28 @@ export const getBarUpdateEncodings = (props: BarSpecProps): EncodeEntry => ({
strokeWidth: getStrokeWidth(props),
});

export const getBarOpacity = ({ children }: BarSpecProps): ProductionRule<NumericValueRef> => {
export const getBarOpacity = (props: BarSpecProps): ProductionRule<NumericValueRef> => {
const { children } = props;
const rules: ({ test?: string } & NumericValueRef)[] = [DEFAULT_OPACITY_RULE];
// if there aren't any interactive components, then we don't need to add special opacity rules
if (!hasInteractiveChildren(children)) {
return [DEFAULT_OPACITY_RULE];
return rules;
}

addTooltipMarkOpacityRules(rules, props);

// if a bar is hovered/selected, all other bars should have reduced opacity
if (hasPopover(children)) {
return [
{
test: `!${SELECTED_ITEM} && ${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${MARK_ID}`,
...getHighlightOpacityValue(DEFAULT_OPACITY_RULE),
},
{
test: `${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${MARK_ID}`,
...getHighlightOpacityValue(DEFAULT_OPACITY_RULE),
value: 1 / HIGHLIGHT_CONTRAST_RATIO,
},
{ test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}`, ...DEFAULT_OPACITY_RULE },
DEFAULT_OPACITY_RULE,
...rules,
];
}
return [
{
test: `${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${MARK_ID}`,
...getHighlightOpacityValue(),
},
DEFAULT_OPACITY_RULE,
];
return rules;
};

export const getStroke = ({ children, color, colorScheme }: BarSpecProps): ProductionRule<ColorValueRef> => {
Expand Down
17 changes: 8 additions & 9 deletions src/stories/components/ChartTooltip/ChartTooltip.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import React, { ReactElement } from 'react';

import { ChartTooltip } from '@components/ChartTooltip/ChartTooltip';
import useChartProps from '@hooks/useChartProps';
import { Area, Bar, categorical12, Chart, Datum, Line } from '@rsc';
import { Area, Bar, Chart, Datum, Line, categorical12 } from '@rsc';
import { browserData } from '@stories/data/data';
import { formatTimestamp } from '@stories/storyUtils';
import { StoryFn } from '@storybook/react';
Expand All @@ -32,7 +32,9 @@ export default {
},
};

const barData = browserData.map(datum => datum.category === 'Chrome' ? ({ ...datum, excludeFromTooltip: true }) : datum);
const barData = browserData.map((datum) =>
datum.category === 'Chrome' ? { ...datum, excludeFromTooltip: true } : datum
);

const StackedBarTooltipStory: StoryFn<typeof ChartTooltip> = (args): ReactElement => {
const chartProps = useChartProps({ data: barData, width: 600 });
Expand Down Expand Up @@ -72,7 +74,9 @@ const lineData = [
{ datetime: 1668409200000, point: 25, value: 10932, users: 4913, series: 'Add Freeform table' },
];

const disabledLineData = lineData.map(datum => datum.series === 'Add Fallout' ? { ...datum, excludeFromTooltip: true } : datum);
const disabledLineData = lineData.map((datum) =>
datum.series === 'Add Fallout' ? { ...datum, excludeFromTooltip: true } : datum
);

const LineTooltipStory: StoryFn<typeof ChartTooltip> = (args): ReactElement => {
const chartProps = useChartProps({ data: lineData, width: 600 });
Expand All @@ -85,9 +89,8 @@ const LineTooltipStory: StoryFn<typeof ChartTooltip> = (args): ReactElement => {
);
};


const DisabledSeriesLineTooltipStory: StoryFn<typeof ChartTooltip> = (args): ReactElement => {
const chartProps = useChartProps({ data: disabledLineData, width: 600, colors: ['gray-300', ...categorical12]});
const chartProps = useChartProps({ data: disabledLineData, width: 600, colors: ['gray-300', ...categorical12] });
return (
<Chart {...chartProps}>
<Line color="series">
Expand Down Expand Up @@ -126,7 +129,6 @@ StackedBarChart.args = {
<div>Users: {datum.value}</div>
</div>
),
excludeDataKeys: [],
};

const DodgedBarChart = DodgedBarTooltipStory.bind({});
Expand All @@ -138,7 +140,6 @@ DodgedBarChart.args = {
<div>Users: {datum.value}</div>
</div>
),
excludeDataKeys: [],
};

const LineChart = bindWithProps(LineTooltipStory);
Expand All @@ -151,7 +152,6 @@ LineChart.args = {
<div>Users: {Number(datum.users).toLocaleString()}</div>
</div>
),
excludeDataKeys: [],
};

const AreaChart = bindWithProps(AreaTooltipStory);
Expand All @@ -164,7 +164,6 @@ AreaChart.args = {
<div>Users: {Number(datum.users).toLocaleString()}</div>
</div>
),
excludeDataKeys: [],
};

const DisabledSeriesLineChart = bindWithProps(DisabledSeriesLineTooltipStory);
Expand Down
83 changes: 83 additions & 0 deletions src/stories/components/ChartTooltip/HighlightBy.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { ReactElement } from 'react';

import { ChartTooltip } from '@components/ChartTooltip/ChartTooltip';
import useChartProps from '@hooks/useChartProps';
import { Bar, Chart, Datum } from '@rsc';
import { browserData } from '@stories/data/data';
import { StoryFn } from '@storybook/react';
import { bindWithProps } from '@test-utils';

export default {
title: 'RSC/ChartTooltip/HighlightBy',
component: ChartTooltip,
argTypes: {
children: {
description: '`(datum) => React.ReactElement`',
control: {
type: null,
},
},
},
};

interface LineData extends Datum {
value?: number;
series?: string;
category?: string;
}

const StackedBarTooltipStory: StoryFn<typeof ChartTooltip> = (args): ReactElement => {
const chartProps = useChartProps({ data: browserData, width: 600 });
return (
<Chart {...chartProps}>
<Bar color="series">
<ChartTooltip {...args} />
</Bar>
</Chart>
);
};

const dialogCallback = (datum: LineData) => (
<div className="bar-tooltip">
<div>Operating system: {datum.series}</div>
<div>Browser: {datum.category}</div>
<div>Users: {datum.value}</div>
</div>
);

const Basic = bindWithProps(StackedBarTooltipStory);
Basic.args = {
highlightBy: 'item',
children: dialogCallback,
};

const Dimension = bindWithProps(StackedBarTooltipStory);
Dimension.args = {
highlightBy: 'dimension',
children: dialogCallback,
};

const Series = bindWithProps(StackedBarTooltipStory);
Series.args = {
highlightBy: 'series',
children: dialogCallback,
};

const Keys = bindWithProps(StackedBarTooltipStory);
Keys.args = {
highlightBy: ['series'],
children: dialogCallback,
};

export { Basic, Dimension, Series, Keys };
100 changes: 100 additions & 0 deletions src/stories/components/ChartTooltip/HighlightBy.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { HIGHLIGHT_CONTRAST_RATIO } from '@constants';
import {
allElementsHaveAttributeValue,
findAllMarksByGroupName,
findChart,
hoverNthElement,
render,
} from '@test-utils';

import { Basic, Dimension, Keys, Series } from './HighlightBy.story';

describe('Basic', () => {
test('Only the hovered element should be highlighted', async () => {
render(<Basic {...Basic.args} />);

const chart = await findChart();
expect(chart).toBeInTheDocument();

const bars = await findAllMarksByGroupName(chart, 'bar0');
expect(bars).toHaveLength(9);

await hoverNthElement(bars, 2);
// highlighed bar
expect(bars[2]).toHaveAttribute('opacity', '1');
// all other bars
expect(
allElementsHaveAttributeValue(
[...bars.slice(0, 2), ...bars.slice(3)],
'opacity',
1 / HIGHLIGHT_CONTRAST_RATIO
)
).toBe(true);
});
});

describe('Dimension', () => {
test('All the bars with the same dimension should be highlighted', async () => {
render(<Dimension {...Dimension.args} />);

const chart = await findChart();
expect(chart).toBeInTheDocument();

const bars = await findAllMarksByGroupName(chart, 'bar0');
expect(bars).toHaveLength(9);

await hoverNthElement(bars, 2);
// first three bars (same dimension)
expect(allElementsHaveAttributeValue(bars.slice(0, 2), 'opacity', '1')).toBe(true);
// all other bars
expect(allElementsHaveAttributeValue(bars.slice(3), 'opacity', 1 / HIGHLIGHT_CONTRAST_RATIO)).toBe(true);
});
});

describe('Series', () => {
test('All the bars with the same series should be highlighted', async () => {
render(<Series {...Series.args} />);

const chart = await findChart();
expect(chart).toBeInTheDocument();

const bars = await findAllMarksByGroupName(chart, 'bar0');
expect(bars).toHaveLength(9);

await hoverNthElement(bars, 2);
// bars 2, 5, and 8 (same series)
expect(allElementsHaveAttributeValue([bars[2], bars[5], bars[8]], 'opacity', '1')).toBe(true);
// all other bars
expect(
allElementsHaveAttributeValue(
[...bars.slice(0, 1), ...bars.slice(3, 4), ...bars.slice(6, 7)],
'opacity',
1 / HIGHLIGHT_CONTRAST_RATIO
)
).toBe(true);
});
});

describe('Keys', () => {
test('All the bars with the same keys should be highlighted', async () => {
render(<Keys {...Keys.args} />);

const chart = await findChart();
expect(chart).toBeInTheDocument();

const bars = await findAllMarksByGroupName(chart, 'bar0');
expect(bars).toHaveLength(9);

await hoverNthElement(bars, 2);
// bars 2, 5, and 8 (same series)
expect(allElementsHaveAttributeValue([bars[2], bars[5], bars[8]], 'opacity', '1')).toBe(true);
// all other bars
expect(
allElementsHaveAttributeValue(
[...bars.slice(0, 1), ...bars.slice(3, 4), ...bars.slice(6, 7)],
'opacity',
1 / HIGHLIGHT_CONTRAST_RATIO
)
).toBe(true);
});
});

0 comments on commit 19b9a23

Please sign in to comment.