{"id":"charts-honest-charts","slug":"honest-charts","title":"Honest charts","text":"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.\n\n### A truncated axis\n\nThree 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:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        CategoryAxis brands = new CategoryAxis();\n        NumberAxis cycles = new NumberAxis(\"Charge cycles\", 970, 1015, 5);\n        BarChart<String, Number> chart = new BarChart<>(brands, cycles);\n        chart.setTitle(\"Brand C lasts longest!\");\n        chart.setLegendVisible(false);\n\n        XYChart.Series<String, Number> series = new XYChart.Series<>();\n        series.setName(\"Cycles\");\n        series.getData().add(new XYChart.Data<>(\"Brand A\", 980));\n        series.getData().add(new XYChart.Data<>(\"Brand B\", 1000));\n        series.getData().add(new XYChart.Data<>(\"Brand C\", 1010));\n        chart.getData().add(series);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Batteries\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[],\"window\":{\"windows\":[{\"title\":\"Batteries\",\"scene\":{\"root\":{\"n\":1,\"type\":\"BarChart\",\"title\":\"Brand C lasts longest!\",\"legendVisible\":false,\"xAxis\":{\"type\":\"CategoryAxis\",\"label\":null,\"autoRanging\":true,\"categories\":[\"Brand A\",\"Brand B\",\"Brand C\"]},\"yAxis\":{\"type\":\"NumberAxis\",\"label\":\"Charge cycles\",\"autoRanging\":false,\"lowerBound\":970,\"upperBound\":1015,\"tickUnit\":5,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"series\":[{\"name\":\"Cycles\",\"data\":[[\"Brand A\",980],[\"Brand B\",1000],[\"Brand C\",1010]]}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Batteries\\\"\\n  BarChart \\\"Brand C lasts longest!\\\" (no legend)\\n    x axis CategoryAxis\\n    y axis NumberAxis \\\"Charge cycles\\\" 970 to 1015 by 5\\n    series \\\"Cycles\\\": (\\\"Brand A\\\", 980) (\\\"Brand B\\\", 1000) (\\\"Brand C\\\", 1010)\"}\n```\n\nBrand 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.\n\nThe 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:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        CategoryAxis brands = new CategoryAxis();\n        NumberAxis cycles = new NumberAxis(\"Charge cycles\", 0, 1200, 200);\n        BarChart<String, Number> chart = new BarChart<>(brands, cycles);\n        chart.setTitle(\"Charge cycles per battery\");\n        chart.setLegendVisible(false);\n\n        XYChart.Series<String, Number> series = new XYChart.Series<>();\n        series.setName(\"Cycles\");\n        series.getData().add(new XYChart.Data<>(\"Brand A\", 980));\n        series.getData().add(new XYChart.Data<>(\"Brand B\", 1000));\n        series.getData().add(new XYChart.Data<>(\"Brand C\", 1010));\n        chart.getData().add(series);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Batteries\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[],\"window\":{\"windows\":[{\"title\":\"Batteries\",\"scene\":{\"root\":{\"n\":1,\"type\":\"BarChart\",\"title\":\"Charge cycles per battery\",\"legendVisible\":false,\"xAxis\":{\"type\":\"CategoryAxis\",\"label\":null,\"autoRanging\":true,\"categories\":[\"Brand A\",\"Brand B\",\"Brand C\"]},\"yAxis\":{\"type\":\"NumberAxis\",\"label\":\"Charge cycles\",\"autoRanging\":false,\"lowerBound\":0,\"upperBound\":1200,\"tickUnit\":200,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"series\":[{\"name\":\"Cycles\",\"data\":[[\"Brand A\",980],[\"Brand B\",1000],[\"Brand C\",1010]]}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Batteries\\\"\\n  BarChart \\\"Charge cycles per battery\\\" (no legend)\\n    x axis CategoryAxis\\n    y axis NumberAxis \\\"Charge cycles\\\" 0 to 1200 by 200\\n    series \\\"Cycles\\\": (\\\"Brand A\\\", 980) (\\\"Brand B\\\", 1000) (\\\"Brand C\\\", 1010)\"}\n```\n\nNow 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.\n\n### Rules of thumb\n\n- **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.\n- **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.\n- **Charts that are compared share their scale.** Two charts side by side with different y axes look alike when their numbers aren't.\n- **Labels say what and in which unit**, and a title says what is shown, not what to think of it.\n\nIn this module's tasks, honest bounds are set with numbers (the window as text shows bounds that are set, and not `forceZeroInRange`).\n\n### Honest bounds, computed\n\nWhen 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:\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<Integer> cycles = new ArrayList<>();\n        cycles.add(980);\n        cycles.add(1000);\n        cycles.add(1010);\n        System.out.println(Bounds.upperBound(cycles, 200));\n        System.out.println(Bounds.upperBound(cycles, 10));\n        System.out.println(Bounds.upperBound(new ArrayList<>(), 50));\n    }\n}\n// ==== Bounds.java ====\nimport java.util.List;\n\npublic class Bounds {\n\n    public static int upperBound(List<Integer> values, int tick) {\n        int max = 0;\n        for (int value : values) {\n            max = Math.max(max, value);\n        }\n        int upper = tick;\n        while (upper < max) {\n            upper += tick;\n        }\n        return upper;\n    }\n}\n```\n\n```output\n1200\n1010\n50\n```\n\nThe 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)`.\n","task":"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:\n\n```text\nservice,percent\nService A,96\nService B,97\nService C,99\n```\n","kind":"code","seed":"import java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        CategoryAxis services = new CategoryAxis();\n        services.setLabel(\"Service\");\n        NumberAxis percent = new NumberAxis(\"On time (%)\", 95, 100, 1);\n        BarChart<String, Number> chart = new BarChart<>(services, percent);\n        chart.setTitle(\"Week 41\");\n        chart.setLegendVisible(false);\n\n        XYChart.Series<String, Number> series = new XYChart.Series<>();\n        series.setName(\"On time\");\n        List<String> lines = Files.readAllLines(Paths.get(\"ontime.csv\"));\n        for (int i = 1; i < lines.size(); i++) {\n            String[] parts = lines.get(i).split(\",\");\n            series.getData().add(new XYChart.Data<>(parts[0], Integer.valueOf(parts[1])));\n        }\n        chart.getData().add(series);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Parcels\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        CategoryAxis services = new CategoryAxis();\n        services.setLabel(\"Service\");\n        NumberAxis percent = new NumberAxis(\"On time (%)\", 0, 100, 10);\n        BarChart<String, Number> chart = new BarChart<>(services, percent);\n        chart.setTitle(\"Parcels delivered on time\");\n        chart.setLegendVisible(false);\n\n        XYChart.Series<String, Number> series = new XYChart.Series<>();\n        series.setName(\"On time\");\n        List<String> lines = Files.readAllLines(Paths.get(\"ontime.csv\"));\n        for (int i = 1; i < lines.size(); i++) {\n            String[] parts = lines.get(i).split(\",\");\n            series.getData().add(new XYChart.Data<>(parts[0], Integer.valueOf(parts[1])));\n        }\n        chart.getData().add(series);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Parcels\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["The y axis is made by `new NumberAxis(\"On time (%)\", 95, 100, 1)`: the numbers are the lower bound, the upper bound and the tick unit.","A percentage goes from 0 to 100, so the honest axis is `new NumberAxis(\"On time (%)\", 0, 100, 10)`. Then change the text in `setTitle`."],"tests":[{"name":"Test 1","stdin":"","files":{"ontime.csv":"service,percent\nService A,96\nService B,97\nService C,99\n"},"expect":"","hidden":false,"events":[],"window":{"windows":[{"title":"Parcels","scene":{"root":{"n":1,"type":"BarChart","title":"Parcels delivered on time","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Service","autoRanging":true,"categories":["Service A","Service B","Service C"]},"yAxis":{"type":"NumberAxis","label":"On time (%)","autoRanging":false,"lowerBound":0,"upperBound":100,"tickUnit":10,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"On time","data":[["Service A",96],["Service B",97],["Service C",99]]}]}}}],"problems":[]},"outline":"Window \"Parcels\"\n  BarChart \"Parcels delivered on time\" (no legend)\n    x axis CategoryAxis \"Service\"\n    y axis NumberAxis \"On time (%)\" 0 to 100 by 10\n    series \"On time\": (\"Service A\", 96) (\"Service B\", 97) (\"Service C\", 99)"},{"name":"Test 2","stdin":"","files":{"ontime.csv":"service,percent\nService D,88\nService E,91\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Parcels","scene":{"root":{"n":1,"type":"BarChart","title":"Parcels delivered on time","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Service","autoRanging":true,"categories":["Service D","Service E"]},"yAxis":{"type":"NumberAxis","label":"On time (%)","autoRanging":false,"lowerBound":0,"upperBound":100,"tickUnit":10,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"On time","data":[["Service D",88],["Service E",91]]}]}}}],"problems":[]},"outline":"Window \"Parcels\"\n  BarChart \"Parcels delivered on time\" (no legend)\n    x axis CategoryAxis \"Service\"\n    y axis NumberAxis \"On time (%)\" 0 to 100 by 10\n    series \"On time\": (\"Service D\", 88) (\"Service E\", 91)"},{"name":"Test 3","stdin":"","files":{"ontime.csv":"service,percent\nService F,100\nService G,95\nService H,97\nService I,98\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Parcels","scene":{"root":{"n":1,"type":"BarChart","title":"Parcels delivered on time","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Service","autoRanging":true,"categories":["Service F","Service G","Service H","Service I"]},"yAxis":{"type":"NumberAxis","label":"On time (%)","autoRanging":false,"lowerBound":0,"upperBound":100,"tickUnit":10,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"On time","data":[["Service F",100],["Service G",95],["Service H",97],["Service I",98]]}]}}}],"problems":[]},"outline":"Window \"Parcels\"\n  BarChart \"Parcels delivered on time\" (no legend)\n    x axis CategoryAxis \"Service\"\n    y axis NumberAxis \"On time (%)\" 0 to 100 by 10\n    series \"On time\": (\"Service F\", 100) (\"Service G\", 95) (\"Service H\", 97) (\"Service I\", 98)"}],"require":[],"forbid":[],"more":[{"task":"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.\n\nThe check runs:\n\n```java\nSystem.out.println(Bounds.niceTick(1010));\nSystem.out.println(Bounds.niceTick(47));\nSystem.out.println(Bounds.niceTick(0));\n```\n\nand expects:\n\n```\n200\n5\n1\n```\n","kind":"code","seed":"// ==== Bounds.java ====\nimport java.util.List;\n\npublic class Bounds {\n\n    public static int upperBound(List<Integer> values, int tick) {\n        int max = 0;\n        for (int value : values) {\n            max = Math.max(max, value);\n        }\n        int upper = tick;\n        while (upper < max) {\n            upper += tick;\n        }\n        return upper;\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(Bounds.niceTick(1010));\n        System.out.println(Bounds.niceTick(47));\n        System.out.println(Bounds.niceTick(0));\n    }\n}\n","solution":"// ==== Bounds.java ====\nimport java.util.List;\n\npublic class Bounds {\n\n    public static int upperBound(List<Integer> values, int tick) {\n        int max = 0;\n        for (int value : values) {\n            max = Math.max(max, value);\n        }\n        int upper = tick;\n        while (upper < max) {\n            upper += tick;\n        }\n        return upper;\n    }\n\n    public static int niceTick(int max) {\n        int power = 1;\n        while (true) {\n            if (power * 10 >= max) {\n                return power;\n            }\n            if (power * 2 * 10 >= max) {\n                return power * 2;\n            }\n            if (power * 5 * 10 >= max) {\n                return power * 5;\n            }\n            power = power * 10;\n        }\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(Bounds.niceTick(1010));\n        System.out.println(Bounds.niceTick(47));\n        System.out.println(Bounds.niceTick(0));\n    }\n}\n","hints":["Go through the sequence by powers of ten: 1, 10, 100, ... For each power, try the power itself, then 2 times it, then 5 times it, and return the first whose 10 ticks reach `max`.","A `while (true)` loop can hold the three checks, each with a `return`, and multiply the power by 10 at its end."],"tests":[{"name":"Test 1","stdin":"","call":"System.out.println(Bounds.niceTick(1010));\nSystem.out.println(Bounds.niceTick(47));\nSystem.out.println(Bounds.niceTick(0));\n","expect":"200\n5\n1","hidden":false},{"name":"Test 2","stdin":"","call":"System.out.println(Bounds.niceTick(10) + \" \" + Bounds.niceTick(11) + \" \" + Bounds.niceTick(20) + \" \" + Bounds.niceTick(21));\nSystem.out.println(Bounds.niceTick(50) + \" \" + Bounds.niceTick(51) + \" \" + Bounds.niceTick(100) + \" \" + Bounds.niceTick(101));\n","expect":"1 2 2 5\n5 10 10 20","hidden":true},{"name":"System.out.println(Bounds.niceTick(1) + \" \" + Bounds.niceTick(7) + \" \" + Bounds.niceTick(130) + \" \" + Bounds.niceTick(4999) + \" \" + Bounds.niceTick(5001));","stdin":"","call":"System.out.println(Bounds.niceTick(1) + \" \" + Bounds.niceTick(7) + \" \" + Bounds.niceTick(130) + \" \" + Bounds.niceTick(4999) + \" \" + Bounds.niceTick(5001));\n","expect":"1 1 20 500 1000","hidden":true}],"require":[],"forbid":[]},{"task":"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:\n\n```text\nplace,votes\nLake camp,34\nScience museum,21\nZoo,47\nClimbing hall,12\n```\n","kind":"code","seed":"// ==== Main.java ====\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        List<String> lines = Files.readAllLines(Paths.get(\"votes.csv\"));\n\n        stage.setTitle(\"Club trip\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== Bounds.java ====\nimport java.util.List;\n\npublic class Bounds {\n\n    public static int upperBound(List<Integer> values, int tick) {\n        int max = 0;\n        for (int value : values) {\n            max = Math.max(max, value);\n        }\n        int upper = tick;\n        while (upper < max) {\n            upper += tick;\n        }\n        return upper;\n    }\n\n    public static int niceTick(int max) {\n        int power = 1;\n        while (true) {\n            if (power * 10 >= max) {\n                return power;\n            }\n            if (power * 2 * 10 >= max) {\n                return power * 2;\n            }\n            if (power * 5 * 10 >= max) {\n                return power * 5;\n            }\n            power = power * 10;\n        }\n    }\n}\n","solution":"// ==== Main.java ====\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        List<String> lines = Files.readAllLines(Paths.get(\"votes.csv\"));\n        List<String> places = new ArrayList<>();\n        List<Integer> votes = new ArrayList<>();\n        for (int i = 1; i < lines.size(); i++) {\n            String[] parts = lines.get(i).split(\",\");\n            places.add(parts[0]);\n            votes.add(Integer.valueOf(parts[1]));\n        }\n        int max = 0;\n        for (int count : votes) {\n            max = Math.max(max, count);\n        }\n        int tick = Bounds.niceTick(max);\n\n        CategoryAxis placeAxis = new CategoryAxis();\n        placeAxis.setLabel(\"Place\");\n        NumberAxis voteAxis = new NumberAxis(\"Votes\", 0, Bounds.upperBound(votes, tick), tick);\n        BarChart<String, Number> chart = new BarChart<>(placeAxis, voteAxis);\n        chart.setTitle(\"Where should the trip go?\");\n        chart.setLegendVisible(false);\n        XYChart.Series<String, Number> series = new XYChart.Series<>();\n        series.setName(\"Votes\");\n        for (int i = 0; i < places.size(); i++) {\n            series.getData().add(new XYChart.Data<>(places.get(i), votes.get(i)));\n        }\n        chart.getData().add(series);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Club trip\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== Bounds.java ====\nimport java.util.List;\n\npublic class Bounds {\n\n    public static int upperBound(List<Integer> values, int tick) {\n        int max = 0;\n        for (int value : values) {\n            max = Math.max(max, value);\n        }\n        int upper = tick;\n        while (upper < max) {\n            upper += tick;\n        }\n        return upper;\n    }\n\n    public static int niceTick(int max) {\n        int power = 1;\n        while (true) {\n            if (power * 10 >= max) {\n                return power;\n            }\n            if (power * 2 * 10 >= max) {\n                return power * 2;\n            }\n            if (power * 5 * 10 >= max) {\n                return power * 5;\n            }\n            power = power * 10;\n        }\n    }\n}\n","hints":["Read the rows into two lists first, the places and the votes, so that the bounds can be computed before the axis is made.","Find the largest vote, `int tick = Bounds.niceTick(max);`, then `new NumberAxis(\"Votes\", 0, Bounds.upperBound(votes, tick), tick)`."],"tests":[{"name":"Test 1","stdin":"","files":{"votes.csv":"place,votes\nLake camp,34\nScience museum,21\nZoo,47\nClimbing hall,12\n"},"expect":"","hidden":false,"events":[],"window":{"windows":[{"title":"Club trip","scene":{"root":{"n":1,"type":"BarChart","title":"Where should the trip go?","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Place","autoRanging":true,"categories":["Lake camp","Science museum","Zoo","Climbing hall"]},"yAxis":{"type":"NumberAxis","label":"Votes","autoRanging":false,"lowerBound":0,"upperBound":50,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Votes","data":[["Lake camp",34],["Science museum",21],["Zoo",47],["Climbing hall",12]]}]}}}],"problems":[]},"outline":"Window \"Club trip\"\n  BarChart \"Where should the trip go?\" (no legend)\n    x axis CategoryAxis \"Place\"\n    y axis NumberAxis \"Votes\" 0 to 50 by 5\n    series \"Votes\": (\"Lake camp\", 34) (\"Science museum\", 21) (\"Zoo\", 47) (\"Climbing hall\", 12)"},{"name":"Test 2","stdin":"","files":{"votes.csv":"place,votes\nBeach,130\nOld castle,64\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Club trip","scene":{"root":{"n":1,"type":"BarChart","title":"Where should the trip go?","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Place","autoRanging":true,"categories":["Beach","Old castle"]},"yAxis":{"type":"NumberAxis","label":"Votes","autoRanging":false,"lowerBound":0,"upperBound":140,"tickUnit":20,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Votes","data":[["Beach",130],["Old castle",64]]}]}}}],"problems":[]},"outline":"Window \"Club trip\"\n  BarChart \"Where should the trip go?\" (no legend)\n    x axis CategoryAxis \"Place\"\n    y axis NumberAxis \"Votes\" 0 to 140 by 20\n    series \"Votes\": (\"Beach\", 130) (\"Old castle\", 64)"},{"name":"Test 3","stdin":"","files":{"votes.csv":"place,votes\nNowhere,0\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Club trip","scene":{"root":{"n":1,"type":"BarChart","title":"Where should the trip go?","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":"Place","autoRanging":true,"categories":["Nowhere"]},"yAxis":{"type":"NumberAxis","label":"Votes","autoRanging":false,"lowerBound":0,"upperBound":1,"tickUnit":1,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Votes","data":[["Nowhere",0]]}]}}}],"problems":[]},"outline":"Window \"Club trip\"\n  BarChart \"Where should the trip go?\" (no legend)\n    x axis CategoryAxis \"Place\"\n    y axis NumberAxis \"Votes\" 0 to 1 by 1\n    series \"Votes\": (\"Nowhere\", 0)"}],"require":[],"forbid":[]}]}