]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.trend/src/org/simantics/trend/impl/Plot.java
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
index bb5de8c0dd22bb56ecaea3646cda27df927cc081..d014d6556a0633d6704338bf1c565e1e9141da3c 100644 (file)
-/*******************************************************************************\r
- * Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.trend.impl;\r
-\r
-import java.awt.AlphaComposite;\r
-import java.awt.BasicStroke;\r
-import java.awt.Color;\r
-import java.awt.Composite;\r
-import java.awt.Font;\r
-import java.awt.FontMetrics;\r
-import java.awt.GradientPaint;\r
-import java.awt.Graphics2D;\r
-import java.awt.Paint;\r
-import java.awt.font.GlyphVector;\r
-import java.awt.font.LineMetrics;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Line2D;\r
-import java.awt.geom.Path2D;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.text.Format;\r
-import java.text.NumberFormat;\r
-import java.text.ParseException;\r
-import java.util.ArrayList;\r
-import java.util.List;\r
-\r
-import org.simantics.databoard.Bindings;\r
-import org.simantics.databoard.accessor.error.AccessorException;\r
-import org.simantics.databoard.binding.Binding;\r
-import org.simantics.databoard.binding.NumberBinding;\r
-import org.simantics.databoard.binding.error.BindingException;\r
-import org.simantics.g2d.utils.GridSpacing;\r
-import org.simantics.g2d.utils.GridUtil;\r
-import org.simantics.history.HistoryException;\r
-import org.simantics.history.util.Stream;\r
-import org.simantics.history.util.ValueBand;\r
-import org.simantics.scenegraph.utils.ColorUtil;\r
-import org.simantics.trend.configuration.TrendItem.Renderer;\r
-import org.simantics.trend.configuration.TrendSpec;\r
-import org.simantics.utils.format.TimeFormat;\r
-\r
-public class Plot extends TrendGraphicalNode {\r
-\r
-    public static final double VALUE_TIP_BOX_PLOT_MARGIN = 7;\r
-    private static final double VALUE_TIP_BOX_FILM_MARGIN = 5;\r
-\r
-    private static final long serialVersionUID = 6335497685577733932L;\r
-\r
-    public static final BasicStroke BORDER_LINE_STROKE =\r
-            new BasicStroke(1.0f,\r
-                    BasicStroke.CAP_SQUARE,\r
-                    BasicStroke.JOIN_MITER,\r
-                    10.0f, null, 0.0f);\r
-\r
-    public static final BasicStroke MILESTONE_LINE_STROKE =\r
-            new BasicStroke(1.0f,\r
-                    BasicStroke.CAP_SQUARE,\r
-                    BasicStroke.JOIN_MITER,\r
-                    10.0f, null, 0.0f);\r
-\r
-    public static final BasicStroke TREND_LINE_STROKE =\r
-            new BasicStroke(1.0f,\r
-                    BasicStroke.CAP_BUTT,\r
-                    BasicStroke.JOIN_MITER,\r
-                    10.0f, null, 0.0f);\r
-\r
-    public static final BasicStroke DASHED_LINE_STROKE =\r
-               new BasicStroke(2.0f,              // Width\r
-                    BasicStroke.CAP_BUTT,    // End cap\r
-                    BasicStroke.JOIN_MITER,    // Join style\r
-                    5.0f,                      // Miter limit\r
-                    new float[] {5.0f,5.0f}, // Dash pattern\r
-                    0.0f);                     // Dash phase                   \r
-    public static final BasicStroke DASHED_LINE_STROKE_2 =\r
-               new BasicStroke(2.0f,              // Width\r
-                    BasicStroke.CAP_BUTT,    // End cap\r
-                    BasicStroke.JOIN_MITER,    // Join style\r
-                    5.0f,                      // Miter limit\r
-                    new float[] {5.0f,5.0f}, // Dash pattern\r
-                    5.0f);                     // Dash phase                   \r
-    public static final BasicStroke DASHED_LINE_STROKE_INVERSE =\r
-               new BasicStroke(1.0f,              // Width\r
-                    BasicStroke.CAP_BUTT,    // End cap\r
-                    BasicStroke.JOIN_MITER,    // Join style\r
-                    5.0f,                      // Miter limit\r
-                    new float[] {3.0f,8.0f}, // Dash pattern\r
-                    0.0f);                     // Dash phase                   \r
-\r
-    public static final Color PLOT_AREA_BG_GRADIENT_COLOR_TOP = new Color(228, 228, 248);\r
-    public static final Color PLOT_AREA_BG_GRADIENT_COLOR_BOTTOM = new Color(250, 250, 250);\r
-\r
-    static final Font MILESTONE_FONT, BASELINE_FONT, TOOLTIP_FONT;    \r
-    \r
-    static final GridSpacing SOME_SPACING = GridSpacing.makeGridSpacing(100, 100, 15);\r
-    \r
-    public static final Color GRID_LINE_COLOR = new Color(190, 190, 220);\r
-\r
-    static final double DIAMOND_SIZE = 7;\r
-    static final Path2D DIAMOND;\r
-       \r
-       double analogAreaHeight;\r
-       double binaryAreaHeight;\r
-\r
-       Rectangle2D valueTipBoxBounds = new Rectangle2D.Double();\r
-\r
-       @SuppressWarnings("unused")\r
-    @Override\r
-       protected void doRender(Graphics2D g2d) {\r
-               //long startTime = System.nanoTime();\r
-               TrendNode trend = (TrendNode) getParent();\r
-               TrendSpec ts = trend.spec;\r
-               ViewRenderingProfile rprof = trend.renderingProfile;\r
-               double w = bounds.getWidth();\r
-               double h = bounds.getHeight();\r
-               \r
-               double from = trend.horizRuler.from;\r
-               double end = trend.horizRuler.end;\r
-               GridSpacing xGrid = trend.horizRuler.spacing;\r
-               GridSpacing yGrid = trend.vertRuler != null ? trend.vertRuler.spacing : Plot.SOME_SPACING;\r
-               \r
-               if (w<1. || h<1.) return; \r
-               \r
-               // Prepare data\r
-               if (trend.shapedirty) {\r
-                       trend.shapedirty = false;\r
-                       for (ItemNode node : trend.analogItems) prepareItem(node, 0, analogAreaHeight);\r
-                       for (ItemNode node : trend.binaryItems) prepareItem(node, 0, analogAreaHeight);\r
-               }\r
-               \r
-               // Fill gradient\r
-               Gradient: {\r
-                       Paint bgp = rprof.backgroundColor2 == null ? rprof.backgroundColor1\r
-                                       : new GradientPaint(\r
-                                                       0.f, (float) h, rprof.backgroundColor1, \r
-                                                       0.f, 0.f, rprof.backgroundColor2, false);\r
-//                     g2d.setPaint(Color.white);\r
-                       g2d.setPaint( bgp );\r
-                       g2d.fill(bounds);\r
-               }\r
-                       \r
-               // Draw grid lines\r
-               GridLines: if (ts.viewProfile.showGrid) {\r
-                       g2d.setPaint( rprof.gridColor );\r
-                       g2d.setStroke( GridUtil.GRID_LINE_STROKE );\r
-                       GridUtil.paintGridLines(\r
-                               xGrid, yGrid, \r
-                               g2d, \r
-                               from - trend.horizRuler.basetime, \r
-                               trend.vertRuler != null ? trend.vertRuler.min : 0, \r
-                               w, \r
-                               h,\r
-                               analogAreaHeight);\r
-               }\r
-               \r
-               Rectangle2D oldClip = g2d.getClipBounds();\r
-               \r
-               // Draw analog items\r
-               AnalogItems: if ( !trend.analogItems.isEmpty() ) {\r
-                       Rectangle2D analogAreaClip = new Rectangle2D.Double(0, 0, getWidth(), analogAreaHeight);\r
-                       g2d.setClip( analogAreaClip );\r
-                       \r
-                       for (int phase=0; phase<4; phase++) {\r
-                               for (int i=0; i<trend.analogItems.size(); i++) {\r
-                                       ItemNode data = trend.analogItems.get(i);\r
-                                       drawItem(g2d, data, 0, analogAreaHeight, phase);\r
-                               }\r
-                       }\r
-                       g2d.setClip(oldClip);\r
-               }\r
-\r
-               Separator: if ( !trend.analogItems.isEmpty() && !trend.binaryItems.isEmpty() ) {\r
-                       g2d.setColor( Color.BLACK );\r
-                       g2d.setStroke( GridUtil.GRID_LINE_STROKE );\r
-                       g2d.drawLine(0, (int) analogAreaHeight, (int) getWidth(), (int) analogAreaHeight);\r
-               }\r
-               \r
-               // Draw binary items\r
-               BinaryItems: if ( !trend.binaryItems.isEmpty() ) {\r
-                       Rectangle2D binaryAreaClip = new Rectangle2D.Double(0, analogAreaHeight, getWidth(), binaryAreaHeight);\r
-                       g2d.setClip( binaryAreaClip );\r
-                       for (int phase=0; phase<4; phase++) {\r
-                               for (int i=0; i<trend.binaryItems.size(); i++) {\r
-                                       ItemNode data = trend.binaryItems.get(i);\r
-                                       double y = analogAreaHeight + i*BINARY[3];\r
-                                       drawItem(g2d, data, y, BINARY[2], phase);\r
-                               }\r
-                       }\r
-                       g2d.setClip(oldClip);\r
-                       \r
-                       // Draw labels\r
-                       g2d.setFont( RULER_FONT );\r
-                       for (int i=0; i<trend.binaryItems.size(); i++)\r
-                       {\r
-                               ItemNode data = trend.binaryItems.get(i);\r
-                               g2d.setColor( data.color );\r
-                               double fh = 9.; // font height\r
-                               double y = analogAreaHeight + i*BINARY[3] + 1.f;\r
-                               double fy = y+(BINARY[3]-fh)/2+fh;\r
-                               g2d.drawString( data.item.label, ((float) getWidth())+7.f, (float) fy);\r
-                       }\r
-               }\r
-               \r
-               // Draw milestones\r
-               Milestone: if (ts.viewProfile.showMilestones) {\r
-                       double sx = getWidth() / ( end - from );\r
-                       MilestoneSpec mss = trend.milestones;\r
-                       List<Milestone> ls = mss.milestones;\r
-                       if ( ls.isEmpty() ) break Milestone;\r
-                       \r
-                       Line2D line = new Line2D.Double(0, 0, 0, h);\r
-                       g2d.setStroke( MILESTONE_LINE_STROKE );\r
-                       Rectangle2D diamondRegion = new Rectangle2D.Double(0, -DIAMOND_SIZE*2, w, DIAMOND_SIZE*2);\r
-                       \r
-                       for (int i=mss.milestones.size()-1; i>=0; i--) {\r
-                               Milestone ms = mss.milestones.get( i );\r
-                               if ( ms.hidden ) continue;\r
-                               boolean isBaseline = i == mss.baseline; \r
-                               double time = ms.time;\r
-                               double x = (time-from)*sx;\r
-                               if (x<-DIAMOND_SIZE*2 || x>w+DIAMOND_SIZE*2) continue;\r
-                               x = Math.floor(x);\r
-\r
-                               // Diamond\r
-                               g2d.setClip(diamondRegion);\r
-                               g2d.translate( x, 0);\r
-                               g2d.setColor( isBaseline ? Color.LIGHT_GRAY : Color.DARK_GRAY );\r
-                               g2d.fill( DIAMOND );\r
-                               g2d.setColor( Color.BLACK );\r
-                               g2d.draw( DIAMOND );\r
-\r
-                               // Text\r
-                               Font f = isBaseline ? BASELINE_FONT : MILESTONE_FONT;\r
-                               g2d.setFont( f );\r
-                               g2d.setColor( isBaseline ? Color.black : Color.ORANGE );\r
-                               GlyphVector glyphVector = f.createGlyphVector(g2d.getFontRenderContext(), ms.label);\r
-                   double cx = glyphVector.getVisualBounds().getCenterX();\r
-                   double cy = glyphVector.getVisualBounds().getHeight();\r
-                               g2d.drawString(ms.label, (float)(-cx), (float)(-DIAMOND_SIZE+cy/2) );\r
-                               g2d.setClip( null );\r
-                               \r
-                               // Line\r
-                               if (x>=0 && x<w) {\r
-                                       g2d.setColor( Color.BLACK );\r
-                                       g2d.draw( line );\r
-                               }\r
-                               \r
-                               g2d.translate(-x, 0);                           \r
-                       }\r
-               }\r
-               \r
-               // Draw hover marker\r
-               Hoverer: {\r
-                       Double time = trend.valueTipTime;\r
-                       if ( time != null && time>=from && time<=end && !Double.isNaN(time)) {\r
-                               double sx = getWidth() / ( end - from );\r
-                               double x = (time-from)*sx;\r
-                               Line2D line = new Line2D.Double(x, 0, x, h);\r
-                               g2d.setStroke( DASHED_LINE_STROKE_2 );\r
-                               g2d.setColor( trend.valueTipHover ? Color.GRAY : Color.WHITE );\r
-                               g2d.draw( line );\r
-                               \r
-                               g2d.setStroke( DASHED_LINE_STROKE );\r
-                               g2d.setColor( Color.BLACK );\r
-                               g2d.draw( line );\r
-                               \r
-//                             g2d.setStroke( DASHED_LINE_STROKE_INVERSE );\r
-//                             g2d.setColor( Color.white );\r
-//                             g2d.draw( line );\r
-                       }\r
-               }\r
-\r
-               // Draw border\r
-               Border: {\r
-                       g2d.setStroke(BORDER_LINE_STROKE);\r
-                       g2d.setColor( Color.BLACK );\r
-                       Rectangle2D rect = new Rectangle2D.Double();\r
-                       rect.setFrame(0, 0, w, h);\r
-                       g2d.draw(rect);\r
-               }\r
-\r
-               //long endTime = System.nanoTime();\r
-//             System.out.println("Plot render: "+((double)(endTime-startTime)/1000000)+" ms");\r
-       }\r
-\r
-       public void drawItem(Graphics2D g, ItemNode data, double y, double height, int phase) {\r
-               TrendNode trend = getTrend();\r
-               double from = trend.horizRuler.from;\r
-               double end = trend.horizRuler.end;\r
-\r
-//             trend.vertRulerIndex\r
-//             boolean selected = trend.singleAxis ? false : trend.vertRuler \r
-//             selected &= !trend.printing;\r
-\r
-               VertRuler ruler = data.ruler;\r
-               AffineTransform at = g.getTransform();\r
-               try {\r
-                       //double pixelsPerSecond = (end-from) / getWidth();\r
-                       g.translate(0, y);\r
-                       g.setStroke(data.stroke);\r
-\r
-                       AffineTransform ab = new AffineTransform();\r
-                       if (data.item.renderer == Renderer.Analog) {\r
-                               ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );\r
-                               ab.translate(-from, -ruler.max);\r
-//                             if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);\r
-                               data.draw(g, phase, false);\r
-                       }\r
-                       if (data.item.renderer == Renderer.Binary) {\r
-                               ab.scale( getWidth()/(end-from), 1/*height*/ );\r
-                               ab.translate(-from, 0);\r
-//                             if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);\r
-                               data.draw(g, phase, false);\r
-                       }\r
-//             } catch (HistoryException e) {\r
-//                     e.printStackTrace();\r
-//             } catch (AccessorException e) {\r
-//                     e.printStackTrace();\r
-               } finally {\r
-                       g.setTransform(at);\r
-               }\r
-       }\r
-\r
-       /**\r
-        * Prepare item for draw.\r
-        *  \r
-        * @param data\r
-        * @param y\r
-        * @param height\r
-        */\r
-       public void prepareItem(ItemNode data, double y, double height) {\r
-               TrendNode tn = getTrend();\r
-               double from = tn.horizRuler.from;\r
-               double end = tn.horizRuler.end;\r
-               \r
-               VertRuler ruler = data.ruler;\r
-               try {\r
-                       double pixelsPerSecond = (end-from) / getWidth();\r
-                       \r
-                       AffineTransform ab = new AffineTransform();\r
-                       if (data.item.renderer == Renderer.Analog) {\r
-                               ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );\r
-                               ab.translate(-from, -ruler.max);\r
-                               data.prepareLine(from, end, pixelsPerSecond, ab);\r
-                       }\r
-                       if (data.item.renderer == Renderer.Binary) {\r
-                               ab.scale( getWidth()/(end-from), 1/*height*/ );\r
-                               ab.translate(-from, 0);\r
-                               data.prepareLine(from, end, pixelsPerSecond, ab);\r
-                       }\r
-               } catch (HistoryException e) {\r
-                       e.printStackTrace();\r
-               } catch (AccessorException e) {\r
-                       e.printStackTrace();\r
-               }\r
-       }\r
-       \r
-       static {\r
-               \r
-               DIAMOND = new Path2D.Double();\r
-               DIAMOND.moveTo(0, -DIAMOND_SIZE*2);\r
-               DIAMOND.lineTo(DIAMOND_SIZE, -DIAMOND_SIZE);\r
-               DIAMOND.lineTo(0, 0);\r
-               DIAMOND.lineTo(-DIAMOND_SIZE, -DIAMOND_SIZE);\r
-               DIAMOND.lineTo(0, -DIAMOND_SIZE*2);\r
-\r
-               MILESTONE_FONT = new Font("Tahoma", 0, (int) (DIAMOND_SIZE*1.2) );\r
-               BASELINE_FONT = new Font("Tahoma", Font.BOLD, (int) (DIAMOND_SIZE*1.2) );\r
-               TOOLTIP_FONT = new Font("Tahoma", 0, 13 );\r
-       }\r
-\r
-       \r
-       ///  ValueTip\r
-       public static final AlphaComposite composite66 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .80f);\r
-       \r
-       void drawValuetip( Graphics2D g, double time ) throws HistoryException, BindingException {\r
-               TrendNode trend = getTrend();\r
-               Font font = TOOLTIP_FONT;\r
-               FontMetrics fm = g.getFontMetrics( font );\r
-\r
-               //double from = trend.horizRuler.from;\r
-               //double end = trend.horizRuler.end;\r
-               //double pixelsPerSecond = (end-from) / trend.plot.getWidth();\r
-               \r
-               double marginInPlot = VALUE_TIP_BOX_PLOT_MARGIN;\r
-               double marginOnFilm = VALUE_TIP_BOX_FILM_MARGIN;\r
-               double marginBetweenNamesAndValues = 16;\r
-               double quantumWidthOfValueArea = 20;\r
-               double marginBetweenLines = 5;\r
-               double textAreaHeight = 0;\r
-               double textAreaWidth = 0;\r
-               double valueAreaLeft = 0;\r
-               double valueAreaRight = 0;\r
-               double valueAreaWidth = 0;\r
-               \r
-               double x, y, w, h;\r
-               \r
-               List<TipLine> tipLines = new ArrayList<TipLine>( trend.allItems.size()+1 );\r
-               \r
-               /// Initialize\r
-               // Add time\r
-               {\r
-                       TipLine tl = new TipLine();\r
-                       tipLines.add(tl);\r
-                       tl.label = "Time";\r
-                       LineMetrics lm = fm.getLineMetrics(tl.label, g);\r
-                       tl.height = lm.getHeight();\r
-                       tl.labelBaseline = fm.getAscent();\r
-                       textAreaHeight += marginBetweenLines;\r
-                       textAreaHeight += tl.height; \r
-                       tl.labelWidth = fm.stringWidth( tl.label );\r
-                       textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);                 \r
-                       tl.color = Color.WHITE;\r
-\r
-                       Format f = NumberFormat.getInstance();\r
-                       if (trend.timeFormat == org.simantics.trend.configuration.TimeFormat.Time) {                    \r
-                               f = new TimeFormat(trend.horizRuler.iEnd, 3);\r
-                       }\r
-                       double t = time - trend.horizRuler.basetime;\r
-                       String formattedTime = f.format( time - trend.horizRuler.basetime );\r
-                       double roundedTime = t;\r
-                       try {\r
-                               roundedTime = (Double) f.parseObject(formattedTime);\r
-                       } catch (ParseException e) {\r
-                               // Should never happen.\r
-                       }\r
-                       boolean actuallyLessThan = t < roundedTime;\r
-                       boolean actuallyMoreThan = t > roundedTime;\r
-                       tl.value = actuallyLessThan ? "< " + formattedTime\r
-                                       : actuallyMoreThan ? "> " + formattedTime\r
-                                       : formattedTime;\r
-                       tl.valueWidth = fm.stringWidth( tl.value );\r
-                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueWidth);\r
-               }\r
-               \r
-               // Add items\r
-               nextItem:\r
-               for ( ItemNode i : trend.allItems )\r
-               {\r
-                       TipLine tl = new TipLine();\r
-                       tipLines.add(tl);\r
-                       // Get Label\r
-                       tl.label = i.item.label;\r
-                       LineMetrics lm = fm.getLineMetrics(tl.label, g);\r
-                       tl.height = lm.getHeight();\r
-                       tl.labelBaseline = fm.getAscent();\r
-                       textAreaHeight += tl.height; \r
-                       textAreaHeight += marginBetweenLines;\r
-                       tl.labelWidth = fm.stringWidth( tl.label );\r
-                       textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);\r
-                       tl.color = ColorUtil.gamma( i.color, 0.55 );\r
-                       \r
-                       // Get Value\r
-                       Stream s = i.openStream( /*pixelsPerSecond*/0 );\r
-                       if ( s!=null ) {\r
-                               \r
-                               int index = s.binarySearch(Bindings.DOUBLE, time);\r
-                               if (index<0) index = -index-2;\r
-                               if ( index<0 || index>=s.count() ) continue nextItem;\r
-                               boolean isLast = index+1>=s.count();\r
-                               \r
-                               \r
-                               ValueBand vb = new ValueBand(s.sampleBinding);                          \r
-                               try {\r
-                                       vb.setSample( s.accessor.get(index, s.sampleBinding) );\r
-                               } catch (AccessorException e) {\r
-                                       throw new HistoryException(e);\r
-                               }\r
-                               \r
-                               if ( vb.getSample()!=null ) {\r
-                                       \r
-                                       if (isLast && vb.hasEndTime() && vb.getEndTimeDouble()<time) continue nextItem;\r
-                                                                               \r
-                                       if ( !vb.isNanSample() && !vb.isNullValue() ) {\r
-                                               Binding b = vb.getValueBinding();\r
-                                               if ( b instanceof NumberBinding) {\r
-                                                       double v = vb.getValueDouble();\r
-                                                       tl.value = trend.valueFormat.format.format( v );\r
-                                                       tl.number = true;\r
-                                                       \r
-                                                       int desimalPos = tl.value.indexOf('.');\r
-                                                       if (desimalPos<0) desimalPos = tl.value.indexOf(',');\r
-                                                       if ( desimalPos>=0 ) {\r
-                                                               String beforeDesimal = tl.value.substring(0, desimalPos);\r
-                                                               String afterDesimal = tl.value.substring(desimalPos, tl.value.length());\r
-                                                               tl.valueLeftWidth = fm.stringWidth(beforeDesimal);\r
-                                                               tl.valueRightWidth = fm.stringWidth(afterDesimal);\r
-                                                               tl.valueWidth = tl.valueLeftWidth + tl.valueRightWidth;\r
-                                                       } else {\r
-                                                               tl.valueWidth = tl.valueLeftWidth = fm.stringWidth(tl.value);\r
-                                                       }\r
-                                                       \r
-                                                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth+tl.valueRightWidth);\r
-                                                       valueAreaLeft = Math.max(valueAreaLeft, tl.valueLeftWidth);\r
-                                                       valueAreaRight = Math.max(valueAreaRight, tl.valueRightWidth);\r
-                                               } else {\r
-                                                       Object v = vb.getValue();\r
-                                                       tl.value = b.toString(v);\r
-                                                       tl.number = false;\r
-                                                       tl.valueLeftWidth = tl.valueRightWidth = fm.stringWidth( tl.value );\r
-                                                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth);\r
-                                               }\r
-                                       }\r
-                               }\r
-                       }\r
-               }\r
-               \r
-               // Layout\r
-               double halfQuantum = quantumWidthOfValueArea/2;\r
-               valueAreaWidth = Math.ceil( valueAreaWidth / quantumWidthOfValueArea ) * quantumWidthOfValueArea;\r
-               valueAreaLeft = Math.ceil( valueAreaLeft / halfQuantum ) * halfQuantum;\r
-               valueAreaRight = Math.ceil( valueAreaRight / halfQuantum ) * halfQuantum;\r
-               double finalValueAreaWidth = Math.max(valueAreaWidth, valueAreaLeft + valueAreaRight);\r
-               w = marginOnFilm + textAreaWidth + marginBetweenNamesAndValues + finalValueAreaWidth + marginOnFilm + 0;\r
-               h = marginOnFilm + textAreaHeight + marginOnFilm;\r
-               double maxX = trend.plot.getWidth() - marginInPlot - w;\r
-               double maxY = trend.plot.getHeight() - marginInPlot - h;\r
-               x = marginInPlot + (maxX - marginInPlot)*trend.spec.viewProfile.valueViewPositionX;\r
-               y = marginInPlot + (maxY - marginInPlot)*trend.spec.viewProfile.valueViewPositionY;\r
-\r
-               if ( x < TrendLayout.VERT_MARGIN ) x = TrendLayout.VERT_MARGIN;\r
-\r
-               valueTipBoxBounds.setFrame(x, y, w, h);\r
-               //System.out.println("value tip bounds: " + valueTipBounds);\r
-\r
-               // Draw\r
-               Rectangle2D rect = new Rectangle2D.Double(0, 0, w, h);\r
-               Composite oldComposite = g.getComposite();\r
-               AffineTransform oldTransform = g.getTransform();\r
-               try {                   \r
-                       g.setComposite(composite66);\r
-                       g.translate(x, y);\r
-                       g.setColor(Color.BLACK);\r
-                       g.fill( rect );\r
-                       g.setFont( font );\r
-                       g.setComposite(oldComposite);\r
-                       \r
-                       y = marginInPlot;\r
-                       for (TipLine tl : tipLines) {\r
-                               g.setColor( tl.color );\r
-                               x = marginInPlot;\r
-                               g.drawString( tl.label, (float)x, (float)(y+tl.labelBaseline));\r
-\r
-                               if ( tl.value!=null ) {\r
-                                       x = marginInPlot + textAreaWidth + marginBetweenNamesAndValues;\r
-                                       if ( tl.number ) {\r
-                                               x += valueAreaLeft - tl.valueLeftWidth;\r
-                                       } else {\r
-                                               x += (finalValueAreaWidth - tl.valueWidth)/2;\r
-                                       }\r
-                                       g.drawString(tl.value, (float) x, (float) (y+tl.labelBaseline));\r
-                               }\r
-                               \r
-                               y+=tl.height;\r
-                               y+=marginBetweenLines;\r
-                       }\r
-                       \r
-               } finally {\r
-                       g.setTransform( oldTransform );\r
-                       g.setComposite( oldComposite );\r
-               }\r
-       }\r
-\r
-       static class TipLine {\r
-               String label, value;\r
-               Color color;\r
-               double labelWidth, height, valueLeftWidth, valueRightWidth, valueWidth, labelBaseline;\r
-               boolean number;\r
-\r
-               @Override\r
-               public String toString() {\r
-                       return "TipLine[label=" + label + ", value=" + value + ", color=" + color + ", labelWidth=" + labelWidth\r
-                                       + ", height=" + height + ", valueLeftWidth=" + valueLeftWidth + ", valueRightWidth="\r
-                                       + valueRightWidth + ", valueWidth=" + valueWidth + ", labelBaseline=" + labelBaseline + ", number="\r
-                                       + number + "]";\r
-               }\r
-       }\r
-\r
-       public void renderValueTip(Graphics2D g2d) {\r
-               TrendNode trend = getTrend();\r
-               if ( trend.valueTipTime != null ) {\r
-                       AffineTransform at = g2d.getTransform();                                \r
-                       try {\r
-                               g2d.transform( getTransform() );\r
-                               drawValuetip( g2d, trend.valueTipTime );\r
-                       } catch (HistoryException e) {\r
-                               e.printStackTrace();\r
-                       } catch (BindingException e) {\r
-                               e.printStackTrace();\r
-                       } finally {\r
-                               g2d.setTransform( at );\r
-                       }                       \r
-               }\r
-       }\r
-       \r
-       /**\r
-        * Pick item (Binary node)\r
-        * \r
-        * @param pt coordinate in trend coordinate system\r
-        * @return item node\r
-        */\r
-       public ItemNode pickItem(Point2D pt)\r
-       {\r
-               TrendNode trend = getTrend();\r
-               double y = pt.getY()-getY();\r
-               double x = pt.getX()-getX();\r
-               if (y<analogAreaHeight || y>analogAreaHeight+binaryAreaHeight) return null;\r
-               if (x<0 || x+getX()>trend.getBounds().getWidth()) return null;\r
-               for (int i=0; i<trend.binaryItems.size(); i++) {\r
-                       double sy = analogAreaHeight + i*BINARY[3];\r
-                       double ey = analogAreaHeight + (i+1)*BINARY[3];\r
-                       if ( y>=sy && y<ey ) return trend.binaryItems.get(i);\r
-               }\r
-               return null;\r
-       }\r
-               \r
-}\r
+/*******************************************************************************
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.trend.impl;
+
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.GradientPaint;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.font.GlyphVector;
+import java.awt.font.LineMetrics;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.text.Format;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.accessor.error.AccessorException;
+import org.simantics.databoard.binding.Binding;
+import org.simantics.databoard.binding.NumberBinding;
+import org.simantics.databoard.binding.error.BindingException;
+import org.simantics.g2d.utils.GridSpacing;
+import org.simantics.g2d.utils.GridUtil;
+import org.simantics.history.HistoryException;
+import org.simantics.history.util.Stream;
+import org.simantics.history.util.ValueBand;
+import org.simantics.scenegraph.utils.ColorUtil;
+import org.simantics.trend.configuration.TrendItem.Renderer;
+import org.simantics.trend.configuration.TrendSpec;
+import org.simantics.utils.format.TimeFormat;
+
+public class Plot extends TrendGraphicalNode {
+
+    public static final double VALUE_TIP_BOX_PLOT_MARGIN = 7;
+    private static final double VALUE_TIP_BOX_FILM_MARGIN = 5;
+
+    private static final long serialVersionUID = 6335497685577733932L;
+
+    public static final BasicStroke BORDER_LINE_STROKE =
+            new BasicStroke(1.0f,
+                    BasicStroke.CAP_SQUARE,
+                    BasicStroke.JOIN_MITER,
+                    10.0f, null, 0.0f);
+
+    public static final BasicStroke MILESTONE_LINE_STROKE =
+            new BasicStroke(1.0f,
+                    BasicStroke.CAP_SQUARE,
+                    BasicStroke.JOIN_MITER,
+                    10.0f, null, 0.0f);
+
+    public static final BasicStroke TREND_LINE_STROKE =
+            new BasicStroke(1.0f,
+                    BasicStroke.CAP_BUTT,
+                    BasicStroke.JOIN_MITER,
+                    10.0f, null, 0.0f);
+
+    public static final BasicStroke DASHED_LINE_STROKE =
+               new BasicStroke(2.0f,              // Width
+                    BasicStroke.CAP_BUTT,    // End cap
+                    BasicStroke.JOIN_MITER,    // Join style
+                    5.0f,                      // Miter limit
+                    new float[] {5.0f,5.0f}, // Dash pattern
+                    0.0f);                     // Dash phase                   
+    public static final BasicStroke DASHED_LINE_STROKE_2 =
+               new BasicStroke(2.0f,              // Width
+                    BasicStroke.CAP_BUTT,    // End cap
+                    BasicStroke.JOIN_MITER,    // Join style
+                    5.0f,                      // Miter limit
+                    new float[] {5.0f,5.0f}, // Dash pattern
+                    5.0f);                     // Dash phase                   
+    public static final BasicStroke DASHED_LINE_STROKE_INVERSE =
+               new BasicStroke(1.0f,              // Width
+                    BasicStroke.CAP_BUTT,    // End cap
+                    BasicStroke.JOIN_MITER,    // Join style
+                    5.0f,                      // Miter limit
+                    new float[] {3.0f,8.0f}, // Dash pattern
+                    0.0f);                     // Dash phase                   
+
+    public static final Color PLOT_AREA_BG_GRADIENT_COLOR_TOP = new Color(228, 228, 248);
+    public static final Color PLOT_AREA_BG_GRADIENT_COLOR_BOTTOM = new Color(250, 250, 250);
+
+    static final Font MILESTONE_FONT, BASELINE_FONT, TOOLTIP_FONT;    
+    
+    static final GridSpacing SOME_SPACING = GridSpacing.makeGridSpacing(100, 100, 15);
+    
+    public static final Color GRID_LINE_COLOR = new Color(190, 190, 220);
+
+    static final double DIAMOND_SIZE = 7;
+    static final Path2D DIAMOND;
+       
+       double analogAreaHeight;
+       double binaryAreaHeight;
+
+       Rectangle2D valueTipBoxBounds = new Rectangle2D.Double();
+
+       @SuppressWarnings("unused")
+    @Override
+       protected void doRender(Graphics2D g2d) {
+               //long startTime = System.nanoTime();
+               TrendNode trend = (TrendNode) getParent();
+               TrendSpec ts = trend.spec;
+               ViewRenderingProfile rprof = trend.renderingProfile;
+               double w = bounds.getWidth();
+               double h = bounds.getHeight();
+               
+               double from = trend.horizRuler.from;
+               double end = trend.horizRuler.end;
+               GridSpacing xGrid = trend.horizRuler.spacing;
+               GridSpacing yGrid = trend.vertRuler != null ? trend.vertRuler.spacing : Plot.SOME_SPACING;
+               
+               if (w<1. || h<1.) return; 
+               
+               // Prepare data
+               if (trend.shapedirty) {
+                       trend.shapedirty = false;
+                       for (ItemNode node : trend.analogItems) prepareItem(node, 0, analogAreaHeight);
+                       for (ItemNode node : trend.binaryItems) prepareItem(node, 0, analogAreaHeight);
+               }
+               
+               // Fill gradient
+               Gradient: {
+                       Paint bgp = rprof.backgroundColor2 == null ? rprof.backgroundColor1
+                                       : new GradientPaint(
+                                                       0.f, (float) h, rprof.backgroundColor1, 
+                                                       0.f, 0.f, rprof.backgroundColor2, false);
+//                     g2d.setPaint(Color.white);
+                       g2d.setPaint( bgp );
+                       g2d.fill(bounds);
+               }
+                       
+               // Draw grid lines
+               GridLines: if (ts.viewProfile.showGrid) {
+                       g2d.setPaint( rprof.gridColor );
+                       g2d.setStroke( GridUtil.GRID_LINE_STROKE );
+                       GridUtil.paintGridLines(
+                               xGrid, yGrid, 
+                               g2d, 
+                               from - trend.horizRuler.basetime, 
+                               trend.vertRuler != null ? trend.vertRuler.min : 0, 
+                               w, 
+                               h,
+                               analogAreaHeight);
+               }
+               
+               Rectangle2D oldClip = g2d.getClipBounds();
+               
+               // Draw analog items
+               AnalogItems: if ( !trend.analogItems.isEmpty() ) {
+                       Rectangle2D analogAreaClip = new Rectangle2D.Double(0, 0, getWidth(), analogAreaHeight);
+                       g2d.setClip( analogAreaClip );
+                       
+                       for (int phase=0; phase<4; phase++) {
+                               for (int i=0; i<trend.analogItems.size(); i++) {
+                                       ItemNode data = trend.analogItems.get(i);
+                                       drawItem(g2d, data, 0, analogAreaHeight, phase);
+                               }
+                       }
+                       g2d.setClip(oldClip);
+               }
+
+               Separator: if ( !trend.analogItems.isEmpty() && !trend.binaryItems.isEmpty() ) {
+                       g2d.setColor( Color.BLACK );
+                       g2d.setStroke( GridUtil.GRID_LINE_STROKE );
+                       g2d.drawLine(0, (int) analogAreaHeight, (int) getWidth(), (int) analogAreaHeight);
+               }
+               
+               // Draw binary items
+               BinaryItems: if ( !trend.binaryItems.isEmpty() ) {
+                       Rectangle2D binaryAreaClip = new Rectangle2D.Double(0, analogAreaHeight, getWidth(), binaryAreaHeight);
+                       g2d.setClip( binaryAreaClip );
+                       for (int phase=0; phase<4; phase++) {
+                               for (int i=0; i<trend.binaryItems.size(); i++) {
+                                       ItemNode data = trend.binaryItems.get(i);
+                                       double y = analogAreaHeight + i*BINARY[3];
+                                       drawItem(g2d, data, y, BINARY[2], phase);
+                               }
+                       }
+                       g2d.setClip(oldClip);
+                       
+                       // Draw labels
+                       g2d.setFont( RULER_FONT );
+                       for (int i=0; i<trend.binaryItems.size(); i++)
+                       {
+                               ItemNode data = trend.binaryItems.get(i);
+                               g2d.setColor( data.color );
+                               double fh = 9.; // font height
+                               double y = analogAreaHeight + i*BINARY[3] + 1.f;
+                               double fy = y+(BINARY[3]-fh)/2+fh;
+                               g2d.drawString( data.item.label, ((float) getWidth())+7.f, (float) fy);
+                       }
+               }
+               
+               // Draw milestones
+               Milestone: if (ts.viewProfile.showMilestones) {
+                       double sx = getWidth() / ( end - from );
+                       MilestoneSpec mss = trend.milestones;
+                       List<Milestone> ls = mss.milestones;
+                       if ( ls.isEmpty() ) break Milestone;
+                       
+                       Line2D line = new Line2D.Double(0, 0, 0, h);
+                       g2d.setStroke( MILESTONE_LINE_STROKE );
+                       Rectangle2D diamondRegion = new Rectangle2D.Double(0, -DIAMOND_SIZE*2, w, DIAMOND_SIZE*2);
+                       
+                       for (int i=mss.milestones.size()-1; i>=0; i--) {
+                               Milestone ms = mss.milestones.get( i );
+                               if ( ms.hidden ) continue;
+                               boolean isBaseline = i == mss.baseline; 
+                               double time = ms.time;
+                               double x = (time-from)*sx;
+                               if (x<-DIAMOND_SIZE*2 || x>w+DIAMOND_SIZE*2) continue;
+                               x = Math.floor(x);
+
+                               // Diamond
+                               g2d.setClip(diamondRegion);
+                               g2d.translate( x, 0);
+                               g2d.setColor( isBaseline ? Color.LIGHT_GRAY : Color.DARK_GRAY );
+                               g2d.fill( DIAMOND );
+                               g2d.setColor( Color.BLACK );
+                               g2d.draw( DIAMOND );
+
+                               // Text
+                               Font f = isBaseline ? BASELINE_FONT : MILESTONE_FONT;
+                               g2d.setFont( f );
+                               g2d.setColor( isBaseline ? Color.black : Color.ORANGE );
+                               GlyphVector glyphVector = f.createGlyphVector(g2d.getFontRenderContext(), ms.label);
+                   double cx = glyphVector.getVisualBounds().getCenterX();
+                   double cy = glyphVector.getVisualBounds().getHeight();
+                               g2d.drawString(ms.label, (float)(-cx), (float)(-DIAMOND_SIZE+cy/2) );
+                               g2d.setClip( null );
+                               
+                               // Line
+                               if (x>=0 && x<w) {
+                                       g2d.setColor( Color.BLACK );
+                                       g2d.draw( line );
+                               }
+                               
+                               g2d.translate(-x, 0);                           
+                       }
+               }
+               
+               // Draw hover marker
+               Hoverer: {
+                       Double time = trend.valueTipTime;
+                       if ( time != null && time>=from && time<=end && !Double.isNaN(time)) {
+                               double sx = getWidth() / ( end - from );
+                               double x = (time-from)*sx;
+                               Line2D line = new Line2D.Double(x, 0, x, h);
+                               g2d.setStroke( DASHED_LINE_STROKE_2 );
+                               g2d.setColor( trend.valueTipHover ? Color.GRAY : Color.WHITE );
+                               g2d.draw( line );
+                               
+                               g2d.setStroke( DASHED_LINE_STROKE );
+                               g2d.setColor( Color.BLACK );
+                               g2d.draw( line );
+                               
+//                             g2d.setStroke( DASHED_LINE_STROKE_INVERSE );
+//                             g2d.setColor( Color.white );
+//                             g2d.draw( line );
+                       }
+               }
+
+               // Draw border
+               Border: {
+                       g2d.setStroke(BORDER_LINE_STROKE);
+                       g2d.setColor( Color.BLACK );
+                       Rectangle2D rect = new Rectangle2D.Double();
+                       rect.setFrame(0, 0, w, h);
+                       g2d.draw(rect);
+               }
+
+               //long endTime = System.nanoTime();
+//             System.out.println("Plot render: "+((double)(endTime-startTime)/1000000)+" ms");
+       }
+
+       public void drawItem(Graphics2D g, ItemNode data, double y, double height, int phase) {
+               TrendNode trend = getTrend();
+               double from = trend.horizRuler.from;
+               double end = trend.horizRuler.end;
+
+//             trend.vertRulerIndex
+//             boolean selected = trend.singleAxis ? false : trend.vertRuler 
+//             selected &= !trend.printing;
+
+               VertRuler ruler = data.ruler;
+               AffineTransform at = g.getTransform();
+               try {
+                       //double pixelsPerSecond = (end-from) / getWidth();
+                       g.translate(0, y);
+                       g.setStroke(data.stroke);
+
+                       AffineTransform ab = new AffineTransform();
+                       if (data.item.renderer == Renderer.Analog) {
+                               ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );
+                               ab.translate(-from, -ruler.max);
+//                             if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);
+                               data.draw(g, phase, false);
+                       }
+                       if (data.item.renderer == Renderer.Binary) {
+                               ab.scale( getWidth()/(end-from), 1/*height*/ );
+                               ab.translate(-from, 0);
+//                             if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab);
+                               data.draw(g, phase, false);
+                       }
+//             } catch (HistoryException e) {
+//                     e.printStackTrace();
+//             } catch (AccessorException e) {
+//                     e.printStackTrace();
+               } finally {
+                       g.setTransform(at);
+               }
+       }
+
+       /**
+        * Prepare item for draw.
+        *  
+        * @param data
+        * @param y
+        * @param height
+        */
+       public void prepareItem(ItemNode data, double y, double height) {
+               TrendNode tn = getTrend();
+               double from = tn.horizRuler.from;
+               double end = tn.horizRuler.end;
+               
+               VertRuler ruler = data.ruler;
+               try {
+                       double pixelsPerSecond = (end-from) / getWidth();
+                       
+                       AffineTransform ab = new AffineTransform();
+                       if (data.item.renderer == Renderer.Analog) {
+                               ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) );
+                               ab.translate(-from, -ruler.max);
+                               data.prepareLine(from, end, pixelsPerSecond, ab);
+                       }
+                       if (data.item.renderer == Renderer.Binary) {
+                               ab.scale( getWidth()/(end-from), 1/*height*/ );
+                               ab.translate(-from, 0);
+                               data.prepareLine(from, end, pixelsPerSecond, ab);
+                       }
+               } catch (HistoryException e) {
+                       e.printStackTrace();
+               } catch (AccessorException e) {
+                       e.printStackTrace();
+               }
+       }
+       
+       static {
+               
+               DIAMOND = new Path2D.Double();
+               DIAMOND.moveTo(0, -DIAMOND_SIZE*2);
+               DIAMOND.lineTo(DIAMOND_SIZE, -DIAMOND_SIZE);
+               DIAMOND.lineTo(0, 0);
+               DIAMOND.lineTo(-DIAMOND_SIZE, -DIAMOND_SIZE);
+               DIAMOND.lineTo(0, -DIAMOND_SIZE*2);
+
+               MILESTONE_FONT = new Font("Tahoma", 0, (int) (DIAMOND_SIZE*1.2) );
+               BASELINE_FONT = new Font("Tahoma", Font.BOLD, (int) (DIAMOND_SIZE*1.2) );
+               TOOLTIP_FONT = new Font("Tahoma", 0, 13 );
+       }
+
+       
+       ///  ValueTip
+       public static final AlphaComposite composite66 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .80f);
+       
+       void drawValuetip( Graphics2D g, double time ) throws HistoryException, BindingException {
+               TrendNode trend = getTrend();
+               Font font = TOOLTIP_FONT;
+               FontMetrics fm = g.getFontMetrics( font );
+
+               //double from = trend.horizRuler.from;
+               //double end = trend.horizRuler.end;
+               //double pixelsPerSecond = (end-from) / trend.plot.getWidth();
+               
+               double marginInPlot = VALUE_TIP_BOX_PLOT_MARGIN;
+               double marginOnFilm = VALUE_TIP_BOX_FILM_MARGIN;
+               double marginBetweenNamesAndValues = 16;
+               double quantumWidthOfValueArea = 20;
+               double marginBetweenLines = 5;
+               double textAreaHeight = 0;
+               double textAreaWidth = 0;
+               double valueAreaLeft = 0;
+               double valueAreaRight = 0;
+               double valueAreaWidth = 0;
+               
+               double x, y, w, h;
+               
+               List<TipLine> tipLines = new ArrayList<TipLine>( trend.allItems.size()+1 );
+               
+               /// Initialize
+               // Add time
+               {
+                       TipLine tl = new TipLine();
+                       tipLines.add(tl);
+                       tl.label = "Time";
+                       LineMetrics lm = fm.getLineMetrics(tl.label, g);
+                       tl.height = lm.getHeight();
+                       tl.labelBaseline = fm.getAscent();
+                       textAreaHeight += marginBetweenLines;
+                       textAreaHeight += tl.height; 
+                       tl.labelWidth = fm.stringWidth( tl.label );
+                       textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);                 
+                       tl.color = Color.WHITE;
+
+                       Format f = NumberFormat.getInstance();
+                       if (trend.timeFormat == org.simantics.trend.configuration.TimeFormat.Time) {                    
+                               f = new TimeFormat(trend.horizRuler.iEnd, 3);
+                       }
+                       double t = time - trend.horizRuler.basetime;
+                       String formattedTime = f.format( time - trend.horizRuler.basetime );
+                       double roundedTime = t;
+                       try {
+                               roundedTime = (Double) f.parseObject(formattedTime);
+                       } catch (ParseException e) {
+                               // Should never happen.
+                       }
+                       boolean actuallyLessThan = t < roundedTime;
+                       boolean actuallyMoreThan = t > roundedTime;
+                       tl.value = actuallyLessThan ? "< " + formattedTime
+                                       : actuallyMoreThan ? "> " + formattedTime
+                                       : formattedTime;
+                       tl.valueWidth = fm.stringWidth( tl.value );
+                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueWidth);
+               }
+               
+               // Add items
+               nextItem:
+               for ( ItemNode i : trend.allItems )
+               {
+                       TipLine tl = new TipLine();
+                       tipLines.add(tl);
+                       // Get Label
+                       tl.label = i.item.label;
+                       LineMetrics lm = fm.getLineMetrics(tl.label, g);
+                       tl.height = lm.getHeight();
+                       tl.labelBaseline = fm.getAscent();
+                       textAreaHeight += tl.height; 
+                       textAreaHeight += marginBetweenLines;
+                       tl.labelWidth = fm.stringWidth( tl.label );
+                       textAreaWidth = Math.max(tl.labelWidth, textAreaWidth);
+                       tl.color = ColorUtil.gamma( i.color, 0.55 );
+                       
+                       // Get Value
+                       Stream s = i.openStream( /*pixelsPerSecond*/0 );
+                       if ( s!=null ) {
+                               
+                               int index = s.binarySearch(Bindings.DOUBLE, time);
+                               if (index<0) index = -index-2;
+                               if ( index<0 || index>=s.count() ) continue nextItem;
+                               boolean isLast = index+1>=s.count();
+                               
+                               
+                               ValueBand vb = new ValueBand(s.sampleBinding);                          
+                               try {
+                                       vb.setSample( s.accessor.get(index, s.sampleBinding) );
+                               } catch (AccessorException e) {
+                                       throw new HistoryException(e);
+                               }
+                               
+                               if ( vb.getSample()!=null ) {
+
+                                       // gitlab #54: this can cause the value tip to not render values at all
+                                       // for items that have not been flushed at the point in time when this drawing
+                                       // is done. These circumstances are always temporary and later refreshes will
+                                       // remedy the situation. Having this logic can cause item values to randomly
+                                       // not be shown which is actually even more confusing to the user than rendering
+                                       // the latest flushed sample value.
+                                       // 
+                                       // For this reason, the if below has been commented out.
+                                       //if (isLast && vb.hasEndTime() && vb.getEndTimeDouble()<time) continue nextItem;
+                                                                               
+                                       if ( !vb.isNanSample() && !vb.isNullValue() ) {
+                                               Binding b = vb.getValueBinding();
+                                               if ( b instanceof NumberBinding) {
+                                                       double v = vb.getValueDouble();
+                                                       tl.value = trend.valueFormat.format.format( v );
+                                                       tl.number = true;
+                                                       
+                                                       int desimalPos = tl.value.indexOf('.');
+                                                       if (desimalPos<0) desimalPos = tl.value.indexOf(',');
+                                                       if ( desimalPos>=0 ) {
+                                                               String beforeDesimal = tl.value.substring(0, desimalPos);
+                                                               String afterDesimal = tl.value.substring(desimalPos, tl.value.length());
+                                                               tl.valueLeftWidth = fm.stringWidth(beforeDesimal);
+                                                               tl.valueRightWidth = fm.stringWidth(afterDesimal);
+                                                               tl.valueWidth = tl.valueLeftWidth + tl.valueRightWidth;
+                                                       } else {
+                                                               tl.valueWidth = tl.valueLeftWidth = fm.stringWidth(tl.value);
+                                                       }
+                                                       
+                                                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth+tl.valueRightWidth);
+                                                       valueAreaLeft = Math.max(valueAreaLeft, tl.valueLeftWidth);
+                                                       valueAreaRight = Math.max(valueAreaRight, tl.valueRightWidth);
+                                               } else {
+                                                       Object v = vb.getValue();
+                                                       tl.value = b.toString(v);
+                                                       tl.number = false;
+                                                       tl.valueLeftWidth = tl.valueRightWidth = fm.stringWidth( tl.value );
+                                                       valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth);
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               // Layout
+               double halfQuantum = quantumWidthOfValueArea/2;
+               valueAreaWidth = Math.ceil( valueAreaWidth / quantumWidthOfValueArea ) * quantumWidthOfValueArea;
+               valueAreaLeft = Math.ceil( valueAreaLeft / halfQuantum ) * halfQuantum;
+               valueAreaRight = Math.ceil( valueAreaRight / halfQuantum ) * halfQuantum;
+               double finalValueAreaWidth = Math.max(valueAreaWidth, valueAreaLeft + valueAreaRight);
+               w = marginOnFilm + textAreaWidth + marginBetweenNamesAndValues + finalValueAreaWidth + marginOnFilm + 0;
+               h = marginOnFilm + textAreaHeight + marginOnFilm;
+               double maxX = trend.plot.getWidth() - marginInPlot - w;
+               double maxY = trend.plot.getHeight() - marginInPlot - h;
+               x = marginInPlot + (maxX - marginInPlot)*trend.spec.viewProfile.valueViewPositionX;
+               y = marginInPlot + (maxY - marginInPlot)*trend.spec.viewProfile.valueViewPositionY;
+
+               if ( x < TrendLayout.VERT_MARGIN ) x = TrendLayout.VERT_MARGIN;
+
+               valueTipBoxBounds.setFrame(x, y, w, h);
+               //System.out.println("value tip bounds: " + valueTipBounds);
+
+               // Draw
+               Rectangle2D rect = new Rectangle2D.Double(0, 0, w, h);
+               Composite oldComposite = g.getComposite();
+               AffineTransform oldTransform = g.getTransform();
+               try {                   
+                       g.setComposite(composite66);
+                       g.translate(x, y);
+                       g.setColor(Color.BLACK);
+                       g.fill( rect );
+                       g.setFont( font );
+                       g.setComposite(oldComposite);
+                       
+                       y = marginInPlot;
+                       for (TipLine tl : tipLines) {
+                               g.setColor( tl.color );
+                               x = marginInPlot;
+                               g.drawString( tl.label, (float)x, (float)(y+tl.labelBaseline));
+
+                               if ( tl.value!=null ) {
+                                       x = marginInPlot + textAreaWidth + marginBetweenNamesAndValues;
+                                       if ( tl.number ) {
+                                               x += valueAreaLeft - tl.valueLeftWidth;
+                                       } else {
+                                               x += (finalValueAreaWidth - tl.valueWidth)/2;
+                                       }
+                                       g.drawString(tl.value, (float) x, (float) (y+tl.labelBaseline));
+                               }
+                               
+                               y+=tl.height;
+                               y+=marginBetweenLines;
+                       }
+                       
+               } finally {
+                       g.setTransform( oldTransform );
+                       g.setComposite( oldComposite );
+               }
+       }
+
+       static class TipLine {
+               String label, value;
+               Color color;
+               double labelWidth, height, valueLeftWidth, valueRightWidth, valueWidth, labelBaseline;
+               boolean number;
+
+               @Override
+               public String toString() {
+                       return "TipLine[label=" + label + ", value=" + value + ", color=" + color + ", labelWidth=" + labelWidth
+                                       + ", height=" + height + ", valueLeftWidth=" + valueLeftWidth + ", valueRightWidth="
+                                       + valueRightWidth + ", valueWidth=" + valueWidth + ", labelBaseline=" + labelBaseline + ", number="
+                                       + number + "]";
+               }
+       }
+
+       public void renderValueTip(Graphics2D g2d) {
+               TrendNode trend = getTrend();
+               if ( trend.valueTipTime != null ) {
+                       AffineTransform at = g2d.getTransform();                                
+                       try {
+                               g2d.transform( getTransform() );
+                               drawValuetip( g2d, trend.valueTipTime );
+                       } catch (HistoryException e) {
+                               e.printStackTrace();
+                       } catch (BindingException e) {
+                               e.printStackTrace();
+                       } finally {
+                               g2d.setTransform( at );
+                       }                       
+               }
+       }
+       
+       /**
+        * Pick item (Binary node)
+        * 
+        * @param pt coordinate in trend coordinate system
+        * @return item node
+        */
+       public ItemNode pickItem(Point2D pt)
+       {
+               TrendNode trend = getTrend();
+               double y = pt.getY()-getY();
+               double x = pt.getX()-getX();
+               if (y<analogAreaHeight || y>analogAreaHeight+binaryAreaHeight) return null;
+               if (x<0 || x+getX()>trend.getBounds().getWidth()) return null;
+               for (int i=0; i<trend.binaryItems.size(); i++) {
+                       double sy = analogAreaHeight + i*BINARY[3];
+                       double ey = analogAreaHeight + (i+1)*BINARY[3];
+                       if ( y>=sy && y<ey ) return trend.binaryItems.get(i);
+               }
+               return null;
+       }
+               
+}