1
/*******************************************************************************
2
* Copyright (c) 2011, 2012 Ericsson
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
10
* Francois Chouinard - Initial API and implementation
11
* Bernd Hufmann - Changed to updated histogram data model
12
* Francois Chouinard - Initial API and implementation
13
*******************************************************************************/
15
package org.eclipse.linuxtools.tmf.ui.views.histogram;
17
import org.eclipse.linuxtools.tmf.ui.views.TmfView;
18
import org.eclipse.swt.SWT;
19
import org.eclipse.swt.events.ControlEvent;
20
import org.eclipse.swt.events.ControlListener;
21
import org.eclipse.swt.events.KeyEvent;
22
import org.eclipse.swt.events.KeyListener;
23
import org.eclipse.swt.events.MouseEvent;
24
import org.eclipse.swt.events.MouseListener;
25
import org.eclipse.swt.events.MouseTrackListener;
26
import org.eclipse.swt.events.PaintEvent;
27
import org.eclipse.swt.events.PaintListener;
28
import org.eclipse.swt.graphics.Color;
29
import org.eclipse.swt.graphics.Font;
30
import org.eclipse.swt.graphics.FontData;
31
import org.eclipse.swt.graphics.GC;
32
import org.eclipse.swt.graphics.Image;
33
import org.eclipse.swt.layout.GridData;
34
import org.eclipse.swt.layout.GridLayout;
35
import org.eclipse.swt.widgets.Canvas;
36
import org.eclipse.swt.widgets.Composite;
37
import org.eclipse.swt.widgets.Display;
38
import org.eclipse.swt.widgets.Text;
41
* Re-usable histogram widget.
43
* It has the following features:
45
* <li>Y-axis labels displaying min/max count values
46
* <li>X-axis labels displaying time range
47
* <li>a histogram displaying the distribution of values over time (note that
48
* the histogram might not necessarily fill the whole canvas)
50
* The widget also has 2 'markers' to identify:
52
* <li>a red dashed line over the bar that contains the currently selected event
53
* <li>a dark red dashed line that delimits the right end of the histogram (if
54
* it doesn't fill the canvas)
56
* Clicking on the histogram will select the current event at the mouse
59
* Once the histogram is selected, there is some limited keyboard support:
61
* <li>Home: go to the first histogram bar
62
* <li>End: go to the last histogram bar
63
* <li>Left: go to the previous histogram
64
* <li>Right: go to the next histogram bar
66
* Finally, when the mouse hovers over the histogram, a tool tip showing the
67
* following information about the corresponding histogram bar time range:
69
* <li>start of the time range
70
* <li>end of the time range
71
* <li>number of events in that time range
75
* @author Francois Chouinard
77
public abstract class Histogram implements ControlListener, PaintListener, KeyListener, MouseListener, MouseTrackListener, IHistogramModelListener {
79
// ------------------------------------------------------------------------
81
// ------------------------------------------------------------------------
84
private final Color fBackgroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE);
85
private final Color fCurrentEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_RED);
86
private final Color fLastEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_RED);
87
private final Color fHistoBarColor = new Color(Display.getDefault(), 74, 112, 139);
89
// Timestamp scale (nanosecond)
91
* The time scale of the histogram (nano seconds)
93
public static final byte TIME_SCALE = -9;
96
* The histogram bar width (number of pixels).
98
public static final int HISTOGRAM_BAR_WIDTH = 1;
100
// ------------------------------------------------------------------------
102
// ------------------------------------------------------------------------
106
* The parent TMF view.
108
protected TmfView fParentView;
110
// Histogram text fields
111
private Text fMaxNbEventsText;
112
private Text fMinNbEventsText;
113
private Text fTimeRangeStartText;
114
private Text fTimeRangeEndText;
117
* Histogram drawing area
119
protected Canvas fCanvas;
121
* The histogram data model.
123
protected final HistogramDataModel fDataModel;
125
* The histogram data model scaled to current resolution and screen width.
127
protected HistogramScaledData fScaledData;
129
protected long fCurrentEventTime = 0;
131
// ------------------------------------------------------------------------
133
// ------------------------------------------------------------------------
136
* Standard constructor.
138
* @param view A reference to the parent TMF view.
139
* @param parent A parent composite
141
public Histogram(final TmfView view, final Composite parent) {
144
createWidget(parent);
145
fDataModel = new HistogramDataModel();
146
fDataModel.addHistogramListener(this);
149
fCanvas.addControlListener(this);
150
fCanvas.addPaintListener(this);
151
fCanvas.addKeyListener(this);
152
fCanvas.addMouseListener(this);
153
fCanvas.addMouseTrackListener(this);
157
* Dispose resources and deregisters listeners.
159
public void dispose() {
160
fHistoBarColor.dispose();
161
fDataModel.removeHistogramListener(this);
164
private void createWidget(final Composite parent) {
166
final Color labelColor = parent.getBackground();
167
final Font fFont = adjustFont(parent);
169
final int initalWidth = 10;
171
// --------------------------------------------------------------------
172
// Define the histogram
173
// --------------------------------------------------------------------
175
final GridLayout gridLayout = new GridLayout();
176
gridLayout.numColumns = 3;
177
gridLayout.marginHeight = 0;
178
gridLayout.marginWidth = 0;
179
gridLayout.marginTop = 0;
180
gridLayout.horizontalSpacing = 0;
181
gridLayout.verticalSpacing = 0;
182
gridLayout.marginLeft = 0;
183
gridLayout.marginRight = 0;
184
final Composite composite = new Composite(parent, SWT.FILL);
185
composite.setLayout(gridLayout);
187
// Use all the horizontal space
188
GridData gridData = new GridData();
189
gridData.horizontalAlignment = SWT.FILL;
190
gridData.verticalAlignment = SWT.FILL;
191
gridData.grabExcessHorizontalSpace = true;
192
composite.setLayoutData(gridData);
195
gridData = new GridData();
196
gridData.horizontalAlignment = SWT.RIGHT;
197
gridData.verticalAlignment = SWT.TOP;
198
fMaxNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
199
fMaxNbEventsText.setFont(fFont);
200
fMaxNbEventsText.setBackground(labelColor);
201
fMaxNbEventsText.setEditable(false);
202
fMaxNbEventsText.setText("0"); //$NON-NLS-1$
203
fMaxNbEventsText.setLayoutData(gridData);
206
gridData = new GridData();
207
gridData.horizontalSpan = 2;
208
gridData.verticalSpan = 2;
209
gridData.horizontalAlignment = SWT.FILL;
210
gridData.verticalAlignment = SWT.FILL;
211
gridData.grabExcessHorizontalSpace = true;
212
fCanvas = new Canvas(composite, SWT.BORDER | SWT.DOUBLE_BUFFERED);
213
fCanvas.setLayoutData(gridData);
215
// Y-axis min event (always 0...)
216
gridData = new GridData();
217
gridData.horizontalAlignment = SWT.RIGHT;
218
gridData.verticalAlignment = SWT.BOTTOM;
219
fMinNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
220
fMinNbEventsText.setFont(fFont);
221
fMinNbEventsText.setBackground(labelColor);
222
fMinNbEventsText.setEditable(false);
223
fMinNbEventsText.setText("0"); //$NON-NLS-1$
224
fMinNbEventsText.setLayoutData(gridData);
227
gridData = new GridData(initalWidth, SWT.DEFAULT);
228
gridData.horizontalAlignment = SWT.RIGHT;
229
gridData.verticalAlignment = SWT.BOTTOM;
230
final Text dummyText = new Text(composite, SWT.READ_ONLY);
231
dummyText.setFont(fFont);
232
dummyText.setBackground(labelColor);
233
dummyText.setEditable(false);
234
dummyText.setText(""); //$NON-NLS-1$
235
dummyText.setLayoutData(gridData);
237
// Window range start time
238
gridData = new GridData();
239
gridData.horizontalAlignment = SWT.LEFT;
240
gridData.verticalAlignment = SWT.BOTTOM;
241
fTimeRangeStartText = new Text(composite, SWT.READ_ONLY);
242
fTimeRangeStartText.setFont(fFont);
243
fTimeRangeStartText.setBackground(labelColor);
244
fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(0));
245
fTimeRangeStartText.setLayoutData(gridData);
247
// Window range end time
248
gridData = new GridData();
249
gridData.horizontalAlignment = SWT.RIGHT;
250
gridData.verticalAlignment = SWT.BOTTOM;
251
fTimeRangeEndText = new Text(composite, SWT.READ_ONLY);
252
fTimeRangeEndText.setFont(fFont);
253
fTimeRangeEndText.setBackground(labelColor);
254
fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(0));
255
fTimeRangeEndText.setLayoutData(gridData);
258
private Font adjustFont(final Composite composite) {
259
// Reduce font size for a more pleasing rendering
260
final int fontSizeAdjustment = -2;
261
final Font font = composite.getFont();
262
final FontData fontData = font.getFontData()[0];
263
return new Font(font.getDevice(), fontData.getName(), fontData.getHeight() + fontSizeAdjustment, fontData.getStyle());
266
// ------------------------------------------------------------------------
268
// ------------------------------------------------------------------------
271
* Returns the start time (equal first bucket time).
272
* @return the start time.
274
public long getStartTime() {
275
return fDataModel.getFirstBucketTime();
279
* Returns the end time.
280
* @return the end time.
282
public long getEndTime() {
283
return fDataModel.getEndTime();
287
* Returns the time limit (end of last bucket)
288
* @return the time limit.
290
public long getTimeLimit() {
291
return fDataModel.getTimeLimit();
295
* Returns a data model reference.
296
* @return data model.
298
public HistogramDataModel getDataModel() {
302
// ------------------------------------------------------------------------
304
// ------------------------------------------------------------------------
306
* Updates the time range.
307
* @param startTime A start time
308
* @param endTime A end time.
310
public abstract void updateTimeRange(long startTime, long endTime);
313
* Clear the histogram and reset the data
315
public void clear() {
321
* Increase the histogram bucket corresponding to [timestamp]
325
public void countEvent(final long eventCount, final long timestamp) {
326
fDataModel.countEvent(eventCount, timestamp);
330
* Sets the current event time and refresh the display
334
public void setCurrentEvent(final long timestamp) {
335
fCurrentEventTime = (timestamp > 0) ? timestamp : 0;
336
fDataModel.setCurrentEventNotifyListeners(timestamp);
340
* Computes the timestamp of the bucket at [offset]
342
* @param offset offset from the left on the histogram
343
* @return the start timestamp of the corresponding bucket
345
public synchronized long getTimestamp(final int offset) {
346
assert offset > 0 && offset < fScaledData.fWidth;
348
return fDataModel.getFirstBucketTime() + fScaledData.fBucketDuration * offset;
349
} catch (final Exception e) {
350
return 0; // TODO: Fix that racing condition (NPE)
355
* Computes the offset of the timestamp in the histogram
357
* @param timestamp the timestamp
358
* @return the offset of the corresponding bucket (-1 if invalid)
360
public synchronized int getOffset(final long timestamp) {
361
if (timestamp < fDataModel.getFirstBucketTime() || timestamp > fDataModel.getEndTime())
363
return (int) ((timestamp - fDataModel.getFirstBucketTime()) / fScaledData.fBucketDuration);
367
* Move the currently selected bar cursor to a non-empty bucket.
369
* @param keyCode the SWT key code
371
protected void moveCursor(final int keyCode) {
373
if (fScaledData.fCurrentBucket == HistogramScaledData.OUT_OF_RANGE_BUCKET)
381
while (index < fScaledData.fLastBucket && fScaledData.fData[index] == 0)
383
if (index < fScaledData.fLastBucket)
384
fScaledData.fCurrentBucket = index;
387
case SWT.ARROW_RIGHT:
388
index = fScaledData.fCurrentBucket + 1;
389
while (index < fScaledData.fWidth && fScaledData.fData[index] == 0)
391
if (index < fScaledData.fLastBucket)
392
fScaledData.fCurrentBucket = index;
396
index = fScaledData.fLastBucket;
397
while (index >= 0 && fScaledData.fData[index] == 0)
400
fScaledData.fCurrentBucket = index;
404
index = fScaledData.fCurrentBucket - 1;
405
while (index >= 0 && fScaledData.fData[index] == 0)
408
fScaledData.fCurrentBucket = index;
415
updateCurrentEventTime();
419
* Refresh the histogram display
422
public void modelUpdated() {
423
if (!fCanvas.isDisposed() && fCanvas.getDisplay() != null)
424
fCanvas.getDisplay().asyncExec(new Runnable() {
427
if (!fCanvas.isDisposed()) {
428
// Retrieve and normalize the data
429
final int canvasWidth = fCanvas.getBounds().width;
430
final int canvasHeight = fCanvas.getBounds().height;
431
if (canvasWidth <= 0 || canvasHeight <= 0)
433
fDataModel.setCurrentEvent(fCurrentEventTime);
434
fScaledData = fDataModel.scaleTo(canvasWidth, canvasHeight, HISTOGRAM_BAR_WIDTH);
435
synchronized(fScaledData) {
436
if (fScaledData != null) {
438
// Display histogram and update X-,Y-axis labels
439
fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(fDataModel.getFirstBucketTime()));
440
fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(fDataModel.getEndTime()));
441
fMaxNbEventsText.setText(Long.toString(fScaledData.fMaxValue));
442
// The Y-axis area might need to be re-sized
443
fMaxNbEventsText.getParent().layout();
451
// ------------------------------------------------------------------------
453
// ------------------------------------------------------------------------
455
private void updateCurrentEventTime() {
456
final long bucketStartTime = getTimestamp(fScaledData.fCurrentBucket);
457
((HistogramView) fParentView).updateCurrentEventTime(bucketStartTime);
460
// ------------------------------------------------------------------------
462
// ------------------------------------------------------------------------
464
* Image key string for the canvas.
466
protected final String IMAGE_KEY = "double-buffer-image"; //$NON-NLS-1$
469
public void paintControl(final PaintEvent event) {
472
final int canvasWidth = fCanvas.getBounds().width;
473
final int canvasHeight = fCanvas.getBounds().height;
475
// Make sure we have something to draw upon
476
if (canvasWidth <= 0 || canvasHeight <= 0)
479
// Retrieve image; re-create only if necessary
480
Image image = (Image) fCanvas.getData(IMAGE_KEY);
481
if (image == null || image.getBounds().width != canvasWidth || image.getBounds().height != canvasHeight) {
482
image = new Image(event.display, canvasWidth, canvasHeight);
483
fCanvas.setData(IMAGE_KEY, image);
486
// Draw the histogram on its canvas
487
final GC imageGC = new GC(image);
488
formatImage(imageGC, image);
489
event.gc.drawImage(image, 0, 0);
493
private void formatImage(final GC imageGC, final Image image) {
495
if (fScaledData == null)
498
final HistogramScaledData scaledData = new HistogramScaledData(fScaledData);
501
// Get drawing boundaries
502
final int width = image.getBounds().width;
503
final int height = image.getBounds().height;
505
// Clear the drawing area
506
imageGC.setBackground(fBackgroundColor);
507
imageGC.fillRectangle(0, 0, image.getBounds().width + 1, image.getBounds().height + 1);
509
// Draw the histogram bars
510
imageGC.setBackground(fHistoBarColor);
511
final int limit = width < scaledData.fWidth ? width : scaledData.fWidth;
512
for (int i = 1; i < limit; i++) {
513
final int value = (int) (scaledData.fData[i] * scaledData.fScalingFactor);
514
imageGC.fillRectangle(i, height - value, 1, value);
517
// Draw the current event bar
518
final int currentBucket = scaledData.fCurrentBucket;
519
if (currentBucket >= 0 && currentBucket < limit)
520
drawDelimiter(imageGC, fCurrentEventColor, height, currentBucket);
522
// Add a dashed line as a delimiter (at the right of the last bar)
523
int lastEventIndex = limit - 1;
524
while (lastEventIndex >= 0 && scaledData.fData[lastEventIndex] == 0)
526
lastEventIndex += (lastEventIndex < limit - 1) ? 1 : 0;
527
drawDelimiter(imageGC, fLastEventColor, height, lastEventIndex);
528
} catch (final Exception e) {
533
private void drawDelimiter(final GC imageGC, final Color color, final int height, final int index) {
534
imageGC.setBackground(color);
535
final int dash = height / 4;
536
imageGC.fillRectangle(index, 0 * dash, 1, dash - 1);
537
imageGC.fillRectangle(index, 1 * dash, 1, dash - 1);
538
imageGC.fillRectangle(index, 2 * dash, 1, dash - 1);
539
imageGC.fillRectangle(index, 3 * dash, 1, height - 3 * dash);
542
// ------------------------------------------------------------------------
544
// ------------------------------------------------------------------------
547
public void keyPressed(final KeyEvent event) {
548
moveCursor(event.keyCode);
552
public void keyReleased(final KeyEvent event) {
555
// ------------------------------------------------------------------------
557
// ------------------------------------------------------------------------
560
public void mouseDoubleClick(final MouseEvent event) {
564
public void mouseDown(final MouseEvent event) {
565
if (fDataModel.getNbEvents() > 0 && fScaledData.fLastBucket >= event.x) {
566
fScaledData.fCurrentBucket = event.x;
567
updateCurrentEventTime();
572
public void mouseUp(final MouseEvent event) {
575
// ------------------------------------------------------------------------
576
// MouseTrackListener
577
// ------------------------------------------------------------------------
580
public void mouseEnter(final MouseEvent event) {
584
public void mouseExit(final MouseEvent event) {
588
public void mouseHover(final MouseEvent event) {
589
if (fDataModel.getNbEvents() > 0 && fScaledData != null && fScaledData.fLastBucket >= event.x) {
590
final String tooltip = formatToolTipLabel(event.x);
591
fCanvas.setToolTipText(tooltip);
595
private String formatToolTipLabel(final int index) {
596
long startTime = fScaledData.getBucketStartTime(fScaledData.fCurrentBucket);
597
// negative values are possible if time values came into the model in decreasing order
600
final long endTime = fScaledData.getBucketEndTime(fScaledData.fCurrentBucket);
601
final int nbEvents = (index >= 0) ? fScaledData.fData[index] : 0;
603
final StringBuffer buffer = new StringBuffer();
604
buffer.append("Range = ["); //$NON-NLS-1$
605
buffer.append(HistogramUtils.nanosecondsToString(startTime));
606
buffer.append(","); //$NON-NLS-1$
607
buffer.append(HistogramUtils.nanosecondsToString(endTime));
608
buffer.append(")\n"); //$NON-NLS-1$
609
buffer.append("Event count = "); //$NON-NLS-1$
610
buffer.append(nbEvents);
611
return buffer.toString();
614
// ------------------------------------------------------------------------
616
// ------------------------------------------------------------------------
619
public void controlMoved(final ControlEvent event) {
620
fDataModel.complete();
624
public void controlResized(final ControlEvent event) {
625
fDataModel.complete();