1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
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
;
14 import java
.math
.BigDecimal
;
15 import java
.text
.Format
;
16 import java
.util
.ArrayList
;
17 import java
.util
.Arrays
;
18 import java
.util
.Comparator
;
19 import java
.util
.HashMap
;
20 import java
.util
.HashSet
;
21 import java
.util
.Iterator
;
22 import java
.util
.List
;
25 import java
.util
.stream
.Stream
;
27 import org
.eclipse
.jdt
.annotation
.Nullable
;
28 import org
.eclipse
.swt
.SWT
;
29 import org
.eclipse
.swt
.events
.MouseAdapter
;
30 import org
.eclipse
.swt
.events
.MouseEvent
;
31 import org
.eclipse
.swt
.events
.PaintEvent
;
32 import org
.eclipse
.swt
.events
.PaintListener
;
33 import org
.eclipse
.swt
.graphics
.Color
;
34 import org
.eclipse
.swt
.graphics
.GC
;
35 import org
.eclipse
.swt
.graphics
.Point
;
36 import org
.eclipse
.swt
.graphics
.Rectangle
;
37 import org
.eclipse
.swt
.widgets
.Composite
;
38 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
39 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
40 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
.ChartType
;
41 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
42 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.views
.LamiReportViewTabPage
;
44 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalManager
;
45 import org
.swtchart
.IAxis
;
46 import org
.swtchart
.IAxisTick
;
47 import org
.swtchart
.IBarSeries
;
48 import org
.swtchart
.ISeries
;
49 import org
.swtchart
.ISeries
.SeriesType
;
50 import org
.swtchart
.Range
;
52 import com
.google
.common
.collect
.Iterators
;
55 * Bar chart Viewer for LAMI views.
57 * @author Alexandre Montplaisir
58 * @author Jonathan Rajotte-Julien
59 * @author Mathieu Desnoyers
61 public class LamiBarChartViewer
extends LamiXYChartViewer
{
63 private static final double LOGSCALE_EPSILON_FACTOR
= 100.0;
65 private class Mapping
{
66 final private @Nullable Integer fInternalValue
;
67 final private @Nullable Integer fModelValue
;
69 public Mapping(@Nullable Integer internalValue
, @Nullable Integer modelValue
) {
70 fInternalValue
= internalValue
;
71 fModelValue
= modelValue
;
74 public @Nullable Integer
getInternalValue() {
75 return fInternalValue
;
78 public @Nullable Integer
getModelValue() {
83 private final String
[] fCategories
;
84 private final Map
<ISeries
, List
<Mapping
>> fIndexPerSeriesMapping
;
85 private final Map
<LamiTableEntry
, Mapping
> fEntryToCategoriesMap
;
87 private LamiGraphRange fYInternalRange
= new LamiGraphRange(checkNotNull(BigDecimal
.ZERO
), checkNotNull(BigDecimal
.ONE
));
88 private LamiGraphRange fYExternalRange
;
92 * Creates a bar chart Viewer instance based on SWTChart.
95 * The parent composite to draw in.
97 * The {@link LamiReportViewTabPage} parent page
99 * The information about the chart to build
101 public LamiBarChartViewer(Composite parent
, LamiReportViewTabPage page
, LamiChartModel chartModel
) {
102 super(parent
, page
, chartModel
);
104 List
<LamiTableEntryAspect
> xAxisAspects
= getXAxisAspects();
105 List
<LamiTableEntryAspect
> yAxisAspects
= getYAxisAspects();
107 /* bar chart cannot deal with multiple X series */
108 if (getChartModel().getChartType() != ChartType
.BAR_CHART
&& xAxisAspects
.size() != 1) {
109 throw new IllegalArgumentException("Invalid configuration passed to a bar chart."); //$NON-NLS-1$
112 /* Enable categories */
113 getChart().getAxisSet().getXAxis(0).enableCategory(true);
115 LamiTableEntryAspect xAxisAspect
= xAxisAspects
.get(0);
116 List
<LamiTableEntry
> entries
= getResultTable().getEntries();
117 boolean logscale
= chartModel
.yAxisIsLog();
118 fIndexPerSeriesMapping
= new HashMap
<>();
119 fEntryToCategoriesMap
= new HashMap
<>();
121 /* Categories index mapping */
122 Format formatter
= null;
123 if (xAxisAspect
.isContinuous()) {
124 formatter
= getContinuousAxisFormatter(xAxisAspects
, entries
, null, null);
127 List
<@Nullable String
> xCategories
= new ArrayList
<>();
128 for (int i
= 0; i
< entries
.size(); i
++) {
129 String string
= xAxisAspect
.resolveString(entries
.get(i
));
130 if (string
== null) {
131 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(null, i
));
135 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(xCategories
.size(), i
));
136 if (formatter
!= null) {
137 string
= formatter
.format(xAxisAspect
.resolveNumber(entries
.get(i
)));
140 xCategories
.add(string
);
143 fCategories
= xCategories
.toArray(new String
[0]);
145 /* The y values range */
146 /* Clamp minimum to zero or negative value */
147 fYExternalRange
= getRange(yAxisAspects
, true);
150 * Log scale magic course 101:
152 * It uses the relative difference divided by a factor
153 * (100) to get as close as it can to the actual minimum but still a
154 * little bit smaller. This is used as a workaround of SWTCHART
155 * limitations regarding custom scale drawing in log scale mode, bogus
156 * representation of NaN double values and limited support of multiple
159 * This should be good enough for most users.
161 double min
= Double
.MAX_VALUE
;
162 double max
= Double
.MIN_VALUE
;
163 double logScaleEpsilon
= ZERO_DOUBLE
;
166 /* Find minimum and maximum values excluding <= 0 values */
167 for (LamiTableEntryAspect aspect
: yAxisAspects
) {
168 for (LamiTableEntry entry
: entries
) {
169 Number externalValue
= aspect
.resolveNumber(entry
);
170 if (externalValue
== null) {
173 Double value
= getInternalDoubleValue(externalValue
, fYInternalRange
, fYExternalRange
);
177 min
= Math
.min(min
, value
);
178 max
= Math
.max(max
, value
);
182 if (min
== Double
.MAX_VALUE
) {
183 /* Series are empty in log scale*/
187 double delta
= max
- min
;
188 logScaleEpsilon
= min
- ((min
* delta
) / (LOGSCALE_EPSILON_FACTOR
* max
));
191 for (LamiTableEntryAspect yAxisAspect
: yAxisAspects
) {
192 if (!yAxisAspect
.isContinuous() || yAxisAspect
.isTimeStamp()) {
193 /* Only plot continuous aspects */
197 List
<Double
> validXValues
= new ArrayList
<>();
198 List
<Double
> validYValues
= new ArrayList
<>();
199 List
<Mapping
> indexMapping
= new ArrayList
<>();
201 for (int i
= 0; i
< entries
.size(); i
++) {
202 Integer categoryIndex
= checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fInternalValue
;
204 if (categoryIndex
== null) {
205 /* Invalid value do not show */
209 Double yValue
= ZERO_DOUBLE
;
210 @Nullable Number number
= yAxisAspect
.resolveNumber(entries
.get(i
));
212 if (number
== null) {
214 * Null value for y is the same as zero since this is a bar
217 yValue
= ZERO_DOUBLE
;
219 yValue
= getInternalDoubleValue(number
, fYInternalRange
, fYExternalRange
);
222 if (logscale
&& yValue
<= ZERO_DOUBLE
) {
224 * Less or equal to 0 values can't be plotted on a log
225 * scale. We map them to the mean of the >=0 minimal value
226 * and the calculated log scale magic epsilon.
228 yValue
= (min
+ logScaleEpsilon
) / 2.0;
231 validXValues
.add(checkNotNull(categoryIndex
).doubleValue());
232 validYValues
.add(yValue
.doubleValue());
233 indexMapping
.add(new Mapping(categoryIndex
, checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fModelValue
));
236 String name
= yAxisAspect
.getLabel();
238 if (validXValues
.isEmpty() || validYValues
.isEmpty()) {
239 /* No need to plot an empty series */
243 IBarSeries barSeries
= (IBarSeries
) getChart().getSeriesSet().createSeries(SeriesType
.BAR
, name
);
244 barSeries
.setXSeries(validXValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
245 barSeries
.setYSeries(validYValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
246 fIndexPerSeriesMapping
.put(barSeries
, indexMapping
);
249 setBarSeriesColors();
251 /* Set all y axis logscale mode */
252 Stream
.of(getChart().getAxisSet().getYAxes()).forEach(axis
-> axis
.enableLogScale(logscale
));
254 /* Set the formatter on the Y axis */
255 IAxisTick yTick
= getChart().getAxisSet().getYAxis(0).getTick();
256 yTick
.setFormat(getContinuousAxisFormatter(yAxisAspects
, entries
, fYInternalRange
, fYExternalRange
));
259 * SWTChart workaround: SWTChart fiddles with tick mark visibility based
260 * on the fact that it can parse the label to double or not.
262 * If the label happens to be a double, it checks for the presence of
263 * that value in its own tick labels to decide if it should add it or
264 * not. If it happens that the parsed value is already present in its
265 * map, the tick gets a visibility of false.
267 * The X axis does not have this problem since SWTCHART checks on label
268 * angle, and if it is != 0 simply does no logic regarding visibility.
269 * So simply set a label angle of 1 to the axis.
271 yTick
.setTickLabelAngle(1);
273 /* Adjust the chart range */
274 getChart().getAxisSet().adjustRange();
276 if (logscale
&& logScaleEpsilon
!= max
) {
277 getChart().getAxisSet().getYAxis(0).setRange(new Range(logScaleEpsilon
, max
));
280 /* Once the chart is filled, refresh the axis labels */
281 refreshDisplayLabels();
283 /* Add mouse listener */
284 getChart().getPlotArea().addMouseListener(new LamiBarChartMouseDownListener());
286 /* Custom Painter listener to highlight the current selection */
287 getChart().getPlotArea().addPaintListener(new LamiBarChartPainterListener());
290 private final class LamiBarChartMouseDownListener
extends MouseAdapter
{
293 public void mouseDown(@Nullable MouseEvent event
) {
294 if (event
== null || event
.button
!= 1) {
298 boolean ctrlMode
= false;
299 int xMouseLocation
= event
.x
;
300 int yMouseLocation
= event
.y
;
302 Set
<Integer
> selections
;
303 if ((event
.stateMask
& SWT
.CTRL
) != 0) {
305 selections
= getSelection();
307 /* Reset selection state */
309 selections
= new HashSet
<>();
312 ISeries
[] series
= getChart().getSeriesSet().getSeries();
315 * Iterate over all series, get the rectangle bounds for each
316 * category, and find the category index under the mouse.
318 * Since categories map directly to the index of the fResultTable
319 * and that this table is immutable the index of the entry
320 * corresponds to the categories index. Signal to all LamiViewer and
321 * LamiView the update of selection.
323 for (ISeries oneSeries
: series
) {
324 IBarSeries barSerie
= ((IBarSeries
) oneSeries
);
325 Rectangle
[] recs
= barSerie
.getBounds();
327 for (int j
= 0; j
< recs
.length
; j
++) {
328 Rectangle rectangle
= recs
[j
];
329 if (rectangle
.contains(xMouseLocation
, yMouseLocation
)) {
330 int index
= getTableEntryIndexFromGraphIndex(checkNotNull(oneSeries
), j
);
331 if (!ctrlMode
|| (index
>= 0 && !selections
.remove(index
))) {
332 selections
.add(index
);
338 /* Save the current selection internally */
339 setSelection(selections
);
340 /* Signal all Lami viewers & views of the selection */
341 LamiSelectionUpdateSignal signal
= new LamiSelectionUpdateSignal(this,
342 selections
, getPage());
343 TmfSignalManager
.dispatchSignal(signal
);
349 protected void redraw() {
350 setBarSeriesColors();
355 * Set the chart series colors according to the selection state. Use light
356 * colors when a selection is present.
358 private void setBarSeriesColors() {
359 Iterator
<Color
> colorsIt
;
362 colorsIt
= Iterators
.cycle(LIGHT_COLORS
);
364 colorsIt
= Iterators
.cycle(COLORS
);
367 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
368 ((IBarSeries
) series
).setBarColor(colorsIt
.next());
372 private final class LamiBarChartPainterListener
implements PaintListener
{
374 public void paintControl(@Nullable PaintEvent e
) {
375 if (e
== null || !isSelected()) {
379 Iterator
<Color
> colorsIt
= Iterators
.cycle(COLORS
);
382 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
383 Color color
= colorsIt
.next();
384 for (int index
: getSelection()) {
385 int graphIndex
= getGraphIndexFromTableEntryIndex(series
, index
);
386 if (graphIndex
< 0) {
391 Rectangle
[] bounds
= ((IBarSeries
) series
).getBounds();
392 if (bounds
.length
!= fCategories
.length
) {
394 * The plot is too cramped and SWTChart currently does
395 * its best on rectangle drawing and returns the
396 * rectangle that it is able to draw.
398 * For now we simply do not draw since it is really hard
399 * to see anyway. A better way to visualize the value
400 * would be a full cross for each selection based on
405 Rectangle rectangle
= bounds
[graphIndex
];
406 gc
.setBackground(color
);
407 gc
.fillRectangle(rectangle
);
414 protected void refreshDisplayLabels() {
415 /* Only if we have at least 1 category */
416 if (fCategories
.length
== 0) {
420 /* Only refresh if labels are visible */
421 IAxis xAxis
= getChart().getAxisSet().getXAxis(0);
422 if (!xAxis
.getTick().isVisible() || !xAxis
.isCategoryEnabled()) {
427 * Shorten all the labels to 5 characters plus "…" when the longest
428 * label length is more than 50% of the chart height.
431 Rectangle rect
= getChart().getClientArea();
432 int lengthLimit
= (int) (rect
.height
* 0.40);
434 GC gc
= new GC(fParent
);
435 gc
.setFont(xAxis
.getTick().getFont());
437 /* Find the longest category string */
438 String longestString
= Arrays
.stream(fCategories
).max(Comparator
.comparingInt(String
::length
)).orElse(fCategories
[0]);
440 /* Get the length and height of the longest label in pixels */
441 Point pixels
= gc
.stringExtent(longestString
);
443 // Completely arbitrary
446 String
[] displayCategories
= new String
[fCategories
.length
];
447 if (pixels
.x
> lengthLimit
) {
448 /* We have to cut down some strings */
449 for (int i
= 0; i
< fCategories
.length
; i
++) {
450 if (fCategories
[i
].length() > cutLen
) {
451 displayCategories
[i
] = fCategories
[i
].substring(0, cutLen
) + ELLIPSIS
;
453 displayCategories
[i
] = fCategories
[i
];
457 /* All strings should fit */
458 displayCategories
= Arrays
.copyOf(fCategories
, fCategories
.length
);
460 xAxis
.setCategorySeries(displayCategories
);
466 private int getTableEntryIndexFromGraphIndex(ISeries series
, int index
) {
467 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
468 if (indexes
== null || index
> indexes
.size() || index
< 0) {
472 Mapping mapping
= indexes
.get(index
);
473 Integer modelValue
= mapping
.getModelValue();
474 if (modelValue
!= null) {
475 return modelValue
.intValue();
480 private int getGraphIndexFromTableEntryIndex(ISeries series
, int index
) {
481 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
482 if (indexes
== null || index
< 0) {
486 int internalIndex
= -1;
487 for (Mapping mapping
: indexes
) {
488 if (mapping
.getModelValue() == index
) {
489 Integer internalValue
= mapping
.getInternalValue();
490 if (internalValue
!= null) {
491 internalIndex
= internalValue
.intValue();
496 return internalIndex
;