Skip to content

Commit

Permalink
Merge pull request #29 from adobe/an-333938/seriesLineHighlight
Browse files Browse the repository at this point in the history
feat: lines get highlighted with hover and select interactions
  • Loading branch information
marshallpete authored Nov 14, 2023
2 parents 79f97dc + eb9f8d2 commit 3dac343
Show file tree
Hide file tree
Showing 24 changed files with 519 additions and 155 deletions.
3 changes: 2 additions & 1 deletion src/specBuilder/area/areaSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DEFAULT_METRIC,
FILTERED_TABLE,
MARK_ID,
SERIES_ID,
TABLE,
} from '@constants';
import { AreaSpecProps } from 'types';
Expand Down Expand Up @@ -151,7 +152,7 @@ const defaultSignals = [
{
name: 'area0_hoveredSeries',
on: [
{ events: '@area0:mouseover', update: `datum.${DEFAULT_COLOR}` },
{ events: '@area0:mouseover', update: `datum.${SERIES_ID}` },
{ events: '@area0:mouseout', update: 'null' },
],
value: null,
Expand Down
4 changes: 2 additions & 2 deletions src/specBuilder/area/areaSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ export const addData = produce<Data[], [AreaSpecProps]>(
}
);

export const addSignals = produce<Signal[], [AreaSpecProps]>((signals, { children, name, color }) => {
export const addSignals = produce<Signal[], [AreaSpecProps]>((signals, { children, name }) => {
if (!children.length) return;
if (!hasSignalByName(signals, `${name}_controlledHoveredId`)) {
signals.push(getControlledHoverSignal(name));
}
if (!hasSignalByName(signals, `${name}_hoveredSeries`)) {
signals.push(getSeriesHoveredSignal(name, color));
signals.push(getSeriesHoveredSignal(name));
}
if (!hasSignalByName(signals, `${name}_selectedId`)) {
signals.push(getGenericSignal(`${name}_selectedId`));
Expand Down
15 changes: 0 additions & 15 deletions src/specBuilder/bar/barUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
PADDING_RATIO,
STACK_ID,
} from '@constants';
import { getOpacityProductionRule } from '@specBuilder/marks/markUtils';
import { BarSpecProps } from 'types';
import { RectEncodeEntry } from 'vega';

Expand Down Expand Up @@ -57,7 +56,6 @@ import {
getDodgedDimensionEncodings,
getDodgedGroupMark,
getFillStrokeOpacity,
getHighlightOpacityValue,
getMetricEncodings,
getOrientationProperties,
getStackedCornerRadiusEncodings,
Expand Down Expand Up @@ -363,19 +361,6 @@ describe('barUtils', () => {
});
});

describe('getHighlightOpacityValue()', () => {
test('should divide a signal ref by the highlight contract ratio', () => {
expect(getHighlightOpacityValue(getOpacityProductionRule(DEFAULT_COLOR))).toStrictEqual({
signal: `scale('opacity', datum.${DEFAULT_COLOR}) / ${HIGHLIGHT_CONTRAST_RATIO}`,
});
});
test('shold divide a value ref by the highlight contrast ratio', () => {
expect(getHighlightOpacityValue(getOpacityProductionRule({ value: 0.5 }))).toStrictEqual({
value: 0.5 / HIGHLIGHT_CONTRAST_RATIO,
});
});
});

describe('getAnnotationPositionOffset()', () => {
test('returns 13.5 for vertical orientation', () => {
expect(getAnnotationPositionOffset(defaultBarProps, { value: 12345 })).toEqual('13.5');
Expand Down
11 changes: 1 addition & 10 deletions src/specBuilder/bar/barUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import {
CORNER_RADIUS,
DISCRETE_PADDING,
FILTERED_TABLE,
HIGHLIGHT_CONTRAST_RATIO,
MARK_ID,
STACK_ID,
} from '@constants';
import {
getColorProductionRule,
getCursor,
getHighlightOpacityValue,
getOpacityProductionRule,
getStrokeDashProductionRule,
getTooltip,
Expand Down Expand Up @@ -444,15 +444,6 @@ export const getStrokeWidth = ({ children, lineWidth, name }: BarSpecProps): Pro
];
};

export const getHighlightOpacityValue = (
opacityValue: { signal: string } | { value: number }
): ProductionRule<NumericValueRef> => {
if ('signal' in opacityValue) {
return { signal: `${opacityValue.signal} / ${HIGHLIGHT_CONTRAST_RATIO}` };
}
return { value: opacityValue.value / HIGHLIGHT_CONTRAST_RATIO };
};

export const getBarPadding = (paddingRatio: number, paddingOuter?: number) => {
const paddingInner = paddingRatio;
return {
Expand Down
5 changes: 4 additions & 1 deletion src/specBuilder/legend/legendHighlightUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ describe('setHoverOpacityForMarks()', () => {
test('fillOpacity encoding already exists, rules should be added in the correct spot', () => {
const marks = JSON.parse(
JSON.stringify([
{ ...defaultMark, encode: { ...defaultMark.encode, update: { fillOpacity: [{ value: 1 }] } } },
{
...defaultMark,
encode: { ...defaultMark.encode, update: { strokeOpacity: [], fillOpacity: [{ value: 1 }] } },
},
])
);
setHoverOpacityForMarks(marks);
Expand Down
41 changes: 22 additions & 19 deletions src/specBuilder/legend/legendHighlightUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,32 @@ export const setHoverOpacityForMarks = (marks: Mark[]) => {
const { update } = mark.encode;
const { fillOpacity, strokeOpacity } = update;

// the production rule that sets the fill opacity for this mark
const fillOpacityRule = getOpacityRule(fillOpacity);
// the new production rule for highlighting
const highlightFillOpacityRule = getHighlightOpacityRule(fillOpacityRule);
if ('fillOpacity' in update) {
// the production rule that sets the fill opacity for this mark
const fillOpacityRule = getOpacityRule(fillOpacity);
// the new production rule for highlighting
const highlightFillOpacityRule = getHighlightOpacityRule(fillOpacityRule);

if (!Array.isArray(update.fillOpacity)) {
update.fillOpacity = [];
if (!Array.isArray(update.fillOpacity)) {
update.fillOpacity = [];
}
// // need to insert the new test in the second to last slot
const fillRuleInsertIndex = Math.max(update.fillOpacity.length - 1, 0);
update.fillOpacity.splice(fillRuleInsertIndex, 0, highlightFillOpacityRule);
}
// // need to insert the new test in the second to last slot
const fillRuleInsertIndex = Math.max(update.fillOpacity.length - 1, 0);
update.fillOpacity.splice(fillRuleInsertIndex, 0, highlightFillOpacityRule);

// the production rule that sets the stroke opacity for this mark
const strokeOpacityRule = getOpacityRule(strokeOpacity);
// the new production rule for highlighting
const highlightStrokeOpacityRule = getHighlightOpacityRule(strokeOpacityRule);

if (!Array.isArray(update.strokeOpacity)) {
update.strokeOpacity = [];
if ('strokeOpacity' in update) {
// the production rule that sets the stroke opacity for this mark
const strokeOpacityRule = getOpacityRule(strokeOpacity);
// the new production rule for highlighting
const highlightStrokeOpacityRule = getHighlightOpacityRule(strokeOpacityRule);
if (!Array.isArray(update.strokeOpacity)) {
update.strokeOpacity = [];
}
// // need to insert the new test in the second to last slot
const strokeRuleInsertIndex = Math.max(update.strokeOpacity.length - 1, 0);
update.strokeOpacity.splice(strokeRuleInsertIndex, 0, highlightStrokeOpacityRule);
}
// // need to insert the new test in the second to last slot
const strokeRuleInsertIndex = Math.max(update.strokeOpacity.length - 1, 0);
update.strokeOpacity.splice(strokeRuleInsertIndex, 0, highlightStrokeOpacityRule);
});
};

Expand Down
5 changes: 4 additions & 1 deletion src/specBuilder/legend/legendTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {
DEFAULT_CATEGORICAL_DIMENSION,
DEFAULT_COLOR,
Expand Down Expand Up @@ -37,6 +36,10 @@ export const defaultMark: LineMark = {
y: { scale: 'y', field: DEFAULT_METRIC },
stroke: { scale: 'color', field: DEFAULT_COLOR },
},
update: {
strokeOpacity: [],
fillOpacity: [],
},
},
};

Expand Down
6 changes: 4 additions & 2 deletions src/specBuilder/line/lineSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const defaultLineProps: LineSpecProps = {
lineType: { value: 'solid' },
opacity: { value: 1 },
colorScheme: DEFAULT_COLOR_SCHEME,
interactiveMarkName: undefined,
popoverMarkName: undefined,
};

const getMetricRangeElement = (props?: Partial<MetricRangeProps>): MetricRangeElement =>
Expand Down Expand Up @@ -566,7 +568,7 @@ describe('lineSpecBuilder', () => {
],
},
{
name: 'line0_voronoiHoveredId',
name: 'line0_hoveredId',
value: null,
on: [
{
Expand Down Expand Up @@ -617,7 +619,7 @@ describe('lineSpecBuilder', () => {
],
},
{
name: 'line0_voronoiHoveredId',
name: 'line0_hoveredId',
value: null,
on: [
{
Expand Down
24 changes: 19 additions & 5 deletions src/specBuilder/line/lineSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ import { Data, Mark, Scale, Signal, Spec } from 'vega';

import { addTimeTransform, getTableData } from '../data/dataUtils';
import { addContinuousDimensionScale, addFieldToFacetScaleDomain, addMetricScale } from '../scale/scaleSpecBuilder';
import { getGenericSignal, getUncontrolledHoverSignal, hasSignalByName } from '../signal/signalSpecBuilder';
import {
getGenericSignal,
getSeriesHoveredSignal,
getUncontrolledHoverSignal,
hasSignalByName,
} from '../signal/signalSpecBuilder';
import {
getInteractiveMarkName,
getLineHighlightedData,
getLineHoverMarks,
getLineMark,
getLinePointMark,
getLinePointsData,
getPopoverMarkName,
} from './lineUtils';

export const addLine = produce<Spec, [LineProps & { colorScheme?: ColorScheme; index?: number }]>(
Expand All @@ -51,17 +58,21 @@ export const addLine = produce<Spec, [LineProps & { colorScheme?: ColorScheme; i
...props
}
) => {
const sanitizedChildren = sanitizeMarkChildren(children);
const lineName = toCamelCase(name || `line${index}`);
// put props back together now that all defaults are set
const lineProps: LineSpecProps = {
children: sanitizeMarkChildren(children),
children: sanitizedChildren,
color,
colorScheme,
dimension,
index,
interactiveMarkName: getInteractiveMarkName(sanitizedChildren, lineName),
lineType,
metric,
name: toCamelCase(name || `line${index}`),
name: lineName,
opacity,
popoverMarkName: getPopoverMarkName(sanitizedChildren, lineName),
scaleType,
...props,
};
Expand Down Expand Up @@ -94,8 +105,11 @@ export const addSignals = produce<Signal[], [LineSpecProps]>((signals, props) =>
signals.push(...getMetricRangeSignals(props));

if (!hasInteractiveChildren(children)) return;
if (!hasSignalByName(signals, `${name}_voronoiHoveredId`)) {
signals.push(getUncontrolledHoverSignal(`${name}_voronoi`, true));
if (!hasSignalByName(signals, `${name}_hoveredId`)) {
signals.push(getUncontrolledHoverSignal(`${name}`, true, `${name}_voronoi`));
}
if (!hasSignalByName(signals, `${name}_hoveredSeries`)) {
signals.push(getSeriesHoveredSignal(`${name}`, true, `${name}_voronoi`));
}
if (!hasSignalByName(signals, `${name}_selectedId`)) {
signals.push(getGenericSignal(`${name}_selectedId`));
Expand Down
Loading

0 comments on commit 3dac343

Please sign in to comment.