001/* ====================================================== 002 * JFreeChart : a chart library for the Java(tm) platform 003 * ====================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert and Contributors. 006 * 007 * Project Info: https://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 private MovingAverage() { 050 // no requirement to instantiate 051 } 052 053 /** 054 * Creates a new {@link TimeSeriesCollection} containing a moving average 055 * series for each series in the source collection. 056 * 057 * @param source the source collection. 058 * @param suffix the suffix added to each source series name to create the 059 * corresponding moving average series name. 060 * @param periodCount the number of periods in the moving average 061 * calculation. 062 * @param skip the number of initial periods to skip. 063 * 064 * @return A collection of moving average time series. 065 */ 066 public static TimeSeriesCollection createMovingAverage( 067 TimeSeriesCollection source, String suffix, int periodCount, 068 int skip) { 069 070 Args.nullNotPermitted(source, "source"); 071 if (periodCount < 1) { 072 throw new IllegalArgumentException("periodCount must be greater " 073 + "than or equal to 1."); 074 } 075 076 TimeSeriesCollection result = new TimeSeriesCollection(); 077 for (int i = 0; i < source.getSeriesCount(); i++) { 078 TimeSeries sourceSeries = source.getSeries(i); 079 TimeSeries maSeries = createMovingAverage(sourceSeries, 080 sourceSeries.getKey() + suffix, periodCount, skip); 081 result.addSeries(maSeries); 082 } 083 return result; 084 085 } 086 087 /** 088 * Creates a new {@link TimeSeries} containing moving average values for 089 * the given series. If the series is empty (contains zero items), the 090 * result is an empty series. 091 * 092 * @param source the source series. 093 * @param name the series key ({@code null} not permitted). 094 * @param periodCount the number of periods used in the average 095 * calculation. 096 * @param skip the number of initial periods to skip. 097 * 098 * @return The moving average series. 099 */ 100 public static TimeSeries createMovingAverage(TimeSeries source, 101 String name, int periodCount, int skip) { 102 103 Args.nullNotPermitted(source, "source"); 104 if (periodCount < 1) { 105 throw new IllegalArgumentException("periodCount must be greater " 106 + "than or equal to 1."); 107 } 108 109 TimeSeries result = new TimeSeries(name); 110 111 if (source.getItemCount() > 0) { 112 113 // if the initial averaging period is to be excluded, then 114 // calculate the index of the 115 // first data item to have an average calculated... 116 long firstSerial = source.getTimePeriod(0).getSerialIndex() + skip; 117 118 for (int i = source.getItemCount() - 1; i >= 0; i--) { 119 120 // get the current data item... 121 RegularTimePeriod period = source.getTimePeriod(i); 122 long serial = period.getSerialIndex(); 123 124 if (serial >= firstSerial) { 125 // work out the average for the earlier values... 126 int n = 0; 127 double sum = 0.0; 128 long serialLimit = period.getSerialIndex() - periodCount; 129 int offset = 0; 130 boolean finished = false; 131 132 while ((offset < periodCount) && (!finished)) { 133 if ((i - offset) >= 0) { 134 TimeSeriesDataItem item = source.getRawDataItem( 135 i - offset); 136 RegularTimePeriod p = item.getPeriod(); 137 Number v = item.getValue(); 138 long currentIndex = p.getSerialIndex(); 139 if (currentIndex > serialLimit) { 140 if (v != null) { 141 sum = sum + v.doubleValue(); 142 n = n + 1; 143 } 144 } 145 else { 146 finished = true; 147 } 148 } 149 offset = offset + 1; 150 } 151 if (n > 0) { 152 result.add(period, sum / n); 153 } 154 else { 155 result.add(period, null); 156 } 157 } 158 159 } 160 } 161 162 return result; 163 164 } 165 166 /** 167 * Creates a new {@link TimeSeries} containing moving average values for 168 * the given series, calculated by number of points (irrespective of the 169 * 'age' of those points). If the series is empty (contains zero items), 170 * the result is an empty series. 171 * <p> 172 * Developed by Benoit Xhenseval (www.ObjectLab.co.uk). 173 * 174 * @param source the source series. 175 * @param name the name of the new series. 176 * @param pointCount the number of POINTS used in the average calculation 177 * (not periods!) 178 * 179 * @return The moving average series. 180 */ 181 public static TimeSeries createPointMovingAverage(TimeSeries source, 182 String name, int pointCount) { 183 184 Args.nullNotPermitted(source, "source"); 185 if (pointCount < 2) { 186 throw new IllegalArgumentException("periodCount must be greater " 187 + "than or equal to 2."); 188 } 189 190 TimeSeries result = new TimeSeries(name); 191 double rollingSumForPeriod = 0.0; 192 for (int i = 0; i < source.getItemCount(); i++) { 193 // get the current data item... 194 TimeSeriesDataItem current = source.getRawDataItem(i); 195 RegularTimePeriod period = current.getPeriod(); 196 // FIXME: what if value is null on next line? 197 rollingSumForPeriod += current.getValue().doubleValue(); 198 199 if (i > pointCount - 1) { 200 // remove the point i-periodCount out of the rolling sum. 201 TimeSeriesDataItem startOfMovingAvg = source.getRawDataItem( 202 i - pointCount); 203 rollingSumForPeriod -= startOfMovingAvg.getValue() 204 .doubleValue(); 205 result.add(period, rollingSumForPeriod / pointCount); 206 } 207 else if (i == pointCount - 1) { 208 result.add(period, rollingSumForPeriod / pointCount); 209 } 210 } 211 return result; 212 } 213 214 /** 215 * Creates a new {@link XYDataset} containing the moving averages of each 216 * series in the {@code source} dataset. 217 * 218 * @param source the source dataset. 219 * @param suffix the string to append to source series names to create 220 * target series names. 221 * @param period the averaging period. 222 * @param skip the length of the initial skip period. 223 * 224 * @return The dataset. 225 */ 226 public static XYDataset createMovingAverage(XYDataset source, String suffix, 227 long period, long skip) { 228 229 return createMovingAverage(source, suffix, (double) period, 230 (double) skip); 231 232 } 233 234 235 /** 236 * Creates a new {@link XYDataset} containing the moving averages of each 237 * series in the {@code source} dataset. 238 * 239 * @param source the source dataset. 240 * @param suffix the string to append to source series names to create 241 * target series names. 242 * @param period the averaging period. 243 * @param skip the length of the initial skip period. 244 * 245 * @return The dataset. 246 */ 247 public static XYDataset createMovingAverage(XYDataset source, 248 String suffix, double period, double skip) { 249 250 Args.nullNotPermitted(source, "source"); 251 XYSeriesCollection result = new XYSeriesCollection(); 252 for (int i = 0; i < source.getSeriesCount(); i++) { 253 XYSeries s = createMovingAverage(source, i, source.getSeriesKey(i) 254 + suffix, period, skip); 255 result.addSeries(s); 256 } 257 return result; 258 } 259 260 /** 261 * Creates a new {@link XYSeries} containing the moving averages of one 262 * series in the {@code source} dataset. 263 * 264 * @param source the source dataset. 265 * @param series the series index (zero based). 266 * @param name the name for the new series. 267 * @param period the averaging period. 268 * @param skip the length of the initial skip period. 269 * 270 * @return The dataset. 271 */ 272 public static XYSeries createMovingAverage(XYDataset source, 273 int series, String name, double period, double skip) { 274 275 Args.nullNotPermitted(source, "source"); 276 if (period < Double.MIN_VALUE) { 277 throw new IllegalArgumentException("period must be positive."); 278 } 279 if (skip < 0.0) { 280 throw new IllegalArgumentException("skip must be >= 0.0."); 281 } 282 283 XYSeries result = new XYSeries(name); 284 285 if (source.getItemCount(series) > 0) { 286 287 // if the initial averaging period is to be excluded, then 288 // calculate the lowest x-value to have an average calculated... 289 double first = source.getXValue(series, 0) + skip; 290 291 for (int i = source.getItemCount(series) - 1; i >= 0; i--) { 292 293 // get the current data item... 294 double x = source.getXValue(series, i); 295 296 if (x >= first) { 297 // work out the average for the earlier values... 298 int n = 0; 299 double sum = 0.0; 300 double limit = x - period; 301 int offset = 0; 302 boolean finished = false; 303 304 while (!finished) { 305 if ((i - offset) >= 0) { 306 double xx = source.getXValue(series, i - offset); 307 Number yy = source.getY(series, i - offset); 308 if (xx > limit) { 309 if (yy != null) { 310 sum = sum + yy.doubleValue(); 311 n = n + 1; 312 } 313 } 314 else { 315 finished = true; 316 } 317 } 318 else { 319 finished = true; 320 } 321 offset = offset + 1; 322 } 323 if (n > 0) { 324 result.add(x, sum / n); 325 } 326 else { 327 result.add(x, null); 328 } 329 } 330 331 } 332 } 333 334 return result; 335 336 } 337 338}