Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.knowm.xchart;

import java.awt.image.BufferedImage;
import java.util.Objects;
import org.knowm.xchart.internal.chartpart.IChart;

/** A helper class with static methods for rendering Charts as console/terminal text */
public final class ConsoleEncoder {

private static final int DEFAULT_COLUMNS = 120;
private static final char[] RAMP = {' ', '\u2591', '\u2592', '\u2593', '\u2588'};

/** Constructor - Private constructor to prevent instantiation */
private ConsoleEncoder() {}

/**
* Generate a console string for a given chart using default dimensions
*
* @param chart the chart to render
* @return a console string for a given chart
*/
public static String getConsoleString(IChart chart) {

BufferedImage bufferedImage =
BitmapEncoder.getBufferedImage(Objects.requireNonNull(chart, "chart cannot be null"));
int rows =
Math.max(
1,
(int)
Math.round(
DEFAULT_COLUMNS
* bufferedImage.getHeight()
/ (double) bufferedImage.getWidth()
/ 2.0));
return getConsoleString(bufferedImage, DEFAULT_COLUMNS, rows);
}

/**
* Generate a console string for a given chart
*
* @param chart the chart to render
* @param columns number of character columns
* @param rows number of character rows
* @return a console string for a given chart
*/
public static String getConsoleString(IChart chart, int columns, int rows) {

Objects.requireNonNull(chart, "chart cannot be null");
validateDimensions(columns, rows);
return getConsoleString(BitmapEncoder.getBufferedImage(chart), columns, rows);
}

/**
* Print a console string for a given chart to System.out
*
* @param chart the chart to render
*/
public static void printConsole(IChart chart) {

System.out.print(getConsoleString(chart));
}

private static void validateDimensions(int columns, int rows) {

if (columns <= 0) {
throw new IllegalArgumentException("columns must be greater than 0");
}
if (rows <= 0) {
throw new IllegalArgumentException("rows must be greater than 0");
}
}

private static String getConsoleString(BufferedImage bufferedImage, int columns, int rows) {

StringBuilder builder = new StringBuilder(rows * (columns + 1));
int imageWidth = bufferedImage.getWidth();
int imageHeight = bufferedImage.getHeight();

for (int row = 0; row < rows; row++) {
int startY = getBlockStart(row, rows, imageHeight);
int endY = getBlockEnd(row, rows, imageHeight, startY);

for (int col = 0; col < columns; col++) {
int startX = getBlockStart(col, columns, imageWidth);
int endX = getBlockEnd(col, columns, imageWidth, startX);
builder.append(getGlyph(bufferedImage, startX, endX, startY, endY));
}

if (row < rows - 1) {
builder.append('\n');
}
}

return builder.toString();
}

private static int getBlockStart(int block, int blockCount, int pixelCount) {

return Math.min((int) Math.floor(block * (double) pixelCount / blockCount), pixelCount - 1);
}

private static int getBlockEnd(int block, int blockCount, int pixelCount, int blockStart) {

int blockEnd = (int) Math.ceil((block + 1) * (double) pixelCount / blockCount);
return Math.min(Math.max(blockEnd, blockStart + 1), pixelCount);
}

private static char getGlyph(
BufferedImage bufferedImage, int startX, int endX, int startY, int endY) {

double luminanceSum = 0;
int pixelCount = 0;

for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
int rgb = bufferedImage.getRGB(x, y);
int red = (rgb >> 16) & 0xFF;
int green = (rgb >> 8) & 0xFF;
int blue = rgb & 0xFF;
luminanceSum += 0.299 * red + 0.587 * green + 0.114 * blue;
pixelCount++;
}
}

double averageLuminance = luminanceSum / pixelCount;
double darkness = 1.0 - averageLuminance / 255.0;
int rampIndex = (int) Math.round(darkness * (RAMP.length - 1));
return RAMP[Math.max(0, Math.min(rampIndex, RAMP.length - 1))];
}
}
82 changes: 82 additions & 0 deletions xchart/src/test/java/org/knowm/xchart/ConsoleEncoderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.knowm.xchart;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

public class ConsoleEncoderTest {

@Test
public void getConsoleStringRendersRequestedGrid() {

XYChart chart = getChart();

String consoleString = ConsoleEncoder.getConsoleString(chart, 80, 24);

assertFalse(consoleString.isEmpty());
assertTrue(consoleString.contains("\n"));
String[] lines = consoleString.split("\n", -1);
assertEquals(24, lines.length);
for (String line : lines) {
assertEquals(80, line.length());
}
}

@Test
public void getConsoleStringRendersSmallGrid() {

XYChart chart = getChart();

String consoleString = ConsoleEncoder.getConsoleString(chart, 10, 5);

String[] lines = consoleString.split("\n", -1);
assertEquals(5, lines.length);
for (String line : lines) {
assertEquals(10, line.length());
}
}

@Test
public void getConsoleStringMapsDarkPixelsToVisibleGlyphs() {

XYChart chart = getChart();

String consoleString = ConsoleEncoder.getConsoleString(chart, 80, 24);

assertTrue(consoleString.chars().anyMatch(ch -> ch != ' ' && ch != '\n'));
assertTrue(consoleString.chars().anyMatch(ch -> ch == ' '));
}

@Test
public void getConsoleStringDefaultDimensionsDoesNotThrow() {

XYChart chart = getChart();

String consoleString = assertDoesNotThrow(() -> ConsoleEncoder.getConsoleString(chart));

assertTrue(consoleString.contains("\n"));
}

@Test
public void getConsoleStringRejectsInvalidArguments() {

XYChart chart = getChart();

assertThrows(NullPointerException.class, () -> ConsoleEncoder.getConsoleString(null));
assertThrows(IllegalArgumentException.class, () -> ConsoleEncoder.getConsoleString(chart, 0, 24));
assertThrows(IllegalArgumentException.class, () -> ConsoleEncoder.getConsoleString(chart, 80, 0));
assertThrows(IllegalArgumentException.class, () -> ConsoleEncoder.getConsoleString(chart, -1, 24));
assertThrows(IllegalArgumentException.class, () -> ConsoleEncoder.getConsoleString(chart, 80, -1));
}

private XYChart getChart() {

XYChart chart = new XYChartBuilder().width(400).height(300).build();
chart.addSeries("series", new double[] {1, 2, 3}, new double[] {4, 5, 6});
return chart;
}
}
Loading