killbill-uncached

analytics: add simple smoothing functions Signed-off-by:

5/17/2013 7:18:49 PM

Details

diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
index fbef095..6fbcf1a 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
@@ -39,6 +39,8 @@ import com.ning.billing.osgi.bundles.analytics.api.BusinessSnapshot;
 import com.ning.billing.osgi.bundles.analytics.api.user.AnalyticsUserApi;
 import com.ning.billing.osgi.bundles.analytics.json.NamedXYTimeSeries;
 import com.ning.billing.osgi.bundles.analytics.reports.ReportsUserApi;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother.SmootherType;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.UserType;
@@ -69,6 +71,7 @@ public class AnalyticsServlet extends HttpServlet {
 
     private static final String REPORTS = "reports";
     private static final String REPORTS_QUERY_NAME = "name";
+    private static final String REPORTS_SMOOTHER_NAME = "smooth";
 
     private static final ObjectMapper mapper = ObjectMapperProvider.get();
 
@@ -179,8 +182,10 @@ public class AnalyticsServlet extends HttpServlet {
         final LocalDate startDate = Strings.emptyToNull(req.getParameter(QUERY_START_DATE)) != null ? DATE_FORMAT.parseLocalDate(req.getParameter(QUERY_START_DATE)) : null;
         final LocalDate endDate = Strings.emptyToNull(req.getParameter(QUERY_END_DATE)) != null ? DATE_FORMAT.parseLocalDate(req.getParameter(QUERY_END_DATE)) : null;
 
+        final SmootherType smootherType = Smoother.fromString(Strings.emptyToNull(req.getParameter(REPORTS_SMOOTHER_NAME)));
+
         // TODO PIERRE Switch to an equivalent of StreamingOutputStream?
-        final List<NamedXYTimeSeries> result = reportsUserApi.getTimeSeriesDataForReport(reportNames, startDate, endDate);
+        final List<NamedXYTimeSeries> result = reportsUserApi.getTimeSeriesDataForReport(reportNames, startDate, endDate, smootherType);
 
         resp.getOutputStream().write(mapper.writeValueAsBytes(result));
         resp.setContentType("application/json");
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
index 638a597..bd5ed23 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/json/XY.java
@@ -18,8 +18,10 @@ package com.ning.billing.osgi.bundles.analytics.json;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class XY {
@@ -46,10 +48,15 @@ public class XY {
         this.xDate = xDate;
     }
 
+    public XY(final LocalDate xDate, final Float y) {
+        this(xDate.toDateTimeAtStartOfDay(DateTimeZone.UTC), y);
+    }
+
     public String getX() {
         return x;
     }
 
+    @JsonIgnore
     public DateTime getxDate() {
         return xDate;
     }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java
new file mode 100644
index 0000000..aaff67b
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/AverageSmoother.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+public class AverageSmoother extends Smoother {
+
+    public AverageSmoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        super(dataForReports, dateGranularity);
+    }
+
+    @Override
+    public float computeSmoothedValue(final float accumulator, final int accumulatorSize) {
+        return accumulator / accumulatorSize;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java
new file mode 100644
index 0000000..89579e8
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/DateGranularity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+public enum DateGranularity {
+    WEEKLY,
+    MONTHLY
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java
new file mode 100644
index 0000000..6ce8a8e
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/Smoother.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTimeConstants;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+import com.google.common.base.Function;
+
+public abstract class Smoother {
+
+    private final Map<String, Map<String, List<XY>>> dataForReports;
+    private final DateGranularity dateGranularity;
+
+    public static enum SmootherType {
+        AVERAGE_WEEKLY,
+        AVERAGE_MONTHLY,
+        SUM_WEEKLY,
+        SUM_MONTHLY;
+
+        public Smoother createSmoother(final Map<String, Map<String, List<XY>>> dataForReports) {
+            switch (this) {
+                case AVERAGE_WEEKLY:
+                    return new AverageSmoother(dataForReports, DateGranularity.WEEKLY);
+                case AVERAGE_MONTHLY:
+                    return new AverageSmoother(dataForReports, DateGranularity.MONTHLY);
+                case SUM_WEEKLY:
+                    return new SummingSmoother(dataForReports, DateGranularity.WEEKLY);
+                case SUM_MONTHLY:
+                    return new SummingSmoother(dataForReports, DateGranularity.MONTHLY);
+                default:
+                    return null;
+            }
+        }
+    }
+
+    public static SmootherType fromString(@Nullable final String smootherName) {
+        if (smootherName == null) {
+            return null;
+        } else {
+            return SmootherType.valueOf(smootherName.toUpperCase());
+        }
+    }
+
+    public Smoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        this.dataForReports = dataForReports;
+        this.dateGranularity = dateGranularity;
+    }
+
+    public abstract float computeSmoothedValue(float accumulator, int accumulatorSize);
+
+    // Assume the data is already sorted
+    public void smooth() {
+        for (final Map<String, List<XY>> dataForReport : dataForReports.values()) {
+            for (final String pivotName : dataForReport.keySet()) {
+                final List<XY> dataForPivot = dataForReport.get(pivotName);
+                final List<XY> smoothedData = smooth(dataForPivot);
+                dataForReport.put(pivotName, smoothedData);
+            }
+        }
+    }
+
+    public Map<String, Map<String, List<XY>>> getDataForReports() {
+        return dataForReports;
+    }
+
+    private List<XY> smooth(final List<XY> inputData) {
+        switch (dateGranularity) {
+            case WEEKLY:
+                return smooth(inputData,
+                              new Function<XY, LocalDate>() {
+                                  @Override
+                                  public LocalDate apply(final XY input) {
+                                      return input.getxDate().toLocalDate().withDayOfWeek(DateTimeConstants.MONDAY);
+                                  }
+                              });
+            case MONTHLY:
+                return smooth(inputData,
+                              new Function<XY, LocalDate>() {
+                                  @Override
+                                  public LocalDate apply(final XY input) {
+                                      return input.getxDate().toLocalDate().withDayOfMonth(1);
+                                  }
+                              });
+            default:
+                return inputData;
+        }
+    }
+
+    private List<XY> smooth(final List<XY> inputData, final Function<XY, LocalDate> truncator) {
+        final List<XY> smoothedData = new LinkedList<XY>();
+
+        LocalDate currentTruncatedDate = truncator.apply(inputData.get(0));
+        Float accumulator = (float) 0;
+        int accumulatorSize = 0;
+        for (final XY xy : inputData) {
+            final LocalDate zeTruncatedDate = truncator.apply(xy);
+            //noinspection ConstantConditions
+            if (zeTruncatedDate.compareTo(currentTruncatedDate) != 0) {
+                smoothedData.add(new XY(currentTruncatedDate, computeSmoothedValue(accumulator, accumulatorSize)));
+                accumulator = (float) 0;
+                accumulatorSize = 0;
+            }
+
+            accumulator += xy.getY();
+            accumulatorSize++;
+            currentTruncatedDate = zeTruncatedDate;
+        }
+
+        return smoothedData;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java
new file mode 100644
index 0000000..890979c
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/analysis/SummingSmoother.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.reports.analysis;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ning.billing.osgi.bundles.analytics.json.XY;
+
+public class SummingSmoother extends Smoother {
+
+    public SummingSmoother(final Map<String, Map<String, List<XY>>> dataForReports, final DateGranularity dateGranularity) {
+        super(dataForReports, dateGranularity);
+    }
+
+    @Override
+    public float computeSmoothedValue(final float accumulator, final int accumulatorSize) {
+        return accumulator;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java
index f7b18d4..cc0b89c 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/reports/ReportsUserApi.java
@@ -39,6 +39,8 @@ import com.ning.billing.osgi.bundles.analytics.BusinessExecutor;
 import com.ning.billing.osgi.bundles.analytics.dao.BusinessDBIProvider;
 import com.ning.billing.osgi.bundles.analytics.json.NamedXYTimeSeries;
 import com.ning.billing.osgi.bundles.analytics.json.XY;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother;
+import com.ning.billing.osgi.bundles.analytics.reports.analysis.Smoother.SmootherType;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
 
 import com.google.common.base.Predicate;
@@ -67,7 +69,8 @@ public class ReportsUserApi {
 
     public List<NamedXYTimeSeries> getTimeSeriesDataForReport(final String[] reportNames,
                                                               @Nullable final LocalDate startDate,
-                                                              @Nullable final LocalDate endDate) {
+                                                              @Nullable final LocalDate endDate,
+                                                              @Nullable final SmootherType smootherType) {
         // Mapping of report name -> pivots -> data
         final Map<String, Map<String, List<XY>>> dataForReports = new ConcurrentHashMap<String, Map<String, List<XY>>>();
 
@@ -77,10 +80,20 @@ public class ReportsUserApi {
         // Filter the data first
         filterValues(dataForReports, startDate, endDate);
 
-        // Normalize the data
-        normalizeXValues(dataForReports, startDate, endDate);
+        // Normalize and sort the data
+        normalizeAndSortXValues(dataForReports, startDate, endDate);
 
-        // Build the named timeseries
+        // Smooth the data if needed and build the named timeseries
+        if (smootherType != null) {
+            final Smoother smoother = smootherType.createSmoother(dataForReports);
+            smoother.smooth();
+            return buildNamedXYTimeSeries(smoother.getDataForReports());
+        } else {
+            return buildNamedXYTimeSeries(dataForReports);
+        }
+    }
+
+    private List<NamedXYTimeSeries> buildNamedXYTimeSeries(final Map<String, Map<String, List<XY>>> dataForReports) {
         final List<NamedXYTimeSeries> results = new LinkedList<NamedXYTimeSeries>();
         for (final String reportName : dataForReports.keySet()) {
             // Sort the pivots by name for a consistent display in the dashboard
@@ -146,7 +159,7 @@ public class ReportsUserApi {
     }
 
     // TODO PIERRE Naive implementation
-    private void normalizeXValues(final Map<String, Map<String, List<XY>>> dataForReports, @Nullable final LocalDate startDate, @Nullable final LocalDate endDate) {
+    private void normalizeAndSortXValues(final Map<String, Map<String, List<XY>>> dataForReports, @Nullable final LocalDate startDate, @Nullable final LocalDate endDate) {
         DateTime minDate = null;
         if (startDate != null) {
             minDate = startDate.toDateTimeAtStartOfDay(DateTimeZone.UTC);
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html b/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html
index c1a76ba..c4759e2 100644
--- a/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html
+++ b/osgi-bundles/bundles/analytics/src/main/resources/static/analytics.html
@@ -65,8 +65,11 @@
         }
 
         // Get the data for a set of reports
-        function doGetData(position, reports, from, to, fn) {
+        function doGetData(position, reports, from, to, smoothFunction, fn) {
           var request_url = "http://" + $VAR_SERVER + ":" + $VAR_PORT + "/plugins/killbill-analytics/reports?startDate=" + from + "&endDate=" + to + "&name=" + reports.join("&name=");
+          if (smoothFunction) {
+            request_url = request_url + "&smooth=" + smoothFunction;
+          }
 
           return $.ajax({
             type: "GET",
@@ -92,6 +95,8 @@
 
           // Map of position (starting from the top) to an array of reports
           var reports = {}
+          // Map of position (starting from the top) to a smoothing function
+          var smoothFunctions = {}
           for (var i = 1; i < 10; i++) {
             var reportsI = $.url().param('report' + i);
             if (!reportsI) {
@@ -102,6 +107,8 @@
             } else {
               reports[i] = [reportsI];
             }
+
+            smoothFunctions[i] = $.url().param('smooth' + i);
           }
 
           // Set sane defaults
@@ -116,7 +123,7 @@
           var futuresData = {}
           for (var position in reports) {
             // Fetch the data
-            var future = doGetData(position, reports[position], from, to, function(zePosition, reports, reportsData) {
+            var future = doGetData(position, reports[position], from, to, smoothFunctions[position], function(zePosition, reports, reportsData) {
               console.log(typeof reportsData);
               if (!(reportsData instanceof Array) || reportsData.length == 0) {
                 futuresData[zePosition] = [ { "name": "No data", "values": [] } ];