Wednesday, February 13, 2013

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

Sunday, February 3, 2013

ZK Pivottable: Get Value from Pivot Model



Introduction

This article describe how to get value from pivot model as needed (e.g., draw graph by pivot model content or use another component to display the content of pivot model), including basic value, value of calculators and grand total.

Pre-request

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

Use calculator
http://ben-bai.blogspot.tw/2012/10/use-custom-calculator-in-zk-pivottable.html

And the official documents in References

The Program

index.zul

A simple zul page contains a pivottable, a button and a textbox, the textbox will show the current values of pivottable while button clicked.

<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.TestComposer">
        <vlayout>
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}" />
            <button id="btn" label="update" />
            <textbox id="tbx" hflex="1" rows="20" />
        </vlayout>
    </window>
</zk>


TestComposer.java

Provide data and show pivot model value in textbox.

Regarding to show pivot model value, please refer to showCurrentContent method, it climbs rowHeaderTree and columnHeaderTree to get all current leaf node then get there value by TabularPivotModel#getValue.

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.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;
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
    private Textbox tbx;
    private TabularPivotModel _pivotModel;

    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    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);

            // 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);
            _pivotModel.setFieldType("DateTwo", PivotField.Type.DATA);

            PivotField field = _pivotModel.getField("RowOne");
            _pivotModel.setFieldSubtotals(field, new Calculator[] {StandardCalculator.SUM, StandardCalculator.MAX});
            field = _pivotModel.getField("ColumnOne");
            _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 matches
     * the order of column name's order
     * @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("ColumnOne - " + (r.nextInt(2) + 1));
            data.add("ColumnTwo - " + (r.nextInt(2) + 1));
            data.add(r.nextInt(100)+1000);
            data.add(r.nextInt(100)+1000);
            result.add(data);
        }
        return result;
    }
    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "RowOne", "RowTwo",
                "ColumnOne", "ColumnTwo",
                "DataOne", "DateTwo"
        });
    }

    /**
     * Display the current content of pivottable
     */
    @Listen ("onClick = #btn")
    public void showCurrentContent () {
        StringBuilder sb = new StringBuilder("");

        PivotHeaderTree rowHeaderTree = _pivotModel.getRowHeaderTree();
        PivotHeaderTree columnHeaderTree = _pivotModel.getColumnHeaderTree();
        PivotHeaderNode rRoot = rowHeaderTree.getRoot();
        PivotHeaderNode cRoot = columnHeaderTree.getRoot();
        // the length of data fields under a column
        int dataFieldsLength = _pivotModel.getDataFields().length;

        List<PivotHeaderNode> rows = getNodeList(rowHeaderTree);
        List<PivotHeaderNode> columns = getNodeList(columnHeaderTree);

        rows.add(rRoot);
        for (PivotHeaderNode row : rows) { // for each row
            // show original data
            displayColumns(rRoot, cRoot, row, columns, dataFieldsLength, -1, sb);
            // not first level row node
            if (row.getDepth() > 1) {
                PivotHeaderNode parentRow = row.getParent();
                List children = parentRow.getChildren();
                // is last child
                if (children.get(children.size()-1).equals(row)) {
                    for (int calIdx = 0; calIdx < parentRow.getSubtotalCount(false); calIdx++) {
                        // show row subtotal
                        displayColumns(rRoot, cRoot, parentRow, columns, dataFieldsLength, calIdx, sb);
                    }
                }
            }
        }

        tbx.setValue(sb.toString());
    }
    /**
     * Get all current "leaf" node of row/column
     * @param headerTree
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<PivotHeaderNode> getNodeList (PivotHeaderTree headerTree) {
        PivotHeaderNode root = headerTree.getRoot();
        List<PivotHeaderNode> nodes = new ArrayList<PivotHeaderNode>();
        List<PivotHeaderNode> tmp = new ArrayList<PivotHeaderNode>();
        nodes = (List<PivotHeaderNode>)root.getChildren();

        boolean foundAllRows = false;
        while (!foundAllRows) {
            foundAllRows = true;
            for (PivotHeaderNode phn : nodes) {
                // get only leaf nodes
                // you can adjust condition as needed
                if (phn.isOpen()) {
                    List<PivotHeaderNode> children = (List<PivotHeaderNode>)phn.getChildren();
                    if (children != null && children.size() > 0) {
                        tmp.addAll(children);
                        foundAllRows = false;
                    } else {
                        tmp.add(phn);
                    }
                } else {
                    tmp.add(phn);
                }
            }
            nodes = tmp;
            tmp = new ArrayList<PivotHeaderNode>();
        }
        return nodes;
    }
    private void displayColumns (PivotHeaderNode rRoot, PivotHeaderNode cRoot, 
            PivotHeaderNode row, List<PivotHeaderNode> columns, int dataFieldsLength, int rowCalIdx, StringBuilder sb) {
        for (PivotHeaderNode column : columns) { // for each column
            // for each data field
            for (int i = 0; i < dataFieldsLength; i++) {
                if (i > 0) {
                    sb.append("\t");
                }
                // get data value from pivot model by row node, column node and data index
                sb.append(_pivotModel.getValue(row, rowCalIdx, column, -1, i) == null?
                        "####" : _pivotModel.getValue(row, rowCalIdx, column, -1, i));

                // last data and
                // not first level node
                if (i+1 == dataFieldsLength
                    && column.getDepth() > 1) {
                    PivotHeaderNode parentColumn = column.getParent();
                    List children = parentColumn.getChildren();
                    // is last child
                    if (children.get(children.size()-1).equals(column)) {
                        // for each column calculator
                        for (int calIdx = 0; calIdx < parentColumn.getSubtotalCount(false); calIdx++) {
                            sb.append("\t|\t");
                            // again, for each data field
                            for (int j = 0; j < dataFieldsLength; j++) {
                                if (j > 0) {
                                    sb.append("\t");
                                }
                                // get data value from pivot model by row node, column node and data index
                                sb.append(_pivotModel.getValue(row, rowCalIdx, parentColumn, calIdx, j) == null?
                                        "####" : _pivotModel.getValue(row, rowCalIdx, parentColumn, calIdx, j));
                            }
                        }
                    }
                }
            }
            sb.append("\t|\t");
        }
        for (int i = 0; i < dataFieldsLength; i++) {
            if (i > 0) {
                sb.append("\t");
            }
            // grand total for columns
            sb.append(_pivotModel.getValue(row, rowCalIdx, cRoot, -1, i) == null?
                    "####" : _pivotModel.getValue(row, rowCalIdx, cRoot, -1, i));
        }
        sb.append("\n");
    }
}


The Result

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


References

Prepare data for pivottable
http://books.zkoss.org/wiki/ZK_Pivottable_Essentials/Working_With_Pivottable/Prepare_Data

Javadoc of TabularPivotModel
http://www.zkoss.org/javadoc/latest/zkpvt/org/zkoss/pivot/impl/TabularPivotModel.html

Download

Project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/GetValueFromPivotmodel

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

Tuesday, January 22, 2013

ZK Pivottable: Sort Pivottable Column Data


Introduction

We have implemented 'Display Data in ZK Pivottable' in previous post (http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html), in this post, we will try to sort the column data in pivottable.

The Composer

TestComposer.java

package test;

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

import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.Pivottable;

import org.zkoss.pivot.event.PivotUIEvent;
import org.zkoss.pivot.impl.SimplePivotHeaderTree;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;

import org.zkoss.zk.ui.event.MouseEvent;
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.1 CE 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;

    @Wire("#pivottable")
    private Pivottable pivottable;

    private int _rowLevelToSort = 1;    // the level of rows to sort
    private int _fieldIndexToSort = 0; // the index of data field under _sortColumn
    private PivotHeaderNode _columnNodeToSort; // the column node with respect to _sortColumn

    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    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("Row_Level_001", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_Level_002", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_Level_003", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_Level_004", PivotField.Type.ROW);

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

            // assign datas, the order matches to the order of data field
            _pivotModel.setFieldType("Data_Field_001", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_Field_002", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_Field_003", PivotField.Type.DATA);
        }
        return _pivotModel;
    }
    /**
     * prepare the data for pivottable's model
     * The order of object put into data list matches
     * the order of column name's order
     * @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 < 10000; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("Row_Level_001 - " + (r.nextInt(10) + 1));
            data.add("Row_Level_002 - " + (r.nextInt(10) + 1));
            data.add("Row_Level_003 - " + (r.nextInt(10) + 1));
            data.add("Row_Level_004 - " + (r.nextInt(10) + 1));
            data.add("Column_Level_001 - " + (r.nextInt(10) + 1));
            data.add("Column_Level_002 - " + (r.nextInt(10) + 1));
            data.add(r.nextInt(10000));
            data.add(r.nextDouble() * 10000.0);
            data.add(r.nextInt(100));
            result.add(data);
        }
        return result;
    }
    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "Row_Level_001", "Row_Level_002", "Row_Level_003", "Row_Level_004",
                "Column_Level_001", "Column_Level_002",
                "Data_Field_001", "Data_Field_002", "Data_Field_003"
        });
    }

    /**
     * ** Added **
     * sort column ascending
     */
    @Listen("onClick = #btnSortAsc")
    public void doSortAscending (MouseEvent event) {
        doSort(true);
    }

    /**
     * ** Added **
     * sort column descending
     */
    @Listen("onClick = #btnSortDesc")
    public void doSortDescending (MouseEvent event) {
        doSort(false);
    }
    /**
     * ** Added **
     * sort column
     * @param ascending boolean, true: ascending, false: descending
     */
    private void doSort (boolean ascending) {
        if (_columnNodeToSort != null) {
            // get the rowHeaderTree
            SimplePivotHeaderTree rowHeaderTree = (SimplePivotHeaderTree)_pivotModel.getRowHeaderTree();
            // create comparator
            ColumnHeaderComparator comparator = new ColumnHeaderComparator(_pivotModel,
                    _columnNodeToSort, _fieldIndexToSort, _rowLevelToSort, ascending);
            // do sort
            rowHeaderTree.sort(comparator);
            // reset model to trigger rerender
            pivottable.setModel(_pivotModel);
        }
    }

    /**
     * ** Added **
     * Update sort attributes from click event of pivottable
     */
    @Listen("onPivotPopup = #pivottable")
    public void updateSortAttributes (PivotUIEvent e) {
        PivotField dataField = e.getDataField();

        _rowLevelToSort = e.getRowContext() != null? e.getRowContext().getNode().getDepth() : 0;
        if (dataField != null) {
            _columnNodeToSort = e.getColumnContext().getNode();
            _fieldIndexToSort = getFieldIndexToSort(dataField.getFieldName(), _pivotModel.getDataFields());
        }
    }
    /**
     * ** Added **
     * get the index of the field to sort
     * @param fieldToSort the field to sort
     * @param fields all fields get from pivot model
     * @return int the index of the field to sort
     */
    private int getFieldIndexToSort (String fieldToSort, TabularPivotField[] fields) {
        for (int i = 0; i < fields.length; i++) {
            if (fieldToSort.equals(fields[i].getFieldName()))
                return i;
        }
        return 0;
    }
}


