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
.math
.BigDecimal
;
15 import java
.util
.ArrayList
;
16 import java
.util
.Arrays
;
17 import java
.util
.HashMap
;
18 import java
.util
.HashSet
;
19 import java
.util
.Iterator
;
20 import java
.util
.List
;
23 import java
.util
.TreeSet
;
24 import java
.util
.stream
.Collectors
;
25 import java
.util
.stream
.Stream
;
27 import org
.eclipse
.jdt
.annotation
.NonNull
;
28 import org
.eclipse
.jdt
.annotation
.Nullable
;
29 import org
.eclipse
.swt
.SWT
;
30 import org
.eclipse
.swt
.events
.MouseAdapter
;
31 import org
.eclipse
.swt
.events
.MouseEvent
;
32 import org
.eclipse
.swt
.events
.MouseMoveListener
;
33 import org
.eclipse
.swt
.events
.PaintEvent
;
34 import org
.eclipse
.swt
.events
.PaintListener
;
35 import org
.eclipse
.swt
.graphics
.Color
;
36 import org
.eclipse
.swt
.graphics
.GC
;
37 import org
.eclipse
.swt
.graphics
.Point
;
38 import org
.eclipse
.swt
.widgets
.Composite
;
39 import org
.eclipse
.swt
.widgets
.Display
;
40 import org
.eclipse
.swt
.widgets
.Event
;
41 import org
.eclipse
.swt
.widgets
.Listener
;
42 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
.ChartType
;
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
.format
.LamiLabelFormat
;
48 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
49 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalManager
;
50 import org
.swtchart
.IAxisTick
;
51 import org
.swtchart
.ILineSeries
;
52 import org
.swtchart
.ISeries
;
53 import org
.swtchart
.ISeries
.SeriesType
;
54 import org
.swtchart
.LineStyle
;
56 import com
.google
.common
.collect
.BiMap
;
57 import com
.google
.common
.collect
.HashBiMap
;
58 import com
.google
.common
.collect
.Iterators
;
61 * XY Scatter chart viewer for Lami views
63 * @author Jonathan Rajotte-Julien
65 public class LamiScatterViewer
extends LamiXYChartViewer
{
67 private static final int SELECTION_SNAP_RANGE_MULTIPLIER
= 20;
68 private static final int SELECTION_CROSS_SIZE_MULTIPLIER
= 3;
70 private final Map
<ISeries
, List
<Integer
>> fIndexMapping
;
72 /* Use a scale from 0 to 1 internally for both axes */
73 private LamiGraphRange fXInternalRange
= new LamiGraphRange(checkNotNull(BigDecimal
.ZERO
), checkNotNull(BigDecimal
.ONE
));
74 private LamiGraphRange fYInternalRange
= new LamiGraphRange(checkNotNull(BigDecimal
.ZERO
), checkNotNull(BigDecimal
.ONE
));
76 private @Nullable LamiGraphRange fXExternalRange
= null;
77 private @Nullable LamiGraphRange fYExternalRange
= null;
79 /* The current data point for the hovering cross */
80 private Point fHoveringCrossDataPoint
;
88 * Result table populating this chart
92 public LamiScatterViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel graphModel
) {
93 super(parent
, resultTable
, graphModel
);
94 if (getChartModel().getChartType() != ChartType
.XY_SCATTER
) {
95 throw new IllegalStateException("Chart type not a Scatter Chart " + getChartModel().getChartType().toString()); //$NON-NLS-1$
98 /* Inspect X series */
99 fIndexMapping
= new HashMap
<>();
101 fHoveringCrossDataPoint
= new Point(-1, -1);
103 List
<LamiTableEntryAspect
> xAxisAspects
= getXAxisAspects();
104 if (xAxisAspects
.stream().distinct().count() == 1) {
105 LamiTableEntryAspect singleXAspect
= xAxisAspects
.get(0);
106 xAxisAspects
.clear();
107 xAxisAspects
.add(singleXAspect
);
110 BiMap
<@Nullable String
, Integer
> xMap
= checkNotNull(HashBiMap
.create());
111 boolean xIsLog
= graphModel
.xAxisIsLog();
113 boolean areXAspectsContinuous
= areAspectsContinuous(xAxisAspects
);
114 boolean areXAspectsTimeStamp
= areAspectsTimeStamp(xAxisAspects
);
116 /* Check all aspect are the same type */
117 for (LamiTableEntryAspect aspect
: xAxisAspects
) {
118 if (aspect
.isContinuous() != areXAspectsContinuous
) {
119 throw new IllegalStateException("Some X aspects are continuous and some are not"); //$NON-NLS-1$
121 if (aspect
.isTimeStamp() != areXAspectsTimeStamp
) {
122 throw new IllegalStateException("Some X aspects are time based and some are not"); //$NON-NLS-1$
127 * When xAxisAspects are discrete create a map for all values of all
130 if (!areXAspectsContinuous
) {
131 generateLabelMap(xAxisAspects
, checkNotNull(xMap
));
134 * Always clamp the range to min and max
136 * TODO: in the future this could be based on the result of the
137 * delta between max and min multiplied by a ratio like it is done in
140 fXExternalRange
= getRange(xAxisAspects
, false);
146 List
<LamiTableEntryAspect
> yAxisAspects
= getYAxisAspects();
147 BiMap
<@Nullable String
, Integer
> yMap
= checkNotNull(HashBiMap
.create());
148 boolean yIsLog
= graphModel
.yAxisIsLog();
150 boolean areYAspectsContinuous
= areAspectsContinuous(yAxisAspects
);
151 boolean areYAspectsTimeStamp
= areAspectsTimeStamp(yAxisAspects
);
153 /* Check all aspect are the same type */
154 for (LamiTableEntryAspect aspect
: yAxisAspects
) {
155 if (aspect
.isContinuous() != areYAspectsContinuous
) {
156 throw new IllegalStateException("Some Y aspects are continuous and some are not"); //$NON-NLS-1$
158 if (aspect
.isTimeStamp() != areYAspectsTimeStamp
) {
159 throw new IllegalStateException("Some Y aspects are time based and some are not"); //$NON-NLS-1$
164 * When yAspects are discrete create a map for all values of all series
166 if (!areYAspectsContinuous
) {
167 generateLabelMap(yAxisAspects
, yMap
);
170 * Only clamp the range to the minimum value if it is a time stamp since
171 * plotting from 1970 would make little sense.
173 fYExternalRange
= getRange(yAxisAspects
, areYAspectsTimeStamp
);
176 /* Plot the series */
178 for (LamiTableEntryAspect yAspect
: getYAxisAspects()) {
180 LamiTableEntryAspect xAspect
;
181 if (xAxisAspects
.size() == 1) {
182 /* Always map to the same x series */
183 xAspect
= xAxisAspects
.get(0);
184 name
= yAspect
.getLabel();
186 xAspect
= xAxisAspects
.get(index
);
187 name
= (yAspect
.getName() + ' ' + Messages
.LamiScatterViewer_by
+ ' ' + xAspect
.getName());
190 List
<@Nullable Double
> xDoubleSeries
;
191 List
<@Nullable Double
> yDoubleSeries
;
193 if (xAspect
.isContinuous()) {
194 xDoubleSeries
= getResultTable().getEntries().stream()
196 Number number
= xAspect
.resolveNumber(entry
);
197 if (number
!= null && fXExternalRange
!= null) {
198 return getInternalDoubleValue(number
, fXInternalRange
, fXExternalRange
);
202 .collect(Collectors
.toList());
204 xDoubleSeries
= getResultTable().getEntries().stream()
206 String string
= xAspect
.resolveString(entry
);
207 Integer value
= xMap
.get(string
);
209 return Double
.valueOf(value
.doubleValue());
213 .collect(Collectors
.toList());
216 if (yAspect
.isContinuous()) {
217 yDoubleSeries
= getResultTable().getEntries().stream()
219 Number number
= yAspect
.resolveNumber(entry
);
220 if (number
!= null && fYExternalRange
!= null) {
221 return getInternalDoubleValue(number
, fYInternalRange
, fYExternalRange
);
225 .collect(Collectors
.toList());
227 yDoubleSeries
= getResultTable().getEntries().stream()
229 String string
= yAspect
.resolveString(entry
);
230 Integer value
= yMap
.get(string
);
232 return Double
.valueOf(value
.doubleValue());
236 .collect(Collectors
.toList());
239 List
<@Nullable Double
> validXDoubleSeries
= new ArrayList
<>();
240 List
<@Nullable Double
> validYDoubleSeries
= new ArrayList
<>();
241 List
<Integer
> indexSeriesCorrespondance
= new ArrayList
<>();
243 if (xDoubleSeries
.size() != yDoubleSeries
.size()) {
244 throw new IllegalStateException("Series sizes don't match!"); //$NON-NLS-1$
247 /* Check for invalid tuple value. Any null elements are invalid */
248 for (int i
= 0; i
< xDoubleSeries
.size(); i
++) {
249 Double xValue
= xDoubleSeries
.get(i
);
250 Double yValue
= yDoubleSeries
.get(i
);
251 if (xValue
== null || yValue
== null) {
252 /* Reject this tuple */
255 if ((xIsLog
&& xValue
<= ZERO_DOUBLE
) || (yIsLog
&& yValue
<= ZERO_DOUBLE
)) {
257 * Equal or less than 0 values can't be plotted on log scale
261 validXDoubleSeries
.add(xValue
);
262 validYDoubleSeries
.add(yValue
);
263 indexSeriesCorrespondance
.add(i
);
266 if (validXDoubleSeries
.isEmpty() || validXDoubleSeries
.isEmpty()) {
267 /* No need to plot an empty series */
272 ILineSeries scatterSeries
= (ILineSeries
) getChart().getSeriesSet().createSeries(SeriesType
.LINE
, name
);
273 scatterSeries
.setLineStyle(LineStyle
.NONE
);
275 double[] xserie
= validXDoubleSeries
.stream().mapToDouble(elem
-> checkNotNull(elem
).doubleValue()).toArray();
276 double[] yserie
= validYDoubleSeries
.stream().mapToDouble(elem
-> checkNotNull(elem
).doubleValue()).toArray();
277 scatterSeries
.setXSeries(xserie
);
278 scatterSeries
.setYSeries(yserie
);
279 fIndexMapping
.put(scatterSeries
, indexSeriesCorrespondance
);
283 /* Modify x axis related chart styling */
284 IAxisTick xTick
= getChart().getAxisSet().getXAxis(0).getTick();
285 if (areXAspectsContinuous
) {
286 xTick
.setFormat(getContinuousAxisFormatter(xAxisAspects
, getResultTable().getEntries(), fXInternalRange
, fXExternalRange
));
288 xTick
.setFormat(new LamiLabelFormat(checkNotNull(xMap
)));
289 updateTickMark(checkNotNull(xMap
), xTick
, getChart().getPlotArea().getSize().x
);
291 /* Remove vertical grid line */
292 getChart().getAxisSet().getXAxis(0).getGrid().setStyle(LineStyle
.NONE
);
295 /* Modify Y axis related chart styling */
296 IAxisTick yTick
= getChart().getAxisSet().getYAxis(0).getTick();
297 if (areYAspectsContinuous
) {
298 yTick
.setFormat(getContinuousAxisFormatter(yAxisAspects
, getResultTable().getEntries(), fYInternalRange
, fYExternalRange
));
300 yTick
.setFormat(new LamiLabelFormat(checkNotNull(yMap
)));
301 updateTickMark(checkNotNull(yMap
), yTick
, getChart().getPlotArea().getSize().y
);
303 /* Remove horizontal grid line */
304 getChart().getAxisSet().getYAxis(0).getGrid().setStyle(LineStyle
.NONE
);
308 * SWTChart workaround: SWTChart fiddles with tick mark visibility based
309 * on the fact that it can parse the label to double or not.
311 * If the label happens to be a double, it checks for the presence of
312 * that value in its own tick labels to decide if it should add it or
313 * not. If it happens that the parsed value is already present in its
314 * map, the tick gets a visibility of false.
316 * The X axis does not have this problem since SWTCHART checks on label
317 * angle, and if it is != 0 simply does no logic regarding visibility.
318 * So simply set a label angle of 1 to the axis.
320 yTick
.setTickLabelAngle(1);
322 setLineSeriesColor();
324 /* Put log scale if necessary */
325 if (xIsLog
&& areXAspectsContinuous
&& !areXAspectsTimeStamp
) {
326 Stream
.of(getChart().getAxisSet().getXAxes()).forEach(axis
-> axis
.enableLogScale(xIsLog
));
329 if (yIsLog
&& areYAspectsContinuous
&& !areYAspectsTimeStamp
) {
330 /* Set the axis as logscale */
331 Stream
.of(getChart().getAxisSet().getYAxes()).forEach(axis
-> axis
.enableLogScale(yIsLog
));
333 getChart().getAxisSet().adjustRange();
338 getChart().getPlotArea().addMouseListener(new LamiScatterMouseDownListener());
341 * Hovering cross listener
343 getChart().getPlotArea().addMouseMoveListener(new HoveringCrossListener());
346 * Mouse exit listener: reset state of hovering cross on mouse exit.
348 getChart().getPlotArea().addListener(SWT
.MouseExit
, new Listener() {
351 public void handleEvent(@Nullable Event event
) {
353 fHoveringCrossDataPoint
.x
= -1;
354 fHoveringCrossDataPoint
.y
= -1;
361 * Selections and hovering cross painting
363 getChart().getPlotArea().addPaintListener(new LamiScatterPainterListener());
365 /* On resize check for axis tick updating */
366 getChart().addListener(SWT
.Resize
, new Listener() {
368 public void handleEvent(@Nullable Event event
) {
369 if (yTick
.getFormat() instanceof LamiLabelFormat
) {
370 updateTickMark(checkNotNull(yMap
), yTick
, getChart().getPlotArea().getSize().y
);
372 if (xTick
.getFormat() instanceof LamiLabelFormat
) {
373 updateTickMark(checkNotNull(xMap
), xTick
, getChart().getPlotArea().getSize().x
);
379 private void generateLabelMap(List
<LamiTableEntryAspect
> aspects
, BiMap
<@Nullable String
, Integer
> map
) {
380 TreeSet
<@Nullable String
> set
= new TreeSet
<>();
381 for (LamiTableEntryAspect aspect
: aspects
) {
382 for (LamiTableEntry entry
: getResultTable().getEntries()) {
383 String string
= aspect
.resolveString(entry
);
384 if (string
!= null) {
389 /* Ordered label mapping to double */
390 for (String string
: set
) {
391 map
.put(string
, map
.size());
396 * Set the chart series colors.
398 private void setLineSeriesColor() {
399 Iterator
<Color
> colorsIt
;
401 colorsIt
= Iterators
.cycle(COLORS
);
403 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
404 ((ILineSeries
) series
).setSymbolColor((colorsIt
.next()));
406 * Generate initial array of Color to enable per point color change
407 * on selection in the future
409 ArrayList
<Color
> colors
= new ArrayList
<>();
410 for (int i
= 0; i
< series
.getXSeries().length
; i
++) {
411 Color color
= ((ILineSeries
) series
).getSymbolColor();
412 colors
.add(checkNotNull(color
));
414 ((ILineSeries
) series
).setSymbolColors(colors
.toArray(new Color
[colors
.size()]));
418 // ------------------------------------------------------------------------
420 // ------------------------------------------------------------------------
422 private final class HoveringCrossListener
implements MouseMoveListener
{
425 public void mouseMove(@Nullable MouseEvent e
) {
429 ISeries
[] series
= getChart().getSeriesSet().getSeries();
430 @Nullable Point closest
= null;
431 double closestDistance
= -1.0;
433 for (ISeries oneSeries
: series
) {
434 ILineSeries lineSerie
= (ILineSeries
) oneSeries
;
435 for (int i
= 0; i
< lineSerie
.getXSeries().length
; i
++) {
436 Point dataPoint
= lineSerie
.getPixelCoordinates(i
);
439 * Find the distance between the data point and the mouse
440 * location and compare it to the symbol size * the range
441 * multiplier, so when a user hovers the mouse near the dot
442 * the cursor cross snaps to it.
444 int snapRangeRadius
= lineSerie
.getSymbolSize() * SELECTION_SNAP_RANGE_MULTIPLIER
;
447 * FIXME if and only if performance of this code is an issue
448 * for large sets, this can be accelerated by getting the
449 * distance squared, and if it is smaller than
450 * snapRangeRadius squared, then check hypot.
452 double distance
= Math
.hypot(dataPoint
.x
- e
.x
, dataPoint
.y
- e
.y
);
453 if (distance
< snapRangeRadius
) {
454 if (closestDistance
== -1 || distance
< closestDistance
) {
456 closestDistance
= distance
;
461 if (closest
!= null) {
462 fHoveringCrossDataPoint
.x
= closest
.x
;
463 fHoveringCrossDataPoint
.y
= closest
.y
;
465 fHoveringCrossDataPoint
.x
= -1;
466 fHoveringCrossDataPoint
.y
= -1;
472 private final class LamiScatterMouseDownListener
extends MouseAdapter
{
475 public void mouseDown(@Nullable MouseEvent event
) {
476 if (event
== null || event
.button
!= 1) {
480 int xMouseLocation
= event
.x
;
481 int yMouseLocation
= event
.y
;
483 boolean ctrlMode
= false;
485 ISeries
[] series
= getChart().getSeriesSet().getSeries();
486 Set
<Integer
> selections
= getSelection();
488 /* Check for ctrl on click */
489 if ((event
.stateMask
& SWT
.CTRL
) != 0) {
490 selections
= getSelection();
493 /* Reset selection */
495 selections
= new HashSet
<>();
498 for (ISeries oneSeries
: series
) {
499 ILineSeries lineSerie
= (ILineSeries
) oneSeries
;
502 double closestDistance
= -1;
503 for (int i
= 0; i
< lineSerie
.getXSeries().length
; i
++) {
504 Point dataPoint
= lineSerie
.getPixelCoordinates(i
);
507 * Find the distance between the data point and the mouse
508 * location, and compare it to the symbol size so when a
509 * user clicks on a symbol it selects it.
511 double distance
= Math
.hypot(dataPoint
.x
- xMouseLocation
, dataPoint
.y
- yMouseLocation
);
512 int snapRangeRadius
= lineSerie
.getSymbolSize() * SELECTION_SNAP_RANGE_MULTIPLIER
;
513 if (distance
< snapRangeRadius
) {
514 if (closestDistance
== -1 || distance
< closestDistance
) {
516 closestDistance
= distance
;
521 /* Translate to global index */
522 int tableEntryIndex
= getTableEntryIndexFromGraphIndex(checkNotNull(oneSeries
), closest
);
523 if (tableEntryIndex
< 0) {
526 LamiTableEntry entry
= getResultTable().getEntries().get(tableEntryIndex
);
527 int index
= getResultTable().getEntries().indexOf(entry
);
529 if (!ctrlMode
|| !selections
.remove(index
)) {
530 selections
.add(index
);
532 /* Do no iterate since we already found a match */
536 setSelection(selections
);
537 /* Signal all Lami viewers & views of the selection */
538 LamiSelectionUpdateSignal signal
= new LamiSelectionUpdateSignal(this,
539 selections
, checkNotNull(getResultTable().hashCode()));
540 TmfSignalManager
.dispatchSignal(signal
);
545 private final class LamiScatterPainterListener
implements PaintListener
{
548 public void paintControl(@Nullable PaintEvent e
) {
554 /* Draw the selection */
555 drawSelectedDot(checkNotNull(gc
));
557 /* Draw the hovering cross */
558 drawHoveringCross(checkNotNull(gc
));
561 private void drawSelectedDot(GC gc
) {
563 Iterator
<Color
> colorsIt
;
564 colorsIt
= Iterators
.cycle(COLORS
);
565 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
567 /* Get series colors */
568 Color color
= colorsIt
.next();
569 int symbolSize
= ((ILineSeries
) series
).getSymbolSize();
571 for (int index
: getInternalSelections()) {
572 int graphIndex
= getGraphIndexFromTableEntryIndex(series
, index
);
574 if (graphIndex
< 0) {
577 Point point
= series
.getPixelCoordinates(graphIndex
);
579 /* Create a colored dot for selection */
580 gc
.setBackground(color
);
581 gc
.fillOval(point
.x
- symbolSize
, point
.y
- symbolSize
, symbolSize
* 2, symbolSize
* 2);
585 gc
.setLineStyle(SWT
.LINE_SOLID
);
587 int drawingDelta
= SELECTION_CROSS_SIZE_MULTIPLIER
* symbolSize
;
588 gc
.drawLine(point
.x
, point
.y
- drawingDelta
, point
.x
, point
.y
+ drawingDelta
);
589 /* Horizontal line */
590 gc
.drawLine(point
.x
- drawingDelta
, point
.y
, point
.x
+ drawingDelta
, point
.y
);
597 private void drawHoveringCross(GC gc
) {
599 gc
.setLineStyle(SWT
.LINE_SOLID
);
600 gc
.setForeground(Display
.getCurrent().getSystemColor(SWT
.COLOR_BLACK
));
601 gc
.setBackground(Display
.getCurrent().getSystemColor(SWT
.COLOR_WHITE
));
603 gc
.drawLine(fHoveringCrossDataPoint
.x
, 0, fHoveringCrossDataPoint
.x
, getChart().getPlotArea().getSize().y
);
604 /* Horizontal line */
605 gc
.drawLine(0, fHoveringCrossDataPoint
.y
, getChart().getPlotArea().getSize().x
, fHoveringCrossDataPoint
.y
);
609 // ------------------------------------------------------------------------
611 // ------------------------------------------------------------------------
613 private int getTableEntryIndexFromGraphIndex(ISeries series
, int index
) {
614 List
<Integer
> indexes
= fIndexMapping
.get(series
);
615 if (indexes
== null || index
> indexes
.size() || index
< 0) {
618 return indexes
.get(index
);
621 private int getGraphIndexFromTableEntryIndex(ISeries series
, int index
) {
622 List
<Integer
> indexes
= fIndexMapping
.get(series
);
623 if (indexes
== null || !indexes
.contains(index
)) {
626 return indexes
.indexOf(index
);
630 protected void refreshDisplayLabels() {
634 * Return the current selection in internal mapping
636 * @return the internal selections
638 protected Set
<Integer
> getInternalSelections() {
639 /* Translate to internal table location */
640 Set
<Integer
> indexes
= super.getSelection();
641 Set
<Integer
> internalIndexes
= indexes
.stream()
642 .mapToInt(index
-> getResultTable().getEntries().indexOf((getResultTable().getEntries().get(index
))))
644 .collect(Collectors
.toSet());
645 return internalIndexes
;
648 private static void updateTickMark(BiMap
<@Nullable String
, Integer
> map
, IAxisTick tick
, int availableLenghtPixel
) {
649 int nbLabels
= Math
.max(1, map
.size());
650 int stepSizePixel
= availableLenghtPixel
/ nbLabels
;
652 * This step is a limitation on swtchart side regarding minimal grid
653 * step hint size. When the step size are smaller it get defined as the
654 * "default" value for the axis instead of the smallest one.
656 if (IAxisTick
.MIN_GRID_STEP_HINT
> stepSizePixel
) {
657 stepSizePixel
= (int) IAxisTick
.MIN_GRID_STEP_HINT
;
659 tick
.setTickMarkStepHint(stepSizePixel
);
663 protected void setSelection(@NonNull Set
<@NonNull Integer
> selection
) {
664 super.setSelection(selection
);
666 /* Set color of selected symbol */
667 Iterator
<Color
> colorsIt
= Iterators
.cycle(COLORS
);
668 Iterator
<Color
> lightColorsIt
= Iterators
.cycle(LIGHT_COLORS
);
670 Set
<Integer
> currentSelections
= getInternalSelections();
672 for (ISeries series
: getChart().getSeriesSet().getSeries()) {
674 Color lightColor
= lightColorsIt
.next();
675 Color color
= colorsIt
.next();
676 Color
[] colors
= ((ILineSeries
) series
).getSymbolColors();
678 if (currentSelections
.isEmpty()) {
679 /* Put all symbols to the normal colors */
680 Arrays
.fill(colors
, color
);
683 * Fill with light colors to represent the deselected state. The
684 * paint listener is then responsible for drawing the cross and
685 * the dark colors for the selection.
687 Arrays
.fill(colors
, lightColor
);
689 ((ILineSeries
) series
).setSymbolColors(colors
);