29

In my program I want to draw a simple score line graph. I have a text file and on each line is an integer score, which I read in and want to pass as argument to my graph class. I'm having some trouble implementing the graph class and all the examples I've seen have their methods in the same class as their main, which I won't have.

I want to be able to pass my array to the object and generate a graph, but when calling my paint method it is asking me for a Graphics g... This is what I have so far:

public class Graph extends JPanel {

    public void paintGraph (Graphics g){

        ArrayList<Integer> scores = new ArrayList<Integer>(10);

        Random r = new Random();

        for (int i : scores){
            i = r.nextInt(20);
            System.out.println(r);
        }

        int y1;
        int y2;

        for (int i = 0; i < scores.size(); i++){
            y1 = scores.get(i);
            y2 = scores.get(i+1);
            g.drawLine(i, y1, i+1, y2);
        }
    }
}

For now I have inserted a simple random number generator to fill up my array.

I have an existing frame and basically want to instantiate the Graph class and mount the panel onto my frame. I'm really sorry that this question seems so jumbled by the way, but I've had little sleep...

The code in my main statement is:

testFrame = new JFrame();
testFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Graph graph = new Graph();
testFrame.add(graph);

I'm not sure exactly what an SSCE is but this is my attempt at one:

public class Test {

    JFrame testFrame;
    public Test() {
        testFrame = new JFrame();
        testFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Graph graph = new Graph();
        testFrame.add(graph);
        testFrame.setBounds(100, 100, 764, 470);
        testFrame.setVisible(true);
    }

Graph.java

public class Graph extends JPanel {
    public Graph() {
       setSize(500, 500);
    }

