/*******************************************************************************
* Copyright (c) 2015, 2016 Ericsson
*
* 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
*
* Contributors:
* Alexis Cabana-Loriaux - Initial API and implementation
*
*******************************************************************************/
package org.eclipse.tracecompass.internal.tmf.ui.viewers.piecharts;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.linuxtools.dataviewers.piechart.PieChart;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.tracecompass.internal.tmf.ui.viewers.piecharts.model.TmfPieChartStatisticsModel;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
/**
* Creates a viewer containing 2 pie charts, one for showing information about
* the current selection, and the second one for showing information about the
* current time-range selection. It follows the MVC pattern, being a view.
*
* This class is closely related with the IPieChartViewerState interface that
* acts as a state machine for the general layout of the charts.
*
* @author Alexis Cabana-Loriaux
* @since 2.0
*
*/
public class TmfPieChartViewer extends Composite {
/**
* The pie chart containing global information about the trace
*/
private PieChart fGlobalPC;
/**
* The name of the piechart containing the statistics about the global trace
*/
private String fGlobalPCname;
/**
* The pie chart containing information about the current time-range
* selection
*/
private PieChart fTimeRangePC;
/**
* The name of the piechart containing the statistics about the current
* selection
*/
private String fTimeRangePCname;
/**
* The listener for the mouse movement event.
*/
private Listener fMouseMoveListener;
/**
* The listener for the mouse right click event.
*/
private MouseListener fMouseClickListener;
/**
* The list of listener to notify when an event type is selected
*/
private ListenerList fEventTypeSelectedListeners = new ListenerList(ListenerList.IDENTITY);
/**
* The name of the slice containing the too little slices
*/
private String fOthersSliceName;
/**
* Implementation of the State design pattern to reorder the layout
* depending on the selection. This variable holds the current state of the
* layout.
*/
private IPieChartViewerState fCurrentState;
/**
* Represents the minimum percentage a slice of pie must have in order to be
* shown
*/
private static final float MIN_PRECENTAGE_TO_SHOW_SLICE = 0.025F;// 2.5%
/**
* Represents the maximum number of slices of the pie charts. WE don't want
* to pollute the viewer with too much slice entries.
*/
private static final int NB_MAX_SLICES = 10;
/**
* The data that has to be presented by the pie charts
*/
private TmfPieChartStatisticsModel fModel = null;
/**
* @param parent
* The parent composite that will hold the viewer
*/
public TmfPieChartViewer(Composite parent) {
super(parent, SWT.NONE);
fGlobalPCname = Messages.TmfStatisticsView_GlobalSelectionPieChartName;
fTimeRangePCname = Messages.TmfStatisticsView_TimeRangeSelectionPieChartName;
fOthersSliceName = Messages.TmfStatisticsView_PieChartOthersSliceName;
initContent();
}
// ------------------------------------------------------------------------
// Class methods
// ------------------------------------------------------------------------
/**
* Called by this class' constructor. Constructs the basic viewer containing
* the charts, as well as their listeners
*/
private synchronized void initContent() {
setLayout(new FillLayout());
fGlobalPC = null;
fTimeRangePC = null;
// Setup listeners for the tooltips
fMouseMoveListener = new Listener() {
@Override
public void handleEvent(org.eclipse.swt.widgets.Event event) {
PieChart pc = (PieChart) event.widget;
switch (event.type) {
/* Get tooltip information on the slice */
case SWT.MouseMove:
int sliceIndex = pc.getSliceIndexFromPosition(0, event.x, event.y);
if (sliceIndex < 0) {
// mouse is outside the chart
pc.setToolTipText(null);
break;
}
float percOfSlice = (float) pc.getSlicePercent(0, sliceIndex);
String percent = String.format("%.1f", percOfSlice); //$NON-NLS-1$
Long nbEvents = Long.valueOf((long) pc.getSeriesSet().getSeries()[sliceIndex].getXSeries()[0]);
String text = Messages.TmfStatisticsView_PieChartToolTipTextName + " = " + //$NON-NLS-1$
pc.getSeriesSet().getSeries()[sliceIndex].getId() + "\n"; //$NON-NLS-1$
text += Messages.TmfStatisticsView_PieChartToolTipTextEventCount + " = "//$NON-NLS-1$
+ nbEvents.toString() + " (" + percent + "%)"; //$NON-NLS-1$ //$NON-NLS-2$
pc.setToolTipText(text);
return;
default:
}
}
};
fMouseClickListener = new MouseListener() {
@Override
public void mouseUp(MouseEvent e) {
}
@Override
public void mouseDown(MouseEvent e) {
PieChart pc = (PieChart) e.widget;
int slicenb = pc.getSliceIndexFromPosition(0, e.x, e.y);
if (slicenb < 0 || slicenb >= pc.getSeriesSet().getSeries().length) {
// mouse is outside the chart
return;
}
Event selectionEvent = new Event();
selectionEvent.text = pc.getSeriesSet().getSeries()[slicenb].getId();
notifyEventTypeSelectionListener(selectionEvent);
}
@Override
public void mouseDoubleClick(MouseEvent e) {
}
};
// at creation no content is selected
setCurrentState(new PieChartViewerStateNoContentSelected(this));
}
/**
* Updates the data contained in the Global PieChart by using a Map.
* Normally, this method is only called by the state machine.
*/
synchronized void updateGlobalPieChart() {
if (getGlobalPC() == null) {
fGlobalPC = new PieChart(this, SWT.NONE);
getGlobalPC().getTitle().setText(fGlobalPCname);
getGlobalPC().getAxisSet().getXAxis(0).getTitle().setText(""); //Hide the title over the legend //$NON-NLS-1$
getGlobalPC().getLegend().setVisible(true);
getGlobalPC().getLegend().setPosition(SWT.RIGHT);
getGlobalPC().addListener(SWT.MouseMove, fMouseMoveListener);
getGlobalPC().addMouseListener(fMouseClickListener);
} else if (getGlobalPC().isDisposed() || fModel == null || fModel.getPieChartGlobalModel() == null) {
return;
}
Map<String, Long> totalEventCountForChart = getTotalEventCountForChart(true);
if (totalEventCountForChart == null) {
return;
}
updatePieChartWithData(fGlobalPC, totalEventCountForChart, MIN_PRECENTAGE_TO_SHOW_SLICE, fOthersSliceName);
}
/**
* Updates the data contained in the Time-Range PieChart by using a Map.
* Normally, this method is only called by the state machine.
*/
synchronized void updateTimeRangeSelectionPieChart() {
if (getTimeRangePC() == null) {
fTimeRangePC = new PieChart(this, SWT.NONE);
getTimeRangePC().getTitle().setText(fTimeRangePCname);
getTimeRangePC().getAxisSet().getXAxis(0).getTitle().setText(""); //Hide the title over the legend //$NON-NLS-1$
getTimeRangePC().getLegend().setPosition(SWT.BOTTOM);
getTimeRangePC().getLegend().setVisible(true);
getTimeRangePC().addListener(SWT.MouseMove, fMouseMoveListener);
getTimeRangePC().addMouseListener(fMouseClickListener);
}
else if (getTimeRangePC().isDisposed()) {
return;
}
Map<String, Long> totalEventCountForChart = getTotalEventCountForChart(false);
if (totalEventCountForChart == null) {
return;
}
updatePieChartWithData(fTimeRangePC, totalEventCountForChart, MIN_PRECENTAGE_TO_SHOW_SLICE, fOthersSliceName);
}
/* return the chart-friendly map given by the TmfPieChartStatisticsModel */
private Map<String, Long> getTotalEventCountForChart(boolean isGlobal) {
if (fModel == null) {
return null;
}
Map<ITmfTrace, Map<String, Long>> chartModel;
if (isGlobal) {
chartModel = fModel.getPieChartGlobalModel();
} else {
chartModel = fModel.getPieChartSelectionModel();
}
if (chartModel == null) {
return null;
}
Map<String, Long> totalEventCountForChart = new HashMap<>();
for (Entry<ITmfTrace, Map<String, Long>> entry : chartModel.entrySet()) {
Map<String, Long> traceEventCount = entry.getValue();
if (traceEventCount == null) {
continue;
}
for (Entry<String, Long> event : traceEventCount.entrySet()) {
final Long value = totalEventCountForChart.get(event.getKey());
if (value != null) {
totalEventCountForChart.put(event.getKey(), value + event.getValue());
} else {
totalEventCountForChart.put(event.getKey(), event.getValue());
}
}
}
return totalEventCountForChart;
}
/**
* Reinitializes the charts to their initial state, without any data
*/
public synchronized void reinitializeCharts() {
if (isDisposed()) {
return;
}
if (getGlobalPC() != null && !getGlobalPC().isDisposed()) {
getGlobalPC().dispose();
}
fGlobalPC = new PieChart(this, SWT.NONE);
getGlobalPC().getTitle().setText(fGlobalPCname);
getGlobalPC().getAxisSet().getXAxis(0).getTitle().setText(""); //Hide the title over the legend //$NON-NLS-1$
if (getTimeRangePC() != null && !getTimeRangePC().isDisposed()) {
getTimeRangePC().dispose();
fTimeRangePC = null;
}
layout();
setCurrentState(new PieChartViewerStateNoContentSelected(this));
}
/**
* Function used to update or create the slices of a PieChart to match the
* content of a Map passed in parameter. It also provides a facade to use
* the PieChart API
*/
private static void updatePieChartWithData(
final PieChart chart,
final Map<String, Long> slices,
final float minimumSizeOfSlice,
final String nameOfOthers) {
List<EventOccurrenceObject> chartValues = new ArrayList<>();
Long eventTotal = 0L;
for (Entry<String, Long> entry : slices.entrySet()) {
eventTotal += entry.getValue();
chartValues.add(new EventOccurrenceObject(entry.getKey(), entry.getValue()));
}
// No events in the selection
if (eventTotal == 0) {
// clear the chart and show "NO DATA"
return;
}
/*
* filter out the event types taking too little space in the chart and
* label the whole group together. The remaining slices will be showing
*/
List<EventOccurrenceObject> filteredChartValues = new ArrayList<>();
Long othersEntryCount = 0L;
int nbSlices = 0;
for (EventOccurrenceObject entry : chartValues) {
if (entry.getNbOccurence() / eventTotal.floatValue() > minimumSizeOfSlice && nbSlices <= NB_MAX_SLICES) {
filteredChartValues.add(entry);
nbSlices++;
} else {
othersEntryCount += entry.getNbOccurence();
}
}
Collections.sort(filteredChartValues);
// Add the "Others" slice in the pie if its not empty
if (othersEntryCount != 0) {
filteredChartValues.add(new EventOccurrenceObject(nameOfOthers, othersEntryCount));
}
// put the entries in the chart and add their percentage
double[][] tempValues = new double[filteredChartValues.size()][1];
String[] tempNames = new String[filteredChartValues.size()];
int index = 0;
for (EventOccurrenceObject entry : filteredChartValues) {
tempValues[index][0] = entry.getNbOccurence();
tempNames[index] = entry.getName();
index++;
}
chart.addPieChartSeries(tempNames, tempValues);
}
/**
* Refresh this viewer
*
* @param refreshGlobal
* if we have to refresh the global piechart
* @param refreshSelection
* if we have to refresh the selection piechart
*/
public synchronized void refresh(boolean refreshGlobal, boolean refreshSelection) {
if (fModel == null) {
reinitializeCharts();
} else {
if (refreshGlobal) {
/* will update the global pc */
getCurrentState().newGlobalEntries(this);
}
if (refreshSelection) {
// Check if the selection is empty
int nbEventsType = 0;
Map<String, Long> selectionModel = getTotalEventCountForChart(false);
for (Long l : selectionModel.values()) {
if (l != 0) {
nbEventsType++;
}
}
// Check if the selection is empty or if
// there is enough event types to show in the piecharts
if (nbEventsType < 2) {
getCurrentState().newEmptySelection(this);
} else {
getCurrentState().newSelection(this);
}
}
}
}
/**
* @param l
* the listener to add
*/
public void addEventTypeSelectionListener(Listener l) {
fEventTypeSelectedListeners.add(l);
}
/**
* @param l
* the listener to remove
*/
public void removeEventTypeSelectionListener(Listener l) {
fEventTypeSelectedListeners.remove(l);
}
/* Notify all listeners that an event type has been selected */
private void notifyEventTypeSelectionListener(Event e) {
for (Object o : fEventTypeSelectedListeners.getListeners()) {
((Listener) o).handleEvent(e);
}
}
// ------------------------------------------------------------------------
// Getters
// ------------------------------------------------------------------------
/**
* @return the global piechart
*/
synchronized PieChart getGlobalPC() {
return fGlobalPC;
}
/**
* @return the time-range selection piechart
*/
synchronized PieChart getTimeRangePC() {
return fTimeRangePC;
}
/**
* @return the current state of the viewer
*/
synchronized IPieChartViewerState getCurrentState() {
return fCurrentState;
}
// ------------------------------------------------------------------------
// Setters
// ------------------------------------------------------------------------
/**
* @return the model
*/
public TmfPieChartStatisticsModel getModel() {
return fModel;
}
/**
* @param model
* the model to set
*/
public void setInput(TmfPieChartStatisticsModel model) {
fModel = model;
}
/**
* Normally, this method is only called by the state machine
*
* @param newChart
* the new PieChart
*/
public synchronized void setTimeRangePC(PieChart newChart) {
fTimeRangePC = newChart;
}
/**
* Setter method for the state.
*
* @param newState
* The new state of the viewer Normally only called by classes
* implementing the IPieChartViewerState interface.
*/
public synchronized void setCurrentState(final IPieChartViewerState newState) {
fCurrentState = newState;
}
/**
* Nested class used to handle and sort more easily the pair (Name, Number
* of occurrences)
*
* @author Alexis Cabana-Loriaux
*/
private static class EventOccurrenceObject implements Comparable<EventOccurrenceObject> {
private String fName;
private Long fNbOccurrences;
EventOccurrenceObject(String name, Long nbOccurences) {
this.fName = name;
this.fNbOccurrences = nbOccurences;
}
@Override
public int compareTo(EventOccurrenceObject other) {
// descending order
return Long.compare(other.getNbOccurence(), this.getNbOccurence());
}
public String getName() {
return fName;
}
public Long getNbOccurence() {
return fNbOccurrences;
}
}
}