/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.chromium.latency.walt; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.widget.RelativeLayout; import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.components.AxisBase; import com.github.mikephil.charting.components.Description; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.formatter.IAxisValueFormatter; import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; import com.github.mikephil.charting.utils.ColorTemplate; import java.text.DecimalFormat; import java.util.ArrayList; public class HistogramChart extends RelativeLayout implements View.OnClickListener { static final float GROUP_SPACE = 0.1f; private HistogramData histogramData; private BarChart barChart; public HistogramChart(Context context, AttributeSet attrs) { super(context, attrs); inflate(getContext(), R.layout.histogram, this); barChart = (BarChart) findViewById(R.id.bar_chart); findViewById(R.id.button_close_bar_chart).setOnClickListener(this); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HistogramChart); final String descString; final int numDataSets; final float binWidth; try { descString = a.getString(R.styleable.HistogramChart_description); numDataSets = a.getInteger(R.styleable.HistogramChart_numDataSets, 1); binWidth = a.getFloat(R.styleable.HistogramChart_binWidth, 5f); } finally { a.recycle(); } ArrayList<IBarDataSet> dataSets = new ArrayList<>(numDataSets); for (int i = 0; i < numDataSets; i++) { final BarDataSet dataSet = new BarDataSet(new ArrayList<BarEntry>(), ""); dataSet.setColor(ColorTemplate.MATERIAL_COLORS[i]); dataSets.add(dataSet); } BarData barData = new BarData(dataSets); barData.setBarWidth((1f - GROUP_SPACE)/numDataSets); barChart.setData(barData); histogramData = new HistogramData(numDataSets, binWidth); groupBars(barData); final Description desc = new Description(); desc.setText(descString); desc.setTextSize(12f); barChart.setDescription(desc); XAxis xAxis = barChart.getXAxis(); xAxis.setGranularityEnabled(true); xAxis.setGranularity(1); xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); xAxis.setValueFormatter(new IAxisValueFormatter() { DecimalFormat df = new DecimalFormat("#.##"); @Override public String getFormattedValue(float value, AxisBase axis) { return df.format(histogramData.getDisplayValue(value)); } }); barChart.setFitBars(true); barChart.invalidate(); } BarChart getBarChart() { return barChart; } /** * Re-implementation of BarData.groupBars(), but allows grouping with only 1 BarDataSet * This adjusts the x-coordinates of entries, which centers the bars between axis labels */ static void groupBars(final BarData barData) { IBarDataSet max = barData.getMaxEntryCountSet(); int maxEntryCount = max.getEntryCount(); float groupSpaceWidthHalf = GROUP_SPACE / 2f; float barWidthHalf = barData.getBarWidth() / 2f; float interval = barData.getGroupWidth(GROUP_SPACE, 0); float fromX = 0; for (int i = 0; i < maxEntryCount; i++) { float start = fromX; fromX += groupSpaceWidthHalf; for (IBarDataSet set : barData.getDataSets()) { fromX += barWidthHalf; if (i < set.getEntryCount()) { BarEntry entry = set.getEntryForIndex(i); if (entry != null) { entry.setX(fromX); } } fromX += barWidthHalf; } fromX += groupSpaceWidthHalf; float end = fromX; float innerInterval = end - start; float diff = interval - innerInterval; // correct rounding errors if (diff > 0 || diff < 0) { fromX += diff; } } barData.notifyDataChanged(); } public void clearData() { histogramData.clear(); for (IBarDataSet dataSet : barChart.getBarData().getDataSets()) { dataSet.clear(); } barChart.getBarData().notifyDataChanged(); barChart.invalidate(); } public void addEntry(int dataSetIndex, double value) { histogramData.addEntry(barChart.getBarData(), dataSetIndex, value); recalculateXAxis(); } public void addEntry(double value) { addEntry(0, value); } private void recalculateXAxis() { final XAxis xAxis = barChart.getXAxis(); xAxis.setAxisMinimum(0); xAxis.setAxisMaximum(histogramData.getNumBins()); barChart.notifyDataSetChanged(); barChart.invalidate(); } public void setLabel(int dataSetIndex, String label) { barChart.getBarData().getDataSetByIndex(dataSetIndex).setLabel(label); barChart.getLegendRenderer().computeLegend(barChart.getBarData()); barChart.invalidate(); } public void setLabel(String label) { setLabel(0, label); } public void setDescription(String description) { getBarChart().getDescription().setText(description); } public void setLegendEnabled(boolean enabled) { barChart.getLegend().setEnabled(enabled); barChart.notifyDataSetChanged(); barChart.invalidate(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.button_close_bar_chart: this.setVisibility(GONE); } } static class HistogramData { private float binWidth; private final ArrayList<ArrayList<Double>> rawData; private double minBin = 0; private double maxBin = 100; private double min = 0; private double max = 100; HistogramData(int numDataSets, float binWidth) { this.binWidth = binWidth; rawData = new ArrayList<>(numDataSets); for (int i = 0; i < numDataSets; i++) { rawData.add(new ArrayList<Double>()); } } float getBinWidth() { return binWidth; } double getMinBin() { return minBin; } void clear() { for (int i = 0; i < rawData.size(); i++) { rawData.get(i).clear(); } } private boolean isEmpty() { for (ArrayList<Double> data : rawData) { if (!data.isEmpty()) return false; } return true; } void addEntry(BarData barData, int dataSetIndex, double value) { if (isEmpty()) { min = value; max = value; } else { if (value < min) min = value; if (value > max) max = value; } rawData.get(dataSetIndex).add(value); recalculateDataSet(barData); } void recalculateDataSet(final BarData barData) { minBin = Math.floor(min / binWidth) * binWidth; maxBin = Math.floor(max / binWidth) * binWidth; int[][] bins = new int[rawData.size()][getNumBins()]; for (int setNum = 0; setNum < rawData.size(); setNum++) { for (Double d : rawData.get(setNum)) { ++bins[setNum][(int) (Math.floor((d - minBin) / binWidth))]; } } for (int setNum = 0; setNum < barData.getDataSetCount(); setNum++) { final IBarDataSet dataSet = barData.getDataSetByIndex(setNum); dataSet.clear(); for (int i = 0; i < bins[setNum].length; i++) { dataSet.addEntry(new BarEntry(i, bins[setNum][i])); } } groupBars(barData); barData.notifyDataChanged(); } int getNumBins() { return (int) (((maxBin - minBin) / binWidth) + 1); } double getDisplayValue(float value) { return value * getBinWidth() + getMinBin(); } } }