1 /*******************************************************************************
2 * Copyright (c) 2016 EfficiOS Inc., Jonathan Rajotte-Julien
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
.HashMap
;
17 import java
.util
.HashSet
;
18 import java
.util
.Iterator
;
19 import java
.util
.List
;
22 import java
.util
.TreeSet
;
23 import java
.util
.stream
.Collectors
;
24 import java
.util
.stream
.Stream
;
26 import org
.eclipse
.jdt
.annotation
.NonNull
;
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
.MouseMoveListener
;
32 import org
.eclipse
.swt
.events
.PaintEvent
;
33 import org
.eclipse
.swt
.events
.PaintListener
;
34 import org
.eclipse
.swt
.graphics
.Color
;
35 import org
.eclipse
.swt
.graphics
.GC
;
36 import org
.eclipse
.swt
.graphics
.Point
;
37 import org
.eclipse
.swt
.widgets
.Composite
;
38 import org
.eclipse
.swt
.widgets
.Display
;
39 import org
.eclipse
.swt
.widgets
.Event
;
40 import org
.eclipse
.swt
.widgets
.Listener
;
41 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
42 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
.ChartType
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiLabelFormat
;
45 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
46 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
47 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
48 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalManager
;
49 import org
.swtchart
.IAxisTick
;
50 import org
.swtchart
.ILineSeries
;
51 import org
.swtchart
.ISeries
;
52 import org
.swtchart
.ISeries
.SeriesType
;
53 import org
.swtchart
.LineStyle
;
55 import com
.google
.common
.collect
.BiMap
;
56 import com
.google
.common
.collect
.HashBiMap
;
57 import com
.google
.common
.collect
.Iterators
;
60 * XY Scatter chart viewer for Lami views
62 * @author Jonathan Rajotte-Julien
64 public class LamiScatterViewer
extends LamiXYChartViewer
{
66 private static final int SELECTION_SNAP_RANGE_MULTIPLIER
= 20;
67 private static final int SELECTION_CROSS_SIZE_MULTIPLIER
= 3;
69 private final Map
<ISeries
, List
<Integer
>> fIndexMapping
;
71 /* The current data point for the hovering cross */
72 private Point fHoveringCrossDataPoint
;
80 * Result table populating this chart
84 public LamiScatterViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel graphModel
) {
85 super(parent
, resultTable
, graphModel
);
86 if (getChartModel().getChartType() != ChartType
.XY_SCATTER
) {
87 throw new IllegalStateException("Chart type not a Scatter Chart " + getChartModel().getChartType().toString()); //$NON-NLS-1$
90 /* Inspect X series */
91 fIndexMapping
= new HashMap
<>();
93 fHoveringCrossDataPoint
= new Point(-1, -1);
95 List
<LamiTableEntryAspect
> xAxisAspects
= getXAxisAspects();
96 if (xAxisAspects
.stream().distinct().count() == 1) {
97 LamiTableEntryAspect singleXAspect
= xAxisAspects
.get(0);
99 xAxisAspects
.add(singleXAspect
);
102 BiMap
<@Nullable String
, Integer
> xMap
= checkNotNull(HashBiMap
.create());
103 boolean xIsLog
= graphModel
.xAxisIsLog();
105 boolean areXAspectsContinuous
= areAspectsContinuous(xAxisAspects
);
106 boolean areXAspectsTimeStamp
= areAspectsTimeStamp(xAxisAspects
);
108 /* Check all aspect are the same type */
109 for (LamiTableEntryAspect aspect
: xAxisAspects
) {
110 if (aspect
.isContinuous() != areXAspectsContinuous
) {
111 throw new IllegalStateException("Some X aspects are continuous and some are not"); //$NON-NLS-1$
113 if (aspect
.isTimeStamp() != areXAspectsTimeStamp
) {
114 throw new IllegalStateException("Some X aspects are time based and some are not"); //$NON-NLS-1$
119 * When xAxisAspects are discrete create a map for all values of all
122 if (!areXAspectsContinuous
) {
123 generateLabelMap(xAxisAspects
, checkNotNull(xMap
));
129 List
<LamiTableEntryAspect
> yAxisAspects
= getYAxisAspects();
130 BiMap
<@Nullable String
, Integer
> yMap
= checkNotNull(HashBiMap
.create());
131 boolean yIsLog
= graphModel
.yAxisIsLog();
133 boolean areYAspectsContinuous
= areAspectsContinuous(yAxisAspects
);
134 boolean areYAspectsTimeStamp
= areAspectsTimeStamp(yAxisAspects
);
136 /* Check all aspect are the same type */
137 for (LamiTableEntryAspect aspect
: yAxisAspects
) {
138 if (aspect
.isContinuous() != areYAspectsContinuous
) {
139 throw new IllegalStateException("Some Y aspects are continuous and some are not"); //$NON-NLS-1$
141 if (aspect
.isTimeStamp() != areYAspectsTimeStamp
) {
142 throw new IllegalStateException("Some Y aspects are time based and some are not"); //$NON-NLS-1$
147 * When yAspects are discrete create a map for all values of all series
149 if (!areYAspectsContinuous
) {
150 generateLabelMap(yAxisAspects
, yMap
);
153 /* Plot the series */
155 for (LamiTableEntryAspect yAspect
: getYAxisAspects()) {
156 String name
= ""; //$NON-NLS-1$
157 LamiTableEntryAspect xAspect
;
158 if (xAxisAspects
.size() == 1) {
159 /* Always map to the same x series */
160 xAspect
= xAxisAspects
.get(0);
161 name
= yAspect
.getLabel();
163 xAspect
= xAxisAspects
.get(index
);
164 name
= (yAspect
.getName() + ' ' + Messages
.LamiScatterViewer_by
+ ' ' + xAspect
.getName());
167 List
<@Nullable Double
> xDoubleSeries
= new ArrayList
<>();
168 List
<@Nullable Double
> yDoubleSeries
= new ArrayList
<>();
170 if (xAspect
.isContinuous()) {
171 xDoubleSeries
= getResultTable().getEntries().stream().map((entry
-> xAspect
.resolveDouble(entry
))).collect(Collectors
.toList());
173 xDoubleSeries
= getResultTable().getEntries().stream().map(entry
-> {
174 String string
= xAspect
.resolveString(entry
);
175 Integer value
= xMap
.get(string
);
177 return Double
.valueOf(value
.doubleValue());
181 }).collect(Collectors
.toList());
184 if (yAspect
.isContinuous()) {
185 yDoubleSeries
= getResultTable().getEntries().stream().map((entry
-> yAspect
.resolveDouble(entry
))).collect(Collectors
.toList());
187 yDoubleSeries
= getResultTable().getEntries().stream().map(entry
-> {
188 String string
= yAspect
.resolveString(entry
);
189 Integer value
= yMap
.get(string
);
191 return Double
.valueOf(value
.doubleValue());
195 }).collect(Collectors
.toList());
198 List
<@Nullable Double
> validXDoubleSeries
= new ArrayList
<>();
199 List
<@Nullable Double
> validYDoubleSeries
= new ArrayList
<>();
200 List
<Integer
> indexSeriesCorrespondance
= new ArrayList
<>();
202 if (xDoubleSeries
.size() != yDoubleSeries
.size()) {
203 throw new IllegalStateException("Series sizes don't match!"); //$NON-NLS-1$
206 /* Check for invalid tuple value. Any null elements are invalid */
207 for (int i
= 0; i
< xDoubleSeries
.size(); i
++) {
208 Double xValue
= xDoubleSeries
.get(i
);
209 Double yValue
= yDoubleSeries
.get(i
);
210 if (xValue
== null || yValue
== null) {
211 /* Reject this tuple */
214 if ((xIsLog
&& xValue
<= ZERO
) || (yIsLog
&& yValue
<= ZERO
)) {
216 * Equal or less than 0 values can't be plotted on log scale
220 validXDoubleSeries
.add(xValue
);
221 validYDoubleSeries
.add(yValue
);
222 indexSeriesCorrespondance
.add(i
);
225 if (validXDoubleSeries
.isEmpty() || validXDoubleSeries
.isEmpty()) {
226 /* No need to plot an empty series */
231 ILineSeries scatterSeries
= (ILineSeries
) getChart().getSeriesSet().createSeries(SeriesType
.LINE
, name
);
232 scatterSeries
.setLineStyle(LineStyle
.NONE
);
234 double[] xserie
= validXDoubleSeries
.stream().mapToDouble(elem
-> checkNotNull(elem
).doubleValue()).toArray();
235 double[] yserie
= validYDoubleSeries
.stream().mapToDouble(elem
-> checkNotNull(elem
).doubleValue()).toArray();
236 scatterSeries
.setXSeries(xserie
);
237 scatterSeries
.setYSeries(yserie
);
238 fIndexMapping
.put(scatterSeries
, indexSeriesCorrespondance
);
242 /* Modify x axis related chart styling */
243 IAxisTick xTick
= getChart().getAxisSet().getXAxis(0).getTick();
244 if (areXAspectsContinuous
) {
245 xTick
.setFormat(getContinuousAxisFormatter(xAxisAspects
, getResultTable().getEntries()));
247 xTick
.setFormat(new LamiLabelFormat(checkNotNull(xMap
)));
248 updateTickMark(checkNotNull(xMap
), xTick
, getChart().getPlotArea().getSize().x
);
250 /* Remove vertical grid line */
251 getChart().getAxisSet().getXAxis(0).getGrid().setStyle(LineStyle
.NONE
);
254 /* Modify Y axis related chart styling */
255 IAxisTick yTick
= getChart().getAxisSet().getYAxis(0).getTick();
256 if (areYAspectsContinuous
) {
257 yTick
.setFormat(getContinuousAxisFormatter(yAxisAspects
, getResultTable().getEntries()));
259 yTick
.setFormat(new LamiLabelFormat(checkNotNull(yMap
)));
260 updateTickMark(checkNotNull(yMap
), yTick
, getChart().getPlotArea().getSize().y
);
262 /* Remove horizontal grid line */
263 getChart().getAxisSet().getYAxis(0).getGrid().setStyle(LineStyle
.NONE
);
267 * SWTChart workaround: SWTChart fiddles with tick mark visibility based
268 * on the fact that it can parse the label to double or not.
270 * If the label happens to be a double, it checks for the presence of
271 * that value in its own tick labels to decide if it should add it or
272 * not. If it happens that the parsed value is already present in its
273 * map, the tick gets a visibility of false.
275 * The X axis does not have this problem since SWTCHART checks on label
276 * angle, and if it is != 0 simply does no logic regarding visibility.
277 * So simply set a label angle of 1 to the axis.
279 yTick
.setTickLabelAngle(1);
281 setLineSeriesColor();
283 /* Put log scale if necessary */
284 if (xIsLog
&& areXAspectsContinuous
&& !areXAspectsTimeStamp
) {
285 Stream
.of(getChart().getAxisSet().getXAxes()).forEach(axis
-> axis
.enableLogScale(xIsLog
));
288 if (yIsLog
&& areYAspectsContinuous
&& !areYAspectsTimeStamp
) {
289 /* Set the axis as logscale */
290 Stream
.of(getChart().getAxisSet().getYAxes()).forEach(axis
-> axis
.enableLogScale(yIsLog
));
292 getChart().getAxisSet().adjustRange();
297 getChart().getPlotArea().addMouseListener(new LamiScatterMouseDownListener());
300 * Hovering cross listener
302 getChart().getPlotArea().addMouseMoveListener(new HoveringCrossListener());
305 * Mouse exit listener: reset state of hovering cross on mouse exit.
307 getChart().getPlotArea().addListener(SWT
.MouseExit
, new Listener() {
310 public void handleEvent(@Nullable Event event
) {
312 fHoveringCrossDataPoint
.x
= -1;
313 fHoveringCrossDataPoint
.y
= -1;
320 * Selections and hovering cross painting
322 getChart().getPlotArea().addPaintListener(new LamiScatterPainterListener());
324 /* On resize check for axis tick updating */
325 getChart().addListener(SWT
.Resize
, new Listener() {
327 public void handleEvent(@Nullable Event event
) {
328 if (yTick
.getFormat() instanceof LamiLabelFormat
) {
329 updateTickMark(checkNotNull(yMap
), yTick
, getChart().getPlotArea().getSize().y
);
331 if (xTick
.getFormat() instanceof LamiLabelFormat
) {
332 updateTickMark(checkNotNull(xMap
), xTick
, getChart().getPlotArea().getSize().x
);
338 private void generateLabelMap(List
<LamiTableEntryAspect
> aspects
, BiMap
<@Nullable String
, Integer
> map
) {
339 TreeSet
<@Nullable String
> set
= new TreeSet
<>();
340 for (LamiTableEntryAspect aspect
: aspects
) {
341 for (LamiTableEntry entry
: getResultTable().getEntries()) {
342 String string
= aspect
.resolveString(entry
);
343 if (string
!= null) {
348 /* Ordered label mapping to double */
349 for (String string
: set
) {
350 map
.put(string
, map
.size());
355 * Set the chart series colors.
357 private void setLineSeriesColor() {
358 Iterator
<Color
> colorsIt
;
360 colorsIt
= Iterators
.cycle(COLORS
);
362 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
363 ((ILineSeries
) series
).setSymbolColor((colorsIt
.next()));
365 * Generate initial array of Color to enable per point color change
366 * on selection in the future
368 ArrayList
<Color
> colors
= new ArrayList
<>();
369 for (int i
= 0; i
< series
.getXSeries().length
; i
++) {
370 Color color
= ((ILineSeries
) series
).getSymbolColor();
371 colors
.add(checkNotNull(color
));
373 ((ILineSeries
) series
).setSymbolColors(colors
.toArray(new Color
[colors
.size()]));
377 // ------------------------------------------------------------------------
379 // ------------------------------------------------------------------------
381 private final class HoveringCrossListener
implements MouseMoveListener
{
384 public void mouseMove(@Nullable MouseEvent e
) {
388 ISeries
[] series
= getChart().getSeriesSet().getSeries();
389 @Nullable Point closest
= null;
390 double closestDistance
= -1.0;
392 for (ISeries oneSeries
: series
) {
393 ILineSeries lineSerie
= (ILineSeries
) oneSeries
;
394 for (int i
= 0; i
< lineSerie
.getXSeries().length
; i
++) {
395 Point dataPoint
= lineSerie
.getPixelCoordinates(i
);
398 * Find the distance between the data point and the mouse
399 * location and compare it to the symbol size * the range
400 * multiplier, so when a user hovers the mouse near the dot
401 * the cursor cross snaps to it.
403 int snapRangeRadius
= lineSerie
.getSymbolSize() * SELECTION_SNAP_RANGE_MULTIPLIER
;
406 * FIXME if and only if performance of this code is an issue
407 * for large sets, this can be accelerated by getting the
408 * distance squared, and if it is smaller than
409 * snapRangeRadius squared, then check hypot.
411 double distance
= Math
.hypot(dataPoint
.x
- e
.x
, dataPoint
.y
- e
.y
);
412 if (distance
< snapRangeRadius
) {
413 if (closestDistance
== -1 || distance
< closestDistance
) {
415 closestDistance
= distance
;
420 if (closest
!= null) {
421 fHoveringCrossDataPoint
.x
= closest
.x
;
422 fHoveringCrossDataPoint
.y
= closest
.y
;
424 fHoveringCrossDataPoint
.x
= -1;
425 fHoveringCrossDataPoint
.y
= -1;
431 private final class LamiScatterMouseDownListener
extends MouseAdapter
{
434 public void mouseDown(@Nullable MouseEvent event
) {
435 if (event
== null || event
.button
!= 1) {
439 int xMouseLocation
= event
.x
;
440 int yMouseLocation
= event
.y
;
442 boolean ctrlMode
= false;
444 ISeries
[] series
= getChart().getSeriesSet().getSeries();
445 Set
<Integer
> selections
= getSelection();
447 /* Check for ctrl on click */
448 if ((event
.stateMask
& SWT
.CTRL
) != 0) {
449 selections
= getSelection();
452 /* Reset selection */
454 selections
= new HashSet
<>();
457 for (ISeries oneSeries
: series
) {
458 ILineSeries lineSerie
= (ILineSeries
) oneSeries
;
461 double closestDistance
= -1;
462 for (int i
= 0; i
< lineSerie
.getXSeries().length
; i
++) {
463 Point dataPoint
= lineSerie
.getPixelCoordinates(i
);
466 * Find the distance between the data point and the mouse
467 * location, and compare it to the symbol size so when a
468 * user clicks on a symbol it selects it.
470 double distance
= Math
.hypot(dataPoint
.x
- xMouseLocation
, dataPoint
.y
- yMouseLocation
);
471 int snapRangeRadius
= lineSerie
.getSymbolSize() * SELECTION_SNAP_RANGE_MULTIPLIER
;
472 if (distance
< snapRangeRadius
) {
473 if (closestDistance
== -1 || distance
< closestDistance
) {
475 closestDistance
= distance
;
480 /* Translate to global index */
481 int tableEntryIndex
= getTableEntryIndexFromGraphIndex(checkNotNull(oneSeries
), closest
);
482 if (tableEntryIndex
< 0) {
485 LamiTableEntry entry
= getResultTable().getEntries().get(tableEntryIndex
);
486 int index
= getResultTable().getEntries().indexOf(entry
);
488 if (!ctrlMode
|| !selections
.remove(index
)) {
489 selections
.add(index
);
491 /* Do no iterate since we already found a match */
495 setSelection(selections
);
496 /* Signal all Lami viewers & views of the selection */
497 LamiSelectionUpdateSignal signal
= new LamiSelectionUpdateSignal(this,
498 selections
, checkNotNull(getResultTable().hashCode()));
499 TmfSignalManager
.dispatchSignal(signal
);
504 private final class LamiScatterPainterListener
implements PaintListener
{
507 public void paintControl(@Nullable PaintEvent e
) {
513 /* Draw the selection */
514 drawSelectedDot(checkNotNull(gc
));
516 /* Draw the hovering cross */
517 drawHoveringCross(checkNotNull(gc
));
520 private void drawSelectedDot(GC gc
) {
522 Iterator
<Color
> colorsIt
;
523 colorsIt
= Iterators
.cycle(COLORS
);
524 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
526 /* Get series colors */
527 Color color
= colorsIt
.next();
528 int symbolSize
= ((ILineSeries
) series
).getSymbolSize();
530 for (int index
: getInternalSelections()) {
531 int graphIndex
= getGraphIndexFromTableEntryIndex(series
, index
);
533 if (graphIndex
< 0) {
536 Point point
= series
.getPixelCoordinates(graphIndex
);
538 /* Create a colored dot for selection */
539 gc
.setBackground(color
);
540 gc
.fillOval(point
.x
- symbolSize
, point
.y
- symbolSize
, symbolSize
* 2, symbolSize
* 2);
544 gc
.setLineStyle(SWT
.LINE_SOLID
);
546 int drawingDelta
= SELECTION_CROSS_SIZE_MULTIPLIER
* symbolSize
;
547 gc
.drawLine(point
.x
, point
.y
- drawingDelta
, point
.x
, point
.y
+ drawingDelta
);
548 /* Horizontal line */
549 gc
.drawLine(point
.x
- drawingDelta
, point
.y
, point
.x
+ drawingDelta
, point
.y
);
556 private void drawHoveringCross(GC gc
) {
558 gc
.setLineStyle(SWT
.LINE_SOLID
);
559 gc
.setForeground(Display
.getCurrent().getSystemColor(SWT
.COLOR_BLACK
));
560 gc
.setBackground(Display
.getCurrent().getSystemColor(SWT
.COLOR_WHITE
));
562 gc
.drawLine(fHoveringCrossDataPoint
.x
, 0, fHoveringCrossDataPoint
.x
, getChart().getPlotArea().getSize().y
);
563 /* Horizontal line */
564 gc
.drawLine(0, fHoveringCrossDataPoint
.y
, getChart().getPlotArea().getSize().x
, fHoveringCrossDataPoint
.y
);
568 // ------------------------------------------------------------------------
570 // ------------------------------------------------------------------------
572 private int getTableEntryIndexFromGraphIndex(ISeries series
, int index
) {
573 List
<Integer
> indexes
= fIndexMapping
.get(series
);
574 if (indexes
== null || index
> indexes
.size() || index
< 0) {
577 return indexes
.get(index
);
580 private int getGraphIndexFromTableEntryIndex(ISeries series
, int index
) {
581 List
<Integer
> indexes
= fIndexMapping
.get(series
);
582 if (indexes
== null || !indexes
.contains(index
)) {
585 return indexes
.indexOf(index
);
589 protected void refreshDisplayLabels() {
593 * Return the current selection in internal mapping
595 * @return the internal selections
597 protected Set
<Integer
> getInternalSelections() {
598 /* Translate to internal table location */
599 Set
<Integer
> indexes
= super.getSelection();
600 Set
<Integer
> internalIndexes
= indexes
.stream()
601 .mapToInt(index
-> getResultTable().getEntries().indexOf((getResultTable().getEntries().get(index
))))
603 .collect(Collectors
.toSet());
604 return internalIndexes
;
607 private static void updateTickMark(BiMap
<@Nullable String
, Integer
> map
, IAxisTick tick
, int availableLenghtPixel
) {
608 int nbLabels
= Math
.max(1, map
.size());
609 int stepSizePixel
= availableLenghtPixel
/ nbLabels
;
611 * This step is a limitation on swtchart side regarding minimal grid
612 * step hint size. When the step size are smaller it get defined as the
613 * "default" value for the axis instead of the smallest one.
615 if (IAxisTick
.MIN_GRID_STEP_HINT
> stepSizePixel
) {
616 stepSizePixel
= (int) IAxisTick
.MIN_GRID_STEP_HINT
;
618 tick
.setTickMarkStepHint(stepSizePixel
);
622 protected void setSelection(@NonNull Set
<@NonNull Integer
> selection
) {
623 super.setSelection(selection
);
625 /* Set color of selected symbol */
626 Iterator
<Color
> colorsIt
= Iterators
.cycle(COLORS
);
627 Iterator
<Color
> lightColorsIt
= Iterators
.cycle(LIGHT_COLORS
);
629 Set
<Integer
> currentSelections
= getInternalSelections();
631 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
633 Color lightColor
= lightColorsIt
.next();
634 Color color
= colorsIt
.next();
635 Color
[] colors
= ((ILineSeries
) series
).getSymbolColors();
637 if (currentSelections
.isEmpty()) {
638 /* Put all symbols to the normal colors */
639 Arrays
.fill(colors
, color
);
642 * Fill with light colors to represent the deselected state. The
643 * paint listener is then responsible for drawing the cross and
644 * the dark colors for the selection.
646 Arrays
.fill(colors
, lightColor
);
648 ((ILineSeries
) series
).setSymbolColors(colors
);