Snippets

Piotr Szrajber Smart M.App - summarizer stage

Created by Piotr Szrajber last modified
//# sourceURL=customization.js
/**
 * Stage summarizer creates a second crossfilter on the same data and allows
 * to perform computations that are only partially dependent on current stage's filter
 * without explicitly creating another stage in UI.
 * 
 */
var SummarizerStage = class {

    constructor(stage, gvc) {
        this.originalStage = stage;
        this.data = this.prepareData(stage.rows(), stage);
        this.gvc = gvc
        this.dc = gvc.dc;
        this.chartCounter = 0;

        let myStage = this.summarizerStage = this.createSummarizerStage();
        this.dimensions = this.summarizerStage.stageModel().fields.reduce((accumulation, field) => {
            let dimension = myStage.facts().dimension(function(d){
                return d[field.id];
            });
            accumulation[field.id] = dimension;
            return accumulation;
        }, {});
    }

    createSummarizerStage() {
        let stageId = "summarizer",
            summarizerStage = this.gvc.dataStage(stageId),
            originalStageModel = this.originalStage.stageModel(),
            stageModel = {
                id: stageId,
                fields: originalStageModel.fields,
                values: originalStageModel.values
            };

        summarizerStage.stageModel(stageModel);
        summarizerStage.table(this.data);
        summarizerStage.emitTable(stageModel);
        return summarizerStage;
    }

    // create copy of the data without fields and values of the other stage
    prepareData (data, stage) {
        let stageModel = stage.stageModel(),
            dimensionIds = stageModel.fields.map(field => field.id),
            measureIds = stageModel.values.map(value => value.id),
            ret = data.map(row => {
                return Object.keys(row).filter(key => {
                    if (dimensionIds.includes(key)) return false;
                    if (measureIds.includes(key)) return false;
                    if (/^\$/.test(key)) return false;
                    return true;
                }).reduce((accumulation, key) => {
                    accumulation[key] = row[key];
                    return accumulation;
                }, {});
            });
            return ret;
    }

    findDimensionByName(name) {
        return this.originalStage.stageModel().fields.find(field => field.name === name);
    }

    findMeasureByName(name) {
        return this.originalStage.stageModel().values.find(value => value.name === name);
    }

    createNumberDisplay(selector, config) {
        let domElement = document.querySelector(selector);
        if (!domElement) {
            console.error(`No ${selector} in the DOM`);
            return;
        }
        let measure = this.findMeasureByName(config.measure);
        if (!measure) {
            console.error(`No measure with name ${config.name}`);
            return null;
        }

        let chart = this.gvc.chart.number(domElement);
        this.summarizerStage.mountWidgets({
            id: `chart_${this.chartCounter++}`,
            chart: "number",
            name: config.name,
            values: measure,
            number: {
                html: config.html,
                format: config.format
            }
        }, chart);
        this.widgets = this.widgets || {};
        this.widgets[selector] = chart;
        return chart;
    }

    filter(filters) {
        if (!filters) {
            for (let dimensionName in this.dimensions) {
                this.dimensions[dimensionName].filter(null);
            }
        }
        for (let filterName in filters) {
            let dimensionModel = this.findDimensionByName(filterName);
            if (!dimensionModel) {
                console.warn(`No dimension named ${filterName}`);
                continue;
            }
            let dimensionObj = this.dimensions[dimensionModel.id];
            let filter = filters[filterName];
            if (Array.isArray(filter)) {
                if (filter.length > 0) {
                    dimensionObj.filter(function (d) {
                        return filter.indexOf(d) > -1;
                    });
                } else {
                    dimensionObj.filter(null);
                }
            } else {
                dimensionObj.filter(filter);
            }
        }
        for (let selector in this.widgets) {
            this.widgets[selector].redraw();
        }
    }
};

let GLOBALS = {};

/**
 * Function that waits till the charts are ready (they already have SVG element)
 * @param {Function} fn callback
 * @return {void}
 */
function chartsReady(fn) {
    let observer = new MutationObserver(function(mutations, me) {
        let chartWithSvg = document.querySelector(".widget-chart.dc-chart>svg");
        if (chartWithSvg) {
            fn();
            me.disconnect();
        }
    });

    observer.observe(document, {
        childList: true,
        subtree: true
    });
}

