package com.github.mikephil.charting.components; import android.graphics.Paint; import com.github.mikephil.charting.utils.ColorTemplate; import com.github.mikephil.charting.utils.FSize; import com.github.mikephil.charting.utils.Utils; import com.github.mikephil.charting.utils.ViewPortHandler; import java.util.ArrayList; import java.util.List; /** * Class representing the legend of the chart. The legend will contain one entry * per color and DataSet. Multiple colors in one DataSet are grouped together. * The legend object is NOT available before setting data to the chart. * * @author Philipp Jahoda */ public class Legend extends ComponentBase { public enum LegendPosition { RIGHT_OF_CHART, RIGHT_OF_CHART_CENTER, RIGHT_OF_CHART_INSIDE, LEFT_OF_CHART, LEFT_OF_CHART_CENTER, LEFT_OF_CHART_INSIDE, BELOW_CHART_LEFT, BELOW_CHART_RIGHT, BELOW_CHART_CENTER, ABOVE_CHART_LEFT, ABOVE_CHART_RIGHT, ABOVE_CHART_CENTER, PIECHART_CENTER } public enum LegendForm { SQUARE, CIRCLE, LINE } public enum LegendDirection { LEFT_TO_RIGHT, RIGHT_TO_LEFT } /** * the legend colors array, each color is for the form drawn at the same * index */ private int[] mColors; /** the legend text array. a null label will start a group. */ private String[] mLabels; /** * colors that will be appended to the end of the colors array after * calculating the legend. */ private int[] mExtraColors; /** * labels that will be appended to the end of the labels array after * calculating the legend. a null label will start a group. */ private String[] mExtraLabels; /** * Are the legend labels/colors a custom value or auto calculated? If false, * then it's auto, if true, then custom. default false (automatic legend) */ private boolean mIsLegendCustom = false; /** the position relative to the chart the legend is drawn on */ private LegendPosition mPosition = LegendPosition.BELOW_CHART_LEFT; /** the text direction for the legend */ private LegendDirection mDirection = LegendDirection.LEFT_TO_RIGHT; /** the shape/form the legend colors are drawn in */ private LegendForm mShape = LegendForm.SQUARE; /** the size of the legend forms/shapes */ private float mFormSize = 8f; /** * the space between the legend entries on a horizontal axis, default 6f */ private float mXEntrySpace = 6f; /** * the space between the legend entries on a vertical axis, default 5f */ private float mYEntrySpace = 0f; /** * the space between the legend entries on a vertical axis, default 2f * private float mYEntrySpace = 2f; /** the space between the form and the * actual label/text */ private float mFormToTextSpace = 5f; /** the space that should be left between stacked forms */ private float mStackSpace = 3f; /** the maximum relative size out of the whole chart view in percent */ private float mMaxSizePercent = 0.95f; /** default constructor */ public Legend() { mFormSize = Utils.convertDpToPixel(8f); mXEntrySpace = Utils.convertDpToPixel(6f); mYEntrySpace = Utils.convertDpToPixel(0f); mFormToTextSpace = Utils.convertDpToPixel(5f); mTextSize = Utils.convertDpToPixel(10f); mStackSpace = Utils.convertDpToPixel(3f); this.mXOffset = Utils.convertDpToPixel(5f); this.mYOffset = Utils.convertDpToPixel(7f); } /** * Constructor. Provide colors and labels for the legend. * * @param colors * @param labels */ public Legend(int[] colors, String[] labels) { this(); if (colors == null || labels == null) { throw new IllegalArgumentException("colors array or labels array is NULL"); } if (colors.length != labels.length) { throw new IllegalArgumentException( "colors array and labels array need to be of same size"); } this.mColors = colors; this.mLabels = labels; } /** * Constructor. Provide colors and labels for the legend. * * @param colors * @param labels */ public Legend(List<Integer> colors, List<String> labels) { this(); if (colors == null || labels == null) { throw new IllegalArgumentException("colors array or labels array is NULL"); } if (colors.size() != labels.size()) { throw new IllegalArgumentException( "colors array and labels array need to be of same size"); } this.mColors = Utils.convertIntegers(colors); this.mLabels = Utils.convertStrings(labels); } /** * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. * @param colors */ public void setComputedColors(List<Integer> colors) { mColors = Utils.convertIntegers(colors); } /** * This method sets the automatically computed labels for the legend. Use setCustom(...) to set custom labels. * @param labels */ public void setComputedLabels(List<String> labels) { mLabels = Utils.convertStrings(labels); } /** * returns the maximum length in pixels across all legend labels + formsize * + formtotextspace * * @param p the paint object used for rendering the text * @return */ public float getMaximumEntryWidth(Paint p) { float max = 0f; for (int i = 0; i < mLabels.length; i++) { if (mLabels[i] != null) { float length = (float) Utils.calcTextWidth(p, mLabels[i]); if (length > max) max = length; } } return max + mFormSize + mFormToTextSpace; } /** * returns the maximum height in pixels across all legend labels * * @param p the paint object used for rendering the text * @return */ public float getMaximumEntryHeight(Paint p) { float max = 0f; for (int i = 0; i < mLabels.length; i++) { if (mLabels[i] != null) { float length = (float) Utils.calcTextHeight(p, mLabels[i]); if (length > max) max = length; } } return max; } /** * returns all the colors the legend uses * * @return */ public int[] getColors() { return mColors; } /** * returns all the labels the legend uses * * @return */ public String[] getLabels() { return mLabels; } /** * Returns the legend-label at the given index. * * @param index * @return */ public String getLabel(int index) { return mLabels[index]; } /** * colors that will be appended to the end of the colors array after * calculating the legend. */ public int[] getExtraColors() { return mExtraColors; } /** * labels that will be appended to the end of the labels array after * calculating the legend. a null label will start a group. */ public String[] getExtraLabels() { return mExtraLabels; } /** * Colors and labels that will be appended to the end of the auto calculated * colors and labels arrays after calculating the legend. (if the legend has * already been calculated, you will need to call notifyDataSetChanged() to * let the changes take effect) */ public void setExtra(List<Integer> colors, List<String> labels) { this.mExtraColors = Utils.convertIntegers(colors); this.mExtraLabels = Utils.convertStrings(labels); } /** * Colors and labels that will be appended to the end of the auto calculated * colors and labels arrays after calculating the legend. (if the legend has * already been calculated, you will need to call notifyDataSetChanged() to * let the changes take effect) */ public void setExtra(int[] colors, String[] labels) { this.mExtraColors = colors; this.mExtraLabels = labels; } /** * Sets a custom legend's labels and colors arrays. The colors count should * match the labels count. * Each color is for the form drawn at the same * index. * A null label will start a group. * A ColorTemplate.COLOR_SKIP * color will avoid drawing a form This will disable the feature that * automatically calculates the legend labels and colors from the datasets. * Call resetCustom() to re-enable automatic calculation (and then * notifyDataSetChanged() is needed to auto-calculate the legend again) */ public void setCustom(int[] colors, String[] labels) { if (colors.length != labels.length) { throw new IllegalArgumentException( "colors array and labels array need to be of same size"); } mLabels = labels; mColors = colors; mIsLegendCustom = true; } /** * Sets a custom legend's labels and colors arrays. The colors count should * match the labels count. * Each color is for the form drawn at the same * index. * A null label will start a group. * A ColorTemplate.COLOR_SKIP * color will avoid drawing a form This will disable the feature that * automatically calculates the legend labels and colors from the datasets. * Call resetCustom() to re-enable automatic calculation (and then * notifyDataSetChanged() is needed to auto-calculate the legend again) */ public void setCustom(List<Integer> colors, List<String> labels) { if (colors.size() != labels.size()) { throw new IllegalArgumentException( "colors array and labels array need to be of same size"); } mColors = Utils.convertIntegers(colors); mLabels = Utils.convertStrings(labels); mIsLegendCustom = true; } /** * Calling this will disable the custom legend labels (set by * setCustom(...)). Instead, the labels will again be calculated * automatically (after notifyDataSetChanged() is called). */ public void resetCustom() { mIsLegendCustom = false; } /** * @return true if a custom legend labels and colors has been set default * false (automatic legend) */ public boolean isLegendCustom() { return mIsLegendCustom; } /** * returns the position of the legend relative to the chart * * @return */ public LegendPosition getPosition() { return mPosition; } /** * sets the position of the legend relative to the whole chart * * @param pos */ public void setPosition(LegendPosition pos) { mPosition = pos; } /** * returns the text direction of the legend * * @return */ public LegendDirection getDirection() { return mDirection; } /** * sets the text direction of the legend * * @param pos */ public void setDirection(LegendDirection pos) { mDirection = pos; } /** * returns the current form/shape that is set for the legend * * @return */ public LegendForm getForm() { return mShape; } /** * sets the form/shape of the legend forms * * @param shape */ public void setForm(LegendForm shape) { mShape = shape; } /** * sets the size in pixels of the legend forms, this is internally converted * in dp, default 8f * * @param size */ public void setFormSize(float size) { mFormSize = Utils.convertDpToPixel(size); } /** * returns the size in dp of the legend forms * * @return */ public float getFormSize() { return mFormSize; } /** * returns the space between the legend entries on a horizontal axis in * pixels * * @return */ public float getXEntrySpace() { return mXEntrySpace; } /** * sets the space between the legend entries on a horizontal axis in pixels, * converts to dp internally * * @param space */ public void setXEntrySpace(float space) { mXEntrySpace = Utils.convertDpToPixel(space); } /** * returns the space between the legend entries on a vertical axis in pixels * * @return */ public float getYEntrySpace() { return mYEntrySpace; } /** * sets the space between the legend entries on a vertical axis in pixels, * converts to dp internally * * @param space */ public void setYEntrySpace(float space) { mYEntrySpace = Utils.convertDpToPixel(space); } /** * returns the space between the form and the actual label/text * * @return */ public float getFormToTextSpace() { return mFormToTextSpace; } /** * sets the space between the form and the actual label/text, converts to dp * internally * * @param mFormToTextSpace */ public void setFormToTextSpace(float space) { this.mFormToTextSpace = Utils.convertDpToPixel(space); } /** * returns the space that is left out between stacked forms (with no label) * * @return */ public float getStackSpace() { return mStackSpace; } /** * sets the space that is left out between stacked forms (with no label) * * @param space */ public void setStackSpace(float space) { mStackSpace = space; } /** * calculates the full width the fully drawn legend will use in pixels * * @return */ public float getFullWidth(Paint labelpaint) { float width = 0f; for (int i = 0; i < mLabels.length; i++) { // grouped forms have null labels if (mLabels[i] != null) { // make a step to the left if (mColors[i] != ColorTemplate.COLOR_SKIP) width += mFormSize + mFormToTextSpace; width += Utils.calcTextWidth(labelpaint, mLabels[i]); if (i < mLabels.length - 1) width += mXEntrySpace; } else { width += mFormSize; if (i < mLabels.length - 1) width += mStackSpace; } } return width; } /** * Calculates the full height of the drawn legend. * * @param mLegendLabelPaint * @return */ public float getFullHeight(Paint labelpaint) { float height = 0f; for (int i = 0; i < mLabels.length; i++) { // grouped forms have null labels if (mLabels[i] != null) { height += Utils.calcTextHeight(labelpaint, mLabels[i]); if (i < mLabels.length - 1) height += mYEntrySpace; } } return height; } /** the total width of the legend (needed width space) */ public float mNeededWidth = 0f; /** the total height of the legend (needed height space) */ public float mNeededHeight = 0f; public float mTextHeightMax = 0f; public float mTextWidthMax = 0f; /** flag that indicates if word wrapping is enabled */ private boolean mWordWrapEnabled = false; /** * Should the legend word wrap? / this is currently supported only for: * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word * wrapping a legend takes a toll on performance. / you may want to set * maxSizePercent when word wrapping, to set the point where the text wraps. * / default: false * * @param enabled */ public void setWordWrapEnabled(boolean enabled) { mWordWrapEnabled = enabled; } /** * If this is set, then word wrapping the legend is enabled. This means the * legend will not be cut off if too long. * * @return */ public boolean isWordWrapEnabled() { return mWordWrapEnabled; } /** * The maximum relative size out of the whole chart view. / If the legend is * to the right/left of the chart, then this affects the width of the * legend. / If the legend is to the top/bottom of the chart, then this * affects the height of the legend. / If the legend is the center of the * piechart, then this defines the size of the rectangular bounds out of the * size of the "hole". / default: 0.95f (95%) * * @return */ public float getMaxSizePercent() { return mMaxSizePercent; } /** * The maximum relative size out of the whole chart view. / If * the legend is to the right/left of the chart, then this affects the width * of the legend. / If the legend is to the top/bottom of the chart, then * this affects the height of the legend. / If the legend is the center of * the PieChart, then this defines the size of the rectangular bounds out of * the size of the "hole". / default: 0.95f (95%) * * @param maxSize */ public void setMaxSizePercent(float maxSize) { mMaxSizePercent = maxSize; } private FSize[] mCalculatedLabelSizes = new FSize[] {}; private Boolean[] mCalculatedLabelBreakPoints = new Boolean[] {}; private FSize[] mCalculatedLineSizes = new FSize[] {}; public FSize[] getCalculatedLabelSizes() { return mCalculatedLabelSizes; } public Boolean[] getCalculatedLabelBreakPoints() { return mCalculatedLabelBreakPoints; } public FSize[] getCalculatedLineSizes() { return mCalculatedLineSizes; } /** * Calculates the dimensions of the Legend. This includes the maximum width * and height of a single entry, as well as the total width and height of * the Legend. * * @param labelpaint */ public void calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler) { if (mPosition == LegendPosition.RIGHT_OF_CHART || mPosition == LegendPosition.RIGHT_OF_CHART_CENTER || mPosition == LegendPosition.LEFT_OF_CHART || mPosition == LegendPosition.LEFT_OF_CHART_CENTER || mPosition == LegendPosition.PIECHART_CENTER) { mNeededWidth = getMaximumEntryWidth(labelpaint); mNeededHeight = getFullHeight(labelpaint); mTextWidthMax = mNeededWidth; mTextHeightMax = getMaximumEntryHeight(labelpaint); } else if (mPosition == LegendPosition.BELOW_CHART_LEFT || mPosition == LegendPosition.BELOW_CHART_RIGHT || mPosition == LegendPosition.BELOW_CHART_CENTER || mPosition == LegendPosition.ABOVE_CHART_LEFT || mPosition == LegendPosition.ABOVE_CHART_RIGHT || mPosition == LegendPosition.ABOVE_CHART_CENTER) { int labelCount = mLabels.length; float labelLineHeight = Utils.getLineHeight(labelpaint); float labelLineSpacing = Utils.getLineSpacing(labelpaint) + mYEntrySpace; float contentWidth = viewPortHandler.contentWidth(); // Prepare arrays for calculated layout ArrayList<FSize> calculatedLabelSizes = new ArrayList<FSize>(labelCount); ArrayList<Boolean> calculatedLabelBreakPoints = new ArrayList<Boolean>(labelCount); ArrayList<FSize> calculatedLineSizes = new ArrayList<FSize>(); // Start calculating layout float maxLineWidth = 0.f; float currentLineWidth = 0.f; float requiredWidth = 0.f; int stackedStartIndex = -1; for (int i = 0; i < labelCount; i++) { boolean drawingForm = mColors[i] != ColorTemplate.COLOR_SKIP; calculatedLabelBreakPoints.add(false); if (stackedStartIndex == -1) { // we are not stacking, so required width is for this label // only requiredWidth = 0.f; } else { // add the spacing appropriate for stacked labels/forms requiredWidth += mStackSpace; } // grouped forms have null labels if (mLabels[i] != null) { calculatedLabelSizes.add(Utils.calcTextSize(labelpaint, mLabels[i])); requiredWidth += drawingForm ? mFormToTextSpace + mFormSize : 0.f; requiredWidth += calculatedLabelSizes.get(i).width; } else { calculatedLabelSizes.add(new FSize(0.f, 0.f)); requiredWidth += drawingForm ? mFormSize : 0.f; if (stackedStartIndex == -1) { // mark this index as we might want to break here later stackedStartIndex = i; } } if (mLabels[i] != null || i == labelCount - 1) { float requiredSpacing = currentLineWidth == 0.f ? 0.f : mXEntrySpace; if (!mWordWrapEnabled || // No word wrapping, it must fit. currentLineWidth == 0.f || // The line is empty, it // must fit. (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) // It // simply // fits { // Expand current line currentLineWidth += requiredSpacing + requiredWidth; } else { // It doesn't fit, we need to wrap a line // Add current line size to array calculatedLineSizes.add(new FSize(currentLineWidth, labelLineHeight)); maxLineWidth = Math.max(maxLineWidth, currentLineWidth); // Start a new line calculatedLabelBreakPoints.set(stackedStartIndex > -1 ? stackedStartIndex : i, true); currentLineWidth = requiredWidth; } if (i == labelCount - 1) { // Add last line size to array calculatedLineSizes.add(new FSize(currentLineWidth, labelLineHeight)); maxLineWidth = Math.max(maxLineWidth, currentLineWidth); } } stackedStartIndex = mLabels[i] != null ? -1 : stackedStartIndex; } mCalculatedLabelSizes = calculatedLabelSizes.toArray( new FSize[calculatedLabelSizes.size()]); mCalculatedLabelBreakPoints = calculatedLabelBreakPoints .toArray(new Boolean[calculatedLabelBreakPoints.size()]); mCalculatedLineSizes = calculatedLineSizes .toArray(new FSize[calculatedLineSizes.size()]); mTextWidthMax = getMaximumEntryWidth(labelpaint); mTextHeightMax = getMaximumEntryHeight(labelpaint); mNeededWidth = maxLineWidth; mNeededHeight = labelLineHeight * (float) (mCalculatedLineSizes.length) + labelLineSpacing * (float) (mCalculatedLineSizes.length == 0 ? 0 : (mCalculatedLineSizes.length - 1)); } else { /* RIGHT_OF_CHART_INSIDE, LEFT_OF_CHART_INSIDE */ mNeededWidth = getFullWidth(labelpaint); mNeededHeight = getMaximumEntryHeight(labelpaint); mTextWidthMax = getMaximumEntryWidth(labelpaint); mTextHeightMax = mNeededHeight; } } }