There are two parts regarding to the sort feature, one is 'public void updateSortAttributes (PivotUIEvent e)', it updates the sorting attributes when a data cell or a row head clicked, the other is 'private void doSort (boolean ascending)', it will sort a column's data with respect to the sorting attributes while sort ascending / descending button clicked.

ColumnHeaderComparator.java


package test;

import java.math.BigDecimal;
import java.util.Comparator;

import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.impl.TabularPivotModel;

/**
 * ** Added **
 *  the comparator for sort row header node
 *  Tested with ZK 6.0.1 CE and ZK Pivottable 2.0.0
 */
public class ColumnHeaderComparator implements Comparator<PivotHeaderNode> {
    private TabularPivotModel _pivotModel; // the data model
    private PivotHeaderNode _columnNodeToSort; // the column node to sort
    private int _fieldIndexToSort; // the field index of data fields to sort
    private int _rowLevelToSort; // the row level to sort
    private boolean _ascending; // sort direction

    /**
     * Constructor
     * @param pivotModel TabularPivotModel, The pivottable's model
     * @param columnNodeToSort PivotHeaderNode, The column to sort
     * @param fieldIndexToSort int, The index of the field under the columnNodeToSort to sort
     * @param rowLevelToSort int, The level of row header node to sort
     * @param ascending boolean, true: sort ascending, false: sort descending
     */
    public ColumnHeaderComparator (TabularPivotModel pivotModel, PivotHeaderNode columnNodeToSort,
            int fieldIndexToSort, int rowLevelToSort, boolean ascending) {
        _pivotModel = pivotModel;
        _columnNodeToSort = columnNodeToSort;
        _fieldIndexToSort = fieldIndexToSort;
        _rowLevelToSort = rowLevelToSort;
        _ascending = ascending;
    }
    /**
     * compare two node
     */
    public int compare (PivotHeaderNode n1, PivotHeaderNode n2) {
        int result = 0;
        // get the level of two node
        int l1 = n1.getDepth();
        int l2 = n2.getDepth();

        // only compare if node is at sort level
        if (l1 == _rowLevelToSort
            && l2 == _rowLevelToSort) {
            // get the values and compare
            Number v1 = _pivotModel.getValue(n1, -1, _columnNodeToSort, -1, _fieldIndexToSort);
            Number v2 = _pivotModel.getValue(n2, -1, _columnNodeToSort, -1, _fieldIndexToSort);
            result = (_ascending? 1 : -1 ) * (new BigDecimal(v1.toString()).compareTo(new BigDecimal(v2.toString())));
        }

        return result;
    }
}


