Skip to content
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
42 changes: 6 additions & 36 deletions src/specBuilder/line/lineMarkUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@
import { SERIES_ID } from '@constants';
import {
getColorProductionRule,
getCursor,
getHighlightOpacityValue,
getLineWidthProductionRule,
getOpacityProductionRule,
getStrokeDashProductionRule,
getTooltip,
getVoronoiPath,
getXProductionRule,
hasPopover,
} from '@specBuilder/marks/markUtils';
import { MarkChildElement, OpacityFacet, ScaleType } from 'types';
import { LineMark, Mark, NumericValueRef, PathMark, ProductionRule, RuleMark, SymbolMark } from 'vega';
import { OpacityFacet, ScaleType } from 'types';
import { LineMark, Mark, NumericValueRef, ProductionRule, RuleMark, SymbolMark } from 'vega';

import {
getHighlightBackgroundPoint,
Expand Down Expand Up @@ -121,7 +120,7 @@ const getDisplayOnHoverRules = (name: string, opacity: OpacityFacet) => {
export const getLineHoverMarks = (
lineProps: LineMarkProps,
dataSource: string,
secondaryHighlightedMetric?: string
secondaryHighlightedMetric?: string,
): Mark[] => {
const { children, dimension, metric, name, scaleType } = lineProps;
return [
Expand All @@ -138,7 +137,7 @@ export const getLineHoverMarks = (
// points used for the voronoi transform
getPointsForVoronoi(dataSource, dimension, metric, name, scaleType),
// voronoi transform used to get nearest point paths
getVoronoiPath(children, name),
getVoronoiPath(children, `${name}_pointsForVoronoi`, name),
];
};

Expand Down Expand Up @@ -166,7 +165,7 @@ const getPointsForVoronoi = (
dimension: string,
metric: string,
name: string,
scaleType: ScaleType
scaleType: ScaleType,
): SymbolMark => {
return {
name: `${name}_pointsForVoronoi`,
Expand All @@ -185,32 +184,3 @@ const getPointsForVoronoi = (
},
};
};

const getVoronoiPath = (children: MarkChildElement[], name: string): PathMark => {
return {
name: `${name}_voronoi`,
type: 'path',
from: { data: `${name}_pointsForVoronoi` },
encode: {
enter: {
fill: { value: 'transparent' },
stroke: { value: 'transparent' },
isVoronoi: { value: true },
// Don't add a tooltip if there are no interactive children. We only want the other hover marks for metric ranges.
tooltip: getTooltip(children, name, true),
},
update: {
cursor: getCursor(children),
},
},
transform: [
{
type: 'voronoi',
x: `datum.x`,
y: `datum.y`,
// on initial render, width/height could be 0 which causes problems
size: [{ signal: 'max(width, 1)' }, { signal: 'max(height, 1)' }],
},
],
};
};
36 changes: 35 additions & 1 deletion src/specBuilder/marks/markUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ChartTooltip } from '@components/ChartTooltip';
import { MetricRange } from '@components/MetricRange';
import { Trendline } from '@components/Trendline';
import { BACKGROUND_COLOR, DEFAULT_TRANSFORMED_TIME_DIMENSION, HIGHLIGHT_CONTRAST_RATIO } from '@constants';
import { getScaleName } from '@specBuilder/scale/scaleSpecBuilder';
import {
getColorValue,
getLineWidthPixelsFromLineWidth,
Expand All @@ -39,11 +40,11 @@ import {
ColorValueRef,
Cursor,
NumericValueRef,
PathMark,
ProductionRule,
ScaledValueRef,
SignalRef,
} from 'vega';
import { getScaleName } from '@specBuilder/scale/scaleSpecBuilder';

/**
* If a popover exists on the mark, then set the cursor to a pointer.
Expand Down Expand Up @@ -190,3 +191,36 @@ export const getXProductionRule = (scaleType: ScaleType, dimension: string): Pro
}
return { scale, field: dimension };
};

/**
* Gets the voronoi path used for tooltips and popovers
* @param children
* @param dataSource name of the point data source the voronoi is based on
* @param markName
* @returns PathMark
*/
export const getVoronoiPath = (children: MarkChildElement[], dataSource: string, markName: string): PathMark => ({
name: `${markName}_voronoi`,
type: 'path',
from: { data: dataSource },
encode: {
enter: {
fill: { value: 'transparent' },
stroke: { value: 'transparent' },
isVoronoi: { value: true },
tooltip: getTooltip(children, markName, true),
},
update: {
cursor: getCursor(children),
},
},
transform: [
{
type: 'voronoi',
x: `datum.x`,
y: `datum.y`,
// on initial render, width/height could be 0 which causes problems
size: [{ signal: 'max(width, 1)' }, { signal: 'max(height, 1)' }],
},
],
});
53 changes: 53 additions & 0 deletions src/specBuilder/scatter/scatterMarkUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { createElement } from 'react';

import { Trendline } from '@components/Trendline';
import { GroupMark } from 'vega';
import { defaultScatterProps } from './scatterTestUtils';
import { addScatterMarks, getScatterHoverMarks } from './scatterMarkUtils';
import { ChartTooltip } from '@components/ChartTooltip';

describe('addScatterMarks()', () => {
test('should add the scatter group with the symbol marks', () => {
const marks = addScatterMarks([], defaultScatterProps);
expect(marks).toHaveLength(1);
expect(marks[0].name).toBe('scatter0_group');
expect(marks[0].type).toBe('group');
expect((marks[0] as GroupMark).marks).toHaveLength(1);
});

test('should use "multiply" blend mode in light mode', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, colorScheme: 'light' });
expect((marks[0] as GroupMark).marks?.[0].encode?.enter?.blend).toEqual({ value: 'multiply' });
});
test('should "screen" blend mode in dark mode', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, colorScheme: 'dark' });
expect((marks[0] as GroupMark).marks?.[0].encode?.enter?.blend).toEqual({ value: 'screen' });
});
test('should add trendline marks if trendline exists as a child', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, children: [createElement(Trendline)] });
expect(marks).toHaveLength(2);
expect(marks[1].name).toBe('scatter0Trendline0_group');
});
});

