analysis.lami: correctly handle Number (double, long etc.) type graphing
[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.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;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.stream.Stream;
25
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;
50
51 import com.google.common.collect.Iterators;
52
53 /**
54 * Bar chart Viewer for LAMI views.
55 *
56 * @author Alexandre Montplaisir
57 * @author Jonathan Rajotte-Julien
58 * @author Mathieu Desnoyers
59 */
60 public class LamiBarChartViewer extends LamiXYChartViewer {
61
62 private static final double LOGSCALE_EPSILON_FACTOR = 100.0;
63
64 private class Mapping {
65 final private @Nullable Integer fInternalValue;
66 final private @Nullable Integer fModelValue;
67
68 public Mapping(@Nullable Integer internalValue, @Nullable Integer modelValue) {
69 fInternalValue = internalValue;
70 fModelValue = modelValue;
71 }
72
73 public @Nullable Integer getInternalValue() {
74 return fInternalValue;
75 }
76
77 public @Nullable Integer getModelValue() {
78 return fModelValue;
79 }
80 }
81
82 private final String[] fCategories;
83 private final Map<ISeries, List<Mapping>> fIndexPerSeriesMapping;
84 private final Map<LamiTableEntry, Mapping> fEntryToCategoriesMap;
85
86 private LamiGraphRange fYInternalRange = new LamiGraphRange(checkNotNull(BigDecimal.ZERO), checkNotNull(BigDecimal.ONE));
87 private LamiGraphRange fYExternalRange;
88
89
90 /**
91 * Creates a bar chart Viewer instance based on SWTChart.
92 *
93 * @param parent
94 * The parent composite to draw in.
95 * @param resultTable
96 * The result table containing the data from which to build the
97 * chart
98 * @param chartModel
99 * The information about the chart to build
100 */
101 public LamiBarChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) {
102 super(parent, resultTable, chartModel);
103
104 List<LamiTableEntryAspect> xAxisAspects = getXAxisAspects();
105 List<LamiTableEntryAspect> yAxisAspects = getYAxisAspects();
106
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$
110 }
111
112 /* Enable categories */
113 getChart().getAxisSet().getXAxis(0).enableCategory(true);
114
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<>();
120
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));
127 continue;
128 }
129 fEntryToCategoriesMap.put(entries.get(i), new Mapping(xCategories.size(), i));
130 xCategories.add(string);
131
132 }
133 fCategories = xCategories.toArray(new String[0]);
134
135 /* The y values range */
136 /* Clamp minimum to zero or negative value */
137 fYExternalRange = getRange(yAxisAspects, true);
138
139 /*
140 * Log scale magic course 101:
141 *
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
147 * size series.
148 *
149 * This should be good enough for most users.
150 */
151 double min = Double.MAX_VALUE;
152 double max = Double.MIN_VALUE;
153 double logScaleEpsilon = ZERO_DOUBLE;
154 if (logscale) {
155
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) {
161 continue;
162 }
163 Double value = getInternalDoubleValue(externalValue, fYInternalRange, fYExternalRange);
164 if (value <= 0) {
165 continue;
166 }
167 min = Math.min(min, value);
168 max = Math.max(max, value);
169 }
170 }
171
172 if (min == Double.MAX_VALUE) {
173 /* Series are empty in log scale*/
174 return;
175 }
176
177 double delta = max - min;
178 logScaleEpsilon = min - ((min * delta) / (LOGSCALE_EPSILON_FACTOR * max));
179 }
180
181 for (LamiTableEntryAspect yAxisAspect : yAxisAspects) {
182 if (!yAxisAspect.isContinuous() || yAxisAspect.isTimeStamp()) {
183 /* Only plot continuous aspects */
184 continue;
185 }
186
187 List<Double> validXValues = new ArrayList<>();
188 List<Double> validYValues = new ArrayList<>();
189 List<Mapping> indexMapping = new ArrayList<>();
190
191 for (int i = 0; i < entries.size(); i++) {
192 Integer categoryIndex = checkNotNull(fEntryToCategoriesMap.get(checkNotNull(entries.get(i)))).fInternalValue;
193
194 if (categoryIndex == null) {
195 /* Invalid value do not show */
196 continue;
197 }
198
199 Double yValue = ZERO_DOUBLE;
200 @Nullable Number number = yAxisAspect.resolveNumber(entries.get(i));
201
202 if (number == null) {
203 /*
204 * Null value for y is the same as zero since this is a bar
205 * chart
206 */
207 yValue = ZERO_DOUBLE;
208 } else {
209 yValue = getInternalDoubleValue(number, fYInternalRange, fYExternalRange);
210 }
211
212 if (logscale && yValue <= ZERO_DOUBLE) {
213 /*
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.
217 */
218 yValue = (min + logScaleEpsilon) / 2.0;
219 }
220
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));
224 }
225
226 String name = yAxisAspect.getLabel();
227
228 if (validXValues.isEmpty() || validYValues.isEmpty()) {
229 /* No need to plot an empty series */
230 continue;
231 }
232
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);
237 }
238
239 setBarSeriesColors();
240
241 /* Set all y axis logscale mode */
242 Stream.of(getChart().getAxisSet().getYAxes()).forEach(axis -> axis.enableLogScale(logscale));
243
244 /* Set the formatter on the Y axis */
245 IAxisTick yTick = getChart().getAxisSet().getYAxis(0).getTick();
246 yTick.setFormat(getContinuousAxisFormatter(yAxisAspects, entries, fYInternalRange, fYExternalRange));
247
248 /*
249 * SWTChart workaround: SWTChart fiddles with tick mark visibility based
250 * on the fact that it can parse the label to double or not.
251 *
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.
256 *
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.
260 */
261 yTick.setTickLabelAngle(1);
262
263 /* Adjust the chart range */
264 getChart().getAxisSet().adjustRange();
265
266 if (logscale && logScaleEpsilon != max) {
267 getChart().getAxisSet().getYAxis(0).setRange(new Range(logScaleEpsilon, max));
268 }
269
270 /* Once the chart is filled, refresh the axis labels */
271 refreshDisplayLabels();
272
273 /* Add mouse listener */
274 getChart().getPlotArea().addMouseListener(new LamiBarChartMouseDownListener());
275
276 /* Custom Painter listener to highlight the current selection */
277 getChart().getPlotArea().addPaintListener(new LamiBarChartPainterListener());
278 }
279
280 private final class LamiBarChartMouseDownListener extends MouseAdapter {
281
282 @Override
283 public void mouseDown(@Nullable MouseEvent event) {
284 if (event == null || event.button != 1) {
285 return;
286 }
287
288 boolean ctrlMode = false;
289 int xMouseLocation = event.x;
290 int yMouseLocation = event.y;
291
292 Set<Integer> selections;
293 if ((event.stateMask & SWT.CTRL) != 0) {
294 ctrlMode = true;
295 selections = getSelection();
296 } else {
297 /* Reset selection state */
298 unsetSelection();
299 selections = new HashSet<>();
300 }
301
302 ISeries[] series = getChart().getSeriesSet().getSeries();
303
304 /*
305 * Iterate over all series, get the rectangle bounds for each
306 * category, and find the category index under the mouse.
307 *
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.
312 */
313 for (ISeries oneSeries : series) {
314 IBarSeries barSerie = ((IBarSeries) oneSeries);
315 Rectangle[] recs = barSerie.getBounds();
316
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);
323 }
324 }
325 }
326 }
327
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);
334 redraw();
335 }
336 }
337
338 @Override
339 protected void redraw() {
340 setBarSeriesColors();
341 super.redraw();
342 }
343
344 /**
345 * Set the chart series colors according to the selection state. Use light
346 * colors when a selection is present.
347 */
348 private void setBarSeriesColors() {
349 Iterator<Color> colorsIt;
350
351 if (isSelected()) {
352 colorsIt = Iterators.cycle(LIGHT_COLORS);
353 } else {
354 colorsIt = Iterators.cycle(COLORS);
355 }
356
357 for (ISeries series : getChart().getSeriesSet().getSeries()) {
358 ((IBarSeries) series).setBarColor(colorsIt.next());
359 }
360 }
361
362 private final class LamiBarChartPainterListener implements PaintListener {
363 @Override
364 public void paintControl(@Nullable PaintEvent e) {
365 if (e == null || !isSelected()) {
366 return;
367 }
368
369 Iterator<Color> colorsIt = Iterators.cycle(COLORS);
370 GC gc = e.gc;
371
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) {
377 /* Invalid index */
378 continue;
379 }
380
381 Rectangle[] bounds = ((IBarSeries) series).getBounds();
382 if (bounds.length != fCategories.length) {
383 /*
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.
387 *
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
391 * their coordinates.
392 */
393 continue;
394 }
395 Rectangle rectangle = bounds[graphIndex];
396 gc.setBackground(color);
397 gc.fillRectangle(rectangle);
398 }
399 }
400 }
401 }
402
403 @Override
404 protected void refreshDisplayLabels() {
405 /* Only if we have at least 1 category */
406 if (fCategories.length == 0) {
407 return;
408 }
409
410 /* Only refresh if labels are visible */
411 IAxis xAxis = getChart().getAxisSet().getXAxis(0);
412 if (!xAxis.getTick().isVisible() || !xAxis.isCategoryEnabled()) {
413 return;
414 }
415
416 /*
417 * Shorten all the labels to 5 characters plus "…" when the longest
418 * label length is more than 50% of the chart height.
419 */
420
421 Rectangle rect = getChart().getClientArea();
422 int lengthLimit = (int) (rect.height * 0.40);
423
424 GC gc = new GC(fParent);
425 gc.setFont(xAxis.getTick().getFont());
426
427 /* Find the longest category string */
428 String longestString = Arrays.stream(fCategories).max(Comparator.comparingInt(String::length)).orElse(fCategories[0]);
429
430 /* Get the length and height of the longest label in pixels */
431 Point pixels = gc.stringExtent(longestString);
432
433 // Completely arbitrary
434 int cutLen = 5;
435
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;
442 } else {
443 displayCategories[i] = fCategories[i];
444 }
445 }
446 } else {
447 /* All strings should fit */
448 displayCategories = Arrays.copyOf(fCategories, fCategories.length);
449 }
450 xAxis.setCategorySeries(displayCategories);
451
452 /* Cleanup */
453 gc.dispose();
454 }
455
456 private int getTableEntryIndexFromGraphIndex(ISeries series, int index) {
457 List<Mapping> indexes = fIndexPerSeriesMapping.get(series);
458 if (indexes == null || index > indexes.size() || index < 0) {
459 return -1;
460 }
461
462 Mapping mapping = indexes.get(index);
463 Integer modelValue = mapping.getModelValue();
464 if (modelValue != null) {
465 return modelValue.intValue();
466 }
467 return -1;
468 }
469
470 private int getGraphIndexFromTableEntryIndex(ISeries series, int index) {
471 List<Mapping> indexes = fIndexPerSeriesMapping.get(series);
472 if (indexes == null || index < 0) {
473 return -1;
474 }
475
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();
482 break;
483 }
484 }
485 }
486 return internalIndex;
487 }
488 }
This page took 0.055894 seconds and 5 git commands to generate.