1 /*******************************************************************************
2 * Copyright (c) 2011 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 *******************************************************************************/
13 package org
.eclipse
.linuxtools
.lttng
.ui
.views
.histogram
;
15 import org
.eclipse
.linuxtools
.tmf
.ui
.views
.TmfView
;
16 import org
.eclipse
.swt
.SWT
;
17 import org
.eclipse
.swt
.events
.ControlEvent
;
18 import org
.eclipse
.swt
.events
.ControlListener
;
19 import org
.eclipse
.swt
.events
.KeyEvent
;
20 import org
.eclipse
.swt
.events
.KeyListener
;
21 import org
.eclipse
.swt
.events
.MouseEvent
;
22 import org
.eclipse
.swt
.events
.MouseListener
;
23 import org
.eclipse
.swt
.events
.MouseTrackListener
;
24 import org
.eclipse
.swt
.events
.PaintEvent
;
25 import org
.eclipse
.swt
.events
.PaintListener
;
26 import org
.eclipse
.swt
.graphics
.Color
;
27 import org
.eclipse
.swt
.graphics
.Font
;
28 import org
.eclipse
.swt
.graphics
.FontData
;
29 import org
.eclipse
.swt
.graphics
.GC
;
30 import org
.eclipse
.swt
.graphics
.Image
;
31 import org
.eclipse
.swt
.layout
.GridData
;
32 import org
.eclipse
.swt
.layout
.GridLayout
;
33 import org
.eclipse
.swt
.widgets
.Canvas
;
34 import org
.eclipse
.swt
.widgets
.Composite
;
35 import org
.eclipse
.swt
.widgets
.Display
;
36 import org
.eclipse
.swt
.widgets
.Text
;
39 * <b><u>Histogram</u></b>
41 * Re-usable histogram widget with the following features:
43 * <li>Y-axis labels displaying min/max count values
44 * <li>X-axis labels displaying time range
45 * <li>a histogram displaying the distribution of values over time (note that
46 * the histogram might not necessarily fill the whole canvas)
48 * The widget also has 2 'markers' to identify:
50 * <li>a red dashed line over the bar that contains the currently selected event
51 * <li>a dark red dashed line that delimits the right end of the histogram (if
52 * it doesn't fill the canvas)
54 * Clicking on the histogram will select the current event at the mouse
57 * Once the histogram is selected, there is some limited keyboard support:
59 * <li>Home: go to the first histogram bar
60 * <li>End: go to the last histogram bar
61 * <li>Left: go to the previous histogram
62 * <li>Right: go to the next histogram bar
64 * Finally, when the mouse hovers over the histogram, a tool tip showing the
65 * following information about the corresponding histogram bar time range:
67 * <li>start of the time range
68 * <li>end of the time range
69 * <li>number of events in that time range
72 public abstract class Histogram
implements ControlListener
, PaintListener
, KeyListener
, MouseListener
, MouseTrackListener
{
74 // ------------------------------------------------------------------------
76 // ------------------------------------------------------------------------
78 // Histogram refresh frequency
79 private final static int REFRESH_FREQUENCY
= HistogramDataModel
.DEFAULT_NUMBER_OF_BUCKETS
;
82 private final Color fBackgroundColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_WHITE
);
83 private final Color fCurrentEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_RED
);
84 private final Color fLastEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_DARK_RED
);
85 private final Color fHistoBarColor
= new Color(Display
.getDefault(), 74, 112, 139);
87 // Timestamp scale (nanosecond)
88 public static final byte TIME_SCALE
= -9;
90 // ------------------------------------------------------------------------
92 // ------------------------------------------------------------------------
95 protected TmfView fParentView
;
97 // Histogram text fields
98 private Text fMaxNbEventsText
;
99 private Text fMinNbEventsText
;
100 private Text fTimeRangeStartText
;
101 private Text fTimeRangeEndText
;
103 // Histogram drawing area
104 protected Canvas fCanvas
;
107 protected final HistogramDataModel fDataModel
;
108 protected HistogramScaledData fScaledData
;
110 protected long fCurrentEventTime
= 0;
112 // ------------------------------------------------------------------------
114 // ------------------------------------------------------------------------
116 public Histogram(TmfView view
, Composite parent
) {
119 createWidget(parent
);
120 fDataModel
= new HistogramDataModel();
123 fCanvas
.addControlListener(this);
124 fCanvas
.addPaintListener(this);
125 fCanvas
.addKeyListener(this);
126 fCanvas
.addMouseListener(this);
127 fCanvas
.addMouseTrackListener(this);
130 public void dispose() {
131 fHistoBarColor
.dispose();
134 private void createWidget(Composite parent
) {
136 final Color labelColor
= parent
.getDisplay().getSystemColor(SWT
.COLOR_TITLE_INACTIVE_BACKGROUND
);
137 final Font fFont
= adjustFont(parent
);
139 final int initalWidth
= 10;
141 // --------------------------------------------------------------------
142 // Define the histogram
143 // --------------------------------------------------------------------
145 GridLayout gridLayout
= new GridLayout();
146 gridLayout
.numColumns
= 3;
147 gridLayout
.marginHeight
= 0;
148 gridLayout
.marginWidth
= 0;
149 gridLayout
.marginTop
= 0;
150 gridLayout
.horizontalSpacing
= 0;
151 gridLayout
.verticalSpacing
= 0;
152 gridLayout
.marginLeft
= 0;
153 gridLayout
.marginRight
= 0;
154 Composite composite
= new Composite(parent
, SWT
.FILL
);
155 composite
.setLayout(gridLayout
);
157 // Use all the horizontal space
158 GridData gridData
= new GridData();
159 gridData
.horizontalAlignment
= SWT
.FILL
;
160 gridData
.verticalAlignment
= SWT
.FILL
;
161 gridData
.grabExcessHorizontalSpace
= true;
162 composite
.setLayoutData(gridData
);
165 gridData
= new GridData();
166 gridData
.horizontalAlignment
= SWT
.RIGHT
;
167 gridData
.verticalAlignment
= SWT
.TOP
;
168 fMaxNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
169 fMaxNbEventsText
.setFont(fFont
);
170 fMaxNbEventsText
.setBackground(labelColor
);
171 fMaxNbEventsText
.setEditable(false);
172 fMaxNbEventsText
.setText("0"); //$NON-NLS-1$
173 fMaxNbEventsText
.setLayoutData(gridData
);
176 gridData
= new GridData();
177 gridData
.horizontalSpan
= 2;
178 gridData
.verticalSpan
= 2;
179 gridData
.horizontalAlignment
= SWT
.FILL
;
180 gridData
.verticalAlignment
= SWT
.FILL
;
181 gridData
.grabExcessHorizontalSpace
= true;
182 fCanvas
= new Canvas(composite
, SWT
.BORDER
| SWT
.DOUBLE_BUFFERED
);
183 fCanvas
.setLayoutData(gridData
);
185 // Y-axis min event (always 0...)
186 gridData
= new GridData();
187 gridData
.horizontalAlignment
= SWT
.RIGHT
;
188 gridData
.verticalAlignment
= SWT
.BOTTOM
;
189 fMinNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
190 fMinNbEventsText
.setFont(fFont
);
191 fMinNbEventsText
.setBackground(labelColor
);
192 fMinNbEventsText
.setEditable(false);
193 fMinNbEventsText
.setText("0"); //$NON-NLS-1$
194 fMinNbEventsText
.setLayoutData(gridData
);
197 gridData
= new GridData(initalWidth
, SWT
.DEFAULT
);
198 gridData
.horizontalAlignment
= SWT
.RIGHT
;
199 gridData
.verticalAlignment
= SWT
.BOTTOM
;
200 Text dummyText
= new Text(composite
, SWT
.READ_ONLY
);
201 dummyText
.setFont(fFont
);
202 dummyText
.setBackground(labelColor
);
203 dummyText
.setEditable(false);
204 dummyText
.setText(""); //$NON-NLS-1$
205 dummyText
.setLayoutData(gridData
);
207 // Window range start time
208 gridData
= new GridData();
209 gridData
.horizontalAlignment
= SWT
.LEFT
;
210 gridData
.verticalAlignment
= SWT
.BOTTOM
;
211 fTimeRangeStartText
= new Text(composite
, SWT
.READ_ONLY
);
212 fTimeRangeStartText
.setFont(fFont
);
213 fTimeRangeStartText
.setBackground(labelColor
);
214 fTimeRangeStartText
.setText(HistogramUtils
.nanosecondsToString(0));
215 fTimeRangeStartText
.setLayoutData(gridData
);
217 // Window range end time
218 gridData
= new GridData();
219 gridData
.horizontalAlignment
= SWT
.RIGHT
;
220 gridData
.verticalAlignment
= SWT
.BOTTOM
;
221 fTimeRangeEndText
= new Text(composite
, SWT
.READ_ONLY
);
222 fTimeRangeEndText
.setFont(fFont
);
223 fTimeRangeEndText
.setBackground(labelColor
);
224 fTimeRangeEndText
.setText(HistogramUtils
.nanosecondsToString(0));
225 fTimeRangeEndText
.setLayoutData(gridData
);
228 private Font
adjustFont(Composite composite
) {
229 // Reduce font size for a more pleasing rendering
230 int fontSizeAdjustment
= -2;
231 Font font
= composite
.getFont();
232 FontData fontData
= font
.getFontData()[0];
233 return new Font(font
.getDevice(), fontData
.getName(), fontData
.getHeight() + fontSizeAdjustment
, fontData
.getStyle());
236 // ------------------------------------------------------------------------
238 // ------------------------------------------------------------------------
240 public long getStartTime() {
241 return fDataModel
.getStartTime();
244 public long getEndTime() {
245 return fDataModel
.getEndTime();
248 public long getTimeLimit() {
249 return fDataModel
.getTimeLimit();
252 // ------------------------------------------------------------------------
254 // ------------------------------------------------------------------------
256 public abstract void updateTimeRange(long startTime
, long endTime
);
259 * Clear the histogram and reset the data
261 public void clear() {
268 * Increase the histogram bucket corresponding to [timestamp]
272 public void countEvent(long timestamp
) {
273 fDataModel
.countEvent(timestamp
);
274 if (fDataModel
.getNbEvents() % REFRESH_FREQUENCY
== 0) {
276 refresh(); // This is intentional. Exercise left to the reader :-)
281 * Sets the current event time and refresh the display
285 public void setCurrentEvent(long timestamp
) {
286 fCurrentEventTime
= (timestamp
> 0) ? timestamp
: 0;
287 fDataModel
.setCurrentEvent(timestamp
);
292 * Computes the timestamp of the bucket at [offset]
294 * @param offset offset from the left on the histogram
295 * @return the start timestamp of the corresponding bucket
297 public synchronized long getTimestamp(int offset
) {
298 assert offset
> 0 && offset
< fScaledData
.fWidth
;
300 return fDataModel
.getStartTime() + fScaledData
.fBucketDuration
* offset
;
301 } catch (Exception e
) {
302 return 0; // TODO: Fix that racing condition (NPE)
307 * Computes the offset of the timestamp in the histogram
309 * @param timestamp the timestamp
310 * @return the offset of the corresponding bucket (-1 if invalid)
312 public synchronized int getOffset(long timestamp
) {
313 if (timestamp
< fDataModel
.getStartTime() || timestamp
> fDataModel
.getEndTime())
315 return (int) ((timestamp
- fDataModel
.getStartTime()) / fScaledData
.fBucketDuration
);
319 * Move the currently selected bar cursor to a non-empty bucket.
321 * @param keyCode the SWT key code
323 protected void moveCursor(int keyCode
) {
325 if (fScaledData
.fCurrentBucket
== HistogramScaledData
.OUT_OF_RANGE_BUCKET
)
333 while (index
< fScaledData
.fLastBucket
&& fScaledData
.fData
[index
] == 0)
335 if (index
< fScaledData
.fLastBucket
)
336 fScaledData
.fCurrentBucket
= index
;
339 case SWT
.ARROW_RIGHT
:
340 index
= fScaledData
.fCurrentBucket
+ 1;
341 while (index
< fScaledData
.fWidth
&& fScaledData
.fData
[index
] == 0)
343 if (index
< fScaledData
.fLastBucket
)
344 fScaledData
.fCurrentBucket
= index
;
348 index
= fScaledData
.fLastBucket
;
349 while (index
>= 0 && fScaledData
.fData
[index
] == 0)
352 fScaledData
.fCurrentBucket
= index
;
356 index
= fScaledData
.fCurrentBucket
- 1;
357 while (index
>= 0 && fScaledData
.fData
[index
] == 0)
360 fScaledData
.fCurrentBucket
= index
;
367 updateCurrentEventTime();
371 * Refresh the histogram display
373 protected void refresh() {
374 if (!fCanvas
.isDisposed() && fCanvas
.getDisplay() != null) {
375 fCanvas
.getDisplay().asyncExec(new Runnable() {
378 if (!fCanvas
.isDisposed()) {
379 // Retrieve and normalize the data
380 int canvasWidth
= fCanvas
.getBounds().width
;
381 int canvasHeight
= fCanvas
.getBounds().height
;
382 if (canvasWidth
<= 0 || canvasHeight
<= 0)
384 fDataModel
.setCurrentEvent(fCurrentEventTime
);
385 fScaledData
= fDataModel
.scaleTo(canvasWidth
, canvasHeight
);
387 // Display histogram and update X-,Y-axis labels
388 fTimeRangeStartText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getStartTime()));
389 fTimeRangeEndText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getEndTime()));
390 fMaxNbEventsText
.setText(Long
.toString(fScaledData
.fMaxValue
));
391 // The Y-axis area might need to be re-sized
392 fMaxNbEventsText
.getParent().layout();
399 // ------------------------------------------------------------------------
401 // ------------------------------------------------------------------------
403 private void updateCurrentEventTime() {
404 long bucketStartTime
= getTimestamp(fScaledData
.fCurrentBucket
);
405 ((HistogramView
) fParentView
).updateCurrentEventTime(bucketStartTime
);
408 // ------------------------------------------------------------------------
410 // ------------------------------------------------------------------------
412 protected final String IMAGE_KEY
= "double-buffer-image"; //$NON-NLS-1$
415 public void paintControl(PaintEvent event
) {
418 int canvasWidth
= fCanvas
.getBounds().width
;
419 int canvasHeight
= fCanvas
.getBounds().height
;
421 // Make sure we have something to draw upon
422 if (canvasWidth
<= 0 || canvasHeight
<= 0)
425 // Retrieve image; re-create only if necessary
426 Image image
= (Image
) fCanvas
.getData(IMAGE_KEY
);
427 if (image
== null || image
.getBounds().width
!= canvasWidth
|| image
.getBounds().height
!= canvasHeight
) {
428 image
= new Image(event
.display
, canvasWidth
, canvasHeight
);
429 fCanvas
.setData(IMAGE_KEY
, image
);
432 // Draw the histogram on its canvas
433 GC imageGC
= new GC(image
);
434 formatImage(imageGC
, image
);
435 event
.gc
.drawImage(image
, 0, 0);
439 private void formatImage(GC imageGC
, Image image
) {
441 if (fScaledData
== null)
444 HistogramScaledData scaledData
= new HistogramScaledData(fScaledData
);
447 // Get drawing boundaries
448 int width
= image
.getBounds().width
;
449 int height
= image
.getBounds().height
;
451 // Clear the drawing area
452 imageGC
.setBackground(fBackgroundColor
);
453 imageGC
.fillRectangle(0, 0, image
.getBounds().width
+ 1, image
.getBounds().height
+ 1);
455 // Draw the histogram bars
456 imageGC
.setBackground(fHistoBarColor
);
457 int limit
= width
< scaledData
.fWidth ? width
: scaledData
.fWidth
;
458 for (int i
= 1; i
< limit
; i
++) {
459 int value
= (int) (scaledData
.fData
[i
] * scaledData
.fScalingFactor
);
460 imageGC
.fillRectangle(i
, height
- value
, 1, value
);
463 // Draw the current event bar
464 int currentBucket
= scaledData
.fCurrentBucket
;
465 if (currentBucket
>= 0 && currentBucket
< limit
) {
466 drawDelimiter(imageGC
, fCurrentEventColor
, height
, currentBucket
);
469 // Add a dashed line as a delimiter (at the right of the last bar)
470 int lastEventIndex
= limit
- 1;
471 while (lastEventIndex
>= 0 && scaledData
.fData
[lastEventIndex
] == 0)
473 lastEventIndex
+= (lastEventIndex
< limit
- 1) ?
1 : 0;
474 drawDelimiter(imageGC
, fLastEventColor
, height
, lastEventIndex
);
475 } catch (Exception e
) {
480 private void drawDelimiter(GC imageGC
, Color color
, int height
, int index
) {
481 imageGC
.setBackground(color
);
482 int dash
= height
/ 4;
483 imageGC
.fillRectangle(index
, 0 * dash
, 1, dash
- 1);
484 imageGC
.fillRectangle(index
, 1 * dash
, 1, dash
- 1);
485 imageGC
.fillRectangle(index
, 2 * dash
, 1, dash
- 1);
486 imageGC
.fillRectangle(index
, 3 * dash
, 1, height
- 3 * dash
);
489 // ------------------------------------------------------------------------
491 // ------------------------------------------------------------------------
494 public void keyPressed(KeyEvent event
) {
495 moveCursor(event
.keyCode
);
499 public void keyReleased(KeyEvent event
) {
502 // ------------------------------------------------------------------------
504 // ------------------------------------------------------------------------
507 public void mouseDoubleClick(MouseEvent event
) {
511 public void mouseDown(MouseEvent event
) {
512 if (fDataModel
.getNbEvents() > 0 && fScaledData
.fLastBucket
>= event
.x
) {
513 fScaledData
.fCurrentBucket
= event
.x
;
514 updateCurrentEventTime();
519 public void mouseUp(MouseEvent event
) {
522 // ------------------------------------------------------------------------
523 // MouseTrackListener
524 // ------------------------------------------------------------------------
527 public void mouseEnter(MouseEvent event
) {
531 public void mouseExit(MouseEvent event
) {
535 public void mouseHover(MouseEvent event
) {
536 if (fDataModel
.getNbEvents() > 0 && fScaledData
!= null && fScaledData
.fLastBucket
>= event
.x
) {
537 String tooltip
= formatToolTipLabel(event
.x
);
538 fCanvas
.setToolTipText(tooltip
);
542 private String
formatToolTipLabel(int index
) {
543 long startTime
= fDataModel
.getStartTime() + fScaledData
.fCurrentBucket
* fScaledData
.fBucketDuration
;
544 long endTime
= startTime
+ fScaledData
.fBucketDuration
;
545 int nbEvents
= (index
>= 0) ? fScaledData
.fData
[index
] : 0;
547 StringBuffer buffer
= new StringBuffer();
548 buffer
.append("Range = ["); //$NON-NLS-1$
549 buffer
.append(HistogramUtils
.nanosecondsToString(startTime
));
550 buffer
.append(","); //$NON-NLS-1$
551 buffer
.append(HistogramUtils
.nanosecondsToString(endTime
));
552 buffer
.append(")\n"); //$NON-NLS-1$
553 buffer
.append("Event count = "); //$NON-NLS-1$
554 buffer
.append(nbEvents
);
555 return buffer
.toString();
558 // ------------------------------------------------------------------------
560 // ------------------------------------------------------------------------
563 public void controlMoved(ControlEvent event
) {
568 public void controlResized(ControlEvent event
) {