forked from jjoe64/GraphView
-
Notifications
You must be signed in to change notification settings - Fork 1
/
GraphView.java
627 lines (561 loc) · 16.9 KB
/
GraphView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
package com.jjoe64.graphview;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import com.jjoe64.graphview.compatible.ScaleGestureDetector;
/**
* GraphView is a Android View for creating zoomable and scrollable graphs.
* This is the abstract base class for all graphs. Extend this class and implement {@link #drawSeries(Canvas, GraphViewData[], float, float, float, double, double, double, double, float)} to display a custom graph.
* Use {@link LineGraphView} for creating a line chart.
*
* @author jjoe64 - jonas gehring - http://www.jjoe64.com
*
* Copyright (C) 2011 Jonas Gehring
* Licensed under the GNU Lesser General Public License (LGPL)
* http://www.gnu.org/licenses/lgpl.html
*/
abstract public class GraphView extends LinearLayout {
static final private class GraphViewConfig {
static final float BORDER = 20;
static final float VERTICAL_LABEL_WIDTH = 100;
static final float HORIZONTAL_LABEL_HEIGHT = 80;
}
private class GraphViewContentView extends View {
private float lastTouchEventX;
private float graphwidth;
/**
* @param context
*/
public GraphViewContentView(Context context) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
}
/**
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
paint.setAntiAlias(true);
// normal
paint.setStrokeWidth(0);
float border = GraphViewConfig.BORDER;
float horstart = 0;
float height = getHeight();
float width = getWidth() - 1;
double maxY = getMaxY();
double minY = getMinY();
double diffY = maxY - minY;
double maxX = getMaxX(false);
double minX = getMinX(false);
double diffX = maxX - minX;
float graphheight = height - (2 * border);
graphwidth = width;
if (horlabels == null) {
horlabels = generateHorlabels(graphwidth);
}
if (verlabels == null) {
verlabels = generateVerlabels(graphheight);
}
// vertical lines
paint.setTextAlign(Align.LEFT);
int vers = verlabels.length - 1;
for (int i = 0; i < verlabels.length; i++) {
paint.setColor(Color.DKGRAY);
float y = ((graphheight / vers) * i) + border;
canvas.drawLine(horstart, y, width, y, paint);
}
// horizontal labels + lines
int hors = horlabels.length - 1;
for (int i = 0; i < horlabels.length; i++) {
paint.setColor(Color.DKGRAY);
float x = ((graphwidth / hors) * i) + horstart;
canvas.drawLine(x, height - border, x, border, paint);
paint.setTextAlign(Align.CENTER);
if (i==horlabels.length-1)
paint.setTextAlign(Align.RIGHT);
if (i==0)
paint.setTextAlign(Align.LEFT);
paint.setColor(Color.WHITE);
canvas.drawText(horlabels[i], x, height - 4, paint);
}
paint.setTextAlign(Align.CENTER);
canvas.drawText(title, (graphwidth / 2) + horstart, border - 4, paint);
if (maxY != minY) {
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(3);
for (int i=0; i<graphSeries.size(); i++) {
paint.setColor(graphSeries.get(i).color);
drawSeries(canvas, _values(i), graphwidth, graphheight, border, minX, minY, diffX, diffY, horstart);
}
if (showLegend) drawLegend(canvas, height, width);
}
}
private void onMoveGesture(float f) {
// view port update
if (viewportSize != 0) {
viewportStart -= f*viewportSize/graphwidth;
// minimal and maximal view limit
double minX = getMinX(true);
double maxX = getMaxX(true);
if (viewportStart < minX) {
viewportStart = minX;
} else if (viewportStart+viewportSize > maxX) {
viewportStart = maxX - viewportSize;
}
// labels have to be regenerated
horlabels = null;
verlabels = null;
viewVerLabels.invalidate();
}
invalidate();
}
/**
* @param event
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isScrollable()) {
return super.onTouchEvent(event);
}
boolean handled = false;
// first scale
if (scalable && scaleDetector != null) {
scaleDetector.onTouchEvent(event);
handled = scaleDetector.isInProgress();
}
if (!handled) {
// if not scaled, scroll
if ((event.getAction() & MotionEvent.ACTION_DOWN) == MotionEvent.ACTION_DOWN) {
handled = true;
}
if ((event.getAction() & MotionEvent.ACTION_UP) == MotionEvent.ACTION_UP) {
lastTouchEventX = 0;
handled = true;
}
if ((event.getAction() & MotionEvent.ACTION_MOVE) == MotionEvent.ACTION_MOVE) {
if (lastTouchEventX != 0) {
onMoveGesture(event.getX() - lastTouchEventX);
}
lastTouchEventX = event.getX();
handled = true;
}
}
return handled;
}
}
/**
* one data set for a graph series
*/
static public class GraphViewData {
public final double valueX;
public final double valueY;
public GraphViewData(double valueX, double valueY) {
super();
this.valueX = valueX;
this.valueY = valueY;
}
}
/**
* a graph series
*/
static public class GraphViewSeries {
final String description;
final int color;
final GraphViewData[] values;
public GraphViewSeries(GraphViewData[] values) {
description = null;
color = 0xff0077cc; // blue version
this.values = values;
}
public GraphViewSeries(String description, Integer color, GraphViewData[] values) {
super();
this.description = description;
if (color == null) {
color = 0xff0077cc; // blue version
}
this.color = color;
this.values = values;
}
}
public enum LegendAlign {
TOP, MIDDLE, BOTTOM
}
private class VerLabelsView extends View {
/**
* @param context
*/
public VerLabelsView(Context context) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 10));
}
/**
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
// normal
paint.setStrokeWidth(0);
float border = GraphViewConfig.BORDER;
float height = getHeight();
float graphheight = height - (2 * border);
if (verlabels == null) {
verlabels = generateVerlabels(graphheight);
}
// vertical labels
paint.setTextAlign(Align.LEFT);
int vers = verlabels.length - 1;
for (int i = 0; i < verlabels.length; i++) {
float y = ((graphheight / vers) * i) + border;
paint.setColor(Color.WHITE);
canvas.drawText(verlabels[i], 0, y, paint);
}
}
}
protected final Paint paint;
private String[] horlabels;
private String[] verlabels;
private String title;
private boolean scrollable;
private double viewportStart;
private double viewportSize;
private final View viewVerLabels;
private ScaleGestureDetector scaleDetector;
private boolean scalable;
private NumberFormat numberformatter;
private final List<GraphViewSeries> graphSeries;
private boolean showLegend = false;
private float legendWidth = 120;
private LegendAlign legendAlign = LegendAlign.MIDDLE;
private boolean manualYAxis;
private double manualMaxYValue;
private double manualMinYValue;
/**
*
* @param context
* @param title [optional]
*/
public GraphView(Context context, String title) {
super(context);
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
if (title == null)
title = "";
else
this.title = title;
paint = new Paint();
graphSeries = new ArrayList<GraphViewSeries>();
viewVerLabels = new VerLabelsView(context);
addView(viewVerLabels);
addView(new GraphViewContentView(context), new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 1));
}
private GraphViewData[] _values(int idxSeries) {
GraphViewData[] values = graphSeries.get(idxSeries).values;
if (viewportStart == 0 && viewportSize == 0) {
// all data
return values;
} else {
// viewport
List<GraphViewData> listData = new ArrayList<GraphViewData>();
for (int i=0; i<values.length; i++) {
if (values[i].valueX >= viewportStart) {
if (values[i].valueX > viewportStart+viewportSize) {
listData.add(values[i]); // one more for nice scrolling
break;
} else {
listData.add(values[i]);
}
} else {
if (listData.isEmpty()) {
listData.add(values[i]);
}
listData.set(0, values[i]); // one before, for nice scrolling
}
}
return listData.toArray(new GraphViewData[listData.size()]);
}
}
public void addSeries(GraphViewSeries series) {
graphSeries.add(series);
}
public void removeSeries(int index)
{
if (index < 0 || index >= graphSeries.size())
{
throw new IndexOutOfBoundsException("No series at index " + index);
}
graphSeries.remove(index);
}
public void removeSeries(GraphViewSeries series)
{
graphSeries.remove(series);
}
protected void drawLegend(Canvas canvas, float height, float width) {
int shapeSize = 15;
// rect
paint.setARGB(180, 100, 100, 100);
float legendHeight = (shapeSize+5)*graphSeries.size() +5;
float lLeft = width-legendWidth - 10;
float lTop;
switch (legendAlign) {
case TOP:
lTop = 10;
break;
case MIDDLE:
lTop = height/2 - legendHeight/2;
break;
default:
lTop = height - GraphViewConfig.BORDER - legendHeight -10;
}
float lRight = lLeft+legendWidth;
float lBottom = lTop+legendHeight;
canvas.drawRoundRect(new RectF(lLeft, lTop, lRight, lBottom), 8, 8, paint);
for (int i=0; i<graphSeries.size(); i++) {
paint.setColor(graphSeries.get(i).color);
canvas.drawRect(new RectF(lLeft+5, lTop+5+(i*(shapeSize+5)), lLeft+5+shapeSize, lTop+((i+1)*(shapeSize+5))), paint);
if (graphSeries.get(i).description != null) {
paint.setColor(Color.WHITE);
paint.setTextAlign(Align.LEFT);
canvas.drawText(graphSeries.get(i).description, lLeft+5+shapeSize+5, lTop+shapeSize+(i*(shapeSize+5)), paint);
}
}
}
abstract public void drawSeries(Canvas canvas, GraphViewData[] values, float graphwidth, float graphheight, float border, double minX, double minY, double diffX, double diffY, float horstart);
/**
* formats the label
* can be overwritten
* @param value x and y values
* @param isValueX if false, value y wants to be formatted
* @return value to display
*/
protected String formatLabel(double value, boolean isValueX) {
if (numberformatter == null) {
numberformatter = NumberFormat.getNumberInstance();
double highestvalue = getMaxY();
double lowestvalue = getMinY();
if (highestvalue - lowestvalue < 0.1) {
numberformatter.setMaximumFractionDigits(6);
} else if (highestvalue - lowestvalue < 1) {
numberformatter.setMaximumFractionDigits(4);
} else if (highestvalue - lowestvalue < 20) {
numberformatter.setMaximumFractionDigits(3);
} else if (highestvalue - lowestvalue < 100) {
numberformatter.setMaximumFractionDigits(1);
} else {
numberformatter.setMaximumFractionDigits(0);
}
}
return numberformatter.format(value);
}
private String[] generateHorlabels(float graphwidth) {
int numLabels = (int) (graphwidth/GraphViewConfig.VERTICAL_LABEL_WIDTH);
String[] labels = new String[numLabels+1];
double min = getMinX(false);
double max = getMaxX(false);
for (int i=0; i<=numLabels; i++) {
labels[i] = formatLabel(min + ((max-min)*i/numLabels), true);
}
return labels;
}
synchronized private String[] generateVerlabels(float graphheight) {
int numLabels = (int) (graphheight/GraphViewConfig.HORIZONTAL_LABEL_HEIGHT);
String[] labels = new String[numLabels+1];
double min = getMinY();
double max = getMaxY();
for (int i=0; i<=numLabels; i++) {
labels[numLabels-i] = formatLabel(min + ((max-min)*i/numLabels), false);
}
return labels;
}
public LegendAlign getLegendAlign() {
return legendAlign;
}
public float getLegendWidth() {
return legendWidth;
}
private double getMaxX(boolean ignoreViewport) {
// if viewport is set, use this
if (!ignoreViewport && viewportSize != 0) {
return viewportStart+viewportSize;
} else {
// otherwise use the max x value
// values must be sorted by x, so the last value has the largest X value
double highest = 0;
if (graphSeries.size() > 0)
{
GraphViewData[] values = graphSeries.get(0).values;
highest = values[values.length-1].valueX;
for (int i=1; i<graphSeries.size(); i++) {
values = graphSeries.get(i).values;
highest = Math.max(highest, values[values.length-1].valueX);
}
}
return highest;
}
}
private double getMaxY() {
double largest;
if (manualYAxis) {
largest = manualMaxYValue;
} else {
largest = Integer.MIN_VALUE;
for (int i=0; i<graphSeries.size(); i++) {
GraphViewData[] values = _values(i);
for (int ii=0; ii<values.length; ii++)
if (values[ii].valueY > largest)
largest = values[ii].valueY;
}
}
return largest;
}
private double getMinX(boolean ignoreViewport) {
// if viewport is set, use this
if (!ignoreViewport && viewportSize != 0) {
return viewportStart;
} else {
// otherwise use the min x value
// values must be sorted by x, so the first value has the smallest X value
double lowest = 0;
if (graphSeries.size() > 0)
{
GraphViewData[] values = graphSeries.get(0).values;
lowest = values[0].valueX;
for (int i=1; i<graphSeries.size(); i++) {
values = graphSeries.get(i).values;
lowest = Math.min(lowest, values[0].valueX);
}
}
return lowest;
}
}
private double getMinY() {
double smallest;
if (manualYAxis) {
smallest = manualMinYValue;
} else {
smallest = Integer.MAX_VALUE;
for (int i=0; i<graphSeries.size(); i++) {
GraphViewData[] values = _values(i);
for (int ii=0; ii<values.length; ii++)
if (values[ii].valueY < smallest)
smallest = values[ii].valueY;
}
}
return smallest;
}
public boolean isScrollable() {
return scrollable;
}
public boolean isShowLegend() {
return showLegend;
}
/**
* set's static horizontal labels (from left to right)
* @param horlabels if null, labels were generated automatically
*/
public void setHorizontalLabels(String[] horlabels) {
this.horlabels = horlabels;
}
public void setLegendAlign(LegendAlign legendAlign) {
this.legendAlign = legendAlign;
}
public void setLegendWidth(float legendWidth) {
this.legendWidth = legendWidth;
}
/**
* you have to set the bounds {@link #setManualYAxisBounds(double, double)}. That automatically enables manualYAxis-flag.
* if you want to disable the menual y axis, call this method with false.
* @param manualYAxis
*/
public void setManualYAxis(boolean manualYAxis) {
this.manualYAxis = manualYAxis;
}
/**
* set manual Y axis limit
* @param max
* @param min
*/
public void setManualYAxisBounds(double max, double min) {
manualMaxYValue = max;
manualMinYValue = min;
manualYAxis = true;
}
/**
* this forces scrollable = true
* @param scalable
*/
synchronized public void setScalable(boolean scalable) {
this.scalable = scalable;
if (scalable == true && scaleDetector == null) {
scrollable = true; // automatically forces this
scaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
double newSize = viewportSize*detector.getScaleFactor();
double diff = newSize-viewportSize;
viewportStart += diff/2;
viewportSize -= diff;
if (diff < 0) {
// viewportStart must not be < minX
double minX = getMinX(true);
if (viewportStart < minX) {
viewportStart = minX;
}
// viewportStart + viewportSize must not be > maxX
double maxX = getMaxX(true);
double overlap = viewportStart + viewportSize - maxX;
if (overlap > 0) {
// scroll left
if (viewportStart-overlap > minX) {
viewportStart -= overlap;
} else {
// maximal scale
viewportStart = minX;
viewportSize = maxX - viewportStart;
}
}
}
verlabels = null;
horlabels = null;
numberformatter = null;
invalidate();
viewVerLabels.invalidate();
return true;
}
});
}
}
/**
* the user can scroll (horizontal) the graph. This is only useful if you use a viewport {@link #setViewPort(double, double)} which doesn't displays all data.
* @param scrollable
*/
public void setScrollable(boolean scrollable) {
this.scrollable = scrollable;
}
public void setShowLegend(boolean showLegend) {
this.showLegend = showLegend;
}
/**
* set's static vertical labels (from top to bottom)
* @param verlabels if null, labels were generated automatically
*/
public void setVerticalLabels(String[] verlabels) {
this.verlabels = verlabels;
}
/**
* set's the viewport for the graph.
* @param start x-value
* @param size
*/
public void setViewPort(double start, double size) {
viewportStart = start;
viewportSize = size;
}
}