Skip to content

Commit

Permalink
Merge pull request adobe#57 from adobe/normailizedTrendlineDimension
Browse files Browse the repository at this point in the history
Normailized trendline dimension
  • Loading branch information
marshallpete authored Jan 2, 2024
2 parents f1a74a7 + 1724b71 commit 0ec51de
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 116 deletions.
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ export const LEGEND_TOOLTIP_DELAY = 350;
// signal names
// 'backgroundColor' is an undocumented protected signal name used by vega
export const BACKGROUND_COLOR = 'chartBackgroundColor';

// time constants
export const MS_PER_DAY = 86400000;
40 changes: 23 additions & 17 deletions src/specBuilder/line/lineSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import {
getMetricRanges,
} from '@specBuilder/metricRange/metricRangeUtils';
import { getFacetsFromProps } from '@specBuilder/specUtils';
import { getTrendlineData, getTrendlineMarks, getTrendlineSignals } from '@specBuilder/trendline/trendlineUtils';
import {
addTrendlineData,
getTrendlineMarks,
getTrendlineScales,
getTrendlineSignals,
} from '@specBuilder/trendline/trendlineUtils';
import { sanitizeMarkChildren, toCamelCase } from '@utils';
import { produce } from 'immer';
import { ColorScheme, LineProps, LineSpecProps, MarkChildElement } from 'types';
Expand Down Expand Up @@ -91,7 +96,7 @@ export const addData = produce<Data[], [LineSpecProps]>((data, props) => {
data.push(getLineHighlightedData(name, FILTERED_TABLE, hasPopover(children)));
}
if (staticPoint) data.push(getLineStaticPointData(name, staticPoint, FILTERED_TABLE));
data.push(...getTrendlineData(props));
addTrendlineData(data, props);
});

export const addSignals = produce<Signal[], [LineSpecProps]>((signals, props) => {
Expand All @@ -114,21 +119,22 @@ export const addSignals = produce<Signal[], [LineSpecProps]>((signals, props) =>
}
});

export const setScales = produce<Scale[], [LineSpecProps]>(
(scales, { metric, dimension, color, lineType, opacity, padding, scaleType, children, name }) => {
// add dimension scale
addContinuousDimensionScale(scales, { scaleType, dimension, padding });
// add color to the color domain
addFieldToFacetScaleDomain(scales, 'color', color);
// add lineType to the lineType domain
addFieldToFacetScaleDomain(scales, 'lineType', lineType);
// add opacity to the opacity domain
addFieldToFacetScaleDomain(scales, 'opacity', opacity);
// find the linear scale and add our fields to it
addMetricScale(scales, getMetricKeys(metric, children, name));
return scales;
}
);
export const setScales = produce<Scale[], [LineSpecProps]>((scales, props) => {
const { metric, dimension, color, lineType, opacity, padding, scaleType, children, name } = props;
// add dimension scale
addContinuousDimensionScale(scales, { scaleType, dimension, padding });
// add color to the color domain
addFieldToFacetScaleDomain(scales, 'color', color);
// add lineType to the lineType domain
addFieldToFacetScaleDomain(scales, 'lineType', lineType);
// add opacity to the opacity domain
addFieldToFacetScaleDomain(scales, 'opacity', opacity);
// find the linear scale and add our fields to it
addMetricScale(scales, getMetricKeys(metric, children, name));
// add trendline scales
scales.push(...getTrendlineScales(props));
return scales;
});

