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
.util
.ArrayList
;
16 import java
.util
.Arrays
;
17 import java
.util
.Comparator
;
18 import java
.util
.HashMap
;
19 import java
.util
.HashSet
;
20 import java
.util
.Iterator
;
21 import java
.util
.List
;
24 import java
.util
.stream
.Stream
;
26 import org
.eclipse
.jdt
.annotation
.Nullable
;
27 import org
.eclipse
.swt
.SWT
;
28 import org
.eclipse
.swt
.events
.MouseAdapter
;
29 import org
.eclipse
.swt
.events
.MouseEvent
;
30 import org
.eclipse
.swt
.events
.PaintEvent
;
31 import org
.eclipse
.swt
.events
.PaintListener
;
32 import org
.eclipse
.swt
.graphics
.Color
;
33 import org
.eclipse
.swt
.graphics
.GC
;
34 import org
.eclipse
.swt
.graphics
.Point
;
35 import org
.eclipse
.swt
.graphics
.Rectangle
;
36 import org
.eclipse
.swt
.widgets
.Composite
;
37 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
38 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
39 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
.ChartType
;
40 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
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
.tmf
.core
.signal
.TmfSignalManager
;
44 import org
.swtchart
.IAxis
;
45 import org
.swtchart
.IAxisTick
;
46 import org
.swtchart
.IBarSeries
;
47 import org
.swtchart
.ISeries
;
48 import org
.swtchart
.ISeries
.SeriesType
;
49 import org
.swtchart
.Range
;
51 import com
.google
.common
.collect
.Iterators
;
54 * Bar chart Viewer for LAMI views.
56 * @author Alexandre Montplaisir
57 * @author Jonathan Rajotte-Julien
58 * @author Mathieu Desnoyers
60 public class LamiBarChartViewer
extends LamiXYChartViewer
{
62 private static final double LOGSCALE_EPSILON_FACTOR
= 100.0;
64 private class Mapping
{
65 final private @Nullable Integer fInternalValue
;
66 final private @Nullable Integer fModelValue
;
68 public Mapping(@Nullable Integer internalValue
, @Nullable Integer modelValue
) {
69 fInternalValue
= internalValue
;
70 fModelValue
= modelValue
;
73 public @Nullable Integer
getInternalValue() {
74 return fInternalValue
;
77 public @Nullable Integer
getModelValue() {
82 private final String
[] fCategories
;
83 private final Map
<ISeries
, List
<Mapping
>> fIndexPerSeriesMapping
;
84 private final Map
<LamiTableEntry
, Mapping
> fEntryToCategoriesMap
;
86 private LamiGraphRange fYInternalRange
= new LamiGraphRange(checkNotNull(BigDecimal
.ZERO
), checkNotNull(BigDecimal
.ONE
));
87 private LamiGraphRange fYExternalRange
;
91 * Creates a bar chart Viewer instance based on SWTChart.
94 * The parent composite to draw in.
96 * The result table containing the data from which to build the
99 * The information about the chart to build
101 public LamiBarChartViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel chartModel
) {
102 super(parent
, resultTable
, 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 List
<@Nullable String
> xCategories
= new ArrayList
<>();
123 for (int i
= 0; i
< entries
.size(); i
++) {
124 String string
= xAxisAspect
.resolveString(entries
.get(i
));
125 if (string
== null) {
126 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(null, i
));
129 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(xCategories
.size(), i
));
130 xCategories
.add(string
);
133 fCategories
= xCategories
.toArray(new String
[0]);
135 /* The y values range */
136 /* Clamp minimum to zero or negative value */
137 fYExternalRange
= getRange(yAxisAspects
, true);
140 * Log scale magic course 101:
142 * It uses the relative difference divided by a factor
143 * (100) to get as close as it can to the actual minimum but still a
144 * little bit smaller. This is used as a workaround of SWTCHART
145 * limitations regarding custom scale drawing in log scale mode, bogus
146 * representation of NaN double values and limited support of multiple
149 * This should be good enough for most users.
151 double min
= Double
.MAX_VALUE
;
152 double max
= Double
.MIN_VALUE
;
153 double logScaleEpsilon
= ZERO_DOUBLE
;
156 /* Find minimum and maximum values excluding <= 0 values */
157 for (LamiTableEntryAspect aspect
: yAxisAspects
) {
158 for (LamiTableEntry entry
: entries
) {
159 Number externalValue
= aspect
.resolveNumber(entry
);
160 if (externalValue
== null) {
163 Double value
= getInternalDoubleValue(externalValue
, fYInternalRange
, fYExternalRange
);
167 min
= Math
.min(min
, value
);
168 max
= Math
.max(max
, value
);
172 if (min
== Double
.MAX_VALUE
) {
173 /* Series are empty in log scale*/
177 double delta
= max
- min
;
178 logScaleEpsilon
= min
- ((min
* delta
) / (LOGSCALE_EPSILON_FACTOR
* max
));
181 for (LamiTableEntryAspect yAxisAspect
: yAxisAspects
) {
182 if (!yAxisAspect
.isContinuous() || yAxisAspect
.isTimeStamp()) {
183 /* Only plot continuous aspects */
187 List
<Double
> validXValues
= new ArrayList
<>();
188 List
<Double
> validYValues
= new ArrayList
<>();
189 List
<Mapping
> indexMapping
= new ArrayList
<>();
191 for (int i
= 0; i
< entries
.size(); i
++) {
192 Integer categoryIndex
= checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fInternalValue
;
194 if (categoryIndex
== null) {
195 /* Invalid value do not show */
199 Double yValue
= ZERO_DOUBLE
;
200 @Nullable Number number
= yAxisAspect
.resolveNumber(entries
.get(i
));
202 if (number
== null) {
204 * Null value for y is the same as zero since this is a bar
207 yValue
= ZERO_DOUBLE
;
209 yValue
= getInternalDoubleValue(number
, fYInternalRange
, fYExternalRange
);
212 if (logscale
&& yValue
<= ZERO_DOUBLE
) {
214 * Less or equal to 0 values can't be plotted on a log
215 * scale. We map them to the mean of the >=0 minimal value
216 * and the calculated log scale magic epsilon.
218 yValue
= (min
+ logScaleEpsilon
) / 2.0;
221 validXValues
.add(checkNotNull(categoryIndex
).doubleValue());
222 validYValues
.add(yValue
.doubleValue());
223 indexMapping
.add(new Mapping(categoryIndex
, checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fModelValue
));
226 String name
= yAxisAspect
.getLabel();
228 if (validXValues
.isEmpty() || validYValues
.isEmpty()) {
229 /* No need to plot an empty series */
233 IBarSeries barSeries
= (IBarSeries
) getChart().getSeriesSet().createSeries(SeriesType
.BAR
, name
);
234 barSeries
.setXSeries(validXValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
235 barSeries
.setYSeries(validYValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
236 fIndexPerSeriesMapping
.put(barSeries
, indexMapping
);
239 setBarSeriesColors();
241 /* Set all y axis logscale mode */
242 Stream
.of(getChart().getAxisSet().getYAxes()).forEach(axis
-> axis
.enableLogScale(logscale
));
244 /* Set the formatter on the Y axis */
245 IAxisTick yTick
= getChart().getAxisSet().getYAxis(0).getTick();
246 yTick
.setFormat(getContinuousAxisFormatter(yAxisAspects
, entries
, fYInternalRange
, fYExternalRange
));
249 * SWTChart workaround: SWTChart fiddles with tick mark visibility based
250 * on the fact that it can parse the label to double or not.
252 * If the label happens to be a double, it checks for the presence of
253 * that value in its own tick labels to decide if it should add it or
254 * not. If it happens that the parsed value is already present in its
255 * map, the tick gets a visibility of false.
257 * The X axis does not have this problem since SWTCHART checks on label
258 * angle, and if it is != 0 simply does no logic regarding visibility.
259 * So simply set a label angle of 1 to the axis.
261 yTick
.setTickLabelAngle(1);
263 /* Adjust the chart range */
264 getChart().getAxisSet().adjustRange();
266 if (logscale
&& logScaleEpsilon
!= max
) {
267 getChart().getAxisSet().getYAxis(0).setRange(new Range(logScaleEpsilon
, max
));
270 /* Once the chart is filled, refresh the axis labels */
271 refreshDisplayLabels();
273 /* Add mouse listener */
274 getChart().getPlotArea().addMouseListener(new LamiBarChartMouseDownListener());
276 /* Custom Painter listener to highlight the current selection */
277 getChart().getPlotArea().addPaintListener(new LamiBarChartPainterListener());
280 private final class LamiBarChartMouseDownListener
extends MouseAdapter
{
283 public void mouseDown(@Nullable MouseEvent event
) {
284 if (event
== null || event
.button
!= 1) {
288 boolean ctrlMode
= false;
289 int xMouseLocation
= event
.x
;
290 int yMouseLocation
= event
.y
;
292 Set
<Integer
> selections
;
293 if ((event
.stateMask
& SWT
.CTRL
) != 0) {
295 selections
= getSelection();
297 /* Reset selection state */
299 selections
= new HashSet
<>();
302 ISeries
[] series
= getChart().getSeriesSet().getSeries();
305 * Iterate over all series, get the rectangle bounds for each
306 * category, and find the category index under the mouse.
308 * Since categories map directly to the index of the fResultTable
309 * and that this table is immutable the index of the entry
310 * corresponds to the categories index. Signal to all LamiViewer and
311 * LamiView the update of selection.
313 for (ISeries oneSeries
: series
) {
314 IBarSeries barSerie
= ((IBarSeries
) oneSeries
);
315 Rectangle
[] recs
= barSerie
.getBounds();
317 for (int j
= 0; j
< recs
.length
; j
++) {
318 Rectangle rectangle
= recs
[j
];
319 if (rectangle
.contains(xMouseLocation
, yMouseLocation
)) {
320 int index
= getTableEntryIndexFromGraphIndex(checkNotNull(oneSeries
), j
);
321 if (!ctrlMode
|| (index
>= 0 && !selections
.remove(index
))) {
322 selections
.add(index
);
328 /* Save the current selection internally */
329 setSelection(selections
);
330 /* Signal all Lami viewers & views of the selection */
331 LamiSelectionUpdateSignal signal
= new LamiSelectionUpdateSignal(this,
332 selections
, getResultTable().hashCode());
333 TmfSignalManager
.dispatchSignal(signal
);
339 protected void redraw() {
340 setBarSeriesColors();
345 * Set the chart series colors according to the selection state. Use light
346 * colors when a selection is present.
348 private void setBarSeriesColors() {
349 Iterator
<Color
> colorsIt
;
352 colorsIt
= Iterators
.cycle(LIGHT_COLORS
);
354 colorsIt
= Iterators
.cycle(COLORS
);
357 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
358 ((IBarSeries
) series
).setBarColor(colorsIt
.next());
362 private final class LamiBarChartPainterListener
implements PaintListener
{
364 public void paintControl(@Nullable PaintEvent e
) {
365 if (e
== null || !isSelected()) {
369 Iterator
<Color
> colorsIt
= Iterators
.cycle(COLORS
);
372 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
373 Color color
= colorsIt
.next();
374 for (int index
: getSelection()) {
375 int graphIndex
= getGraphIndexFromTableEntryIndex(series
, index
);
376 if (graphIndex
< 0) {
381 Rectangle
[] bounds
= ((IBarSeries
) series
).getBounds();
382 if (bounds
.length
!= fCategories
.length
) {
384 * The plot is too cramped and SWTChart currently does
385 * its best on rectangle drawing and returns the
386 * rectangle that it is able to draw.
388 * For now we simply do not draw since it is really hard
389 * to see anyway. A better way to visualize the value
390 * would be a full cross for each selection based on
395 Rectangle rectangle
= bounds
[graphIndex
];
396 gc
.setBackground(color
);
397 gc
.fillRectangle(rectangle
);
404 protected void refreshDisplayLabels() {
405 /* Only if we have at least 1 category */
406 if (fCategories
.length
== 0) {
410 /* Only refresh if labels are visible */
411 IAxis xAxis
= getChart().getAxisSet().getXAxis(0);
412 if (!xAxis
.getTick().isVisible() || !xAxis
.isCategoryEnabled()) {
417 * Shorten all the labels to 5 characters plus "…" when the longest
418 * label length is more than 50% of the chart height.
421 Rectangle rect
= getChart().getClientArea();
422 int lengthLimit
= (int) (rect
.height
* 0.40);
424 GC gc
= new GC(fParent
);
425 gc
.setFont(xAxis
.getTick().getFont());
427 /* Find the longest category string */
428 String longestString
= Arrays
.stream(fCategories
).max(Comparator
.comparingInt(String
::length
)).orElse(fCategories
[0]);
430 /* Get the length and height of the longest label in pixels */
431 Point pixels
= gc
.stringExtent(longestString
);
433 // Completely arbitrary
436 String
[] displayCategories
= new String
[fCategories
.length
];
437 if (pixels
.x
> lengthLimit
) {
438 /* We have to cut down some strings */
439 for (int i
= 0; i
< fCategories
.length
; i
++) {
440 if (fCategories
[i
].length() > cutLen
) {
441 displayCategories
[i
] = fCategories
[i
].substring(0, cutLen
) + ELLIPSIS
;
443 displayCategories
[i
] = fCategories
[i
];
447 /* All strings should fit */
448 displayCategories
= Arrays
.copyOf(fCategories
, fCategories
.length
);
450 xAxis
.setCategorySeries(displayCategories
);
456 private int getTableEntryIndexFromGraphIndex(ISeries series
, int index
) {
457 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
458 if (indexes
== null || index
> indexes
.size() || index
< 0) {
462 Mapping mapping
= indexes
.get(index
);
463 Integer modelValue
= mapping
.getModelValue();
464 if (modelValue
!= null) {
465 return modelValue
.intValue();
470 private int getGraphIndexFromTableEntryIndex(ISeries series
, int index
) {
471 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
472 if (indexes
== null || index
< 0) {
476 int internalIndex
= -1;
477 for (Mapping mapping
: indexes
) {
478 if (mapping
.getModelValue() == index
) {
479 Integer internalValue
= mapping
.getInternalValue();
480 if (internalValue
!= null) {
481 internalIndex
= internalValue
.intValue();
486 return internalIndex
;