This is the comparator, the sorting result will depends on how you implement it.

The ZUL Page

index.zul

<zk xmlns:w="client"><!-- ** Added ** namespace for Client Side Programming -->
    <!-- Tested with ZK 6.0.1 CE and ZK Pivottable 2.0.0 -->
    <vlayout>
        <div>
            <!-- ** Added ** the description of how to use sort feature -->
            <vbox>
                <label value="Description:" />
                <label value="Click data cell to choose the column field to sort" />
                <label value="The sort level will be the latest extended level of the clicked row" />
                <label value="Click row header node to change the sort level" />
                <label value="Click 'sort ascending' / 'sort descending' button to sort" />
            </vbox>
        </div>
        <!-- window, apply a SelectorComposer -->
        <window id="win" xmlns:w="client"
            apply="test.TestComposer">
            <!-- pivottable, get model from window's composer -->
            <pivottable id="pivottable" model="${win$composer.pivotModel}">
                <!-- ** Added ** Blank block for position the sort button -->
                <div></div>
                <!-- ** Added ** the sort button to do sort ascending / descending -->
                <div>
                    <!-- The sort ascending button -->
                    <button id="btnSortAsc" label="Sort Ascending" />
                    <!-- The sort descending button -->
                    <button id="btnSortDesc" label="Sort Descending" />
                </div>
            </pivottable>
        </window>
    </vlayout>
    <!-- ** Added ** define some style and override some function
                    for better user experience
                    actually the 'style' fragment and the 'script' fragment
                    are not related to the sort feature -->
    <style>
        <!-- style for focused data cell / row head -->
        .focused-data-cell .z-pivottable-field-wrapper {
            background-color: #CCCCCC;
        }
        .focused-row-head .z-pivottable-field-wrapper {
            background-color: #AADDDD;
        }
    </style>
    <script type="text/javascript"><![CDATA[
        var _Pwgt = {},
            _Bwgt = {};
        // override pivottable js function
        zk.afterLoad("pivot", function () {
            zk.override(pivot.Pivottable.prototype, _Pwgt, {
                // override pivottable bind_ for restore the scrollLeft
                bind_: function(desktop, skipper, after) {
                    _Pwgt.bind_.apply(this, arguments); //call the original method
                    var oldLeft;
                    if (this.id == 'pivottable') { // limit the affect target
                        if (oldLeft = pivot.Pivottable.pivottableOldLeft) {
                            var wgt = this;

                            setTimeout(function (){
                                wgt.$n('scroll').scrollLeft = oldLeft;
                                pivot.Pivottable.pivottableOldLeft = null;
                            }, 200);
                        }
                    }
                },
                // override pivottable doClick_ for style the clicked data cell or row head
                doClick_: function (evt) {
                    _Pwgt.doClick_.apply(this, arguments); //call the original method
                    // clear old scroll if any
                    pivot.Pivottable.pivottableOldLeft = null;
                    if (this.id == 'pivottable') { // limit the affect target
                        var tar = evt.domTarget,
                            jtar = jq(tar),
                            old;
                        if (!jtar.hasClass('z-pivottable-icon')) {
                            // click on data cell
                            if (jtar.hasClass('z-pivottable-cell-field')
                                || ((tar = tar.parentNode) && (jtar = jq(tar)) && jtar.hasClass('z-pivottable-cell-field'))) {
                                // remove old focus
                                if (old = this._oldFocusDataCell) {
                                    jq(old).removeClass('focused-data-cell');
                                }
                                // also remove old row head's style, denotes use latest level as sort level
                                if (old = this._oldFocusRowHead) {
                                    jq(old).removeClass('focused-row-head');
                                }
                                // add focus for denotes use clicked data field to do sort
                                jtar.addClass('focused-data-cell');
                                this._oldFocusDataCell = jtar[0];
                            } else if (jtar.hasClass('z-pivottable-row-field')
                                    || ((tar = tar.parentNode) && (jtar = jq(tar)) && jtar.hasClass('z-pivottable-row-field'))) {
                                // click on row header node
                                // remove old focus
                                if (old = this._oldFocusRowHead) {
                                    jq(old).removeClass('focused-row-head');
                                }
                                // add focus for denotes use specific sort level
                                jtar.addClass('focused-row-head');
                                this._oldFocusRowHead = jtar[0];
                            }
                        }
                    }
                }
            });
        });
        // override button js function
        zk.afterLoad("zul.wgt", function () {
            zk.override(zul.wgt.Button.prototype, _Bwgt, {
                // override doClick_ for store old pivottable scrollLeft
                doClick_: function (evt) {
                    _Bwgt.doClick_.apply(this, arguments); //call the original method
                    var id = this.id;
                    if (id == 'btnSortAsc'
                        || id == 'btnSortDesc') { // limit the affect target
                        // store the old scrollLeft of pivottable
                        pivot.Pivottable.pivottableOldLeft =
                            zk.Widget.$('$pivottable').$n('scroll').scrollLeft;
                    }
                }
            });
        });
    ]]></script>
