4a0a776a4a5ff836f1a05c8aedb91049bdb42618
[deliverable/tracecompass.git] / analysis / org.eclipse.tracecompass.analysis.lami.ui / src / org / eclipse / tracecompass / internal / provisional / analysis / lami / ui / viewers / LamiXYChartViewer.java
1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson
3 *
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 *******************************************************************************/
9
10 package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers;
11
12 import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
13 import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
14
15 import java.text.Format;
16 import java.util.ArrayList;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.concurrent.TimeUnit;
21 import java.util.function.ToDoubleFunction;
22
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;
46
47 import com.google.common.collect.ImmutableList;
48
49 /**
50 * Abstract XYChart Viewer for LAMI views.
51 *
52 * @author Michael Jeanson
53 *
54 */
55 public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer {
56
57 /** Ellipsis character */
58 protected static final String ELLIPSIS = "…"; //$NON-NLS-1$
59
60 /**
61 * String representing unknown values. Can be present even in numerical
62 * aspects!
63 */
64 protected static final String UNKNOWN = "?"; //$NON-NLS-1$
65
66 /** Zero value */
67 protected static final double ZERO = 0.0;
68
69 /** Symbol for seconds (used in the custom ns -> s conversion) */
70 private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$
71
72 /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
73 private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$
74
75 /**
76 * Function to use to map Strings read from the data table to doubles for
77 * use in SWTChart series.
78 */
79 protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> {
80 if (str == null || str.equals(UNKNOWN)) {
81 return ZERO;
82 }
83 return Double.parseDouble(str);
84 };
85
86 /**
87 * List of standard colors
88 */
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)
96 );
97
98 /**
99 * List of "light" colors (when unselected)
100 */
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)
108 );
109
110 /**
111 * Time stamp formatter for intervals in the days range.
112 */
113 protected static final LamiTimeStampFormat DAYS_FORMATTER = new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$
114
115 /**
116 * Time stamp formatter for intervals in the hours range.
117 */
118 protected static final LamiTimeStampFormat HOURS_FORMATTER = new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$
119
120 /**
121 * Time stamp formatter for intervals in the minutes range.
122 */
123 protected static final LamiTimeStampFormat MINUTES_FORMATTER = new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$
124
125 /**
126 * Time stamp formatter for intervals in the seconds range.
127 */
128 protected static final LamiTimeStampFormat SECONDS_FORMATTER = new LamiTimeStampFormat("ss"); //$NON-NLS-1$
129
130 /**
131 * Time stamp formatter for intervals in the milliseconds range.
132 */
133 protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER = new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$
134
135 /**
136 * Decimal formatter to display nanoseconds as seconds.
137 */
138 protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new DecimalUnitFormat(0.000000001);
139
140 /**
141 * Default decimal formatter.
142 */
143 protected static final DecimalUnitFormat DECIMAL_FORMATTER = new DecimalUnitFormat();
144
145 private final Listener fResizeListener = event -> {
146 /* Refresh the titles to fit the current chart size */
147 refreshDisplayTitles();
148
149 /* Refresh the Axis labels to fit the current chart size */
150 refreshDisplayLabels();
151 };
152
153 private final LamiResultTable fResultTable;
154 private final LamiChartModel fChartModel;
155
156 private final Chart fChart;
157
158 private final String fChartTitle;
159 private final String fXTitle;
160 private final String fYTitle;
161
162 private boolean fSelected;
163 private Set<Integer> fSelection;
164
165 /**
166 * Creates a Viewer instance based on SWTChart.
167 *
168 * @param parent
169 * The parent composite to draw in.
170 * @param resultTable
171 * The result table containing the data from which to build the
172 * chart
173 * @param chartModel
174 * The information about the chart to build
175 */
176 public LamiXYChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) {
177 super(parent);
178
179 fParent = parent;
180 fResultTable = resultTable;
181 fChartModel = chartModel;
182 fSelection = new HashSet<>();
183
184 fChart = new Chart(parent, SWT.NONE);
185 fChart.addListener(SWT.Resize, fResizeListener);
186
187 /* Set Chart title */
188 fChartTitle = fResultTable.getTableClass().getTableTitle();
189
190 /* Set X axis title */
191 if (fChartModel.getXSeriesColumns().size() == 1) {
192 /*
193 * There is only 1 series in the chart, we will use its name as the
194 * Y axis (and hide the legend).
195 */
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$
200 }
201 fXTitle = seriesName;
202 } else {
203 /*
204 * There are multiple series in the chart, if they all share the same
205 * units, display that.
206 */
207 long nbDiffAspects = getXAxisAspects().stream()
208 .map(aspect -> aspect.getUnits())
209 .distinct()
210 .count();
211
212 String units = getXAxisAspects().get(0).getUnits();
213 if (nbDiffAspects == 1 && units != null) {
214 /* All aspects use the same unit type */
215
216 // The time duration formatter converts ns to s on the axis
217 if (NANOSECONDS_SYMBOL.equals(units)) {
218 units = SECONDS_SYMBOL;
219 }
220 fXTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$
221 } else {
222 /* Various unit types, just say "Value" */
223 fXTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName);
224 }
225 }
226
227 /* Set Y axis title */
228 if (fChartModel.getYSeriesColumns().size() == 1) {
229 /*
230 * There is only 1 series in the chart, we will use its name as the
231 * Y axis (and hide the legend).
232 */
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$
237 }
238 fYTitle = seriesName;
239 fChart.getLegend().setVisible(false);
240 } else {
241 /*
242 * There are multiple series in the chart, if they all share the same
243 * units, display that.
244 */
245 long nbDiffAspects = getYAxisAspects().stream()
246 .map(aspect -> aspect.getUnits())
247 .distinct()
248 .count();
249
250 String units = getYAxisAspects().get(0).getUnits();
251 if (nbDiffAspects == 1 && units != null) {
252 /* All aspects use the same unit type */
253
254 // The time duration formatter converts ns to s on the axis
255 if (NANOSECONDS_SYMBOL.equals(units)) {
256 units = SECONDS_SYMBOL;
257 }
258 fYTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$
259 } else {
260 /* Various unit types, just say "Value" */
261 fYTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName);
262 }
263
264 /* Put legend at the bottom */
265 fChart.getLegend().setPosition(SWT.BOTTOM);
266 }
267
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));
274
275 /* Set X label 90 degrees */
276 fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
277
278 /* Refresh the titles to fit the current chart size */
279 refreshDisplayTitles();
280
281 fChart.addDisposeListener(e -> {
282 /* Dispose resources of this class */
283 LamiXYChartViewer.super.dispose();
284 });
285 }
286
287 /**
288 * Util method to check if a list of aspects are all continuous.
289 *
290 * @param axisAspects
291 * The list of aspects to check.
292 * @return true is all aspects are continuous, otherwise false.
293 */
294 protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) {
295 return axisAspects.stream().allMatch(aspect -> aspect.isContinuous());
296 }
297
298 /**
299 * Util method to check if a list of aspects are all time stamps.
300 *
301 * @param axisAspects
302 * The list of aspects to check.
303 * @return true is all aspects are time stamps, otherwise false.
304 */
305 protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) {
306 return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp());
307 }
308
309 /**
310 * Util method to check if a list of aspects are all time durations.
311 *
312 * @param axisAspects
313 * The list of aspects to check.
314 * @return true is all aspects are time durations, otherwise false.
315 */
316 protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) {
317 return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration());
318 }
319
320 /**
321 * Util method that will return a formatter based on the aspects linked to an axis
322 *
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.
326 *
327 * @param axisAspects
328 * The list of aspects of the axis.
329 * @param entries
330 * The list of entries of the chart.
331 * @return a formatter for the axis.
332 */
333 protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries) {
334
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;
339
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);
346 }
347 }
348 }
349 long duration = (long) max - (long) min;
350
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;
359 } else {
360 return MILLISECONDS_FORMATTER;
361 }
362 } else if (areAspectsTimeDuration(axisAspects)) {
363 /* Set the time duration formatter */
364 return NANO_TO_SECS_FORMATTER;
365
366 } else {
367 /* For other numeric aspects, use the default decimal unit formatter */
368 return DECIMAL_FORMATTER;
369 }
370 }
371
372 /**
373 * Get the chart result table.
374 *
375 * @return The chart result table.
376 */
377 protected LamiResultTable getResultTable() {
378 return fResultTable;
379 }
380
381 /**
382 * Get the chart model.
383 *
384 * @return The chart model.
385 */
386 protected LamiChartModel getChartModel() {
387 return fChartModel;
388 }
389
390 /**
391 * Get the chart object.
392 * @return The chart object.
393 */
394 protected Chart getChart() {
395 return fChart;
396 }
397
398 /**
399 * Is a selection made in the chart.
400 *
401 * @return true if there is a selection.
402 */
403 protected boolean isSelected() {
404 return fSelected;
405 }
406
407 /**
408 * Set the selection index.
409 *
410 * @param selection the index to select.
411 */
412 protected void setSelection(Set<Integer> selection) {
413 fSelection = selection;
414 fSelected = !selection.isEmpty();
415 }
416
417 /**
418 * Unset the chart selection.
419 */
420 protected void unsetSelection() {
421 fSelection.clear();
422 fSelected = false;
423 }
424
425 /**
426 * Get the current selection index.
427 *
428 * @return the current selection index.
429 */
430 protected Set<Integer> getSelection() {
431 return fSelection;
432 }
433
434 @Override
435 public @Nullable Control getControl() {
436 return fChart.getParent();
437 }
438
439 @Override
440 public void refresh() {
441 Display.getDefault().asyncExec(() -> {
442 if (!fChart.isDisposed()) {
443 fChart.redraw();
444 }
445 });
446 }
447
448 @Override
449 public void dispose() {
450 fChart.dispose();
451 /* The control's DisposeListener will call super.dispose() */
452 }
453
454 /**
455 * Get a list of all the aspect of the Y axis.
456 *
457 * @return The aspects for the Y axis
458 */
459 protected List<LamiTableEntryAspect> getYAxisAspects() {
460
461 List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>();
462
463 for (String colName : getChartModel().getYSeriesColumns()) {
464 yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName)));
465 }
466
467 return yAxisAspects;
468 }
469
470 /**
471 * Get a list of all the aspect of the X axis.
472 *
473 * @return The aspects for the X axis
474 */
475 protected List<LamiTableEntryAspect> getXAxisAspects() {
476
477 List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>();
478
479 for (String colName : getChartModel().getXSeriesColumns()) {
480 xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName)));
481 }
482
483 return xAxisAspects;
484 }
485
486 /**
487 * Set the ITitle object text to a substring of canonicalTitle that when
488 * rendered in the chart will fit maxPixelLength.
489 */
490 private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) {
491 if (title.isVisible()) {
492
493 String newTitle = canonicalTitle;
494
495 /* Get the title font */
496 Font font = title.getFont();
497
498 GC gc = new GC(fParent);
499 gc.setFont(font);
500
501 /* Get the length and height of the canonical title in pixels */
502 Point pixels = gc.stringExtent(canonicalTitle);
503
504 /*
505 * If the title is too long, generate a shortened version based on the
506 * average character width of the current font.
507 */
508 if (pixels.x > maxPixelLength) {
509 int charwidth = gc.getFontMetrics().getAverageCharWidth();
510
511 int minimum = 3;
512
513 int strLen = ((maxPixelLength / charwidth) - minimum);
514
515 if (strLen > minimum) {
516 newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS;
517 } else {
518 newTitle = ELLIPSIS;
519 }
520 }
521
522 title.setText(newTitle);
523
524 // Cleanup
525 gc.dispose();
526 }
527 }
528
529 /**
530 * Refresh the Chart, XAxis and YAxis titles to fit the current
531 * chart size.
532 */
533 private void refreshDisplayTitles() {
534 Rectangle chartRect = fChart.getClientArea();
535 Rectangle plotRect = fChart.getPlotArea().getClientArea();
536
537 ITitle chartTitle = checkNotNull(fChart.getTitle());
538 refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width);
539
540 ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle());
541 refreshDisplayTitle(xTitle, fXTitle, plotRect.width);
542
543 ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle());
544 refreshDisplayTitle(yTitle, fYTitle, plotRect.height);
545 }
546
547 /**
548 * Get the aspect with the given name
549 *
550 * @param aspects
551 * The list of aspects to search into
552 * @param aspectName
553 * The name of the aspect we are looking for
554 * @return The corresponding aspect
555 */
556 protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) {
557 for (LamiTableEntryAspect lamiTableEntryAspect : aspects) {
558
559 if (lamiTableEntryAspect.getLabel().equals(aspectName)) {
560 return lamiTableEntryAspect;
561 }
562 }
563
564 return null;
565 }
566
567 /**
568 * Refresh the axis labels to fit the current chart size.
569 */
570 protected abstract void refreshDisplayLabels();
571
572 /**
573 * Redraw the chart.
574 */
575 protected void redraw() {
576 refresh();
577 }
578
579 /**
580 * Signal handler for selection update.
581 *
582 * @param signal
583 * The selection update signal
584 */
585 @TmfSignalHandler
586 public void updateSelection(LamiSelectionUpdateSignal signal) {
587 if (getResultTable().hashCode() != signal.getSignalHash() || equals(signal.getSource())) {
588 /* The signal is not for us */
589 return;
590 }
591 setSelection(signal.getEntryIndex());
592
593 redraw();
594 }
595 }
This page took 0.044739 seconds and 4 git commands to generate.