From 099818aee792e84d4d4dd05b0087580cd913a8ff Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:56:44 -0700 Subject: [PATCH 1/2] feat: add ConsoleEncoder for terminal/ASCII chart output Add a self-contained ConsoleEncoder utility alongside BitmapEncoder that renders any IChart as a Unicode block-glyph string for terminal output. It reuses BitmapEncoder.getBufferedImage(chart), downsamples the image into a character grid by averaging each block's luminance, and maps darker pixels to denser glyphs so chart data is visible on the light background. No core rendering, styler, or IChart code is changed. --- .../java/org/knowm/xchart/ConsoleEncoder.java | 132 ++++++++++++++++++ .../org/knowm/xchart/ConsoleEncoderTest.java | 82 +++++++++++ 2 files changed, 214 insertions(+) create mode 100644 xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java create mode 100644 xchart/src/test/java/org/knowm/xchart/ConsoleEncoderTest.java diff --git a/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java b/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java new file mode 100644 index 00000000..900d5872 --- /dev/null +++ b/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java @@ -0,0 +1,132 @@ +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 + * @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 + * @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 + */ + 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) { + + validateDimensions(columns, 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))]; + } +} diff --git a/xchart/src/test/java/org/knowm/xchart/ConsoleEncoderTest.java b/xchart/src/test/java/org/knowm/xchart/ConsoleEncoderTest.java new file mode 100644 index 00000000..99efd4c8 --- /dev/null +++ b/xchart/src/test/java/org/knowm/xchart/ConsoleEncoderTest.java @@ -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; + } +} From e1f2525a72d3d0725ceea58877c39e1660f41df1 Mon Sep 17 00:00:00 2001 From: Tim Molter Date: Mon, 22 Jun 2026 13:18:50 +0200 Subject: [PATCH 2/2] docs: fill in @param Javadoc and drop redundant dimension validation Complete the empty @param chart tags on the public ConsoleEncoder methods and remove the duplicate validateDimensions call in the private overload; validation already runs early in the public (chart, columns, rows) overload before rasterization, and the default overload always passes positive values. Co-Authored-By: Claude Opus 4.8 (1M context) --- xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java b/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java index 900d5872..78a0c624 100644 --- a/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java +++ b/xchart/src/main/java/org/knowm/xchart/ConsoleEncoder.java @@ -16,7 +16,7 @@ private ConsoleEncoder() {} /** * Generate a console string for a given chart using default dimensions * - * @param chart + * @param chart the chart to render * @return a console string for a given chart */ public static String getConsoleString(IChart chart) { @@ -38,7 +38,7 @@ public static String getConsoleString(IChart chart) { /** * Generate a console string for a given chart * - * @param 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 @@ -53,7 +53,7 @@ public static String getConsoleString(IChart chart, int columns, int rows) { /** * Print a console string for a given chart to System.out * - * @param chart + * @param chart the chart to render */ public static void printConsole(IChart chart) { @@ -72,8 +72,6 @@ private static void validateDimensions(int columns, int rows) { private static String getConsoleString(BufferedImage bufferedImage, int columns, int rows) { - validateDimensions(columns, rows); - StringBuilder builder = new StringBuilder(rows * (columns + 1)); int imageWidth = bufferedImage.getWidth(); int imageHeight = bufferedImage.getHeight();