import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
+import java.math.BigDecimal;
import java.text.Format;
import java.util.ArrayList;
import java.util.HashSet;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry;
-import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTimeStampFormat;
+import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiDecimalUnitFormat;
+import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiTimeStampFormat;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
import org.swtchart.Chart;
import org.swtchart.ITitle;
*/
protected static final String UNKNOWN = "?"; //$NON-NLS-1$
- /** Zero value */
- protected static final double ZERO = 0.0;
-
- /** Symbol for seconds (used in the custom ns -> s conversion) */
- private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$
-
- /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
- private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$
+ /** Zero long value */
+ protected static final long ZERO_LONG = 0L;
+ /** Zero double value */
+ protected static final double ZERO_DOUBLE = 0.0;
/**
* Function to use to map Strings read from the data table to doubles for
*/
protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> {
if (str == null || str.equals(UNKNOWN)) {
- return ZERO;
+ return ZERO_LONG;
}
return Double.parseDouble(str);
};
/**
* Decimal formatter to display nanoseconds as seconds.
*/
- protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new DecimalUnitFormat(0.000000001);
+ protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new LamiDecimalUnitFormat(0.000000001);
/**
* Default decimal formatter.
*/
- protected static final DecimalUnitFormat DECIMAL_FORMATTER = new DecimalUnitFormat();
+ protected static final DecimalUnitFormat DECIMAL_FORMATTER = new LamiDecimalUnitFormat();
+
+ /** Symbol for seconds (used in the custom ns -> s conversion) */
+ private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$
+
+ /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
+ private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$
+
+ /** Maximum amount of digits that can be represented into a double */
+ private static final int BIG_DECIMAL_DIVISION_SCALE = 22;
private final Listener fResizeListener = event -> {
/* Refresh the titles to fit the current chart size */
private final Chart fChart;
private final String fChartTitle;
- private final String fXTitle;
- private final String fYTitle;
+
+ private String fXLabel;
+ private @Nullable String fXUnits;
+
+ private String fYLabel;
+ private @Nullable String fYUnits;
private boolean fSelected;
private Set<Integer> fSelection;
+ private final ToolBar fToolBar;
+
/**
* Creates a Viewer instance based on SWTChart.
*
fChartModel = chartModel;
fSelection = new HashSet<>();
+ fXLabel = ""; //$NON-NLS-1$
+ fYLabel = ""; //$NON-NLS-1$
+
fChart = new Chart(parent, SWT.NONE);
fChart.addListener(SWT.Resize, fResizeListener);
if (fChartModel.getXSeriesColumns().size() == 1) {
/*
* There is only 1 series in the chart, we will use its name as the
- * Y axis (and hide the legend).
+ * X axis.
*/
- String seriesName = getChartModel().getXSeriesColumns().get(0);
- // The time duration formatter converts ns to s on the axis
- if (NANOSECONDS_SYMBOL.equals(getXAxisAspects().get(0).getUnits())) {
- seriesName = getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$
- }
- fXTitle = seriesName;
+ innerSetXTitle(getXAxisAspects().get(0).getName(), getXAxisAspects().get(0).getUnits());
} else {
/*
* There are multiple series in the chart, if they all share the same
* units, display that.
*/
- long nbDiffAspects = getXAxisAspects().stream()
+ long nbDiffAspectsUnits = getXAxisAspects().stream()
.map(aspect -> aspect.getUnits())
.distinct()
.count();
- String units = getXAxisAspects().get(0).getUnits();
- if (nbDiffAspects == 1 && units != null) {
- /* All aspects use the same unit type */
+ long nbDiffAspectName = getXAxisAspects().stream()
+ .map(aspect -> aspect.getName())
+ .distinct()
+ .count();
- // The time duration formatter converts ns to s on the axis
- if (NANOSECONDS_SYMBOL.equals(units)) {
- units = SECONDS_SYMBOL;
- }
- fXTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$
- } else {
- /* Various unit types, just say "Value" */
- fXTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName);
+ String xBaseTitle = Messages.LamiViewer_DefaultValueName;
+ if (nbDiffAspectName == 1) {
+ xBaseTitle = getXAxisAspects().get(0).getName();
}
+
+ String units = null;
+ if (nbDiffAspectsUnits == 1) {
+ /* All aspects use the same unit type */
+ units = getXAxisAspects().get(0).getUnits();
+ }
+
+ innerSetXTitle(xBaseTitle, units);
}
/* Set Y axis title */
* There is only 1 series in the chart, we will use its name as the
* Y axis (and hide the legend).
*/
- String seriesName = getChartModel().getYSeriesColumns().get(0);
- // The time duration formatter converts ns to s on the axis
- if (NANOSECONDS_SYMBOL.equals(getYAxisAspects().get(0).getUnits())) {
- seriesName = getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$
- }
- fYTitle = seriesName;
+ innerSetYTitle(getYAxisAspects().get(0).getName(), getYAxisAspects().get(0).getUnits());
+
+ /* Hide the legend */
fChart.getLegend().setVisible(false);
} else {
/*
* There are multiple series in the chart, if they all share the same
* units, display that.
*/
- long nbDiffAspects = getYAxisAspects().stream()
+ long nbDiffAspectsUnits = getYAxisAspects().stream()
.map(aspect -> aspect.getUnits())
.distinct()
.count();
- String units = getYAxisAspects().get(0).getUnits();
- if (nbDiffAspects == 1 && units != null) {
- /* All aspects use the same unit type */
+ long nbDiffAspectName = getYAxisAspects().stream()
+ .map(aspect -> aspect.getName())
+ .distinct()
+ .count();
- // The time duration formatter converts ns to s on the axis
- if (NANOSECONDS_SYMBOL.equals(units)) {
- units = SECONDS_SYMBOL;
- }
- fYTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$
- } else {
- /* Various unit types, just say "Value" */
- fYTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName);
+ String yBaseTitle = Messages.LamiViewer_DefaultValueName;
+ if (nbDiffAspectName == 1) {
+ yBaseTitle = getYAxisAspects().get(0).getName();
}
+ String units = null;
+ if (nbDiffAspectsUnits == 1) {
+ /* All aspects use the same unit type */
+ units = getYAxisAspects().get(0).getUnits();
+ }
+
+ innerSetYTitle(yBaseTitle, units);
+
/* Put legend at the bottom */
fChart.getLegend().setPosition(SWT.BOTTOM);
}
/* Refresh the titles to fit the current chart size */
refreshDisplayTitles();
+ fToolBar = createChartToolBar();
+
fChart.addDisposeListener(e -> {
/* Dispose resources of this class */
LamiXYChartViewer.super.dispose();
});
}
+ /**
+ * Set the Y axis title and refresh the chart.
+ *
+ * @param label
+ * the label string.
+ * @param units
+ * the units string.
+ */
+ protected void setYTitle(@Nullable String label, @Nullable String units) {
+ innerSetYTitle(label, units);
+ }
+
+ private void innerSetYTitle(@Nullable String label, @Nullable String units) {
+ fYLabel = nullToEmptyString(label);
+ innerSetYUnits(units);
+ refreshDisplayTitles();
+ }
+
+ /**
+ * Set the units on the Y Axis title and refresh the chart.
+ *
+ * @param units
+ * the units string.
+ */
+ protected void setYUnits(@Nullable String units) {
+ innerSetYUnits(units);
+ }
+
+ private void innerSetYUnits(@Nullable String units) {
+ /*
+ * All time durations in the Lami protocol are nanoseconds, on the
+ * charts we use an axis formater that converts back to seconds as a
+ * base unit and then uses prefixes like nano and milli depending on the
+ * range.
+ *
+ * So set the units to seconds in the title to match the base unit of
+ * the formater.
+ */
+ if (NANOSECONDS_SYMBOL.equals(units)) {
+ fYUnits = SECONDS_SYMBOL;
+ } else {
+ fYUnits = units;
+ }
+ refreshDisplayTitles();
+ }
+
+ /**
+ * Get the Y axis title string.
+ *
+ * If the units is non-null, the title will be: "label (units)"
+ *
+ * If the units is null, the title will be: "label"
+ *
+ * @return the title of the Y axis.
+ */
+ protected String getYTitle() {
+ if (fYUnits == null) {
+ return fYLabel;
+ }
+ return fYLabel + " (" + fYUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Set the X axis title and refresh the chart.
+ *
+ * @param label
+ * the label string.
+ * @param units
+ * the units string.
+ */
+ protected void setXTitle(@Nullable String label, @Nullable String units) {
+ innerSetXTitle(label, units);
+ }
+
+ private void innerSetXTitle(@Nullable String label, @Nullable String units) {
+ fXLabel = nullToEmptyString(label);
+ innerSetXUnits(units);
+ refreshDisplayTitles();
+ }
+
+ /**
+ * Set the units on the X Axis title.
+ *
+ * @param units
+ * the units string
+ */
+ protected void setXUnits(@Nullable String units) {
+ innerSetXUnits(units);
+ }
+
+ private void innerSetXUnits(@Nullable String units) {
+ /* The time duration formatter converts ns to s on the axis */
+ if (NANOSECONDS_SYMBOL.equals(units)) {
+ fXUnits = SECONDS_SYMBOL;
+ } else {
+ fXUnits = units;
+ }
+ refreshDisplayTitles();
+ }
+
+ /**
+ * Get the X axis title string.
+ *
+ * If the units is non-null, the title will be: "label (units)"
+ *
+ * If the units is null, the title will be: "label"
+ *
+ * @return the title of the Y axis.
+ */
+ protected String getXTitle() {
+ if (fXUnits == null) {
+ return fXLabel;
+ }
+ return fXLabel + " (" + fXUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
/**
* Util method to check if a list of aspects are all continuous.
*
* The list of aspects of the axis.
* @param entries
* The list of entries of the chart.
+ * @param internalRange
+ * The internal range for value transformation
+ * @param externalRange
+ * The external range for value transformation
* @return a formatter for the axis.
*/
- protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries) {
+ protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) {
+
+ Format formatter = DECIMAL_FORMATTER;
if (areAspectsTimeStamp(axisAspects)) {
/* Set a TimeStamp formatter depending on the duration between the first and last value */
- double max = Double.MIN_VALUE;
- double min = Double.MAX_VALUE;
+ BigDecimal max = new BigDecimal(Long.MIN_VALUE);
+ BigDecimal min = new BigDecimal(Long.MAX_VALUE);
for (LamiTableEntry entry : entries) {
for (LamiTableEntryAspect aspect : axisAspects) {
- Double current = aspect.resolveDouble(entry);
- if (current != null) {
- max = Math.max(max, current);
- min = Math.min(min, current);
+ @Nullable Number number = aspect.resolveNumber(entry);
+ if (number != null) {
+ BigDecimal current = new BigDecimal(number.toString());
+ max = current.max(max);
+ min = current.min(min);
}
}
}
- long duration = (long) max - (long) min;
+ long duration = max.subtract(min).longValue();
if (duration > TimeUnit.DAYS.toNanos(1)) {
- return DAYS_FORMATTER;
+ formatter = DAYS_FORMATTER;
} else if (duration > TimeUnit.HOURS.toNanos(1)) {
- return HOURS_FORMATTER;
+ formatter = HOURS_FORMATTER;
} else if (duration > TimeUnit.MINUTES.toNanos(1)) {
- return MINUTES_FORMATTER;
+ formatter = MINUTES_FORMATTER;
} else if (duration > TimeUnit.SECONDS.toNanos(15)) {
- return SECONDS_FORMATTER;
+ formatter = SECONDS_FORMATTER;
} else {
- return MILLISECONDS_FORMATTER;
+ formatter = MILLISECONDS_FORMATTER;
}
+ ((LamiTimeStampFormat) formatter).setInternalRange(internalRange);
+ ((LamiTimeStampFormat) formatter).setExternalRange(externalRange);
+
} else if (areAspectsTimeDuration(axisAspects)) {
- /* Set the time duration formatter */
- return NANO_TO_SECS_FORMATTER;
+ /* Set the time duration formatter. */
+ formatter = NANO_TO_SECS_FORMATTER;
+ ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange);
+ ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange);
} else {
- /* For other numeric aspects, use the default decimal unit formatter */
- return DECIMAL_FORMATTER;
+ /*
+ * For other numeric aspects, use the default lami decimal unit
+ * formatter.
+ */
+ formatter = DECIMAL_FORMATTER;
+ ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange);
+ ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange);
}
+
+ return formatter;
}
/**
return fChart;
}
+ /**
+ * @return the toolBar
+ */
+ public ToolBar getToolBar() {
+ return fToolBar;
+ }
+
/**
* Is a selection made in the chart.
*
refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width);
ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle());
- refreshDisplayTitle(xTitle, fXTitle, plotRect.width);
+ refreshDisplayTitle(xTitle, getXTitle(), plotRect.width);
ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle());
- refreshDisplayTitle(yTitle, fYTitle, plotRect.height);
+ refreshDisplayTitle(yTitle, getYTitle(), plotRect.height);
}
/**
redraw();
}
+
+ /**
+ * Create a tool bar on top right of the chart. Contained actions:
+ * <ul>
+ * <li>Dispose the current viewer, also known as "Close the chart"</li>
+ * </ul>
+ *
+ * This tool bar should only appear when the mouse enters the composite.
+ *
+ * @return the tool bar
+ */
+ protected ToolBar createChartToolBar() {
+ Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE);
+ ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL);
+
+ /* Default state */
+ toolBar.moveAbove(null);
+ toolBar.setVisible(false);
+
+ /*
+ * Close chart button
+ */
+ ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH);
+ closeButton.setImage(removeImage);
+ closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip);
+ closeButton.addSelectionListener(new SelectionListener() {
+ @Override
+ public void widgetSelected(@Nullable SelectionEvent e) {
+ Composite parent = getParent();
+ dispose();
+ parent.layout();
+ }
+
+ @Override
+ public void widgetDefaultSelected(@Nullable SelectionEvent e) {
+ }
+ });
+
+ toolBar.pack();
+ toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0));
+
+ /* Visibility toggle filter */
+ Listener toolBarVisibilityToggleListener = e -> {
+ if (e.widget instanceof Control) {
+ Control control = (Control) e.widget;
+ Point display = control.toDisplay(e.x, e.y);
+ Point location = getChart().getParent().toControl(display);
+
+ /*
+ * Only set to visible if we are at the right location, in the
+ * right shell.
+ */
+ boolean visible = getChart().getBounds().contains(location) &&
+ control.getShell().equals(getChart().getShell());
+ getToolBar().setVisible(visible);
+ }
+ };
+
+ /* Filter to make sure we hide the toolbar if we exit the window */
+ Listener hideToolBarListener = (e -> getToolBar().setVisible(false));
+
+ /*
+ * Add the filters to the main Display, and remove them when we dispose
+ * the chart.
+ */
+ Display display = getChart().getDisplay();
+ display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener);
+ display.addFilter(SWT.MouseExit, hideToolBarListener);
+
+ getChart().addDisposeListener(e -> {
+ display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener);
+ display.removeFilter(SWT.MouseExit, hideToolBarListener);
+ });
+
+ /* Reposition the tool bar on resize */
+ getChart().addListener(SWT.Resize, new Listener() {
+ @Override
+ public void handleEvent(@Nullable Event event) {
+ toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0));
+ }
+ });
+
+ return toolBar;
+ }
+
+ /**
+ * Get a {@link LamiGraphRange} that covers all data points in the result
+ * table.
+ * <p>
+ * The returned range will be the minimum and maximum of the resolved values
+ * of the passed aspects for all result entries. If <code>clampToZero</code>
+ * is true, a positive minimum value will be clamped down to zero.
+ *
+ * @param aspects
+ * The aspects that the range will represent
+ * @param clampToZero
+ * If true, a positive minimum value will be clamped down to zero
+ * @return the range
+ */
+ protected LamiGraphRange getRange(List<LamiTableEntryAspect> aspects, boolean clampToZero) {
+ /* Find the minimum and maximum values */
+ BigDecimal min = new BigDecimal(Long.MAX_VALUE);
+ BigDecimal max = new BigDecimal(Long.MIN_VALUE);
+ for (LamiTableEntryAspect lamiTableEntryAspect : aspects) {
+ for (LamiTableEntry entry : getResultTable().getEntries()) {
+ @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry);
+ if (number != null) {
+ BigDecimal current = new BigDecimal(number.toString());
+ min = current.min(min);
+ max = current.max(max);
+ }
+ }
+ }
+
+ if (clampToZero) {
+ min = min.min(BigDecimal.ZERO);
+ }
+
+ /* Do not allow a range with a zero delta default to 1 */
+ if (max.equals(min)) {
+ max = min.add(BigDecimal.ONE);
+ }
+
+ return new LamiGraphRange(checkNotNull(min), checkNotNull(max));
+ }
+
+ /**
+ * Transform an external value into an internal value. Since SWTChart only
+ * support Double and Lami can pass Long values, loss of precision might
+ * happen. To minimize this, transform the raw values to an internal
+ * representation based on a linear transformation.
+ *
+ * The internal value =
+ *
+ * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) +
+ * internalMinimum
+ *
+ * @param number
+ * The number to transform
+ * @param internalRange
+ * The internal range definition to be used
+ * @param externalRange
+ * The external range definition to be used
+ * @return the transformed value in Double comprised inside the internal
+ * range
+ */
+ protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) {
+ BigDecimal value = new BigDecimal(number.toString());
+
+ if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) {
+ return internalRange.getMinimum().doubleValue();
+ }
+
+ BigDecimal internalValue = value
+ .subtract(externalRange.getMinimum())
+ .multiply(internalRange.getDelta())
+ .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN)
+ .add(internalRange.getMinimum());
+
+ return internalValue.doubleValue();
+ }
}