Search code examples
androidandroid-studioandroid-layoutcanvaspaint

Drawing a custom line graph with Paint on Android


I'm trying to draw a line graph using Canvas and Paint on an Android App

First I generate some data points with generateData() which creates random values for the Y data point and i times 50 for the X data point.

I expected each X point to be seperated by 50 pixels (as a scale) and thus draw a similar graph like this:

Coordinates for the Canvas

The aplication class

    public class Plotter extends View {

    private List<Float> xPosList, yPosList;
    private List<Path> pathList;
    private Path path;
    private Paint paint;

    private ConstraintLayout cl;
    private TextView stockPriceView;


    public Plotter(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        this.xPosList = new ArrayList<>();
        this.yPosList = new ArrayList<>();
        this.pathList = new ArrayList<>();
        this.paint = new Paint();
        this.paint.setStrokeWidth(20);
        this.paint.setColor(Color.GREEN);


        generateData();
    }

    /***
     * Generates random float data points from 5 to 100 and creates a path to plot
     */
    private void generateData() {

        int min = 5;
        int max = 100;
        double random = 0;

        float xPos = 0;
        float yPos = 0;

        for (int i = 1; i <= 20; i++) {
            random = min + Math.random() * (max - min);
            xPos = 50 * i;                                                                          //50 pixels
            yPos = (float)random;

            this.xPosList.add(xPos);
            this.yPosList.add(yPos);

            path = new Path();                                                                      //Create path
            path.moveTo(xPos, yPos);                                                                //Add values to path
            this.pathList.add(path);                                                                //Add path to pathList
        }
    }

    /***
     * Clears the points list
     */
    private void clearData() {
        this.xPosList.clear();
        this.yPosList.clear();
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                clearData();
                generateData();
                break;
            case MotionEvent.ACTION_UP:
                invalidate();                                                                       //Refresh canvas
                break;
        }
        return true;                                                                                //Activate event
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Paint p = new Paint();
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

        p.setColor(Color.GREEN);
        p.setStrokeWidth(10);
        p.setStyle(Paint.Style.FILL);
        /***
         * Better use 50 by 50 pixels
         */

        float startX = 0;                                                                           //Start graph at bottom left
        float startY = canvas.getHeight();                                                          //Start at the bottom (max height)

        float nextX = 0;
        float nextY = 0;

        for (int i = 0; i < this.xPosList.size(); i++){

            nextX = this.xPosList.get(i);
            //TODO: Find a better function
            nextY = (canvas.getHeight() - this.yPosList.get(i));
            canvas.drawLine(startX, startY, nextX, nextY, p);                                       //Draw segment

            startX = nextX;                                                                         //Save previous X point
            startY = nextY;                                                                         //Save previous Y point
        }

        //TODO: Find a better way to manage this
        cl = (ConstraintLayout) ((ViewGroup)this.getParent());                                      //get parent Layout
        this.stockPriceView = cl.findViewById(R.id.stockPriceText);                                 //access the sibling
        if (this.stockPriceView != null) {
            this.stockPriceView.setText((nextY)+"");                                           //Write number
        }
    }
}

Although my current output is not far from desired, it is not correct.

Ignore the negative value


Solution

  • It looks like your max y value will be 100px and that is not very much and pales in comparison to the 1000px max x value. You need to convert the y values to dps or some other scaling value to fill up more of your view.

    In detail, within generateData() x will be set to a range of 50..1,000 in increments of 50 while y values will be randomly assigned values between 5 and 100. In the drawing code, you use these values for drawLine() which takes pixels for its arguments. 1,000 pixels in the x-direction will get you some distance on most devices (333.3 dps on a device with a density of 3 pixels/dp), but a maximum value of 100 for the y-value will get you at most 33.3 dps on the same device - that is 1/10 the distance.

    Let's say that you want the x values to span the width of the Plotter view and, likewise for the y-axis, the values should span the height. So, when x == 0, the x value for drawLine() should also be 0. When the x-value is 1,000, the x value for drawLine() should be with width of the Plotter view or

    xview = viewWidth * x/1,000

    Likewise, for the y value for drawLine():

    yview = viewHeight * y/100

    Change the drawing for loop to something like this:

    float viewWidth = getWidth();
    float viewHeight = getHeight();
    for (int i = 0; i < this.xPosList.size(); i++) {
    
        nextX = this.xPosList.get(i);
        nextY = this.yPosList.get(i);
        canvas.drawLine(viewWidth * startX / 1000, viewHeight - (viewHeight * startY / 100),
                viewWidth * nextX / 1000, viewHeight - (viewHeight * nextY / 100), p);                                       //Draw segment
    
        startX = nextX;                                                                         //Save previous X point
        startY = nextY;                                                                         //Save previous Y point
    }
    

    and you will get a plot that looks something like this:

    enter image description here

    Here is another way using the canvas translate() method:

    float viewWidth = getWidth();
    float viewHeight = getHeight();
    canvas.save();
    // Flip the canvas vertically.
    canvas.scale(1f, -1f, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
    for (int i = 0; i < this.xPosList.size(); i++) {
    
        nextX = this.xPosList.get(i);
        nextY = this.yPosList.get(i);
        canvas.drawLine(viewWidth * startX / 1000, viewHeight * startY / 100,
                viewWidth * nextX / 1000, viewHeight * nextY / 100, p);                                       //Draw segment
    
        startX = nextX;                                                                         //Save previous X point
        startY = nextY;                                                                         //Save previous Y point
    }
    canvas.restore();