Skip to content

Commit

Permalink
feat: highlightBy for area
Browse files Browse the repository at this point in the history
  • Loading branch information
marshallpete committed Apr 29, 2024
1 parent cb5f07c commit a2e33fc
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 178 deletions.
12 changes: 9 additions & 3 deletions src/RscChart.tsx
Expand Up @@ -121,7 +121,7 @@ export const RscChart = forwardRef<ChartHandle, RscChartProps>(
UNSAFE_vegaSpec,
});

const { controlledHoverSignal } = useSpecProps(spec);
const { controlledHoveredIdSignal, controlledHoveredGroupSignal } = useSpecProps(spec);
const chartConfig = useMemo(() => getChartConfig(config, colorScheme), [config, colorScheme]);

useEffect(() => {
Expand Down Expand Up @@ -181,8 +181,14 @@ export const RscChart = forwardRef<ChartHandle, RscChartProps>(
// get the correct tooltip to render based on the hovered item
const tooltip = tooltips.find((t) => t.name === value.rscComponentName)?.callback;
if (tooltip && !('index' in value)) {
if (controlledHoverSignal) {
chartView.current?.signal(controlledHoverSignal.name, value?.[MARK_ID] ?? null);
if (controlledHoveredIdSignal) {
chartView.current?.signal(controlledHoveredIdSignal.name, value?.[MARK_ID] ?? null);
}
if (controlledHoveredGroupSignal) {
const key = Object.keys(value).find((k) => k.endsWith('_groupId'));
if (key) {
chartView.current?.signal(controlledHoveredGroupSignal.name, value[key]);
}
}
return renderToStaticMarkup(
<div className="rsc-tooltip" data-testid="rsc-tooltip">
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useSpecProps.tsx
Expand Up @@ -15,7 +15,10 @@ import { Spec } from 'vega';

export default function useSpecProps(spec: Spec) {
return useMemo(() => {
const controlledHoverSignal = spec.signals?.find((signal) => signal.name.includes('controlledHoveredId'));
return { controlledHoverSignal };
const controlledHoveredIdSignal = spec.signals?.find((signal) => signal.name.includes('controlledHoveredId'));
const controlledHoveredGroupSignal = spec.signals?.find((signal) =>
signal.name.includes('controlledHoveredGroup')
);
return { controlledHoveredIdSignal, controlledHoveredGroupSignal };
}, [spec]);
}
4 changes: 3 additions & 1 deletion src/specBuilder/area/areaSpecBuilder.test.ts
Expand Up @@ -18,6 +18,7 @@ import {
DEFAULT_COLOR,
DEFAULT_COLOR_SCHEME,
DEFAULT_METRIC,
DEFAULT_OPACITY_RULE,
DEFAULT_TIME_DIMENSION,
DEFAULT_TRANSFORMED_TIME_DIMENSION,
FILTERED_TABLE,
Expand Down Expand Up @@ -106,7 +107,8 @@ const defaultSpec = initializeSpec({
update: {
x: { field: DEFAULT_TRANSFORMED_TIME_DIMENSION, scale: 'xTime' },
cursor: undefined,
fillOpacity: [{ value: 0.8 }],
fillOpacity: { value: 0.8 },
opacity: [DEFAULT_OPACITY_RULE],
},
},
interactive: false,
Expand Down
159 changes: 92 additions & 67 deletions src/specBuilder/area/areaSpecBuilder.ts
Expand Up @@ -22,21 +22,27 @@ import {
SELECTED_ITEM,
SELECTED_SERIES,
} from '@constants';
import {
addTooltipData,
addTooltipSignals,
isHighlightedByDimension,
isHighlightedByGroup,
} from '@specBuilder/chartTooltip/chartTooltipUtils';
import { getTooltipProps, hasPopover } from '@specBuilder/marks/markUtils';
import {
addHighlightedSeriesSignalEvents,
getControlledHoverSignal,
hasSignalByName,
getControlledHoveredGroupSignal,
getControlledHoveredIdSignal,
} from '@specBuilder/signal/signalSpecBuilder';
import { spectrumColors } from '@themes';
import { sanitizeMarkChildren, toCamelCase } from '@utils';
import { produce } from 'immer';
import { AreaProps, AreaSpecProps, ColorScheme, MarkChildElement, ScaleType } from 'types';
import { Data, Mark, Scale, Signal, Spec } from 'vega';
import { Data, Mark, Scale, Signal, SourceData, Spec } from 'vega';

import { addTimeTransform, getFilteredTableData, getTableData, getTransformSort } from '../data/dataUtils';
import { addContinuousDimensionScale, addFieldToFacetScaleDomain, addMetricScale } from '../scale/scaleSpecBuilder';
import { getAreaMark, getX } from './areaUtils';
import { getTooltipProps } from '@specBuilder/marks/markUtils';

export const addArea = produce<Spec, [AreaProps & { colorScheme?: ColorScheme; index?: number }]>(
(
Expand Down Expand Up @@ -93,62 +99,76 @@ export const addArea = produce<Spec, [AreaProps & { colorScheme?: ColorScheme; i
}
);

export const addData = produce<Data[], [AreaSpecProps]>(
(data, { name, dimension, scaleType, color, metric, metricEnd, metricStart, order, children }) => {
if (scaleType === 'time') {
const tableData = getTableData(data);
tableData.transform = addTimeTransform(tableData.transform ?? [], dimension);
}
export const addData = produce<Data[], [AreaSpecProps]>((data, props) => {
const { children, color, dimension, metric, metricEnd, metricStart, name, order, scaleType } = props;
if (scaleType === 'time') {
const tableData = getTableData(data);
tableData.transform = addTimeTransform(tableData.transform ?? [], dimension);
}

if (!metricEnd || !metricStart) {
const filteredTableData = getFilteredTableData(data);
// if metricEnd and metricStart don't exist, then we are using metric so we will support stacked
filteredTableData.transform = [
...(filteredTableData.transform ?? []),
{
type: 'stack',
groupby: [dimension],
field: metric,
sort: getTransformSort(order),
as: [`${metric}0`, `${metric}1`],
},
];
}
if (!metricEnd || !metricStart) {
const filteredTableData = getFilteredTableData(data);
// if metricEnd and metricStart don't exist, then we are using metric so we will support stacked
filteredTableData.transform = [
...(filteredTableData.transform ?? []),
{
type: 'stack',
groupby: [dimension],
field: metric,
sort: getTransformSort(order),
as: [`${metric}0`, `${metric}1`],
},
];
}

if (children.length) {
const hoverSignal = `${name}_controlledHoveredId`;
if (children.length) {
const areaHasPopover = hasPopover(children);
data.push(getAreaHighlightedData(name, areaHasPopover, isHighlightedByGroup(props)));
if (areaHasPopover) {
data.push({
name: `${name}_highlightedDataPoint`,
name: `${name}_selectedDataSeries`,
source: FILTERED_TABLE,
transform: [
{
type: 'filter',
expr: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID} || !${SELECTED_ITEM} && ${hoverSignal} && ${hoverSignal} === datum.${MARK_ID}`,
expr: `${SELECTED_SERIES} && ${SELECTED_SERIES} === datum.${color}`,
},
],
});
if (children.some((child) => child.type === ChartPopover)) {
data.push({
name: `${name}_selectedDataSeries`,
source: FILTERED_TABLE,
transform: [
{
type: 'filter',
expr: `${SELECTED_SERIES} && ${SELECTED_SERIES} === datum.${color}`,
},
],
});
}
}
}
);
addTooltipData(data, props, false);
});

export const addSignals = produce<Signal[], [AreaSpecProps]>((signals, { children, name }) => {
export const getAreaHighlightedData = (name: string, hasPopover: boolean, hasGroupId: boolean): SourceData => {
const highlightedExpr = hasGroupId
? `${name}_controlledHoveredGroup === datum.${name}_groupId`
: `${name}_controlledHoveredId === datum.${MARK_ID}`;
const expr = hasPopover
? `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID} || !${SELECTED_ITEM} && ${highlightedExpr}`
: highlightedExpr;
return {
name: `${name}_highlightedData`,
source: FILTERED_TABLE,
transform: [
{
type: 'filter',
expr,
},
],
};
};

export const addSignals = produce<Signal[], [AreaSpecProps]>((signals, props) => {
const { children, name } = props;
if (!children.length) return;
if (!hasSignalByName(signals, `${name}_controlledHoveredId`)) {
signals.push(getControlledHoverSignal(name));
}
addHighlightedSeriesSignalEvents(signals, name, 1, getTooltipProps(children)?.excludeDataKeys);
if (!isHighlightedByGroup(props)) {
signals.push(getControlledHoveredIdSignal(name));
} else {
signals.push(getControlledHoveredGroupSignal(name));
}
addTooltipSignals(signals, props);
});

export const setScales = produce<Scale[], [AreaSpecProps]>(
Expand Down Expand Up @@ -189,16 +209,17 @@ export const addAreaMarks = produce<Mark[], [AreaSpecProps]>((marks, props) => {
},
marks: [
getAreaMark({
name,
color,
colorScheme,
children,
dimension,
isHighlightedByGroup: isHighlightedByGroup(props),
isStacked,
metricStart,
metricEnd,
isStacked,
dimension,
scaleType,
name,
opacity,
scaleType,
}),
...getAnchorPointMark(props),
],
Expand All @@ -218,7 +239,7 @@ const getAnchorPointMark = ({ children, name, dimension, metric, scaleType }: Ar
{
name: `${name}_anchorPoint`,
type: 'symbol',
from: { data: `${name}_highlightedDataPoint` },
from: { data: `${name}_highlightedData` },
interactive: false,
encode: {
enter: {
Expand All @@ -237,42 +258,46 @@ const getAnchorPointMark = ({ children, name, dimension, metric, scaleType }: Ar
/**
* returns a circle symbol and a rule on the hovered/selected point
*/
const getHoverMarks = ({ children, name, dimension, metric, color, scaleType }: AreaSpecProps): Mark[] => {
const getHoverMarks = (props: AreaSpecProps): Mark[] => {
const { children, name, dimension, metric, scaleType, color } = props;
if (!children.length) return [];
return [
const highlightMarks: Mark[] = [
{
name: `${name}_rule`,
type: 'rule',
from: { data: `${name}_highlightedDataPoint` },
name: `${name}_point`,
type: 'symbol',
from: { data: `${name}_highlightedData` },
interactive: false,
encode: {
enter: {
y: { value: 0 },
y2: { signal: 'height' },
strokeWidth: { value: 1 },
y: { scale: 'yLinear', field: `${metric}1` },
stroke: { scale: COLOR_SCALE, field: color },
fill: { signal: BACKGROUND_COLOR },
},
update: {
x: getX(scaleType, dimension),
},
},
},
{
name: `${name}_point`,
type: 'symbol',
from: { data: `${name}_highlightedDataPoint` },
];
if (isHighlightedByDimension(props)) {
highlightMarks.unshift({
name: `${name}_rule`,
type: 'rule',
from: { data: `${name}_highlightedData` },
interactive: false,
encode: {
enter: {
y: { scale: 'yLinear', field: `${metric}1` },
stroke: { scale: COLOR_SCALE, field: color },
fill: { signal: BACKGROUND_COLOR },
y: { value: 0 },
y2: { signal: 'height' },
strokeWidth: { value: 1 },
},
update: {
x: getX(scaleType, dimension),
},
},
},
];
});
}
return highlightMarks;
};

/**
Expand Down
37 changes: 17 additions & 20 deletions src/specBuilder/area/areaUtils.test.ts
Expand Up @@ -14,6 +14,7 @@ import {
COLOR_SCALE,
DEFAULT_COLOR,
DEFAULT_COLOR_SCHEME,
DEFAULT_OPACITY_RULE,
DEFAULT_TRANSFORMED_TIME_DIMENSION,
} from '@constants';

Expand Down Expand Up @@ -64,11 +65,10 @@ describe('getAreaMark', () => {
scale: 'xLinear',
field: 'dimension',
},
fillOpacity: [
{
value: 0.5,
},
],
fillOpacity: {
value: 0.5,
},
opacity: [DEFAULT_OPACITY_RULE],
},
},
});
Expand Down Expand Up @@ -126,11 +126,10 @@ describe('getAreaMark', () => {
scale: 'xLinear',
field: 'dimension',
},
fillOpacity: [
{
value: 0.5,
},
],
fillOpacity: {
value: 0.5,
},
opacity: [DEFAULT_OPACITY_RULE],
},
},
});
Expand Down Expand Up @@ -180,11 +179,10 @@ describe('getAreaMark', () => {
scale: 'xTime',
field: DEFAULT_TRANSFORMED_TIME_DIMENSION,
},
fillOpacity: [
{
value: 0.5,
},
],
fillOpacity: {
value: 0.5,
},
opacity: [DEFAULT_OPACITY_RULE],
},
},
});
Expand Down Expand Up @@ -233,11 +231,10 @@ describe('getAreaMark', () => {
scale: 'xPoint',
field: 'dimension',
},
fillOpacity: [
{
value: 0.5,
},
],
fillOpacity: {
value: 0.5,
},
opacity: [DEFAULT_OPACITY_RULE],
},
},
});
Expand Down

0 comments on commit a2e33fc

Please sign in to comment.