Repository: knowm/XChart (java)

Issue:
Cannot change annotation position in category chart
Hi everyone,
I'm trying to change position of annotation and haven't possibility to do it. 
Also it would be great to introduce displaying total value for this type of chart. 

The problem with position also exists for tooltips.
See attached screenshot
![annotation_bug](https://user-images.githubusercontent.com/8298687/72525767-f6226600-3875-11ea-907b-be5f2ddd0ff2.png)

Important **CategoryChart** example settings:
Stacked = true
HasAnnotations = true

for tooltips:
ToolTipsEnabled = true
ToolTipsAlwaysVisible = true


Pull request:
[resolves #402] Cannot change annotation position in category chart
resolves issue #402 , style add two properties
- annotationsPosition: set  annotations position, must be tween 0 and 1, default is 0.5
- showTotalAnnotations: show total annotations, default is false not show

The results are as follows:
defaulte style
```java
    chart.getStyler().setStacked(true);
    chart.getStyler().setHasAnnotations(true);
    chart.getStyler().setShowTotalAnnotations(true);
```
- default
![an-1](https://user-images.githubusercontent.com/57353473/72714816-5123c800-3baa-11ea-81ff-c8d2dda63989.png)

- `chart.getStyler().setAnnotationsPosition(0f)`
![an-2 0](https://user-images.githubusercontent.com/57353473/72715103-d313f100-3baa-11ea-9b18-3fb980bbdf5f.png)

- `chart.getStyler().setAnnotationsPosition(1f)`
![an-3 0](https://user-images.githubusercontent.com/57353473/72715183-fb9beb00-3baa-11ea-84dd-827e32fda6a7.png)

- `chart.getStyler().setShowTotalAnnotations(false)`
![an-4 0](https://user-images.githubusercontent.com/57353473/72715298-2e45e380-3bab-11ea-8fe7-b4d367c47a89.png)



Patch:
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category_Bar.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category_Bar.java
index 6d1d8259..4f6c822b 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category_Bar.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/PlotContent_Category_Bar.java
@@ -131,6 +131,7 @@ public class PlotContent_Category_Bar<ST extends CategoryStyler, S extends Categ
     int seriesCounter = 0;
     double[] accumulatedStackOffsetPos = new double[numCategories];
     double[] accumulatedStackOffsetNeg = new double[numCategories];
+    double[] accumulatedStackOffsetTotalYOffset = new double[numCategories];
 
     boolean toolTipsEnabled = chart.getStyler().isToolTipsEnabled();
 
@@ -261,6 +262,12 @@ public class PlotContent_Category_Bar<ST extends CategoryStyler, S extends Categ
             getBounds().getHeight() - (yTopMargin + (yTop - yMin) / (yMax - yMin) * yTickSpace);
         double yOffset = getBounds().getY() + yTransform;
 
+        // Record the first series yOffset value, update totalYOffset value
+        // when next is greater then 0
+        if (seriesCounter == 0 || next.doubleValue() > 0) {
+          accumulatedStackOffsetTotalYOffset[categoryCounter] = yOffset;
+        }
+
         double zeroTransform =
             getBounds().getHeight() - (yTopMargin + (yBottom - yMin) / (yMax - yMin) * yTickSpace);
         double zeroOffset = getBounds().getY() + zeroTransform;
@@ -380,47 +387,16 @@ public class PlotContent_Category_Bar<ST extends CategoryStyler, S extends Categ
           // }
 
           if (stylerCategory.hasAnnotations() && next != null) {
-
-            String numberAsString = chart.getYAxisFormat().format(next);
-
-            TextLayout textLayout =
-                new TextLayout(
-                    numberAsString,
-                    stylerCategory.getAnnotationsFont(),
-                    new FontRenderContext(null, true, false));
-
-            AffineTransform rot =
-                AffineTransform.getRotateInstance(
-                    -1 * Math.toRadians(stylerCategory.getAnnotationsRotation()), 0, 0);
-            Shape shape = textLayout.getOutline(rot);
-            Rectangle2D annotationRectangle = textLayout.getBounds();
-
-            double annotationX;
-            if (stylerCategory.getAnnotationsRotation() > 0) {
-              double annotationXDelta =
-                  annotationRectangle.getHeight() / 2 + annotationRectangle.getWidth() / 2;
-              double rotationOffset =
-                  annotationXDelta * stylerCategory.getAnnotationsRotation() / 90;
-              annotationX =
-                  xOffset + barWidth / 2 - annotationRectangle.getWidth() / 2 + rotationOffset;
-            } else {
-              annotationX = xOffset + barWidth / 2 - annotationRectangle.getWidth() / 2;
-            }
-            double annotationY;
-            if (next.doubleValue() >= 0.0) {
-              annotationY = yOffset - 4;
-            } else {
-              annotationY = zeroOffset + 4 + annotationRectangle.getHeight();
-            }
-
-            g.setColor(stylerCategory.getAnnotationsFontColor());
-            g.setFont(stylerCategory.getAnnotationsFont());
-            AffineTransform orig = g.getTransform();
-            AffineTransform at = new AffineTransform();
-            at.translate(annotationX, annotationY);
-            g.transform(at);
-            g.fill(shape);
-            g.setTransform(orig);
+            drawAnnotations(g, next, xOffset, yOffset, zeroOffset, barWidth, false);
+          }
+          if (stylerCategory.hasAnnotations()
+              && stylerCategory.isShowTotalAnnotations()
+              && seriesCounter == (seriesMap.size() - 1)) {
+            Number totalNext =
+                accumulatedStackOffsetPos[categoryCounter - 1]
+                    - accumulatedStackOffsetNeg[categoryCounter - 1];
+            double totalYOffset = accumulatedStackOffsetTotalYOffset[categoryCounter - 1];
+            drawAnnotations(g, totalNext, xOffset, totalYOffset, zeroOffset, barWidth, true);
           }
         } else if (CategorySeriesRenderStyle.Stick.equals(
             series.getChartCategorySeriesRenderStyle())) {
@@ -561,4 +537,62 @@ public class PlotContent_Category_Bar<ST extends CategoryStyler, S extends Categ
       seriesCounter++;
     }
   }
+
+  private void drawAnnotations(
+      Graphics2D g,
+      Number next,
+      double xOffset,
+      double yOffset,
+      double zeroOffset,
+      double barWidth,
+      boolean isTotalAnnotations) {
+    String numberAsString = chart.getYAxisFormat().format(next);
+
+    TextLayout textLayout =
+        new TextLayout(
+            numberAsString,
+            stylerCategory.getAnnotationsFont(),
+            new FontRenderContext(null, true, false));
+
+    AffineTransform rot =
+        AffineTransform.getRotateInstance(
+            -1 * Math.toRadians(stylerCategory.getAnnotationsRotation()), 0, 0);
+    Shape shape = textLayout.getOutline(rot);
+    Rectangle2D annotationRectangle = textLayout.getBounds();
+
+    double annotationX;
+    if (stylerCategory.getAnnotationsRotation() > 0) {
+      double annotationXDelta =
+          annotationRectangle.getHeight() / 2 + annotationRectangle.getWidth() / 2;
+      double rotationOffset = annotationXDelta * stylerCategory.getAnnotationsRotation() / 90;
+      annotationX = xOffset + barWidth / 2 - annotationRectangle.getWidth() / 2 + rotationOffset;
+    } else {
+      annotationX = xOffset + barWidth / 2 - annotationRectangle.getWidth() / 2;
+    }
+    double annotationY;
+    if (isTotalAnnotations) {
+      annotationY = yOffset - 4;
+    } else {
+      if (next.doubleValue() >= 0.0) {
+        annotationY =
+            yOffset
+                + (zeroOffset - yOffset) * (1 - stylerCategory.getAnnotationsPosition())
+                + annotationRectangle.getHeight() * stylerCategory.getAnnotationsPosition();
+      } else {
+        annotationY =
+            zeroOffset
+                - (zeroOffset - yOffset) * (1 - stylerCategory.getAnnotationsPosition())
+                + annotationRectangle.getHeight() * (1 - stylerCategory.getAnnotationsPosition());
+      }
+    }
+
+    g.setColor(stylerCategory.getAnnotationsFontColor());
+    g.setFont(stylerCategory.getAnnotationsFont());
+    AffineTransform orig = g.getTransform();
+    AffineTransform at = new AffineTransform();
+    at.translate(annotationX, annotationY);
+    g.transform(at);
+    g.fill(shape);
+    g.setTransform(orig);
+  }
 }
diff --git a/xchart/src/main/java/org/knowm/xchart/style/Styler.java b/xchart/src/main/java/org/knowm/xchart/style/Styler.java
index f9415bfb..8e870bd4 100644
--- a/xchart/src/main/java/org/knowm/xchart/style/Styler.java
+++ b/xchart/src/main/java/org/knowm/xchart/style/Styler.java
@@ -70,6 +70,8 @@ public abstract class Styler {
   private Font annotationsFont;
   private Color annotationsFontColor;
   private int annotationsRotation = 0;
+  private float annotationsPosition = 0.5f;
+  private boolean showTotalAnnotations = false;
   // Misc. ///////////////////////////////
   private boolean antiAlias = true;
   private String decimalPattern;
@@ -823,6 +825,31 @@ public abstract class Styler {
     return this;
   }
 
+  public float getAnnotationsPosition() {
+
+    return annotationsPosition;
+  }
+
+  public Styler setAnnotationsPosition(float annotationsPosition) {
+
+    if (annotationsPosition < 0 || annotationsPosition > 1) {
+      throw new IllegalArgumentException("Annotations position must be tween 0 and 1!!!");
+    }
+    this.annotationsPosition = annotationsPosition;
+    return this;
+  }
+
+  public boolean isShowTotalAnnotations() {
+
+    return showTotalAnnotations;
+  }
+
+  public Styler setShowTotalAnnotations(boolean showTotalAnnotations) {
+
+    this.showTotalAnnotations = showTotalAnnotations;
+    return this;
+  }
+
   public String getDecimalPattern() {
 
     return decimalPattern;


Test patch:
diff --git a/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue402.java b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue402.java
new file mode 100644
index 00000000..2247a866
--- /dev/null
+++ b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue402.java
@@ -0,0 +1,44 @@
+package org.knowm.xchart.standalone.issues;
+
+import org.knowm.xchart.CategoryChart;
+import org.knowm.xchart.CategoryChartBuilder;
+import org.knowm.xchart.SwingWrapper;
+import org.knowm.xchart.demo.charts.ExampleChart;
+import org.knowm.xchart.style.Styler.LegendPosition;
+
+public class TestForIssue402 implements ExampleChart<CategoryChart> {
+
+  public static void main(String[] args) {
+    ExampleChart<CategoryChart> exampleChart = new TestForIssue402();
+    CategoryChart chart = exampleChart.getChart();
+    new SwingWrapper<CategoryChart>(chart).displayChart();
+  }
+
+  @Override
+  public CategoryChart getChart() {
+
+    // Create Chart
+    CategoryChart chart =
+        new CategoryChartBuilder()
+            .width(800)
+            .height(600)
+            .title("TestForIssue402")
+            .xAxisTitle("x")
+            .yAxisTitle("y")
+            .build();
+
+    // Customize Chart
+    chart.getStyler().setLegendPosition(LegendPosition.OutsideE);
+    chart.getStyler().setStacked(true);
+    chart.getStyler().setHasAnnotations(true);
+    chart.getStyler().setShowTotalAnnotations(true);
+
+    // Series
+    chart.addSeries("a", new double[] {0, 1, 2, 3, 4}, new double[] {40, 35, -45, -60, -60});
+    chart.addSeries("b", new double[] {0, 1, 2, 3, 4}, new double[] {50, 65, 60, -70, 30});
+    chart.addSeries("c", new double[] {0, 1, 2, 3, 4}, new double[] {70, 45, -55, -80, 40});
+    chart.addSeries("d", new double[] {0, 1, 2, 3, 4}, new double[] {90, 80, 75, 75, 50});
+
+    return chart;
+  }
+}