Monday, February 25, 2013

ZK Drag and Drop: Make Everything Resizable


Introduction

In ZK, some comopnents like textbox, button, link are not resizable, this article describe how to use a draggable div to make those non resizable components resizable.

The Program

make_components_resizable.zul

A resizer div and several custom resizable components in this page, will display the resizer div while mouseover the right-side or the bottom of custom resizable component, fire event while resizer div is dragged to resize component.

<!-- 
    Tested with ZK 6.0.2 (Chrome, IE9)
 -->
<zk xmlns:w="client">
    <style>
        .custom-sizer {
            position: absolute;
            display: none;
            border: 4px solid #aaa;
            z-index: 999999;
        }
        .custom-resizable-comp {
            display: block;
        }
    </style>
    <script><![CDATA[
        // get the dom element of custom resizer div
        function _getCustomResizer () {
            return jq('.custom-sizer')[0];
        }
        // hide the custom resizer div
        function _hideCustomResizer () {
            if (!zk.Widget.customResizing)
                _getCustomResizer().style.display = 'none';
        }
        // override zk.Widget to make components resizable
        zk.afterLoad("zul", function () {
            var _wgt = {};
            zk.override(zk.Widget.prototype, _wgt, {
                doMouseMove_: function (evt) {
                    _wgt.doMouseMove_.apply(this, arguments);
                    // do nothing if resizing
                    if (zk.Widget.customResizing) return;
                    // is custom resizable component
                    if (this._isResizable()) {
                        // keep widget instance
                        zk.Widget.customResizableWidget = this;
                        // at the right side of the component
                        if (this._atRightSide(evt)) {
                            // keep 'resizing width'
                            zk.Widget.customResizableWidgetDir = 'w';
                            // show custom resizer as vertical bar
                            // at the right side of this component
                            this._showRightResizer();
                        } else if (this._atBottom(evt)) { // at bottom of the component
                            // keep 'resizing height'
                            zk.Widget.customResizableWidgetDir = 'h';
                            // show custom resizer as horizontal bar
                            // at the bottom of this component
                            this._showBottomResizer();
                        } else { // not close to right side/bottom
                            // hide custom resizer
                            _hideCustomResizer();
                        }
                    }
                },
                // is resizable if has the specific class
                // 'custom-resizable-comp'
                _isResizable: function () {
                    return jq(this.$n()).hasClass('custom-resizable-comp');
                },
                // the mouse cursor is near the right side of component
                _atRightSide: function (evt) {
                    var left = evt.pageX, // cursor x position
                        $n = jq(this.$n()),
                        wRight = $n.offset().left + $n.width(); // the right side of component
                    // their distance is smaller than 3px
                    return (Math.abs(left - wRight) < 3);
                },
                // the mouse cursor is near the bottom of component
                _atBottom: function (evt) {
                    var top = evt.pageY, // cursor y position
                        $n = jq(this.$n()),
                        wBottom = $n.offset().top + $n.height(); // the bottom of component
                    // their distance is smaller than 3px
                    return (Math.abs(top - wBottom) < 3);
                },
                // display custom resizer at the right side of component
                _showRightResizer: function () {
                    // do nothing if resizing
                    if (zk.Widget.customResizing) return;
                    var resizer = _getCustomResizer(),
                        rstyle = resizer.style,
                        $n = jq(this.$n()),
                        noffset = $n.offset();

                    // adjust the position of custom resizer
                    rstyle.left = noffset.left + $n.width() + 'px';
                    rstyle.top = noffset.top + 'px';
                    // adjust the size of custom resizer
                    rstyle.width = '0px';
                    rstyle.height = $n.height() + 'px';
                    // adjust cursor and show custom resizer
                    rstyle.cursor = 'e-resize';
                    rstyle.display = 'block';
                },
                _showBottomResizer: function () {
                    if (zk.Widget.customResizing) return;
                    var resizer = _getCustomResizer(),
                        rstyle = resizer.style,
                        $n = jq(this.$n()),
                        noffset = $n.offset();
    
                    // adjust the position of custom resizer
                    rstyle.left = noffset.left + 'px';
                    rstyle.top = noffset.top + $n.height() + 'px';
                    // adjust the size of custom resizer
                    rstyle.width = $n.width() + 'px';
                    rstyle.height = '0px';
                    // adjust cursor and show custom resizer
                    rstyle.cursor = 's-resize';
                    rstyle.display = 'block';
                }
            });
        });
    ]]></script>
    <div id="customSizer" sclass="custom-sizer" draggable="true" use="test.custom.component.div.CustomResizer">
        <attribute w:name="doMouseOut_"><![CDATA[
            function (evt) {
                // hide custom resizer if mouseout it
                this.$doMouseOut_(evt);
                _hideCustomResizer();
            }
        ]]></attribute>
        <attribute w:name="getDragOptions_"><![CDATA[
            function (map) {
                var dragOptions = {};
                for (var key in map) {
                    dragOptions[key] = map[key];
                }
                var oldBeforeSizing = dragOptions.starteffect,
                    oldAfterSizing = dragOptions.endeffect,
                    oldDragging = dragOptions.change;
                // at the beginning of dragging
                dragOptions.starteffect = function (dg) {
                    // mark resizing
                    zk.Widget.customResizing = true;
                    // save the starting x position if resizing width
                    // or save the starting y position if resizing height
                    dg._delta = zk.Widget.customResizableWidgetDir == 'w'?
                            dg._currentDelta()[0] : dg._currentDelta()[1];
                    // do original function
                    oldBeforeSizing(dg);
                };
                // at the end of dragging
                dragOptions.endeffect = function (dg, evt) {
                    // remove resizing mark
                    zk.Widget.customResizing = false;
                    // get resizing direction (w: width, h: height)
                    // evaluate new size
                    // get the uuid of the component to resize
                    var resizeWidth = zk.Widget.customResizableWidgetDir == 'w',
                        $szwgt = jq(zk.Widget.customResizableWidget),
                        delta = resizeWidth?
                            (dg._currentDelta()[0] - dg._delta) : (dg._currentDelta()[1] - dg._delta),
                        newValue = resizeWidth?
                            ($szwgt.width() + delta) : ($szwgt.height() + delta),
                        uuid = zk.Widget.customResizableWidget.uuid,
                        data;
                    if (newValue <= 0) newValue = 1;
                    // fire onCustomResize event to custom resizer
                    // 'value', 'reference' and 'resizeAttribute' are
                    // the keys of data map
                    data = {value: newValue,
                            reference: uuid,
                            resizeAttribute: (resizeWidth? 'width' : 'height')};
                    zk.Widget.$('$customSizer').fire('onCustomResize', data);
                    // call original function
                    oldAfterSizing(dg, evt);
                    // hide custom resizer
                    _getCustomResizer().style.display = 'none';
                };
                // while dragging
                dragOptions.change = function (drag, pt, evt) {
                    // call original function
                    oldDragging(drag, pt, evt);
                    var wgt = drag.control,
                        n = wgt.$n(),
                        $szwgt = jq(zk.Widget.customResizableWidget);
                    if (n.style.display != 'block') n.style.display = 'block';
                    // keep 'top' unchanged and make 'left' follow mouse position
                    // if resizing width,
                    // keep 'left' unchanged and make 'top' follow mouse position
                    // if resizing height,
                    n.style.left = (zk.Widget.customResizableWidgetDir == 'h'?
                            $szwgt.offset().left+'px' : evt.pageX+'px');
                    n.style.top = (zk.Widget.customResizableWidgetDir == 'w'?
                            $szwgt.offset().top+'px' : evt.pageY+'px');
                }
                // clear ghosting function to
                // avoid the faker dom element while dragging
                dragOptions.ghosting = null;
                return dragOptions;
            }
        ]]></attribute>
    </div>
    <vlayout>
        <hlayout>
            <textbox sclass="custom-resizable-comp"
                value="resizable textbox" />
            <checkbox sclass="custom-resizable-comp"
                label="resizable checkbox" />
            <vlayout>
                <a sclass="custom-resizable-comp"
                    href="http://www.zkoss.org">
                    resizable link
                </a>
                resizable menubar
                <menubar sclass="custom-resizable-comp">
                    <menu sclass="custom-resizable-comp" label="File (resizable)">
                        <menupopup sclass="custom-resizable-comp">
                            <menuitem sclass="custom-resizable-comp" label="New (resizable)" onClick="alert(self.label)"/>
                            <menuitem sclass="custom-resizable-comp" label="Exit (resizable)" onClick="alert(self.label)"/>
                        </menupopup>
                    </menu>
                </menubar>
            </vlayout>
        </hlayout>
        <button sclass="custom-resizable-comp"
            mold="trendy"
            label="resizable button" />
    </vlayout>
