UNPKG

universe

Version:

The fastest way to query and explore multivariate datasets

383 lines (347 loc) 11.5 kB
'use strict' var Promise = require('q') var _ = require('./lodash') var expressions = require('./expressions') var aggregation = require('./aggregation') module.exports = function (service) { return { filter: filter, filterAll: filterAll, applyFilters: applyFilters, makeFunction: makeFunction, scanForDynamicFilters: scanForDynamicFilters, } function filter(column, fil, isRange, replace) { return getColumn(column) .then(function (column) { // Clone a copy of the new filters var newFilters = _.assign({}, service.filters) // Here we use the registered column key despite the filter key passed, just in case the filter key's ordering is ordered differently :) var filterKey = column.key if (column.complex === 'array') { filterKey = JSON.stringify(column.key) } if (column.complex === 'function') { filterKey = column.key.toString() } // Build the filter object newFilters[filterKey] = buildFilterObject(fil, isRange, replace) return applyFilters(newFilters) }) } function getColumn(column) { var exists = service.column.find(column) // If the filters dimension doesn't exist yet, try and create it return Promise.try(function () { if (!exists) { return service.column({ key: column, temporary: true, }) .then(function () { // It was able to be created, so retrieve and return it return service.column.find(column) }) } // It exists, so just return what we found return exists }) } function filterAll(fils) { // If empty, remove all filters if (!fils) { service.columns.forEach(function (col) { col.dimension.filterAll() }) return applyFilters({}) } // Clone a copy for the new filters var newFilters = _.assign({}, service.filters) var ds = _.map(fils, function (fil) { return getColumn(fil.column) .then(function (column) { // Here we use the registered column key despite the filter key passed, just in case the filter key's ordering is ordered differently :) var filterKey = column.complex ? JSON.stringify(column.key) : column.key // Build the filter object newFilters[filterKey] = buildFilterObject(fil.value, fil.isRange, fil.replace) }) }) return Promise.all(ds) .then(function () { return applyFilters(newFilters) }) } function buildFilterObject(fil, isRange, replace) { if (_.isUndefined(fil)) { return false } if (_.isFunction(fil)) { return { value: fil, function: fil, replace: true, type: 'function', } } if (_.isObject(fil)) { return { value: fil, function: makeFunction(fil), replace: true, type: 'function', } } if (_.isArray(fil)) { return { value: fil, replace: isRange || replace, type: isRange ? 'range' : 'inclusive', } } return { value: fil, replace: replace, type: 'exact', } } function applyFilters(newFilters) { var ds = _.map(newFilters, function (fil, i) { var existing = service.filters[i] // Filters are the same, so no change is needed on this column if (fil === existing) { return Promise.resolve() } var column // Retrieve complex columns by decoding the column key as json if (i.charAt(0) === '[') { column = service.column.find(JSON.parse(i)) } else { // Retrieve the column normally column = service.column.find(i) } // Toggling a filter value is a bit different from replacing them if (fil && existing && !fil.replace) { newFilters[i] = fil = toggleFilters(fil, existing) } // If no filter, remove everything from the dimension if (!fil) { return Promise.resolve(column.dimension.filterAll()) } if (fil.type === 'exact') { return Promise.resolve(column.dimension.filterExact(fil.value)) } if (fil.type === 'range') { return Promise.resolve(column.dimension.filterRange(fil.value)) } if (fil.type === 'inclusive') { return Promise.resolve(column.dimension.filterFunction(function (d) { return fil.value.indexOf(d) > -1 })) } if (fil.type === 'function') { return Promise.resolve(column.dimension.filterFunction(fil.function)) } // By default if something craps up, just remove all filters return Promise.resolve(column.dimension.filterAll()) }) return Promise.all(ds) .then(function () { // Save the new filters satate service.filters = newFilters // Pluck and remove falsey filters from the mix var tryRemoval = [] _.forEach(service.filters, function (val, key) { if (!val) { tryRemoval.push({ key: key, val: val, }) delete service.filters[key] } }) // If any of those filters are the last dependency for the column, then remove the column return Promise.all(_.map(tryRemoval, function (v) { var column = service.column.find((v.key.charAt(0) === '[') ? JSON.parse(v.key) : v.key) if (column.temporary && !column.dynamicReference) { return service.clear(column.key) } })) }) .then(function () { // Call the filterListeners and wait for their return return Promise.all(_.map(service.filterListeners, function (listener) { return listener() })) }) .then(function () { return service }) } function toggleFilters(fil, existing) { // Exact from Inclusive if (fil.type === 'exact' && existing.type === 'inclusive') { fil.value = _.xor([fil.value], existing.value) } else if (fil.type === 'inclusive' && existing.type === 'exact') { // Inclusive from Exact fil.value = _.xor(fil.value, [existing.value]) } else if (fil.type === 'inclusive' && existing.type === 'inclusive') { // Inclusive / Inclusive Merge fil.value = _.xor(fil.value, existing.value) } else if (fil.type === 'exact' && existing.type === 'exact') { // Exact / Exact // If the values are the same, remove the filter entirely if (fil.value === existing.value) { return false } // They they are different, make an array fil.value = [fil.value, existing.value] } // Set the new type based on the merged values if (!fil.value.length) { fil = false } else if (fil.value.length === 1) { fil.type = 'exact' fil.value = fil.value[0] } else { fil.type = 'inclusive' } return fil } function scanForDynamicFilters(query) { // Here we check to see if there are any relative references to the raw data // being used in the filter. If so, we need to build those dimensions and keep // them updated so the filters can be rebuilt if needed // The supported keys right now are: $column, $data var columns = [] walk(query.filter) return columns function walk(obj) { _.forEach(obj, function (val, key) { // find the data references, if any var ref = findDataReferences(val, key) if (ref) { columns.push(ref) } // if it's a string if (_.isString(val)) { ref = findDataReferences(null, val) if (ref) { columns.push(ref) } } // If it's another object, keep looking if (_.isObject(val)) { walk(val) } }) } } function findDataReferences(val, key) { // look for the $data string as a value if (key === '$data') { return true } // look for the $column key and it's value as a string if (key && key === '$column') { if (_.isString(val)) { return val } console.warn('The value for filter "$column" must be a valid column key', val) return false } } function makeFunction(obj, isAggregation) { var subGetters // Detect raw $data reference if (_.isString(obj)) { var dataRef = findDataReferences(null, obj) if (dataRef) { var data = service.cf.all() return function () { return data } } } if (_.isString(obj) || _.isNumber(obj) || _.isBoolean(obj)) { return function (d) { if (typeof d === 'undefined') { return obj } return expressions.$eq(d, function () { return obj }) } } // If an array, recurse into each item and return as a map if (_.isArray(obj)) { subGetters = _.map(obj, function (o) { return makeFunction(o, isAggregation) }) return function (d) { return subGetters.map(function (s) { return s(d) }) } } // If object, return a recursion function that itself, returns the results of all of the object keys if (_.isObject(obj)) { subGetters = _.map(obj, function (val, key) { // Get the child var getSub = makeFunction(val, isAggregation) // Detect raw $column references var dataRef = findDataReferences(val, key) if (dataRef) { var column = service.column.find(dataRef) var data = column.values return function () { return data } } // If expression, pass the parentValue and the subGetter if (expressions[key]) { return function (d) { return expressions[key](d, getSub) } } var aggregatorObj = aggregation.parseAggregatorParams(key) if (aggregatorObj) { // Make sure that any further operations are for aggregations // and not filters isAggregation = true // here we pass true to makeFunction which denotes that // an aggregatino chain has started and to stop using $AND getSub = makeFunction(val, isAggregation) // If it's an aggregation object, be sure to pass in the children, and then any additional params passed into the aggregation string return function () { return aggregatorObj.aggregator.apply(null, [getSub()].concat(aggregatorObj.params)) } } // It must be a string then. Pluck that string key from parent, and pass it as the new value to the subGetter return function (d) { d = d[key] return getSub(d, getSub) } }) // All object expressions are basically AND's // Return AND with a map of the subGetters if (isAggregation) { if (subGetters.length === 1) { return function (d) { return subGetters[0](d) } } return function (d) { return _.map(subGetters, function (getSub) { return getSub(d) }) } } return function (d) { return expressions.$and(d, function (d) { return _.map(subGetters, function (getSub) { return getSub(d) }) }) } } console.log('no expression found for ', obj) return false } }