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
.graphics
.Color
;
27 import org
.eclipse
.swt
.graphics
.Font
;
28 import org
.eclipse
.swt
.graphics
.GC
;
29 import org
.eclipse
.swt
.graphics
.Point
;
30 import org
.eclipse
.swt
.graphics
.Rectangle
;
31 import org
.eclipse
.swt
.widgets
.Composite
;
32 import org
.eclipse
.swt
.widgets
.Control
;
33 import org
.eclipse
.swt
.widgets
.Display
;
34 import org
.eclipse
.swt
.widgets
.Listener
;
35 import org
.eclipse
.tracecompass
.common
.core
.format
.DecimalUnitFormat
;
36 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
37 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
38 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
39 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
40 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTimeStampFormat
;
41 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
42 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalHandler
;
43 import org
.eclipse
.tracecompass
.tmf
.ui
.viewers
.TmfViewer
;
44 import org
.swtchart
.Chart
;
45 import org
.swtchart
.ITitle
;
47 import com
.google
.common
.collect
.ImmutableList
;
50 * Abstract XYChart Viewer for LAMI views.
52 * @author Michael Jeanson
55 public abstract class LamiXYChartViewer
extends TmfViewer
implements ILamiViewer
{
57 /** Ellipsis character */
58 protected static final String ELLIPSIS
= "…"; //$NON-NLS-1$
61 * String representing unknown values. Can be present even in numerical
64 protected static final String UNKNOWN
= "?"; //$NON-NLS-1$
67 protected static final double ZERO
= 0.0;
69 /** Symbol for seconds (used in the custom ns -> s conversion) */
70 private static final String SECONDS_SYMBOL
= "s"; //$NON-NLS-1$
72 /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
73 private static final String NANOSECONDS_SYMBOL
= "ns"; //$NON-NLS-1$
76 * Function to use to map Strings read from the data table to doubles for
77 * use in SWTChart series.
79 protected static final ToDoubleFunction
<@Nullable String
> DOUBLE_MAPPER
= str
-> {
80 if (str
== null || str
.equals(UNKNOWN
)) {
83 return Double
.parseDouble(str
);
87 * List of standard colors
89 protected static final List
<@NonNull Color
> COLORS
= ImmutableList
.of(
90 new Color(Display
.getDefault(), 72, 120, 207),
91 new Color(Display
.getDefault(), 106, 204, 101),
92 new Color(Display
.getDefault(), 214, 95, 95),
93 new Color(Display
.getDefault(), 180, 124, 199),
94 new Color(Display
.getDefault(), 196, 173, 102),
95 new Color(Display
.getDefault(), 119, 190, 219)
99 * List of "light" colors (when unselected)
101 protected static final List
<@NonNull Color
> LIGHT_COLORS
= ImmutableList
.of(
102 new Color(Display
.getDefault(), 173, 195, 233),
103 new Color(Display
.getDefault(), 199, 236, 197),
104 new Color(Display
.getDefault(), 240, 196, 196),
105 new Color(Display
.getDefault(), 231, 213, 237),
106 new Color(Display
.getDefault(), 231, 222, 194),
107 new Color(Display
.getDefault(), 220, 238, 246)
111 * Time stamp formatter for intervals in the days range.
113 protected static final LamiTimeStampFormat DAYS_FORMATTER
= new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$
116 * Time stamp formatter for intervals in the hours range.
118 protected static final LamiTimeStampFormat HOURS_FORMATTER
= new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$
121 * Time stamp formatter for intervals in the minutes range.
123 protected static final LamiTimeStampFormat MINUTES_FORMATTER
= new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$
126 * Time stamp formatter for intervals in the seconds range.
128 protected static final LamiTimeStampFormat SECONDS_FORMATTER
= new LamiTimeStampFormat("ss"); //$NON-NLS-1$
131 * Time stamp formatter for intervals in the milliseconds range.
133 protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER
= new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$
136 * Decimal formatter to display nanoseconds as seconds.
138 protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER
= new DecimalUnitFormat(0.000000001);
141 * Default decimal formatter.
143 protected static final DecimalUnitFormat DECIMAL_FORMATTER
= new DecimalUnitFormat();
145 private final Listener fResizeListener
= event
-> {
146 /* Refresh the titles to fit the current chart size */
147 refreshDisplayTitles();
149 /* Refresh the Axis labels to fit the current chart size */
150 refreshDisplayLabels();
153 private final LamiResultTable fResultTable
;
154 private final LamiChartModel fChartModel
;
156 private final Chart fChart
;
158 private final String fChartTitle
;
159 private final String fXTitle
;
160 private final String fYTitle
;
162 private boolean fSelected
;
163 private Set
<Integer
> fSelection
;
166 * Creates a Viewer instance based on SWTChart.
169 * The parent composite to draw in.
171 * The result table containing the data from which to build the
174 * The information about the chart to build
176 public LamiXYChartViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel chartModel
) {
180 fResultTable
= resultTable
;
181 fChartModel
= chartModel
;
182 fSelection
= new HashSet
<>();
184 fChart
= new Chart(parent
, SWT
.NONE
);
185 fChart
.addListener(SWT
.Resize
, fResizeListener
);
187 /* Set Chart title */
188 fChartTitle
= fResultTable
.getTableClass().getTableTitle();
190 /* Set X axis title */
191 if (fChartModel
.getXSeriesColumns().size() == 1) {
193 * There is only 1 series in the chart, we will use its name as the
194 * Y axis (and hide the legend).
196 String seriesName
= getChartModel().getXSeriesColumns().get(0);
197 // The time duration formatter converts ns to s on the axis
198 if (NANOSECONDS_SYMBOL
.equals(getXAxisAspects().get(0).getUnits())) {
199 seriesName
= getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
201 fXTitle
= seriesName
;
204 * There are multiple series in the chart, if they all share the same
205 * units, display that.
207 long nbDiffAspects
= getXAxisAspects().stream()
208 .map(aspect
-> aspect
.getUnits())
212 String units
= getXAxisAspects().get(0).getUnits();
213 if (nbDiffAspects
== 1 && units
!= null) {
214 /* All aspects use the same unit type */
216 // The time duration formatter converts ns to s on the axis
217 if (NANOSECONDS_SYMBOL
.equals(units
)) {
218 units
= SECONDS_SYMBOL
;
220 fXTitle
= Messages
.LamiViewer_DefaultValueName
+ " (" + units
+ ')'; //$NON-NLS-1$
222 /* Various unit types, just say "Value" */
223 fXTitle
= nullToEmptyString(Messages
.LamiViewer_DefaultValueName
);
227 /* Set Y axis title */
228 if (fChartModel
.getYSeriesColumns().size() == 1) {
230 * There is only 1 series in the chart, we will use its name as the
231 * Y axis (and hide the legend).
233 String seriesName
= getChartModel().getYSeriesColumns().get(0);
234 // The time duration formatter converts ns to s on the axis
235 if (NANOSECONDS_SYMBOL
.equals(getYAxisAspects().get(0).getUnits())) {
236 seriesName
= getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
238 fYTitle
= seriesName
;
239 fChart
.getLegend().setVisible(false);
242 * There are multiple series in the chart, if they all share the same
243 * units, display that.
245 long nbDiffAspects
= getYAxisAspects().stream()
246 .map(aspect
-> aspect
.getUnits())
250 String units
= getYAxisAspects().get(0).getUnits();
251 if (nbDiffAspects
== 1 && units
!= null) {
252 /* All aspects use the same unit type */
254 // The time duration formatter converts ns to s on the axis
255 if (NANOSECONDS_SYMBOL
.equals(units
)) {
256 units
= SECONDS_SYMBOL
;
258 fYTitle
= Messages
.LamiViewer_DefaultValueName
+ " (" + units
+ ')'; //$NON-NLS-1$
260 /* Various unit types, just say "Value" */
261 fYTitle
= nullToEmptyString(Messages
.LamiViewer_DefaultValueName
);
264 /* Put legend at the bottom */
265 fChart
.getLegend().setPosition(SWT
.BOTTOM
);
268 /* Set all titles and labels font color to black */
269 fChart
.getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
270 fChart
.getAxisSet().getXAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
271 fChart
.getAxisSet().getYAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
272 fChart
.getAxisSet().getXAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
273 fChart
.getAxisSet().getYAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
275 /* Set X label 90 degrees */
276 fChart
.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
278 /* Refresh the titles to fit the current chart size */
279 refreshDisplayTitles();
281 fChart
.addDisposeListener(e
-> {
282 /* Dispose resources of this class */
283 LamiXYChartViewer
.super.dispose();
288 * Util method to check if a list of aspects are all continuous.
291 * The list of aspects to check.
292 * @return true is all aspects are continuous, otherwise false.
294 protected static boolean areAspectsContinuous(List
<LamiTableEntryAspect
> axisAspects
) {
295 return axisAspects
.stream().allMatch(aspect
-> aspect
.isContinuous());
299 * Util method to check if a list of aspects are all time stamps.
302 * The list of aspects to check.
303 * @return true is all aspects are time stamps, otherwise false.
305 protected static boolean areAspectsTimeStamp(List
<LamiTableEntryAspect
> axisAspects
) {
306 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeStamp());
310 * Util method to check if a list of aspects are all time durations.
313 * The list of aspects to check.
314 * @return true is all aspects are time durations, otherwise false.
316 protected static boolean areAspectsTimeDuration(List
<LamiTableEntryAspect
> axisAspects
) {
317 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeDuration());
321 * Util method that will return a formatter based on the aspects linked to an axis
323 * If all aspects are time stamps, return a timestamp formatter tuned to the interval.
324 * If all aspects are time durations, return the nanoseconds to seconds formatter.
325 * Otherwise, return the generic decimal formatter.
328 * The list of aspects of the axis.
330 * The list of entries of the chart.
331 * @return a formatter for the axis.
333 protected static Format
getContinuousAxisFormatter(List
<LamiTableEntryAspect
> axisAspects
, List
<LamiTableEntry
> entries
) {
335 if (areAspectsTimeStamp(axisAspects
)) {
336 /* Set a TimeStamp formatter depending on the duration between the first and last value */
337 double max
= Double
.MIN_VALUE
;
338 double min
= Double
.MAX_VALUE
;
340 for (LamiTableEntry entry
: entries
) {
341 for (LamiTableEntryAspect aspect
: axisAspects
) {
342 Double current
= aspect
.resolveDouble(entry
);
343 if (current
!= null) {
344 max
= Math
.max(max
, current
);
345 min
= Math
.min(min
, current
);
349 long duration
= (long) max
- (long) min
;
351 if (duration
> TimeUnit
.DAYS
.toNanos(1)) {
352 return DAYS_FORMATTER
;
353 } else if (duration
> TimeUnit
.HOURS
.toNanos(1)) {
354 return HOURS_FORMATTER
;
355 } else if (duration
> TimeUnit
.MINUTES
.toNanos(1)) {
356 return MINUTES_FORMATTER
;
357 } else if (duration
> TimeUnit
.SECONDS
.toNanos(15)) {
358 return SECONDS_FORMATTER
;
360 return MILLISECONDS_FORMATTER
;
362 } else if (areAspectsTimeDuration(axisAspects
)) {
363 /* Set the time duration formatter */
364 return NANO_TO_SECS_FORMATTER
;
367 /* For other numeric aspects, use the default decimal unit formatter */
368 return DECIMAL_FORMATTER
;
373 * Get the chart result table.
375 * @return The chart result table.
377 protected LamiResultTable
getResultTable() {
382 * Get the chart model.
384 * @return The chart model.
386 protected LamiChartModel
getChartModel() {
391 * Get the chart object.
392 * @return The chart object.
394 protected Chart
getChart() {
399 * Is a selection made in the chart.
401 * @return true if there is a selection.
403 protected boolean isSelected() {
408 * Set the selection index.
410 * @param selection the index to select.
412 protected void setSelection(Set
<Integer
> selection
) {
413 fSelection
= selection
;
414 fSelected
= !selection
.isEmpty();
418 * Unset the chart selection.
420 protected void unsetSelection() {
426 * Get the current selection index.
428 * @return the current selection index.
430 protected Set
<Integer
> getSelection() {
435 public @Nullable Control
getControl() {
436 return fChart
.getParent();
440 public void refresh() {
441 Display
.getDefault().asyncExec(() -> {
442 if (!fChart
.isDisposed()) {
449 public void dispose() {
451 /* The control's DisposeListener will call super.dispose() */
455 * Get a list of all the aspect of the Y axis.
457 * @return The aspects for the Y axis
459 protected List
<LamiTableEntryAspect
> getYAxisAspects() {
461 List
<LamiTableEntryAspect
> yAxisAspects
= new ArrayList
<>();
463 for (String colName
: getChartModel().getYSeriesColumns()) {
464 yAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
471 * Get a list of all the aspect of the X axis.
473 * @return The aspects for the X axis
475 protected List
<LamiTableEntryAspect
> getXAxisAspects() {
477 List
<LamiTableEntryAspect
> xAxisAspects
= new ArrayList
<>();
479 for (String colName
: getChartModel().getXSeriesColumns()) {
480 xAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
487 * Set the ITitle object text to a substring of canonicalTitle that when
488 * rendered in the chart will fit maxPixelLength.
490 private void refreshDisplayTitle(ITitle title
, String canonicalTitle
, int maxPixelLength
) {
491 if (title
.isVisible()) {
493 String newTitle
= canonicalTitle
;
495 /* Get the title font */
496 Font font
= title
.getFont();
498 GC gc
= new GC(fParent
);
501 /* Get the length and height of the canonical title in pixels */
502 Point pixels
= gc
.stringExtent(canonicalTitle
);
505 * If the title is too long, generate a shortened version based on the
506 * average character width of the current font.
508 if (pixels
.x
> maxPixelLength
) {
509 int charwidth
= gc
.getFontMetrics().getAverageCharWidth();
513 int strLen
= ((maxPixelLength
/ charwidth
) - minimum
);
515 if (strLen
> minimum
) {
516 newTitle
= canonicalTitle
.substring(0, strLen
) + ELLIPSIS
;
522 title
.setText(newTitle
);
530 * Refresh the Chart, XAxis and YAxis titles to fit the current
533 private void refreshDisplayTitles() {
534 Rectangle chartRect
= fChart
.getClientArea();
535 Rectangle plotRect
= fChart
.getPlotArea().getClientArea();
537 ITitle chartTitle
= checkNotNull(fChart
.getTitle());
538 refreshDisplayTitle(chartTitle
, fChartTitle
, chartRect
.width
);
540 ITitle xTitle
= checkNotNull(fChart
.getAxisSet().getXAxis(0).getTitle());
541 refreshDisplayTitle(xTitle
, fXTitle
, plotRect
.width
);
543 ITitle yTitle
= checkNotNull(fChart
.getAxisSet().getYAxis(0).getTitle());
544 refreshDisplayTitle(yTitle
, fYTitle
, plotRect
.height
);
548 * Get the aspect with the given name
551 * The list of aspects to search into
553 * The name of the aspect we are looking for
554 * @return The corresponding aspect
556 protected static @Nullable LamiTableEntryAspect
getAspectFromName(List
<LamiTableEntryAspect
> aspects
, String aspectName
) {
557 for (LamiTableEntryAspect lamiTableEntryAspect
: aspects
) {
559 if (lamiTableEntryAspect
.getLabel().equals(aspectName
)) {
560 return lamiTableEntryAspect
;
568 * Refresh the axis labels to fit the current chart size.
570 protected abstract void refreshDisplayLabels();
575 protected void redraw() {
580 * Signal handler for selection update.
583 * The selection update signal
586 public void updateSelection(LamiSelectionUpdateSignal signal
) {
587 if (getResultTable().hashCode() != signal
.getSignalHash() || equals(signal
.getSource())) {
588 /* The signal is not for us */
591 setSelection(signal
.getEntryIndex());