package ru.orangesoftware.financisto2.view; import java.util.List; import ru.orangesoftware.financisto2.R; import ru.orangesoftware.financisto2.graph.Report2DPoint; import ru.orangesoftware.financisto2.model.Currency; import ru.orangesoftware.financisto2.model.PeriodValue; import ru.orangesoftware.financisto2.utils.MyPreferences; import ru.orangesoftware.financisto2.utils.Utils; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Paint.Align; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.graphics.drawable.shapes.RectShape; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * Report 2D chart view. View to draw dynamic 2D reports. * @author Abdsandryk */ public class Report2DChartView extends View { // background and axis elements private ShapeDrawable background; private ShapeDrawable[] axis; private ShapeDrawable[] pointsShapes; // graphics configuration private int padding = 10; private int graphPadding = 10; private int txtHeight = 12; private int selected = -1; // List of points to be drawn List<Report2DPoint> points; // points to represent as references in the chart private Report2DPoint meanPoint; private Report2DPoint zeroPoint; // statistics data double max, min, absMin, absMax = 0; double meanNonNull = 0; // reference currency private Currency currency; // Paints private Paint labelPaint; private Paint gridPaint; private Paint currencyPaint; private Paint amountPaint; private Paint pathPaint; private Paint valuesPaint; /* * True if all the points are positive or negative. In this case, the * chart will reflect all data in modulus. * False if there are positive and negative points to be represented. */ private boolean absoluteCalculation = true; // space to draw labels vertically private int xSpace = 16; // space to draw labels and information bellow the chart private int ySpace = 44; // Colors private int bgColor = 0xFF010101; private int bgChartColor = Color.BLACK; private int axisColor = 0xFFDEDEDE; private int pathColor = Color.YELLOW; private int txtColor = 0xFFBBBBBB; private int pointColor = Color.YELLOW; private int selectedPointPosColor = Color.GREEN; private int selectedPointNegColor = Color.RED; private int gridColor = 0xFF222222; public static final int meanColor = 0xFF206DED; // flag to indicate if the view was initialized private boolean initialized = false; /** * Default view constructor. * @param context The activity context. */ public Report2DChartView(Context context) { super(context); init(); } /** * Default view constructor. * @param context The activity context. * @param attrs Attributes. */ public Report2DChartView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * Default view constructor. * @param context The activity context. * @param attrs Attributes * @param defStyle */ public Report2DChartView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } /** * Initialize drawing elements */ private void init() { labelPaint = new Paint(); labelPaint.setColor(txtColor); labelPaint.setTextSize(10); labelPaint.setStyle(Paint.Style.FILL); labelPaint.setTextAlign(Align.CENTER); labelPaint.setAntiAlias(true); currencyPaint = new Paint(); currencyPaint.setAntiAlias(true); currencyPaint.setColor(txtColor); currencyPaint.setTextAlign(Align.CENTER); currencyPaint.setTextSize(12); amountPaint = new Paint(); amountPaint.setAntiAlias(true); amountPaint.setColor(txtColor); amountPaint.setTextAlign(Align.LEFT); amountPaint.setTextSize(12); valuesPaint = new Paint(); valuesPaint.setAntiAlias(true); valuesPaint.setTextAlign(Align.CENTER); valuesPaint.setTextSize(12); pathPaint = new Paint(); pathPaint.setColor(pathColor); pathPaint.setStrokeWidth(1); pathPaint.setAntiAlias(true); gridPaint = new Paint(); gridPaint.setColor(gridColor); // plot chart background background = new ShapeDrawable(new RectShape()); background = new ShapeDrawable(new RectShape()); background.getPaint().setColor(bgColor); axis = new ShapeDrawable[3]; // 0 = background axis[0] = new ShapeDrawable(new RectShape()); axis[0].getPaint().setColor(axisColor); axis[1] = new ShapeDrawable(new RectShape()); axis[1].getPaint().setColor(bgChartColor); } /** * Refresh the view. Call onDraw(canvas). */ public void refresh() { // call onDraw to refresh view chart this.invalidate(); } /** * Set data to plot. * @param points Points representing the data to plot. * @param max Maximum value. * @param min Minimum value. * @param absMax Maximum value in modulus. * @param absMin Minimum value in modulus. * @param meanNonNull Mean value excluding null values. */ public void setDataToPlot(List<Report2DPoint> points, double max, double min, double absMax, double absMin, double meanNonNull) { this.points = points; this.max = max; this.min = min; this.absMax = absMax; this.absMin = absMin; this.meanNonNull = meanNonNull; selected = -1; // decide if the path will be represented in modulus or not. if ((min*max>=0)) { // all points negative or positive absoluteCalculation = true; } else { // negative and positive points will be represented. absoluteCalculation = false; } // call onDraw to refresh view chart this.invalidate(); } /** * Called to refresh data in chart. */ protected void onDraw(Canvas canvas) { if (points!=null) { calculatePointsPosition(); } if (initialized) { drawChartAxis(canvas); drawPath(canvas); drawPoints(canvas); if (selected>=0) { drawSelectedPoint(canvas); } drawLabels(canvas); } } /** * Draw the background and the grid. * @param canvas Canvas to draw the chart. */ private void drawChartAxis(Canvas canvas) { int w = this.getWidth(); int h = this.getHeight(); background.setBounds(0,0,w,h); axis[0].setBounds(xSpace+padding,padding,w-padding,h-padding-ySpace); axis[1].setBounds(xSpace+padding+1,padding,w-padding,h-padding-ySpace-1); background.draw(canvas); axis[0].draw(canvas); axis[1].draw(canvas); if (points!=null && points.size()>0) { // draw grid lines for (int i=1; i<points.size(); i++) { canvas.drawLine(points.get(i).getX(), padding, points.get(i).getX(), getHeight()-padding-ySpace-1, gridPaint); } // draw month labels if (points.size()<=12) { for (int i=0; i<points.size(); i++) { labelPaint.setTextSize(10); canvas.drawText(points.get(i).getMonthShortString(this.getContext()), points.get(i).getX(), getHeight()-ySpace-padding+txtHeight, labelPaint); labelPaint.setTextSize(9); canvas.drawText(points.get(i).getYearString(), points.get(i).getX(), getHeight()-ySpace-padding+2*txtHeight-1, labelPaint); } } else { labelPaint.setTextSize(10); canvas.drawText(points.get(0).getMonthShortString(this.getContext()), points.get(0).getX(), getHeight()-ySpace-padding+txtHeight, labelPaint); canvas.drawText(points.get(points.size()-1).getMonthShortString(this.getContext()), points.get(points.size()-1).getX(), getHeight()-ySpace-padding+txtHeight, labelPaint); labelPaint.setTextSize(9); canvas.drawText(points.get(0).getYearString(), points.get(0).getX(), getHeight()-ySpace-padding+2*txtHeight-1, labelPaint); canvas.drawText(points.get(points.size()-1).getYearString(), points.get(points.size()-1).getX(), getHeight()-ySpace-padding+2*txtHeight-1, labelPaint); labelPaint.setTextSize(12); canvas.drawText(getResources().getString(R.string.period), padding+xSpace+(getWidth()-xSpace-2*padding)/2, getHeight()-ySpace-padding/2+txtHeight, labelPaint); } } canvas.drawLine(padding+xSpace+1, padding, getWidth()-padding, padding, gridPaint); // draw mean gridPaint.setColor(meanColor); canvas.drawLine(padding+xSpace+1, meanPoint.getY(), getWidth()-padding, meanPoint.getY(), gridPaint); gridPaint.setColor(gridColor); if (zeroPoint!=null) { canvas.drawLine(padding+xSpace+1, zeroPoint.getY(), getWidth()-padding, zeroPoint.getY(), gridPaint); } } /** * Draw axis labels. * @param canvas Canvas to draw the chart. */ private void drawLabels(Canvas canvas) { Rect currencyBounds = new Rect(); currencyPaint.getTextBounds(currency.symbol, 0, currency.symbol.length(), currencyBounds); canvas.drawText(currency.symbol, padding+xSpace-currencyBounds.width()-5, padding+currencyBounds.height(), currencyPaint); // Draw point coordinates currencyPaint.setTextAlign(Align.LEFT); currencyPaint.setTextSize(12); canvas.drawText("x:", padding, getHeight()-padding, currencyPaint); canvas.drawText("y:", padding+getWidth()/2, getHeight()-padding, currencyPaint); // set desired drawing location int amountX = 0; int amountY = 0; String amount = getResources().getString(R.string.amount); // draw bounding rectangle before rotating text Rect amountBounds = new Rect(); amountPaint.getTextBounds(amount, 0, amount.length(), amountBounds); if (amountBounds.width()>0) { amountY = amountBounds.width()+(getHeight()-ySpace-amountBounds.width())/2; } if (amountBounds.height()>0) { amountX = amountBounds.height()+(padding+xSpace-amountBounds.height())/2; } // rotate the canvas on center of the text to draw canvas.rotate(-90, amountX, amountY); // draw the rotated text canvas.drawText(amount, amountX, amountY, amountPaint); // undo the rotate canvas.restore(); } /** * Draw evolution path. * @param canvas Canvas to draw the chart. */ private void drawPath(Canvas canvas) { for (int i=0; i<points.size()-1; i++) { canvas.drawLine(points.get(i).getX(), points.get(i).getY(), points.get(i+1).getX(), points.get(i+1).getY(), pathPaint); } } /** * Draw points in chart. * @param canvas Canvas to draw the chart. */ private void drawPoints(Canvas canvas) { for (int i=0; i<pointsShapes.length; i++) { pointsShapes[i].draw(canvas); } } /** * Highlight selected point. * @param canvas Canvas to draw the chart. */ private void drawSelectedPoint(Canvas canvas) { if (points.get(selected).isNegative()) { valuesPaint.setColor(selectedPointNegColor); } else { valuesPaint.setColor(selectedPointPosColor); } String x = points.get(selected).getMonthLongString(this.getContext())+" "+points.get(selected).getYearString(); canvas.drawText(x, 30 + (getWidth()/2-30)/2, getHeight()-padding, valuesPaint); String value = ""; if (absoluteCalculation) { long v = (long)points.get(selected).getPointData().getValue(); value = Utils.amountToString(currency, v>0?v:-v); } else { value = Utils.amountToString(currency, (long)points.get(selected).getPointData().getValue()); } canvas.drawText(value, getWidth()/2+30+ (getWidth()/2-30)/2, getHeight()-padding, valuesPaint); } /** * Calculate the position of points in the chart. */ private void calculatePointsPosition() { int w = this.getWidth(); int h = this.getHeight(); pointsShapes = new ShapeDrawable[2*points.size()]; int x; Double y; double value; double mean = 0; for (int i=0; i<points.size(); i++) { if (absoluteCalculation) { value = Math.abs(points.get(i).getPointData().getValue()); y = h - ySpace - padding - graphPadding - (value-absMin)*(h-ySpace-2*padding-2*graphPadding)/(absMax-absMin); } else { value = points.get(i).getPointData().getValue(); y = h - ySpace - padding - graphPadding - (value-min)*(h-ySpace-2*padding-2*graphPadding)/(max-min); } x = xSpace+padding+(w-xSpace-2*padding)/(points.size()-1)*i; mean += value; points.get(i).setX(x); points.get(i).setY((int)Math.round(y)); pointsShapes[i] = new ShapeDrawable(new OvalShape()); if (selected==i) { if (points.get(i).isNegative()) { pointsShapes[i].getPaint().setColor(selectedPointNegColor); } else { pointsShapes[i].getPaint().setColor(selectedPointPosColor); } } else { pointsShapes[i].getPaint().setColor(pointColor); } pointsShapes[i].setBounds((int)points.get(i).getX()-4, (int)points.get(i).getY()-4, (int)points.get(i).getX()+4, (int)points.get(i).getY()+4); pointsShapes[i+points.size()] = new ShapeDrawable(new OvalShape()); pointsShapes[i+points.size()].getPaint().setColor(Color.BLACK); pointsShapes[i+points.size()].setBounds((int)points.get(i).getX()-2, (int)points.get(i).getY()-2, (int)points.get(i).getX()+2, (int)points.get(i).getY()+2); } boolean considerNulls = MyPreferences.considerNullResultsInReport(this.getContext()); if (considerNulls) { mean = mean/points.size(); } else { mean = meanNonNull; } meanPoint = new Report2DPoint(new PeriodValue(null, 0)); if (absoluteCalculation) { mean = Math.abs(mean); meanPoint.setY((int)(h - ySpace - padding - graphPadding - (mean-absMin)*(h-ySpace-2*padding-2*graphPadding)/(absMax-absMin))); if (absMin<=0 && absMax>=0) { zeroPoint = new Report2DPoint(new PeriodValue(null, 0)); zeroPoint.setY((int)(h - ySpace - padding - graphPadding - (-absMin)*(h-ySpace-2*padding-2*graphPadding)/(absMax-absMin))); } else { zeroPoint = null; } } else { meanPoint.setY((int)(h - ySpace - padding - graphPadding - (mean-min)*(h-ySpace-2*padding-2*graphPadding)/(max-min))); if (absMin<=0 && absMax>=0) { zeroPoint = new Report2DPoint(new PeriodValue(null, 0)); zeroPoint.setY((int)(h - ySpace - padding - graphPadding - (-min)*(h-ySpace-2*padding-2*graphPadding)/(max-min))); } else { zeroPoint = null; } } initialized = true; } /** * @return True if the chart is plot with values in modulus, false otherwise. */ public boolean isAbsoluteCalculation() { return absoluteCalculation; } /** * Set flag to indicate if the char is plot with values in modulus or not. * @param absoluteCalculation */ public void setAbsoluteCalculation(boolean absoluteCalculation) { this.absoluteCalculation = absoluteCalculation; } /** * Set the chart reference currency. * @param currency Reference currency. */ public void setCurrency(Currency currency) { this.currency = currency; } @Override public boolean onTouchEvent(MotionEvent event) { // get point to highlight as selection int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { if (event.getY()>padding && event.getY()<getHeight()-padding-ySpace) { float dmin = getWidth(); float d = 0; int sel = -1; for (int i=0;i<points.size();i++) { d = Math.abs(points.get(i).getX()-event.getX()); if (d<dmin) { dmin = d; sel = i; } } if (sel>=0) { selected = sel; invalidate(); } } } return true; } }