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
.util
.ArrayList
;
15 import java
.util
.Arrays
;
16 import java
.util
.Comparator
;
17 import java
.util
.HashMap
;
18 import java
.util
.HashSet
;
19 import java
.util
.Iterator
;
20 import java
.util
.List
;
23 import java
.util
.stream
.Stream
;
25 import org
.eclipse
.jdt
.annotation
.Nullable
;
26 import org
.eclipse
.swt
.SWT
;
27 import org
.eclipse
.swt
.events
.MouseAdapter
;
28 import org
.eclipse
.swt
.events
.MouseEvent
;
29 import org
.eclipse
.swt
.events
.PaintEvent
;
30 import org
.eclipse
.swt
.events
.PaintListener
;
31 import org
.eclipse
.swt
.graphics
.Color
;
32 import org
.eclipse
.swt
.graphics
.GC
;
33 import org
.eclipse
.swt
.graphics
.Point
;
34 import org
.eclipse
.swt
.graphics
.Rectangle
;
35 import org
.eclipse
.swt
.widgets
.Composite
;
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
.LamiChartModel
.ChartType
;
39 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
40 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
41 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
42 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalManager
;
43 import org
.swtchart
.IAxis
;
44 import org
.swtchart
.IAxisTick
;
45 import org
.swtchart
.IBarSeries
;
46 import org
.swtchart
.ISeries
;
47 import org
.swtchart
.ISeries
.SeriesType
;
48 import org
.swtchart
.Range
;
50 import com
.google
.common
.collect
.Iterators
;
53 * Bar chart Viewer for LAMI views.
55 * @author Alexandre Montplaisir
56 * @author Jonathan Rajotte-Julien
57 * @author Mathieu Desnoyers
59 public class LamiBarChartViewer
extends LamiXYChartViewer
{
61 private static final double LOGSCALE_EPSILON_FACTOR
= 100.0;
63 private class Mapping
{
64 final private @Nullable Integer fInternalValue
;
65 final private @Nullable Integer fModelValue
;
67 public Mapping(@Nullable Integer internalValue
, @Nullable Integer modelValue
) {
68 fInternalValue
= internalValue
;
69 fModelValue
= modelValue
;
72 public @Nullable Integer
getInternalValue() {
73 return fInternalValue
;
76 public @Nullable Integer
getModelValue() {
81 private final String
[] fCategories
;
82 private final Map
<ISeries
, List
<Mapping
>> fIndexPerSeriesMapping
;
83 private final Map
<LamiTableEntry
, Mapping
> fEntryToCategoriesMap
;
86 * Creates a bar chart Viewer instance based on SWTChart.
89 * The parent composite to draw in.
91 * The result table containing the data from which to build the
94 * The information about the chart to build
96 public LamiBarChartViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel chartModel
) {
97 super(parent
, resultTable
, chartModel
);
99 List
<LamiTableEntryAspect
> xAxisAspects
= getXAxisAspects();
100 List
<LamiTableEntryAspect
> yAxisAspects
= getYAxisAspects();
102 /* bar chart cannot deal with multiple X series */
103 if (getChartModel().getChartType() != ChartType
.BAR_CHART
&& xAxisAspects
.size() != 1) {
104 throw new IllegalArgumentException("Invalid configuration passed to a bar chart."); //$NON-NLS-1$
107 /* Enable categories */
108 getChart().getAxisSet().getXAxis(0).enableCategory(true);
110 LamiTableEntryAspect xAxisAspect
= xAxisAspects
.get(0);
111 List
<LamiTableEntry
> entries
= getResultTable().getEntries();
112 boolean logscale
= chartModel
.yAxisIsLog();
113 fIndexPerSeriesMapping
= new HashMap
<>();
114 fEntryToCategoriesMap
= new HashMap
<>();
116 /* Categories index mapping */
117 List
<@Nullable String
> xCategories
= new ArrayList
<>();
118 for (int i
= 0; i
< entries
.size(); i
++) {
119 String string
= xAxisAspect
.resolveString(entries
.get(i
));
120 if (string
== null) {
121 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(null, i
));
124 fEntryToCategoriesMap
.put(entries
.get(i
), new Mapping(xCategories
.size(), i
));
125 xCategories
.add(string
);
128 fCategories
= xCategories
.toArray(new String
[0]);
131 * Log scale magic course 101:
133 * It uses the relative difference divided by a factor
134 * (100) to get as close as it can to the actual minimum but still a
135 * little bit smaller. This is used as a workaround of SWTCHART
136 * limitations regarding custom scale drawing in log scale mode, bogus
137 * representation of NaN double values and limited support of multiple
140 * This should be good enough for most users.
142 double min
= Double
.MAX_VALUE
;
143 double max
= Double
.MIN_VALUE
;
144 double logScaleEpsilon
= ZERO
;
147 /* Find minimum and maximum values */
148 for (LamiTableEntryAspect aspect
: yAxisAspects
) {
149 for (LamiTableEntry entry
: entries
) {
150 Double value
= aspect
.resolveDouble(entry
);
151 if (value
== null || value
<= 0) {
154 min
= Math
.min(min
, value
);
155 max
= Math
.max(max
, value
);
159 double delta
= max
- min
;
160 logScaleEpsilon
= min
- ((min
* delta
) / (LOGSCALE_EPSILON_FACTOR
* max
));
163 for (LamiTableEntryAspect yAxisAspect
: yAxisAspects
) {
164 if (!yAxisAspect
.isContinuous() || yAxisAspect
.isTimeStamp()) {
165 /* Only plot continuous aspects */
169 List
<Double
> validXValues
= new ArrayList
<>();
170 List
<Double
> validYValues
= new ArrayList
<>();
171 List
<Mapping
> indexMapping
= new ArrayList
<>();
173 for (int i
= 0; i
< entries
.size(); i
++) {
174 Integer categoryIndex
= checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fInternalValue
;
175 Double yValue
= yAxisAspect
.resolveDouble(entries
.get(i
));
176 if (categoryIndex
== null) {
177 /* Invalid value do not show */
181 if (yValue
== null) {
183 * Null value for y is the same as zero since this is a bar
186 yValue
= Double
.valueOf(ZERO
);
189 if (logscale
&& yValue
<= ZERO
) {
191 * Less or equal to 0 values can't be plotted on a log
192 * scale. We map them to the mean of the >=0 minimal value
193 * and the calculated log scale magic epsilon.
195 yValue
= (min
+ logScaleEpsilon
) / 2.0;
198 validXValues
.add(checkNotNull(categoryIndex
).doubleValue());
199 validYValues
.add(yValue
.doubleValue());
200 indexMapping
.add(new Mapping(categoryIndex
, checkNotNull(fEntryToCategoriesMap
.get(checkNotNull(entries
.get(i
)))).fModelValue
));
203 String name
= yAxisAspect
.getLabel();
205 if (validXValues
.isEmpty() || validYValues
.isEmpty()) {
206 /* No need to plot an empty series */
210 IBarSeries barSeries
= (IBarSeries
) getChart().getSeriesSet().createSeries(SeriesType
.BAR
, name
);
211 barSeries
.setXSeries(validXValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
212 barSeries
.setYSeries(validYValues
.stream().mapToDouble(Double
::doubleValue
).toArray());
213 fIndexPerSeriesMapping
.put(barSeries
, indexMapping
);
216 setBarSeriesColors();
218 /* Set all y axis logscale mode */
219 Stream
.of(getChart().getAxisSet().getYAxes()).forEach(axis
-> axis
.enableLogScale(logscale
));
221 /* Set the formatter on the Y axis */
222 IAxisTick yTick
= getChart().getAxisSet().getYAxis(0).getTick();
223 yTick
.setFormat(getContinuousAxisFormatter(yAxisAspects
, entries
));
224 yTick
.setTickLabelAngle(1);
226 /* Adjust the chart range */
227 getChart().getAxisSet().adjustRange();
229 getChart().getAxisSet().getYAxis(0).setRange(new Range(logScaleEpsilon
, max
));
232 /* Once the chart is filled, refresh the axis labels */
233 refreshDisplayLabels();
235 /* Add mouse listener */
236 getChart().getPlotArea().addMouseListener(new LamiBarChartMouseDownListener());
238 /* Custom Painter listener to highlight the current selection */
239 getChart().getPlotArea().addPaintListener(new LamiBarChartPainterListener());
242 private final class LamiBarChartMouseDownListener
extends MouseAdapter
{
245 public void mouseDown(@Nullable MouseEvent event
) {
246 if (event
== null || event
.button
!= 1) {
250 boolean ctrlMode
= false;
251 int xMouseLocation
= event
.x
;
252 int yMouseLocation
= event
.y
;
254 Set
<Integer
> selections
;
255 if ((event
.stateMask
& SWT
.CTRL
) != 0) {
257 selections
= getSelection();
259 /* Reset selection state */
261 selections
= new HashSet
<>();
264 ISeries
[] series
= getChart().getSeriesSet().getSeries();
267 * Iterate over all series, get the rectangle bounds for each
268 * category, and find the category index under the mouse.
270 * Since categories map directly to the index of the fResultTable
271 * and that this table is immutable the index of the entry
272 * corresponds to the categories index. Signal to all LamiViewer and
273 * LamiView the update of selection.
275 for (ISeries oneSeries
: series
) {
276 IBarSeries barSerie
= ((IBarSeries
) oneSeries
);
277 Rectangle
[] recs
= barSerie
.getBounds();
279 for (int j
= 0; j
< recs
.length
; j
++) {
280 Rectangle rectangle
= recs
[j
];
281 if (rectangle
.contains(xMouseLocation
, yMouseLocation
)) {
282 int index
= getTableEntryIndexFromGraphIndex(checkNotNull(oneSeries
), j
);
283 if (!ctrlMode
|| (index
>= 0 && !selections
.remove(index
))) {
284 selections
.add(index
);
290 /* Save the current selection internally */
291 setSelection(selections
);
292 /* Signal all Lami viewers & views of the selection */
293 LamiSelectionUpdateSignal signal
= new LamiSelectionUpdateSignal(this,
294 selections
, getResultTable().hashCode());
295 TmfSignalManager
.dispatchSignal(signal
);
301 protected void redraw() {
302 setBarSeriesColors();
307 * Set the chart series colors according to the selection state. Use light
308 * colors when a selection is present.
310 private void setBarSeriesColors() {
311 Iterator
<Color
> colorsIt
;
314 colorsIt
= Iterators
.cycle(LIGHT_COLORS
);
316 colorsIt
= Iterators
.cycle(COLORS
);
319 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
320 ((IBarSeries
) series
).setBarColor(colorsIt
.next());
324 private final class LamiBarChartPainterListener
implements PaintListener
{
326 public void paintControl(@Nullable PaintEvent e
) {
327 if (e
== null || !isSelected()) {
331 Iterator
<Color
> colorsIt
= Iterators
.cycle(COLORS
);
334 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
335 Color color
= colorsIt
.next();
336 for (int index
: getSelection()) {
337 int graphIndex
= getGraphIndexFromTableEntryIndex(series
, index
);
338 if (graphIndex
< 0) {
343 Rectangle
[] bounds
= ((IBarSeries
) series
).getBounds();
344 if (bounds
.length
!= fCategories
.length
) {
346 * The plot is too cramped and SWTChart currently does
347 * its best on rectangle drawing and returns the
348 * rectangle that it is able to draw.
350 * For now we simply do not draw since it is really hard
351 * to see anyway. A better way to visualize the value
352 * would be a full cross for each selection based on
357 Rectangle rectangle
= bounds
[graphIndex
];
358 gc
.setBackground(color
);
359 gc
.fillRectangle(rectangle
);
366 protected void refreshDisplayLabels() {
367 /* Only if we have at least 1 category */
368 if (fCategories
.length
== 0) {
372 /* Only refresh if labels are visible */
373 IAxis xAxis
= getChart().getAxisSet().getXAxis(0);
374 if (!xAxis
.getTick().isVisible() || !xAxis
.isCategoryEnabled()) {
379 * Shorten all the labels to 5 characters plus "…" when the longest
380 * label length is more than 50% of the chart height.
383 Rectangle rect
= getChart().getClientArea();
384 int lengthLimit
= (int) (rect
.height
* 0.40);
386 GC gc
= new GC(fParent
);
387 gc
.setFont(xAxis
.getTick().getFont());
389 /* Find the longest category string */
390 String longestString
= Arrays
.stream(fCategories
).max(Comparator
.comparingInt(String
::length
)).orElse(fCategories
[0]);
392 /* Get the length and height of the longest label in pixels */
393 Point pixels
= gc
.stringExtent(longestString
);
395 // Completely arbitrary
398 String
[] displayCategories
= new String
[fCategories
.length
];
399 if (pixels
.x
> lengthLimit
) {
400 /* We have to cut down some strings */
401 for (int i
= 0; i
< fCategories
.length
; i
++) {
402 if (fCategories
[i
].length() > cutLen
) {
403 displayCategories
[i
] = fCategories
[i
].substring(0, cutLen
) + ELLIPSIS
;
405 displayCategories
[i
] = fCategories
[i
];
409 /* All strings should fit */
410 displayCategories
= Arrays
.copyOf(fCategories
, fCategories
.length
);
412 xAxis
.setCategorySeries(displayCategories
);
418 private int getTableEntryIndexFromGraphIndex(ISeries series
, int index
) {
419 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
420 if (indexes
== null || index
> indexes
.size() || index
< 0) {
424 Mapping mapping
= indexes
.get(index
);
425 Integer modelValue
= mapping
.getModelValue();
426 if (modelValue
!= null) {
427 return modelValue
.intValue();
432 private int getGraphIndexFromTableEntryIndex(ISeries series
, int index
) {
433 List
<Mapping
> indexes
= fIndexPerSeriesMapping
.get(series
);
434 if (indexes
== null || index
< 0) {
438 int internalIndex
= -1;
439 for (Mapping mapping
: indexes
) {
440 if (mapping
.getModelValue() == index
) {
441 Integer internalValue
= mapping
.getInternalValue();
442 if (internalValue
!= null) {
443 internalIndex
= internalValue
.intValue();
448 return internalIndex
;