/* * RHQ Management Platform * Copyright (C) 2005-2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.coregui.client.inventory.common.graph; import java.util.ArrayList; import java.util.List; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementUnits; import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite; import org.rhq.core.domain.measurement.composite.MeasurementNumericValueAndUnits; import org.rhq.core.domain.measurement.composite.MeasurementOOBComposite; import org.rhq.core.domain.util.PageList; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.JsonMetricProducer; import org.rhq.coregui.client.Messages; import org.rhq.coregui.client.inventory.common.detail.summary.AbstractActivityView; import org.rhq.coregui.client.inventory.common.graph.graphtype.StackedBarMetricGraphImpl; import org.rhq.coregui.client.util.Log; import org.rhq.coregui.client.util.MeasurementConverterClient; /** * The data portion of the graphs making these methods accessible via JSNI to * classes extending this and implementing graphs. * Provide i18n labels and format the json data for the graph. * @see AbstractMetricGraph * * @author Mike Thompson */ public class MetricGraphData implements JsonMetricProducer { private static final Integer DEFAULT_CHART_HEIGHT = 200; // i18n final protected Messages MSG = CoreGUI.getMessages(); private final String chartTitleMinLabel = MSG.chart_title_min_label(); private final String chartTitleAvgLabel = MSG.chart_title_avg_label(); private final String chartTitlePeakLabel = MSG.chart_title_peak_label(); private final String chartDateLabel = MSG.chart_date_label(); private final String chartTimeLabel = MSG.chart_time_label(); private final String chartDownLabel = MSG.chart_down_label(); private final String chartUnknownLabel = MSG.chart_unknown_label(); private final String chartNoDataLabel = MSG.chart_no_data_label(); private final String chartSingleValueLabel = MSG.chart_single_value_label(); private final String chartHoverStartLabel = MSG.chart_hover_start_label(); private final String chartHoverEndLabel = MSG.common_title_end(); private final String chartHoverPeriodLabel = MSG.chart_hover_period_label(); private final String chartHoverBarLabel = MSG.chart_hover_bar_label(); private final String chartHoverTimeFormat = MSG.chart_hover_time_format(); private final String chartHoverDateFormat = MSG.chart_hover_date_format(); private int entityId = 0; private String entityName; private int definitionId; private int portalId; private MeasurementUnits adjustedMeasurementUnits; private MeasurementDefinition definition; private List<MeasurementDataNumericHighLowComposite> metricData; private PageList<MeasurementOOBComposite> measurementOOBCompositeList; private MeasurementOOBComposite lastOOB; private Integer chartHeight; private boolean isPortalGraph; private boolean isSummaryGraph; private boolean hideLegend; private double average = 0.0; private double min = Double.MAX_VALUE; private double max = Double.MIN_VALUE; private MetricGraphData(int portalId) { isPortalGraph = true; this.portalId = portalId; } public static MetricGraphData createForDashboard(int portalId) { return new MetricGraphData(portalId); } private MetricGraphData(int entityId, String entityName, MeasurementDefinition def, List<MeasurementDataNumericHighLowComposite> metricData) { this.entityName = entityName; setEntityId(entityId); setDefinitionId(def.getId()); this.definition = def; this.metricData = metricData; this.isPortalGraph = false; calcMinMaxAvg(metricData); } public static MetricGraphData createForResourceGroup(int groupId, String groupName, MeasurementDefinition def, List<MeasurementDataNumericHighLowComposite> metricData) { return new MetricGraphData(groupId, groupName, def, metricData); } private MetricGraphData(int entityId, String entityName, MeasurementDefinition measurementDef, List<MeasurementDataNumericHighLowComposite> metricData, PageList<MeasurementOOBComposite> measurementOOBCompositeList, boolean summaryGraph) { this.entityName = entityName; setEntityId(entityId); setDefinitionId(measurementDef.getId()); this.definition = measurementDef; this.metricData = metricData; this.measurementOOBCompositeList = measurementOOBCompositeList; this.isPortalGraph = false; this.isSummaryGraph = summaryGraph; calcMinMaxAvg(metricData); } public static MetricGraphData createForResource(int resourceId, String resourceName, MeasurementDefinition measurementDef, List<MeasurementDataNumericHighLowComposite> metrics, PageList<MeasurementOOBComposite> measurementOOBCompositeList) { return new MetricGraphData(resourceId, resourceName, measurementDef, metrics, measurementOOBCompositeList, false); } public static MetricGraphData createForResourceSummary(int resourceId, String resourceName, MeasurementDefinition measurementDef, List<MeasurementDataNumericHighLowComposite> metrics, PageList<MeasurementOOBComposite> measurementOOBCompositeList) { return new MetricGraphData(resourceId, resourceName, measurementDef, metrics, measurementOOBCompositeList, true); } private void calcMinMaxAvg(List<MeasurementDataNumericHighLowComposite> metricData) { int averageCount = 0; for (MeasurementDataNumericHighLowComposite measurement : metricData) { if (!Double.isNaN(measurement.getLowValue())) { min = Math.min(min, measurement.getLowValue()); } if (!Double.isNaN(measurement.getHighValue())) { max = Math.max(max, measurement.getHighValue()); } if (!Double.isNaN(measurement.getValue())) { average = average + measurement.getValue(); averageCount++; } } average = average / averageCount; } public int getEntityId() { return this.entityId; } public void setEntityId(int entityId) { this.entityId = entityId; } public void setEntityName(String entityName) { this.entityName = entityName; } public int getDefinitionId() { return definitionId; } public void setDefinitionId(int definitionId) { this.definitionId = definitionId; } public MeasurementDefinition getDefinition() { return definition; } public void setDefinition(MeasurementDefinition definition) { this.definition = definition; } public String getChartId() { if (isPortalGraph) { if (definition != null) { return entityId + "-" + definition.getId(); } else { // case when portlet has not been configured yet return ""; } } else { return entityId + "-" + definitionId; } } public void setMetricData(List<MeasurementDataNumericHighLowComposite> metricData) { this.metricData = metricData; } public String getChartTitleMinLabel() { return chartTitleMinLabel; } public String getChartTitleAvgLabel() { return chartTitleAvgLabel; } public String getChartTitlePeakLabel() { return chartTitlePeakLabel; } public String getChartAverage() { if (average != 0.0) { return AbstractActivityView.convertLastValueForDisplay(average, definition); } else { return ""; } } public String getChartMax() { if (max != Double.MIN_VALUE) { return AbstractActivityView.convertLastValueForDisplay(max, definition); } else { return ""; } } public String getChartMin() { if (min != Double.MAX_VALUE) { return AbstractActivityView.convertLastValueForDisplay(min, definition); } else { return ""; } } public String getChartDateLabel() { return chartDateLabel; } public String getChartDownLabel() { return chartDownLabel; } public String getChartTimeLabel() { return chartTimeLabel; } public String getChartUnknownLabel() { return chartUnknownLabel; } public String getChartNoDataLabel() { return chartNoDataLabel; } public String getChartSingleValueLabel() { return chartSingleValueLabel; } public String getChartHoverStartLabel() { return chartHoverStartLabel; } public String getChartHoverEndLabel() { return chartHoverEndLabel; } public String getChartHoverPeriodLabel() { return chartHoverPeriodLabel; } public String getChartHoverBarLabel() { return chartHoverBarLabel; } public String getChartHoverTimeFormat() { return chartHoverTimeFormat; } public String getChartHoverDateFormat() { return chartHoverDateFormat; } public int getPortalId() { return portalId; } public Integer getChartHeight() { if (null != chartHeight) { return chartHeight; } else { return DEFAULT_CHART_HEIGHT; } } public void setChartHeight(Integer chartHeight) { this.chartHeight = chartHeight; } public boolean isPortalGraph() { return isPortalGraph; } public boolean isHideLegend() { return hideLegend; } public void setHideLegend(boolean hideLegend) { this.hideLegend = hideLegend; } public String getChartTitle() { if (definition != null) { return (entityName == null) ? definition.getDisplayName() : entityName + " - " + definition.getDisplayName(); } else { // handle case when dashboard portlet has not been configured yet. return ""; } } /** * Returns the y-axis units normalized to highest scale (Bytes -> Gb). * NOTE: this requires a dependency such that getJsonMetrics is called * before this method as the adjustedMeasurementUnits are calculated in that method. * @return yAxisUnits -- normalized to highest UOM. */ public String getYAxisUnits() { if (adjustedMeasurementUnits == null) { Log.warn("ResourceMetricD3GraphView.adjustedMeasurementUnits is populated by getJsonMetrics. Make sure it is called first."); return ""; } else { return adjustedMeasurementUnits.toString(); } } public String getXAxisTitle() { return MSG.view_charts_time_axis_label(); } public String getXAxisTimeFormatHours() { return MSG.chart_xaxis_time_format_hours(); } public String getXAxisTimeFormatHoursMinutes() { return MSG.chart_xaxis_time_format_hours_minutes(); } @Override /** * Format the json for the front JSNI(javascript) UI to consume. * @todo: future: this should really use GSON or some Json marshaller */ public String getJsonMetrics() { StringBuilder sb = new StringBuilder(); if (null != metricData) { sb = new StringBuilder("["); long firstBarTime = metricData.get(0).getTimestamp(); long secondBarTime = metricData.get(1).getTimestamp(); long barDuration = secondBarTime - firstBarTime; String barDurationString = MeasurementConverterClient.format((double) barDuration, MeasurementUnits.MILLISECONDS, true); calculateOOB(); // find the lowest value and use it's UOM to translated everything else into MeasurementDataNumericHighLowComposite lowestValue = null; for (MeasurementDataNumericHighLowComposite measurement : metricData) { if (!Double.isNaN(measurement.getValue())) { if (null == lowestValue) { lowestValue = measurement; } if (measurement.getLowValue() < lowestValue.getLowValue()) { lowestValue = measurement; } } } if (null != lowestValue) { MeasurementNumericValueAndUnits adjustedMeasurementUnitsAndValue = MeasurementConverterClient.fit( lowestValue.getLowValue(), definition.getUnits()); adjustedMeasurementUnits = adjustedMeasurementUnitsAndValue.getUnits(); } else { adjustedMeasurementUnits = definition.getUnits(); } for (MeasurementDataNumericHighLowComposite measurement : metricData) { sb.append("{ \"x\":" + measurement.getTimestamp() + ","); if (null != lastOOB) { sb.append(" \"baselineMin\":" + lastOOB.getBlMin() + ", "); sb.append(" \"baselineMax\":" + lastOOB.getBlMax() + ", "); } if (!Double.isNaN(measurement.getValue())) { Double newHigh = normalizeUnitsAndValues( new MeasurementNumericValueAndUnits(measurement.getHighValue(), definition.getUnits()), adjustedMeasurementUnits); Double newAvg = normalizeUnitsAndValues(new MeasurementNumericValueAndUnits(measurement.getValue(), definition.getUnits()), adjustedMeasurementUnits); Double newLow = normalizeUnitsAndValues( new MeasurementNumericValueAndUnits(measurement.getLowValue(), definition.getUnits()), adjustedMeasurementUnits); sb.append(" \"barDuration\": \"" + barDurationString + "\", "); sb.append(" \"high\":" + newHigh + ","); sb.append(" \"highFormatted\":\"" + MeasurementConverterClient.format(measurement.getHighValue(), definition.getUnits(), true, 0, 3) + "\","); sb.append(" \"low\":" + newLow + ","); sb.append(" \"lowFormatted\":\"" + MeasurementConverterClient.format( measurement.getLowValue(), definition.getUnits(), true, 0, 3) + "\","); sb.append(" \"avg\":" + newAvg + ","); sb.append(" \"avgFormatted\":\"" + MeasurementConverterClient.format(measurement.getValue(), definition.getUnits(), true, 0, 3) + "\"},"); } else { // give it some values so that we dont have NaN sb.append(" \"high\":0,"); sb.append(" \"low\":0,"); sb.append(" \"avg\":0,"); sb.append(" \"nodata\":true },"); } if (!sb.toString().endsWith("},")) { sb.append(" },"); } } sb.setLength(sb.length() - 1); // delete the last ',' sb.append("]"); } Log.debug("Json data for: " + getChartTitle()); Log.debug(sb.toString()); return sb.toString(); } public static Double normalizeUnitsAndValues(MeasurementNumericValueAndUnits valueAndUnits, MeasurementUnits targetUnits) { if (valueAndUnits.getUnits() == MeasurementUnits.PERCENTAGE) { return valueAndUnits.getValue() * 100; } Double multiplier = MeasurementUnits.calculateOffset(valueAndUnits.getUnits(), targetUnits); return valueAndUnits.getValue() * multiplier; } /** * This is cleaning the data as sometimes the data coming from the metric query * is erroneous: for instance the low is greater than the high. This causes the * geometries to get weird. We normally should not have to do this! * @todo: Remove this data cleansing once we have fixed it at the metric query. * * @param low supposed low value * @param high supposed high value * @return the real low value */ private Double cleanseLow(Double low, Double avg, Double high) { double highLowMin = Math.min(low, high); return Math.min(highLowMin, avg); } /** * When the scale is adjusted down to a level where bars no longer have * highs and lows different from the avg value then have probably hit a * scale where aggregates no longer exist and we are looking the individual * values. At the very least the data is less interesting and a trendline * connecting the points has less meaning because we are essentially a scatterplot. * A different algorithm could be used here but this will suffice. * @return true if the graphs should show the bar avg line meaning there is aggregates in the data * @see StackedBarMetricGraphImpl */ public boolean showBarAvgTrendLine() { int numberOfAggBars = 0; for (MeasurementDataNumericHighLowComposite measurement : metricData) { boolean noValuesInCurrentBarUndefined = (!Double.isNaN(measurement.getValue()) && !Double.isNaN(measurement.getHighValue()) && !Double.isNaN(measurement.getLowValue())); boolean foundAggregateBar = (measurement.getValue() != measurement.getHighValue() || measurement .getHighValue() != measurement.getLowValue()); // if there exists a even one aggregate bar then I can short circuit this and exit if (noValuesInCurrentBarUndefined && foundAggregateBar) { numberOfAggBars++; if (numberOfAggBars > 4) { return true; } } } return false; } public boolean isSummaryGraph() { return isSummaryGraph; } private void calculateOOB() { if (measurementOOBCompositeList != null && !measurementOOBCompositeList.isEmpty()) { Log.debug("OOB List size: " + measurementOOBCompositeList.size()); List<MeasurementOOBComposite> selectedOOBs = new ArrayList<MeasurementOOBComposite>(); for (MeasurementOOBComposite measurementOOBComposite : measurementOOBCompositeList) { Log.debug("measurementOOBComposite = " + measurementOOBComposite); if (measurementOOBComposite.getDefinitionId() == definitionId) { selectedOOBs.add(measurementOOBComposite); } } // take the last one (most current) matching the defId lastOOB = selectedOOBs.isEmpty() ? null : selectedOOBs.get(selectedOOBs.size() - 1); } else { lastOOB = null; } } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("MetricGraphData"); sb.append("{chartTitleMinLabel='").append(chartTitleMinLabel).append('\''); sb.append(", chartTitleAvgLabel='").append(chartTitleAvgLabel).append('\''); sb.append(", chartTitlePeakLabel='").append(chartTitlePeakLabel).append('\''); sb.append(", chartDateLabel='").append(chartDateLabel).append('\''); sb.append(", chartTimeLabel='").append(chartTimeLabel).append('\''); sb.append(", chartDownLabel='").append(chartDownLabel).append('\''); sb.append(", chartUnknownLabel='").append(chartUnknownLabel).append('\''); sb.append(", chartNoDataLabel='").append(chartNoDataLabel).append('\''); sb.append(", chartHoverStartLabel='").append(chartHoverStartLabel).append('\''); sb.append(", chartHoverEndLabel='").append(chartHoverEndLabel).append('\''); sb.append(", chartHoverPeriodLabel='").append(chartHoverPeriodLabel).append('\''); sb.append(", chartHoverBarLabel='").append(chartHoverBarLabel).append('\''); sb.append(", chartHoverTimeFormat='").append(chartHoverTimeFormat).append('\''); sb.append(", chartHoverDateFormat='").append(chartHoverDateFormat).append('\''); sb.append(", entityId=").append(entityId); sb.append(", entityName='").append(entityName).append('\''); sb.append(", definitionId=").append(definitionId); sb.append(", isPortalGraph=").append(isPortalGraph); sb.append('}'); return sb.toString(); } }