/
ASCIIGraph.java
284 lines (237 loc) · 8.72 KB
/
ASCIIGraph.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
package com.mitchtalmadge.asciidata.graph;
import com.mitchtalmadge.asciidata.graph.util.SeriesUtils;
import java.text.DecimalFormat;
/**
* A two-axis graph created entirely from ASCII characters.
* Printable to a console, chat server, or anywhere else where text-style graphs are convenient.
*
* @author MitchTalmadge
* @author kroitor
*/
public class ASCIIGraph {
/**
* The data series, with index being the x-axis and value being the y-axis.
*/
private double[] series;
/**
* The minimum value in the series.
*/
private double min;
/**
* The maximum value in the series.
*/
private double max;
/**
* The range of the data in the series.
*/
private double range;
/**
* The number of rows in the graph.
*/
private int numRows;
/**
* The number of columns in the graph, including the axis and ticks.
*/
private int numCols;
/**
* How wide the ticks are. Ticks are left-padded with spaces to be this length.
*/
private int tickWidth = 8;
/**
* How the ticks should be formatted.
*/
private DecimalFormat tickFormat = new DecimalFormat("###0.00");
/**
* The index at which the axis starts.
*/
private int axisIndex;
/**
* Ths index at which the line starts.
*/
private int lineIndex;
private ASCIIGraph(double[] series) {
this.series = series;
}
/**
* Calculates the instance fields used for plotting.
*/
private void calculateFields() {
// Get minimum and maximum from series.
double[] minMax = SeriesUtils.getMinAndMaxValues(this.series);
this.min = minMax[0];
this.max = minMax[1];
this.range = max - min;
axisIndex = tickWidth + 1;
lineIndex = axisIndex + 1;
// Since the graph is made of ASCII characters, it needs whole-number counts of rows and columns.
this.numRows = numRows == 0 ? (int) Math.round(max - min) + 1 : numRows;
// For columns, add the width of the tick marks, the width of the axis, and the length of the series.
this.numCols = tickWidth + (axisIndex - tickWidth) + series.length;
}
/**
* Creates an ASCIIGraph instance from the given series.
*
* @param series The series of data, where index is the x-axis and value is the y-axis.
* @return A new ASCIIGraph instance.
*/
public static ASCIIGraph fromSeries(double[] series) {
return new ASCIIGraph(series);
}
/**
* Determines the number of rows in the graph.
* By default, the number of rows will be equal to the range of the series + 1.
*
* @param numRows The number of rows desired. If 0, uses the default.
* @return This instance.
*/
public ASCIIGraph withNumRows(int numRows) {
this.numRows = numRows;
return this;
}
/**
* Determines the minimum width of the ticks on the axis.
* Ticks will be left-padded with spaces if they are not already this length.
* Defaults to 8.
*
* @param tickWidth The width of the ticks on the axis.
* @return This instance.
*/
public ASCIIGraph withTickWidth(int tickWidth) {
this.tickWidth = tickWidth;
return this;
}
/**
* Determines how the ticks will be formatted.
* Defaults to "###0.00".
*
* @param tickFormat The format of the ticks.
* @return This instance.
*/
public ASCIIGraph withTickFormat(DecimalFormat tickFormat) {
this.tickFormat = tickFormat;
return this;
}
/**
* Plots the graph and returns it as a String.
*
* @return The string representation of the graph, using new lines.
*/
public String plot() {
calculateFields();
// ---- PLOTTING ---- //
// The graph is initially stored in a 2D array, later turned into Strings.
char[][] graph = new char[numRows][numCols];
// Fill the graph with space characters.
for (int row = 0; row < numRows; row++) {
for (int col = 0; col < graph[row].length; col++) {
graph[row][col] = ' ';
}
}
// Draw the ticks and graph.
drawTicksAndAxis(graph);
// Draw the line.
drawLine(graph);
// Convert the 2D char array graph to a String using newlines.
return convertGraphToString(graph);
}
/**
* Adds the tick marks and axis to the graph.
*
* @param graph The graph.
*/
private void drawTicksAndAxis(char[][] graph) {
// Add the labels and the axis.
for (int row = 0; row < graph.length; row++) {
double y = determineYValueAtRow(row);
// Compute and Format Tick
char[] tick = formatTick(y).toCharArray();
// Insert Tick
System.arraycopy(tick, 0, graph[row], 0, tick.length);
// Insert Axis line. '┼' is used at the origin.
graph[row][axisIndex] = (y == 0) ? '┼' : '┤';
}
}
/**
* Adds the line to the graph.
*
* @param graph The graph.
*/
private void drawLine(char[][] graph) {
// The row closest to y when x = 0.
int initialRow = determineRowAtYValue(series[0]);
// Modify the axis to show the start.
graph[initialRow][axisIndex] = '┼';
for (int x = 0; x < series.length - 1; x++) {
// The start and end locations of the line.
int startRow = determineRowAtYValue(series[x]);
int endRow = determineRowAtYValue(series[x + 1]);
if (startRow == endRow) { // The line is horizontal.
graph[startRow][lineIndex + x] = '─';
} else { // The line has slope.
// Draw the curved lines.
graph[startRow][lineIndex + x] = (startRow < endRow) ? '╮' : '╯';
graph[endRow][lineIndex + x] = (startRow < endRow) ? '╰' : '╭';
// If the rows are more than 1 row apart, we need to fill in the gap with vertical lines.
int lowerRow = Math.min(startRow, endRow);
int upperRow = Math.max(startRow, endRow);
for (int row = lowerRow + 1; row < upperRow; row++) {
graph[row][lineIndex + x] = '│';
}
}
}
}
/**
* Determines the row closest to the given y-axis value.
*
* @param yValue The value of y.
* @return The closest row to the given y-axis value.
*/
private int determineRowAtYValue(double yValue) {
// ((yValue - min) / range) creates a ratio -- how deep the y-value is into the range.
// Multiply that by the number of rows to determine how deep the y-value is into the number of rows.
// Then invert it buy subtracting it from the number of rows, since 0 is actually the top.
// 1 is subtracted from numRows since it is a length, and we start at 0.
return (numRows - 1) - (int) Math.round(((yValue - min) / range) * (numRows - 1));
}
/**
* Determines the y-axis value corresponding to the given row.
*
* @param row The row.
* @return The y-axis value at the given row.
*/
private double determineYValueAtRow(int row) {
// Compute the current y value by starting with the maximum and subtracting how far down we are in rows.
// Splitting the range into chunks based on the number of rows gives us how much to subtract per row.
// (-1 from the number of rows because it is a length, and the last row index is actually numRows - 1).
return max - (row * (range / (numRows - 1)));
}
/**
* Formats the given value as a tick mark on the graph.
* Pads the tick mark with the correct number of spaces
* in order to be {@link ASCIIGraph#tickWidth} characters long.
*
* @param value The value of the tick mark.
* @return The formatted tick mark.
*/
private String formatTick(double value) {
StringBuilder paddedTick = new StringBuilder();
String formattedValue = tickFormat.format(value);
for (int i = 0; i < tickWidth - formattedValue.length(); i++) {
paddedTick.append(' ');
}
return paddedTick.append(formattedValue).toString();
}
/**
* Converts the 2D char array representation of the graph into a String representation.
*
* @param graph The 2D char array representation of the graph.
* @return The String representation of the graph.
*/
private String convertGraphToString(char[][] graph) {
StringBuilder stringGraph = new StringBuilder();
for (char[] row : graph) {
stringGraph.append(row).append('\n');
}
return stringGraph.toString();
}
}