changeset 9186:eec4df8165a1

Implemented 'ShowLineLabel' for area themes.
author gernotbelger
date Thu, 28 Jun 2018 10:47:04 +0200
parents 4778333ea2cd
children b3c11c5fdbd5
files artifacts/src/main/java/org/dive4elements/river/exports/AbstractChartGenerator.java artifacts/src/main/java/org/dive4elements/river/jfree/EnhancedLineAndShapeRenderer.java artifacts/src/main/java/org/dive4elements/river/jfree/StableXYDifferenceRenderer.java artifacts/src/main/java/org/dive4elements/river/jfree/StyledAreaSeriesCollection.java
diffstat 4 files changed, 304 insertions(+), 277 deletions(-) [+]
line wrap: on
line diff
--- a/artifacts/src/main/java/org/dive4elements/river/exports/AbstractChartGenerator.java	Thu Jun 28 10:47:00 2018 +0200
+++ b/artifacts/src/main/java/org/dive4elements/river/exports/AbstractChartGenerator.java	Thu Jun 28 10:47:04 2018 +0200
@@ -791,11 +791,10 @@
      *            as such.
      */
     private void applyThemes(final XYPlot plot, final XYDataset series, final int idx, final boolean isArea) {
-        if (isArea) {
+        if (isArea)
             applyAreaTheme(plot, (StyledAreaSeriesCollection) series, idx);
-        } else {
+        else
             applyLineTheme(plot, series, idx);
-        }
     }
 
     /**
--- a/artifacts/src/main/java/org/dive4elements/river/jfree/EnhancedLineAndShapeRenderer.java	Thu Jun 28 10:47:00 2018 +0200
+++ b/artifacts/src/main/java/org/dive4elements/river/jfree/EnhancedLineAndShapeRenderer.java	Thu Jun 28 10:47:04 2018 +0200
@@ -45,8 +45,7 @@
     private static final long serialVersionUID = 1L;
 
     /** Own log. */
-    private static final Logger log =
-        Logger.getLogger(EnhancedLineAndShapeRenderer.class);
+    private static final Logger log = Logger.getLogger(EnhancedLineAndShapeRenderer.class);
 
     protected BooleanList isMinimumShapeVisible;
     protected BooleanList isMaximumShapeVisible;
@@ -61,95 +60,85 @@
     protected BooleanList showLineLabelBG;
     protected Map<Integer, Color> lineLabelBGColors;
 
