| /********************************************************************************* |
| * Copyright (c) 2020-2021 Robert Bosch GmbH and others. |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Robert Bosch GmbH - initial API and implementation |
| ******************************************************************************** |
| */ |
| |
| package org.eclipse.app4mc.amalthea.visualizations.standard; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueBetaDistribution; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueConstant; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueGaussDistribution; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueHistogram; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueHistogramEntry; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueInterval; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueStatistics; |
| import org.eclipse.app4mc.amalthea.model.ContinuousValueWeibullEstimatorsDistribution; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueBetaDistribution; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueConstant; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueGaussDistribution; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueHistogram; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueHistogramEntry; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueInterval; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueStatistics; |
| import org.eclipse.app4mc.amalthea.model.DiscreteValueWeibullEstimatorsDistribution; |
| import org.eclipse.app4mc.amalthea.model.IContinuousValueDeviation; |
| import org.eclipse.app4mc.amalthea.model.IDiscreteValueDeviation; |
| import org.eclipse.app4mc.amalthea.model.ITimeDeviation; |
| import org.eclipse.app4mc.amalthea.model.Time; |
| import org.eclipse.app4mc.amalthea.model.TimeBetaDistribution; |
| import org.eclipse.app4mc.amalthea.model.TimeConstant; |
| import org.eclipse.app4mc.amalthea.model.TimeGaussDistribution; |
| import org.eclipse.app4mc.amalthea.model.TimeHistogram; |
| import org.eclipse.app4mc.amalthea.model.TimeHistogramEntry; |
| import org.eclipse.app4mc.amalthea.model.TimeInterval; |
| import org.eclipse.app4mc.amalthea.model.TimeStatistics; |
| import org.eclipse.app4mc.amalthea.model.TimeUnit; |
| import org.eclipse.app4mc.amalthea.model.TimeWeibullEstimatorsDistribution; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.emf.ecore.EObject; |
| |
| import javafx.geometry.Pos; |
| import javafx.scene.chart.AreaChart; |
| import javafx.scene.chart.NumberAxis; |
| import javafx.scene.chart.XYChart; |
| import javafx.scene.chart.XYChart.Data; |
| import javafx.scene.chart.XYChart.Series; |
| import javafx.scene.control.Label; |
| import javafx.scene.layout.BorderPane; |
| import javafx.scene.paint.Color; |
| |
| public abstract class AbstractDeviationChart { |
| |
| private static final String CHART_SERIES_AREA_LINE = ".chart-series-area-line"; |
| private static final String CHART_SERIES_AREA_FILL = ".chart-series-area-fill"; |
| protected static final List<String> DEVIATION_LIST = Collections.unmodifiableList(Arrays.asList( |
| "Constant", "Histogram", "Boundaries", "Statistic", "Uniform", "Beta", "Gauss", "Weibull")); |
| |
| protected static final String ARROW = "\u2192"; |
| |
| protected AreaChart<Number, Number> addNewChart(BorderPane pane, EObject deviation, String unit) { |
| NumberAxis xAxis = new NumberAxis(); |
| String part1 = getXAxisType(deviation, "values"); |
| String part2 = (unit == null || unit.isEmpty()) ? "" : " [" + unit + "]"; |
| xAxis.setLabel(part1 + part2); |
| xAxis.setForceZeroInRange(false); |
| xAxis.setMinorTickVisible(false); |
| |
| NumberAxis yAxis = new NumberAxis(); |
| yAxis.setLabel("density"); |
| yAxis.setForceZeroInRange(false); |
| yAxis.setTickMarkVisible(false); |
| yAxis.setTickLabelsVisible(false); |
| yAxis.setMinorTickVisible(false); |
| |
| AreaChart<Number, Number> chart = new AreaChart<>(xAxis, yAxis); |
| chart.setTitle(getChartTitle(deviation)); |
| chart.setAnimated(false); |
| chart.setVerticalGridLinesVisible(false); |
| chart.setHorizontalGridLinesVisible(false); |
| chart.setLegendVisible(false); |
| chart.lookup(".chart-vertical-zero-line").setStyle("-fx-stroke: rgba(0, 0, 0, 0.0);"); |
| |
| pane.setCenter(chart); |
| return chart; |
| } |
| |
| private String getChartTitle(EObject deviation) { |
| if (deviation != null) { |
| String className = deviation.eClass().getName(); |
| for (String genericName : DEVIATION_LIST) { |
| if (className.contains(genericName)) return genericName; |
| } |
| } |
| return null; |
| } |
| |
| private String getXAxisType(EObject deviation, String defaultText) { |
| if (deviation != null && deviation.eContainer()!= null && deviation.eContainingFeature() != null) { |
| String containerClassName = deviation.eContainer().eClass().getName(); |
| String featureName = deviation.eContainingFeature().getName(); |
| if (containerClassName.contains("Ticks")) { |
| return "ticks"; |
| } |
| if (deviation.eClass().getName().contains("Time")) { |
| return "time"; |
| } |
| if (featureName.contains("Latency")) { |
| return "cycles"; |
| } |
| if (featureName.contains("occurrences")) { |
| return "occurrences"; |
| } |
| } |
| |
| return defaultText; |
| } |
| |
| protected Label addNewStatus(BorderPane pane, String status) { |
| // add status line for warnings |
| if (status != null) { |
| Label statusLabel = new Label(status); |
| statusLabel.setMaxWidth(Double.MAX_VALUE); |
| statusLabel.setAlignment(Pos.CENTER); |
| statusLabel.setTextFill(Color.RED); |
| |
| pane.setBottom(statusLabel); |
| return statusLabel; |
| } |
| return null; |
| } |
| |
| protected void setChartXBounds(AreaChart<Number, Number> chart, double lowerBound, double upperBound) { |
| if (lowerBound >= upperBound) return; |
| |
| if (chart.getXAxis() instanceof NumberAxis) { |
| NumberAxis axis = (NumberAxis) chart.getXAxis(); |
| |
| // ValueAxis<Number> allows to set the bounds |
| axis.setAutoRanging(false); |
| axis.setLowerBound(lowerBound); |
| axis.setUpperBound(upperBound); |
| axis.setMinorTickCount(0); |
| axis.setTickUnit((upperBound - lowerBound) / 5); |
| } |
| } |
| |
| protected void setChartYBounds(AreaChart<Number, Number> chart, double upperBound) { |
| if (chart.getYAxis() instanceof NumberAxis) { |
| NumberAxis axis = (NumberAxis) chart.getYAxis(); |
| |
| // ValueAxis<Number> allows to set the bounds |
| axis.setAutoRanging(false); |
| axis.setLowerBound(0); |
| axis.setUpperBound(upperBound); |
| } |
| } |
| |
| protected void addSeriesStandard(AreaChart<Number, Number> chart, final Series<Number, Number> series) { |
| if (series.getData().isEmpty()) return; |
| |
| chart.getData().add(series); |
| |
| // set standard color for line and fill |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(0, 128, 0, 1.0);"); |
| series.getNode().lookup(CHART_SERIES_AREA_FILL).setStyle("-fx-fill: rgba(0, 128, 0, 0.15);"); |
| |
| // hide line markers |
| for (Data<Number, Number> data : series.getData()) { |
| data.getNode().setVisible(false); |
| } |
| } |
| |
| protected void addSeriesOffLimit(AreaChart<Number, Number> chart, final Series<Number, Number> series) { |
| if (series.getData().isEmpty()) return; |
| |
| chart.getData().add(series); |
| |
| // set color and dash style for line; no fill |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(128, 128, 128, 1.0); -fx-stroke-dash-array: 2;"); |
| series.getNode().lookup(CHART_SERIES_AREA_FILL).setStyle("-fx-fill: rgba(0, 0, 0, 0.0);"); |
| |
| // hide line markers |
| for (Data<Number, Number> data : series.getData()) { |
| data.getNode().setVisible(false); |
| } |
| } |
| |
| protected void addSeriesGradient(AreaChart<Number, Number> chart, final Series<Number, Number> series) { |
| if (series.getData().isEmpty()) return; |
| |
| chart.getData().add(series); |
| |
| // set color and dash style for line; no fill |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(0, 0, 0, 0.0);"); |
| series.getNode().lookup(CHART_SERIES_AREA_FILL).setStyle("-fx-fill: linear-gradient(to top, rgba(0, 128, 0, 0.3), rgba(0, 128, 0, 0.0));"); |
| |
| // hide line markers |
| for (Data<Number, Number> data : series.getData()) { |
| data.getNode().setVisible(false); |
| } |
| } |
| |
| protected void addSinglePeek(AreaChart<Number, Number> chart, double y, double x) { |
| if (chart.getYAxis() instanceof NumberAxis) { |
| final Series<Number, Number> series = new XYChart.Series<>(); |
| chart.getData().add(series); |
| series.getData().add(new XYChart.Data<>(x, 0.0)); |
| series.getData().add(new XYChart.Data<>(x, y)); |
| |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(0, 128, 0, 1.0);"); |
| series.getData().get(1).getNode().setVisible(false); |
| } |
| } |
| |
| protected void addMarkers(AreaChart<Number, Number> chart, double y, Double min, Double avg, Double max) { |
| if (chart.getYAxis() instanceof NumberAxis) { |
| // add lower bounds marker |
| if (min != null) { |
| final Series<Number, Number> series = new XYChart.Series<>(); |
| chart.getData().add(series); |
| series.setName("min"); |
| series.getData().add(new XYChart.Data<>(min, 0.0)); |
| series.getData().add(new XYChart.Data<>(min, y)); |
| |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(0, 0, 0, 1.0);"); |
| series.getData().get(1).getNode().setVisible(false); |
| } |
| |
| // add upper bounds marker |
| if (max != null) { |
| final Series<Number, Number> series = new XYChart.Series<>(); |
| chart.getData().add(series); |
| series.setName("max"); |
| series.getData().add(new XYChart.Data<>(max, 0.0)); |
| series.getData().add(new XYChart.Data<>(max, y)); |
| |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(0, 0, 0, 1.0);"); |
| series.getData().get(1).getNode().setVisible(false); |
| } |
| |
| // add average marker if computed average is in correct bounds (skip otherwise) |
| if (avg != null && (min == null || min <= avg) && (max == null || max >= avg)) { |
| final Series<Number, Number> series = new XYChart.Series<>(); |
| chart.getData().add(series); |
| series.setName("avg"); |
| series.getData().add(new XYChart.Data<>(avg, 0.0)); |
| series.getData().add(new XYChart.Data<>(avg, y)); |
| |
| series.getNode().lookup(CHART_SERIES_AREA_LINE).setStyle("-fx-stroke: rgba(150, 5, 5, 1.0); -fx-stroke-dash-array: 3;"); |
| series.getData().get(1).getNode().setVisible(false); |
| } |
| } |
| } |
| |
| |
| // ***** Some checks ***** |
| |
| public boolean isValid(IDiscreteValueDeviation dev) { |
| // check constant value |
| if (dev instanceof DiscreteValueConstant) { |
| return true; |
| } |
| |
| // check histogram entries |
| if (dev instanceof DiscreteValueHistogram) { |
| final EList<DiscreteValueHistogramEntry> entries = ((DiscreteValueHistogram) dev).getEntries(); |
| if (entries.isEmpty()) return false; |
| |
| for (DiscreteValueHistogramEntry entry : entries) { |
| final Long lowerBound = entry.getLowerBound(); |
| final Long upperBound = entry.getUpperBound(); |
| if (lowerBound == null) return false; |
| if (upperBound == null) return false; |
| if (lowerBound.compareTo(upperBound) > 0) return false; |
| if (entry.getOccurrences() < 0) return false; |
| } |
| return true; |
| } |
| |
| // check attributes of (abstract) interval; check average if available |
| if (dev instanceof DiscreteValueInterval) { |
| final DiscreteValueInterval interval = (DiscreteValueInterval) dev; |
| final Long lowerBound = interval.getLowerBound(); |
| final Long upperBound = interval.getUpperBound(); |
| if (lowerBound == null) return false; |
| if (upperBound == null) return false; |
| if (!isValidMinAvgMax(lowerBound.doubleValue(), getAverage(interval), upperBound.doubleValue())) return false; |
| } |
| |
| // check Weibull parameters |
| if (dev instanceof DiscreteValueWeibullEstimatorsDistribution) { |
| final DiscreteValueWeibullEstimatorsDistribution weibullDist = (DiscreteValueWeibullEstimatorsDistribution) dev; |
| final double promille = weibullDist.getPRemainPromille(); |
| return promille > 0 && promille < 1000; |
| } |
| |
| // check Beta parameters |
| if (dev instanceof DiscreteValueBetaDistribution) { |
| final DiscreteValueBetaDistribution betaDist = (DiscreteValueBetaDistribution) dev; |
| return betaDist.getAlpha() > 0 && betaDist.getBeta() > 0; |
| } |
| |
| // check Gauss parameters |
| if (dev instanceof DiscreteValueGaussDistribution) { |
| final DiscreteValueGaussDistribution gaussDist = (DiscreteValueGaussDistribution) dev; |
| if (gaussDist.getSd() <= 0) return false; |
| if (!isValidMinAvgMax(gaussDist.getLowerBound(), null, gaussDist.getUpperBound())) return false; |
| } |
| |
| return true; |
| } |
| |
| public boolean isValid(IContinuousValueDeviation dev) { |
| // check constant value |
| if (dev instanceof ContinuousValueConstant) { |
| return true; |
| } |
| |
| // check histogram entries |
| if (dev instanceof ContinuousValueHistogram) { |
| final EList<ContinuousValueHistogramEntry> entries = ((ContinuousValueHistogram) dev).getEntries(); |
| if (entries.isEmpty()) return false; |
| |
| for (ContinuousValueHistogramEntry entry : entries) { |
| final Double lowerBound = entry.getLowerBound(); |
| final Double upperBound = entry.getUpperBound(); |
| if (lowerBound == null) return false; |
| if (upperBound == null) return false; |
| if (lowerBound.compareTo(upperBound) > 0) return false; |
| if (entry.getOccurrences() < 0) return false; |
| } |
| return true; |
| } |
| |
| // check attributes of (abstract) interval; check average if available |
| if (dev instanceof ContinuousValueInterval) { |
| final ContinuousValueInterval interval = (ContinuousValueInterval) dev; |
| final Double lowerBound = interval.getLowerBound(); |
| final Double upperBound = interval.getUpperBound(); |
| if (lowerBound == null) return false; |
| if (upperBound == null) return false; |
| if (!isValidMinAvgMax(lowerBound, getAverage(interval), upperBound)) return false; |
| } |
| |
| // check Weibull parameters |
| if (dev instanceof ContinuousValueWeibullEstimatorsDistribution) { |
| final ContinuousValueWeibullEstimatorsDistribution weibullDist = (ContinuousValueWeibullEstimatorsDistribution) dev; |
| final double promille = weibullDist.getPRemainPromille(); |
| return promille > 0 && promille < 1000; |
| } |
| |
| // check Beta parameters |
| if (dev instanceof ContinuousValueBetaDistribution) { |
| final ContinuousValueBetaDistribution betaDist = (ContinuousValueBetaDistribution) dev; |
| return betaDist.getAlpha() > 0 && betaDist.getBeta() > 0; |
| } |
| |
| // check Gauss parameters |
| if (dev instanceof ContinuousValueGaussDistribution) { |
| final ContinuousValueGaussDistribution gaussDist = (ContinuousValueGaussDistribution) dev; |
| if (gaussDist.getSd() <= 0) return false; |
| if (!isValidMinAvgMax(gaussDist.getLowerBound(), null, gaussDist.getUpperBound())) return false; |
| } |
| |
| return true; |
| } |
| |
| public boolean isValid(ITimeDeviation dev) { |
| // check constant value |
| if (dev instanceof TimeConstant) { |
| return isValidTime(((TimeConstant) dev).getValue()); |
| } |
| |
| // check histogram entries |
| if (dev instanceof TimeHistogram) { |
| final EList<TimeHistogramEntry> entries = ((TimeHistogram) dev).getEntries(); |
| if (entries.isEmpty()) return false; |
| |
| for (TimeHistogramEntry entry : entries) { |
| final Time lowerBound = entry.getLowerBound(); |
| final Time upperBound = entry.getUpperBound(); |
| if (!isValidTime(lowerBound)) return false; |
| if (!isValidTime(upperBound)) return false; |
| if (lowerBound.compareTo(upperBound) > 0) return false; |
| if (entry.getOccurrences() < 0) return false; |
| } |
| return true; |
| } |
| |
| // check Statistics parameters (part 1) |
| if (dev instanceof TimeStatistics) { |
| final TimeStatistics statistics = (TimeStatistics) dev; |
| if (!isValidTime(statistics.getAverage())) return false; |
| } |
| |
| // check Weibull parameters (part 1) |
| if (dev instanceof TimeWeibullEstimatorsDistribution) { |
| final TimeWeibullEstimatorsDistribution weibullDist = (TimeWeibullEstimatorsDistribution) dev; |
| if (!isValidTime(weibullDist.getAverage())) return false; |
| final double promille = weibullDist.getPRemainPromille(); |
| if (promille <= 0) return false; |
| if (promille >= 1000) return false; |
| } |
| |
| // check attributes of (abstract) interval; check average if available |
| if (dev instanceof TimeInterval) { |
| final TimeInterval interval = (TimeInterval) dev; |
| final Time lowerBound = interval.getLowerBound(); |
| final Time upperBound = interval.getUpperBound(); |
| if (!isValidTime(lowerBound)) return false; |
| if (!isValidTime(upperBound)) return false; |
| if (!isValidMinAvgMax(lowerBound, getAverage(interval), upperBound)) return false; |
| } |
| |
| // check Beta parameters |
| if (dev instanceof TimeBetaDistribution) { |
| final TimeBetaDistribution betaDist = (TimeBetaDistribution) dev; |
| return betaDist.getAlpha() > 0 && betaDist.getBeta() > 0; |
| } |
| |
| // check Gauss parameters |
| if (dev instanceof TimeGaussDistribution) { |
| final TimeGaussDistribution gaussDist = (TimeGaussDistribution) dev; |
| final Time mean = gaussDist.getMean(); |
| final Time sd = gaussDist.getSd(); |
| |
| if (!isValidTime(mean)) return false; |
| if (!isValidTime(sd)) return false; |
| if (sd.getValue().signum() < 1) return false; |
| if (!isValidMinAvgMax(gaussDist.getLowerBound(), null, gaussDist.getUpperBound())) return false; |
| } |
| |
| return true; |
| } |
| |
| public boolean isValidTime(Time time) { |
| return (time != null && time.getValue() != null && time.getUnit() != TimeUnit._UNDEFINED_); |
| } |
| |
| // ***** Some utility functions ***** |
| |
| public <T extends Comparable<T>> boolean isValidMinAvgMax(T min, T avg, T max) { |
| return isSinglePeek(min, max) || isValidRange(min, avg, max); |
| } |
| |
| public <T extends Comparable<T>> boolean isSinglePeek(T min, T max) { |
| return min != null && max != null && min.compareTo(max) == 0; |
| } |
| |
| public <T extends Comparable<T>> boolean isValidRange(T min, T avg, T max) { |
| if (min != null && max != null && min.compareTo(max) >= 0) { |
| return false; |
| } |
| if (min != null && avg != null && min.compareTo(avg) >= 0) { |
| return false; |
| } |
| if (avg != null && max != null && avg.compareTo(max) >= 0) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| public Time getAverage(TimeInterval obj) { |
| Time avg = null; |
| if (obj instanceof TimeStatistics) { |
| avg = ((TimeStatistics) obj).getAverage(); |
| } |
| if (obj instanceof TimeWeibullEstimatorsDistribution) { |
| avg = ((TimeWeibullEstimatorsDistribution) obj).getAverage(); |
| } |
| return avg; |
| } |
| |
| public Double getAverage(DiscreteValueInterval obj) { |
| Double avg = null; |
| if (obj instanceof DiscreteValueStatistics) { |
| avg = ((DiscreteValueStatistics) obj).getAverage(); |
| } |
| if (obj instanceof DiscreteValueWeibullEstimatorsDistribution) { |
| avg = ((DiscreteValueWeibullEstimatorsDistribution) obj).getAverage(); |
| } |
| return avg; |
| } |
| |
| public Double getAverage(ContinuousValueInterval obj) { |
| Double avg = null; |
| if (obj instanceof ContinuousValueStatistics) { |
| avg = ((ContinuousValueStatistics) obj).getAverage(); |
| } |
| if (obj instanceof ContinuousValueWeibullEstimatorsDistribution) { |
| avg = ((ContinuousValueWeibullEstimatorsDistribution) obj).getAverage(); |
| } |
| return avg; |
| } |
| |
| } |