    @Override
    public void paintComponent(Graphics g) {
        Graphics2D gr = (Graphics2D) g; // This is if you want to use Graphics2D
        // Now do the drawing here
        ArrayList<Integer> scores = new ArrayList<Integer>(10);

        Random r = new Random();

        for (int i : scores) {
            i = r.nextInt(20);
            System.out.println(r);
        }

        int y1;
        int y2;

        for (int i = 0; i < scores.size() - 1; i++) {
            y1 = (scores.get(i)) * 10;
            y2 = (scores.get(i + 1)) * 10;
            gr.drawLine(i * 10, y1, (i + 1) * 10, y2);
        }
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user1058210
  • 1,639
  • 7
  • 29
  • 49
  • 1
    Your code suggests that you've not yet read the tutorials on how to do Swing Graphics, and you need to do this before attempting this stuff. There's no substitute for this. For e.g., you need to override paintComponent, but I don't see this anywhere in your code. You can start here: [Performing Custom Painting](http://docs.oracle.com/javase/tutorial/uiswing/painting/index.html). – Hovercraft Full Of Eels Jan 01 '12 at 13:40
  • *"I'm really sorry that this question seems so jumbled.."* What 'question'? I don't see one in that mess of words. *"but I've had little sleep."* Not our concern, not our problem & not an excuse for dumping your random thoughts to a Q&A site. Please wait till you've had sleep before posting more questions. – Andrew Thompson Jan 01 '12 at 13:45
  • 2
    @AndrewThompson I'm sorry about that - I know it's not a valid excuse - just thought there might be some who could sympathise with the state I'm in and forgive the lack of precision in my question and maybe infer the problem even though I may not have made it totally explicit. PetarMinchev, thanks for being able to do that! – user1058210 Jan 01 '12 at 14:11
  • Also, your x-axis is going to be *very* compressed seeing as how it increments by only one. You might want to make this a little bigger (and your y-axis too) by multiplying both by a scaling factor. – Hovercraft Full Of Eels Jan 01 '12 at 14:14
  • Also, won't your code throw an array out of bounds exception when you try call `get(i + 1)` in that for loop of yours? – Hovercraft Full Of Eels Jan 01 '12 at 14:16
  • @HovercraftFullOfEels Hey, sorry I was trying out your suggestions! That's another good point about the array - I've changed the max to one less - maybe it wasn't throwing an exception because of the expanding ability of an ArrayList? – user1058210 Jan 01 '12 at 14:29
  • @user1058210: ArrayList can expand, but a list is no larger than its `size()` method says it is. I suspect that no exception was occurring because this method was never called. – Hovercraft Full Of Eels Jan 01 '12 at 14:31
  • @HovercraftFullOfEels I have added in an SSCE now – user1058210 Jan 01 '12 at 14:42

6 Answers6

61

Problems with your code and suggestions:

  • Again you need to change the preferredSize of the component (here the Graph JPanel), not the size
  • Don't set the JFrame's bounds.
  • Call pack() on your JFrame after adding components to it and before calling setVisible(true)
  • Your foreach loop won't work since the size of your ArrayList is 0 (test it to see that this is correct). Instead use a for loop going from 0 to 10.
  • You should not have program logic inside of your paintComponent(...) method but only painting code. So I would make the ArrayList a class variable and fill it inside of the class's constructor.

For example:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.*;

@SuppressWarnings("serial")
public class DrawGraph extends JPanel {
   private static final int MAX_SCORE = 20;
   private static final int PREF_W = 800;
   private static final int PREF_H = 650;
   private static final int BORDER_GAP = 30;
   private static final Color GRAPH_COLOR = Color.green;
   private static final Color GRAPH_POINT_COLOR = new Color(150, 50, 50, 180);
   private static final Stroke GRAPH_STROKE = new BasicStroke(3f);
   private static final int GRAPH_POINT_WIDTH = 12;
   private static final int Y_HATCH_CNT = 10;
   private List<Integer> scores;

   public DrawGraph(List<Integer> scores) {
      this.scores = scores;
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D)g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

      double xScale = ((double) getWidth() - 2 * BORDER_GAP) / (scores.size() - 1);
      double yScale = ((double) getHeight() - 2 * BORDER_GAP) / (MAX_SCORE - 1);

      List<Point> graphPoints = new ArrayList<Point>();
      for (int i = 0; i < scores.size(); i++) {
         int x1 = (int) (i * xScale + BORDER_GAP);
         int y1 = (int) ((MAX_SCORE - scores.get(i)) * yScale + BORDER_GAP);
         graphPoints.add(new Point(x1, y1));
      }

      // create x and y axes 
      g2.drawLine(BORDER_GAP, getHeight() - BORDER_GAP, BORDER_GAP, BORDER_GAP);
      g2.drawLine(BORDER_GAP, getHeight() - BORDER_GAP, getWidth() - BORDER_GAP, getHeight() - BORDER_GAP);

      // create hatch marks for y axis. 
      for (int i = 0; i < Y_HATCH_CNT; i++) {
         int x0 = BORDER_GAP;
         int x1 = GRAPH_POINT_WIDTH + BORDER_GAP;
         int y0 = getHeight() - (((i + 1) * (getHeight() - BORDER_GAP * 2)) / Y_HATCH_CNT + BORDER_GAP);
         int y1 = y0;
         g2.drawLine(x0, y0, x1, y1);
      }

      // and for x axis
      for (int i = 0; i < scores.size() - 1; i++) {
         int x0 = (i + 1) * (getWidth() - BORDER_GAP * 2) / (scores.size() - 1) + BORDER_GAP;
         int x1 = x0;
         int y0 = getHeight() - BORDER_GAP;
         int y1 = y0 - GRAPH_POINT_WIDTH;
         g2.drawLine(x0, y0, x1, y1);
      }

      Stroke oldStroke = g2.getStroke();
      g2.setColor(GRAPH_COLOR);
      g2.setStroke(GRAPH_STROKE);
      for (int i = 0; i < graphPoints.size() - 1; i++) {
         int x1 = graphPoints.get(i).x;
         int y1 = graphPoints.get(i).y;
         int x2 = graphPoints.get(i + 1).x;
         int y2 = graphPoints.get(i + 1).y;
         g2.drawLine(x1, y1, x2, y2);         
      }

      g2.setStroke(oldStroke);      
      g2.setColor(GRAPH_POINT_COLOR);
      for (int i = 0; i < graphPoints.size(); i++) {
         int x = graphPoints.get(i).x - GRAPH_POINT_WIDTH / 2;
         int y = graphPoints.get(i).y - GRAPH_POINT_WIDTH / 2;;
         int ovalW = GRAPH_POINT_WIDTH;
         int ovalH = GRAPH_POINT_WIDTH;
         g2.fillOval(x, y, ovalW, ovalH);
      }
   }

   @Override
   public Dimension getPreferredSize() {
      return new Dimension(PREF_W, PREF_H);
   }

   private static void createAndShowGui() {
      List<Integer> scores = new ArrayList<Integer>();
      Random random = new Random();
      int maxDataPoints = 16;
      int maxScore = 20;
      for (int i = 0; i < maxDataPoints ; i++) {
         scores.add(random.nextInt(maxScore));
      }
      DrawGraph mainPanel = new DrawGraph(scores);

      JFrame frame = new JFrame("DrawGraph");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

Which will create a graph that looks like so: enter image description here

user1585643
  • 211
  • 1
  • 2
  • 9
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • It worked - as in the line graph was drawn!! Thanks for being so patient me! The only problem is that for some reason eclipse is not recognising the setPreferredSize method - I've checked the API and it's still fully supported - have no idea what's happening there but I can hopefully sort that out by myself - thanks again! – user1058210 Jan 01 '12 at 15:21
  • @user1058210: You must be using it wrong. Please show us how you try to call it. Are you calling it outside of a constructor or method block? – Hovercraft Full Of Eels Jan 01 '12 at 15:28
45

Just complementing Hovercraft Full Of Eels's solution:

I reworked his code, tweaked it a bit, adding a grid, axis labels and now the Y-axis goes from the minimum value present up to the maximum value. I planned on adding a couple of getters/setters but I didn't need them, you can add them if you want.

Here is the Gist link, I'll also paste the code below: GraphPanel on Gist

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class GraphPanel extends JPanel {

    private int width = 800;
    private int heigth = 400;
    private int padding = 25;
    private int labelPadding = 25;
    private Color lineColor = new Color(44, 102, 230, 180);
    private Color pointColor = new Color(100, 100, 100, 180);
    private Color gridColor = new Color(200, 200, 200, 200);
    private static final Stroke GRAPH_STROKE = new BasicStroke(2f);
    private int pointWidth = 4;
    private int numberYDivisions = 10;
    private List<Double> scores;

    public GraphPanel(List<Double> scores) {
        this.scores = scores;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        double xScale = ((double) getWidth() - (2 * padding) - labelPadding) / (scores.size() - 1);
        double yScale = ((double) getHeight() - 2 * padding - labelPadding) / (getMaxScore() - getMinScore());

        List<Point> graphPoints = new ArrayList<>();
        for (int i = 0; i < scores.size(); i++) {
            int x1 = (int) (i * xScale + padding + labelPadding);
            int y1 = (int) ((getMaxScore() - scores.get(i)) * yScale + padding);
            graphPoints.add(new Point(x1, y1));
        }

        // draw white background
        g2.setColor(Color.WHITE);
        g2.fillRect(padding + labelPadding, padding, getWidth() - (2 * padding) - labelPadding, getHeight() - 2 * padding - labelPadding);
        g2.setColor(Color.BLACK);

        // create hatch marks and grid lines for y axis.
        for (int i = 0; i < numberYDivisions + 1; i++) {
            int x0 = padding + labelPadding;
            int x1 = pointWidth + padding + labelPadding;
            int y0 = getHeight() - ((i * (getHeight() - padding * 2 - labelPadding)) / numberYDivisions + padding + labelPadding);
            int y1 = y0;
            if (scores.size() > 0) {
                g2.setColor(gridColor);
                g2.drawLine(padding + labelPadding + 1 + pointWidth, y0, getWidth() - padding, y1);
                g2.setColor(Color.BLACK);
                String yLabel = ((int) ((getMinScore() + (getMaxScore() - getMinScore()) * ((i * 1.0) / numberYDivisions)) * 100)) / 100.0 + "";
                FontMetrics metrics = g2.getFontMetrics();
                int labelWidth = metrics.stringWidth(yLabel);
                g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (metrics.getHeight() / 2) - 3);
            }
            g2.drawLine(x0, y0, x1, y1);
        }

        // and for x axis
        for (int i = 0; i < scores.size(); i++) {
            if (scores.size() > 1) {
                int x0 = i * (getWidth() - padding * 2 - labelPadding) / (scores.size() - 1) + padding + labelPadding;
                int x1 = x0;
                int y0 = getHeight() - padding - labelPadding;
                int y1 = y0 - pointWidth;
                if ((i % ((int) ((scores.size() / 20.0)) + 1)) == 0) {
                    g2.setColor(gridColor);
                    g2.drawLine(x0, getHeight() - padding - labelPadding - 1 - pointWidth, x1, padding);
                    g2.setColor(Color.BLACK);
                    String xLabel = i + "";
                    FontMetrics metrics = g2.getFontMetrics();
                    int labelWidth = metrics.stringWidth(xLabel);
                    g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3);
                }
                g2.drawLine(x0, y0, x1, y1);
            }
        }

        // create x and y axes 
        g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, padding + labelPadding, padding);
        g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, getWidth() - padding, getHeight() - padding - labelPadding);