// The order that marks are added is important since it determines the draw order.
export const addLineMarks = produce<Mark[], [LineSpecProps]>((marks, props) => {
Expand Down
138 changes: 90 additions & 48 deletions src/specBuilder/trendline/trendlineUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ import {
DEFAULT_CONTINUOUS_DIMENSION,
DEFAULT_METRIC,
FILTERED_TABLE,
MS_PER_DAY,
TRENDLINE_VALUE,
} from '@constants';
import { baseData } from '@specBuilder/specUtils';
import { LineSpecProps } from 'types';
import { Facet, From } from 'vega';
import { Data, Facet, From } from 'vega';

import {
addTrendlineData,
applyTrendlinePropDefaults,
getMovingAverageTransform,
getPolynomialOrder,
getRegressionTransform,
getTrendlineData,
getTrendlineDimensionRangeTransforms,
getTrendlineMarks,
getTrendlineParamFormulaTransforms,
Expand All @@ -54,6 +56,8 @@ const defaultLineProps: LineSpecProps = {
popoverMarkName: undefined,
};

const getDefaultData = (): Data[] => JSON.parse(JSON.stringify(baseData));

describe('getTrendlines()', () => {
test('should return an array of trendline props', () => {
const children = [
Expand Down Expand Up @@ -146,65 +150,98 @@ describe('getTrendlineMarks()', () => {
});
});

describe('getTrendlineData()', () => {
test('should return data source for trendline', () => {
const trendlineData = getTrendlineData(defaultLineProps);
expect(trendlineData).toStrictEqual([
{
name: 'line0Trendline0_data',
source: FILTERED_TABLE,
transform: [
{
as: [TRENDLINE_VALUE],
fields: [DEFAULT_METRIC],
groupby: [DEFAULT_COLOR],
ops: ['mean'],
type: 'joinaggregate',
},
],
},
]);
describe('addTrendlineData()', () => {
test('should add normalized dimension for regression trendline', () => {
const trendlineData = getDefaultData();
addTrendlineData(trendlineData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'linear' })],
});
expect(trendlineData[0].transform).toHaveLength(3);
expect(trendlineData[0].transform?.[1]).toStrictEqual({
as: ['datetimeMin'],
fields: ['datetime'],
ops: ['min'],
type: 'joinaggregate',
});
expect(trendlineData[0].transform?.[2]).toStrictEqual({
as: 'datetimeNormalized',
expr: `(datum.datetime - datum.datetimeMin + ${MS_PER_DAY}) / ${MS_PER_DAY}`,
type: 'formula',
});
});

test('should not add normalized dimension in not regression trendline', () => {
const trendlineData = getDefaultData();
addTrendlineData(trendlineData, defaultLineProps);
expect(trendlineData[0].transform).toHaveLength(1);
});

test('should add datasource for trendline', () => {
const trendlineData = getDefaultData();
expect(trendlineData).toHaveLength(2);
addTrendlineData(trendlineData, defaultLineProps);
expect(trendlineData).toHaveLength(3);
expect(trendlineData[2]).toStrictEqual({
name: 'line0Trendline0_data',
source: FILTERED_TABLE,
transform: [
{
as: [TRENDLINE_VALUE],
fields: ['value'],
groupby: ['series'],
ops: ['mean'],
type: 'joinaggregate',
},
],
});
});

test('should add data sources for hover interactiontions if ChartTooltip exists', () => {
const trendlineData = getTrendlineData({
const trendlineData = getDefaultData();
addTrendlineData(trendlineData, {
...defaultLineProps,
children: [createElement(Trendline, {}, createElement(ChartTooltip))],
});
expect(trendlineData).toHaveLength(5);
expect(trendlineData[3]).toHaveProperty('name', 'line0_allTrendlineData');
expect(trendlineData[4]).toHaveProperty('name', 'line0Trendline_highlightedData');
expect(trendlineData).toHaveLength(7);
expect(trendlineData[5]).toHaveProperty('name', 'line0_allTrendlineData');
expect(trendlineData[6]).toHaveProperty('name', 'line0Trendline_highlightedData');
});

test('should add _highResolutionData if doing a regression method', () => {
const trendlineData = getTrendlineData({
const trendlineData = getDefaultData();

addTrendlineData(trendlineData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'linear' })],
});
expect(trendlineData).toHaveLength(1);
expect(trendlineData[0]).toHaveProperty('name', 'line0Trendline0_highResolutionData');
expect(trendlineData).toHaveLength(3);
expect(trendlineData[2]).toHaveProperty('name', 'line0Trendline0_highResolutionData');
});

test('should add _params and _data if doing a regression method and there is a tooltip on the trendline', () => {
const trendlineData = getTrendlineData({
const trendlineData = getDefaultData();

addTrendlineData(trendlineData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'linear' }, createElement(ChartTooltip))],
});
expect(trendlineData).toHaveLength(5);
expect(trendlineData[1]).toHaveProperty('name', 'line0Trendline0_params');
expect(trendlineData[2]).toHaveProperty('name', 'line0Trendline0_data');
expect(trendlineData).toHaveLength(7);
expect(trendlineData[3]).toHaveProperty('name', 'line0Trendline0_params');
expect(trendlineData[4]).toHaveProperty('name', 'line0Trendline0_data');
});

test('should add window trandform and then dimension range filter transform for movingAverage', () => {
const trendlineData = getTrendlineData({
const trendlineData = getDefaultData();
addTrendlineData(trendlineData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'movingAverage-3', dimensionRange: [1, 2] })],
});
expect(trendlineData).toHaveLength(1);
expect(trendlineData[0]).toHaveProperty('name', 'line0Trendline0_data');
expect(trendlineData[0].transform).toHaveLength(2);
expect(trendlineData[0].transform?.[0]).toHaveProperty('type', 'window');
expect(trendlineData[0].transform?.[1]).toHaveProperty('type', 'filter');
expect(trendlineData).toHaveLength(3);
expect(trendlineData[2]).toHaveProperty('name', 'line0Trendline0_data');
expect(trendlineData[2].transform).toHaveLength(2);
expect(trendlineData[2].transform?.[0]).toHaveProperty('type', 'window');
expect(trendlineData[2].transform?.[1]).toHaveProperty('type', 'filter');
});
});

Expand All @@ -231,36 +268,41 @@ describe('getTrendlineDimensionRangeTransforms()', () => {

describe('getTrendlineParamFormulaTransforms()', () => {
test('should return the correct formula for each polynomial method', () => {
expect(getTrendlineParamFormulaTransforms('x', 'linear')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'linear', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1)'
);
expect(getTrendlineParamFormulaTransforms('x', 'quadratic')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'quadratic', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1) + datum.coef[2] * pow(datum.x, 2)'
);
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-1')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-1', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1)'
);
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-2')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-2', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1) + datum.coef[2] * pow(datum.x, 2)'
);
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-3')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-3', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1) + datum.coef[2] * pow(datum.x, 2) + datum.coef[3] * pow(datum.x, 3)'
);
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-8')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'polynomial-8', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * pow(datum.x, 1) + datum.coef[2] * pow(datum.x, 2) + datum.coef[3] * pow(datum.x, 3) + datum.coef[4] * pow(datum.x, 4) + datum.coef[5] * pow(datum.x, 5) + datum.coef[6] * pow(datum.x, 6) + datum.coef[7] * pow(datum.x, 7) + datum.coef[8] * pow(datum.x, 8)'
);
});
test('should return the correct formula for other non-polynomial regression methods', () => {
expect(getTrendlineParamFormulaTransforms('x', 'exponential')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'exponential', 'linear')[0].expr).toEqual(
'datum.coef[0] + exp(datum.coef[1] * datum.x)'
);
expect(getTrendlineParamFormulaTransforms('x', 'logarithmic')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'logarithmic', 'linear')[0].expr).toEqual(
'datum.coef[0] + datum.coef[1] * log(datum.x)'
);
expect(getTrendlineParamFormulaTransforms('x', 'power')[0].expr).toEqual(
expect(getTrendlineParamFormulaTransforms('x', 'power', 'linear')[0].expr).toEqual(
'datum.coef[0] * pow(datum.x, datum.coef[1])'
);
});
test('should use normalized dimension for time scaleType', () => {
expect(getTrendlineParamFormulaTransforms('x', 'exponential', 'time')[0].expr).toEqual(
'datum.coef[0] + exp(datum.coef[1] * datum.xNormalized)'
);
});
});

describe('getRegressionTransform()', () => {
Expand All @@ -280,14 +322,14 @@ describe('getRegressionTransform()', () => {
expect(getRegressionTransform(defaultLineProps, 'polynomial-25', false)).toHaveProperty('order', 25);
expect(getRegressionTransform(defaultLineProps, 'power', false).order).toBeUndefined();
});
test('should use datetime0 as ouput dimension if scaleType is time', () => {
test('should use ${dimension}Normalized as ouput dimension if scaleType is time', () => {
const transform = getRegressionTransform(
{ ...defaultLineProps, dimension: 'x', scaleType: 'time' },
'linear',
false
);
expect(transform.as).toHaveLength(2);
expect(transform.as).toEqual(['datetime0', TRENDLINE_VALUE]);
expect(transform.as).toEqual(['xNormalized', TRENDLINE_VALUE]);
});
test('should have params on transform and no `as` property when params is true', () => {
const transform = getRegressionTransform(defaultLineProps, 'linear', true);
Expand Down
Loading

0 comments on commit 0ec51de

Please sign in to comment.