All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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());
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy