Package net.opentsdb.graph

Source Code of net.opentsdb.graph.Plot

// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 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 Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.graph;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.opentsdb.core.DataPoint;
import net.opentsdb.core.DataPoints;
import net.opentsdb.meta.Annotation;

/**
* Produces files to generate graphs with Gnuplot.
* <p>
* This class takes a bunch of {@link DataPoints} instances and generates a
* Gnuplot script as well as the corresponding data files to feed to Gnuplot.
*/
public final class Plot {

  private static final Logger LOG = LoggerFactory.getLogger(Plot.class);

  /** Mask to use on 32-bit unsigned integers to avoid sign extension.  */
  private static final long UNSIGNED = 0x00000000FFFFFFFFL;

  /** Default (current) timezone.  */
  private static final TimeZone DEFAULT_TZ = TimeZone.getDefault();

  /** Start time (UNIX timestamp in seconds) on 32 bits ("unsigned" int). */
  private final int start_time;

  /** End time (UNIX timestamp in seconds) on 32 bits ("unsigned" int). */
  private final int end_time;

  /** All the DataPoints we want to plot. */
  private ArrayList<DataPoints> datapoints =
    new ArrayList<DataPoints>();

  /** List of global annotations */
  private List<Annotation> globals = null;
 
  /** Per-DataPoints Gnuplot options. */
  private ArrayList<String> options = new ArrayList<String>();

  /** Global Gnuplot parameters. */
  private Map<String, String> params;

  /** Minimum width / height allowed. */
  private static final short MIN_PIXELS = 100;

  /** Width of the graph to generate, in pixels. */
  private short width = (short) 1024;

  /** Height of the graph to generate, in pixels. */
  private short height = (short) 768;

  /**
   * Number of seconds of difference to apply in order to get local time.
   * Gnuplot always renders timestamps in UTC, so we simply apply a delta
   * to get local time.
   */
  private final int utc_offset;

  /**
   * Constructor.
   * @param start_time Timestamp of the start time of the graph.
   * @param end_time Timestamp of the end time of the graph.
   * @throws IllegalArgumentException if either timestamp is 0 or negative.
   * @throws IllegalArgumentException if {@code start_time >= end_time}.
   */
  public Plot(final long start_time, final long end_time) {
    this(start_time, end_time, DEFAULT_TZ);
  }

  /**
   * Constructor.
   * @param start_time Timestamp of the start time of the graph.
   * @param end_time Timestamp of the end time of the graph.
   * @param tz Timezone to use to render the timestamps.
   * If {@code null} the current timezone as of when the JVM started is used.
   * @throws IllegalArgumentException if either timestamp is 0 or negative.
   * @throws IllegalArgumentException if {@code start_time >= end_time}.
   * @since 1.1
   */
   public Plot(final long start_time, final long end_time, TimeZone tz) {
    if ((start_time & 0xFFFFFFFF00000000L) != 0) {
      throw new IllegalArgumentException("Invalid start time: " + start_time);
    } else if ((end_time & 0xFFFFFFFF00000000L) != 0) {
      throw new IllegalArgumentException("Invalid end time: " + end_time);
    } else if (start_time >= end_time) {
      throw new IllegalArgumentException("start time (" + start_time
        + ") is greater than or equal to end time: " + end_time);
    }
    this.start_time = (int) start_time;
    this.end_time = (int) end_time;
    if (tz == null) {
      tz = DEFAULT_TZ;
    }
    this.utc_offset = tz.getOffset(System.currentTimeMillis()) / 1000;
  }

  /**
   * Sets the global parameters for this plot.
   * @param params Each entry is a Gnuplot setting that will be written as-is
   * in the Gnuplot script file: {@code set KEY VALUE}.
   * When the value is {@code null} the script will instead contain
   * {@code unset KEY}.
   * <p>
   * Special parameters with a special meaning (since OpenTSDB 1.1):
   * <ul>
   * <li>{@code bgcolor}: Either {@code transparent} or an RGB color in
   * hexadecimal (with a leading 'x' as in {@code x01AB23}).</li>
   * <li>{@code fgcolor}: An RGB color in hexadecimal ({@code x42BEE7}).</li>
   * </ul>
   */
  public void setParams(final Map<String, String> params) {
    this.params = params;
  }

