1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.chromium.latency.walt; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.util.AttributeSet; 22 import android.view.View; 23 import android.widget.RelativeLayout; 24 25 import com.github.mikephil.charting.charts.BarChart; 26 import com.github.mikephil.charting.components.AxisBase; 27 import com.github.mikephil.charting.components.Description; 28 import com.github.mikephil.charting.components.XAxis; 29 import com.github.mikephil.charting.data.BarData; 30 import com.github.mikephil.charting.data.BarDataSet; 31 import com.github.mikephil.charting.data.BarEntry; 32 import com.github.mikephil.charting.formatter.IAxisValueFormatter; 33 import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; 34 import com.github.mikephil.charting.utils.ColorTemplate; 35 36 import java.text.DecimalFormat; 37 import java.util.ArrayList; 38 39 public class HistogramChart extends RelativeLayout implements View.OnClickListener { 40 41 static final float GROUP_SPACE = 0.1f; 42 private HistogramData histogramData; 43 private BarChart barChart; 44 HistogramChart(Context context, AttributeSet attrs)45 public HistogramChart(Context context, AttributeSet attrs) { 46 super(context, attrs); 47 inflate(getContext(), R.layout.histogram, this); 48 49 barChart = (BarChart) findViewById(R.id.bar_chart); 50 findViewById(R.id.button_close_bar_chart).setOnClickListener(this); 51 52 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HistogramChart); 53 final String descString; 54 final int numDataSets; 55 final float binWidth; 56 try { 57 descString = a.getString(R.styleable.HistogramChart_description); 58 numDataSets = a.getInteger(R.styleable.HistogramChart_numDataSets, 1); 59 binWidth = a.getFloat(R.styleable.HistogramChart_binWidth, 5f); 60 } finally { 61 a.recycle(); 62 } 63 64 ArrayList<IBarDataSet> dataSets = new ArrayList<>(numDataSets); 65 for (int i = 0; i < numDataSets; i++) { 66 final BarDataSet dataSet = new BarDataSet(new ArrayList<BarEntry>(), ""); 67 dataSet.setColor(ColorTemplate.MATERIAL_COLORS[i]); 68 dataSets.add(dataSet); 69 } 70 71 BarData barData = new BarData(dataSets); 72 barData.setBarWidth((1f - GROUP_SPACE)/numDataSets); 73 barChart.setData(barData); 74 histogramData = new HistogramData(numDataSets, binWidth); 75 groupBars(barData); 76 final Description desc = new Description(); 77 desc.setText(descString); 78 desc.setTextSize(12f); 79 barChart.setDescription(desc); 80 81 XAxis xAxis = barChart.getXAxis(); 82 xAxis.setGranularityEnabled(true); 83 xAxis.setGranularity(1); 84 xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); 85 xAxis.setValueFormatter(new IAxisValueFormatter() { 86 DecimalFormat df = new DecimalFormat("#.##"); 87 88 @Override 89 public String getFormattedValue(float value, AxisBase axis) { 90 return df.format(histogramData.getDisplayValue(value)); 91 } 92 }); 93 94 barChart.setFitBars(true); 95 barChart.invalidate(); 96 } 97 getBarChart()98 BarChart getBarChart() { 99 return barChart; 100 } 101 102 /** 103 * Re-implementation of BarData.groupBars(), but allows grouping with only 1 BarDataSet 104 * This adjusts the x-coordinates of entries, which centers the bars between axis labels 105 */ groupBars(final BarData barData)106 static void groupBars(final BarData barData) { 107 IBarDataSet max = barData.getMaxEntryCountSet(); 108 int maxEntryCount = max.getEntryCount(); 109 float groupSpaceWidthHalf = GROUP_SPACE / 2f; 110 float barWidthHalf = barData.getBarWidth() / 2f; 111 float interval = barData.getGroupWidth(GROUP_SPACE, 0); 112 float fromX = 0; 113 114 for (int i = 0; i < maxEntryCount; i++) { 115 float start = fromX; 116 fromX += groupSpaceWidthHalf; 117 118 for (IBarDataSet set : barData.getDataSets()) { 119 fromX += barWidthHalf; 120 if (i < set.getEntryCount()) { 121 BarEntry entry = set.getEntryForIndex(i); 122 if (entry != null) { 123 entry.setX(fromX); 124 } 125 } 126 fromX += barWidthHalf; 127 } 128 129 fromX += groupSpaceWidthHalf; 130 float end = fromX; 131 float innerInterval = end - start; 132 float diff = interval - innerInterval; 133 134 // correct rounding errors 135 if (diff > 0 || diff < 0) { 136 fromX += diff; 137 } 138 } 139 barData.notifyDataChanged(); 140 } 141 clearData()142 public void clearData() { 143 histogramData.clear(); 144 for (IBarDataSet dataSet : barChart.getBarData().getDataSets()) { 145 dataSet.clear(); 146 } 147 barChart.getBarData().notifyDataChanged(); 148 barChart.invalidate(); 149 } 150 addEntry(int dataSetIndex, double value)151 public void addEntry(int dataSetIndex, double value) { 152 histogramData.addEntry(barChart.getBarData(), dataSetIndex, value); 153 recalculateXAxis(); 154 } 155 addEntry(double value)156 public void addEntry(double value) { 157 addEntry(0, value); 158 } 159 recalculateXAxis()160 private void recalculateXAxis() { 161 final XAxis xAxis = barChart.getXAxis(); 162 xAxis.setAxisMinimum(0); 163 xAxis.setAxisMaximum(histogramData.getNumBins()); 164 barChart.notifyDataSetChanged(); 165 barChart.invalidate(); 166 } 167 setLabel(int dataSetIndex, String label)168 public void setLabel(int dataSetIndex, String label) { 169 barChart.getBarData().getDataSetByIndex(dataSetIndex).setLabel(label); 170 barChart.getLegendRenderer().computeLegend(barChart.getBarData()); 171 barChart.invalidate(); 172 } 173 setLabel(String label)174 public void setLabel(String label) { 175 setLabel(0, label); 176 } 177 setDescription(String description)178 public void setDescription(String description) { 179 getBarChart().getDescription().setText(description); 180 } 181 setLegendEnabled(boolean enabled)182 public void setLegendEnabled(boolean enabled) { 183 barChart.getLegend().setEnabled(enabled); 184 barChart.notifyDataSetChanged(); 185 barChart.invalidate(); 186 } 187 188 @Override onClick(View v)189 public void onClick(View v) { 190 switch (v.getId()) { 191 case R.id.button_close_bar_chart: 192 this.setVisibility(GONE); 193 } 194 } 195 196 static class HistogramData { 197 private float binWidth; 198 private final ArrayList<ArrayList<Double>> rawData; 199 private double minBin = 0; 200 private double maxBin = 100; 201 private double min = 0; 202 private double max = 100; 203 HistogramData(int numDataSets, float binWidth)204 HistogramData(int numDataSets, float binWidth) { 205 this.binWidth = binWidth; 206 rawData = new ArrayList<>(numDataSets); 207 for (int i = 0; i < numDataSets; i++) { 208 rawData.add(new ArrayList<Double>()); 209 } 210 } 211 getBinWidth()212 float getBinWidth() { 213 return binWidth; 214 } 215 getMinBin()216 double getMinBin() { 217 return minBin; 218 } 219 clear()220 void clear() { 221 for (int i = 0; i < rawData.size(); i++) { 222 rawData.get(i).clear(); 223 } 224 } 225 isEmpty()226 private boolean isEmpty() { 227 for (ArrayList<Double> data : rawData) { 228 if (!data.isEmpty()) return false; 229 } 230 return true; 231 } 232 addEntry(BarData barData, int dataSetIndex, double value)233 void addEntry(BarData barData, int dataSetIndex, double value) { 234 if (isEmpty()) { 235 min = value; 236 max = value; 237 } else { 238 if (value < min) min = value; 239 if (value > max) max = value; 240 } 241 242 rawData.get(dataSetIndex).add(value); 243 recalculateDataSet(barData); 244 } 245 recalculateDataSet(final BarData barData)246 void recalculateDataSet(final BarData barData) { 247 minBin = Math.floor(min / binWidth) * binWidth; 248 maxBin = Math.floor(max / binWidth) * binWidth; 249 250 int[][] bins = new int[rawData.size()][getNumBins()]; 251 252 for (int setNum = 0; setNum < rawData.size(); setNum++) { 253 for (Double d : rawData.get(setNum)) { 254 ++bins[setNum][(int) (Math.floor((d - minBin) / binWidth))]; 255 } 256 } 257 258 for (int setNum = 0; setNum < barData.getDataSetCount(); setNum++) { 259 final IBarDataSet dataSet = barData.getDataSetByIndex(setNum); 260 dataSet.clear(); 261 for (int i = 0; i < bins[setNum].length; i++) { 262 dataSet.addEntry(new BarEntry(i, bins[setNum][i])); 263 } 264 } 265 groupBars(barData); 266 barData.notifyDataChanged(); 267 } 268 getNumBins()269 int getNumBins() { 270 return (int) (((maxBin - minBin) / binWidth) + 1); 271 } 272 getDisplayValue(float value)273 double getDisplayValue(float value) { 274 return value * getBinWidth() + getMinBin(); 275 } 276 } 277 } 278