PhilJay / MPAndroidChart

A powerful 🚀 Android chart view / graph view library, supporting line- bar- pie- radar- bubble- and candlestick charts as well as scaling, panning and animations.
Other
37.68k stars 9.02k forks source link

MPAndroidChart custom marker sometimes not shown #4219

Open agustinsivoplas opened 6 years ago

agustinsivoplas commented 6 years ago

I have a BarChart which is populated when the metric is tapped. Also this BarChart has a custom marker, I dont understand the reason but sometimes works and sometimes not works. (Check the video). Seems like the chart is not invalidated correctly.

Code for Fragment which contains the chart (XML).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingBottom="@dimen/workout_summary_expand_collapse_container_padding_bottom"
    android:clipChildren="false"
    android:clipToPadding="false">

    <com.github.mikephil.charting.charts.BarChart
        android:id="@+id/workout_summary_premium_metrics_detail_bar_chart"
        android:layout_width="match_parent"
        android:layout_height="@dimen/workout_detail_bar_chart_height"
        android:layout_marginTop="10dp"
        android:background="@drawable/background_detail_sub_section_card"
        android:layout_marginStart="@dimen/workout_summary_premium_margin"
        android:layout_marginEnd="@dimen/workout_summary_premium_margin"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:visibility="gone"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/workout_summary_premium_metrics_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp" />
</LinearLayout>

Code for Fragment which contains the chart.

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    setUpChart();
}

private void setUpChart() {
    int colorGray = ContextCompat.getColor(getActivity(), R.color.gray_chart_dash);
    Typeface typeface = FontUtil.getTypeface(getContext(), FontUtil.CHART_FONT);

    barChart.getLegend().setEnabled(false);
    Description description = new Description();
    description.setText("");
    barChart.setDescription(description);
    barChart.getAxisRight().setEnabled(false);
    barChart.getAxisLeft().setTypeface(typeface);
    barChart.getAxisLeft().setTextColor(colorGray);
    barChart.getAxisLeft().setGridColor(colorGray);
    barChart.getAxisLeft().setAxisLineColor(Color.TRANSPARENT);
    barChart.getXAxis().setAxisLineColor(Color.TRANSPARENT);
    barChart.getXAxis().setGridColor(ContextCompat.getColor(getActivity(), R.color.gray_chart_dash));
    barChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM);
    barChart.getXAxis().setTypeface(typeface);
    barChart.getXAxis().setTextColor(colorGray);
    barChart.getXAxis().setGranularity(1f);
    barChart.setNoDataTextColor(ContextCompat.getColor(getContext(), R.color.defaultTextColor));
    barChart.setNoDataTextTypeface(typeface);
    barChart.setExtraOffsets(0f, 35f, 0, 10f);

    //setup custom marker
    barChart.setMarker(new CustomChartMarkerView(getContext(), R.layout.custom_chart_marker_view,
            R.drawable.background_meditation_chart_text, R.color.defaultBackgroundAlpha, barChart, true, true, false));

    barChart.setHighlightPerTapEnabled(true);
    barChart.setHighlightPerDragEnabled(true);
    barChart.setTouchEnabled(true);
    barChart.setPinchZoom(false);
    barChart.setScaleEnabled(false);
    barChart.setDoubleTapToZoomEnabled(false);
    barChart.setDrawMarkers(true);
}