  /**
   * Sets the dimensions of the graph (in pixels).
   * @param width The width of the graph produced (in pixels).
   * @param height The height of the graph produced (in pixels).
   * @throws IllegalArgumentException if the width or height are negative,
   * zero or "too small" (e.g. less than 100x100 pixels).
   */
  public void setDimensions(final short width, final short height) {
    if (width < MIN_PIXELS || height < MIN_PIXELS) {
      final String what = width < MIN_PIXELS ? "width" : "height";
      throw new IllegalArgumentException(what + " smaller than " + MIN_PIXELS
                                         + " in " + width + 'x' + height);
    }
    this.width = width;
    this.height = height;
  }

  /** @param globals A list of global annotation objects, may be null */
  public void setGlobals(final List<Annotation> globals) {
    this.globals = globals;
  }
 
  /**
   * Adds some data points to this plot.
   * @param datapoints The data points to plot.
   * @param options The options to apply to this specific series.
   */
  public void add(final DataPoints datapoints,
                  final String options) {
    // Technically, we could check the number of data points in the
    // datapoints argument in order to do something when there are none, but
    // this is potentially expensive with a SpanGroup since it requires
    // iterating through the entire SpanGroup.  We'll check this later
    // when we're trying to use the data, in order to avoid multiple passes
    // through the entire data.
    this.datapoints.add(datapoints);
    this.options.add(options);
  }

  /**
   * Returns a view on the datapoints in this plot.
   * Do not attempt to modify the return value.
   */
  public Iterable<DataPoints> getDataPoints() {
    return datapoints;
  }

  /**
   * Generates the Gnuplot script and data files.
   * @param basepath The base path to use.  A number of new files will be
   * created and their names will all start with this string.
   * @return The number of data points sent to Gnuplot.  This can be less
   * than the number of data points involved in the query due to things like
   * aggregation or downsampling.
   * @throws IOException if there was an error while writing one of the files.
   */
  public int dumpToFiles(final String basepath) throws IOException {
    int npoints = 0;
    final int nseries = datapoints.size();
    final String datafiles[] = nseries > 0 ? new String[nseries] : null;
    for (int i = 0; i < nseries; i++) {
      datafiles[i] = basepath + "_" + i + ".dat";
      final PrintWriter datafile = new PrintWriter(datafiles[i]);
      try {
        for (final DataPoint d : datapoints.get(i)) {
          final long ts = d.timestamp() / 1000;
          if (ts >= (start_time & UNSIGNED) && ts <= (end_time & UNSIGNED)) {
            npoints++;
          }
          datafile.print(ts + utc_offset);
          datafile.print(' ');
          if (d.isInteger()) {
            datafile.print(d.longValue());
          } else {
            final double value = d.doubleValue();
            if (value != value || Double.isInfinite(value)) {
              throw new IllegalStateException("NaN or Infinity found in"
                  + " datapoints #" + i + ": " + value + " d=" + d);
            }
            datafile.print(value);
          }
          datafile.print('\n');
        }
      } finally {
        datafile.close();
      }
    }

    if (npoints == 0) {
      // Gnuplot doesn't like empty graphs when xrange and yrange aren't
      // entirely defined, because it can't decide on good ranges with no
      // data.  We always set the xrange, but the yrange is supplied by the
      // user.  Let's make sure it defines a min and a max.
      params.put("yrange", "[0:10]")// Doesn't matter what values we use.
    }
    writeGnuplotScript(basepath, datafiles);
    return npoints;
  }

