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 - Reformat histogram labels on format change
13 *******************************************************************************/
15 package org
.eclipse
.linuxtools
.tmf
.ui
.views
.histogram
;
17 import org
.eclipse
.linuxtools
.tmf
.core
.event
.ITmfTimestamp
;
18 import org
.eclipse
.linuxtools
.tmf
.core
.event
.TmfTimestamp
;
19 import org
.eclipse
.linuxtools
.tmf
.core
.event
.TmfTimestampFormat
;
20 import org
.eclipse
.linuxtools
.tmf
.core
.signal
.TmfSignalHandler
;
21 import org
.eclipse
.linuxtools
.tmf
.core
.signal
.TmfSignalManager
;
22 import org
.eclipse
.linuxtools
.tmf
.core
.signal
.TmfTimestampFormatUpdateSignal
;
23 import org
.eclipse
.linuxtools
.tmf
.ui
.views
.TmfView
;
24 import org
.eclipse
.swt
.SWT
;
25 import org
.eclipse
.swt
.events
.ControlEvent
;
26 import org
.eclipse
.swt
.events
.ControlListener
;
27 import org
.eclipse
.swt
.events
.KeyEvent
;
28 import org
.eclipse
.swt
.events
.KeyListener
;
29 import org
.eclipse
.swt
.events
.MouseEvent
;
30 import org
.eclipse
.swt
.events
.MouseListener
;
31 import org
.eclipse
.swt
.events
.MouseTrackListener
;
32 import org
.eclipse
.swt
.events
.PaintEvent
;
33 import org
.eclipse
.swt
.events
.PaintListener
;
34 import org
.eclipse
.swt
.graphics
.Color
;
35 import org
.eclipse
.swt
.graphics
.Font
;
36 import org
.eclipse
.swt
.graphics
.FontData
;
37 import org
.eclipse
.swt
.graphics
.GC
;
38 import org
.eclipse
.swt
.graphics
.Image
;
39 import org
.eclipse
.swt
.graphics
.Point
;
40 import org
.eclipse
.swt
.graphics
.Rectangle
;
41 import org
.eclipse
.swt
.layout
.FillLayout
;
42 import org
.eclipse
.swt
.layout
.GridData
;
43 import org
.eclipse
.swt
.layout
.GridLayout
;
44 import org
.eclipse
.swt
.widgets
.Canvas
;
45 import org
.eclipse
.swt
.widgets
.Composite
;
46 import org
.eclipse
.swt
.widgets
.Display
;
47 import org
.eclipse
.swt
.widgets
.Text
;
50 * Re-usable histogram widget.
52 * It has the following features:
54 * <li>Y-axis labels displaying min/max count values
55 * <li>X-axis labels displaying time range
56 * <li>a histogram displaying the distribution of values over time (note that
57 * the histogram might not necessarily fill the whole canvas)
59 * The widget also has 2 'markers' to identify:
61 * <li>a red dashed line over the bar that contains the currently selected event
62 * <li>a dark red dashed line that delimits the right end of the histogram (if
63 * it doesn't fill the canvas)
65 * Clicking on the histogram will select the current event at the mouse
68 * Once the histogram is selected, there is some limited keyboard support:
70 * <li>Home: go to the first histogram bar
71 * <li>End: go to the last histogram bar
72 * <li>Left: go to the previous histogram
73 * <li>Right: go to the next histogram bar
75 * Finally, when the mouse hovers over the histogram, a tool tip showing the
76 * following information about the corresponding histogram bar time range:
78 * <li>start of the time range
79 * <li>end of the time range
80 * <li>number of events in that time range
84 * @author Francois Chouinard
86 public abstract class Histogram
implements ControlListener
, PaintListener
, KeyListener
, MouseListener
, MouseTrackListener
, IHistogramModelListener
{
88 // ------------------------------------------------------------------------
90 // ------------------------------------------------------------------------
93 private final Color fBackgroundColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_WHITE
);
94 private final Color fCurrentEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_RED
);
95 private final Color fLastEventColor
= Display
.getCurrent().getSystemColor(SWT
.COLOR_DARK_RED
);
96 private final Color fHistoBarColor
= new Color(Display
.getDefault(), 74, 112, 139);
98 // ------------------------------------------------------------------------
100 // ------------------------------------------------------------------------
103 * The parent TMF view.
105 protected TmfView fParentView
;
107 private Composite fParent
;
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
;
122 * The histogram data model.
124 protected final HistogramDataModel fDataModel
;
127 * The histogram data model scaled to current resolution and screen width.
129 protected HistogramScaledData fScaledData
;
132 * The current event value
134 protected long fCurrentEventTime
= 0L;
136 // ------------------------------------------------------------------------
138 // ------------------------------------------------------------------------
143 * @param view A reference to the parent TMF view.
144 * @param parent A parent composite
146 public Histogram(final TmfView view
, final Composite parent
) {
150 createWidget(parent
);
151 fDataModel
= new HistogramDataModel();
152 fDataModel
.addHistogramListener(this);
155 fCanvas
.addControlListener(this);
156 fCanvas
.addPaintListener(this);
157 fCanvas
.addKeyListener(this);
158 fCanvas
.addMouseListener(this);
159 fCanvas
.addMouseTrackListener(this);
161 TmfSignalManager
.register(this);
165 * Dispose resources and unregisters listeners.
167 public void dispose() {
168 TmfSignalManager
.deregister(this);
170 fHistoBarColor
.dispose();
171 fDataModel
.removeHistogramListener(this);
174 private void createWidget(final Composite parent
) {
176 final Color labelColor
= parent
.getBackground();
177 fFont
= adjustFont(parent
);
179 final int initalWidth
= 10;
181 // --------------------------------------------------------------------
182 // Define the histogram
183 // --------------------------------------------------------------------
185 final GridLayout gridLayout
= new GridLayout();
186 gridLayout
.numColumns
= 3;
187 gridLayout
.marginHeight
= 0;
188 gridLayout
.marginWidth
= 0;
189 gridLayout
.marginTop
= 0;
190 gridLayout
.horizontalSpacing
= 0;
191 gridLayout
.verticalSpacing
= 0;
192 gridLayout
.marginLeft
= 0;
193 gridLayout
.marginRight
= 0;
194 final Composite composite
= new Composite(parent
, SWT
.FILL
);
195 composite
.setLayout(gridLayout
);
197 // Use all the horizontal space
198 GridData gridData
= new GridData();
199 gridData
.horizontalAlignment
= SWT
.FILL
;
200 gridData
.verticalAlignment
= SWT
.FILL
;
201 gridData
.grabExcessHorizontalSpace
= true;
202 composite
.setLayoutData(gridData
);
205 gridData
= new GridData();
206 gridData
.horizontalAlignment
= SWT
.RIGHT
;
207 gridData
.verticalAlignment
= SWT
.TOP
;
208 fMaxNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
209 fMaxNbEventsText
.setFont(fFont
);
210 fMaxNbEventsText
.setBackground(labelColor
);
211 fMaxNbEventsText
.setEditable(false);
212 fMaxNbEventsText
.setText("0"); //$NON-NLS-1$
213 fMaxNbEventsText
.setLayoutData(gridData
);
216 Composite canvasComposite
= new Composite(composite
, SWT
.BORDER
);
217 gridData
= new GridData();
218 gridData
.horizontalSpan
= 2;
219 gridData
.verticalSpan
= 2;
220 gridData
.horizontalAlignment
= SWT
.FILL
;
221 gridData
.verticalAlignment
= SWT
.FILL
;
222 gridData
.grabExcessHorizontalSpace
= true;
223 canvasComposite
.setLayoutData(gridData
);
224 canvasComposite
.setLayout(new FillLayout());
225 fCanvas
= new Canvas(canvasComposite
, SWT
.DOUBLE_BUFFERED
);
227 // Y-axis min event (always 0...)
228 gridData
= new GridData();
229 gridData
.horizontalAlignment
= SWT
.RIGHT
;
230 gridData
.verticalAlignment
= SWT
.BOTTOM
;
231 fMinNbEventsText
= new Text(composite
, SWT
.READ_ONLY
| SWT
.RIGHT
);
232 fMinNbEventsText
.setFont(fFont
);
233 fMinNbEventsText
.setBackground(labelColor
);
234 fMinNbEventsText
.setEditable(false);
235 fMinNbEventsText
.setText("0"); //$NON-NLS-1$
236 fMinNbEventsText
.setLayoutData(gridData
);
239 gridData
= new GridData(initalWidth
, SWT
.DEFAULT
);
240 gridData
.horizontalAlignment
= SWT
.RIGHT
;
241 gridData
.verticalAlignment
= SWT
.BOTTOM
;
242 final Text dummyText
= new Text(composite
, SWT
.READ_ONLY
);
243 dummyText
.setFont(fFont
);
244 dummyText
.setBackground(labelColor
);
245 dummyText
.setEditable(false);
246 dummyText
.setText(""); //$NON-NLS-1$
247 dummyText
.setLayoutData(gridData
);
249 // Window range start time
250 gridData
= new GridData();
251 gridData
.horizontalAlignment
= SWT
.LEFT
;
252 gridData
.verticalAlignment
= SWT
.BOTTOM
;
253 fTimeRangeStartText
= new Text(composite
, SWT
.READ_ONLY
);
254 fTimeRangeStartText
.setFont(fFont
);
255 fTimeRangeStartText
.setBackground(labelColor
);
256 fTimeRangeStartText
.setText(TmfTimestamp
.ZERO
.toString());
257 fTimeRangeStartText
.setLayoutData(gridData
);
259 // Window range end time
260 gridData
= new GridData();
261 gridData
.horizontalAlignment
= SWT
.RIGHT
;
262 gridData
.verticalAlignment
= SWT
.BOTTOM
;
263 fTimeRangeEndText
= new Text(composite
, SWT
.READ_ONLY
);
264 fTimeRangeEndText
.setFont(fFont
);
265 fTimeRangeEndText
.setBackground(labelColor
);
266 fTimeRangeEndText
.setText(TmfTimestamp
.ZERO
.toString());
267 fTimeRangeEndText
.setLayoutData(gridData
);
270 private static Font
adjustFont(final Composite composite
) {
271 // Reduce font size for a more pleasing rendering
272 final int fontSizeAdjustment
= -2;
273 final Font font
= composite
.getFont();
274 final FontData fontData
= font
.getFontData()[0];
275 return new Font(font
.getDevice(), fontData
.getName(), fontData
.getHeight() + fontSizeAdjustment
, fontData
.getStyle());
278 // ------------------------------------------------------------------------
280 // ------------------------------------------------------------------------
283 * Returns the start time (equal first bucket time)
284 * @return the start time.
286 public long getStartTime() {
287 return fDataModel
.getFirstBucketTime();
291 * Returns the end time.
292 * @return the end time.
294 public long getEndTime() {
295 return fDataModel
.getEndTime();
299 * Returns the time limit (end of last bucket)
300 * @return the time limit.
302 public long getTimeLimit() {
303 return fDataModel
.getTimeLimit();
307 * Returns a data model reference.
308 * @return data model.
310 public HistogramDataModel
getDataModel() {
314 // ------------------------------------------------------------------------
316 // ------------------------------------------------------------------------
318 * Updates the time range.
319 * @param startTime A start time
320 * @param endTime A end time.
322 public abstract void updateTimeRange(long startTime
, long endTime
);
325 * Clear the histogram and reset the data
327 public void clear() {
333 * Increase the histogram bucket corresponding to [timestamp]
336 * The new event count
338 * The latest timestamp
340 public void countEvent(final long eventCount
, final long timestamp
) {
341 fDataModel
.countEvent(eventCount
, timestamp
);
345 * Sets the current event time and refresh the display
348 * The time of the current event
350 public void setCurrentEvent(final long timestamp
) {
351 fCurrentEventTime
= (timestamp
> 0) ? timestamp
: 0;
352 fDataModel
.setCurrentEventNotifyListeners(timestamp
);
356 * Computes the timestamp of the bucket at [offset]
358 * @param offset offset from the left on the histogram
359 * @return the start timestamp of the corresponding bucket
361 public synchronized long getTimestamp(final int offset
) {
362 assert offset
> 0 && offset
< fScaledData
.fWidth
;
364 return fDataModel
.getFirstBucketTime() + fScaledData
.fBucketDuration
* offset
;
365 } catch (final Exception e
) {
366 return 0; // TODO: Fix that racing condition (NPE)
371 * Computes the offset of the timestamp in the histogram
373 * @param timestamp the timestamp
374 * @return the offset of the corresponding bucket (-1 if invalid)
376 public synchronized int getOffset(final long timestamp
) {
377 if (timestamp
< fDataModel
.getFirstBucketTime() || timestamp
> fDataModel
.getEndTime()) {
380 return (int) ((timestamp
- fDataModel
.getFirstBucketTime()) / fScaledData
.fBucketDuration
);
384 * Move the currently selected bar cursor to a non-empty bucket.
386 * @param keyCode the SWT key code
388 protected void moveCursor(final int keyCode
) {
390 if (fScaledData
.fCurrentBucket
== HistogramScaledData
.OUT_OF_RANGE_BUCKET
) {
399 while (index
< fScaledData
.fLastBucket
&& fScaledData
.fData
[index
] == 0) {
402 if (index
< fScaledData
.fLastBucket
) {
403 fScaledData
.fCurrentBucket
= index
;
407 case SWT
.ARROW_RIGHT
:
408 index
= fScaledData
.fCurrentBucket
+ 1;
409 while (index
< fScaledData
.fWidth
&& fScaledData
.fData
[index
] == 0) {
412 if (index
< fScaledData
.fLastBucket
) {
413 fScaledData
.fCurrentBucket
= index
;
418 index
= fScaledData
.fLastBucket
;
419 while (index
>= 0 && fScaledData
.fData
[index
] == 0) {
423 fScaledData
.fCurrentBucket
= index
;
428 index
= fScaledData
.fCurrentBucket
- 1;
429 while (index
>= 0 && fScaledData
.fData
[index
] == 0) {
433 fScaledData
.fCurrentBucket
= index
;
441 updateCurrentEventTime();
445 * Refresh the histogram display
448 public void modelUpdated() {
449 if (!fCanvas
.isDisposed() && fCanvas
.getDisplay() != null) {
450 fCanvas
.getDisplay().asyncExec(new Runnable() {
453 if (!fCanvas
.isDisposed()) {
454 // Retrieve and normalize the data
455 final int canvasWidth
= fCanvas
.getBounds().width
;
456 final int canvasHeight
= fCanvas
.getBounds().height
;
457 if (canvasWidth
<= 0 || canvasHeight
<= 0) {
460 fDataModel
.setCurrentEvent(fCurrentEventTime
);
461 fScaledData
= fDataModel
.scaleTo(canvasWidth
, canvasHeight
, 1);
462 synchronized(fScaledData
) {
463 if (fScaledData
!= null) {
465 // Display histogram and update X-,Y-axis labels
466 fTimeRangeStartText
.setText(TmfTimestampFormat
.getDefaulTimeFormat().format(fDataModel
.getFirstBucketTime()));
467 fTimeRangeEndText
.setText(TmfTimestampFormat
.getDefaulTimeFormat().format(fDataModel
.getEndTime()));
468 fMaxNbEventsText
.setText(Long
.toString(fScaledData
.fMaxValue
));
469 // The Y-axis area might need to be re-sized
470 fMaxNbEventsText
.getParent().layout();
479 // ------------------------------------------------------------------------
481 // ------------------------------------------------------------------------
483 private void updateCurrentEventTime() {
484 final long bucketStartTime
= getTimestamp(fScaledData
.fCurrentBucket
);
485 ((HistogramView
) fParentView
).updateCurrentEventTime(bucketStartTime
);
488 // ------------------------------------------------------------------------
490 // ------------------------------------------------------------------------
492 * Image key string for the canvas.
494 protected final String IMAGE_KEY
= "double-buffer-image"; //$NON-NLS-1$
497 public void paintControl(final PaintEvent event
) {
500 final int canvasWidth
= fCanvas
.getBounds().width
;
501 final int canvasHeight
= fCanvas
.getBounds().height
;
503 // Make sure we have something to draw upon
504 if (canvasWidth
<= 0 || canvasHeight
<= 0) {
508 // Retrieve image; re-create only if necessary
509 Image image
= (Image
) fCanvas
.getData(IMAGE_KEY
);
510 if (image
== null || image
.getBounds().width
!= canvasWidth
|| image
.getBounds().height
!= canvasHeight
) {
511 image
= new Image(event
.display
, canvasWidth
, canvasHeight
);
512 fCanvas
.setData(IMAGE_KEY
, image
);
515 // Draw the histogram on its canvas
516 final GC imageGC
= new GC(image
);
517 formatImage(imageGC
, image
);
518 event
.gc
.drawImage(image
, 0, 0);
522 private void formatImage(final GC imageGC
, final Image image
) {
524 if (fScaledData
== null) {
528 final HistogramScaledData scaledData
= new HistogramScaledData(fScaledData
);
531 // Get drawing boundaries
532 final int width
= image
.getBounds().width
;
533 final int height
= image
.getBounds().height
;
535 // Clear the drawing area
536 imageGC
.setBackground(fBackgroundColor
);
537 imageGC
.fillRectangle(0, 0, image
.getBounds().width
+ 1, image
.getBounds().height
+ 1);
539 // Draw the histogram bars
540 imageGC
.setBackground(fHistoBarColor
);
541 final int limit
= width
< scaledData
.fWidth ? width
: scaledData
.fWidth
;
542 for (int i
= 1; i
< limit
; i
++) {
543 final int value
= (int) Math
.ceil(scaledData
.fData
[i
] * scaledData
.fScalingFactor
);
544 imageGC
.fillRectangle(i
, height
- value
, 1, value
);
547 // Draw the current event bar
548 final int currentBucket
= scaledData
.fCurrentBucket
;
549 if (currentBucket
>= 0 && currentBucket
< limit
) {
550 drawDelimiter(imageGC
, fCurrentEventColor
, height
, currentBucket
);
553 // Add a dashed line as a delimiter (at the right of the last bar)
554 int lastEventIndex
= limit
- 1;
555 while (lastEventIndex
>= 0 && scaledData
.fData
[lastEventIndex
] == 0) {
558 lastEventIndex
+= (lastEventIndex
< limit
- 1) ?
1 : 0;
559 drawDelimiter(imageGC
, fLastEventColor
, height
, lastEventIndex
);
560 } catch (final Exception e
) {
565 private static void drawDelimiter(final GC imageGC
, final Color color
,
566 final int height
, final int index
) {
567 imageGC
.setBackground(color
);
568 final int dash
= height
/ 4;
569 imageGC
.fillRectangle(index
, 0 * dash
, 1, dash
- 1);
570 imageGC
.fillRectangle(index
, 1 * dash
, 1, dash
- 1);
571 imageGC
.fillRectangle(index
, 2 * dash
, 1, dash
- 1);
572 imageGC
.fillRectangle(index
, 3 * dash
, 1, height
- 3 * dash
);
575 // ------------------------------------------------------------------------
577 // ------------------------------------------------------------------------
580 public void keyPressed(final KeyEvent event
) {
581 moveCursor(event
.keyCode
);
585 public void keyReleased(final KeyEvent event
) {
588 // ------------------------------------------------------------------------
590 // ------------------------------------------------------------------------
593 public void mouseDoubleClick(final MouseEvent event
) {
597 public void mouseDown(final MouseEvent event
) {
598 if (fDataModel
.getNbEvents() > 0 && fScaledData
.fLastBucket
>= event
.x
) {
599 fScaledData
.fCurrentBucket
= event
.x
;
600 updateCurrentEventTime();
605 public void mouseUp(final MouseEvent event
) {
608 // ------------------------------------------------------------------------
609 // MouseTrackListener
610 // ------------------------------------------------------------------------
613 public void mouseEnter(final MouseEvent event
) {
617 public void mouseExit(final MouseEvent event
) {
621 public void mouseHover(final MouseEvent event
) {
622 if (fDataModel
.getNbEvents() > 0 && fScaledData
!= null && fScaledData
.fLastBucket
>= event
.x
) {
623 final String tooltip
= formatToolTipLabel(event
.x
);
624 fCanvas
.setToolTipText(tooltip
);
628 private String
formatToolTipLabel(final int index
) {
629 long startTime
= fScaledData
.getBucketStartTime(index
);
630 // negative values are possible if time values came into the model in decreasing order
634 final long endTime
= fScaledData
.getBucketEndTime(index
);
635 final int nbEvents
= (index
>= 0) ? fScaledData
.fData
[index
] : 0;
637 final StringBuffer buffer
= new StringBuffer();
638 buffer
.append("Range = ["); //$NON-NLS-1$
639 buffer
.append(new TmfTimestamp(startTime
, ITmfTimestamp
.NANOSECOND_SCALE
).toString());
640 buffer
.append(","); //$NON-NLS-1$
641 buffer
.append(new TmfTimestamp(endTime
, ITmfTimestamp
.NANOSECOND_SCALE
).toString());
642 buffer
.append(")\n"); //$NON-NLS-1$
643 buffer
.append("Event count = "); //$NON-NLS-1$
644 buffer
.append(nbEvents
);
645 return buffer
.toString();
648 // ------------------------------------------------------------------------
650 // ------------------------------------------------------------------------
653 public void controlMoved(final ControlEvent event
) {
654 fDataModel
.complete();
658 public void controlResized(final ControlEvent event
) {
659 fDataModel
.complete();
662 // ------------------------------------------------------------------------
664 // ------------------------------------------------------------------------
667 * Format the timestamp and update the display
669 * @param signal the incoming signal
673 public void timestampFormatUpdated(TmfTimestampFormatUpdateSignal signal
) {
674 Point size
= fTimeRangeStartText
.getSize();
675 String newTS
= TmfTimestampFormat
.getDefaulTimeFormat().format(fDataModel
.getFirstBucketTime());
676 size
.x
= getTextSize(newTS
);
677 fTimeRangeStartText
.setSize(size
);
678 fTimeRangeStartText
.setText(newTS
);
680 newTS
= TmfTimestampFormat
.getDefaulTimeFormat().format(fDataModel
.getEndTime());
681 Rectangle rect
= fTimeRangeEndText
.getBounds();
682 int newWidth
= getTextSize(newTS
);
683 rect
.x
+= rect
.width
- newWidth
;
684 rect
.width
= newWidth
;
685 fTimeRangeEndText
.setBounds(rect
);
686 fTimeRangeEndText
.setText(newTS
);
690 * Compute the width of a String.
692 * @param text the Text to measure
693 * @return The result size
696 private int getTextSize(final String text
) {
697 GC controlGC
= new GC(fParent
);
698 controlGC
.setFont(fFont
);
701 for (int pos
= 0; pos
< text
.length(); pos
++) {
702 textSize
+= controlGC
.getAdvanceWidth(text
.charAt(pos
));
704 // Add an extra space
705 textSize
+= controlGC
.getAdvanceWidth(' ');