package com.github.mikephil.charting.components; import android.graphics.DashPathEffect; 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 LegendForm { /** * Avoid drawing a form */ NONE, /** * Do not draw the a form, but leave space for it */ EMPTY, /** * Use default (default dataset's form to the legend's form) */ DEFAULT, /** * Draw a square */ SQUARE, /** * Draw a circle */ CIRCLE, /** * Draw a horizontal line */ LINE } public enum LegendHorizontalAlignment { LEFT, CENTER, RIGHT } public enum LegendVerticalAlignment { TOP, CENTER, BOTTOM } public enum LegendOrientation { HORIZONTAL, VERTICAL } public enum LegendDirection { LEFT_TO_RIGHT, RIGHT_TO_LEFT } /** * The legend entries array */ private LegendEntry[] mEntries = new LegendEntry[]{}; /** * Entries that will be appended to the end of the auto calculated entries after calculating the legend. * (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) */ private LegendEntry[] mExtraEntries; /** * 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; private LegendHorizontalAlignment mHorizontalAlignment = LegendHorizontalAlignment.LEFT; private LegendVerticalAlignment mVerticalAlignment = LegendVerticalAlignment.BOTTOM; private LegendOrientation mOrientation = LegendOrientation.HORIZONTAL; private boolean mDrawInside = false; /** * 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 size of the legend forms/shapes */ private float mFormLineWidth = 3f; /** * Line dash path effect used for shapes that consist of lines. */ private DashPathEffect mFormLineDashEffect = null; /** * 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() { this.mTextSize = Utils.convertDpToPixel(10f); this.mXOffset = Utils.convertDpToPixel(5f); this.mYOffset = Utils.convertDpToPixel(3f); // 2 } /** * Constructor. Provide entries for the legend. * * @param entries */ public Legend(LegendEntry[] entries) { this(); if (entries == null) { throw new IllegalArgumentException("entries array is NULL"); } this.mEntries = entries; } /** * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. * * @param entries */ public void setEntries(List entries) { mEntries = entries.toArray(new LegendEntry[entries.size()]); } public LegendEntry[] getEntries() { return mEntries; } /** * 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; float maxFormSize = 0f; float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); for (LegendEntry entry : mEntries) { final float formSize = Utils.convertDpToPixel( Float.isNaN(entry.formSize) ? mFormSize : entry.formSize); if (formSize > maxFormSize) maxFormSize = formSize; String label = entry.label; if (label == null) continue; float length = (float) Utils.calcTextWidth(p, label); if (length > max) max = length; } return max + maxFormSize + formToTextSpace; } /** * 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 (LegendEntry entry : mEntries) { String label = entry.label; if (label == null) continue; float length = (float) Utils.calcTextHeight(p, label); if (length > max) max = length; } return max; } public LegendEntry[] getExtraEntries() { return mExtraEntries; } public void setExtra(List entries) { mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); } public void setExtra(LegendEntry[] entries) { if (entries == null) entries = new LegendEntry[]{}; mExtraEntries = entries; } /** * Entries that will be appended to the end of the auto calculated * entries 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) { List entries = new ArrayList<>(); for (int i = 0; i < Math.min(colors.length, labels.length); i++) { final LegendEntry entry = new LegendEntry(); entry.formColor = colors[i]; entry.label = labels[i]; if (entry.formColor == ColorTemplate.COLOR_SKIP || entry.formColor == 0) entry.form = LegendForm.NONE; else if (entry.formColor == ColorTemplate.COLOR_NONE) entry.form = LegendForm.EMPTY; entries.add(entry); } mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); } /** * Sets a custom legend's entries array. * * A null label will start a group. * This will disable the feature that automatically calculates the legend * entries from the datasets. * Call resetCustom() to re-enable automatic calculation (and then * notifyDataSetChanged() is needed to auto-calculate the legend again) */ public void setCustom(LegendEntry[] entries) { mEntries = entries; mIsLegendCustom = true; } /** * Sets a custom legend's entries array. * * A null label will start a group. * This will disable the feature that automatically calculates the legend * entries 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 entries) { mEntries = entries.toArray(new LegendEntry[entries.size()]); mIsLegendCustom = true; } /** * Calling this will disable the custom legend entries (set by * setCustom(...)). Instead, the entries will again be calculated * automatically (after notifyDataSetChanged() is called). */ public void resetCustom() { mIsLegendCustom = false; } /** * @return true if a custom legend entries has been set default * false (automatic legend) */ public boolean isLegendCustom() { return mIsLegendCustom; } /** * returns the horizontal alignment of the legend * * @return */ public LegendHorizontalAlignment getHorizontalAlignment() { return mHorizontalAlignment; } /** * sets the horizontal alignment of the legend * * @param value */ public void setHorizontalAlignment(LegendHorizontalAlignment value) { mHorizontalAlignment = value; } /** * returns the vertical alignment of the legend * * @return */ public LegendVerticalAlignment getVerticalAlignment() { return mVerticalAlignment; } /** * sets the vertical alignment of the legend * * @param value */ public void setVerticalAlignment(LegendVerticalAlignment value) { mVerticalAlignment = value; } /** * returns the orientation of the legend * * @return */ public LegendOrientation getOrientation() { return mOrientation; } /** * sets the orientation of the legend * * @param value */ public void setOrientation(LegendOrientation value) { mOrientation = value; } /** * returns whether the legend will draw inside the chart or outside * * @return */ public boolean isDrawInsideEnabled() { return mDrawInside; } /** * sets whether the legend will draw inside the chart or outside * * @param value */ public void setDrawInside(boolean value) { mDrawInside = value; } /** * 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 dp of the legend forms, default 8f * * @param size */ public void setFormSize(float size) { mFormSize = size; } /** * returns the size in dp of the legend forms * * @return */ public float getFormSize() { return mFormSize; } /** * sets the line width in dp for forms that consist of lines, default 3f * * @param size */ public void setFormLineWidth(float size) { mFormLineWidth = size; } /** * returns the line width in dp for drawing forms that consist of lines * * @return */ public float getFormLineWidth() { return mFormLineWidth; } /** * Sets the line dash path effect used for shapes that consist of lines. * * @param dashPathEffect */ public void setFormLineDashEffect(DashPathEffect dashPathEffect) { mFormLineDashEffect = dashPathEffect; } /** * @return The line dash path effect used for shapes that consist of lines. */ public DashPathEffect getFormLineDashEffect() { return mFormLineDashEffect; } /** * 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 = 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 = 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 space */ public void setFormToTextSpace(float space) { this.mFormToTextSpace = 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; } /** * 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. / default: 0.95f (95%) * * @param maxSize */ public void setMaxSizePercent(float maxSize) { mMaxSizePercent = maxSize; } private List mCalculatedLabelSizes = new ArrayList<>(16); private List mCalculatedLabelBreakPoints = new ArrayList<>(16); private List mCalculatedLineSizes = new ArrayList<>(16); public List getCalculatedLabelSizes() { return mCalculatedLabelSizes; } public List getCalculatedLabelBreakPoints() { return mCalculatedLabelBreakPoints; } public List 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) { float defaultFormSize = Utils.convertDpToPixel(mFormSize); float stackSpace = Utils.convertDpToPixel(mStackSpace); float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); float xEntrySpace = Utils.convertDpToPixel(mXEntrySpace); float yEntrySpace = Utils.convertDpToPixel(mYEntrySpace); boolean wordWrapEnabled = mWordWrapEnabled; LegendEntry[] entries = mEntries; int entryCount = entries.length; mTextWidthMax = getMaximumEntryWidth(labelpaint); mTextHeightMax = getMaximumEntryHeight(labelpaint); switch (mOrientation) { case VERTICAL: { float maxWidth = 0f, maxHeight = 0f, width = 0f; float labelLineHeight = Utils.getLineHeight(labelpaint); boolean wasStacked = false; for (int i = 0; i < entryCount; i++) { LegendEntry e = entries[i]; boolean drawingForm = e.form != LegendForm.NONE; float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); String label = e.label; if (!wasStacked) width = 0.f; if (drawingForm) { if (wasStacked) width += stackSpace; width += formSize; } // grouped forms have null labels if (label != null) { // make a step to the left if (drawingForm && !wasStacked) width += formToTextSpace; else if (wasStacked) { maxWidth = Math.max(maxWidth, width); maxHeight += labelLineHeight + yEntrySpace; width = 0.f; wasStacked = false; } width += Utils.calcTextWidth(labelpaint, label); maxHeight += labelLineHeight + yEntrySpace; } else { wasStacked = true; width += formSize; if (i < entryCount - 1) width += stackSpace; } maxWidth = Math.max(maxWidth, width); } mNeededWidth = maxWidth; mNeededHeight = maxHeight; break; } case HORIZONTAL: { float labelLineHeight = Utils.getLineHeight(labelpaint); float labelLineSpacing = Utils.getLineSpacing(labelpaint) + yEntrySpace; float contentWidth = viewPortHandler.contentWidth() * mMaxSizePercent; // Start calculating layout float maxLineWidth = 0.f; float currentLineWidth = 0.f; float requiredWidth = 0.f; int stackedStartIndex = -1; mCalculatedLabelBreakPoints.clear(); mCalculatedLabelSizes.clear(); mCalculatedLineSizes.clear(); for (int i = 0; i < entryCount; i++) { LegendEntry e = entries[i]; boolean drawingForm = e.form != LegendForm.NONE; float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); String label = e.label; mCalculatedLabelBreakPoints.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 += stackSpace; } // grouped forms have null labels if (label != null) { mCalculatedLabelSizes.add(Utils.calcTextSize(labelpaint, label)); requiredWidth += drawingForm ? formToTextSpace + formSize : 0.f; requiredWidth += mCalculatedLabelSizes.get(i).width; } else { mCalculatedLabelSizes.add(FSize.getInstance(0.f, 0.f)); requiredWidth += drawingForm ? formSize : 0.f; if (stackedStartIndex == -1) { // mark this index as we might want to break here later stackedStartIndex = i; } } if (label != null || i == entryCount - 1) { float requiredSpacing = currentLineWidth == 0.f ? 0.f : xEntrySpace; if (!wordWrapEnabled // No word wrapping, it must fit. // The line is empty, it must fit || currentLineWidth == 0.f // It simply fits || (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) { // Expand current line currentLineWidth += requiredSpacing + requiredWidth; } else { // It doesn't fit, we need to wrap a line // Add current line size to array mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); maxLineWidth = Math.max(maxLineWidth, currentLineWidth); // Start a new line mCalculatedLabelBreakPoints.set( stackedStartIndex > -1 ? stackedStartIndex : i, true); currentLineWidth = requiredWidth; } if (i == entryCount - 1) { // Add last line size to array mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); maxLineWidth = Math.max(maxLineWidth, currentLineWidth); } } stackedStartIndex = label != null ? -1 : stackedStartIndex; } mNeededWidth = maxLineWidth; mNeededHeight = labelLineHeight * (float) (mCalculatedLineSizes.size()) + labelLineSpacing * (float) (mCalculatedLineSizes.size() == 0 ? 0 : (mCalculatedLineSizes.size() - 1)); break; } } mNeededHeight += mYOffset; mNeededWidth += mXOffset; } }