mawcsco
mawcsco

Reputation: 624

Chart with inverted y axis

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."

Upvotes: 7

Views: 3689

Answers (4)

Birdasaur
Birdasaur

Reputation: 795

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...?");
            }
        }
    }
}   

Upvotes: 0

elidio xg
elidio xg

Reputation: 41

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>

Upvotes: -1

harshtuna
harshtuna

Reputation: 757

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

Upvotes: 1

Sergey Grinev
Sergey Grinev

Reputation: 34508

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));

Upvotes: 3

Related Questions