Friday, February 15, 2013

ZK Pivottable: Highlight Changed Cells and Update Values Partially


Introduction

This article describe how to update the live data partially as needed and highlight changed cells based on their status.

* Basic concept:

If the values are changed but structure is the same, update data partially

If the structure is changed, set new pivot model to pivottable and sync status of each cell

Pre-request

Get Value from Pivot Model
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-get-value-from-pivot-model.html

Compare Pivotmodel Structure
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-compare-pivotmodel.html

Sync Structure of Pivot Model
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-sync-structure-of-pivot.html

Compare Value of Specific Cell
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-compare-value-of-specific.html

Update Pivottable by Javascript
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-update-pivottable-by.html


The Program

index.zul

A pivottable, a textbox to display the added data, a button to add data, a pivot field control to control the fields in pivottable, also contains the javascript function to update the cells of pivottable.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <script><![CDATA[
        function update (items) {
            if (items && items.length > 0) {
                var dataRow = jq('.z-pivottable-cell-field')[0].parentNode,
                    style,
                    item,
                    rowIdx,
                    colIdx,
                    val,
                    idx,
                    currentRow = 0,
                    dir;
                // for each item
                for (idx = 0; idx < items.length; idx++) {
                    item = items[idx];
                    rowIdx = item.rowIdx;
                    colIdx = item.colIdx;
                    val = item.val;
                    dir = item.dir;
                    style = '';
                    // find row
                    while (currentRow < rowIdx) {
                        dataRow = dataRow.nextSibling;
                        currentRow++;
                    }
                    // find cell
                    cell = jq(dataRow).find('td')[colIdx];
                    // create style
                    if (dir) style = (dir == 'up'? 'style="color: green;"' : 'style="color: red;"');
                    // update cell with style
                    cell.firstChild.innerHTML = '<span '+style+'>' + val + '</span>';
                }
            }
        }
    ]]></script>
    <!-- window, apply a SelectorComposer -->
    <window id="win" apply="test.PVTPartialUpdateComposer">
        <vlayout>
            <textbox id="tbx" value="Added values: " width="800px" rows="10" />
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}"
                use="test.CustomPivottable"
                pageSize="9999" />
            <button id="btn" label="test add" />
            <pivot-field-control id="pfc" />
        </vlayout>
    </window>
</zk>


PVTPartialUpdateComposer.java

Update changed cells or sync all cell as needed.

package test;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
import org.zkoss.pivot.ui.PivotFieldControl;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zul.Textbox;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 * @author benbai123
 *
 */
