bd48497410e701f49d3906381d6147ed01798bdb
[deliverable/tracecompass.git] / analysis / org.eclipse.tracecompass.analysis.lami.ui / src / org / eclipse / tracecompass / internal / provisional / analysis / lami / ui / viewers / LamiBarChartViewer.java
1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
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
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;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.stream.Stream;
24
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;
49
50 import com.google.common.collect.Iterators;
51
52 /**
53 * Bar chart Viewer for LAMI views.
54 *
55 * @author Alexandre Montplaisir
56 * @author Jonathan Rajotte-Julien
57 * @author Mathieu Desnoyers
58 */
59 public class LamiBarChartViewer extends LamiXYChartViewer {
60
61 private static final double LOGSCALE_EPSILON_FACTOR = 100.0;
62
63 private class Mapping {
64 final private @Nullable Integer fInternalValue;
65 final private @Nullable Integer fModelValue;
66
67 public Mapping(@Nullable Integer internalValue, @Nullable Integer modelValue) {
68 fInternalValue = internalValue;
69 fModelValue = modelValue;
70 }
71
72 public @Nullable Integer getInternalValue() {
73 return fInternalValue;
74 }
75
76 public @Nullable Integer getModelValue() {
77 return fModelValue;
78 }
79 }
80
81 private final String[] fCategories;
82 private final Map<ISeries, List<Mapping>> fIndexPerSeriesMapping;
83 private final Map<LamiTableEntry, Mapping> fEntryToCategoriesMap;
84
85 /**
86 * Creates a bar chart Viewer instance based on SWTChart.
87 *
88 * @param parent
89 * The parent composite to draw in.
90 * @param resultTable
91 * The result table containing the data from which to build the
92 * chart
93 * @param chartModel
94 * The information about the chart to build
95 */
96 public LamiBarChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) {
97 super(parent, resultTable, chartModel);
98
99 List<LamiTableEntryAspect> xAxisAspects = getXAxisAspects();
100 List<LamiTableEntryAspect> yAxisAspects = getYAxisAspects();
101
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$
105 }
106
107 /* Enable categories */
108 getChart().getAxisSet().getXAxis(0).enableCategory(true);
109
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<>();
115
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));
122 continue;
123 }
124 fEntryToCategoriesMap.put(entries.get(i), new Mapping(xCategories.size(), i));
125 xCategories.add(string);
126
127 }
128 fCategories = xCategories.toArray(new String[0]);
129
130 /*
131 * Log scale magic course 101:
132 *
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
138 * size series.
139 *
140 * This should be good enough for most users.
141 */
142 double min = Double.MAX_VALUE;
143 double max = Double.MIN_VALUE;
144 double logScaleEpsilon = ZERO;
145 if (logscale) {
146
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) {
152 continue;
153 }
154 min = Math.min(min, value);
155 max = Math.max(max, value);
156 }
157 }
158
159 double delta = max - min;
160 logScaleEpsilon = min - ((min * delta) / (LOGSCALE_EPSILON_FACTOR * max));
161 }
162
163 for (LamiTableEntryAspect yAxisAspect : yAxisAspects) {
164 if (!yAxisAspect.isContinuous() || yAxisAspect.isTimeStamp()) {
165 /* Only plot continuous aspects */
166 continue;
167 }
168
169 List<Double> validXValues = new ArrayList<>();
170 List<Double> validYValues = new ArrayList<>();
171 List<Mapping> indexMapping = new ArrayList<>();
172
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 */
178 continue;
179 }
180
181 if (yValue == null) {
182 /*
183 * Null value for y is the same as zero since this is a bar
184 * chart
185 */
186 yValue = Double.valueOf(ZERO);
187 }
188
189 if (logscale && yValue <= ZERO) {
190 /*
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.
194 */
195 yValue = (min + logScaleEpsilon) / 2.0;
196 }
197
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));
201 }
202
203 String name = yAxisAspect.getLabel();
204
205 if (validXValues.isEmpty() || validYValues.isEmpty()) {
206 /* No need to plot an empty series */
207 continue;
208 }
209
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);
214 }
215
216 setBarSeriesColors();
217
218 /* Set all y axis logscale mode */
219 Stream.of(getChart().getAxisSet().getYAxes()).forEach(axis -> axis.enableLogScale(logscale));
220
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);
225
226 /* Adjust the chart range */
227 getChart().getAxisSet().adjustRange();
228 if (logscale) {
229 getChart().getAxisSet().getYAxis(0).setRange(new Range(logScaleEpsilon, max));
230 }
231
232 /* Once the chart is filled, refresh the axis labels */
233 refreshDisplayLabels();
234
235 /* Add mouse listener */
236 getChart().getPlotArea().addMouseListener(new LamiBarChartMouseDownListener());
237
238 /* Custom Painter listener to highlight the current selection */
239 getChart().getPlotArea().addPaintListener(new LamiBarChartPainterListener());
240 }
241
242 private final class LamiBarChartMouseDownListener extends MouseAdapter {
243
244 @Override
245 public void mouseDown(@Nullable MouseEvent event) {
246 if (event == null || event.button != 1) {
247 return;
248 }
249
250 boolean ctrlMode = false;
251 int xMouseLocation = event.x;
252 int yMouseLocation = event.y;
253
254 Set<Integer> selections;
255 if ((event.stateMask & SWT.CTRL) != 0) {
256 ctrlMode = true;
257 selections = getSelection();
258 } else {
259 /* Reset selection state */
260 unsetSelection();
261 selections = new HashSet<>();
262 }
263
264 ISeries[] series = getChart().getSeriesSet().getSeries();
265
266 /*
267 * Iterate over all series, get the rectangle bounds for each
268 * category, and find the category index under the mouse.
269 *
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.
274 */
275 for (ISeries oneSeries : series) {
276 IBarSeries barSerie = ((IBarSeries) oneSeries);
277 Rectangle[] recs = barSerie.getBounds();
278
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);
285 }
286 }
287 }
288 }
289
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);
296 redraw();
297 }
298 }
299
300 @Override
301 protected void redraw() {
302 setBarSeriesColors();
303 super.redraw();
304 }
305
306 /**
307 * Set the chart series colors according to the selection state. Use light
308 * colors when a selection is present.
309 */
310 private void setBarSeriesColors() {
311 Iterator<Color> colorsIt;
312
313 if (isSelected()) {
314 colorsIt = Iterators.cycle(LIGHT_COLORS);
315 } else {
316 colorsIt = Iterators.cycle(COLORS);
317 }
318
319 for (ISeries series : getChart().getSeriesSet().getSeries()) {
320 ((IBarSeries) series).setBarColor(colorsIt.next());
321 }
322 }
323
324 private final class LamiBarChartPainterListener implements PaintListener {
325 @Override
326 public void paintControl(@Nullable PaintEvent e) {
327 if (e == null || !isSelected()) {
328 return;
329 }
330
331 Iterator<Color> colorsIt = Iterators.cycle(COLORS);
332 GC gc = e.gc;
333
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) {
339 /* Invalid index */
340 continue;
341 }
342
343 Rectangle[] bounds = ((IBarSeries) series).getBounds();
344 if (bounds.length != fCategories.length) {
345 /*
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.
349 *
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
353 * their coordinates.
354 */
355 continue;
356 }
357 Rectangle rectangle = bounds[graphIndex];
358 gc.setBackground(color);
359 gc.fillRectangle(rectangle);
360 }
361 }
362 }
363 }
364
365 @Override
366 protected void refreshDisplayLabels() {
367 /* Only if we have at least 1 category */
368 if (fCategories.length == 0) {
369 return;
370 }
371
372 /* Only refresh if labels are visible */
373 IAxis xAxis = getChart().getAxisSet().getXAxis(0);
374 if (!xAxis.getTick().isVisible() || !xAxis.isCategoryEnabled()) {
375 return;
376 }
377
378 /*
379 * Shorten all the labels to 5 characters plus "…" when the longest
380 * label length is more than 50% of the chart height.
381 */
382
383 Rectangle rect = getChart().getClientArea();
384 int lengthLimit = (int) (rect.height * 0.40);
385
386 GC gc = new GC(fParent);
387 gc.setFont(xAxis.getTick().getFont());
388
389 /* Find the longest category string */
390 String longestString = Arrays.stream(fCategories).max(Comparator.comparingInt(String::length)).orElse(fCategories[0]);
391
392 /* Get the length and height of the longest label in pixels */
393 Point pixels = gc.stringExtent(longestString);
394
395 // Completely arbitrary
396 int cutLen = 5;
397
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;
404 } else {
405 displayCategories[i] = fCategories[i];
406 }
407 }
408 } else {
409 /* All strings should fit */
410 displayCategories = Arrays.copyOf(fCategories, fCategories.length);
411 }
412 xAxis.setCategorySeries(displayCategories);
413
414 /* Cleanup */
415 gc.dispose();
416 }
417
418 private int getTableEntryIndexFromGraphIndex(ISeries series, int index) {
419 List<Mapping> indexes = fIndexPerSeriesMapping.get(series);
420 if (indexes == null || index > indexes.size() || index < 0) {
421 return -1;
422 }
423
424 Mapping mapping = indexes.get(index);
425 Integer modelValue = mapping.getModelValue();
426 if (modelValue != null) {
427 return modelValue.intValue();
428 }
429 return -1;
430 }
431
432 private int getGraphIndexFromTableEntryIndex(ISeries series, int index) {
433 List<Mapping> indexes = fIndexPerSeriesMapping.get(series);
434 if (indexes == null || index < 0) {
435 return -1;
436 }
437
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();
444 break;
445 }
446 }
447 }
448 return internalIndex;
449 }
450 }
This page took 0.041105 seconds and 4 git commands to generate.