        Stroke oldStroke = g2.getStroke();
        g2.setColor(lineColor);
        g2.setStroke(GRAPH_STROKE);
        for (int i = 0; i < graphPoints.size() - 1; i++) {
            int x1 = graphPoints.get(i).x;
            int y1 = graphPoints.get(i).y;
            int x2 = graphPoints.get(i + 1).x;
            int y2 = graphPoints.get(i + 1).y;
            g2.drawLine(x1, y1, x2, y2);
        }

        g2.setStroke(oldStroke);
        g2.setColor(pointColor);
        for (int i = 0; i < graphPoints.size(); i++) {
            int x = graphPoints.get(i).x - pointWidth / 2;
            int y = graphPoints.get(i).y - pointWidth / 2;
            int ovalW = pointWidth;
            int ovalH = pointWidth;
            g2.fillOval(x, y, ovalW, ovalH);
        }
    }

//    @Override
//    public Dimension getPreferredSize() {
//        return new Dimension(width, heigth);
//    }
    private double getMinScore() {
        double minScore = Double.MAX_VALUE;
        for (Double score : scores) {
            minScore = Math.min(minScore, score);
        }
        return minScore;
    }

    private double getMaxScore() {
        double maxScore = Double.MIN_VALUE;
        for (Double score : scores) {
            maxScore = Math.max(maxScore, score);
        }
        return maxScore;
    }