@SuppressWarnings("rawtypes")
public class PVTPartialUpdateComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;
    @Wire
    private Textbox tbx;
    @Wire
    private Pivottable pivottable;
    @Wire
    private PivotFieldControl pfc;

    private DecimalFormat floatFormat = new DecimalFormat("##,###.00");
    private DecimalFormat intFormat = new DecimalFormat("##,###");
    // model used by pivottable
    // the model used by pivottable currently
    private TabularPivotModel _pivotModel;

    // copy of first model
    // used to determine the status of cells
    private TabularPivotModel _pivotModelFirstSnapshot;

    // model contains the newest raw data
    private TabularPivotModel _latestPivotModel;

    // used to store the value of current view
    private List<Number> _currentValues = null;

    // newest raw data
    private List<List<Object>> _latestRawData;

    @SuppressWarnings("unchecked")
    public void doAfterCompose (Component comp) throws Exception {
        super.doAfterCompose(comp);
        // init pivot field control
        pfc.setModel((TabularPivotModel)pivottable.getModel());
    }
    /**
     * Get pivottable's model, also make a snapshot of it
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = PVTModelProvider.getPivotModel();

            _pivotModelFirstSnapshot = PVTUtils.createModelSnapshot(_pivotModel);

            // init latest raw data
            _latestRawData = new ArrayList<List<Object>>();
            _latestRawData.addAll((List<List<Object>>)_pivotModel.getRawData());
            // init latest pivot model
            _latestPivotModel = _pivotModel;
        }
        return _pivotModel;
    }

    /**
     * Display the current content of pivottable
     * @throws Exception 
     */
    @Listen ("onClick = #btn")
    public void addData () throws Exception {
        // used to collect new data to display in textbox
        StringBuilder sb = new StringBuilder("");
        List<List<Object>> newData = PVTModelProvider.getNewDatas(sb);
        if (newData != null && newData.size() > 0) {
            tbx.setValue(tbx.getValue() + "\n" + sb.toString());
            _latestRawData.addAll(newData);
            _latestPivotModel = PVTUtils.cloneModelWithData(_pivotModel, _latestRawData); 
            if (PVTUtils.isStructureEqual(_pivotModel, _latestPivotModel, true, false)) {
                updateChangedData(_pivotModel, _latestPivotModel);
            } else {
                // clear stored value if structure is changed
                _currentValues = null;
                pivottable.setModel(_latestPivotModel);
                pfc.setModel((TabularPivotModel)pivottable.getModel());
                doUpdate(true);
                _pivotModel = _latestPivotModel;
            }
        }
    }
    /**
     * called when a node is opened/closed
     */
    @Listen ("onPivotNodeOpen = #pivottable")
    public void onPvtOpen () {
        syncOrReplace();
    }
    /**
     * called when the fields of pivottable are changed
     */
    @Listen ("onPivotFieldControlChange = #pfc")
    public void syncModelStructure () {
        PVTUtils.syncModelStructure(_pivotModel, _pivotModelFirstSnapshot);
        syncOrReplace();
    }
    /**
     * sync model structure and all cell status as needed
     */
    private void syncOrReplace () {
        // drop old values however
        _currentValues = null;
        // sync model structure if not the same instance
        if (_pivotModel != _latestPivotModel) {
            PVTUtils.syncModelStructure(_pivotModel, _latestPivotModel);
        }
        if (_pivotModel == _latestPivotModel
            || PVTUtils.isStructureEqual(_pivotModel, _latestPivotModel, true, false)) {
            doUpdate(true);
        } else {
            pivottable.setModel(_latestPivotModel);
            pfc.setModel((TabularPivotModel)pivottable.getModel());
            doUpdate(true);
            _pivotModel = _latestPivotModel;
        }
    }
    /**
     * update changed data only
     * @param currentModel
     * @param newModel
     */
    private void updateChangedData (TabularPivotModel currentModel, TabularPivotModel newModel) {
        // keep it as long as possible to reduce processing time,
        // another choice is drop it each time to reduce memory consumption
        if (_currentValues == null) {
            storeValue();
        }
        doUpdate(false);
    }
    /**
     * store values of current view of pivottable
     */
    private void storeValue () {

        _currentValues = new ArrayList<Number>();

        List<PivotHeaderNode> rows = PVTUtils.getRowLeafList(_pivotModel);
        List<PivotHeaderNode> columns = PVTUtils.getColumnLeafList(_pivotModel);

        rows.add(_pivotModel.getRowHeaderTree().getRoot());
        columns.add(_pivotModel.getColumnHeaderTree().getRoot());

        for (PivotHeaderNode row : rows) { // for each row
            // store original data
            storeCurrentValues(_pivotModel, row, columns, -1);

            // not first level row node
            if (row.getDepth() > 1) {
                PivotHeaderNode tmpRow = row;
                PivotHeaderNode parentRow = tmpRow.getParent();
                // continuously check ancestors
                // do if not first level and is last child
                while (tmpRow.getDepth() > 1
                        && PVTUtils.isLastChild(parentRow, tmpRow)) {
                    for (int calIdx = 0; calIdx < parentRow.getSubtotalCount(false); calIdx++) {
                        // store row subtotal
                        storeCurrentValues(_pivotModel, parentRow, columns, calIdx);
                    }
                    tmpRow = parentRow;
                    parentRow = tmpRow.getParent();
                }
            }
        }
    }

    /** loop through cells of a row
     * store values of current view of pivottable
     * 
     * @param model the model of current pivottable
     * @param row the row to loop through each cell
     * @param columns columns of row
     * @param rowCalIdx calculator index of row
     */
    private void storeCurrentValues (TabularPivotModel model, 
            PivotHeaderNode row, List<PivotHeaderNode> columns, int rowCalIdx) {
        // the length of data fields under a column
        int dataFieldsLength = model.getDataFields().length;
        // for each column
        for (PivotHeaderNode column : columns) {
            // for each data field
            for (int i = 0; i < dataFieldsLength; i++) {
                // get data value from pivot model by row node, column node and data index
                _currentValues.add(model.getValue(row, rowCalIdx, column, -1, i));
                
                // last data and
                // not first level node
                if (i+1 == dataFieldsLength
                    && column.getDepth() > 1) {
                    PivotHeaderNode tmpCol = column;
                    PivotHeaderNode parentColumn = tmpCol.getParent();
                    // continuously check ancestors
                    // do if not first level and is last child
                    while (tmpCol.getDepth() > 1
                            && PVTUtils.isLastChild(parentColumn, tmpCol)) {
                        // for each column calculator
                        for (int calIdx = 0; calIdx < parentColumn.getSubtotalCount(false); calIdx++) {
                            // again, for each data field
                            for (int j = 0; j < dataFieldsLength; j++) {
                                // get data value from pivot model by row node, column node and data index
                                _currentValues.add(model.getValue(row, rowCalIdx, parentColumn, calIdx, j));
                            }
                        }
                        tmpCol = parentColumn;
                        parentColumn = tmpCol.getParent();
                    }
                }
            }
        }
    }

    /**
     * execute script to update client side cells of pivottable
     * @param syncAll true: sync status of each cell, false: update changed cell only
     */
    private void doUpdate (boolean syncAll) {

        // used to store the update script
        StringBuilder sb = new StringBuilder("");

        // get row/column nodes from latest pivot model
        List<PivotHeaderNode> rows = PVTUtils.getRowLeafList(_latestPivotModel);
        List<PivotHeaderNode> columns = PVTUtils.getColumnLeafList(_latestPivotModel);
        // add column root for grand total for columns
        columns.add(_latestPivotModel.getColumnHeaderTree().getRoot());
        // add row root for grand total for rows
        rows.add(_latestPivotModel.getRowHeaderTree().getRoot());
        // init Position object, track the index of row/cell,
        // also track index of current value if not syncAll as needed
        Position pos = new Position(0, 0);
        // init CellAttributes object, track the attributes of a cell while loop through nodes
        CellAttributes cell = new CellAttributes(null, null, null, null, null);

        // script start
        sb.append("update([");
        // iterate through current view to add the elements to update
        for (PivotHeaderNode row : rows) { // for each row
            // add row keys and clear other attributes
            cell.updateAttributes(PVTUtils.getNodeKeys(row), null, null, null, null);
            // add elements to update
            addUpdateElements(_latestPivotModel, row, columns, -1, sb, pos, cell, syncAll);
            // to the first column of next row
            pos.toNextRow();

            // not first level row node,
            // have to check and update parent sub-total if
            // current row is the last child
            if (row.getDepth() > 1) {
                PivotHeaderNode tmpRow = row;
                PivotHeaderNode parentRow = tmpRow.getParent();
                // continuously check ancestors
                // do if not first level and is last child
                while (tmpRow.getDepth() > 1
                        && PVTUtils.isLastChild(parentRow, tmpRow)) {
                    parentRow = tmpRow.getParent();
                    for (int calIdx = 0; calIdx < parentRow.getSubtotalCount(false); calIdx++) {
                        cell.setRowKeys(PVTUtils.getNodeKeys(parentRow));
                        cell.setRowCalculatorLabelKey(parentRow.getField().getSubtotals()[calIdx].getLabelKey());
                        addUpdateElements(_latestPivotModel, parentRow, columns, calIdx, sb, pos, cell, syncAll);
                        
                        pos.toNextRow();
                    }
                    tmpRow = parentRow;
                    parentRow = tmpRow.getParent();
                }
            }
        }

        // remove last comma
        if (sb.substring(sb.length()-1).equals(",")) {
            sb.replace(sb.length()-1, sb.length(), "");
        }
        sb.append("]);");

        String cmd = sb.toString();
        if (!"update([]);".equals(cmd)) {
            Clients.evalJavaScript(sb.toString());
        }
        sb.setLength(0);
    }

    /**
     * loop through whole row to check
     * and add elements to update as needed
     * @param model the latest model
     * @param row the row to loop through
     * @param columns all columns of row
     * @param rowCalIdx calculator index of row
     * @param sb StringBuilder to append elements to update
     * @param pos Position class to track current position and pointer of old value list
     * @param cell CellAttributes class to track cell information
     * @param syncAll whether to sync all cell status or update changed value only
     */
    private void addUpdateElements (TabularPivotModel model, 
            PivotHeaderNode row, List<PivotHeaderNode> columns, int rowCalIdx,
            StringBuilder sb, Position pos, CellAttributes cell, boolean syncAll) {
        TabularPivotField[] dataFields = model.getDataFields();
        // the length of data fields under a column
        int dataFieldsLength = dataFields.length;

        for (PivotHeaderNode column : columns) { // for each column
            cell.setColKeys(PVTUtils.getNodeKeys(column));
            // for each data field
            for (int i = 0; i < dataFieldsLength; i++) {
                cell.setDataFieldName(dataFields[i].getFieldName());

                addUpdateElement(_pivotModelFirstSnapshot, _currentValues, cell,
                        model.getValue(row, rowCalIdx, column, -1, i), pos, sb, syncAll);
                // get data value from pivot model by row node, column node and data index

                pos.increaseCol();
                // last data and
                // not first level node
                if (i+1 == dataFieldsLength
                    && column.getDepth() > 1) {
                    PivotHeaderNode tmpCol = column;
                    PivotHeaderNode parentColumn = tmpCol.getParent();
                    // continuously check ancestors
                    // do if not first level and is last child
                    while (tmpCol.getDepth() > 1
                            && PVTUtils.isLastChild(parentColumn, tmpCol)) {
                        cell.setColKeys(PVTUtils.getNodeKeys(parentColumn));

                        // for each column calculator
                        for (int calIdx = 0; calIdx < parentColumn.getSubtotalCount(false); calIdx++) {
                            Calculator cal = parentColumn.getField().getSubtotals()[calIdx];

                            cell.setColCalculatorLabelKey(cal.getLabelKey());
                            // again, for each data field
                            for (int j = 0; j < dataFieldsLength; j++) {
                                cell.setDataFieldName(dataFields[j].getFieldName());
                                addUpdateElement(_pivotModelFirstSnapshot, _currentValues, cell,
                                        model.getValue(row, rowCalIdx, parentColumn, calIdx, j), pos, sb, syncAll);
                                // get data value from pivot model by row node, column node and data index

                                pos.increaseCol();
                            }
                        }
                        tmpCol = parentColumn;
                        parentColumn = tmpCol.getParent();
                    }
                    // clear column calculator if any before next loop
                    cell.setColCalculatorLabelKey(null);
                }
            }
        }
    }
    /**
     * add the element to update to StringBuilder as needed
     * @param basemodel the model as the compare base
     * @param valueList the list of old values (might be null if not syncAll)
     * @param cell attributes describe current cell
     * @param value latest value of current cell
     * @param pos position of the row/column in current view of pivottable
     * @param sb StringBuilder contains update script
     * @param syncAll whether sync all cell or update changed cell only
     */
    private void addUpdateElement (TabularPivotModel basemodel, List<Number> valueList,
            CellAttributes cell, Number value, Position pos, StringBuilder sb, boolean syncAll) {
        String dir;
        // sync all
        if (syncAll) {
            dir = PVTUtils.getDirection(basemodel, cell, value);
            // status 'up' or 'down'
            if (dir != null && !dir.isEmpty()) {
                addUpdateElement(pos.getRowIdx(), pos.getColIdx(), value, dir, sb);
            }
        } else {
            // sync changed
            int ptr = pos.getPtr();
            if (value != null) {
                Number oldv = valueList.get(ptr);
                double valToComp = (oldv == null? 0 : oldv.doubleValue());
                // value changed
                if (value.doubleValue() != valToComp) {
                    dir = PVTUtils.getDirection(basemodel, cell, value);
                    addUpdateElement(pos.getRowIdx(), pos.getColIdx(), value, dir, sb);
                    // update value list
                    valueList.set(ptr, value);
                }
            }
        }
    }
    private void addUpdateElement (int rowIdx, int colIdx, Number val, String dir, StringBuilder sb) {
        sb.append("{rowIdx:")
            .append(rowIdx).append(",")
            .append("colIdx:")
            .append(colIdx).append(",")
            .append("val:'")
            .append(val instanceof Integer ? intFormat.format(val) : floatFormat.format(val)).append("',");
        if (dir != null) {
            sb.append("dir:")
                .append("'").append(dir).append("'");
        }
        sb.append("},");
    }
}


PVTModelProvider.java

Provide first pivot model and add data.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotModel;

/**
 * Provide pivot model, create new data
 * @author benbai123
 *
 */
public class PVTModelProvider {
    /**
     * Get pivottable's model, also make a snapshot of it
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    public static TabularPivotModel getPivotModel () throws Exception {
        List<List<Object>> rawData = getData();
        TabularPivotModel pivotModel;
        pivotModel = new TabularPivotModel(rawData, getColumns());

        // assign rows, the order matches to the level of row node field
        pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
        pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
        pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

        // assign columns, the order matches to the level of column node field
        pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
        pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);
        pivotModel.setFieldType("ColumnThree", PivotField.Type.COLUMN);

        // assign datas, the order matches to the order of data field
        pivotModel.setFieldType("DataOne", PivotField.Type.DATA);
        pivotModel.setFieldType("DateTwo", PivotField.Type.DATA);

        PivotField field = pivotModel.getField("RowOne");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        field = pivotModel.getField("RowTwo");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        field = pivotModel.getField("ColumnOne");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        field = pivotModel.getField("ColumnTwo");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});

        return pivotModel;
    }

    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public static List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add("ColumnThree - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }
    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public static List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo", "ColumnThree",
                "DataOne", "DateTwo"
        });
    }
    /**
     * Generate some random data
     * @return
     */
    public static List<List<Object>> getNewDatas(StringBuilder sb) {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();
        int amount = 1;

        for (int i = 0; i < amount; i++) {
            List<Object> data = new ArrayList<Object>();
            Object o;
            o = "RowOne - " + (r.nextInt(5) + 1);
            sb.append("add: ")
                .append(o)
                .append("\t");
            data.add(o);
            o = "RowTwo - " + (r.nextInt(5) + 1);
            sb.append(o)
                .append("\t");
            data.add(o);
            o = "RowThree - " + (r.nextInt(5) + 1);
            sb.append(o)
                .append("\t");
            data.add(o);
            o = "ColumnOne - " + (r.nextInt(5) + 1);
            sb.append(o)
                .append("\t");
            data.add(o);
            o = "ColumnTwo - " + (r.nextInt(5) + 1);
            sb.append(o)
                .append("\t");
            data.add(o);
            o = "ColumnThree - " + (r.nextInt(5) + 1);
            sb.append(o)
                .append("\t");
            data.add(o);
            o = -5 + r.nextInt(11); // -5 ~ 5
            sb.append(o)
                .append("\t");
            data.add(o);
            o = -5 + r.nextInt(11);
            sb.append(o)
                .append("\t\n");
            data.add(o);
            result.add(data);
        }
        return result;
    }
}


