/olap-cube-js

olap cube javascript

Primary LanguageJavaScript

Build Status codecov Version npm

OLAP cube.js

The simplest data analysis tools written in javascript. This solution is a means for extracting and replenishing data, which together with your data storage means and a means of providing aggregate data, is intended for decision making.

Table of Contents

Features:

Getting Started

Prerequisites

For install the software you need a package manager - npm which is installed with Node.js

Installing

Then in the console run the following command

npm install olap-cube-js

How Cube is work?

Structure

// This is an array of data from server
let facts = [
    { id: 1, region: 'North', year: 2017, month: 'January', product: 'Product 1', category: 'Category 1', value: 737 },
    { id: 2, region: 'South', year: 2017, month: 'April', product: 'Product 2', category: 'Category 1', value: 155 },
    { id: 3, region: 'West',  year: 2018, month: 'April', product: 'Product 3', category: 'Category 2', value: 112 },
    { id: 4, region: 'West',  year: 2018, month: 'April', product: 'Product 1', category: 'Category 2', value: 319 },
]

// This is the data schema we need to obtain
let dimensionHierarchies = [
    {
        dimensionTable: {
            dimension: 'regions',
            keyProps: ['region'],
        }
    },
    {
        dimensionTable: {
            dimension: 'date',
            keyProps: ['year', 'month']
        }
    },  
    {
        dimensionTable: {
            dimension: 'products',
            keyProps: ['product'],
        },
        level: [
            {
                dimensionTable: {
                    dimension: 'categories',
                    keyProps: ['category']
                }
            }
        ]
    }
];

// We send it all to the constructor
let cube = Cube.create(facts, dimensionHierarchies);

Now the cube will represent the structure below:

let structure = {
    dimensionHierarchies: [
        {
            dimensionTable: {
                dimension: 'regions',
                keyProps: ['region'],
                members: [
                    { id: 1, region: 'North' },
                    { id: 2, region: 'South' },
                    { id: 3, region: 'West' }
                ],
                otherProps: []
            },
            level: []
        },
        {
            dimensionTable: {
                dimension: 'date',
                keyProps: ['year', 'month'],
                members: [
                    { id: 1, year: 2017, month: 'January' },
                    { id: 2, year: 2017, month: 'April' },
                    { id: 3, year: 2018, month: 'April' }
                ],
                otherProps: []
            },
            level: []
        },
        {
            dimensionTable: {
                dimension: 'products',
                keyProps: ['product'],
                members: [
                    { id: 1, product: 'Product 1', categories_id: 1 },
                    { id: 2, product: 'Product 2', categories_id: 1 },
                    { id: 3, product: 'Product 3', categories_id: 2 },
                    { id: 4, product: 'Product 1', categories_id: 2 },
                ],
                otherProps: []
            },
            level: [
                {
                    dimensionTable: {
                        dimension: 'categories',
                        keyProps: ['category'],
                        members: [
                            { id: 1, category: 'Category 1' },
                            { id: 2, category: 'Category 2' },
                        ],
                        otherProps: []
                    },
                    level: []
                }
            ]
        }
    ],
    cellTable: [
        { id: 1, regions_id: 1, date_id: 1, products_id: 1, value: 737 },
        { id: 2, regions_id: 2, date_id: 2, products_id: 2, value: 155 },
        { id: 3, regions_id: 3, date_id: 3, products_id: 3, value: 112 },
        { id: 4, regions_id: 3, date_id: 3, products_id: 4, value: 319 },
    ]
};

Sets

A set is a collection of distinct objects. Set provides a specialized syntax for getting and manipulating the multidimensional data stored in OLAP cubes. Access to the elements of the OLAP-cube can be carried out several types of sets

Types of sets: Set, Subset, EmptySet, Multiset

Set, that type determines one element:
w : ( x , y , z ) → wxyz ,

Subset, that type determines several elements:
W : ( x , y ) → W = { wz1 , wz2 , … , wzn } ,

