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]
324 * The new event count
326 * The latest timestamp
328 public void countEvent(final long eventCount
, final long timestamp
) {
329 fDataModel
.countEvent(eventCount
, timestamp
);
333 * Sets the current event time and refresh the display
336 * The time of the current event
338 public void setCurrentEvent(final long timestamp
) {
339 fCurrentEventTime
= (timestamp
> 0) ? timestamp
: 0;
340 fDataModel
.setCurrentEventNotifyListeners(timestamp
);
344 * Computes the timestamp of the bucket at [offset]
346 * @param offset offset from the left on the histogram
347 * @return the start timestamp of the corresponding bucket
349 public synchronized long getTimestamp(final int offset
) {
350 assert offset
> 0 && offset
< fScaledData
.fWidth
;
352 return fDataModel
.getFirstBucketTime() + fScaledData
.fBucketDuration
* offset
;
353 } catch (final Exception e
) {
354 return 0; // TODO: Fix that racing condition (NPE)
359 * Computes the offset of the timestamp in the histogram
361 * @param timestamp the timestamp
362 * @return the offset of the corresponding bucket (-1 if invalid)
364 public synchronized int getOffset(final long timestamp
) {
365 if (timestamp
< fDataModel
.getFirstBucketTime() || timestamp
> fDataModel
.getEndTime()) {
368 return (int) ((timestamp
- fDataModel
.getFirstBucketTime()) / fScaledData
.fBucketDuration
);
372 * Move the currently selected bar cursor to a non-empty bucket.
374 * @param keyCode the SWT key code
376 protected void moveCursor(final int keyCode
) {
378 if (fScaledData
.fCurrentBucket
== HistogramScaledData
.OUT_OF_RANGE_BUCKET
) {
387 while (index
< fScaledData
.fLastBucket
&& fScaledData
.fData
[index
] == 0) {
390 if (index
< fScaledData
.fLastBucket
) {
391 fScaledData
.fCurrentBucket
= index
;
395 case SWT
.ARROW_RIGHT
:
396 index
= fScaledData
.fCurrentBucket
+ 1;
397 while (index
< fScaledData
.fWidth
&& fScaledData
.fData
[index
] == 0) {
400 if (index
< fScaledData
.fLastBucket
) {
401 fScaledData
.fCurrentBucket
= index
;
406 index
= fScaledData
.fLastBucket
;
407 while (index
>= 0 && fScaledData
.fData
[index
] == 0) {
411 fScaledData
.fCurrentBucket
= index
;
416 index
= fScaledData
.fCurrentBucket
- 1;
417 while (index
>= 0 && fScaledData
.fData
[index
] == 0) {
421 fScaledData
.fCurrentBucket
= index
;
429 updateCurrentEventTime();
433 * Refresh the histogram display
436 public void modelUpdated() {
437 if (!fCanvas
.isDisposed() && fCanvas
.getDisplay() != null) {
438 fCanvas
.getDisplay().asyncExec(new Runnable() {
441 if (!fCanvas
.isDisposed()) {
442 // Retrieve and normalize the data
443 final int canvasWidth
= fCanvas
.getBounds().width
;
444 final int canvasHeight
= fCanvas
.getBounds().height
;
445 if (canvasWidth
<= 0 || canvasHeight
<= 0) {
448 fDataModel
.setCurrentEvent(fCurrentEventTime
);
449 fScaledData
= fDataModel
.scaleTo(canvasWidth
, canvasHeight
, HISTOGRAM_BAR_WIDTH
);
450 synchronized(fScaledData
) {
451 if (fScaledData
!= null) {
453 // Display histogram and update X-,Y-axis labels
454 fTimeRangeStartText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getFirstBucketTime()));
455 fTimeRangeEndText
.setText(HistogramUtils
.nanosecondsToString(fDataModel
.getEndTime()));
456 fMaxNbEventsText
.setText(Long
.toString(fScaledData
.fMaxValue
));
457 // The Y-axis area might need to be re-sized
458 fMaxNbEventsText
.getParent().layout();
467 // ------------------------------------------------------------------------
469 // ------------------------------------------------------------------------
471 private void updateCurrentEventTime() {
472 final long bucketStartTime
= getTimestamp(fScaledData
.fCurrentBucket
);
473 ((HistogramView
) fParentView
).updateCurrentEventTime(bucketStartTime
);
476 // ------------------------------------------------------------------------
478 // ------------------------------------------------------------------------
480 * Image key string for the canvas.
482 protected final String IMAGE_KEY
= "double-buffer-image"; //$NON-NLS-1$
485 public void paintControl(final PaintEvent event
) {
488 final int canvasWidth
= fCanvas
.getBounds().width
;
489 final int canvasHeight
= fCanvas
.getBounds().height
;
491 // Make sure we have something to draw upon
492 if (canvasWidth
<= 0 || canvasHeight
<= 0) {
496 // Retrieve image; re-create only if necessary
497 Image image
= (Image
) fCanvas
.getData(IMAGE_KEY
);
498 if (image
== null || image
.getBounds().width
!= canvasWidth
|| image
.getBounds().height
!= canvasHeight
) {
499 image
= new Image(event
.display
, canvasWidth
, canvasHeight
);
500 fCanvas
.setData(IMAGE_KEY
, image
);
503 // Draw the histogram on its canvas
504 final GC imageGC
= new GC(image
);
505 formatImage(imageGC
, image
);
506 event
.gc
.drawImage(image
, 0, 0);
510 private void formatImage(final GC imageGC
, final Image image
) {
512 if (fScaledData
== null) {
516 final HistogramScaledData scaledData
= new HistogramScaledData(fScaledData
);
519 // Get drawing boundaries
520 final int width
= image
.getBounds().width
;
521 final int height
= image
.getBounds().height
;
523 // Clear the drawing area
524 imageGC
.setBackground(fBackgroundColor
);
525 imageGC
.fillRectangle(0, 0, image
.getBounds().width
+ 1, image
.getBounds().height
+ 1);
527 // Draw the histogram bars
528 imageGC
.setBackground(fHistoBarColor
);
529 final int limit
= width
< scaledData
.fWidth ? width
: scaledData
.fWidth
;
530 for (int i
= 1; i
< limit
; i
++) {
531 final int value
= (int) (scaledData
.fData
[i
] * scaledData
.fScalingFactor
);
532 imageGC
.fillRectangle(i
, height
- value
, 1, value
);
535 // Draw the current event bar
536 final int currentBucket
= scaledData
.fCurrentBucket
;
537 if (currentBucket
>= 0 && currentBucket
< limit
) {
538 drawDelimiter(imageGC
, fCurrentEventColor
, height
, currentBucket
);
541 // Add a dashed line as a delimiter (at the right of the last bar)
542 int lastEventIndex
= limit
- 1;
543 while (lastEventIndex
>= 0 && scaledData
.fData
[lastEventIndex
] == 0) {
546 lastEventIndex
+= (lastEventIndex
< limit
- 1) ?
1 : 0;
547 drawDelimiter(imageGC
, fLastEventColor
, height
, lastEventIndex
);
548 } catch (final Exception e
) {
553 private void drawDelimiter(final GC imageGC
, final Color color
, final int height
, final int index
) {
554 imageGC
.setBackground(color
);
555 final int dash
= height
/ 4;
556 imageGC
.fillRectangle(index
, 0 * dash
, 1, dash
- 1);
557 imageGC
.fillRectangle(index
, 1 * dash
, 1, dash
- 1);
558 imageGC
.fillRectangle(index
, 2 * dash
, 1, dash
- 1);
559 imageGC
.fillRectangle(index
, 3 * dash
, 1, height
- 3 * dash
);
562 // ------------------------------------------------------------------------
564 // ------------------------------------------------------------------------
567 public void keyPressed(final KeyEvent event
) {
568 moveCursor(event
.keyCode
);
572 public void keyReleased(final KeyEvent event
) {
575 // ------------------------------------------------------------------------
577 // ------------------------------------------------------------------------
580 public void mouseDoubleClick(final MouseEvent event
) {
584 public void mouseDown(final MouseEvent event
) {
585 if (fDataModel
.getNbEvents() > 0 && fScaledData
.fLastBucket
>= event
.x
) {
586 fScaledData
.fCurrentBucket
= event
.x
;
587 updateCurrentEventTime();
592 public void mouseUp(final MouseEvent event
) {
595 // ------------------------------------------------------------------------
596 // MouseTrackListener
597 // ------------------------------------------------------------------------
600 public void mouseEnter(final MouseEvent event
) {
604 public void mouseExit(final MouseEvent event
) {
608 public void mouseHover(final MouseEvent event
) {
609 if (fDataModel
.getNbEvents() > 0 && fScaledData
!= null && fScaledData
.fLastBucket
>= event
.x
) {
610 final String tooltip
= formatToolTipLabel(event
.x
);
611 fCanvas
.setToolTipText(tooltip
);
615 private String
formatToolTipLabel(final int index
) {
616 long startTime
= fScaledData
.getBucketStartTime(fScaledData
.fCurrentBucket
);
617 // negative values are possible if time values came into the model in decreasing order
621 final long endTime
= fScaledData
.getBucketEndTime(fScaledData
.fCurrentBucket
);
622 final int nbEvents
= (index
>= 0) ? fScaledData
.fData
[index
] : 0;
624 final StringBuffer buffer
= new StringBuffer();
625 buffer
.append("Range = ["); //$NON-NLS-1$
626 buffer
.append(HistogramUtils
.nanosecondsToString(startTime
));
627 buffer
.append(","); //$NON-NLS-1$
628 buffer
.append(HistogramUtils
.nanosecondsToString(endTime
));
629 buffer
.append(")\n"); //$NON-NLS-1$
630 buffer
.append("Event count = "); //$NON-NLS-1$
631 buffer
.append(nbEvents
);
632 return buffer
.toString();
635 // ------------------------------------------------------------------------
637 // ------------------------------------------------------------------------
640 public void controlMoved(final ControlEvent event
) {
641 fDataModel
.complete();
645 public void controlResized(final ControlEvent event
) {
646 fDataModel
.complete();