</zk>


CustomResizer.java

The class extends div, listening to custom resize event and resizing component as needed.

package test.custom.component.div;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;
import org.zkoss.zul.impl.XulElement;

import test.custom.component.event.CustomResizeEvent;

/**
 * Tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class CustomResizer extends Div {
    private static final long serialVersionUID = 5597493971151879186L;

    static {
        // listen to custom resize event
        addClientEvent(CustomResizer.class, CustomResizeEvent.ON_CUSTOM_RESIZE, CE_DUPLICATE_IGNORE
                | CE_IMPORTANT | CE_NON_DEFERRABLE);
    }
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();

        // custom resize event
        if (CustomResizeEvent.ON_CUSTOM_RESIZE.equals(cmd)) {
            CustomResizeEvent evt = CustomResizeEvent.getCustomResizeEvent(request);
            // get the component to resize
            Component ref = evt.getReference();
            if (ref != null) {
                // get new size
                int value = evt.getValue();
                // get direction (width/height)
                String dir = evt.getResizeAttribute();
                // do resize
                if ("width".equals(dir)) {
                    if (ref instanceof XulElement) {
                        ((XulElement)ref).setWidth(value+"px");
                    } else if (ref instanceof HtmlBasedComponent) {
                        ((HtmlBasedComponent)ref).setWidth(value+"px");
                    } 
                } else if ("height".equals(dir)) {
                    if (ref instanceof XulElement) {
                        ((XulElement)ref).setHeight(value+"px");
                    } else if (ref instanceof HtmlBasedComponent) {
                        ((HtmlBasedComponent)ref).setHeight(value+"px");
                    } 
                }
            }
            // post event
            Events.postEvent(evt);
        } else {
            super.service(request, everError);
        }
    }
}


CustomResizeEvent.java

Used to keep the data of custom resize event.

package test.custom.component.event;

import java.util.Map;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

/**
 * Tested with ZK 6.0.2
 * Basically you can think an Event object
 * is just a POJO to keep the data within an event
 * @author benbai123
 *
 */
