|
@@ -0,0 +1,332 @@
|
|
|
|
|
+package hr.tacitus.graphs;
|
|
|
|
|
+
|
|
|
|
|
+import android.content.Context;
|
|
|
|
|
+import android.content.res.TypedArray;
|
|
|
|
|
+import android.graphics.Canvas;
|
|
|
|
|
+import android.graphics.Color;
|
|
|
|
|
+import android.graphics.Paint;
|
|
|
|
|
+import android.graphics.RectF;
|
|
|
|
|
+import android.support.annotation.Nullable;
|
|
|
|
|
+import android.text.Layout;
|
|
|
|
|
+import android.text.StaticLayout;
|
|
|
|
|
+import android.text.TextPaint;
|
|
|
|
|
+import android.util.AttributeSet;
|
|
|
|
|
+import android.view.View;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Created by Bero on 7.7.2017..
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+public class PieChartView extends View {
|
|
|
|
|
+
|
|
|
|
|
+ class PieChartItem {
|
|
|
|
|
+ float value;
|
|
|
|
|
+ // color for this pie chart item
|
|
|
|
|
+ Paint paint;
|
|
|
|
|
+ // text
|
|
|
|
|
+ String description;
|
|
|
|
|
+
|
|
|
|
|
+ // cached value
|
|
|
|
|
+ int descriptionWidth;
|
|
|
|
|
+
|
|
|
|
|
+ float getAngle() {
|
|
|
|
|
+ return value * 3.6f;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static float MAX_LEGEND_WIDTH_PERCENTAGE = 35f;
|
|
|
|
|
+ private static float MAX_LEGEND_WIDTH_PX = 300;
|
|
|
|
|
+ private static float LEGEND_PADDING_HORIZONTAL = 20f;
|
|
|
|
|
+ private static float LEGEND_PADDING_TOP = 20f;
|
|
|
|
|
+
|
|
|
|
|
+ private float titleTextSizeSp = 16.f;
|
|
|
|
|
+
|
|
|
|
|
+ private boolean showLegend = true;
|
|
|
|
|
+ private float legendTextSizeSp = 10.f;
|
|
|
|
|
+ private float legendSquareSize = 20f;
|
|
|
|
|
+ private float legendWidth;
|
|
|
|
|
+
|
|
|
|
|
+ private float chartPadding = 64.f;
|
|
|
|
|
+ private float chartTextSizeSp = 10.f;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private String title = "This is a title";
|
|
|
|
|
+
|
|
|
|
|
+ private List<Integer> colors;
|
|
|
|
|
+
|
|
|
|
|
+ private TextPaint titlePaint;
|
|
|
|
|
+ private TextPaint valuePaint;
|
|
|
|
|
+ private TextPaint legendDescriptionPaint;
|
|
|
|
|
+ private StaticLayout titleTextLayout;
|
|
|
|
|
+
|
|
|
|
|
+ private RectF titleRect;
|
|
|
|
|
+ private RectF legendRect;
|
|
|
|
|
+ private RectF pieChartRect;
|
|
|
|
|
+
|
|
|
|
|
+ private List<PieChartItem> items = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ private boolean readyToDraw = false;
|
|
|
|
|
+
|
|
|
|
|
+ public PieChartView(Context context) {
|
|
|
|
|
+ super(context);
|
|
|
|
|
+ init(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public PieChartView(Context context, AttributeSet attrs) {
|
|
|
|
|
+ super(context, attrs);
|
|
|
|
|
+ init(attrs);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public PieChartView(Context context, AttributeSet attrs, int defStyle) {
|
|
|
|
|
+ super(context, attrs, defStyle);
|
|
|
|
|
+ init(attrs);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void readAttributes(AttributeSet attributeSet) {
|
|
|
|
|
+ TypedArray a = getContext().obtainStyledAttributes(attributeSet, R.styleable.PieChartView);
|
|
|
|
|
+ if (a != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ title = a.getString(R.styleable.PieChartView_title);
|
|
|
|
|
+ showLegend = a.getBoolean(R.styleable.PieChartView_showLegend, true);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ a.recycle();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Nullable
|
|
|
|
|
+ private RectF getLegendRect() {
|
|
|
|
|
+ if (showLegend) {
|
|
|
|
|
+ float left = titleRect.right - legendWidth;
|
|
|
|
|
+ float right = titleRect.right;
|
|
|
|
|
+ float top = titleRect.bottom;
|
|
|
|
|
+ float bottom = getBottom();
|
|
|
|
|
+
|
|
|
|
|
+ return new RectF(left, top, right, bottom);
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private RectF getPieChartRect() {
|
|
|
|
|
+ float left = titleRect.left + chartPadding;
|
|
|
|
|
+ float top = titleRect.top + chartPadding;
|
|
|
|
|
+ float right = titleRect.right - chartPadding;
|
|
|
|
|
+ if (showLegend)
|
|
|
|
|
+ right -= legendRect.width();
|
|
|
|
|
+ float bottom = getBottom() - getPaddingLeft() - chartPadding;
|
|
|
|
|
+ RectF rectF = new RectF(left, top, right, bottom);
|
|
|
|
|
+ // width must be equal to height
|
|
|
|
|
+ if (rectF.width() != rectF.height()) {
|
|
|
|
|
+ float width = Math.min(rectF.width(), rectF.height());
|
|
|
|
|
+ left = rectF.centerX() - width / 2;
|
|
|
|
|
+ right = left + width;
|
|
|
|
|
+ top = rectF.centerY() - width / 2;
|
|
|
|
|
+ bottom = top + width;
|
|
|
|
|
+
|
|
|
|
|
+ rectF = new RectF(left, top, right, bottom);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return rectF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private RectF getTitleRect() {
|
|
|
|
|
+ return new RectF(getPaddingLeft(), getPaddingTop(),
|
|
|
|
|
+ getWidth() - getPaddingLeft() - getPaddingRight(), 32.f);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void initTitleText() {
|
|
|
|
|
+ // int width = (int) titlePaint.measureText(title);
|
|
|
|
|
+ titleTextLayout = new StaticLayout(title, titlePaint, (int) titleRect.width(),
|
|
|
|
|
+ Layout.Alignment.ALIGN_CENTER, 1.0f, 0, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void init(AttributeSet attributeSet) {
|
|
|
|
|
+ if (attributeSet != null)
|
|
|
|
|
+ readAttributes(attributeSet);
|
|
|
|
|
+
|
|
|
|
|
+ titlePaint = new TextPaint();
|
|
|
|
|
+ titlePaint.setAntiAlias(true);
|
|
|
|
|
+ titlePaint.setTextSize(Utils.spsToPixels(titleTextSizeSp, getResources()));
|
|
|
|
|
+ titlePaint.setColor(Color.BLACK);
|
|
|
|
|
+
|
|
|
|
|
+ valuePaint = new TextPaint();
|
|
|
|
|
+ valuePaint.setAntiAlias(true);
|
|
|
|
|
+ valuePaint.setTextSize(Utils.spsToPixels(chartTextSizeSp, getResources()));
|
|
|
|
|
+ valuePaint.setColor(Color.BLACK);
|
|
|
|
|
+
|
|
|
|
|
+ legendDescriptionPaint = new TextPaint();
|
|
|
|
|
+ legendDescriptionPaint.setAntiAlias(true);
|
|
|
|
|
+ legendDescriptionPaint.setTextSize(Utils.spsToPixels(legendTextSizeSp, getResources()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void initRectangles() {
|
|
|
|
|
+ titleRect = getTitleRect();
|
|
|
|
|
+ if (showLegend) {
|
|
|
|
|
+ updateLegendWidth();
|
|
|
|
|
+ }
|
|
|
|
|
+ legendRect = getLegendRect();
|
|
|
|
|
+ pieChartRect = getPieChartRect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void drawTitle(Canvas canvas) {
|
|
|
|
|
+ if (titleTextLayout != null)
|
|
|
|
|
+ titleTextLayout.draw(canvas);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void drawChart(Canvas canvas) {
|
|
|
|
|
+
|
|
|
|
|
+ float startAngle = -90.f;
|
|
|
|
|
+ for (PieChartItem item : items) {
|
|
|
|
|
+ canvas.drawArc(pieChartRect, startAngle, item.getAngle(), true, item.paint);
|
|
|
|
|
+ startAngle += item.getAngle();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ startAngle = 90.f;
|
|
|
|
|
+ for (PieChartItem item : items) {
|
|
|
|
|
+ float angleMiddle = startAngle - item.getAngle() / 2;
|
|
|
|
|
+ float r = pieChartRect.width() / 2;
|
|
|
|
|
+ float x = (float) Math.cos(Math.toRadians(angleMiddle)) * r;
|
|
|
|
|
+ float y = (float) Math.sin(Math.toRadians(angleMiddle)) * r;
|
|
|
|
|
+
|
|
|
|
|
+ String text = Double.toString(item.value);
|
|
|
|
|
+ // Depending on quadrant, adjust text.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ float textWidth = valuePaint.measureText(text);
|
|
|
|
|
+ float left = x + pieChartRect.centerX();
|
|
|
|
|
+ float top = pieChartRect.centerY() - y;
|
|
|
|
|
+
|
|
|
|
|
+ // RectF textRect = new RectF(left, top, left + textWidth, top + valuePaint.getTextSize());
|
|
|
|
|
+ canvas.drawText(Integer.toHexString(item.paint.getColor()), left, top, valuePaint);
|
|
|
|
|
+
|
|
|
|
|
+ startAngle -= item.getAngle();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void drawLegend(Canvas canvas) {
|
|
|
|
|
+ Paint p = new Paint();
|
|
|
|
|
+ p.setColor(Color.RED);
|
|
|
|
|
+ canvas.drawRect(legendRect, p);
|
|
|
|
|
+
|
|
|
|
|
+ float left = legendRect.left + LEGEND_PADDING_HORIZONTAL;
|
|
|
|
|
+ float top = legendRect.top + LEGEND_PADDING_TOP;
|
|
|
|
|
+
|
|
|
|
|
+ // Draw color explanations
|
|
|
|
|
+ for (int i = 0; i < items.size(); ++i) {
|
|
|
|
|
+ PieChartItem item = items.get(i);
|
|
|
|
|
+
|
|
|
|
|
+ canvas.drawRect(new RectF(left, top, left + legendSquareSize, top + legendSquareSize), item.paint);
|
|
|
|
|
+ // canvas.drawText(Double.toString(item.value), left + 30f, top + 20f, valuePaint);
|
|
|
|
|
+ StaticLayout staticLayout = new StaticLayout(item.description,
|
|
|
|
|
+ valuePaint, Utils.textWidth(item.description, valuePaint, true),
|
|
|
|
|
+ Layout.Alignment.ALIGN_NORMAL, 0, 0, false);
|
|
|
|
|
+
|
|
|
|
|
+ canvas.save();
|
|
|
|
|
+ canvas.translate(left + (legendSquareSize * 1.5f), top);
|
|
|
|
|
+ staticLayout.draw(canvas);
|
|
|
|
|
+
|
|
|
|
|
+ canvas.restore();
|
|
|
|
|
+
|
|
|
|
|
+ top += legendSquareSize * 1.5f;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onDraw(Canvas canvas) {
|
|
|
|
|
+ if (!readyToDraw)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ drawTitle(canvas);
|
|
|
|
|
+ drawChart(canvas);
|
|
|
|
|
+ if (showLegend)
|
|
|
|
|
+ drawLegend(canvas);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
|
|
|
|
|
+ super.onSizeChanged(xNew, yNew, xOld, yOld);
|
|
|
|
|
+
|
|
|
|
|
+ initRectangles();
|
|
|
|
|
+ initTitleText();
|
|
|
|
|
+
|
|
|
|
|
+ readyToDraw = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void initColors(int count) {
|
|
|
|
|
+ colors = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 3 sets of 2 main colors - rb, rg, bg
|
|
|
|
|
+ int maxValue = 220;
|
|
|
|
|
+ int perSetGain = (int) (maxValue / (Math.ceil(count / 6.) + 1));
|
|
|
|
|
+
|
|
|
|
|
+ float r = maxValue, g = 0, b = 0;
|
|
|
|
|
+ for (int i = 0; i < count; ++i) {
|
|
|
|
|
+ int color = Color.rgb((int) r, (int) g, (int) b);
|
|
|
|
|
+ colors.add(color);
|
|
|
|
|
+
|
|
|
|
|
+ r = Color.green(color);
|
|
|
|
|
+ g = Color.blue(color);
|
|
|
|
|
+ b = Color.red(color);
|
|
|
|
|
+
|
|
|
|
|
+ if (i != 0 && (i + 1) % 3 == 0) {
|
|
|
|
|
+ if (i == 2) {
|
|
|
|
|
+ g = maxValue;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ r -= perSetGain;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void createItems(float[] values) {
|
|
|
|
|
+ for (int i = 0; i < values.length; ++i) {
|
|
|
|
|
+ PieChartItem pieChartItem = new PieChartItem();
|
|
|
|
|
+ pieChartItem.value = values[i];
|
|
|
|
|
+ pieChartItem.paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
|
+ pieChartItem.paint.setColor(colors.get(i));
|
|
|
|
|
+ pieChartItem.description = Double.toString(values[i]);
|
|
|
|
|
+
|
|
|
|
|
+ pieChartItem.descriptionWidth = Utils.textWidth(pieChartItem.description, legendDescriptionPaint, true);
|
|
|
|
|
+
|
|
|
|
|
+ items.add(pieChartItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private float getMaxLegendWidth() {
|
|
|
|
|
+ float width = getWidth();
|
|
|
|
|
+ float maxPercWidth = width * MAX_LEGEND_WIDTH_PERCENTAGE;
|
|
|
|
|
+ return Math.min(maxPercWidth, MAX_LEGEND_WIDTH_PX);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Calculates required legend width.
|
|
|
|
|
+ */
|
|
|
|
|
+ private void updateLegendWidth() {
|
|
|
|
|
+ float maxWidth = getMaxLegendWidth();
|
|
|
|
|
+
|
|
|
|
|
+ legendWidth = 0;
|
|
|
|
|
+ for (PieChartItem item : items) {
|
|
|
|
|
+ float width = item.descriptionWidth + 2 * LEGEND_PADDING_HORIZONTAL + legendSquareSize * 1.5f;
|
|
|
|
|
+ if (width > legendWidth)
|
|
|
|
|
+ legendWidth = width;
|
|
|
|
|
+
|
|
|
|
|
+ if (legendWidth >= maxWidth) {
|
|
|
|
|
+ legendWidth = maxWidth;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void setValues(float[] values) {
|
|
|
|
|
+ items.clear();
|
|
|
|
|
+
|
|
|
|
|
+ initColors(values.length);
|
|
|
|
|
+ createItems(values);
|
|
|
|
|
+ initRectangles();
|
|
|
|
|
+ invalidate();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|