MDX Manipulation
The MdxApi
contains all the functions to extract information from an MDX query string and/or update it.
Design
Usage
Parsing
In order to read or modify an MDX statement, the statement string must first be parsed.
The parsing consists of converting the Mdx
into a tree of JavaScript objects.
If the input MDX string is not syntactically valid, trying to parse it will raise a parsing error and the MDX API will not be able to process this string.
Each node of the tree produced by the parsing is called a MdxNode
.
This includes the root node, so the result of the parsing of an MDX string is a MdxNode
.
We ask that you don't directly mutate
MdxNode
objects. This constraint allows us to cacheMdxNode
s produced by the MDX API, speeding up later processing of similar MDX.If you want to modify an
MdxNode
, create a clone of the object first. Some third-party libraries provide functions to make this more convenient, such as Lodash’s_.cloneDeep
/_.clone
and immutability-helper’supdate
.
Transforming an MDX String
Since we often need to make consecutive calls to different MDX API methods and since parsing is an expensive operation, all the methods of the MDX API reading or updating an MDX statement take as input a MdxNode
and return a MdxNode
.
Additionally, most of MDX API functions require the server Discovery
information along the statement.
Indeed, the discovery is necessary to know which part of the MDX query corresponds to a hierarchy or level, what are the default members and so on.
This is why most of the functions will take as input an object of type StatementAndDiscovery
and also return a StatementAndDiscovery
to make calls chainable.
Thus, each modification of an MDX string requires to first parse it, then call the wanted MDX API method with a StatementAndDiscovery
, then retrieve the statement
part of the resulting StatementAndDiscovery
and convert it back to a string.
This is done automatically by the transform
function of the MdxApi
.
Filters
The MDX filtering API is very extensive as this is the part of the API that is more likely to be used in projects.
ActiveFilter
The main object used to manipulate the filters of an MDX statement is called ActiveFilter
.
"Active" here means that this filter is currently in use in a statement, as opposed to the class Filter which describes a type of filter, outside of any statement context.
The function mdxApi.filters.selectFilters
retrieves the list of active filters from an MDX.
Filter Selectors
Filter selectors are objects that can be used to filter out some active filters from the list of filters returned in selectFilters
.
They are plugins because their ultimate goal is to be store as filter bookmarks so that one can display, in a dashboard, a widget filtering a given hierarchy only.
Selectors represent the persisted information about a filter, so that one can find again a filter even after most of the MDX changed.
The list of important built-in implementations is:
Name | Parameters | Role | Usage |
---|---|---|---|
AllSelector | None | Keeps all active filters | This is the selector the Wizard uses by default |
UserSelector | None | Filters out server filters | With this selector one sees the filters contained in the MDX |
KeySelector | key | Keeps only filters of a given type | If one wants to list only TopCount filters for instance |
HierarchySelector | hierarchy | Keeps only filters filtering a given hierarchy | If one wants to list only filters on Measures for instance |
HierarchyKeyIndexSelector | hierarchy, key, index | Keeps only one active filter with the given parameters | Track an active filter even if the MDX is heavily modified |
ActiveFilter Interface
The result of an MDX query is filtered either:
- by filters in the statement itself, which are called user filters because the user wrote the statement.
- by members of slicing hierarchies not expressed on the main axes of the select statement.
These filters are called server filters because they are applied by the server implicitly. The method
remove
is not available on those type of filter.
Both filters share the same type AugmentedActiveFilter
.
Filters Plugins
Each filter plugin describes a type of filter that can be used in a query:
- how to recognize their type of filters in the MDX
- how to change the MDX when an active filter is being created with this key
- how to render the UI to edit the filter in the Edit filter popup
- how to render the UI to see/edit the filter in the Wizard
Each filter plugin is responsible of choosing the value
that will be contained in the created ActiveFilter
based on the MDX.
This value is a JavaScript Object that has a different shape depending on the plugin used.
The list of built-in filters is:
Name | Description | Example of handled MDX | Value shape |
---|---|---|---|
explicit | List selected members explicitly | {[D1].[H1].[AllMember].[M1], [D1].[H1].[AllMember].[M1]} | { members: ['[AllMember].[M1]', '[AllMember].[M2]'], mode: 'include'} |
topcount | TopCount, BottomPercent etc... | TopCount(Filter(*, NOT IsEmpty(measure)), n, measure) | {measure: 'pnl.SUM', order: 'TOP', mode: 'COUNT', amount: 10, byParent: false, levelDepth: 1} |
numeric-binary | Compare measure with value | Filter(...[L1].Members, [Measures].[pnl.SUM] > 0) | {measure: 'pnl.SUM', level: 'L1', constant: 0, op: '>', yoda: false} |
numeric-between | Compare measure with two values | Filter(...[L1].Members, [Measures].[a] > 0 AND [Measures].[a] < 10 | {measure: 'a', level: 'L1', low: 0, high: 10, inside: true} |
string-binary | Compare member label with String | Filter(...[L1].Members, [D1].[H1].CurrentMember.MEMBER_CAPTION > "a" | {level: 'L1', constant: 0, op: '>', yoda: false} |
string-between | Compare member label with two Strings | Combine NumericBetween with StringBinary | Combine also the values |
substring | Check if member label contains or not a String | Filter(...[L].Members, Left([D].[H].CurrentMember.member_caption length) <> str) | {level: 'L1', mode: Mode.Left, inverted: true} |
emptiness | Check if measure is empty or not | Filter(...[L].Members, NOT IsEmpty([Measures].[pnl.SUM])) | {measure: 'pnl.SUM', level: 'L', empty: false} |
Setting the Default Filter Type per Hierarchy
You might have a hierarchy for which you would always want to use the same filter type when adding a filter to it.
Let's say, you always want to default to topcount
when adding a filter to the [Trades].[Trades]
hierarchy.
Then you can define this setting:
"filtering.hierarchyToFilterType.[Trades].[Trades]": "topcount"
Basic Filter Manipulation Example
import {ActiveUIConsumer} from '@activeviam/activeui-sdk';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import PropTypes from 'prop-types';
import React from 'react';
const {TextArea} = Input;
const initialValue = `{
"amount": 10,
"byParent": false,
"levelDepth": 1,
"measure": "contributors.COUNT",
"mode": "COUNT",
"order": "TOP"
}`;
class App extends React.Component {
state = {
discovery: null,
hierarchy: '[Underlyings].[Products]',
key: 'topcount',
mdx:
'SELECT [Measures].[contributors.COUNT] ON COLUMNS FROM [EquityDerivativesCube]',
position: 0,
value: initialValue,
};
async componentDidMount() {
const discovery = await this.props.activePivotServer.getDiscovery().get();
this.setState({discovery});
}
getOutput = () => {
const {mdxApi} = this.props;
const {discovery, hierarchy, key, mdx, position, value} = this.state;
if (!discovery) {
return 'Discovery loading...';
}
let parsedValue;
try {
parsedValue = JSON.parse(value);
} catch (e) {
return 'Value not parseable';
}
try {
return mdxApi.parsing.toString(
mdxApi.parsing.parseExpression(
mdxApi.transform(mdx, discovery, snd =>
mdxApi.filters.addFilter(
snd,
position,
key,
hierarchy,
parsedValue,
),
),
),
{indent: true, notEmpty: () => true},
);
} catch (e) {
return 'Cannot apply filter';
}
};
render() {
return (
<div style={{display: 'flex'}}>
<div style={{width: '50%'}}>
<h2>Input</h2>
<p>MDX</p>
<TextArea
style={{minHeight: 50}}
value={this.state.mdx}
onChange={({target: {value: mdx}}) => {
this.setState({mdx});
}}
/>
<p>Hierarchy</p>
<Input
value={this.state.hierarchy}
onChange={({target: {value: hierarchy}}) => {
this.setState({hierarchy});
}}
/>
<p>Key</p>
<Input
value={this.state.key}
onChange={({target: {value: key}}) => {
this.setState({key});
}}
/>
<p>Position</p>
<InputNumber
value={this.state.position}
onChange={position => {
this.setState({position});
}}
/>
<p>Value</p>
<TextArea
style={{minHeight: 200}}
value={this.state.value}
onChange={({target: {value}}) => {
this.setState({value});
}}
/>
</div>
<div style={{width: '50%', paddingLeft: 15}}>
<h2>Output</h2>
<pre>{this.getOutput()}</pre>
</div>
</div>
);
}
}
App.propTypes = {
activePivotServer: PropTypes.object.isRequired,
mdxApi: PropTypes.object.isRequired,
};
export default () => (
<ActiveUIConsumer>
{({mdx: mdxApi, queries: {serversPool}}) => (
<App
activePivotServer={serversPool.getActivePivotServer(
"https://your.activepivot.server",
)}
mdxApi={mdxApi}
/>
)}
</ActiveUIConsumer>
);
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';
import App from './App';
const activeUI = createActiveUI();
const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: "https://your.activepivot.server"});
render(
<ActiveUIProvider activeUI={activeUI}>
<App />
</ActiveUIProvider>,
document.getElementById('root'),
);
<div id="root"></div>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-variant: tabular-nums;
}
.CodeMirror pre {
/* Using !important because CodeMirror will apply its own style after but we want to force this font stack. */
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}