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