Last active
May 17, 2021 13:59
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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