public class CustomResizeEvent extends Event {
    private static final long serialVersionUID = -8839289786864776054L;

    public static final String ON_CUSTOM_RESIZE = "onCustomResize";
    private int _value;
    private final Component _reference;
    private String _resizeAttribute;
    @SuppressWarnings("rawtypes")
    public static CustomResizeEvent getCustomResizeEvent (org.zkoss.zk.au.AuRequest request) {
        // get data map
        Map data = request.getData();
        // get values by keys
        int value = (Integer)data.get("value");
        final Component reference = request.getDesktop().getComponentByUuidIfAny((String)data.get("reference"));
        String resizeAttribute = (String)data.get("resizeAttribute");
        // create event instance, return it
        return new CustomResizeEvent(ON_CUSTOM_RESIZE, request.getComponent(), value, reference, resizeAttribute);
    }
    // Constructor
    public CustomResizeEvent (String name, Component target,
            int value, Component reference, String resizeAttribute) {
        super(name, target);
        _value = value;
        _reference = reference;
        _resizeAttribute = resizeAttribute;
    }
    // getters
    public int getValue () {
        return _value;
    }
    public Component getReference () {
        return _reference;
    }
    public String getResizeAttribute () {
        return _resizeAttribute;
    }
}


The Result

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

Reference

Source code of widget.js
https://github.com/zkoss/zk/blob/6.0/zk/src/archive/web/js/zk/widget.js