  /**
   * Generates the Gnuplot script.
   * @param basepath The base path to use.
   * @param datafiles The names of the data files that need to be plotted,
   * in the order in which they ought to be plotted.  It is assumed that
   * the ith file will correspond to the ith entry in {@code datapoints}.
   * Can be {@code null} if there's no data to plot.
   */
  private void writeGnuplotScript(final String basepath,
                                  final String[] datafiles) throws IOException {
    final String script_path = basepath + ".gnuplot";
    final PrintWriter gp = new PrintWriter(script_path);
    try {
      // XXX don't hardcode all those settings.  At least not like that.
      gp.append("set term png small size ")
        // Why the fuck didn't they also add methods for numbers?
        .append(Short.toString(width)).append(",")
        .append(Short.toString(height));
      final String smooth = params.remove("smooth");
      final String fgcolor = params.remove("fgcolor");
      String bgcolor = params.remove("bgcolor");
      if (fgcolor != null && bgcolor == null) {
        // We can't specify a fgcolor without specifying a bgcolor.
        bgcolor = "xFFFFFF"// So use a default.
      }
      if (bgcolor != null) {
        if (fgcolor != null && "transparent".equals(bgcolor)) {
          // In case we need to specify a fgcolor but we wanted a transparent
          // background, we also need to pass a bgcolor otherwise the first
          // hex color will be mistakenly taken as a bgcolor by Gnuplot.
          bgcolor = "transparent xFFFFFF";
        }
        gp.append(' ').append(bgcolor);
      }
      if (fgcolor != null) {
        gp.append(' ').append(fgcolor);
      }

      gp.append("\n"
                + "set xdata time\n"
                + "set timefmt \"%s\"\n"
                + "if (GPVAL_VERSION < 4.6) set xtics rotate; else set xtics rotate right\n"
                + "set output \"").append(basepath + ".png").append("\"\n"
                + "set xrange [\"")
        .append(String.valueOf((start_time & UNSIGNED) + utc_offset))
        .append("\":\"")
        .append(String.valueOf((end_time & UNSIGNED) + utc_offset))
        .append("\"]\n");
      if (!params.containsKey("format x")) {
        gp.append("set format x \"").append(xFormat()).append("\"\n");
      }
      final int nseries = datapoints.size();
      if (nseries > 0) {
        gp.write("set grid\n"
                 + "set style data linespoints\n");
        if (!params.containsKey("key")) {
          gp.write("set key right box\n");
        }
      } else {
        gp.write("unset key\n");
        if (params == null || !params.containsKey("label")) {
          gp.write("set label \"No data\" at graph 0.5,0.9 center\n");
        }
      }

      if (params != null) {
        for (final Map.Entry<String, String> entry : params.entrySet()) {
          final String key = entry.getKey();
          final String value = entry.getValue();
          if (value != null) {
            gp.append("set ").append(key)
              .append(' ').append(value).write('\n');
          } else {
            gp.append("unset ").append(key).write('\n');
          }
        }
      }
      for (final String opts : options) {
        if (opts.contains("x1y2")) {
          // Create a second scale for the y-axis on the right-hand side.
          gp.write("set y2tics border\n");
          break;
        }
      }
     
      // compile annotations to determine if we have any to graph
      final List<Annotation> notes = new ArrayList<Annotation>();
      for (int i = 0; i < nseries; i++) {
        final DataPoints dp = datapoints.get(i);
        final List<Annotation> series_notes = dp.getAnnotations();
        if (series_notes != null && !series_notes.isEmpty()) {
          notes.addAll(series_notes);
        }
      }
      if (globals != null) {
        notes.addAll(globals);
      }
      if (notes.size() > 0) {
        Collections.sort(notes);
        for(Annotation note : notes) {
          String ts = Long.toString(note.getStartTime());
          String value = new String(note.getDescription());
          gp.append("set arrow from \"").append(ts).append("\", graph 0 to \"");
          gp.append(ts).append("\", graph 1 nohead ls 3\n");
          gp.append("set object rectangle at \"").append(ts);
          gp.append("\", graph 0 size char (strlen(\"").append(value);
          gp.append("\")), char 1 front fc rgbcolor \"white\"\n");
          gp.append("set label \"").append(value).append("\" at \"");
          gp.append(ts).append("\", graph 0 front center\n");
        }
      }

      gp.write("plot ");
      for (int i = 0; i < nseries; i++) {
        final DataPoints dp = datapoints.get(i);
        final String title = dp.metricName() + dp.getTags();
        gp.append(" \"").append(datafiles[i]).append("\" using 1:2");
        if (smooth != null) {
          gp.append(" smooth ").append(smooth);
        }
        // TODO(tsuna): Escape double quotes in title.
        gp.append(" title \"").append(title).write('"');
        final String opts = options.get(i);
        if (!opts.isEmpty()) {
          gp.append(' ').write(opts);
        }
        if (i != nseries - 1) {
          gp.print(", \\");
        }
        gp.write('\n');
      }
      if (nseries == 0) {
        gp.write('0');
      }
    } finally {
      gp.close();
      LOG.info("Wrote Gnuplot script to " + script_path);
    }
  }

  /**
   * Finds some sensible default formatting for the X axis (time).
   * @return The Gnuplot time format string to use.
   */
  private String xFormat() {
    long timespan = (end_time & UNSIGNED) - (start_time & UNSIGNED);
    if (timespan < 2100) {  // 35m
      return "%H:%M:%S";
    } else if (timespan < 86400) {  // 1d
      return "%H:%M";
    } else if (timespan < 604800) {  // 1w
      return "%a %H:%M";
    } else if (timespan < 1209600) {  // 2w
      return "%a %d %H:%M";
    } else if (timespan < 7776000) {  // 90d
      return "%b %d";
    } else {
      return "%Y/%m/%d";
    }
  }

}
TOP

Related Classes of net.opentsdb.graph.Plot

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.