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:35 UTC
[1/3] zeppelin git commit: [ZEPPELIN-2217] AdvancedTransformation for
Visualization
Repository: zeppelin
Updated Branches:
refs/heads/master 2173b4013 -> 45cc8a9e8
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js
new file mode 100644
index 0000000..6fde659
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js
@@ -0,0 +1,1746 @@
+/*
+ * 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.
+ */
+
+import * as Util from './advanced-transformation-util.js'
+
+/* eslint-disable max-len */
+const MockParameter = {
+ 'floatParam': { valueType: 'float', defaultValue: 10, description: '', },
+ 'intParam': { valueType: 'int', defaultValue: 50, description: '', },
+ 'jsonParam': { valueType: 'JSON', defaultValue: '', description: '', widget: 'textarea', },
+ 'stringParam1': { valueType: 'string', defaultValue: '', description: '', },
+ 'stringParam2': { valueType: 'string', defaultValue: '', description: '', widget: 'input', },
+ 'boolParam': { valueType: 'boolean', defaultValue: false, description: '', widget: 'checkbox', },
+ 'optionParam': { valueType: 'string', defaultValue: 'line', description: '', widget: 'option', optionValues: [ 'line', 'smoothedLine', ], },
+}
+/* eslint-enable max-len */
+
+const MockAxis1 = {
+ 'keyAxis': { dimension: 'multiple', axisType: 'key', },
+ 'aggrAxis': { dimension: 'multiple', axisType: 'aggregator', },
+ 'groupAxis': { dimension: 'multiple', axisType: 'group', },
+}
+
+const MockAxis2 = {
+ 'singleKeyAxis': { dimension: 'single', axisType: 'key', },
+ 'limitedAggrAxis': { dimension: 'multiple', axisType: 'aggregator', maxAxisCount: 2, },
+ 'groupAxis': { dimension: 'multiple', axisType: 'group', },
+}
+
+const MockAxis3 = {
+ 'customAxis1': { dimension: 'single', axisType: 'unique', },
+ 'customAxis2': { dimension: 'multiple', axisType: 'value', },
+}
+
+const MockAxis4 = {
+ 'key1Axis': { dimension: 'multiple', axisType: 'key', },
+ 'key2Axis': { dimension: 'multiple', axisType: 'key', },
+ 'aggrAxis': { dimension: 'multiple', axisType: 'aggregator', },
+ 'groupAxis': { dimension: 'multiple', axisType: 'group', },
+}
+
+
+// test spec for axis, param, widget
+const MockSpec = {
+ charts: {
+ 'object-chart': {
+ transform: { method: 'object', },
+ sharedAxis: true,
+ axis: JSON.parse(JSON.stringify(MockAxis1)),
+ parameter: MockParameter,
+ },
+
+ 'array-chart': {
+ transform: { method: 'array', },
+ sharedAxis: true,
+ axis: JSON.parse(JSON.stringify(MockAxis1)),
+ parameter: {
+ 'arrayChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+
+ 'drillDown-chart': {
+ transform: { method: 'drill-down', },
+ axis: JSON.parse(JSON.stringify(MockAxis2)),
+ parameter: {
+ 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+
+ 'raw-chart': {
+ transform: { method: 'raw', },
+ axis: JSON.parse(JSON.stringify(MockAxis3)),
+ parameter: {
+ 'rawChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+ },
+}
+
+// test spec for transformation
+const MockSpec2 = {
+ charts: {
+ 'object-chart': {
+ transform: { method: 'object', },
+ sharedAxis: false,
+ axis: JSON.parse(JSON.stringify(MockAxis1)),
+ parameter: MockParameter,
+ },
+
+ 'array-chart': {
+ transform: { method: 'array', },
+ sharedAxis: false,
+ axis: JSON.parse(JSON.stringify(MockAxis1)),
+ parameter: {
+ 'arrayChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+
+ 'drillDown-chart': {
+ transform: { method: 'drill-down', },
+ sharedAxis: false,
+ axis: JSON.parse(JSON.stringify(MockAxis1)),
+ parameter: {
+ 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+
+ 'array2Key-chart': {
+ transform: { method: 'array:2-key', },
+ sharedAxis: false,
+ axis: JSON.parse(JSON.stringify(MockAxis4)),
+ parameter: {
+ 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+
+ 'raw-chart': {
+ transform: { method: 'raw', },
+ sharedAxis: false,
+ axis: JSON.parse(JSON.stringify(MockAxis3)),
+ parameter: {
+ 'rawChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', },
+ },
+ },
+ },
+}
+
+/* eslint-disable max-len */
+const MockTableDataColumn = [
+ {'name': 'age', 'index': 0, 'aggr': 'sum',},
+ {'name': 'job', 'index': 1, 'aggr': 'sum',},
+ {'name': 'marital', 'index': 2, 'aggr': 'sum',},
+ {'name': 'education', 'index': 3, 'aggr': 'sum',},
+ {'name': 'default', 'index': 4, 'aggr': 'sum',},
+ {'name': 'balance', 'index': 5, 'aggr': 'sum',},
+ {'name': 'housing', 'index': 6, 'aggr': 'sum',},
+ {'name': 'loan', 'index': 7, 'aggr': 'sum',},
+ {'name': 'contact', 'index': 8, 'aggr': 'sum',},
+ {'name': 'day', 'index': 9, 'aggr': 'sum',},
+ {'name': 'month', 'index': 10, 'aggr': 'sum',},
+ {'name': 'duration', 'index': 11, 'aggr': 'sum',},
+ {'name': 'campaign', 'index': 12, 'aggr': 'sum',},
+ {'name': 'pdays', 'index': 13, 'aggr': 'sum',},
+ {'name': 'previous', 'index': 14, 'aggr': 'sum',},
+ {'name': 'poutcome', 'index': 15, 'aggr': 'sum',},
+ {'name': 'y', 'index': 16, 'aggr': 'sum',}
+]
+
+const MockTableDataRows1 = [
+ [ '44', 'services', 'single', 'tertiary', 'no', '106', 'no', 'no', 'unknown', '12', 'jun', '109', '2', '-1', '0', 'unknown', 'no' ],
+ [ '43', 'services', 'married', 'primary', 'no', '-88', 'yes', 'yes', 'cellular', '17', 'apr', '313', '1', '147', '2', 'failure', 'no' ],
+ [ '39', 'services', 'married', 'secondary', 'no', '9374', 'yes', 'no', 'unknown', '20', 'may', '273', '1', '-1', '0', 'unknown', 'no' ],
+ [ '33', 'services', 'single', 'tertiary', 'no', '4789', 'yes', 'yes', 'cellular', '11', 'may', '220', '1', '339', '4', 'failure', 'no' ],
+]
+
+/* eslint-enable max-len */
+
+describe('advanced-transformation-util', () => {
+ describe('getCurrent* funcs', () => {
+ it('should set return proper value of the current chart', () => {
+ const config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+ expect(Util.getCurrentChart(config)).toEqual('object-chart')
+ expect(Util.getCurrentChartTransform(config)).toEqual({method: 'object'})
+ // use `toBe` to compare reference
+ expect(Util.getCurrentChartAxis(config)).toBe(config.axis['object-chart'])
+ // use `toBe` to compare reference
+ expect(Util.getCurrentChartParam(config)).toBe(config.parameter['object-chart'])
+ })
+ })
+
+ describe('useSharedAxis', () => {
+ it('should set chartChanged for initial drawing', () => {
+ const config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+ expect(Util.useSharedAxis(config, 'object-chart')).toEqual(true)
+ expect(Util.useSharedAxis(config, 'array-chart')).toEqual(true)
+ expect(Util.useSharedAxis(config, 'drillDown-chart')).toBeUndefined()
+ expect(Util.useSharedAxis(config, 'raw-chart')).toBeUndefined()
+ })
+ })
+
+ describe('initializeConfig', () => {
+ const config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+
+ it('should set chartChanged for initial drawing', () => {
+ expect(config.chartChanged).toBe(true)
+ expect(config.parameterChanged).toBe(false)
+ })
+
+ it('should set panel toggles ', () => {
+ expect(config.panel.columnPanelOpened).toBe(true)
+ expect(config.panel.parameterPanelOpened).toBe(false)
+ })
+
+ it('should set version and initialized', () => {
+ expect(config.spec.version).toBeDefined()
+ expect(config.spec.initialized).toBe(true)
+ })
+
+ it('should set chart', () => {
+ expect(config.chart.current).toBe('object-chart')
+ expect(config.chart.available).toEqual([
+ 'object-chart',
+ 'array-chart',
+ 'drillDown-chart',
+ 'raw-chart',
+ ])
+ })
+
+ it('should set sharedAxis', () => {
+ expect(config.sharedAxis).toEqual({
+ keyAxis: [], aggrAxis: [], groupAxis: [],
+ })
+ // should use `toBe` to compare object reference
+ expect(config.sharedAxis).toBe(config.axis['object-chart'])
+ // should use `toBe` to compare object reference
+ expect(config.sharedAxis).toBe(config.axis['array-chart'])
+ })
+
+ it('should set paramSpecs', () => {
+ const expected = Util.getSpecs(MockParameter)
+ expect(config.paramSpecs['object-chart']).toEqual(expected)
+ expect(config.paramSpecs['array-chart'].length).toEqual(1)
+ expect(config.paramSpecs['drillDown-chart'].length).toEqual(1)
+ expect(config.paramSpecs['raw-chart'].length).toEqual(1)
+ })
+
+ it('should set parameter with default value', () => {
+ expect(Object.keys(MockParameter).length).toBeGreaterThan(0) // length > 0
+ for (let paramName in MockParameter) {
+ expect(config.parameter['object-chart'][paramName])
+ .toEqual(MockParameter[paramName].defaultValue)
+ }
+ })
+
+ it('should set axisSpecs', () => {
+ const expected = Util.getSpecs(MockAxis1)
+ expect(config.axisSpecs['object-chart']).toEqual(expected)
+ expect(config.axisSpecs['array-chart'].length).toEqual(3)
+ expect(config.axisSpecs['drillDown-chart'].length).toEqual(3)
+ expect(config.axisSpecs['raw-chart'].length).toEqual(2)
+ })
+
+ it('should prepare axis depending on dimension', () => {
+ expect(config.axis['object-chart']).toEqual({
+ keyAxis: [], aggrAxis: [], groupAxis: [],
+ })
+ expect(config.axis['array-chart']).toEqual({
+ keyAxis: [], aggrAxis: [], groupAxis: [],
+ })
+ // it's ok not to set single dimension axis
+ expect(config.axis['drillDown-chart']).toEqual({ limitedAggrAxis: [], groupAxis: [], })
+ // it's ok not to set single dimension axis
+ expect(config.axis['raw-chart']).toEqual({ customAxis2: [], })
+ })
+
+ })
+
+ describe('axis', () => {
+
+ })
+
+ describe('parameter:widget', () => {
+ it('isInputWidget', () => {
+ expect(Util.isInputWidget(MockParameter.stringParam1)).toBe(true)
+ expect(Util.isInputWidget(MockParameter.stringParam2)).toBe(true)
+
+ expect(Util.isInputWidget(MockParameter.boolParam)).toBe(false)
+ expect(Util.isInputWidget(MockParameter.jsonParam)).toBe(false)
+ expect(Util.isInputWidget(MockParameter.optionParam)).toBe(false)
+ })
+
+ it('isOptionWidget', () => {
+ expect(Util.isOptionWidget(MockParameter.optionParam)).toBe(true)
+
+ expect(Util.isOptionWidget(MockParameter.stringParam1)).toBe(false)
+ expect(Util.isOptionWidget(MockParameter.stringParam2)).toBe(false)
+ expect(Util.isOptionWidget(MockParameter.boolParam)).toBe(false)
+ expect(Util.isOptionWidget(MockParameter.jsonParam)).toBe(false)
+ })
+
+ it('isCheckboxWidget', () => {
+ expect(Util.isCheckboxWidget(MockParameter.boolParam)).toBe(true)
+
+ expect(Util.isCheckboxWidget(MockParameter.stringParam1)).toBe(false)
+ expect(Util.isCheckboxWidget(MockParameter.stringParam2)).toBe(false)
+ expect(Util.isCheckboxWidget(MockParameter.jsonParam)).toBe(false)
+ expect(Util.isCheckboxWidget(MockParameter.optionParam)).toBe(false)
+ })
+
+ it('isTextareaWidget', () => {
+ expect(Util.isTextareaWidget(MockParameter.jsonParam)).toBe(true)
+
+ expect(Util.isTextareaWidget(MockParameter.stringParam1)).toBe(false)
+ expect(Util.isTextareaWidget(MockParameter.stringParam2)).toBe(false)
+ expect(Util.isTextareaWidget(MockParameter.boolParam)).toBe(false)
+ expect(Util.isTextareaWidget(MockParameter.optionParam)).toBe(false)
+ })
+ })
+
+ describe('parameter:parseParameter', () => {
+ const paramSpec = Util.getSpecs(MockParameter)
+
+ it('should parse number', () => {
+ const params = { intParam: '3', }
+ const parsed = Util.parseParameter(paramSpec, params)
+ expect(parsed.intParam).toBe(3)
+ })
+
+ it('should parse float', () => {
+ const params = { floatParam: '0.10', }
+ const parsed = Util.parseParameter(paramSpec, params)
+ expect(parsed.floatParam).toBe(0.10)
+ })
+
+ it('should parse boolean', () => {
+ const params1 = { boolParam: 'true', }
+ const parsed1 = Util.parseParameter(paramSpec, params1)
+ expect(typeof parsed1.boolParam).toBe('boolean')
+ expect(parsed1.boolParam).toBe(true)
+
+ const params2 = { boolParam: 'false', }
+ const parsed2 = Util.parseParameter(paramSpec, params2)
+ expect(typeof parsed2.boolParam).toBe('boolean')
+ expect(parsed2.boolParam).toBe(false)
+ })
+
+ it('should parse JSON', () => {
+ const params = { jsonParam: '{ "a": 3 }', }
+ const parsed = Util.parseParameter(paramSpec, params)
+ expect(typeof parsed.jsonParam).toBe('object')
+ expect(JSON.stringify(parsed.jsonParam)).toBe('{"a":3}')
+ })
+
+ it('should not parse string', () => {
+ const params = { stringParam: 'example', }
+ const parsed = Util.parseParameter(paramSpec, params)
+ expect(typeof parsed.stringParam).toBe('string')
+ expect(parsed.stringParam).toBe('example')
+ })
+
+ })
+
+ describe('removeDuplicatedColumnsInMultiDimensionAxis', () => {
+ let config = {}
+
+ beforeEach(() => {
+ config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+ config.chart.current = 'drillDown-chart' // set non-sharedAxis chart
+ })
+
+ it('should remove duplicated axis names in config when axis is not aggregator', () => {
+ const addColumn = function(config, col) {
+ const axis = Util.getCurrentChartAxis(config)['groupAxis']
+ axis.push(col)
+ const axisSpecs = Util.getCurrentChartAxisSpecs(config)
+ Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[2])
+ }
+
+ addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, })
+ addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, })
+ addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, })
+
+ expect(Util.getCurrentChartAxis(config)['groupAxis'].length).toEqual(1)
+ })
+
+ it('should remove duplicated axis names in config when axis is aggregator', () => {
+ const addColumn = function(config, value) {
+ const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis']
+ axis.push(value)
+ const axisSpecs = Util.getCurrentChartAxisSpecs(config)
+ Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[1])
+ }
+
+ config.chart.current = 'drillDown-chart' // set non-sharedAxis chart
+ addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, })
+ addColumn(config, { name: 'columnA', aggr: 'aggr', index: 0, })
+ addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, })
+
+ expect(Util.getCurrentChartAxis(config)['limitedAggrAxis'].length).toEqual(2)
+ })
+ })
+
+ describe('applyMaxAxisCount', () => {
+ const config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+
+ const addColumn = function(config, value) {
+ const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis']
+ axis.push(value)
+ const axisSpecs = Util.getCurrentChartAxisSpecs(config)
+ Util.applyMaxAxisCount(config, axisSpecs[1])
+ }
+
+ it('should remove duplicated axis names in config', () => {
+ config.chart.current = 'drillDown-chart' // set non-sharedAxis chart
+
+ addColumn(config, 'columnA')
+ addColumn(config, 'columnB')
+ addColumn(config, 'columnC')
+ addColumn(config, 'columnD')
+
+ expect(Util.getCurrentChartAxis(config)['limitedAggrAxis']).toEqual([
+ 'columnC', 'columnD',
+ ])
+ })
+ })
+
+ describe('getColumnsFromAxis', () => {
+ it('should return proper value for regular axis spec (key, aggr, group)', () => {
+ const config = {}
+
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+ const chart = 'object-chart'
+ config.chart.current = chart
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ axis['keyAxis'].push('columnA')
+ axis['keyAxis'].push('columnB')
+ axis['aggrAxis'].push('columnC')
+ axis['groupAxis'].push('columnD')
+ axis['groupAxis'].push('columnE')
+ axis['groupAxis'].push('columnF')
+
+ const column = Util.getColumnsFromAxis(axisSpecs, axis)
+ expect(column.key).toEqual([ 'columnA', 'columnB', ])
+ expect(column.aggregator).toEqual([ 'columnC', ])
+ expect(column.group).toEqual([ 'columnD', 'columnE', 'columnF', ])
+ })
+
+ it('should return proper value for custom axis spec', () => {
+ const config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec))
+ Util.initializeConfig(config, spec)
+ const chart = 'raw-chart' // for test custom columns
+ config.chart.current = chart
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ axis['customAxis1'] = ['columnA']
+ axis['customAxis2'].push('columnB')
+ axis['customAxis2'].push('columnC')
+ axis['customAxis2'].push('columnD')
+
+ const column = Util.getColumnsFromAxis(axisSpecs, axis)
+ expect(column.custom.unique).toEqual([ 'columnA', ])
+ expect(column.custom.value).toEqual([ 'columnB', 'columnC', 'columnD', ])
+ })
+ })
+
+ // it's hard to test all methods for transformation.
+ // so let's do behavioral (black-box) test instead of
+ describe('getTransformer', () => {
+
+ describe('method: raw', () => {
+ let config = {}
+ const spec = JSON.parse(JSON.stringify(MockSpec2))
+ Util.initializeConfig(config, spec)
+
+ it('should return original rows when transform.method is `raw`', () => {
+ const chart = 'raw-chart'
+ config.chart.current = chart
+
+ const rows = [ { 'r1': 1, }, ]
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, rows, axisSpecs, axis).transformer
+ const transformed = transformer()
+
+ expect(transformed).toBe(rows)
+ })
+ })
+
+ describe('array method', () => {
+ let config = {}
+ const chart = 'array-chart'
+ let ageColumn = null
+ let balanceColumn = null
+ let educationColumn = null
+ let martialColumn = null
+ let tableDataRows = []
+
+ beforeEach(() => {
+ const spec = JSON.parse(JSON.stringify(MockSpec2))
+ config = {}
+ Util.initializeConfig(config, spec)
+ config.chart.current = chart
+ tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1))
+ ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0]))
+ balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5]))
+ educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3]))
+ martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2]))
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ 159, ], }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => {
+ ageColumn.aggr = 'count'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ let { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(count)', value: [ 4, ], }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => {
+ ageColumn.aggr = 'avg'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(avg)', value: [ (44 + 43 + 39 + 33) / 4.0, ], }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => {
+ ageColumn.aggr = 'max'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(max)', value: [ 44, ], }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => {
+ ageColumn.aggr = 'min'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(min)', value: [ 33, ], }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ 159, ], },
+ { selector: 'balance(sum)', value: [ 14181, ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([ 'married', 'single', ])
+ expect(rows).toEqual([
+ { selector: 'married', value: [ 82, ], },
+ { selector: 'single', value: [ 77, ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([
+ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)',
+ ])
+ expect(rows).toEqual([
+ { selector: 'married / age(sum)', value: [ 82 ] },
+ { selector: 'married / balance(sum)', value: [ 9286 ] },
+ { selector: 'single / age(sum)', value: [ 77 ] },
+ { selector: 'single / balance(sum)', value: [ 4895 ] },
+ ])
+ })
+
+ it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital.education', ])
+ expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'married.primary', value: [ '43' ] },
+ { selector: 'married.secondary', value: [ '39' ] },
+ { selector: 'single.tertiary', value: [ 77 ] },
+ ])
+ })
+
+ it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ 82, 77, ] },
+ ])
+ })
+
+ it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].keyAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital.education')
+ expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ '43', '39', 77, ] },
+ ])
+ })
+
+ it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'primary', value: [ '43', null, ] },
+ { selector: 'secondary', value: [ '39', null, ] },
+ { selector: 'tertiary', value: [ null, 77, ] },
+ ])
+ })
+ }) // end: describe('method: array')
+
+ describe('method: object', () => {
+ let config = {}
+ const chart = 'object-chart'
+ let ageColumn = null
+ let balanceColumn = null
+ let educationColumn = null
+ let martialColumn = null
+ const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1))
+
+ beforeEach(() => {
+ const spec = JSON.parse(JSON.stringify(MockSpec2))
+ config = {}
+ Util.initializeConfig(config, spec)
+ config.chart.current = chart
+ ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0]))
+ balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5]))
+ educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3]))
+ martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2]))
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([{ 'age(sum)': 44 + 43 + 39 + 33, }])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => {
+ ageColumn.aggr = 'count'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([{ 'age(count)': 4, }])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => {
+ ageColumn.aggr = 'avg'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { 'age(avg)': (44 + 43 + 39 + 33) / 4.0, }
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => {
+ ageColumn.aggr = 'max'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([{ 'age(max)': 44, }])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => {
+ ageColumn.aggr = 'min'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([{ 'age(min)': 33, }])
+ })
+
+ it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(rows).toEqual([{ 'age(sum)': 159, 'balance(sum)': 14181, }])
+ })
+
+ it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([ 'married', 'single', ])
+ expect(rows).toEqual([
+ { single: 77, married: 82, }
+ ])
+ })
+
+ it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([
+ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)',
+ ])
+ expect(rows).toEqual([{
+ 'married / age(sum)': 82,
+ 'single / age(sum)': 77,
+ 'married / balance(sum)': 9286,
+ 'single / balance(sum)': 4895,
+ }])
+ })
+
+ it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital.education', ])
+ expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(rows).toEqual([{
+ 'married.primary': '43', 'married.secondary': '39', 'single.tertiary': 77,
+ }])
+ })
+
+ it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { 'age(sum)': 82, marital: 'married', },
+ { 'age(sum)': 77, marital: 'single', },
+ ])
+ })
+
+ it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].keyAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital.education')
+ expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { 'age(sum)': '43', 'marital.education': 'married.primary' },
+ { 'age(sum)': '39', 'marital.education': 'married.secondary' },
+ { 'age(sum)': 77, 'marital.education': 'single.tertiary' },
+ ])
+ })
+
+ it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(rows).toEqual([
+ { primary: '43', secondary: '39', marital: 'married' },
+ { tertiary: 44 + 33, marital: 'single' },
+ ])
+ })
+ }) // end: describe('method: object')
+
+ describe('method: drill-down', () => {
+ let config = {}
+ const chart = 'drillDown-chart'
+ let ageColumn = null
+ let balanceColumn = null
+ let educationColumn = null
+ let martialColumn = null
+ const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1))
+
+ beforeEach(() => {
+ const spec = JSON.parse(JSON.stringify(MockSpec2))
+ config = {}
+ Util.initializeConfig(config, spec)
+ config.chart.current = chart
+ ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0]))
+ balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5]))
+ educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3]))
+ martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2]))
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: 44 + 43 + 39 + 33, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => {
+ ageColumn.aggr = 'count'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(count)', value: 4, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => {
+ ageColumn.aggr = 'avg'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(avg)', value: (44 + 43 + 39 + 33) / 4.0, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => {
+ ageColumn.aggr = 'max'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(max)', value: 44, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => {
+ ageColumn.aggr = 'min'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, } = transformer()
+ expect(rows).toEqual([
+ { selector: 'age(min)', value: 33, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ '', ])
+ expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: 159, drillDown: [ ], },
+ { selector: 'balance(sum)', value: 14181, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ {
+ selector: 'age(sum)',
+ value: 159,
+ drillDown: [
+ { group: 'married', value: 82 },
+ { group: 'single', value: 77 },
+ ],
+ },
+ ])
+ })
+
+ it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital', ])
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([ 'age(sum)', 'balance(sum)' ])
+ expect(rows).toEqual([
+ {
+ selector: 'age(sum)',
+ value: 159,
+ drillDown: [
+ { group: 'married', value: 82 },
+ { group: 'single', value: 77 },
+ ],
+ },
+ {
+ selector: 'balance(sum)',
+ value: 14181,
+ drillDown: [
+ { group: 'married', value: 9286 },
+ { group: 'single', value: 4895 },
+ ],
+ },
+ ])
+ })
+
+ it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('')
+ expect(keyNames).toEqual([ 'marital.education', ])
+ expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ {
+ selector: 'age(sum)',
+ value: 159,
+ drillDown: [
+ { group: 'married.primary', value: '43' },
+ { group: 'married.secondary', value: '39' },
+ { group: 'single.tertiary', value: 77 },
+ ],
+ },
+ ])
+ })
+
+ it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'married', 'single', ])
+ expect(rows).toEqual([
+ { selector: 'married', value: 82, drillDown: [ ], },
+ { selector: 'single', value: 77, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].keyAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital.education')
+ expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'married.primary', value: '43', drillDown: [ ], },
+ { selector: 'married.secondary', value: '39', drillDown: [ ], },
+ { selector: 'single.tertiary', value: 77, drillDown: [ ], },
+ ])
+ })
+
+ it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].keyAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer()
+
+ expect(keyColumnName).toEqual('marital')
+ expect(keyNames).toEqual([ 'married', 'single', ])
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'married', 'single', ])
+ expect(rows).toEqual([
+ {
+ selector: 'married',
+ value: 82,
+ drillDown: [
+ { group: 'primary', value: '43' },
+ { group: 'secondary', value: '39' },
+ { group: 'tertiary', value: null },
+ ],
+ },
+ {
+ selector: 'single',
+ value: 77,
+ drillDown: [
+ { group: 'primary', value: null },
+ { group: 'secondary', value: null },
+ { group: 'tertiary', value: 77 },
+ ],
+ },
+ ])
+ })
+ }) // end: describe('method: drill-down')
+
+ describe('method: array:2-key', () => {
+ let config = {}
+ const chart = 'array2Key-chart'
+ let ageColumn = null
+ let balanceColumn = null
+ let educationColumn = null
+ let martialColumn = null
+ let daysColumn = null
+ let pDaysColumn = null
+ const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1))
+
+ beforeEach(() => {
+ const spec = JSON.parse(JSON.stringify(MockSpec2))
+ config = {}
+ Util.initializeConfig(config, spec)
+ config.chart.current = chart
+ ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0]))
+ martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2]))
+ educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3]))
+ balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5]))
+ daysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[9]))
+ pDaysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[13]))
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key2Names, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key2Names).toEqual([])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ { aggregated: 44 + 43 + 39 + 33, }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(count)', () => {
+ ageColumn.aggr = 'count'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key2Names, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key2Names).toEqual([])
+ expect(selectors).toEqual([ 'age(count)', ])
+ expect(rows).toEqual([
+ { selector: 'age(count)', value: [ { aggregated: 4, }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(avg)', () => {
+ ageColumn.aggr = 'avg'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key2Names, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key2Names).toEqual([])
+ expect(selectors).toEqual([ 'age(avg)', ])
+ expect(rows).toEqual([
+ { selector: 'age(avg)', value: [ { aggregated: (44 + 43 + 39 + 33) / 4.0, }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(max)', () => {
+ ageColumn.aggr = 'max'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key2Names, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key2Names).toEqual([])
+ expect(selectors).toEqual([ 'age(max)', ])
+ expect(rows).toEqual([
+ { selector: 'age(max)', value: [ { aggregated: 44, }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(min)', () => {
+ ageColumn.aggr = 'min'
+ config.axis[chart].aggrAxis.push(ageColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key2Names, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key2Names).toEqual([])
+ expect(selectors).toEqual([ 'age(min)', ])
+ expect(rows).toEqual([
+ { selector: 'age(min)', value: [ { aggregated: 33, }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 0 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, groupNames, selectors, } = transformer()
+
+ expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ])
+ expect(rows).toEqual([
+ { selector: 'age(sum)', value: [ { aggregated: 159 } ] },
+ { selector: 'balance(sum)', value: [ { aggregated: 14181 }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, groupNames, selectors, } = transformer()
+
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([ 'married', 'single', ])
+ expect(rows).toEqual([
+ { selector: 'married', value: [ { aggregated: 82 }, ] },
+ { selector: 'single', value: [ { aggregated: 77 }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 1 group, 2 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ balanceColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, groupNames, selectors, } = transformer()
+
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual([
+ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)',
+ ])
+ expect(rows).toEqual([
+ { selector: 'married / age(sum)', value: [ { aggregated: 82 }, ] },
+ { selector: 'married / balance(sum)', value: [ { aggregated: 9286 }, ] },
+ { selector: 'single / age(sum)', value: [ { aggregated: 77 }, ] },
+ { selector: 'single / balance(sum)', value: [ { aggregated: 4895 }, ] },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 0 key2, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, groupNames, selectors, } = transformer()
+
+ expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'married.primary', value: [ { aggregated: '43' }, ] },
+ { selector: 'married.secondary', value: [ { aggregated: '39' }, ] },
+ { selector: 'single.tertiary', value: [ { aggregated: 77 }, ] },
+ ])
+ })
+
+ it('should transform properly: 1 key1, 0 key2, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key1Axis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key1ColumnName).toEqual('balance')
+ expect(key2Names).toEqual([])
+ expect(key2ColumnName).toEqual('')
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ {
+ selector: 'age(sum)',
+ value: [
+ { aggregated: '43', key1: '-88' },
+ { aggregated: '44', key1: '106' },
+ { aggregated: '33', key1: '4789' },
+ { aggregated: '39', key1: '9374' },
+ ]
+ }
+ ])
+ })
+
+ it('should transform properly: 0 key1, 1 key2, 0 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key2Axis.push(balanceColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key1ColumnName).toEqual('')
+ expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key2ColumnName).toEqual('balance')
+ expect(groupNames).toEqual([ 'age(sum)', ])
+ expect(selectors).toEqual([ 'age(sum)', ])
+ expect(rows).toEqual([
+ {
+ selector: 'age(sum)',
+ value: [
+ { aggregated: '43', key2: '-88' },
+ { aggregated: '44', key2: '106' },
+ { aggregated: '33', key2: '4789' },
+ { aggregated: '39', key2: '9374' },
+ ]
+ },
+ ])
+ })
+
+ it('should transform properly: 1 key1, 0 key2, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key1Axis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key1ColumnName).toEqual('balance')
+ expect(key2Names).toEqual([])
+ expect(key2ColumnName).toEqual('')
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'primary', value: [ { aggregated: '43', key1: '-88' }, ] },
+ { selector: 'secondary', value: [ { aggregated: '39', key1: '9374' }, ] },
+ {
+ selector: 'tertiary',
+ value: [
+ { aggregated: '44', key1: '106' },
+ { aggregated: '33', key1: '4789' },
+ ]
+ },
+ ])
+ })
+
+ it('should transform properly: 0 key1, 1 key2, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key2Axis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([])
+ expect(key1ColumnName).toEqual('')
+ expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key2ColumnName).toEqual('balance')
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(rows).toEqual([
+ { selector: 'primary', value: [ { aggregated: '43', key2: '-88' }, ] },
+ { selector: 'secondary', value: [ { aggregated: '39', key2: '9374' }, ] },
+ {
+ selector: 'tertiary',
+ value: [
+ { aggregated: '44', key2: '106' },
+ { aggregated: '33', key2: '4789' },
+ ]
+ },
+ ])
+ })
+
+ it('should transform properly: 1 key1, 1 key2, 1 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key1Axis.push(pDaysColumn)
+ config.axis[chart].key2Axis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([ '-1', '147', '339', ])
+ expect(key1ColumnName).toEqual('pdays')
+ expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key2ColumnName).toEqual('balance')
+ expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ])
+ expect(rows).toEqual([
+ {
+ selector: 'primary',
+ value: [ { aggregated: '43', key1: '147', key2: '-88' }, ]
+ },
+ {
+ selector: 'secondary',
+ value: [ { aggregated: '39', key1: '-1', key2: '9374' }, ]
+ },
+ {
+ selector: 'tertiary',
+ value: [
+ { aggregated: '44', key1: '-1', key2: '106' },
+ { aggregated: '33', key1: '339', key2: '4789' },
+ ]
+ },
+ ])
+ })
+
+ it('should transform properly: 1 key1, 1 key2, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'sum'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].key1Axis.push(pDaysColumn)
+ config.axis[chart].key2Axis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(educationColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([ '-1', '147', '339', ])
+ expect(key1ColumnName).toEqual('pdays')
+ expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key2ColumnName).toEqual('balance')
+ expect(groupNames).toEqual([ 'primary.married', 'secondary.married', 'tertiary.single', ])
+ expect(selectors).toEqual([ 'primary.married', 'secondary.married', 'tertiary.single', ])
+ expect(rows).toEqual([
+ {
+ selector: 'primary.married',
+ value: [ { aggregated: '43', key1: '147', key2: '-88'}, ]
+ },
+ {
+ selector: 'secondary.married',
+ value: [ { aggregated: '39', key1: '-1', key2: '9374' }, ]
+ },
+ {
+ selector: 'tertiary.single',
+ value: [
+ { aggregated: '44', key1: '-1', key2: '106' },
+ { aggregated: '33', key1: '339', key2: '4789' },
+ ]
+ },
+ ])
+ })
+
+ it('should transform properly: 1 key1, 1 key2, 2 group, 1 aggr(sum)', () => {
+ ageColumn.aggr = 'min'
+ daysColumn.aggr = 'max'
+ config.axis[chart].aggrAxis.push(ageColumn)
+ config.axis[chart].aggrAxis.push(daysColumn)
+ config.axis[chart].key1Axis.push(pDaysColumn)
+ config.axis[chart].key2Axis.push(balanceColumn)
+ config.axis[chart].groupAxis.push(martialColumn)
+
+ const axisSpecs = config.axisSpecs[chart]
+ const axis = config.axis[chart]
+ const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer
+
+ const { rows, key1Names, key1ColumnName,
+ key2Names, key2ColumnName, groupNames, selectors, } = transformer()
+
+ expect(key1Names).toEqual([ '-1', '147', '339', ])
+ expect(key1ColumnName).toEqual('pdays')
+ expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ])
+ expect(key2ColumnName).toEqual('balance')
+ expect(groupNames).toEqual([ 'married', 'single', ])
+ expect(selectors).toEqual(
+ [ 'married / age(min)', 'married / day(max)', 'single / age(min)', 'single / day(max)', ]
+ )
+ expect(rows).toEqual([
+ {
+ selector: 'married / age(min)',
+ value: [
+ { aggregated: '39', key1: '-1', key2: '9374' },
+ { aggregated: '43', key1: '147', key2: '-88' },
+ ]
+ },
+ {
+ selector: 'married / day(max)',
+ value: [
+ { aggregated: '20', key1: '-1', key2: '9374' },
+ { aggregated: '17', key1: '147', key2: '-88' },
+ ]
+ },
+ {
+ selector: 'single / age(min)',
+ value: [
+ { aggregated: '44', key1: '-1', key2: '106' },
+ { aggregated: '33', key1: '339', key2: '4789' },
+ ]
+ },
+ {
+ selector: 'single / day(max)',
+ value: [
+ { aggregated: '12', key1: '-1', key2: '106' },
+ { aggregated: '11', key1: '339', key2: '4789' },
+ ]
+ },
+ ])
+ })
+
+ }) // end: describe('method: array:2-key')
+
+ }) // end: describe('getTransformer')
+})
+
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/src/app/tabledata/advanced-transformation.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation.js b/zeppelin-web/src/app/tabledata/advanced-transformation.js
new file mode 100644
index 0000000..d754f4d
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/advanced-transformation.js
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+
+import Transformation from './transformation';
+
+import {
+ getCurrentChart, getCurrentChartAxis, getCurrentChartParam,
+ serializeSharedAxes, useSharedAxis,
+ getCurrentChartAxisSpecs, getCurrentChartParamSpecs,
+ initializeConfig, resetAxisConfig, resetParameterConfig,
+ isAggregatorAxis, isGroupAxis, isKeyAxis, isSingleDimensionAxis,
+ removeDuplicatedColumnsInMultiDimensionAxis, applyMaxAxisCount,
+ isInputWidget, isOptionWidget, isCheckboxWidget, isTextareaWidget, parseParameter,
+ getTransformer,
+} from './advanced-transformation-util';
+
+const SETTING_TEMPLATE = 'app/tabledata/advanced-transformation-setting.html';
+
+export default class AdvancedTransformation extends Transformation {
+ constructor(config, spec) {
+ super(config);
+
+ this.columns = []; /** [{ name, index, comment }] */
+ this.props = {};
+ this.spec = spec
+
+ initializeConfig(config, spec);
+ }
+
+ emitConfigChange(conf) {
+ conf.chartChanged = false
+ conf.parameterChanged = false
+ this.emitConfig(conf)
+ }
+
+ emitChartChange(conf) {
+ conf.chartChanged = true
+ conf.parameterChanged = false
+ this.emitConfig(conf)
+ }
+
+ emitParameterChange(conf) {
+ conf.chartChanged = false
+ conf.parameterChanged = true
+ this.emitConfig(conf)
+ }
+
+ getSetting() {
+ const self = this; /** for closure */
+ const configInstance = self.config; /** for closure */
+
+ if (self.spec.initialized) {
+ self.spec.initialized = false
+ self.emitConfig(configInstance)
+ }
+
+ return {
+ template: SETTING_TEMPLATE,
+ scope: {
+ config: configInstance,
+ columns: self.columns,
+ resetAxisConfig: () => {
+ resetAxisConfig(configInstance)
+ self.emitChartChange(configInstance)
+ },
+
+ resetParameterConfig: () => {
+ resetParameterConfig(configInstance)
+ self.emitParameterChange(configInstance)
+ },
+
+ toggleColumnPanel: () => {
+ configInstance.panel.columnPanelOpened = !configInstance.panel.columnPanelOpened
+ self.emitConfigChange(configInstance)
+ },
+
+ toggleParameterPanel: () => {
+ configInstance.panel.parameterPanelOpened = !configInstance.panel.parameterPanelOpened
+ self.emitConfigChange(configInstance)
+ },
+
+ getAxisAnnotation: (axisSpec) => {
+ let anno = `${axisSpec.name}`
+ if (axisSpec.valueType) {
+ anno = `${anno} (${axisSpec.valueType})`
+ }
+
+ return anno
+ },
+
+ getAxisTypeAnnotation: (axisSpec) => {
+ let anno = `${axisSpec.axisType}`
+
+ let minAxisCount = axisSpec.minAxisCount
+ let maxAxisCount = axisSpec.maxAxisCount
+
+ if (isSingleDimensionAxis(axisSpec)) {
+ maxAxisCount = 1
+ }
+
+ let comment = ''
+ if (minAxisCount) { comment = `min: ${minAxisCount}` }
+ if (minAxisCount && maxAxisCount) { comment = `${comment}, `}
+ if (maxAxisCount) { comment = `${comment}max: ${maxAxisCount}` }
+
+ if (comment !== '') {
+ anno = `${anno} (${comment})`
+ }
+
+ return anno
+ },
+
+ getAxisTypeAnnotationColor: (axisSpec) => {
+ if (isAggregatorAxis(axisSpec)) {
+ return { 'background-color': '#5782bd' };
+ } else if (isGroupAxis(axisSpec)) {
+ return { 'background-color': '#cd5c5c' };
+ } else if (isKeyAxis(axisSpec)) {
+ return { 'background-color': '#906ebd' };
+ } else {
+ return { 'background-color': '#62bda9' };
+ }
+ },
+
+ useSharedAxis: (chartName) => { return useSharedAxis(configInstance, chartName) },
+ isGroupAxis: (axisSpec) => { return isGroupAxis(axisSpec) },
+ isKeyAxis: (axisSpec) => { return isKeyAxis(axisSpec) },
+ isAggregatorAxis: (axisSpec) => { return isAggregatorAxis(axisSpec) },
+ isSingleDimensionAxis: (axisSpec) => { return isSingleDimensionAxis(axisSpec) },
+ getSingleDimensionAxis: (axisSpec) => { return getCurrentChartAxis(configInstance)[axisSpec.name] },
+
+ chartChanged: (selected) => {
+ configInstance.chart.current = selected
+ self.emitChartChange(configInstance)
+ },
+
+ axisChanged: function(e, ui, axisSpec) {
+ removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec)
+ applyMaxAxisCount(configInstance, axisSpec)
+
+ self.emitChartChange(configInstance)
+ },
+
+ aggregatorChanged: (colIndex, axisSpec, aggregator) => {
+ if (isSingleDimensionAxis(axisSpec)) {
+ getCurrentChartAxis(configInstance)[axisSpec.name].aggr = aggregator
+ } else {
+ getCurrentChartAxis(configInstance)[axisSpec.name][colIndex].aggr = aggregator
+ removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec)
+ }
+
+ self.emitChartChange(configInstance)
+ },
+
+ removeFromAxis: function(colIndex, axisSpec) {
+ if (isSingleDimensionAxis(axisSpec)) {
+ getCurrentChartAxis(configInstance)[axisSpec.name] = null
+ } else {
+ getCurrentChartAxis(configInstance)[axisSpec.name].splice(colIndex, 1)
+ }
+
+ self.emitChartChange(configInstance)
+ },
+
+ isInputWidget: function(paramSpec) { return isInputWidget(paramSpec) },
+ isCheckboxWidget: function(paramSpec) { return isCheckboxWidget(paramSpec) },
+ isOptionWidget: function(paramSpec) { return isOptionWidget(paramSpec) },
+ isTextareaWidget: function(paramSpec) { return isTextareaWidget(paramSpec) },
+
+ parameterChanged: (paramSpec) => {
+
+ configInstance.chartChanged = false
+ configInstance.parameterChanged = true
+ self.emitParameterChange(configInstance)
+ },
+
+ parameterOnKeyDown: function(event, paramSpec) {
+ const code = event.keyCode || event.which;
+ if (code === 13 && isInputWidget(paramSpec)) {
+ self.emitParameterChange(configInstance)
+ } else if (code === 13 && event.shiftKey && isTextareaWidget(paramSpec)) {
+ self.emitParameterChange(configInstance)
+ }
+
+ event.stopPropagation() /** avoid to conflict with paragraph shortcuts */
+ },
+
+ }
+ }
+ }
+
+ transform(tableData) {
+ this.columns = tableData.columns; /** used in `getSetting` */
+ /** initialize in `transform` instead of `getSetting` because this method is called before */
+ serializeSharedAxes(this.config)
+
+ const conf = this.config
+ const chart = getCurrentChart(conf)
+ const axis = getCurrentChartAxis(conf)
+ const axisSpecs = getCurrentChartAxisSpecs(conf)
+ const param = getCurrentChartParam(conf)
+ const paramSpecs = getCurrentChartParamSpecs(conf)
+ const parsedParam = parseParameter(paramSpecs, param)
+
+ let { transformer, column, } = getTransformer(conf, tableData.rows, axisSpecs, axis)
+
+ return {
+ chartChanged: conf.chartChanged,
+ parameterChanged: conf.parameterChanged,
+
+ chart: chart, /** current chart */
+ axis: axis, /** persisted axis */
+ parameter: parsedParam, /** persisted parameter */
+ column: column,
+
+ transformer: transformer,
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/src/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js
index 820d2d6..5afc9ff 100644
--- a/zeppelin-web/src/index.js
+++ b/zeppelin-web/src/index.js
@@ -23,6 +23,7 @@ import './app/tabledata/transformation.js';
import './app/tabledata/pivot.js';
import './app/tabledata/passthrough.js';
import './app/tabledata/columnselector.js';
+import './app/tabledata/advanced-transformation.js';
import './app/visualization/visualization.js';
import './app/visualization/builtins/visualization-table.js';
import './app/visualization/builtins/visualization-nvd3chart.js';
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
index d0e9b00..5bdb14c 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
@@ -55,6 +55,7 @@ public class HeliumBundleFactory {
public static final String HELIUM_LOCAL_MODULE_DIR = "local_modules";
public static final String HELIUM_BUNDLES_SRC_DIR = "src";
public static final String HELIUM_BUNDLES_SRC = "load.js";
+ public static final String YARN_CACHE_DIR = "yarn-cache";
public static final String PACKAGE_JSON = "package.json";
public static final String HELIUM_BUNDLE_CACHE = "helium.bundle.cache.js";
public static final String HELIUM_BUNDLE = "helium.bundle.js";
@@ -68,6 +69,7 @@ public class HeliumBundleFactory {
private final File heliumLocalRepoDirectory;
private final File heliumBundleDirectory;
private final File heliumLocalModuleDirectory;
+ private final File yarnCacheDir;
private ZeppelinConfiguration conf;
private File tabledataModulePath;
private File visualizationModulePath;
@@ -98,6 +100,7 @@ public class HeliumBundleFactory {
this.heliumLocalRepoDirectory = new File(moduleDownloadPath, HELIUM_LOCAL_REPO);
this.heliumBundleDirectory = new File(heliumLocalRepoDirectory, HELIUM_BUNDLES_DIR);
this.heliumLocalModuleDirectory = new File(heliumLocalRepoDirectory, HELIUM_LOCAL_MODULE_DIR);
+ this.yarnCacheDir = new File(heliumLocalRepoDirectory, YARN_CACHE_DIR);
this.conf = conf;
this.defaultNpmRegistryUrl = conf.getHeliumNpmRegistry();
@@ -110,7 +113,7 @@ public class HeliumBundleFactory {
gson = new Gson();
}
- void installNodeAndNpm() {
+ void installNodeAndNpm() throws TaskRunnerException {
if (nodeAndNpmInstalled) {
return;
}
@@ -126,6 +129,8 @@ public class HeliumBundleFactory {
YarnInstaller yarnInstaller = frontEndPluginFactory.getYarnInstaller(getProxyConfig());
yarnInstaller.setYarnVersion(YARN_VERSION);
yarnInstaller.install();
+ String yarnCacheDirPath = yarnCacheDir.getAbsolutePath();
+ yarnCommand(frontEndPluginFactory, "config set cache-folder " + yarnCacheDirPath);
configureLogger();
nodeAndNpmInstalled = true;
@@ -362,7 +367,11 @@ public class HeliumBundleFactory {
}
// 0. install node, npm (should be called before `downloadPackage`
- installNodeAndNpm();
+ try {
+ installNodeAndNpm();
+ } catch (TaskRunnerException e) {
+ throw new IOException(e);
+ }
// 1. prepare directories
if (!heliumLocalRepoDirectory.exists() || !heliumLocalRepoDirectory.isDirectory()) {
@@ -391,7 +400,7 @@ public class HeliumBundleFactory {
prepareSource(pkg, moduleNameVersion, mainFileName);
// 4. install node and local modules for a bundle
- copyFrameworkModuleToInstallPath(recopyLocalModule); // should copy local modules first
+ copyFrameworkModulesToInstallPath(recopyLocalModule); // should copy local modules first
installNodeModules(fpf);
// 5. let's bundle and update cache
@@ -421,7 +430,44 @@ public class HeliumBundleFactory {
}
}
- void copyFrameworkModuleToInstallPath(boolean recopy)
+ void copyFrameworkModule(boolean recopy, FileFilter filter,
+ File src, File dest) throws IOException {
+ if (src != null) {
+ if (recopy && dest.exists()) {
+ FileUtils.deleteDirectory(dest);
+ }
+
+ if (!dest.exists()) {
+ FileUtils.copyDirectory(
+ src,
+ dest,
+ filter);
+ }
+ }
+ }
+
+ void deleteYarnCache() {
+ FilenameFilter filter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ if ((name.startsWith("npm-zeppelin-vis-") ||
+ name.startsWith("npm-zeppelin-tabledata-") ||
+ name.startsWith("npm-zeppelin-spell-")) &&
+ dir.isDirectory()) {
+ return true;
+ }
+
+ return false;
+ }
+ };
+
+ File[] localModuleCaches = yarnCacheDir.listFiles(filter);
+ for (File f : localModuleCaches) {
+ FileUtils.deleteQuietly(f);
+ }
+ }
+
+ void copyFrameworkModulesToInstallPath(boolean recopy)
throws IOException {
FileFilter npmPackageCopyFilter = new FileFilter() {
@@ -436,56 +482,27 @@ public class HeliumBundleFactory {
}
};
- // install tabledata module
FileUtils.forceMkdir(heliumLocalModuleDirectory);
+ // should delete yarn caches for local modules since they might be updated
+ deleteYarnCache();
+ // install tabledata module
File tabledataModuleInstallPath = new File(heliumLocalModuleDirectory,
"zeppelin-tabledata");
- if (tabledataModulePath != null) {
- if (recopy && tabledataModuleInstallPath.exists()) {
- FileUtils.deleteDirectory(tabledataModuleInstallPath);
-
- }
-
- if (!tabledataModuleInstallPath.exists()) {
- FileUtils.copyDirectory(
- tabledataModulePath,
- tabledataModuleInstallPath,
- npmPackageCopyFilter);
- }
- }
+ copyFrameworkModule(recopy, npmPackageCopyFilter,
+ tabledataModulePath, tabledataModuleInstallPath);
// install visualization module
File visModuleInstallPath = new File(heliumLocalModuleDirectory,
"zeppelin-vis");
- if (visualizationModulePath != null) {
- if (recopy && visModuleInstallPath.exists()) {
- FileUtils.deleteDirectory(visModuleInstallPath);
- }
-
- if (!visModuleInstallPath.exists()) {
- FileUtils.copyDirectory(
- visualizationModulePath,
- visModuleInstallPath,
- npmPackageCopyFilter);
- }
- }
+ copyFrameworkModule(recopy, npmPackageCopyFilter,
+ visualizationModulePath, visModuleInstallPath);
// install spell module
File spellModuleInstallPath = new File(heliumLocalModuleDirectory,
"zeppelin-spell");
- if (spellModulePath != null) {
- if (recopy && spellModuleInstallPath.exists()) {
- FileUtils.deleteDirectory(spellModuleInstallPath);
- }
-
- if (!spellModuleInstallPath.exists()) {
- FileUtils.copyDirectory(
- spellModulePath,
- spellModuleInstallPath,
- npmPackageCopyFilter);
- }
- }
+ copyFrameworkModule(recopy, npmPackageCopyFilter,
+ spellModulePath, spellModuleInstallPath);
}
private WebpackResult getWebpackResultFromOutput(String output) {
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
index d55358f..5feac1d 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
@@ -66,7 +66,7 @@ public class HeliumBundleFactoryTest {
new File(moduleDir, "visualization"),
new File(moduleDir, "spell"));
hbf.installNodeAndNpm();
- hbf.copyFrameworkModuleToInstallPath(true);
+ hbf.copyFrameworkModulesToInstallPath(true);
}
@After
[2/3] zeppelin git commit: [ZEPPELIN-2217] AdvancedTransformation for
Visualization
Posted by mo...@apache.org.
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
+ }
+}
[3/3] zeppelin git commit: [ZEPPELIN-2217] AdvancedTransformation for
Visualization
Posted by mo...@apache.org.
[ZEPPELIN-2217] AdvancedTransformation for Visualization
### What is this PR for?
`AdvancedTransformation` has more detailed options while providing existing features of `PivotTransformation` and `ColumnselectorTransformation` which Zeppelin already has
![av_in_30sec](https://cloud.githubusercontent.com/assets/4968473/24037330/c9478e86-0b40-11e7-9886-1ffb85042a7a.gif)
Here are some features which advanced-transformation can provide.
1. **(screenshot)** multiple sub charts
2. **(screenshot)** parameter widgets: `input`, `checkbox`, `option`, `textarea`
3. **(screenshot)** expand/fold axis and parameter panels
4. **(screenshot)** clear axis and parameter panels
5. **(screenshot)** remove duplicated columns in an axis
6. **(screenshot)** limit column count in an axis
7. configurable char axes: `valueType`, `axisType`, `description`, ...
8. configurable chart parameters
9. lazy transformation
10. parsing parameters automatically based on their type: `int`, `float`, `string`, `JSON`
11. multiple transformation methods
12. re-initialize whole configuration based on spec hash.
13. **(screenshot)** shared axis
#### API Details: Spec
`AdvancedTransformation` requires `spec` which includes axis and parameter details for charts.
- Let's create 2 sub-charts called `simple-line` and `step-line`.
- Each sub chart can have different `axis` and `parameter` depending on their requirements.
```js
constructor(targetEl, config) {
super(targetEl, config)
const spec = {
charts: {
'simple-line': {
sharedAxis: true, /** set if you want to share axes between sub charts, default is `false` */
axis: {
'xAxis': { dimension: 'multiple', axisType: 'key', },
'yAxis': { dimension: 'multiple', axisType: 'aggregator'},
'category': { dimension: 'multiple', axisType: 'group', },
},
parameter: {
'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
'yAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of yAxis', },
'dashLength': { valueType: 'int', defaultValue: 0, description: 'the length of dash', },
},
},
'step-line': {
axis: {
'xAxis': { dimension: 'single', axisType: 'unique', },
'yAxis': { dimension: 'multiple', axisType: 'value', },
},
parameter: {
'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
'yAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of yAxis', },
'noStepRisers': { valueType: 'boolean', defaultValue: false, description: 'no risers in step line', widget: 'checkbox', },
},
},
}
this.transformation = new AdvancedTransformation(config, spec)
}
```
#### API Details: Axis Spec
| Field Name | Available Values (type) | Description |
| --- | --- | --- |
|`dimension` | `single` | Axis can contains only 1 column |
|`dimension` | `multiple` | Axis can contains multiple columns |
|`axisType` | `key` | Column(s) in this axis will be used as `key` like in `PivotTransformation`. These columns will be served in `column.key` |
|`axisType` | `aggregator` | Column(s) in this axis will be used as `value` like in `PivotTransformation`. These columns will be served in `column.aggregator` |
|`axisType` | `group` | Column(s) in this axis will be used as `group` like in `PivotTransformation`. These columns will be served in `column.group` |
|`axisType` | (string) | Any string value can be used here. These columns will be served in `column.custom` |
|`maxAxisCount` | (int) | The maximum column count that this axis can contains. (unlimited if `undefined`) |
|`valueType` | (string) | Describe the value type just for annotation |
Here is an example.
```js
axis: {
'xAxis': { dimension: 'multiple', axisType: 'key', },
'yAxis': { dimension: 'multiple', axisType: 'aggregator'},
'category': { dimension: 'multiple', axisType: 'group', maxAxisCount: 2, valueType: 'string', },
},
```
#### API Details: Parameter Spec
| Field Name | Available Values (type) | Description |
| --- | --- | --- |
|`valueType` | `string` | Parameter which has string value |
|`valueType` | `int` | Parameter which has int value |
|`valueType` | `float` | Parameter which has float value |
|`valueType` | `boolean` | Parameter which has boolean value used with `checkbox` widget usually |
|`valueType` | `JSON` | Parameter which has JSON value used with `textarea` widget usually. `defaultValue` should be `""` (empty string). This ||`defaultValue` | (any) | Default value of this parameter. `JSON` type should have `""` (empty string) |
|`description` | (string) | Description of this parameter. This value will be parsed as HTML for pretty output |
|`widget` | `input` | Use [input](https://developer.mozilla.org/en/docs/Web/HTML/Element/input) widget. This is the default widget (if `widget` is undefined)|
|`widget` | `checkbox` | Use [checkbox](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox) widget. |
|`widget` | `textarea` | Use [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) widget. |
|`widget` | `option` | Use [select + option](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) widget. This parameter should have `optionValues` field as well. |
|`optionValues` | (Array<string>) | Available option values used with the `option` widget |
Here is an example.
```js
parameter: {
// string type, input widget
'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
// boolean type, checkbox widget
'inverted': { widget: 'checkbox', valueType: 'boolean', defaultValue: false, description: 'invert x and y axes', },
// string type, option widget with `optionValues`
'graphType': { widget: 'option', valueType: 'string', defaultValue: 'line', description: 'graph type', optionValues: [ 'line', 'smoothedLine', 'step', ], },
// HTML in `description`
'dateFormat': { valueType: 'string', defaultValue: '', description: 'format of date (<a href="https://docs.amcharts.com/3/javascriptcharts/AmGraph#dateFormat">doc</a>) (e.g YYYY-MM-DD)', },
// JSON type, textarea widget
'yAxisGuides': { widget: 'textarea', valueType: 'JSON', defaultValue: '', description: 'guides of yAxis ', },
```
#### API Details: Transformer Spec
`AdvancedTransformation` supports 3 transformation methods. The return value will depend on the transformation method type.
```js
const spec = {
charts: {
'simple': {
/** default value of `transform.method` is the flatten cube. */
axis: { ... },
parameter: { ... }
},
'cube-group': {
transform: { method: 'cube', },
axis: { ... },
parameter: { ... },
}
'no-group': {
transform: { method: 'raw', },
axis: { ... },
parameter: { ... },
}
```
| Field Name | Available Values (type) | Description |
| --- | --- | --- |
|`method` | `object` | designed for [amcharts: serial](https://www.amcharts.com/demos/date-based-data/) |
|`method` | `array` | designed for [highcharts: column](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/demo/column-basic/) |
|`method` | `drill-down` | designed for [highcharts: drill-down](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/demo/column-drilldown/) |
|`method` | `raw` | will return the original `tableData.rows` |
Whatever you specified as `transform.method`, the `transformer` value will be always function for lazy computation.
```js
// advanced-transformation.util#getTransformer
if (transformSpec.method === 'raw') {
transformer = () => { return rows; }
} else if (transformSpec.method === 'array') {
transformer = () => {
...
return { ... }
}
}
```
#### Feature Details: Automatic parameter parsing
Advanced transformation will parse parameter values automatically based on their type: `int`, `float`, `string`, `JSON`
- See also `advanced-transformation-util.js#parseParameter`
#### Feature Details: re-initialize the whole configuration based on spec hash
```js
// advanced-transformation-util#initializeConfig
const currentVersion = JSON.stringify(spec)
if (!config.spec || !config.spec.version || config.spec.version !== currentVersion) {
spec.version = currentVersion
// reset config...
}
```
#### Feature Details: Shared Axes
If you set `sharedAxis` to `true` in chart specification, then these charts will share their axes. (default is `false`)
```js
const spec = {
charts: {
'column': {
transform: { method: 'array', },
sharedAxis: true,
axis: { ... },
parameter: { ... },
},
'stacked': {
transform: { method: 'array', },
sharedAxis: true,
axis: { ... }
parameter: { ... },
},
```
![sharedaxis](https://cloud.githubusercontent.com/assets/4968473/24207116/6999ad8a-0f63-11e7-8b61-273b712612fc.gif)
#### API Details: Usage in Visualization#render()
Let's assume that we want to create 2 sub-charts called `basic` and `no-group`.
- https://github.com/1ambda/zeppelin-ultimate-line-chart (an practical example)
```js
drawBasicChart(parameter, column, transformer) {
const { ... } = transformer()
}
drawNoGroupChart(parameter, column, transformer) {
const { ... } = transformer()
}
render(data) {
const { chart, parameter, column, transformer, } = data
if (chart === 'basic') {
this.drawBasicChart(parameter, column, transformer)
} else if (chart === 'no-group') {
this.drawNoGroupChart(parameter, column, transformer)
}
}
```
### What type of PR is it?
[Feature]
### Todos
NONE
### What is the Jira issue?
[ZEPPELIN-2217](https://issues.apache.org/jira/browse/ZEPPELIN-2217)
### How should this be tested?
1. Clone https://github.com/1ambda/zeppelin-ultimate-line-chart
2. Create a symbolic link `ultimate-line-chart.json` into `$ZEPPELIN_HOME/helium`
3. Modify the `artifact` value to proper absolute path considering your local machine.
4. Install the above visualization in `localhost:9000/#helium`
5. Test it
### Screenshots (if appropriate)
#### 1. *(screenshot)* multiple sub charts
![av_multiple_charts](https://cloud.githubusercontent.com/assets/4968473/24034638/7b84dba0-0b35-11e7-989d-059ccc87f968.gif)
#### 2. *(screenshot)* parameter widgets: `input`, `checkbox`, `option`, `textarea`
![av_widgets_new](https://cloud.githubusercontent.com/assets/4968473/24034652/88679d6c-0b35-11e7-835a-3970d7124850.gif)
#### 3. *(screenshot)* expand/fold axis and parameter panels
![av_fold_expand](https://cloud.githubusercontent.com/assets/4968473/24034653/8a634ddc-0b35-11e7-9851-15280a6b5fd3.gif)
#### 4. *(screenshot)* clear axis and parameter panels
![av_clean_buttons](https://cloud.githubusercontent.com/assets/4968473/24034654/8d3dc14a-0b35-11e7-98c7-3aeddce6d80a.gif)
#### 5. *(screenshot)* remove duplicated columns in an axis
![av_duplicated_columns](https://cloud.githubusercontent.com/assets/4968473/24034657/910f4d20-0b35-11e7-9e9b-d9e2f799a5dd.gif)
#### 6. *(screenshot)* limit column count in an axis
![av_maxaxiscount](https://cloud.githubusercontent.com/assets/4968473/24034679/a5e8eb34-0b35-11e7-89cd-070f3790d511.gif)
### Questions:
* Does the licenses files need update? - NO
* Is there breaking changes for older versions? - NO
* Does this needs documentation? - NO
Author: 1ambda <1a...@gmail.com>
Closes #2098 from 1ambda/ZEPPELIN-2217/advanced-transformation and squashes the following commits:
6cde7c9 [1ambda] fix reset params when spec change
c75a3f2 [1ambda] fix: Reset persisted axis
6a2130a [1ambda] fix: clear config only when axis changed
5464e84 [1ambda] fix: Optimize array 2 key method
9beb1e7 [1ambda] fix: Type error
2408225 [1ambda] test: Add test for array 2key
bf56761 [1ambda] feat: Add array:2-key transform method
7c6768f [1ambda] feat: Use axisSpec.desc as tooltip
f98d4c9 [1ambda] fix: Remove invalid key prop
5cf2ece [1ambda] feat: Add minAxisCount
4887800 [1ambda] fix: Remove local module yarn caches
3e29572 [1ambda] refactor: copyModule func
c91a033 [1ambda] fix: Set yarn cache dir in helium-bundles
04b5140 [1ambda] fix: Import a-tr
0a876cf [1ambda] docs: Update index.md
380b1af [1ambda] docs: Fix typo and add desc for existing trs
908214b [1ambda] docs: Move experimental tags
a009627 [1ambda] feat: Allow dup aggr axis
3b44e92 [1ambda] fix: Remove unuse const
ab6c22e [1ambda] test: Add test for drill-down method
756107a [1ambda] test: Add array transformation method
d819c73 [1ambda] test: Add object method
bf00fba [1ambda] test: Add MockTableData
39fe5ae [1ambda] test: Add test for getColumnsFromAxis
4c393b4 [1ambda] fix: Add polyfill for es6 funcs in test
e92c787 [1ambda] test: Add test for rmDup, aplMaxAxisCount
843f45d [1ambda] test: Add test for getCurrent* funcs
ae5277c [1ambda] test: Add test for initializeConfig
c14a9dc7 [1ambda] test: Add tests for widget, params
c510af1 [1ambda] docs: Add doc for Transformation
52db37b [1ambda] feat: Show panel menus only when opened
17ad4a4 [1ambda] feat: Support chartChanged, parameterChanged
c0d33d3 [1ambda] fix: Sort selectors in drilldown method
cfd6fef [1ambda] feat: sharedAxis
9af80ce [1ambda] style: Indent
79b5654 [1ambda] fix: return the same info in transform
7bee464 [1ambda] fix: Keynames
ee8788e [1ambda] feat: Support drill-down
666025a [1ambda] fix: DON'T reset current chart
ae1891f [1ambda] add array:key transform
4167a2e [1ambda] fix: Sort keyNames
912b5b7 [1ambda] fix: Persist initialized config
f1f6b0c [1ambda] feat: Support ARRAY transform.method
812f9a2 [1ambda] fix: Set proper aggr value when 0 group
20f9437 [1ambda] fix: getCube func
25d51a9 [1ambda] DON'T display aggr.name when aggrColumns.length == 1
f37e13d [1ambda] fix: Add 'object' transform.method
da2370c [1ambda] fix: Add resetAxis, Param funcs
2370682 [1ambda] fix: average is not caculated correctly
dd08e38 [1ambda] fix: Set param panel height to 400
881695a [1ambda] feat: clear chart, param separately
4d0d62b [1ambda] fix: DON'T clean panel config
92676d1 [1ambda] fix: limit parameter panel height to 370
cc29060 [1ambda] feat: parse param description as HTML
9a2d227 [1ambda] fix: Stop event propagation in widgets
fcc625c [1ambda] feat: Automatic param parsing
b4d774c [1ambda] fix: Dont close param panel when enter
088705b [1ambda] refactor: Remove util and add Widget funcs
bf88b4f [1ambda] feat: textare widget and update hook
4e73012 [1ambda] feat: widget checkbox
11b7eaa [1ambda] feat: option widget
5d3efc9 [1ambda] fix: Change panel header
b1d9d31 [1ambda] feat: Save and close with enter key
53f508c [1ambda] feat: custom axisSpec
0dbc431 [1ambda] feat: Support transformer
94d837a [1ambda] feat: Automatic spec versioning
74b8b4e [1ambda] fix: Duplicated radio btn id, name
5b88f08 [1ambda] fix: Modify margin of subchart radio btns
019892c [1ambda] feat: Support transform: flatten
0484e1e [1ambda] feat: Support maxAxisCount in axisSpec
936901b [1ambda] feat: Support undefined valueType in axisSpec
7a454ff [1ambda] feat: Cube Transformation
f0ed02f [1ambda] feat: Support same axis types
49985c6 [1ambda] refactor: Refine axis, param spec
d89e223 [1ambda] feat: advanced-transformation-api
75569ce [1ambda] feat: Support multiple charts in UI
e1fcc2e [1ambda] feat: Support multiple charts
97be629 [1ambda] fix: Add singleDimensionAggregatorChanged
676bd7e [1ambda] refactor: Refine transform API
9fb398e [1ambda] feat: Add clearConfig
a8a4fb1 [1ambda] refactor: Add getAxisInSingleDimension func
9768ecf [1ambda] feat: Add groupBase axis option
91ae54d [1ambda] fix: Overflow issue in single aggr
10c80fc [1ambda] feat: AdvancedTransformation
Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/45cc8a9e
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/45cc8a9e
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/45cc8a9e
Branch: refs/heads/master
Commit: 45cc8a9e8a23a57271dec384245d0012a0e5e608
Parents: 2173b40
Author: 1ambda <1a...@gmail.com>
Authored: Tue Apr 11 23:14:46 2017 +0900
Committer: Lee moon soo <mo...@apache.org>
Committed: Sat Apr 15 07:34:27 2017 +0900
----------------------------------------------------------------------
docs/_includes/themes/zeppelin/_navigation.html | 9 +-
docs/development/writingzeppelinapplication.md | 4 +-
docs/development/writingzeppelinspell.md | 4 +-
.../development/writingzeppelinvisualization.md | 4 +-
...itingzeppelinvisualization_transformation.md | 281 +++
docs/index.md | 9 +-
zeppelin-web/karma.conf.js | 6 +-
zeppelin-web/package.json | 2 +-
.../advanced-transformation-setting.html | 280 +++
.../tabledata/advanced-transformation-util.js | 1259 +++++++++++++
.../advanced-transformation-util.test.js | 1746 ++++++++++++++++++
.../app/tabledata/advanced-transformation.js | 230 +++
zeppelin-web/src/index.js | 1 +
.../zeppelin/helium/HeliumBundleFactory.java | 101 +-
.../helium/HeliumBundleFactoryTest.java | 2 +-
15 files changed, 3878 insertions(+), 60 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/_includes/themes/zeppelin/_navigation.html
----------------------------------------------------------------------
diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html
index 623fac3..4e49a1a 100644
--- a/docs/_includes/themes/zeppelin/_navigation.html
+++ b/docs/_includes/themes/zeppelin/_navigation.html
@@ -117,10 +117,11 @@
<li><a href="{{BASE_PATH}}/security/notebook_authorization.html">Notebook Authorization</a></li>
<li><a href="{{BASE_PATH}}/security/datasource_authorization.html">Data Source Authorization</a></li>
<li role="separator" class="divider"></li>
- <li class="title"><span><b>Helium Framework</b><span></li>
- <li><a href="{{BASE_PATH}}/development/writingzeppelinapplication.html">Writing Zeppelin Application (Experimental)</a></li>
- <li><a href="{{BASE_PATH}}/development/writingzeppelinspell.html">Writing Zeppelin Spell (Experimental)</a></li>
- <li><a href="{{BASE_PATH}}/development/writingzeppelinvisualization.html">Writing Zeppelin Visualization (Experimental)</a></li>
+ <li class="title"><span><b>Helium Framework (Experimental)</b></span></li>
+ <li><a href="{{BASE_PATH}}/development/writingzeppelinapplication.html">Writing Zeppelin Application</a></li>
+ <li><a href="{{BASE_PATH}}/development/writingzeppelinspell.html">Writing Zeppelin Spell</a></li>
+ <li><a href="{{BASE_PATH}}/development/writingzeppelinvisualization.html">Writing Zeppelin Visualization: Basics</a></li>
+ <li><a href="{{BASE_PATH}}/development/writingzeppelinvisualization_transformation.html">Writing Zeppelin Visualization: Transformation</a></li>
<li role="separator" class="divider"></li>
<li class="title"><span><b>Advanced</b><span></li>
<li><a href="{{BASE_PATH}}/install/virtual_machine.html">Zeppelin on Vagrant VM</a></li>
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/development/writingzeppelinapplication.md
----------------------------------------------------------------------
diff --git a/docs/development/writingzeppelinapplication.md b/docs/development/writingzeppelinapplication.md
index 7048cb3..59f980a 100644
--- a/docs/development/writingzeppelinapplication.md
+++ b/docs/development/writingzeppelinapplication.md
@@ -1,6 +1,6 @@
---
layout: page
-title: "Writing a new Application(Experimental)"
+title: "Writing a new Application"
description: "Apache Zeppelin Application is a package that runs on Interpreter process and displays it's output inside of the notebook. Make your own Application in Apache Zeppelin is quite easy."
group: development
---
@@ -19,7 +19,7 @@ limitations under the License.
-->
{% include JB/setup %}
-# Writing a new Application (Experimental)
+# Writing a new Application
<div id="toc"></div>
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/development/writingzeppelinspell.md
----------------------------------------------------------------------
diff --git a/docs/development/writingzeppelinspell.md b/docs/development/writingzeppelinspell.md
index 02a2301..1eefe83 100644
--- a/docs/development/writingzeppelinspell.md
+++ b/docs/development/writingzeppelinspell.md
@@ -1,6 +1,6 @@
---
layout: page
-title: "Writing a new Spell(Experimental)"
+title: "Writing a new Spell"
description: "Spell is a kind of interpreter that runs on browser not on backend. So, technically it's the frontend interpreter. "
group: development
---
@@ -19,7 +19,7 @@ limitations under the License.
-->
{% include JB/setup %}
-# Writing a new Spell (Experimental)
+# Writing a new Spell
<div id="toc"></div>
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/development/writingzeppelinvisualization.md
----------------------------------------------------------------------
diff --git a/docs/development/writingzeppelinvisualization.md b/docs/development/writingzeppelinvisualization.md
index 5ccd970..5a0601f 100644
--- a/docs/development/writingzeppelinvisualization.md
+++ b/docs/development/writingzeppelinvisualization.md
@@ -1,6 +1,6 @@
---
layout: page
-title: "Writing a new Visualization(Experimental)"
+title: "Writing a new Visualization"
description: "Apache Zeppelin Visualization is a pluggable package that can be loaded/unloaded on runtime through Helium framework in Zeppelin. A Visualization is a javascript npm package and user can use them just like any other built-in visualization in a note."
group: development
---
@@ -19,7 +19,7 @@ limitations under the License.
-->
{% include JB/setup %}
-# Writing a new Visualization (Experimental)
+# Writing a new Visualization
<div id="toc"></div>
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/development/writingzeppelinvisualization_transformation.md
----------------------------------------------------------------------
diff --git a/docs/development/writingzeppelinvisualization_transformation.md b/docs/development/writingzeppelinvisualization_transformation.md
new file mode 100644
index 0000000..22bf130
--- /dev/null
+++ b/docs/development/writingzeppelinvisualization_transformation.md
@@ -0,0 +1,281 @@
+---
+layout: page
+title: "Transformations for Zeppelin Visualization"
+description: "Description for Transformations"
+group: development
+---
+<!--
+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.
+-->
+{% include JB/setup %}
+
+# Transformations for Zeppelin Visualization
+
+<div id="toc"></div>
+
+## Overview
+
+Transformations
+
+- **renders** setting which allows users to set columns and
+- **transforms** table rows according to the configured columns.
+
+Zeppelin provides 4 types of transformations.
+
+## 1. PassthroughTransformation
+
+`PassthroughTransformation` is the simple transformation which does not convert original tabledata at all.
+
+See [passthrough.js](https://github.com/apache/zeppelin/blob/master/zeppelin-web/src/app/tabledata/passthrough.js)
+
+## 2. ColumnselectorTransformation
+
+`ColumnselectorTransformation` is uses when you need `N` axes but do not need aggregation.
+
+See [columnselector.js](https://github.com/apache/zeppelin/blob/master/zeppelin-web/src/app/tabledata/columnselector.js)
+
+## 3. PivotTransformation
+
+`PivotTransformation` provides group by and aggregation. Every chart using `PivotTransformation` has 3 axes. `Keys`, `Groups` and `Values`.
+
+See [pivot.js](https://github.com/apache/zeppelin/blob/master/zeppelin-web/src/app/tabledata/pivot.js)
+
+## 4. AdvancedTransformation
+
+`AdvancedTransformation` has more detailed options while providing existing features of `PivotTransformation` and `ColumnselectorTransformation`
+
+- multiple sub charts
+- configurable chart axes
+- parameter widgets: `input`, `checkbox`, `option`, `textarea`
+- parsing parameters automatically based on their types
+- expand / fold axis and parameter panels
+- multiple transformation methods while supporting lazy converting
+- re-initialize the whole configuration based on spec hash.
+
+### Spec
+
+`AdvancedTransformation` requires `spec` which includes axis and parameter details for charts.
+
+Let's create 2 sub-charts called `line` and `no-group`. Each sub chart can have different axis and parameter depending on their requirements.
+
+<br/>
+
+```js
+class AwesomeVisualization extends Visualization {
+ constructor(targetEl, config) {
+ super(targetEl, config)
+
+ const spec = {
+ charts: {
+ 'line': {
+ transform: { method: 'object', },
+ sharedAxis: false, /** set if you want to share axes between sub charts, default is `false` */
+ axis: {
+ 'xAxis': { dimension: 'multiple', axisType: 'key', description: 'serial', },
+ 'yAxis': { dimension: 'multiple', axisType: 'aggregator', description: 'serial', },
+ 'category': { dimension: 'multiple', axisType: 'group', description: 'categorical', },
+ },
+ parameter: {
+ 'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
+ 'yAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of yAxis', },
+ 'lineWidth': { valueType: 'int', defaultValue: 0, description: 'width of line', },
+ },
+ },
+
+ 'no-group': {
+ transform: { method: 'object', },
+ sharedAxis: false,
+ axis: {
+ 'xAxis': { dimension: 'single', axisType: 'key', },
+ 'yAxis': { dimension: 'multiple', axisType: 'value', },
+ },
+ parameter: {
+ 'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
+ 'yAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of yAxis', },
+ },
+ },
+ }
+
+ this.transformation = new AdvancedTransformation(config, spec)
+ }
+
+ ...
+
+ // `render` will be called whenever `axis` or `parameter` is changed
+ render(data) {
+ const { chart, parameter, column, transformer, } = data
+
+ if (chart === 'line') {
+ const transformed = transformer()
+ // draw line chart
+ } else if (chart === 'no-group') {
+ const transformed = transformer()
+ // draw no-group chart
+ }
+ }
+}
+```
+
+<br/>
+
+### Spec: `axis`
+
+| Field Name | Available Values (type) | Description |
+| --- | --- | --- |
+|`dimension` | `single` | Axis can contains only 1 column |
+|`dimension` | `multiple` | Axis can contains multiple columns |
+|`axisType` | `key` | Column(s) in this axis will be used as `key` like in `PivotTransformation`. These columns will be served in `column.key` |
+|`axisType` | `aggregator` | Column(s) in this axis will be used as `value` like in `PivotTransformation`. These columns will be served in `column.aggregator` |
+|`axisType` | `group` | Column(s) in this axis will be used as `group` like in `PivotTransformation`. These columns will be served in `column.group` |
+|`axisType` | (string) | Any string value can be used here. These columns will be served in `column.custom` |
+|`maxAxisCount` (optional) | (int) | The max number of columns that this axis can contain. (unlimited if `undefined`) |
+|`minAxisCount` (optional) | (int) | The min number of columns that this axis should contain to draw chart. (`1` in case of single dimension) |
+|`description` (optional) | (string) | Description for the axis. |
+
+<br/>
+
+Here is an example.
+
+```js
+axis: {
+ 'xAxis': { dimension: 'multiple', axisType: 'key', },
+ 'yAxis': { dimension: 'multiple', axisType: 'aggregator'},
+ 'category': { dimension: 'multiple', axisType: 'group', maxAxisCount: 2, valueType: 'string', },
+},
+```
+
+<br/>
+
+### Spec: `sharedAxis`
+
+If you set `sharedAxis: false` for sub charts, then their axes are persisted in global space (shared). It's useful for when you creating multiple sub charts sharing their axes but have different parameters. For example,
+
+- `basic-column`, `stacked-column`, `percent-column`
+- `pie` and `donut`
+
+<br/>
+
+Here is an example.
+
+```js
+ const spec = {
+ charts: {
+ 'column': {
+ transform: { method: 'array', },
+ sharedAxis: true,
+ axis: { ... },
+ parameter: { ... },
+ },
+
+ 'stacked': {
+ transform: { method: 'array', },
+ sharedAxis: true,
+ axis: { ... }
+ parameter: { ... },
+ },
+```
+
+<br/>
+
+### Spec: `parameter`
+
+| Field Name | Available Values (type) | Description |
+| --- | --- | --- |
+|`valueType` | `string` | Parameter which has string value |
+|`valueType` | `int` | Parameter which has int value |
+|`valueType` | `float` | Parameter which has float value |
+|`valueType` | `boolean` | Parameter which has boolean value used with `checkbox` widget usually |
+|`valueType` | `JSON` | Parameter which has JSON value used with `textarea` widget usually. `defaultValue` should be `""` (empty string). This ||`defaultValue` | (any) | Default value of this parameter. `JSON` type should have `""` (empty string) |
+|`description` | (string) | Description of this parameter. This value will be parsed as HTML for pretty output |
+|`widget` | `input` | Use [input](https://developer.mozilla.org/en/docs/Web/HTML/Element/input) widget. This is the default widget (if `widget` is undefined)|
+|`widget` | `checkbox` | Use [checkbox](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox) widget. |
+|`widget` | `textarea` | Use [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) widget. |
+|`widget` | `option` | Use [select + option](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) widget. This parameter should have `optionValues` field as well. |
+|`optionValues` | (Array<string>) | Available option values used with the `option` widget |
+
+<br/>
+
+Here is an example.
+
+```js
+parameter: {
+ // string type, input widget
+ 'xAxisUnit': { valueType: 'string', defaultValue: '', description: 'unit of xAxis', },
+
+ // boolean type, checkbox widget
+ 'inverted': { widget: 'checkbox', valueType: 'boolean', defaultValue: false, description: 'invert x and y axes', },
+
+ // string type, option widget with `optionValues`
+ 'graphType': { widget: 'option', valueType: 'string', defaultValue: 'line', description: 'graph type', optionValues: [ 'line', 'smoothedLine', 'step', ], },
+
+ // HTML in `description`
+ 'dateFormat': { valueType: 'string', defaultValue: '', description: 'format of date (<a href="https://docs.amcharts.com/3/javascriptcharts/AmGraph#dateFormat">doc</a>) (e.g YYYY-MM-DD)', },
+
+ // JSON type, textarea widget
+ 'yAxisGuides': { widget: 'textarea', valueType: 'JSON', defaultValue: '', description: 'guides of yAxis ', },
+```
+
+<br/>
+
+### Spec: `transform`
+
+| Field Name | Available Values (type) | Description |
+| --- | --- | --- |
+|`method` | `object` | designed for rows requiring object manipulation |
+|`method` | `array` | designed for rows requiring array manipulation |
+|`method` | `array:2-key` | designed for xyz charts (e.g bubble chart) |
+|`method` | `drill-down` | designed for drill-down charts |
+|`method` | `raw` | will return the original `tableData.rows` |
+
+<br/>
+
+Whatever you specified as `transform.method`, the `transformer` value will be always function for lazy computation.
+
+```js
+// advanced-transformation.util#getTransformer
+
+if (transformSpec.method === 'raw') {
+ transformer = () => { return rows; }
+} else if (transformSpec.method === 'array') {
+ transformer = () => {
+ ...
+ return { ... }
+ }
+}
+```
+
+Here is actual usage.
+
+```js
+class AwesomeVisualization extends Visualization {
+ constructor(...) { /** setup your spec */ }
+
+ ...
+
+ // `render` will be called whenever `axis` or `parameter` are changed
+ render(data) {
+ const { chart, parameter, column, transformer, } = data
+
+ if (chart === 'line') {
+ const transformed = transformer()
+ // draw line chart
+ } else if (chart === 'no-group') {
+ const transformed = transformer()
+ // draw no-group chart
+ }
+ }
+
+ ...
+}
+```
+
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/docs/index.md
----------------------------------------------------------------------
diff --git a/docs/index.md b/docs/index.md
index 8065aef..043538d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -172,10 +172,11 @@ Join to our [Mailing list](https://zeppelin.apache.org/community.html) and repor
* [Shiro Authentication](./security/shiroauthentication.html)
* [Notebook Authorization](./security/notebook_authorization.html)
* [Data Source Authorization](./security/datasource_authorization.html)
-* Helium Framework
- * [Writing Zeppelin Application (Experimental)](./development/writingzeppelinapplication.html)
- * [Writing Zeppelin Spell (Experimental)](./development/writingzeppelinspell.html)
- * [Writing Zeppelin Visualization (Experimental)](./development/writingzeppelinvisualization.html)
+* Helium Framework (Experimental)
+ * [Writing Zeppelin Application](./development/writingzeppelinapplication.html)
+ * [Writing Zeppelin Spell](./development/writingzeppelinspell.html)
+ * [Writing Zeppelin Visualization: Basic](./development/writingzeppelinvisualization.html)
+ * [Writing Zeppelin Visualization: Transformation](./development/writingzeppelinvisualization_transformation.html)
* Advanced
* [Apache Zeppelin on Vagrant VM](./install/virtual_machine.html)
* [Zeppelin on Spark Cluster Mode (Standalone via Docker)](./install/spark_cluster_mode.html#spark-standalone-mode)
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/karma.conf.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js
index b24e36a..f47741a 100644
--- a/zeppelin-web/karma.conf.js
+++ b/zeppelin-web/karma.conf.js
@@ -37,6 +37,9 @@ module.exports = function(config) {
// list of files / patterns to load in the browser
files: [
+ // for polyfill
+ 'node_modules/babel-polyfill/dist/polyfill.js',
+
// bower:js
'bower_components/jquery/dist/jquery.js',
'bower_components/es5-shim/es5-shim.js',
@@ -124,8 +127,7 @@ module.exports = function(config) {
preprocessors: {
'src/*/{*.js,!(test)/**/*.js}': 'coverage',
- 'src/index.js': ['webpack', 'sourcemap',],
- 'src/**/*.test.js': ['webpack', 'sourcemap',],
+ 'src/**/*.js': ['webpack', 'sourcemap',],
},
coverageReporter: {
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-web/package.json b/zeppelin-web/package.json
index 6e1252b..99dc058 100644
--- a/zeppelin-web/package.json
+++ b/zeppelin-web/package.json
@@ -16,7 +16,7 @@
"dev:watch": "grunt watch-webpack-dev",
"dev": "npm-run-all --parallel dev:server dev:watch",
"visdev": "npm-run-all --parallel visdev:server dev:watch",
- "pretest": "npm install karma-phantomjs-launcher",
+ "pretest": "npm install karma-phantomjs-launcher babel-polyfill",
"test": "karma start karma.conf.js"
},
"dependencies": {
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/45cc8a9e/zeppelin-web/src/app/tabledata/advanced-transformation-setting.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-setting.html b/zeppelin-web/src/app/tabledata/advanced-transformation-setting.html
new file mode 100644
index 0000000..8393bf3
--- /dev/null
+++ b/zeppelin-web/src/app/tabledata/advanced-transformation-setting.html
@@ -0,0 +1,280 @@
+<!--
+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.
+-->
+
+<div class="panel panel-default" style="margin-top: 10px; margin-bottom: 11px;">
+
+ <!-- panel: axis (configured column) information -->
+ <div class="panel-heading"
+ style="padding: 6px 12px 6px 12px; font-size: 13px;">
+ <span style="vertical-align: middle; display: inline-block; margin-top: 3px;">Charts</span>
+ <span style="float: right;">
+ <div class="btn-group" role="group" aria-label="...">
+ <div type="button" ng-click="resetAxisConfig()"
+ ng-if="config.panel.columnPanelOpened"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </div>
+ <div type="button" ng-if="config.panel.columnPanelOpened"
+ ng-click="toggleColumnPanel()"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-minus" style="font-size: 12px;" aria-hidden="true"></i>
+ </div>
+ <div type="button" ng-if="!config.panel.columnPanelOpened"
+ ng-click="toggleColumnPanel()"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-expand" style="font-size: 11px;" aria-hidden="true"></i>
+ </div>
+ </div>
+ </span>
+ <div style="clear: both;"></div> <!-- to fix previous span which has float: right -->
+ </div>
+ <div class="panel-body" ng-if="config.panel.columnPanelOpened"
+ style="padding: 8px; margin-top: 3px;">
+ <ul class="noDot">
+ <li class="liVertical" ng-repeat="chart in config.chart.available">
+ <label class="radio-inline">
+ <input type="radio" style="margin-top: 1px; margin-left: -17px;"
+ ng-checked="config.chart.current === chart"
+ ng-click="chartChanged(chart)" value="{{chart}}" />
+ <span style="vertical-align: middle;">
+ {{chart}} {{useSharedAxis(chart) ? '(shared)' : ''}}
+ </span>
+ </label>
+ </li>
+ </ul>
+ </div>
+
+ <!-- panel: available columns -->
+ <div class="panel-heading" ng-if="config.panel.columnPanelOpened"
+ style="padding: 6px 12px 6px 12px; font-size: 13px; border-top: 1px solid #ddd; border-top-left-radius: 0px; border-top-right-radius: 0px;">
+ <span>Available Columns</span>
+ </div>
+ <div class="panel-body" ng-if="config.panel.columnPanelOpened"
+ style="padding: 8px; margin-top: 3px;">
+ <ul class="noDot">
+ <li class="liVertical" ng-repeat="column in columns">
+ <div class="btn btn-default btn-xs"
+ style="background-color: #EFEFEF;"
+ data-drag="true"
+ data-jqyoui-options="{revert: 'invalid', helper: 'clone'}"
+ ng-model="columns"
+ jqyoui-draggable="{index: {{$index}}, placeholder: 'keep'}">
+ {{column.name | limitTo: 30}}{{column.name.length > 30 ? '...' : ''}}
+ </div>
+ </li>
+ </ul>
+ </div>
+
+ <!-- panel: axis (configured columns) -->
+ <hr style="margin: 1px;" ng-if="config.panel.columnPanelOpened" />
+ <div class="panel-body" ng-if="config.panel.columnPanelOpened"
+ style="margin-top: 7px; padding-top: 9px; padding-bottom: 4px;">
+ <div class="row">
+ <div class="col-sm-4 col-md-3"
+ ng-repeat="axisSpec in config.axisSpecs[config.chart.current]">
+ <div class="columns lightBold">
+ <!-- axis name -->
+ <span class="label label-default"
+ uib-tooltip="{{axisSpec.description ? axisSpec.description : ''}}"
+ style="font-weight: 300; font-size: 13px; margin-left: 1px;">
+ {{getAxisAnnotation(axisSpec)}}
+ </span>
+ <span class="label label-default"
+ ng-style="getAxisTypeAnnotationColor(axisSpec)"
+ style="font-weight: 300; font-size: 13px; margin-left: 3px;">
+ {{getAxisTypeAnnotation(axisSpec)}}
+ </span>
+
+ <!-- axis box: in case of single dimension -->
+ <ul data-drop="true"
+ ng-if="isSingleDimensionAxis(axisSpec)"
+ ng-model="config.axis[config.chart.current][axisSpec.name]"
+ jqyoui-droppable="{onDrop:'axisChanged(axisSpec)'}"
+ class="list-unstyled"
+ style="height:36px; border-radius: 6px; margin-top: 7px; overflow: visible !important;">
+ <li ng-if="config.axis[config.chart.current][axisSpec.name]">
+
+ <!-- in case of axis is single dimension and not aggregator -->
+ <div ng-if="!isAggregatorAxis(axisSpec)"
+ class="btn btn-default btn-xs"
+ style="background-color: #EFEFEF;">
+ {{ getSingleDimensionAxis(axisSpec).name }}
+ <span class="fa fa-close" ng-click="removeFromAxis(null, axisSpec)"></span>
+ </div>
+
+ <!-- in case of axis is single dimension and aggregator -->
+ <div class="btn-group">
+ <div ng-if="isAggregatorAxis(axisSpec)"
+ class="btn btn-default btn-xs dropdown-toggle"
+ style="background-color: #EFEFEF; "
+ type="button" data-toggle="dropdown">
+ {{getSingleDimensionAxis(axisSpec).name | limitTo: 30}}{{getSingleDimensionAxis(axisSpec).name > 30 ? '...' : ''}}
+ <span style="color:#717171;">
+ <span class="lightBold" style="text-transform: uppercase;">{{getSingleDimensionAxis(axisSpec).aggr}}</span>
+ </span>
+ <span class="fa fa-close" ng-click="removeFromAxis(null, axisSpec)"></span>
+ </div>
+ <ul class="dropdown-menu" role="menu">
+ <li ng-click="aggregatorChanged(null, axisSpec, 'sum')"><a>sum</a></li>
+ <li ng-click="aggregatorChanged(null, axisSpec, 'count')"><a>count</a></li>
+ <li ng-click="aggregatorChanged(null, axisSpec, 'avg')"><a>avg</a></li>
+ <li ng-click="aggregatorChanged(null, axisSpec, 'min')"><a>min</a></li>
+ <li ng-click="aggregatorChanged(null, axisSpec, 'max')"><a>max</a></li>
+ </ul>
+ </div>
+
+ </li>
+ </ul>
+
+ <!-- axis box: in case of multiple dimensions -->
+ <ul data-drop="true"
+ ng-if="!isSingleDimensionAxis(axisSpec) "
+ ng-model="config.axis[config.chart.current][axisSpec.name]"
+ jqyoui-droppable="{multiple: true, onDrop:'axisChanged(axisSpec)'}"
+ class="list-unstyled"
+ style="height: 108px; border-radius: 6px; margin-top: 7px; overflow: auto !important;">
+
+ <span ng-repeat="col in config.axis[config.chart.current][axisSpec.name]">
+
+ <!-- in case of axis is multiple dimensions and not aggregator -->
+ <span ng-if="!isAggregatorAxis(axisSpec)"
+ class="btn btn-default btn-xs"
+ style="background-color: #EFEFEF; margin: 2px 0px 0px 2px;">
+ {{col.name}}
+ <span class="fa fa-close" ng-click="removeFromAxis($index, axisSpec)"></span>
+ </span>
+
+ <!-- in case of axis is multiple dimension and aggregator -->
+ <span class="btn-group">
+ <span ng-if="isAggregatorAxis(axisSpec)"
+ class="btn btn-default btn-xs dropdown-toggle"
+ style="background-color: #EFEFEF; margin: 2px 0px 0px 2px;"
+ type="button" data-toggle="dropdown">
+ {{col.name | limitTo: 30}}{{col.name.length > 30 ? '...' : ''}}
+ <span style="color:#717171; margin: 0px;">
+ <span class="lightBold"
+ style="text-transform: uppercase; margin: 0px;">{{col.aggr}}
+ </span>
+ </span>
+ <span class="fa fa-close" style="margin: 0px;" ng-click="removeFromAxis($index, axisSpec)"></span>
+ </span>
+ <ul class="dropdown-menu" role="menu">
+ <li ng-click="aggregatorChanged($index, axisSpec, 'sum')"><a>sum</a></li>
+ <li ng-click="aggregatorChanged($index, axisSpec, 'count')"><a>count</a></li>
+ <li ng-click="aggregatorChanged($index, axisSpec, 'avg')"><a>avg</a></li>
+ <li ng-click="aggregatorChanged($index, axisSpec, 'min')"><a>min</a></li>
+ <li ng-click="aggregatorChanged($index, axisSpec, 'max')"><a>max</a></li>
+ </ul>
+ </span>
+
+ </span>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+ </div>
+
+</div>
+
+<!-- panel: parameter information -->
+<div class="panel panel-default">
+
+ <div class="panel-heading" style="padding: 6px 12px 6px 12px; font-size: 13px;">
+ <span style="vertical-align: middle; display: inline-block; margin-top: 3px;">Parameters</span>
+ <span style="float: right;">
+ <div class="btn-group" role="group" aria-label="...">
+ <div type="button" ng-click="parameterChanged()"
+ ng-if="config.panel.parameterPanelOpened"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-floppy-o" aria-hidden="true"></i>
+ </div>
+ <div type="button" ng-click="resetParameterConfig()"
+ ng-if="config.panel.parameterPanelOpened"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </div>
+ <div type="button" ng-if="config.panel.parameterPanelOpened"
+ ng-click="toggleParameterPanel()"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-minus" style="font-size: 12px;" aria-hidden="true"></i>
+ </div>
+ <div type="button" ng-if="!config.panel.parameterPanelOpened"
+ ng-click="toggleParameterPanel()"
+ class="btn btn-default" style="padding: 2px 5px 2px 5px;">
+ <i class="fa fa-expand" style="font-size: 11px;" aria-hidden="true"></i>
+ </div>
+ </div>
+ </span>
+ <div style="clear: both;"></div> <!-- to fix previous span which has float: right -->
+ </div>
+ <div class="panel-body"
+ ng-if="config.panel.parameterPanelOpened"
+ style="padding-top: 13px; padding-bottom: 13px; height: 400px; overflow: auto;">
+ <table class="table table-striped">
+ <tr>
+ <th style="font-size: 12px; font-style: italic">Name</th>
+ <th style="font-size: 12px; font-style: italic">Type</th>
+ <th style="font-size: 12px; font-style: italic">Description</th>
+ <th style="font-size: 12px; font-style: italic">Value</th>
+ </tr>
+ <tr>
+ </tr>
+
+ <tr data-ng-repeat="paramSpec in config.paramSpecs[config.chart.current]">
+ <td style="font-weight: 400; vertical-align: middle;">{{paramSpec.name}}</td>
+ <td style="font-weight: 400; vertical-align: middle;">{{paramSpec.valueType}}</td>
+ <td ng-bind-html="paramSpec.description"
+ style="font-weight: 400; vertical-align: middle;"></td>
+ <td>
+ <div ng-if="isInputWidget(paramSpec)"
+ class="input-group">
+ <input type="text" class="form-control input-sm"
+ style="font-weight: 400; font-size: 12px; vertical-align:middle; border-radius: 5px;"
+ ng-keydown="parameterOnKeyDown($event, paramSpec)"
+ data-ng-model="config.parameter[config.chart.current][paramSpec.name]" />
+ </div>
+ <div class="btn-group"
+ ng-if="isOptionWidget(paramSpec)">
+ <select class="form-control input-sm"
+ ng-keydown="parameterOnKeyDown($event, paramSpec)"
+ ng-change="parameterChanged(paramSpec)"
+ data-ng-model="config.parameter[config.chart.current][paramSpec.name]"
+ data-ng-options="optionValue for optionValue in paramSpec.optionValues"
+ style="font-weight: 400; font-size: 12px;">
+ </select>
+ </div>
+
+ <div ng-if="isCheckboxWidget(paramSpec)">
+ <input type="checkbox"
+ ng-keydown="parameterOnKeyDown($event, paramSpec)"
+ ng-click="parameterChanged(paramSpec)"
+ data-ng-model="config.parameter[config.chart.current][paramSpec.name]"
+ data-ng-checked="config.parameter[config.chart.current][paramSpec.name]" />
+ </div>
+
+ <div ng-if="isTextareaWidget(paramSpec)">
+ <textarea class="form-control" rows="3"
+ ng-keydown="parameterOnKeyDown($event, paramSpec)"
+ data-ng-model="config.parameter[config.chart.current][paramSpec.name]"
+ style="font-weight: 400; font-size: 12px;">
+ </textarea>
+ </div>
+
+ </td>
+ </tr>
+ </table>
+ </div>
+
+</div>