的
This commit is contained in:
+180
@@ -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;
|
||||
}
|
||||
}
|
||||
+639
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+351
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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();
|
||||
|
||||
}
|
||||
+422
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -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();
|
||||
}
|
||||
}
|
||||
+40
@@ -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);
|
||||
}
|
||||
}
|
||||
+514
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+513
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user