Please Note This forum exists for community support for the Mango product family and the Radix IoT Platform. Although Radix IoT employees participate in this forum from time to time, there is no guarantee of a response to anything posted here, nor can Radix IoT, LLC guarantee the accuracy of any information expressed or conveyed. Specific project questions from customers with active support contracts are asked to send requests to support@radixiot.com.

Radix IoT Website Mango 3 Documentation Website Mango 4 Documentation Website

How to chart with data points in the X axis instead of time


  • Hello
    Im trying to make a simple chart that would have in the x axis the distinct integral values of each individual data point of my point query and not the time that is default
    Is there an easy way to do that ?
    Basically if i have lets say 63 data points , i want to get the integral of the day of each one and put it in a chart and sort them by the smaller to the bigger
    So i can have at a glance an idea which of the data points are under performing comparing to the others
    Please guide me through
    thank you


  • Hi alexzer, welcome to the forum!

    Interesting request. I found two ways to hack this into the ma-serial-chart component,

    1. Purely through UI setup:
    <md-input-container flex style="display:none">
        <label>Preset</label>
        <ma-date-range-picker from="from" to="to" preset="PREVIOUS_DAY" update-interval="30 minutes"></ma-date-range-picker>
    </md-input-container>
    <ma-get-point-value point-xid="point1-xid" point="point1"></ma-get-point-value>
    <ma-point-statistics point="point1" rendered=false from="from" to="to" statistics="statsObj1"></ma-point-statistics>
    <ma-serial-chart style="height: 300px; width: 100%" series-1-values='[{"value":statsObj1.integral.value, "timestamp":point1.deviceName + "-" + point1.name}]' default-type="column" options='{"categoryAxis":{"parseDates":false}}'></ma-serial>
    

    Then you would extend this example with the other 62 points by either adding them as ma-get-point-value / ma-point-statistics for each point, or you could look into using an ma-point-query with the ma-point-statistics (it can take an array of points as an argument instead of just one. Check out the API docs!). This example also won't sort them for you, so you probably want to use a filter like I do in the second example, but you may have to be on guard for an infinite digest loop, which may require modifying the user module below (probably need to not call sort on input if it's already sorted). You could also add a date selector if you needed to look at specific intervals other than just "Yesterday."

    1. Using an alphanumeric meta point to compute the integrals array for you. This will have the advantage of having a history you can easily look back through.
    <ma-get-point-value point-xid="meta-point-xid" point="point1"></ma-get-point-value>
    <ma-serial-chart style="height: 300px; width: 100%" series-1-values='point1.value|fromJson|sortByNumField:"value":true' default-type="column" options='{"categoryAxis":{"parseDates":false}}'></ma-serial-chart>
    

    and then the alphanumeric meta point is going to do something like this on perhaps a 'start of day' update event:

    values = [];
    //You could also do a DataPointQuery.query if you don't want to worry about keeping the meta
    // point's context up to date
    for(var point : CONTEXT_POINTS) { 
      values.push( {"value" : this[point].past(DAY).integral, "timestamp": this[point].getDataPointWrapper().getExtendedName() });
    }
    return JSON.stringify(values);
    

    In this example there are two angular filters, fromJson and sortByNumField defined in a userModule.js file in my filestore, and linked to by my UI settings:

    define(['angular', 'require'], function(angular, require) {
    'use strict';
    
    var userModule = angular.module('userModule', ['maUiApp']);
    
    userModule.filter('fromJson', ['$filter', function($filter) {
        return function(input) {
            //console.log(input);
            try {
                return JSON.parse(input);
            } catch {
                console.log("Parsing error in fromJson filter for input: " + input);
                return null;
            }
        };
    }]);
    
    userModule.filter('sortByNumField', ['$filter', function($filter) {
        return function(input, field, asc) {
            try {
                return input.sort(function(a,b){ 
                    if(asc) return a[field]-b[field];
                    return b[field]-a[field];
                });
            } catch {
                console.log("Error sorting input: " + input);
                return input;
            }
        };
    }]);
    
    return userModule;
    
    }); // define
    

    Hope that helps!


  • Hi philip thanks a lot for the fast response, much appreciated ...
    Im playing around with the features of mango to see if its what my clients needs and as soon as i confirm him we will buy the licence
    I tried the first solution you suggested
    On a static trial its working fine

    <div layout="row">
        <md-input-container flex="20">
            <label>Choose a first point</label>
            <ma-point-list limit="200" ng-model="point1"></ma-point-list>
        </md-input-container>
        <md-input-container flex="20">
            <label>Choose a second point</label>
            <ma-point-list limit="200" ng-model="point2"></ma-point-list>
        </md-input-container>
         <md-input-container flex="20">
            <label>Choose a third point</label>
            <ma-point-list limit="200" ng-model="point3"></ma-point-list>
        </md-input-container>
         <md-input-container flex="20">
            <label>Choose a fourth point</label>
            <ma-point-list limit="200" ng-model="point4"></ma-point-list>
        </md-input-container>
        
    </div>
    
    
    <ma-get-point-value point-xid="point1-xid" point="point1" from="dateBar.from" to="dateBar.to" rollup="{{dateBar.rollupType}}" rollup-interval="{{dateBar.rollupIntervals}} {{dateBar.rollupIntervalPeriod}}"></ma-get-point-value>
    <ma-get-point-value point-xid="point2-xid" point="point2" from="dateBar.from" to="dateBar.to" rollup="{{dateBar.rollupType}}" rollup-interval="{{dateBar.rollupIntervals}} {{dateBar.rollupIntervalPeriod}}"></ma-get-point-value>
    <ma-get-point-value point-xid="point3-xid" point="point3" from="dateBar.from" to="dateBar.to" rollup="{{dateBar.rollupType}}" rollup-interval="{{dateBar.rollupIntervals}} {{dateBar.rollupIntervalPeriod}}"></ma-get-point-value>
    <ma-get-point-value point-xid="point4-xid" point="point4" from="dateBar.from" to="dateBar.to" rollup="{{dateBar.rollupType}}" rollup-interval="{{dateBar.rollupIntervals}} {{dateBar.rollupIntervalPeriod}}"></ma-get-point-value>
    <ma-point-statistics point="point1" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj1"></ma-point-statistics>
    <ma-point-statistics point="point2" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj2"></ma-point-statistics>
    <ma-point-statistics point="point3" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj3"></ma-point-statistics>
    <ma-point-statistics point="point4" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj4"></ma-point-statistics>
    <ma-serial-chart style="height: 300px; width: 100%" series-1-values='[{"value":statsObj1.integral.value, "timestamp":point1.deviceName + "-" + point1.name}]' series-2-values='[{"value":statsObj2.integral.value, "timestamp":point2.deviceName + "-" + point2.name}]' series-3-values='[{"value":statsObj3.integral.value, "timestamp":point3.deviceName + "-" + point3.name}]' series-4-values='[{"value":statsObj4.integral.value, "timestamp":point4.deviceName + "-" + point4.name}]' default-type="column" options='{"categoryAxis":{"parseDates":false}}'></ma-serial>
    

    Gives an output which looks correct

    0_1526650864445_763065a6-b4ca-4c18-8240-f618567f80ac-image.png


    But when i try to make it via a point query it does not give me each data point as an individual bar but combine them together ..
    I have a limit of 3 to make it more readable

    <div layout="row">
        <md-input-container flex="50">
            <label>Device name</label>
            <input ng-init="dvName=''" ng-model="dvName" ng-model-options="{debounce:1000}">
        </md-input-container>
        <md-input-container flex="50">
            <label>Point name</label>
            <input ng-init="ptName=''" ng-model="ptName" ng-model-options="{debounce:1000}">
        </md-input-container>
    </div>
    
    <ma-point-query query="{$and: true, deviceName:dvName, name:ptName}" limit="3" points="points"></ma-point-query>
    <ma-point-values points="points" values="combined" from="dateBar.from" to="dateBar.to" rollup="{{dateBar.rollupType}}" rollup-interval="{{dateBar.rollupIntervals}} {{dateBar.rollupIntervalPeriod}}">
    </ma-point-values>
    <ma-point-statistics points="points" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj"></ma-point-statistics>
    
    
    <ma-serial-chart style="height: 300px; width: 100%" points="points" values="combined"  series-1-values='[{"value":statsObj.integral.value, "timestamp":points.deviceName + "-" + points.name}]' default-type="column" options='{"categoryAxis":{"parseDates":false}}'></ma-serial>
    
    

    And this give an output of :
    0_1526651217061_be8666b5-0874-4f25-a592-63fbbc7d4afe-image.png

    I think my series-x-values is not correct or what ?
    thank you


  • Hi alexzer,

    What follows is perhaps not the ideal solution, but it does work (you may want to change the inputs back).

    <ma-point-query query="{$and: true, deviceName:'Mango Internal', name:'oint'}" limit="3" points="points"></ma-point-query>
    
    <br/>
    <ma-point-statistics points="points" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj"></ma-point-statistics>
    <ma-calc input="outputArray|collateIntegrals:points:statsObj" output="outputArray"></ma-calc>
    <ma-serial-chart  style="height: 300px; width: 100%" series-1-values="outputArray" default-type="column" 
    options='{"categoryAxis":{"parseDates":false}}'></ma-serial-chart>
    

    and this required a filter in a user module again: https://help.infiniteautomation.com/getting-started-with-a-user-module/

    Here's my user module from this question now:

    define(['angular', 'require'], function(angular, require) {
    'use strict';
    
    var userModule = angular.module('userModule', ['maUiApp']);
    
    userModule.filter('collateIntegrals', ['$filter', function($filter) {
        return function(input, points, statistics) {
            if(!input)
                input = [];
            
            if(!points || !statistics)
                return;
                
            var output = [];
            var minLength = points.length < statistics.length ? points.length : statistics.length;
            for(var k = 0; k < minLength; k+=1) {
                output.push({"value": statistics[k].integral.value, "timestamp": points[k].deviceName + " - " + points[k].name});
            }
            if(output.length != input.length) {
                return output;
            } for(var k = 0; k < output.length; k+=1)
                if(output[k].value !== input[k].value || output[k].timestamp !== input[k].timestamp) {
                    return output;
                }
            return input; //Prevent infinite angular digest loop
        };
    }]);
    
    userModule.filter('fromJson', ['$filter', function($filter) {
        return function(input) {
            //console.log(input);
            try {
                return JSON.parse(input);
            } catch {
                console.log("Parsing error in fromJson filter for input: " + input);
                return null;
            }
        };
    }]);
    
    userModule.filter('sortByNumField', ['$filter', function($filter) {
        return function(input, field, asc) {
            try {
                return input.sort(function(a,b){ 
                    if(asc) return a[field]-b[field];
                    return b[field]-a[field];
                });
            } catch {
                console.log("Error sorting input: " + input);
                return input;
            }
        };
    }]);
    
    return userModule;
    
    }); // define
    

    Notice that series-1-values is the ouput array from the filter, so all points will be in a single series. It's possible this would be easier as a custom component, and example of which is in the next post....


  • Or you could use a component. It's about the same, but you're less likely to be troubled by infinite digest loop issues during development. Here's the markup:

    <ma-point-query query="{$and: true, deviceName:'Mango Internal', name:'oint'}" limit="3" points="points"></ma-point-query>
    
    <br/>
    <ma-point-statistics points="points" rendered=false from="dateBar.from" to="dateBar.to" statistics="statsObj"></ma-point-statistics>
    <integral-collator points="points" statistics="statsObj" output-var="outputArray2"></integral-collator>
    <ma-serial-chart  style="height: 300px; width: 100%" series-1-values="outputArray2" default-type="column" 
    options='{"categoryAxis":{"parseDates":false}}'></ma-serial-chart>
    

    And now we'll need to define this component in our user module,

    define(['angular', 'require'], function(angular, require) {
    'use strict';
    
    var userModule = angular.module('userModule', ['maUiApp']);
    
    class IntegralCollatorController {
        static get $$ngIsClass() { return true; }
        
        $onChanges(changes) {
            if (changes.points || changes.statistics && (this.points && this.statistics)) {
                this.calculateOutput();
            }
        }
        
        calculateOutput() {
            if(!this.points || !this.statistics)
                return;
                
            var output = [];
            var minLength = this.points.length < this.statistics.length ? this.points.length : this.statistics.length;
            for(var k = 0; k < minLength; k+=1) {
                output.push({"value": this.statistics[k].integral.value, "timestamp": this.points[k].deviceName + " - " + this.points[k].name});
            }
            //TODO sort output
            this.outputVar = output;
        }
    }
    
    userModule.component('integralCollator', {
        bindings: {
            points: '<',
            statistics: '<',
            outputVar: '='
        },
        controller: IntegralCollatorController
    });
    
    return userModule;
    
    }); // define
    

    This is probably the right solution, but both techniques are interesting.


  • Hi ! yes the second solution works fine !
    thank you :)
    No control of the sorting output thought yet !


  • You can see I put a //TODO sort output in there. You could pretty easily lift that sort function from the sortByNumField filter I showed, or you could use that filter in the ma-serial-chart with series-1-values='outputArray2|sortByNumField:"value":true'


  • Hi, my client finally bought the licence so we are starting to develop.
    I have the following error in my console but also some times rendered as html instead of my page ... Im not sure what is this mismatch but it has to do with the component you wrote for me
    Any idea how to fix this ?
    thank you

    Error bootstrapping Mango app: Mismatched anonymous define() module: function(angular, require) { 'use strict'; var userModule = angular.module('userModule', ['maUiApp']); class IntegralCollatorController { static get $$ngIsClass() { return true; } $onChanges(changes) { if (changes.points || changes.statistics && (this.points && this.statistics)) { this.calculateOutput(); } } calculateOutput() { if(!this.points || !this.statistics) return; var output = []; var minLength = this.points.length < this.statistics.length ? this.points.length : this.statistics.length; for(var k = 0; k < minLength; k+=1) { output.push({"value": this.statistics[k].integral.value, "timestamp": this.points[k].name}); } //TODO sort output this.outputVar = output; console.log(output); } } userModule.component('integralCollator', { bindings: { points: '<', statistics: '<', outputVar: '=' }, controller: IntegralCollatorController }); return userModule; } http://requirejs.org/docs/errors.html#mismatch
    Show stack trace
    Error: Mismatched anonymous define() module: function(angular, require) {
    'use strict';
    
    var userModule = angular.module('userModule', ['maUiApp']);
    
    class IntegralCollatorController {
        static get $$ngIsClass() { return true; }
        
        $onChanges(changes) {
            if (changes.points || changes.statistics && (this.points && this.statistics)) {
                this.calculateOutput();
            }
        }
        
        calculateOutput() {
            if(!this.points || !this.statistics)
                return;
                
            var output = [];
            var minLength = this.points.length < this.statistics.length ? this.points.length : this.statistics.length;
            for(var k = 0; k < minLength; k+=1) {
                output.push({"value": this.statistics[k].integral.value, "timestamp": this.points[k].name});
            }
            //TODO sort output
            this.outputVar = output;
            console.log(output);
        }
    }
    
    userModule.component('integralCollator', {
        bindings: {
            points: '<',
            statistics: '<',
            outputVar: '='
        },
        controller: IntegralCollatorController
    });
    
    return userModule;
    
    }
    http://requirejs.org/docs/errors.html#mismatch
        at makeError (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:39:108173)
        at O (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:39:115062)
        at Object.a [as require] (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:39:121430)
        at requirejs (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:39:108712)
        at Function.b.configureLocale (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:76:52295)
        at Function.b.setUser (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:76:51806)
        at m.r [as $get] (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:76:55709)
        at Object.invoke (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:269:26286)
        at http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:269:24185
        at u (http://2.38.152.46:8082/modules/mangoUI/web/mangoUi~ngMango~ngMangoServices.js?v=4b59ed889675c8a103ef:269:25665)
    

  • @alexzer how are you including the user module in your application? You can't just put it in as a script tag. Please see https://help.infiniteautomation.com/getting-started-with-a-user-module/