PVTUtils.java

A bunch of utilities to handle/retrieve status of pivot model, most of them are part of previous posts.

package test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.PivotHeaderTree;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;

/**
 * Utilities to handle/retrieve pivot model status
 * @author benbai123
 *
 */
public class PVTUtils {
    public static int INDEX_NOT_FOUND = -2;
    public static PivotHeaderNode NODE_NOT_FOUND = null;

    /**
     * Create snapshot of a pivot model
     * @param model
     * @return
     */
    @SuppressWarnings("unchecked")
    public static TabularPivotModel createModelSnapshot (TabularPivotModel model) {
        // copy raw data
        List<List<Object>> rawData = new ArrayList<List<Object>>();
        rawData.addAll((List<List<Object>>)model.getRawData());

        TabularPivotField[] fields = model.getFields();

        // get columns from old model
        List<String> columns = new ArrayList<String>();
        // set field
        for (TabularPivotField tpf : fields) {
            columns.add(tpf.getFieldName());
        }

        TabularPivotModel snapShot = new TabularPivotModel(rawData, columns);
        syncModelStructure(model, snapShot);
        return snapShot;
    }
    /**
     * Create a new pivot model based on
     * current pivot model and new data 
     * @param model
     * @param newData
     * @return
     */
    public static TabularPivotModel cloneModelWithData (TabularPivotModel model, List<List<Object>>newData) {
        TabularPivotField[] fields = model.getFields();

        // get columns from old model
        List<String> columns = new ArrayList<String>();
        // set field
        for (TabularPivotField tpf : fields) {
            columns.add(tpf.getFieldName());
        }

        TabularPivotModel newModel = new TabularPivotModel(newData, columns);
        PVTUtils.syncModelStructure(model, newModel);
        return newModel;
    }
    /**
     * called when the fields of first pivottable are changed
     */
    public static void syncModelStructure (TabularPivotModel model, TabularPivotModel modelTwo) {
        syncFields(model.getRowFields(), modelTwo);
        syncFields(model.getColumnFields(), modelTwo);
        syncFields(model.getDataFields(), modelTwo);
        syncFields(model.getFields(PivotField.Type.UNUSED), modelTwo);
        syncOpenStatus(model.getRowHeaderTree().getRoot(), modelTwo.getRowHeaderTree().getRoot(), false);
        syncOpenStatus(model.getColumnHeaderTree().getRoot(), modelTwo.getColumnHeaderTree().getRoot(), false);
    }
    private static void syncFields (TabularPivotField[] fields, TabularPivotModel model) {
        for (TabularPivotField f : fields) {
            model.setFieldType(f.getFieldName(), f.getType());

            PivotField field = model.getField(f.getFieldName());
            model.setFieldSubtotals(field, f.getSubtotals());
        }
    }
    /**
     * Synchronize the open status of two pivot header trees
     * 
     * @param root the root of the base pivot header tree (or its sub trees)
     * @param rootTwo the root of the pivot header tree (or its sub trees) to sync
     * @param checkAll whether sync whole tree, <br>
     * true: sync whole tree, put every node of base pivot header tree into open list to sync<br>
     * false: sync only current view, only put the displayed node into open list to sync
     */
    private static void syncOpenStatus (PivotHeaderNode root, PivotHeaderNode rootTwo, boolean checkAll) {
        Map<Object, PivotHeaderNode> originalOpenMap = new HashMap<Object, PivotHeaderNode>();

        // sync displayed node only if not checkAll
        // so do not need to scan whole header tree
        for (PivotHeaderNode node : root.getChildren()) {
            // checkAll: sync each node
            // !checkAll: sync displayed node
            if (checkAll
                || (node.getDepth() == 1 || node.getParent().isOpen())) {
                originalOpenMap.put(node.getKey(), node);
            }
        }
        // for each node in children of rootTwo
        for (PivotHeaderNode newNode : rootTwo.getChildren()) {
            PivotHeaderNode node = originalOpenMap.get(newNode.getKey());
            if (node != null) {
                newNode.setOpen(node.isOpen());
                // recursively sync sub trees
                syncOpenStatus(node, newNode, checkAll);
            }
        }
    }
    /**
     * Check whether two pivot models are structure-equal
     * @param modelOne the first pivot model
     * @param modelTwo the second pivot model
     * @param openedOnly whether only check the opened nodes and leaf
     * @param openedOnly whether only check the leaf nodes
     * @return boolean
     */
    public static boolean isStructureEqual (TabularPivotModel modelOne, TabularPivotModel modelTwo, boolean openedOnly, boolean leafOnly) {
        boolean equal = true;

        List<PivotHeaderNode> rows = getNodeList(modelOne.getRowHeaderTree(), openedOnly, leafOnly);
        List<PivotHeaderNode> columns = getNodeList(modelOne.getColumnHeaderTree(), openedOnly, leafOnly);
        List<PivotHeaderNode> rowsTwo = getNodeList(modelTwo.getRowHeaderTree(), openedOnly, leafOnly);
        List<PivotHeaderNode> columnsTwo = getNodeList(modelTwo.getColumnHeaderTree(), openedOnly, leafOnly);
        TabularPivotField[] dataFields = modelOne.getDataFields();
        TabularPivotField[] dataFieldsTwo = modelTwo.getDataFields();

        if (rows.size() != rowsTwo.size()
            || columns.size() != columnsTwo.size()
            || dataFields.length != dataFieldsTwo.length) {
            equal = false;
        } else {
            equal = isNodesEqual(rows, rowsTwo, openedOnly) && isNodesEqual(columns, columnsTwo, openedOnly);
        }
        return equal;
    }
    /**
     * check whether a node is the last child of specified parent node
     * @param parent
     * @param node
     * @return
     */
    public static boolean isLastChild (PivotHeaderNode parent, PivotHeaderNode node) {
        List<? extends PivotHeaderNode> children = parent.getChildren();
        // use == to make sure they are the same instance
        return node.getParent() == parent
                && children.get(children.size()-1) == node;
    }
    public static List<PivotHeaderNode> getRowLeafList (TabularPivotModel model) {
        return PVTUtils.getNodeList(model.getRowHeaderTree(), true, true);
    }
    public static  List<PivotHeaderNode> getColumnLeafList (TabularPivotModel model) {
        return PVTUtils.getNodeList(model.getColumnHeaderTree(), true, true);
    }
    /**
     * Get pivot nodes in a pivot header tree
     * @param headerTree the pivot header tree to get pivot nodes
     * @param openedOnly whether get only the opened nodes and leaf
     * @param leafOnly whether get only leaf node
     * @return
     */
    @SuppressWarnings("unchecked")
    public static List<PivotHeaderNode> getNodeList (PivotHeaderTree headerTree, boolean openedOnly, boolean leafOnly) {
        PivotHeaderNode root = headerTree.getRoot();
        List<PivotHeaderNode> all = new ArrayList<PivotHeaderNode>();
        List<PivotHeaderNode> nodes = new ArrayList<PivotHeaderNode>();
        List<PivotHeaderNode> tmp = new ArrayList<PivotHeaderNode>();
        nodes = (List<PivotHeaderNode>)root.getChildren();

        // all: all target nodes
        // nodes: the node list to loop through
        // tmp: temp store the children while loop through nodes
        boolean foundAllNodes = false;
        while (!foundAllNodes) {
            foundAllNodes = true;
            for (PivotHeaderNode phn : nodes) {
                // get only opened and leaf nodes
                // if opened only
                if (phn.isOpen() || !openedOnly) {
                    List<PivotHeaderNode> children = (List<PivotHeaderNode>)phn.getChildren();
                    if (children != null && children.size() > 0) {
                        tmp.addAll(children);
                        foundAllNodes = false;
                    } else {
                        // add to all if haven't found any children
                        // so do not need to loop through them again
                        if (foundAllNodes && leafOnly) {
                            all.add(phn);
                        } else {
                            // already found some children,
                            // add to tmp to make the order correct 
                            tmp.add(phn);
                        }
                    }
                } else {
                    if (foundAllNodes && leafOnly) {
                        all.add(phn);
                    } else {
                        tmp.add(phn);
                    }
                }
            }
            if (!leafOnly) {
                all.addAll(nodes);
            }
            nodes = tmp;
            tmp = new ArrayList<PivotHeaderNode>();
        }
        return all;
    }

