Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
lecho.lib.hellocharts.renderer.BubbleChartRenderer Maven / Gradle / Ivy
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());
}
}