@Override
public void onWorkoutSummaryItemSelected(@NonNull WorkoutSummaryItem item) {
    resetChart();
    BarData barData = new BarData();
    barData.setBarWidth(Constants.BAR_SMALL_WIDTH);

    if (!item.entries.isEmpty()) {
        BarDataSet dataSet = new BarDataSet(item.entries, null);
        dataSet.setColor(ContextCompat.getColor(getContext(), R.color.workout_summary_item_selected));
        dataSet.setDrawValues(false);
        dataSet.setDrawIcons(false);
        dataSet.setHighLightColor(ContextCompat.getColor(getContext(), R.color.defaultTextColor));
        dataSet.setHighLightAlpha(255);
        dataSet.setHighlightEnabled(true);

        barChart.getXAxis().setValueFormatter(new IAxisValueFormatter() {
            @Override
            public String getFormattedValue(float value, AxisBase axis) {
                SimpleDateFormat dateFormat;
                DateTime date;
                if (granularity == GetWorkoutSummaryRequest.SummaryGranularity.Year) {
                    dateFormat = DATE_FORMAT_X_AXIS_MONTH;
                    date = TimeUtils.fromMonthsBetweenEpoch((int) value);
                } else {
                    dateFormat = DATE_FORMAT_X_AXIS_DAY;
                    date = TimeUtils.fromDaysBetweenEpoch((int) value);
                }
                return dateFormat.format(date.toDate());
            }
        });

        barChart.getAxisLeft().setAxisMaximum(dataSet.getYMax() + 5);
        ((CustomChartMarkerView) barChart.getMarker()).setBottomUnit(item.unit);
        barData.addDataSet(dataSet);
    }

    barChart.setData(barData);
    barChart.animateXY(0, Constants.ANIMATION_DURATION);
    barChart.invalidate();
}

private void resetChart() {
    barChart.highlightValue(null);
    barChart.clear();
    barChart.fitScreen();
    if (barChart.getData() != null) {
        barChart.getData().clearValues();
    }
    barChart.getXAxis().setValueFormatter(null);
    barChart.notifyDataSetChanged();
    barChart.invalidate();
}

CustomMarker code.

public class CustomChartMarkerView extends MarkerView {

    public static final String MARKER_TYPE_TIMESTAMP = "timestamp";
    public static final String MARKER_TYPE_MINUTES = "minutes";
    public static final String MARKER_TYPE_SECONDS = "seconds";
    public static final String MARKER_TYPE_HOURS = "hours";
    public static final String MARKER_TYPE_WEEK_MONTH_YEAR = "week_month_year";
    public static final String MARKER_TYPE_MINUTES_FROM_EPOCH = "minutes_from_epoch";

    private CustomTextView topLabel;
    private CustomTextView bottomLabel;
    private String topUnit;
    private String bottomUnit;
    private boolean useAxisXFormatter = false;
    private boolean useAxisYFormatter = false;
    private boolean hasBringToFront = false;
    private long extraTimestampOffset = 0; //Hack for meditations activity charts
    private String extraTopUnitTextInfo = ""; //Hack workout details charts
    private String extraBottomUnitTextInfo = ""; //Hack workout details charts
    private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("hh:mm a", Locale.getDefault());
    private final SimpleDateFormat DATE_FORMAT_WEEK_MONTH_YEAR = new SimpleDateFormat("MMM d", Locale.getDefault());

    public CustomChartMarkerView(Context context, @LayoutRes int layoutResource,
                                 @DrawableRes int backgroundRes, @ColorRes int textColor,
                                 String topUnit, String bottomUnit, Chart chart, boolean useAxisXFormatter,
                                 boolean useAxisYFormatter, boolean hasBringToFront) {

        super(context, layoutResource);

        this.useAxisXFormatter = useAxisXFormatter;
        this.useAxisYFormatter = useAxisYFormatter;
        this.hasBringToFront = hasBringToFront;

        this.topUnit = topUnit;
        this.bottomUnit = bottomUnit;

        setChartView(chart);
        setBackgroundResource(backgroundRes);

        topLabel = findViewById(R.id.chart_marker_view_top_label);
        bottomLabel = findViewById(R.id.chart_marker_view_bottom_label);

        topLabel.setTextColor(ContextCompat.getColor(context, textColor));
        bottomLabel.setTextColor(ContextCompat.getColor(context, textColor));
    }

