UNPKG

compassql

Version:

CompassQL visualization query language

451 lines 18.5 kB
import { isString } from 'datalib/src/util'; import { isAggregateOp } from 'vega-lite/build/src/aggregate'; import { isChannel } from 'vega-lite/build/src/channel'; import { isTimeUnit } from 'vega-lite/build/src/timeunit'; import * as TYPE from 'vega-lite/build/src/type'; import { getFullName } from 'vega-lite/build/src/type'; import { DEFAULT_PROP_PRECEDENCE, getEncodingNestedProp, isEncodingNestedParent, Property, SORT_PROPS, VIEW_PROPS } from '../property'; import { PropIndex } from '../propindex'; import { isArray, isBoolean, keys } from '../util'; import { isShortWildcard, isWildcard, SHORT_WILDCARD } from '../wildcard'; import { isAutoCountQuery, isDisabledAutoCountQuery, isEnabledAutoCountQuery, isFieldQuery, isValueQuery } from './encoding'; import { fromSpec, getVlStack } from './spec'; export function getReplacerIndex(replaceIndex) { return replaceIndex.map(r => getReplacer(r)); } export function getReplacer(replace) { return (s) => { if (replace[s] !== undefined) { return replace[s]; } return s; }; } export function value(v, replacer) { if (isWildcard(v)) { // Return the enum array if it's a full wildcard, or just return SHORT_WILDCARD for short ones. if (!isShortWildcard(v) && v.enum) { return SHORT_WILDCARD + JSON.stringify(v.enum); } else { return SHORT_WILDCARD; } } if (replacer) { return replacer(v); } return v; } export function replace(v, replacer) { if (replacer) { return replacer(v); } return v; } export const REPLACE_NONE = new PropIndex(); export const INCLUDE_ALL = // FIXME: remove manual TRANSFORM concat once we really support enumerating transform. [] .concat(DEFAULT_PROP_PRECEDENCE, SORT_PROPS, [Property.TRANSFORM, Property.STACK], VIEW_PROPS) .reduce((pi, prop) => pi.set(prop, true), new PropIndex()); export function vlSpec(vlspec, include = INCLUDE_ALL, replace = REPLACE_NONE) { const specQ = fromSpec(vlspec); return spec(specQ, include, replace); } export const PROPERTY_SUPPORTED_CHANNELS = { axis: { x: true, y: true, row: true, column: true }, legend: { color: true, opacity: true, size: true, shape: true }, scale: { x: true, y: true, color: true, opacity: true, row: true, column: true, size: true, shape: true }, sort: { x: true, y: true, path: true, order: true }, stack: { x: true, y: true } }; /** * Returns a shorthand for a spec query * @param specQ a spec query * @param include Dict Set listing property types (key) to be included in the shorthand * @param replace Dictionary of replace function for values of a particular property type (key) */ export function spec(specQ, include = INCLUDE_ALL, replace = REPLACE_NONE) { const parts = []; if (include.get(Property.MARK)) { parts.push(value(specQ.mark, replace.get(Property.MARK))); } if (specQ.transform && specQ.transform.length > 0) { parts.push('transform:' + JSON.stringify(specQ.transform)); } let stack; if (include.get(Property.STACK)) { stack = getVlStack(specQ); } if (specQ.encodings) { const encodings = specQ.encodings .reduce((encQs, encQ) => { // Exclude encoding mapping with autoCount=false as they are basically disabled. if (!isDisabledAutoCountQuery(encQ)) { let str; if (!!stack && encQ.channel === stack.fieldChannel) { str = encoding(Object.assign({}, encQ, { stack: stack.offset }), include, replace); } else { str = encoding(encQ, include, replace); } if (str) { // only add if the shorthand isn't an empty string. encQs.push(str); } } return encQs; }, []) .sort() // sort at the end to ignore order .join('|'); if (encodings) { parts.push(encodings); } } for (let viewProp of VIEW_PROPS) { const propString = viewProp.toString(); if (include.get(viewProp) && !!specQ[propString]) { const value = specQ[propString]; parts.push(`${propString}=${JSON.stringify(value)}`); } } return parts.join('|'); } /** * Returns a shorthand for an encoding query * @param encQ an encoding query * @param include Dict Set listing property types (key) to be included in the shorthand * @param replace Dictionary of replace function for values of a particular property type (key) */ export function encoding(encQ, include = INCLUDE_ALL, replace = REPLACE_NONE) { const parts = []; if (include.get(Property.CHANNEL)) { parts.push(value(encQ.channel, replace.get(Property.CHANNEL))); } if (isFieldQuery(encQ)) { const fieldDefStr = fieldDef(encQ, include, replace); if (fieldDefStr) { parts.push(fieldDefStr); } } else if (isValueQuery(encQ)) { parts.push(encQ.value); } else if (isAutoCountQuery(encQ)) { parts.push('autocount()'); } return parts.join(':'); } /** * Returns a field definition shorthand for an encoding query * @param encQ an encoding query * @param include Dict Set listing property types (key) to be included in the shorthand * @param replace Dictionary of replace function for values of a particular property type (key) */ export function fieldDef(encQ, include = INCLUDE_ALL, replacer = REPLACE_NONE) { if (include.get(Property.AGGREGATE) && isDisabledAutoCountQuery(encQ)) { return '-'; } const fn = func(encQ, include, replacer); const props = fieldDefProps(encQ, include, replacer); let fieldAndParams; if (isFieldQuery(encQ)) { // field fieldAndParams = include.get('field') ? value(encQ.field, replacer.get('field')) : '...'; // type if (include.get(Property.TYPE)) { if (isWildcard(encQ.type)) { fieldAndParams += ',' + value(encQ.type, replacer.get(Property.TYPE)); } else { const typeShort = ((encQ.type || TYPE.QUANTITATIVE) + '').substr(0, 1); fieldAndParams += ',' + value(typeShort, replacer.get(Property.TYPE)); } } // encoding properties fieldAndParams += props .map(p => { let val = p.value instanceof Array ? '[' + p.value + ']' : p.value; return ',' + p.key + '=' + val; }) .join(''); } else if (isAutoCountQuery(encQ)) { fieldAndParams = '*,q'; } if (!fieldAndParams) { return null; } if (fn) { let fnPrefix = isString(fn) ? fn : SHORT_WILDCARD + (keys(fn).length > 0 ? JSON.stringify(fn) : ''); return fnPrefix + '(' + fieldAndParams + ')'; } return fieldAndParams; } /** * Return function part of */ function func(fieldQ, include, replacer) { if (include.get(Property.AGGREGATE) && fieldQ.aggregate && !isWildcard(fieldQ.aggregate)) { return replace(fieldQ.aggregate, replacer.get(Property.AGGREGATE)); } else if (include.get(Property.AGGREGATE) && isEnabledAutoCountQuery(fieldQ)) { // autoCount is considered a part of aggregate return replace('count', replacer.get(Property.AGGREGATE)); } else if (include.get(Property.TIMEUNIT) && fieldQ.timeUnit && !isWildcard(fieldQ.timeUnit)) { return replace(fieldQ.timeUnit, replacer.get(Property.TIMEUNIT)); } else if (include.get(Property.BIN) && fieldQ.bin && !isWildcard(fieldQ.bin)) { return 'bin'; } else { let fn = null; for (const prop of [Property.AGGREGATE, Property.AUTOCOUNT, Property.TIMEUNIT, Property.BIN]) { const val = fieldQ[prop]; if (include.get(prop) && fieldQ[prop] && isWildcard(val)) { // assign fnEnumIndex[prop] = array of enum values or just "?" if it is SHORT_WILDCARD fn = fn || {}; fn[prop] = isShortWildcard(val) ? val : val.enum; } } if (fn && fieldQ.hasFn) { fn.hasFn = true; } return fn; } } /** * Return key-value of parameters of field defs */ function fieldDefProps(fieldQ, include, replacer) { /** Encoding properties e.g., Scale, Axis, Legend */ const props = []; // Parameters of function such as bin will be just top-level properties if (!isBoolean(fieldQ.bin) && !isShortWildcard(fieldQ.bin)) { const bin = fieldQ.bin; for (const child in bin) { const prop = getEncodingNestedProp('bin', child); if (prop && include.get(prop) && bin[child] !== undefined) { props.push({ key: child, value: value(bin[child], replacer.get(prop)) }); } } // Sort to make sure that parameter are ordered consistently props.sort((a, b) => a.key.localeCompare(b.key)); } for (const parent of [Property.SCALE, Property.SORT, Property.STACK, Property.AXIS, Property.LEGEND]) { if (!isWildcard(fieldQ.channel) && !PROPERTY_SUPPORTED_CHANNELS[parent][fieldQ.channel]) { continue; } if (include.get(parent) && fieldQ[parent] !== undefined) { const parentValue = fieldQ[parent]; if (isBoolean(parentValue) || parentValue === null) { // `scale`, `axis`, `legend` can be false/null. props.push({ key: parent + '', value: parentValue || false // return true or false (false if null) }); } else if (isString(parentValue)) { // `sort` can be a string (ascending/descending). props.push({ key: parent + '', value: replace(JSON.stringify(parentValue), replacer.get(parent)) }); } else { let nestedPropChildren = []; for (const child in parentValue) { const nestedProp = getEncodingNestedProp(parent, child); if (nestedProp && include.get(nestedProp) && parentValue[child] !== undefined) { nestedPropChildren.push({ key: child, value: value(parentValue[child], replacer.get(nestedProp)) }); } } if (nestedPropChildren.length > 0) { const nestedPropObject = nestedPropChildren .sort((a, b) => a.key.localeCompare(b.key)) .reduce((o, item) => { o[item.key] = item.value; return o; }, {}); // Sort to make sure that parameter are ordered consistently props.push({ key: parent + '', value: JSON.stringify(nestedPropObject) }); } } } } return props; } export function parse(shorthand) { // TODO(https://github.com/uwdata/compassql/issues/259): // Do not split directly, but use an upgraded version of `getClosingBraceIndex()` let splitShorthand = shorthand.split('|'); let specQ = { mark: splitShorthand[0], encodings: [] }; for (let i = 1; i < splitShorthand.length; i++) { let part = splitShorthand[i]; const splitPart = splitWithTail(part, ':', 1); const splitPartKey = splitPart[0]; const splitPartValue = splitPart[1]; if (isChannel(splitPartKey) || splitPartKey === '?') { const encQ = shorthandParser.encoding(splitPartKey, splitPartValue); specQ.encodings.push(encQ); continue; } if (splitPartKey === 'transform') { specQ.transform = JSON.parse(splitPartValue); continue; } } return specQ; } /** * Split a string n times into substrings with the specified delimiter and return them as an array. * @param str The string to be split * @param delim The delimiter string used to separate the string * @param number The value used to determine how many times the string is split */ export function splitWithTail(str, delim, count) { let result = []; let lastIndex = 0; for (let i = 0; i < count; i++) { let indexOfDelim = str.indexOf(delim, lastIndex); if (indexOfDelim !== -1) { result.push(str.substring(lastIndex, indexOfDelim)); lastIndex = indexOfDelim + 1; } else { break; } } result.push(str.substr(lastIndex)); // If the specified count is greater than the number of delimiters that exist in the string, // an empty string will be pushed count minus number of delimiter occurence times. if (result.length !== count + 1) { while (result.length !== count + 1) { result.push(''); } } return result; } export var shorthandParser; (function (shorthandParser) { function encoding(channel, fieldDefShorthand) { let encQMixins = fieldDefShorthand.indexOf('(') !== -1 ? fn(fieldDefShorthand) : rawFieldDef(splitWithTail(fieldDefShorthand, ',', 2)); return Object.assign({ channel }, encQMixins); } shorthandParser.encoding = encoding; function rawFieldDef(fieldDefPart) { const fieldQ = {}; fieldQ.field = fieldDefPart[0]; fieldQ.type = getFullName(fieldDefPart[1].toUpperCase()) || '?'; let partParams = fieldDefPart[2]; let closingBraceIndex = 0; let i = 0; while (i < partParams.length) { let propEqualSignIndex = partParams.indexOf('=', i); let parsedValue; if (propEqualSignIndex !== -1) { let prop = partParams.substring(i, propEqualSignIndex); if (partParams[i + prop.length + 1] === '{') { let openingBraceIndex = i + prop.length + 1; closingBraceIndex = getClosingIndex(openingBraceIndex, partParams, '}'); const value = partParams.substring(openingBraceIndex, closingBraceIndex + 1); parsedValue = JSON.parse(value); // index after next comma i = closingBraceIndex + 2; } else if (partParams[i + prop.length + 1] === '[') { // find closing square bracket let openingBracketIndex = i + prop.length + 1; let closingBracketIndex = getClosingIndex(openingBracketIndex, partParams, ']'); const value = partParams.substring(openingBracketIndex, closingBracketIndex + 1); parsedValue = JSON.parse(value); // index after next comma i = closingBracketIndex + 2; } else { let propIndex = i; // Substring until the next comma (or end of the string) let nextCommaIndex = partParams.indexOf(',', i + prop.length); if (nextCommaIndex === -1) { nextCommaIndex = partParams.length; } // index after next comma i = nextCommaIndex + 1; parsedValue = JSON.parse(partParams.substring(propIndex + prop.length + 1, nextCommaIndex)); } if (isEncodingNestedParent(prop)) { fieldQ[prop] = parsedValue; } else { // prop is a property of the aggregation function such as bin fieldQ.bin = fieldQ.bin || {}; fieldQ.bin[prop] = parsedValue; } } else { // something is wrong with the format of the partParams // exits loop if don't have then infintie loop break; } } return fieldQ; } shorthandParser.rawFieldDef = rawFieldDef; function getClosingIndex(openingBraceIndex, str, closingChar) { for (let i = openingBraceIndex; i < str.length; i++) { if (str[i] === closingChar) { return i; } } } shorthandParser.getClosingIndex = getClosingIndex; function fn(fieldDefShorthand) { const fieldQ = {}; // Aggregate, Bin, TimeUnit as wildcard case if (fieldDefShorthand[0] === '?') { let closingBraceIndex = getClosingIndex(1, fieldDefShorthand, '}'); let fnEnumIndex = JSON.parse(fieldDefShorthand.substring(1, closingBraceIndex + 1)); for (let encodingProperty in fnEnumIndex) { if (isArray(fnEnumIndex[encodingProperty])) { fieldQ[encodingProperty] = { enum: fnEnumIndex[encodingProperty] }; } else { // Definitely a `SHORT_WILDCARD` fieldQ[encodingProperty] = fnEnumIndex[encodingProperty]; } } return Object.assign({}, fieldQ, rawFieldDef(splitWithTail(fieldDefShorthand.substring(closingBraceIndex + 2, fieldDefShorthand.length - 1), ',', 2))); } else { let func = fieldDefShorthand.substring(0, fieldDefShorthand.indexOf('(')); let insideFn = fieldDefShorthand.substring(func.length + 1, fieldDefShorthand.length - 1); let insideFnParts = splitWithTail(insideFn, ',', 2); if (isAggregateOp(func)) { return Object.assign({ aggregate: func }, rawFieldDef(insideFnParts)); } else if (isTimeUnit(func)) { return Object.assign({ timeUnit: func }, rawFieldDef(insideFnParts)); } else if (func === 'bin') { return Object.assign({ bin: {} }, rawFieldDef(insideFnParts)); } } } shorthandParser.fn = fn; })(shorthandParser || (shorthandParser = {})); //# sourceMappingURL=shorthand.js.map