/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.redgeek.android.eventrend.graph; import java.util.ArrayList; import net.redgeek.android.eventrend.Preferences; import net.redgeek.android.eventrend.datum.EntryEditActivity; import net.redgeek.android.eventrend.db.CategoryDbTable; import net.redgeek.android.eventrend.db.EntryDbTable; import net.redgeek.android.eventrend.db.EvenTrendDbAdapter; import net.redgeek.android.eventrend.primitives.Datapoint; import net.redgeek.android.eventrend.primitives.TimeSeries; import net.redgeek.android.eventrend.primitives.TimeSeriesCollector; import net.redgeek.android.eventrend.primitives.Tuple; import net.redgeek.android.eventrend.util.DateUtil; import net.redgeek.android.eventrend.util.Number; import net.redgeek.android.eventrend.util.DateUtil.Period; import android.app.AlertDialog; import android.app.Dialog; import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Region; public class Graph { // Set by the GraphView when lookup up a datapoint/series from screen // coordinates: public TimeSeries mSelectedSeries; public Datapoint mSelectedDatapoint; public int mSelectedColor; // Dialogs private static final int DIALOG_POINT_INFO = 0; private static final int DIALOG_RANGE_INFO = 1; // UI elements private Path mAxis; private Paint mAxisPaint; private Paint mBackgroundPaint; private Paint mMarkerPaint; private Paint mLabelPrimaryPaint; private Paint mLabelHighlightPaint; // Private data private Context mCtx; private TimeSeriesCollector mTSC; private Transformation mTransform; private DateUtil mDates; private int mDecimals; private Tuple mGraphSize; private Tuple mPlotSize; private long mStartMS; private long mEndMS; private Tuple mPlotOffset; private Tuple mBoundsMins; private Tuple mBoundsMaxs; private Period mSpan; private boolean mShowTrends = true; private boolean mShowGoals = true; private boolean mShowMarkers = false; public Graph(Context context, TimeSeriesCollector tsc, float viewWidth, float viewHeight) { mTSC = tsc; mCtx = context; setupData(viewWidth, viewHeight); setupUI(); } private void setupData(float viewWidth, float viewHeight) { mGraphSize = new Tuple(); mPlotSize = new Tuple(); mTransform = new Transformation(); mDates = new DateUtil(); mPlotOffset = new Tuple(GraphView.LEFT_MARGIN, GraphView.TOP_MARGIN + GraphView.PLOT_TOP_PAD); mBoundsMins = new Tuple(); mBoundsMaxs = new Tuple(); setGraphSize(viewWidth, viewHeight); mDecimals = Preferences.getDecimalPlaces(mCtx); } public void setDecimals(int decimals) { mDecimals = decimals; } private void setupUI() { mAxisPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mAxisPaint.setStyle(Paint.Style.STROKE); mAxisPaint.setStrokeWidth(GraphView.AXIS_WIDTH); mMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mMarkerPaint.setStyle(Paint.Style.STROKE); mMarkerPaint.setStrokeWidth(1); mLabelPrimaryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLabelPrimaryPaint.setStyle(Paint.Style.STROKE); mLabelPrimaryPaint.setStrokeWidth(GraphView.LABEL_WIDTH); mLabelHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLabelHighlightPaint.setStyle(Paint.Style.STROKE); mLabelHighlightPaint.setStrokeWidth(GraphView.LABEL_WIDTH); mBackgroundPaint = new Paint(); mBackgroundPaint.setStyle(Paint.Style.FILL); setColorScheme(); mAxis = new Path(); } public void setColorScheme() { if (Preferences.getDefaultGraphIsBlack(mCtx) == true) { mAxisPaint.setColor(Color.LTGRAY); mMarkerPaint.setColor(Color.DKGRAY); mLabelPrimaryPaint.setColor(Color.LTGRAY); mLabelHighlightPaint.setColor(Color.WHITE); mBackgroundPaint.setColor(Color.BLACK); } else { mAxisPaint.setColor(Color.DKGRAY); mMarkerPaint.setColor(Color.LTGRAY); mLabelPrimaryPaint.setColor(Color.DKGRAY); mLabelHighlightPaint.setColor(Color.BLACK); mBackgroundPaint.setColor(Color.WHITE); } } public float getXMargins() { return GraphView.LEFT_MARGIN + GraphView.RIGHT_MARGIN; } public float getYMargins() { return GraphView.TOP_MARGIN + GraphView.BOTTOM_MARGIN; } public void setGraphSize(float width, float height) { mGraphSize = new Tuple(width, height); mPlotSize.x = mGraphSize.x - getXMargins(); mPlotSize.y = mGraphSize.y - getYMargins() - GraphView.PLOT_TOP_PAD - GraphView.PLOT_BOTTOM_PAD; if (width == 0.0f) mPlotSize.x = 1; if (height == 0.0f) mPlotSize.y = 1; mTransform.setPlotSize(mPlotSize); } public Period getSpan() { return mSpan; } public long getGraphStart() { return mStartMS; } public long getGraphEnd() { return mEndMS; } public void setGraphRange(long start, long end) { mStartMS = start; mEndMS = end; resetBounds(start, end); } public Tuple getGraphSize() { return mGraphSize; } public Tuple getPlotOffset() { return mPlotOffset; } public void viewTrends(boolean b) { mShowTrends = b; } public void viewGoals(boolean b) { mShowGoals = b; } public void viewMarkers(boolean b) { mShowMarkers = b; } public TimeSeriesCollector getTimeSeriesCollector() { return mTSC; } public void resetBounds(long milliStart, long milliEnd) { mStartMS = milliStart; mEndMS = milliEnd; mBoundsMins.x = (float) mStartMS; mBoundsMaxs.x = (float) mEndMS; mBoundsMins.y = 0; mBoundsMaxs.y = mPlotSize.y; } public void setupRange(TimeSeries ts, boolean showGoals) { mBoundsMins.y = ts.getVisibleValueMin(); mBoundsMaxs.y = ts.getVisibleValueMax(); if (mBoundsMins.y >= mBoundsMaxs.y) { mBoundsMins.y = 0; mBoundsMaxs.y = mPlotSize.y; } if (showGoals) { float goal = ts.getDbRow().getGoal(); if (goal > mBoundsMaxs.y) mBoundsMaxs.y = goal; if (goal < mBoundsMins.y) mBoundsMins.y = goal; } if (mBoundsMins.x == mBoundsMaxs.x) { mBoundsMins.x--; mBoundsMaxs.x++; } if (Math.abs(mBoundsMins.y - mBoundsMaxs.y) < GraphView.MINIMUM_DELTA) { mBoundsMins.y--; mBoundsMaxs.y++; } return; } public synchronized void plot(Canvas canvas) { int offset; if (canvas == null) return; if (mGraphSize.x <= 0 || mGraphSize.y <= 0 || mPlotSize.x <= 0 || mPlotSize.y <= 0) return; if (mTSC.lock() == false) return; TimeSeries ts = null; ArrayList<TimeSeries> series = mTSC.getAllEnabledSeries(); mTransform.clear(); drawBaseAxis(canvas); drawXLabels(canvas); offset = 0; for (int i = 0; i < series.size(); i++) { ts = series.get(i); if (ts.isEnabled() == false) continue; canvas.save(); drawPlotClipRect(canvas); setupRange(ts, mShowGoals); mTransform.setVirtualSize(mBoundsMins, mBoundsMaxs); mTransform.transformPath(ts.getDatapoints()); ts.setPointRadius(GraphView.POINT_RADIUS); ts.drawPath(canvas); if (mShowGoals == true) { drawGoal(canvas, ts, offset); } if (mShowTrends == true) { ts.drawTrend(canvas); } if (mShowMarkers == true) { drawTrendMarker(canvas, ts); } canvas.restore(); drawYLabels(canvas, ts, offset); offset++; } mTSC.unlock(); return; } private void drawPlotClipRect(Canvas canvas) { canvas.clipRect(0, 0, mGraphSize.x, mGraphSize.y); canvas.clipRect(0 + GraphView.LEFT_MARGIN, 0 + GraphView.TOP_MARGIN, mGraphSize.x - GraphView.RIGHT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN, Region.Op.INTERSECT); canvas.translate(mPlotOffset.x, mPlotOffset.y); } private void drawBaseAxis(Canvas canvas) { mAxis.rewind(); mAxis.moveTo(GraphView.LEFT_MARGIN, GraphView.TOP_MARGIN); mAxis.lineTo(GraphView.LEFT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN); mAxis.lineTo(mGraphSize.x - GraphView.RIGHT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN); canvas.drawPath(mAxis, mAxisPaint); } private void drawXLabels(Canvas canvas) { float tick = 0.0f; float tickStep = 0.0f; float tickExtra = 0.0f; int tickMultiplier = 1; long msToNextPeriod = 0; Paint p; mTransform.setVirtualSize(mBoundsMins, mBoundsMaxs); mDates.setSpan(mStartMS, mEndMS); mDates.setBaseTime(mStartMS); mSpan = mDates.getSpan(); msToNextPeriod = mDates.msToNextPeriod(mSpan); tick = (float) (mStartMS + msToNextPeriod); mDates.advanceInMs(msToNextPeriod); tickStep = mDates.msInPeriod(mSpan); tick = mTransform.shiftXDimension(tick); tick = mTransform.scaleXDimension(tick); tickStep = mTransform.scaleXDimension(tickStep); if (tickStep < GraphView.TICK_MIN_DISTANCE) tickMultiplier = (int) (GraphView.TICK_MIN_DISTANCE / tickStep) + 1; for (int i = 0; tick <= mPlotSize.x; i++) { String[] label = mDates.getLabel(mSpan); tickExtra = 0.0f; p = mLabelPrimaryPaint; if (mDates.isUnitChanged() == true) { tickExtra = GraphView.TICK_LENGTH; p = mLabelHighlightPaint; if (mShowMarkers == true) { canvas.drawLine(tick + GraphView.LEFT_MARGIN, GraphView.TOP_MARGIN, tick + GraphView.LEFT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN, mMarkerPaint); } } if (i % tickMultiplier == 0) { canvas.drawLine(tick + GraphView.LEFT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN, tick + GraphView.LEFT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN + GraphView.TICK_LENGTH + tickExtra, mAxisPaint); canvas.drawText(label[0], tick + GraphView.LEFT_MARGIN + 2, mGraphSize.y - GraphView.BOTTOM_MARGIN + GraphView.TEXT_HEIGHT, p); if (label[1].equals("") == false) { canvas.drawText(label[1], tick + GraphView.LEFT_MARGIN + 2, mGraphSize.y - GraphView.BOTTOM_MARGIN + (GraphView.TEXT_HEIGHT * 2), p); } } mDates.advance(mSpan, 1); tick += tickStep; } } private void drawYLabels(Canvas canvas, TimeSeries ts, int seriesIndex) { float x = 5; float y = (seriesIndex * GraphView.TEXT_HEIGHT) + GraphView.TOP_MARGIN + GraphView.PLOT_TOP_PAD; int maxLabels = (int) (((mPlotSize.y - GraphView.TEXT_HEIGHT) / 2) / GraphView.TEXT_HEIGHT); if (seriesIndex >= maxLabels) { x += GraphView.LEFT_MARGIN; y = seriesIndex * GraphView.TEXT_HEIGHT; } if (y > (mPlotSize.y / 2)) { x += 20; y -= maxLabels * GraphView.TEXT_HEIGHT; } ts.drawText(canvas, "" + Number.Round(mBoundsMaxs.y, mDecimals), x, y); y = mGraphSize.y - y - GraphView.TEXT_HEIGHT; ts.drawText(canvas, "" + Number.Round(mBoundsMins.y, mDecimals), x, y); canvas.drawLine(GraphView.LEFT_MARGIN, GraphView.TOP_MARGIN + GraphView.PLOT_TOP_PAD, GraphView.LEFT_MARGIN + GraphView.TICK_LENGTH, GraphView.TOP_MARGIN + GraphView.PLOT_TOP_PAD, mAxisPaint); canvas.drawLine(GraphView.LEFT_MARGIN, mGraphSize.y - GraphView.BOTTOM_MARGIN - GraphView.PLOT_BOTTOM_PAD, GraphView.LEFT_MARGIN + GraphView.TICK_LENGTH, mGraphSize.y - GraphView.BOTTOM_MARGIN - GraphView.PLOT_BOTTOM_PAD, mAxisPaint); } private void drawTrendMarker(Canvas canvas, TimeSeries ts) { Datapoint d = ts.getLastVisible(); if (d == null) d = ts.getFirstPostVisible(); if (d == null) d = ts.getLastPreVisible(); if (d == null) return; float y = d.mTrendScreen.y; float label = Number.Round(d.mTrend.y, mDecimals); ts.drawText(canvas, "" + label, 2, y + GraphView.TEXT_HEIGHT - 3); ts.drawMarker(canvas, new Tuple(0, y), new Tuple(mGraphSize.x - GraphView.RIGHT_MARGIN, y)); } public void drawGoal(Canvas canvas, TimeSeries ts, int i) { float goal = ts.getDbRow().getGoal(); float y = goal; y = mTransform.shiftYDimension(y); y = mTransform.scaleYDimension(y); y = mGraphSize.y - GraphView.BOTTOM_MARGIN - (GraphView.PLOT_BOTTOM_PAD * 2) - y; if (y < 0 || y > mGraphSize.y) return; ts.drawText(canvas, "" + Number.Round(goal, mDecimals), (i * GraphView.LEFT_MARGIN) + 2, y + GraphView.TEXT_HEIGHT - 3); ts.drawGoal(canvas, new Tuple(0, y), new Tuple(mGraphSize.x - GraphView.RIGHT_MARGIN, y)); } public void lookupDatapoint(Tuple t) { TimeSeries ts = null; Datapoint d = null; for (int i = 0; i < mTSC.numSeries(); i++) { ts = (TimeSeries) mTSC.getSeries(i); d = ts.lookupVisibleDatapoint(t); if (d != null) break; } mSelectedDatapoint = d; if (mSelectedDatapoint != null) { try { mSelectedColor = Color.parseColor(ts.getColor()); } catch (IllegalArgumentException e) { mSelectedColor = Color.BLACK; } pointInfoDialog(mSelectedDatapoint).show(); } return; } protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_POINT_INFO: return pointInfoDialog(mSelectedDatapoint); case DIALOG_RANGE_INFO: return rangeInfoDialog(mSelectedDatapoint.mCatId); default: } return null; } private Dialog pointInfoDialog(Datapoint mSelected) { Builder b = new AlertDialog.Builder(mCtx); TimeSeries ts = mTSC.getSeriesByIdLocking(mSelected.mCatId); int decimals = Preferences.getDecimalPlaces(mCtx); EvenTrendDbAdapter dbh = ((GraphActivity) mCtx).getDbh(); CategoryDbTable.Row cat = dbh.fetchCategory(mSelected.mCatId); Number.RunningStats stats = ts.getValueStats(); float value = mSelected.mValue.y; float trend = Number.Round(mSelected.mTrend.y, decimals); float pointDeviation = Number.Round((value - trend) / stats.mStdDev, decimals); String devStr = "" + pointDeviation; if (pointDeviation > 0) devStr = "+" + pointDeviation; String info = "Category: " + cat.getCategoryName() + "\n" + "Timestamp: " + mSelected.toLabelString() + "\n" + "Value: " + value + " (" + devStr + " Std Dev)\n" + "Trend: " + trend + "\n"; if (mSelected.mSynthetic == false) { info += "Aggregate of: " + mSelected.mNEntries + " entries\n"; info += "Type: " + cat.getType() + "\n"; } else { info += "Type: Calculated\n"; } b.setTitle("Entry Info"); b.setMessage(info); if (mSelected.mSynthetic == false) { b.setNegativeButton("Edit", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { mTSC.clearCache(); // TODO: just invalidate/update the one // point Intent i = new Intent(mCtx, EntryEditActivity.class); i.putExtra(EntryDbTable.KEY_ROWID, mSelectedDatapoint.mEntryId); ((GraphActivity) mCtx).startActivity(i); } }); } b.setNeutralButton("Range Info", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { rangeInfoDialog(mSelectedDatapoint.mCatId).show(); } }); b.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }); Dialog d = b.create(); return d; } private Dialog rangeInfoDialog(long catId) { Builder b = new AlertDialog.Builder(mCtx); EvenTrendDbAdapter dbh = ((GraphActivity) mCtx).getDbh(); CategoryDbTable.Row cat = dbh.fetchCategory(catId); TimeSeries ts = mTSC.getSeriesByIdLocking(catId); int decimals = Preferences.getDecimalPlaces(mCtx); String info = "Category: " + cat.getCategoryName() + "\n"; if (ts != null) { Number.RunningStats valueStats = ts.getValueStats(); Number.RunningStats timestampStats = ts.getTimestampStats(); String tsAvgPeriod = DateUtil.toString(timestampStats.mMean); String tsAvgEntry = DateUtil.toString(timestampStats.mEntryMean); String tsVar = DateUtil.toStringSquared(timestampStats.mVar); String tsSD = DateUtil.toString(timestampStats.mStdDev); Datapoint first = ts.getFirstVisible(); Datapoint last = ts.getLastVisible(); info += "Values:\n" + " " + DateUtil.toTimestamp(first.mMillis) + " -\n" + " " + DateUtil.toTimestamp(last.mMillis) + "\n" + " Range: " + ts.getVisibleValueMin() + " - " + ts.getVisibleValueMax() + "\n" + " Average: " + Number.Round(valueStats.mMean, decimals) + "\n" + " Std Dev.: " + Number.Round(valueStats.mStdDev, decimals) + "\n" + " Variance: " + Number.Round(valueStats.mVar, decimals) + "\n" + " Trend: " + Number.Round(ts.getTrendStats().mMin, decimals) + " - " + Number.Round(ts.getTrendStats().mMax, decimals) + "\n" + "Date Goal is Reached:\n" + "Time Between Datapoints:\n" + " Avgerage: " + tsAvgPeriod + "\n" + " Std Dev.: " + tsSD + "\n" + " Variance: " + tsVar + "\n" + "Time Between Entries:\n" + " Avg/Entry: " + tsAvgEntry + "\n"; } b.setTitle("Visible Range Info"); b.setMessage(info); b.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }); Dialog d = b.create(); return d; } }