</zk>


The majore part is the two button added, others are only make things better but not related to the sort feature.

The Result
View the demo flash on line
http://screencast.com/t/eD4z1UZjEWWG

You can find the flash file at github:
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/SortPivottableColumn.swf

Reference
http://books.zkoss.org/wiki/ZK_Pivottable_Essentials


Download
The full project is at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/SortPivottableColumn

Saturday, January 19, 2013

Extending and Customizing ZK Component as Separate Component


Introduction

This article reply to Senthil's blog post (http://emrpms.blogspot.in/2013/01/zk-datebox-customize-datebox-as.html), describe how to define a custom component that extends ZK component and do customization as needed.

Pre-request

The yearbox/monthbox customization by client side programming
http://ben-bai.blogspot.tw/2013/01/zk-datebox-customize-datebox-as.html

The basic tutorial of how to create a component
http://ben-bai.blogspot.tw/2012/08/zk-component-development-tutorial.html

Project Structure

We specify src and src/archive as two source folders separately.

src for .java files (not used in this sample)
src/archive for others (lang-addon.xml, .js, .wpd and .css.dsp)



The Program

lang-addon.xml

Define all component here, since we put it in the location that will be loaded by ZK automatically so no need to specify it in zk.xml.

<!-- Tested with ZK 6.0.2 -->

