Skip to content

Commit

Permalink
fix: incorrect boxplot when using temporal field
Browse files Browse the repository at this point in the history
  • Loading branch information
chanwutk committed Jul 24, 2020
1 parent 1ca8c13 commit 9b5b19a
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 97 deletions.
190 changes: 93 additions & 97 deletions src/compositemark/boxplot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function normalizeBoxPlot(

const boxPlotType = getBoxPlotType(extent);
const {
bins,
timeUnits,
transform,
continuousAxisChannelDef,
continuousAxis,
Expand Down Expand Up @@ -221,117 +223,109 @@ export function normalizeBoxPlot(
})
];

// ## Filtered Layers
if (boxPlotType === 'min-max') {
return {
...outerSpec,
transform: (outerSpec.transform ?? []).concat(transform),
layer: boxLayers
};
}

let filteredLayersMixins: NormalizedUnitSpec | NormalizedLayerSpec;
// Tukey Box Plot

if (boxPlotType !== 'min-max') {
const lowerBoxExpr = `datum["lower_box_${continuousAxisChannelDef.field}"]`;
const upperBoxExpr = `datum["upper_box_${continuousAxisChannelDef.field}"]`;
const iqrExpr = `(${upperBoxExpr} - ${lowerBoxExpr})`;
const lowerWhiskerExpr = `${lowerBoxExpr} - ${extent} * ${iqrExpr}`;
const upperWhiskerExpr = `${upperBoxExpr} + ${extent} * ${iqrExpr}`;
const fieldExpr = `datum["${continuousAxisChannelDef.field}"]`;
const lowerBoxExpr = `datum["lower_box_${continuousAxisChannelDef.field}"]`;
const upperBoxExpr = `datum["upper_box_${continuousAxisChannelDef.field}"]`;
const iqrExpr = `(${upperBoxExpr} - ${lowerBoxExpr})`;
const lowerWhiskerExpr = `${lowerBoxExpr} - ${extent} * ${iqrExpr}`;
const upperWhiskerExpr = `${upperBoxExpr} + ${extent} * ${iqrExpr}`;
const fieldExpr = `datum["${continuousAxisChannelDef.field}"]`;

const joinaggregateTransform: JoinAggregateTransform = {
joinaggregate: boxParamsQuartiles(continuousAxisChannelDef.field),
groupby
};
const joinaggregateTransform: JoinAggregateTransform = {
joinaggregate: boxParamsQuartiles(continuousAxisChannelDef.field),
groupby
};

let filteredWhiskerSpec: NormalizedLayerSpec = undefined;
if (boxPlotType === 'tukey') {
filteredWhiskerSpec = {
transform: [
const filteredWhiskerSpec: NormalizedLayerSpec = {
transform: [
{
filter: `(${lowerWhiskerExpr} <= ${fieldExpr}) && (${fieldExpr} <= ${upperWhiskerExpr})`
},
{
aggregate: [
{
filter: `(${lowerWhiskerExpr} <= ${fieldExpr}) && (${fieldExpr} <= ${upperWhiskerExpr})`
op: 'min',
field: continuousAxisChannelDef.field,
as: 'lower_whisker_' + continuousAxisChannelDef.field
},
{
aggregate: [
{
op: 'min',
field: continuousAxisChannelDef.field,
as: 'lower_whisker_' + continuousAxisChannelDef.field
},
{
op: 'max',
field: continuousAxisChannelDef.field,
as: 'upper_whisker_' + continuousAxisChannelDef.field
},
// preserve lower_box / upper_box
{
op: 'min',
field: 'lower_box_' + continuousAxisChannelDef.field,
as: 'lower_box_' + continuousAxisChannelDef.field
},
{
op: 'max',
field: 'upper_box_' + continuousAxisChannelDef.field,
as: 'upper_box_' + continuousAxisChannelDef.field
},
...aggregate
],
groupby
}
op: 'max',
field: continuousAxisChannelDef.field,
as: 'upper_whisker_' + continuousAxisChannelDef.field
},
// preserve lower_box / upper_box
{
op: 'min',
field: 'lower_box_' + continuousAxisChannelDef.field,
as: 'lower_box_' + continuousAxisChannelDef.field
},
{
op: 'max',
field: 'upper_box_' + continuousAxisChannelDef.field,
as: 'upper_box_' + continuousAxisChannelDef.field
},
...aggregate
],
layer: whiskerLayers
};
}

const {tooltip, ...encodingWithoutSizeColorContinuousAxisAndTooltip} = encodingWithoutSizeColorAndContinuousAxis;

const {scale, axis} = continuousAxisChannelDef;
const title = getTitle(continuousAxisChannelDef);
const axisWithoutTitle = omit(axis, ['title']);

const outlierLayersMixins = partLayerMixins<BoxPlotPartsMixins>(markDef, 'outliers', config.boxplot, {
transform: [{filter: `(${fieldExpr} < ${lowerWhiskerExpr}) || (${fieldExpr} > ${upperWhiskerExpr})`}],
mark: 'point',
encoding: {
[continuousAxis]: {
field: continuousAxisChannelDef.field,
type: continuousAxisChannelDef.type,
...(title !== undefined ? {title} : {}),
...(scale !== undefined ? {scale} : {}),
// add axis without title since we already added the title above
...(isEmpty(axisWithoutTitle) ? {} : {axis: axisWithoutTitle})
},
...encodingWithoutSizeColorContinuousAxisAndTooltip,
...(customTooltipWithoutAggregatedField ? {tooltip: customTooltipWithoutAggregatedField} : {})
groupby
}
})[0];

if (outlierLayersMixins && filteredWhiskerSpec) {
filteredLayersMixins = {
transform: [joinaggregateTransform],
layer: [outlierLayersMixins, filteredWhiskerSpec]
};
} else if (outlierLayersMixins) {
filteredLayersMixins = outlierLayersMixins;
filteredLayersMixins.transform.unshift(joinaggregateTransform);
} else if (filteredWhiskerSpec) {
filteredLayersMixins = filteredWhiskerSpec;
filteredLayersMixins.transform.unshift(joinaggregateTransform);
}
}
],
layer: whiskerLayers
};

if (filteredLayersMixins) {
// tukey box plot with outliers included
return {
...outerSpec,
layer: [
filteredLayersMixins,
{
// boxplot
transform,
layer: boxLayers
}
]
const {tooltip, ...encodingWithoutSizeColorContinuousAxisAndTooltip} = encodingWithoutSizeColorAndContinuousAxis;

const {scale, axis} = continuousAxisChannelDef;
const title = getTitle(continuousAxisChannelDef);
const axisWithoutTitle = omit(axis, ['title']);

const outlierLayersMixins = partLayerMixins<BoxPlotPartsMixins>(markDef, 'outliers', config.boxplot, {
transform: [{filter: `(${fieldExpr} < ${lowerWhiskerExpr}) || (${fieldExpr} > ${upperWhiskerExpr})`}],
mark: 'point',
encoding: {
[continuousAxis]: {
field: continuousAxisChannelDef.field,
type: continuousAxisChannelDef.type,
...(title !== undefined ? {title} : {}),
...(scale !== undefined ? {scale} : {}),
// add axis without title since we already added the title above
...(isEmpty(axisWithoutTitle) ? {} : {axis: axisWithoutTitle})
},
...encodingWithoutSizeColorContinuousAxisAndTooltip,
...(customTooltipWithoutAggregatedField ? {tooltip: customTooltipWithoutAggregatedField} : {})
}
})[0];

let filteredLayersMixins: NormalizedLayerSpec;
const filteredLayersMixinsTransforms = [...bins, ...timeUnits, joinaggregateTransform];
if (outlierLayersMixins) {
filteredLayersMixins = {
transform: filteredLayersMixinsTransforms,
layer: [outlierLayersMixins, filteredWhiskerSpec]
};
} else {
filteredLayersMixins = filteredWhiskerSpec;
filteredLayersMixins.transform.unshift(...filteredLayersMixinsTransforms);
}

return {
...outerSpec,
transform: (outerSpec.transform ?? []).concat(transform),
layer: boxLayers
layer: [
filteredLayersMixins,
{
// boxplot
transform,
layer: boxLayers
}
]
};
}

Expand Down Expand Up @@ -423,6 +417,8 @@ function boxParams(
];

return {
bins,
timeUnits,
transform,
groupby,
aggregate,
Expand Down
27 changes: 27 additions & 0 deletions test/compositemark/boxplot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,4 +1070,31 @@ describe('normalizeBoxIQR', () => {
const {tooltip} = normalizedSpecWithTooltip['layer'][0]['layer'][0]['encoding'];
expect(tooltip).toEqual({field: 'year', type: 'quantitative'});
});

it("should include timeUnit transform in filteredLayerMixins' transform", () => {
const field = 'Date';
const timeUnit = 'year';
const normalizedSpec = normalize(
{
data: {url: 'data/population.json'},
mark: 'boxplot',
encoding: {
x: {
field,
type: 'temporal',
timeUnit
},
y: {field: 'Anomaly', type: 'quantitative'}
}
},
defaultConfig
);

const filteredLayerMixins = normalizedSpec['layer'][1];
expect(filteredLayerMixins.transform[0]).toEqual({
timeUnit: {unit: 'year'},
field,
as: `${timeUnit}_${field}`
});
});
});

0 comments on commit 9b5b19a

Please sign in to comment.