package com.github.mikephil.charting.renderer; import android.graphics.Matrix; import android.graphics.Path; import android.graphics.RectF; import android.view.View; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.interfaces.ChartInterface; import com.github.mikephil.charting.utils.Utils; import java.util.ArrayList; /** * Transformer class that contains all matrices and is responsible for * transforming values into pixels on the screen and backwards. * * @author Philipp Jahoda */ public class Transformer { /** matrix to map the values to the screen pixels */ protected Matrix mMatrixValueToPx = new Matrix(); /** matrix for handling the different offsets of the chart */ protected Matrix mMatrixOffset = new Matrix(); /** matrix used for touch events */ protected final Matrix mMatrixTouch = new Matrix(); /** if set to true, the y-axis is inverted and low values start at the top */ private boolean mInvertYAxis = false; /** minimum scale value on the y-axis */ private float mMinScaleY = 1f; /** minimum scale value on the x-axis */ private float mMinScaleX = 1f; /** contains the current scale factor of the x-axis */ private float mScaleX = 1f; /** contains the current scale factor of the y-axis */ private float mScaleY = 1f; /** offset that allows the chart to be dragged over its bounds on the x-axis */ private float mTransOffsetX = 0f; /** offset that allows the chart to be dragged over its bounds on the x-axis */ private float mTransOffsetY = 0f; public Transformer() { } /** * Prepares the matrix that transforms values to pixels. * * @param chart */ public void prepareMatrixValuePx(ChartInterface chart) { float scaleX = (float) ((chart.getWidth() - chart.getOffsetRight() - chart .getOffsetLeft()) / chart.getDeltaX()); float scaleY = (float) ((chart.getHeight() - chart.getOffsetTop() - chart .getOffsetBottom()) / chart.getDeltaY()); // setup all matrices mMatrixValueToPx.reset(); mMatrixValueToPx.postTranslate(0, -chart.getYChartMin()); mMatrixValueToPx.postScale(scaleX, -scaleY); } /** * Prepares the matrix that contains all offsets. * * @param chart */ public void prepareMatrixOffset(ChartInterface chart) { mMatrixOffset.reset(); // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); if (!mInvertYAxis) mMatrixOffset.postTranslate(chart.getOffsetLeft(), chart.getHeight() - chart.getOffsetBottom()); else { mMatrixOffset.setTranslate(chart.getOffsetLeft(), -chart.getOffsetTop()); mMatrixOffset.postScale(1.0f, -1.0f); } // mMatrixOffset.set(offset); // mMatrixOffset.reset(); // // mMatrixOffset.postTranslate(mOffsetLeft, getHeight() - // mOffsetBottom); } /** * Transforms an arraylist of Entry into a float array containing the x and * y values transformed with all matrices for the LINECHART or SCATTERCHART. * * @param entries * @return */ public float[] generateTransformedValuesLineScatter(ArrayList<? extends Entry> entries, float phaseY) { float[] valuePoints = new float[entries.size() * 2]; for (int j = 0; j < valuePoints.length; j += 2) { Entry e = entries.get(j / 2); if (e != null) { valuePoints[j] = e.getXIndex(); valuePoints[j + 1] = e.getVal() * phaseY; } } pointValuesToPixel(valuePoints); return valuePoints; } /** * Transforms an arraylist of Entry into a float array containing the x and * y values transformed with all matrices for the BARCHART. * * @param entries * @param dataSet the dataset index * @return */ public float[] generateTransformedValuesBarChart(ArrayList<? extends Entry> entries, int dataSet, BarData bd, float phaseY) { float[] valuePoints = new float[entries.size() * 2]; int setCount = bd.getDataSetCount(); float space = bd.getGroupSpace(); for (int j = 0; j < valuePoints.length; j += 2) { Entry e = entries.get(j / 2); // calculate the x-position, depending on datasetcount float x = e.getXIndex() + (j / 2 * (setCount - 1)) + dataSet + 0.5f + space * (j / 2) + space / 2f; float y = e.getVal(); valuePoints[j] = x; valuePoints[j + 1] = y * phaseY; } pointValuesToPixel(valuePoints); return valuePoints; } /** * transform a path with all the given matrices VERY IMPORTANT: keep order * to value-touch-offset * * @param path */ public void pathValueToPixel(Path path) { path.transform(mMatrixValueToPx); path.transform(mMatrixTouch); path.transform(mMatrixOffset); } /** * Transforms multiple paths will all matrices. * * @param paths */ public void pathValuesToPixel(ArrayList<Path> paths) { for (int i = 0; i < paths.size(); i++) { pathValueToPixel(paths.get(i)); } } /** * Transform an array of points with all matrices. VERY IMPORTANT: Keep * matrix order "value-touch-offset" when transforming. * * @param pts */ public void pointValuesToPixel(float[] pts) { mMatrixValueToPx.mapPoints(pts); mMatrixTouch.mapPoints(pts); mMatrixOffset.mapPoints(pts); } /** * Transform a rectangle with all matrices. * * @param r */ public void rectValueToPixel(RectF r) { mMatrixValueToPx.mapRect(r); mMatrixTouch.mapRect(r); mMatrixOffset.mapRect(r); } /** * Transform a rectangle with all matrices with potential animation phases. * * @param r * @param phaseY */ public void rectValueToPixel(RectF r, float phaseY) { // multiply the height of the rect with the phase if (r.top > 0) r.top *= phaseY; else r.bottom *= phaseY; mMatrixValueToPx.mapRect(r); mMatrixTouch.mapRect(r); mMatrixOffset.mapRect(r); } /** * transforms multiple rects with all matrices * * @param rects */ public void rectValuesToPixel(ArrayList<RectF> rects) { for (int i = 0; i < rects.size(); i++) rectValueToPixel(rects.get(i)); } /** * Transforms the given array of touch positions (pixels) (x, y, x, y, ...) * into values on the chart. * * @param pixels */ public void pixelsToValue(float[] pixels) { Matrix tmp = new Matrix(); // invert all matrixes to convert back to the original value mMatrixOffset.invert(tmp); tmp.mapPoints(pixels); mMatrixTouch.invert(tmp); tmp.mapPoints(pixels); mMatrixValueToPx.invert(tmp); tmp.mapPoints(pixels); } /** * ################ ################ ################ ################ */ /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */ /** * Zooms in by 1.4f, x and y are the coordinates (in pixels) of the zoom * center. * * @param x * @param y */ public Matrix zoomIn(float x, float y) { Matrix save = new Matrix(); save.set(mMatrixTouch); save.postScale(1.4f, 1.4f, x, y); return save; } /** * Zooms out by 0.7f, x and y are the coordinates (in pixels) of the zoom * center. */ public Matrix zoomOut(float x, float y) { Matrix save = new Matrix(); save.set(mMatrixTouch); save.postScale(0.7f, 0.7f, x, y); return save; } /** * Zooms in or out by the given scale factor. x and y are the coordinates * (in pixels) of the zoom center. * * @param scaleX if < 1f --> zoom out, if > 1f --> zoom in * @param scaleY if < 1f --> zoom out, if > 1f --> zoom in * @param x * @param y */ public Matrix zoom(float scaleX, float scaleY, float x, float y) { Matrix save = new Matrix(); save.set(mMatrixTouch); // Log.i(LOG_TAG, "Zooming, x: " + x + ", y: " + y); save.postScale(scaleX, scaleY, x, y); return save; } /** * Resets all zooming and dragging and makes the chart fit exactly it's * bounds. */ public Matrix fitScreen() { Matrix save = new Matrix(); save.set(mMatrixTouch); float[] vals = new float[9]; save.getValues(vals); // reset all translations and scaling vals[Matrix.MTRANS_X] = 0f; vals[Matrix.MTRANS_Y] = 0f; vals[Matrix.MSCALE_X] = 1f; vals[Matrix.MSCALE_Y] = 1f; save.setValues(vals); return save; } /** * If this is set to true, the y-axis is inverted which means that low * values are on top of the chart, high values on bottom. * * @param enabled */ public void setInvertYAxisEnabled(boolean enabled) { mInvertYAxis = enabled; } /** * If this returns true, the y-axis is inverted. * * @return */ public boolean isInvertYAxisEnabled() { return mInvertYAxis; } /** * Centers the viewport around the specified position (x-index and y-value) * in the chart. Centering the viewport outside the bounds of the chart is * not possible. Makes most sense in combination with the * setScaleMinima(...) method. * * @param pts the position to center view viewport to * @param chart * @return save */ public synchronized void centerViewPort(final float[] pts, final ChartInterface chart) { final View v = chart.getChartView(); v.post(new Runnable() { @Override public void run() { Matrix save = new Matrix(); save.set(mMatrixTouch); pointValuesToPixel(pts); final float x = pts[0] - chart.getOffsetLeft(); final float y = pts[1] - chart.getOffsetTop(); save.postTranslate(-x, -y); refresh(save, chart); } }); } /** * call this method to refresh the graph with a given matrix * * @param newMatrix * @return */ public Matrix refresh(Matrix newMatrix, ChartInterface chart) { mMatrixTouch.set(newMatrix); // make sure scale and translation are within their bounds limitTransAndScale(mMatrixTouch, chart.getContentRect()); chart.getChartView().invalidate(); newMatrix.set(mMatrixTouch); return newMatrix; } /** * limits the maximum scale and X translation of the given matrix * * @param matrix */ private void limitTransAndScale(Matrix matrix, RectF content) { float[] vals = new float[9]; matrix.getValues(vals); float curTransX = vals[Matrix.MTRANS_X]; float curScaleX = vals[Matrix.MSCALE_X]; float curTransY = vals[Matrix.MTRANS_Y]; float curScaleY = vals[Matrix.MSCALE_Y]; // min scale-x is 1f mScaleX = Math.max(mMinScaleX, curScaleX); // min scale-y is 1f mScaleY = Math.max(mMinScaleY, curScaleY); float width = 0f; float height = 0f; if (content != null) { width = content.width(); height = content.height(); } float maxTransX = -width * (mScaleX - 1f); float newTransX = Math.min(Math.max(curTransX, maxTransX - mTransOffsetX), mTransOffsetX); // if(curScaleX < mMinScaleX) { // newTransX = (-width * (mScaleX - 1f)) / 2f; // } float maxTransY = height * (mScaleY - 1f); float newTransY = Math.max(Math.min(curTransY, maxTransY + mTransOffsetY), -mTransOffsetY); // if(curScaleY < mMinScaleY) { // newTransY = (height * (mScaleY - 1f)) / 2f; // } vals[Matrix.MTRANS_X] = newTransX; vals[Matrix.MSCALE_X] = mScaleX; vals[Matrix.MTRANS_Y] = newTransY; vals[Matrix.MSCALE_Y] = mScaleY; matrix.setValues(vals); } /** * Sets the minimum scale values for both axes. This limits the extent to * which the user can zoom-out. * * @param scaleXmin * @param scaleYmin */ public void setScaleMinima(float scaleXmin, float scaleYmin, ChartInterface chart) { if (scaleXmin < 1f) scaleXmin = 1f; if (scaleYmin < 1f) scaleYmin = 1f; mMinScaleX = scaleXmin; mMinScaleY = scaleYmin; Matrix save = zoom(mMinScaleX, mMinScaleY, 0f, 0f); refresh(save, chart); } // /** // * transforms the given rect objects with the touch matrix only // * // * @param paths // */ // public void transformRectsTouch(ArrayList<RectF> rects) { // for (int i = 0; i < rects.size(); i++) { // mMatrixTouch.mapRect(rects.get(i)); // } // } // // /** // * transforms the given path objects with the touch matrix only // * // * @param paths // */ // public void transformPathsTouch(ArrayList<Path> paths) { // for (int i = 0; i < paths.size(); i++) { // paths.get(i).transform(mMatrixTouch); // } // } public Matrix getTouchMatrix() { return mMatrixTouch; } public Matrix getValueMatrix() { return mMatrixValueToPx; } public Matrix getOffsetMatrix() { return mMatrixOffset; } /** * returns the current x-scale factor */ public float getScaleX() { return mScaleX; } /** * returns the current y-scale factor */ public float getScaleY() { return mScaleY; } /** * if the chart is fully zoomed out, return true * * @return */ public boolean isFullyZoomedOut() { if (isFullyZoomedOutX() && isFullyZoomedOutY()) return true; else return false; } /** * Returns true if the chart is fully zoomed out on it's y-axis (vertical). * * @return */ public boolean isFullyZoomedOutY() { if (mScaleY > mMinScaleY || mMinScaleY > 1f) return false; else return true; } /** * Returns true if the chart is fully zoomed out on it's x-axis * (horizontal). * * @return */ public boolean isFullyZoomedOutX() { if (mScaleX > mMinScaleX || mMinScaleX > 1f) return false; else return true; } /** * Set an offset in dp that allows the user to drag the chart over it's * bounds on the x-axis. * * @param offset */ public void setDragOffsetX(float offset) { mTransOffsetX = Utils.convertDpToPixel(offset); } /** * Set an offset in dp that allows the user to drag the chart over it's * bounds on the y-axis. * * @param offset */ public void setDragOffsetY(float offset) { mTransOffsetY = Utils.convertDpToPixel(offset); } /** * Returns true if both drag offsets (x and y) are zero or smaller. * * @return */ public boolean hasNoDragOffset() { return mTransOffsetX <= 0 && mTransOffsetY <= 0 ? true : false; } }