package net.bradmont.holograph; import android.content.res.TypedArray; import android.content.Context; import android.graphics.Color; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import java.lang.Math; import net.bradmont.openmpd.R; public class BarGraph extends View { /* Defaults that can be overridden in XML */ protected int maxItems = 0; // max items to show at a time protected int minItems = 0; // min items to show at a time protected float barWeight = 1; // relative weight of bars protected float spacingWeight = 1; // relative weight of space between bars protected int [] colors = null; protected int lineColor = 0; protected int maxTicks = 8; // maximum number of value labels protected int ticks = maxTicks; // maximum number of value labels protected int labelTextSize = 16; protected Paint [] barPaints = null; protected Paint linePaint = null; protected float width; protected float height = 0f; protected float canvas_bottom = 0f; protected float data_bottom = 0f; protected float canvas_left = 0f; protected float data_left = 0f; protected float canvas_top = 0f; protected float canvas_right = 0f; protected float barWidth; protected float spacingWidth; protected float barHeightFactor; protected float maxValue = 0; protected float tickSpacing = 0; protected float tickWidth = 0; protected int labelZeros = -1; // swipe related: protected float mTranslationX = 0; protected float mDownX = 0; private static final String [] suffixes = {"", "0", "00", "k", "0k", "00k", "M", "0M", "00M", "G" }; protected Object [][] values = new Integer [0][0]; protected String [] labels = null; protected String [] groups = null; protected int [] groupLabelOffset = null; public BarGraph(Context context, AttributeSet attrs){ super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.BarGraph, 0,0); try { // get values from XML maxItems = a.getInteger(R.styleable.BarGraph_maxItems, 10); minItems = a.getInteger(R.styleable.BarGraph_minItems, 1); barWeight = a.getFloat(R.styleable.BarGraph_barWeight, 1); spacingWeight = a.getFloat(R.styleable.BarGraph_spacingWeight, 1); lineColor = a.getInteger(R.styleable.BarGraph_lineColor, 1); maxTicks = a.getInteger(R.styleable.BarGraph_maxTicks, 8); ticks = maxTicks; labelTextSize = a.getDimensionPixelSize(R.styleable.BarGraph_labelTextSize, 16); // colors is a comma separated list of color codes; process them. String colors_attr = a.getString(R.styleable.BarGraph_colors); if (colors_attr == null){ colors_attr = "#33b5e5";} String [] colors_list = colors_attr.split(","); colors = new int [colors_list.length]; for (int i = 0; i < colors_list.length; i++){ colors[i] = Color.parseColor(colors_list[i]); } } finally { a.recycle(); } init(); } private void init(){ barPaints = new Paint [colors.length]; for (int i = 0; i < colors.length; i++){ barPaints[i] = new Paint(Paint.ANTI_ALIAS_FLAG); barPaints[i].setColor(colors[i]); barPaints[i].setStyle(Paint.Style.FILL); } linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setColor(lineColor); linePaint.setTextSize(labelTextSize); } protected void onSizeChanged (int w, int h, int oldw, int oldh){ // Account for padding float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); width = (float)w - xpad; height = (float)h - ypad; canvas_left = getPaddingLeft(); canvas_bottom = height+getPaddingTop(); canvas_right = getPaddingLeft()+width; canvas_top = getPaddingTop(); String sampleLabel = makeBiggestLabel(ticks, (int)tickSpacing); float label_width = linePaint.measureText( sampleLabel); // Figure out bar and spacing widths int bars = getBarCount(); float barWidths = bars + (.33f * 1.5f); //float factor = width / ((barWeight*barWidths + spacingWeight*bars)); float factor = (width-label_width) / ((barWeight*barWidths + spacingWeight*bars)); barWidth = factor * barWeight; tickWidth = barWidth * .33f; spacingWidth = factor * spacingWeight; // boundaries for data data_left = canvas_left + label_width + (tickWidth * 1.5f); if (labels != null){ Rect r = new Rect(); linePaint.getTextBounds(sampleLabel, 0, sampleLabel.length() - 1, r); float label_height = Math.abs(r.height()); // Labels at angle, calculate height: label_height = (label_width + label_height) /(float) Math.sqrt(2); data_bottom = canvas_bottom - (label_height + tickWidth); barHeightFactor = (height-label_height - tickWidth) / maxValue; } else { data_bottom = canvas_bottom ; barHeightFactor = height / maxValue; } setDrawingCacheEnabled(false); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // draw bars String extraLabelRight=""; String extraLabelLeft = ""; for (int i = 0; i < values.length ; i++){ int index = values.length - i -1; float bottom=data_bottom; // Bottom changes with each bar float right = canvas_right - spacingWidth/2 - i * (barWidth + spacingWidth) - mTranslationX; float left = right - barWidth; float label_left = left; if (left < data_left){ left = data_left; } if (right > canvas_right){ right = canvas_right; } // don't draw if bar is outside data area if (left < canvas_right && right > data_left){ // draw the bar in segments for (int j=0; j < values[index].length; j++){ float val = 0f; if (values[index][j] instanceof Float){ val = ((Float) values[index][j]).floatValue(); } else if (values[index][j] instanceof Integer){ val = ((Integer) values[index][j]).floatValue(); } float top = bottom - val*barHeightFactor; //Log.i("net.bradmont.holograph", String.format("value %f: %f %f %f %f", val, bottom, top, right, left)); Rect r = new Rect((int)left, (int)top, (int)right, (int)bottom); canvas.drawRect(r, barPaints[j % barPaints.length]); bottom = top; // stacked bars; bottom is top of previous } if (labels != null){ String label = labels[index]; canvas.save(); canvas.rotate(-45, label_left, canvas_bottom); canvas.drawText(label , label_left, canvas_bottom, linePaint); canvas.restore(); } if (groups != null){ // draw group label line if it goes here String group = groups[index]; Rect r = new Rect(); linePaint.getTextBounds(group, 0, group.length() - 1, r); float y = data_bottom - ((ticks-1) * tickSpacing*barHeightFactor) + r.height(); if (groupLabelOffset[index] == 0){ float x = right -barWidth ; if (x < data_left + spacingWidth){ x = data_left + spacingWidth; } canvas.drawText(group , x, y, linePaint); } else if (!extraLabelLeft.equals(group)&& // left-and label when necessary left - (barWidth + spacingWidth) * (groupLabelOffset[index]) < data_left){ float x = data_left + spacingWidth; canvas.drawText(group , x, y, linePaint); extraLabelLeft = group; } else if (!extraLabelRight.equals(group) && // right-hand label when necessary right - (barWidth + spacingWidth) * (groupLabelOffset[index] +1) > canvas_right - spacingWidth - r.width()){ float x = canvas_right - barWidth; canvas.drawText(group , x, y, linePaint); extraLabelRight = group; } // draw group divider line if it goes here if (index!=0 && !groups[index-1].equals(groups[index])){ if (left - spacingWidth/2 > data_left ){ canvas.drawLine(left - (spacingWidth/2), data_bottom, left - (spacingWidth/2), canvas_top, linePaint); } } /*else if (index!=0 && !groups[index-1].equals(groups[index])){ float y = data_bottom - (maxTicks * tickSpacing*barHeightFactor); String group = groups[index]; Rect r = new Rect(); linePaint.getTextBounds(group, 0, group.length() - 1, r); y = y - r.centerY(); float x = left; if (right-barWidth < data_left){ x = right-barWidth; } canvas.drawText(group , left, y, linePaint); }*/ } } } // draw axes canvas.drawLine(data_left, data_bottom, data_left, canvas_top, linePaint); canvas.drawLine(data_left, data_bottom, canvas_right, data_bottom, linePaint); //Log.i("net.bradmont.holograph", String.format("ticks: %d; tickSpacing %f", maxTicks, tickSpacing)); Rect r = new Rect(); for (int tick = 0; tick < ticks; tick++){ float y = data_bottom - (tick * tickSpacing*barHeightFactor); if (ticks % 2 != tick % 2 ){ // label every other tick; start at 0 if we have an odd // number of ticks, else start at first non-zero tick // this way the top tick always has a label canvas.drawLine(data_left, y, data_left - tickWidth, y, linePaint); String label = makeLabel (tick* (int)tickSpacing); linePaint.getTextBounds(label, 0, label.length() - 1, r); if (tick == ticks - 1) { y = y + r.height(); } else if (tick != 0) { y = y - r.centerY(); } canvas.drawText(label , canvas_left, y, linePaint); } else { canvas.drawLine(data_left, y, data_left - (tickWidth/2), y, linePaint); } } // cache it setDrawingCacheEnabled(true); } @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: mDownX = mTranslationX + motionEvent.getX(); return true; //case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTranslationX = (mDownX - motionEvent.getX()) ; invalidate(); if (mTranslationX > 0){ mTranslationX = 0;} // if we scrolled past right edge, snap back if (mTranslationX < -1 * (values.length - getBarCount() )*(barWidth+spacingWidth) ){ // if we scrolled past left edge, snap back mTranslationX = -1 * (values.length - getBarCount() )*(barWidth+spacingWidth); } invalidate(); return true; case MotionEvent.ACTION_MOVE: mTranslationX = (mDownX - motionEvent.getX()) ; getParent().requestDisallowInterceptTouchEvent(true); if (mTranslationX > 0){ mTranslationX = 0;} // don't scroll past right edge if (mTranslationX < -1 * (values.length - getBarCount() )*(barWidth+spacingWidth) ){ // don't scroll past left edge mTranslationX = -1 * (values.length - getBarCount() )*(barWidth+spacingWidth); } invalidate(); return true; } return false; } private float sumValues(Object [] list){ float sum = 0f; for (int i = 0; i < list.length; i++){ if (list[i] instanceof Float){ sum += ((Float) list[i]).floatValue(); } else if (list[i] instanceof Integer){ sum += ((Integer) list[i]).floatValue(); } } return sum; } /** * Returns the highest total value that needs to be displayed for * any visible element of the list */ public float getMaxValue(){ int start = (values.length - maxItems) >0?values.length- maxItems:0; if (values.length < 1){ return 0f;} float max = sumValues(values[start]); for (int i = start; i < values.length; i++){ float val = sumValues(values[i]); if (val > max){ max = val; } } //Log.i("BarGraph", String.format("Max: %f", max)); return max; } public int getBarCount(){ int numBars = values.length; if (numBars > maxItems) { return maxItems; } else if (numBars < minItems) { return minItems; } return numBars; } // accessors public Object [][] getValues() { return values; } public void setValues(Object [] values) { if (values.length == 0){ setValues(new Float[0][0]); } else { Object [][] vvs = new Object[values.length][1]; for (int i = 0; i < values.length; i++){ vvs[i][0] = values[i]; } setValues(vvs); } } public void setValues(Object [][] values) { this.values = values; init(); maxValue = getMaxValue(); calcNiceTicks(maxValue); barHeightFactor = height / maxValue; setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public void setLabels(String [] labels){ this.labels = labels; init(); setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public void setGroups(String [] groups){ this.groups = groups; groupLabelOffset = new int[groups.length]; for (int i = 0; i < groups.length; i++){ int group_before = 0; int group_after = 0; // how many group members before & after this element while (i+group_after < groups.length && groups[i+group_after].equals(groups[i])){ group_after++; } while (i-group_before >= 0 && groups[i-group_before].equals(groups[i])){ group_before++; } // how far is this element from the label? if ((group_after + group_before +1) % 2 == 0){ groupLabelOffset[i] = (group_before - group_after+1) / 2; } else { groupLabelOffset[i] = (group_before - group_after) / 2; } } init(); setDrawingCacheEnabled(false); invalidate(); requestLayout(); } /* Pretty ticks algorithm thanks to Steffen L Norgen, at http://www.esurient-systems.ca/2011/03/algorithm-for-optimal-scaling-on-chart_8199.html */ protected void calcNiceTicks(float max){ tickSpacing = prettyValue (maxValue / (maxTicks -1), false); ticks = (int) Math.ceil(maxValue / tickSpacing) + 1; maxValue = (float) Math.ceil( maxValue / tickSpacing) * tickSpacing; } protected float prettyValue(float range, boolean round){ double exponent; double fraction; double niceFraction; exponent = Math.floor(Math.log10(range)); fraction = range/Math.pow(10, exponent); if (round){ if (fraction < 1.5){ niceFraction = 1; } else if (fraction < 3){ niceFraction = 2; } else if (fraction < 7){ niceFraction = 5; } else { niceFraction = 10; } } else { if (fraction <= 1){ niceFraction = 1; } else if (fraction <= 2.01){ niceFraction = 2; } else if (fraction <= 5.01){ niceFraction = 5; } else { niceFraction = 10; } } return (float) (niceFraction * Math.pow(10, exponent)); } public int getMaxItems() { return maxItems; } public void setMaxItems(int maxItems) { this.maxItems= maxItems; setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public int getMinItems() { return minItems; } public void setMinItems(int minItems) { this.minItems= minItems; setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public float getBarWeight() { return barWeight; } public void setBarWeight(int barWeight) { this.barWeight= barWeight; setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public float getSpacingWeight() { return spacingWeight; } public void setSpacingWeight(int spacingWeight) { this.spacingWeight= spacingWeight; setDrawingCacheEnabled(false); invalidate(); requestLayout(); } public int [] getColors() { return colors; } public void setColors(int [] colors) { this.colors = new int [colors.length]; for (int i = 0; i < colors.length; i++){ this.colors[i] = colors[i]; } setDrawingCacheEnabled(false); invalidate(); requestLayout(); } private String makeLabel(int label){ String result = Integer.toString(label); if (result.length() <= labelZeros){ return result; } try { result = result.substring(0, result.length() - labelZeros); } catch (java.lang.StringIndexOutOfBoundsException e){ return result; } return result + suffixes[labelZeros]; } private String makeBiggestLabel(int ticks, int tickSpacing){ labelZeros = countZeros(Integer.toString( tickSpacing * (ticks - 1))); int biggest_label = ticks; float label_width = linePaint.measureText(Integer.toString( tickSpacing * ticks)); for (int i = 1; i < ticks; i++){ if (ticks % 2 != i % 2 ){ if (countZeros(Integer.toString( tickSpacing * i)) < labelZeros){ labelZeros = countZeros(Integer.toString( tickSpacing * i)); } if (linePaint.measureText(Integer.toString( tickSpacing * i)) > label_width){ biggest_label = i; label_width = linePaint.measureText(Integer.toString( tickSpacing * i)); } } } if (labelZeros >= suffixes.length){ labelZeros = suffixes.length - 1; } return makeLabel(biggest_label * tickSpacing); } /* counts the 0s at the end of a string */ private int countZeros(String number){ int result = 0; while (number.endsWith("0")){ result++; number = number.substring(0, number.length()-1); } return result; } }