describe('getScatterHoverMarks()', () => {
test('should retrurn the voronoi mark if there is a tooltip', () => {
expect(getScatterHoverMarks(defaultScatterProps)).toHaveLength(0);

const marks = getScatterHoverMarks({ ...defaultScatterProps, children: [createElement(ChartTooltip)] });
expect(marks).toHaveLength(1);
expect(marks[0].name).toBe('scatter0_voronoi');
});
});
91 changes: 91 additions & 0 deletions src/specBuilder/scatter/scatterMarkUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 { FILTERED_TABLE } from '@constants';
import {
getColorProductionRule,
getLineWidthProductionRule,
getOpacityProductionRule,
getStrokeDashProductionRule,
getSymbolSizeProductionRule,
getVoronoiPath,
getXProductionRule,
hasInteractiveChildren,
} from '@specBuilder/marks/markUtils';
import { getTrendlineMarks } from '@specBuilder/trendline';
import { produce } from 'immer';
import { ScatterSpecProps } from 'types';
import { GroupMark, Mark, PathMark, SymbolMark } from 'vega';

export const addScatterMarks = produce<Mark[], [ScatterSpecProps]>((marks, props) => {
const { name } = props;

const scatterGroup: GroupMark = {
name: `${name}_group`,
type: 'group',
marks: [getScatterMark(props), ...getScatterHoverMarks(props)],
};

marks.push(scatterGroup);
marks.push(...getTrendlineMarks(props));
});

export const getScatterMark = ({
color,
colorScheme,
dimension,
dimensionScaleType,
lineType,
lineWidth,
metric,
name,
opacity,
size,
}: ScatterSpecProps): SymbolMark => ({
name,
type: 'symbol',
from: {
data: FILTERED_TABLE,
},
encode: {
enter: {
/**
* the blend mode makes it possible to tell when there are overlapping points
* in light mode, the points are darker when they overlap (multiply)
* in dark mode, the points are lighter when they overlap (screen)
*/
blend: { value: colorScheme === 'light' ? 'multiply' : 'screen' },
fill: getColorProductionRule(color, colorScheme),
shape: { value: 'circle' },
strokeDash: getStrokeDashProductionRule(lineType),
strokeWidth: getLineWidthProductionRule(lineWidth),
stroke: getColorProductionRule(color, colorScheme),
size: getSymbolSizeProductionRule(size),
},
update: {
fillOpacity: [getOpacityProductionRule(opacity)],
x: getXProductionRule(dimensionScaleType, dimension),
y: { scale: 'yLinear', field: metric },
},
},
});

/**
* Gets the vornoi path mark if there are any interactive children
* @param scatterProps ScatterSpecProps
* @returns PathMark[]
*/
export const getScatterHoverMarks = ({ children, name }: ScatterSpecProps): PathMark[] => {
if (!hasInteractiveChildren(children)) {
return [];
}
return [getVoronoiPath(children, name, name)];
};
54 changes: 3 additions & 51 deletions src/specBuilder/scatter/scatterSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,11 @@
import { createElement } from 'react';

import { Trendline } from '@components/Trendline';
import {
DEFAULT_COLOR,
DEFAULT_COLOR_SCHEME,
DEFAULT_DIMENSION_SCALE_TYPE,
DEFAULT_LINEAR_DIMENSION,
DEFAULT_METRIC,
} from '@constants';
import { DEFAULT_COLOR } from '@constants';
import { initializeSpec } from '@specBuilder/specUtils';
import { ScatterSpecProps } from 'types';
import { GroupMark } from 'vega';

import { addData, addScatterMarks, addSignals, setScales } from './scatterSpecBuilder';

const defaultScatterProps: ScatterSpecProps = {
children: [],
color: { value: 'categorical-100' },
colorScaleType: 'ordinal',
colorScheme: DEFAULT_COLOR_SCHEME,
dimension: DEFAULT_LINEAR_DIMENSION,
dimensionScaleType: DEFAULT_DIMENSION_SCALE_TYPE,
index: 0,
interactiveMarkName: 'scatter0',
lineType: { value: 'solid' },
lineWidth: { value: 0 },
metric: DEFAULT_METRIC,
name: 'scatter0',
opacity: { value: 1 },
size: { value: 'M' },
};
import { addData, addSignals, setScales } from './scatterSpecBuilder';
import { defaultScatterProps } from './scatterTestUtils';

describe('addData()', () => {
test('should add time transform is dimensionScaleType === "time"', () => {
Expand Down Expand Up @@ -104,27 +80,3 @@ describe('setScales()', () => {
expect(scales[2].domain).toEqual({ data: 'table', fields: ['weight'] });
});
});

describe('addScatterMarks()', () => {
test('should add the scatter group with the symbol marks', () => {
const marks = addScatterMarks([], defaultScatterProps);
expect(marks).toHaveLength(1);
expect(marks[0].name).toBe('scatter0_group');
expect(marks[0].type).toBe('group');
expect((marks[0] as GroupMark).marks).toHaveLength(1);
});

test('should use "multiply" blend mode in light mode', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, colorScheme: 'light' });
expect((marks[0] as GroupMark).marks?.[0].encode?.enter?.blend).toEqual({ value: 'multiply' });
});
test('should "screen" blend mode in dark mode', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, colorScheme: 'dark' });
expect((marks[0] as GroupMark).marks?.[0].encode?.enter?.blend).toEqual({ value: 'screen' });
});
test('should add trendline marks if trendline exists as a child', () => {
const marks = addScatterMarks([], { ...defaultScatterProps, children: [createElement(Trendline)] });
expect(marks).toHaveLength(2);
expect(marks[1].name).toBe('scatter0Trendline0_group');
});
});
Loading