Render last known value in time series chart hairline value tip
[simantics/platform.git] / bundles / org.simantics.trend / src / org / simantics / trend / impl / Plot.java
1 /*******************************************************************************
2  * Industry THTH ry.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  *     VTT Technical Research Centre of Finland - initial API and implementation
10  *******************************************************************************/
11 package org.simantics.trend.impl;
12
13 import java.awt.AlphaComposite;
14 import java.awt.BasicStroke;
15 import java.awt.Color;
16 import java.awt.Composite;
17 import java.awt.Font;
18 import java.awt.FontMetrics;
19 import java.awt.GradientPaint;
20 import java.awt.Graphics2D;
21 import java.awt.Paint;
22 import java.awt.font.GlyphVector;
23 import java.awt.font.LineMetrics;
24 import java.awt.geom.AffineTransform;
25 import java.awt.geom.Line2D;
26 import java.awt.geom.Path2D;
27 import java.awt.geom.Point2D;
28 import java.awt.geom.Rectangle2D;
29 import java.text.Format;
30 import java.text.NumberFormat;
31 import java.text.ParseException;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 import org.simantics.databoard.Bindings;
36 import org.simantics.databoard.accessor.error.AccessorException;
37 import org.simantics.databoard.binding.Binding;
38 import org.simantics.databoard.binding.NumberBinding;
39 import org.simantics.databoard.binding.error.BindingException;
40 import org.simantics.g2d.utils.GridSpacing;
41 import org.simantics.g2d.utils.GridUtil;
42 import org.simantics.history.HistoryException;
43 import org.simantics.history.util.Stream;
44 import org.simantics.history.util.ValueBand;
45 import org.simantics.scenegraph.utils.ColorUtil;
46 import org.simantics.trend.configuration.TrendItem.Renderer;
47 import org.simantics.trend.configuration.TrendSpec;
48 import org.simantics.utils.format.TimeFormat;
49
50 public class Plot extends TrendGraphicalNode {
51
52     public static final double VALUE_TIP_BOX_PLOT_MARGIN = 7;
53     private static final double VALUE_TIP_BOX_FILM_MARGIN = 5;
54
55     private static final long serialVersionUID = 6335497685577733932L;
56
57     public static final BasicStroke BORDER_LINE_STROKE =
58             new BasicStroke(1.0f,
59                     BasicStroke.CAP_SQUARE,
60                     BasicStroke.JOIN_MITER,
61                     10.0f, null, 0.0f);
62
63     public static final BasicStroke MILESTONE_LINE_STROKE =
64             new BasicStroke(1.0f,
65                     BasicStroke.CAP_SQUARE,
66                     BasicStroke.JOIN_MITER,
67                     10.0f, null, 0.0f);
68
69     public static final BasicStroke TREND_LINE_STROKE =
70             new BasicStroke(1.0f,
71                     BasicStroke.CAP_BUTT,
72                     BasicStroke.JOIN_MITER,
73                     10.0f, null, 0.0f);
74
75     public static final BasicStroke DASHED_LINE_STROKE =
76                 new BasicStroke(2.0f,              // Width
77                     BasicStroke.CAP_BUTT,    // End cap
78                     BasicStroke.JOIN_MITER,    // Join style
79                     5.0f,                      // Miter limit
80                     new float[] {5.0f,5.0f}, // Dash pattern
81                     0.0f);                     // Dash phase                    
82     public static final BasicStroke DASHED_LINE_STROKE_2 =
83                 new BasicStroke(2.0f,              // Width
84                     BasicStroke.CAP_BUTT,    // End cap
85                     BasicStroke.JOIN_MITER,    // Join style
86                     5.0f,                      // Miter limit
87                     new float[] {5.0f,5.0f}, // Dash pattern
88                     5.0f);                     // Dash phase                    
89     public static final BasicStroke DASHED_LINE_STROKE_INVERSE =
90                 new BasicStroke(1.0f,              // Width
91                     BasicStroke.CAP_BUTT,    // End cap
92                     BasicStroke.JOIN_MITER,    // Join style
93                     5.0f,                      // Miter limit
94                     new float[] {3.0f,8.0f}, // Dash pattern
95                     0.0f);                     // Dash phase                    
96
97     public static final Color PLOT_AREA_BG_GRADIENT_COLOR_TOP = new Color(228, 228, 248);
98     public static final Color PLOT_AREA_BG_GRADIENT_COLOR_BOTTOM = new Color(250, 250, 250);
99
100     static final Font MILESTONE_FONT, BASELINE_FONT, TOOLTIP_FONT;    
101     
102     static final GridSpacing SOME_SPACING = GridSpacing.makeGridSpacing(100, 100, 15);
103     
104     public static final Color GRID_LINE_COLOR = new Color(190, 190, 220);
105
106     static final double DIAMOND_SIZE = 7;
107     static final Path2D DIAMOND;
108         
109         double analogAreaHeight;
110         double binaryAreaHeight;
111
112         Rectangle2D valueTipBoxBounds = new Rectangle2D.Double();
113
114         @SuppressWarnings("unused")
115     @Override
116         protected void doRender(Graphics2D g2d) {
117                 //long startTime = System.nanoTime();
118                 TrendNode trend = (TrendNode) getParent();
119                 TrendSpec ts = trend.spec;
120                 ViewRenderingProfile rprof = trend.renderingProfile;
121                 double w = bounds.getWidth();
122                 double h = bounds.getHeight();
123                 
124                 double from = trend.horizRuler.from;
125                 double end = trend.horizRuler.end;
126                 GridSpacing xGrid = trend.horizRuler.spacing;
127                 GridSpacing yGrid = trend.vertRuler != null ? trend.vertRuler.spacing : Plot.SOME_SPACING;
128                 
129                 if (w<1. || h<1.) return; 
130                 
131                 // Prepare data
132                 if (trend.shapedirty) {
133                         trend.shapedirty = false;
134                         for (ItemNode node : trend.analogItems) prepareItem(node, 0, analogAreaHeight);
135                         for (ItemNode node : trend.binaryItems) prepareItem(node, 0, analogAreaHeight);
136                 }
137                 
138                 // Fill gradient
139                 Gradient: {
140                         Paint bgp = rprof.backgroundColor2 == null ? rprof.backgroundColor1
141                                         : new GradientPaint(
142                                                         0.f, (float) h, rprof.backgroundColor1, 
143                                                         0.f, 0.f, rprof.backgroundColor2, false);
144 //                      g2d.setPaint(Color.white);
145                         g2d.setPaint( bgp );
146                         g2d.fill(bounds);
147                 }
148                         
149                 // Draw grid lines
150                 GridLines: if (ts.viewProfile.showGrid) {
151                         g2d.setPaint( rprof.gridColor );
152                         g2d.setStroke( GridUtil.GRID_LINE_STROKE );
153                         GridUtil.paintGridLines(
154                                 xGrid, yGrid, 
155                                 g2d, 
156                                 from - trend.horizRuler.basetime, 
157                                 trend.vertRuler != null ? trend.vertRuler.min : 0, 
158                                 w, 
159                                 h,
160                                 analogAreaHeight);
161                 }
162                 
163                 Rectangle2D oldClip = g2d.getClipBounds();
164                 
165                 // Draw analog items
166                 AnalogItems: if ( !trend.analogItems.isEmpty() ) {
167                         Rectangle2D analogAreaClip = new Rectangle2D.Double(0, 0, getWidth(), analogAreaHeight);
168                         g2d.setClip( analogAreaClip );
169                         
170                         for (int phase=0; phase<4; phase++) {
171                                 for (int i=0; i<trend.analogItems.size(); i++) {
172                                         ItemNode data = trend.analogItems.get(i);
173                                         drawItem(g2d, data, 0, analogAreaHeight, phase);
174                                 }
175                         }
176                         g2d.setClip(oldClip);
177                 }
178
179                 Separator: if ( !trend.analogItems.isEmpty() && !trend.binaryItems.isEmpty() ) {
180                         g2d.setColor( Color.BLACK );
181                         g2d.setStroke( GridUtil.GRID_LINE_STROKE );
182                         g2d.drawLine(0, (int) analogAreaHeight, (int) getWidth(), (int) analogAreaHeight);
183                 }
184                 
185                 // Draw binary items
186                 BinaryItems: if ( !trend.binaryItems.isEmpty() ) {
187                         Rectangle2D binaryAreaClip = new Rectangle2D.Double(0, analogAreaHeight, getWidth(), binaryAreaHeight);
188                         g2d.setClip( binaryAreaClip );
189                         for (int phase=0; phase<4; phase++) {
190                                 for (int i=0; i<trend.binaryItems.size(); i++) {
191                                         ItemNode data = trend.binaryItems.get(i);
192                                         double y = analogAreaHeight + i*BINARY[3];
193                                         drawItem(g2d, data, y, BINARY[2], phase);
194                                 }
195                         }
196                         g2d.setClip(oldClip);
197                         
198                         // Draw labels
199                         g2d.setFont( RULER_FONT );
200                         for (int i=0; i<trend.binaryItems.size(); i++)
201                         {
202                                 ItemNode data = trend.binaryItems.get(i);
203                                 g2d.setColor( data.color );
204                                 double fh = 9.; // font height
205                                 double y = analogAreaHeight + i*BINARY[3] + 1.f;
206                                 double fy = y+(BINARY[3]-fh)/2+fh;
207                                 g2d.drawString( data.item.label, ((float) getWidth())+7.f, (float) fy);
208                         }
209                 }
210                 
211                 // Draw milestones
212                 Milestone: if (ts.viewProfile.showMilestones) {
213                         double sx = getWidth() / ( end - from );
214                         MilestoneSpec mss = trend.milestones;
215                         List<Milestone> ls = mss.milestones;
216                         if ( ls.isEmpty() ) break Milestone;
217                         
218                         Line2D line = new Line2D.Double(0, 0, 0, h);
219                         g2d.setStroke( MILESTONE_LINE_STROKE );
220                         Rectangle2D diamondRegion = new Rectangle2D.Double(0, -DIAMOND_SIZE*2, w, DIAMOND_SIZE*2);
221                         
222                         for (int i=mss.milestones.size()-1; i>=0; i--) {
223                                 Milestone ms = mss.milestones.get( i );
224                                 if ( ms.hidden ) continue;
225                                 boolean isBaseline = i == mss.baseline; 
226                                 double time = ms.time;
227                                 double x = (time-from)*sx;
228                                 if (x<-DIAMOND_SIZE*2 || x>w+DIAMOND_SIZE*2) continue;
229                                 x = Math.floor(x);
230
231                                 // Diamond
232                                 g2d.setClip(diamondRegion);
233                                 g2d.translate( x, 0);
234                                 g2d.setColor( isBaseline ? Color.LIGHT_GRAY : Color.DARK_GRAY );
235                                 g2d.fill( DIAMOND );
236                                 g2d.setColor( Color.BLACK );
237                                 g2d.draw( DIAMOND );
238
239                                 // Text
240                                 Font f = isBaseline ? BASELINE_FONT : MILESTONE_FONT;
241                                 g2d.setFont( f );
242                                 g2d.setColor( isBaseline ? Color.black : Color.ORANGE );
243                                 GlyphVector glyphVector = f.createGlyphVector(g2d.getFontRenderContext(), ms.label);
244                     double cx = glyphVector.getVisualBounds().getCenterX();
245                     double cy = glyphVector.getVisualBounds().getHeight();
246                                 g2d.drawString(ms.label, (float)(-cx), (float)(-DIAMOND_SIZE+cy/2) );
247                                 g2d.setClip( null );
248                                 
249                                 // Line
250                                 if (x>=0 && x<w) {
251                                         g2d.setColor( Color.BLACK );
252                                         g2d.draw( line );
253                                 }
254                                 
255                                 g2d.translate(-x, 0);                           
256                         }
257                 }
258                 
259                 // Draw hover marker
260                 Hoverer: {
261                         Double time = trend.valueTipTime;
262                         if ( time != null && time>=from && time<=end && !Double.isNaN(time)) {
263                                 double sx = getWidth() / ( end - from );
264                                 double x = (time-from)*sx;
265                                 Line2D line = new Line2D.Double(x, 0, x, h);
266                                 g2d.setStroke( DASHED_LINE_STROKE_2 );
267                                 g2d.setColor( trend.valueTipHover ? Color.GRAY : Color.WHITE );
268                                 g2d.draw( line );
269                                 
270                                 g2d.setStroke( DASHED_LINE_STROKE );
271                                 g2d.setColor( Color.BLACK );
272                                 g2d.draw( line );
273                                 
274 //                              g2d.setStroke( DASHED_LINE_STROKE_INVERSE );
275 //                              g2d.setColor( Color.white );
276 //                              g2d.draw( line );
277                         }
278                 }
279
280                 // Draw border
281                 Border: {
282                         g2d.setStroke(BORDER_LINE_STROKE);
283                         g2d.setColor( Color.BLACK );
284                         Rectangle2D rect = new Rectangle2D.Double();
285                         rect.setFrame(0, 0, w, h);
286                         g2d.draw(rect);
287                 }
288
289                 //long endTime = System.nanoTime();
290 //              System.out.println("Plot render: "+((double)(endTime-startTime)/1000000)+" ms");
291         }
292
293         public void drawItem(Graphics2D g, ItemNode data, double y, double height, int phase) {
294                 TrendNode trend = getTrend();
295                 double from = trend.horizRuler.from;
296                 double end = trend.horizRuler.end;
297
298 //              trend.vertRulerIndex
299 //              boolean selected = trend.singleAxis ? false : trend.vertRuler 
300 //              selected &= !trend.printing;
301
302                 VertRuler ruler = data.ruler;
303                 AffineTransform at = g.getTransform();
304                 try {
305                         //double pixelsPerSecond = (end-from) / getWidth();
306                         g.translate(0, y);
307                         g.setStroke(data.stroke);
308
309                         AffineTransform ab = new AffineTransform();
310                         if (data.item.renderer == Renderer.Analog) {
311                                 ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );
312                                 ab.translate(-from, -ruler.max);
313 //                              if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);
314                                 data.draw(g, phase, false);
315                         }
316                         if (data.item.renderer == Renderer.Binary) {
317                                 ab.scale( getWidth()/(end-from), 1/*height*/ );
318                                 ab.translate(-from, 0);
319 //                              if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);
320                                 data.draw(g, phase, false);
321                         }
322 //              } catch (HistoryException e) {
323 //                      e.printStackTrace();
324 //              } catch (AccessorException e) {
325 //                      e.printStackTrace();
326                 } finally {
327                         g.setTransform(at);
328                 }
329         }
330
331         /**
332          * Prepare item for draw.
333          *  
334          * @param data
335          * @param y
336          * @param height
337          */
338         public void prepareItem(ItemNode data, double y, double height) {
339                 TrendNode tn = getTrend();
340                 double from = tn.horizRuler.from;
341                 double end = tn.horizRuler.end;
342                 
343                 VertRuler ruler = data.ruler;
344                 try {
345                         double pixelsPerSecond = (end-from) / getWidth();
346                         
347                         AffineTransform ab = new AffineTransform();
348                         if (data.item.renderer == Renderer.Analog) {
349                                 ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );
350                                 ab.translate(-from, -ruler.max);
351                                 data.prepareLine(from, end, pixelsPerSecond, ab);
352                         }
353                         if (data.item.renderer == Renderer.Binary) {
354                                 ab.scale( getWidth()/(end-from), 1/*height*/ );
355                                 ab.translate(-from, 0);
356                                 data.prepareLine(from, end, pixelsPerSecond, ab);
357                         }
358                 } catch (HistoryException e) {
359                         e.printStackTrace();
360                 } catch (AccessorException e) {
361                         e.printStackTrace();
362                 }
363         }
364         
365         static {
366                 
367                 DIAMOND = new Path2D.Double();
368                 DIAMOND.moveTo(0, -DIAMOND_SIZE*2);
369                 DIAMOND.lineTo(DIAMOND_SIZE, -DIAMOND_SIZE);
370                 DIAMOND.lineTo(0, 0);
371                 DIAMOND.lineTo(-DIAMOND_SIZE, -DIAMOND_SIZE);
372                 DIAMOND.lineTo(0, -DIAMOND_SIZE*2);
373
374                 MILESTONE_FONT = new Font("Tahoma", 0, (int) (DIAMOND_SIZE*1.2) );
375                 BASELINE_FONT = new Font("Tahoma", Font.BOLD, (int) (DIAMOND_SIZE*1.2) );
376                 TOOLTIP_FONT = new Font("Tahoma", 0, 13 );
377         }
378
379         
380         ///  ValueTip
381         public static final AlphaComposite composite66 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .80f);
382         
383         void drawValuetip( Graphics2D g, double time ) throws HistoryException, BindingException {
384                 TrendNode trend = getTrend();
385                 Font font = TOOLTIP_FONT;
386                 FontMetrics fm = g.getFontMetrics( font );
387
388                 //double from = trend.horizRuler.from;
389                 //double end = trend.horizRuler.end;
390                 //double pixelsPerSecond = (end-from) / trend.plot.getWidth();
391                 
392                 double marginInPlot = VALUE_TIP_BOX_PLOT_MARGIN;
393                 double marginOnFilm = VALUE_TIP_BOX_FILM_MARGIN;
394                 double marginBetweenNamesAndValues = 16;
395                 double quantumWidthOfValueArea = 20;
396                 double marginBetweenLines = 5;
397                 double textAreaHeight = 0;
398                 double textAreaWidth = 0;
399                 double valueAreaLeft = 0;
400                 double valueAreaRight = 0;
401                 double valueAreaWidth = 0;
402                 
403                 double x, y, w, h;
404                 
405                 List<TipLine> tipLines = new ArrayList<TipLine>( trend.allItems.size()+1 );
406                 
407                 /// Initialize
408                 // Add time
409                 {
410                         TipLine tl = new TipLine();
411                         tipLines.add(tl);
412                         tl.label = "Time";
413                         LineMetrics lm = fm.getLineMetrics(tl.label, g);
414                         tl.height = lm.getHeight();
415                         tl.labelBaseline = fm.getAscent();
416                         textAreaHeight += marginBetweenLines;
417                         textAreaHeight += tl.height; 
418                         tl.labelWidth = fm.stringWidth( tl.label );
419                         textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);                 
420                         tl.color = Color.WHITE;
421
422                         Format f = NumberFormat.getInstance();
423                         if (trend.timeFormat == org.simantics.trend.configuration.TimeFormat.Time) {                    
424                                 f = new TimeFormat(trend.horizRuler.iEnd, 3);
425                         }
426                         double t = time - trend.horizRuler.basetime;
427                         String formattedTime = f.format( time - trend.horizRuler.basetime );
428                         double roundedTime = t;
429                         try {
430                                 roundedTime = (Double) f.parseObject(formattedTime);
431                         } catch (ParseException e) {
432                                 // Should never happen.
433                         }
434                         boolean actuallyLessThan = t < roundedTime;
435                         boolean actuallyMoreThan = t > roundedTime;
436                         tl.value = actuallyLessThan ? "< " + formattedTime
437                                         : actuallyMoreThan ? "> " + formattedTime
438                                         : formattedTime;
439                         tl.valueWidth = fm.stringWidth( tl.value );
440                         valueAreaWidth = Math.max(valueAreaWidth, tl.valueWidth);
441                 }
442                 
443                 // Add items
444                 nextItem:
445                 for ( ItemNode i : trend.allItems )
446                 {
447                         TipLine tl = new TipLine();
448                         tipLines.add(tl);
449                         // Get Label
450                         tl.label = i.item.label;
451                         LineMetrics lm = fm.getLineMetrics(tl.label, g);
452                         tl.height = lm.getHeight();
453                         tl.labelBaseline = fm.getAscent();
454                         textAreaHeight += tl.height; 
455                         textAreaHeight += marginBetweenLines;
456                         tl.labelWidth = fm.stringWidth( tl.label );
457                         textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);
458                         tl.color = ColorUtil.gamma( i.color, 0.55 );
459                         
460                         // Get Value
461                         Stream s = i.openStream( /*pixelsPerSecond*/0 );
462                         if ( s!=null ) {
463                                 
464                                 int index = s.binarySearch(Bindings.DOUBLE, time);
465                                 if (index<0) index = -index-2;
466                                 if ( index<0 || index>=s.count() ) continue nextItem;
467                                 boolean isLast = index+1>=s.count();
468                                 
469                                 
470                                 ValueBand vb = new ValueBand(s.sampleBinding);                          
471                                 try {
472                                         vb.setSample( s.accessor.get(index, s.sampleBinding) );
473                                 } catch (AccessorException e) {
474                                         throw new HistoryException(e);
475                                 }
476                                 
477                                 if ( vb.getSample()!=null ) {
478
479                                         // gitlab #54: this can cause the value tip to not render values at all
480                                         // for items that have not been flushed at the point in time when this drawing
481                                         // is done. These circumstances are always temporary and later refreshes will
482                                         // remedy the situation. Having this logic can cause item values to randomly
483                                         // not be shown which is actually even more confusing to the user than rendering
484                                         // the latest flushed sample value.
485                                         // 
486                                         // For this reason, the if below has been commented out.
487                                         //if (isLast && vb.hasEndTime() && vb.getEndTimeDouble()<time) continue nextItem;
488                                                                                 
489                                         if ( !vb.isNanSample() && !vb.isNullValue() ) {
490                                                 Binding b = vb.getValueBinding();
491                                                 if ( b instanceof NumberBinding) {
492                                                         double v = vb.getValueDouble();
493                                                         tl.value = trend.valueFormat.format.format( v );
494                                                         tl.number = true;
495                                                         
496                                                         int desimalPos = tl.value.indexOf('.');
497                                                         if (desimalPos<0) desimalPos = tl.value.indexOf(',');
498                                                         if ( desimalPos>=0 ) {
499                                                                 String beforeDesimal = tl.value.substring(0, desimalPos);
500                                                                 String afterDesimal = tl.value.substring(desimalPos, tl.value.length());
501                                                                 tl.valueLeftWidth = fm.stringWidth(beforeDesimal);
502                                                                 tl.valueRightWidth = fm.stringWidth(afterDesimal);
503                                                                 tl.valueWidth = tl.valueLeftWidth + tl.valueRightWidth;
504                                                         } else {
505                                                                 tl.valueWidth = tl.valueLeftWidth = fm.stringWidth(tl.value);
506                                                         }
507                                                         
508                                                         valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth+tl.valueRightWidth);
509                                                         valueAreaLeft = Math.max(valueAreaLeft, tl.valueLeftWidth);
510                                                         valueAreaRight = Math.max(valueAreaRight, tl.valueRightWidth);
511                                                 } else {
512                                                         Object v = vb.getValue();
513                                                         tl.value = b.toString(v);
514                                                         tl.number = false;
515                                                         tl.valueLeftWidth = tl.valueRightWidth = fm.stringWidth( tl.value );
516                                                         valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth);
517                                                 }
518                                         }
519                                 }
520                         }
521                 }
522                 
523                 // Layout
524                 double halfQuantum = quantumWidthOfValueArea/2;
525                 valueAreaWidth = Math.ceil( valueAreaWidth / quantumWidthOfValueArea ) * quantumWidthOfValueArea;
526                 valueAreaLeft = Math.ceil( valueAreaLeft / halfQuantum ) * halfQuantum;
527                 valueAreaRight = Math.ceil( valueAreaRight / halfQuantum ) * halfQuantum;
528                 double finalValueAreaWidth = Math.max(valueAreaWidth, valueAreaLeft + valueAreaRight);
529                 w = marginOnFilm + textAreaWidth + marginBetweenNamesAndValues + finalValueAreaWidth + marginOnFilm + 0;
530                 h = marginOnFilm + textAreaHeight + marginOnFilm;
531                 double maxX = trend.plot.getWidth() - marginInPlot - w;
532                 double maxY = trend.plot.getHeight() - marginInPlot - h;
533                 x = marginInPlot + (maxX - marginInPlot)*trend.spec.viewProfile.valueViewPositionX;
534                 y = marginInPlot + (maxY - marginInPlot)*trend.spec.viewProfile.valueViewPositionY;
535
536                 if ( x < TrendLayout.VERT_MARGIN ) x = TrendLayout.VERT_MARGIN;
537
538                 valueTipBoxBounds.setFrame(x, y, w, h);
539                 //System.out.println("value tip bounds: " + valueTipBounds);
540
541                 // Draw
542                 Rectangle2D rect = new Rectangle2D.Double(0, 0, w, h);
543                 Composite oldComposite = g.getComposite();
544                 AffineTransform oldTransform = g.getTransform();
545                 try {                   
546                         g.setComposite(composite66);
547                         g.translate(x, y);
548                         g.setColor(Color.BLACK);
549                         g.fill( rect );
550                         g.setFont( font );
551                         g.setComposite(oldComposite);
552                         
553                         y = marginInPlot;
554                         for (TipLine tl : tipLines) {
555                                 g.setColor( tl.color );
556                                 x = marginInPlot;
557                                 g.drawString( tl.label, (float)x, (float)(y+tl.labelBaseline));
558
559                                 if ( tl.value!=null ) {
560                                         x = marginInPlot + textAreaWidth + marginBetweenNamesAndValues;
561                                         if ( tl.number ) {
562                                                 x += valueAreaLeft - tl.valueLeftWidth;
563                                         } else {
564                                                 x += (finalValueAreaWidth - tl.valueWidth)/2;
565                                         }
566                                         g.drawString(tl.value, (float) x, (float) (y+tl.labelBaseline));
567                                 }
568                                 
569                                 y+=tl.height;
570                                 y+=marginBetweenLines;
571                         }
572                         
573                 } finally {
574                         g.setTransform( oldTransform );
575                         g.setComposite( oldComposite );
576                 }
577         }
578
579         static class TipLine {
580                 String label, value;
581                 Color color;
582                 double labelWidth, height, valueLeftWidth, valueRightWidth, valueWidth, labelBaseline;
583                 boolean number;
584
585                 @Override
586                 public String toString() {
587                         return "TipLine[label=" + label + ", value=" + value + ", color=" + color + ", labelWidth=" + labelWidth
588                                         + ", height=" + height + ", valueLeftWidth=" + valueLeftWidth + ", valueRightWidth="
589                                         + valueRightWidth + ", valueWidth=" + valueWidth + ", labelBaseline=" + labelBaseline + ", number="
590                                         + number + "]";
591                 }
592         }
593
594         public void renderValueTip(Graphics2D g2d) {
595                 TrendNode trend = getTrend();
596                 if ( trend.valueTipTime != null ) {
597                         AffineTransform at = g2d.getTransform();                                
598                         try {
599                                 g2d.transform( getTransform() );
600                                 drawValuetip( g2d, trend.valueTipTime );
601                         } catch (HistoryException e) {
602                                 e.printStackTrace();
603                         } catch (BindingException e) {
604                                 e.printStackTrace();
605                         } finally {
606                                 g2d.setTransform( at );
607                         }                       
608                 }
609         }
610         
611         /**
612          * Pick item (Binary node)
613          * 
614          * @param pt coordinate in trend coordinate system
615          * @return item node
616          */
617         public ItemNode pickItem(Point2D pt)
618         {
619                 TrendNode trend = getTrend();
620                 double y = pt.getY()-getY();
621                 double x = pt.getX()-getX();
622                 if (y<analogAreaHeight || y>analogAreaHeight+binaryAreaHeight) return null;
623                 if (x<0 || x+getX()>trend.getBounds().getWidth()) return null;
624                 for (int i=0; i<trend.binaryItems.size(); i++) {
625                         double sy = analogAreaHeight + i*BINARY[3];
626                         double ey = analogAreaHeight + (i+1)*BINARY[3];
627                         if ( y>=sy && y<ey ) return trend.binaryItems.get(i);
628                 }
629                 return null;
630         }
631                 
632 }