    /**
     * Compare the nodes under two node lists one by one
     * @param list the first node list
     * @param listTwo the second node list
     * @param openedOnly whether compare calculator according to open status
     * @return
     */
    private static boolean isNodesEqual (List<PivotHeaderNode> list, List<PivotHeaderNode> listTwo, boolean openedOnly) {
        boolean equal = true;
        int i, j;
        // compare nodes
        for (i = 0; i < list.size() && equal; i++) {
            PivotHeaderNode node = list.get(i);
            PivotHeaderNode nodeTwo = listTwo.get(i);

            // key should be equal
            // depth should be equal
            // subtotal count should be equal
            //
            // openedOnly: get subtotal count according to the open stats of node
            // !openedOnly: get subtotal count as the node is opened
            if (!node.getKey().equals(nodeTwo.getKey())
                || node.getDepth() != nodeTwo.getDepth()
                || node.getSubtotalCount(node.isOpen() || !openedOnly) != nodeTwo.getSubtotalCount(nodeTwo.isOpen() || !openedOnly)) {
                equal = false;
                break;
            }

            // check calculators if any
            // openedOnly: get subtotal count according to the open stats of node
            // !openedOnly: get subtotal count as the node is opened
            if (node.getSubtotalCount(node.isOpen() || !openedOnly) > 0) {
                Calculator[] cals = node.getField().getSubtotals();
                Calculator[] calsTwo = nodeTwo.getField().getSubtotals();
                for (j = 0; j < cals.length; j++) {
                    Calculator cal = cals[j];
                    Calculator calTwo = calsTwo[j];

                    // label and label key should be euqal
                    if (!cal.getLabel().equals(calTwo.getLabel())
                        || !cal.getLabelKey().equals(calTwo.getLabelKey())) {
                        equal = false;
                        break;
                    }
                }
            }
        }
        return equal;
    }

    /**
     * get a list of key to this node
     * @param node pivot header node
     * @return
     */
    public static List<Object> getNodeKeys (PivotHeaderNode node) {
        List<Object> keys = new ArrayList<Object>();
        if (node != null) {
            while (node.getDepth() > 0) {
                keys.add(0, node.getKey());
                node = node.getParent();
            }
        }
        return keys;
    }
    /**
     * get the status of a cell
     * @param model the base pivot model to compare
     * @param cell the attributes denote a cell
     * @param value the new value
     * @return<br>
     * "up": new value is larger than old value of that cell in base pivot model<br>
     * "down": new value is smaller than old value of that cell in base pivot model<br>
     * null: otherwise
     */
    public static String getDirection (TabularPivotModel model, CellAttributes cell, Number value) {
        String dir = null;
        double base = 0.0;
        if (value != null) {
            Number oldValue = getValue(model, cell);
            if (oldValue != null) {
                base = oldValue.doubleValue();
            }
            double newValue = value.doubleValue();
            dir = newValue > base? "up" :
                    newValue < base? "down" : null;
        }
        return dir;
    }
    /**
     * get value from a model based on cellAttr
     * @param model the model to get value
     * @param cellAttr the attributes represent a specific cell
     * @return
     */
    public static Number getValue (TabularPivotModel model, CellAttributes cellAttr) {
        PivotHeaderNode row = findNode(model.getRowHeaderTree().getRoot(), cellAttr.getRowKeys());
        PivotHeaderNode col = findNode(model.getColumnHeaderTree().getRoot(), cellAttr.getColKeys());
        int dataIdx = findDataFieldIndex(model, cellAttr.getDataFieldName());
        int rowCalIdx = -1;
        int colCalIdx = -1;
        if (row != null) {
            rowCalIdx = findCalculatorIndex(row, cellAttr.getRowCalculatorLabelKey());
        }
        if (col != null) {
            colCalIdx = findCalculatorIndex(col, cellAttr.getColCalculatorLabelKey());
        }
        if (row == NODE_NOT_FOUND // row should exists
            || col == NODE_NOT_FOUND // col should exists
            || dataIdx == INDEX_NOT_FOUND // data field should exists
            || rowCalIdx == INDEX_NOT_FOUND // row calculator should exists if _rowCalculatorLabelKey is not null
            || colCalIdx == INDEX_NOT_FOUND) { // column calculator should exists if _colCalculatorLabelKey is not null
            return null;
        }
        return model.getValue(row, rowCalIdx, col, colCalIdx, dataIdx);
    }
    /**
     * find the corresponding node in a pivot model
     * @param root The root of pivot header tree to search the corresponding node
     * @param keys list of node key
     * @return PivotHeaderNode the corresponding node, NODE_NOT_FOUND denotes not found
     */
    public static PivotHeaderNode findNode (PivotHeaderNode root, List<Object> keys) {
        PivotHeaderNode node = null;
        boolean found = true;
        node = root;
        if (keys == null || keys.size() == 0) {
            return root; // grand total
        }
        // for each key
        for (Object o : keys) {
            if (found) { // stop if not found in previous loop
                found = false;
                // search until found a row with the key
                for (PivotHeaderNode child : node.getChildren()) {
                    if (child.getKey().equals(o)) {
                        node = child;
                        found = true;
                        break;
                    }
                }
            }
        }

        return found? node : // header nood
            NODE_NOT_FOUND; // not found
    }
    /**
     * Search the corresponding data field index in a pivot model
     * @param model the model to search
     * @param fieldName the name of data field
     * @return int the corresponding data field index, INDEX_NOT_FOUND denotes not found
     */
    public static int findDataFieldIndex (TabularPivotModel model, String fieldName) {
        // field name should not be null
        if (fieldName != null) {
            TabularPivotField[] dataFields = model.getDataFields();
            for (int idx = 0; idx < dataFields.length; idx++) {
                if (dataFields[idx].getFieldName().equals(fieldName)) {
                    return idx;
                }
            }
        }
        return INDEX_NOT_FOUND; // not found
    }
    /**
     * Search the corresponding calculator index of a pivot field
     * @param field the field to search
     * @param labelKey the labelKey of calculator
     * @return int the corresponding calculator index, INDEX_NOT_FOUND denotes not found
     */
    public static int findCalculatorIndex (PivotHeaderNode node, String labelKey) {
        if (labelKey == null) return -1;
        Calculator[] cals = node.getField().getSubtotals();
        for (int i = 0; i < cals.length; i++) {
            if (cals[i].getLabelKey().equals(labelKey)) {
                return i;
            }
        }
        return INDEX_NOT_FOUND; // not found
    }
}


CustomPivottable.java

Post the event of node open/close of pivottable.

package test;

import org.zkoss.pivot.Pivottable;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.ui.event.Events;

public class CustomPivottable extends Pivottable {

    private static final long serialVersionUID = 4770700380255057252L;

    public void service(AuRequest request, boolean everError) {
        String cmd = request.getCommand();
        super.service(request, everError);
        // post onPivotNodeOpen event
        if ("onPivotNodeOpen".equals(cmd)) {
            Events.postEvent("onPivotNodeOpen", this, request.getData());
        }
    }
}


Position.java

Attributes used to denote a cell in current view of pivottable

package test;

/**
 * attributes used to denote a cell in current view of pivottable
 * and the index points to old value while
 * loop through values and build update script as needed
 * @author benbai123
 *
 */
public class Position {
    // index of current row in the view
    private int _rowIdx = 0;
    // index of current column in the view
    private int _colIdx = 0;
    // index to get stored value
    private int _ptr = 0;
    // constructor
    public Position (int rowIdx, int colIdx) {
        _rowIdx = rowIdx;
        _colIdx = colIdx;
    }
    // move to next row
    public void toNextRow () {
        increaseRow();
        resetCol();
    }
    // control
    public void increaseRow () {
        _rowIdx++;
    }
    public void increaseCol () {
        _colIdx++;
    }
    public void resetCol () {
        _colIdx = 0;
    }
    // getters
    public int getRowIdx () {
        return _rowIdx;
    }
    public int getColIdx () {
        return _colIdx;
    }
    public int getPtr () {
        _ptr ++;
        return _ptr - 1;
    }
}


CellAttributes.java

Attributes used to denotes a specific cell

package test;

import java.util.List;

/**
 * attributes used to denotes a specific cell
 * @author benbai123
 *
 */
public class CellAttributes {
    // keys to denote a row node
    private List<Object> _rowKeys;
    // keys to denote a column node
    private List<Object> _colKeys;
    // name to denotes a data field
    private String _dataFieldName;
    // labelKey to denote a row calculator
    private String _rowCalculatorLabelKey;
    // labelKey to denote a column calculator
    private String _colCalculatorLabelKey;