EmptySet, that type determines all elements:
W : () → W = { wx1 y1 z1 , wx1 y1 z2 , … , wxn yn zn } ,

EmptySet, that type determines union of elements:
W : ({ z1 , z2 }) → W = { Wx1 y1 , Wxn yn } = { wx1 y1 , wxn yn }z1 ∪ { wx1 y1 , wxn yn }z2 .
Now using different types of sets, you can access the elements of the cube

API

Access to measures of the cells

Access to measures is possible through access to cube cells

Set

Define the set with maximum cardinality. For this fixate all dimensions of the first level of the hierarchy.

Example:

let set = { regions: { id: 1 }, date: { id: 1 }, products: { id: 1 } }

execute:

cube.getCellsBySet(set)

return:

[
    { id: 1, value: 737, regions_id: 1, date_id: 1, products_id: 1 }
]

execute:

cube.getFactsBySet(set)

return:

[
    { id: 1, region: 'North', year: 2017, month: 'January', product: 'Product 1', category: 'Category 1', value: 737 }
]
Subset

Fixate some of the dimensions:

let subSet = { regions: { id: 3 } }

execute:

cube.getCellsBySet(subSet)

return:

[
    { id: 3, value: 112, regions_id: 3, date_id: 3, products_id: 3 },
    { id: 4, value: 319, regions_id: 3, date_id: 3, products_id: 4 },
]

execute:

cube.getFactsBySet(subSet)

return:

[
    { id: 3, region: 'West',  year: 2018, month: 'April', product: 'Product 3', category: 'Category 2', value: 112 },
    { id: 4, region: 'West',  year: 2018, month: 'April', product: 'Product 1', category: 'Category 2', value: 319 },
]
EmptySet

This way you can take all the facts from the cube back:

let emptySet = {}

execute:

cube.getCellsBySet(emptySet)
// or little shorter
cube.getCells()

return:

[
    { id: 1, value: 737, regions_id: 1, date_id: 1, products_id: 1 },
    { id: 2, value: 155, regions_id: 2, date_id: 2, products_id: 2 },
    { id: 3, value: 112, regions_id: 3, date_id: 3, products_id: 3 },
    { id: 4, value: 319, regions_id: 3, date_id: 3, products_id: 4 },
]

execute:

cube.getFactsBySet(emptySet)
// or little shorter
cube.getFacts()

return:

[
    { id: 1, region: 'North', year: 2017, month: 'January', product: 'Product 1', category: 'Category 1', value: 737 },
    { id: 2, region: 'South', year: 2017, month: 'April', product: 'Product 2', category: 'Category 1', value: 155 },
    { id: 3, region: 'West',  year: 2018, month: 'April', product: 'Product 3', category: 'Category 2', value: 112 },
    { id: 4, region: 'West',  year: 2018, month: 'April', product: 'Product 1', category: 'Category 2', value: 319 },
]
Multiset

Fixate a plurality of dimension values:

let multiSet = { regions: [ { id: 1 }, { id: 2 } ] }

execute:

cube.getCellsBySet(multiSet)

return:

[
    { id: 1, value: 737, regions_id: 1, date_id: 1, products_id: 1 },
    { id: 2, value: 155, regions_id: 2, date_id: 2, products_id: 2 },
]

execute:

cube.getFactsBySet(multiSet)

return:

[
    { id: 1, region: 'North', year: 2017, month: 'January', product: 'Product 1', category: 'Category 1', value: 737 },
    { id: 2, region: 'South', year: 2017, month: 'April',   product: 'Product 2', category: 'Category 1', value: 155 },
]

Access to members of the dimensions

EmptySet

Simple call return all members of the dimension:

cube.getDimensionMembersBySet('products', {})
// or little shorter
cube.getDimensionMembers('products')

return:

[
    { id: 1, product: 'Product 1', categories_id: 1 },
    { id: 2, product: 'Product 2', categories_id: 1 },
    { id: 3, product: 'Product 3', categories_id: 2 },
    { id: 4, product: 'Product 1', categories_id: 2 },
]
SubSet

