Commit | Line | Data |
---|---|---|
4208b510 AM |
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 | ||
5b973e7c | 14 | import java.math.BigDecimal; |
4208b510 AM |
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 | ||
5b973e7c JR |
86 | private LamiGraphRange fYInternalRange = new LamiGraphRange(checkNotNull(BigDecimal.ZERO), checkNotNull(BigDecimal.ONE)); |
87 | private LamiGraphRange fYExternalRange; | |
88 | ||
89 | ||
4208b510 AM |
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 | ||
5b973e7c JR |
135 | /* The y values range */ |
136 | /* Clamp minimum to zero or negative value */ | |
137 | fYExternalRange = getRange(yAxisAspects, true); | |
138 | ||
4208b510 AM |
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; | |
5b973e7c | 153 | double logScaleEpsilon = ZERO_DOUBLE; |
4208b510 AM |
154 | if (logscale) { |
155 | ||
5b973e7c | 156 | /* Find minimum and maximum values excluding <= 0 values */ |
4208b510 AM |
157 | for (LamiTableEntryAspect aspect : yAxisAspects) { |
158 | for (LamiTableEntry entry : entries) { | |
5b973e7c JR |
159 | Number externalValue = aspect.resolveNumber(entry); |
160 | if (externalValue == null) { | |
161 | continue; | |
162 | } | |
163 | Double value = getInternalDoubleValue(externalValue, fYInternalRange, fYExternalRange); | |
164 | if (value <= 0) { | |
4208b510 AM |
165 | continue; |
166 | } | |
167 | min = Math.min(min, value); | |
168 | max = Math.max(max, value); | |
169 | } | |
170 | } | |
171 | ||
5b973e7c JR |
172 | if (min == Double.MAX_VALUE) { |
173 | /* Series are empty in log scale*/ | |
174 | return; | |
175 | } | |
176 | ||
4208b510 AM |
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; | |
5b973e7c | 193 | |
4208b510 AM |
194 | if (categoryIndex == null) { |
195 | /* Invalid value do not show */ | |
196 | continue; | |
197 | } | |
198 | ||
5b973e7c JR |
199 | Double yValue = ZERO_DOUBLE; |
200 | @Nullable Number number = yAxisAspect.resolveNumber(entries.get(i)); | |
201 | ||
202 | if (number == null) { | |
4208b510 AM |
203 | /* |
204 | * Null value for y is the same as zero since this is a bar | |
205 | * chart | |
206 | */ | |
5b973e7c JR |
207 | yValue = ZERO_DOUBLE; |
208 | } else { | |
209 | yValue = getInternalDoubleValue(number, fYInternalRange, fYExternalRange); | |
4208b510 AM |
210 | } |
211 | ||
5b973e7c | 212 | if (logscale && yValue <= ZERO_DOUBLE) { |
4208b510 AM |
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(); | |
5b973e7c JR |
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 | */ | |
4208b510 AM |
261 | yTick.setTickLabelAngle(1); |
262 | ||
263 | /* Adjust the chart range */ | |
264 | getChart().getAxisSet().adjustRange(); | |
5b973e7c JR |
265 | |
266 | if (logscale && logScaleEpsilon != max) { | |
4208b510 AM |
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 | } |