Chart with inverted y axis
Asked Answered
G

4

7

Using JavaFX Charts, I need to invert the y-axis of a stacked area chart so that a positive zero is at the top and the positive numbers work downward on the y-axis. Below is a mock-up of what I'm trying to achieve.

Inverted Y-Axis with positive numbers

What is the best (read: shortest development time and high code-reuse) way to achieve this in JavaFX?

UPDATE

Converting the data to negative numbers is not an option. I'm looking for answers that will work with the positive numbers, "untouched."

Garrison answered 2/8, 2013 at 21:56 Comment(0)
S
3

You can use regular axis with negative values, but add TickLabelFormatter which will strip minus sign.

    final NumberAxis yAxis = new NumberAxis(-25, 0, 5);

    yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) {
        @Override
        public String toString(Number value) {
            // note we are printing minus value
            return String.format("%7.1f", -value.doubleValue());
        }
    });

    series1.getData().add(new XYChart.Data("Jan", -1));
    series1.getData().add(new XYChart.Data("Feb", -5));
    series1.getData().add(new XYChart.Data("Mar", -20));
Standoffish answered 6/8, 2013 at 13:36 Comment(2)
Are you suggesting that I convert all of my data to negative numbers before using it in the charts?Garrison
yes. It's not the best way of course, but much faster then extending chart with inversion functionality. To avoid direct update of data you can either extend XYChart.Data class with inversion logic or introduce an utility method which will add inverted numbers to chart.Standoffish
C
1

Not easy option is to enable inverse() operation on axes. Without patching JRE classes it is quite complicated.

Some hints on how to approach this:

1) extend ValueAxis or Axis (NumberAxis is final unfortunately)

2) add boolean field and inverse() method to your axis class

public void inverse() {
    inversed = !inversed; // boolean property
    invalidateRange();
    requestAxisLayout();
}

3) if you extend ValueAxis - you will need to compensate the offset applied by the superclass (and intercept the code where the size of the axis is changing)

@Override
public Long getValueForDisplay(double displayPosition) {
    if (inversed)
        return super.getValueForDisplay(offset - displayPosition);
    else
        return super.getValueForDisplay(displayPosition);
}

@Override
public double getDisplayPosition(Long value) {
    if (inversed)
        return offset - super.getDisplayPosition(value);
    else
        return super.getDisplayPosition(value);
}

4) (the ugliest piece) un-hide label ticks suppressed by Axis class - the original implementation depends on default order of ticks. I did not find any other way other that unlock it via reflection. So this is very brittle.

@Override
protected void layoutChildren() {
    final Side side = getSide();
    boolean isHorisontal = null == side || side.isHorizontal();
    this.offset = isHorisontal ? getWidth() : getHeight();
    super.layoutChildren();
    if (inversed) {
        double prevEnd = isHorisontal ? offset + getTickLabelGap() : 0;
        for (TickMark m : getTickMarks()) {
            double position = m.getPosition();
            try {
                final Text textNode = (Text) textNodeField.get(m);
                final Bounds bounds = textNode.getLayoutBounds();
                if (0 <= position && position <= offset)
                    if (isHorisontal) {
                        textNode.setVisible(position < prevEnd);
                        prevEnd = position - (bounds.getWidth() + getTickLabelGap());
                    } else {
                        textNode.setVisible(position > prevEnd);
                        prevEnd = position + (bounds.getHeight() + getTickLabelGap());
                    }
            } catch (IllegalAccessException ignored) {
            }
        }
    }
}

Y Axis is inverted in this example this is how it looks like

Coonhound answered 16/8, 2015 at 18:7 Comment(2)
Unfortunately as of OpenJFX 16 offset has private access only in ValueAxis. Folks following this pathway will also need to implement a calculation in their class for the offset. I think this should work based on the logic of the underlying ValueAxis layoutChildren(): public double getOffset() { if(this.getSide().isVertical()) return getHeight(); else return 0; }Lindo
Nice work @harshtuna. Your answer enabled me to implement my updates below.Lindo
L
0

Slight update on @harshtuna 's answer to avoid the reflection part. The following code worked for me using OpenJFX 16

@Override
protected void layoutChildren() {
    final Side side = getSide();
    boolean isHorizontal = null == side || side.isHorizontal();
    double offSetting = isHorizontal ? getWidth() : getHeight();

    super.layoutChildren();
    if (inversed) {
        double prevEnd = isHorizontal ? offSetting + getTickLabelGap() : 0;
        for (TickMark m : getTickMarks()) {
            double position = m.getPosition();
            try {
                if (0 <= position && position <= offSetting)
                    if (isHorizontal) {
                        m.setTextVisible(position < prevEnd);
                        prevEnd = position - (2 + getTickLabelGap());
                    } else {
                        m.setTextVisible(position > prevEnd);
                        prevEnd = position + (2 + getTickLabelGap());
                    }
            } catch (Exception ignored) {
                System.out.println("illegal...?");
            }
        }
    }
}   
Lindo answered 21/6, 2021 at 16:58 Comment(0)
I
-1

Use the properties lowerBound and upperBound of the axis. The lowerBound must be higher than upperBound. Here's how you can do using fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<HBox         
    prefHeight="600.0" 
    prefWidth="800.0" 
    xmlns:fx="http://javafx.com/fxml/1">    

    <javafx.scene.chart.LineChart 
        fx:id="chart" >
        <xAxis >
            <javafx.scene.chart.NumberAxis 
                label="X Axis"
                lowerBound="0.01"
                upperBound="100"
                tickUnit="10"
                autoRanging="false" /> 
        </xAxis>
        <yAxis> 
            <javafx.scene.chart.NumberAxis 
                    label="Y Axis"
                    lowerBound="100"
                    upperBound="0.001"
                    tickUnit="10"
                    autoRanging="false"
                />
        </yAxis>
    </javafx.scene.chart.LineChart>            
</Hbox>
Inman answered 4/9, 2016 at 7:10 Comment(1)
This will not provide the desired effect. This will set the lower and upper bound labels correctly but will not fill in the intermediate ticks nor their labels. Internally the tick labeling system assumes a positive upward number line.Lindo

© 2022 - 2024 — McMap. All rights reserved.