Queries with the second argument return some members of the dimension in accordance with the passed set

cube.getDimensionMembersBySet('products', { categories: { id: 1 } })

return:

[
    { id: 1, product: 'Product 1', categories_id: 1 },
    { id: 2, product: 'Product 2', categories_id: 1 },
]

Other example:

cube.getDimensionMembersBySet('regions', { categories: { id: 1 } })

return:

[
    { id: 1, region: 'North' },
    { id: 2, region: 'South' },
]
Multiset
cube.getDimensionMembersBySet('products', { regions: [{ id: 2 }, { id: 3 }] } )

return:

[
    { id: 2, product: 'Product 2', categories_id: 1 },
    { id: 3, product: 'Product 3', categories_id: 2 },
    { id: 4, product: 'Product 1', categories_id: 2 },
]

Editing dimension members

let regions = cube.getDimensionMembers('regions')
let member = regions[0]
member['region'] = 'East'; 

Adding dimension members

let member = { product: 'Product 3' }
cube.addDimensionMember('products', member)

Removing dimension members

let member = { id: 2 }
cube.removeDimensionMember('products', member)

Adding facts

let facts = [
    { id: 3, region: 'South', product: 'Product 3', value: 30 }
]
cube.addFacts(facts)

Removing facts

let facts = [
    { id: 3, region: 'South', product: 'Product 3', value: 30 }
]
cube.removeFacts(facts)

Added dimension hierarchy

let facts = [
    { id: 1, product: 'TV', mark: 'Sony', country: 'China', count: 2 },
    { id: 1, product: 'TV', mark: 'Samsung', country: 'Niderland', count: 3 }
];
let cube = Cube.create(facts, [])
cube.addDimensionHierarchy({
    dimensionTable: {
        dimension: 'product',
        keyProps: ['product']
    },
    level: [
        {
            dimensionTable: {
                dimension: 'mark',
                keyProps: ['mark']
            },
        }
    ]
})
console.log(cube.cellTable)

return:

[
    { id: 1, product_id: 1, country: 'China', count: 2 },
    { id: 1, product_id: 2, country: 'Niderland', count: 3 }
]

Removing dimension hierarchy

Returns the result back to the addition of the hierarchy

cube.removeDimensionHierarchy(cube.dimensionHierarchies[0])

Filling empty cells

Fills the fact table with all possible missing combinations. For example, for a table, such data will represent empty cells

let dimensionHierarchies = [
     {
         dimension: 'regions',
         keyProps: ['region']
     },{
         dimension: 'products',
         keyProps: ['product']
     }
 ];

let facts = [
    { id: 1, region: 'North', product: 'Product 1', value: 10 },
    { id: 2, region: 'South', product: 'Product 2', value: 20 }
];
let cube = Cube.create(facts, dimensionHierarchies)

Execute filling:

let defaultMeasures = { value: 0 }; // properties for empty cells
cube.fillEmptyCells(defaultMeasures);

Now get the facts back:

let factsFilled = cube.getFacts()

factsFilled will be:

[
    { id: 1, region: 'North', product: 'Product 1', value: 10 },
    { id: 2, region: 'South', product: 'Product 2', value: 20 },
    { region: 'North', product: 'Product 2', value: 0 },
    { region: 'South', product: 'Product 1', value: 0 }
]

Removing empty cells

You can remove all or some of the empty cells. At the same time, some of the dimension members can be removed too if no more cells found correspond to the dimension member

cube.removeCell(cube.getEmptyCells())
// or
cube.removeCell(cube.getEmptyCells().filter(({ value }) => !value))

Drill-up members

let markMembers = cube.drillUpMembers('product', productMembers, 'mark')

Drill-down members

let productMembers = cube.drillDownMembers('mark', markMembers, 'product')

Slice

let member = cube.getDimensionMembers('mark')[0]
let subCube = cube.slice(member)

Dice

