charts: Add custom scatter chart
authorGabriel-Andrew Pollo-Guilbert <gabrielpolloguilbert@gmail.com>
Tue, 12 Jul 2016 15:10:22 +0000 (11:10 -0400)
committerGenevieve Bastien <gbastien+lttng@versatic.net>
Thu, 2 Mar 2017 19:41:07 +0000 (14:41 -0500)
Change-Id: I1ced02d5e7bb6f34bfa11a58d7dacbe2ba00f6d8
Signed-off-by: Gabriel-Andrew Pollo-Guilbert <gabrielpolloguilbert@gmail.com>
Signed-off-by: Geneviève Bastien <gbastien+lttng@versatic.net>
Reviewed-on: https://git.eclipse.org/r/77160
Reviewed-by: Hudson CI
Reviewed-by: Matthew Khouzam <matthew.khouzam@ericsson.com>
tmf/org.eclipse.tracecompass.tmf.chart.core/src/org/eclipse/tracecompass/internal/provisional/tmf/chart/core/descriptor/IDescriptorVisitor.java
tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/provisional/tmf/chart/ui/chart/IChartViewer.java
tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/consumer/ScatterStringConsumer.java [new file with mode: 0644]
tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtChartPoint.java [new file with mode: 0644]
tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtScatterChart.java [new file with mode: 0644]
tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtXYChartViewer.java

index 5ec0b28fb54b74d3548edd3d649c703281d1306e..6d00a184d2992630c27e898dc96e00254b4791d2 100644 (file)
@@ -38,7 +38,9 @@ public interface IDescriptorVisitor {
      * @param desc
      *            A duration descriptor
      */
-    void visit(DataChartDurationDescriptor<?, ? extends Number> desc);
+    default void visit(DataChartDurationDescriptor<?, ? extends Number> desc) {
+        visit((DataChartNumericalDescriptor<?, ? extends Number>) desc);
+    }
 
     /**
      * Method for visiting a {@link DataChartTimestampDescriptor}.
@@ -46,6 +48,8 @@ public interface IDescriptorVisitor {
      * @param desc
      *            A timestamp descriptor
      */
-    void visit(DataChartTimestampDescriptor<?> desc);
+    default void visit(DataChartTimestampDescriptor<?> desc) {
+        visit((DataChartNumericalDescriptor<?, Long>) desc);
+    }
 
 }
index 28e76ba62c6e809a60ed54a1b05779991de32497..2059b62d33631f89edddb6886256b708db1222e2 100644 (file)
@@ -20,6 +20,7 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Display;
 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.tmf.chart.ui.swtchart.SwtScatterChart;
 
 import com.google.common.collect.ImmutableList;
 
