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 nbDiffAspects
= getXAxisAspects().stream()
218 .map(aspect
-> aspect
.getUnits())
222 String units
= getXAxisAspects().get(0).getUnits();
223 if (nbDiffAspects
== 1 && units
!= null) {
224 /* All aspects use the same unit type */
226 // The time duration formatter converts ns to s on the axis
227 if (NANOSECONDS_SYMBOL
.equals(units
)) {
228 units
= SECONDS_SYMBOL
;
230 fXTitle
= Messages
.LamiViewer_DefaultValueName
+ " (" + units
+ ')'; //$NON-NLS-1$
232 /* Various unit types, just say "Value" */
233 fXTitle
= nullToEmptyString(Messages
.LamiViewer_DefaultValueName
);
237 /* Set Y axis title */
238 if (fChartModel
.getYSeriesColumns().size() == 1) {
240 * There is only 1 series in the chart, we will use its name as the
241 * Y axis (and hide the legend).
243 String seriesName
= getChartModel().getYSeriesColumns().get(0);
244 // The time duration formatter converts ns to s on the axis
245 if (NANOSECONDS_SYMBOL
.equals(getYAxisAspects().get(0).getUnits())) {
246 seriesName
= getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
248 fYTitle
= seriesName
;
249 fChart
.getLegend().setVisible(false);
252 * There are multiple series in the chart, if they all share the same
253 * units, display that.
255 long nbDiffAspects
= getYAxisAspects().stream()
256 .map(aspect
-> aspect
.getUnits())
260 String units
= getYAxisAspects().get(0).getUnits();
261 if (nbDiffAspects
== 1 && units
!= null) {
262 /* All aspects use the same unit type */
264 // The time duration formatter converts ns to s on the axis
265 if (NANOSECONDS_SYMBOL
.equals(units
)) {
266 units
= SECONDS_SYMBOL
;
268 fYTitle
= Messages
.LamiViewer_DefaultValueName
+ " (" + units
+ ')'; //$NON-NLS-1$
270 /* Various unit types, just say "Value" */
271 fYTitle
= nullToEmptyString(Messages
.LamiViewer_DefaultValueName
);
274 /* Put legend at the bottom */
275 fChart
.getLegend().setPosition(SWT
.BOTTOM
);
278 /* Set all titles and labels font color to black */
279 fChart
.getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
280 fChart
.getAxisSet().getXAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
281 fChart
.getAxisSet().getYAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
282 fChart
.getAxisSet().getXAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
283 fChart
.getAxisSet().getYAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
285 /* Set X label 90 degrees */
286 fChart
.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
288 /* Refresh the titles to fit the current chart size */
289 refreshDisplayTitles();
291 fToolBar
= createChartToolBar();
293 fChart
.addDisposeListener(e
-> {
294 /* Dispose resources of this class */
295 LamiXYChartViewer
.super.dispose();
300 * Util method to check if a list of aspects are all continuous.
303 * The list of aspects to check.
304 * @return true is all aspects are continuous, otherwise false.
306 protected static boolean areAspectsContinuous(List
<LamiTableEntryAspect
> axisAspects
) {
307 return axisAspects
.stream().allMatch(aspect
-> aspect
.isContinuous());
311 * Util method to check if a list of aspects are all time stamps.
314 * The list of aspects to check.
315 * @return true is all aspects are time stamps, otherwise false.
317 protected static boolean areAspectsTimeStamp(List
<LamiTableEntryAspect
> axisAspects
) {
318 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeStamp());
322 * Util method to check if a list of aspects are all time durations.
325 * The list of aspects to check.
326 * @return true is all aspects are time durations, otherwise false.
328 protected static boolean areAspectsTimeDuration(List
<LamiTableEntryAspect
> axisAspects
) {
329 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeDuration());
333 * Util method that will return a formatter based on the aspects linked to an axis
335 * If all aspects are time stamps, return a timestamp formatter tuned to the interval.
336 * If all aspects are time durations, return the nanoseconds to seconds formatter.
337 * Otherwise, return the generic decimal formatter.
340 * The list of aspects of the axis.
342 * The list of entries of the chart.
343 * @return a formatter for the axis.
345 protected static Format
getContinuousAxisFormatter(List
<LamiTableEntryAspect
> axisAspects
, List
<LamiTableEntry
> entries
) {
347 if (areAspectsTimeStamp(axisAspects
)) {
348 /* Set a TimeStamp formatter depending on the duration between the first and last value */
349 double max
= Double
.MIN_VALUE
;
350 double min
= Double
.MAX_VALUE
;
352 for (LamiTableEntry entry
: entries
) {
353 for (LamiTableEntryAspect aspect
: axisAspects
) {
354 Double current
= aspect
.resolveDouble(entry
);
355 if (current
!= null) {
356 max
= Math
.max(max
, current
);
357 min
= Math
.min(min
, current
);
361 long duration
= (long) max
- (long) min
;
363 if (duration
> TimeUnit
.DAYS
.toNanos(1)) {
364 return DAYS_FORMATTER
;
365 } else if (duration
> TimeUnit
.HOURS
.toNanos(1)) {
366 return HOURS_FORMATTER
;
367 } else if (duration
> TimeUnit
.MINUTES
.toNanos(1)) {
368 return MINUTES_FORMATTER
;
369 } else if (duration
> TimeUnit
.SECONDS
.toNanos(15)) {
370 return SECONDS_FORMATTER
;
372 return MILLISECONDS_FORMATTER
;
374 } else if (areAspectsTimeDuration(axisAspects
)) {
375 /* Set the time duration formatter */
376 return NANO_TO_SECS_FORMATTER
;
379 /* For other numeric aspects, use the default decimal unit formatter */
380 return DECIMAL_FORMATTER
;
385 * Get the chart result table.
387 * @return The chart result table.
389 protected LamiResultTable
getResultTable() {
394 * Get the chart model.
396 * @return The chart model.
398 protected LamiChartModel
getChartModel() {
403 * Get the chart object.
404 * @return The chart object.
406 protected Chart
getChart() {
411 * @return the toolBar
413 public ToolBar
getToolBar() {
418 * Is a selection made in the chart.
420 * @return true if there is a selection.
422 protected boolean isSelected() {
427 * Set the selection index.
429 * @param selection the index to select.
431 protected void setSelection(Set
<Integer
> selection
) {
432 fSelection
= selection
;
433 fSelected
= !selection
.isEmpty();
437 * Unset the chart selection.
439 protected void unsetSelection() {
445 * Get the current selection index.
447 * @return the current selection index.
449 protected Set
<Integer
> getSelection() {
454 public @Nullable Control
getControl() {
455 return fChart
.getParent();
459 public void refresh() {
460 Display
.getDefault().asyncExec(() -> {
461 if (!fChart
.isDisposed()) {
468 public void dispose() {
470 /* The control's DisposeListener will call super.dispose() */
474 * Get a list of all the aspect of the Y axis.
476 * @return The aspects for the Y axis
478 protected List
<LamiTableEntryAspect
> getYAxisAspects() {
480 List
<LamiTableEntryAspect
> yAxisAspects
= new ArrayList
<>();
482 for (String colName
: getChartModel().getYSeriesColumns()) {
483 yAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
490 * Get a list of all the aspect of the X axis.
492 * @return The aspects for the X axis
494 protected List
<LamiTableEntryAspect
> getXAxisAspects() {
496 List
<LamiTableEntryAspect
> xAxisAspects
= new ArrayList
<>();
498 for (String colName
: getChartModel().getXSeriesColumns()) {
499 xAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
506 * Set the ITitle object text to a substring of canonicalTitle that when
507 * rendered in the chart will fit maxPixelLength.
509 private void refreshDisplayTitle(ITitle title
, String canonicalTitle
, int maxPixelLength
) {
510 if (title
.isVisible()) {
512 String newTitle
= canonicalTitle
;
514 /* Get the title font */
515 Font font
= title
.getFont();
517 GC gc
= new GC(fParent
);
520 /* Get the length and height of the canonical title in pixels */
521 Point pixels
= gc
.stringExtent(canonicalTitle
);
524 * If the title is too long, generate a shortened version based on the
525 * average character width of the current font.
527 if (pixels
.x
> maxPixelLength
) {
528 int charwidth
= gc
.getFontMetrics().getAverageCharWidth();
532 int strLen
= ((maxPixelLength
/ charwidth
) - minimum
);
534 if (strLen
> minimum
) {
535 newTitle
= canonicalTitle
.substring(0, strLen
) + ELLIPSIS
;
541 title
.setText(newTitle
);
549 * Refresh the Chart, XAxis and YAxis titles to fit the current
552 private void refreshDisplayTitles() {
553 Rectangle chartRect
= fChart
.getClientArea();
554 Rectangle plotRect
= fChart
.getPlotArea().getClientArea();
556 ITitle chartTitle
= checkNotNull(fChart
.getTitle());
557 refreshDisplayTitle(chartTitle
, fChartTitle
, chartRect
.width
);
559 ITitle xTitle
= checkNotNull(fChart
.getAxisSet().getXAxis(0).getTitle());
560 refreshDisplayTitle(xTitle
, fXTitle
, plotRect
.width
);
562 ITitle yTitle
= checkNotNull(fChart
.getAxisSet().getYAxis(0).getTitle());
563 refreshDisplayTitle(yTitle
, fYTitle
, plotRect
.height
);
567 * Get the aspect with the given name
570 * The list of aspects to search into
572 * The name of the aspect we are looking for
573 * @return The corresponding aspect
575 protected static @Nullable LamiTableEntryAspect
getAspectFromName(List
<LamiTableEntryAspect
> aspects
, String aspectName
) {
576 for (LamiTableEntryAspect lamiTableEntryAspect
: aspects
) {
578 if (lamiTableEntryAspect
.getLabel().equals(aspectName
)) {
579 return lamiTableEntryAspect
;
587 * Refresh the axis labels to fit the current chart size.
589 protected abstract void refreshDisplayLabels();
594 protected void redraw() {
599 * Signal handler for selection update.
602 * The selection update signal
605 public void updateSelection(LamiSelectionUpdateSignal signal
) {
606 if (getResultTable().hashCode() != signal
.getSignalHash() || equals(signal
.getSource())) {
607 /* The signal is not for us */
610 setSelection(signal
.getEntryIndex());
616 * Create a tool bar on top right of the chart. Contained actions:
618 * <li>Dispose the current viewer, also known as "Close the chart"</li>
621 * This tool bar should only appear when the mouse enters the composite.
623 * @return the tool bar
625 protected ToolBar
createChartToolBar() {
626 Image removeImage
= PlatformUI
.getWorkbench().getSharedImages().getImage(ISharedImages
.IMG_ELCL_REMOVE
);
627 ToolBar toolBar
= new ToolBar(getChart(), SWT
.HORIZONTAL
);
630 toolBar
.moveAbove(null);
631 toolBar
.setVisible(false);
636 ToolItem closeButton
= new ToolItem(toolBar
, SWT
.PUSH
);
637 closeButton
.setImage(removeImage
);
638 closeButton
.setToolTipText(Messages
.LamiXYChartViewer_CloseChartToolTip
);
639 closeButton
.addSelectionListener(new SelectionListener() {
641 public void widgetSelected(@Nullable SelectionEvent e
) {
642 Composite parent
= getParent();
648 public void widgetDefaultSelected(@Nullable SelectionEvent e
) {
653 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));
655 /* Visibility toggle filter */
656 Listener toolBarVisibilityToggleListener
= e
-> {
657 if (e
.widget
instanceof Control
) {
658 Control control
= (Control
) e
.widget
;
659 Point display
= control
.toDisplay(e
.x
, e
.y
);
660 Point location
= getChart().getParent().toControl(display
);
663 * Only set to visible if we are at the right location, in the
666 boolean visible
= getChart().getBounds().contains(location
) &&
667 control
.getShell().equals(getChart().getShell());
668 getToolBar().setVisible(visible
);
672 /* Filter to make sure we hide the toolbar if we exit the window */
673 Listener hideToolBarListener
= (e
-> getToolBar().setVisible(false));
676 * Add the filters to the main Display, and remove them when we dispose
679 Display display
= getChart().getDisplay();
680 display
.addFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
681 display
.addFilter(SWT
.MouseExit
, hideToolBarListener
);
683 getChart().addDisposeListener(e
-> {
684 display
.removeFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
685 display
.removeFilter(SWT
.MouseExit
, hideToolBarListener
);
688 /* Reposition the tool bar on resize */
689 getChart().addListener(SWT
.Resize
, new Listener() {
691 public void handleEvent(@Nullable Event event
) {
692 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));