Sunday, April 21, 2013

ZK Pivottable: Filter Row or Column Header Value


Introduction

This article describe how to use the value of row or column field as filter to filter raw data.

Pre-request


Pass Event to Other Component
http://ben-bai.blogspot.tw/2012/12/pass-event-to-other-component.html


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

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

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

ZK Pivottable: Get Distinct Values of Field
http://ben-bai.blogspot.tw/2013/03/zk-pivottable-get-distinct-values-of.html

The Program

index.zul

A pivottable and a filter

<zk>
    <!-- Tested with ZK 6.0.1 CE and ZK Pivottable 2.0.0 -->
    <!-- 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}" />
        <div style="margin-top: 10px;">
            Filter info:
            <label id="lb" />
        </div>
        <!-- filter list -->
        <div id="filter" />
    </window>
</zk>


TestComposer.java

Provide model, listen to onPivotPopup to update filter list, listen to onFilterChanged to update pivottable with filtered data.

package test;

import java.util.List;
import org.zkoss.pivot.Pivottable;

import org.zkoss.pivot.event.PivotUIEvent;
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.Div;
import org.zkoss.zul.Label;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {

    private static final long serialVersionUID = -8249566421884806620L;
    @Wire
    Pivottable pivottable;
    @Wire
    Div filter; // div that will contain filter list
    @Wire
    Label lb; // filter info
    // pivot model with the 'whole' raw data
    private TabularPivotModel _pivotModel;
    // model provider, provide the columns, raw data and pivot model
    private PivotModelProvider _modelProvider = new PivotModelProvider();

    // handler to do the works of filter
    // NOTE: Renew it if changed to a complete different model
    private FilterHandler _filterHandler = new FilterHandler();

    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = _modelProvider.getPivotModel();
        }
        return PVTUtils.cloneModelWithData(_pivotModel, getFilteredData());
    }

    /**
     * update the selected field for filter
     * @param e
     */
    @Listen("onPivotPopup = #pivottable")
    public void updateFilterIndex (PivotUIEvent e) {
        if (e.getRowContext() != null
            && e.getColumnContext() == null) {
            // clicked on row field
            updateFilter(_pivotModel.getRowFields()[e.getRowContext().getNode().getDepth()-1]);
        } else if (e.getRowContext() == null
                && e.getColumnContext() != null) {
            // clicked on column field
            updateFilter(_pivotModel.getColumnFields()[e.getColumnContext().getNode().getDepth()-1]);
        }
    }
    /**
     * called while filter is changed
     * update filter value of selected field
     * @param event
     */
    @Listen("onFilterChanged = #pivottable")
    public void updateLimit (FilterChangedEvent event) {
        _filterHandler.updateLimit(event.getFieldName(), event.getValue(), event.isChecked());
        updatePivottable();
    }
    /**
     * update the filter list
     * @param field the field to update filter list
     */
    private void updateFilter (TabularPivotField field) {
        _filterHandler.updateFilter(pivottable,
                field,
                _modelProvider.getColumns(),
                getRawData(),
                filter);
        // update field info label
        lb.setValue("field type: " + field.getType() + ", field name: " + field.getFieldName());
    }
    /**
     * update pivottable with filtered pivot model
     */
    private void updatePivottable () {
        // store current structure at first
        PVTUtils.syncModelStructure((TabularPivotModel)pivottable.getModel(), _pivotModel);
        TabularPivotModel filteredModel = PVTUtils.cloneModelWithData(_pivotModel, getFilteredData());
        pivottable.setModel(filteredModel);
    }
    /**
     * get the filtered data
     * @return
     */
    private Iterable<List<Object>> getFilteredData () {
        return _filterHandler.filterData(_pivotModel, _modelProvider.getColumns(), getRawData());
    }
    /**
     * get complete raw data
     * @return
     */
    @SuppressWarnings("unchecked")
    private Iterable<List<Object>> getRawData () {
        return (Iterable<List<Object>>)_pivotModel.getRawData();
    }
}


PivotModelProvider.java

Provide base model, raw data and columns.

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.impl.TabularPivotModel;

