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 * Bernd Hufmann - Changed to updated histogram data model
12 *******************************************************************************/
14 package org
.eclipse
.linuxtools
.lttng
.ui
.views
.histogram
;
16 import org
.eclipse
.linuxtools
.tmf
.ui
.views
.TmfView
;
17 import org
.eclipse
.swt
.SWT
;
18 import org
.eclipse
.swt
.events
.ControlEvent
;
19 import org
.eclipse
.swt
.events
.ControlListener
;
20 import org
.eclipse
.swt
.events
.KeyEvent
;
21 import org
.eclipse
.swt
.events
.KeyListener
;
22 import org
.eclipse
.swt
.events
.MouseEvent
;
23 import org
.eclipse
.swt
.events
.MouseListener
;
24 import org
.eclipse
.swt
.events
.MouseTrackListener
;
25 import org
.eclipse
.swt
.events
.PaintEvent
;
26 import org
.eclipse
.swt
.events
.PaintListener
;
27 import org
.eclipse
.swt
.graphics
.Color
;
28 import org
.eclipse
.swt
.graphics
.Font
;
29 import org
.eclipse
.swt
.graphics
.FontData
;
30 import org
.eclipse
.swt
.graphics
.GC
;
31 import org
.eclipse
.swt
.graphics
.Image
;
32 import org
.eclipse
.swt
.layout
.GridData
;
33 import org
.eclipse
.swt
.layout
.GridLayout
;
34 import org
.eclipse
.swt
.widgets
.Canvas
;
35 import org
.eclipse
.swt
.widgets
.Composite
;
36 import org
.eclipse
.swt
.widgets
.Display
;
37 import org
.eclipse
.swt
.widgets
.Text
;
40 * <b><u>Histogram</u></b>
42 * Re-usable histogram widget with the following features:
44 * <li>Y-axis labels displaying min/max count values
45 * <li>X-axis labels displaying time range
46 * <li>a histogram displaying the distribution of values over time (note that
47 * the histogram might not necessarily fill the whole canvas)
49 * The widget also has 2 'markers' to identify:
51 * <li>a red dashed line over the bar that contains the currently selected event
52 * <li>a dark red dashed line that delimits the right end of the histogram (if
53 * it doesn't fill the canvas)
55 * Clicking on the histogram will select the current event at the mouse
58 * Once the histogram is selected, there is some limited keyboard support:
60 * <li>Home: go to the first histogram bar
61 * <li>End: go to the last histogram bar
62 * <li>Left: go to the previous histogram
63 * <li>Right: go to the next histogram bar
65 * Finally, when the mouse hovers over the histogram, a tool tip showing the
66 * following information about the corresponding histogram bar time range:
68 * <li>start of the time range
69 * <li>end of the time range
70 * <li>number of events in that time range
73 public abstract class Histogram
implements ControlListener
, PaintListener
, KeyListener
, MouseListener
, MouseTrackListener
, IHistogramModelListener
{
75 // ------------------------------------------------------------------------
77 // ------------------------------------------------------------------------
80 private final Color fBackgroundColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_WHITE
);
81 private final Color fCurrentEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_RED
);
82 private final Color fLastEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_DARK_RED
);
83 private final Color fHistoBarColor
= new Color(Display
.getDefault(), 74, 112, 139);
85 // Timestamp scale (nanosecond)
86 public static final byte TIME_SCALE
= -9;
88 public static final int HISTOGRAM_BAR_WIDTH
= 1;
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();
121 fDataModel
.addHistogramListener(this);
124 fCanvas
.addControlListener(this);
125 fCanvas
.addPaintListener(this);
126 fCanvas
.addKeyListener(this);
127 fCanvas
.addMouseListener(this);
128 fCanvas
.addMouseTrackListener(this);
131 public void dispose() {
132 fHistoBarColor
.dispose();
133 fDataModel
.removeHistogramListener(this);
136 private void createWidget(Composite parent
) {
138 final Color labelColor
= parent
.getBackground();
139 final Font fFont
= adjustFont(parent
);
141 final int initalWidth
= 10;
143 // --------------------------------------------------------------------
144 // Define the histogram
145 // --------------------------------------------------------------------
147 GridLayout gridLayout
= new GridLayout();
148 gridLayout
.numColumns
= 3;
149 gridLayout
.marginHeight
= 0;
150 gridLayout
.marginWidth
= 0;
151 gridLayout
.marginTop
= 0;
152 gridLayout
.horizontalSpacing
= 0;
153 gridLayout
.verticalSpacing
= 0;
154 gridLayout
.marginLeft
= 0;
155 gridLayout
.marginRight
= 0;
156 Composite composite
= new Composite(parent
, SWT
.FILL
);
157 composite
.setLayout(gridLayout
);
159 // Use all the horizontal space
160 GridData gridData
= new GridData();
161 gridData
.horizontalAlignment
= SWT
.FILL
;
162 gridData
.verticalAlignment
= SWT
.FILL
;
163 gridData
.grabExcessHorizontalSpace
= true;
164 composite
.setLayoutData(gridData
);
167 gridData
= new GridData();
168 gridData
.horizontalAlignment
= SWT
.RIGHT
;
169 gridData
.verticalAlignment
= SWT
.TOP
;
170 fMaxNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
171 fMaxNbEventsText
.setFont(fFont
);
172 fMaxNbEventsText
.setBackground(labelColor
);
173 fMaxNbEventsText
.setEditable(false);
174 fMaxNbEventsText
.setText("0"); //$NON-NLS-1$
175 fMaxNbEventsText
.setLayoutData(gridData
);
178 gridData
= new GridData();
179 gridData
.horizontalSpan
= 2;
180 gridData
.verticalSpan
= 2;
181 gridData
.horizontalAlignment
= SWT
.FILL
;
182 gridData
.verticalAlignment
= SWT
.FILL
;
183 gridData
.grabExcessHorizontalSpace
= true;
184 fCanvas
= new Canvas(composite
, SWT
.BORDER
| SWT
.DOUBLE_BUFFERED
);
185 fCanvas
.setLayoutData(gridData
);
187 // Y-axis min event (always 0...)
188 gridData
= new GridData();
189 gridData
.horizontalAlignment
= SWT
.RIGHT
;
190 gridData
.verticalAlignment
= SWT
.BOTTOM
;
191 fMinNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
192 fMinNbEventsText
.setFont(fFont
);
193 fMinNbEventsText
.setBackground(labelColor
);
194 fMinNbEventsText
.setEditable(false);
195 fMinNbEventsText
.setText("0"); //$NON-NLS-1$
196 fMinNbEventsText
.setLayoutData(gridData
);
199 gridData
= new GridData(initalWidth
, SWT
.DEFAULT
);
200 gridData
.horizontalAlignment
= SWT
.RIGHT
;
201 gridData
.verticalAlignment
= SWT
.BOTTOM
;
202 Text dummyText
= new Text(composite
, SWT
.READ_ONLY
);
203 dummyText
.setFont(fFont
);
204 dummyText
.setBackground(labelColor
);
205 dummyText
.setEditable(false);
206 dummyText
.setText(""); //$NON-NLS-1$
207 dummyText
.setLayoutData(gridData
);
209 // Window range start time
210 gridData
= new GridData();
211 gridData
.horizontalAlignment
= SWT
.LEFT
;
212 gridData
.verticalAlignment
= SWT
.BOTTOM
;
213 fTimeRangeStartText
= new Text(composite
, SWT
.READ_ONLY
);
214 fTimeRangeStartText
.setFont(fFont
);
215 fTimeRangeStartText
.setBackground(labelColor
);
216 fTimeRangeStartText
.setText(HistogramUtils
.nanosecondsToString(0));
217 fTimeRangeStartText
.setLayoutData(gridData
);
219 // Window range end time
220 gridData
= new GridData();
221 gridData
.horizontalAlignment
= SWT
.RIGHT
;
222 gridData
.verticalAlignment
= SWT
.BOTTOM
;
223 fTimeRangeEndText
= new Text(composite
, SWT
.READ_ONLY
);
224 fTimeRangeEndText
.setFont(fFont
);
225 fTimeRangeEndText
.setBackground(labelColor
);
226 fTimeRangeEndText
.setText(HistogramUtils
.nanosecondsToString(0));
227 fTimeRangeEndText
.setLayoutData(gridData
);
230 private Font
adjustFont(Composite composite
) {
231 // Reduce font size for a more pleasing rendering
232 int fontSizeAdjustment
= -2;
233 Font font
= composite
.getFont();
234 FontData fontData
= font
.getFontData()[0];
235 return new Font(font
.getDevice(), fontData
.getName(), fontData
.getHeight() + fontSizeAdjustment
, fontData
.getStyle());
238 // ------------------------------------------------------------------------
240 // ------------------------------------------------------------------------
242 public long getStartTime() {
243 return fDataModel
.getFirstBucketTime();
246 public long getEndTime() {
247 return fDataModel
.getEndTime();
250 public long getTimeLimit() {
251 return fDataModel
.getTimeLimit();
254 public HistogramDataModel
getDataModel() {
258 // ------------------------------------------------------------------------
260 // ------------------------------------------------------------------------
262 public abstract void updateTimeRange(long startTime
, long endTime
);
265 * Clear the histogram and reset the data
267 public void clear() {
273 * Increase the histogram bucket corresponding to [timestamp]
277 public void countEvent(long eventCount
, long timestamp
) {
278 fDataModel
.countEvent(eventCount
, timestamp
);
282 * Sets the current event time and refresh the display
286 public void setCurrentEvent(long timestamp
) {
287 fCurrentEventTime
= (timestamp
> 0) ? timestamp
: 0;
288 fDataModel
.setCurrentEventNotifyListeners(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
.getFirstBucketTime() + 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
.getFirstBucketTime() || timestamp
> fDataModel
.getEndTime())
315 return (int) ((timestamp
- fDataModel
.getFirstBucketTime()) / 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
374 public void modelUpdated() {
375 if (!fCanvas
.isDisposed() && fCanvas
.getDisplay() != null) {
376 fCanvas
.getDisplay().asyncExec(new Runnable() {
379 if (!fCanvas
.isDisposed()) {
380 // Retrieve and normalize the data
381 int canvasWidth
= fCanvas
.getBounds().width
;
382 int canvasHeight
= fCanvas
.getBounds().height
;
383 if (canvasWidth
<= 0 || canvasHeight
<= 0)
385 fDataModel
.setCurrentEvent(fCurrentEventTime
);
386 fScaledData
= fDataModel
.scaleTo(canvasWidth
, canvasHeight
, HISTOGRAM_BAR_WIDTH
);
388 // Display histogram and update X-,Y-axis labels
389 fTimeRangeStartText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getFirstBucketTime()));
390 fTimeRangeEndText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getEndTime()));
391 fMaxNbEventsText
.setText(Long
.toString(fScaledData
.fMaxValue
));
392 // The Y-axis area might need to be re-sized
393 fMaxNbEventsText
.getParent().layout();
400 // ------------------------------------------------------------------------
402 // ------------------------------------------------------------------------
404 private void updateCurrentEventTime() {
405 long bucketStartTime
= getTimestamp(fScaledData
.fCurrentBucket
);
406 ((HistogramView
) fParentView
).updateCurrentEventTime(bucketStartTime
);
409 // ------------------------------------------------------------------------
411 // ------------------------------------------------------------------------
413 protected final String IMAGE_KEY
= "double-buffer-image"; //$NON-NLS-1$
416 public void paintControl(PaintEvent event
) {
419 int canvasWidth
= fCanvas
.getBounds().width
;
420 int canvasHeight
= fCanvas
.getBounds().height
;
422 // Make sure we have something to draw upon
423 if (canvasWidth
<= 0 || canvasHeight
<= 0)
426 // Retrieve image; re-create only if necessary
427 Image image
= (Image
) fCanvas
.getData(IMAGE_KEY
);
428 if (image
== null || image
.getBounds().width
!= canvasWidth
|| image
.getBounds().height
!= canvasHeight
) {
429 image
= new Image(event
.display
, canvasWidth
, canvasHeight
);
430 fCanvas
.setData(IMAGE_KEY
, image
);
433 // Draw the histogram on its canvas
434 GC imageGC
= new GC(image
);
435 formatImage(imageGC
, image
);
436 event
.gc
.drawImage(image
, 0, 0);
440 private void formatImage(GC imageGC
, Image image
) {
442 if (fScaledData
== null)
445 HistogramScaledData scaledData
= new HistogramScaledData(fScaledData
);
448 // Get drawing boundaries
449 int width
= image
.getBounds().width
;
450 int height
= image
.getBounds().height
;
452 // Clear the drawing area
453 imageGC
.setBackground(fBackgroundColor
);
454 imageGC
.fillRectangle(0, 0, image
.getBounds().width
+ 1, image
.getBounds().height
+ 1);
456 // Draw the histogram bars
457 imageGC
.setBackground(fHistoBarColor
);
458 int limit
= width
< scaledData
.fWidth ? width
: scaledData
.fWidth
;
459 for (int i
= 1; i
< limit
; i
++) {
460 int value
= (int) (scaledData
.fData
[i
] * scaledData
.fScalingFactor
);
461 imageGC
.fillRectangle(i
, height
- value
, 1, value
);
464 // Draw the current event bar
465 int currentBucket
= scaledData
.fCurrentBucket
;
466 if (currentBucket
>= 0 && currentBucket
< limit
) {
467 drawDelimiter(imageGC
, fCurrentEventColor
, height
, currentBucket
);
470 // Add a dashed line as a delimiter (at the right of the last bar)
471 int lastEventIndex
= limit
- 1;
472 while (lastEventIndex
>= 0 && scaledData
.fData
[lastEventIndex
] == 0)
474 lastEventIndex
+= (lastEventIndex
< limit
- 1) ?
1 : 0;
475 drawDelimiter(imageGC
, fLastEventColor
, height
, lastEventIndex
);
476 } catch (Exception e
) {
481 private void drawDelimiter(GC imageGC
, Color color
, int height
, int index
) {
482 imageGC
.setBackground(color
);
483 int dash
= height
/ 4;
484 imageGC
.fillRectangle(index
, 0 * dash
, 1, dash
- 1);
485 imageGC
.fillRectangle(index
, 1 * dash
, 1, dash
- 1);
486 imageGC
.fillRectangle(index
, 2 * dash
, 1, dash
- 1);
487 imageGC
.fillRectangle(index
, 3 * dash
, 1, height
- 3 * dash
);
490 // ------------------------------------------------------------------------
492 // ------------------------------------------------------------------------
495 public void keyPressed(KeyEvent event
) {
496 moveCursor(event
.keyCode
);
500 public void keyReleased(KeyEvent event
) {
503 // ------------------------------------------------------------------------
505 // ------------------------------------------------------------------------
508 public void mouseDoubleClick(MouseEvent event
) {
512 public void mouseDown(MouseEvent event
) {
513 if (fDataModel
.getNbEvents() > 0 && fScaledData
.fLastBucket
>= event
.x
) {
514 fScaledData
.fCurrentBucket
= event
.x
;
515 updateCurrentEventTime();
520 public void mouseUp(MouseEvent event
) {
523 // ------------------------------------------------------------------------
524 // MouseTrackListener
525 // ------------------------------------------------------------------------
528 public void mouseEnter(MouseEvent event
) {
532 public void mouseExit(MouseEvent event
) {
536 public void mouseHover(MouseEvent event
) {
537 if (fDataModel
.getNbEvents() > 0 && fScaledData
!= null && fScaledData
.fLastBucket
>= event
.x
) {
538 String tooltip
= formatToolTipLabel(event
.x
);
539 fCanvas
.setToolTipText(tooltip
);
543 private String
formatToolTipLabel(int index
) {
544 long startTime
= fScaledData
.getBucketStartTime(fScaledData
.fCurrentBucket
);
545 // negative values are possible if time values came into the model in decreasing order
549 long endTime
= fScaledData
.getBucketEndTime(fScaledData
.fCurrentBucket
);
550 int nbEvents
= (index
>= 0) ? fScaledData
.fData
[index
] : 0;
552 StringBuffer buffer
= new StringBuffer();
553 buffer
.append("Range = ["); //$NON-NLS-1$
554 buffer
.append(HistogramUtils
.nanosecondsToString(startTime
));
555 buffer
.append(","); //$NON-NLS-1$
556 buffer
.append(HistogramUtils
.nanosecondsToString(endTime
));
557 buffer
.append(")\n"); //$NON-NLS-1$
558 buffer
.append("Event count = "); //$NON-NLS-1$
559 buffer
.append(nbEvents
);
560 return buffer
.toString();
563 // ------------------------------------------------------------------------
565 // ------------------------------------------------------------------------
568 public void controlMoved(ControlEvent event
) {
569 fDataModel
.complete();
573 public void controlResized(ControlEvent event
) {
574 fDataModel
.complete();