    // constructor
    public CellAttributes (List<Object> rowKeys, List<Object> colKeys, String dataFieldName,
            String rowCalculatorLabelKey, String colCalculatorLabelKey) {
        _rowKeys = rowKeys;
        _colKeys = colKeys;
        _dataFieldName = dataFieldName;
        _rowCalculatorLabelKey = rowCalculatorLabelKey;
        _colCalculatorLabelKey = colCalculatorLabelKey;
    }
    /**
     * used to reset all attributes
     * @param rowKeys
     * @param colKeys
     * @param dataFieldName
     * @param rowCalculatorLabelKey
     * @param colCalculatorLabelKey
     */
    public void updateAttributes (List<Object> rowKeys, List<Object> colKeys, String dataFieldName,
            String rowCalculatorLabelKey, String colCalculatorLabelKey) {
        _rowKeys = rowKeys;
        _colKeys = colKeys;
        _dataFieldName = dataFieldName;
        _rowCalculatorLabelKey = rowCalculatorLabelKey;
        _colCalculatorLabelKey = colCalculatorLabelKey;
    }
    // setters/getters
    public void setRowKeys (List<Object> rowKeys) {
        _rowKeys = rowKeys;
    }
    public List<Object> getRowKeys () {
        return _rowKeys;
    }
    public void setColKeys (List<Object> colKeys) {
        _colKeys = colKeys;
    }
    public List<Object> getColKeys () {
        return _colKeys;
    }
    public void setDataFieldName (String dataFieldName) {
        _dataFieldName = dataFieldName;
    }
    public String getDataFieldName () {
        return _dataFieldName;
    }
    public void setRowCalculatorLabelKey (String rowCalculatorLabelKey) {
        _rowCalculatorLabelKey = rowCalculatorLabelKey;
    }
    public String getRowCalculatorLabelKey () {
        return _rowCalculatorLabelKey;
    }
    public void setColCalculatorLabelKey (String colCalculatorLabelKey) {
        _colCalculatorLabelKey = colCalculatorLabelKey;
    }
    public String getColCalculatorLabelKey () {
        return _colCalculatorLabelKey;
    }
}


The Result

View demo on line
http://screencast.com/t/g4c3BfhCj2Re

Reference

Start point of APIs
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/UpdateLivedataPartially

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/UpdateDataPartially.swf

Wednesday, February 13, 2013

ZK Pivottable: Compare Value of Specific Cell


Introduction

This article describe how to compare the value of specific node in two pivot models based on the 'key of rows', 'key of columns', 'data field name' and 'calculator label key' in java code.

Pre-request

Display Data in ZK Pivottable
http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html

Sync Structure of Pivot Model
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-sync-structure-of-pivot.html

The Program

index.zul

There are two pivottables with different structure, a textbox will display the message with respect to the compare result while button clicked.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <!-- window, apply a SelectorComposer -->
    <window id="win"
        apply="test.TestComposer">
        <vlayout>
            <!-- textbox, display the compare result -->
            <textbox id="tbx" rows="13" width="500px" />
            <!-- button, click to do compare and show result in the textbox above -->
            <button id="btn" label="show compare result" />
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}" />
            <pivottable id="pivottableTwo" model="${win$composer.pivotModelTwo}" />
        </vlayout>
    </window>
</zk>


TestComposer.java

There is a inner class 'CellAttr' used to describe a specific cell, create several CellAttr in doAfterCompose method and try to get and compare the values from two pivot model based on each CellAttr when button clicked.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Textbox;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;

    @Wire
    Textbox tbx;

    private TabularPivotModel _pivotModel;
    private TabularPivotModel _pivotModelTwo;

    // A list of specific cell
    private List<CellAttr> cells;

    /**
     * Add several CellAttr
     */
    @SuppressWarnings("unchecked")
    public void doAfterCompose (Component comp) throws Exception {
        super.doAfterCompose(comp);
        cells = new ArrayList<CellAttr>();
        // grand total of columns of RowOne - 1
        cells.add(new CellAttr(Arrays.asList(
                new String[]{"RowOne - 1"}
            ), 
            null,
            "DataOne",
            null,
            null
        ));
        // Row node: "RowOne - 1" - "RowTwo - 1" - "RowThree - 5"
        // Column node: "ColumnOne - 1" - "ColumnTwo - 1"
        // only exists in second pivottable
        cells.add(new CellAttr(Arrays.asList(
                new String[]{"RowOne - 1", "RowTwo - 1", "RowThree - 5"}
            ), 
            Arrays.asList(
                new String[]{"ColumnOne - 1", "ColumnTwo - 1"}
            ), 
            "DataOne",
            null,
            null
        ));
        // Row node: "RowOne - 1"
        // Column node: "ColumnOne - 1"
        // Row Calculator Label Key: MAX Label Key
        // only exists in first pivottable
        cells.add(new CellAttr(Arrays.asList(
                new String[]{"RowOne - 1"}
            ), 
            Arrays.asList(
                new String[]{"ColumnOne - 1"}
            ), 
            "DataOne",
            StandardCalculator.MAX.getLabelKey(),
            null
        ));
        // Row node: "RowOne - 1"
        // Column node: "ColumnOne - 1"
        // Row Calculator Label Key: SUM Label Key
        cells.add(new CellAttr(Arrays.asList(
                new String[]{"RowOne - 1"}
            ), 
            Arrays.asList(
                new String[]{"ColumnOne - 1"}
            ), 
            "DataOne",
            StandardCalculator.SUM.getLabelKey(),
            null
        ));
    }
    /**
     * model for first pivottable
     * @return
     * @throws Exception
     */
    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = getPivotModel(getData(), getColumns());
            // only first pivottable has MAX calculator on RowOne
            PivotField field = _pivotModel.getField("RowOne");
            _pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        }
        return _pivotModel;
    }
    /**
     * model for second pivottable
     * @return
     * @throws Exception
     */
    public TabularPivotModel getPivotModelTwo () throws Exception {
        if (_pivotModelTwo == null) {
            List<List<Object>> rawData = getData();

            // only second pivottable has RowThree - 5
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - 1");
            data.add("RowTwo - 1");
            data.add("RowThree - 5");
            data.add("ColumnOne - 1");
            data.add("ColumnTwo - 1");
            data.add(5);
            rawData.add(data);

            _pivotModelTwo = getPivotModel(rawData, getColumns());
        }
        return _pivotModelTwo;
    }

    /**
     * create a pivot model
     * @param data
     * @param Columns
     * @return
     * @throws Exception
     */
    public TabularPivotModel getPivotModel (List<List<Object>> data, List<String> columns) throws Exception {
        TabularPivotModel pivotModel = new TabularPivotModel(data, columns);

        // assign rows, the order matches to the level of row node field
        pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
        pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
        pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

        // assign columns, the order matches to the level of column node field
        pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
        pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

        // assign datas, the order matches to the order of data field
        pivotModel.setFieldType("DataOne", PivotField.Type.DATA);

        PivotField field = pivotModel.getField("RowOne");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM});
        return pivotModel;
    }

    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }

    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo",
                "DataOne"
        });
    }
    @Listen("onClick = #btn")
    public void doCompare () {
        StringBuilder sb = new StringBuilder("");
        for (CellAttr cell : cells) {
            sb.append(cell.getCellInfo());
            Number val = getValue(_pivotModel, cell);
            Number valTwo = getValue(_pivotModelTwo, cell);
            if (val == null) {
                sb.append("First pivottable does not contain this cell");
            } else {
                sb.append("The value of this cell in first pivottable is ")
                    .append(val);
            }
            sb.append("\n");
            if (valTwo == null) {
                sb.append("Second pivottable does not contain this cell");
            } else {
                sb.append("The value of this cell in second pivottable is ")
                    .append(valTwo);
            }
            sb.append("\n");
            if (val != null
                && valTwo != null) {
                if (val.doubleValue() > valTwo.doubleValue()) {
                    sb.append("The value of this cell in second pivottable is smaller than first pivottable\n");
                } else if (val.doubleValue() < valTwo.doubleValue()) {
                    sb.append("The value of this cell in second pivottable is grater than first pivottable\n");
                }
            }
            sb.append("\n\n");
        }
        tbx.setValue(sb.toString());
    }
    /**
     * get value from a model based on cellAttr
     * @param model the model to get value
     * @param cellAttr the attributes represent a specific cell
     * @return
     */
    public Number getValue (TabularPivotModel model, CellAttr cellAttr) {
        PivotHeaderNode row = findNode(model.getRowHeaderTree().getRoot(), cellAttr._rowKeys);
        PivotHeaderNode col = findNode(model.getColumnHeaderTree().getRoot(), cellAttr._colKeys);
        int dataIdx = findDataFieldIndex(model, cellAttr._dataFieldName);
        int rowCalIdx = -1;
        int colCalIdx = -1;
        if (row != null) {
            rowCalIdx = findCalculatorIndex(row, cellAttr._rowCalculatorLabelKey);
        }
        if (col != null) {
            colCalIdx = findCalculatorIndex(col, cellAttr._colCalculatorLabelKey);
        }
        if (row == null // row should exists
            || col == null // col should exists
            || dataIdx == -2 // data field should exists
            || rowCalIdx == -2 // row calculator should exists if _rowCalculatorLabelKey is not null
            || colCalIdx == -2) { // column calculator should exists if _colCalculatorLabelKey is not null
            return null;
        }
        return model.getValue(row, rowCalIdx, col, colCalIdx, dataIdx);
    }
    /**
     * find the corresponding node in a pivot model
     * @param root The root of pivot header tree to search the corresponding node
     * @param keys list of node key
     * @return PivotHeaderNode the corresponding node, null denotes not found
     */
    public static PivotHeaderNode findNode (PivotHeaderNode root, List keys) {
        PivotHeaderNode node = null;
        boolean found = true;
        node = root;
        if (keys == null || keys.size() == 0) {
            return root; // grand total
        }
        // for each key
        for (Object o : keys) {
            if (found) { // stop if not found in previous loop
                found = false;
                // search until found a row with the key
                for (PivotHeaderNode child : node.getChildren()) {
                    if (child.getKey().equals(o)) {
                        node = child;
                        found = true;
                        break;
                    }
                }
            }
        }

        return found? node : // header nood
                null; // not found
    }
    /**
     * Search the corresponding data field index in a pivot model
     * @param model the model to search
     * @param fieldName the name of data field
     * @return int the corresponding data field index, -2 denotes not found
     */
    public static int findDataFieldIndex (TabularPivotModel model, String fieldName) {
        // field name should not be null
        if (fieldName != null) {
            TabularPivotField[] dataFields = model.getDataFields();
            for (int idx = 0; idx < dataFields.length; idx++) {
                if (dataFields[idx].getFieldName().equals(fieldName)) {
                    return idx;
                }
            }
        }
        return -2; // not found
    }
    /**
     * Search the corresponding calculator index of a pivot field
     * @param field the field to search
     * @param labelKey the labelKey of calculator
     * @return int the corresponding calculator index, -2 denotes not found
     */
    public static int findCalculatorIndex (PivotHeaderNode node, String labelKey) {
        if (labelKey == null) return -1;
        Calculator[] cals = node.getField().getSubtotals();
        for (int i = 0; i < cals.length; i++) {
            if (cals[i].getLabelKey().equals(labelKey)) {
                return i;
            }
        }
        return -2; // not found
    }
    /** Inner class represent a specific data cell
     * 
     * @author benbai123
     *
     */
    class CellAttr {
        public List<String> _rowKeys;
        public List<String> _colKeys;
        public String _dataFieldName;
        public String _rowCalculatorLabelKey;
        public String _colCalculatorLabelKey;
        public CellAttr (List<String> rowKeys, List<String> colKeys, String dataFieldName,
                String rowCalculatorLabelKey, String colCalculatorLabelKey) {
            _rowKeys = rowKeys;
            _colKeys = colKeys;
            _dataFieldName = dataFieldName;
            _rowCalculatorLabelKey = rowCalculatorLabelKey;
            _colCalculatorLabelKey = colCalculatorLabelKey;
        }
        public String getCellInfo () {
            StringBuilder sb = new StringBuilder("");
            sb.append("Row: ");
            if (_rowKeys == null || _rowKeys.size() == 0) {
                sb.append("Grand Total");
            } else {
                for (String key : _rowKeys) {
                    sb.append(key).append(" - ");
                }
            }
            sb.append("\nColumn: ");
            if (_colKeys == null || _colKeys.size() == 0) {
                sb.append("Grand Total");
            } else {
                for (String key : _colKeys) {
                    sb.append(key).append(" - ");
                }
            }
            sb.append("\nData: ")
                .append(_dataFieldName)
                .append("\nRow Calculator: ")
                .append(_rowCalculatorLabelKey == null? "Data" : _rowCalculatorLabelKey)
                .append("\nColumn Calculator: ")
                .append(_colCalculatorLabelKey == null? "Data" : _rowCalculatorLabelKey)
                .append("\n");
            return sb.toString();
        }
    }
}


