You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2017/04/14 22:34:36 UTC

[2/3] zeppelin git commit: [ZEPPELIN-2217] AdvancedTransformation for Visualization

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/src/app/tabledata/advanced-transformation-util.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js
new file mode 100644
index 0000000..0bcefb6
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js
@@ -0,0 +1,1259 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export function getCurrentChart(config) {
+  return config.chart.current;
+}
+
+export function getCurrentChartTransform(config) {
+  return config.spec.charts[getCurrentChart(config)].transform
+}
+
+export function getCurrentChartAxis(config) {
+  return config.axis[getCurrentChart(config)]
+}
+
+export function getCurrentChartParam(config) {
+  return config.parameter[getCurrentChart(config)]
+}
+
+export function getCurrentChartAxisSpecs(config) {
+  return config.axisSpecs[getCurrentChart(config)]
+}
+
+export function getCurrentChartParamSpecs(config) {
+  return config.paramSpecs[getCurrentChart(config)]
+}
+
+export function useSharedAxis(config, chart) {
+  return config.spec.charts[chart].sharedAxis
+}
+
+export function serializeSharedAxes(config) {
+  const availableCharts = getAvailableChartNames(config.spec.charts)
+  for (let i = 0; i < availableCharts.length; i++) {
+    const chartName = availableCharts[i];
+    if (useSharedAxis(config, chartName)) {
+      /** use reference :) in case of sharedAxis */
+      config.axis[chartName] = config.sharedAxis
+    }
+  }
+}
+
+export const Widget = {
+  INPUT: 'input', /** default */
+  OPTION: 'option',
+  CHECKBOX: 'checkbox',
+  TEXTAREA: 'textarea',
+}
+
+export function isInputWidget(paramSpec) {
+  return (paramSpec && !paramSpec.widget) || (paramSpec && paramSpec.widget === Widget.INPUT);
+}
+
+export function isOptionWidget(paramSpec) {
+  return paramSpec && paramSpec.widget === Widget.OPTION;
+}
+
+export function isCheckboxWidget(paramSpec) {
+  return paramSpec && paramSpec.widget === Widget.CHECKBOX;
+}
+
+export function isTextareaWidget(paramSpec) {
+  return paramSpec && paramSpec.widget === Widget.TEXTAREA;
+}
+
+export const ParameterValueType = {
+  INT: 'int',
+  FLOAT: 'float',
+  BOOLEAN: 'boolean',
+  STRING: 'string',
+  JSON: 'JSON',
+}
+
+export function parseParameter(paramSpecs, param) {
+  /** copy original params */
+  const parsed = JSON.parse(JSON.stringify(param))
+
+  for (let i = 0 ; i < paramSpecs.length; i++) {
+    const paramSpec = paramSpecs[i]
+    const name = paramSpec.name
+
+    if (paramSpec.valueType === ParameterValueType.INT &&
+      typeof parsed[name] !== 'number') {
+
+      try { parsed[name] = parseInt(parsed[name]); }
+      catch (error) { parsed[name] = paramSpec.defaultValue; }
+    }
+    else if (paramSpec.valueType === ParameterValueType.FLOAT &&
+      typeof parsed[name] !== 'number') {
+
+      try { parsed[name] = parseFloat(parsed[name]); }
+      catch (error) { parsed[name] = paramSpec.defaultValue; }
+    }
+    else if (paramSpec.valueType === ParameterValueType.BOOLEAN) {
+      if (parsed[name] === 'false') {
+        parsed[name] = false;
+      } else if (parsed[name] === 'true') {
+        parsed[name] = true;
+      } else if (typeof parsed[name] !== 'boolean') {
+        parsed[name] = paramSpec.defaultValue;
+      }
+    }
+    else if (paramSpec.valueType === ParameterValueType.JSON) {
+      if (parsed[name] !== null && typeof parsed[name] !== 'object') {
+        try { parsed[name] = JSON.parse(parsed[name]); }
+        catch (error) { parsed[name] = paramSpec.defaultValue; }
+      } else if (parsed[name] === null) {
+        parsed[name] = paramSpec.defaultValue;
+      }
+    }
+  }
+
+  return parsed
+}
+
+export const AxisType = {
+  AGGREGATOR: 'aggregator',
+  KEY: 'key',
+  GROUP: 'group',
+}
+
+export function isAggregatorAxis(axisSpec) {
+  return axisSpec && axisSpec.axisType === AxisType.AGGREGATOR
+}
+export function isGroupAxis(axisSpec) {
+  return axisSpec && axisSpec.axisType === AxisType.GROUP
+}
+export function isKeyAxis(axisSpec) {
+  return axisSpec && axisSpec.axisType === AxisType.KEY
+}
+export function isSingleDimensionAxis(axisSpec) {
+  return axisSpec && axisSpec.dimension === 'single'
+}
+
+/**
+ * before: { name: { ... } }
+ * after: [ { name, ... } ]
+ *
+ * add the `name` field while converting to array to easily manipulate
+ */
+export function getSpecs(specObject) {
+  const specs = [];
+  for (let name in specObject) {
+    const singleSpec = specObject[name];
+    if (!singleSpec) { continue }
+    singleSpec.name = name;
+    specs.push(singleSpec);
+  }
+
+  return specs
+}
+
+export function getAvailableChartNames(charts) {
+  const available = []
+  for (var name in charts) {
+    available.push(name)
+  }
+
+  return available
+}
+
+export function applyMaxAxisCount(config, axisSpec) {
+  if (isSingleDimensionAxis(axisSpec) || typeof axisSpec.maxAxisCount === 'undefined') {
+    return;
+  }
+
+  const columns = getCurrentChartAxis(config)[axisSpec.name]
+  if (columns.length <= axisSpec.maxAxisCount) { return; }
+
+  const sliced = columns.slice(1)
+  getCurrentChartAxis(config)[axisSpec.name] = sliced
+}
+
+export function removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpec) {
+  if (isSingleDimensionAxis(axisSpec)) { return config }
+
+  const columns = getCurrentChartAxis(config)[axisSpec.name]
+  const uniqObject = columns.reduce((acc, col) => {
+    if (!acc[`${col.name}(${col.aggr})`]) { acc[`${col.name}(${col.aggr})`] = col }
+    return acc
+  }, {})
+
+  const filtered = []
+  for (let name in uniqObject) {
+    const col = uniqObject[name]
+    filtered.push(col)
+  }
+
+  getCurrentChartAxis(config)[axisSpec.name] = filtered
+  return config
+}
+
+export function clearAxisConfig(config) {
+  delete config.axis /** Object: persisted axis for each chart */
+  delete config.sharedAxis
+}
+
+export function initAxisConfig(config) {
+  if (!config.axis) { config.axis = {} }
+  if (!config.sharedAxis) { config.sharedAxis = {} }
+
+  const spec = config.spec
+  const availableCharts = getAvailableChartNames(spec.charts)
+
+  if (!config.axisSpecs) { config.axisSpecs = {}; }
+  for (let i = 0; i < availableCharts.length; i++) {
+    const chartName = availableCharts[i];
+
+    if (!config.axis[chartName]) {
+      config.axis[chartName] = {};
+    }
+    const axisSpecs = getSpecs(spec.charts[chartName].axis)
+    if (!config.axisSpecs[chartName]) {
+      config.axisSpecs[chartName] = axisSpecs;
+    }
+
+    /** initialize multi-dimension axes */
+    for (let i = 0; i < axisSpecs.length; i++) {
+      const axisSpec = axisSpecs[i]
+      if (isSingleDimensionAxis(axisSpec)) {
+        continue;
+      }
+
+      /** intentionally nested if-stmt is used because order of conditions matter here */
+      if (!useSharedAxis(config, chartName)) {
+        if (!Array.isArray(config.axis[chartName][axisSpec.name])) {
+          config.axis[chartName][axisSpec.name] = []
+        }
+      } else if (useSharedAxis(config, chartName)) {
+        /**
+         * initialize multiple times even if shared axis because it's not that expensive, assuming that
+         * all charts using shared axis have the same axis specs
+         */
+        if (!Array.isArray(config.sharedAxis[axisSpec.name])) {
+          config.sharedAxis[axisSpec.name] = []
+        }
+      }
+    }
+  }
+
+  /** this function should be called after initializing */
+  serializeSharedAxes(config)
+}
+
+export function resetAxisConfig(config) {
+  clearAxisConfig(config)
+  initAxisConfig(config)
+}
+
+export function clearParameterConfig(config) {
+  delete config.parameter  /** Object: persisted parameter for each chart */
+}
+
+export function initParameterConfig(config) {
+  if (!config.parameter) { config.parameter = {} }
+
+  const spec = config.spec
+  const availableCharts = getAvailableChartNames(spec.charts)
+
+  if (!config.paramSpecs) { config.paramSpecs = {}; }
+  for (let i = 0; i < availableCharts.length; i++) {
+    const chartName = availableCharts[i];
+
+    if (!config.parameter[chartName]) { config.parameter[chartName] = {}; }
+    const paramSpecs = getSpecs(spec.charts[chartName].parameter)
+    if (!config.paramSpecs[chartName]) { config.paramSpecs[chartName] = paramSpecs; }
+
+    for (let i = 0; i < paramSpecs.length; i++) {
+      const paramSpec = paramSpecs[i];
+      if (!config.parameter[chartName][paramSpec.name]) {
+        config.parameter[chartName][paramSpec.name] = paramSpec.defaultValue;
+      }
+    }
+  }
+}
+
+export function resetParameterConfig(config) {
+  clearParameterConfig(config)
+  initParameterConfig(config)
+}
+
+export function getSpecVersion(availableCharts, spec) {
+  const axisHash = {}
+  const paramHash = {}
+
+  for (let i = 0; i < availableCharts.length; i++) {
+    const chartName = availableCharts[i];
+    const axisSpecs = getSpecs(spec.charts[chartName].axis)
+    axisHash[chartName] = axisSpecs
+
+    const paramSpecs = getSpecs(spec.charts[chartName].parameter)
+    paramHash[chartName] = paramSpecs
+  }
+
+  return { axisVersion: JSON.stringify(axisHash), paramVersion: JSON.stringify(paramHash), }
+}
+
+export function initializeConfig(config, spec) {
+  config.chartChanged = true
+  config.parameterChanged = false
+
+  let updated = false
+
+  const availableCharts = getAvailableChartNames(spec.charts)
+  const { axisVersion, paramVersion, } = getSpecVersion(availableCharts, spec)
+
+  if (!config.spec || !config.spec.version ||
+    !config.spec.version.axis ||
+    config.spec.version.axis !== axisVersion) {
+
+    spec.initialized = true
+    updated = true
+
+    delete config.chart      /** Object: contains current, available chart */
+    config.panel = { columnPanelOpened: true, parameterPanelOpened: false, }
+
+    clearAxisConfig(config)
+    delete config.axisSpecs  /** Object: persisted axisSpecs for each chart */
+  }
+
+  if (!config.spec || !config.spec.version ||
+    !config.spec.version.parameter ||
+    config.spec.version.parameter !== paramVersion) {
+
+    updated = true
+
+    clearParameterConfig(config)
+    delete config.paramSpecs /** Object: persisted paramSpecs for each chart */
+  }
+
+  if (!spec.version) { spec.version = {} }
+  spec.version.axis = axisVersion
+  spec.version.parameter = paramVersion
+
+  if (!config.spec || updated) { config.spec = spec; }
+
+  if (!config.chart) {
+    config.chart = {};
+    config.chart.current = availableCharts[0];
+    config.chart.available = availableCharts;
+  }
+
+  /** initialize config.axis, config.axisSpecs for each chart */
+  initAxisConfig(config)
+
+  /** initialize config.parameter for each chart */
+  initParameterConfig(config)
+  return config
+}
+
+export function getColumnsForMultipleAxes(axisType, axisSpecs, axis) {
+  const axisNames = []
+  let column = {}
+
+  for(let i = 0; i < axisSpecs.length; i++) {
+    const axisSpec = axisSpecs[i];
+
+    if (axisType === AxisType.KEY && isKeyAxis(axisSpec)) {
+      axisNames.push(axisSpec.name)
+    } else if (axisType === AxisType.GROUP && isGroupAxis(axisSpec)) {
+      axisNames.push(axisSpec.name)
+    } else if (axisType.AGGREGATOR && isAggregatorAxis(axisSpec)) {
+      axisNames.push(axisSpec.name)
+    }
+  }
+
+  for(let axisName of axisNames) {
+    const columns = axis[axisName];
+    if (typeof axis[axisName] === 'undefined') { continue }
+    if (!column[axisName]) { column[axisName] = [] }
+    column[axisName] = column[axisName].concat(columns)
+  }
+
+  return column
+}
+
+export function getColumnsFromAxis(axisSpecs, axis) {
+  const keyAxisNames = [];
+  const groupAxisNames = [];
+  const aggrAxisNames = [];
+
+  for(let i = 0; i < axisSpecs.length; i++) {
+    const axisSpec = axisSpecs[i];
+
+    if (isKeyAxis(axisSpec)) { keyAxisNames.push(axisSpec.name); }
+    else if (isGroupAxis(axisSpec)) { groupAxisNames.push(axisSpec.name); }
+    else if (isAggregatorAxis(axisSpec)) { aggrAxisNames.push(axisSpec.name); }
+  }
+
+  let keyColumns = [];
+  let groupColumns = [];
+  let aggregatorColumns = [];
+  let customColumn = {};
+
+  for(let axisName in axis) {
+    const columns = axis[axisName];
+    if (keyAxisNames.includes(axisName)) {
+      keyColumns = keyColumns.concat(columns);
+    } else if (groupAxisNames.includes(axisName)) {
+      groupColumns = groupColumns.concat(columns);
+    } else if (aggrAxisNames.includes(axisName)) {
+      aggregatorColumns = aggregatorColumns.concat(columns);
+    } else {
+      const axisType = axisSpecs.filter(s => s.name === axisName)[0].axisType
+      if (!customColumn[axisType]) { customColumn[axisType] = []; }
+      customColumn[axisType] = customColumn[axisType].concat(columns);
+    }
+  }
+
+  return {
+    key: keyColumns,
+    group: groupColumns,
+    aggregator: aggregatorColumns,
+    custom: customColumn,
+  }
+}
+
+export const Aggregator = {
+  SUM: 'sum',
+  COUNT: 'count',
+  AVG: 'avg',
+  MIN: 'min',
+  MAX: 'max',
+}
+
+const TransformMethod = {
+  /**
+   * `raw` is designed for users who want to get raw (original) rows.
+   */
+  RAW: 'raw',
+  /**
+   * `object` is * designed for serial(line, area) charts.
+   */
+  OBJECT: 'object',
+  /**
+   * `array` is designed for column, pie charts which have categorical `key` values.
+   * But some libraries may require `OBJECT` transform method even if pie, column charts.
+   *
+   * `row.value` will be filled for `keyNames`.
+   * In other words, if you have `keyNames` which is length 4,
+   * every `row.value`'s length will be 4 too.
+   * (DO NOT use this transform method for serial (numerical) x axis charts which have so-oo many keys)
+   */
+  ARRAY: 'array',
+  ARRAY_2_KEY: 'array:2-key',
+  DRILL_DOWN: 'drill-down',
+}
+
+/** return function for lazy computation */
+export function getTransformer(conf, rows, axisSpecs, axis) {
+  let transformer = () => {}
+
+  const transformSpec = getCurrentChartTransform(conf)
+  if (!transformSpec) { return transformer }
+
+  const method = transformSpec.method
+
+  const columns = getColumnsFromAxis(axisSpecs, axis);
+  const keyColumns = columns.key;
+  const groupColumns = columns.group;
+  const aggregatorColumns = columns.aggregator;
+  const customColumns = columns.custom
+
+  let column = {
+    key: keyColumns, group: groupColumns, aggregator: aggregatorColumns, custom: customColumns,
+  }
+
+  if (method === TransformMethod.RAW) {
+    transformer = () => { return rows; }
+  } else if (method === TransformMethod.OBJECT) {
+    transformer = () => {
+      const { cube, schema, keyColumnName,  keyNames, groupNameSet, selectorNameWithIndex, } =
+        getKGACube(rows, keyColumns, groupColumns, aggregatorColumns)
+
+      const {
+        transformed, groupNames, sortedSelectors
+      } = getObjectRowsFromKGACube(cube, schema, aggregatorColumns,
+        keyColumnName, keyNames, groupNameSet, selectorNameWithIndex)
+
+      return {
+        rows: transformed, keyColumnName,
+        keyNames,
+        groupNames: groupNames,
+        selectors: sortedSelectors,
+      }
+    }
+  } else if (method === TransformMethod.ARRAY) {
+    transformer = () => {
+      const { cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex, } =
+        getKGACube(rows, keyColumns, groupColumns, aggregatorColumns)
+
+      const {
+        transformed, groupNames, sortedSelectors,
+      } = getArrayRowsFromKGACube(cube, schema, aggregatorColumns,
+        keyColumnName, keyNames, groupNameSet, selectorNameWithIndex)
+
+      return {
+        rows: transformed, keyColumnName,
+        keyNames,
+        groupNames: groupNames,
+        selectors: sortedSelectors,
+      }
+    }
+  } else if (method === TransformMethod.ARRAY_2_KEY) {
+    const keyAxisColumn = getColumnsForMultipleAxes(AxisType.KEY, axisSpecs, axis)
+    column.key = keyAxisColumn
+
+    let key1Columns = []
+    let key2Columns = []
+
+    // since ARRAY_2_KEY :)
+    let i = 0
+    for (let axisName in keyAxisColumn) {
+      if (i === 2) { break }
+
+      if (i === 0) { key1Columns = keyAxisColumn[axisName] }
+      else if (i === 1) { key2Columns  = keyAxisColumn[axisName] }
+      i++
+    }
+
+    const { cube, schema,
+      key1ColumnName, key1Names, key2ColumnName, key2Names,
+      groupNameSet, selectorNameWithIndex,
+    } = getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggregatorColumns)
+
+    const {
+      transformed, groupNames, sortedSelectors,
+      key1NameWithIndex, key2NameWithIndex,
+    } = getArrayRowsFromKKGACube(cube, schema, aggregatorColumns,
+      key1Names, key2Names, groupNameSet, selectorNameWithIndex)
+
+    transformer = () => {
+      return {
+        rows: transformed,
+        key1Names: key1Names,
+        key1ColumnName: key1ColumnName,
+        key1NameWithIndex: key1NameWithIndex,
+        key2Names: key2Names,
+        key2ColumnName: key2ColumnName,
+        key2NameWithIndex: key2NameWithIndex,
+        groupNames: groupNames,
+        selectors: sortedSelectors,
+      }
+    }
+  }
+  else if (method === TransformMethod.DRILL_DOWN) {
+    transformer = () => {
+      const { cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex, } =
+        getKAGCube(rows, keyColumns, groupColumns, aggregatorColumns)
+
+      const {
+        transformed, groupNames, sortedSelectors,
+      } = getDrilldownRowsFromKAGCube(cube, schema, aggregatorColumns,
+        keyColumnName, keyNames, groupNameSet, selectorNameWithIndex)
+
+      return {
+        rows: transformed, keyColumnName, keyNames,
+        groupNames: groupNames,
+        selectors: sortedSelectors,
+      }
+    }
+  }
+
+  return { transformer: transformer, column: column, }
+}
+
+const AggregatorFunctions = {
+  sum: function(a, b) {
+    const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
+    const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
+    return varA + varB;
+  },
+  count: function(a, b) {
+    const varA = (a !== undefined) ? parseInt(a) : 0;
+    const varB = (b !== undefined) ? 1 : 0;
+    return varA + varB;
+  },
+  min: function(a, b) {
+    const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
+    const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
+    return Math.min(varA,varB);
+  },
+  max: function(a, b) {
+    const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
+    const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
+    return Math.max(varA,varB);
+  },
+  avg: function(a, b, c) {
+    const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
+    const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
+    return varA + varB;
+  }
+};
+
+const AggregatorFunctionDiv = {
+  sum: false,
+  min: false,
+  max: false,
+  count: false,
+  avg: true
+};
+
+/** nested cube `(key) -> (group) -> aggregator` */
+export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) {
+  const schema = {
+    key: keyColumns.length !== 0,
+    group: groupColumns.length !== 0,
+    aggregator: aggrColumns.length !== 0,
+  };
+
+  let cube = {}
+  const entry = {}
+
+  const keyColumnName = keyColumns.map(c => c.name).join('.')
+  const groupNameSet = new Set()
+  const keyNameSet = new Set()
+  const selectorNameWithIndex = {} /** { selectorName: index } */
+  let indexCounter = 0
+
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i];
+    let e = entry;
+    let c = cube;
+
+    // key: add to entry
+    let mergedKeyName = undefined
+    if (schema.key) {
+      mergedKeyName = keyColumns.map(c => row[c.index]).join('.')
+      if (!e[mergedKeyName]) { e[mergedKeyName] = { children: {}, } }
+      e = e[mergedKeyName].children
+      // key: add to row
+      if (!c[mergedKeyName]) { c[mergedKeyName] = {} }
+      c = c[mergedKeyName]
+
+      keyNameSet.add(mergedKeyName)
+    }
+
+    let mergedGroupName = undefined
+    if (schema.group) {
+      mergedGroupName = groupColumns.map(c => row[c.index]).join('.')
+
+      // add group to entry
+      if (!e[mergedGroupName]) { e[mergedGroupName] = { children: {}, } }
+      e = e[mergedGroupName].children
+      // add group to row
+      if (!c[mergedGroupName]) { c[mergedGroupName] = {} }
+      c = c[mergedGroupName]
+      groupNameSet.add(mergedGroupName)
+    }
+
+    for (let a = 0; a < aggrColumns.length; a++) {
+      const aggrColumn = aggrColumns[a]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+      // update groupNameSet
+      if (!mergedGroupName) {
+        groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */
+      }
+
+      // update selectorNameWithIndex
+      const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName)
+      if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) {
+        selectorNameWithIndex[selector] = indexCounter
+        indexCounter = indexCounter + 1
+      }
+
+      // add aggregator to entry
+      if (!e[aggrName]) {
+        e[aggrName] = { type: 'aggregator', order: aggrColumn, index: aggrColumn.index, }
+      }
+
+      // add aggregatorName to row
+      if (!c[aggrName]) {
+        c[aggrName] = {
+          aggr: aggrColumn.aggr,
+          value: (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1,
+          count: 1,
+        }
+      } else {
+        const value = AggregatorFunctions[aggrColumn.aggr](
+          c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1)
+        const count = (AggregatorFunctionDiv[aggrColumn.aggr]) ?
+          c[aggrName].count + 1 : c[aggrName].count
+
+        c[aggrName].value = value
+        c[aggrName].count = count
+      }
+
+    } /** end loop for aggrColumns */
+  }
+
+  let keyNames = null
+  if (!schema.key) {
+    const mergedGroupColumnName = groupColumns.map(c => c.name).join('.')
+    cube = { [mergedGroupColumnName]: cube, }
+    keyNames = [ mergedGroupColumnName, ]
+  } else {
+    keyNames = Object.keys(cube).sort() /** keys should be sorted */
+  }
+
+  return {
+    cube: cube,
+    schema: schema,
+    keyColumnName: keyColumnName,
+    keyNames: keyNames,
+    groupNameSet: groupNameSet,
+    selectorNameWithIndex: selectorNameWithIndex,
+  }
+}
+
+/** nested cube `(key) -> aggregator -> (group)` for drill-down support */
+export function getKAGCube(rows, keyColumns, groupColumns, aggrColumns) {
+  const schema = {
+    key: keyColumns.length !== 0,
+    group: groupColumns.length !== 0,
+    aggregator: aggrColumns.length !== 0,
+  };
+
+  let cube = {}
+
+  const keyColumnName = keyColumns.map(c => c.name).join('.')
+  const groupNameSet = new Set()
+  const keyNameSet = new Set()
+  const selectorNameWithIndex = {} /** { selectorName: index } */
+  let indexCounter = 0
+
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i];
+    let c = cube;
+
+    // key: add to entry
+    let mergedKeyName = undefined
+    if (schema.key) {
+      mergedKeyName = keyColumns.map(c => row[c.index]).join('.')
+      // key: add to row
+      if (!c[mergedKeyName]) { c[mergedKeyName] = {} }
+      c = c[mergedKeyName]
+
+      keyNameSet.add(mergedKeyName)
+    }
+
+    let mergedGroupName = undefined
+    if (schema.group) {
+      mergedGroupName = groupColumns.map(c => row[c.index]).join('.')
+      groupNameSet.add(mergedGroupName)
+    }
+
+    for (let a = 0; a < aggrColumns.length; a++) {
+      const aggrColumn = aggrColumns[a]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+      // update groupNameSet
+      if (!mergedGroupName) {
+        groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */
+      }
+
+      // update selectorNameWithIndex
+      const selector = getSelectorName(mergedKeyName, aggrColumns.length, aggrName)
+      if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) {
+        selectorNameWithIndex[selector] = indexCounter
+        indexCounter = indexCounter + 1
+      }
+
+      // add aggregatorName to row
+      if (!c[aggrName]) {
+        const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1
+        const count = 1
+
+        c[aggrName] = { aggr: aggrColumn.aggr, value: value, count: count, children: {}, }
+      } else {
+        const value = AggregatorFunctions[aggrColumn.aggr](
+          c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1)
+        const count = (AggregatorFunctionDiv[aggrColumn.aggr]) ?
+          c[aggrName].count + 1 : c[aggrName].count
+
+        c[aggrName].value = value
+        c[aggrName].count = count
+      }
+
+      // add aggregated group (for drill-down) to row iff group is enabled
+      if (mergedGroupName) {
+        if (!c[aggrName].children[mergedGroupName]) {
+          const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1
+          const count = 1
+
+          c[aggrName].children[mergedGroupName] = { value: value, count: count, }
+        } else {
+          const drillDownedValue = c[aggrName].children[mergedGroupName].value
+          const drillDownedCount = c[aggrName].children[mergedGroupName].count
+          const value = AggregatorFunctions[aggrColumn.aggr](
+            drillDownedValue, row[aggrColumn.index], drillDownedCount + 1)
+          const count = (AggregatorFunctionDiv[aggrColumn.aggr]) ?
+            drillDownedCount + 1 : drillDownedCount
+
+          c[aggrName].children[mergedGroupName].value = value
+          c[aggrName].children[mergedGroupName].count = count
+        }
+
+      }
+
+    } /** end loop for aggrColumns */
+  }
+
+  let keyNames = null
+  if (!schema.key) {
+    const mergedGroupColumnName = groupColumns.map(c => c.name).join('.')
+    cube = { [mergedGroupColumnName]: cube, }
+    keyNames = [ mergedGroupColumnName, ]
+  } else {
+    keyNames = Object.keys(cube).sort() /** keys should be sorted */
+  }
+
+  return {
+    cube: cube,
+    schema: schema,
+    keyColumnName: keyColumnName,
+    keyNames: keyNames,
+    groupNameSet: groupNameSet,
+    selectorNameWithIndex: selectorNameWithIndex,
+  }
+}
+/** nested cube `(key1) -> (key2) -> (group) -> aggregator` */
+export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrColumns) {
+  const schema = {
+    key1: key1Columns.length !== 0,
+    key2: key2Columns.length !== 0,
+    group: groupColumns.length !== 0,
+    aggregator: aggrColumns.length !== 0,
+  };
+
+  let cube = {}
+  const entry = {}
+
+  const key1ColumnName = key1Columns.map(c => c.name).join('.')
+  const key1NameSet = {}
+  const key2ColumnName = key2Columns.map(c => c.name).join('.')
+  const key2NameSet = {}
+  const groupNameSet = new Set()
+  const selectorNameWithIndex = {} /** { selectorName: index } */
+  let indexCounter = 0
+
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i];
+    let e = entry;
+    let c = cube;
+
+    // key1: add to entry
+    let mergedKey1Name = undefined
+    if (schema.key1) {
+      mergedKey1Name = key1Columns.map(c => row[c.index]).join('.')
+      if (!e[mergedKey1Name]) { e[mergedKey1Name] = { children: {}, } }
+      e = e[mergedKey1Name].children
+      // key1: add to row
+      if (!c[mergedKey1Name]) { c[mergedKey1Name] = {} }
+      c = c[mergedKey1Name]
+
+      if (!key1NameSet[mergedKey1Name]) { key1NameSet[mergedKey1Name] = true }
+    }
+
+    // key2: add to entry
+    let mergedKey2Name = undefined
+    if (schema.key2) {
+      mergedKey2Name = key2Columns.map(c => row[c.index]).join('.')
+      if (!e[mergedKey2Name]) { e[mergedKey2Name] = { children: {}, } }
+      e = e[mergedKey2Name].children
+      // key2: add to row
+      if (!c[mergedKey2Name]) { c[mergedKey2Name] = {} }
+      c = c[mergedKey2Name]
+
+      if (!key2NameSet[mergedKey2Name]) { key2NameSet[mergedKey2Name] = true }
+    }
+
+    let mergedGroupName = undefined
+    if (schema.group) {
+      mergedGroupName = groupColumns.map(c => row[c.index]).join('.')
+
+      // add group to entry
+      if (!e[mergedGroupName]) { e[mergedGroupName] = { children: {}, } }
+      e = e[mergedGroupName].children
+      // add group to row
+      if (!c[mergedGroupName]) { c[mergedGroupName] = {} }
+      c = c[mergedGroupName]
+      groupNameSet.add(mergedGroupName)
+    }
+
+    for (let a = 0; a < aggrColumns.length; a++) {
+      const aggrColumn = aggrColumns[a]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+      // update groupNameSet
+      if (!mergedGroupName) {
+        groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */
+      }
+
+      // update selectorNameWithIndex
+      const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName)
+      if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) {
+        selectorNameWithIndex[selector] = indexCounter
+        indexCounter = indexCounter + 1
+      }
+
+      // add aggregator to entry
+      if (!e[aggrName]) {
+        e[aggrName] = { type: 'aggregator', order: aggrColumn, index: aggrColumn.index, }
+      }
+
+      // add aggregatorName to row
+      if (!c[aggrName]) {
+        c[aggrName] = {
+          aggr: aggrColumn.aggr,
+          value: (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1,
+          count: 1,
+        }
+      } else {
+        const value = AggregatorFunctions[aggrColumn.aggr](
+          c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1)
+        const count = (AggregatorFunctionDiv[aggrColumn.aggr]) ?
+          c[aggrName].count + 1 : c[aggrName].count
+
+        c[aggrName].value = value
+        c[aggrName].count = count
+      }
+
+    } /** end loop for aggrColumns */
+  }
+
+  let key1Names = Object.keys(key1NameSet).sort() /** keys should be sorted */
+  let key2Names = Object.keys(key2NameSet).sort() /** keys should be sorted */
+
+  return {
+    cube: cube,
+    schema: schema,
+    key1ColumnName: key1ColumnName,
+    key1Names: key1Names,
+    key2ColumnName: key2ColumnName,
+    key2Names: key2Names,
+    groupNameSet: groupNameSet,
+    selectorNameWithIndex: selectorNameWithIndex,
+  }
+}
+
+export function getSelectorName(mergedGroupName, aggrColumnLength, aggrColumnName) {
+  if (!mergedGroupName) {
+    return aggrColumnName
+  } else {
+    return (aggrColumnLength > 1) ?
+      `${mergedGroupName} / ${aggrColumnName}` : mergedGroupName
+  }
+}
+
+export function getCubeValue(obj, aggregator, aggrColumnName) {
+  let value = null /** default is null */
+  try {
+    /** if AVG or COUNT, calculate it now, previously we can't because we were doing accumulation */
+    if (aggregator === Aggregator.AVG) {
+      value = obj[aggrColumnName].value / obj[aggrColumnName].count
+    } else if (aggregator === Aggregator.COUNT) {
+      value = obj[aggrColumnName].value
+    } else {
+      value = obj[aggrColumnName].value
+    }
+
+    if (typeof value === 'undefined') { value = null }
+  } catch (error) { /** iognore */ }
+
+  return value
+}
+
+export function getNameWithIndex(names) {
+  const nameWithIndex = {}
+
+  for (let i = 0; i < names.length; i++) {
+    const name = names[i]
+    nameWithIndex[name] = i
+  }
+
+  return nameWithIndex
+}
+
+export function getArrayRowsFromKKGACube(cube, schema, aggregatorColumns,
+                                         key1Names, key2Names, groupNameSet, selectorNameWithIndex) {
+
+  const sortedSelectors = Object.keys(selectorNameWithIndex).sort()
+  const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors)
+
+  const selectorRows = new Array(sortedSelectors.length)
+  const key1NameWithIndex = getNameWithIndex(key1Names)
+  const key2NameWithIndex = getNameWithIndex(key2Names)
+
+  fillSelectorRows(schema, cube, selectorRows,
+    aggregatorColumns, sortedSelectorNameWithIndex,
+    key1Names, key2Names, key1NameWithIndex, key2NameWithIndex)
+
+  return {
+    key1NameWithIndex: key1NameWithIndex,
+    key2NameWithIndex: key2NameWithIndex,
+    transformed: selectorRows,
+    groupNames: Array.from(groupNameSet).sort(),
+    sortedSelectors: sortedSelectors,
+  }
+}
+
+/** truly mutable style func. will return nothing */
+export function fillSelectorRows(schema, cube, selectorRows,
+                                 aggrColumns, selectorNameWithIndex,
+                                 key1Names, key2Names) {
+
+  function fill(grouped, mergedGroupName, key1Name, key2Name) {
+    // should iterate aggrColumns in the most nested loop to utilize memory locality
+    for (let aggrColumn of aggrColumns) {
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+      const value = getCubeValue(grouped, aggrColumn.aggr, aggrName)
+      const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName)
+      const selectorIndex = selectorNameWithIndex[selector]
+
+      if (typeof selectorRows[selectorIndex] === 'undefined') {
+        selectorRows[selectorIndex] = { selector: selector, value: [], }
+      }
+
+      const row = { aggregated: value, }
+
+      if (typeof key1Name !== 'undefined') { row.key1 = key1Name }
+      if (typeof key2Name !== 'undefined') { row.key2 = key2Name }
+
+      selectorRows[selectorIndex].value.push(row)
+    }
+  }
+
+  function iterateGroupNames(keyed, key1Name, key2Name) {
+    if (!schema.group) {
+      fill(keyed, undefined, key1Name, key2Name)
+    } else {
+      // assuming sparse distribution (usual case)
+      // otherwise we need to iterate using `groupNameSet`
+      const availableGroupNames = Object.keys(keyed)
+
+      for (let groupName of availableGroupNames) {
+        const grouped = keyed[groupName]
+        fill(grouped, groupName, key1Name, key2Name)
+      }
+    }
+  }
+
+  if (schema.key1 && schema.key2) {
+    for (let key1Name of key1Names) {
+      const key1ed = cube[key1Name]
+
+      // assuming sparse distribution (usual case)
+      // otherwise we need to iterate using `key2Names`
+      const availableKey2Names = Object.keys(key1ed)
+
+      for (let key2Name of availableKey2Names) {
+        const keyed = key1ed[key2Name]
+        iterateGroupNames(keyed, key1Name, key2Name)
+      }
+    }
+  } else if (schema.key1 && !schema.key2) {
+    for (let key1Name of key1Names) {
+      const keyed = cube[key1Name]
+      iterateGroupNames(keyed, key1Name, undefined)
+    }
+  } else if (!schema.key1 && schema.key2) {
+    for (let key2Name of key2Names) {
+      const keyed = cube[key2Name]
+      iterateGroupNames(keyed, undefined, key2Name)
+    }
+  } else {
+    iterateGroupNames(cube, undefined, undefined)
+  }
+}
+
+export function getArrayRowsFromKGACube(cube, schema, aggregatorColumns,
+                                        keyColumnName, keyNames, groupNameSet,
+                                        selectorNameWithIndex) {
+
+  const sortedSelectors = Object.keys(selectorNameWithIndex).sort()
+  const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors)
+
+  const keyArrowRows = new Array(sortedSelectors.length)
+  const keyNameWithIndex = getNameWithIndex(keyNames)
+
+  for(let i = 0; i < keyNames.length; i++) {
+    const key = keyNames[i]
+
+    const obj = cube[key]
+    fillArrayRow(schema, aggregatorColumns, obj,
+      groupNameSet, sortedSelectorNameWithIndex,
+      key, keyNames, keyArrowRows, keyNameWithIndex)
+  }
+
+  return {
+    transformed: keyArrowRows,
+    groupNames: Array.from(groupNameSet).sort(),
+    sortedSelectors: sortedSelectors,
+  }
+}
+
+/** truly mutable style func. will return nothing, just modify `keyArrayRows` */
+export function fillArrayRow(schema, aggrColumns, obj,
+                             groupNameSet, selectorNameWithIndex,
+                             keyName, keyNames, keyArrayRows, keyNameWithIndex) {
+
+  function fill(target, mergedGroupName, aggr, aggrName) {
+    const value = getCubeValue(target, aggr, aggrName)
+    const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName)
+    const selectorIndex = selectorNameWithIndex[selector]
+    const keyIndex = keyNameWithIndex[keyName]
+
+    if (typeof keyArrayRows[selectorIndex] === 'undefined') {
+      keyArrayRows[selectorIndex] = {
+        selector: selector, value: new Array(keyNames.length)
+      }
+    }
+    keyArrayRows[selectorIndex].value[keyIndex] = value
+  }
+
+  /** when group is empty */
+  if (!schema.group) {
+    for(let i = 0; i < aggrColumns.length; i++) {
+      const aggrColumn = aggrColumns[i]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+      fill(obj, undefined, aggrColumn.aggr, aggrName)
+    }
+  } else {
+    for(let i = 0; i < aggrColumns.length; i++) {
+      const aggrColumn = aggrColumns[i]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+      for (let groupName of groupNameSet) {
+        const grouped = obj[groupName]
+        fill(grouped, groupName, aggrColumn.aggr, aggrName)
+      }
+    }
+  }
+}
+
+export function getObjectRowsFromKGACube(cube, schema, aggregatorColumns,
+                                         keyColumnName, keyNames, groupNameSet,
+                                         selectorNameWithIndex) {
+
+  const rows = keyNames.reduce((acc, key) => {
+    const obj = cube[key]
+    const row = getObjectRow(schema, aggregatorColumns, obj, groupNameSet)
+
+    if (schema.key) { row[keyColumnName] = key }
+    acc.push(row)
+
+    return acc
+  }, [])
+
+  return {
+    transformed: rows,
+    sortedSelectors: Object.keys(selectorNameWithIndex).sort(),
+    groupNames: Array.from(groupNameSet).sort(),
+  }
+}
+
+export function getObjectRow(schema, aggrColumns, obj, groupNameSet) {
+  const row = {}
+
+  function fill(row, target, mergedGroupName, aggr, aggrName) {
+    const value = getCubeValue(target, aggr, aggrName)
+    const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName)
+    row[selector] = value
+  }
+
+  /** when group is empty */
+  if (!schema.group) {
+    for(let i = 0; i < aggrColumns.length; i++) {
+      const aggrColumn = aggrColumns[i]
+      const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+      fill(row, obj, undefined, aggrColumn.aggr, aggrName)
+    }
+
+    return row
+  }
+
+  /** when group is specified */
+  for(let i = 0; i < aggrColumns.length; i++) {
+    const aggrColumn = aggrColumns[i]
+    const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+    for (let groupName of groupNameSet) {
+      const grouped = obj[groupName]
+
+      if (grouped) {
+        fill(row, grouped, groupName, aggrColumn.aggr, aggrName)
+      }
+    }
+  }
+
+  return row
+}
+
+export function getDrilldownRowsFromKAGCube(cube, schema, aggregatorColumns,
+                                            keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) {
+
+  const sortedSelectors = Object.keys(selectorNameWithIndex).sort()
+  const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors)
+
+  const rows = new Array(sortedSelectors.length)
+
+  const groupNames = Array.from(groupNameSet).sort()
+
+  keyNames.map(key => {
+    const obj = cube[key]
+    fillDrillDownRow(schema, obj, rows, key,
+      sortedSelectorNameWithIndex, aggregatorColumns, groupNames)
+  })
+
+  return {
+    transformed: rows,
+    groupNames: groupNames,
+    sortedSelectors: sortedSelectors,
+    sortedSelectorNameWithIndex: sortedSelectorNameWithIndex,
+  }
+}
+
+/** truly mutable style func. will return nothing, just modify `rows` */
+export function fillDrillDownRow(schema, obj, rows, key,
+                                 selectorNameWithIndex, aggrColumns, groupNames) {
+  /** when group is empty */
+  for(let i = 0; i < aggrColumns.length; i++) {
+    const row = {}
+    const aggrColumn = aggrColumns[i]
+    const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`
+
+    const value = getCubeValue(obj, aggrColumn.aggr, aggrName)
+    const selector = getSelectorName((schema.key) ? key : undefined, aggrColumns.length, aggrName)
+
+    const selectorIndex = selectorNameWithIndex[selector]
+    row.value = value
+    row.drillDown = []
+    row.selector = selector
+
+    if (schema.group) {
+      row.drillDown = []
+
+      for(let groupName of groupNames) {
+        const value = getCubeValue(obj[aggrName].children, aggrColumn.aggr, groupName)
+        row.drillDown.push({ group: groupName, value: value, })
+      }
+    }
+
+    rows[selectorIndex] = row
+  }
+}