UNPKG

virool-pivot

Version:

A web-based exploratory visualization UI for Druid.io

873 lines (752 loc) 30.2 kB
import * as Q from 'q'; import { List, OrderedSet } from 'immutable'; import { Class, Instance, isInstanceOf, immutableEqual, immutableArraysEqual, immutableLookupsEqual } from 'immutable-class'; import { Duration, Timezone, minute, second } from 'chronoshift'; import { $, ply, r, Expression, ExpressionJS, Executor, External, DruidExternal, RefExpression, basicExecutorFactory, Dataset, Attributes, AttributeInfo, AttributeJSs, SortAction, SimpleFullType, DatasetFullType, PlyTypeSimple, CustomDruidAggregations, helper } from 'plywood'; import { hasOwnProperty, verifyUrlSafeName, makeUrlSafeName, makeTitle, immutableListsEqual } from '../../utils/general/general'; import { getWallTimeString } from '../../utils/time/time'; import { Dimension, DimensionJS } from '../dimension/dimension'; import { Measure, MeasureJS } from '../measure/measure'; import { Filter, FilterJS } from '../filter/filter'; import { SplitsJS } from '../splits/splits'; import { MaxTime, MaxTimeJS } from '../max-time/max-time'; import { RefreshRule, RefreshRuleJS } from '../refresh-rule/refresh-rule'; function formatTimeDiff(diff: number): string { diff = Math.round(Math.abs(diff) / 1000); // turn to seconds if (diff < 60) return 'less than 1 minute'; diff = Math.floor(diff / 60); // turn to minutes if (diff === 1) return '1 minute'; if (diff < 60) return diff + ' minutes'; diff = Math.floor(diff / 60); // turn to hours if (diff === 1) return '1 hour'; if (diff <= 24) return diff + ' hours'; diff = Math.floor(diff / 24); // turn to days return diff + ' days'; } function makeUniqueDimensionList(dimensions: Dimension[]): List<Dimension> { var seen: Lookup<number> = {}; return List(dimensions.filter((dimension) => { var dimensionName = dimension.name.toLowerCase(); if (seen[dimensionName]) return false; seen[dimensionName] = 1; return true; })); } function makeUniqueMeasureList(measures: Measure[]): List<Measure> { var seen: Lookup<number> = {}; return List(measures.filter((measure) => { var measureName = measure.name.toLowerCase(); if (seen[measureName]) return false; seen[measureName] = 1; return true; })); } export interface DataSourceValue { name: string; title?: string; engine: string; source: string; subsetFilter?: Expression; rollup?: boolean; options?: DataSourceOptions; introspection: string; attributeOverrides: Attributes; attributes: Attributes; derivedAttributes?: Lookup<Expression>; dimensions: List<Dimension>; measures: List<Measure>; timeAttribute: RefExpression; defaultTimezone: Timezone; defaultFilter: Filter; defaultDuration: Duration; defaultSortMeasure: string; defaultPinnedDimensions?: OrderedSet<string>; refreshRule: RefreshRule; maxTime?: MaxTime; external?: External; executor?: Executor; } export interface DataSourceJS { name: string; title?: string; engine: string; source: string; subsetFilter?: ExpressionJS; rollup?: boolean; options?: DataSourceOptions; introspection?: string; attributeOverrides?: AttributeJSs; attributes?: AttributeJSs; derivedAttributes?: Lookup<ExpressionJS>; dimensions?: DimensionJS[]; measures?: MeasureJS[]; timeAttribute?: string; defaultTimezone?: string; defaultFilter?: FilterJS; defaultDuration?: string; defaultSortMeasure?: string; defaultPinnedDimensions?: string[]; refreshRule?: RefreshRuleJS; maxTime?: MaxTimeJS; longForm?: LongForm; } export interface DataSourceOptions { customAggregations?: CustomDruidAggregations; defaultSplits?: SplitsJS; // Deprecated defaultSplitDimension?: string; skipIntrospection?: boolean; disableAutofill?: boolean; attributeOverrides?: AttributeJSs; } export interface DataSourceContext { executor?: Executor; external?: External; } export interface LongForm { metricColumn: string; possibleAggregates: Lookup<any>; addSubsetFilter?: boolean; titleNameTrim?: string; values: LongFormValue[]; } export interface LongFormValue { value: string; aggregates: string[]; } function measuresFromLongForm(longForm: LongForm): Measure[] { var { metricColumn, values, possibleAggregates, titleNameTrim } = longForm; var myPossibleAggregates: Lookup<Expression> = {}; for (var agg in possibleAggregates) { if (!hasOwnProperty(possibleAggregates, agg)) continue; myPossibleAggregates[agg] = Expression.fromJSLoose(possibleAggregates[agg]); } var measures: Measure[] = []; for (var value of values) { var aggregates = value.aggregates; if (!Array.isArray(aggregates)) { throw new Error('must have aggregates in longForm value'); } for (var aggregate of aggregates) { var myExpression = myPossibleAggregates[aggregate]; if (!myExpression) throw new Error(`can not find aggregate ${aggregate} for value ${value.value}`); var name = makeUrlSafeName(`${aggregate}_${value.value}`); measures.push(new Measure({ name, title: makeTitle(titleNameTrim ? name.replace(titleNameTrim, '') : name), expression: myExpression.substitute((ex) => { if (ex instanceof RefExpression && ex.name === 'filtered') { return $('main').filter($(metricColumn).is(r(value.value))); } return null; }) })); } } return measures; } function filterFromLongFrom(longForm: LongForm): Expression { var { metricColumn, values } = longForm; return $(metricColumn).in(values.map(v => v.value)); } var check: Class<DataSourceValue, DataSourceJS>; export class DataSource implements Instance<DataSourceValue, DataSourceJS> { static DEFAULT_INTROSPECTION = 'autofill-all'; static INTROSPECTION_VALUES = ['none', 'no-autofill', 'autofill-dimensions-only', 'autofill-measures-only', 'autofill-all']; static DEFAULT_TIMEZONE = Timezone.UTC; static DEFAULT_DURATION = Duration.fromJS('P1D'); static isDataSource(candidate: any): candidate is DataSource { return isInstanceOf(candidate, DataSource); } static updateMaxTime(dataSource: DataSource): Q.Promise<DataSource> { if (dataSource.refreshRule.isRealtime()) { return Q(dataSource.changeMaxTime(MaxTime.fromNow())); } var ex = ply().apply('maxTime', $('main').max(dataSource.timeAttribute)); return dataSource.executor(ex).then((dataset: Dataset) => { var maxTimeDate = <Date>dataset.data[0]['maxTime']; if (!isNaN(maxTimeDate as any)) { return dataSource.changeMaxTime(MaxTime.fromDate(maxTimeDate)); } return dataSource; }); } static fromJS(parameters: DataSourceJS, context: DataSourceContext = {}): DataSource { const { executor, external } = context; var engine = parameters.engine; var introspection = parameters.introspection; var attributeOverrideJSs = parameters.attributeOverrides; // Back compat. var options = parameters.options || {}; if (options.skipIntrospection) { if (!introspection) introspection = 'none'; delete options.skipIntrospection; } if (options.disableAutofill) { if (!introspection) introspection = 'no-autofill'; delete options.disableAutofill; } if (options.attributeOverrides) { if (!attributeOverrideJSs) attributeOverrideJSs = options.attributeOverrides; delete options.attributeOverrides; } if (options.defaultSplitDimension) { options.defaultSplits = options.defaultSplitDimension; delete options.defaultSplitDimension; } // End Back compat. introspection = introspection || DataSource.DEFAULT_INTROSPECTION; if (DataSource.INTROSPECTION_VALUES.indexOf(introspection) === -1) { throw new Error(`invalid introspection value ${introspection}, must be one of ${DataSource.INTROSPECTION_VALUES.join(', ')}`); } var refreshRule = parameters.refreshRule ? RefreshRule.fromJS(parameters.refreshRule) : RefreshRule.query(); var maxTime = parameters.maxTime ? MaxTime.fromJS(parameters.maxTime) : null; if (!maxTime && refreshRule.isRealtime()) { maxTime = MaxTime.fromNow(); } var timeAttributeName = parameters.timeAttribute; if (engine === 'druid' && !timeAttributeName) { timeAttributeName = '__time'; } var timeAttribute = timeAttributeName ? $(timeAttributeName) : null; var attributeOverrides = AttributeInfo.fromJSs(attributeOverrideJSs || []); var attributes = AttributeInfo.fromJSs(parameters.attributes || []); var derivedAttributes: Lookup<Expression> = null; if (parameters.derivedAttributes) { derivedAttributes = helper.expressionLookupFromJS(parameters.derivedAttributes); } var dimensions = makeUniqueDimensionList((parameters.dimensions || []).map((d) => Dimension.fromJS(d))); var measures = makeUniqueMeasureList((parameters.measures || []).map((m) => Measure.fromJS(m))); if (timeAttribute && !Dimension.getDimensionByExpression(dimensions, timeAttribute)) { dimensions = dimensions.unshift(new Dimension({ name: timeAttributeName, expression: timeAttribute, kind: 'time' })); } var subsetFilter = parameters.subsetFilter ? Expression.fromJSLoose(parameters.subsetFilter) : null; var longForm = parameters.longForm; if (longForm) { measures = measures.concat(measuresFromLongForm(longForm)) as List<Measure>; if (longForm.addSubsetFilter) { if (!subsetFilter) subsetFilter = Expression.TRUE; subsetFilter = subsetFilter.and(filterFromLongFrom(longForm)).simplify(); } } var value: DataSourceValue = { executor: null, name: parameters.name, title: parameters.title, engine, source: parameters.source, subsetFilter, rollup: parameters.rollup, options, introspection, attributeOverrides, attributes, derivedAttributes, dimensions, measures, timeAttribute, defaultTimezone: parameters.defaultTimezone ? Timezone.fromJS(parameters.defaultTimezone) : DataSource.DEFAULT_TIMEZONE, defaultFilter: parameters.defaultFilter ? Filter.fromJS(parameters.defaultFilter) : Filter.EMPTY, defaultDuration: parameters.defaultDuration ? Duration.fromJS(parameters.defaultDuration) : DataSource.DEFAULT_DURATION, defaultSortMeasure: parameters.defaultSortMeasure || (measures.size ? measures.first().name : null), defaultPinnedDimensions: OrderedSet(parameters.defaultPinnedDimensions || []), refreshRule, maxTime }; if (external) value.external = external; if (executor) value.executor = executor; return new DataSource(value); } public name: string; public title: string; public engine: string; public source: string; public subsetFilter: Expression; public rollup: boolean; public options: DataSourceOptions; public introspection: string; public attributes: Attributes; public attributeOverrides: Attributes; public derivedAttributes: Lookup<Expression>; public dimensions: List<Dimension>; public measures: List<Measure>; public timeAttribute: RefExpression; public defaultTimezone: Timezone; public defaultFilter: Filter; public defaultDuration: Duration; public defaultSortMeasure: string; public defaultPinnedDimensions: OrderedSet<string>; public refreshRule: RefreshRule; public maxTime: MaxTime; public executor: Executor; public external: External; constructor(parameters: DataSourceValue) { var name = parameters.name; verifyUrlSafeName(name); this.name = name; this.title = parameters.title || makeTitle(name); this.engine = parameters.engine || 'druid'; this.source = parameters.source || name; this.subsetFilter = parameters.subsetFilter; this.rollup = Boolean(parameters.rollup); this.options = parameters.options || {}; this.introspection = parameters.introspection || DataSource.DEFAULT_INTROSPECTION; this.attributes = parameters.attributes || []; this.attributeOverrides = parameters.attributeOverrides || []; this.derivedAttributes = parameters.derivedAttributes; this.dimensions = parameters.dimensions || List([]); this.measures = parameters.measures || List([]); this.timeAttribute = parameters.timeAttribute; this.defaultTimezone = parameters.defaultTimezone; this.defaultFilter = parameters.defaultFilter; this.defaultDuration = parameters.defaultDuration; this.defaultSortMeasure = parameters.defaultSortMeasure; this.defaultPinnedDimensions = parameters.defaultPinnedDimensions; this.refreshRule = parameters.refreshRule; this.maxTime = parameters.maxTime; this.executor = parameters.executor; this.external = parameters.external; this._validateDefaults(); } public valueOf(): DataSourceValue { var value: DataSourceValue = { name: this.name, title: this.title, engine: this.engine, source: this.source, subsetFilter: this.subsetFilter, rollup: this.rollup, options: this.options, introspection: this.introspection, attributeOverrides: this.attributeOverrides, attributes: this.attributes, derivedAttributes: this.derivedAttributes, dimensions: this.dimensions, measures: this.measures, timeAttribute: this.timeAttribute, defaultTimezone: this.defaultTimezone, defaultFilter: this.defaultFilter, defaultDuration: this.defaultDuration, defaultSortMeasure: this.defaultSortMeasure, defaultPinnedDimensions: this.defaultPinnedDimensions, refreshRule: this.refreshRule, maxTime: this.maxTime }; if (this.executor) value.executor = this.executor; if (this.external) value.external = this.external; return value; } public toJS(): DataSourceJS { var js: DataSourceJS = { name: this.name, title: this.title, engine: this.engine, source: this.source, subsetFilter: this.subsetFilter ? this.subsetFilter.toJS() : null, introspection: this.introspection, dimensions: this.dimensions.toArray().map(dimension => dimension.toJS()), measures: this.measures.toArray().map(measure => measure.toJS()), defaultTimezone: this.defaultTimezone.toJS(), defaultFilter: this.defaultFilter.toJS(), defaultDuration: this.defaultDuration.toJS(), defaultSortMeasure: this.defaultSortMeasure, defaultPinnedDimensions: this.defaultPinnedDimensions.toArray(), refreshRule: this.refreshRule.toJS() }; if (this.rollup) js.rollup = true; if (this.timeAttribute) js.timeAttribute = this.timeAttribute.name; if (this.attributeOverrides.length) js.attributeOverrides = AttributeInfo.toJSs(this.attributeOverrides); if (this.attributes.length) js.attributes = AttributeInfo.toJSs(this.attributes); if (this.derivedAttributes) js.derivedAttributes = helper.expressionLookupToJS(this.derivedAttributes); if (Object.keys(this.options).length) js.options = this.options; if (this.maxTime) js.maxTime = this.maxTime.toJS(); return js; } public toJSON(): DataSourceJS { return this.toJS(); } public toString(): string { return `[DataSource: ${this.name}]`; } public equals(other: DataSource): boolean { return this.equalsWithoutMaxTime(other) && Boolean(this.maxTime) === Boolean(other.maxTime) && (!this.maxTime || this.maxTime.equals(other.maxTime)); } public equalsWithoutMaxTime(other: DataSource): boolean { return DataSource.isDataSource(other) && this.name === other.name && this.title === other.title && this.engine === other.engine && this.source === other.source && immutableEqual(this.subsetFilter, other.subsetFilter) && this.rollup === other.rollup && JSON.stringify(this.options) === JSON.stringify(other.options) && this.introspection === other.introspection && immutableArraysEqual(this.attributeOverrides, other.attributeOverrides) && immutableArraysEqual(this.attributes, other.attributes) && immutableLookupsEqual(this.derivedAttributes, other.derivedAttributes) && immutableListsEqual(this.dimensions, other.dimensions) && immutableListsEqual(this.measures, other.measures) && immutableEqual(this.timeAttribute, other.timeAttribute) && this.defaultTimezone.equals(other.defaultTimezone) && this.defaultFilter.equals(other.defaultFilter) && this.defaultDuration.equals(other.defaultDuration) && this.defaultSortMeasure === other.defaultSortMeasure && this.defaultPinnedDimensions.equals(other.defaultPinnedDimensions) && this.refreshRule.equals(other.refreshRule); } private _validateDefaults() { var { measures, defaultSortMeasure } = this; if (defaultSortMeasure) { if (!measures.find((measure) => measure.name === defaultSortMeasure)) { throw new Error(`can not find defaultSortMeasure '${defaultSortMeasure}' in data source '${this.name}'`); } } } public getMainTypeContext(): DatasetFullType { // ToDo: use external getFullType instead var { attributes, derivedAttributes } = this; if (!attributes) return null; var datasetType: Lookup<SimpleFullType> = {}; for (var attribute of attributes) { datasetType[attribute.name] = (attribute as any); } for (var name in derivedAttributes) { datasetType[name] = { type: <PlyTypeSimple>derivedAttributes[name].type }; } return { type: 'DATASET', datasetType }; } public getIssues(): string[] { var { dimensions, measures } = this; var mainTypeContext = this.getMainTypeContext(); var issues: string[] = []; dimensions.forEach((dimension) => { try { dimension.expression.referenceCheckInTypeContext(mainTypeContext); } catch (e) { issues.push(`failed to validate dimension '${dimension.name}': ${e.message}`); } }); var measureTypeContext: DatasetFullType = { type: 'DATASET', datasetType: { main: mainTypeContext } }; measures.forEach((measure) => { try { measure.expression.referenceCheckInTypeContext(measureTypeContext); } catch (e) { var message = e.message; // If we get here it is possible that the user has misunderstood what the meaning of a measure is and have tried // to do something like $volume / $volume. We detect this here by checking for a reference to $main // If there is no main reference raise a more informative issue. if (measure.expression.getFreeReferences().indexOf('main') === -1) { message = 'measure must contain a $main reference'; } issues.push(`failed to validate measure '${measure.name}': ${message}`); } }); return issues; } public createExternal(requester: Requester.PlywoodRequester<any>, introspectionStrategy: string, timeout: number): DataSource { if (this.engine !== 'druid') return; // Only Druid supported for now. var value = this.valueOf(); var context = { timeout }; if (this.introspection === 'none') { value.external = new DruidExternal({ suppress: true, dataSource: this.source, rollup: this.rollup, timeAttribute: this.timeAttribute.name, customAggregations: this.options.customAggregations, attributes: AttributeInfo.override(this.deduceAttributes(), this.attributeOverrides), derivedAttributes: this.derivedAttributes, introspectionStrategy, filter: this.subsetFilter, allowSelectQueries: true, context, requester }); } else { value.external = new DruidExternal({ suppress: true, dataSource: this.source, rollup: this.rollup, timeAttribute: this.timeAttribute.name, attributeOverrides: this.attributeOverrides, derivedAttributes: this.derivedAttributes, customAggregations: this.options.customAggregations, introspectionStrategy, filter: this.subsetFilter, allowSelectQueries: true, context, requester }); } return new DataSource(value); } public introspect(): Q.Promise<DataSource> { var { external } = this; if (this.engine === 'native') return Q(this); if (!external) throw new Error(`must have external to introspect in ${this.name}`); var countDistinctReferences: string[] = []; if (this.measures) { countDistinctReferences = [].concat.apply([], this.measures.toArray().map((measure) => { return Measure.getCountDistinctReferences(measure.expression); })); } return external.introspect() .then((introspectedExternal) => { if (immutableArraysEqual(external.attributes, introspectedExternal.attributes)) return this; if (!countDistinctReferences) { var attributes = introspectedExternal.attributes; for (var attribute of attributes) { // This is a metric that should really be a HLL if (attribute.type === 'NUMBER' && countDistinctReferences.indexOf(attribute.name) !== -1) { introspectedExternal = introspectedExternal.updateAttribute(AttributeInfo.fromJS({ name: attribute.name, special: 'unique' })); } } } var value = this.addAttributes(introspectedExternal.attributes).valueOf(); value.external = introspectedExternal; value.executor = basicExecutorFactory({ datasets: { main: introspectedExternal } }); return new DataSource(value); }); } public attachExecutor(executor: Executor): DataSource { var value = this.valueOf(); value.executor = executor; return new DataSource(value); } public toClientDataSource(): DataSource { var value = this.valueOf(); // Do not reveal the subset filter to the client value.subsetFilter = null; // No need for any introspection on the client value.introspection = 'none'; // No point sending over the maxTime if (this.refreshRule.isRealtime()) { value.maxTime = null; } // No need for the overrides value.attributeOverrides = null; return new DataSource(value); } public isQueryable(): boolean { return Boolean(this.executor); } public getMaxTimeDate(): Date { var { refreshRule } = this; if (refreshRule.isFixed()) return refreshRule.time; // refreshRule is query or realtime var { maxTime } = this; if (!maxTime) return null; return second.ceil(maxTime.time, Timezone.UTC); } public updatedText(): string { var { refreshRule } = this; if (refreshRule.isRealtime()) { return 'Updated ~1 second ago'; } else if (refreshRule.isFixed()) { return `Fixed to ${getWallTimeString(refreshRule.time, this.defaultTimezone, true)}`; } else { // refreshRule is query var { maxTime } = this; if (maxTime) { return `Updated ${formatTimeDiff(Date.now() - maxTime.time.valueOf())} ago`; } else { return null; } } } public shouldUpdateMaxTime(): boolean { if (!this.refreshRule.shouldUpdate(this.maxTime)) return false; return Boolean(this.executor) || this.refreshRule.isRealtime(); } public getDimension(dimensionName: string): Dimension { return Dimension.getDimension(this.dimensions, dimensionName); } public getDimensionByExpression(expression: Expression): Dimension { return Dimension.getDimensionByExpression(this.dimensions, expression); } public getDimensionByKind(kind: string): List<Dimension> { return <List<Dimension>>this.dimensions.filter((d) => d.kind === kind); } public getTimeDimension() { return this.getDimensionByExpression(this.timeAttribute); } public isTimeAttribute(ex: Expression) { return ex.equals(this.timeAttribute); } public getMeasure(measureName: string): Measure { return Measure.getMeasure(this.measures, measureName); } public getMeasureByExpression(expression: Expression): Measure { return this.measures.find(measure => measure.expression.equals(expression)); } public changeDimensions(dimensions: List<Dimension>): DataSource { var value = this.valueOf(); value.dimensions = dimensions; return new DataSource(value); } public rolledUp(): boolean { return this.engine === 'druid'; } /** * This function tries to deduce the structure of the dataSource based on the dimensions and measures defined within. * It should only be used when, for some reason, introspection if not available. */ public deduceAttributes(): Attributes { const { dimensions, measures, timeAttribute, attributeOverrides } = this; var attributes: Attributes = []; if (timeAttribute) { attributes.push(AttributeInfo.fromJS({ name: timeAttribute.name, type: 'TIME' })); } dimensions.forEach((dimension) => { var expression = dimension.expression; if (expression.equals(timeAttribute)) return; var references = expression.getFreeReferences(); for (var reference of references) { if (helper.findByName(attributes, reference)) continue; attributes.push(AttributeInfo.fromJS({ name: reference, type: 'STRING' })); } }); measures.forEach((measure) => { var expression = measure.expression; var references = Measure.getAggregateReferences(expression); var countDistinctReferences = Measure.getCountDistinctReferences(expression); for (var reference of references) { if (helper.findByName(attributes, reference)) continue; if (countDistinctReferences.indexOf(reference) !== -1) { attributes.push(AttributeInfo.fromJS({ name: reference, special: 'unique' })); } else { attributes.push(AttributeInfo.fromJS({ name: reference, type: 'NUMBER' })); } } }); if (attributeOverrides.length) { attributes = AttributeInfo.override(attributes, attributeOverrides); } return attributes; } public addAttributes(newAttributes: Attributes): DataSource { var { introspection, dimensions, measures, attributes } = this; if (introspection === 'none') return this; var autofillDimensions = introspection === 'autofill-dimensions-only' || introspection === 'autofill-all'; var autofillMeasures = introspection === 'autofill-measures-only' || introspection === 'autofill-all'; var $main = $('main'); for (var newAttribute of newAttributes) { var { name, type, special } = newAttribute; // Already exists if (attributes && helper.findByName(attributes, name)) continue; var expression: Expression; switch (type) { case 'TIME': if (!autofillDimensions) continue; expression = $(name); if (this.getDimensionByExpression(expression)) continue; // Add to the start dimensions = dimensions.unshift(new Dimension({ name: makeUrlSafeName(name), kind: 'time', expression })); break; case 'STRING': if (special === 'unique' || special === 'theta') { if (!autofillMeasures) continue; var newMeasures = Measure.measuresFromAttributeInfo(newAttribute); newMeasures.forEach((newMeasure) => { if (this.getMeasureByExpression(newMeasure.expression)) return; measures = measures.push(newMeasure); }); } else { if (!autofillDimensions) continue; expression = $(name); if (this.getDimensionByExpression(expression)) continue; dimensions = dimensions.push(new Dimension({ name: makeUrlSafeName(name), expression })); } break; case 'SET/STRING': if (!autofillDimensions) continue; expression = $(name); if (this.getDimensionByExpression(expression)) continue; dimensions = dimensions.push(new Dimension({ name: makeUrlSafeName(name), expression })); break; case 'BOOLEAN': if (!autofillDimensions) continue; expression = $(name); if (this.getDimensionByExpression(expression)) continue; dimensions = dimensions.push(new Dimension({ name: makeUrlSafeName(name), kind: 'boolean', expression })); break; case 'NUMBER': if (!autofillMeasures) continue; var newMeasures = Measure.measuresFromAttributeInfo(newAttribute); newMeasures.forEach((newMeasure) => { if (this.getMeasureByExpression(newMeasure.expression)) return; measures = (name === 'count') ? measures.unshift(newMeasure) : measures.push(newMeasure); }); break; default: throw new Error(`unsupported type ${type}`); } } if (!this.rolledUp() && !measures.find(m => m.name === 'count')) { measures = measures.unshift(new Measure({ name: 'count', expression: $main.count() })); } var value = this.valueOf(); value.attributes = attributes ? AttributeInfo.override(attributes, newAttributes) : newAttributes; value.dimensions = dimensions; value.measures = measures; if (!value.defaultSortMeasure) { value.defaultSortMeasure = measures.size ? measures.first().name : null; } if (!value.timeAttribute && dimensions.first().kind === 'time') { value.timeAttribute = <RefExpression>dimensions.first().expression; } return new DataSource(value); } public changeMaxTime(maxTime: MaxTime) { var value = this.valueOf(); value.maxTime = maxTime; return new DataSource(value); } public getDefaultSortAction(): SortAction { return new SortAction({ expression: $(this.defaultSortMeasure), direction: SortAction.DESCENDING }); } } check = DataSource;