<language-addon>
    <addon-name>customDatebox</addon-name>
    <language-name>xul/html</language-name>

    <!-- It specifies what language addons this addon
        depends on. If specified, this addon will be
        parsed after all the specified addons are parsed -->
    <depends>zul</depends>

    <!-- define yearbox -->
    <component>
        <component-name>yearbox</component-name>
        <!-- extends datebox component,
            this denotes using all files (java/js/css.dsp)
            of the original datebox by default if
            no other file(s) specified -->
        <extends>datebox</extends>
        <!-- specify custom widget class file -->
        <widget-class>custom.zk.components.Yearbox</widget-class>
    </component>
    <!-- define monthbox -->
    <component>
        <component-name>monthbox</component-name>
        <!-- extends datebox component,
            this denotes using all files (java/js/css.dsp)
            of the original datebox by default if
            no other file(s) specified -->
        <extends>datebox</extends>
        <!-- specify custom widget class file -->
        <widget-class>custom.zk.components.Monthbox</widget-class>
    </component>
</language-addon>


zk.wpd

Define the dependency and loading sequence

<!-- Tested with ZK 6.0.2 -->
<!-- note the depends attribute should be zul.db
    since ZK supports load package on demand 
    will not load all recuired package if you only
    specify zul
    
    according to lang.xml in zul project, it more
    likely just load zul and zul.wgt by default -->
<package name="custom.zk.components" language="xul/html" depends="zul.db">
    <widget name="Yearbox" />
    <widget name="Monthbox" />
    <script src="override.js"/>
</package>


Yearbox.js