public class PivotModelProvider {
    // pivot model with the 'whole' raw data
    private TabularPivotModel _pivotModel;
    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    public TabularPivotModel getPivotModel () {
        if (_pivotModel == null) {
            _pivotModel = new TabularPivotModel(getData(), getColumns());

            // assign rows, the order matches to the level of row node field
            _pivotModel.setFieldType("Row_01", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_02", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_03", PivotField.Type.ROW);

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

            // assign datas, the order matches to the order of data field
            _pivotModel.setFieldType("Data_01", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_02", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_03", PivotField.Type.DATA);
        }
        return _pivotModel;
    }
    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "Row_01", "Row_02", "Row_03",
                "Column_01", "Column_02",
                "Data_01", "Data_02", "Data_03"
        });
    }
    /**
     * 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
     */
    private List<List<Object>> getData() {
        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("Row_01 - " + (r.nextInt(5) + 1));
            data.add("Row_02 - " + (r.nextInt(5) + 1));
            data.add("Row_03 - " + (r.nextInt(5) + 1));
            data.add("Column_01 - " + (r.nextInt(5) + 1));
            data.add("Column_02 - " + (r.nextInt(5) + 1));
            data.add(r.nextInt(10000));
            data.add(r.nextDouble() * 10000.0);
            data.add(r.nextInt(100));
            result.add(data);
        }
        return result;
    }
}


FilterHandler.java

Handle filter task

package test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
import org.zkoss.pivot.util.PivotModels;
import org.zkoss.zk.ui.event.CheckEvent;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Checkbox;
import org.zkoss.zul.Div;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;

/**
 * Class to handle tasks with respect to filter
 *
 * NOTE: A FilterHandler is rely on a specific set of fields and
 * should be renew after the fields of pivot model are changed 
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
public class FilterHandler {
    /**
     * map that contains all Limit object
     * use field name as the key
     * 
     */
    private Map<String, Limit> _fieldsLimitsMap = new HashMap<String, Limit>();
    /**
     * map that contains all field index
     * use field name as the key
     */
    private Map<String, Integer> _indexMap = new HashMap<String, Integer>();;

    @SuppressWarnings("rawtypes")
    public void updateFilter (Pivottable pivottable, TabularPivotField field, List<String> columns, Iterable<List<Object>> rawData, Div filter) {
        List distinctValues = PVTUtils.getDistinctValues(rawData, columns, getFieldIndex(columns, field.getFieldName()));
        updateFilterList(pivottable, distinctValues, field, filter);
    }
    /**
     * update the limited values of a field
     * @param fieldName the name of the field to update
     * @param value the value to update
     * @param accept whether accept the value above
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void updateLimit (String fieldName, Object value, boolean accept) {
        // try find Limit object
        Limit limit = _fieldsLimitsMap.get(fieldName);
        // create a new one if not found
        if (limit == null) {
            limit = new Limit(fieldName, new HashSet());
            _fieldsLimitsMap.put(fieldName, limit);
        }
        // remove value from limited values if the value is accepted
        // add value to limited values if the value is not accepted
        if (accept) {
            limit.getLimitedValues().remove(value);
        } else {
            limit.getLimitedValues().add(value);
        }
    }
    /**
     * filter data by limits
     * @param model pivot model, get all row/column fields from it
     * @param rawData raw data, to filter it
     * @return the filtered raw data
     */
    public Iterable<List<Object>> filterData (TabularPivotModel model, final List<String> columns, Iterable<List<Object>> rawData) {
        // field name of row/column fields
        final List<String> rcColumns = new ArrayList<String>();
        // keep a final object of limit map so can be used in inner class
        final Map<String, Limit> limits = _fieldsLimitsMap;
        // add all row/column field names
        for (TabularPivotField tpf : model.getRowFields()) {
            rcColumns.add(tpf.getFieldName());
        }
        for (TabularPivotField tpf : model.getColumnFields()) {
            rcColumns.add(tpf.getFieldName());
        }
        return PivotModels.filter(rawData, new PivotModels.Filter<List<Object>>() {
            public boolean keep(List<Object> row) {
                // for each row/column (field names)
                for (String s : rcColumns) {
                    // find Limit object
                    Limit l = limits.get(s);
                    if (l != null) {
                        // find value index
                        int index = getFieldIndex(columns, s);
                        // get value
                        Object value = row.get(index);
                        // do not keep the value if the value is in limit
                        if (l.getLimitedValues().contains(value)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        });
    }
    /**
     * create the filter list
     * @param pivottable pivottable, to fire onFilterChanged event as needed
     * @param distinctValues all different values, used to construct the filter list
     * @param field pivot field, used to get fieldName
     * @param filter the div component specified in index.zul
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void updateFilterList (final Pivottable pivottable, List distinctValues, final TabularPivotField field, Div filter) {
        // clear old children
        filter.getChildren().clear();

        Listbox lb = new Listbox();
        lb.setWidth("200px");
        final String fieldName = field.getFieldName();
        Limit limit = _fieldsLimitsMap.get(fieldName);

        // for each value of this field
        for (final Object value : distinctValues) {
            Listitem li = new Listitem();
            Listcell lc = new Listcell();
            Checkbox cb = new Checkbox(value.toString());

            // update checked status of checkbox according to
            // whether this value is a limited value
            if (limit != null && limit.getLimitedValues().contains(value)) {
                cb.setChecked(false);
            } else {
                cb.setChecked(true);
            }
            cb.setParent(lc);
            lc.setParent(li);
            li.setParent(lb);

            // add onCheck event listener to checkbox
            cb.addEventListener("onCheck", new EventListener() {
                public void onEvent (Event event) {
                    // fire event to pivottable with
                    // the information of changed filter attributes
                    Events.postEvent(new FilterChangedEvent(pivottable, fieldName, value, ((CheckEvent)event).isChecked() ));
                }
            });
        }
        // add listbox to div
        lb.setParent(filter);
    }
    /**
     * get the index in raw data of a field
     * @param columns columns used in pivot model
     * @param fieldName name of the field to search index
     * @return int the found index
     */
    private int getFieldIndex (List<String> columns, String fieldName) {
        // search it from index map at first
        if (!_indexMap.containsKey(fieldName)) {
            int index = PVTUtils.getFieldIndex(columns, fieldName);
            // store it to index map
            _indexMap.put(fieldName, index);
        }
        return _indexMap.get(fieldName);
    }
}


PVTUtils.java

Utils of pivottable

package test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
/**
 * utilities for pivottable
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
public class PVTUtils {
    /**
     * Create a new pivot model based on
     * current pivot model and new data 
     * @param model
     * @param newData
     * @return
     */
    public static TabularPivotModel cloneModelWithData (TabularPivotModel model, Iterable<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;
    }
    /**
     * get the index in raw data of a field
     * @param columns columns used in pivot model
     * @param fieldName name of the field to search index
     * @return int the found index
     */
    public static int getFieldIndex (List<String> columns, String fieldName) {
        int index = -1;
        // search field name in columns
        for (int i = 0; i < columns.size(); i++) {
            if (columns.get(i).equals(fieldName)) {
                index = i;
                break;
            }
        }
        return index;
    }
    /**
     * get all different values of a field
     * @param rawData the raw data to get different values from
     * @param columns all columns in pivot model
     * @param index the index to get value from list
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static List getDistinctValues (Iterable<List<Object>> rawData, List<String> columns, int index) {
        // set used to hold distinct values
        Set s = new HashSet();
        // result list
        List result = new ArrayList();

        if (index == -1) return result;
        // add all value to set directly
        for (List<Object> data : rawData) {
            s.add(data.get(index));
        }
        // copy to list then sort the list
        for (Object o : s) {
            result.add(o);
        }
        Collections.sort(result);
        return result;
    }
    /**
     * sync the structure of pivot model
     * 
     * @param model the base pivot model
     * @param modelTwo the pivot model to adjust its structure
     */
    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);
    }
    /**
     * sync pivot fields of pivot model
     * @param fields the base fields
     * @param model the pivot model to adjust its fields
     */
    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());
            model.setFieldKeyComparator(field, f.getComparator());
        }
    }
    /**
     * 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);
            }
        }
    }
}


Limit.java

Contains the restricted values of a pivot field

package test;

import java.util.HashSet;
import java.util.Set;

/**
 * Class for hold limited values of a field
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 *
 */
