1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.viewers
;
12 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNull
;
13 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.nullToEmptyString
;
15 import java
.text
.Format
;
16 import java
.util
.ArrayList
;
17 import java
.util
.HashSet
;
18 import java
.util
.List
;
20 import java
.util
.concurrent
.TimeUnit
;
21 import java
.util
.function
.ToDoubleFunction
;
23 import org
.eclipse
.jdt
.annotation
.NonNull
;
24 import org
.eclipse
.jdt
.annotation
.Nullable
;
25 import org
.eclipse
.swt
.SWT
;
26 import org
.eclipse
.swt
.events
.SelectionEvent
;
27 import org
.eclipse
.swt
.events
.SelectionListener
;
28 import org
.eclipse
.swt
.graphics
.Color
;
29 import org
.eclipse
.swt
.graphics
.Font
;
30 import org
.eclipse
.swt
.graphics
.GC
;
31 import org
.eclipse
.swt
.graphics
.Image
;
32 import org
.eclipse
.swt
.graphics
.Point
;
33 import org
.eclipse
.swt
.graphics
.Rectangle
;
34 import org
.eclipse
.swt
.widgets
.Composite
;
35 import org
.eclipse
.swt
.widgets
.Control
;
36 import org
.eclipse
.swt
.widgets
.Display
;
37 import org
.eclipse
.swt
.widgets
.Event
;
38 import org
.eclipse
.swt
.widgets
.Listener
;
39 import org
.eclipse
.swt
.widgets
.ToolBar
;
40 import org
.eclipse
.swt
.widgets
.ToolItem
;
41 import org
.eclipse
.tracecompass
.common
.core
.format
.DecimalUnitFormat
;
42 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
45 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
46 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTimeStampFormat
;
47 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
48 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalHandler
;
49 import org
.eclipse
.tracecompass
.tmf
.ui
.viewers
.TmfViewer
;
50 import org
.eclipse
.ui
.ISharedImages
;
51 import org
.eclipse
.ui
.PlatformUI
;
52 import org
.swtchart
.Chart
;
53 import org
.swtchart
.ITitle
;
55 import com
.google
.common
.collect
.ImmutableList
;
58 * Abstract XYChart Viewer for LAMI views.
60 * @author Michael Jeanson
63 public abstract class LamiXYChartViewer
extends TmfViewer
implements ILamiViewer
{
65 /** Ellipsis character */
66 protected static final String ELLIPSIS
= "…"; //$NON-NLS-1$
69 * String representing unknown values. Can be present even in numerical
72 protected static final String UNKNOWN
= "?"; //$NON-NLS-1$
75 protected static final double ZERO
= 0.0;
77 /** Symbol for seconds (used in the custom ns -> s conversion) */
78 private static final String SECONDS_SYMBOL
= "s"; //$NON-NLS-1$
80 /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
81 private static final String NANOSECONDS_SYMBOL
= "ns"; //$NON-NLS-1$
84 * Function to use to map Strings read from the data table to doubles for
85 * use in SWTChart series.
87 protected static final ToDoubleFunction
<@Nullable String
> DOUBLE_MAPPER
= str
-> {
88 if (str
== null || str
.equals(UNKNOWN
)) {
91 return Double
.parseDouble(str
);
95 * List of standard colors
97 protected static final List
<@NonNull Color
> COLORS
= ImmutableList
.of(
98 new Color(Display
.getDefault(), 72, 120, 207),
99 new Color(Display
.getDefault(), 106, 204, 101),
100 new Color(Display
.getDefault(), 214, 95, 95),
101 new Color(Display
.getDefault(), 180, 124, 199),
102 new Color(Display
.getDefault(), 196, 173, 102),
103 new Color(Display
.getDefault(), 119, 190, 219)
107 * List of "light" colors (when unselected)
109 protected static final List
<@NonNull Color
> LIGHT_COLORS
= ImmutableList
.of(
110 new Color(Display
.getDefault(), 173, 195, 233),
111 new Color(Display
.getDefault(), 199, 236, 197),
112 new Color(Display
.getDefault(), 240, 196, 196),
113 new Color(Display
.getDefault(), 231, 213, 237),
114 new Color(Display
.getDefault(), 231, 222, 194),
115 new Color(Display
.getDefault(), 220, 238, 246)
119 * Time stamp formatter for intervals in the days range.
121 protected static final LamiTimeStampFormat DAYS_FORMATTER
= new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$
124 * Time stamp formatter for intervals in the hours range.
126 protected static final LamiTimeStampFormat HOURS_FORMATTER
= new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$
129 * Time stamp formatter for intervals in the minutes range.
131 protected static final LamiTimeStampFormat MINUTES_FORMATTER
= new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$
134 * Time stamp formatter for intervals in the seconds range.
136 protected static final LamiTimeStampFormat SECONDS_FORMATTER
= new LamiTimeStampFormat("ss"); //$NON-NLS-1$
139 * Time stamp formatter for intervals in the milliseconds range.
141 protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER
= new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$
144 * Decimal formatter to display nanoseconds as seconds.
146 protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER
= new DecimalUnitFormat(0.000000001);
149 * Default decimal formatter.
151 protected static final DecimalUnitFormat DECIMAL_FORMATTER
= new DecimalUnitFormat();
153 private final Listener fResizeListener
= event
-> {
154 /* Refresh the titles to fit the current chart size */
155 refreshDisplayTitles();
157 /* Refresh the Axis labels to fit the current chart size */
158 refreshDisplayLabels();
161 private final LamiResultTable fResultTable
;
162 private final LamiChartModel fChartModel
;
164 private final Chart fChart
;
166 private final String fChartTitle
;
167 private final String fXTitle
;
168 private final String fYTitle
;
170 private boolean fSelected
;
171 private Set
<Integer
> fSelection
;
173 private final ToolBar fToolBar
;
176 * Creates a Viewer instance based on SWTChart.
179 * The parent composite to draw in.
181 * The result table containing the data from which to build the
184 * The information about the chart to build
186 public LamiXYChartViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel chartModel
) {
190 fResultTable
= resultTable
;
191 fChartModel
= chartModel
;
192 fSelection
= new HashSet
<>();
194 fChart
= new Chart(parent
, SWT
.NONE
);
195 fChart
.addListener(SWT
.Resize
, fResizeListener
);
197 /* Set Chart title */
198 fChartTitle
= fResultTable
.getTableClass().getTableTitle();
200 /* Set X axis title */
201 if (fChartModel
.getXSeriesColumns().size() == 1) {
203 * There is only 1 series in the chart, we will use its name as the
204 * Y axis (and hide the legend).
206 String seriesName
= getChartModel().getXSeriesColumns().get(0);
207 // The time duration formatter converts ns to s on the axis
208 if (NANOSECONDS_SYMBOL
.equals(getXAxisAspects().get(0).getUnits())) {
209 seriesName
= getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
211 fXTitle
= seriesName
;
214 * There are multiple series in the chart, if they all share the same
215 * units, display that.
217 long nbDiffAspectsUnits
= getXAxisAspects().stream()
218 .map(aspect
-> aspect
.getUnits())
222 long nbDiffAspectName
= getXAxisAspects().stream()
223 .map(aspect
-> aspect
.getName())
227 String xBaseTitle
= Messages
.LamiViewer_DefaultValueName
;
228 if (nbDiffAspectName
== 1) {
229 xBaseTitle
= getXAxisAspects().get(0).getName();
232 String units
= getXAxisAspects().get(0).getUnits();
233 if (nbDiffAspectsUnits
== 1 && units
!= null) {
234 /* All aspects use the same unit type */
236 // The time duration formatter converts ns to s on the axis
237 if (NANOSECONDS_SYMBOL
.equals(units
)) {
238 units
= SECONDS_SYMBOL
;
240 fXTitle
= xBaseTitle
+ " (" + units
+ ')'; //$NON-NLS-1$
242 /* Various unit types, just say "Value" */
243 fXTitle
= nullToEmptyString(xBaseTitle
);
247 /* Set Y axis title */
248 if (fChartModel
.getYSeriesColumns().size() == 1) {
250 * There is only 1 series in the chart, we will use its name as the
251 * Y axis (and hide the legend).
253 String seriesName
= getChartModel().getYSeriesColumns().get(0);
254 // The time duration formatter converts ns to s on the axis
255 if (NANOSECONDS_SYMBOL
.equals(getYAxisAspects().get(0).getUnits())) {
256 seriesName
= getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
258 fYTitle
= seriesName
;
259 fChart
.getLegend().setVisible(false);
262 * There are multiple series in the chart, if they all share the same
263 * units, display that.
265 long nbDiffAspectsUnits
= getYAxisAspects().stream()
266 .map(aspect
-> aspect
.getUnits())
270 long nbDiffAspectName
= getYAxisAspects().stream()
271 .map(aspect
-> aspect
.getName())
275 String yBaseTitle
= Messages
.LamiViewer_DefaultValueName
;
276 if (nbDiffAspectName
== 1) {
277 yBaseTitle
= getYAxisAspects().get(0).getName();
280 String units
= getYAxisAspects().get(0).getUnits();
281 if (nbDiffAspectsUnits
== 1 && units
!= null) {
282 /* All aspects use the same unit type */
284 // The time duration formatter converts ns to s on the axis
285 if (NANOSECONDS_SYMBOL
.equals(units
)) {
286 units
= SECONDS_SYMBOL
;
288 fYTitle
= yBaseTitle
+ " (" + units
+ ')'; //$NON-NLS-1$
290 /* Various unit types, don't display any units */
291 fYTitle
= nullToEmptyString(yBaseTitle
);
294 /* Put legend at the bottom */
295 fChart
.getLegend().setPosition(SWT
.BOTTOM
);
298 /* Set all titles and labels font color to black */
299 fChart
.getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
300 fChart
.getAxisSet().getXAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
301 fChart
.getAxisSet().getYAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
302 fChart
.getAxisSet().getXAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
303 fChart
.getAxisSet().getYAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
305 /* Set X label 90 degrees */
306 fChart
.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
308 /* Refresh the titles to fit the current chart size */
309 refreshDisplayTitles();
311 fToolBar
= createChartToolBar();
313 fChart
.addDisposeListener(e
-> {
314 /* Dispose resources of this class */
315 LamiXYChartViewer
.super.dispose();
320 * Util method to check if a list of aspects are all continuous.
323 * The list of aspects to check.
324 * @return true is all aspects are continuous, otherwise false.
326 protected static boolean areAspectsContinuous(List
<LamiTableEntryAspect
> axisAspects
) {
327 return axisAspects
.stream().allMatch(aspect
-> aspect
.isContinuous());
331 * Util method to check if a list of aspects are all time stamps.
334 * The list of aspects to check.
335 * @return true is all aspects are time stamps, otherwise false.
337 protected static boolean areAspectsTimeStamp(List
<LamiTableEntryAspect
> axisAspects
) {
338 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeStamp());
342 * Util method to check if a list of aspects are all time durations.
345 * The list of aspects to check.
346 * @return true is all aspects are time durations, otherwise false.
348 protected static boolean areAspectsTimeDuration(List
<LamiTableEntryAspect
> axisAspects
) {
349 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeDuration());
353 * Util method that will return a formatter based on the aspects linked to an axis
355 * If all aspects are time stamps, return a timestamp formatter tuned to the interval.
356 * If all aspects are time durations, return the nanoseconds to seconds formatter.
357 * Otherwise, return the generic decimal formatter.
360 * The list of aspects of the axis.
362 * The list of entries of the chart.
363 * @return a formatter for the axis.
365 protected static Format
getContinuousAxisFormatter(List
<LamiTableEntryAspect
> axisAspects
, List
<LamiTableEntry
> entries
) {
367 if (areAspectsTimeStamp(axisAspects
)) {
368 /* Set a TimeStamp formatter depending on the duration between the first and last value */
369 double max
= Double
.MIN_VALUE
;
370 double min
= Double
.MAX_VALUE
;
372 for (LamiTableEntry entry
: entries
) {
373 for (LamiTableEntryAspect aspect
: axisAspects
) {
374 Double current
= aspect
.resolveDouble(entry
);
375 if (current
!= null) {
376 max
= Math
.max(max
, current
);
377 min
= Math
.min(min
, current
);
381 long duration
= (long) max
- (long) min
;
383 if (duration
> TimeUnit
.DAYS
.toNanos(1)) {
384 return DAYS_FORMATTER
;
385 } else if (duration
> TimeUnit
.HOURS
.toNanos(1)) {
386 return HOURS_FORMATTER
;
387 } else if (duration
> TimeUnit
.MINUTES
.toNanos(1)) {
388 return MINUTES_FORMATTER
;
389 } else if (duration
> TimeUnit
.SECONDS
.toNanos(15)) {
390 return SECONDS_FORMATTER
;
392 return MILLISECONDS_FORMATTER
;
394 } else if (areAspectsTimeDuration(axisAspects
)) {
395 /* Set the time duration formatter */
396 return NANO_TO_SECS_FORMATTER
;
399 /* For other numeric aspects, use the default decimal unit formatter */
400 return DECIMAL_FORMATTER
;
405 * Get the chart result table.
407 * @return The chart result table.
409 protected LamiResultTable
getResultTable() {
414 * Get the chart model.
416 * @return The chart model.
418 protected LamiChartModel
getChartModel() {
423 * Get the chart object.
424 * @return The chart object.
426 protected Chart
getChart() {
431 * @return the toolBar
433 public ToolBar
getToolBar() {
438 * Is a selection made in the chart.
440 * @return true if there is a selection.
442 protected boolean isSelected() {
447 * Set the selection index.
449 * @param selection the index to select.
451 protected void setSelection(Set
<Integer
> selection
) {
452 fSelection
= selection
;
453 fSelected
= !selection
.isEmpty();
457 * Unset the chart selection.
459 protected void unsetSelection() {
465 * Get the current selection index.
467 * @return the current selection index.
469 protected Set
<Integer
> getSelection() {
474 public @Nullable Control
getControl() {
475 return fChart
.getParent();
479 public void refresh() {
480 Display
.getDefault().asyncExec(() -> {
481 if (!fChart
.isDisposed()) {
488 public void dispose() {
490 /* The control's DisposeListener will call super.dispose() */
494 * Get a list of all the aspect of the Y axis.
496 * @return The aspects for the Y axis
498 protected List
<LamiTableEntryAspect
> getYAxisAspects() {
500 List
<LamiTableEntryAspect
> yAxisAspects
= new ArrayList
<>();
502 for (String colName
: getChartModel().getYSeriesColumns()) {
503 yAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
510 * Get a list of all the aspect of the X axis.
512 * @return The aspects for the X axis
514 protected List
<LamiTableEntryAspect
> getXAxisAspects() {
516 List
<LamiTableEntryAspect
> xAxisAspects
= new ArrayList
<>();
518 for (String colName
: getChartModel().getXSeriesColumns()) {
519 xAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
526 * Set the ITitle object text to a substring of canonicalTitle that when
527 * rendered in the chart will fit maxPixelLength.
529 private void refreshDisplayTitle(ITitle title
, String canonicalTitle
, int maxPixelLength
) {
530 if (title
.isVisible()) {
532 String newTitle
= canonicalTitle
;
534 /* Get the title font */
535 Font font
= title
.getFont();
537 GC gc
= new GC(fParent
);
540 /* Get the length and height of the canonical title in pixels */
541 Point pixels
= gc
.stringExtent(canonicalTitle
);
544 * If the title is too long, generate a shortened version based on the
545 * average character width of the current font.
547 if (pixels
.x
> maxPixelLength
) {
548 int charwidth
= gc
.getFontMetrics().getAverageCharWidth();
552 int strLen
= ((maxPixelLength
/ charwidth
) - minimum
);
554 if (strLen
> minimum
) {
555 newTitle
= canonicalTitle
.substring(0, strLen
) + ELLIPSIS
;
561 title
.setText(newTitle
);
569 * Refresh the Chart, XAxis and YAxis titles to fit the current
572 private void refreshDisplayTitles() {
573 Rectangle chartRect
= fChart
.getClientArea();
574 Rectangle plotRect
= fChart
.getPlotArea().getClientArea();
576 ITitle chartTitle
= checkNotNull(fChart
.getTitle());
577 refreshDisplayTitle(chartTitle
, fChartTitle
, chartRect
.width
);
579 ITitle xTitle
= checkNotNull(fChart
.getAxisSet().getXAxis(0).getTitle());
580 refreshDisplayTitle(xTitle
, fXTitle
, plotRect
.width
);
582 ITitle yTitle
= checkNotNull(fChart
.getAxisSet().getYAxis(0).getTitle());
583 refreshDisplayTitle(yTitle
, fYTitle
, plotRect
.height
);
587 * Get the aspect with the given name
590 * The list of aspects to search into
592 * The name of the aspect we are looking for
593 * @return The corresponding aspect
595 protected static @Nullable LamiTableEntryAspect
getAspectFromName(List
<LamiTableEntryAspect
> aspects
, String aspectName
) {
596 for (LamiTableEntryAspect lamiTableEntryAspect
: aspects
) {
598 if (lamiTableEntryAspect
.getLabel().equals(aspectName
)) {
599 return lamiTableEntryAspect
;
607 * Refresh the axis labels to fit the current chart size.
609 protected abstract void refreshDisplayLabels();
614 protected void redraw() {
619 * Signal handler for selection update.
622 * The selection update signal
625 public void updateSelection(LamiSelectionUpdateSignal signal
) {
626 if (getResultTable().hashCode() != signal
.getSignalHash() || equals(signal
.getSource())) {
627 /* The signal is not for us */
630 setSelection(signal
.getEntryIndex());
636 * Create a tool bar on top right of the chart. Contained actions:
638 * <li>Dispose the current viewer, also known as "Close the chart"</li>
641 * This tool bar should only appear when the mouse enters the composite.
643 * @return the tool bar
645 protected ToolBar
createChartToolBar() {
646 Image removeImage
= PlatformUI
.getWorkbench().getSharedImages().getImage(ISharedImages
.IMG_ELCL_REMOVE
);
647 ToolBar toolBar
= new ToolBar(getChart(), SWT
.HORIZONTAL
);
650 toolBar
.moveAbove(null);
651 toolBar
.setVisible(false);
656 ToolItem closeButton
= new ToolItem(toolBar
, SWT
.PUSH
);
657 closeButton
.setImage(removeImage
);
658 closeButton
.setToolTipText(Messages
.LamiXYChartViewer_CloseChartToolTip
);
659 closeButton
.addSelectionListener(new SelectionListener() {
661 public void widgetSelected(@Nullable SelectionEvent e
) {
662 Composite parent
= getParent();
668 public void widgetDefaultSelected(@Nullable SelectionEvent e
) {
673 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));
675 /* Visibility toggle filter */
676 Listener toolBarVisibilityToggleListener
= e
-> {
677 if (e
.widget
instanceof Control
) {
678 Control control
= (Control
) e
.widget
;
679 Point display
= control
.toDisplay(e
.x
, e
.y
);
680 Point location
= getChart().getParent().toControl(display
);
683 * Only set to visible if we are at the right location, in the
686 boolean visible
= getChart().getBounds().contains(location
) &&
687 control
.getShell().equals(getChart().getShell());
688 getToolBar().setVisible(visible
);
692 /* Filter to make sure we hide the toolbar if we exit the window */
693 Listener hideToolBarListener
= (e
-> getToolBar().setVisible(false));
696 * Add the filters to the main Display, and remove them when we dispose
699 Display display
= getChart().getDisplay();
700 display
.addFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
701 display
.addFilter(SWT
.MouseExit
, hideToolBarListener
);
703 getChart().addDisposeListener(e
-> {
704 display
.removeFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
705 display
.removeFilter(SWT
.MouseExit
, hideToolBarListener
);
708 /* Reposition the tool bar on resize */
709 getChart().addListener(SWT
.Resize
, new Listener() {
711 public void handleEvent(@Nullable Event event
) {
712 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));