The customized widget class for yearbox, simply extends original datebox since what we need is only the widget class "custom.zk.components.Yearbox" so we can use the API widget.$instanceof to detect whether a calendar pop is belongs to a yearbox.

//
// Tested with ZK 6.0.2
//
(function () {
    // simply extends Datebox,
    // what we need is only the widget class
    // custom.zk.components.Yearbox
    custom.zk.components.Yearbox = zk.$extends(zul.db.Datebox, {
    });
})();


Monthbox.js

The customized widget class for monthbox, simply extends original datebox since what we need is only the widget class "custom.zk.components.Monthbox" so we can use the API widget.$instanceof to detect whether a calendar pop is belongs to a monthbox.

//
// Tested with ZK 6.0.2
//
(function () {
    // simply extends Datebox,
    // what we need is only the widget class
    // custom.zk.components.Monthbox
    custom.zk.components.Monthbox = zk.$extends(zul.db.Datebox, {
    });
})();

override.js

Override zul.db.CalendarPop here, change default action according to the parent's class of calendar pop. We use the API widget.$instanceof to detect whether a calendar pop is belongs to a yearbox or a monthbox.

zk.afterLoad("zul.db", function () {
    // Datebox Calendar Renderer
    var _Cwgt = {};
    zk.override(zul.db.CalendarPop.prototype, _Cwgt, {
        // switch the view after redraw or open as needed
        redraw: function (out) {
            _Cwgt.redraw.apply(this, arguments); //call the original method
            this._customChangeView ();
        },
        open: function (silent) {
            _Cwgt.open.apply(this, arguments); //call the original method
            this._customChangeView ();
        },
        _customChangeView: function () {
            // cannot show month/day
            if (this._isYearboxCalPop()) {
                var view = this._view;
                // switch to year view as needed
                if (view == 'month' || view == 'day')
                    this._setView("year");
            } else if (this._isMonthboxCalPop()) {
                // cannot show day view
                // switch to month view as needed
                if (this._view == 'day')
                    this._setView("month");
            }
        },
        // customize the chooseDate function
        _chooseDate: function (target, val) {
            var view = this._view;
            if (this._isYearboxCalPop()
                || this._isMonthboxCalPop()) {
                // do customize chooseDate if the parent (datebox)
                // has specific class
                var date = this.getTime(),
                    year = (view == 'decade' || view == 'year')? val : date.getFullYear(),
                    month = view == 'month'? val : 0;
                // set date value
                this._setTime(year, month, 1);
                if (view == 'decade') {
                    // switch to year view if at decade view
                    this._setView("year");
                } else if (this._isMonthboxCalPop()
                    && view == 'year') {
                    // switch to month view if at year view and the month view is allowed
                    this._setView("month");
                } else if (this._isMonthboxCalPop() && view == 'month'
                    || this._isYearboxCalPop() && view == 'year') {
                    // close calendar and update value if already at the smallest allowed view
                    this.close();
                    this.parent.updateChange_();
                }
            } else {
                _Cwgt._chooseDate.apply(this, arguments); //call the original method
            }
        },
        _isYearboxCalPop: function () {
            return this.parent.$instanceof(custom.zk.components.Yearbox);
        },
        _isMonthboxCalPop: function () {
            return this.parent.$instanceof(custom.zk.components.Monthbox);
        }
    });
});


index.zul

Test page

<zk>
    <vbox>
        <label value="yearbox" />
        <yearbox format="yyyy">
            <attribute name="onChange"><![CDATA[
                Date date = self.getValue();
                alert("Year: " + (date.getYear()+1900));
            ]]></attribute>
        </yearbox>

        <label value="monthbox" />
        <monthbox format="yyyy-MM">
            <attribute name="onChange"><![CDATA[
                Date date = self.getValue();
                alert("Year: " + (date.getYear()+1900) + "\n"
                    + "Month: " + (date.getMonth()+1));
            ]]></attribute>
        </monthbox>
    </vbox>
</zk>


The Result

The result is the same as http://ben-bai.blogspot.tw/2013/01/zk-datebox-customize-datebox-as.html

Reference

Datebox.js
https://github.com/zkoss/zk/blob/6.0/zul/src/archive/web/js/zul/db/Datebox.js

lang.xml in Zul project
https://github.com/zkoss/zk/blob/6.0/zul/src/archive/metainfo/zk/lang.xml


Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Components_Customization/DateboxCustomization