@@ -82,9 +83,7 @@ public interface IChartViewer {
              * TODO
              */
         case SCATTER_CHART:
-            /**
-             * TODO
-             */
+            return new SwtScatterChart(parent, data, model);
         case PIE_CHART:
             /**
              * TODO
diff --git a/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/consumer/ScatterStringConsumer.java b/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/consumer/ScatterStringConsumer.java
new file mode 100644 (file)
index 0000000..dbccf5b
--- /dev/null
@@ -0,0 +1,119 @@
+/*******************************************************************************
+ * 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 v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.internal.tmf.chart.ui.consumer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.resolver.IStringResolver;
+import org.eclipse.tracecompass.internal.tmf.chart.core.consumer.IDataConsumer;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * This class processes string values in order to create valid data for a
+ * scatter chart. It takes a {@link IStringResolver} for mapping values.
+ * <p>
+ * The current implementation of the scatter chart maps each unique string to an
+ * int. It is different than the bar chart because the other one cannot allow
+ * multiple Y values on an X value. With this consumer, all object sharing a
+ * same string value will also share the same value on the axis.
+ *
+ * @author Gabriel-Andrew Pollo-Guilbert
+ */
+public class ScatterStringConsumer implements IDataConsumer {
+
+    // ------------------------------------------------------------------------
+    // Members
+    // ------------------------------------------------------------------------
+
+    private final IStringResolver<Object> fResolver;
+    private final BiMap<String, Integer> fMap;
+    /** The list of value for each object consumed */
+    private final List<String> fList = new ArrayList<>();
+
+    // ------------------------------------------------------------------------
+    // Constructors
+    // ------------------------------------------------------------------------
+
+    /**
+     * Constructor.
+     *
+     * @param resolver
+     *            The resolver that consumes values
+     */
+    public ScatterStringConsumer(IStringResolver<Object> resolver) {
+        fResolver = resolver;
+        fMap = HashBiMap.create();
+    }
+
+    /**
+     * Surcharged constructor with a bimap provided.
+     *
+     * @param resolver
+     *            The resolver that consumes values
+     * @param map
+     *            The bimap to store values
+     */
+    public ScatterStringConsumer(IStringResolver<Object> resolver, BiMap<String, Integer> map) {
+        fResolver = resolver;
+        fMap = map;
+    }
+
+    // ------------------------------------------------------------------------
+    // Overriden methods
+    // ------------------------------------------------------------------------
+
+    @Override
+    public boolean test(Object obj) {
+        return true;
+    }
+
+    @Override
+    public void accept(@NonNull Object obj) {
+        String str = fResolver.getMapper().apply(obj);
+
+        /* Convert null string to unknown */
+        if (str == null) {
+            str = "?"; //$NON-NLS-1$
+        }
+
+        fList.add(str);
+        fMap.putIfAbsent(str, fMap.size());
+    }
+
+    // ------------------------------------------------------------------------
+    // Accessors
+    // ------------------------------------------------------------------------
+
+    /**
+     * Accessor that returns the list of string value for each object.
+     *
+     * @return The list of string
+     */
+    public List<String> getList() {
+        return ImmutableList.copyOf(fList);
+    }
+
+    /**
+     * Accessor that returns the map between strings and numbers used in a
+     * scatter chart.
+     *
+     * @return The map of string
+     */
+    public BiMap<String, Integer> getMap() {
+        return ImmutableBiMap.copyOf(fMap);
+    }
+
+}
diff --git a/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtChartPoint.java b/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtChartPoint.java
new file mode 100644 (file)
index 0000000..f3b0e64
--- /dev/null
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * 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 v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.internal.tmf.chart.ui.swtchart;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.swtchart.ISeries;
+
+/**
+ * This class is used for storing informations about a point inside a SWT
+ * series. Rather than using coordinates, it uses a reference to the series
+ * itself and an index in order to decrease selection of multiple points that
+ * have the same position.
+ * <p>
+ * The methods {@link #equals(Object)} and {@link #hashCode()} have been
+ * overridden in order to allow two different objects that represent the same
+ * selection to look like they are the same. It is useful when storing them
+ * inside an hash data structure.
+ *
+ * @author Gabriel-Andrew Pollo-Guilbert
+ */
+public class SwtChartPoint {
+
+    // ------------------------------------------------------------------------
+    // Members
+    // ------------------------------------------------------------------------
+
+    private final ISeries fSeries;
+    private final int fIndex;
+
+    // ------------------------------------------------------------------------
+    // Constructors
+    // ------------------------------------------------------------------------
+
+    /**
+     * Default constructor.
+     *
+     * @param series
+     *            The series that owns the point
+     * @param index
+     *            The index of the point in the series
+     */
+    public SwtChartPoint(ISeries series, int index) {
+        fSeries = series;
+        fIndex = index;
+    }
+
+    /**
+     * Copy contructor.
+     *
+     * @param selection
+     *            The selection to copy
+     */
+    public SwtChartPoint(SwtChartPoint selection) {
+        fSeries = selection.fSeries;
+        fIndex = selection.fIndex;
+    }
+
+    // ------------------------------------------------------------------------
+    // Overriden methods
+    // ------------------------------------------------------------------------
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof SwtChartPoint)) {
+            return false;
+        }
+
+        SwtChartPoint point = (SwtChartPoint) obj;
+        return (point.fSeries == fSeries) && (point.fIndex == fIndex);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fSeries, fIndex);
+    }
+
+    // ------------------------------------------------------------------------
+    // Accessors
+    // ------------------------------------------------------------------------
+
+    /**
+     * Accessor that returns the series who owns the selection.
+     *
+     * @return The SWT series of the selection
+     */
+    public ISeries getSeries() {
+        return fSeries;
+    }
+
+    /**
+     * Accessor that returns the index of the selection in the series.
+     *
+     * @return The index of the selection
+     */
+    public int getIndex() {
+        return fIndex;
+    }
+
+}
diff --git a/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtScatterChart.java b/tmf/org.eclipse.tracecompass.tmf.chart.ui/src/org/eclipse/tracecompass/internal/tmf/chart/ui/swtchart/SwtScatterChart.java
new file mode 100644 (file)
index 0000000..e2a54ad
--- /dev/null
@@ -0,0 +1,557 @@
+/*******************************************************************************
+ * 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 v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+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.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+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.swt.SWT;
+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.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.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.format.LabelFormat;
+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;
+
+/**
+ * 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 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();
+
+        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();
+                int size = consumer.getData().size();
+
+                xData = new double[size];
+                for (int i = 0; i < size; i++) {
+                    Number number = checkNotNull(consumer.getData().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();
+
+                yData = new double[consumer.getData().size()];
+                for (int i = 0; i < yData.length; i++) {
+                    Number number = checkNotNull(consumer.getData().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.size() > 0) {
+
+            /* Find the longest category string */
+            String longestString = fXStringMap.keySet().stream()
+                    .max(Comparator.comparingInt(String::length))
+                    .orElse(fXStringMap.keySet().iterator().next());
+
+            /* 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();
+    }
+
+    // ------------------------------------------------------------------------
+    // 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;
+
+                for (int i = 0; i < series.getXSeries().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) {
+                        if (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 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);
+        }
+
+        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);
+        }
+    }
+
+}
index 0aaaa29722c7659e9e227e80e8874c2465f2c357..c7d208f77e0b9f3dea193c1ccd2bb70823c63148 100644 (file)
@@ -86,7 +86,7 @@ public abstract class SwtXYChartViewer extends TmfViewer implements IChartViewer
     /**
      * Ellipsis character
      */
-    private static final char ELLIPSIS = '…';
+    protected static final char ELLIPSIS = '…';
     /**
      * Time stamp formatter for intervals in the days range
      */
@@ -111,7 +111,6 @@ public abstract class SwtXYChartViewer extends TmfViewer implements IChartViewer
     private static final int CLOSE_BUTTON_SIZE = 25;
     private static final int CLOSE_BUTTON_MARGIN = 5;
 
-
     // ------------------------------------------------------------------------
     // Members
     // ------------------------------------------------------------------------
This page took 0.040727 seconds and 5 git commands to generate.