diff --git a/org.adempiere.server-feature/server.product.functionaltest.launch b/org.adempiere.server-feature/server.product.functionaltest.launch
index 7a8babe344..a37df902c7 100644
--- a/org.adempiere.server-feature/server.product.functionaltest.launch
+++ b/org.adempiere.server-feature/server.product.functionaltest.launch
@@ -183,11 +183,11 @@
+
+
+
-
-
-
@@ -393,6 +393,8 @@
+
+
diff --git a/org.adempiere.server-feature/server.product.launch b/org.adempiere.server-feature/server.product.launch
index 5e1cf188eb..0abcba73f7 100644
--- a/org.adempiere.server-feature/server.product.launch
+++ b/org.adempiere.server-feature/server.product.launch
@@ -187,11 +187,11 @@
+
+
+
-
-
-
@@ -415,6 +415,8 @@
+
+
diff --git a/org.adempiere.ui.zk-feature/feature.xml b/org.adempiere.ui.zk-feature/feature.xml
index 7955aa5e01..e326803159 100644
--- a/org.adempiere.ui.zk-feature/feature.xml
+++ b/org.adempiere.ui.zk-feature/feature.xml
@@ -57,4 +57,18 @@
version="0.0.0"
unpack="false"/>
+
+
+
+
diff --git a/org.adempiere.ui.zk/META-INF/MANIFEST.MF b/org.adempiere.ui.zk/META-INF/MANIFEST.MF
index c39679ed5a..0ff3a10623 100644
--- a/org.adempiere.ui.zk/META-INF/MANIFEST.MF
+++ b/org.adempiere.ui.zk/META-INF/MANIFEST.MF
@@ -213,7 +213,8 @@ Require-Bundle: org.adempiere.base;bundle-version="0.0.0",
com.sun.activation.jakarta.activation;bundle-version="1.2.1",
org.adempiere.base.process,
com.github.librepdf.openpdf;bundle-version="1.3.26",
- com.github.librepdf.openpdf-fonts-extra;bundle-version="1.3.26"
+ com.github.librepdf.openpdf-fonts-extra;bundle-version="1.3.26",
+ org.idempiere.zk.billboard
Bundle-Activator: org.adempiere.webui.WebUIActivator
Eclipse-ExtensibleAPI: true
Web-ContextPath: webui
diff --git a/org.adempiere.ui.zk/OSGI-INF/jfgchartrenderer.xml b/org.adempiere.ui.zk/OSGI-INF/jfgchartrenderer.xml
index 235c359f1e..fa2495214c 100644
--- a/org.adempiere.ui.zk/OSGI-INF/jfgchartrenderer.xml
+++ b/org.adempiere.ui.zk/OSGI-INF/jfgchartrenderer.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/org.idempiere.test/idempiere.unit.test.launch b/org.idempiere.test/idempiere.unit.test.launch
index 256b8489c7..2a0a9ef5fa 100644
--- a/org.idempiere.test/idempiere.unit.test.launch
+++ b/org.idempiere.test/idempiere.unit.test.launch
@@ -172,11 +172,11 @@
+
+
+
-
-
-
@@ -393,6 +393,8 @@
+
+
diff --git a/org.idempiere.zk.billboard.chart/.project b/org.idempiere.zk.billboard.chart/.project
new file mode 100644
index 0000000000..4a2ed66b71
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/.project
@@ -0,0 +1,39 @@
+
+
+ org.idempiere.zk.billboard.chart
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.pde.ds.core.builder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/org.idempiere.zk.billboard.chart/.settings/org.eclipse.core.resources.prefs b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/org.idempiere.zk.billboard.chart/.settings/org.eclipse.m2e.core.prefs b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000000..f897a7f1cb
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/org.idempiere.zk.billboard.chart/.settings/org.eclipse.pde.core.prefs b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.pde.core.prefs
new file mode 100755
index 0000000000..f29e940a00
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/.settings/org.eclipse.pde.core.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/org.idempiere.zk.billboard.chart/META-INF/MANIFEST.MF b/org.idempiere.zk.billboard.chart/META-INF/MANIFEST.MF
new file mode 100644
index 0000000000..c981d2fc91
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/META-INF/MANIFEST.MF
@@ -0,0 +1,27 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Billboard.js Chart
+Bundle-SymbolicName: org.idempiere.zk.billboard.chart
+Bundle-Version: 10.0.0.qualifier
+Bundle-Activator: org.idempiere.zk.billboard.chart.Activator
+Bundle-Vendor: iDempiere
+Bundle-RequiredExecutionEnvironment: JavaSE-11
+Automatic-Module-Name: org.idempiere.zk.billboard.chart
+Import-Package: org.osgi.framework,
+ org.osgi.service.component,
+ org.osgi.service.component.annotations
+Bundle-ActivationPolicy: lazy
+Require-Bundle: org.adempiere.ui.zk,
+ org.adempiere.base,
+ zul,
+ zcommon,
+ zel,
+ zhtml,
+ zjavassist,
+ zk,
+ zkbind,
+ zkplus,
+ zkwebfragment,
+ zweb,
+ org.idempiere.zk.billboard
+Service-Component: OSGI-INF/org.idempiere.zk.billboard.chart.ChartRendererServiceImpl.xml
diff --git a/org.idempiere.zk.billboard.chart/OSGI-INF/org.idempiere.zk.billboard.chart.ChartRendererServiceImpl.xml b/org.idempiere.zk.billboard.chart/OSGI-INF/org.idempiere.zk.billboard.chart.ChartRendererServiceImpl.xml
new file mode 100644
index 0000000000..16a3686729
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/OSGI-INF/org.idempiere.zk.billboard.chart.ChartRendererServiceImpl.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/org.idempiere.zk.billboard.chart/build.properties b/org.idempiere.zk.billboard.chart/build.properties
new file mode 100644
index 0000000000..67e6565494
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/build.properties
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = target/classes/
+bin.includes = META-INF/,\
+ .,\
+ OSGI-INF/
diff --git a/org.idempiere.zk.billboard.chart/pom.xml b/org.idempiere.zk.billboard.chart/pom.xml
new file mode 100644
index 0000000000..5894c539bd
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/pom.xml
@@ -0,0 +1,12 @@
+
+ 4.0.0
+
+ org.idempiere
+ org.idempiere.parent
+ ${revision}
+ ../org.idempiere.parent/pom.xml
+
+ org.idempiere.zk.billboard.chart
+ eclipse-plugin
+
diff --git a/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/Activator.java b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/Activator.java
new file mode 100644
index 0000000000..4410098983
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/Activator.java
@@ -0,0 +1,51 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.zk.billboard.chart;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ *
+ * @author hengsin
+ *
+ */
+public class Activator implements BundleActivator {
+
+ private static BundleContext context;
+
+ static BundleContext getContext() {
+ return context;
+ }
+
+ public void start(BundleContext bundleContext) throws Exception {
+ Activator.context = bundleContext;
+ }
+
+ public void stop(BundleContext bundleContext) throws Exception {
+ Activator.context = null;
+ }
+
+}
diff --git a/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartBuilder.java b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartBuilder.java
new file mode 100644
index 0000000000..b79eda8d85
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartBuilder.java
@@ -0,0 +1,545 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.zk.billboard.chart;
+
+import java.math.BigDecimal;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+
+import org.adempiere.exceptions.DBException;
+import org.compiere.model.MChart;
+import org.compiere.model.MChartDatasource;
+import org.compiere.model.MQuery;
+import org.compiere.model.MRole;
+import org.compiere.model.MTable;
+import org.compiere.util.CLogger;
+import org.compiere.util.DB;
+import org.compiere.util.Env;
+import org.compiere.util.Util;
+import org.idempiere.zk.billboard.Billboard;
+import org.zkoss.zul.CategoryModel;
+import org.zkoss.zul.ChartModel;
+import org.zkoss.zul.PieModel;
+import org.zkoss.zul.SimpleCategoryModel;
+import org.zkoss.zul.SimplePieModel;
+import org.zkoss.zul.SimpleXYModel;
+import org.zkoss.zul.XYModel;
+
+/**
+ * Render AD_Chart using zk-billboard.
+ * Note: 3d chart not supported by zk-billboard
+ * @author hengsin
+ *
+ */
+public class ChartBuilder {
+
+ private final static CLogger log = CLogger.getCLogger(ChartBuilder.class);
+ protected final SimpleDateFormat tsDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+
+ private MChart mChart;
+ private HashMap queries;
+ private ChartModel chartModel;
+ private Date minDate;
+ private Date maxDate;
+
+ public ChartBuilder(MChart chart) {
+ this.mChart = chart;
+ }
+
+ /**
+ *
+ * @param type
+ * @return Billboard
+ */
+ public Billboard createChart() {
+ String type = mChart.getChartType();
+
+ if (MChart.CHARTTYPE_BarChart.equals(type))
+ {
+ if (mChart.isTimeSeries())
+ return createXYBarChart();
+ return createBarChart();
+ }
+ else if (MChart.CHARTTYPE_3DBarChart.equals(type))
+ {
+ return create3DBarChart();
+ }
+ else if (MChart.CHARTTYPE_StackedBarChart.equals(type))
+ {
+ if (mChart.isTimeSeries())
+ return createXYBarChart();
+ return createStackedBarChart();
+ }
+ else if (MChart.CHARTTYPE_3DStackedBarChart.equals(type))
+ {
+ return create3DStackedBarChart();
+ }
+ else if (MChart.CHARTTYPE_3DPieChart.equals(type))
+ {
+ return create3DPieChart();
+ }
+ else if (MChart.CHARTTYPE_PieChart.equals(type))
+ {
+ return createPieChart();
+ }
+ else if (MChart.CHARTTYPE_3DLineChart.equals(type))
+ {
+ return create3DLineChart();
+ }
+ else if (MChart.CHARTTYPE_AreaChart.equals(type))
+ {
+ return createAreaChart();
+ }
+ else if (MChart.CHARTTYPE_StackedAreaChart.equals(type))
+ {
+ return createStackedAreaChart();
+ }
+ else if (MChart.CHARTTYPE_LineChart.equals(type))
+ {
+ if (mChart.isTimeSeries())
+ return createTimeSeriesChart();
+ return createLineChart();
+ }
+ else if (MChart.CHARTTYPE_RingChart.equals(type))
+ {
+ return createRingChart();
+ }
+ else if (MChart.CHARTTYPE_WaterfallChart.equals(type))
+ {
+ return createWaterfallChart();
+ }
+ else
+ {
+ throw new IllegalArgumentException("unknown chart type=" + type);
+ }
+ }
+
+ public void loadData() {
+ queries = new HashMap();
+ for ( MChartDatasource ds : mChart.getDatasources() )
+ {
+ addData(ds);
+ }
+ }
+
+ private void addData(MChartDatasource ds) {
+
+ String value = ds.getValueColumn();
+ String category;
+ String unit = "D";
+
+ if ( !mChart.isTimeSeries() )
+ category = ds.getCategoryColumn();
+ else
+ {
+ if ( mChart.getTimeUnit().equals(MChart.TIMEUNIT_Week))
+ {
+ unit = "W";
+ }
+ else if ( mChart.getTimeUnit().equals(MChart.TIMEUNIT_Month))
+ {
+ unit = "MM";
+ }
+ else if ( mChart.getTimeUnit().equals(MChart.TIMEUNIT_Quarter))
+ {
+ unit = "Q";
+ }
+ else if ( mChart.getTimeUnit().equals(MChart.TIMEUNIT_Year))
+ {
+ unit = "Y";
+ }
+
+ category = " TRUNC(" + ds.getDateColumn() + ", '" + unit + "') ";
+ }
+
+ String series = DB.TO_STRING(ds.getName());
+ boolean hasSeries = false;
+ if (ds.getSeriesColumn() != null)
+ {
+ series = ds.getSeriesColumn();
+ hasSeries = true;
+ }
+
+ String where = ds.getWhereClause();
+ if ( !Util.isEmpty(where))
+ {
+ where = Env.parseContext(Env.getCtx(), mChart.getWindowNo(), where, true);
+ }
+
+ boolean hasWhere = false;
+
+ String sql = "SELECT " + value + ", " + category + ", " + series
+ + " FROM " + ds.getFromClause();
+ if ( !Util.isEmpty(where))
+ {
+ sql += " WHERE " + where;
+ hasWhere = true;
+ }
+
+ Date currentDate = Env.getContextAsDate(Env.getCtx(), "#Date");
+ Date startDate = null;
+ Date endDate = null;
+
+ int scope = mChart.getTimeScope();
+ int offset = ds.getTimeOffset();
+
+ if ( mChart.isTimeSeries() && scope != 0 )
+ {
+ offset += -scope;
+ startDate = increment(currentDate, mChart.getTimeUnit(), offset);
+ endDate = increment(startDate, mChart.getTimeUnit(), scope);
+ }
+
+ if ( startDate != null && endDate != null )
+ {
+ sql += hasWhere ? " AND " : " WHERE ";
+ sql += category + ">=TRUNC(" + DB.TO_DATE(new Timestamp(startDate.getTime())) + ", '" + unit + "') AND ";
+ sql += category + "<=TRUNC(" + DB.TO_DATE(new Timestamp(endDate.getTime())) + ", '" + unit + "') ";
+ }
+
+ if (sql.indexOf('@') >= 0) {
+ sql = Env.parseContext(Env.getCtx(), 0, sql, false, true);
+ }
+
+ MRole role = MRole.getDefault(Env.getCtx(), false);
+ sql = role.addAccessSQL(sql, null, true, false);
+
+ if (hasSeries)
+ sql += " GROUP BY " + series + ", " + category + " ORDER BY " + series + ", " + category;
+ else
+ sql += " GROUP BY " + category + " ORDER BY " + category;
+
+ log.log(Level.FINE, sql);
+
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+ ChartModel dataset = getChartModel();
+ minDate = null;
+ maxDate = null;
+
+ try
+ {
+ pstmt = DB.prepareStatement(sql, null);
+ rs = pstmt.executeQuery();
+ while(rs.next())
+ {
+ String key = rs.getString(2);
+ String seriesName = rs.getString(3);
+ if (seriesName == null)
+ seriesName = ds.getName();
+ String queryWhere = "";
+ if ( hasWhere )
+ queryWhere += where + " AND ";
+
+ queryWhere += series + " = " + DB.TO_STRING(seriesName) + " AND " + category + " = " ;
+
+ if (mChart.isTimeSeries())
+ {
+ Date date = rs.getDate(2);
+ if (minDate == null || minDate.compareTo(date) > 0)
+ minDate = date;
+ if (maxDate == null || maxDate.compareTo(date) < 0)
+ maxDate = date;
+ }
+
+ if ( mChart.isTimeSeries() && dataset instanceof XYModel )
+ {
+ XYModel xy = (XYModel) dataset;
+
+ Date date = rs.getDate(2);
+ BigDecimal tsvalue = rs.getBigDecimal(1);
+ xy.addValue(seriesName, date.getTime(), tsvalue);
+ key = tsDateFormat.format(date);
+ queryWhere += DB.TO_DATE(new Timestamp(date.getTime()));
+ }
+ else {
+ queryWhere += DB.TO_STRING(key);
+ }
+
+ MQuery query = new MQuery(ds.getAD_Table_ID());
+ String keyCol = MTable.get(Env.getCtx(), ds.getAD_Table_ID()).getKeyColumns()[0];
+ String whereClause = keyCol + " IN (SELECT " + ds.getKeyColumn() + " FROM "
+ + ds.getFromClause() + " WHERE " + queryWhere + " )";
+ query.addRestriction(whereClause.toString());
+ query.setRecordCount(1);
+
+ HashMap map = getQueries();
+
+ if (dataset instanceof PieModel) {
+ ((PieModel) dataset).setValue(key, rs.getBigDecimal(1));
+ map.put(key, query);
+ }
+ else if ( dataset instanceof CategoryModel ) {
+ ((CategoryModel) dataset).setValue(seriesName, key, rs.getBigDecimal(1));
+ map.put(seriesName + "__" + key, query);
+ }
+ else if (dataset instanceof XYModel ) {
+ map.put(seriesName + "__" + key, query);
+ }
+ }
+ }
+ catch (SQLException e)
+ {
+ throw new DBException(e, sql);
+ }
+ finally
+ {
+ DB.close(rs, pstmt);
+ rs = null; pstmt = null;
+ }
+ }
+
+ private Date increment(Date lastDate, String timeUnit, int qty) {
+
+ if ( lastDate == null )
+ return null;
+
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(lastDate);
+
+ if ( timeUnit.equals(MChart.TIMEUNIT_Day))
+ cal.add(Calendar.DAY_OF_YEAR, qty);
+ else if ( timeUnit.equals(MChart.TIMEUNIT_Week))
+ cal.add(Calendar.WEEK_OF_YEAR, qty);
+ else if ( timeUnit.equals(MChart.TIMEUNIT_Month))
+ cal.add(Calendar.MONTH, qty);
+ else if ( timeUnit.equals(MChart.TIMEUNIT_Quarter))
+ cal.add(Calendar.MONTH, 3*qty);
+ else if ( timeUnit.equals(MChart.TIMEUNIT_Year))
+ cal.add(Calendar.YEAR, qty);
+
+ return cal.getTime();
+ }
+
+ public CategoryModel getCategoryModel() {
+ chartModel = new SimpleCategoryModel();
+ loadData();
+ return (CategoryModel) chartModel;
+ }
+
+ public XYModel getXYModel() {
+ chartModel = new SimpleXYModel();
+ loadData();
+ return (XYModel) chartModel;
+ }
+
+ public PieModel getPieModel() {
+ chartModel = new SimplePieModel();
+ loadData();
+ return (PieModel) chartModel;
+ }
+
+ public ChartModel getChartModel() {
+ return chartModel;
+ }
+
+ public HashMap getQueries() {
+ return queries;
+ }
+
+ public MQuery getQuery(String key) {
+
+
+ if ( queries.containsKey(key) )
+ {
+ return queries.get(key);
+ }
+
+ return null;
+ }
+
+ private Billboard createXYBarChart() {
+ Billboard billboard = newBillboard("bar");
+ XYModel xymodel = getXYModel();
+ CategoryModel model = new SimpleCategoryModel();
+ Collection> seriesList = xymodel.getSeries();
+ for(Comparable> series : seriesList) {
+ int count = xymodel.getDataCount(series);
+ for(int i = 0; i < count; i++) {
+ Number value = xymodel.getY(series, i);
+ Number category = xymodel.getX(series, i);
+ Date date = new Date(category.longValue());
+ String categoryLabel = null;
+ categoryLabel = tsDateFormat.format(date);
+ Number oldValue = model.getValue(series, categoryLabel);
+ if (oldValue != null)
+ value = oldValue.doubleValue() + value.doubleValue();
+ model.setValue(series, categoryLabel, value);
+ }
+ }
+ billboard.setModel(model);
+ return billboard;
+ }
+
+ private Billboard createTimeSeriesChart() {
+ Billboard billboard = newBillboard("line");
+ XYModel xymodel = getXYModel();
+ CategoryModel model = new SimpleCategoryModel();
+ Collection> seriesList = xymodel.getSeries();
+ for(Comparable> series : seriesList) {
+ int count = xymodel.getDataCount(series);
+ for(int i = 0; i < count; i++) {
+ Number value = xymodel.getY(series, i);
+ Number category = xymodel.getX(series, i);
+ Date date = new Date(category.longValue());
+ String categoryLabel = null;
+ categoryLabel = tsDateFormat.format(date);
+ Number oldValue = model.getValue(series, categoryLabel);
+ if (oldValue != null)
+ value = oldValue.doubleValue() + value.doubleValue();
+ model.setValue(series, categoryLabel, value);
+ }
+ }
+ billboard.setModel(model);
+ return billboard;
+ }
+
+ private Billboard createWaterfallChart() {
+ Billboard billboard = newBillboard("waterfall");
+ CategoryModel model = getCategoryModel();
+ CategoryModel waterfallModel = new SimpleCategoryModel();
+ Collection> seriesList = model.getSeries();
+ Map, BigDecimal> valueMap = new HashMap, BigDecimal>();
+ Collection> categories = model.getCategories();
+ for(Comparable> series : seriesList) {
+ for(Comparable> category : categories) {
+ BigDecimal value = (BigDecimal) model.getValue(series, category);
+ BigDecimal diff = value;
+ BigDecimal oldValue = valueMap.get(series);
+ if (oldValue != null) {
+ diff = diff.subtract(oldValue);
+ }
+ valueMap.put(series, value);
+ waterfallModel.setValue(series, category, diff);
+ }
+ }
+ billboard.setModel(waterfallModel);
+ return billboard;
+ }
+
+ private Billboard newBillboard(String type) {
+ Billboard billboard = new Billboard();
+ if (mChart.isDisplayLegend()) {
+ billboard.setLegend(true, false);
+ billboard.addLegendOptions("location", "bottom"); //bottom, right
+ }
+ billboard.setTickAxisLabel(mChart.getDomainLabel());
+ billboard.setValueAxisLabel(mChart.getRangeLabel());
+ billboard.setTitle(mChart.getName());
+ billboard.setType(type);
+ return billboard;
+ }
+
+ private Billboard createRingChart() {
+ Billboard billboard = newBillboard("donut");
+ billboard.setModel(getPieModel());
+ return billboard;
+ }
+
+ private Billboard createPieChart() {
+ Billboard billboard = newBillboard("pie");
+ billboard.setModel(getPieModel());
+ return billboard;
+ }
+
+ private Billboard create3DPieChart() {
+ return createPieChart();
+ }
+
+ private Billboard createBarChart() {
+ Billboard billboard = newBillboard("bar");
+ billboard.setModel(getCategoryModel());
+ if (MChart.CHARTORIENTATION_Vertical.equals(mChart.getChartOrientation()))
+ billboard.setOrient(Billboard.VERTICAL_ORIENTATION);
+ else if (MChart.CHARTORIENTATION_Horizontal.equals(mChart.getChartOrientation()))
+ billboard.setOrient(Billboard.HORIZONTAL_ORIENTATION);
+ return billboard;
+ }
+
+ private Billboard create3DBarChart() {
+ return createBarChart();
+ }
+
+ private Billboard createStackedBarChart() {
+ Billboard billboard = newBillboard("stacked_bar");
+ billboard.setModel(getCategoryModel());
+ if (MChart.CHARTORIENTATION_Vertical.equals(mChart.getChartOrientation()))
+ billboard.setOrient(Billboard.VERTICAL_ORIENTATION);
+ else if (MChart.CHARTORIENTATION_Horizontal.equals(mChart.getChartOrientation()))
+ billboard.setOrient(Billboard.HORIZONTAL_ORIENTATION);
+ return billboard;
+ }
+
+ private Billboard create3DStackedBarChart() {
+ return createStackedBarChart();
+ }
+
+ private Billboard createAreaChart() {
+ Billboard billboard = newBillboard("area");
+ billboard.setModel(getCategoryModel());
+ return billboard;
+ }
+
+ private Billboard createStackedAreaChart() {
+ Billboard billboard = newBillboard("stacked_area");
+ billboard.setModel(getCategoryModel());
+ return billboard;
+ }
+
+ private Billboard createLineChart() {
+ Billboard billboard = newBillboard("line");
+ billboard.setModel(getCategoryModel());
+ return billboard;
+ }
+
+ private Billboard create3DLineChart() {
+ return createLineChart();
+ }
+
+ public Date getMinDate() {
+ return minDate;
+ }
+
+ public void setMinDate(Date minDate) {
+ this.minDate = minDate;
+ }
+
+ public Date getMaxDate() {
+ return maxDate;
+ }
+
+ public void setMaxDate(Date maxDate) {
+ this.maxDate = maxDate;
+ }
+}
diff --git a/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartRendererServiceImpl.java b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartRendererServiceImpl.java
new file mode 100644
index 0000000000..97e632a300
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/ChartRendererServiceImpl.java
@@ -0,0 +1,242 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.zk.billboard.chart;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Map;
+
+import org.adempiere.webui.apps.AEnv;
+import org.adempiere.webui.apps.graph.IChartRendererService;
+import org.adempiere.webui.apps.graph.model.ChartModel;
+import org.adempiere.webui.apps.graph.model.GoalModel;
+import org.adempiere.webui.apps.graph.model.IndicatorModel;
+import org.adempiere.webui.component.Label;
+import org.compiere.model.MChart;
+import org.compiere.model.MQuery;
+import org.compiere.model.MSysConfig;
+import org.compiere.util.Msg;
+import org.idempiere.zk.billboard.Billboard;
+import org.zkoss.json.JSONObject;
+import org.zkoss.zk.au.out.AuScript;
+import org.zkoss.zk.ui.Component;
+import org.zkoss.zk.ui.Executions;
+import org.zkoss.zk.ui.event.Event;
+import org.zkoss.zk.ui.event.EventListener;
+import org.zkoss.zk.ui.util.Clients;
+import org.zkoss.zul.CategoryModel;
+import org.zkoss.zul.Div;
+import org.zkoss.zul.PieModel;
+
+/**
+ * @author hengsin
+ *
+ */
+@org.osgi.service.component.annotations.Component(name="org.idempiere.zk.billboard.chart.ChartRendererServiceImpl", immediate = true,
+service = IChartRendererService.class, property = {"service.ranking:Integer=0"})
+public class ChartRendererServiceImpl implements IChartRendererService {
+
+ /**
+ *
+ */
+ public ChartRendererServiceImpl() {
+ }
+
+ /* (non-Javadoc)
+ */
+ @Override
+ public boolean renderPerformanceIndicator(Component parent, int chartWidth, int chartHeight, IndicatorModel model) {
+ PerformanceGraphBuilder builder = new PerformanceGraphBuilder();
+ Billboard billboard = builder.createIndicatorChart(model);
+ billboard.setStyle("width: "+chartWidth+"px;height: "+chartHeight+"px;");
+ parent.appendChild(billboard);
+
+ return true;
+ }
+
+ @Override
+ public boolean renderPerformanceGraph(Component parent, int chartWidth, int chartHeight,
+ GoalModel goalModel) {
+ PerformanceGraphBuilder builder = new PerformanceGraphBuilder();
+ Billboard billboard = builder.createPerformanceChart(goalModel, chartWidth, chartHeight);
+ parent.appendChild(billboard);
+
+ return true;
+ }
+
+ @Override
+ public boolean renderChart(Component parent, int width, int height,
+ ChartModel chartModel, boolean showTitle) {
+ ChartBuilder builder = new ChartBuilder(chartModel.chart);
+ Billboard billboard = builder.createChart();
+ billboard.setStyle("width: " + width + "px;" + " height: " + height + "px;");
+ if (!showTitle)
+ billboard.setTitle("");
+ updateUI(parent, chartModel, builder, billboard, width, height);
+
+ return true;
+ }
+
+ private void updateUI(Component parent, ChartModel chartModel, ChartBuilder builder, Billboard billboard, int width, int height) {
+ // set billboard time series properties
+ Date minDate = builder.getMinDate();
+ Date maxDate = builder.getMaxDate();
+ if (chartModel.chart.isTimeSeries() && minDate != null && maxDate != null)
+ {
+ billboard.setTimeSeries(true);
+
+ int noOfPeriod = 0;
+ if (width < MSysConfig.getIntValue("CHART_MIN_WIDTH_3_PERIOD", 230, chartModel.chart.getAD_Client_ID()))
+ noOfPeriod = 3;
+ else if (width < MSysConfig.getIntValue("CHART_MIN_WIDTH_6_PERIOD", 320, chartModel.chart.getAD_Client_ID()))
+ noOfPeriod = 6;
+
+ Calendar c = Calendar.getInstance();
+ c.setTime(maxDate);
+
+ String timeUnit = chartModel.chart.getTimeUnit();
+ if (chartModel.chart.getTimeScope() == 1)
+ {
+ if (timeUnit.equals(MChart.TIMEUNIT_Week))
+ timeUnit = MChart.TIMEUNIT_Day;
+ else if (timeUnit.equals(MChart.TIMEUNIT_Month))
+ timeUnit = MChart.TIMEUNIT_Week;
+ else if (timeUnit.equals(MChart.TIMEUNIT_Quarter))
+ timeUnit = MChart.TIMEUNIT_Month;
+ else if (timeUnit.equals(MChart.TIMEUNIT_Year))
+ timeUnit = MChart.TIMEUNIT_Quarter;
+ }
+
+ if (timeUnit.equals(MChart.TIMEUNIT_Day))
+ {
+ billboard.setTimeSeriesInterval("1 days");
+ billboard.setTimeSeriesFormat("%D"); // e.g. 03/26/08 %m/%d/%y
+ if (noOfPeriod != 0)
+ c.add(Calendar.DAY_OF_MONTH, -1 * (noOfPeriod - 1));
+ }
+ else if (timeUnit.equals(MChart.TIMEUNIT_Week))
+ {
+ billboard.setTimeSeriesInterval("1 weeks");
+ billboard.setTimeSeriesFormat("%D"); // e.g. 03/26/08 %m/%d/%y
+ if (noOfPeriod != 0)
+ c.add(Calendar.WEEK_OF_YEAR, -1 * (noOfPeriod - 1));
+ }
+ else if (timeUnit.equals(MChart.TIMEUNIT_Month))
+ {
+ billboard.setTimeSeriesInterval("1 months");
+ billboard.setTimeSeriesFormat("%b %Y"); // e.g. Sep 2008
+ if (noOfPeriod != 0)
+ c.add(Calendar.MONTH, -1 * (noOfPeriod - 1));
+ }
+ else if (timeUnit.equals(MChart.TIMEUNIT_Quarter))
+ {
+ billboard.setTimeSeriesInterval("3 months");
+ billboard.setTimeSeriesFormat("%b %Y"); // e.g. Sep 2008
+ if (noOfPeriod != 0)
+ c.add(Calendar.MONTH, -1 * (noOfPeriod - 1) * 3);
+ }
+ else if (timeUnit.equals(MChart.TIMEUNIT_Year))
+ {
+ billboard.setTimeSeriesInterval("1 years");
+ billboard.setTimeSeriesFormat("%Y"); // e.g. 2008
+ if (noOfPeriod != 0)
+ c.add(Calendar.YEAR, -1 * (noOfPeriod - 1));
+ }
+
+ if (noOfPeriod != 0)
+ {
+ Date startDate = c.getTime();
+ if (minDate.before(startDate))
+ minDate = startDate;
+ }
+
+ }
+
+ parent.getChildren().clear();
+ parent.appendChild(billboard);
+
+ Label label = new Label(Msg.translate(chartModel.chart.getCtx(), "NoDataAvailable"));
+ Div labelDiv = new Div();
+ labelDiv.setStyle("padding: 10px;");
+ labelDiv.appendChild(label);
+ Div div = new Div();
+ div.appendChild(labelDiv);
+ parent.appendChild(div);
+ div.setVisible(false);
+
+ if (Executions.getCurrent() != null)
+ {
+ String script = "var parent = jq('#" + parent.getUuid() + "');";
+ script += "var billboard = parent.children().first(); ";
+ script += "var div = parent.children().eq(1); ";
+ script += "if (billboard.children().length == 0) {";
+ script += "div.show(); ";
+ script += "billboard.hide(); ";
+ script += "parent.height(div.css('height')); }";
+ script += "else {";
+ script += "div.hide(); ";
+ script += "billboard.show(); ";
+ script += "}";
+ Clients.response(new AuScript(script));
+ }
+
+ ZoomListener listener = new ZoomListener(builder.getQueries(), billboard.getModel());
+ billboard.addEventListener("onDataClick", listener);
+ }
+
+ private static class ZoomListener implements EventListener {
+ private Map queries;
+ private org.zkoss.zul.ChartModel model;
+
+ private ZoomListener(Map queries, org.zkoss.zul.ChartModel model) {
+ this.queries = queries;
+ this.model = model;
+ }
+
+ @Override
+ public void onEvent(Event event) throws Exception {
+ JSONObject json = (JSONObject) event.getData();
+ Number seriesIndex = (Number) json.get("seriesIndex");
+ Number pointIndex = (Number) json.get("pointIndex");
+ if (pointIndex == null)
+ pointIndex = Integer.valueOf(0);
+
+ MQuery query = null;
+ if (model instanceof PieModel) {
+ PieModel pieModel = (PieModel) model;
+ Object category = pieModel.getCategory(pointIndex.intValue());
+ if (category != null)
+ query = queries.get(category.toString());
+ } else if (model instanceof CategoryModel) {
+ CategoryModel categoryModel = (CategoryModel) model;
+ Object series = categoryModel.getSeries(seriesIndex.intValue());
+ Object category = categoryModel.getCategory(pointIndex.intValue());
+ query = queries.get(series.toString()+"__"+category.toString());
+ }
+ if (query != null)
+ AEnv.zoom(query);
+ }
+ }
+}
diff --git a/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/PerformanceGraphBuilder.java b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/PerformanceGraphBuilder.java
new file mode 100644
index 0000000000..edf715c897
--- /dev/null
+++ b/org.idempiere.zk.billboard.chart/src/org/idempiere/zk/billboard/chart/PerformanceGraphBuilder.java
@@ -0,0 +1,363 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.zk.billboard.chart;
+
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import org.adempiere.apps.graph.GraphColumn;
+import org.adempiere.webui.apps.AEnv;
+import org.adempiere.webui.apps.graph.model.GoalModel;
+import org.adempiere.webui.apps.graph.model.IndicatorModel;
+import org.adempiere.webui.component.ZkCssHelper;
+import org.compiere.model.MColorSchema;
+import org.compiere.model.MGoal;
+import org.compiere.model.MQuery;
+import org.compiere.model.X_PA_Goal;
+import org.idempiere.zk.billboard.Billboard;
+import org.zkoss.json.JSONObject;
+import org.zkoss.zk.ui.event.Event;
+import org.zkoss.zk.ui.event.EventListener;
+import org.zkoss.zul.CategoryModel;
+import org.zkoss.zul.DialModel;
+import org.zkoss.zul.DialModelRange;
+import org.zkoss.zul.DialModelScale;
+import org.zkoss.zul.PieModel;
+import org.zkoss.zul.SimpleCategoryModel;
+import org.zkoss.zul.SimplePieModel;
+
+/**
+ *
+ * @author hengsin
+ *
+ */
+public class PerformanceGraphBuilder {
+
+ public Billboard createIndicatorChart(IndicatorModel model) {
+ Billboard billboard = new Billboard();
+ billboard.setType("gauge");
+ DialModel dialModel = createDialModel(model);
+ buildDialRendererOptions(billboard, dialModel);
+ billboard.setModel(dialModel);
+ return billboard;
+ }
+
+ private void buildDialRendererOptions(Billboard billboard, DialModel dialModel) {
+ DialModelScale dialScale = dialModel.getScale(0);
+ billboard.addRendererOptions("min", 0);
+ billboard.addRendererOptions("max", dialScale.getScaleUpperBound());
+ List intervals = new ArrayList();
+ List intervalColors = new ArrayList();
+ for(int i = 0; i < dialScale.rangeSize(); i++) {
+ DialModelRange dialRange = dialScale.getRange(i);
+ double upperBound = dialRange.getUpperBound();
+ intervals.add(upperBound);
+ intervalColors.add(dialRange.getRangeColor());
+ }
+ List ticks = new ArrayList(intervals);
+ ticks.add(0, 0d);
+ billboard.addRendererOptions("ticks", ticks.toArray(new Double[0]));
+ billboard.addRendererOptions("intervals", intervals.toArray(new Double[0]));
+ billboard.addRendererOptions("intervalColors", intervalColors.toArray(new String[0]));
+ billboard.addRendererOptions("tickColor", dialScale.getTickColor());
+ billboard.addRendererOptions("background", dialModel.getFrameBgColor());
+ }
+
+ private DialModel createDialModel(IndicatorModel model)
+ {
+ DialModel dialModel = new DialModel();
+
+ MColorSchema colorSchema = model.goalModel.getColorSchema();
+ int upperBound = 0;
+ int [] rangeBounds = new int[]{colorSchema.getMark1Percent(), colorSchema.getMark2Percent(), colorSchema.getMark3Percent(), colorSchema.getMark4Percent()};
+ for (int rangeBound : rangeBounds)
+ {
+ if (rangeBound > upperBound)
+ {
+ if (rangeBound == 9999)
+ {
+ upperBound = (int) Math.floor(upperBound*1.5);
+ break;
+ }
+ else
+ {
+ upperBound = rangeBound;
+ }
+ }
+ }
+ DialModelScale dialScale = dialModel.newScale(0, upperBound, 180, -180, colorSchema.getMark2Percent() - colorSchema.getMark1Percent(), 5);
+ int rangeLo = 0;
+ for (int rangeHi : rangeBounds){
+ if (rangeHi==9999)
+ rangeHi = (int) Math.floor(rangeLo*1.5);
+ if (rangeLo < rangeHi) {
+ dialScale.newRange(rangeLo, rangeHi, "#"+ZkCssHelper.createHexColorString(colorSchema.getColor(rangeHi)), 0.5, 0.5);
+ rangeLo = rangeHi;
+ }
+ }
+
+ dialModel.setFrameBgColor("#"+ZkCssHelper.createHexColorString(model.dialBackground));
+ dialScale.setTickFont(new Font("SansSerif", Font.BOLD, 8));
+ dialScale.setValueFont(new Font("SansSerif", Font.BOLD, 8));
+ dialModel.setFrameFgColor("#000000");
+ dialScale.setTickColor("#"+ZkCssHelper.createHexColorString(model.tickColor));
+ //
+ dialScale.setValue(model.goalModel.getPercent());
+ return dialModel;
+ }
+
+ public Billboard createPerformanceChart(GoalModel goalModel, int chartWidth, int chartHeight) {
+ if(X_PA_Goal.CHARTTYPE_BarChart.equals(goalModel.chartType))
+ {
+ return createBarChart(goalModel, chartWidth, chartHeight);
+ }
+ else if (X_PA_Goal.CHARTTYPE_PieChart.equals(goalModel.chartType))
+ {
+ return createPieChart(goalModel, chartWidth, chartHeight);
+ }
+ else if (X_PA_Goal.CHARTTYPE_AreaChart.equals(goalModel.chartType))
+ {
+ return createAreaChart(goalModel, chartWidth, chartHeight);
+ }
+ else if (X_PA_Goal.CHARTTYPE_LineChart.equals(goalModel.chartType))
+ {
+ return createLineChart(goalModel, chartWidth, chartHeight);
+ }
+ else if (X_PA_Goal.CHARTTYPE_RingChart.equals(goalModel.chartType))
+ {
+ return createDonutChart(goalModel, chartWidth, chartHeight);
+ }
+ else if (X_PA_Goal.CHARTTYPE_WaterfallChart.equals(goalModel.chartType))
+ {
+ return createWaterfallChart(goalModel, chartWidth, chartHeight);
+ }
+ else
+ {
+ throw new IllegalArgumentException("unknown chart type=" + goalModel.chartType);
+ }
+ }
+
+ private Billboard createWaterfallChart(GoalModel goalModel, int chartWidth,
+ int chartHeight) {
+ Billboard billboard = new Billboard();
+ billboard.setType("waterfall");
+ CategoryModel chartModel = createWaterfallModel(goalModel);
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ billboard.setTickAxisLabel(goalModel.xAxisLabel);
+ billboard.setValueAxisLabel(goalModel.yAxisLabel);
+ billboard.setLegend(false, false);
+ buildRendererOptions(billboard, goalModel);
+ return billboard;
+ }
+
+ private Billboard createDonutChart(GoalModel goalModel, int chartWidth,
+ int chartHeight) {
+ Billboard billboard = new Billboard();
+ billboard.setType("donut");
+ PieModel chartModel = new SimplePieModel();
+ List list = goalModel.columnList;
+ for (int i = 0; i < list.size(); i++){
+ chartModel.setValue(list.get(i).getLabel(), list.get(i).getValue());
+ }
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.setLegend(true, true);
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ return billboard;
+ }
+
+ private Billboard createLineChart(GoalModel goalModel, int chartWidth,
+ int chartHeight) {
+ Billboard billboard = new Billboard();
+ billboard.setType("line");
+ CategoryModel chartModel = createCategoryModel(goalModel, true);
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ billboard.setTickAxisLabel(goalModel.xAxisLabel);
+ billboard.setValueAxisLabel(goalModel.yAxisLabel);
+ billboard.setLegend(false, false);
+ buildRendererOptions(billboard, goalModel);
+ return billboard;
+ }
+
+ private Billboard createAreaChart(GoalModel goalModel, int chartWidth,
+ int chartHeight) {
+ Billboard billboard = new Billboard();
+ billboard.setType("area");
+ CategoryModel chartModel = createCategoryModel(goalModel, true);
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ billboard.setLegend(false, false);
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ billboard.setTickAxisLabel(goalModel.xAxisLabel);
+ billboard.setValueAxisLabel(goalModel.yAxisLabel);
+ buildRendererOptions(billboard, goalModel);
+ return billboard;
+ }
+
+ private Billboard createPieChart(GoalModel goalModel, int chartWidth,
+ int chartHeight) {
+ Billboard billboard = new Billboard();
+ billboard.setType("pie");
+ PieModel chartModel = new SimplePieModel();
+ List list = goalModel.columnList;
+ for (int i = 0; i < list.size(); i++){
+ chartModel.setValue(list.get(i).getLabel(), list.get(i).getValue());
+ }
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ billboard.setLegend(true, true);
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ return billboard;
+ }
+
+ private Billboard createBarChart(final GoalModel goalModel, int chartWidth, int chartHeight) {
+
+ Billboard billboard = new Billboard();
+ billboard.setRenderdefer(500);
+ billboard.setType("bar");
+ CategoryModel chartModel = createCategoryModel(goalModel, true);
+ billboard.setModel(chartModel);
+ billboard.setStyle("width: " + chartWidth + "px" +
+ "; height: "+chartHeight+"px");
+ billboard.addEventListener("onDataClick", new ZoomListener(goalModel, chartModel.getCategories().toArray(new Comparable>[0])));
+ if (goalModel.showTitle)
+ billboard.setTitle(goalModel.goal.getMeasure().getName());
+ billboard.setTickAxisLabel(goalModel.xAxisLabel);
+ billboard.setValueAxisLabel(goalModel.yAxisLabel);
+ billboard.setLegend(false, false);
+ buildRendererOptions(billboard, goalModel);
+ return billboard;
+ }
+
+ private CategoryModel createCategoryModel(GoalModel goalModel, boolean linear) {
+ CategoryModel chartModel = new SimpleCategoryModel();
+ List list = goalModel.columnList;
+ for (int i = 0; i < list.size(); i++){
+ String series = goalModel.xAxisLabel;
+ if (!linear) {
+ if (list.get(i).getDate() != null) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(list.get(i).getDate());
+ series = Integer.toString(cal.get(Calendar.YEAR));
+ }
+ }
+ chartModel.setValue(series, list.get(i).getLabel(), list.get(i).getValue());
+ }
+ return chartModel;
+ }
+
+ private CategoryModel createWaterfallModel(GoalModel goalModel) {
+ return createCategoryModel(goalModel, true);
+ }
+
+ private void buildRendererOptions(Billboard billboard, GoalModel goalModel) {
+ List intervals = new ArrayList();
+ List intervalColors = new ArrayList();
+ MColorSchema colorSchema = goalModel.goal.getColorSchema();
+ int upperBound = 0;
+ int [] rangeBounds = new int[]{colorSchema.getMark1Percent(), colorSchema.getMark2Percent(), colorSchema.getMark3Percent(), colorSchema.getMark4Percent()};
+ for (int rangeBound : rangeBounds)
+ {
+ if (rangeBound > upperBound)
+ {
+ if (rangeBound == 9999)
+ {
+ upperBound = (int) Math.floor(upperBound*1.5);
+ break;
+ }
+ else
+ {
+ upperBound = rangeBound;
+ }
+ }
+ }
+ int rangeLo = 0;
+ for (int rangeHi : rangeBounds){
+ if (rangeHi==9999)
+ rangeHi = (int) Math.floor(rangeLo*1.5);
+ if (rangeLo < rangeHi) {
+ intervals.add(Double.valueOf(rangeHi));
+ intervalColors.add("#"+ZkCssHelper.createHexColorString(colorSchema.getColor(rangeHi)));
+ rangeLo = rangeHi;
+ }
+ }
+ billboard.addRendererOptions("intervals", intervals.toArray(new Double[0]));
+ billboard.addRendererOptions("intervalColors", intervalColors.toArray(new String[0]));
+ }
+
+ private static class ZoomListener implements EventListener {
+ private GoalModel goalModel;
+ private Comparable>[] categories;
+
+ private ZoomListener(GoalModel goalModel, Comparable>[] comparables) {
+ this.goalModel = goalModel;
+ this.categories = comparables;
+ }
+
+ @Override
+ public void onEvent(Event event) throws Exception {
+ JSONObject json = (JSONObject) event.getData();
+ Number pointIndex = (Number) json.get("pointIndex");
+ if (pointIndex == null)
+ pointIndex = Integer.valueOf(0);
+ Comparable> categoryLabel = categories[pointIndex.intValue()];
+ List list = goalModel.columnList;
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i).getLabel().equals(categoryLabel)) {
+ zoom(goalModel.goal, list.get(i));
+ return;
+ }
+ }
+ }
+
+ private void zoom(MGoal goal, GraphColumn graphColumn) {
+ MQuery query = graphColumn.getMQuery(goal);
+ if (query != null)
+ AEnv.zoom(query);
+ }
+
+ }
+}
diff --git a/org.idempiere.zk.billboard/.project b/org.idempiere.zk.billboard/.project
new file mode 100644
index 0000000000..c873bc4b4d
--- /dev/null
+++ b/org.idempiere.zk.billboard/.project
@@ -0,0 +1,34 @@
+
+
+ org.idempiere.zk.billboard
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/org.idempiere.zk.billboard/.settings/org.eclipse.core.resources.prefs b/org.idempiere.zk.billboard/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.idempiere.zk.billboard/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/org.idempiere.zk.billboard/.settings/org.eclipse.m2e.core.prefs b/org.idempiere.zk.billboard/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000000..f897a7f1cb
--- /dev/null
+++ b/org.idempiere.zk.billboard/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/org.idempiere.zk.billboard/.settings/org.eclipse.pde.core.prefs b/org.idempiere.zk.billboard/.settings/org.eclipse.pde.core.prefs
new file mode 100755
index 0000000000..f29e940a00
--- /dev/null
+++ b/org.idempiere.zk.billboard/.settings/org.eclipse.pde.core.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/org.idempiere.zk.billboard/META-INF/MANIFEST.MF b/org.idempiere.zk.billboard/META-INF/MANIFEST.MF
new file mode 100644
index 0000000000..a318939cbd
--- /dev/null
+++ b/org.idempiere.zk.billboard/META-INF/MANIFEST.MF
@@ -0,0 +1,23 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Billboard
+Bundle-SymbolicName: org.idempiere.zk.billboard
+Bundle-Version: 10.0.0.qualifier
+Bundle-Vendor: iDempiere
+Automatic-Module-Name: org.idempiere.zk.billboard
+Bundle-RequiredExecutionEnvironment: JavaSE-11
+Export-Package: metainfo.zk,
+ org.idempiere.zk.billboard,
+ web.js.zul.billboard,
+ web.js.zul.billboard.css,
+ web.js.zul.billboard.ext,
+ web.js.zul.billboard.mold
+Require-Bundle: zcommon,
+ zel,
+ zhtml,
+ zjavassist,
+ zk,
+ zkbind,
+ zkplus,
+ zul,
+ zweb
diff --git a/org.idempiere.zk.billboard/README.md b/org.idempiere.zk.billboard/README.md
new file mode 100644
index 0000000000..7e9d713702
--- /dev/null
+++ b/org.idempiere.zk.billboard/README.md
@@ -0,0 +1,7 @@
+# org.idempiere.zk.billboard
+
+1. Wrap https://github.com/naver/billboard.js as zk component.
+
+2. To update, replace billboard.pkgd.js and billboard.pkgd.src.js with latest billboard.pkgd.js and billboard.pkgd.min.js from https://github.com/naver/billboard.js (Note that due to naming convention of zk, billboard.pkgd.js=billboard.pkgd.min.js and billboard.pkgd.src.js=billboard.pkgd.js ).
+
+3. To update, replace billboard.css with latest billboard.css from https://github.com/naver/billboard.js. Add !important to padding and text-align of .bb-tooltip th and padding of .bb-tooltip td to fix conflict with zk css
diff --git a/org.idempiere.zk.billboard/build.properties b/org.idempiere.zk.billboard/build.properties
new file mode 100644
index 0000000000..56d7765555
--- /dev/null
+++ b/org.idempiere.zk.billboard/build.properties
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = target/classes/
+bin.includes = META-INF/,\
+ .
diff --git a/org.idempiere.zk.billboard/pom.xml b/org.idempiere.zk.billboard/pom.xml
new file mode 100644
index 0000000000..44fa9ef201
--- /dev/null
+++ b/org.idempiere.zk.billboard/pom.xml
@@ -0,0 +1,12 @@
+
+ 4.0.0
+
+ org.idempiere
+ org.idempiere.parent
+ ${revision}
+ ../org.idempiere.parent/pom.xml
+
+ org.idempiere.zk.billboard
+ eclipse-plugin
+
diff --git a/org.idempiere.zk.billboard/src/metainfo/zk/lang-addon.xml b/org.idempiere.zk.billboard/src/metainfo/zk/lang-addon.xml
new file mode 100644
index 0000000000..22391a83e6
--- /dev/null
+++ b/org.idempiere.zk.billboard/src/metainfo/zk/lang-addon.xml
@@ -0,0 +1,25 @@
+
+
+
+ billboard
+ zul
+ xul/html
+
+ org.idempiere.zk.billboard.Version
+ 3.5.1.20220905
+
+
+ billboard
+ org.idempiere.zk.billboard.Billboard
+
+ default
+ zul.billboard.Billboard
+ mold/billboard.js
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/org.idempiere.zk.billboard/src/org/idempiere/zk/billboard/Billboard.java b/org.idempiere.zk.billboard/src/org/idempiere/zk/billboard/Billboard.java
new file mode 100644
index 0000000000..45cbb65f0e
--- /dev/null
+++ b/org.idempiere.zk.billboard/src/org/idempiere/zk/billboard/Billboard.java
@@ -0,0 +1,469 @@
+/***********************************************************************
+ * This file is part of iDempiere ERP Open Source *
+ * http://www.idempiere.org *
+ * *
+ * Copyright (C) Contributors *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software *
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
+ * MA 02110-1301, USA. *
+ * *
+ * Contributors: *
+ * - hengsin *
+ **********************************************************************/
+package org.idempiere.zk.billboard;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.zkoss.json.JSONArray;
+import org.zkoss.json.JSONObject;
+import org.zkoss.zk.au.AuRequest;
+import org.zkoss.zk.ui.event.Events;
+import org.zkoss.zul.CategoryModel;
+import org.zkoss.zul.ChartModel;
+import org.zkoss.zul.DialModel;
+import org.zkoss.zul.PieModel;
+import org.zkoss.zul.event.ChartDataEvent;
+import org.zkoss.zul.event.ChartDataListener;
+import org.zkoss.zul.impl.XulElement;
+
+/**
+ *
+ * @author hengsin
+ *
+ */
+public class Billboard extends XulElement {
+ /**
+ * generated serial id
+ */
+ private static final long serialVersionUID = -3888636406033151303L;
+
+ // Must
+ private ChartModel _model;
+
+ private ChartDataListener _dataListener;
+
+ // Optional
+ private String _title = "";
+ private String _type = "line";
+ private String _orient = "vertical";
+ private Map _rendererOptions;
+ private Map _legend;
+ private boolean timeSeries = false;
+ private String timeSeriesInterval = "1 months"; //"1 days", "1 year", "1 weeks"
+ private String timeSeriesFormat = "%b %Y"; //%Y - year, %m - month, %#d - day
+ private String tickAxisLabel = null;
+ private String valueAxisLabel = null;
+ private String[] seriesColors = null;
+ private int xAxisAngle = 0;
+
+ public static final String ON_DATA_CLICK_EVENT = "onDataClick";
+
+ public static final String VERTICAL_ORIENTATION = "vertical";
+ public static final String HORIZONTAL_ORIENTATION = "horizontal";
+
+ // Event Listener
+ static {
+ addClientEvent(Billboard.class, Events.ON_CLICK, CE_IMPORTANT);
+ addClientEvent(Billboard.class, ON_DATA_CLICK_EVENT, CE_IMPORTANT);
+ }
+
+ @Override
+ protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
+ throws java.io.IOException {
+ super.renderProperties(renderer);
+
+ render(renderer, "type", _type);
+ render(renderer, "title", _title);
+ render(renderer, "orient", _orient);
+ render(renderer, "timeSeries", timeSeries);
+ render(renderer, "xAxisAngle", xAxisAngle);
+ if (timeSeries) {
+ if (timeSeriesInterval != null)
+ render(renderer, "timeSeriesInterval", timeSeriesInterval);
+ if (timeSeriesFormat != null)
+ render(renderer, "timeSeriesFormat", timeSeriesFormat);
+ }
+
+ String model = toJSONArray(transferToJSONObject(getModel()));
+ render(renderer, "model", model);
+
+ if (_rendererOptions != null && !_rendererOptions.isEmpty()) {
+ JSONObject jData = mapToJSON(_rendererOptions);
+ render(renderer, "rendererOptions", jData.toString());
+ }
+
+ if (_legend != null && !_legend.isEmpty()) {
+ JSONObject jData = mapToJSON(_legend);
+ render(renderer, "legend", jData.toString());
+ }
+
+ if (tickAxisLabel != null)
+ render(renderer, "tickAxisLabel", tickAxisLabel);
+ if (valueAxisLabel != null)
+ render(renderer, "valueAxisLabel", valueAxisLabel);
+
+ if (seriesColors != null && seriesColors.length > 0) {
+ JSONArray jData = new JSONArray();
+ for(String s : seriesColors) {
+ jData.add(s);
+ }
+ render(renderer, "seriesColors", jData.toString());
+ }
+ /**
+ * JSON String Content
+ * "values": "X axis", "Line1":value1, "Line2": value2}
+ * [
+ * {"values":"Q1","'2001'":20,"'2002'":40},
+ * {"values":"Q2","'2001'":35,"'2002'":60},
+ * {"values":"Q3","'2001'":40,"'2002'":70},
+ * {"values":"Q4","'2001'":55,"'2002'":90}
+ * ]
+ */
+ }
+
+ private JSONObject mapToJSON(Map map) {
+ JSONObject jData = new JSONObject();
+ for(String key : map.keySet()) {
+ Object value = map.get(key);
+ jData.put(key, value);
+ }
+ return jData;
+ }
+
+ @Override
+ public void service(AuRequest request, boolean everError) {
+ if (Events.ON_CLICK.equals(request.getCommand())) {
+ Events.postEvent(Events.ON_CLICK, this, request.getData());
+ } else if (ON_DATA_CLICK_EVENT.equals(request.getCommand())) {
+ Events.postEvent(ON_DATA_CLICK_EVENT, this, request.getData());
+ } else {
+ super.service(request, everError);
+ }
+ }
+
+ private class DefaultChartDataListener implements ChartDataListener, Serializable {
+ private static final long serialVersionUID = 20091125153002L;
+
+ public void onChange(ChartDataEvent event) {
+ invalidate(); // Force redraw
+ }
+ }
+
+ /**
+ *
+ * @return {@link ChartModel}
+ */
+ public ChartModel getModel() {
+ return _model;
+ }
+
+ /**
+ *
+ * @param model
+ */
+ public void setModel(ChartModel model) {
+ if (_model != model) {
+ if (_model != null)
+ _model.removeChartDataListener(_dataListener);
+
+ _model = model;
+
+ if (_dataListener == null) {
+ _dataListener = new DefaultChartDataListener();
+ _model.addChartDataListener(_dataListener);
+ }
+ invalidate(); // Always redraw
+ }
+ }
+
+ /**
+ *
+ * @return chart title
+ */
+ public String getTitle() {
+ return _title;
+ }
+
+ /**
+ * set chart title
+ * @param title
+ */
+ public void setTitle(String title) {
+ if(!title.equals(this._title)) {
+ this._title = title;
+ smartUpdate("title", _title);
+ invalidate();
+ }
+ }
+
+ /**
+ *
+ * @return chart type
+ */
+ public String getType() {
+ return _type;
+ }
+
+ /**
+ * set chart type
+ * @param type
+ */
+ public void setType(String type) {
+ if(!type.equals(this._type)) {
+ if(isValid(type)) {
+ this._type = type;
+ smartUpdate("type", _type);
+ invalidate(); // Always redraw
+ }
+ }
+ }
+
+ /**
+ *
+ * @return chart orientation (horizontal or vertical)
+ */
+ public String getOrient() {
+ return _orient;
+ }
+
+ /**
+ * set chart orientation
+ * @param orient
+ */
+ public void setOrient(String orient) {
+ if(!orient.equals(this._orient)) {
+ this._orient = orient;
+ smartUpdate("orient", _orient);
+ invalidate();
+ }
+ }
+
+ /**
+ *
+ * @param key
+ * @param value
+ */
+ public void addRendererOptions(String key, Object value) {
+ if (_rendererOptions == null)
+ _rendererOptions = new HashMap();
+ _rendererOptions.put(key, value);
+ }
+
+ /**
+ *
+ * @param key
+ * @param value
+ */
+ public void addLegendOptions(String key, Object value) {
+ if (_legend == null)
+ _legend = new HashMap();
+ _legend.put(key, value);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private List transferToJSONObject(ChartModel model) {
+ LinkedList list = new LinkedList();
+
+ if (model == null || _type == null)
+ return list;
+
+ if ("gauge".equals(_type)) {
+ DialModel dialModel = (DialModel) model;
+ JSONObject json = new JSONObject();
+ json.put("value", new double[]{dialModel.getValue(0)});
+ list.add(json);
+ }
+ else if ("pie".equals(_type) || "donut".equals(_type)) {
+ PieModel tempModel = (PieModel) model;
+ for (int i = 0; i < tempModel.getCategories().size(); i++) {
+ Comparable category = tempModel.getCategory(i);
+ JSONObject json = new JSONObject();
+ json.put("category", category);
+ json.put("value", tempModel.getValue(category));
+ list.add(json);
+ }
+
+ } else {
+ CategoryModel tempModel = (CategoryModel) model;
+ int seriesLength = tempModel.getSeries().size();
+ for (int j = 0; j < seriesLength; j++) {
+ Comparable series = tempModel.getSeries(j);
+ for (int i = 0; i < tempModel.getCategories().size(); i++) {
+ Comparable category = tempModel.getCategory(i);
+ Number value = tempModel.getValue(series, category);
+ if (value != null) {
+ JSONObject jData = new JSONObject();
+ jData.put("category", category);
+ jData.put("series", series);
+ jData.put("value", value != null ? value : 0.00d);
+ list.add(jData);
+ }
+ }
+ }
+ }
+
+ return list;
+ }
+
+ // Helper
+ private static String toJSONArray(List list) {
+ // list may be null.
+ if (list == null || list.isEmpty())
+ return "";
+
+ final StringBuffer sb = new StringBuffer().append('[');
+ for (Iterator it = list.iterator(); it.hasNext();) {
+ String s = String.valueOf(it.next());
+ sb.append(s).append(',');
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ sb.append(']');
+ return sb.toString().replaceAll("\\\\", "");
+ }
+
+ //supported chart type
+ private static final List