001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------ 028 * MovingAverage.java 029 * ------------------ 030 * (C) Copyright 2003-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Benoit Xhenseval; 034 * 035 */ 036 037package org.jfree.data.time; 038 039import org.jfree.chart.util.Args; 040import org.jfree.data.xy.XYDataset; 041import org.jfree.data.xy.XYSeries; 042import org.jfree.data.xy.XYSeriesCollection; 043 044/** 045 * A utility class for calculating moving averages of time series data. 046 */ 047public class MovingAverage { 048 049 /** 050 * Creates a new {@link TimeSeriesCollection} containing a moving average 051 * series for each series in the source collection. 052 * 053 * @param source the source collection. 054 * @param suffix the suffix added to each source series name to create the 055 * corresponding moving average series name. 056 * @param periodCount the number of periods in the moving average 057 * calculation. 058 * @param skip the number of initial periods to skip. 059 * 060 * @return A collection of moving average time series. 061 */ 062 public static TimeSeriesCollection createMovingAverage( 063 TimeSeriesCollection source, String suffix, int periodCount, 064 int skip) { 065 066 Args.nullNotPermitted(source, "source"); 067 if (periodCount < 1) { 068 throw new IllegalArgumentException("periodCount must be greater " 069 + "than or equal to 1."); 070 } 071 072 TimeSeriesCollection result = new TimeSeriesCollection(); 073 for (int i = 0; i < source.getSeriesCount(); i++) { 074 TimeSeries sourceSeries = source.getSeries(i); 075 TimeSeries maSeries = createMovingAverage(sourceSeries, 076 sourceSeries.getKey() + suffix, periodCount, skip); 077 result.addSeries(maSeries); 078 } 079 return result; 080 081 } 082 083 /** 084 * Creates a new {@link TimeSeries} containing moving average values for 085 * the given series. If the series is empty (contains zero items), the 086 * result is an empty series. 087 * 088 * @param source the source series. 089 * @param name the name of the new series. 090 * @param periodCount the number of periods used in the average 091 * calculation. 092 * @param skip the number of initial periods to skip. 093 * 094 * @return The moving average series. 095 */ 096 public static TimeSeries createMovingAverage(TimeSeries source, 097 String name, int periodCount, int skip) { 098 099 Args.nullNotPermitted(source, "source"); 100 if (periodCount < 1) { 101 throw new IllegalArgumentException("periodCount must be greater " 102 + "than or equal to 1."); 103 } 104 105 TimeSeries result = new TimeSeries(name); 106 107 if (source.getItemCount() > 0) { 108 109 // if the initial averaging period is to be excluded, then 110 // calculate the index of the 111 // first data item to have an average calculated... 112 long firstSerial = source.getTimePeriod(0).getSerialIndex() + skip; 113 114 for (int i = source.getItemCount() - 1; i >= 0; i--) { 115 116 // get the current data item... 117 RegularTimePeriod period = source.getTimePeriod(i); 118 long serial = period.getSerialIndex(); 119 120 if (serial >= firstSerial) { 121 // work out the average for the earlier values... 122 int n = 0; 123 double sum = 0.0; 124 long serialLimit = period.getSerialIndex() - periodCount; 125 int offset = 0; 126 boolean finished = false; 127 128 while ((offset < periodCount) && (!finished)) { 129 if ((i - offset) >= 0) { 130 TimeSeriesDataItem item = source.getRawDataItem( 131 i - offset); 132 RegularTimePeriod p = item.getPeriod(); 133 Number v = item.getValue(); 134 long currentIndex = p.getSerialIndex(); 135 if (currentIndex > serialLimit) { 136 if (v != null) { 137 sum = sum + v.doubleValue(); 138 n = n + 1; 139 } 140 } 141 else { 142 finished = true; 143 } 144 } 145 offset = offset + 1; 146 } 147 if (n > 0) { 148 result.add(period, sum / n); 149 } 150 else { 151 result.add(period, null); 152 } 153 } 154 155 } 156 } 157 158 return result; 159 160 } 161 162 /** 163 * Creates a new {@link TimeSeries} containing moving average values for 164 * the given series, calculated by number of points (irrespective of the 165 * 'age' of those points). If the series is empty (contains zero items), 166 * the result is an empty series. 167 * <p> 168 * Developed by Benoit Xhenseval (www.ObjectLab.co.uk). 169 * 170 * @param source the source series. 171 * @param name the name of the new series. 172 * @param pointCount the number of POINTS used in the average calculation 173 * (not periods!) 174 * 175 * @return The moving average series. 176 */ 177 public static TimeSeries createPointMovingAverage(TimeSeries source, 178 String name, int pointCount) { 179 180 Args.nullNotPermitted(source, "source"); 181 if (pointCount < 2) { 182 throw new IllegalArgumentException("periodCount must be greater " 183 + "than or equal to 2."); 184 } 185 186 TimeSeries result = new TimeSeries(name); 187 double rollingSumForPeriod = 0.0; 188 for (int i = 0; i < source.getItemCount(); i++) { 189 // get the current data item... 190 TimeSeriesDataItem current = source.getRawDataItem(i); 191 RegularTimePeriod period = current.getPeriod(); 192 // FIXME: what if value is null on next line? 193 rollingSumForPeriod += current.getValue().doubleValue(); 194 195 if (i > pointCount - 1) { 196 // remove the point i-periodCount out of the rolling sum. 197 TimeSeriesDataItem startOfMovingAvg = source.getRawDataItem( 198 i - pointCount); 199 rollingSumForPeriod -= startOfMovingAvg.getValue() 200 .doubleValue(); 201 result.add(period, rollingSumForPeriod / pointCount); 202 } 203 else if (i == pointCount - 1) { 204 result.add(period, rollingSumForPeriod / pointCount); 205 } 206 } 207 return result; 208 } 209 210 /** 211 * Creates a new {@link XYDataset} containing the moving averages of each 212 * series in the {@code source} dataset. 213 * 214 * @param source the source dataset. 215 * @param suffix the string to append to source series names to create 216 * target series names. 217 * @param period the averaging period. 218 * @param skip the length of the initial skip period. 219 * 220 * @return The dataset. 221 */ 222 public static XYDataset createMovingAverage(XYDataset source, String suffix, 223 long period, long skip) { 224 225 return createMovingAverage(source, suffix, (double) period, 226 (double) skip); 227 228 } 229 230 231 /** 232 * Creates a new {@link XYDataset} containing the moving averages of each 233 * series in the {@code source} dataset. 234 * 235 * @param source the source dataset. 236 * @param suffix the string to append to source series names to create 237 * target series names. 238 * @param period the averaging period. 239 * @param skip the length of the initial skip period. 240 * 241 * @return The dataset. 242 */ 243 public static XYDataset createMovingAverage(XYDataset source, 244 String suffix, double period, double skip) { 245 246 Args.nullNotPermitted(source, "source"); 247 XYSeriesCollection result = new XYSeriesCollection(); 248 for (int i = 0; i < source.getSeriesCount(); i++) { 249 XYSeries s = createMovingAverage(source, i, source.getSeriesKey(i) 250 + suffix, period, skip); 251 result.addSeries(s); 252 } 253 return result; 254 } 255 256 /** 257 * Creates a new {@link XYSeries} containing the moving averages of one 258 * series in the {@code source} dataset. 259 * 260 * @param source the source dataset. 261 * @param series the series index (zero based). 262 * @param name the name for the new series. 263 * @param period the averaging period. 264 * @param skip the length of the initial skip period. 265 * 266 * @return The dataset. 267 */ 268 public static XYSeries createMovingAverage(XYDataset source, 269 int series, String name, double period, double skip) { 270 271 Args.nullNotPermitted(source, "source"); 272 if (period < Double.MIN_VALUE) { 273 throw new IllegalArgumentException("period must be positive."); 274 } 275 if (skip < 0.0) { 276 throw new IllegalArgumentException("skip must be >= 0.0."); 277 } 278 279 XYSeries result = new XYSeries(name); 280 281 if (source.getItemCount(series) > 0) { 282 283 // if the initial averaging period is to be excluded, then 284 // calculate the lowest x-value to have an average calculated... 285 double first = source.getXValue(series, 0) + skip; 286 287 for (int i = source.getItemCount(series) - 1; i >= 0; i--) { 288 289 // get the current data item... 290 double x = source.getXValue(series, i); 291 292 if (x >= first) { 293 // work out the average for the earlier values... 294 int n = 0; 295 double sum = 0.0; 296 double limit = x - period; 297 int offset = 0; 298 boolean finished = false; 299 300 while (!finished) { 301 if ((i - offset) >= 0) { 302 double xx = source.getXValue(series, i - offset); 303 Number yy = source.getY(series, i - offset); 304 if (xx > limit) { 305 if (yy != null) { 306 sum = sum + yy.doubleValue(); 307 n = n + 1; 308 } 309 } 310 else { 311 finished = true; 312 } 313 } 314 else { 315 finished = true; 316 } 317 offset = offset + 1; 318 } 319 if (n > 0) { 320 result.add(x, sum / n); 321 } 322 else { 323 result.add(x, null); 324 } 325 } 326 327 } 328 } 329 330 return result; 331 332 } 333 334}