The Result

View demo on line
http://screencast.com/t/UepdOCle

Reference

Start point of APIs
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/CompareValueOfSpecificCell

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/CompareValueOfSpecificCell.swf

ZK Pivottable: Update Pivottable by Javascript



Introduction

Assume you already known which row/col should be updated and the old value/new value of that cell, you can update pivottable by javascript and update only one cell instead of update the pivotmodel then recalculate/rerender, this way you can save lots of network bandwidth.

This article describe how to update pivottable by javascript.

Pre-request

Display Data in ZK Pivottable
http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html

The Program

index.zul

There is a pivottable on this page, a textbox let you input the update script, the update script will be executed while button clicked.

The format of update script is update (rowIdx, colIdx, val, dir), where rowIdx denotes the index of row, starts from 0, colIdx denotes the index of column, starts from 0, you can locate a specific cell by rowIdx and colIdx, val denotes the value to update, dir denotes whether the new value is grater/lower than the old value, based on dir the new value will be colored by green/red as needed.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <script><![CDATA[
        function update (rowIdx, colIdx, val, dir) {
            var dataRow = jq('.z-pivottable-cell-field')[0].parentNode,
                style,
                currentRow = 0;
            style = '';
            // find data tr by rowIdx
            while (currentRow < rowIdx) {
                dataRow = dataRow.nextSibling;
                currentRow++;
            }
            // find data td by colIdx
            cell = jq(dataRow).find('td')[colIdx];
            // create style by dir ('up' or 'down')
            if (dir) style = (dir == 'up'? 'style="color: green;"' : 'style="color: red;"');
            // update data cell
            cell.firstChild.innerHTML = '<span '+style+'>' + val + '</span>';
        }
    ]]></script>
    <!-- window, apply a SelectorComposer -->
    <window id="win"
        apply="test.TestComposer">
        <vlayout>
            <textbox id="tbx" width="700px" />
            <button label="updateBtn">
                <attribute name="onClick"><![CDATA[
                    Clients.evalJavaScript(tbx.getValue());
                ]]></attribute>
            </button>
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}" />
        </vlayout>
    </window>
</zk>


TestComposer.java

Simply provide a pivotmodel.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotModel;

import org.zkoss.zk.ui.select.SelectorComposer;


/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;

    private TabularPivotModel _pivotModel;

    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = new TabularPivotModel(getData(), getColumns());

            // assign rows, the order matches to the level of row node field
            _pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
            _pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
            _pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

            // assign columns, the order matches to the level of column node field
            _pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
            _pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

            // assign datas, the order matches to the order of data field
            _pivotModel.setFieldType("DataOne", PivotField.Type.DATA);

            PivotField field = _pivotModel.getField("RowOne");
            _pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});

        }
        return _pivotModel;
    }

    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }

    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo",
                "DataOne"
        });
    }
}


The Result

View demo on line
http://screencast.com/t/WzufgfjpLNS

Reference

The dom tree of pivottable from firebug

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/UpdatePivottableByJavascript

Demo flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/UpdatePivottableByJavascript.swf

ZK Pivottable: Sync Structure of Pivot Model


Introduction

This article describe how to copy the structure from one pivottable to another, the structure here denotes row field, column field, data field, calculator and open status.

Pre-request

Sync the Open Status of Pivotmodel
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-sync-open-status-of.html

The Program

index.zul

There are two pivottables, a button and a pivot field control, the button will add Min calculator to first pivot table and then call the function to copy the structure to second pivottable, the pivot field control controls the first pivottable, the second pivottable will copy the structure from first pivottable while fields are changed.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <!-- window, apply a SelectorComposer -->
    <window id="win"
        apply="test.TestComposer">
        <vlayout>
            <button id="addBtn" label="add Min calculator to RowOne" />
            <!-- pivottable, get model from window's composer -->
            <hlayout>
                <pivottable id="pivottable" model="${win$composer.pivotModel}"
                    width="500px"
                    grandTotalForColumns="false"
                    use="test.ListenOpenPivottable" />
                <pivot-field-control id="pfc" width="300px" />
            </hlayout>
            <pivottable id="pivottableTwo" model="${win$composer.pivotModelTwo}"
                width="500px"
                grandTotalForColumns="false" />
        </vlayout>
    </window>
</zk>


TestComposer.java