let markMember = cube.getDimensionMembers('mark')[0]
let regionMember = cube.getDimensionMembers('region')[0]
let subCube = cube.dice({ mark: markMember, region: regionMember })

Additional member props

It may be that the dimension member may content additional properties from the fact table that do not participate in creating own surrogate key, for this use the property otherProps

let facts = [{ id: 1, nikname: 'Monkey', name: 'Albert', surname: 'Einstein', countryBirth: 'Germany' }]
let dimensionHierarchies = [
   {
       dimensionTable: {
           dimension: 'user',
           keyProps: ['nikname'],
           otherProps: ['name', 'surname']
       },
       dimensionTable: {
           dimension: 'country',
           keyProps: ['countryBirth'],
       }
   }
]
let cube = Cube.create(facts, dimensionHierarchies)
let members = cube.getDimensionMembers('user')

return:

[
    { id: 1, nikname: 'Monkey', name: 'Albert', surname: 'Einstein' }
]

Custom members

let facts = [{ id: 1, nikname: 'Monkey', group: 'Administrators' }];
let dimensionHierarchies = [
    {
        dimensionTable: {
            dimension: 'user',
            keyProps: ['nikname'],
            foreignKey: 'USER_ID'
        },
        level: [
            {
                dimensionTable: {
                    dimension: 'group',
                    keyProps: ['group'],
                    primaryKey: 'ID',
                    foreignKey: 'GROUP_ID'
                }
            }
        ]
    }
];
let cube = Cube.create(facts, dimensionHierarchies);

execute:

let userMember = cube.getDimensionMembers('user')[0] 

return:

{ id: 1, nikname: 'Monkey', GROUP_ID: 1 }

execute:

let groupMember = cube.getDimensionMembers('group')[0];

return:

{ ID: 1, group: 'Administrators' }

execute:

let cell = cube.getCells()[0];

return:

{ id: 1, USER_ID: 1 }

Default Member Options

let dimensionHierarchies = [
    {
        dimensionTable: {
            dimension: 'user',
            keyProps: ['nikname'],
            defaultMemberOptions: {
                nikname: 'anonymous'
            }
        }
    }
];
let cube = Cube.create([], dimensionHierarchies)
cube.addDimensionMember('user')

Custom facts

Like custom members, some times need make custom facts

let factTable = {
    facts: [
        { saleId: 1, saleCount: 1 }
    ],
    primaryKey: 'saleId'
};
let dimensionHierarchies = [
    {
        dimensionTable: {
            dimension: 'saleCount',
            keyProps: ['saleCount']
        }
    }
];
let cube = Cube.create(factTable, dimensionHierarchies)

Default Fact Options

let factTable = {
    facts: [
        { id: 1, x: 1, y: 1, isOpen: true },
        { id: 1, x: 2, y: 2, isOpen: true },
    ],
    defaultFactOptions: {
        isOpen: false
    }
};
let dimensionHierarchies = [
    {
        dimensionTable: {
            dimension: 'x',
            keyProps: ['x']
        }
    },
    {
        dimensionTable: {
            dimension: 'y',
            keyProps: ['y']
        }
    }
];
let cube = Cube.create(factTable, dimensionHierarchies)
cube.fillEmptyCells();
cube.addDimensionMember('x', { x: 3 })

Versioning

We use SemVer for versioning.

Todo

In future versions:

API

  • Add support for single keyProp in schema and single level
  • Update method addMember without rollup options (then more than one member will be added)

Code quality

  • Update code with JsDoc
  • Add amd/umd/common/ES6 builds
  • Security protection for the key values
  • Add validation for all public methods
  • Remove responsibility for "id" prop at facts (residuals)

Perhaps

  • Add unbalanced, ragged hierarchies, multiple hierarchies (each cube dimension can contains more then one hierarchies, dimension with both fiscal and calendar years is one classic example)
  • Add calculated members
  • Add MDX query language
  • Add Speed tests

Deploy

  • Fix test cover

Docs

  • Update readme file (rename Set to Space?)