-
-    public EnhancedLineAndShapeRenderer(boolean lines, boolean shapes) {
+    public EnhancedLineAndShapeRenderer(final boolean lines, final boolean shapes) {
         super(lines, shapes);
         this.isMinimumShapeVisible = new BooleanList();
         this.isMaximumShapeVisible = new BooleanList();
-        this.showLineLabel         = new BooleanList();
-        this.showLineLabelBG       = new BooleanList();
-        this.seriesMinimum         = new HashMap<Integer, Double>();
-        this.seriesMaximum         = new HashMap<Integer, Double>();
-        this.seriesMinimumX        = new HashMap<Integer, Double>();
-        this.lineLabelFonts        = new HashMap<Integer, Font>();
-        this.lineLabelTextColors   = new HashMap<Integer, Color>();
-        this.lineLabelBGColors     = new HashMap<Integer, Color>();
+        this.showLineLabel = new BooleanList();
+        this.showLineLabelBG = new BooleanList();
+        this.seriesMinimum = new HashMap<>();
+        this.seriesMaximum = new HashMap<>();
+        this.seriesMinimumX = new HashMap<>();
+        this.lineLabelFonts = new HashMap<>();
+        this.lineLabelTextColors = new HashMap<>();
+        this.lineLabelBGColors = new HashMap<>();
     }
 
-
     /**
      * Draw a background-box of a text to render.
-     * @param g2 graphics device to use
-     * @param text text to draw
-     * @param textX x-position for text
-     * @param textY y-position for text
-     * @param bgColor color to fill box with.
+     *
+     * @param g2
+     *            graphics device to use
+     * @param text
+     *            text to draw
+     * @param textX
+     *            x-position for text
+     * @param textY
+     *            y-position for text
+     * @param bgColor
+     *            color to fill box with.
      */
-    public static void drawTextBox(Graphics2D g2,
-        String text, float textX, float textY, Color bgColor
-    ) {
-        Rectangle2D hotspotBox = g2.getFontMetrics().getStringBounds(text, g2);
-        float w = (float)hotspotBox.getWidth();
-        float h = (float)hotspotBox.getHeight();
-        hotspotBox.setRect(textX, textY-h, w, h);
-        Color oldColor = g2.getColor();
+    public static void drawTextBox(final Graphics2D g2, final String text, final float textX, final float textY, final Color bgColor) {
+        final Rectangle2D hotspotBox = g2.getFontMetrics().getStringBounds(text, g2);
+        final float w = (float) hotspotBox.getWidth();
+        final float h = (float) hotspotBox.getHeight();
+        hotspotBox.setRect(textX, textY - h, w, h);
+        final Color oldColor = g2.getColor();
         g2.setColor(bgColor);
         g2.fill(hotspotBox);
         g2.setColor(oldColor);
     }
 
-
     /**
      * Whether or not a specific item in a series (maybe the maxima) should
      * be rendered with shape.
      */
-    public boolean getItemShapeVisible(
-        XYDataset dataset,
-        int series,
-        int item
-    ){
+    private boolean getItemShapeVisible(final XYDataset dataset, final int series, final int item) {
         if (super.getItemShapeVisible(series, item)) {
             return true;
         }
 
-        if (isMinimumShapeVisible(series)
-            && isMinimum(dataset, series, item)
-        ) {
+        if (isMinimumShapeVisible(series) && isMinimum(dataset, series, item)) {
             return true;
         }
 
-        if (isMaximumShapeVisible(series)
-            && isMaximum(dataset, series, item)
-        ) {
+        if (isMaximumShapeVisible(series) && isMaximum(dataset, series, item)) {
             return true;
         }
 
         return false;
     }
 
-
     /**
      * Rectangle used to draw maximums shape.
      */
-    public Shape getMaximumShape(int series, int column) {
+    private Shape getMaximumShape(final int series, final int column) {
         return new Rectangle2D.Double(-5d, -5d, 10d, 10d);
     }
 
-
     /**
      * Rectangle used to draw minimums shape.
      */
-    public Shape getMinimumShape(int series, int column) {
+    private Shape getMinimumShape(final int series, final int column) {
         return new Rectangle2D.Double(-5d, -5d, 10d, 10d);
     }
 
-
     /** Get fill paint for the maximum indicators. */
-    public Paint getMaximumFillPaint(int series, int column) {
-        Paint p = getItemPaint(series, column);
+    private Paint getMaximumFillPaint(final int series, final int column) {
+        final Paint p = getItemPaint(series, column);
 
         if (p instanceof Color) {
-            Color c = (Color) p;
+            final Color c = (Color) p;
             Color b = c;
 
             for (int i = 0; i < 2; i++) {
@@ -163,13 +152,12 @@
         return p;
     }
 
-
     /** Get fill paint for the minimum indicators. */
-    public Paint getMinimumFillPaint(int series, int column) {
-        Paint p = getItemPaint(series, column);
+    private Paint getMinimumFillPaint(final int series, final int column) {
+        final Paint p = getItemPaint(series, column);
 
         if (p instanceof Color) {
-            Color c = (Color) p;
+            final Color c = (Color) p;
             Color b = c;
 
             for (int i = 0; i < 2; i++) {
@@ -183,82 +171,63 @@
         return p;
     }
 
-
     /**
      * Overrides XYLineAndShapeRenderer.drawSecondaryPass() to call an adapted
      * method getItemShapeVisible() which now takes an XYDataset. So, 99% of
      * code equal the code in XYLineAndShapeRenderer.
      */
     @Override
-    protected void drawSecondaryPass(
-        Graphics2D       g2,
-        XYPlot           plot,
-        XYDataset        dataset,
-        int              pass,
-        int              series,
-        int              item,
-        ValueAxis        domainAxis,
-        Rectangle2D      dataArea,
-        ValueAxis        rangeAxis,
-        CrosshairState   crosshairState,
-        EntityCollection entities
-    ) {
+    protected void drawSecondaryPass(final Graphics2D g2, final XYPlot plot, final XYDataset dataset, final int pass, final int series, final int item,
+            final ValueAxis domainAxis, final Rectangle2D dataArea, final ValueAxis rangeAxis, final CrosshairState crosshairState,
+            final EntityCollection entities) {
         Shape entityArea = null;
 
         // get the data point...
-        double x1 = dataset.getXValue(series, item);
-        double y1 = dataset.getYValue(series, item);
+        final double x1 = dataset.getXValue(series, item);
+        final double y1 = dataset.getYValue(series, item);
         if (Double.isNaN(y1) || Double.isNaN(x1)) {
             return;
         }
 
-        PlotOrientation orientation = plot.getOrientation();
-        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
-        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
-        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
-        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
+        final PlotOrientation orientation = plot.getOrientation();
+        final RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
+        final RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
+        final double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
+        final double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
 
         if (getItemShapeVisible(dataset, series, item)) {
             Shape shape = null;
 
             // OPTIMIZE: instead of calculating minimum and maximum for every
-            //           point, calculate it just once (assume that dataset
-            //           content does not change during rendering).
-            // NOTE:     Above OPTIMIZE might already be fulfilled to
-            //           most extend.
-            boolean isMinimum = isMinimumShapeVisible(series)
-                && isMinimum(dataset, series, item);
+            // point, calculate it just once (assume that dataset
+            // content does not change during rendering).
+            // NOTE: Above OPTIMIZE might already be fulfilled to
+            // most extend.
+            final boolean isMinimum = isMinimumShapeVisible(series) && isMinimum(dataset, series, item);
 
-            boolean isMaximum = isMaximumShapeVisible(series)
-                && isMaximum(dataset, series, item);
+            final boolean isMaximum = isMaximumShapeVisible(series) && isMaximum(dataset, series, item);
 
             if (isMinimum) {
                 log.debug("Create a Minimum shape.");
                 shape = getMinimumShape(series, item);
-            }
-            else if (isMaximum) {
+            } else if (isMaximum) {
                 log.debug("Create a Maximum shape.");
                 shape = getMaximumShape(series, item);
-            }
-            else {
+            } else {
                 shape = getItemShape(series, item);
             }
 
             if (orientation == PlotOrientation.HORIZONTAL) {
-                shape = ShapeUtilities.createTranslatedShape(shape, transY1,
-                        transX1);
-            }
-            else if (orientation == PlotOrientation.VERTICAL) {
-                shape = ShapeUtilities.createTranslatedShape(shape, transX1,
-                        transY1);
+                shape = ShapeUtilities.createTranslatedShape(shape, transY1, transX1);
+            } else if (orientation == PlotOrientation.VERTICAL) {
+                shape = ShapeUtilities.createTranslatedShape(shape, transX1, transY1);
             }
             entityArea = shape;
             if (shape.intersects(dataArea)) {
                 if (getItemShapeFilled(series, item)) {
                     if (getUseFillPaint()) {
                         g2.setPaint(getItemFillPaint(series, item));
-                    }
-                    else {
+                    } else {
                         g2.setPaint(getItemPaint(series, item));
                     }
                     g2.fill(shape);
@@ -266,8 +235,7 @@
                 if (getDrawOutlines()) {
                     if (getUseOutlinePaint()) {
                         g2.setPaint(getItemOutlinePaint(series, item));
-                    }
-                    else {
+                    } else {
                         g2.setPaint(getItemPaint(series, item));
                     }
                     g2.setStroke(getItemOutlineStroke(series, item));
@@ -280,8 +248,7 @@
                     g2.setPaint(getItemOutlinePaint(series, item));
                     g2.setStroke(getItemOutlineStroke(series, item));
                     g2.draw(shape);
-                }
-                else if (isMaximum) {
+                } else if (isMaximum) {
                     g2.setPaint(getMaximumFillPaint(series, item));
                     g2.fill(shape);
                     g2.setPaint(getItemOutlinePaint(series, item));
@@ -299,78 +266,27 @@
         }
 
         // Draw the item label if there is one...
-        if (isItemLabelVisible(series, item)) {
-            drawItemLabel(g2, orientation, dataset, series, item, xx, yy,
-                    (y1 < 0.0));
-        }
+        if (isItemLabelVisible(series, item))
+            drawItemLabel(g2, orientation, dataset, series, item, xx, yy, (y1 < 0.0));
 
         // Draw label of line.
-        if (dataset instanceof XYSeriesCollection
-            && isShowLineLabel(series)
-            && isMinimumX (dataset, series, item)
-            ) {
-            XYSeries xYSeries = ((XYSeriesCollection)dataset)
-                .getSeries(series);
-            String waterlevelLabel = (xYSeries instanceof HasLabel)
-                ? ((HasLabel)xYSeries).getLabel()
-                : xYSeries.getKey().toString();
+        if (dataset instanceof XYSeriesCollection && isShowLineLabel(series) && isMinimumX(dataset, series, item)) {
+            final XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(series);
+            final String waterlevelLabel = (xYSeries instanceof HasLabel) ? ((HasLabel) xYSeries).getLabel() : xYSeries.getKey().toString();
             // TODO Force water of some German rivers to flow
             // direction mountains.
 
-            Font oldFont = g2.getFont();
-
-            Color oldColor = g2.getColor();
-            g2.setFont(this.getLineLabelFont(series));
-            g2.setColor(this.getLineLabelTextColor(series));
-            g2.setBackground(Color.black);
-
-            // Try to always display label if the data is visible.
-            if (!isPointInRect(dataArea, xx, yy)) {
-                // Move into the data area.
-                xx = Math.max(xx, dataArea.getMinX());
-                xx = Math.min(xx, dataArea.getMaxX());
-                yy = Math.max(yy, dataArea.getMinY());
-                yy = Math.min(yy, dataArea.getMaxY());
-            }
+            final Font labelFont = this.getLineLabelFont(series);
+            final Color labelColor = this.getLineLabelTextColor(series);
+            final boolean showBG = isShowLineLabelBG(series);
+            final Color bgColor = getLineLabelBGColor(series);
 
-            // Move to right until no collisions exist anymore
-            Shape hotspot = TextUtilities.calculateRotatedStringBounds(
-                waterlevelLabel, g2, (float)xx, (float)yy-3f,
-                TextAnchor.CENTER_LEFT,
-                0f, TextAnchor.CENTER_LEFT);
-            while (JFreeUtil.collides(hotspot, entities,
-                                      CollisionFreeLineLabelEntity.class)) {
-                xx += 5f;
-                hotspot = TextUtilities.calculateRotatedStringBounds(
-                    waterlevelLabel,
-                    g2,
-                    (float)xx,
-                    (float)yy-3f,
-                    TextAnchor.CENTER_LEFT,
-                    0f,
-                    TextAnchor.CENTER_LEFT);
-            }
-
-            // Register to avoid collissions.
-            entities.add(new CollisionFreeLineLabelEntity(hotspot,
-                1, "", ""));
-
-            // Fill background.
-            if (isShowLineLabelBG(series)) {
-                drawTextBox(g2, waterlevelLabel, (float)xx, (float)yy-3f,
-                    getLineLabelBGColor(series));
-            }
-
-            g2.drawString(waterlevelLabel, (float)xx, (float)yy-3f);
-
-            g2.setFont(oldFont);
-            g2.setColor(oldColor);
+            drawLineLabel(g2, dataArea, entities, xx, yy, labelFont, labelColor, showBG, bgColor, waterlevelLabel);
         }
 
-        int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
-        int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
-        updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
-                rangeAxisIndex, transX1, transY1, orientation);
+        final int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
+        final int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
+        updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, rangeAxisIndex, transX1, transY1, orientation);
 
         // Add an entity for the item, but only if it falls within the data
         // area...
@@ -379,77 +295,115 @@
         }
     }
 
+    public static void drawLineLabel(final Graphics2D g2, final Rectangle2D dataArea, final EntityCollection entities, final double labelX, final double labelY,
+            final Font font,
+            final Color fgColor,
+            final boolean showBG, final Color bgColor, final String label) {
+
+        final Font oldFont = g2.getFont();
+        final Color oldColor = g2.getColor();
+
+        g2.setFont(font);
+        g2.setColor(fgColor);
+        g2.setBackground(Color.black);
+
+        // Try to always display label if the data is visible.
+        double posX = labelX;
+        double posY = labelY;
+        if (!isPointInRect(dataArea, posX, posY)) {
+            // Move into the data area.
+            posX = Math.max(posX, dataArea.getMinX());
+            posX = Math.min(posX, dataArea.getMaxX());
+            posY = Math.max(posY, dataArea.getMinY());
+            posY = Math.min(posY, dataArea.getMaxY());
+        }
+
+        // Move to right until no collisions exist anymore
+        Shape hotspot = TextUtilities.calculateRotatedStringBounds(label, g2, (float) posX, (float) posY - 3f, TextAnchor.CENTER_LEFT, 0f,
+                TextAnchor.CENTER_LEFT);
+        while (JFreeUtil.collides(hotspot, entities, CollisionFreeLineLabelEntity.class)) {
+            posX += 5f;
+            hotspot = TextUtilities.calculateRotatedStringBounds(label, g2, (float) posX, (float) posY - 3f, TextAnchor.CENTER_LEFT, 0f,
+                    TextAnchor.CENTER_LEFT);
+        }
+
+        // Register to avoid collissions.
+        entities.add(new CollisionFreeLineLabelEntity(hotspot, 1, "", ""));
+
+        // Fill background.
+        if (showBG)
+            drawTextBox(g2, label, (float) posX, (float) posY - 3f, bgColor);
+
+        g2.drawString(label, (float) posX, (float) posY - 3f);
+
+        g2.setFont(oldFont);
+        g2.setColor(oldColor);
+    }
 
     /**
      * Sets whether or not the minimum should be rendered with shape.
      */
-    public void setIsMinimumShapeVisisble(int series, boolean isVisible) {
+    public void setIsMinimumShapeVisisble(final int series, final boolean isVisible) {
         this.isMinimumShapeVisible.setBoolean(series, isVisible);
     }
 
-
     /**
      * Whether or not the minimum should be rendered with shape.
      */
-    public boolean isMinimumShapeVisible(int series) {
+    private boolean isMinimumShapeVisible(final int series) {
         if (this.isMinimumShapeVisible.size() <= series) {
             return false;
         }
 
-        return isMinimumShapeVisible.getBoolean(series);
+        return this.isMinimumShapeVisible.getBoolean(series);
     }
 
-
     /**
      * Sets whether or not the maximum should be rendered with shape.
      */
-    public void setIsMaximumShapeVisible(int series, boolean isVisible) {
+    public void setIsMaximumShapeVisible(final int series, final boolean isVisible) {
         this.isMaximumShapeVisible.setBoolean(series, isVisible);
     }
 
-
     /**
      * Whether or not the maximum should be rendered with shape.
      */
-    public boolean isMaximumShapeVisible(int series) {
+    private boolean isMaximumShapeVisible(final int series) {
         if (this.isMaximumShapeVisible.size() <= series) {
             return false;
         }
 
-        return isMaximumShapeVisible.getBoolean(series);
+        return this.isMaximumShapeVisible.getBoolean(series);
     }
 
     /** Whether or not a label should be shown for series. */
-    public boolean isShowLineLabel(int series) {
+    private boolean isShowLineLabel(final int series) {
         if (this.showLineLabel.size() <= series) {
             return false;
         }
 
-        return showLineLabel.getBoolean(series);
+        return this.showLineLabel.getBoolean(series);
     }
 
-
     /** Sets whether or not a label should be shown for series. */
-    public void setShowLineLabel(boolean showLineLabel, int series) {
+    public void setShowLineLabel(final boolean showLineLabel, final int series) {
         this.showLineLabel.setBoolean(series, showLineLabel);
     }
 
-
     /** Whether or not a label should be shown for series. */
-    public boolean isShowLineLabelBG(int series) {
+    private boolean isShowLineLabelBG(final int series) {
         if (this.showLineLabelBG.size() <= series) {
             return false;
         }
 
-        return showLineLabelBG.getBoolean(series);
+        return this.showLineLabelBG.getBoolean(series);
     }
 
-
-    public void setShowLineLabelBG(int series, boolean doShow) {
+    public void setShowLineLabelBG(final int series, final boolean doShow) {
         this.showLineLabelBG.setBoolean(series, doShow);
     }
 
-    public Color getLineLabelBGColor(int series) {
+    private Color getLineLabelBGColor(final int series) {
         if (this.lineLabelBGColors.size() <= series) {
             return null;
         }
@@ -457,11 +411,11 @@
         return this.lineLabelBGColors.get(series);
     }
 
-    public void setLineLabelBGColor(int series, Color color) {
+    public void setLineLabelBGColor(final int series, final Color color) {
         this.lineLabelBGColors.put(series, color);
     }
 
-    public Color getLineLabelTextColor(int series) {
+    private Color getLineLabelTextColor(final int series) {
         if (this.lineLabelTextColors.size() <= series) {
             return null;
         }
@@ -469,35 +423,33 @@
         return this.lineLabelTextColors.get(series);
     }
 
-    public void setLineLabelTextColor(int series, Color color) {
+    public void setLineLabelTextColor(final int series, final Color color) {
         this.lineLabelTextColors.put(series, color);
     }
 
-    public void setLineLabelFont(Font font, int series) {
+    public void setLineLabelFont(final Font font, final int series) {
         this.lineLabelFonts.put(series, font);
     }
 
-    public Font getLineLabelFont(int series) {
+    private Font getLineLabelFont(final int series) {
         return this.lineLabelFonts.get(series);
     }
 
-
     /**
      * True if the given item of given dataset has the smallest
      * X value within this set.
      */
-    public boolean isMinimumX(XYDataset dataset, int series, int item) {
+    private boolean isMinimumX(final XYDataset dataset, final int series, final int item) {
         return dataset.getXValue(series, item) == getMinimumX(dataset, series);
     }
 
-
     /**
      * Get Minimum X Value of a given series in a dataset.
      * The value is stored for later use if queried the first time.
      */
-    public double getMinimumX(XYDataset dataset, int series) {
-        Integer key = Integer.valueOf(series);
-        Double  old = seriesMinimumX.get(key);
+    private double getMinimumX(final XYDataset dataset, final int series) {
+        final Integer key = Integer.valueOf(series);
+        final Double old = this.seriesMinimumX.get(key);
 
         if (old != null) {
             return old.doubleValue();
@@ -508,35 +460,33 @@
         double min = Double.MAX_VALUE;
 
         for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
-            double tmpValue = dataset.getXValue(series, i);
+            final double tmpValue = dataset.getXValue(series, i);
 
             if (tmpValue < min) {
                 min = tmpValue;
             }
         }
 
-        seriesMinimumX.put(key, Double.valueOf(min));
+        this.seriesMinimumX.put(key, Double.valueOf(min));
 
         return min;
     }
 
-
     /**
      * True if the given item of given dataset has the smallest
      * Y value within this set.
      */
-    public boolean isMinimum(XYDataset dataset, int series, int item) {
+    private boolean isMinimum(final XYDataset dataset, final int series, final int item) {
         return dataset.getYValue(series, item) == getMinimum(dataset, series);
     }
 
-
     /**
      * Get Minimum Y Value of a given series in a dataset.
      * The value is stored for later use if queried the first time.
      */
-    public double getMinimum(XYDataset dataset, int series) {
-        Integer key = Integer.valueOf(series);
-        Double  old = seriesMinimum.get(key);
+    private double getMinimum(final XYDataset dataset, final int series) {
+        final Integer key = Integer.valueOf(series);
+        final Double old = this.seriesMinimum.get(key);
 
         if (old != null) {
             return old.doubleValue();
@@ -547,35 +497,33 @@
         double min = Double.MAX_VALUE;
 
         for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
-            double tmpValue = dataset.getYValue(series, i);
+            final double tmpValue = dataset.getYValue(series, i);
 
             if (tmpValue < min) {
                 min = tmpValue;
             }
         }
 
-        seriesMinimum.put(key, Double.valueOf(min));
+        this.seriesMinimum.put(key, Double.valueOf(min));
 
         return min;
     }
 
-
     /**
      * True if the given item of given dataset has the biggest
      * Y value within this set.
      */
-    public boolean isMaximum(XYDataset dataset, int series, int item) {
+    private boolean isMaximum(final XYDataset dataset, final int series, final int item) {
         return dataset.getYValue(series, item) == getMaximum(dataset, series);
     }
 
-
     /**
      * Get maximum Y Value of a given series in a dataset.
      * The value is stored for later use if queried the first time.
      */
-    public double getMaximum(XYDataset dataset, int series) {
-        Integer key = Integer.valueOf(series);
-        Double  old = seriesMaximum.get(key);
+    private double getMaximum(final XYDataset dataset, final int series) {
+        final Integer key = Integer.valueOf(series);
+        final Double old = this.seriesMaximum.get(key);
 
         if (old != null) {
             return old.doubleValue();
@@ -586,16 +534,15 @@
         double max = -Double.MAX_VALUE;
 
         for (int i = 0, n = dataset.getItemCount(series); i < n; i++) {
-            double tmpValue = dataset.getYValue(series, i);
+            final double tmpValue = dataset.getYValue(series, i);
 
             if (tmpValue > max) {
                 max = tmpValue;
             }
         }
 
-        seriesMaximum.put(key, Double.valueOf(max));
+        this.seriesMaximum.put(key, Double.valueOf(max));
 
         return max;
     }
-}
-// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/jfree/StableXYDifferenceRenderer.java	Thu Jun 28 10:47:00 2018 +0200
+++ b/artifacts/src/main/java/org/dive4elements/river/jfree/StableXYDifferenceRenderer.java	Thu Jun 28 10:47:04 2018 +0200
@@ -122,6 +122,8 @@
 import org.jfree.chart.urls.XYURLGenerator;
 import org.jfree.data.xy.DefaultXYDataset;
 import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
 import org.jfree.io.SerialUtilities;
 import org.jfree.ui.RectangleEdge;
 import org.jfree.util.PaintUtilities;
@@ -176,6 +178,15 @@
 
     private final boolean drawOriginalSeries;
 
+    /** NumberFormat to use for area. */
+    private NumberFormat areaLabelNumberFormat;
+
+    private int areaCalculationMode;
+
+    private double positiveArea;
+
+    private double negativeArea;
+
     /** The color of the label showing the calculated area. */
     private Color labelColor;
 
@@ -185,21 +196,15 @@
     /** Font to draw label of calculated area with. */
     private Font labelFont;
 
+    /** Whether or not to draw a label that shows the title of the theme. */
+    private boolean drawTitleLabel = false;
+
+    /** Whether or not to draw a label that shows the area of the polygon. */
+    private boolean drawAreaLabel = false;
+
     /** Template to create i18ned label for area. */
     private String areaLabelTamplate;
 
-    /** NumberFormat to use for area. */
-    private NumberFormat areaLabelNumberFormat;
-
-    private int areaCalculationMode;
-
-    private double positiveArea;
-
-    private double negativeArea;
-
-    /** Whether or not to draw a label in the area. */
-    private boolean labelArea = true;
-
     /** Arithmetic centroid of drawn polygons. */
     private Point2D.Double centroid;
 
@@ -217,6 +222,12 @@
      */
     private final boolean roundXCoordinates;
 
+    /** Holds the minimal x value in screen coordinates, will updated during the draw operation */
+    private transient double minimumScreenX = Double.POSITIVE_INFINITY;
+
+    /** Holds the y value in screen coordinates at the minimum x value **/
+    private transient double minimumScreenX_Y = Double.POSITIVE_INFINITY;
+
     /**
      * Creates a new renderer with default attributes.
      */
@@ -270,8 +281,12 @@
         this.areaLabelNumberFormat = nf;
     }
 
-    public void setLabelArea(final boolean label) {
-        this.labelArea = label;
+    public void setShowAreaLabel(final boolean doDrawAreaLabel) {
+        this.drawAreaLabel = doDrawAreaLabel;
+    }
+
+    public void setShowTitleLabel(final boolean doDrawTitleLabel) {
+        this.drawTitleLabel = doDrawTitleLabel;
     }
 
     /** Set font to paint label with. */
@@ -811,6 +826,7 @@
     public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot,
             final ValueAxis domainAxis, final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item,
             final CrosshairState crosshairState, final int pass) {
+
         switch (pass) {
         case 0:
             for (final XYDataset ds : splitByNaNs(dataset)) {
@@ -819,38 +835,7 @@
             break;
         case 1:
             drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);
-        }
-
-        // Find geometric middle, calculate area and paint
-        // a string with it here.
-        if (pass == 1 && this.labelArea && this.areaLabelNumberFormat != null && this.areaLabelTamplate != null) {
-            double center_x = this.centroid.getX();
-            double center_y = this.centroid.getY();
-            center_x = domainAxis.valueToJava2D(center_x, dataArea, plot.getDomainAxisEdge());
-            center_y = rangeAxis.valueToJava2D(center_y, dataArea, plot.getRangeAxisEdge());
-
-            // Respect text-extend if text should appear really centered.
-
-            float area = 0f;
-            if (this.areaCalculationMode == CALCULATE_POSITIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA) {
-                area += Math.abs(this.positiveArea);
-            }
-            if (this.areaCalculationMode == CALCULATE_NEGATIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA) {
-                area += Math.abs(this.negativeArea);
-            }
-            if (area != 0f) {
-                final Color oldColor = g2.getColor();
-                final Font oldFont = g2.getFont();
-                g2.setFont(this.labelFont);
-                final String labelText = String.format(this.areaLabelTamplate, this.areaLabelNumberFormat.format(area));
-                if (this.labelBGColor != null) {
-                    EnhancedLineAndShapeRenderer.drawTextBox(g2, labelText, (float) center_x, (float) center_y, this.labelBGColor);
-                }
-                g2.setColor(this.labelColor);
-                g2.drawString(labelText, (float) center_x, (float) center_y);
-                g2.setFont(oldFont);
-                g2.setColor(oldColor);
-            }
+            break;
         }
     }
 
@@ -884,21 +869,18 @@
             final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
             final CrosshairState x_crosshairState) {
 
-        if (!((0 == x_series) && (0 == x_item))) {
+        if (x_series != 0 || x_item != 0)
             return;
-        }
 
         final boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount());
 
         // check if either series is a degenerate case (i.e. less than 2 points)
-        if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) {
+        if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend))
             return;
-        }
 
         // check if series are disjoint (i.e. domain-spans do not overlap)
-        if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) {
+        if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset))
             return;
-        }
 
         // polygon definitions
         final List<Double> l_minuendXs = new LinkedList<>();
@@ -1268,6 +1250,66 @@
         createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
     }
 
+    private void drawItemPass1(final Graphics2D g2, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot, final ValueAxis domainAxis,
+            final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item, final CrosshairState crosshairState) {
+
+        doDrawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);
+
+        final int lastSeries = dataset.getSeriesCount() - 1;
+        final int lastItem = dataset.getItemCount(series) - 1;
+        if (series == lastSeries && item == lastItem) {
+            // draw labels: only once per theme!
+            drawAreaLabel(g2, dataArea, plot, domainAxis, rangeAxis);
+            drawTitleLabel(g2, dataArea, info.getOwner().getEntityCollection(), dataset);
+        }
+    }
+
+    private void drawAreaLabel(final Graphics2D g2, final Rectangle2D dataArea, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis) {
+
+        if (!this.drawAreaLabel)
+            return;
+
+        if (this.areaLabelNumberFormat == null || this.areaLabelTamplate == null || this.centroid == null)
+            return;
+
+        // Respect text-extend if text should appear really centered.
+        float area = 0f;
+        if (this.areaCalculationMode == CALCULATE_POSITIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
+            area += Math.abs(this.positiveArea);
+        if (this.areaCalculationMode == CALCULATE_NEGATIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
+            area += Math.abs(this.negativeArea);
+
+        if (area != 0f) {
+
+            final String labelText = String.format(this.areaLabelTamplate, this.areaLabelNumberFormat.format(area));
+
+            final double center_x = this.centroid.getX();
+            final double center_y = this.centroid.getY();
+
+            final double screenX = domainAxis.valueToJava2D(center_x, dataArea, plot.getDomainAxisEdge());
+            final double screenY = rangeAxis.valueToJava2D(center_y, dataArea, plot.getRangeAxisEdge());
+
+            drawLabel(g2, labelText, screenX, screenY);
+        }
+    }
+
+    private void drawTitleLabel(final Graphics2D g2, final Rectangle2D dataArea, final EntityCollection entities, final XYDataset dataset) {
+
+        if (!this.drawTitleLabel)
+            return;
+
+        if (Double.isInfinite(this.minimumScreenX))
+            return;
+
+        if (dataset instanceof XYSeriesCollection) {
+            final XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(0);
+            final String label = (xYSeries instanceof HasLabel) ? ((HasLabel) xYSeries).getLabel() : xYSeries.getKey().toString();
+
+            EnhancedLineAndShapeRenderer.drawLineLabel(g2, dataArea, entities, this.minimumScreenX, this.minimumScreenX_Y, this.labelFont, this.labelColor,
+                    this.labelBGColor != null, this.labelBGColor, label);
+        }
+    }
+
     /**
      * Draws the visual representation of a single data item, second pass. In
      * the second pass, the renderer draws the lines and shapes for the
@@ -1296,15 +1338,14 @@
      *            crosshair information for the plot
      *            (<code>null</code> permitted).
      */
-    private void drawItemPass1(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
+    private void doDrawItemPass1(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
             final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
             final CrosshairState x_crosshairState) {
 
         Shape l_entityArea = null;
         EntityCollection l_entities = null;
-        if (null != x_info) {
+        if (x_info != null)
             l_entities = x_info.getOwner().getEntityCollection();
-        }
 
         final Paint l_seriesPaint = getItemPaint(x_series, x_item);
         final Stroke l_seriesStroke = getItemStroke(x_series, x_item);
@@ -1320,6 +1361,13 @@
         final double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, l_domainAxisLocation);
         final double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, l_rangeAxisLocation);
 
+        /* update minimumScreenX -> used to position title */
+        // REMARK: ignore points with y == 0.0 --> else the label sticks on the zero-line, becaue most area themes start at 0.0
+        if (l_x1 < this.minimumScreenX && Math.abs(l_y1) > 0.01) {
+            this.minimumScreenX = l_x1;
+            this.minimumScreenX_Y = l_y1;
+        }
+
         // These are the shapes of the series items.
         if (getShapesVisible()) {
             Shape l_shape = getItemShape(x_series, x_item);
@@ -1344,10 +1392,10 @@
         } // if (getShapesVisible())
 
         // add an entity for the item...
-        if (null != l_entities) {
-            if (null == l_entityArea) {
+        if (l_entities != null) {
+            if (l_entityArea == null)
                 l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 4, 4);
-            }
+
             String l_tip = null;
             final XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, x_item);
             if (null != l_tipGenerator) {
@@ -1363,9 +1411,8 @@
         }
 
         // draw the item label if there is one...
-        if (isItemLabelVisible(x_series, x_item)) {
+        if (isItemLabelVisible(x_series, x_item))
             drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, x_item, l_x1, l_y1, (l_y1 < 0.0));
-        }
 
         final int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis);
         final int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis);
@@ -1379,11 +1426,10 @@
         final double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, (x_item - 1)), x_dataArea, l_rangeAxisLocation);
 
         Line2D l_line = null;
-        if (PlotOrientation.HORIZONTAL == l_orientation) {
+        if (PlotOrientation.HORIZONTAL == l_orientation)
             l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
-        } else if (PlotOrientation.VERTICAL == l_orientation) {
+        else if (PlotOrientation.VERTICAL == l_orientation)
             l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);
-        }
 
         if ((null != l_line) && l_line.intersects(x_dataArea)) {
             x_graphics.setPaint(getItemPaint(x_series, x_item));
@@ -1720,4 +1766,20 @@
         this.negativePaint = SerialUtilities.readPaint(stream);
         this.legendShape = SerialUtilities.readShape(stream);
     }
+
+    private void drawLabel(final Graphics2D g2, final String labelText, final double screenX, final double screenY) {
+
+        final Color oldColor = g2.getColor();
+        final Font oldFont = g2.getFont();
+
+        g2.setFont(this.labelFont);
+        if (this.labelBGColor != null)
+            EnhancedLineAndShapeRenderer.drawTextBox(g2, labelText, (float) screenX, (float) screenY, this.labelBGColor);
+
+        g2.setColor(this.labelColor);
+        g2.drawString(labelText, (float) screenX, (float) screenY);
+
+        g2.setFont(oldFont);
+        g2.setColor(oldColor);
+    }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/jfree/StyledAreaSeriesCollection.java	Thu Jun 28 10:47:00 2018 +0200
+++ b/artifacts/src/main/java/org/dive4elements/river/jfree/StyledAreaSeriesCollection.java	Thu Jun 28 10:47:04 2018 +0200
@@ -77,7 +77,9 @@
         applyOutlineStyle(renderer);
         applyShowLine(renderer);
         applyShowAreaLabel(renderer);
+        applyShowLineLabel(renderer);
         applyPointStyle(renderer);
+        applyShowMinimumMaximum(renderer);
         if (this.mode == FILL_MODE.UNDER) {
             renderer.setAreaCalculationMode(
                     StableXYDifferenceRenderer.CALCULATE_NEGATIVE_AREA);
@@ -96,6 +98,17 @@
         return renderer;
     }
 
+    private void applyShowMinimumMaximum(final StableXYDifferenceRenderer renderer) {
+
+        // TODO: nice to have
+
+        // final boolean minimumVisible = this.theme.parseShowMinimum();
+        // renderer.setIsMinimumShapeVisible(minimumVisible);
+        //
+        // final boolean maximumVisible = this.theme.parseShowMaximum();
+        // renderer.setIsMaximumShapeVisible(maximumVisible);
+    }
+
     private void applyFillColor(final StableXYDifferenceRenderer renderer) {
 
         Paint paint = parseFillPaint();
@@ -169,9 +182,15 @@
         renderer.setOutlinePaint(c);
     }
 
-    /** Inform renderer whether it should draw a label. */
     private void applyShowAreaLabel(final StableXYDifferenceRenderer renderer) {
-        renderer.setLabelArea(this.theme.parseShowAreaLabel());
+        renderer.setShowAreaLabel(this.theme.parseShowAreaLabel());
+    }
+
+    private void applyShowLineLabel(final StableXYDifferenceRenderer renderer) {
+        // REMARK: using 'showlinelabel' to activate labeling the line with the title of the theme. This is the same behaviour
+        // as for line-themes.
+        final boolean showLabelLine = this.theme.parseShowLineLabel();
+        renderer.setShowTitleLabel(showLabelLine);
     }
 
     private void applyOutlineStyle(final StableXYDifferenceRenderer renderer) {

http://dive4elements.wald.intevation.org