package com.ibm.nmon.gui.chart.builder; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.List; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.labels.XYToolTipGenerator; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.chart.util.RelativeDateFormat; import org.jfree.util.UnitType; import org.jfree.data.time.FixedMillisecond; import org.jfree.data.xy.XYDataset; import com.ibm.nmon.data.DataSet; import com.ibm.nmon.data.DataRecord; import com.ibm.nmon.data.DataType; import com.ibm.nmon.data.DataTuple; import com.ibm.nmon.data.definition.DataDefinition; import com.ibm.nmon.data.definition.NamingMode; import com.ibm.nmon.gui.chart.data.DataTupleXYDataset; import com.ibm.nmon.chart.definition.LineChartDefinition; public class LineChartBuilder extends BaseChartBuilder<LineChartDefinition> { public LineChartBuilder() { super(); } protected JFreeChart createChart() { DateAxis timeAxis = new DateAxis(); NumberAxis valueAxis = new NumberAxis(); valueAxis.setAutoRangeIncludesZero(true); DataTupleXYDataset dataset = new DataTupleXYDataset(definition.isStacked()); XYPlot plot = null; if (definition.isStacked()) { StackedXYAreaRenderer2 renderer = new StackedXYAreaRenderer2(); renderer.setBaseSeriesVisible(true, false); plot = new XYPlot(dataset, timeAxis, valueAxis, renderer); } else { StandardXYItemRenderer renderer = new StandardXYItemRenderer(); renderer.setBaseSeriesVisible(true, false); plot = new XYPlot(dataset, timeAxis, valueAxis, renderer); } if (definition.hasSecondaryYAxis()) { // second Y axis uses a separate dataset and axis plot.setDataset(1, new DataTupleXYDataset(definition.isStacked())); valueAxis = new NumberAxis(); valueAxis.setAutoRangeIncludesZero(true); // secondary axis data cannot be stacked, so use the the standard, line based rendering // for both types StandardXYItemRenderer renderer = new StandardXYItemRenderer(); renderer.setBaseSeriesVisible(true, false); plot.setRangeAxis(1, valueAxis); plot.setRenderer(1, renderer); plot.mapDatasetToRangeAxis(1, 1); } // null title font = it will be set in format // legend will be decided by callers return new JFreeChart("", null, plot, false); } protected void formatChart() { super.formatChart(); chart.setTitle(definition.getTitle()); XYPlot plot = chart.getXYPlot(); plot.getDomainAxis().setLabel(definition.getXAxisLabel()); plot.getRangeAxis().setLabel(definition.getYAxisLabel()); if (definition.usePercentYAxis()) { LineChartBuilder.setPercentYAxis(chart); } if (definition.isStacked()) { StackedXYAreaRenderer2 renderer = (StackedXYAreaRenderer2) plot.getRenderer(); renderer.setLegendArea(new java.awt.Rectangle(10, 10)); renderer.setBaseToolTipGenerator(tooltipGenerator); } else { // show filled markers at each data point StandardXYItemRenderer renderer = (StandardXYItemRenderer) plot.getRenderer(0); renderer.setBaseShapesVisible(true); renderer.setBaseShapesFilled(true); // if no data for more than 1 granularity's time period, do not draw a connecting line renderer.setPlotDiscontinuous(true); renderer.setGapThresholdType(UnitType.ABSOLUTE); recalculateGapThreshold(0); renderer.setBaseToolTipGenerator(tooltipGenerator); } if (definition.hasSecondaryYAxis()) { plot.getRangeAxis(1).setLabel(definition.getSecondaryYAxisLabel()); // show filled markers at each data point StandardXYItemRenderer renderer = (StandardXYItemRenderer) plot.getRenderer(1); renderer.setBaseShapesVisible(true); renderer.setBaseShapesFilled(true); // if no data for more than 1 granularity's time period, do not draw a connecting line renderer.setPlotDiscontinuous(true); renderer.setGapThresholdType(UnitType.ABSOLUTE); recalculateGapThreshold(1); renderer.setBaseToolTipGenerator(tooltipGenerator); } for (int i = 0; i < plot.getRangeAxisCount(); i++) { plot.getRangeAxis(i).setLabelFont(LABEL_FONT); plot.getRangeAxis(i).setTickLabelFont(AXIS_FONT); } plot.getDomainAxis().setLabelFont(LABEL_FONT); plot.getDomainAxis().setTickLabelFont(AXIS_FONT); // gray grid lines plot.setRangeGridlinePaint(GRID_COLOR); plot.setRangeGridlineStroke(GRID_LINES); } public void addLine(DataSet data) { if (chart == null) { throw new IllegalStateException("initChart() must be called first"); } for (DataDefinition dataDefinition : definition.getData()) { DataTupleXYDataset dataset = (DataTupleXYDataset) chart.getXYPlot().getDataset( dataDefinition.usesSecondaryYAxis() ? 1 : 0); addMatchingData(dataset, dataDefinition, data, definition.getLineNamingMode()); } updateChart(); } public void addLinesForData(DataDefinition definition, DataSet data, NamingMode lineNamingMode) { if (chart == null) { throw new IllegalStateException("initChart() must be called first"); } DataTupleXYDataset dataset = (DataTupleXYDataset) chart.getXYPlot().getDataset( definition.usesSecondaryYAxis() ? 1 : 0); addMatchingData(dataset, definition, data, lineNamingMode); updateChart(); } private void addMatchingData(DataTupleXYDataset dataset, DataDefinition definition, DataSet data, NamingMode lineNamingMode) { if (definition == null) { throw new IllegalArgumentException("LineChartDefintion cannot be null"); } if (definition.matchesHost(data)) { for (DataType type : definition.getMatchingTypes(data)) { List<String> fields = definition.getMatchingFields(type); List<String> fieldNames = new java.util.ArrayList<String>(fields.size()); for (String field : fields) { fieldNames.add(lineNamingMode.getName(definition, data, type, field, getGranularity())); } addData(dataset, data, type, fields, fieldNames); } } } private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, List<String> fields, List<String> fieldNames) { long start = System.nanoTime(); double[] totals = new double[fields.size()]; // use NaN as chart data when no values are defined rather than 0 java.util.Arrays.fill(totals, Double.NaN); int n = 0; long lastOutputTime = Math.max(getInterval().getStart(), data.getStartTime()); for (DataRecord record : data.getRecords(getInterval())) { if ((record != null) && record.hasData(type)) { for (int i = 0; i < fields.size(); i++) { if (type.hasField(fields.get(i))) { double value = record.getData(type, fields.get(i)); if (!Double.isNaN(value)) { if (Double.isNaN(totals[i])) { totals[i] = 0; } totals[i] += value; } } } ++n; } // else no data for this type at this time but may still need to output if ((n > 0) && ((record.getTime() - lastOutputTime) >= getGranularity())) { FixedMillisecond graphTime = new FixedMillisecond(record.getTime()); for (int i = 0; i < fields.size(); i++) { if (logger.isTraceEnabled()) { logger.trace(new java.util.Date(record.getTime()) + "\t" + type + "\t" + totals[i] + "\t" + totals[i] / n + "\t" + n + "\t" + (record.getTime() - lastOutputTime)); } if (!Double.isNaN(totals[i])) { // if the plot is listening for dataset changes, it will fire an event for // every data point // this causes a huge amount of GC and very slow response times so the false // value is important here dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); } totals[i] = Double.NaN; } lastOutputTime = record.getTime(); n = 0; } } // output final data point, if needed long endTime = data.getEndTime(); if (endTime != lastOutputTime) { FixedMillisecond graphTime = new FixedMillisecond(endTime); for (int i = 0; i < fields.size(); i++) { if (logger.isTraceEnabled()) { logger.trace(new java.util.Date(endTime) + "\t" + type + "\t" + totals[i] + "\t" + totals[i] / n + "\t" + n + "\t" + (endTime - lastOutputTime)); } if (!Double.isNaN(totals[i])) { dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); } } } // fieldName may not have been used if there was no data // so, search the dataset first before associating tuples for (int i = 0; i < dataset.getSeriesCount(); i++) { int idx = fieldNames.indexOf(dataset.getSeriesKey(i)); if (idx != -1) { dataset.associateTuple(fieldNames.get(idx), null, new DataTuple(data, type, fields.get(idx))); } } if (logger.isDebugEnabled()) { logger.debug("{}: {}-({} fields) added {} data points to chart '{}' in {}ms", data, type, fieldNames.size(), dataset.getItemCount(), definition.getTitle(), (System.nanoTime() - start) / 1000000.0d); } } private void updateChart() { recalculateGapThreshold(0); if (definition.hasSecondaryYAxis()) { recalculateGapThreshold(1); } chart.getXYPlot().configureRangeAxes(); if (chart.getLegend() == null) { int seriesCount = chart.getXYPlot().getDataset(0).getSeriesCount(); if (definition.hasSecondaryYAxis()) { seriesCount += chart.getXYPlot().getDataset(1).getSeriesCount(); } if (seriesCount > 1) { addLegend(); } } } private void recalculateGapThreshold(int datasetIndex) { if (definition.isStacked() && (datasetIndex == 0)) { return; } else { long start = System.nanoTime(); XYPlot plot = chart.getXYPlot(); if (plot.getDataset(datasetIndex).getItemCount(0) > 0) { DataTupleXYDataset dataset = (DataTupleXYDataset) plot.getDataset(datasetIndex); int seriesCount = dataset.getSeriesCount(); double[] averageDistance = new double[seriesCount]; int[] count = new int[seriesCount]; double[] previousX = new double[seriesCount]; java.util.Arrays.fill(averageDistance, 0); java.util.Arrays.fill(count, 0); java.util.Arrays.fill(previousX, dataset.getXValue(0, 0)); for (int i = 1; i < dataset.getItemCount(0); i++) { double currentX = dataset.getXValue(0, i); for (int j = 0; j < seriesCount; j++) { double y = dataset.getYValue(j, i); if (!Double.isNaN(y)) { averageDistance[j] += currentX - previousX[j]; previousX[j] = currentX; ++count[j]; } } } double maxAverage = Double.MIN_VALUE; for (int i = 0; i < seriesCount; i++) { averageDistance[i] /= count[i]; if (averageDistance[i] > maxAverage) { maxAverage = averageDistance[i]; } } ((StandardXYItemRenderer) plot.getRenderer(datasetIndex)).setGapThreshold(maxAverage * 1.25); } else { ((StandardXYItemRenderer) plot.getRenderer()).setGapThreshold(Integer.MAX_VALUE); } if (logger.isTraceEnabled()) { logger.trace("complete for chart '{}', series {} in {}ms", definition.getTitle(), datasetIndex, (System.nanoTime() - start) / 1000000.0d); } } } /** * Sets the X axis to display time relative to the given start time. */ // for relative time, format the x axis differently // the data _does not_ change public static void setRelativeAxis(JFreeChart chart, long startTime) { if (chart != null) { RelativeDateFormat format = new RelativeDateFormat(startTime); // : separators format.setHourSuffix(":"); format.setMinuteSuffix(":"); format.setSecondSuffix(""); // zero pad minutes and seconds DecimalFormat padded = new DecimalFormat("00"); format.setMinuteFormatter(padded); format.setSecondFormatter(padded); XYPlot plot = chart.getXYPlot(); ((DateAxis) plot.getDomainAxis()).setDateFormatOverride(format); } } /** * Sets the X axis to display absolute time. This is the default. * * @param chart */ public static void setAbsoluteAxis(JFreeChart chart) { if (chart != null) { XYPlot plot = chart.getXYPlot(); if (plot.getDomainAxis() instanceof DateAxis) { ((DateAxis) plot.getDomainAxis()).setDateFormatOverride(null); } } } /** * This only sets the first Y axis as a percent. There is no support for having other axes with * percent scales. */ public static void setPercentYAxis(JFreeChart chart) { NumberAxis yAxis = (NumberAxis) chart.getXYPlot().getRangeAxis(); yAxis.setRange(0, 100); } // customize tool tips on the graph to display the date time and the value private final XYToolTipGenerator tooltipGenerator = new XYToolTipGenerator() { private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss"); private final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#,##0.000"); @Override public String generateToolTip(XYDataset dataset, int series, int item) { return (dataset.getSeriesCount() > 1 ? dataset.getSeriesKey(series) + " " : "") + DATE_FORMAT.format(new java.util.Date((long) dataset.getXValue(series, item))) + " - " + NUMBER_FORMAT.format(dataset.getYValue(series, item)); } }; }