-
Notifications
You must be signed in to change notification settings - Fork 592
/
predicate.ts
268 lines (234 loc) · 8.63 KB
/
predicate.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import {SignalRef} from 'vega-typings/types';
import {isArray} from 'vega-util';
import {FieldName, valueExpr, vgField} from './channeldef';
import {DateTime} from './datetime';
import {ExprRef} from './expr';
import {LogicalComposition} from './logical';
import {fieldExpr as timeUnitFieldExpr, normalizeTimeUnit, TimeUnit, TimeUnitParams} from './timeunit';
import {isSignalRef} from './vega.schema';
export type Predicate =
// a) FieldPredicate (but we don't type FieldFilter here so the schema has no nesting
// and thus the documentation shows all of the types clearly)
| FieldEqualPredicate
| FieldRangePredicate
| FieldOneOfPredicate
| FieldLTPredicate
| FieldGTPredicate
| FieldLTEPredicate
| FieldGTEPredicate
| FieldValidPredicate
// b) Selection Predicate
| SelectionPredicate
// c) Vega Expression string
| string;
export type FieldPredicate =
| FieldEqualPredicate
| FieldLTPredicate
| FieldGTPredicate
| FieldLTEPredicate
| FieldGTEPredicate
| FieldRangePredicate
| FieldOneOfPredicate
| FieldValidPredicate;
export interface SelectionPredicate {
/**
* Filter using a selection name or a logical composition of selection names.
*/
selection: LogicalComposition<string>;
}
export function isSelectionPredicate(predicate: LogicalComposition<Predicate>): predicate is SelectionPredicate {
return predicate?.['selection'];
}
export interface FieldPredicateBase {
// TODO: support aggregate
/**
* Time unit for the field to be tested.
*/
timeUnit?: TimeUnit | TimeUnitParams;
/**
* Field to be tested.
*/
field: FieldName;
}
export interface FieldEqualPredicate extends FieldPredicateBase {
/**
* The value that the field should be equal to.
*/
equal: string | number | boolean | DateTime | ExprRef | SignalRef;
}
export function isFieldEqualPredicate(predicate: any): predicate is FieldEqualPredicate {
return predicate && !!predicate.field && predicate.equal !== undefined;
}
export interface FieldLTPredicate extends FieldPredicateBase {
/**
* The value that the field should be less than.
*/
lt: string | number | DateTime | ExprRef | SignalRef;
}
export function isFieldLTPredicate(predicate: any): predicate is FieldLTPredicate {
return predicate && !!predicate.field && predicate.lt !== undefined;
}
export interface FieldLTEPredicate extends FieldPredicateBase {
/**
* The value that the field should be less than or equals to.
*/
lte: string | number | DateTime | ExprRef | SignalRef;
}
export function isFieldLTEPredicate(predicate: any): predicate is FieldLTEPredicate {
return predicate && !!predicate.field && predicate.lte !== undefined;
}
export interface FieldGTPredicate extends FieldPredicateBase {
/**
* The value that the field should be greater than.
*/
gt: string | number | DateTime | ExprRef | SignalRef;
}
export function isFieldGTPredicate(predicate: any): predicate is FieldGTPredicate {
return predicate && !!predicate.field && predicate.gt !== undefined;
}
export interface FieldGTEPredicate extends FieldPredicateBase {
/**
* The value that the field should be greater than or equals to.
*/
gte: string | number | DateTime | ExprRef | SignalRef;
}
export function isFieldGTEPredicate(predicate: any): predicate is FieldGTEPredicate {
return predicate && !!predicate.field && predicate.gte !== undefined;
}
export interface FieldRangePredicate extends FieldPredicateBase {
/**
* An array of inclusive minimum and maximum values
* for a field value of a data item to be included in the filtered data.
* @maxItems 2
* @minItems 2
*/
range: (number | DateTime | null | ExprRef | SignalRef)[] | ExprRef | SignalRef;
}
export function isFieldRangePredicate(predicate: any): predicate is FieldRangePredicate {
if (predicate && predicate.field) {
if (isArray(predicate.range) && predicate.range.length === 2) {
return true;
} else if (isSignalRef(predicate.range)) {
return true;
}
}
return false;
}
export interface FieldOneOfPredicate extends FieldPredicateBase {
/**
* A set of values that the `field`'s value should be a member of,
* for a data item included in the filtered data.
*/
oneOf: string[] | number[] | boolean[] | DateTime[];
}
export interface FieldValidPredicate extends FieldPredicateBase {
/**
* If set to true the field's value has to be valid, meaning both not `null` and not [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN).
*/
valid: boolean;
}
export function isFieldOneOfPredicate(predicate: any): predicate is FieldOneOfPredicate {
return (
predicate && !!predicate.field && (isArray(predicate.oneOf) || isArray(predicate.in)) // backward compatibility
);
}
export function isFieldValidPredicate(predicate: any): predicate is FieldValidPredicate {
return predicate && !!predicate.field && predicate.valid !== undefined;
}
export function isFieldPredicate(
predicate: Predicate
): predicate is
| FieldOneOfPredicate
| FieldEqualPredicate
| FieldRangePredicate
| FieldLTPredicate
| FieldGTPredicate
| FieldLTEPredicate
| FieldGTEPredicate {
return (
isFieldOneOfPredicate(predicate) ||
isFieldEqualPredicate(predicate) ||
isFieldRangePredicate(predicate) ||
isFieldLTPredicate(predicate) ||
isFieldGTPredicate(predicate) ||
isFieldLTEPredicate(predicate) ||
isFieldGTEPredicate(predicate)
);
}
function predicateValueExpr(v: number | string | boolean | DateTime | ExprRef | SignalRef, timeUnit: TimeUnit) {
return valueExpr(v, {timeUnit, wrapTime: true});
}
function predicateValuesExpr(vals: (number | string | boolean | DateTime)[], timeUnit: TimeUnit) {
return vals.map(v => predicateValueExpr(v, timeUnit));
}
// This method is used by Voyager. Do not change its behavior without changing Voyager.
export function fieldFilterExpression(predicate: FieldPredicate, useInRange = true) {
const {field} = predicate;
const timeUnit = normalizeTimeUnit(predicate.timeUnit)?.unit;
const fieldExpr = 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
// TODO: support utc
'time(' + timeUnitFieldExpr(timeUnit, field) + ')'
: vgField(predicate, {expr: 'datum'});
if (isFieldEqualPredicate(predicate)) {
return fieldExpr + '===' + predicateValueExpr(predicate.equal, timeUnit);
} else if (isFieldLTPredicate(predicate)) {
const upper = predicate.lt;
return `${fieldExpr}<${predicateValueExpr(upper, timeUnit)}`;
} else if (isFieldGTPredicate(predicate)) {
const lower = predicate.gt;
return `${fieldExpr}>${predicateValueExpr(lower, timeUnit)}`;
} else if (isFieldLTEPredicate(predicate)) {
const upper = predicate.lte;
return `${fieldExpr}<=${predicateValueExpr(upper, timeUnit)}`;
} else if (isFieldGTEPredicate(predicate)) {
const lower = predicate.gte;
return `${fieldExpr}>=${predicateValueExpr(lower, timeUnit)}`;
} else if (isFieldOneOfPredicate(predicate)) {
return `indexof([${predicateValuesExpr(predicate.oneOf, timeUnit).join(',')}], ${fieldExpr}) !== -1`;
} else if (isFieldValidPredicate(predicate)) {
return fieldValidPredicate(fieldExpr, predicate.valid);
} else if (isFieldRangePredicate(predicate)) {
const {range} = predicate;
const lower = isSignalRef(range) ? {signal: `${range.signal}[0]`} : range[0];
const upper = isSignalRef(range) ? {signal: `${range.signal}[1]`} : range[1];
if (lower !== null && upper !== null && useInRange) {
return (
'inrange(' +
fieldExpr +
', [' +
predicateValueExpr(lower, timeUnit) +
', ' +
predicateValueExpr(upper, timeUnit) +
'])'
);
}
const exprs = [];
if (lower !== null) {
exprs.push(`${fieldExpr} >= ${predicateValueExpr(lower, timeUnit)}`);
}
if (upper !== null) {
exprs.push(`${fieldExpr} <= ${predicateValueExpr(upper, timeUnit)}`);
}
return exprs.length > 0 ? exprs.join(' && ') : 'true';
}
/* istanbul ignore next: it should never reach here */
throw new Error(`Invalid field predicate: ${JSON.stringify(predicate)}`);
}
export function fieldValidPredicate(fieldExpr: string, valid = true) {
if (valid) {
return `isValid(${fieldExpr}) && isFinite(+${fieldExpr})`;
} else {
return `!isValid(${fieldExpr}) || !isFinite(+${fieldExpr})`;
}
}
export function normalizePredicate(f: Predicate): Predicate {
if (isFieldPredicate(f) && f.timeUnit) {
return {
...f,
timeUnit: normalizeTimeUnit(f.timeUnit)?.unit
};
}
return f;
}