Skip to content

Instantly share code, notes, and snippets.

@bric3
Last active May 17, 2021 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bric3/12dc41b29d743e3f140502f58704e282 to your computer and use it in GitHub Desktop.
Save bric3/12dc41b29d743e3f140502f58704e282 to your computer and use it in GitHub Desktop.
Sparkline in java (requires JDK 16, because of Stream::toArray and raw Strings)
package sandbox;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Random;
import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import static java.nio.charset.StandardCharsets.UTF_8;
public class Sparkline {
public static final Pattern DELIMITER = Pattern.compile(",|\\p{javaWhitespace}+");
static PrintStream stdout = System.out;
static InputStream stdin = System.in;
private static final char[] TICKS = {'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'};
public static void main(String... args) {
double[] doubles = null;
if (args.length == 1) {
switch (args[0]) {
case "-":
try (var scanner = new Scanner(new BufferedInputStream(stdin), UTF_8)
.useDelimiter(DELIMITER)) {
final var builder = DoubleStream.builder();
while (scanner.hasNextDouble()) {
final var t = scanner.nextDouble();
builder.accept(t);
}
doubles = builder.build().toArray();
}
break;
case "-h":
case "--help":
help();
return;
case "--example-usage":
example_usage();
return;
}
}
if (doubles == null) {
doubles = Arrays.stream(args)
.flatMap(arg -> Arrays.stream(DELIMITER.split(arg)))
.mapToDouble(Double::parseDouble)
.toArray();
}
if (doubles.length != 0) {
stdout.println(Sparkline.of(doubles));
} else {
help();
}
}
private static void example_usage() {
stdout.println(
"""
# Magnitude of earthquakes worldwide 2.5 and above in the last 24 hours
curl -s https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.csv |
sed '1d' |
cut -d, -f5 |
java Sparkline.java -
# Number of commits in a repo, by author
git shortlog -s | cut -f1 | java Sparkline.java -
# commits for the las 60 days
for day in $(seq 60 -1 0); do
git log --before="${day} days" --after="$[${day}+1] days" --format=oneline |
wc -l
done | java Sparkline.java -
More example here could be found on https://github.com/holman/spark/wiki/Wicked-Cool-Usage
"""
);
}
private static void help() {
int[] intArray = {1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
stdout.println("java Sparkline " + Arrays.stream(intArray).mapToObj(String::valueOf).collect(Collectors.joining(" ")));
stdout.println(Sparkline.of(Arrays.stream(intArray).asDoubleStream().toArray()));
double[] doubleArray = {1.5f, 0.5f, 3.5f, 2.5f, 5.5f, 4.5f, 7.5f, 6.5f};
stdout.println("java Sparkline " + Arrays.stream(doubleArray).mapToObj(String::valueOf).collect(Collectors.joining(" ")));
stdout.println(Sparkline.of(doubleArray));
stdout.println("shuf -i 0-20 | java Sparkline -");
stdout.println(Sparkline.of(new Random().ints(20).asDoubleStream().toArray()));
stdout.println(
"""
Comma or spaces can be used to separate the data, the dot indicates
a double number.
java Sparkline 0,30,55,80 33,150 20,100,2.1 33.4
▁▂▄▅▃█▂▆▁▃
Note this program use a slightly differently scaling algorithm than
https://github.com/holman/spark which may produce slightly different rendering.
More example with --example-usage
"""
);
}
public static String of(int[] ints) {
return Sparkline.of(Arrays.stream(ints).asDoubleStream().toArray());
}
public static String of(long[] longs) {
return Sparkline.of(Arrays.stream(longs).asDoubleStream().toArray());
}
public static String of(double[] doubles) {
final var bounds = Arrays.stream(doubles).collect(
() -> new Object() {
double min = Long.MAX_VALUE;
double max = Long.MIN_VALUE;
},
(acc, v) -> {
if (v < acc.min) acc.min = v;
if (v > acc.max) acc.max = v;
},
(acc, acc2) -> {
if (acc2.min < acc.min) acc.min = acc2.min;
if (acc2.max > acc.max) acc.max = acc2.max;
}
);
double range = bounds.max - bounds.min;
double scale = range / (TICKS.length - 1);
double mid = TICKS.length / 2d;
final var line = Arrays.stream(doubles)
.mapToInt(v -> Double.isNaN(v) ? ' ' : TICKS[(int) (range == 0 ? mid : Math.round((v - bounds.min) / scale))])
.collect(
StringBuilder::new,
(stringBuilder, i) -> stringBuilder.append((char) i),
StringBuilder::append
);
return line.toString();
}
}
package sandbox;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
/*
* Inspired by https://github.com/holman/spark/blob/ab88ac6f8f33698f39ece2f109b1117ef39a68eb/spark-test.sh
*/
class SparklineTest {
private ByteArrayOutputStream stdout;
@BeforeEach
void set_stdout() {
stdout = new ByteArrayOutputStream();
Sparkline.stdout = new PrintStream(stdout, true, UTF_8);
}
@AfterEach
void restore_stdout() {
Sparkline.stdout.close();
Sparkline.stdin = System.in;
Sparkline.stdout = System.out;
}
@Test
void it_shows_help_with_no_argv() {
Sparkline.main();
assertThat(stdout.toString(UTF_8))
.contains("java Sparkline 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1")
.contains("java Sparkline 1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5")
.contains("java Sparkline 0,30,55,80 33,150 20,100,2.1 33.4")
.contains("shuf -i 0-20 | java Sparkline -");
}
@Test
void it_shows_help_with_short_help_option() {
Sparkline.main("-h");
assertThat(stdout.toString(UTF_8))
.contains("java Sparkline 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1")
.contains("java Sparkline 1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5")
.contains("java Sparkline 0,30,55,80 33,150 20,100,2.1 33.4")
.contains("shuf -i 0-20 | java Sparkline -");
}
@Test
void it_shows_help_with_long_help_option() {
Sparkline.main("--help");
assertThat(stdout.toString(UTF_8))
.contains("java Sparkline 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1")
.contains("java Sparkline 1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5")
.contains("java Sparkline 0,30,55,80 33,150 20,100,2.1 33.4")
.contains("shuf -i 0-20 | java Sparkline -");
}
@Test
void it_shows_examples_with_long_example_usage_option() {
Sparkline.main("--example-usage");
assertThat(stdout.toString(UTF_8))
.contains("# Magnitude of earthquakes worldwide 2.5 and above in the last 24 hours")
.contains("# commits for the las 60 days");
}
@Test
void it_charts_pipe_data_delimited_by_space() {
Sparkline.stdin = new ByteArrayInputStream("0 30 55 80 33 150\n".getBytes(UTF_8));
Sparkline.main("-");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁▂▄▅▃█");
}
@Test
void it_charts_pipe_data_delimited_by_comma() {
Sparkline.stdin = new ByteArrayInputStream("0,30,55,80,33,150".getBytes(UTF_8));
Sparkline.main("-");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁▂▄▅▃█");
}
@Test
void it_charts_args_space_delimited_data() {
Sparkline.main("0 30 55 80 33 150");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁▂▄▅▃█");
}
@Test
void it_charts_args_comma_delimited_data() {
Sparkline.main("0,30,55,80,33,150");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁▂▄▅▃█");
}
@Test
void it_charts_args_mixed_space_and_comma_delimited_data() {
Sparkline.main("0,30,55,80", "33,150", "20,100,2.1 33.4");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁▂▄▅▃█▂▆▁▃");
}
@Test
void it_handles_decimals() {
Sparkline.main("5.5", "20");
assertThat(stdout.toString(UTF_8)).isEqualToIgnoringNewLines("▁█");
}
@Test
void it_can_accept_int_arrays() {
assertThat(Sparkline.of(new int[]{1, 5, 22, 13, 5})).isEqualTo("▁▂█▅▂");
}
@Test
void it_can_accept_long_arrays() {
assertThat(Sparkline.of(new long[]{1, 5, 22, 13, 5})).isEqualTo("▁▂█▅▂");
}
@Test
void it_charts_100_lt_300() {
assertThat(Sparkline.of(new int[]{1, 2, 3, 4, 100, 5, 10, 20, 50, 300})).isEqualTo("▁▁▁▁▃▁▁▁▂█");
}
@Test
void it_charts_50_lt_100() {
assertThat(Sparkline.of(new int[]{1, 50, 100})).isEqualTo("▁▄█");
}
@Test
void it_charts_4_lt_8() {
assertThat(Sparkline.of(new int[]{2, 4, 8})).isEqualTo("▁▃█");
}
@Test
void it_charts_no_tier_0() {
assertThat(Sparkline.of(new int[]{1, 2, 3, 4, 5, 6})).isEqualTo("▁▂▄▅▇█");
}
@Test
void it_equalizes_at_midtier_on_same_data() {
assertThat(Sparkline.of(new int[]{1, 1, 1, 1})).isEqualTo("▅▅▅▅");
}
@Test
void it_skips_NaN() {
assertThat(Sparkline.of(new double[]{1d, Double.NaN, 1d, Double.NaN, 1d, 1d, 1d})).isEqualTo("▅ ▅ ▅▅▅");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment