import { createAggregateGroupTransform, createCategorizeNestedTransform, createConvertTransform, createFilterTransform, createOperateEachTransform, createOperationTransform, createSortByAttributeTransform, createSortByValueTransform, createSortTransform } from "./transform"; import { createBinModel, createBoxModel, createPieModel, createStackModel } from "./model"; /** * Used to build up an array of transform functions to operate on an array of data. */ export class DataPipeline { transformArray; constructor(transformArray = []) { this.transformArray = transformArray; } /** * Check if there are any data transformations configured. */ get hasTransforms(){ return (this.transformArray.length > 0); } /** * Add the provided transform function to the transformArray * and assign that transform the provided key. * @param transform * @param key */ addTransform(transform, key: string): DataPipeline { transform.key = key; this.transformArray.push(transform); return this; } /** * Get the index in the transformArray of the transform with provided key. * @param key */ getTransformIndex(key: string){ let transform, transformIndex = -1; for (let i = 0; i < this.transformArray.length; i++){ transform = this.transformArray[i]; if (transform.key === key){ transformIndex = i; break; } } return transformIndex; } /** * Check whether a transform with the provided key exists in the transformArray * @param transformationKey */ hasTransformation(transformationKey: string){ return (this.getTransformIndex(transformationKey) >= 0); } /** * Remove the transform, at the provided index, from transformArray. * @param removeIndex */ removeTransformByIndex(removeIndex: number): DataPipeline { if (removeIndex >= 0 && removeIndex < this.transformArray.length){ this.transformArray.splice(removeIndex, 1); } return this; } /** * Remove the transform, with the provided key, from the transformArray. * @param key */ removeTransformByKey(key: string): DataPipeline { const removeIndex = this.getTransformIndex(key); this.removeTransformByIndex(removeIndex); return this; } /** * Add an Aggregate Group transform to the transformArray. * @param groupKeyArray * @param valueKey * @param operation * @param key */ addAggregateGroupTransform(groupKeyArray: string[], valueKey, operation, key = "aggregateGroupTransform"): DataPipeline { const transform = createAggregateGroupTransform(groupKeyArray, valueKey, operation); return this.addTransform(transform, key); } /** * Add a Categorize Nested transform to the transformArray. * @param key */ addCategorizeNestedTransform(key = "categorizeNestedTransform"): DataPipeline { const transform = createCategorizeNestedTransform(); return this.addTransform(transform, key); } /** * Add a Convert transform to the transformArray. * @param conversionMap * @param key */ addConvertTransform(conversionMap: any, key = "convertTransform"): DataPipeline { const transform = createConvertTransform(conversionMap); return this.addTransform(transform, key); } /** * Add a Filter transform to the transformArray. * @param filter * @param key */ addFilterTransform(filter: Function, key = "filterTransform"): DataPipeline { const transform = createFilterTransform(filter); return this.addTransform(transform, key); } /** * Add an Operation transform to the transformArray. * @param operation * @param key */ addOperationTransform(operation: Function, key = "operationTransform"): DataPipeline { const transform = createOperationTransform(operation); return this.addTransform(transform, key); } /** * Add an Operate Each transform to the transformArray. * @param operation * @param key */ addOperateEachTransform(operation: Function, key = "operateEachTransform"): DataPipeline { const transform = createOperateEachTransform(operation); return this.addTransform(transform, key); } /** * Add an Sort transform to the transformArray. * @param comparator * @param key */ addSortTransform(comparator: Function, key = "sortTransform"): DataPipeline { const transform = createSortTransform(comparator); return this.addTransform(transform, key); } /** * Add an Sort By Value transform to the transformArray. * @param valueMap * @param asc * @param key */ addSortByValueTransform(valueMap: Function, asc = true, key = "sortByValueTransform"): DataPipeline { const transform = createSortByValueTransform(valueMap, asc); return this.addTransform(transform, key); } /** * Add an Sort By Attribute transform to the transformArray. * @param attribute * @param asc * @param ignoreCase * @param key */ addSortByAttributeTransform(attribute, config: any, key = "sortByAttributeTransform"): DataPipeline { const transform = createSortByAttributeTransform(attribute, config); return this.addTransform(transform, key); } /** * Apply all transforms, in the transformArray, to each item in the dataArray. * @param dataArray * @param doCopy */ transform(dataArray, doCopy = true){ let dArray, transform; if(!this.hasTransforms){ return dataArray; } if (doCopy) { dArray = JSON.parse(JSON.stringify(dataArray)); // dArray = dataArray.slice(0); } else { dArray = dataArray; } for (transform of this.transformArray){ dArray = transform(dArray); } return dArray; } /** * Apply all transforms to the dataArray and then return a Bin Data Model. * Good for visualizing the data as a Histogram. * @param dataArray * @param valueMap * @param binWidth */ transformBinModel(dataArray, valueMap: Function, binWidth: number){ return createBinModel(this.transform(dataArray), valueMap, binWidth); } /** * Apply all transforms to the dataArray and then return a Box Data Model. * Good for visualizing the data as a Box Plot. * @param dataArray * @param valueMap */ transformBoxModel(dataArray, valueMap: Function){ return createBoxModel(this.transform(dataArray), valueMap); } /** * Apply all transforms to the dataArray and then return a Pie Data Model. * Good for visualizing the data as a Pie Chart. * @param dataArray * @param valueMap */ transformPieModel(dataArray, valueMap: Function){ return createPieModel(this.transform(dataArray), valueMap); } /** * Apply all transforms to the dataArray and then return a Stack Data Model. * Good for visualizing the data as a Stacked Bar Chart. * @param dataArray * @param mapper */ transformStackModel(dataArray, mapper: any){ return createStackModel(this.transform(dataArray), mapper); } /** * Create a clone of this DataPipeline. */ clone(): DataPipeline{ let tArray = this.transformArray.slice(0); return new DataPipeline(tArray); } } /** * Creates a filter function that filters out items in the provided dataArray from the array result * based on the boolean result of passing that item to the provided testOperation function. * @param dataArray * @param testOperation * @param recursiveAttribute * @returns filtered data array */ function recursiveFilter(dataArray: any[], testOperation: Function, recursiveAttribute = "children"){ const filteredDataArray = []; let filteredChildDataArray; for (const data of dataArray){ filteredChildDataArray = []; const childDataArray = data[recursiveAttribute]; if (childDataArray){ filteredChildDataArray = recursiveFilter(childDataArray, testOperation, recursiveAttribute); } if (filteredChildDataArray.length > 0){ filteredDataArray.push(data); data[recursiveAttribute] = filteredChildDataArray; } else if (testOperation(data)){ filteredDataArray.push(data); } } return filteredDataArray; } /** * Creates a filter function that filters out items in the provided dataArray from the array result * based on the boolean result of passing that item to the provided testOperation function. * @param testOperation * @param recursiveAttribute */ function createRecursiveFilter(testOperation: Function, recursiveAttribute = "children"){ let rFilter: any = (dataArray: any[]) => { return recursiveFilter(dataArray, testOperation, recursiveAttribute); }; rFilter.test = testOperation; return rFilter; } /** * Creates a filter function that tests if the lowercase of the filterColumn attribute of an object * includes provided filterValue. * @param filterColumn * @param filterValue */ function createIncludesTest(filterColumn: string, filterValue: string){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (String(value).toLowerCase().includes(filterValue.toLowerCase())); } return doesPass; }; } /** * Creates a filter function that tests if the lowercase of the filterColumn attribute of an object * is equal to the provided filterValue. * @param filterColumn * @param filterValue */ function createEqualsTextIgnoreCaseTest(filterColumn: string, filterValue: string){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (String(value).toLowerCase() === filterValue.toLowerCase()); } return doesPass; }; } /** * Creates a filter function that tests if the filterColumn attribute of an object is strictly equal * to provided filterValue. * @param filterColumn * @param filterValue */ function createEqualsTextStrictTest(filterColumn: string, filterValue: string){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (String(value) === filterValue); } return doesPass; }; } /** * Creates a filter function that tests if the filterColumn attribute of an object is equal * to provided filterValue. * @param filterColumn * @param filterValue */ function createEqualsTest(filterColumn: string, filterValue){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (value == filterValue); } return doesPass; }; } /** * Creates a filter function that tests if the filterColumn attribute of an object is strictly equal * to provided filterValue. * @param filterColumn * @param filterValue */ function createEqualsStrictTest(filterColumn: string, filterValue){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (value === filterValue); } return doesPass; }; } /** * Creates a filter function that tests if the filterColumn attribute of an object is greater or equal to minRange. * @param filterColumn * @param minRange */ function createMinRangeTest(filterColumn: string, minRange: number){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (value >= minRange); } return doesPass; }; } /** * Creates a filter function that tests if the filterColumn attribute of an object is less than or equal to maxRange. * @param filterColumn * @param maxRange */ function createMaxRangeTest(filterColumn: string, maxRange: number){ return (d) => { let doesPass = false; const value = d[filterColumn]; if (value !== undefined && value !== null){ doesPass = (value <= maxRange); } return doesPass; }; } function createMultipleTest(filter){ return (data) => { let match = false; for(let f of filter.filters){ let columnValue = data[f.column]; if(columnValue && f.value !== undefined && f.value !== null && f.value !== ""){ switch(f.type){ case "includes": match = match || columnValue.toLowerCase().includes(f.value.toLowerCase()); break; case "equalsTextStrict": match = match || String(columnValue) === f.value; break; case "equalsTextIgnoreCase": match = match || String(columnValue).toLowerCase() === f.value.toLowerCase(); break; } } } return match; }; } /** * Produces a DataPipeline with transforms based on the provided object. * @param transforms - object that contains transform directives for resulting DataPipeline * transforms.order - a sort transform config * transforms.order.method - the sort method (ASC, DESC) * transforms.order.column - attribute to sort by * transforms.order.ignoreCase - whether to be case insensitive when sorting column * transforms.filters - contains a list of filter transform config * transforms.filters.${filterName} - a filter transform config * transforms.filters.${filterName}.type - the type of filter ("categorical", "range") * transforms.filters.${filterName}.value - the value to filter by (used by categorical type filter) * transforms.filters.${filterName}.minRange - filter out values less than this value (used by range type filter) * transforms.filters.${filterName}.maxRange - filter out values greater than this value (used by range type filter) * @returns DataPipeline * @example * */ export function getTransformsDataPipeline(transforms: any): DataPipeline { const dataPipeline = new DataPipeline(); let isAsc, key, filter ; for (key in transforms.filters) { if (transforms.filters.hasOwnProperty(key)) { filter = transforms.filters[key]; if (filter){ if (filter.type){ switch (filter.type){ case "categorical": case "includes": if (filter.value !== undefined && filter.value !== null && filter.value !== ""){ const includesTest = createIncludesTest(filter.column, filter.value); const includesFilter = createRecursiveFilter(includesTest); dataPipeline.addOperationTransform(includesFilter, filter.column + "Filter"); } break; case "equalsTextIgnoreCase": if (filter.value !== undefined && filter.value !== null && filter.value !== ""){ const equalsIgnoreCaseTest = createEqualsTextIgnoreCaseTest(filter.column, filter.value); const equalsIgnoreCaseFilter = createRecursiveFilter(equalsIgnoreCaseTest); dataPipeline.addOperationTransform(equalsIgnoreCaseFilter, filter.column + "Filter"); } break; case "equalsTextStrict": if (filter.value !== undefined && filter.value !== null && filter.value !== ""){ const strictTest = createEqualsTextStrictTest(filter.column, filter.value); const strictFilter = createRecursiveFilter(strictTest); dataPipeline.addOperationTransform(strictFilter, filter.column + "Filter"); } break; case "equals": if (filter.value !== undefined && filter.value !== null && filter.value !== ""){ const equalsTest = createEqualsTest(filter.column, filter.value); const equalsFilter = createRecursiveFilter(equalsTest); dataPipeline.addOperationTransform(equalsFilter, filter.column + "Filter"); } break; case "equalsStrict": if (filter.value !== undefined && filter.value !== null && filter.value !== ""){ const equalsStrictTest = createEqualsStrictTest(filter.column, filter.value); const equalsStrictFilter = createRecursiveFilter(equalsStrictTest); dataPipeline.addOperationTransform(equalsStrictFilter, filter.column + "Filter"); } break; case "range": if (filter.minRange !== undefined){ const minTest = createMinRangeTest(filter.column, filter.minRange); const minRangeFilter = createRecursiveFilter(minTest); dataPipeline.addOperationTransform(minRangeFilter, filter.column + "MinFilter"); } if (filter.maxRange !== undefined){ const maxTest = createMaxRangeTest(filter.column, filter.maxRange); const maxRangeFilter = createRecursiveFilter(maxTest); dataPipeline.addOperationTransform(maxRangeFilter, filter.column + "MaxFilter"); } break; case "or": if (filter.filters){ for(let f of filter.filters) { if (f.value !== undefined && f.value !== null && f.value !== "") { const filterTest = createMultipleTest(filter); const recursiveFilter = createRecursiveFilter(filterTest); dataPipeline.addOperationTransform(recursiveFilter, key + "Filter"); break; } } } break; default: break; } } else { const includesFilter = createIncludesTest(filter.column, filter.value); dataPipeline.addFilterTransform(includesFilter, filter.column + "Filter"); } } } } if (transforms.order) { isAsc = (transforms.order.method === "ASC"); dataPipeline.addSortByAttributeTransform(transforms.order.column, { asc: isAsc, ignoreCase: transforms.order.ignoreCase, emptyLocation: transforms.order.emptyLocation }); } return dataPipeline; }