{"id":"charts-formula-series","slug":"series-from-formula","title":"Series from a formula","text":"Not every chart shows measurements. A chart can show what a formula computes, and then the points come from a loop. Module 50 kept an application's logic in a plain class without JavaFX, which can be checked without a window; computed points belong in such a class too, and the window only draws them.\n\n### Computing the points\n\nA cup of tea cools down. A simple model of it (a model, not an exact law of nature): each minute, the tea loses a tenth of how much warmer than the room it is. The class `Cooling` computes the temperature at each minute:\n\n```java\nimport java.util.List;\n\npublic class Main {\n    public static void main(String[] args) {\n        List<Double> tea = Cooling.temperatures(90, 20, 5);\n        System.out.println(tea);\n        System.out.println(\"After 5 minutes: \" + tea.get(5));\n    }\n}\n// ==== Cooling.java ====\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Cooling {\n\n    public static List<Double> temperatures(double start, double room, int minutes) {\n        List<Double> temps = new ArrayList<>();\n        double tea = start;\n        for (int minute = 0; minute <= minutes; minute++) {\n            temps.add(Math.round(tea * 10) / 10.0);\n            tea = room + (tea - room) * 0.9;\n        }\n        return temps;\n    }\n}\n```\n\n```output\n[90.0, 83.0, 76.7, 71.0, 65.9, 61.3]\nAfter 5 minutes: 61.3\n```\n\n- The list holds the temperature at minute 0, 1, 2 and so on: the index is the minute. So the x of each point is its index, and the list needs no x values of its own.\n- `Math.round(x)` rounds a double to the nearest whole number and gives it as a `long` (2.5 goes up to 3). So `Math.round(tea * 10) / 10.0` keeps one decimal: 65.927 becomes 659.27, then 659, then 65.9. Dividing by `10.0`, a double, is what keeps the decimal. Computing with doubles leaves tiny errors (`0.1 + 0.2` is `0.30000000000000004`), and the window as text would show them. Rounding what goes into the chart keeps the numbers readable.\n- The program prints the numbers before any chart exists. Checking them first is easier than reading them off a drawing.\n\n### Charting them\n\nThe window makes a series from each list. The tea in a 20 °C kitchen, and the same tea taken out to a 5 °C balcony:\n\n```java\nimport java.util.List;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.LineChart;\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        NumberAxis minutes = new NumberAxis(\"Minutes\", 0, 30, 5);\n        NumberAxis degrees = new NumberAxis();\n        degrees.setLabel(\"Temperature (°C)\");\n        LineChart<Number, Number> chart = new LineChart<>(minutes, degrees);\n        chart.setTitle(\"A cup of tea cools down\");\n        chart.setCreateSymbols(false);\n\n        chart.getData().add(series(\"In the kitchen\", Cooling.temperatures(90, 20, 30)));\n        chart.getData().add(series(\"On the balcony\", Cooling.temperatures(90, 5, 30)));\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Tea\");\n        stage.show();\n    }\n\n    private XYChart.Series<Number, Number> series(String name, List<Double> values) {\n        XYChart.Series<Number, Number> series = new XYChart.Series<>();\n        series.setName(name);\n        for (int x = 0; x < values.size(); x++) {\n            series.getData().add(new XYChart.Data<>(x, values.get(x)));\n        }\n        return series;\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n// ==== Cooling.java ====\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Cooling {\n\n    public static List<Double> temperatures(double start, double room, int minutes) {\n        List<Double> temps = new ArrayList<>();\n        double tea = start;\n        for (int minute = 0; minute <= minutes; minute++) {\n            temps.add(Math.round(tea * 10) / 10.0);\n            tea = room + (tea - room) * 0.9;\n        }\n        return temps;\n    }\n}\n```\n\n```window\n{\"events\":[],\"window\":{\"windows\":[{\"title\":\"Tea\",\"scene\":{\"root\":{\"n\":1,\"type\":\"LineChart\",\"title\":\"A cup of tea cools down\",\"createSymbols\":false,\"xAxis\":{\"type\":\"NumberAxis\",\"label\":\"Minutes\",\"autoRanging\":false,\"lowerBound\":0,\"upperBound\":30,\"tickUnit\":5,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"yAxis\":{\"type\":\"NumberAxis\",\"label\":\"Temperature (°C)\",\"autoRanging\":true,\"lowerBound\":0,\"upperBound\":100,\"tickUnit\":5,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"series\":[{\"name\":\"In the kitchen\",\"data\":[[0,90],[1,83],[2,76.7],[3,71],[4,65.9],[5,61.3],[6,57.2],[7,53.5],[8,50.1],[9,47.1],[10,44.4],[11,42],[12,39.8],[13,37.8],[14,36],[15,34.4],[16,33],[17,31.7],[18,30.5],[19,29.5],[20,28.5],[21,27.7],[22,26.9],[23,26.2],[24,25.6],[25,25],[26,24.5],[27,24.1],[28,23.7],[29,23.3],[30,23]]},{\"name\":\"On the balcony\",\"data\":[[0,90],[1,81.5],[2,73.9],[3,67],[4,60.8],[5,55.2],[6,50.2],[7,45.7],[8,41.6],[9,37.9],[10,34.6],[11,31.7],[12,29],[13,26.6],[14,24.4],[15,22.5],[16,20.8],[17,19.2],[18,17.8],[19,16.5],[20,15.3],[21,14.3],[22,13.4],[23,12.5],[24,11.8],[25,11.1],[26,10.5],[27,9.9],[28,9.4],[29,9],[30,8.6]]}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Tea\\\"\\n  LineChart \\\"A cup of tea cools down\\\" (no symbols)\\n    x axis NumberAxis \\\"Minutes\\\" 0 to 30 by 5\\n    y axis NumberAxis \\\"Temperature (°C)\\\"\\n    series \\\"In the kitchen\\\": (0, 90) (1, 83) (2, 76.7) (3, 71) (4, 65.9) (5, 61.3) (6, 57.2) (7, 53.5) (8, 50.1) (9, 47.1) (10, 44.4) (11, 42) (12, 39.8) (13, 37.8) (14, 36) (15, 34.4) (16, 33) (17, 31.7) (18, 30.5) (19, 29.5) (20, 28.5) (21, 27.7) (22, 26.9) (23, 26.2) (24, 25.6) (25, 25) (26, 24.5) (27, 24.1) (28, 23.7) (29, 23.3) (30, 23)\\n    series \\\"On the balcony\\\": (0, 90) (1, 81.5) (2, 73.9) (3, 67) (4, 60.8) (5, 55.2) (6, 50.2) (7, 45.7) (8, 41.6) (9, 37.9) (10, 34.6) (11, 31.7) (12, 29) (13, 26.6) (14, 24.4) (15, 22.5) (16, 20.8) (17, 19.2) (18, 17.8) (19, 16.5) (20, 15.3) (21, 14.3) (22, 13.4) (23, 12.5) (24, 11.8) (25, 11.1) (26, 10.5) (27, 9.9) (28, 9.4) (29, 9) (30, 8.6)\"}\n```\n\n- The method `series` turns a name and a list of values into a series, so both lines are made the same way. It's an instance method of Main, as `start` is.\n- `chart.setCreateSymbols(false)` leaves out the marks that a line chart puts on each point. With 31 points per line, the marks would only clutter it. The window as text says `(no symbols)`.\n- Both series share the axes, so the lines can be compared: the tea on the balcony cools faster, and toward a lower temperature.\n\n### Points without a line: ScatterChart\n\nA **ScatterChart** draws the points and no line. It suits points that have no order, where a line from one to the next would mean nothing. Fifteen raindrops land on a paving stone of 50 by 50 cm. Where each one lands is random, so the program uses a `Random` with a seed (module 46): the drops land in the same places at every run, and the window is the same at every run, as the window of a check must be.\n\n```java\nimport java.util.Random;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.ScatterChart;\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        ScatterChart<Number, Number> chart = new ScatterChart<>(new NumberAxis(\"cm\", 0, 50, 10), new NumberAxis(\"cm\", 0, 50, 10));\n        chart.setTitle(\"Raindrops on a paving stone\");\n        chart.setLegendVisible(false);\n\n        Random random = new Random(14);\n        XYChart.Series<Number, Number> drops = new XYChart.Series<>();\n        drops.setName(\"Drops\");\n        for (int i = 0; i < 15; i++) {\n            int x = random.nextInt(51);\n            int y = random.nextInt(51);\n            drops.getData().add(new XYChart.Data<>(x, y));\n        }\n        chart.getData().add(drops);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Rain\");\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\":\"Rain\",\"scene\":{\"root\":{\"n\":1,\"type\":\"ScatterChart\",\"title\":\"Raindrops on a paving stone\",\"legendVisible\":false,\"xAxis\":{\"type\":\"NumberAxis\",\"label\":\"cm\",\"autoRanging\":false,\"lowerBound\":0,\"upperBound\":50,\"tickUnit\":10,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"yAxis\":{\"type\":\"NumberAxis\",\"label\":\"cm\",\"autoRanging\":false,\"lowerBound\":0,\"upperBound\":50,\"tickUnit\":10,\"forceZeroInRange\":true,\"minorTickVisible\":true},\"series\":[{\"name\":\"Drops\",\"data\":[[44,30],[2,5],[50,2],[14,41],[43,21],[36,35],[49,5],[15,27],[9,28],[9,31],[50,31],[8,10],[5,48],[42,50],[39,20]]}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Rain\\\"\\n  ScatterChart \\\"Raindrops on a paving stone\\\" (no legend)\\n    x axis NumberAxis \\\"cm\\\" 0 to 50 by 10\\n    y axis NumberAxis \\\"cm\\\" 0 to 50 by 10\\n    series \\\"Drops\\\": (44, 30) (2, 5) (50, 2) (14, 41) (43, 21) (36, 35) (49, 5) (15, 27) (9, 28) (9, 31) (50, 31) (8, 10) (5, 48) (42, 50) (39, 20)\"}\n```\n\nA ScatterChart is made and filled exactly like a LineChart: axes, series, data points.\n","task":"A café stacks paper cups into a pyramid for its window: one cup on top, two under it, three under those, and so on. Write the class `CupPyramid` in `CupPyramid.java`, with the method `public static List<Integer> cups(int maxRows)`. It returns a list of `maxRows + 1` numbers: at index `r`, the number of cups that a pyramid of `r` rows needs (so 0 at index 0, then 1, then 1 + 2, then 1 + 2 + 3, ...). No JavaFX in it: a chart can use it later.\n\nThe check runs `System.out.println(CupPyramid.cups(5));` and expects:\n\n```\n[0, 1, 3, 6, 10, 15]\n```\n","kind":"code","seed":"// ==== CupPyramid.java ====\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CupPyramid {\n\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(CupPyramid.cups(5));\n    }\n}\n","solution":"// ==== CupPyramid.java ====\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CupPyramid {\n\n    public static List<Integer> cups(int maxRows) {\n        List<Integer> counts = new ArrayList<>();\n        int total = 0;\n        counts.add(total);\n        for (int row = 1; row <= maxRows; row++) {\n            total += row;\n            counts.add(total);\n        }\n        return counts;\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(CupPyramid.cups(5));\n    }\n}\n","hints":["Keep a running total. A pyramid of `r` rows has the cups of `r - 1` rows plus a bottom row of `r` cups.","Add 0 to the list first (a pyramid of no rows), then loop `row` from 1 to `maxRows`: add `row` to the total, and the total to the list."],"tests":[{"name":"System.out.println(CupPyramid.cups(5));","stdin":"","call":"System.out.println(CupPyramid.cups(5));\n","expect":"[0, 1, 3, 6, 10, 15]","hidden":false},{"name":"Test 2","stdin":"","call":"System.out.println(CupPyramid.cups(0));\nSystem.out.println(CupPyramid.cups(1));\n","expect":"[0]\n[0, 1]","hidden":true},{"name":"Test 3","stdin":"","call":"List<Integer> big = CupPyramid.cups(20);\nSystem.out.println(big.size() + \" \" + big.get(12) + \" \" + big.get(20));\n","expect":"21 78 210","hidden":true}],"require":[],"forbid":[{"pattern":"\\bjavafx\\b","message":"Keep CupPyramid free of JavaFX: it computes the numbers, and a window can chart them."}],"more":[{"task":"A ball is thrown straight up. With the pull of gravity rounded to 10 m/s² and no air to slow it, its height in meters after `t` seconds is `start + speed * t - 5 * t * t`, where `start` is the height it leaves the hand at and `speed` its speed upward in m/s. The file `throw.txt` holds both, separated by a comma, such as `2,14`. `Main` already reads them and has the method `height`.\n\nFinish `start`: the window, titled `Ball`, shows a line chart (the scene's root) with the title `A thrown ball`, an auto-ranging x axis labeled `Seconds`, an auto-ranging y axis labeled `Height (m)`, no symbols and no legend. Its one series, `Ball`, has a point (t, height) for t = 0, 0.25, 0.5, 0.75 and so on, as long as the height is at least 0: the first point whose height is below 0 isn't added, and the series ends there. The task card shows the window for `2,14`.\n","kind":"code","seed":"import java.nio.file.Files;\nimport java.nio.file.Paths;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.LineChart;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private double start;\n    private double speed;\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        String[] parts = Files.readAllLines(Paths.get(\"throw.txt\")).get(0).split(\",\");\n        this.start = Double.valueOf(parts[0]);\n        this.speed = Double.valueOf(parts[1]);\n\n        stage.setTitle(\"Ball\");\n        stage.show();\n    }\n\n    private double height(double t) {\n        return this.start + this.speed * t - 5 * t * t;\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 javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.LineChart;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private double start;\n    private double speed;\n\n    @Override\n    public void start(Stage stage) throws Exception {\n        String[] parts = Files.readAllLines(Paths.get(\"throw.txt\")).get(0).split(\",\");\n        this.start = Double.valueOf(parts[0]);\n        this.speed = Double.valueOf(parts[1]);\n\n        NumberAxis seconds = new NumberAxis();\n        seconds.setLabel(\"Seconds\");\n        NumberAxis meters = new NumberAxis();\n        meters.setLabel(\"Height (m)\");\n        LineChart<Number, Number> chart = new LineChart<>(seconds, meters);\n        chart.setTitle(\"A thrown ball\");\n        chart.setCreateSymbols(false);\n        chart.setLegendVisible(false);\n\n        XYChart.Series<Number, Number> ball = new XYChart.Series<>();\n        ball.setName(\"Ball\");\n        double t = 0;\n        while (this.height(t) >= 0) {\n            ball.getData().add(new XYChart.Data<>(t, this.height(t)));\n            t += 0.25;\n        }\n        chart.getData().add(ball);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Ball\");\n        stage.show();\n    }\n\n    private double height(double t) {\n        return this.start + this.speed * t - 5 * t * t;\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["A `while` loop fits: start with `double t = 0`, and as long as `this.height(t) >= 0`, add the point `(t, this.height(t))` and add 0.25 to `t`.","`setCreateSymbols(false)` and `setLegendVisible(false)` are methods of the chart; the labels are set on the two `NumberAxis` objects."],"tests":[{"name":"Test 1","stdin":"","files":{"throw.txt":"2,14\n"},"expect":"","hidden":false,"events":[],"window":{"windows":[{"title":"Ball","scene":{"root":{"n":1,"type":"LineChart","title":"A thrown ball","legendVisible":false,"createSymbols":false,"xAxis":{"type":"NumberAxis","label":"Seconds","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"yAxis":{"type":"NumberAxis","label":"Height (m)","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Ball","data":[[0,2],[0.25,5.1875],[0.5,7.75],[0.75,9.6875],[1,11],[1.25,11.6875],[1.5,11.75],[1.75,11.1875],[2,10],[2.25,8.1875],[2.5,5.75],[2.75,2.6875]]}]}}}],"problems":[]},"outline":"Window \"Ball\"\n  LineChart \"A thrown ball\" (no legend, no symbols)\n    x axis NumberAxis \"Seconds\"\n    y axis NumberAxis \"Height (m)\"\n    series \"Ball\": (0, 2) (0.25, 5.1875) (0.5, 7.75) (0.75, 9.6875) (1, 11) (1.25, 11.6875) (1.5, 11.75) (1.75, 11.1875) (2, 10) (2.25, 8.1875) (2.5, 5.75) (2.75, 2.6875)"},{"name":"Test 2","stdin":"","files":{"throw.txt":"0,10\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Ball","scene":{"root":{"n":1,"type":"LineChart","title":"A thrown ball","legendVisible":false,"createSymbols":false,"xAxis":{"type":"NumberAxis","label":"Seconds","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"yAxis":{"type":"NumberAxis","label":"Height (m)","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Ball","data":[[0,0],[0.25,2.1875],[0.5,3.75],[0.75,4.6875],[1,5],[1.25,4.6875],[1.5,3.75],[1.75,2.1875],[2,0]]}]}}}],"problems":[]},"outline":"Window \"Ball\"\n  LineChart \"A thrown ball\" (no legend, no symbols)\n    x axis NumberAxis \"Seconds\"\n    y axis NumberAxis \"Height (m)\"\n    series \"Ball\": (0, 0) (0.25, 2.1875) (0.5, 3.75) (0.75, 4.6875) (1, 5) (1.25, 4.6875) (1.5, 3.75) (1.75, 2.1875) (2, 0)"},{"name":"Test 3","stdin":"","files":{"throw.txt":"1.5,3\n"},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Ball","scene":{"root":{"n":1,"type":"LineChart","title":"A thrown ball","legendVisible":false,"createSymbols":false,"xAxis":{"type":"NumberAxis","label":"Seconds","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"yAxis":{"type":"NumberAxis","label":"Height (m)","autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Ball","data":[[0,1.5],[0.25,1.9375],[0.5,1.75],[0.75,0.9375]]}]}}}],"problems":[]},"outline":"Window \"Ball\"\n  LineChart \"A thrown ball\" (no legend, no symbols)\n    x axis NumberAxis \"Seconds\"\n    y axis NumberAxis \"Height (m)\"\n    series \"Ball\": (0, 1.5) (0.25, 1.9375) (0.5, 1.75) (0.75, 0.9375)"}],"require":[],"forbid":[]},{"task":"A program should put points at x = 0, 0.5, 1, 1.5 and 2, but its author wrote `i / 2`. What does it print?\n","kind":"predict","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.LineChart;\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        XYChart.Series<Number, Number> squares = new XYChart.Series<>();\n        for (int i = 0; i <= 4; i++) {\n            squares.getData().add(new XYChart.Data<>(i / 2, i * i));\n        }\n        System.out.println(squares.getData().size());\n        System.out.println(squares.getData().get(3));\n        System.out.println(squares.getData().get(4).getXValue());\n\n        LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());\n        chart.getData().add(squares);\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Squares\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.LineChart;\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        XYChart.Series<Number, Number> squares = new XYChart.Series<>();\n        for (int i = 0; i <= 4; i++) {\n            squares.getData().add(new XYChart.Data<>(i / 2, i * i));\n        }\n        System.out.println(squares.getData().size());\n        System.out.println(squares.getData().get(3));\n        System.out.println(squares.getData().get(4).getXValue());\n\n        LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());\n        chart.getData().add(squares);\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Squares\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["`i` is an int and so is `2`, so `i / 2` is integer division: 0, 0, 1, 1, 2. The loop still adds five points.","The point at index 3 was made when `i` was 3: x is `3 / 2`, and y is `3 * 3`. Printing a point gives `Data[x,y,null]`."],"tests":[{"name":"Output","stdin":"","expect":"5\nData[1,9,null]\n2","hidden":false}],"require":[],"forbid":[],"lines":["5","Data[1,9,null]","2"]}]}