    public CustomChartMarkerView(Context context, @LayoutRes int layoutResource,
                                 @DrawableRes int backgroundRes, @ColorRes int textColor,
                                 String topUnit, String bottomUnit, Chart chart) {
        this(context, layoutResource, backgroundRes, textColor, topUnit, bottomUnit, chart, false, false, false);
    }

    public CustomChartMarkerView(Context context, @LayoutRes int layoutResource,
                                 @DrawableRes int backgroundRes, @ColorRes int textColor, Chart chart, boolean useAxisXFormatter,
                                 boolean useAxisYFormatter, boolean hasBringToFront) {
        this(context, layoutResource, backgroundRes, textColor, "", "", chart, useAxisXFormatter, useAxisYFormatter, hasBringToFront);
    }

    @Override
    public void refreshContent(Entry e, Highlight highlight) {
        switch (topUnit.toLowerCase()) {
            case MARKER_TYPE_WEEK_MONTH_YEAR:
                topLabel.setText(getFinalTextLeftUnit(DATE_FORMAT_WEEK_MONTH_YEAR.format(TimeUtils.fromDaysBetweenEpoch((int) e.getX()).toDate())));
                break;
            case MARKER_TYPE_TIMESTAMP:
                topLabel.setText(getFinalTextLeftUnit(DATE_FORMAT.format(new DateTime((int) e.getX() + extraTimestampOffset).toDate())));
                break;
            case MARKER_TYPE_MINUTES:
                topLabel.setText(getFinalTextLeftUnit(TimeUtils.formatSeconds((int) (e.getX() * 60))));
                break;
            case MARKER_TYPE_SECONDS:
                topLabel.setText(getFinalTextLeftUnit(TimeUtils.formatSeconds((int) (e.getX()))));
                break;
            case MARKER_TYPE_HOURS:
                topLabel.setText(getFinalTextLeftUnit(TimeUtils.formatSeconds((int) (e.getX() * 60 * 60))));
                break;
            case MARKER_TYPE_MINUTES_FROM_EPOCH:
                topLabel.setText(getFinalTextLeftUnit(DATE_FORMAT.format(TimeUtils.fromMinutesBetweenEpoch((int) e.getX()).toDate())));
                break;
            default:
                if (useAxisXFormatter && getChartView().getXAxis().getValueFormatter() != null) {
                    String label = getChartView().getXAxis().getValueFormatter().getFormattedValue(e.getX(), getChartView().getXAxis()).replaceAll("\n", " ");
                    topLabel.setText(getFinalTextLeftUnit(label));
                } else {
                    topLabel.setText(getFinalTextLeftUnit(Constants.DECIMAL_FORMAT_1.format(e.getX())));
                }
                break;
        }

        switch (bottomUnit.toLowerCase()) {
            case MARKER_TYPE_WEEK_MONTH_YEAR:
                bottomLabel.setText(getFinalTextRightUnit(DATE_FORMAT_WEEK_MONTH_YEAR.format(TimeUtils.fromDaysBetweenEpoch((int) e.getY()).toDate())));
                break;
            case MARKER_TYPE_TIMESTAMP:
                bottomLabel.setText(getFinalTextRightUnit(DATE_FORMAT.format(new DateTime((int) e.getY() + extraTimestampOffset).toDate())));
                break;
            case MARKER_TYPE_MINUTES:
                bottomLabel.setText(getFinalTextRightUnit(TimeUtils.formatSeconds((int) (e.getY() * 60))));
                break;
            case MARKER_TYPE_SECONDS:
                bottomLabel.setText(getFinalTextRightUnit(TimeUtils.formatSeconds((int) (e.getY()))));
                break;
            case MARKER_TYPE_HOURS:
                bottomLabel.setText(getFinalTextRightUnit(TimeUtils.formatSeconds((int) (e.getY() * 60 * 60))));
                break;
            case MARKER_TYPE_MINUTES_FROM_EPOCH:
                bottomLabel.setText(getFinalTextRightUnit(DATE_FORMAT.format(TimeUtils.fromMinutesBetweenEpoch((int) e.getY()).toDate())));
            default:
                if (useAxisYFormatter) {
                    if (getChartView() instanceof BarChart && ((BarChart) getChartView()).getAxisLeft().getValueFormatter() != null) {
                        AxisBase axis = ((BarChart) getChartView()).getAxisLeft();
                        String label = axis.getValueFormatter().getFormattedValue(e.getY(), axis);
                        bottomLabel.setText(getFinalTextRightUnit(label));
                    } else {
                        bottomLabel.setText(getFinalTextRightUnit(Constants.DECIMAL_FORMAT_1.format(e.getY())));
                    }
                } else {
                    bottomLabel.setText(getFinalTextRightUnit(Constants.DECIMAL_FORMAT_1.format(e.getY())));
                }
                break;
        }

        super.refreshContent(e, highlight);

        /*if (getChartView() instanceof BarChart) {
            setOffset(-getWidth() / 2, -getHeight() + getContext().getResources().getDimensionPixelSize(R.dimen.extra_offset_y_marker_bar));
        } else {
            setOffset(-getWidth() / 2, -highlight.getYPx());
        }*/

        setOffset(-getWidth() / 2, -highlight.getYPx());

        if (hasBringToFront) {
            getChartView().bringToFront();
        }
    }