Source code of drag.js
https://github.com/zkoss/zk/blob/6.0/zk/src/archive/web/js/zk/drag.js

ZK Client Side Programming
http://books.zkoss.org/wiki/Small_Talks/2010/April/Client_Side_Programming

Download

make_components_resizable.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/make_components_resizable.zul

CustomResizer.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/custom/component/div/CustomResizer.java

CustomResizeEvent.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/custom/component/event/CustomResizeEvent.java

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

Friday, February 22, 2013

C String: String Concat (Simple Note)


Simple Note

Code

#include <stdio.h>
#include <string.h>
/**
 * 
 * Tested with Dev-C++ 4.9.9.2
 *  
 * Practice of string copy and concat.
 * char * strcat ( char * destination, const char * source );
 *         Appends a copy of the source string to the destination string
 *         plus a terminating null-character if source contains null-character.
 * char * strncat ( char * destination, char * source, size_t num );
 *         Appends the first num characters of source to destination,
 *         plus a terminating null-character.
 *
 * Note: 
 * 
 */
int main () {
    // a char array without '\0'
    char chArr[4] = {'a', 'b', 'c', 'd'};
    char chArrTwo[20] = "mnopq";
    char chArrThree[] = "abcd";
    char chArrFour[20] = "wxyz";

    // the source (chArr) doesn't have null-char ('\0')
    // will become wrong value after first concat
    printf("original: %s\n", chArrTwo);
    strcat(chArrTwo, chArr); // <-- the line above
    // change the line above to the line below then everything should be okay.
    // strncat(chArrTwo, chArr, 4); // <-- the line below
    printf("append abcd: %s\n", chArrTwo);
    strncat(chArrTwo, chArr, 2);
    printf("append ab: %s\n\n", chArrTwo);

    // the source (chArrThree) contains null-char ('\0')
    // everything should be okay
    printf("original: %s\n", chArrFour);
    strcat(chArrFour, chArrThree);
    printf("append abcd: %s\n", chArrFour);
    strncat(chArrFour, chArrThree, 2);
    printf("append ab: %s\n\n", chArrFour);

    system("PAUSE");
}

Result



References

strcat
http://www.cplusplus.com/reference/cstring/strcat/

strncat
http://www.cplusplus.com/reference/cstring/strncat/

Download

Source code at github
https://github.com/benbai123/C_Cplusplus_Practice/blob/master/C_StringProcessing/string_concat.c

C String: Copy String (Simple Note)


Simple Note

Code

#include <stdio.h>
#include <string.h>
/**
 * 
 * Tested with Dev-C++ 4.9.9.2
 *  
 * Practice of string copy and concat.
 * char * strcpy ( char * destination, const char * source );
 *         copy source to destination
 * char * strncpy ( char * destination, const char * source, size_t num );
 *         copy first n (the specified num) char
 *         from source to destination
 * memset ( void * ptr, int value, size_t num );
 *        Sets the first n (the specified num) bytes of the
 *          block of memory pointed by ptr to the specified value
 *        (interpreted as an unsigned char).
 * 
 */
