/** * GraphView * Copyright 2016 Jonas Gehring * * 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 com.jjoe64.graphview.series; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.support.v4.view.ViewCompat; import android.util.Log; import android.view.animation.AccelerateInterpolator; import com.jjoe64.graphview.GraphView; import com.jjoe64.graphview.RectD; import com.jjoe64.graphview.ValueDependentColor; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; /** * Series with Bars to visualize the data. * The Bars are always vertical. * * @author jjoe64 */ public class BarGraphSeries<E extends DataPointInterface> extends BaseSeries<E> { private static final long ANIMATION_DURATION = 333; /** * paint to do drawing on canvas */ private Paint mPaint; /** * custom paint that can be used. * this will ignore the value dependent color. */ private Paint mCustomPaint; /** * spacing between the bars in percentage. * 0 => no spacing * 100 => the space between the bars is as big as the bars itself */ private int mSpacing; /** * width of a data point * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically * >0 => value is the total distance from one bar to another */ private double mDataWidth; /** * callback to generate value-dependent colors * of the bars */ private ValueDependentColor<E> mValueDependentColor; /** * flag whether the values should drawn * above the bars as text */ private boolean mDrawValuesOnTop; /** * color of the text above the bars. * * @see #mDrawValuesOnTop */ private int mValuesOnTopColor; /** * font size of the text above the bars. * * @see #mDrawValuesOnTop */ private float mValuesOnTopSize; /** * stores the coordinates of the bars to * trigger tap on series events. */ private Map<RectD, E> mDataPoints = new HashMap<RectD, E>(); /** * flag for animated rendering */ private boolean mAnimated; /** * store the last value that was animated */ private double mLastAnimatedValue = Double.NaN; /** * time of start animation */ private long mAnimationStart; /** * animation interpolator */ private AccelerateInterpolator mAnimationInterpolator; /** * frame number of animation to avoid lagging */ private int mAnimationStartFrameNo; /** * creates bar series without any data */ public BarGraphSeries() { mPaint = new Paint(); } /** * creates bar series with data * * @param data data points * important: array has to be sorted from lowest x-value to the highest */ public BarGraphSeries(E[] data) { super(data); mPaint = new Paint(); mAnimationInterpolator = new AccelerateInterpolator(2f); } /** * draws the bars on the canvas * * @param graphView corresponding graphview * @param canvas canvas * @param isSecondScale whether we are plotting the second scale or not */ @Override public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { mPaint.setTextAlign(Paint.Align.CENTER); if (mValuesOnTopSize == 0) { mValuesOnTopSize = graphView.getGridLabelRenderer().getTextSize(); } mPaint.setTextSize(mValuesOnTopSize); resetDataPoints(); // get data double maxX = graphView.getViewport().getMaxX(false); double minX = graphView.getViewport().getMinX(false); double maxY; double minY; if (isSecondScale) { maxY = graphView.getSecondScale().getMaxY(false); minY = graphView.getSecondScale().getMinY(false); } else { maxY = graphView.getViewport().getMaxY(false); minY = graphView.getViewport().getMinY(false); } // Iterate through all bar graph series // so we know how wide to make our bar, // and in what position to put it in int numBarSeries = 0; int currentSeriesOrder = 0; int numValues = 0; boolean isCurrentSeries; SortedSet<Double> xVals = new TreeSet<Double>(); for(Series inspectedSeries: graphView.getSeries()) { if(inspectedSeries instanceof BarGraphSeries) { isCurrentSeries = (inspectedSeries == this); if(isCurrentSeries) { currentSeriesOrder = numBarSeries; } numBarSeries++; // calculate the number of slots for bars based on the minimum distance between // x coordinates in the series. This is divided into the range to find // the placement and width of bar slots // (sections of the x axis for each bar or set of bars) // TODO: Move this somewhere more general and cache it, so we don't recalculate it for each series Iterator<E> curValues = inspectedSeries.getValues(minX, maxX); if (curValues.hasNext()) { xVals.add(curValues.next().getX()); if(isCurrentSeries) { numValues++; } while (curValues.hasNext()) { xVals.add(curValues.next().getX()); if(isCurrentSeries) { numValues++; } } } } } if (numValues == 0) { return; } double minGap = 0; if(mDataWidth > 0.0) { minGap = mDataWidth; } else { Double lastVal = null; for(Double curVal: xVals) { if(lastVal != null) { double curGap = Math.abs(curVal - lastVal); if (minGap == 0 || (curGap > 0 && curGap < minGap)) { minGap = curGap; } } lastVal = curVal; } } int numBarSlots = (minGap == 0) ? 1 : (int)Math.round((maxX - minX)/minGap) + 1; Iterator<E> values = getValues(minX, maxX); // Calculate the overall bar slot width - this includes all bars across // all series, and any spacing between sets of bars int barSlotWidth = numBarSlots == 1 ? graphView.getGraphContentWidth() : graphView.getGraphContentWidth() / (numBarSlots-1); // Total spacing (both sides) between sets of bars double spacing = Math.min(barSlotWidth*mSpacing/100, barSlotWidth*0.98f); // Width of an individual bar double barWidth = (barSlotWidth - spacing) / numBarSeries; // Offset from the center of a given bar to start drawing double offset = barSlotWidth/2; double diffY = maxY - minY; double diffX = maxX - minX; double contentHeight = graphView.getGraphContentHeight(); double contentWidth = graphView.getGraphContentWidth(); double contentLeft = graphView.getGraphContentLeft(); double contentTop = graphView.getGraphContentTop(); // draw data int i=0; while (values.hasNext()) { E value = values.next(); double valY = value.getY() - minY; double ratY = valY / diffY; double y = contentHeight * ratY; double valY0 = 0 - minY; double ratY0 = valY0 / diffY; double y0 = contentHeight * ratY0; double valueX = value.getX(); double valX = valueX - minX; double ratX = valX / diffX; double x = contentWidth * ratX; // hook for value dependent color if (getValueDependentColor() != null) { mPaint.setColor(getValueDependentColor().get(value)); } else { mPaint.setColor(getColor()); } double left = x + contentLeft - offset + spacing/2 + currentSeriesOrder*barWidth; double top = (contentTop - y) + contentHeight; double right = left + barWidth; double bottom = (contentTop - y0) + contentHeight - (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); boolean reverse = top > bottom; if (mAnimated) { if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { long currentTime = System.currentTimeMillis(); if (mAnimationStart == 0) { // start animation mAnimationStart = currentTime; mAnimationStartFrameNo = 0; } else { // anti-lag: wait a few frames if (mAnimationStartFrameNo < 15) { // second time mAnimationStart = currentTime; mAnimationStartFrameNo++; } } float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; float factor = mAnimationInterpolator.getInterpolation(timeFactor); if (timeFactor <= 1.0) { double barHeight = bottom - top; barHeight = barHeight * factor; top = bottom-barHeight; ViewCompat.postInvalidateOnAnimation(graphView); } else { // animation finished mLastAnimatedValue = valueX; } } } if (reverse) { double tmp = top; top = bottom + (graphView.getGridLabelRenderer().isHighlightZeroLines()?4:1); bottom = tmp; } // overdraw left = Math.max(left, contentLeft); right = Math.min(right, contentLeft+contentWidth); bottom = Math.min(bottom, contentTop+contentHeight); top = Math.max(top, contentTop); mDataPoints.put(new RectD(left, top, right, bottom), value); Paint p; if (mCustomPaint != null) { p = mCustomPaint; } else { p = mPaint; } canvas.drawRect((float)left, (float)top, (float)right, (float)bottom, p); // set values on top of graph if (mDrawValuesOnTop) { if (reverse) { top = bottom + mValuesOnTopSize + 4; if (top > contentTop+contentHeight) top = contentTop + contentHeight; } else { top -= 4; if (top<=contentTop) top+=contentTop+4; } mPaint.setColor(mValuesOnTopColor); canvas.drawText( graphView.getGridLabelRenderer().getLabelFormatter().formatLabel(value.getY(), false) , (float) (left+right)/2, (float) top, mPaint); } i++; } } /** * @return the hook to generate value-dependent color. default null */ public ValueDependentColor<E> getValueDependentColor() { return mValueDependentColor; } /** * set a hook to make the color of the bars depending * on the actually value/data. * * @param mValueDependentColor hook * null to disable */ public void setValueDependentColor(ValueDependentColor<E> mValueDependentColor) { this.mValueDependentColor = mValueDependentColor; } /** * @return the spacing between the bars in percentage */ public int getSpacing() { return mSpacing; } /** * @param mSpacing spacing between the bars in percentage. * 0 => no spacing * 100 => the space between the bars is as big as the bars itself */ public void setSpacing(int mSpacing) { this.mSpacing = mSpacing; } /** * @return the interval between data points */ public double getDataWidth() { return mDataWidth; } /** * @param mDataWidth width of a data point (sampling period) * 0 => no prior knowledge of sampling period, interval between bars will be calculated automatically * >0 => value is the total distance from one bar to another */ public void setDataWidth(double mDataWidth) { this.mDataWidth = mDataWidth; } /** * @return whether the values should be drawn above the bars */ public boolean isDrawValuesOnTop() { return mDrawValuesOnTop; } /** * @param mDrawValuesOnTop flag whether the values should drawn * above the bars as text */ public void setDrawValuesOnTop(boolean mDrawValuesOnTop) { this.mDrawValuesOnTop = mDrawValuesOnTop; } /** * @return font color of the values on top of the bars * @see #setDrawValuesOnTop(boolean) */ public int getValuesOnTopColor() { return mValuesOnTopColor; } /** * @param mValuesOnTopColor the font color of the values on top of the bars * @see #setDrawValuesOnTop(boolean) */ public void setValuesOnTopColor(int mValuesOnTopColor) { this.mValuesOnTopColor = mValuesOnTopColor; } /** * @return font size of the values above the bars * @see #setDrawValuesOnTop(boolean) */ public float getValuesOnTopSize() { return mValuesOnTopSize; } /** * @param mValuesOnTopSize font size of the values above the bars * @see #setDrawValuesOnTop(boolean) */ public void setValuesOnTopSize(float mValuesOnTopSize) { this.mValuesOnTopSize = mValuesOnTopSize; } /** * resets the cached coordinates of the bars */ @Override protected void resetDataPoints() { mDataPoints.clear(); } /** * find the corresponding data point by * coordinates. * * @param x pixels * @param y pixels * @return datapoint or null */ @Override protected E findDataPoint(float x, float y) { for (Map.Entry<RectD, E> entry : mDataPoints.entrySet()) { if (x >= entry.getKey().left && x <= entry.getKey().right && y >= entry.getKey().top && y <= entry.getKey().bottom) { return entry.getValue(); } } return null; } /** * custom paint that can be used. * this will ignore the value dependent color. * * @return custom paint or null */ public Paint getCustomPaint() { return mCustomPaint; } /** * custom paint that can be used. * this will ignore the value dependent color. * * @param mCustomPaint custom paint to use or null */ public void setCustomPaint(Paint mCustomPaint) { this.mCustomPaint = mCustomPaint; } /** * draw the series with an animation * * @param animated animation activated or not */ public void setAnimated(boolean animated) { this.mAnimated = animated; } /** * @return rendering is animated or not */ public boolean isAnimated() { return mAnimated; } @Override public void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value) { // TODO } }