analysis.lami: Split axis titles in label and units
[deliverable/tracecompass.git] / analysis / org.eclipse.tracecompass.analysis.lami.ui / src / org / eclipse / tracecompass / internal / provisional / analysis / lami / ui / viewers / LamiXYChartViewer.java
CommitLineData
4208b510
AM
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
10package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers;
11
12import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
13import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
14
5b973e7c 15import java.math.BigDecimal;
4208b510
AM
16import java.text.Format;
17import java.util.ArrayList;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Set;
21import java.util.concurrent.TimeUnit;
22import java.util.function.ToDoubleFunction;
23
24import org.eclipse.jdt.annotation.NonNull;
25import org.eclipse.jdt.annotation.Nullable;
26import org.eclipse.swt.SWT;
7710e6ed
JR
27import org.eclipse.swt.events.SelectionEvent;
28import org.eclipse.swt.events.SelectionListener;
4208b510
AM
29import org.eclipse.swt.graphics.Color;
30import org.eclipse.swt.graphics.Font;
31import org.eclipse.swt.graphics.GC;
7710e6ed 32import org.eclipse.swt.graphics.Image;
4208b510
AM
33import org.eclipse.swt.graphics.Point;
34import org.eclipse.swt.graphics.Rectangle;
35import org.eclipse.swt.widgets.Composite;
36import org.eclipse.swt.widgets.Control;
37import org.eclipse.swt.widgets.Display;
7710e6ed 38import org.eclipse.swt.widgets.Event;
4208b510 39import org.eclipse.swt.widgets.Listener;
7710e6ed
JR
40import org.eclipse.swt.widgets.ToolBar;
41import org.eclipse.swt.widgets.ToolItem;
4208b510
AM
42import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat;
43import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect;
44import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel;
45import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable;
46import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry;
5b973e7c
JR
47import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiDecimalUnitFormat;
48import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiTimeStampFormat;
4208b510
AM
49import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal;
50import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
51import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer;
7710e6ed
JR
52import org.eclipse.ui.ISharedImages;
53import org.eclipse.ui.PlatformUI;
4208b510
AM
54import org.swtchart.Chart;
55import org.swtchart.ITitle;
56
57import com.google.common.collect.ImmutableList;
58
59/**
60 * Abstract XYChart Viewer for LAMI views.
61 *
62 * @author Michael Jeanson
63 *
64 */
65public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer {
66
67 /** Ellipsis character */
68 protected static final String ELLIPSIS = "…"; //$NON-NLS-1$
69
70 /**
71 * String representing unknown values. Can be present even in numerical
72 * aspects!
73 */
74 protected static final String UNKNOWN = "?"; //$NON-NLS-1$
75
5b973e7c
JR
76 /** Zero long value */
77 protected static final long ZERO_LONG = 0L;
78 /** Zero double value */
79 protected static final double ZERO_DOUBLE = 0.0;
4208b510
AM
80
81 /**
82 * Function to use to map Strings read from the data table to doubles for
83 * use in SWTChart series.
84 */
85 protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> {
86 if (str == null || str.equals(UNKNOWN)) {
5b973e7c 87 return ZERO_LONG;
4208b510
AM
88 }
89 return Double.parseDouble(str);
90 };
91
92 /**
93 * List of standard colors
94 */
95 protected static final List<@NonNull Color> COLORS = ImmutableList.of(
96 new Color(Display.getDefault(), 72, 120, 207),
97 new Color(Display.getDefault(), 106, 204, 101),
98 new Color(Display.getDefault(), 214, 95, 95),
99 new Color(Display.getDefault(), 180, 124, 199),
100 new Color(Display.getDefault(), 196, 173, 102),
101 new Color(Display.getDefault(), 119, 190, 219)
102 );
103
104 /**
105 * List of "light" colors (when unselected)
106 */
107 protected static final List<@NonNull Color> LIGHT_COLORS = ImmutableList.of(
108 new Color(Display.getDefault(), 173, 195, 233),
109 new Color(Display.getDefault(), 199, 236, 197),
110 new Color(Display.getDefault(), 240, 196, 196),
111 new Color(Display.getDefault(), 231, 213, 237),
112 new Color(Display.getDefault(), 231, 222, 194),
113 new Color(Display.getDefault(), 220, 238, 246)
114 );
115
116 /**
117 * Time stamp formatter for intervals in the days range.
118 */
119 protected static final LamiTimeStampFormat DAYS_FORMATTER = new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$
120
121 /**
122 * Time stamp formatter for intervals in the hours range.
123 */
124 protected static final LamiTimeStampFormat HOURS_FORMATTER = new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$
125
126 /**
127 * Time stamp formatter for intervals in the minutes range.
128 */
129 protected static final LamiTimeStampFormat MINUTES_FORMATTER = new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$
130
131 /**
132 * Time stamp formatter for intervals in the seconds range.
133 */
134 protected static final LamiTimeStampFormat SECONDS_FORMATTER = new LamiTimeStampFormat("ss"); //$NON-NLS-1$
135
136 /**
137 * Time stamp formatter for intervals in the milliseconds range.
138 */
139 protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER = new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$
140
141 /**
142 * Decimal formatter to display nanoseconds as seconds.
143 */
5b973e7c 144 protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new LamiDecimalUnitFormat(0.000000001);
4208b510
AM
145
146 /**
147 * Default decimal formatter.
148 */
5b973e7c
JR
149 protected static final DecimalUnitFormat DECIMAL_FORMATTER = new LamiDecimalUnitFormat();
150
151 /** Symbol for seconds (used in the custom ns -> s conversion) */
152 private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$
153
154 /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
155 private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$
156
157 /** Maximum amount of digits that can be represented into a double */
158 private static final int BIG_DECIMAL_DIVISION_SCALE = 22;
4208b510
AM
159
160 private final Listener fResizeListener = event -> {
161 /* Refresh the titles to fit the current chart size */
162 refreshDisplayTitles();
163
164 /* Refresh the Axis labels to fit the current chart size */
165 refreshDisplayLabels();
166 };
167
168 private final LamiResultTable fResultTable;
169 private final LamiChartModel fChartModel;
170
171 private final Chart fChart;
172
173 private final String fChartTitle;
b7156f6b
MJ
174
175 private String fXLabel;
176 private @Nullable String fXUnits;
177
178 private String fYLabel;
179 private @Nullable String fYUnits;
4208b510
AM
180
181 private boolean fSelected;
182 private Set<Integer> fSelection;
183
7710e6ed
JR
184 private final ToolBar fToolBar;
185
4208b510
AM
186 /**
187 * Creates a Viewer instance based on SWTChart.
188 *
189 * @param parent
190 * The parent composite to draw in.
191 * @param resultTable
192 * The result table containing the data from which to build the
193 * chart
194 * @param chartModel
195 * The information about the chart to build
196 */
197 public LamiXYChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) {
198 super(parent);
199
200 fParent = parent;
201 fResultTable = resultTable;
202 fChartModel = chartModel;
203 fSelection = new HashSet<>();
204
b7156f6b
MJ
205 fXLabel = ""; //$NON-NLS-1$
206 fYLabel = ""; //$NON-NLS-1$
207
4208b510
AM
208 fChart = new Chart(parent, SWT.NONE);
209 fChart.addListener(SWT.Resize, fResizeListener);
210
211 /* Set Chart title */
212 fChartTitle = fResultTable.getTableClass().getTableTitle();
213
214 /* Set X axis title */
215 if (fChartModel.getXSeriesColumns().size() == 1) {
216 /*
217 * There is only 1 series in the chart, we will use its name as the
b7156f6b 218 * X axis.
4208b510 219 */
b7156f6b 220 innerSetXTitle(getXAxisAspects().get(0).getName(), getXAxisAspects().get(0).getUnits());
4208b510
AM
221 } else {
222 /*
223 * There are multiple series in the chart, if they all share the same
224 * units, display that.
225 */
8d9b1c04 226 long nbDiffAspectsUnits = getXAxisAspects().stream()
4208b510
AM
227 .map(aspect -> aspect.getUnits())
228 .distinct()
229 .count();
230
8d9b1c04
JR
231 long nbDiffAspectName = getXAxisAspects().stream()
232 .map(aspect -> aspect.getName())
233 .distinct()
234 .count();
235
236 String xBaseTitle = Messages.LamiViewer_DefaultValueName;
237 if (nbDiffAspectName == 1) {
238 xBaseTitle = getXAxisAspects().get(0).getName();
239 }
240
b7156f6b
MJ
241 String units = null;
242 if (nbDiffAspectsUnits == 1) {
4208b510 243 /* All aspects use the same unit type */
b7156f6b 244 units = getXAxisAspects().get(0).getUnits();
4208b510 245 }
b7156f6b
MJ
246
247 innerSetXTitle(xBaseTitle, units);
4208b510
AM
248 }
249
250 /* Set Y axis title */
251 if (fChartModel.getYSeriesColumns().size() == 1) {
252 /*
253 * There is only 1 series in the chart, we will use its name as the
254 * Y axis (and hide the legend).
255 */
b7156f6b
MJ
256 innerSetYTitle(getYAxisAspects().get(0).getName(), getYAxisAspects().get(0).getUnits());
257
258 /* Hide the legend */
4208b510
AM
259 fChart.getLegend().setVisible(false);
260 } else {
261 /*
262 * There are multiple series in the chart, if they all share the same
263 * units, display that.
264 */
8d9b1c04 265 long nbDiffAspectsUnits = getYAxisAspects().stream()
4208b510
AM
266 .map(aspect -> aspect.getUnits())
267 .distinct()
268 .count();
269
8d9b1c04
JR
270 long nbDiffAspectName = getYAxisAspects().stream()
271 .map(aspect -> aspect.getName())
272 .distinct()
273 .count();
274
275 String yBaseTitle = Messages.LamiViewer_DefaultValueName;
276 if (nbDiffAspectName == 1) {
277 yBaseTitle = getYAxisAspects().get(0).getName();
278 }
279
b7156f6b
MJ
280 String units = null;
281 if (nbDiffAspectsUnits == 1) {
4208b510 282 /* All aspects use the same unit type */
b7156f6b 283 units = getYAxisAspects().get(0).getUnits();
4208b510
AM
284 }
285
b7156f6b
MJ
286 innerSetYTitle(yBaseTitle, units);
287
4208b510
AM
288 /* Put legend at the bottom */
289 fChart.getLegend().setPosition(SWT.BOTTOM);
290 }
291
292 /* Set all titles and labels font color to black */
293 fChart.getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
294 fChart.getAxisSet().getXAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
295 fChart.getAxisSet().getYAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
296 fChart.getAxisSet().getXAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
297 fChart.getAxisSet().getYAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
298
299 /* Set X label 90 degrees */
300 fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
301
302 /* Refresh the titles to fit the current chart size */
303 refreshDisplayTitles();
304
7710e6ed
JR
305 fToolBar = createChartToolBar();
306
4208b510
AM
307 fChart.addDisposeListener(e -> {
308 /* Dispose resources of this class */
309 LamiXYChartViewer.super.dispose();
310 });
311 }
312
b7156f6b
MJ
313 /**
314 * Set the Y axis title and refresh the chart.
315 *
316 * @param label
317 * the label string.
318 * @param units
319 * the units string.
320 */
321 protected void setYTitle(@Nullable String label, @Nullable String units) {
322 innerSetYTitle(label, units);
323 }
324
325 private void innerSetYTitle(@Nullable String label, @Nullable String units) {
326 fYLabel = nullToEmptyString(label);
327 innerSetYUnits(units);
328 refreshDisplayTitles();
329 }
330
331 /**
332 * Set the units on the Y Axis title and refresh the chart.
333 *
334 * @param units
335 * the units string.
336 */
337 protected void setYUnits(@Nullable String units) {
338 innerSetYUnits(units);
339 }
340
341 private void innerSetYUnits(@Nullable String units) {
342 /*
343 * All time durations in the Lami protocol are nanoseconds, on the
344 * charts we use an axis formater that converts back to seconds as a
345 * base unit and then uses prefixes like nano and milli depending on the
346 * range.
347 *
348 * So set the units to seconds in the title to match the base unit of
349 * the formater.
350 */
351 if (NANOSECONDS_SYMBOL.equals(units)) {
352 fYUnits = SECONDS_SYMBOL;
353 } else {
354 fYUnits = units;
355 }
356 refreshDisplayTitles();
357 }
358
359 /**
360 * Get the Y axis title string.
361 *
362 * If the units is non-null, the title will be: "label (units)"
363 *
364 * If the units is null, the title will be: "label"
365 *
366 * @return the title of the Y axis.
367 */
368 protected String getYTitle() {
369 if (fYUnits == null) {
370 return fYLabel;
371 }
372 return fYLabel + " (" + fYUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$
373 }
374
375 /**
376 * Set the X axis title and refresh the chart.
377 *
378 * @param label
379 * the label string.
380 * @param units
381 * the units string.
382 */
383 protected void setXTitle(@Nullable String label, @Nullable String units) {
384 innerSetXTitle(label, units);
385 }
386
387 private void innerSetXTitle(@Nullable String label, @Nullable String units) {
388 fXLabel = nullToEmptyString(label);
389 innerSetXUnits(units);
390 refreshDisplayTitles();
391 }
392
393 /**
394 * Set the units on the X Axis title.
395 *
396 * @param units
397 * the units string
398 */
399 protected void setXUnits(@Nullable String units) {
400 innerSetXUnits(units);
401 }
402
403 private void innerSetXUnits(@Nullable String units) {
404 /* The time duration formatter converts ns to s on the axis */
405 if (NANOSECONDS_SYMBOL.equals(units)) {
406 fXUnits = SECONDS_SYMBOL;
407 } else {
408 fXUnits = units;
409 }
410 refreshDisplayTitles();
411 }
412
413 /**
414 * Get the X axis title string.
415 *
416 * If the units is non-null, the title will be: "label (units)"
417 *
418 * If the units is null, the title will be: "label"
419 *
420 * @return the title of the Y axis.
421 */
422 protected String getXTitle() {
423 if (fXUnits == null) {
424 return fXLabel;
425 }
426 return fXLabel + " (" + fXUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$
427 }
428
4208b510
AM
429 /**
430 * Util method to check if a list of aspects are all continuous.
431 *
432 * @param axisAspects
433 * The list of aspects to check.
434 * @return true is all aspects are continuous, otherwise false.
435 */
436 protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) {
437 return axisAspects.stream().allMatch(aspect -> aspect.isContinuous());
438 }
439
440 /**
441 * Util method to check if a list of aspects are all time stamps.
442 *
443 * @param axisAspects
444 * The list of aspects to check.
445 * @return true is all aspects are time stamps, otherwise false.
446 */
447 protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) {
448 return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp());
449 }
450
451 /**
452 * Util method to check if a list of aspects are all time durations.
453 *
454 * @param axisAspects
455 * The list of aspects to check.
456 * @return true is all aspects are time durations, otherwise false.
457 */
458 protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) {
459 return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration());
460 }
461
462 /**
463 * Util method that will return a formatter based on the aspects linked to an axis
464 *
465 * If all aspects are time stamps, return a timestamp formatter tuned to the interval.
466 * If all aspects are time durations, return the nanoseconds to seconds formatter.
467 * Otherwise, return the generic decimal formatter.
468 *
469 * @param axisAspects
470 * The list of aspects of the axis.
471 * @param entries
472 * The list of entries of the chart.
5b973e7c
JR
473 * @param internalRange
474 * The internal range for value transformation
475 * @param externalRange
476 * The external range for value transformation
4208b510
AM
477 * @return a formatter for the axis.
478 */
5b973e7c
JR
479 protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) {
480
481 Format formatter = DECIMAL_FORMATTER;
4208b510
AM
482
483 if (areAspectsTimeStamp(axisAspects)) {
484 /* Set a TimeStamp formatter depending on the duration between the first and last value */
5b973e7c
JR
485 BigDecimal max = new BigDecimal(Long.MIN_VALUE);
486 BigDecimal min = new BigDecimal(Long.MAX_VALUE);
4208b510
AM
487
488 for (LamiTableEntry entry : entries) {
489 for (LamiTableEntryAspect aspect : axisAspects) {
5b973e7c
JR
490 @Nullable Number number = aspect.resolveNumber(entry);
491 if (number != null) {
492 BigDecimal current = new BigDecimal(number.toString());
493 max = current.max(max);
494 min = current.min(min);
4208b510
AM
495 }
496 }
497 }
4208b510 498
5b973e7c 499 long duration = max.subtract(min).longValue();
4208b510 500 if (duration > TimeUnit.DAYS.toNanos(1)) {
5b973e7c 501 formatter = DAYS_FORMATTER;
4208b510 502 } else if (duration > TimeUnit.HOURS.toNanos(1)) {
5b973e7c 503 formatter = HOURS_FORMATTER;
4208b510 504 } else if (duration > TimeUnit.MINUTES.toNanos(1)) {
5b973e7c 505 formatter = MINUTES_FORMATTER;
4208b510 506 } else if (duration > TimeUnit.SECONDS.toNanos(15)) {
5b973e7c 507 formatter = SECONDS_FORMATTER;
4208b510 508 } else {
5b973e7c 509 formatter = MILLISECONDS_FORMATTER;
4208b510 510 }
5b973e7c
JR
511 ((LamiTimeStampFormat) formatter).setInternalRange(internalRange);
512 ((LamiTimeStampFormat) formatter).setExternalRange(externalRange);
513
4208b510 514 } else if (areAspectsTimeDuration(axisAspects)) {
5b973e7c
JR
515 /* Set the time duration formatter. */
516 formatter = NANO_TO_SECS_FORMATTER;
517 ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange);
518 ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange);
4208b510
AM
519
520 } else {
5b973e7c
JR
521 /*
522 * For other numeric aspects, use the default lami decimal unit
523 * formatter.
524 */
525 formatter = DECIMAL_FORMATTER;
526 ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange);
527 ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange);
4208b510 528 }
5b973e7c
JR
529
530 return formatter;
4208b510
AM
531 }
532
533 /**
534 * Get the chart result table.
535 *
536 * @return The chart result table.
537 */
538 protected LamiResultTable getResultTable() {
539 return fResultTable;
540 }
541
542 /**
543 * Get the chart model.
544 *
545 * @return The chart model.
546 */
547 protected LamiChartModel getChartModel() {
548 return fChartModel;
549 }
550
551 /**
552 * Get the chart object.
553 * @return The chart object.
554 */
555 protected Chart getChart() {
556 return fChart;
557 }
558
7710e6ed
JR
559 /**
560 * @return the toolBar
561 */
562 public ToolBar getToolBar() {
563 return fToolBar;
564 }
565
4208b510
AM
566 /**
567 * Is a selection made in the chart.
568 *
569 * @return true if there is a selection.
570 */
571 protected boolean isSelected() {
572 return fSelected;
573 }
574
575 /**
576 * Set the selection index.
577 *
578 * @param selection the index to select.
579 */
580 protected void setSelection(Set<Integer> selection) {
581 fSelection = selection;
582 fSelected = !selection.isEmpty();
583 }
584
585 /**
586 * Unset the chart selection.
587 */
588 protected void unsetSelection() {
589 fSelection.clear();
590 fSelected = false;
591 }
592
593 /**
594 * Get the current selection index.
595 *
596 * @return the current selection index.
597 */
598 protected Set<Integer> getSelection() {
599 return fSelection;
600 }
601
602 @Override
603 public @Nullable Control getControl() {
604 return fChart.getParent();
605 }
606
607 @Override
608 public void refresh() {
609 Display.getDefault().asyncExec(() -> {
610 if (!fChart.isDisposed()) {
611 fChart.redraw();
612 }
613 });
614 }
615
616 @Override
617 public void dispose() {
618 fChart.dispose();
619 /* The control's DisposeListener will call super.dispose() */
620 }
621
622 /**
623 * Get a list of all the aspect of the Y axis.
624 *
625 * @return The aspects for the Y axis
626 */
627 protected List<LamiTableEntryAspect> getYAxisAspects() {
628
629 List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>();
630
631 for (String colName : getChartModel().getYSeriesColumns()) {
632 yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName)));
633 }
634
635 return yAxisAspects;
636 }
637
638 /**
639 * Get a list of all the aspect of the X axis.
640 *
641 * @return The aspects for the X axis
642 */
643 protected List<LamiTableEntryAspect> getXAxisAspects() {
644
645 List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>();
646
647 for (String colName : getChartModel().getXSeriesColumns()) {
648 xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName)));
649 }
650
651 return xAxisAspects;
652 }
653
654 /**
655 * Set the ITitle object text to a substring of canonicalTitle that when
656 * rendered in the chart will fit maxPixelLength.
657 */
658 private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) {
659 if (title.isVisible()) {
660
661 String newTitle = canonicalTitle;
662
663 /* Get the title font */
664 Font font = title.getFont();
665
666 GC gc = new GC(fParent);
667 gc.setFont(font);
668
669 /* Get the length and height of the canonical title in pixels */
670 Point pixels = gc.stringExtent(canonicalTitle);
671
672 /*
673 * If the title is too long, generate a shortened version based on the
674 * average character width of the current font.
675 */
676 if (pixels.x > maxPixelLength) {
677 int charwidth = gc.getFontMetrics().getAverageCharWidth();
678
679 int minimum = 3;
680
681 int strLen = ((maxPixelLength / charwidth) - minimum);
682
683 if (strLen > minimum) {
684 newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS;
685 } else {
686 newTitle = ELLIPSIS;
687 }
688 }
689
690 title.setText(newTitle);
691
692 // Cleanup
693 gc.dispose();
694 }
695 }
696
697 /**
698 * Refresh the Chart, XAxis and YAxis titles to fit the current
699 * chart size.
700 */
701 private void refreshDisplayTitles() {
702 Rectangle chartRect = fChart.getClientArea();
703 Rectangle plotRect = fChart.getPlotArea().getClientArea();
704
705 ITitle chartTitle = checkNotNull(fChart.getTitle());
706 refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width);
707
708 ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle());
b7156f6b 709 refreshDisplayTitle(xTitle, getXTitle(), plotRect.width);
4208b510
AM
710
711 ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle());
b7156f6b 712 refreshDisplayTitle(yTitle, getYTitle(), plotRect.height);
4208b510
AM
713 }
714
715 /**
716 * Get the aspect with the given name
717 *
718 * @param aspects
719 * The list of aspects to search into
720 * @param aspectName
721 * The name of the aspect we are looking for
722 * @return The corresponding aspect
723 */
724 protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) {
725 for (LamiTableEntryAspect lamiTableEntryAspect : aspects) {
726
727 if (lamiTableEntryAspect.getLabel().equals(aspectName)) {
728 return lamiTableEntryAspect;
729 }
730 }
731
732 return null;
733 }
734
735 /**
736 * Refresh the axis labels to fit the current chart size.
737 */
738 protected abstract void refreshDisplayLabels();
739
740 /**
741 * Redraw the chart.
742 */
743 protected void redraw() {
744 refresh();
745 }
746
747 /**
748 * Signal handler for selection update.
749 *
750 * @param signal
751 * The selection update signal
752 */
753 @TmfSignalHandler
754 public void updateSelection(LamiSelectionUpdateSignal signal) {
755 if (getResultTable().hashCode() != signal.getSignalHash() || equals(signal.getSource())) {
756 /* The signal is not for us */
757 return;
758 }
759 setSelection(signal.getEntryIndex());
760
761 redraw();
762 }
7710e6ed
JR
763
764 /**
765 * Create a tool bar on top right of the chart. Contained actions:
766 * <ul>
767 * <li>Dispose the current viewer, also known as "Close the chart"</li>
768 * </ul>
769 *
770 * This tool bar should only appear when the mouse enters the composite.
771 *
772 * @return the tool bar
773 */
774 protected ToolBar createChartToolBar() {
775 Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE);
776 ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL);
777
778 /* Default state */
779 toolBar.moveAbove(null);
780 toolBar.setVisible(false);
781
782 /*
783 * Close chart button
784 */
785 ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH);
786 closeButton.setImage(removeImage);
787 closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip);
788 closeButton.addSelectionListener(new SelectionListener() {
789 @Override
790 public void widgetSelected(@Nullable SelectionEvent e) {
791 Composite parent = getParent();
792 dispose();
793 parent.layout();
794 }
795
796 @Override
797 public void widgetDefaultSelected(@Nullable SelectionEvent e) {
798 }
799 });
800
801 toolBar.pack();
802 toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0));
803
804 /* Visibility toggle filter */
805 Listener toolBarVisibilityToggleListener = e -> {
806 if (e.widget instanceof Control) {
807 Control control = (Control) e.widget;
808 Point display = control.toDisplay(e.x, e.y);
809 Point location = getChart().getParent().toControl(display);
810
811 /*
812 * Only set to visible if we are at the right location, in the
813 * right shell.
814 */
815 boolean visible = getChart().getBounds().contains(location) &&
816 control.getShell().equals(getChart().getShell());
817 getToolBar().setVisible(visible);
818 }
819 };
820
821 /* Filter to make sure we hide the toolbar if we exit the window */
822 Listener hideToolBarListener = (e -> getToolBar().setVisible(false));
823
824 /*
825 * Add the filters to the main Display, and remove them when we dispose
826 * the chart.
827 */
828 Display display = getChart().getDisplay();
829 display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener);
830 display.addFilter(SWT.MouseExit, hideToolBarListener);
831
832 getChart().addDisposeListener(e -> {
833 display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener);
834 display.removeFilter(SWT.MouseExit, hideToolBarListener);
835 });
836
837 /* Reposition the tool bar on resize */
838 getChart().addListener(SWT.Resize, new Listener() {
839 @Override
840 public void handleEvent(@Nullable Event event) {
841 toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0));
842 }
843 });
844
845 return toolBar;
846 }
5b973e7c
JR
847
848 /**
849 * Get a {@link LamiGraphRange} that covers all data points in the result
850 * table.
851 * <p>
852 * The returned range will be the minimum and maximum of the resolved values
853 * of the passed aspects for all result entries. If <code>clampToZero</code>
854 * is true, a positive minimum value will be clamped down to zero.
855 *
856 * @param aspects
857 * The aspects that the range will represent
858 * @param clampToZero
859 * If true, a positive minimum value will be clamped down to zero
860 * @return the range
861 */
862 protected LamiGraphRange getRange(List<LamiTableEntryAspect> aspects, boolean clampToZero) {
863 /* Find the minimum and maximum values */
864 BigDecimal min = new BigDecimal(Long.MAX_VALUE);
865 BigDecimal max = new BigDecimal(Long.MIN_VALUE);
866 for (LamiTableEntryAspect lamiTableEntryAspect : aspects) {
867 for (LamiTableEntry entry : getResultTable().getEntries()) {
868 @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry);
869 if (number != null) {
870 BigDecimal current = new BigDecimal(number.toString());
871 min = current.min(min);
872 max = current.max(max);
873 }
874 }
875 }
876
877 if (clampToZero) {
878 min.min(BigDecimal.ZERO);
879 }
880
881 /* Do not allow a range with a zero delta default to 1 */
882 if (max.equals(min)) {
883 max = min.add(BigDecimal.ONE);
884 }
885
886 return new LamiGraphRange(checkNotNull(min), checkNotNull(max));
887 }
888
889 /**
890 * Transform an external value into an internal value. Since SWTChart only
891 * support Double and Lami can pass Long values, loss of precision might
892 * happen. To minimize this, transform the raw values to an internal
893 * representation based on a linear transformation.
894 *
895 * The internal value =
896 *
897 * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) +
898 * internalMinimum
899 *
900 * @param number
901 * The number to transform
902 * @param internalRange
903 * The internal range definition to be used
904 * @param externalRange
905 * The external range definition to be used
906 * @return the transformed value in Double comprised inside the internal
907 * range
908 */
909 protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) {
910 BigDecimal value = new BigDecimal(number.toString());
911
912 if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) {
913 return internalRange.getMinimum().doubleValue();
914 }
915
916 BigDecimal internalValue = value
917 .subtract(externalRange.getMinimum())
918 .multiply(internalRange.getDelta())
919 .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN)
920 .add(internalRange.getMinimum());
921
922 return internalValue.doubleValue();
923 }
4208b510 924}
This page took 0.066721 seconds and 5 git commands to generate.