Skip to content

Commit

Permalink
normalize trendline x data
Browse files Browse the repository at this point in the history
  • Loading branch information
marshallpete committed Dec 15, 2023
1 parent f90a794 commit 2d0009c
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 46 deletions.
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
15 changes: 8 additions & 7 deletions src/specBuilder/trendline/trendlineUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ import {
FILTERED_TABLE,
TRENDLINE_VALUE,
} from '@constants';
import { baseData } from '@specBuilder/specUtils';
import { LineSpecProps } from 'types';
import { Facet, From } from 'vega';

import {
addTrendlineData,
applyTrendlinePropDefaults,
getMovingAverageTransform,
getPolynomialOrder,
getRegressionTransform,
getTrendlineData,
getTrendlineDimensionRangeTransforms,
getTrendlineMarks,
getTrendlineParamFormulaTransforms,
Expand Down Expand Up @@ -146,9 +147,9 @@ describe('getTrendlineMarks()', () => {
});
});

describe('getTrendlineData()', () => {
describe('addTrendlineData()', () => {
test('should return data source for trendline', () => {
const trendlineData = getTrendlineData(defaultLineProps);
const trendlineData = addTrendlineData(baseData, defaultLineProps);
expect(trendlineData).toStrictEqual([
{
name: 'line0Trendline0_data',
Expand All @@ -167,7 +168,7 @@ describe('getTrendlineData()', () => {
});

test('should add data sources for hover interactiontions if ChartTooltip exists', () => {
const trendlineData = getTrendlineData({
const trendlineData = addTrendlineData(baseData, {
...defaultLineProps,
children: [createElement(Trendline, {}, createElement(ChartTooltip))],
});
Expand All @@ -177,7 +178,7 @@ describe('getTrendlineData()', () => {
});

test('should add _highResolutionData if doing a regression method', () => {
const trendlineData = getTrendlineData({
const trendlineData = addTrendlineData(baseData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'linear' })],
});
Expand All @@ -186,7 +187,7 @@ describe('getTrendlineData()', () => {
});

test('should add _params and _data if doing a regression method and there is a tooltip on the trendline', () => {
const trendlineData = getTrendlineData({
const trendlineData = addTrendlineData(baseData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'linear' }, createElement(ChartTooltip))],
});
Expand All @@ -196,7 +197,7 @@ describe('getTrendlineData()', () => {
});

test('should add window trandform and then dimension range filter transform for movingAverage', () => {
const trendlineData = getTrendlineData({
const trendlineData = addTrendlineData(baseData, {
...defaultLineProps,
children: [createElement(Trendline, { method: 'movingAverage-3', dimensionRange: [1, 2] })],
});
Expand Down
141 changes: 119 additions & 22 deletions src/specBuilder/trendline/trendlineUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,26 @@
* governing permissions and limitations under the License.
*/
import { Trendline } from '@components/Trendline';
import { FILTERED_TABLE, MARK_ID, TRENDLINE_VALUE } from '@constants';
import { getSeriesIdTransform } from '@specBuilder/data/dataUtils';
import { getLineHoverMarks, getLineMark } from '@specBuilder/line/lineMarkUtils';
import { LineMarkProps } from '@specBuilder/line/lineUtils';
import { hasInteractiveChildren, hasPopover, hasTooltip } from '@specBuilder/marks/markUtils';
import { FILTERED_TABLE, LINEAR_PADDING, MARK_ID, TRENDLINE_VALUE } from '@constants';
import { getSeriesIdTransform, getTableData } from '@specBuilder/data/dataUtils';
import { getLineHoverMarks, getLineStrokeOpacity } from '@specBuilder/line/lineMarkUtils';
import { LineMarkProps, getXProductionRule } from '@specBuilder/line/lineUtils';
import {
getColorProductionRule,
getLineWidthProductionRule,
getStrokeDashProductionRule,
hasInteractiveChildren,
hasPopover,
hasTooltip,
} from '@specBuilder/marks/markUtils';
import {
getGenericSignal,
getSeriesHoveredSignal,
getUncontrolledHoverSignal,
} from '@specBuilder/signal/signalSpecBuilder';
import { getFacetsFromProps } from '@specBuilder/specUtils';
import { sanitizeTrendlineChildren } from '@utils';
import { produce } from 'immer';
import {
BarSpecProps,
LineSpecProps,
Expand All @@ -32,6 +40,7 @@ import {
TrendlineSpecProps,
} from 'types';
import {
Data,
FilterTransform,
FormulaTransform,
GroupMark,
Expand All @@ -40,6 +49,7 @@ import {
LookupTransform,
RegressionMethod,
RegressionTransform,
Scale,
Signal,
SourceData,
Transforms,
Expand Down Expand Up @@ -115,18 +125,36 @@ export const getTrendlineMarks = (markProps: LineSpecProps): GroupMark[] => {
};

const getTrendlineLineMark = (markProps: LineSpecProps, trendlineProps: TrendlineSpecProps): LineMark => {
const mergedTrendlineProps: LineMarkProps = {
...markProps,
name: trendlineProps.name,
children: trendlineProps.children,
color: trendlineProps.color ? { value: trendlineProps.color } : markProps.color,
metric: trendlineProps.metric,
lineType: { value: trendlineProps.lineType },
lineWidth: { value: trendlineProps.lineWidth },
opacity: { value: trendlineProps.opacity },
displayOnHover: trendlineProps.displayOnHover,
const { colorScheme, dimension, scaleType } = markProps;
const { displayOnHover, lineType, lineWidth, metric, name, opacity } = trendlineProps;
const x =
scaleType === 'time'
? { scale: 'xTrendline', field: `${dimension}Normalized` }
: getXProductionRule(scaleType, dimension);
const color = trendlineProps.color ? { value: trendlineProps.color } : markProps.color;

return {
name,
type: 'line',
from: { data: `${name}_facet` },
interactive: false,
encode: {
enter: {
y: { scale: 'yLinear', field: metric },
stroke: getColorProductionRule(color, colorScheme),
strokeDash: getStrokeDashProductionRule({ value: lineType }),
strokeWidth: getLineWidthProductionRule({ value: lineWidth }),
},
update: {
x,
strokeOpacity: getLineStrokeOpacity({
...markProps,
displayOnHover,
opacity: { value: opacity },
}),
},
},
};
return getLineMark(mergedTrendlineProps, `${trendlineProps.name}_facet`);
};

const getTrendlineHoverMarks = (lineProps: LineSpecProps, highlightRawPoint: boolean): GroupMark => {
Expand All @@ -150,10 +178,60 @@ const getTrendlineHoverMarks = (lineProps: LineSpecProps, highlightRawPoint: boo
};
};

export const addTrendlineData = (data: Data[], markProps: BarSpecProps | LineSpecProps) => {
const { dimension } = markProps;
data.push(...getTrendlineData(markProps));

if ('scaleType' in markProps && markProps.scaleType === 'time') {
// const tableDataIndex = data.findIndex((d) => d.name === FILTERED_TABLE);
const tableData = getTableData(data);
tableData.transform = addNormalizedDimensionTransform(tableData.transform ?? [], dimension);
// if (!data[tableDataIndex].transform) {
// data[tableDataIndex].transform = [];
// }
// // make sure the normalized dimension hasn't been added yet
// if (
// (data[tableDataIndex].transform as Transforms[]).findIndex(
// (transform) => 'as' in transform && transform.as === `${dimension}Normalized`
// ) === -1
// ) {
// const minimumDimension: JoinAggregateTransform = {
// type: 'joinaggregate',
// fields: [dimension],
// as: [`${dimension}Min`],
// ops: ['min'],
// };
// // normalizes the time data to number of days since the minimum date + 1 day
// const normalizedDimension: FormulaTransform = {
// type: 'formula',
// expr: `(datum.${dimension} - datum.${dimension}Min + 86400000) / 86400000`,
// as: `${dimension}Normalized`,
// };
// (data[tableDataIndex].transform as Transforms[]).push(minimumDimension, normalizedDimension);
// }
}
};

const addNormalizedDimensionTransform = produce<Transforms[], [string]>((transforms, dimension) => {
if (transforms.findIndex((transform) => 'as' in transform && transform.as === `${dimension}Normalized`) === -1) {
transforms.push({
type: 'joinaggregate',
fields: [dimension],
as: [`${dimension}Min`],
ops: ['min'],
});
transforms.push({
type: 'formula',
expr: `(datum.${dimension} - datum.${dimension}Min + 86400000) / 86400000`,
as: `${dimension}Normalized`,
});
}
});

/**
* gets the data source for the trendline
* adds the data transforms and data sources for the trendlines
* @param data
* @param markProps
* @param trendlineProps
*/
export const getTrendlineData = (markProps: BarSpecProps | LineSpecProps): SourceData[] => {
const data: SourceData[] = [];
Expand Down Expand Up @@ -327,6 +405,25 @@ export const getTrendlineParamFormulaTransforms = (dimension: string, method: Tr
];
};

export const getTrendlineScales = (props: LineSpecProps | BarSpecProps): Scale[] => {
const { children, dimension, name } = props;
const trendlines = getTrendlines(children, name);
if (trendlines.length && 'scaleType' in props && props.scaleType === 'time') {
return [
{
name: 'xTrendline',
type: 'linear',
range: 'width',
domain: { data: FILTERED_TABLE, fields: [`${dimension}Normalized`] },
padding: LINEAR_PADDING,
zero: false,
nice: false,
},
];
}
return [];
};

/**
* determines if the supplied method is a polynomial method
* @see https://vega.github.io/vega/docs/transforms/regression/
Expand Down Expand Up @@ -434,19 +531,19 @@ export const getRegressionTransform = (
break;
}

let asDimension = dimension;
let trendlineDimension = dimension;
if ('scaleType' in markProps && markProps.scaleType === 'time') {
asDimension = 'datetime0';
trendlineDimension += 'Normalized';
}

return {
type: 'regression',
method: regressionMethod,
order,
groupby: facets,
x: dimension,
x: trendlineDimension,
y: metric,
as: params ? undefined : [asDimension, TRENDLINE_VALUE],
as: params ? undefined : [trendlineDimension, TRENDLINE_VALUE],
params,
};
};
Expand Down

0 comments on commit 2d0009c

Please sign in to comment.