public class Limit {
    // the name to represent a specific field
    private String _fieldName;
    // the limited values
    private Set<Object> _limitedValues;
    public Limit (String fieldName, Set<Object> limitedValues) {
        _fieldName = fieldName;
        _limitedValues = limitedValues;
        if (_limitedValues == null) {
            _limitedValues = new HashSet<Object>();
        }
    }
    public String getFieldName () {
        return _fieldName;
    }
    public Set<Object> getLimitedValues () {
        return _limitedValues;
    }
}


FilterChangedEvent.java

Event that will be fired when the filter is changed.

package test;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

/**
 * Event used to pass the information of updated filter
 * 
 * Event name is "onFilterChanged"
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 *
 */
public class FilterChangedEvent extends Event {

    private static final long serialVersionUID = 5055917746546499563L;
    /**
     * whether a value is checked in filter list,
     * true: checked (denotes this value is allowed)
     * false: not checked (denotes this value is not allowed)
     */
    private boolean _checked = true;
    /**
     * the updated filter value
     */
    private Object _value;
    /**
     * the field related to the updated filter
     */
    private String _fieldName;
    // constructor, 
    public FilterChangedEvent (Component target, String fieldName, Object value, boolean checked) {
        super("onFilterChanged", target);
        _checked = checked;
        _fieldName = fieldName;
        _value = value;
    }
    public String getFieldName () {
        return _fieldName;
    }
    public Object getValue () {
        return _value;
    }
    public boolean isChecked () {
        return _checked;
    }
}


Reference

Filtering input data
http://books.zkoss.org/wiki/ZK_Pivottable_Essentials/Working_With_Pivottable/Prepare_Data#Filtering_input_data

The Result

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

Download

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

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

No comments:

Post a Comment