From 567910e65a31ec48029ecdd73eeff04ad5cdb27d Mon Sep 17 00:00:00 2001 From: Kanit Wongsuphasawat Date: Wed, 17 Jan 2018 11:35:46 -0800 Subject: [PATCH] Refactor: Distinguish between FieldPredicate and SelectionFilter (FieldPredicate can be useful in other scenario besides filter -- e.g., for condition in https://github.com/vega/vega-lite/pull/3239 or for selecting data for annotation (@starry97's project) --- build/vega-lite-schema.json | 240 ++++++++++++++++---------------- site/docs/transform/filter.md | 11 +- src/compile/data/formatparse.ts | 4 +- src/compile/data/parse.ts | 8 +- src/filter.ts | 43 +++--- src/transform.ts | 7 +- test/filter.test.ts | 14 +- 7 files changed, 167 insertions(+), 160 deletions(-) diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index b28ff8edc95..d1a698b0ded 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -1641,41 +1641,6 @@ }, "type": "object" }, - "EqualFilter": { - "additionalProperties": false, - "properties": { - "equal": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "$ref": "#/definitions/DateTime" - } - ], - "description": "The value that the field should be equal to." - }, - "field": { - "description": "Field to be filtered.", - "type": "string" - }, - "timeUnit": { - "$ref": "#/definitions/TimeUnit", - "description": "Time unit for the field to be filtered." - } - }, - "required": [ - "field", - "equal" - ], - "type": "object" - }, "FacetFieldDef": { "additionalProperties": false, "properties": { @@ -1992,16 +1957,132 @@ ], "type": "object" }, + "FieldEqualPredicate": { + "additionalProperties": false, + "properties": { + "equal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/DateTime" + } + ], + "description": "The value that the field should be equal to." + }, + "field": { + "description": "Field to be filtered.", + "type": "string" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit for the field to be filtered." + } + }, + "required": [ + "field", + "equal" + ], + "type": "object" + }, + "FieldOneOfPredicate": { + "additionalProperties": false, + "properties": { + "field": { + "description": "Field to be filtered", + "type": "string" + }, + "oneOf": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "items": { + "type": "boolean" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/definitions/DateTime" + }, + "type": "array" + } + ], + "description": "A set of values that the `field`'s value should be a member of,\nfor a data item included in the filtered data." + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "time unit for the field to be filtered." + } + }, + "required": [ + "field", + "oneOf" + ], + "type": "object" + }, + "FieldRangePredicate": { + "additionalProperties": false, + "properties": { + "field": { + "description": "Field to be filtered", + "type": "string" + }, + "range": { + "description": "An array of inclusive minimum and maximum values\nfor a field value of a data item to be included in the filtered data.", + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/DateTime" + } + ] + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "time unit for the field to be filtered." + } + }, + "required": [ + "field", + "range" + ], + "type": "object" + }, "Filter": { "anyOf": [ { - "$ref": "#/definitions/EqualFilter" + "$ref": "#/definitions/FieldEqualPredicate" }, { - "$ref": "#/definitions/RangeFilter" + "$ref": "#/definitions/FieldRangePredicate" }, { - "$ref": "#/definitions/OneOfFilter" + "$ref": "#/definitions/FieldOneOfPredicate" }, { "$ref": "#/definitions/SelectionFilter" @@ -2016,7 +2097,7 @@ "properties": { "filter": { "$ref": "#/definitions/FilterOperand", - "description": "The `filter` property must be either (1) a filter object for [equal-filters](filter.html#equalfilter),\n[range-filters](filter.html#rangefilter), [one-of filters](filter.html#oneoffilter), or [selection filters](filter.html#selectionfilter);\n(2) a [Vega Expression](filter.html#expression) string,\nwhere `datum` can be used to refer to the current data object; or (3) an array of filters (either objects or expression strings) that must all be true for a datum to pass the filter and be included." + "description": "The `filter` property must be either (1) one of the field predicates: [equal predicate](filter.html#equalfilter),\n[range precidate](filter.html#rangefilter), [one-of predicate](filter.html#oneoffilter);;\n(2) a [selection filter definition](filter.html#selectionfilter)\n(3) a [Vega Expression](filter.html#expression) string,\nwhere `datum` can be used to refer to the current data object; or (3) an array of filters (either objects or expression strings) that must all be true for a datum to pass the filter and be included." } }, "required": [ @@ -3788,53 +3869,6 @@ ], "type": "string" }, - "OneOfFilter": { - "additionalProperties": false, - "properties": { - "field": { - "description": "Field to be filtered", - "type": "string" - }, - "oneOf": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "items": { - "type": "number" - }, - "type": "array" - }, - { - "items": { - "type": "boolean" - }, - "type": "array" - }, - { - "items": { - "$ref": "#/definitions/DateTime" - }, - "type": "array" - } - ], - "description": "A set of values that the `field`'s value should be a member of,\nfor a data item included in the filtered data." - }, - "timeUnit": { - "$ref": "#/definitions/TimeUnit", - "description": "time unit for the field to be filtered." - } - }, - "required": [ - "field", - "oneOf" - ], - "type": "object" - }, "OrderFieldDef": { "additionalProperties": false, "properties": { @@ -4111,40 +4145,6 @@ } ] }, - "RangeFilter": { - "additionalProperties": false, - "properties": { - "field": { - "description": "Field to be filtered", - "type": "string" - }, - "range": { - "description": "An array of inclusive minimum and maximum values\nfor a field value of a data item to be included in the filtered data.", - "items": { - "anyOf": [ - { - "type": "number" - }, - { - "$ref": "#/definitions/DateTime" - } - ] - }, - "maxItems": 2, - "minItems": 2, - "type": "array" - }, - "timeUnit": { - "$ref": "#/definitions/TimeUnit", - "description": "time unit for the field to be filtered." - } - }, - "required": [ - "field", - "range" - ], - "type": "object" - }, "Repeat": { "additionalProperties": false, "properties": { diff --git a/site/docs/transform/filter.md b/site/docs/transform/filter.md index e3740ef7ad4..2df559a3b9c 100644 --- a/site/docs/transform/filter.md +++ b/site/docs/transform/filter.md @@ -30,13 +30,12 @@ Vega-Lite filter transforms must have the `filter` property. For a [Vega Expression](https://vega.github.io/vega/docs/expressions/) string, each datum object can be referred using bound variable `datum`. For example, setting `filter` to `"datum.b2 > 60"` would make the output data includes only items that have values in the field `b2` over 60. -## Filter Object +## Field Predicate Object - -For a filter object, either a `field` or [`selection` name](#selectionfilter) must be provided. The former takes one of the filter operators ([`equal`](#equalfilter), [`range`](#rangefilter), or [`oneOf`](#oneofilter)). Values of these operators can be primitive types (string, number, boolean) or a [DateTime definition object](types.html#datetime) to describe time. In addition, `timeUnit` can be provided to further transform a temporal `field`. +For a filter object, either a `field` must be provided along with one of the predicate properties: ([`equal`](#equalfilter), [`range`](#rangefilter), or [`oneOf`](#oneofilter)). Values of these operators can be primitive types (string, number, boolean) or a [DateTime definition object](types.html#datetime) to describe time. In addition, `timeUnit` can be provided to further transform a temporal `field`. {:#equalfilter} -### Equal Filter +### Equal Predicate {% include table.html props="field,equal,timeUnit" source="EqualFilter" %} @@ -68,7 +67,9 @@ For example, to check if the `car_color` field's value is equal to `"red"`, we c For example, `{"filter": {"field": "car_color", "oneOf":["red", "yellow"]}}` checks if the `car_color` field's value is `"red"` or `"yellow"`. {:#selectionfilter} -### Selection Filter +## Selection Filter + +For a selection filter object, a [`selection` name](#selectionfilter) must be provided. {% include table.html props="selection" source="SelectionFilter" %} diff --git a/src/compile/data/formatparse.ts b/src/compile/data/formatparse.ts index 89863993c6c..634fe72177f 100644 --- a/src/compile/data/formatparse.ts +++ b/src/compile/data/formatparse.ts @@ -1,6 +1,6 @@ import {isCountingAggregateOp} from '../../aggregate'; import {isNumberFieldDef, isTimeFieldDef} from '../../fielddef'; -import {isEqualFilter, isOneOfFilter, isRangeFilter} from '../../filter'; +import {isFieldEqualPredicate, isFieldOneOfPredicate, isFieldPredicate, isFieldRangePredicate} from '../../filter'; import * as log from '../../log'; import {forEachLeave} from '../../logical'; import {isCalculate, isFilter, Transform} from '../../transform'; @@ -54,7 +54,7 @@ export class ParseNode extends DataFlowNode { calcFieldMap[transform.as] = true; } else if (isFilter(transform)) { forEachLeave(transform.filter, (filter) => { - if (isEqualFilter(filter) || isRangeFilter(filter) || isOneOfFilter(filter)) { + if (isFieldPredicate(filter)) { if (filter.timeUnit) { parse[filter.field] = 'date'; } diff --git a/src/compile/data/parse.ts b/src/compile/data/parse.ts index 7d06801f480..f6e8ad9a075 100644 --- a/src/compile/data/parse.ts +++ b/src/compile/data/parse.ts @@ -1,7 +1,7 @@ import {isNumber, isString} from 'vega-util'; import {MAIN, RAW} from '../../data'; import {DateTime, isDateTime} from '../../datetime'; -import {isEqualFilter, isOneOfFilter, isRangeFilter} from '../../filter'; +import {isFieldEqualPredicate, isFieldOneOfPredicate, isFieldRangePredicate} from '../../filter'; import * as log from '../../log'; import {isAggregate, isBin, isCalculate, isFilter, isLookup, isTimeUnit} from '../../transform'; import {Dict, keys} from '../../util'; @@ -76,11 +76,11 @@ export function parseTransformArray(model: Model) { // For EqualFilter, just use the equal property. // For RangeFilter and OneOfFilter, all array members should have // the same type, so we only use the first one. - if (isEqualFilter(filter)) { + if (isFieldEqualPredicate(filter)) { val = filter.equal; - } else if (isRangeFilter(filter)) { + } else if (isFieldRangePredicate(filter)) { val = filter.range[0]; - } else if (isOneOfFilter(filter)) { + } else if (isFieldOneOfPredicate(filter)) { val = (filter.oneOf || filter['in'])[0]; } // else -- for filter expression, we can't infer anything diff --git a/src/filter.ts b/src/filter.ts index c7e9091152b..c412f88ce70 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -9,12 +9,17 @@ import {isArray, isString, logicalExpr} from './util'; export type Filter = - // FieldFilter (but we don't type FieldFilter here so the schema has no nesting + // a) FieldPrecidate (but we don't type FieldFilter here so the schema has no nesting // and thus the documentation shows all of the types clearly) - EqualFilter | RangeFilter | OneOfFilter | - SelectionFilter | string; + FieldEqualPredicate | FieldRangePredicate | FieldOneOfPredicate | + // b) Selection Filter + SelectionFilter | + // c) Vega Expression string + string; -export type FieldFilter = EqualFilter | RangeFilter | OneOfFilter; + + +export type FieldPredicate = FieldEqualPredicate | FieldRangePredicate | FieldOneOfPredicate; export interface SelectionFilter { /** @@ -27,7 +32,7 @@ export function isSelectionFilter(filter: LogicalOperand): filter is Sel return filter && filter['selection']; } -export interface EqualFilter { +export interface FieldEqualPredicate { // TODO: support aggregate /** @@ -47,11 +52,11 @@ export interface EqualFilter { } -export function isEqualFilter(filter: any): filter is EqualFilter { - return filter && !!filter.field && filter.equal!==undefined; +export function isFieldEqualPredicate(filter: any): filter is FieldEqualPredicate { + return filter && !!filter.field && filter.equal !== undefined; } -export interface RangeFilter { +export interface FieldRangePredicate { // TODO: support aggregate /** @@ -74,7 +79,7 @@ export interface RangeFilter { } -export function isRangeFilter(filter: any): filter is RangeFilter { +export function isFieldRangePredicate(filter: any): filter is FieldRangePredicate { if (filter && filter.field) { if (isArray(filter.range) && filter.range.length === 2) { return true; @@ -83,7 +88,7 @@ export function isRangeFilter(filter: any): filter is RangeFilter { return false; } -export interface OneOfFilter { +export interface FieldOneOfPredicate { // TODO: support aggregate /** @@ -104,15 +109,15 @@ export interface OneOfFilter { } -export function isOneOfFilter(filter: any): filter is OneOfFilter { +export function isFieldOneOfPredicate(filter: any): filter is FieldOneOfPredicate { return filter && !!filter.field && ( isArray(filter.oneOf) || isArray(filter.in) // backward compatibility ); } -export function isFieldFilter(filter: Filter): filter is OneOfFilter | EqualFilter | RangeFilter { - return isOneOfFilter(filter) || isEqualFilter(filter) || isRangeFilter(filter); +export function isFieldPredicate(filter: Filter): filter is FieldOneOfPredicate | FieldEqualPredicate | FieldRangePredicate { + return isFieldOneOfPredicate(filter) || isFieldEqualPredicate(filter) || isFieldRangePredicate(filter); } /** @@ -132,7 +137,7 @@ export function expression(model: Model, filterOp: LogicalOperand, node? } // This method is used by Voyager. Do not change its behavior without changing Voyager. -export function fieldFilterExpression(filter: FieldFilter, useInRange=true) { +export function fieldFilterExpression(filter: FieldPredicate, useInRange=true) { const fieldExpr = filter.timeUnit ? // For timeUnit, cast into integer with time() so we can use ===, inrange, indexOf to compare values directly. // TODO: We calculate timeUnit on the fly here. Consider if we would like to consolidate this with timeUnit pipeline @@ -140,15 +145,15 @@ export function fieldFilterExpression(filter: FieldFilter, useInRange=true) { ('time(' + timeUnitFieldExpr(filter.timeUnit, filter.field) + ')') : vgField(filter, {expr: 'datum'}); - if (isEqualFilter(filter)) { + if (isFieldEqualPredicate(filter)) { return fieldExpr + '===' + valueExpr(filter.equal, filter.timeUnit); - } else if (isOneOfFilter(filter)) { + } else if (isFieldOneOfPredicate(filter)) { // "oneOf" was formerly "in" -- so we need to add backward compatibility - const oneOf: OneOfFilter[] = filter.oneOf || filter['in']; + const oneOf: FieldOneOfPredicate[] = filter.oneOf || filter['in']; return 'indexof([' + oneOf.map((v) => valueExpr(v, filter.timeUnit)).join(',') + '], ' + fieldExpr + ') !== -1'; - } else if (isRangeFilter(filter)) { + } else if (isFieldRangePredicate(filter)) { const lower = filter.range[0]; const upper = filter.range[1]; @@ -190,7 +195,7 @@ function valueExpr(v: any, timeUnit: TimeUnit): string { } export function normalizeFilter(f: Filter): Filter { - if (isFieldFilter(f) && f.timeUnit) { + if (isFieldPredicate(f) && f.timeUnit) { return { ...f, timeUnit: normalizeTimeUnit(f.timeUnit) diff --git a/src/transform.ts b/src/transform.ts index aa1e09f691e..b78bb55e1d2 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -8,9 +8,10 @@ import {TimeUnit} from './timeunit'; export interface FilterTransform { /** - * The `filter` property must be either (1) a filter object for [equal-filters](filter.html#equalfilter), - * [range-filters](filter.html#rangefilter), [one-of filters](filter.html#oneoffilter), or [selection filters](filter.html#selectionfilter); - * (2) a [Vega Expression](filter.html#expression) string, + * The `filter` property must be either (1) one of the field predicates: [equal predicate](filter.html#equalfilter), + * [range precidate](filter.html#rangefilter), [one-of predicate](filter.html#oneoffilter);; + * (2) a [selection filter definition](filter.html#selectionfilter) + * (3) a [Vega Expression](filter.html#expression) string, * where `datum` can be used to refer to the current data object; or (3) an array of filters (either objects or expression strings) that must all be true for a datum to pass the filter and be included. */ filter: LogicalOperand; diff --git a/test/filter.test.ts b/test/filter.test.ts index 394ed44187d..3b391faeb74 100644 --- a/test/filter.test.ts +++ b/test/filter.test.ts @@ -1,6 +1,6 @@ import {assert} from 'chai'; -import {expression, fieldFilterExpression, isEqualFilter, isOneOfFilter, isRangeFilter} from '../src/filter'; +import {expression, fieldFilterExpression, isFieldEqualPredicate, isFieldOneOfPredicate, isFieldRangePredicate} from '../src/filter'; import {TimeUnit} from '../src/timeunit'; describe('filter', () => { @@ -11,36 +11,36 @@ describe('filter', () => { describe('isEqualFilter', () => { it('should return true for an equal filter', () => { - assert.isTrue(isEqualFilter(equalFilter)); + assert.isTrue(isFieldEqualPredicate(equalFilter)); }); it('should return false for other filters', () => { [oneOfFilter, rangeFilter, exprFilter].forEach((filter) => { - assert.isFalse(isEqualFilter(filter)); + assert.isFalse(isFieldEqualPredicate(filter)); }); }); }); describe('isOneOfFilter', () => { it('should return true for an in filter', () => { - assert.isTrue(isOneOfFilter(oneOfFilter)); + assert.isTrue(isFieldOneOfPredicate(oneOfFilter)); }); it('should return false for other filters', () => { [equalFilter, rangeFilter, exprFilter].forEach((filter) => { - assert.isFalse(isOneOfFilter(filter)); + assert.isFalse(isFieldOneOfPredicate(filter)); }); }); }); describe('isRangeFilter', () => { it('should return true for a range filter', () => { - assert.isTrue(isRangeFilter(rangeFilter)); + assert.isTrue(isFieldRangePredicate(rangeFilter)); }); it('should return false for other filters', () => { [oneOfFilter, equalFilter, exprFilter].forEach((filter) => { - assert.isFalse(isRangeFilter(filter)); + assert.isFalse(isFieldRangePredicate(filter)); }); }); });