    public void setScores(List<Double> scores) {
        this.scores = scores;
        invalidate();
        this.repaint();
    }

    public List<Double> getScores() {
        return scores;
    }

    private static void createAndShowGui() {
        List<Double> scores = new ArrayList<>();
        Random random = new Random();
        int maxDataPoints = 40;
        int maxScore = 10;
        for (int i = 0; i < maxDataPoints; i++) {
            scores.add((double) random.nextDouble() * maxScore);
//            scores.add((double) i);
        }
        GraphPanel mainPanel = new GraphPanel(scores);
        mainPanel.setPreferredSize(new Dimension(800, 600));
        JFrame frame = new JFrame("DrawGraph");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

It looks like this: Example pic

Rodrigo Castro
  • 1,573
  • 14
  • 38
  • 1
    You can define something like `int graphWidth = getWidth() - 2 * padding - labelPadding; int graphHeight = getHeight() - 2 * padding - labelPadding;` to avoid calculating position every time. – xi.lin Apr 26 '16 at 15:45
10

There exist many open source projects that handle all the drawing of line charts for you with a couple of lines of code. Here's how you can draw a line chart from data in a couple text (CSV) file with the XChart library. Disclaimer: I'm the lead developer of the project.

In this example, two text files exist in ./CSV/CSVChartRows/. Notice that each row in the files represents a data point to be plotted and that each file represents a different series. series1 contains x, y, and error bar data, whereas series2 contains just x and y, data.

series1.csv

1,12,1.4
2,34,1.12
3,56,1.21
4,47,1.5

series2.csv

1,56
2,34
3,12
4,26

Source Code

public class CSVChartRows {

  public static void main(String[] args) throws Exception {

    // import chart from a folder containing CSV files
    XYChart chart = CSVImporter.getChartFromCSVDir("./CSV/CSVChartRows/", DataOrientation.Rows, 600, 400);

    // Show it
    new SwingWrapper(chart).displayChart();
  }
}

Resulting Plot

enter image description here

herrtim
  • 2,697
  • 1
  • 26
  • 36
10

Or simply use the JFreechart library - http://www.jfree.org/jfreechart/ .

DejanLekic
  • 18,787
  • 4
  • 46
  • 77
  • 3
    1+ yep, probably the "correct" solution, assuming that this isn't subject to typical homework restrictions. – Hovercraft Full Of Eels Jan 01 '12 at 15:08
  • 1
    However, nothing like avoiding a whole library for something simple. The previous answer would do the job for something simple. – toto_tico Feb 08 '16 at 20:43
  • 4
    Using a library when it exists to do the job is almost always a better solution. Even for simple cases. It takes less time for a better result. – Florian F May 21 '16 at 13:23
3

Override the paintComponent method of your panel so you can custom draw. Like this:

@Override
public void paintComponent(Graphics g) {
    Graphics2D gr = (Graphics2D) g; //this is if you want to use Graphics2D
    //now do the drawing here
    ...
}
Petar Minchev
  • 46,889
  • 11
  • 103
  • 119
  • Thanks for the help Petar - I've overridden the paintComponent method and implemented my code in the section you noted, but it is still not displaying. In my constructor I have also inserted setSize(500, 500) to ensure that the JPanel is visible, but unfortunately this hasn't fixed it either. I have edited my post to show what I have written in the main statement to see if that is of any help. – user1058210 Jan 01 '12 at 14:03
  • 1
    @user1058210: setSize is usually not followed by most layout managers whereas preferredSizes usually are. You'll want to show your new code with the paintComponent method shown. Also, are you sure that your paintComponent method is a true method override? To be sure, place the `@Override` annotation above it to let the compiler check this for you. Consider creating a small compilable runnable program that demonstrates your problem, an [sscce](http://sscce), and then posting it here if you're still stuck. – Hovercraft Full Of Eels Jan 01 '12 at 14:06
  • @HovercraftFullOfEels thanks for the help - I tried implementing the preferredSize method but it is apparently deprecated. What do you mean by "you'll want to show your new code with the paintComponent method"? Do you mean I should make an explicit reference to it in my main like: graph.paintComponent(g)? I thought the paintComponent method was something that was called automatically and after overriding would call my code automatically. Thanks for the point about the scaling! I had not realised this. I will make an sscce now. – user1058210 Jan 01 '12 at 14:25
  • To set a component's preferredSize, you would either call `setPreferredSize(...)` (sorry Kleopatra), or override `getPreferredSize()`. And yes paintComponent and paint are methods that you do not call directly. I was talking about your own paintComponent override method that you state in your first comment to this answer -- you need to show it to us. – Hovercraft Full Of Eels Jan 01 '12 at 14:29
  • @user1058210 - You should call `repaint()` method on the panel. Then automatically `paintComponent` will be invoked. You shouldn't call it directly. – Petar Minchev Jan 01 '12 at 14:30
  • @HovercraftFullOfEels I tried the setPreferredSize(...) but it doesn't recognise that - it does recognise getPreferredSize() though. I tried calling graph.repaint() before I made the frame visible too but still no luck – user1058210 Jan 01 '12 at 14:41
  • @user1058210: see my comment above about preferred size. – Hovercraft Full Of Eels Jan 01 '12 at 16:21
2

Hovercraft Full Of Eels' answer is very good, but i had to change it a bit in order to get it working on my program:

int y1 = (int) ((this.height - 2 * BORDER_GAP) - (values.get(i) * yScale - BORDER_GAP));

instead of

int y1 = (int) (scores.get(i) * yScale + BORDER_GAP);

because if i used his way the graphic would be upside down

(you'd see it if you used hardcoded values (e.g 1,3,5,7,9) instead of random values)

DarkW
  • 575
  • 1
  • 6
  • 18
  • I had the same issue. Subtracting from height inverts the graph because it is really drawn in reverse. – Rarw May 14 '14 at 15:49