This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
@@ -0,0 +1,180 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.RectF;
import android.graphics.Typeface;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Abstract renderer implementation, every chart renderer extends this class(although it is not required it helps).
*/
public abstract class AbstractChartRenderer implements ChartRenderer {
public int DEFAULT_LABEL_MARGIN_DP = 4;
protected Chart chart;
protected ChartComputator computator;
/**
* Paint for value labels.
*/
protected Paint labelPaint = new Paint();
/**
* Paint for labels background.
*/
protected Paint labelBackgroundPaint = new Paint();
/**
* Holds coordinates for label background rect.
*/
protected RectF labelBackgroundRect = new RectF();
/**
* Font metrics for label paint, used to determine text height.
*/
protected FontMetricsInt fontMetrics = new FontMetricsInt();
/**
* If true maximum and current viewport will be calculated when chart data change or during data animations.
*/
protected boolean isViewportCalculationEnabled = true;
protected float density;
protected float scaledDensity;
protected SelectedValue selectedValue = new SelectedValue();
protected char[] labelBuffer = new char[64];
protected int labelOffset;
protected int labelMargin;
protected boolean isValueLabelBackgroundEnabled;
protected boolean isValueLabelBackgroundAuto;
public AbstractChartRenderer(Context context, Chart chart) {
this.density = context.getResources().getDisplayMetrics().density;
this.scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
this.chart = chart;
this.computator = chart.getChartComputator();
labelMargin = ChartUtils.dp2px(density, DEFAULT_LABEL_MARGIN_DP);
labelOffset = labelMargin;
labelPaint.setAntiAlias(true);
labelPaint.setStyle(Paint.Style.FILL);
labelPaint.setTextAlign(Align.LEFT);
labelPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
labelPaint.setColor(Color.WHITE);
labelBackgroundPaint.setAntiAlias(true);
labelBackgroundPaint.setStyle(Paint.Style.FILL);
}
@Override
public void resetRenderer() {
this.computator = chart.getChartComputator();
}
@Override
public void onChartDataChanged() {
final ChartData data = chart.getChartData();
Typeface typeface = chart.getChartData().getValueLabelTypeface();
if (null != typeface) {
labelPaint.setTypeface(typeface);
}
labelPaint.setColor(data.getValueLabelTextColor());
labelPaint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getValueLabelTextSize()));
labelPaint.getFontMetricsInt(fontMetrics);
this.isValueLabelBackgroundEnabled = data.isValueLabelBackgroundEnabled();
this.isValueLabelBackgroundAuto = data.isValueLabelBackgroundAuto();
this.labelBackgroundPaint.setColor(data.getValueLabelBackgroundColor());
// Important - clear selection when data changed.
selectedValue.clear();
}
/**
* Draws label text and label background if isValueLabelBackgroundEnabled is true.
*/
protected void drawLabelTextAndBackground(Canvas canvas, char[] labelBuffer, int startIndex, int numChars,
int autoBackgroundColor) {
final float textX;
final float textY;
if (isValueLabelBackgroundEnabled) {
if (isValueLabelBackgroundAuto) {
labelBackgroundPaint.setColor(autoBackgroundColor);
}
canvas.drawRect(labelBackgroundRect, labelBackgroundPaint);
textX = labelBackgroundRect.left + labelMargin;
textY = labelBackgroundRect.bottom - labelMargin;
} else {
textX = labelBackgroundRect.left;
textY = labelBackgroundRect.bottom;
}
canvas.drawText(labelBuffer, startIndex, numChars, textX, textY, labelPaint);
}
@Override
public boolean isTouched() {
return selectedValue.isSet();
}
@Override
public void clearTouch() {
selectedValue.clear();
}
@Override
public Viewport getMaximumViewport() {
return computator.getMaximumViewport();
}
@Override
public void setMaximumViewport(Viewport maxViewport) {
if (null != maxViewport) {
computator.setMaxViewport(maxViewport);
}
}
@Override
public Viewport getCurrentViewport() {
return computator.getCurrentViewport();
}
@Override
public void setCurrentViewport(Viewport viewport) {
if (null != viewport) {
computator.setCurrentViewport(viewport);
}
}
@Override
public boolean isViewportCalculationEnabled() {
return isViewportCalculationEnabled;
}
@Override
public void setViewportCalculationEnabled(boolean isEnabled) {
this.isViewportCalculationEnabled = isEnabled;
}
@Override
public void selectValue(SelectedValue selectedValue) {
this.selectedValue.set(selectedValue);
}
@Override
public SelectedValue getSelectedValue() {
return selectedValue;
}
}
@@ -0,0 +1,639 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.TextUtils;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.Axis;
import lecho.lib.hellocharts.model.AxisValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.util.AxisAutoValues;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.util.FloatUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Default axes renderer. Can draw maximum four axes - two horizontal(top/bottom) and two vertical(left/right).
*/
public class AxesRenderer {
private static final int DEFAULT_AXIS_MARGIN_DP = 2;
/**
* Axis positions indexes, used for indexing tabs that holds axes parameters, see below.
*/
private static final int TOP = 0;
private static final int LEFT = 1;
private static final int RIGHT = 2;
private static final int BOTTOM = 3;
/**
* Used to measure label width. If label has mas 5 characters only 5 first characters of this array are used to
* measure text width.
*/
private static final char[] labelWidthChars = new char[]{
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'};
private Chart chart;
private ChartComputator computator;
private int axisMargin;
private float density;
private float scaledDensity;
private Paint[] labelPaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private Paint[] namePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private Paint[] linePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private float[] nameBaselineTab = new float[4];
private float[] labelBaselineTab = new float[4];
private float[] separationLineTab = new float[4];
private int[] labelWidthTab = new int[4];
private int[] labelTextAscentTab = new int[4];
private int[] labelTextDescentTab = new int[4];
private int[] labelDimensionForMarginsTab = new int[4];
private int[] labelDimensionForStepsTab = new int[4];
private int[] tiltedLabelXTranslation = new int[4];
private int[] tiltedLabelYTranslation = new int[4];
private FontMetricsInt[] fontMetricsTab = new FontMetricsInt[]{new FontMetricsInt(), new FontMetricsInt(),
new FontMetricsInt(), new FontMetricsInt()};
/**
* Holds formatted axis value label.
*/
private char[] labelBuffer = new char[64];
/**
* Holds number of values that should be drown for each axis.
*/
private int[] valuesToDrawNumTab = new int[4];
/**
* Holds raw values to draw for each axis.
*/
private float[][] rawValuesTab = new float[4][0];
/**
* Holds auto-generated values that should be drawn, i.e if axis is inside not all auto-generated values should be
* drawn to avoid overdrawing. Used only for auto axes.
*/
private float[][] autoValuesToDrawTab = new float[4][0];
/**
* Holds custom values that should be drawn, used only for custom axes.
*/
private AxisValue[][] valuesToDrawTab = new AxisValue[4][0];
/**
* Buffers for axes lines coordinates(to draw grid in the background).
*/
private float[][] linesDrawBufferTab = new float[4][0];
/**
* Buffers for auto-generated values for each axis, used only if there are auto axes.
*/
private AxisAutoValues[] autoValuesBufferTab = new AxisAutoValues[]{new AxisAutoValues(),
new AxisAutoValues(), new AxisAutoValues(), new AxisAutoValues()};
public AxesRenderer(Context context, Chart chart) {
this.chart = chart;
computator = chart.getChartComputator();
density = context.getResources().getDisplayMetrics().density;
scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
axisMargin = ChartUtils.dp2px(density, DEFAULT_AXIS_MARGIN_DP);
for (int position = 0; position < 4; ++position) {
labelPaintTab[position].setStyle(Paint.Style.FILL);
labelPaintTab[position].setAntiAlias(true);
namePaintTab[position].setStyle(Paint.Style.FILL);
namePaintTab[position].setAntiAlias(true);
linePaintTab[position].setStyle(Paint.Style.STROKE);
linePaintTab[position].setAntiAlias(true);
}
}
public void onChartSizeChanged() {
onChartDataOrSizeChanged();
}
public void onChartDataChanged() {
onChartDataOrSizeChanged();
}
private void onChartDataOrSizeChanged() {
initAxis(chart.getChartData().getAxisXTop(), TOP);
initAxis(chart.getChartData().getAxisXBottom(), BOTTOM);
initAxis(chart.getChartData().getAxisYLeft(), LEFT);
initAxis(chart.getChartData().getAxisYRight(), RIGHT);
}
public void resetRenderer() {
this.computator = chart.getChartComputator();
}
/**
* Initialize attributes and measurement for axes(left, right, top, bottom);
*/
private void initAxis(Axis axis, int position) {
if (null == axis) {
return;
}
initAxisAttributes(axis, position);
initAxisMargin(axis, position);
initAxisMeasurements(axis, position);
}
private void initAxisAttributes(Axis axis, int position) {
initAxisPaints(axis, position);
initAxisTextAlignment(axis, position);
if (axis.hasTiltedLabels()) {
initAxisDimensionForTiltedLabels(position);
intiTiltedLabelsTranslation(axis, position);
} else {
initAxisDimension(position);
}
}
private void initAxisPaints(Axis axis, int position) {
Typeface typeface = axis.getTypeface();
if (null != typeface) {
labelPaintTab[position].setTypeface(typeface);
namePaintTab[position].setTypeface(typeface);
}
labelPaintTab[position].setColor(axis.getTextColor());
labelPaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
labelPaintTab[position].getFontMetricsInt(fontMetricsTab[position]);
namePaintTab[position].setColor(axis.getTextColor());
namePaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
linePaintTab[position].setColor(axis.getLineColor());
labelTextAscentTab[position] = Math.abs(fontMetricsTab[position].ascent);
labelTextDescentTab[position] = Math.abs(fontMetricsTab[position].descent);
labelWidthTab[position] = (int) labelPaintTab[position].measureText(labelWidthChars, 0,
axis.getMaxLabelChars());
}
private void initAxisTextAlignment(Axis axis, int position) {
namePaintTab[position].setTextAlign(Align.CENTER);
if (TOP == position || BOTTOM == position) {
labelPaintTab[position].setTextAlign(Align.CENTER);
} else if (LEFT == position) {
if (axis.isInside()) {
labelPaintTab[position].setTextAlign(Align.LEFT);
} else {
labelPaintTab[position].setTextAlign(Align.RIGHT);
}
} else if (RIGHT == position) {
if (axis.isInside()) {
labelPaintTab[position].setTextAlign(Align.RIGHT);
} else {
labelPaintTab[position].setTextAlign(Align.LEFT);
}
}
}
private void initAxisDimensionForTiltedLabels(int position) {
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
labelDimensionForMarginsTab[position] = pythagoreanFromAscent + pythagoreanFromLabelWidth;
labelDimensionForStepsTab[position] = Math.round(labelDimensionForMarginsTab[position] * 0.75f);
}
private void initAxisDimension(int position) {
if (LEFT == position || RIGHT == position) {
labelDimensionForMarginsTab[position] = labelWidthTab[position];
labelDimensionForStepsTab[position] = labelTextAscentTab[position];
} else if (TOP == position || BOTTOM == position) {
labelDimensionForMarginsTab[position] = labelTextAscentTab[position] +
labelTextDescentTab[position];
labelDimensionForStepsTab[position] = labelWidthTab[position];
}
}
private void intiTiltedLabelsTranslation(Axis axis, int position) {
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
int dx = 0;
int dy = 0;
if (axis.isInside()) {
if (LEFT == position) {
dx = pythagoreanFromAscent;
} else if (RIGHT == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (TOP == position) {
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
} else if (BOTTOM == position) {
dy = -pythagoreanFromLabelWidth / 2;
}
} else {
if (LEFT == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (RIGHT == position) {
dx = pythagoreanFromAscent;
} else if (TOP == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (BOTTOM == position) {
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
}
}
tiltedLabelXTranslation[position] = dx;
tiltedLabelYTranslation[position] = dy;
}
private void initAxisMargin(Axis axis, int position) {
int margin = 0;
if (!axis.isInside() && (axis.isAutoGenerated() || !axis.getValues().isEmpty())) {
margin += axisMargin + labelDimensionForMarginsTab[position];
}
margin += getAxisNameMargin(axis, position);
insetContentRectWithAxesMargins(margin, position);
}
private int getAxisNameMargin(Axis axis, int position) {
int margin = 0;
if (!TextUtils.isEmpty(axis.getName())) {
margin += labelTextAscentTab[position];
margin += labelTextDescentTab[position];
margin += axisMargin;
}
return margin;
}
private void insetContentRectWithAxesMargins(int axisMargin, int position) {
if (LEFT == position) {
chart.getChartComputator().insetContentRect(axisMargin, 0, 0, 0);
} else if (RIGHT == position) {
chart.getChartComputator().insetContentRect(0, 0, axisMargin, 0);
} else if (TOP == position) {
chart.getChartComputator().insetContentRect(0, axisMargin, 0, 0);
} else if (BOTTOM == position) {
chart.getChartComputator().insetContentRect(0, 0, 0, axisMargin);
}
}
private void initAxisMeasurements(Axis axis, int position) {
if (LEFT == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().left + axisMargin;
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin
- labelTextDescentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin;
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin
- labelTextDescentTab[position] - labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().left;
} else if (RIGHT == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().right - axisMargin;
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin
+ labelTextAscentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin;
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin
+ labelTextAscentTab[position] + labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().right;
} else if (BOTTOM == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().bottom - axisMargin
- labelTextDescentTab[position];
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
+ labelTextAscentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
+ labelTextAscentTab[position];
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin +
labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().bottom;
} else if (TOP == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().top + axisMargin
+ labelTextAscentTab[position];
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
- labelTextDescentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
- labelTextDescentTab[position];
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin -
labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().top;
} else {
throw new IllegalArgumentException("Invalid axis position: " + position);
}
}
/**
* Prepare axes coordinates and draw axes lines(if enabled) in the background.
*
* @param canvas
*/
public void drawInBackground(Canvas canvas) {
Axis axis = chart.getChartData().getAxisYLeft();
if (null != axis) {
prepareAxisToDraw(axis, LEFT);
drawAxisLines(canvas, axis, LEFT);
}
axis = chart.getChartData().getAxisYRight();
if (null != axis) {
prepareAxisToDraw(axis, RIGHT);
drawAxisLines(canvas, axis, RIGHT);
}
axis = chart.getChartData().getAxisXBottom();
if (null != axis) {
prepareAxisToDraw(axis, BOTTOM);
drawAxisLines(canvas, axis, BOTTOM);
}
axis = chart.getChartData().getAxisXTop();
if (null != axis) {
prepareAxisToDraw(axis, TOP);
drawAxisLines(canvas, axis, TOP);
}
}
private void prepareAxisToDraw(Axis axis, int position) {
if (axis.isAutoGenerated()) {
prepareAutoGeneratedAxis(axis, position);
} else {
prepareCustomAxis(axis, position);
}
}
/**
* Draw axes labels and names in the foreground.
*
* @param canvas
*/
public void drawInForeground(Canvas canvas) {
Axis axis = chart.getChartData().getAxisYLeft();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, LEFT);
}
axis = chart.getChartData().getAxisYRight();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, RIGHT);
}
axis = chart.getChartData().getAxisXBottom();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, BOTTOM);
}
axis = chart.getChartData().getAxisXTop();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, TOP);
}
}
private void prepareCustomAxis(Axis axis, int position) {
final Viewport maxViewport = computator.getMaximumViewport();
final Viewport visibleViewport = computator.getVisibleViewport();
final Rect contentRect = computator.getContentRectMinusAllMargins();
boolean isAxisVertical = isAxisVertical(position);
float viewportMin, viewportMax;
float scale = 1;
if (isAxisVertical) {
if (maxViewport.height() > 0 && visibleViewport.height() > 0) {
scale = contentRect.height() * (maxViewport.height() / visibleViewport.height());
}
viewportMin = visibleViewport.bottom;
viewportMax = visibleViewport.top;
} else {
if (maxViewport.width() > 0 && visibleViewport.width() > 0) {
scale = contentRect.width() * (maxViewport.width() / visibleViewport.width());
}
viewportMin = visibleViewport.left;
viewportMax = visibleViewport.right;
}
if (scale == 0) {
scale = 1;
}
int module = (int) Math.max(1,
Math.ceil((axis.getValues().size() * labelDimensionForStepsTab[position] * 1.5) / scale));
//Reinitialize tab to hold lines coordinates.
if (axis.hasLines() && (linesDrawBufferTab[position].length < axis.getValues().size() * 4)) {
linesDrawBufferTab[position] = new float[axis.getValues().size() * 4];
}
//Reinitialize tabs to hold all raw values to draw.
if (rawValuesTab[position].length < axis.getValues().size()) {
rawValuesTab[position] = new float[axis.getValues().size()];
}
//Reinitialize tabs to hold all raw values to draw.
if (valuesToDrawTab[position].length < axis.getValues().size()) {
valuesToDrawTab[position] = new AxisValue[axis.getValues().size()];
}
float rawValue;
int valueIndex = 0;
int valueToDrawIndex = 0;
for (AxisValue axisValue : axis.getValues()) {
// Draw axis values that are within visible viewport.
final float value = axisValue.getValue();
if (value >= viewportMin && value <= viewportMax) {
// Draw axis values that have 0 module value, this will hide some labels if there is no place for them.
if (0 == valueIndex % module) {
if (isAxisVertical) {
rawValue = computator.computeRawY(value);
} else {
rawValue = computator.computeRawX(value);
}
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
rawValuesTab[position][valueToDrawIndex] = rawValue;
valuesToDrawTab[position][valueToDrawIndex] = axisValue;
++valueToDrawIndex;
}
}
// If within viewport - increment valueIndex;
++valueIndex;
}
}
valuesToDrawNumTab[position] = valueToDrawIndex;
}
private void prepareAutoGeneratedAxis(Axis axis, int position) {
final Viewport visibleViewport = computator.getVisibleViewport();
final Rect contentRect = computator.getContentRectMinusAllMargins();
boolean isAxisVertical = isAxisVertical(position);
float start, stop;
int contentRectDimension;
if (isAxisVertical) {
start = visibleViewport.bottom;
stop = visibleViewport.top;
contentRectDimension = contentRect.height();
} else {
start = visibleViewport.left;
stop = visibleViewport.right;
contentRectDimension = contentRect.width();
}
FloatUtils.computeAutoGeneratedAxisValues(start, stop, Math.abs(contentRectDimension) /
labelDimensionForStepsTab[position] / 2, autoValuesBufferTab[position]);
//Reinitialize tab to hold lines coordinates.
if (axis.hasLines()
&& (linesDrawBufferTab[position].length < autoValuesBufferTab[position].valuesNumber * 4)) {
linesDrawBufferTab[position] = new float[autoValuesBufferTab[position].valuesNumber * 4];
}
//Reinitialize tabs to hold all raw and auto values.
if (rawValuesTab[position].length < autoValuesBufferTab[position].valuesNumber) {
rawValuesTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
}
if (autoValuesToDrawTab[position].length < autoValuesBufferTab[position].valuesNumber) {
autoValuesToDrawTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
}
float rawValue;
int valueToDrawIndex = 0;
for (int i = 0; i < autoValuesBufferTab[position].valuesNumber; ++i) {
if (isAxisVertical) {
rawValue = computator.computeRawY(autoValuesBufferTab[position].values[i]);
} else {
rawValue = computator.computeRawX(autoValuesBufferTab[position].values[i]);
}
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
rawValuesTab[position][valueToDrawIndex] = rawValue;
autoValuesToDrawTab[position][valueToDrawIndex] = autoValuesBufferTab[position].values[i];
++valueToDrawIndex;
}
}
valuesToDrawNumTab[position] = valueToDrawIndex;
}
private boolean checkRawValue(Rect rect, float rawValue, boolean axisInside, int position, boolean isVertical) {
if (axisInside) {
if (isVertical) {
float marginBottom = labelTextAscentTab[BOTTOM] + axisMargin;
float marginTop = labelTextAscentTab[TOP] + axisMargin;
if (rawValue <= rect.bottom - marginBottom && rawValue >= rect.top + marginTop) {
return true;
} else {
return false;
}
} else {
float margin = labelWidthTab[position] / 2;
if (rawValue >= rect.left + margin && rawValue <= rect.right - margin) {
return true;
} else {
return false;
}
}
}
return true;
}
private void drawAxisLines(Canvas canvas, Axis axis, int position) {
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
float separationX1, separationY1, separationX2, separationY2;
separationX1 = separationY1 = separationX2 = separationY2 = 0;
float lineX1, lineY1, lineX2, lineY2;
lineX1 = lineY1 = lineX2 = lineY2 = 0;
boolean isAxisVertical = isAxisVertical(position);
if (LEFT == position || RIGHT == position) {
separationX1 = separationX2 = separationLineTab[position];
separationY1 = contentRectMargins.bottom;
separationY2 = contentRectMargins.top;
lineX1 = contentRectMargins.left;
lineX2 = contentRectMargins.right;
} else if (TOP == position || BOTTOM == position) {
separationX1 = contentRectMargins.left;
separationX2 = contentRectMargins.right;
separationY1 = separationY2 = separationLineTab[position];
lineY1 = contentRectMargins.top;
lineY2 = contentRectMargins.bottom;
}
// Draw separation line with the same color as axis labels and name.
if (axis.hasSeparationLine()) {
canvas.drawLine(separationX1, separationY1, separationX2, separationY2, labelPaintTab[position]);
}
if (axis.hasLines()) {
int valueToDrawIndex = 0;
for (; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
if (isAxisVertical) {
lineY1 = lineY2 = rawValuesTab[position][valueToDrawIndex];
} else {
lineX1 = lineX2 = rawValuesTab[position][valueToDrawIndex];
}
linesDrawBufferTab[position][valueToDrawIndex * 4 + 0] = lineX1;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 1] = lineY1;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 2] = lineX2;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 3] = lineY2;
}
canvas.drawLines(linesDrawBufferTab[position], 0, valueToDrawIndex * 4, linePaintTab[position]);
}
}
private void drawAxisLabelsAndName(Canvas canvas, Axis axis, int position) {
float labelX, labelY;
labelX = labelY = 0;
boolean isAxisVertical = isAxisVertical(position);
if (LEFT == position || RIGHT == position) {
labelX = labelBaselineTab[position];
} else if (TOP == position || BOTTOM == position) {
labelY = labelBaselineTab[position];
}
for (int valueToDrawIndex = 0; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
int charsNumber = 0;
if (axis.isAutoGenerated()) {
final float value = autoValuesToDrawTab[position][valueToDrawIndex];
charsNumber = axis.getFormatter().formatValueForAutoGeneratedAxis(labelBuffer, value,
autoValuesBufferTab[position].decimals);
} else {
AxisValue axisValue = valuesToDrawTab[position][valueToDrawIndex];
charsNumber = axis.getFormatter().formatValueForManualAxis(labelBuffer, axisValue);
}
if (isAxisVertical) {
labelY = rawValuesTab[position][valueToDrawIndex];
} else {
labelX = rawValuesTab[position][valueToDrawIndex];
}
if (axis.hasTiltedLabels()) {
canvas.save();
canvas.translate(tiltedLabelXTranslation[position], tiltedLabelYTranslation[position]);
canvas.rotate(-45, labelX, labelY);
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
labelPaintTab[position]);
canvas.restore();
} else {
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
labelPaintTab[position]);
}
}
// Drawing axis name
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
if (!TextUtils.isEmpty(axis.getName())) {
if (isAxisVertical) {
canvas.save();
canvas.rotate(-90, contentRectMargins.centerY(), contentRectMargins.centerY());
canvas.drawText(axis.getName(), contentRectMargins.centerY(), nameBaselineTab[position],
namePaintTab[position]);
canvas.restore();
} else {
canvas.drawText(axis.getName(), contentRectMargins.centerX(), nameBaselineTab[position],
namePaintTab[position]);
}
}
}
private boolean isAxisVertical(int position) {
if (LEFT == position || RIGHT == position) {
return true;
} else if (TOP == position || BOTTOM == position) {
return false;
} else {
throw new IllegalArgumentException("Invalid axis position " + position);
}
}
}
@@ -0,0 +1,351 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.formatter.BubbleChartValueFormatter;
import lecho.lib.hellocharts.model.BubbleChartData;
import lecho.lib.hellocharts.model.BubbleValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.ValueShape;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.BubbleChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
public class BubbleChartRenderer extends AbstractChartRenderer {
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private BubbleChartDataProvider dataProvider;
/**
* Additional value added to bubble radius when drawing highlighted bubble, used to give tauch feedback.
*/
private int touchAdditional;
/**
* Scales for bubble radius value, only one is used depending on screen orientation;
*/
private float bubbleScaleX;
private float bubbleScaleY;
/**
* True if bubbleScale = bubbleScaleX so the renderer should used {@link ChartComputator#computeRawDistanceX(float)}
* , if false bubbleScale = bubbleScaleY and renderer should use
* {@link ChartComputator#computeRawDistanceY(float)}.
*/
private boolean isBubbleScaledByX = true;
/**
* Maximum bubble radius.
*/
private float maxRadius;
/**
* Minimal bubble radius in pixels.
*/
private float minRawRadius;
private PointF bubbleCenter = new PointF();
private Paint bubblePaint = new Paint();
/**
* Rect used for drawing bubbles with SHAPE_SQUARE.
*/
private RectF bubbleRect = new RectF();
private boolean hasLabels;
private boolean hasLabelsOnlyForSelected;
private BubbleChartValueFormatter valueFormatter;
private Viewport tempMaximumViewport = new Viewport();
public BubbleChartRenderer(Context context, Chart chart, BubbleChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
bubblePaint.setAntiAlias(true);
bubblePaint.setStyle(Paint.Style.FILL);
}
@Override
public void onChartSizeChanged() {
final ChartComputator computator = chart.getChartComputator();
Rect contentRect = computator.getContentRectMinusAllMargins();
if (contentRect.width() < contentRect.height()) {
isBubbleScaledByX = true;
} else {
isBubbleScaledByX = false;
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
BubbleChartData data = dataProvider.getBubbleChartData();
this.hasLabels = data.hasLabels();
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
this.valueFormatter = data.getFormatter();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
drawBubbles(canvas);
if (isTouched()) {
highlightBubbles(canvas);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final BubbleChartData data = dataProvider.getBubbleChartData();
int valueIndex = 0;
for (BubbleValue bubbleValue : data.getValues()) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
if (bubbleRect.contains(touchX, touchY)) {
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
}
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
final float diffX = touchX - bubbleCenter.x;
final float diffY = touchY - bubbleCenter.y;
final float touchDistance = (float) Math.sqrt((diffX * diffX) + (diffY * diffY));
if (touchDistance <= rawRadius) {
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
}
} else {
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
}
++valueIndex;
}
return isTouched();
}
/**
* Removes empty spaces on sides of chart(left-right for landscape, top-bottom for portrait). *This method should be
* called after layout had been drawn*. Because most often chart is drawn as rectangle with proportions other than
* 1:1 and bubbles have to be drawn as circles not ellipses I am unable to calculate correct margins based on chart
* data only. I need to know chart dimension to remove extra empty spaces, that bad because viewport depends a
* little on contentRectMinusAllMargins.
*/
public void removeMargins() {
final Rect contentRect = computator.getContentRectMinusAllMargins();
if (contentRect.height() == 0 || contentRect.width() == 0) {
// View probably not yet measured, skip removing margins.
return;
}
final float pxX = computator.computeRawDistanceX(maxRadius * bubbleScaleX);
final float pxY = computator.computeRawDistanceY(maxRadius * bubbleScaleY);
final float scaleX = computator.getMaximumViewport().width() / contentRect.width();
final float scaleY = computator.getMaximumViewport().height() / contentRect.height();
float dx = 0;
float dy = 0;
if (isBubbleScaledByX) {
dy = (pxY - pxX) * scaleY * 0.75f;
} else {
dx = (pxX - pxY) * scaleX * 0.75f;
}
Viewport maxViewport = computator.getMaximumViewport();
maxViewport.inset(dx, dy);
Viewport currentViewport = computator.getCurrentViewport();
currentViewport.inset(dx, dy);
computator.setMaxViewport(maxViewport);
computator.setCurrentViewport(currentViewport);
}
private void drawBubbles(Canvas canvas) {
final BubbleChartData data = dataProvider.getBubbleChartData();
for (BubbleValue bubbleValue : data.getValues()) {
drawBubble(canvas, bubbleValue);
}
}
private void drawBubble(Canvas canvas, BubbleValue bubbleValue) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
// Not touched bubbles are a little smaller than touched to give user touch feedback.
rawRadius -= touchAdditional;
bubbleRect.inset(touchAdditional, touchAdditional);
bubblePaint.setColor(bubbleValue.getColor());
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_DRAW);
}
private void drawBubbleShapeAndLabel(Canvas canvas, BubbleValue bubbleValue, float rawRadius, int mode) {
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
canvas.drawRect(bubbleRect, bubblePaint);
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
canvas.drawCircle(bubbleCenter.x, bubbleCenter.y, rawRadius, bubblePaint);
} else {
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
}
if (MODE_HIGHLIGHT == mode) {
if (hasLabels || hasLabelsOnlyForSelected) {
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
}
} else if (MODE_DRAW == mode) {
if (hasLabels) {
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
}
} else {
throw new IllegalStateException("Cannot process bubble in mode: " + mode);
}
}
private void highlightBubbles(Canvas canvas) {
final BubbleChartData data = dataProvider.getBubbleChartData();
BubbleValue bubbleValue = data.getValues().get(selectedValue.getFirstIndex());
highlightBubble(canvas, bubbleValue);
}
private void highlightBubble(Canvas canvas, BubbleValue bubbleValue) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
bubblePaint.setColor(bubbleValue.getDarkenColor());
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_HIGHLIGHT);
}
/**
* Calculate bubble radius and center x and y coordinates. Center x and x will be stored in point parameter, radius
* will be returned as float value.
*/
private float processBubble(BubbleValue bubbleValue, PointF point) {
final float rawX = computator.computeRawX(bubbleValue.getX());
final float rawY = computator.computeRawY(bubbleValue.getY());
float radius = (float) Math.sqrt(Math.abs(bubbleValue.getZ()) / Math.PI);
float rawRadius;
if (isBubbleScaledByX) {
radius *= bubbleScaleX;
rawRadius = computator.computeRawDistanceX(radius);
} else {
radius *= bubbleScaleY;
rawRadius = computator.computeRawDistanceY(radius);
}
if (rawRadius < minRawRadius + touchAdditional) {
rawRadius = minRawRadius + touchAdditional;
}
bubbleCenter.set(rawX, rawY);
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
bubbleRect.set(rawX - rawRadius, rawY - rawRadius, rawX + rawRadius, rawY + rawRadius);
}
return rawRadius;
}
private void drawLabel(Canvas canvas, BubbleValue bubbleValue, float rawX, float rawY) {
final Rect contentRect = computator.getContentRectMinusAllMargins();
final int numChars = valueFormatter.formatChartValue(labelBuffer, bubbleValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = rawX - labelWidth / 2 - labelMargin;
float right = rawX + labelWidth / 2 + labelMargin;
float top = rawY - labelHeight / 2 - labelMargin;
float bottom = rawY + labelHeight / 2 + labelMargin;
if (top < contentRect.top) {
top = rawY;
bottom = rawY + labelHeight + labelMargin * 2;
}
if (bottom > contentRect.bottom) {
top = rawY - labelHeight - labelMargin * 2;
bottom = rawY;
}
if (left < contentRect.left) {
left = rawX;
right = rawX + labelWidth + labelMargin * 2;
}
if (right > contentRect.right) {
left = rawX - labelWidth - labelMargin * 2;
right = rawX;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
bubbleValue.getDarkenColor());
}
private void calculateMaxViewport() {
float maxZ = Float.MIN_VALUE;
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
BubbleChartData data = dataProvider.getBubbleChartData();
// TODO: Optimize.
for (BubbleValue bubbleValue : data.getValues()) {
if (Math.abs(bubbleValue.getZ()) > maxZ) {
maxZ = Math.abs(bubbleValue.getZ());
}
if (bubbleValue.getX() < tempMaximumViewport.left) {
tempMaximumViewport.left = bubbleValue.getX();
}
if (bubbleValue.getX() > tempMaximumViewport.right) {
tempMaximumViewport.right = bubbleValue.getX();
}
if (bubbleValue.getY() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = bubbleValue.getY();
}
if (bubbleValue.getY() > tempMaximumViewport.top) {
tempMaximumViewport.top = bubbleValue.getY();
}
}
maxRadius = (float) Math.sqrt(maxZ / Math.PI);
// Number 4 is determined by trials and errors method, no magic behind it:).
bubbleScaleX = tempMaximumViewport.width() / (maxRadius * 4);
if (bubbleScaleX == 0) {
// case for 0 viewport width.
bubbleScaleX = 1;
}
bubbleScaleY = tempMaximumViewport.height() / (maxRadius * 4);
if (bubbleScaleY == 0) {
// case for 0 viewport height.
bubbleScaleY = 1;
}
// For cases when user sets different than 1 bubble scale in BubbleChartData.
bubbleScaleX *= data.getBubbleScale();
bubbleScaleY *= data.getBubbleScale();
// Prevent cutting of bubbles on the edges of chart area.
tempMaximumViewport.inset(-maxRadius * bubbleScaleX, -maxRadius * bubbleScaleY);
minRawRadius = ChartUtils.dp2px(density, dataProvider.getBubbleChartData().getMinBubbleRadius());
}
}
@@ -0,0 +1,63 @@
package lecho.lib.hellocharts.renderer;
import android.graphics.Canvas;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
/**
* Interface for all chart renderer.
*/
public interface ChartRenderer {
public void onChartSizeChanged();
public void onChartDataChanged();
public void onChartViewportChanged();
public void resetRenderer();
/**
* Draw chart data.
*/
public void draw(Canvas canvas);
/**
* Draw chart data that should not be clipped to contentRect area.
*/
public void drawUnclipped(Canvas canvas);
/**
* Checks if given pixel coordinates corresponds to any chart value. If yes return true and set selectedValue, if
* not selectedValue should be *cleared* and method should return false.
*/
public boolean checkTouch(float touchX, float touchY);
/**
* Returns true if there is value selected.
*/
public boolean isTouched();
/**
* Clear value selection.
*/
public void clearTouch();
public Viewport getMaximumViewport();
public void setMaximumViewport(Viewport maxViewport);
public Viewport getCurrentViewport();
public void setCurrentViewport(Viewport viewport);
public boolean isViewportCalculationEnabled();
public void setViewportCalculationEnabled(boolean isEnabled);
public void selectValue(SelectedValue selectedValue);
public SelectedValue getSelectedValue();
}
@@ -0,0 +1,422 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.PointF;
import android.graphics.RectF;
import lecho.lib.hellocharts.model.Column;
import lecho.lib.hellocharts.model.ColumnChartData;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.SubcolumnValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Magic renderer for ColumnChart.
*/
public class ColumnChartRenderer extends AbstractChartRenderer {
public static final int DEFAULT_SUBCOLUMN_SPACING_DP = 1;
public static final int DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_CHECK_TOUCH = 1;
private static final int MODE_HIGHLIGHT = 2;
private ColumnChartDataProvider dataProvider;
/**
* Additional width for hightlighted column, used to give tauch feedback.
*/
private int touchAdditionalWidth;
/**
* Spacing between sub-columns.
*/
private int subcolumnSpacing;
/**
* Paint used to draw every column.
*/
private Paint columnPaint = new Paint();
/**
* Holds coordinates for currently processed column/sub-column.
*/
private RectF drawRect = new RectF();
/**
* Coordinated of user tauch.
*/
private PointF touchedPoint = new PointF();
private float fillRatio;
private float baseValue;
private Viewport tempMaximumViewport = new Viewport();
public ColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
subcolumnSpacing = ChartUtils.dp2px(density, DEFAULT_SUBCOLUMN_SPACING_DP);
touchAdditionalWidth = ChartUtils.dp2px(density, DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP);
columnPaint.setAntiAlias(true);
columnPaint.setStyle(Paint.Style.FILL);
columnPaint.setStrokeCap(Cap.SQUARE);
}
@Override
public void onChartSizeChanged() {
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
ColumnChartData data = dataProvider.getColumnChartData();
fillRatio = data.getFillRatio();
baseValue = data.getBaseValue();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
public void draw(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
if (data.isStacked()) {
drawColumnForStacked(canvas);
if (isTouched()) {
highlightColumnForStacked(canvas);
}
} else {
drawColumnsForSubcolumns(canvas);
if (isTouched()) {
highlightColumnsForSubcolumns(canvas);
}
}
}
@Override
public void drawUnclipped(Canvas canvas) {
// Do nothing, for this kind of chart there is nothing to draw beyond clipped area
}
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final ColumnChartData data = dataProvider.getColumnChartData();
if (data.isStacked()) {
checkTouchForStacked(touchX, touchY);
} else {
checkTouchForSubcolumns(touchX, touchY);
}
return isTouched();
}
private void calculateMaxViewport() {
final ColumnChartData data = dataProvider.getColumnChartData();
// Column chart always has X values from 0 to numColumns-1, to add some margin on the left and right I added
// extra 0.5 to the each side, that margins will be negative scaled according to number of columns, so for more
// columns there will be less margin.
tempMaximumViewport.set(-0.5f, baseValue, data.getColumns().size() - 0.5f, baseValue);
if (data.isStacked()) {
calculateMaxViewportForStacked(data);
} else {
calculateMaxViewportForSubcolumns(data);
}
}
private void calculateMaxViewportForSubcolumns(ColumnChartData data) {
for (Column column : data.getColumns()) {
for (SubcolumnValue columnValue : column.getValues()) {
if (columnValue.getValue() >= baseValue && columnValue.getValue() > tempMaximumViewport.top) {
tempMaximumViewport.top = columnValue.getValue();
}
if (columnValue.getValue() < baseValue && columnValue.getValue() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = columnValue.getValue();
}
}
}
}
private void calculateMaxViewportForStacked(ColumnChartData data) {
for (Column column : data.getColumns()) {
float sumPositive = baseValue;
float sumNegative = baseValue;
for (SubcolumnValue columnValue : column.getValues()) {
if (columnValue.getValue() >= baseValue) {
sumPositive += columnValue.getValue();
} else {
sumNegative += columnValue.getValue();
}
}
if (sumPositive > tempMaximumViewport.top) {
tempMaximumViewport.top = sumPositive;
}
if (sumNegative < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = sumNegative;
}
}
}
private void drawColumnsForSubcolumns(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
processColumnForSubcolumns(canvas, column, columnWidth, columnIndex, MODE_DRAW);
++columnIndex;
}
}
private void highlightColumnsForSubcolumns(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
Column column = data.getColumns().get(selectedValue.getFirstIndex());
processColumnForSubcolumns(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
}
private void checkTouchForSubcolumns(float touchX, float touchY) {
// Using member variable to hold touch point to avoid too much parameters in methods.
touchedPoint.x = touchX;
touchedPoint.y = touchY;
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
// canvas is not needed for checking touch
processColumnForSubcolumns(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
++columnIndex;
}
}
private void processColumnForSubcolumns(Canvas canvas, Column column, float columnWidth, int columnIndex,
int mode) {
// For n subcolumns there will be n-1 spacing and there will be one
// subcolumn for every columnValue
float subcolumnWidth = (columnWidth - (subcolumnSpacing * (column.getValues().size() - 1)))
/ column.getValues().size();
if (subcolumnWidth < 1) {
subcolumnWidth = 1;
}
// Columns are indexes from 0 to n, column index is also column X value
final float rawX = computator.computeRawX(columnIndex);
final float halfColumnWidth = columnWidth / 2;
final float baseRawY = computator.computeRawY(baseValue);
// First subcolumn will starts at the left edge of current column,
// rawValueX is horizontal center of that column
float subcolumnRawX = rawX - halfColumnWidth;
int valueIndex = 0;
for (SubcolumnValue columnValue : column.getValues()) {
columnPaint.setColor(columnValue.getColor());
if (subcolumnRawX > rawX + halfColumnWidth) {
break;
}
final float rawY = computator.computeRawY(columnValue.getValue());
calculateRectToDraw(columnValue, subcolumnRawX, subcolumnRawX + subcolumnWidth, baseRawY, rawY);
switch (mode) {
case MODE_DRAW:
drawSubcolumn(canvas, column, columnValue, false);
break;
case MODE_HIGHLIGHT:
highlightSubcolumn(canvas, column, columnValue, valueIndex, false);
break;
case MODE_CHECK_TOUCH:
checkRectToDraw(columnIndex, valueIndex);
break;
default:
// There no else, every case should be handled or exception will
// be thrown
throw new IllegalStateException("Cannot process column in mode: " + mode);
}
subcolumnRawX += subcolumnWidth + subcolumnSpacing;
++valueIndex;
}
}
private void drawColumnForStacked(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
// Columns are indexes from 0 to n, column index is also column X value
int columnIndex = 0;
for (Column column : data.getColumns()) {
processColumnForStacked(canvas, column, columnWidth, columnIndex, MODE_DRAW);
++columnIndex;
}
}
private void highlightColumnForStacked(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
// Columns are indexes from 0 to n, column index is also column X value
Column column = data.getColumns().get(selectedValue.getFirstIndex());
processColumnForStacked(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
}
private void checkTouchForStacked(float touchX, float touchY) {
touchedPoint.x = touchX;
touchedPoint.y = touchY;
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
// canvas is not needed for checking touch
processColumnForStacked(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
++columnIndex;
}
}
private void processColumnForStacked(Canvas canvas, Column column, float columnWidth, int columnIndex, int mode) {
final float rawX = computator.computeRawX(columnIndex);
final float halfColumnWidth = columnWidth / 2;
float mostPositiveValue = baseValue;
float mostNegativeValue = baseValue;
float subcolumnBaseValue = baseValue;
int valueIndex = 0;
for (SubcolumnValue columnValue : column.getValues()) {
columnPaint.setColor(columnValue.getColor());
if (columnValue.getValue() >= baseValue) {
// Using values instead of raw pixels make code easier to
// understand(for me)
subcolumnBaseValue = mostPositiveValue;
mostPositiveValue += columnValue.getValue();
} else {
subcolumnBaseValue = mostNegativeValue;
mostNegativeValue += columnValue.getValue();
}
final float rawBaseY = computator.computeRawY(subcolumnBaseValue);
final float rawY = computator.computeRawY(subcolumnBaseValue + columnValue.getValue());
calculateRectToDraw(columnValue, rawX - halfColumnWidth, rawX + halfColumnWidth, rawBaseY, rawY);
switch (mode) {
case MODE_DRAW:
drawSubcolumn(canvas, column, columnValue, true);
break;
case MODE_HIGHLIGHT:
highlightSubcolumn(canvas, column, columnValue, valueIndex, true);
break;
case MODE_CHECK_TOUCH:
checkRectToDraw(columnIndex, valueIndex);
break;
default:
// There no else, every case should be handled or exception will
// be thrown
throw new IllegalStateException("Cannot process column in mode: " + mode);
}
++valueIndex;
}
}
private void drawSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked) {
canvas.drawRect(drawRect, columnPaint);
if (column.hasLabels()) {
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
}
}
private void highlightSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, int valueIndex,
boolean isStacked) {
if (selectedValue.getSecondIndex() == valueIndex) {
columnPaint.setColor(columnValue.getDarkenColor());
canvas.drawRect(drawRect.left - touchAdditionalWidth, drawRect.top, drawRect.right + touchAdditionalWidth,
drawRect.bottom, columnPaint);
if (column.hasLabels() || column.hasLabelsOnlyForSelected()) {
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
}
}
}
private void checkRectToDraw(int columnIndex, int valueIndex) {
if (drawRect.contains(touchedPoint.x, touchedPoint.y)) {
selectedValue.set(columnIndex, valueIndex, SelectedValueType.COLUMN);
}
}
private float calculateColumnWidth() {
// columnWidht should be at least 2 px
float columnWidth = fillRatio * computator.getContentRectMinusAllMargins().width() / computator
.getVisibleViewport().width();
if (columnWidth < 2) {
columnWidth = 2;
}
return columnWidth;
}
private void calculateRectToDraw(SubcolumnValue columnValue, float left, float right, float rawBaseY, float rawY) {
// Calculate rect that will be drawn as column, subcolumn or label background.
drawRect.left = left;
drawRect.right = right;
if (columnValue.getValue() >= baseValue) {
drawRect.top = rawY;
drawRect.bottom = rawBaseY - subcolumnSpacing;
} else {
drawRect.bottom = rawY;
drawRect.top = rawBaseY + subcolumnSpacing;
}
}
private void drawLabel(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked, float offset) {
final int numChars = column.getFormatter().formatChartValue(labelBuffer, columnValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = drawRect.centerX() - labelWidth / 2 - labelMargin;
float right = drawRect.centerX() + labelWidth / 2 + labelMargin;
float top;
float bottom;
if (isStacked && labelHeight < drawRect.height() - (2 * labelMargin)) {
// For stacked columns draw label only if label height is less than subcolumn height - (2 * labelMargin).
if (columnValue.getValue() >= baseValue) {
top = drawRect.top;
bottom = drawRect.top + labelHeight + labelMargin * 2;
} else {
top = drawRect.bottom - labelHeight - labelMargin * 2;
bottom = drawRect.bottom;
}
} else if (!isStacked) {
// For not stacked draw label at the top for positive and at the bottom for negative values
if (columnValue.getValue() >= baseValue) {
top = drawRect.top - offset - labelHeight - labelMargin * 2;
if (top < computator.getContentRectMinusAllMargins().top) {
top = drawRect.top + offset;
bottom = drawRect.top + offset + labelHeight + labelMargin * 2;
} else {
bottom = drawRect.top - offset;
}
} else {
bottom = drawRect.bottom + offset + labelHeight + labelMargin * 2;
if (bottom > computator.getContentRectMinusAllMargins().bottom) {
top = drawRect.bottom - offset - labelHeight - labelMargin * 2;
bottom = drawRect.bottom - offset;
} else {
top = drawRect.bottom + offset;
}
}
} else {
// Draw nothing.
return;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
columnValue.getDarkenColor());
}
}
@@ -0,0 +1,99 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.view.Chart;
public class ComboChartRenderer extends AbstractChartRenderer {
protected List<ChartRenderer> renderers;
protected Viewport unionViewport = new Viewport();
public ComboChartRenderer(Context context, Chart chart) {
super(context, chart);
this.renderers = new ArrayList<>();
}
@Override
public void onChartSizeChanged() {
for (ChartRenderer renderer : renderers) {
renderer.onChartSizeChanged();
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
for (ChartRenderer renderer : renderers) {
renderer.onChartDataChanged();
}
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
int rendererIndex = 0;
for (ChartRenderer renderer : renderers) {
renderer.onChartViewportChanged();
if (rendererIndex == 0) {
unionViewport.set(renderer.getMaximumViewport());
} else {
unionViewport.union(renderer.getMaximumViewport());
}
++rendererIndex;
}
computator.setMaxViewport(unionViewport);
computator.setCurrentViewport(unionViewport);
}
}
public void draw(Canvas canvas) {
for (ChartRenderer renderer : renderers) {
renderer.draw(canvas);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
for (ChartRenderer renderer : renderers) {
renderer.drawUnclipped(canvas);
}
}
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
int rendererIndex = renderers.size() - 1;
for (; rendererIndex >= 0; rendererIndex--) {
ChartRenderer renderer = renderers.get(rendererIndex);
if (renderer.checkTouch(touchX, touchY)) {
selectedValue.set(renderer.getSelectedValue());
break;
}
}
//clear the rest of renderers if value was selected, if value was not selected this loop
// will not be executed.
for (rendererIndex--; rendererIndex >= 0; rendererIndex--) {
ChartRenderer renderer = renderers.get(rendererIndex);
renderer.clearTouch();
}
return isTouched();
}
@Override
public void clearTouch() {
for (ChartRenderer renderer : renderers) {
renderer.clearTouch();
}
selectedValue.clear();
}
}
@@ -0,0 +1,40 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.view.Chart;
public class ComboLineColumnChartRenderer extends ComboChartRenderer {
private ColumnChartRenderer columnChartRenderer;
private LineChartRenderer lineChartRenderer;
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
LineChartDataProvider lineChartDataProvider) {
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider),
new LineChartRenderer(context, chart, lineChartDataProvider));
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
LineChartDataProvider lineChartDataProvider) {
this(context, chart, columnChartRenderer, new LineChartRenderer(context, chart, lineChartDataProvider));
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
LineChartRenderer lineChartRenderer) {
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider), lineChartRenderer);
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
LineChartRenderer lineChartRenderer) {
super(context, chart);
this.columnChartRenderer = columnChartRenderer;
this.lineChartRenderer = lineChartRenderer;
renderers.add(this.columnChartRenderer);
renderers.add(this.lineChartRenderer);
}
}
@@ -0,0 +1,514 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Path;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.Shader;
import lecho.lib.hellocharts.model.Line;
import lecho.lib.hellocharts.model.LineChartData;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.ValueShape;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for line chart. Can draw lines, cubic lines, filled area chart and scattered chart.
*/
public class LineChartRenderer extends AbstractChartRenderer {
private static final float LINE_SMOOTHNESS = 0.16f;
private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 3;
private static final int DEFAULT_TOUCH_TOLERANCE_MARGIN_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private LineChartDataProvider dataProvider;
private int checkPrecision;
private float baseValue;
private int touchToleranceMargin;
private Path path = new Path();
private Paint linePaint = new Paint();
private Paint pointPaint = new Paint();
private Bitmap softwareBitmap;
private Canvas softwareCanvas = new Canvas();
private Viewport tempMaximumViewport = new Viewport();
public LineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchToleranceMargin = ChartUtils.dp2px(density, DEFAULT_TOUCH_TOLERANCE_MARGIN_DP);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Cap.ROUND);
linePaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_LINE_STROKE_WIDTH_DP));
pointPaint.setAntiAlias(true);
pointPaint.setStyle(Paint.Style.FILL);
checkPrecision = ChartUtils.dp2px(density, 2);
}
public void onChartSizeChanged() {
final int internalMargin = calculateContentRectInternalMargin();
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
internalMargin, internalMargin);
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
Bitmap.Config.ARGB_8888);
softwareCanvas.setBitmap(softwareBitmap);
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
final int internalMargin = calculateContentRectInternalMargin();
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
internalMargin, internalMargin);
baseValue = dataProvider.getLineChartData().getBaseValue();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
final LineChartData data = dataProvider.getLineChartData();
final Canvas drawCanvas;
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
// softwareCanvas.
if (null != softwareBitmap) {
drawCanvas = softwareCanvas;
drawCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
} else {
drawCanvas = canvas;
}
for (Line line : data.getLines()) {
if (line.hasLines()) {
if (line.isCubic()) {
drawSmoothPath(drawCanvas, line);
} else if (line.isSquare()) {
drawSquarePath(drawCanvas, line);
} else {
drawPath(drawCanvas, line);
}
}
}
if (null != softwareBitmap) {
canvas.drawBitmap(softwareBitmap, 0, 0, null);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
final LineChartData data = dataProvider.getLineChartData();
int lineIndex = 0;
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
drawPoints(canvas, line, lineIndex, MODE_DRAW);
}
++lineIndex;
}
if (isTouched()) {
// Redraw touched point to bring it to the front
highlightPoints(canvas);
}
}
private boolean checkIfShouldDrawPoints(Line line) {
return line.hasPoints() || line.getValues().size() == 1;
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final LineChartData data = dataProvider.getLineChartData();
int lineIndex = 0;
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
final float rawValueX = computator.computeRawX(pointValue.getX());
final float rawValueY = computator.computeRawY(pointValue.getY());
if (isInArea(rawValueX, rawValueY, touchX, touchY, pointRadius + touchToleranceMargin)) {
selectedValue.set(lineIndex, valueIndex, SelectedValueType.LINE);
}
++valueIndex;
}
}
++lineIndex;
}
return isTouched();
}
private void calculateMaxViewport() {
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
LineChartData data = dataProvider.getLineChartData();
for (Line line : data.getLines()) {
// Calculate max and min for viewport.
for (PointValue pointValue : line.getValues()) {
if (pointValue.getX() < tempMaximumViewport.left) {
tempMaximumViewport.left = pointValue.getX();
}
if (pointValue.getX() > tempMaximumViewport.right) {
tempMaximumViewport.right = pointValue.getX();
}
if (pointValue.getY() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = pointValue.getY();
}
if (pointValue.getY() > tempMaximumViewport.top) {
tempMaximumViewport.top = pointValue.getY();
}
}
}
}
private int calculateContentRectInternalMargin() {
int contentAreaMargin = 0;
final LineChartData data = dataProvider.getLineChartData();
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
int margin = line.getPointRadius() + DEFAULT_TOUCH_TOLERANCE_MARGIN_DP;
if (margin > contentAreaMargin) {
contentAreaMargin = margin;
}
}
}
return ChartUtils.dp2px(density, contentAreaMargin);
}
/**
* Draws lines, uses path for drawing filled area on software canvas. Line is drawn with canvas.drawLines() method.
*/
private void drawPath(Canvas canvas, final Line line) {
prepareLinePaint(line);
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (valueIndex == 0) {
path.moveTo(rawX, rawY);
} else {
path.lineTo(rawX, rawY);
}
++valueIndex;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void drawSquarePath(Canvas canvas, final Line line) {
prepareLinePaint(line);
int valueIndex = 0;
float previousRawY = 0;
for (PointValue pointValue : line.getValues()) {
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (valueIndex == 0) {
path.moveTo(rawX, rawY);
} else {
path.lineTo(rawX, previousRawY);
path.lineTo(rawX, rawY);
}
previousRawY = rawY;
++valueIndex;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void drawSmoothPath(Canvas canvas, final Line line) {
prepareLinePaint(line);
final int lineSize = line.getValues().size();
float prePreviousPointX = Float.NaN;
float prePreviousPointY = Float.NaN;
float previousPointX = Float.NaN;
float previousPointY = Float.NaN;
float currentPointX = Float.NaN;
float currentPointY = Float.NaN;
float nextPointX = Float.NaN;
float nextPointY = Float.NaN;
for (int valueIndex = 0; valueIndex < lineSize; ++valueIndex) {
if (Float.isNaN(currentPointX)) {
PointValue linePoint = line.getValues().get(valueIndex);
currentPointX = computator.computeRawX(linePoint.getX());
currentPointY = computator.computeRawY(linePoint.getY());
}
if (Float.isNaN(previousPointX)) {
if (valueIndex > 0) {
PointValue linePoint = line.getValues().get(valueIndex - 1);
previousPointX = computator.computeRawX(linePoint.getX());
previousPointY = computator.computeRawY(linePoint.getY());
} else {
previousPointX = currentPointX;
previousPointY = currentPointY;
}
}
if (Float.isNaN(prePreviousPointX)) {
if (valueIndex > 1) {
PointValue linePoint = line.getValues().get(valueIndex - 2);
prePreviousPointX = computator.computeRawX(linePoint.getX());
prePreviousPointY = computator.computeRawY(linePoint.getY());
} else {
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
}
}
// nextPoint is always new one or it is equal currentPoint.
if (valueIndex < lineSize - 1) {
PointValue linePoint = line.getValues().get(valueIndex + 1);
nextPointX = computator.computeRawX(linePoint.getX());
nextPointY = computator.computeRawY(linePoint.getY());
} else {
nextPointX = currentPointX;
nextPointY = currentPointY;
}
if (valueIndex == 0) {
// Move to start point.
path.moveTo(currentPointX, currentPointY);
} else {
// Calculate control points.
final float firstDiffX = (currentPointX - prePreviousPointX);
final float firstDiffY = (currentPointY - prePreviousPointY);
final float secondDiffX = (nextPointX - previousPointX);
final float secondDiffY = (nextPointY - previousPointY);
final float firstControlPointX = previousPointX + (LINE_SMOOTHNESS * firstDiffX);
final float firstControlPointY = previousPointY + (LINE_SMOOTHNESS * firstDiffY);
final float secondControlPointX = currentPointX - (LINE_SMOOTHNESS * secondDiffX);
final float secondControlPointY = currentPointY - (LINE_SMOOTHNESS * secondDiffY);
path.cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, secondControlPointY,
currentPointX, currentPointY);
}
// Shift values by one back to prevent recalculation of values that have
// been already calculated.
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
previousPointX = currentPointX;
previousPointY = currentPointY;
currentPointX = nextPointX;
currentPointY = nextPointY;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void prepareLinePaint(final Line line) {
linePaint.setStrokeWidth(ChartUtils.dp2px(density, line.getStrokeWidth()));
linePaint.setColor(line.getColor());
linePaint.setPathEffect(line.getPathEffect());
linePaint.setShader(null);
}
// TODO Drawing points can be done in the same loop as drawing lines but it
// may cause problems in the future with
// implementing point styles.
private void drawPoints(Canvas canvas, Line line, int lineIndex, int mode) {
pointPaint.setColor(line.getPointColor());
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (computator.isWithinContentRect(rawX, rawY, checkPrecision)) {
// Draw points only if they are within contentRectMinusAllMargins, using contentRectMinusAllMargins
// instead of viewport to avoid some
// float rounding problems.
if (MODE_DRAW == mode) {
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius);
if (line.hasLabels()) {
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
}
} else if (MODE_HIGHLIGHT == mode) {
highlightPoint(canvas, line, pointValue, rawX, rawY, lineIndex, valueIndex);
} else {
throw new IllegalStateException("Cannot process points in mode: " + mode);
}
}
++valueIndex;
}
}
private void drawPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY,
float pointRadius) {
if (ValueShape.SQUARE.equals(line.getShape())) {
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
pointPaint);
} else if (ValueShape.CIRCLE.equals(line.getShape())) {
canvas.drawCircle(rawX, rawY, pointRadius, pointPaint);
} else if (ValueShape.DIAMOND.equals(line.getShape())) {
canvas.save();
canvas.rotate(45, rawX, rawY);
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
pointPaint);
canvas.restore();
} else {
throw new IllegalArgumentException("Invalid point shape: " + line.getShape());
}
}
private void highlightPoints(Canvas canvas) {
int lineIndex = selectedValue.getFirstIndex();
Line line = dataProvider.getLineChartData().getLines().get(lineIndex);
drawPoints(canvas, line, lineIndex, MODE_HIGHLIGHT);
}
private void highlightPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, int lineIndex,
int valueIndex) {
if (selectedValue.getFirstIndex() == lineIndex && selectedValue.getSecondIndex() == valueIndex) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
pointPaint.setColor(line.getDarkenColor());
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius + touchToleranceMargin);
if (line.hasLabels() || line.hasLabelsOnlyForSelected()) {
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
}
}
}
private void drawLabel(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, float offset) {
final Rect contentRect = computator.getContentRectMinusAllMargins();
final int numChars = line.getFormatter().formatChartValue(labelBuffer, pointValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = rawX - labelWidth / 2 - labelMargin;
float right = rawX + labelWidth / 2 + labelMargin;
float top;
float bottom;
if (pointValue.getY() >= baseValue) {
top = rawY - offset - labelHeight - labelMargin * 2;
bottom = rawY - offset;
} else {
top = rawY + offset;
bottom = rawY + offset + labelHeight + labelMargin * 2;
}
if (top < contentRect.top) {
top = rawY + offset;
bottom = rawY + offset + labelHeight + labelMargin * 2;
}
if (bottom > contentRect.bottom) {
top = rawY - offset - labelHeight - labelMargin * 2;
bottom = rawY - offset;
}
if (left < contentRect.left) {
left = rawX;
right = rawX + labelWidth + labelMargin * 2;
}
if (right > contentRect.right) {
left = rawX - labelWidth - labelMargin * 2;
right = rawX;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
line.getDarkenColor());
}
private void drawArea(Canvas canvas, Line line) {
final int lineSize = line.getValues().size();
if (lineSize < 2) {
//No point to draw area for one point or empty line.
return;
}
final Rect contentRect = computator.getContentRectMinusAllMargins();
final float baseRawValue = Math.min(contentRect.bottom, Math.max(computator.computeRawY(baseValue),
contentRect.top));
//That checks works only if the last point is the right most one.
final float left = Math.max(computator.computeRawX(line.getValues().get(0).getX()), contentRect.left);
final float right = Math.min(computator.computeRawX(line.getValues().get(lineSize - 1).getX()),
contentRect.right);
path.lineTo(right, baseRawValue);
path.lineTo(left, baseRawValue);
path.close();
linePaint.setStyle(Paint.Style.FILL);
linePaint.setAlpha(line.getAreaTransparency());
linePaint.setShader(line.getGradientToTransparent() ?
new LinearGradient(0, 0, 0, canvas.getHeight(), line.getColor(),
line.getColor() & 0x00ffffff, Shader.TileMode.MIRROR) :
null);
canvas.drawPath(path, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
}
private boolean isInArea(float x, float y, float touchX, float touchY, float radius) {
float diffX = touchX - x;
float diffY = touchY - y;
return Math.pow(diffX, 2) + Math.pow(diffY, 2) <= 2 * Math.pow(radius, 2);
}
}
@@ -0,0 +1,513 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextUtils;
import lecho.lib.hellocharts.formatter.PieChartValueFormatter;
import lecho.lib.hellocharts.model.PieChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.SliceValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.PieChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Default renderer for PieChart. PieChart doesn't use viewport concept so it a little different than others chart
* types.
*/
public class PieChartRenderer extends AbstractChartRenderer {
private static final float MAX_WIDTH_HEIGHT = 100f;
private static final int DEFAULT_START_ROTATION = 45;
private static final float DEFAULT_LABEL_INSIDE_RADIUS_FACTOR = 0.7f;
private static final float DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR = 1.0f;
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 8;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private int rotation = DEFAULT_START_ROTATION;
private PieChartDataProvider dataProvider;
private Paint slicePaint = new Paint();
private float maxSum;
private RectF originCircleOval = new RectF();
private RectF drawCircleOval = new RectF();
private PointF sliceVector = new PointF();
private int touchAdditional;
private float circleFillRatio = 1.0f;
// Center circle related attributes
private boolean hasCenterCircle;
private float centerCircleScale;
private Paint centerCirclePaint = new Paint();
// Text1
private Paint centerCircleText1Paint = new Paint();
private FontMetricsInt centerCircleText1FontMetrics = new FontMetricsInt();
// Text2
private Paint centerCircleText2Paint = new Paint();
private FontMetricsInt centerCircleText2FontMetrics = new FontMetricsInt();
// Separation lines
private Paint separationLinesPaint = new Paint();
private boolean hasLabelsOutside;
private boolean hasLabels;
private boolean hasLabelsOnlyForSelected;
private PieChartValueFormatter valueFormatter;
private Viewport tempMaximumViewport = new Viewport();
private Bitmap softwareBitmap;
private Canvas softwareCanvas = new Canvas();
public PieChartRenderer(Context context, Chart chart, PieChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
slicePaint.setAntiAlias(true);
slicePaint.setStyle(Paint.Style.FILL);
centerCirclePaint.setAntiAlias(true);
centerCirclePaint.setStyle(Paint.Style.FILL);
centerCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
centerCircleText1Paint.setAntiAlias(true);
centerCircleText1Paint.setTextAlign(Align.CENTER);
centerCircleText2Paint.setAntiAlias(true);
centerCircleText2Paint.setTextAlign(Align.CENTER);
separationLinesPaint.setAntiAlias(true);
separationLinesPaint.setStyle(Paint.Style.STROKE);
separationLinesPaint.setStrokeCap(Paint.Cap.ROUND);
separationLinesPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
separationLinesPaint.setColor(Color.TRANSPARENT);
}
@Override
public void onChartSizeChanged() {
calculateCircleOval();
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
Bitmap.Config.ARGB_8888);
softwareCanvas.setBitmap(softwareBitmap);
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
final PieChartData data = dataProvider.getPieChartData();
hasLabelsOutside = data.hasLabelsOutside();
hasLabels = data.hasLabels();
hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
valueFormatter = data.getFormatter();
hasCenterCircle = data.hasCenterCircle();
centerCircleScale = data.getCenterCircleScale();
centerCirclePaint.setColor(data.getCenterCircleColor());
if (null != data.getCenterText1Typeface()) {
centerCircleText1Paint.setTypeface(data.getCenterText1Typeface());
}
centerCircleText1Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText1FontSize()));
centerCircleText1Paint.setColor(data.getCenterText1Color());
centerCircleText1Paint.getFontMetricsInt(centerCircleText1FontMetrics);
if (null != data.getCenterText2Typeface()) {
centerCircleText2Paint.setTypeface(data.getCenterText2Typeface());
}
centerCircleText2Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText2FontSize()));
centerCircleText2Paint.setColor(data.getCenterText2Color());
centerCircleText2Paint.getFontMetricsInt(centerCircleText2FontMetrics);
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
// softwareCanvas.
final Canvas drawCanvas;
if (null != softwareBitmap) {
drawCanvas = softwareCanvas;
drawCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
} else {
drawCanvas = canvas;
}
drawSlices(drawCanvas);
drawSeparationLines(drawCanvas);
if (hasCenterCircle) {
drawCenterCircle(drawCanvas);
}
drawLabels(drawCanvas);
if (null != softwareBitmap) {
canvas.drawBitmap(softwareBitmap, 0, 0, null);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final PieChartData data = dataProvider.getPieChartData();
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
final float circleRadius = originCircleOval.width() / 2f;
sliceVector.set(touchX - centerX, touchY - centerY);
// Check if touch is on circle area, if not return false;
if (sliceVector.length() > circleRadius + touchAdditional) {
return false;
}
// Check if touch is not in center circle, if yes return false;
if (data.hasCenterCircle() && sliceVector.length() < circleRadius * data.getCenterCircleScale()) {
return false;
}
// Get touchAngle and align touch 0 degrees with chart 0 degrees, that why I subtracting start angle,
// adding 360
// and modulo 360 translates i.e -20 degrees to 340 degrees.
final float touchAngle = (pointToAngle(touchX, touchY, centerX, centerY) - rotation + 360f) % 360f;
final float sliceScale = 360f / maxSum;
float lastAngle = 0f; // No start angle here, see above
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (touchAngle >= lastAngle) {
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
}
lastAngle += angle;
++sliceIndex;
}
return isTouched();
}
/**
* Draw center circle with text if {@link PieChartData#hasCenterCircle()} is set true.
*/
private void drawCenterCircle(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float circleRadius = originCircleOval.width() / 2f;
final float centerRadius = circleRadius * data.getCenterCircleScale();
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
canvas.drawCircle(centerX, centerY, centerRadius, centerCirclePaint);
// Draw center text1 and text2 if not empty.
if (!TextUtils.isEmpty(data.getCenterText1())) {
final int text1Height = Math.abs(centerCircleText1FontMetrics.ascent);
if (!TextUtils.isEmpty(data.getCenterText2())) {
// Draw text 2 only if text 1 is not empty.
final int text2Height = Math.abs(centerCircleText2FontMetrics.ascent);
canvas.drawText(data.getCenterText1(), centerX, centerY - text1Height * 0.2f, centerCircleText1Paint);
canvas.drawText(data.getCenterText2(), centerX, centerY + text2Height, centerCircleText2Paint);
} else {
canvas.drawText(data.getCenterText1(), centerX, centerY + text1Height / 4, centerCircleText1Paint);
}
}
}
/**
* Draw all slices for this PieChart, if mode == {@link #MODE_HIGHLIGHT} currently selected slices will be redrawn
* and
* highlighted.
*
* @param canvas
*/
private void drawSlices(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (isTouched() && selectedValue.getFirstIndex() == sliceIndex) {
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_HIGHLIGHT);
} else {
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_DRAW);
}
lastAngle += angle;
++sliceIndex;
}
}
private void drawSeparationLines(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
if (data.getValues().size() < 2) {
//No need for separation lines for 0 or 1 slices.
return;
}
final int sliceSpacing = ChartUtils.dp2px(density, data.getSlicesSpacing());
if (sliceSpacing < 1) {
//No need for separation lines
return;
}
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
final float circleRadius = originCircleOval.width() / 2f;
separationLinesPaint.setStrokeWidth(sliceSpacing);
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle))),
(float) (Math.sin(Math.toRadians(lastAngle))));
normalizeVector(sliceVector);
float x1 = sliceVector.x * (circleRadius + touchAdditional) + originCircleOval.centerX();
float y1 = sliceVector.y * (circleRadius + touchAdditional) + originCircleOval.centerY();
canvas.drawLine(originCircleOval.centerX(), originCircleOval.centerY(), x1, y1, separationLinesPaint);
lastAngle += angle;
}
}
public void drawLabels(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (isTouched()) {
if (hasLabels) {
drawLabel(canvas, sliceValue, lastAngle, angle);
} else if (hasLabelsOnlyForSelected && selectedValue.getFirstIndex() == sliceIndex) {
drawLabel(canvas, sliceValue, lastAngle, angle);
}
} else {
if (hasLabels) {
drawLabel(canvas, sliceValue, lastAngle, angle);
}
}
lastAngle += angle;
++sliceIndex;
}
}
/**
* Method draws single slice from lastAngle to lastAngle+angle, if mode = {@link #MODE_HIGHLIGHT} slice will be
* darken
* and will have bigger radius.
*/
private void drawSlice(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle, int mode) {
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
normalizeVector(sliceVector);
drawCircleOval.set(originCircleOval);
if (MODE_HIGHLIGHT == mode) {
// Add additional touch feedback by setting bigger radius for that slice and darken color.
drawCircleOval.inset(-touchAdditional, -touchAdditional);
slicePaint.setColor(sliceValue.getDarkenColor());
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
} else {
slicePaint.setColor(sliceValue.getColor());
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
}
}
private void drawLabel(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle) {
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
normalizeVector(sliceVector);
final int numChars = valueFormatter.formatChartValue(labelBuffer, sliceValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
final float circleRadius = originCircleOval.width() / 2f;
final float labelRadius;
if (hasLabelsOutside) {
labelRadius = circleRadius * DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR;
} else {
if (hasCenterCircle) {
labelRadius = circleRadius - (circleRadius - (circleRadius * centerCircleScale)) / 2;
} else {
labelRadius = circleRadius * DEFAULT_LABEL_INSIDE_RADIUS_FACTOR;
}
}
final float rawX = labelRadius * sliceVector.x + centerX;
final float rawY = labelRadius * sliceVector.y + centerY;
float left;
float right;
float top;
float bottom;
if (hasLabelsOutside) {
if (rawX > centerX) {
// Right half.
left = rawX + labelMargin;
right = rawX + labelWidth + labelMargin * 3;
} else {
left = rawX - labelWidth - labelMargin * 3;
right = rawX - labelMargin;
}
if (rawY > centerY) {
// Lower half.
top = rawY + labelMargin;
bottom = rawY + labelHeight + labelMargin * 3;
} else {
top = rawY - labelHeight - labelMargin * 3;
bottom = rawY - labelMargin;
}
} else {
left = rawX - labelWidth / 2 - labelMargin;
right = rawX + labelWidth / 2 + labelMargin;
top = rawY - labelHeight / 2 - labelMargin;
bottom = rawY + labelHeight / 2 + labelMargin;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
sliceValue.getDarkenColor());
}
private void normalizeVector(PointF point) {
final float abs = point.length();
point.set(point.x / abs, point.y / abs);
}
/**
* Calculates angle of touched point.
*/
private float pointToAngle(float x, float y, float centerX, float centerY) {
double diffX = x - centerX;
double diffY = y - centerY;
// Pass -diffX to get clockwise degrees order.
double radian = Math.atan2(-diffX, diffY);
float angle = ((float) Math.toDegrees(radian) + 360) % 360;
// Add 90 because atan2 returns 0 degrees at 6 o'clock.
angle += 90f;
return angle;
}
/**
* Calculates rectangle(square) that will constraint chart circle.
*/
private void calculateCircleOval() {
Rect contentRect = computator.getContentRectMinusAllMargins();
final float circleRadius = Math.min(contentRect.width() / 2f, contentRect.height() / 2f);
final float centerX = contentRect.centerX();
final float centerY = contentRect.centerY();
final float left = centerX - circleRadius + touchAdditional;
final float top = centerY - circleRadius + touchAdditional;
final float right = centerX + circleRadius - touchAdditional;
final float bottom = centerY + circleRadius - touchAdditional;
originCircleOval.set(left, top, right, bottom);
final float inest = 0.5f * originCircleOval.width() * (1.0f - circleFillRatio);
originCircleOval.inset(inest, inest);
}
/**
* Viewport is not really important for PieChart, this kind of chart doesn't relay on viewport but uses pixels
* coordinates instead. This method also calculates sum of all SliceValues.
*/
private void calculateMaxViewport() {
tempMaximumViewport.set(0, MAX_WIDTH_HEIGHT, MAX_WIDTH_HEIGHT, 0);
maxSum = 0.0f;
for (SliceValue sliceValue : dataProvider.getPieChartData().getValues()) {
maxSum += Math.abs(sliceValue.getValue());
}
}
public RectF getCircleOval() {
return originCircleOval;
}
public void setCircleOval(RectF orginCircleOval) {
this.originCircleOval = orginCircleOval;
}
public int getChartRotation() {
return rotation;
}
public void setChartRotation(int rotation) {
rotation = (rotation % 360 + 360) % 360;
this.rotation = rotation;
}
/**
* Returns SliceValue that is under given angle, selectedValue (if not null) will be hold slice index.
*/
public SliceValue getValueForAngle(int angle, SelectedValue selectedValue) {
final PieChartData data = dataProvider.getPieChartData();
final float touchAngle = (angle - rotation + 360f) % 360f;
final float sliceScale = 360f / maxSum;
float lastAngle = 0f;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float tempAngle = Math.abs(sliceValue.getValue()) * sliceScale;
if (touchAngle >= lastAngle) {
if (null != selectedValue) {
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
}
return sliceValue;
}
lastAngle += tempAngle;
++sliceIndex;
}
return null;
}
/**
* @see #setCircleFillRatio(float)
*/
public float getCircleFillRatio() {
return this.circleFillRatio;
}
/**
* Set how much of view area should be taken by chart circle. Value should be between 0 and 1. Default is 1 so
* circle will have radius equals min(View.width, View.height).
*/
public void setCircleFillRatio(float fillRatio) {
if (fillRatio < 0) {
fillRatio = 0;
} else if (fillRatio > 1) {
fillRatio = 1;
}
this.circleFillRatio = fillRatio;
calculateCircleOval();
}
}
@@ -0,0 +1,55 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for preview chart based on ColumnChart. In addition to drawing chart data it also draw current viewport as
* preview area.
*/
public class PreviewColumnChartRenderer extends ColumnChartRenderer {
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
private static final int FULL_ALPHA = 255;
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
private Paint previewPaint = new Paint();
public PreviewColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
super(context, chart, dataProvider);
previewPaint.setAntiAlias(true);
previewPaint.setColor(Color.LTGRAY);
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
}
@Override
public void drawUnclipped(Canvas canvas) {
super.drawUnclipped(canvas);
final Viewport currentViewport = computator.getCurrentViewport();
final float left = computator.computeRawX(currentViewport.left);
final float top = computator.computeRawY(currentViewport.top);
final float right = computator.computeRawX(currentViewport.right);
final float bottom = computator.computeRawY(currentViewport.bottom);
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
previewPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(left, top, right, bottom, previewPaint);
previewPaint.setStyle(Paint.Style.STROKE);
previewPaint.setAlpha(FULL_ALPHA);
canvas.drawRect(left, top, right, bottom, previewPaint);
}
public int getPreviewColor() {
return previewPaint.getColor();
}
public void setPreviewColor(int color) {
previewPaint.setColor(color);
}
}
@@ -0,0 +1,54 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for preview chart based on LineChart. In addition to drawing chart data it also draw current viewport as
* preview area.
*/
public class PreviewLineChartRenderer extends LineChartRenderer {
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
private static final int FULL_ALPHA = 255;
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
private Paint previewPaint = new Paint();
public PreviewLineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
super(context, chart, dataProvider);
previewPaint.setAntiAlias(true);
previewPaint.setColor(Color.LTGRAY);
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
}
@Override
public void drawUnclipped(Canvas canvas) {
super.drawUnclipped(canvas);
final Viewport currentViewport = computator.getCurrentViewport();
final float left = computator.computeRawX(currentViewport.left);
final float top = computator.computeRawY(currentViewport.top);
final float right = computator.computeRawX(currentViewport.right);
final float bottom = computator.computeRawY(currentViewport.bottom);
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
previewPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(left, top, right, bottom, previewPaint);
previewPaint.setStyle(Paint.Style.STROKE);
previewPaint.setAlpha(FULL_ALPHA);
canvas.drawRect(left, top, right, bottom, previewPaint);
}
public int getPreviewColor() {
return previewPaint.getColor();
}
public void setPreviewColor(int color) {
previewPaint.setColor(color);
}
}