/* * Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com> * * This file is part of Loop Habit Tracker. * * Loop Habit Tracker is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * Loop Habit Tracker is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.isoron.uhabits.activities.common.views; import android.content.*; import android.graphics.*; import android.support.annotation.*; import android.util.*; import org.isoron.uhabits.*; import org.isoron.uhabits.utils.*; import java.text.*; import java.util.*; public class FrequencyChart extends ScrollableChart { private Paint pGrid; private float em; private SimpleDateFormat dfMonth; private SimpleDateFormat dfYear; private Paint pText, pGraph; private RectF rect, prevRect; private int baseSize; private int paddingTop; private float columnWidth; private int columnHeight; private int nColumns; private int textColor; private int gridColor; private int[] colors; private int primaryColor; private boolean isBackgroundTransparent; @NonNull private HashMap<Long, Integer[]> frequency; private int maxFreq; public FrequencyChart(Context context) { super(context); init(); } public FrequencyChart(Context context, AttributeSet attrs) { super(context, attrs); this.frequency = new HashMap<>(); init(); } public void setColor(int color) { this.primaryColor = color; initColors(); postInvalidate(); } public void setFrequency(HashMap<Long, Integer[]> frequency) { this.frequency = frequency; maxFreq = getMaxFreq(frequency); postInvalidate(); } private int getMaxFreq(HashMap<Long, Integer[]> frequency) { int maxValue = 1; for (Integer[] values : frequency.values()) { for (Integer value : values) { maxValue = Math.max(value, maxValue); } } return maxValue; } public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { this.isBackgroundTransparent = isBackgroundTransparent; initColors(); } protected void initPaints() { pText = new Paint(); pText.setAntiAlias(true); pGraph = new Paint(); pGraph.setTextAlign(Paint.Align.CENTER); pGraph.setAntiAlias(true); pGrid = new Paint(); pGrid.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); rect.set(0, 0, nColumns * columnWidth, columnHeight); rect.offset(0, paddingTop); drawGrid(canvas, rect); pText.setTextAlign(Paint.Align.CENTER); pText.setColor(textColor); pGraph.setColor(primaryColor); prevRect.setEmpty(); GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendar(); currentDate.set(Calendar.DAY_OF_MONTH, 1); currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset()); for (int i = 0; i < nColumns - 1; i++) { rect.set(0, 0, columnWidth, columnHeight); rect.offset(i * columnWidth, 0); drawColumn(canvas, rect, currentDate); currentDate.add(Calendar.MONTH, 1); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { if (height < 9) height = 200; baseSize = height / 8; setScrollerBucketSize(baseSize); pText.setTextSize(baseSize * 0.4f); pGraph.setTextSize(baseSize * 0.4f); pGraph.setStrokeWidth(baseSize * 0.1f); pGrid.setStrokeWidth(baseSize * 0.05f); em = pText.getFontSpacing(); columnWidth = baseSize; columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); columnHeight = 8 * baseSize; nColumns = (int) (width / columnWidth); paddingTop = 0; } private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date) { Integer values[] = frequency.get(date.getTimeInMillis()); float rowHeight = rect.height() / 8.0f; prevRect.set(rect); Integer[] localeWeekdayList = DateUtils.getLocaleWeekdayList(); for (int j = 0; j < localeWeekdayList.length; j++) { rect.set(0, 0, baseSize, baseSize); rect.offset(prevRect.left, prevRect.top + baseSize * j); int i = DateUtils.javaWeekdayToLoopWeekday(localeWeekdayList[j]); if (values != null) drawMarker(canvas, rect, values[i]); rect.offset(0, rowHeight); } drawFooter(canvas, rect, date); } private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date) { Date time = date.getTime(); canvas.drawText(dfMonth.format(time), rect.centerX(), rect.centerY() - 0.1f * em, pText); if (date.get(Calendar.MONTH) == 1) canvas.drawText(dfYear.format(time), rect.centerX(), rect.centerY() + 0.9f * em, pText); } private void drawGrid(Canvas canvas, RectF rGrid) { int nRows = 7; float rowHeight = rGrid.height() / (nRows + 1); pText.setTextAlign(Paint.Align.LEFT); pText.setColor(textColor); pGrid.setColor(gridColor); for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT)) { canvas.drawText(day, rGrid.right - columnWidth, rGrid.top + rowHeight / 2 + 0.25f * em, pText); pGrid.setStrokeWidth(1f); canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); rGrid.offset(0, rowHeight); } canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); } private void drawMarker(Canvas canvas, RectF rect, Integer value) { float padding = rect.height() * 0.2f; // maximal allowed mark radius float maxRadius = (rect.height() - 2 * padding) / 2.0f; // the real mark radius is scaled down by a factor depending on the maximal frequency float scale = 1.0f/maxFreq * value; float radius = maxRadius * scale; int colorIndex = Math.round((colors.length-1) * scale); pGraph.setColor(colors[colorIndex]); canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph); } private float getMaxMonthWidth() { float maxMonthWidth = 0; GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); for (int i = 0; i < 12; i++) { day.set(Calendar.MONTH, i); float monthWidth = pText.measureText(dfMonth.format(day.getTime())); maxMonthWidth = Math.max(maxMonthWidth, monthWidth); } return maxMonthWidth; } private void init() { initPaints(); initColors(); initDateFormats(); initRects(); } private void initColors() { StyledResources res = new StyledResources(getContext()); textColor = res.getColor(R.attr.mediumContrastTextColor); gridColor = res.getColor(R.attr.lowContrastTextColor); colors = new int[4]; colors[0] = gridColor; colors[3] = primaryColor; colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f); colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f); } private void initDateFormats() { dfMonth = DateFormats.fromSkeleton("MMM"); dfYear = DateFormats.fromSkeleton("yyyy"); } private void initRects() { rect = new RectF(); prevRect = new RectF(); } public void populateWithRandomData() { GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); date.set(Calendar.DAY_OF_MONTH, 1); Random rand = new Random(); frequency.clear(); for (int i = 0; i < 40; i++) { Integer values[] = new Integer[7]; for (int j = 0; j < 7; j++) values[j] = rand.nextInt(5); frequency.put(date.getTimeInMillis(), values); date.add(Calendar.MONTH, -1); } } }