int main () {
    // a char array without '\0'
    char chArr[4] = {'a', 'b', 'c', 'd'};
    char chArrTwo[6] = "mnopq";
    char chArrThree[6];
    char chArrFour[6];

    // copy chArr to chArrThree
    // will not add '\0' automatically
    strcpy(chArrThree, chArr);
    printf("chArrThree terminate with \\0? %s \n", (chArrThree[4] == '\0'? "true" : "false"));
    // output chArrThree, may contains wrong value since no \0
    printf("%s\n", chArrThree);


    // add \0 manually
    chArrThree[4] = '\0';
    // copy (replace) first Three char of chArrTwo to chArrThree
    strncpy(chArrThree, chArrTwo, 2);
    printf("chArrThree terminate with \\0? %s \n", (chArrThree[4] == '\0'? "true" : "false"));
    // output chArrThree correctly
    printf("%s\n", chArrThree);

    // use memset to preset all chars in chArrFour to \0
    memset(chArrFour, '\0', sizeof(chArrFour));
    // copy chArr to chArrFour
    strncpy(chArrFour, chArr, sizeof(chArr));
    // NOTE:
    // use strncpy will not clear all specified '\0' --> correct
    // use strcpy as below will clear all specified '\0' --> cause wron string
    // strcpy(chArrFour, chArr);
    printf("chArrFour terminate with \\0? %s \n", (chArrFour[4] == '\0'? "true" : "false"));
    // output chArrFour correctly
    printf("%s\n\n", chArrFour);

    system("PAUSE");
}


Result


References

strcpy
http://www.cplusplus.com/reference/cstring/strcpy/

strncpy
http://www.cplusplus.com/reference/cstring/strncpy/

memset
http://www.cplusplus.com/reference/cstring/memset/

Download

Source code at github
https://github.com/benbai123/C_Cplusplus_Practice/blob/master/C_StringProcessing/string_copy.c

Sunday, February 17, 2013

C String: Declare, Modify and Length (Simple Note)


Simple Note

Code

#include <stdio.h>
#include <string.h>

int main () {
    /**
     * declare, modify and length
     *
     */
    // declare string
    char* chPtr = "abcdefg";// will add '\0' at the tail automatically
    char chArr[] = "bbcdefg";
    char chArrTwo[] = {'c', 'b', 'c', 'd', 'e', 'f', 'g'}; // will not add '\0' automatically
    char chArrThree[] = {'d', 'b', 'c', 'd', 'e', 'f', 'g', '\0', '\0', '\0'};

    // output string
    printf("chPtr = %s\n", chPtr);
    printf("chArr = %s\n", chArr);
    printf("chArrTwo = %s\n", chArrTwo);
    printf("chArrThree = %s\n", chArrThree);

    // output length by sizeof
    // 4, the size of pointer
    printf ("\nLenbgh of char pointer = %d\n", sizeof(chPtr));
    // 8, the size of whole array
    // including a, b, c, d, e, f, g and '\0' (the terminate char added automatically)
    printf ("Length of char array = %d\n", sizeof(chArr));
    // 7, no terminate char
    printf ("Length of char array two = %d\n", sizeof(chArrTwo));
    // 10, including 3 terminate char
    printf ("Length of char array three = %d\n", sizeof(chArrThree));

    // output length by strlen
    // 7, only count the length before first '\0' or array length
    printf ("\nLenbgh of char pointer = %d\n", strlen(chPtr));
    printf ("Length of char array = %d\n", strlen(chArr));
    printf ("Length of char array two = %d\n", strlen(chArrTwo));
    printf ("Length of char array three = %d\n", strlen(chArrThree));

    // replace first char
    // cannot modify char points by a char pointer
    // this line below cause the runtime error
    // chPtr[0] = 'w';
    chArr[0] = 'x';
    chArrTwo[0] = 'y';
    chArrThree[0] = 'z';

    // output string again
    printf("\nchPtr = %s\n", chPtr);
    printf("chArr = %s\n", chArr);
    printf("chArrTwo = %s\n", chArrTwo);
    printf("chArrThree = %s\n\n", chArrThree);
    
    system("PAUSE");
}


Result



Reference

cplusplus
http://www.cplusplus.com/reference/cstring/strlen/

Thread at SF: Is it possible to modify a string of char in C?
http://stackoverflow.com/questions/1011455/is-it-possible-to-modify-a-string-of-char-in-c

Download

Code at github
https://github.com/benbai123/C_Cplusplus_Practice/blob/master/C_StringProcessing/string_declare_modify_and_length.c

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