Provide pivot model, listen to button click, pivot field control change and pivot node open event to sync the structure of second pivottable.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
import org.zkoss.pivot.ui.PivotFieldControl;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;

    @Wire
    PivotFieldControl pfc;
    @Wire
    private Pivottable pivottableTwo;

    private TabularPivotModel _pivotModel;
    private TabularPivotModel _pivotModelTwo;

    @SuppressWarnings("unchecked")
    public void doAfterCompose (Component comp) throws Exception {
        super.doAfterCompose(comp);
        pfc.setModel(getPivotModel());
    }
    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = getPivotModel(getData(), getColumns());
        }
        return _pivotModel;
    }
    public TabularPivotModel getPivotModelTwo () throws Exception {
        if (_pivotModelTwo == null) {
            _pivotModelTwo = getPivotModel(getData(), getColumns());
        }
        return _pivotModelTwo;
    }

    public TabularPivotModel getPivotModel (List<List<Object>> data, List<String> columns) throws Exception {
        TabularPivotModel pivotModel = new TabularPivotModel(data, columns);

        // assign rows, the order matches to the level of row node field
        pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
        pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
        pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

        // assign columns, the order matches to the level of column node field
        pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
        pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

        // assign datas, the order matches to the order of data field
        pivotModel.setFieldType("DataOne", PivotField.Type.DATA);

        PivotField field = pivotModel.getField("RowOne");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        return pivotModel;
    }

    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }

    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo",
                "DataOne"
        });
    }
    /**
     * called when the node of first pivottable is opened/closed
     */
    @Listen ("onPivotNodeOpen = #pivottable")
    public void syncOpenStatus () {
        syncOpenStatus(_pivotModel.getRowHeaderTree().getRoot(), _pivotModelTwo.getRowHeaderTree().getRoot(), false);
        syncOpenStatus(_pivotModel.getColumnHeaderTree().getRoot(), _pivotModelTwo.getColumnHeaderTree().getRoot(), false);

        pivottableTwo.setModel(null);
        pivottableTwo.setModel(_pivotModelTwo);
    }
    /**
     * called when the fields of first pivottable are changed
     */
    @Listen ("onPivotFieldControlChange = #pfc")
    public void syncModelStructure () {
        TabularPivotField[] fields = _pivotModel.getFields();
        for (TabularPivotField f : fields) {
            _pivotModelTwo.setFieldType(f.getFieldName(), f.getType());

            // sync field
            PivotField field = _pivotModelTwo.getField(f.getFieldName());
            // sync calculator
            _pivotModelTwo.setFieldSubtotals(field, f.getSubtotals());
        }
        syncOpenStatus();
    }
    /**
     * called when add button clicked
     */
    @Listen ("onClick = #addBtn")
    public void addCalculator () {
        PivotField field = _pivotModel.getField("RowOne");
        _pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX, StandardCalculator.MIN});
        syncModelStructure();
    }
    /**
     * Synchronize the open status of two pivot header trees
     * 
     * @param root the root of the base pivot header tree (or its sub trees)
     * @param rootTwo the root of the pivot header tree to sync (or its sub trees)
     * @param checkAll whether sync whole tree, <br>
     * true: sync whole tree, put every node of base pivot header tree into open list to sync<br>
     * false: sync only current view, only put the displayed node into open list to sync
     */
    private void syncOpenStatus (PivotHeaderNode root, PivotHeaderNode rootTwo, boolean checkAll) {
        Map<Object, PivotHeaderNode> originalOpenMap = new HashMap<Object, PivotHeaderNode>();

        // sync displayed node only if not checkAll
        // so do not need to scan whole header tree
        for (PivotHeaderNode node : root.getChildren()) {
            // checkAll: sync each node
            // !checkAll: sync displayed node
            if (checkAll
                || (node.getDepth() == 1 || node.getParent().isOpen())) {
                originalOpenMap.put(node.getKey(), node);
            }
        }
        // for each node in children of rootTwo
        for (PivotHeaderNode newNode : rootTwo.getChildren()) {
            PivotHeaderNode node = originalOpenMap.get(newNode.getKey());
            if (node != null) {
                newNode.setOpen(node.isOpen());
                // recursively sync sub trees
                syncOpenStatus(node, newNode, checkAll);
            }
        }
    }
}


ListenOpenPivottable.java

The same as ListenOpenPivottable.java in SyncStatus

package test;

import org.zkoss.pivot.Pivottable;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.ui.event.Events;

/**
 * Tested with ZK 6.0.2 and Pivottable 2.0.0
 *
 */
public class ListenOpenPivottable extends Pivottable {

    private static final long serialVersionUID = 4770700380255057252L;

    public void service(AuRequest request, boolean everError) {
        String cmd = request.getCommand();
        super.service(request, everError);
        // post onPivotNodeOpen event
        if ("onPivotNodeOpen".equals(cmd)) {
            Events.postEvent("onPivotNodeOpen", this, request.getData());
        }
    }
}


The Result

View demo on line
http://screencast.com/t/JmAL1wxdA

Reference

Start point of APIs
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/SyncPivotmodelStructure

Demo flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/SyncPivotModelStructure.swf

Sunday, February 10, 2013

ZK Pivottable: Sync the Open Status of Pivotmodel



Introduction

Assume there are two pivottables with different pivotmodel, the two pivotmodels have the same structure but different value.

This article describe how to synchronize the open status from one pivot model to another, i.e., when open or close the node of one pivottable, the corresponding node in another pivottable will also be opened or closed.

Pre-request

Display Data in Pivottable
http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html

The Program

index.zul

Contains two pivottable, the first one use a custom pivottable that post onPivotNodeOpen event to itself, the open status of second pivottable will follow the first pivottable.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <!-- window, apply a SelectorComposer -->
    <window id="win"
        apply="test.TestComposer">
        <vlayout>
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}" use="test.ListenOpenPivottable" />
            <pivottable id="pivottableTwo" model="${win$composer.pivotModelTwo}" />
        </vlayout>
    </window>
</zk>


TestComposer.java

Synchronize the open status from first pivot model to second pivot model when the node in first pivottable is opened/closed.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotModel;

import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;

    @Wire
    private Pivottable pivottableTwo;

    private TabularPivotModel _pivotModel;
    private TabularPivotModel _pivotModelTwo;

    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = getPivotModel(getData(), getColumns());
        }
        return _pivotModel;
    }
    public TabularPivotModel getPivotModelTwo () throws Exception {
        if (_pivotModelTwo == null) {
            _pivotModelTwo = getPivotModel(getData(), getColumns());
        }
        return _pivotModelTwo;
    }

    public TabularPivotModel getPivotModel (List<List<Object>> data, List<String> columns) throws Exception {
        TabularPivotModel pivotModel = new TabularPivotModel(data, columns);

        // assign rows, the order matches to the level of row node field
        pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
        pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
        pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

        // assign columns, the order matches to the level of column node field
        pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
        pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

        // assign datas, the order matches to the order of data field
        pivotModel.setFieldType("DataOne", PivotField.Type.DATA);

        PivotField field = pivotModel.getField("RowOne");
        pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        return pivotModel;
    }
    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }

    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo",
                "DataOne"
        });
    }
    @Listen ("onPivotNodeOpen = #pivottable")
    public void syncOpenStatus () {
        syncOpenStatus(_pivotModel.getRowHeaderTree().getRoot(), _pivotModelTwo.getRowHeaderTree().getRoot(), false);
        syncOpenStatus(_pivotModel.getColumnHeaderTree().getRoot(), _pivotModelTwo.getColumnHeaderTree().getRoot(), false);

        // reset model
        pivottableTwo.setModel(null);
        pivottableTwo.setModel(_pivotModelTwo);
    }
    /**
     * Synchronize the open status of two pivot header trees
     * 
     * @param root the root of the base pivot header tree (or its sub trees)
     * @param rootTwo the root of the pivot header tree to sync (or its sub trees)
     * @param checkAll whether sync whole tree, <br>
     * true: sync whole tree, put every node of base pivot header tree into open list to sync<br>
     * false: sync only current view, only put the displayed node into open list to sync
     */
    private void syncOpenStatus (PivotHeaderNode root, PivotHeaderNode rootTwo, boolean checkAll) {
        Map<Object, PivotHeaderNode> originalOpenMap = new HashMap<Object, PivotHeaderNode>();

        // sync displayed node only if not checkAll
        // so do not need to scan whole header tree
        for (PivotHeaderNode node : root.getChildren()) {
            // checkAll: sync each node
            // !checkAll: sync displayed node
            if (checkAll
                || (node.getDepth() == 1 || node.getParent().isOpen())) {
                originalOpenMap.put(node.getKey(), node);
            }
        }
        // for each node in children of rootTwo
        for (PivotHeaderNode newNode : rootTwo.getChildren()) {
            PivotHeaderNode node = originalOpenMap.get(newNode.getKey());
            if (node != null) {
                newNode.setOpen(node.isOpen());
                // recursively sync sub trees
                syncOpenStatus(node, newNode, checkAll);
            }
        }
    }
}


ListenOpenPivottable.java

Custom pivottable, will post onPivotNodeOpen event back.

package test;

import org.zkoss.pivot.Pivottable;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.ui.event.Events;

/**
 * Tested with ZK 6.0.2 and Pivottable 2.0.0
 *
 */
public class ListenOpenPivottable extends Pivottable {

    private static final long serialVersionUID = 4770700380255057252L;

    public void service(AuRequest request, boolean everError) {
        String cmd = request.getCommand();
        super.service(request, everError);
        // post onPivotNodeOpen event
        if ("onPivotNodeOpen".equals(cmd)) {
            Events.postEvent("onPivotNodeOpen", this, request.getData());
        }
    }
}


The Result

View demo on line
http://screencast.com/t/HuW2n4kixiXG

Reference

The start point of a bunch of APIs
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html


Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/SyncPivotmodelStatus

ZK Pivottable: Compare Pivotmodel Structure


Introduction

This article describe how to check whether two pivot model has the same structure (opened view or whole model).

Def: "The same structure" denotes they have the same row fields, the same column fields, the same amount of rows, the same amount of columns, the same calculators.

Pre-request

Display Data in Pivottable
http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html

The Program

index.zul

There are two pivottables, in the begining, they are structure-equivalent, click buttons to add something to second pivottable or open/close nodes to see the changed result.

<zk>
    <!-- Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0 -->
    <!-- window, apply a SelectorComposer -->
    <window id="win" xmlns:w="client"
        apply="test.PVTCompareStructureComposer">
        <vlayout>
            <!-- pivottable, get model from window's composer -->
            <button id="addRowBtn" label="add RowThree - 3 to second pivottable" />
            <button id="addCalBtn" label="add Min calculator to RowOne of second pivottable" />
            <button id="compareBtn" label="show compare result" />

            <pivottable id="pivottable" model="${win$composer.pivotModel}" />
            <pivottable id="pivottableTwo" model="${win$composer.pivotModelTwo}" />

             the current view of the two models are structure-equal? 
            <label id="lbOne" value="" />
             the whole structure of the two models are structure-equal? 
            <label id="lbTwo" value="" />
        </vlayout>
    </window>