    private String getFinalTextLeftUnit(String text) {
        StringBuilder builder = new StringBuilder(text.trim());
        if (StringUtils.isNotBlank(topUnit) && !isCustomMarkerUnit(topUnit.toLowerCase())) {
            builder.append(" ").append(topUnit.toUpperCase());
        }

        if (StringUtils.isNotBlank(getExtraTopUnitTextInfo())) {
            builder.append(" ").append(getExtraTopUnitTextInfo());
        }

        return builder.toString();
    }

    private boolean isCustomMarkerUnit(String unit) {
        return MARKER_TYPE_TIMESTAMP.equals(unit) ||
                MARKER_TYPE_MINUTES.equals(unit) ||
                MARKER_TYPE_SECONDS.equals(unit) ||
                MARKER_TYPE_HOURS.equals(unit) ||
                MARKER_TYPE_WEEK_MONTH_YEAR.equals(unit) ||
                MARKER_TYPE_MINUTES_FROM_EPOCH.equals(unit);
    }

    private String getFinalTextRightUnit(String text) {
        StringBuilder builder = new StringBuilder(text.trim());
        if (StringUtils.isNotBlank(bottomUnit) && !isCustomMarkerUnit(bottomUnit.toLowerCase())) {
            builder.append(" ").append(bottomUnit.toUpperCase());
        }

        if (StringUtils.isNotBlank(getExtraBottomUnitTextInfo())) {
            builder.append(" ").append(getExtraBottomUnitTextInfo());
        }

        return builder.toString();
    }

    @Override
    public void draw(Canvas canvas, float posX, float posY) {
        super.draw(canvas, posX, posY);
       getOffsetForDrawingAtPoint(posX, posY);
    }
  }

Thanks in advance.

All the best

Agustin

almic commented 6 years ago

That is very strange! I'm currently working through all 1000+ open issues to whittle it down to a manageable number, so I cannot help you right now. I'm adding some labels and hopefully someone else can help you in the meantime.

MuriloCalegari commented 5 years ago

Hi, I'm also having some weird behavior when using getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM);, could you comment that line and see what happens?

cychancw commented 3 years ago

I have the same problem too. but the problem only happen on real device. It works fine on emulator. In my app, the highest stack on each bar cannot show the markview on real phone, but it is fine on emulator.

Here is my problem I posted: https://stackoverflow.com/questions/68507961/the-marker-view-works-only-on-emulator-but-have-some-problem-on-real-phone-mpa