function findWidgetsByName(names, callback, errback) {
    gsp.bi.stage.findWidgets({
        descriptors: names.map(name => {
            return {
                chartM: {
                    name: name
                }
            };
        })
    }, function(widgets) {
        if (!widgets) return errback();
        callback.apply(null, widgets.map(widgetCollection => widgetCollection[0]));
    });
}

function fixTimelineWidget(widget) {
    let start = new Date(2012, 0, 1),
        end = new Date(2017, 6, 14); //0 is January so 6 is July

    widget.chart.xAxis().scale().domain([start, end]);
    widget.chart.elasticY(false);
    widget.chart.redraw();
}

function padTwo(num) {
    let str = "" + num,
        pad = "00",
        ans = pad.substring(0, pad.length - str.length) + str;
    return ans;
}

function setCurrentDate(year, month, day) {
    let dateElement = document.querySelector(".summary4 .widget-chart"),
        m = padTwo(month + 1),
        d = padTwo(day);
    if (dateElement) {
        dateElement.innerHTML = `<div class="date-display"><span class="year">${year}</span>-<span class="month">${m}</span>-<span class="day">${d}</span></div>`;
    }
}

function createNumberDisplays(importanceWidget, capacityWidget, timelinewidget) {
    gsp.bi.stage.requireLibraries(function(gvc, dc) {
        gsp.bi.stage.findStage(null, function(stage) {
            let ss = new SummarizerStage(stage, gvc);

            // initialize date filter
            ss.filter({
                DATE: Date(2017, 6, 14)
            });
            setCurrentDate(2017, 6, 14);
            // synchronize date filter in the summarizer with the right date of the timeline
            if (timelinewidget) {
                timelinewidget.chart.on("filtered", (chart, range) => {
                    if (range && range[1]) {
                        let year = range[1].getFullYear(),
                            month = range[1].getMonth(), // month w javascripcie jest liczony od 0
                            day = range[1].getDate();
                        ss.filter({
                            DATE: new Date(year, month, day)
                        });
                        setCurrentDate(year, month, day);
                    }
                });
            }
            //
            if (capacityWidget) {
                capacityWidget.chart.on("filtered", (chart, filter) => {
                    ss.filter({NAME: chart.filters()});
                });
            }

            //
            if (importanceWidget) {
                importanceWidget.chart.on("filtered", (chart, filter) => {
                    ss.filter({IMPORTANCE: chart.filters()});
                });
            }

            let summary1Selector = ".summary1 .widget-chart",
                summary2Selector = ".summary2 .widget-chart",
                summary3Selector = ".summary3 .widget-chart",
                summary1,
                summary2,
                summary3;

            if (summary1 = document.querySelector(summary1Selector)) {
                summary1.innerHTML = "";
                ss.createNumberDisplay(summary1Selector, {
                    measure: "STORAGE_Ml_pcent",
                    format: ".2f",
                    html: {
                        one: "Storage percentage: %number",
                        some: "Storage percentage: %number",
                        none: "Storage percentage: %number"
                    }
                });
            }

            if (summary2 = document.querySelector(summary2Selector)) {
                summary2.innerHTML = "";
                ss.createNumberDisplay(summary2Selector, {
                    measure: "BULK_WATER_STORAGE_sum",
                    format: ".2f",
                    html: {
                        one: "Bulk water storage sum: %number",
                        some: "Bulk water storage sum: %number",
                        none: "Bulk water storage sum: %number"
                    }
                });
            }

            if (summary3 = document.querySelector(summary3Selector)) {
                summary3.innerHTML = "";
                let nd = ss.createNumberDisplay(summary3Selector, {
                    measure: "DAY2DAY_LVL_CHANGE_avrg",
                    format: ".2f",
                    html: {
                        one: 'day to day average change: %number',
                        some: 'day to day average change: %number',
                        none: 'day to day average change: %number'
                    }
                });
                let classify = (x) => Math.abs(x) < 1e-6 ? "NO_CHANGE" : x < 0 ? "DECREASE" : "INCREASE";
                nd.formatNumber(function (v){
                    return `<span class="${classify(v)}">${d3.format(".2f")(v)}</span>`;
                });
            }
        });
    });
}

chartsReady(function() {
    findWidgetsByName(["Importance", "Dams Capacity", "Dam Storage Percentage in Time"], function(importanceWidget, capacityWidget, timelinewidget) {
        fixTimelineWidget(timelinewidget);
        createNumberDisplays(importanceWidget, capacityWidget, timelinewidget);
    });
});

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.