</zk>


PVTCompareStructureComposer.java

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.zkoss.pivot.Calculator;
import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.PivotHeaderTree;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.StandardCalculator;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;

import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Label;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 */
@SuppressWarnings("rawtypes")
public class PVTCompareStructureComposer extends SelectorComposer {
    /**
     * generated serial version UID
     */
    private static final long serialVersionUID = -2897873399288955635L;

    @Wire
    private Pivottable pivottable;
    @Wire
    private Pivottable pivottableTwo;
    @Wire
    private Label lbOne;
    @Wire
    private Label lbTwo;
    private TabularPivotModel _pivotModel;
    private TabularPivotModel _pivotModelTwo;

    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = new TabularPivotModel(getData(), getColumns());

            // assign rows, the order matches to the level of row node field
            _pivotModel.setFieldType("RowOne", PivotField.Type.ROW);
            _pivotModel.setFieldType("RowTwo", PivotField.Type.ROW);
            _pivotModel.setFieldType("RowThree", PivotField.Type.ROW);

            // assign columns, the order matches to the level of column node field
            _pivotModel.setFieldType("ColumnOne", PivotField.Type.COLUMN);
            _pivotModel.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

            // assign datas, the order matches to the order of data field
            _pivotModel.setFieldType("DataOne", PivotField.Type.DATA);

            PivotField field = _pivotModel.getField("RowOne");
            _pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});

        }
        return _pivotModel;
    }
    public TabularPivotModel getPivotModelTwo () throws Exception {
        if (_pivotModelTwo == null) {
            _pivotModelTwo = new TabularPivotModel(getData(), getColumns());

            // assign rows, the order matches to the level of row node field
            _pivotModelTwo.setFieldType("RowOne", PivotField.Type.ROW);
            _pivotModelTwo.setFieldType("RowTwo", PivotField.Type.ROW);
            _pivotModelTwo.setFieldType("RowThree", PivotField.Type.ROW);

            // assign columns, the order matches to the level of column node field
            _pivotModelTwo.setFieldType("ColumnOne", PivotField.Type.COLUMN);
            _pivotModelTwo.setFieldType("ColumnTwo", PivotField.Type.COLUMN);

            // assign datas, the order matches to the order of data field
            _pivotModelTwo.setFieldType("DataOne", PivotField.Type.DATA);

            PivotField field = _pivotModelTwo.getField("RowOne");
            _pivotModelTwo.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
        }
        return _pivotModelTwo;
    }

    /**
     * prepare the data for pivottable's model
     * The order of object put into data list should match
     * the order of column name's
     * @return
     * @throws Exception
     */
    public List<List<Object>> getData() throws Exception {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("RowOne - " + (r.nextInt(2) + 1));
            data.add("RowTwo - " + (r.nextInt(2) + 1));
            data.add("RowThree - " + (r.nextInt(2) + 1));
            data.add("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(10));
            result.add(data);
        }
        return result;
    }

    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo", "RowThree",
                "ColumnOne", "ColumnTwo",
                "DataOne"
        });
    }

    @Listen ("onClick = #addRowBtn")
    public void addRow () {
        Random r = new Random();
        List<Object> data = new ArrayList<Object>();
        data.add("RowOne - 1");
        data.add("RowTwo - 1");
        data.add("RowThree - 3");
        data.add("ColumnOne - 1");
        data.add("ColumnTwo - 1");
        data.add(r.nextInt(10));
        ((List<List<Object>>)_pivotModelTwo.getRawData()).add(data);
        _pivotModelTwo.updateRawData();
        pivottableTwo.setModel(null);
        pivottableTwo.setModel(_pivotModelTwo);
    }

    @Listen ("onClick = #addCalBtn")
    public void addCalculator () {
        PivotField field = _pivotModelTwo.getField("RowOne");
        _pivotModelTwo.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX, StandardCalculator.MIN});
    }

    @Listen ("onClick = #compareBtn")
    public void doCompare () {
        boolean isEqual = isStructureEqual(_pivotModel, _pivotModelTwo, true);
        lbOne.setValue(isEqual + "");
        lbOne.setStyle("font-size: 18px;"+(isEqual? "color: green;" : "color: red;"));
        isEqual = isStructureEqual(_pivotModel, _pivotModelTwo, false);
        lbTwo.setValue(isEqual + "");
        lbTwo.setStyle("font-size: 18px;"+(isEqual? "color: green;" : "color: red;"));
    }

    /**
     * Check whether two pivot models are structure-equal
     * @param modelOne the first pivot model
     * @param modelTwo the second pivot model
     * @param openedOnly whether only check the opened node and leaf
     * @return boolean
     */
    private boolean isStructureEqual (TabularPivotModel modelOne, TabularPivotModel modelTwo, boolean openedOnly) {
        boolean equal = true;

        List<PivotHeaderNode> rows = getNodeList(modelOne.getRowHeaderTree(), openedOnly);
        List<PivotHeaderNode> columns = getNodeList(modelOne.getColumnHeaderTree(), openedOnly);
        List<PivotHeaderNode> rowsTwo = getNodeList(modelTwo.getRowHeaderTree(), openedOnly);
        List<PivotHeaderNode> columnsTwo = getNodeList(modelTwo.getColumnHeaderTree(), openedOnly);
        TabularPivotField[] dataFields = modelOne.getDataFields();
        TabularPivotField[] dataFieldsTwo = modelTwo.getDataFields();

        if (rows.size() != rowsTwo.size()
            || columns.size() != columnsTwo.size()
            || dataFields.length != dataFieldsTwo.length) {
            equal = false;
        } else {
            equal = compareNodeList(rows, rowsTwo, openedOnly) && compareNodeList(columns, columnsTwo, openedOnly);
        }
        return equal;
    }
    /**
     * Get pivot nodes in a pivot header tree
     * @param headerTree the pivot header tree to get pivot nodes
     * @param openedOnly whether get only the opened nodes and leaf
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<PivotHeaderNode> getNodeList (PivotHeaderTree headerTree, boolean openedOnly) {
        PivotHeaderNode root = headerTree.getRoot();
        List<PivotHeaderNode> all = new ArrayList<PivotHeaderNode>();
        List<PivotHeaderNode> nodes = new ArrayList<PivotHeaderNode>();
        List<PivotHeaderNode> tmp = new ArrayList<PivotHeaderNode>();
        nodes = (List<PivotHeaderNode>)root.getChildren();

        // all: all target nodes
        // nodes: the node list to loop through
        // tmp: temp store the children while loop through nodes
        boolean foundAllNodes = false;
        while (!foundAllNodes) {
            foundAllNodes = true;
            for (PivotHeaderNode phn : nodes) {
                // get only onened and leaf nodes
                // if opened only
                if (phn.isOpen() || !openedOnly) {
                    List<PivotHeaderNode> children = (List<PivotHeaderNode>)phn.getChildren();
                    if (children != null && children.size() > 0) {
                        tmp.addAll(children);
                        foundAllNodes = false;
                    } else {
                        tmp.add(phn);
                    }
                } else {
                    tmp.add(phn);
                }
            }
            all.addAll(nodes);
            nodes = tmp;
            tmp = new ArrayList<PivotHeaderNode>();
        }
        return all;
    }

    /**
     * Compare the nodes under two node lists one by one
     * @param list the first node list
     * @param listTwo the second node list
     * @param openedOnly whether compare calculator according to open status
     * @return
     */
    private boolean compareNodeList (List<PivotHeaderNode> list, List<PivotHeaderNode> listTwo, boolean openedOnly) {
        boolean equal = true;
        int i, j;
        // compare nodes
        for (i = 0; i < list.size() && equal; i++) {
            PivotHeaderNode node = list.get(i);
            PivotHeaderNode nodeTwo = listTwo.get(i);

            // key should be equal
            // depth should be equal
            // subtotal count should be equal
            //
            // openedOnly: get subtotal count according to the open stats of node
            // !openedOnly: get subtotal count as the node is opened
            if (!node.getKey().equals(nodeTwo.getKey())
                || node.getDepth() != nodeTwo.getDepth()
                || node.getSubtotalCount(node.isOpen() || !openedOnly) != nodeTwo.getSubtotalCount(nodeTwo.isOpen() || !openedOnly)) {
                equal = false;
                break;
            }

            // check calculators if any
            // openedOnly: get subtotal count according to the open stats of node
            // !openedOnly: get subtotal count as the node is opened
            if (node.getSubtotalCount(node.isOpen() || !openedOnly) > 0) {
                Calculator[] cals = node.getField().getSubtotals();
                Calculator[] calsTwo = nodeTwo.getField().getSubtotals();
                for (j = 0; j < cals.length; j++) {
                    Calculator cal = cals[j];
                    Calculator calTwo = calsTwo[j];

                    // label and label key should be euqal
                    if (!cal.getLabel().equals(calTwo.getLabel())
                        || !cal.getLabelKey().equals(calTwo.getLabelKey())) {
                        equal = false;
                        break;
                    }
                }
            }
        }
        return equal;
    }
}


The Result

View demo on line
http://screencast.com/t/z6v4RAvkcv

Reference

The start point of a bunch of APIs
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html


Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/CompareModelStructure

Demo flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/Compare_Pivotmodel_Structure.swf