Java Arena
Step 7 of 8

Honest charts

A chart can persuade: a picture makes a point faster than a table of numbers does. That's why a chart must show its numbers fairly. The same numbers can make a small difference look huge, by a choice that's easy to miss: where the y axis starts.

A truncated axis

Three brands of rechargeable batteries are compared by how many times a battery can be charged before it wears out. The brands and the numbers are made up:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        CategoryAxis brands = new CategoryAxis();
        NumberAxis cycles = new NumberAxis("Charge cycles", 970, 1015, 5);
        BarChart<String, Number> chart = new BarChart<>(brands, cycles);
        chart.setTitle("Brand C lasts longest!");
        chart.setLegendVisible(false);

        XYChart.Series<String, Number> series = new XYChart.Series<>();
        series.setName("Cycles");
        series.getData().add(new XYChart.Data<>("Brand A", 980));
        series.getData().add(new XYChart.Data<>("Brand B", 1000));
        series.getData().add(new XYChart.Data<>("Brand C", 1010));
        chart.getData().add(series);

        stage.setScene(new Scene(chart));
        stage.setTitle("Batteries");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Batteries"
  BarChart "Brand C lasts longest!" (no legend)
    x axis CategoryAxis
    y axis NumberAxis "Charge cycles" 970 to 1015 by 5
    series "Cycles": ("Brand A", 980) ("Brand B", 1000) ("Brand C", 1010)

Brand C's bar is four times as tall as Brand A's, yet its number is only 3 percent bigger (1010 against 980). The y axis starts at 970: it is truncated. Every bar loses the same 970 from its bottom, which takes away almost all of the short bars and leaves the differences.

The length of a bar is what the eye compares, so the length must stand for the value: a bar twice as long must mean twice as much. That holds only when the bars start at 0. The fair chart is the same program with another axis:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        CategoryAxis brands = new CategoryAxis();
        NumberAxis cycles = new NumberAxis("Charge cycles", 0, 1200, 200);
        BarChart<String, Number> chart = new BarChart<>(brands, cycles);
        chart.setTitle("Charge cycles per battery");
        chart.setLegendVisible(false);

        XYChart.Series<String, Number> series = new XYChart.Series<>();
        series.setName("Cycles");
        series.getData().add(new XYChart.Data<>("Brand A", 980));
        series.getData().add(new XYChart.Data<>("Brand B", 1000));
        series.getData().add(new XYChart.Data<>("Brand C", 1010));
        chart.getData().add(series);

        stage.setScene(new Scene(chart));
        stage.setTitle("Batteries");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Batteries"
  BarChart "Charge cycles per battery" (no legend)
    x axis CategoryAxis
    y axis NumberAxis "Charge cycles" 0 to 1200 by 200
    series "Cycles": ("Brand A", 980) ("Brand B", 1000) ("Brand C", 1010)

Now the bars are almost the same, as the numbers are. The title changed too: it says what is shown, and leaves the judging to the reader.

Rules of thumb

  • Bars start at 0. An auto-ranging NumberAxis already includes 0, since its forceZeroInRange is true by default. setForceZeroInRange(false) lets it start near the smallest value instead, and so do bounds that you give. Never do either for bars.
  • Lines may zoom in, in plain sight. A line chart shows how a value changes, and a y axis from 15 to 25 °C can be the right choice for a day's temperatures. The numbers on the axis say where it starts, so a reader can see that it zooms in.
  • Charts that are compared share their scale. Two charts side by side with different y axes look alike when their numbers aren't.
  • Labels say what and in which unit, and a title says what is shown, not what to think of it.

In this module's tasks, honest bounds are set with numbers (the window as text shows bounds that are set, and not forceZeroInRange).

Honest bounds, computed

When the data comes from a file, the program doesn't know the largest value in advance, so it computes the bounds: 0 at the bottom, and at the top the first tick at or above the largest value. A class without JavaFX does it:

Main.java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> cycles = new ArrayList<>();
        cycles.add(980);
        cycles.add(1000);
        cycles.add(1010);
        System.out.println(Bounds.upperBound(cycles, 200));
        System.out.println(Bounds.upperBound(cycles, 10));
        System.out.println(Bounds.upperBound(new ArrayList<>(), 50));
    }
}
Bounds.java
import java.util.List;

public class Bounds {

    public static int upperBound(List<Integer> values, int tick) {
        int max = 0;
        for (int value : values) {
            max = Math.max(max, value);
        }
        int upper = tick;
        while (upper < max) {
            upper += tick;
        }
        return upper;
    }
}
Output
1200
1010
50

The upper bound is a whole number of ticks, so the top of the axis has a number, and it's at least one tick, so an axis for data of only zeros still has a range. The chart then uses new NumberAxis("Charge cycles", 0, Bounds.upperBound(cycles, 200), 200).

Your task

Challenge 1 of 3

A delivery team's weekly report compares three made-up parcel services by the percentage of parcels they deliver on time, read from ontime.csv. Its y axis is truncated: it starts at 95, so a difference of 3 points looks huge, and its title doesn't say what the bars show. Change the y axis to go from 0 to 100 with a tick every 10 (keep its label On time (%)), and the chart's title to Parcels delivered on time. Everything else stays as it is. The task card shows the window for this file:

service,percent
Service A,96
Service B,97
Service C,99

Challenge 2

Which tick unit to use? An auto-ranging axis picks a "nice" one, and Bounds can too. Add the method public static int niceTick(int max) to Bounds (in Bounds.java, next to upperBound). It returns the first number of the sequence 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, ... (each step multiplies by 2, then by 2.5, then by 2, as 1, 2, 5 repeat with one more zero) for which 10 ticks reach the largest value: tick * 10 >= max. So niceTick(1010) is 200 (100 gives only 1000), niceTick(47) is 5 and niceTick(0) is 1.

The check runs:

System.out.println(Bounds.niceTick(1010));
System.out.println(Bounds.niceTick(47));
System.out.println(Bounds.niceTick(0));

and expects:

200
5
1

Challenge 3

A youth club votes on where its trip goes, and the votes are in votes.csv. Bounds (with upperBound and niceTick) is ready. Write start: the window, titled Club trip, shows a bar chart (the scene's root) with the title Where should the trip go?, a category axis labeled Place, no legend, and one series, Votes, with a bar (place, votes) for each row, in the order of the file. Its y axis is labeled Votes, starts at 0 and has bounds computed from the data: the tick unit is niceTick of the largest number of votes, and the upper bound is upperBound of the votes with that tick unit. The task card shows the window for this file, where the axis goes from 0 to 50 with a tick every 5:

place,votes
Lake camp,34
Science museum,21
Zoo,47
Climbing hall,12