| /******************************************************************************* |
| * Copyright (c) 2016 École Polytechnique de Montréal |
| * |
| * All rights reserved. This program and the accompanying materials are |
| * made available under the terms of the Eclipse Public License 2.0 which |
| * accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.tmf.chart.ui.swtchart; |
| |
| import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; |
| |
| import java.text.Format; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.PaintEvent; |
| import org.eclipse.swt.events.PaintListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.tracecompass.common.core.NonNullUtils; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.chart.ChartData; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.chart.ChartModel; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.chart.ChartSeries; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.descriptor.DataChartNumericalDescriptor; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.descriptor.DataChartStringDescriptor; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.descriptor.IDescriptorVisitor; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.resolver.INumericalResolver; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.resolver.IStringResolver; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.signal.ChartSelectionUpdateSignal; |
| import org.eclipse.tracecompass.internal.provisional.tmf.chart.ui.chart.IChartViewer; |
| import org.eclipse.tracecompass.internal.tmf.chart.core.aggregator.IConsumerAggregator; |
| import org.eclipse.tracecompass.internal.tmf.chart.core.consumer.IDataConsumer; |
| import org.eclipse.tracecompass.internal.tmf.chart.core.consumer.NumericalConsumer; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.aggregator.NumericalConsumerAggregator; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.consumer.ScatterStringConsumer; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.consumer.XYChartConsumer; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.consumer.XYSeriesConsumer; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.data.ChartRangeMap; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.dialog.Messages; |
| import org.eclipse.tracecompass.internal.tmf.chart.ui.format.LabelFormat; |
| import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager; |
| import org.swtchart.Chart; |
| import org.swtchart.IAxis; |
| import org.swtchart.IAxisSet; |
| import org.swtchart.IAxisTick; |
| import org.swtchart.ILineSeries; |
| import org.swtchart.ISeries; |
| import org.swtchart.ISeries.SeriesType; |
| import org.swtchart.ISeriesSet; |
| import org.swtchart.LineStyle; |
| |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| import com.google.common.collect.Iterators; |
| |
| /** |
| * Class for building a scatter chart. |
| * |
| * FIXME: In this class, each method have if/then/else structure to cover string |
| * or numerical axes. The specificities for each type of axes should be wrapped |
| * in a small inline class that cover only the specific string or numerical |
| * case. We wouldn't need the ranges and string maps all in the main class, each |
| * sub-class would have only the fields it needs and it will be less |
| * error-prone. |
| * |
| * @author Gabriel-Andrew Pollo-Guilbert |
| */ |
| public final class SwtScatterChart extends SwtXYChartViewer { |
| |
| // ------------------------------------------------------------------------ |
| // Constants |
| // ------------------------------------------------------------------------ |
| |
| private static final int SELECTION_SNAP_RANGE_MULTIPLIER = 20; |
| |
| // ------------------------------------------------------------------------ |
| // Members |
| // ------------------------------------------------------------------------ |
| |
| /** |
| * Map linking X string categories to integer |
| * |
| * FIXME: Either the string map or a range is used for each axis, so instead |
| * of having them both, we should try to group the concept in subclasses |
| */ |
| private final BiMap<String, Integer> fXStringMap = HashBiMap.create(); |
| /** |
| * Map linking Y string categories to integer |
| */ |
| private final BiMap<String, Integer> fYStringMap = HashBiMap.create(); |
| /** |
| * Range map for the X axis |
| */ |
| private ChartRangeMap fXRanges = new ChartRangeMap(); |
| /** |
| * Range map for the Y axis |
| */ |
| private ChartRangeMap fYRanges = new ChartRangeMap(); |
| /** |
| * Map used for showing X categories on the axis |
| */ |
| private BiMap<String, Integer> fVisibleXMap = HashBiMap.create(); |
| /** |
| * Map used for showing Y categories on the axis |
| */ |
| private BiMap<String, Integer> fVisibleYMap = HashBiMap.create(); |
| /** |
| * Coordinates in pixels of the currently hovered point |
| */ |
| private Point fHoveringPoint = new Point(-1, -1); |
| /** |
| * The SWT reference of the currently hovered point |
| */ |
| private @Nullable SwtChartPoint fHoveredPoint; |
| |
| // ------------------------------------------------------------------------ |
| // Constructors |
| // ------------------------------------------------------------------------ |
| |
| /** |
| * Constructor. |
| * |
| * @param parent |
| * parent composite |
| * @param data |
| * configured data series for the chart |
| * @param model |
| * chart model to use |
| */ |
| public SwtScatterChart(Composite parent, ChartData data, ChartModel model) { |
| super(parent, data, model); |
| |
| /* Add the mouse hovering listener */ |
| getChart().getPlotArea().addMouseMoveListener(new MouseHoveringListener()); |
| |
| /* Add the mouse exit listener */ |
| getChart().getPlotArea().addListener(SWT.MouseExit, new MouseExitListener()); |
| |
| /* Add the mouse click listener */ |
| getChart().getPlotArea().addMouseListener(new MouseDownListener()); |
| |
| /* Add the paint listener */ |
| getChart().getPlotArea().addPaintListener(new ScatterPainterListener()); |
| |
| populate(); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Overriden methods |
| // ------------------------------------------------------------------------ |
| |
| // FIXME: This is not SWTchart-specific, it should go higher up |
| private class ConsumerCreatorVisitor implements IDescriptorVisitor { |
| private final boolean fLogScale; |
| private final BiMap<String, Integer> fMap; |
| private @Nullable IDataConsumer fConsumer; |
| |
| ConsumerCreatorVisitor(boolean logScale, BiMap<String, Integer> bimap) { |
| fLogScale = logScale; |
| fMap = bimap; |
| } |
| |
| @Override |
| public void visit(@NonNull DataChartStringDescriptor<?> desc) { |
| fConsumer = new ScatterStringConsumer(IStringResolver.class.cast(desc.getResolver()), fMap); |
| } |
| |
| @Override |
| public void visit(@NonNull DataChartNumericalDescriptor<?, ? extends @NonNull Number> desc) { |
| /* |
| * FIXME: Can this visitor be made generic so that we can have the |
| * right parameters and not need to cast the resolver here? |
| */ |
| INumericalResolver<Object, Number> resolver = INumericalResolver.class.cast(desc.getResolver()); |
| Predicate<@Nullable Number> predicate; |
| if (fLogScale) { |
| predicate = new LogarithmicPredicate(resolver); |
| } else { |
| predicate = Objects::nonNull; |
| } |
| |
| /* Create a consumer for the X descriptor */ |
| fConsumer = new NumericalConsumer(resolver, predicate); |
| } |
| |
| public IDataConsumer getConsumer() { |
| IDataConsumer consumer = fConsumer; |
| if (consumer == null) { |
| throw new NullPointerException("The getConsumer method of the visitor should not be called before visiting a descriptor"); //$NON-NLS-1$ |
| } |
| return consumer; |
| } |
| } |
| |
| @Override |
| protected IDataConsumer getXConsumer(@NonNull ChartSeries series) { |
| ConsumerCreatorVisitor visitor = new ConsumerCreatorVisitor(getModel().isXLogscale(), fXStringMap); |
| series.getX().accept(visitor); |
| return visitor.getConsumer(); |
| } |
| |
| @Override |
| protected IDataConsumer getYConsumer(@NonNull ChartSeries series) { |
| ConsumerCreatorVisitor visitor = new ConsumerCreatorVisitor(getModel().isYLogscale(), fYStringMap); |
| series.getY().accept(visitor); |
| return visitor.getConsumer(); |
| } |
| |
| @Override |
| protected @Nullable IConsumerAggregator getXAggregator() { |
| if (getXDescriptorsInfo().areNumerical()) { |
| return new NumericalConsumerAggregator(); |
| } |
| return null; |
| } |
| |
| @Override |
| protected @Nullable IConsumerAggregator getYAggregator() { |
| if (getYDescriptorsInfo().areNumerical()) { |
| return new NumericalConsumerAggregator(); |
| } |
| return null; |
| } |
| |
| @Override |
| protected ISeries createSwtSeries(ChartSeries chartSeries, ISeriesSet swtSeriesSet, @NonNull Color color) { |
| String title = chartSeries.getY().getName(); |
| |
| if (getXDescriptors().stream().distinct().count() > 1) { |
| title = NLS.bind(Messages.ChartSeries_MultiSeriesTitle, title, chartSeries.getX().getLabel()); |
| } |
| |
| ILineSeries swtSeries = (ILineSeries) swtSeriesSet.createSeries(SeriesType.LINE, title); |
| swtSeries.setLineStyle(LineStyle.NONE); |
| swtSeries.setSymbolColor(color); |
| |
| return swtSeries; |
| } |
| |
| @Override |
| protected void configureSeries(Map<@NonNull ISeries, Object[]> mapper) { |
| XYChartConsumer chartConsumer = getChartConsumer(); |
| |
| /* Obtain the X ranges if possible */ |
| NumericalConsumerAggregator xAggregator = (NumericalConsumerAggregator) chartConsumer.getXAggregator(); |
| if (xAggregator != null) { |
| if (getModel().isXLogscale()) { |
| fXRanges = clampInputDataRange(xAggregator.getChartRanges()); |
| } else { |
| fXRanges = xAggregator.getChartRanges(); |
| } |
| } |
| |
| /* Obtain the Y ranges if possible */ |
| NumericalConsumerAggregator yAggregator = (NumericalConsumerAggregator) chartConsumer.getYAggregator(); |
| if (yAggregator != null) { |
| if (getModel().isYLogscale()) { |
| fYRanges = clampInputDataRange(yAggregator.getChartRanges()); |
| } else { |
| fYRanges = yAggregator.getChartRanges(); |
| } |
| } |
| |
| /* Generate data for each SWT series */ |
| for (XYSeriesConsumer seriesConsumer : chartConsumer.getSeries()) { |
| double[] xData; |
| double[] yData; |
| Object[] object = seriesConsumer.getConsumedElements().toArray(); |
| |
| /* Generate data for the X axis */ |
| if (getXDescriptorsInfo().areNumerical()) { |
| NumericalConsumer consumer = (NumericalConsumer) seriesConsumer.getXConsumer(); |
| List<Number> data = consumer.getData(); |
| int size = data.size(); |
| |
| xData = new double[size]; |
| for (int i = 0; i < size; i++) { |
| Number number = checkNotNull(data.get(i)); |
| xData[i] = fXRanges.getInternalValue(number).doubleValue(); |
| } |
| } else { |
| ScatterStringConsumer consumer = (ScatterStringConsumer) seriesConsumer.getXConsumer(); |
| List<String> list = consumer.getList(); |
| |
| xData = new double[list.size()]; |
| for (int i = 0; i < xData.length; i++) { |
| String str = list.get(i); |
| xData[i] = checkNotNull(fXStringMap.get(str)); |
| } |
| } |
| |
| /* Generate data for the Y axis */ |
| if (getYDescriptorsInfo().areNumerical()) { |
| NumericalConsumer consumer = (NumericalConsumer) seriesConsumer.getYConsumer(); |
| List<Number> data = consumer.getData(); |
| |
| yData = new double[data.size()]; |
| for (int i = 0; i < yData.length; i++) { |
| Number number = checkNotNull(data.get(i)); |
| yData[i] = fYRanges.getInternalValue(number).doubleValue(); |
| } |
| } else { |
| ScatterStringConsumer consumer = (ScatterStringConsumer) seriesConsumer.getYConsumer(); |
| List<String> list = consumer.getList(); |
| |
| yData = new double[list.size()]; |
| for (int i = 0; i < yData.length; i++) { |
| String str = list.get(i); |
| yData[i] = checkNotNull(fYStringMap.get(str)); |
| } |
| } |
| |
| /* Set the data for the SWT series */ |
| ISeries series = checkNotNull(getSeriesMap().get(seriesConsumer.getSeries())); |
| series.setXSeries(xData); |
| series.setYSeries(yData); |
| |
| /* Create a series mapper */ |
| mapper.put(series, checkNotNull(object)); |
| } |
| } |
| |
| @Override |
| protected void configureAxes() { |
| /* Format X axes */ |
| Stream.of(getChart().getAxisSet().getXAxes()).forEach(a -> { |
| IAxisTick tick = checkNotNull(a.getTick()); |
| Format format; |
| |
| /* Give a continuous formatter if the descriptors are numericals */ |
| if (getXDescriptorsInfo().areNumerical()) { |
| format = getContinuousAxisFormatter(fXRanges, getXDescriptorsInfo()); |
| } else { |
| fVisibleXMap = HashBiMap.create(fXStringMap); |
| format = new LabelFormat(fVisibleXMap); |
| updateTickMark(fVisibleXMap, tick, getChart().getPlotArea().getSize().x); |
| } |
| |
| tick.setFormat(format); |
| }); |
| |
| /* Format Y axes */ |
| Stream.of(getChart().getAxisSet().getYAxes()).forEach(a -> { |
| IAxisTick tick = checkNotNull(a.getTick()); |
| Format format; |
| |
| /* Give a continuous formatter if the descriptors are numericals. */ |
| if (getYDescriptorsInfo().areNumerical()) { |
| format = getContinuousAxisFormatter(fYRanges, getYDescriptorsInfo()); |
| } else { |
| fVisibleYMap = HashBiMap.create(fYStringMap); |
| format = new LabelFormat(fVisibleYMap); |
| updateTickMark(fVisibleYMap, tick, getChart().getPlotArea().getSize().y); |
| } |
| |
| tick.setFormat(format); |
| }); |
| } |
| |
| @Override |
| protected void refreshDisplayLabels() { |
| |
| /** |
| * TODO: support for the Y axis too |
| */ |
| |
| /* Only refresh if labels are visible */ |
| Chart chart = getChart(); |
| IAxisSet axisSet = chart.getAxisSet(); |
| IAxis xAxis = axisSet.getXAxis(0); |
| if (!xAxis.getTick().isVisible()) { |
| return; |
| } |
| |
| /* |
| * Shorten all the labels to 5 characters plus "…" when the longest |
| * label length is more than 50% of the chart height. |
| */ |
| Rectangle rect = chart.getClientArea(); |
| int lengthLimit = (int) (rect.height * 0.40); |
| |
| GC gc = new GC(getParent()); |
| gc.setFont(xAxis.getTick().getFont()); |
| |
| // FIXME: the refresh of labels should be done differently for numerical |
| // or string axes. Here this only refreshes the X axis labels for string |
| // labels. |
| if (!fXStringMap.isEmpty()) { |
| |
| /* Find the longest category string */ |
| String longestString = Collections.max(fXStringMap.keySet(), Comparator.comparingInt(String::length)); |
| |
| /* Get the length and height of the longest label in pixels */ |
| Point pixels = gc.stringExtent(longestString); |
| |
| /* Completely arbitrary */ |
| int cutLen = 5; |
| |
| if (pixels.x > lengthLimit) { |
| /* We have to cut down some strings */ |
| for (Entry<String, Integer> entry : fXStringMap.entrySet()) { |
| String reference = checkNotNull(entry.getKey()); |
| |
| if (reference.length() > cutLen) { |
| String key = reference.substring(0, cutLen) + ELLIPSIS; |
| fVisibleXMap.remove(reference); |
| fVisibleXMap.put(key, entry.getValue()); |
| } else { |
| fVisibleXMap.inverse().remove(entry.getValue()); |
| fVisibleXMap.put(reference, entry.getValue()); |
| } |
| } |
| } else { |
| /* All strings should fit */ |
| resetBiMap(fXStringMap, fVisibleXMap); |
| } |
| |
| for (IAxis axis : axisSet.getXAxes()) { |
| IAxisTick tick = axis.getTick(); |
| tick.setFormat(new LabelFormat(fVisibleXMap)); |
| } |
| } |
| |
| /* Cleanup */ |
| gc.dispose(); |
| } |
| |
| @Override |
| protected void setSelection(@NonNull Set<@NonNull Object> set) { |
| super.setSelection(set); |
| |
| /* Set color of selected symbol */ |
| Iterator<Color> colorsIt = Iterators.cycle(COLORS); |
| Iterator<Color> lightColorsIt = Iterators.cycle(COLORS_LIGHT); |
| |
| for (ISeries series : getChart().getSeriesSet().getSeries()) { |
| /* Series color */ |
| Color lightColor = NonNullUtils.checkNotNull(lightColorsIt.next()); |
| Color color = NonNullUtils.checkNotNull(colorsIt.next()); |
| |
| if (set.isEmpty()) { |
| /* Put all symbols to the normal colors */ |
| ((ILineSeries) series).setSymbolColor(color); |
| } else { |
| /* |
| * Fill with light colors to represent the deselected state. The |
| * paint listener is then responsible for drawing the cross and |
| * the dark colors for the selection. |
| */ |
| ((ILineSeries) series).setSymbolColor(lightColor); |
| } |
| } |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Util methods |
| // ------------------------------------------------------------------------ |
| |
| /** |
| * Util method used to reset a bimap from a reference. |
| * |
| * @param reference |
| * Reference map |
| * @param map |
| * Map to modify |
| */ |
| public static <K, V> void resetBiMap(BiMap<K, V> reference, BiMap<K, V> map) { |
| map.clear(); |
| map.putAll(reference); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Listeners |
| // ------------------------------------------------------------------------ |
| |
| private final class MouseHoveringListener implements MouseMoveListener { |
| @Override |
| public void mouseMove(@Nullable MouseEvent event) { |
| if (event == null) { |
| return; |
| } |
| |
| double closestDistance = -1.0; |
| |
| boolean found = false; |
| for (ISeries swtSeries : getChart().getSeriesSet().getSeries()) { |
| ILineSeries series = (ILineSeries) swtSeries; |
| double[] xSeries = series.getXSeries(); |
| |
| for (int i = 0; i < xSeries.length; i++) { |
| Point dataPoint = series.getPixelCoordinates(i); |
| |
| /* |
| * Find the distance between the data point and the mouse |
| * location and compare it to the symbol size * the range |
| * multiplier, so when a user hovers the mouse near the dot |
| * the cursor cross snaps to it. |
| */ |
| int snapRangeRadius = series.getSymbolSize() * SELECTION_SNAP_RANGE_MULTIPLIER; |
| |
| /* |
| * FIXME: if and only if performance of this code is an |
| * issue for large sets, this can be accelerated by getting |
| * the distance squared, and if it is smaller than |
| * snapRangeRadius squared, then check hypot. |
| */ |
| double distance = Math.hypot(dataPoint.x - event.x, dataPoint.y - event.y); |
| |
| if (distance < snapRangeRadius && (closestDistance == -1 || distance < closestDistance)) { |
| fHoveringPoint.x = dataPoint.x; |
| fHoveringPoint.y = dataPoint.y; |
| |
| fHoveredPoint = new SwtChartPoint(series, i); |
| |
| closestDistance = distance; |
| found = true; |
| } |
| } |
| } |
| |
| /* Check if a point was found */ |
| if (!found) { |
| fHoveredPoint = null; |
| } |
| |
| refresh(); |
| } |
| } |
| |
| private final class MouseExitListener implements Listener { |
| @Override |
| public void handleEvent(@Nullable Event event) { |
| if (event != null) { |
| fHoveringPoint.x = -1; |
| fHoveringPoint.y = -1; |
| |
| fHoveredPoint = null; |
| |
| refresh(); |
| } |
| } |
| } |
| |
| private final class MouseDownListener extends MouseAdapter { |
| @Override |
| public void mouseDown(@Nullable MouseEvent event) { |
| if (event == null || event.button != 1) { |
| return; |
| } |
| |
| /* Check if a point is hovered */ |
| SwtChartPoint selection = fHoveredPoint; |
| if (selection == null) { |
| getSelection().clear(); |
| } else { |
| boolean ctrl = (event.stateMask & SWT.CTRL) != 0; |
| getSelection().touch(selection, ctrl); |
| } |
| |
| /* Redraw the selected points */ |
| refresh(); |
| |
| /* Find these points map to which objects */ |
| Set<Object> set = new HashSet<>(); |
| for (SwtChartPoint point : getSelection().getPoints()) { |
| Object[] objects = checkNotNull(getObjectMap().get(point.getSeries())); |
| |
| /* Add objects to the set */ |
| Object obj = objects[point.getIndex()]; |
| if (obj != null) { |
| set.add(obj); |
| } |
| } |
| |
| /* Send the update signal */ |
| setSelection(set); |
| ChartSelectionUpdateSignal signal = new ChartSelectionUpdateSignal(SwtScatterChart.this, getData().getDataProvider(), set); |
| TmfSignalManager.dispatchSignal(signal); |
| } |
| } |
| |
| private final class ScatterPainterListener implements PaintListener { |
| @Override |
| public void paintControl(@Nullable PaintEvent event) { |
| if (event == null) { |
| return; |
| } |
| |
| GC gc = event.gc; |
| if (gc == null) { |
| return; |
| } |
| |
| /* Draw the hovering cross */ |
| drawHoveringCross(gc); |
| |
| /* Don't draw if there's no selection */ |
| if (!getSelection().getPoints().isEmpty()) { |
| /* Draw the selected points */ |
| drawSelectedDot(gc); |
| } |
| |
| } |
| |
| private void drawHoveringCross(GC gc) { |
| if (fHoveredPoint == null) { |
| return; |
| } |
| |
| gc.setLineWidth(1); |
| gc.setLineStyle(SWT.LINE_SOLID); |
| gc.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); |
| gc.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); |
| |
| /* Vertical line */ |
| gc.drawLine(fHoveringPoint.x, 0, fHoveringPoint.x, getChart().getPlotArea().getSize().y); |
| |
| /* Horizontal line */ |
| gc.drawLine(0, fHoveringPoint.y, getChart().getPlotArea().getSize().x, fHoveringPoint.y); |
| } |
| |
| private void drawSelectedDot(GC gc) { |
| for (SwtChartPoint point : getSelection().getPoints()) { |
| ISeries series = point.getSeries(); |
| Point coor = series.getPixelCoordinates(point.getIndex()); |
| int symbolSize = ((ILineSeries) series).getSymbolSize(); |
| |
| Color symbolColor = ((ILineSeries) series).getSymbolColor(); |
| if (symbolColor == null) { |
| continue; |
| } |
| Color darkColor = IChartViewer.getCorrespondingColor(symbolColor); |
| /* Create a colored dot for selection */ |
| gc.setBackground(darkColor); |
| gc.fillOval(coor.x - symbolSize, coor.y - symbolSize, symbolSize * 2, symbolSize * 2); |
| |
| /* Configure cross settings */ |
| gc.setLineWidth(2); |
| gc.setLineStyle(SWT.LINE_SOLID); |
| int drawingDelta = 2 * symbolSize; |
| |
| /* Vertical line */ |
| gc.drawLine(coor.x, coor.y - drawingDelta, coor.x, coor.y + drawingDelta); |
| |
| /* Horizontal line */ |
| gc.drawLine(coor.x - drawingDelta, coor.y, coor.x + drawingDelta, coor.y); |
| } |
| } |
| } |
| |
| } |