blob: 887ea9cbfacf12a1b61f00e070452fd061d91b54 [file] [log] [blame]
/*******************************************************************************
* 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.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
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.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.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.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.BarStringConsumer;
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.tmf.core.signal.TmfSignalManager;
import org.swtchart.IAxis;
import org.swtchart.IAxisTick;
import org.swtchart.IBarSeries;
import org.swtchart.ISeries;
import org.swtchart.ISeries.SeriesType;
import org.swtchart.ISeriesSet;
import com.google.common.collect.Iterators;
/**
* Class for a bar chart.
*
* @author Gabriel-Andrew Pollo-Guilbert
*/
public final class SwtBarChart extends SwtXYChartViewer {
private static final int BAR_PADDING = 20;
/* Maximum percentage of chart for label */
private static final double LENGTH_LIMIT = 0.4;
// ------------------------------------------------------------------------
// Members
// ------------------------------------------------------------------------
/**
* Range map for the Y axis since it must be numerical
*/
private ChartRangeMap fYRanges = new ChartRangeMap();
/**
* Map reprensenting categories on the X axis
*/
private String @Nullable [] fCategories;
// ------------------------------------------------------------------------
// Constructors
// ------------------------------------------------------------------------
/**
* Constructor.
*
* @param parent
* parent composite
* @param data
* configured data series for the chart
* @param model
* chart model to use
*/
public SwtBarChart(Composite parent, ChartData data, ChartModel model) {
super(parent, data, model);
/* Add the mouse click listener */
getChart().getPlotArea().addMouseListener(new MouseDownListener());
/* Add the paint listener */
getChart().getPlotArea().addPaintListener(new BarPainterListener());
populate();
}
// ------------------------------------------------------------------------
// Overriden methods
// ------------------------------------------------------------------------
@Override
public void validateChartData() {
super.validateChartData();
/* Make sure the X axis is not continuous */
if (getXDescriptorsInfo().areNumerical()) {
throw new IllegalArgumentException("Bar chart X axis cannot be numerical."); //$NON-NLS-1$
}
/**
* TODO: allow Y discontinuous by mapping each string to a number
*/
/* Make sure the Y axis is continuous */
if (!getYDescriptorsInfo().areNumerical()) {
throw new IllegalArgumentException("Bar chart Y axis must be numerical."); //$NON-NLS-1$
}
/**
* TODO: allow multiple X axes
*/
/* Make sure there is only one X axis */
if (getXDescriptors().stream().distinct().count() > 1) {
throw new IllegalArgumentException("Bar chart can only have one X axis."); //$NON-NLS-1$
}
}
@Override
protected IDataConsumer getXConsumer(@NonNull ChartSeries series) {
IStringResolver<Object> xResolver = IStringResolver.class.cast(series.getX().getResolver());
return new BarStringConsumer(xResolver);
}
@Override
protected IDataConsumer getYConsumer(@NonNull ChartSeries series) {
INumericalResolver<Object, Number> yResolver = INumericalResolver.class.cast(series.getY().getResolver());
Predicate<@Nullable Number> yPredicate;
if (getModel().isYLogscale()) {
yPredicate = new LogarithmicPredicate(yResolver);
} else {
yPredicate = o -> true;
}
return new NumericalConsumer(yResolver, yPredicate);
}
@Override
protected @Nullable IConsumerAggregator getXAggregator() {
return null;
}
@Override
protected @Nullable IConsumerAggregator getYAggregator() {
return new NumericalConsumerAggregator();
}
@Override
protected ISeries createSwtSeries(ChartSeries chartSeries, ISeriesSet swtSeriesSet, @NonNull Color color) {
String title = chartSeries.getY().getLabel();
IBarSeries swtSeries = (IBarSeries) swtSeriesSet.createSeries(SeriesType.BAR, title);
swtSeries.setBarPadding(BAR_PADDING);
swtSeries.setBarColor(color);
return swtSeries;
}
@Override
protected void configureSeries(Map<@NonNull ISeries, Object[]> mapper) {
XYChartConsumer chartConsumer = getChartConsumer();
NumericalConsumerAggregator aggregator = (NumericalConsumerAggregator) checkNotNull(chartConsumer.getYAggregator());
/* Clamp the Y ranges */
fYRanges = clampInputDataRange(checkNotNull(aggregator.getChartRanges()));
/* Generate data for each SWT series */
for (XYSeriesConsumer seriesConsumer : chartConsumer.getSeries()) {
BarStringConsumer xconsumer = (BarStringConsumer) seriesConsumer.getXConsumer();
NumericalConsumer yConsumer = (NumericalConsumer) seriesConsumer.getYConsumer();
Object[] object = seriesConsumer.getConsumedElements().toArray();
/* Generate categories for the X axis */
Collection<@Nullable String> list = xconsumer.getList();
/*
* The categories are nullable, but swtchart does not support null
* values, so we'll update the null values to an empty string
*/
String @Nullable [] categories = list.toArray(new String[list.size()]);
for (int i = 0; i < list.size(); i++) {
if (categories[i] == null) {
categories[i] = "?"; //$NON-NLS-1$
}
}
fCategories = categories;
/* Generate numerical data for the Y axis */
List<Number> data = yConsumer.getData();
double[] 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();
}
/* Set the data for the SWT series */
ISeries series = checkNotNull(getSeriesMap().get(seriesConsumer.getSeries()));
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 -> {
a.enableCategory(true);
a.setCategorySeries(fCategories);
});
/* Format Y axes */
Stream.of(getChart().getAxisSet().getYAxes()).forEach(a -> {
IAxisTick tick = a.getTick();
tick.setFormat(getContinuousAxisFormatter(fYRanges, getYDescriptorsInfo()));
});
}
@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 */
((IBarSeries) series).setBarColor(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.
*/
((IBarSeries) series).setBarColor(lightColor);
}
}
}
@Override
protected void refreshDisplayLabels() {
String @Nullable [] categories = fCategories;
/* Only if we have at least 1 category */
if (categories == null || categories.length == 0) {
return;
}
/* Only refresh if labels are visible */
IAxis xAxis = getChart().getAxisSet().getXAxis(0);
if (!xAxis.getTick().isVisible() || !xAxis.isCategoryEnabled()) {
return;
}
/*
* Shorten all the labels to 5 characters plus "…" when the longest
* label length is more than a percentage of the chart height.
*/
Rectangle rect = getChart().getClientArea();
int lengthLimit = (int) (rect.height * LENGTH_LIMIT);
GC gc = new GC(getParent());
gc.setFont(xAxis.getTick().getFont());
/* Find the longest category string */
String longestString = Arrays.stream(categories).max(Comparator.comparingInt(String::length)).orElse(categories[0]);
/* Get the length and height of the longest label in pixels */
Point pixels = gc.stringExtent(longestString);
/* Completely arbitrary */
int cutLen = 5;
String[] displayCategories = new String[categories.length];
if (pixels.x > lengthLimit) {
/* We have to cut down some strings */
for (int i = 0; i < categories.length; i++) {
if (categories[i].length() > cutLen) {
displayCategories[i] = categories[i].substring(0, cutLen) + ELLIPSIS;
} else {
displayCategories[i] = categories[i];
}
}
} else {
/* All strings should fit */
displayCategories = Arrays.copyOf(categories, categories.length);
}
xAxis.setCategorySeries(displayCategories);
/* Cleanup */
gc.dispose();
}
// ------------------------------------------------------------------------
// Listeners
// ------------------------------------------------------------------------
private final class MouseDownListener extends MouseAdapter {
@Override
public void mouseDown(@Nullable MouseEvent event) {
if (event == null || event.button != 1) {
return;
}
/* Get the click location */
int xClick = event.x;
int yClick = event.y;
/* Check if CTRL is pressed */
boolean ctrl = (event.stateMask & SWT.CTRL) != 0;
/* Find which series contains the click */
boolean found = false;
for (ISeries swtSeries : getChart().getSeriesSet().getSeries()) {
IBarSeries series = (IBarSeries) swtSeries;
/* Look through each rectangle */
Rectangle[] rectangles = series.getBounds();
for (int i = 0; i < rectangles.length; i++) {
if (rectangles[i].contains(xClick, yClick)) {
getSelection().touch(new SwtChartPoint(series, i), ctrl);
found = true;
}
}
}
/* Check if a selection was found */
if (!found) {
getSelection().clear();
}
/* 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(SwtBarChart.this, getData().getDataProvider(), set);
TmfSignalManager.dispatchSignal(signal);
}
}
private final class BarPainterListener implements PaintListener {
@Override
public void paintControl(@Nullable PaintEvent event) {
if (event == null) {
return;
}
/* Don't draw if there's no selection */
if (getSelection().getPoints().isEmpty()) {
return;
}
/* Create iterators for the colors */
Iterator<Color> colors = Iterators.cycle(COLORS);
Iterator<Color> lights = Iterators.cycle(COLORS_LIGHT);
GC gc = event.gc;
/* Redraw all the series */
for (ISeries swtSeries : getChart().getSeriesSet().getSeries()) {
IBarSeries series = (IBarSeries) swtSeries;
Color color = checkNotNull(colors.next());
Color light = checkNotNull(lights.next());
/* Redraw all the rectangles */
for (int i = 0; i < series.getBounds().length; i++) {
gc.setBackground(light);
/* Check if the rectangle is selected */
for (SwtChartPoint point : getSelection().getPoints()) {
if (point.getSeries() == series && point.getIndex() == i) {
gc.setBackground(color);
break;
}
}
gc.fillRectangle(series.getBounds()[i]);
}
}
}
}
}