Skip to content

Add support for simple horizontal bar chart #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -119,6 +119,7 @@ repaint.
* [x] Area charts
* [x] Step Area charts
* [x] Bar charts
* [x] Horizontal bar charts
* [x] Histogram charts
* [x] Pie charts
* [x] Donut charts
@@ -151,17 +152,18 @@ repaint.

Currently, there are 5 major chart types. Each type has its corresponding `ChartBuilder`, `Styler` and `Series`.

| Chart Type | Builder | Styler | Series | Allowed Data Types | Default Series Render Style |
|---------------|----------------------|----------------|----------------|----------------------|-----------------------------|
| XYChart | XYChartBuilder | XYStyler | XYSeries | Number, Date | Line |
| CategoryChart | CategoryChartBuilder | CategoryStyler | CategorySeries | Number, Date, String | Bar |
| PieChart | PieChartBuilder | PieStyler | PieSeries | String | Pie |
| BubbleChart | BubbleChartBuilder | BubbleStyler | BubbleSeries | Number, Date | Round |
| DialChart | DialChartBuilder | DialStyler | DialSeries | double | Round |
| RadarChart | RadarChartBuilder | RadarStyler | RadarSeries | double[] | Round |
| OHLCChart | OHLCChartBuilder | OHLCStyler | OHLCSeries | OHLC with Date | Candle |
| BoxChart | BoxChartBuilder | BoxStyler | BoxSeries | Number, Date, String | Box |
| HeatMapChart | HeatMapChartBuilder | HeatMapStyler | HeatMapSeries | Number, Date, String | -- |
| Chart Type | Builder | Styler | Series | Allowed Data Types | Default Series Render Style |
|--------------------|---------------------------|---------------------|---------------------|----------------------|-----------------------------|
| XYChart | XYChartBuilder | XYStyler | XYSeries | Number, Date | Line |
| CategoryChart | CategoryChartBuilder | CategoryStyler | CategorySeries | Number, Date, String | Bar |
| PieChart | PieChartBuilder | PieStyler | PieSeries | String | Pie |
| BubbleChart | BubbleChartBuilder | BubbleStyler | BubbleSeries | Number, Date | Round |
| DialChart | DialChartBuilder | DialStyler | DialSeries | double | Round |
| RadarChart | RadarChartBuilder | RadarStyler | RadarSeries | double[] | Round |
| OHLCChart | OHLCChartBuilder | OHLCStyler | OHLCSeries | OHLC with Date | Candle |
| BoxChart | BoxChartBuilder | BoxStyler | BoxSeries | Number, Date, String | Box |
| HeatMapChart | HeatMapChartBuilder | HeatMapStyler | HeatMapSeries | Number, Date, String | -- |
| HorizontalBarChart | HorizontalBarChartBuilder | HorizontalBarStyler | HorizontalBarSeries | Number, Date, String | Bar |

The different Stylers contain chart styling methods specific to the corresponding chart type as well as common styling methods common across all chart types.

@@ -284,6 +286,16 @@ An example of a set of sequence numbers: 12, 15, 17, 19, 20, 23, 25, 28, 30, 33,

`HeatMapChart` take Date, Number or String data types for the X-Axis, Y-Axis.

### HorizontalBarChart

![](https://raw.githubusercontent.com/knowm/XChart/develop/etc/XChart_HorizontalBarChart.png)

`HorizontalBarChart` charts take Date, Number or String data types for the Y-Axis and Number data types for the X-Axis. For the Y-Axis, each category is given its own tick mark.

It supports `labels` and `tooltips`, but more advanced features like `error bars` or `stacking` are not yet implemented.

Series render style is `Bar`.

## Real-time Java Charts using XChart

![](https://raw.githubusercontent.com/knowm/XChart/develop/etc/XChart_SimpleRealtime.gif)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.knowm.xchart.demo.charts.horizontalbar;

import org.knowm.xchart.HorizontalBarChart;
import org.knowm.xchart.HorizontalBarChartBuilder;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.demo.charts.ExampleChart;
import org.knowm.xchart.style.Styler.LegendPosition;

import java.util.Arrays;

/**
* Basic Horizontal Bar Chart
*
* <p>Demonstrates the following:
*
* <ul>
* <li>Integer categories as List
* <li>All positive values
* <li>Single series
* <li>Place legend at Inside-NW position
* <li>Bar Chart Annotations
*/
public class HorizontalBarChart01 implements ExampleChart<HorizontalBarChart> {

public static void main(String[] args) {

ExampleChart<HorizontalBarChart> exampleChart = new HorizontalBarChart01();
HorizontalBarChart chart = exampleChart.getChart();
new SwingWrapper<>(chart).displayChart();
}

@Override
public HorizontalBarChart getChart() {

// Create Chart
HorizontalBarChart chart =
new HorizontalBarChartBuilder()
.width(800)
.height(600)
.title(getClass().getSimpleName())
.yAxisTitle("Score")
.xAxisTitle("Number")
.build();

// Customize Chart
chart.getStyler().setLegendPosition(LegendPosition.InsideNW);
chart.getStyler().setLabelsVisible(false);
chart.getStyler().setPlotGridLinesVisible(false);

// Series
chart.addSeries("test 1", Arrays.asList(4, 5, 9, 6, 5), Arrays.asList(0, 1, 2, 3, 4));

return chart;
}

@Override
public String getExampleChartName() {

return getClass().getSimpleName() + " - Basic Horizontal Bar Chart";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.knowm.xchart.demo.charts.horizontalbar;

import org.knowm.xchart.HorizontalBarChart;
import org.knowm.xchart.HorizontalBarChartBuilder;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.demo.charts.ExampleChart;
import org.knowm.xchart.style.Styler.ChartTheme;

import java.awt.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

/**
* Date Categories
*
* <p>Demonstrates the following:
*
* <ul>
* <li>Date categories as List
* <li>All negative values
* <li>Single series
* <li>No horizontal plot gridlines
* <li>Change series color
* <li>MATLAB Theme
*/
public class HorizontalBarChart02 implements ExampleChart<HorizontalBarChart> {

public static void main(String[] args) {

ExampleChart<HorizontalBarChart> exampleChart = new HorizontalBarChart02();
HorizontalBarChart chart = exampleChart.getChart();
new SwingWrapper<>(chart).displayChart();
}

@Override
public HorizontalBarChart getChart() {

// Create Chart
HorizontalBarChart chart =
new HorizontalBarChartBuilder()
.theme(ChartTheme.Matlab)
.width(800)
.height(600)
.title(getClass().getSimpleName())
.yAxisTitle("Year")
.xAxisTitle("Units Sold")
.build();

// Customize Chart
chart.getStyler().setPlotGridLinesVisible(false);
chart.getStyler().setDatePattern("yyyy");

// Series
List<Date> yData = new ArrayList<Date>();
List<Number> xData = new ArrayList<Number>();

Random random = new Random();
DateFormat sdf = new SimpleDateFormat("yyyy");
Date date = null;
for (int i = 1; i <= 8; i++) {
try {
date = sdf.parse("" + (2000 + i));
} catch (ParseException e) {
e.printStackTrace();
}
yData.add(date);
xData.add(-1 * 0.00000001 * ((random.nextInt(i) + 1)));
}
HorizontalBarSeries series = chart.addSeries("Model 77", xData, yData);
series.setFillColor(new Color(230, 150, 150));

return chart;
}

@Override
public String getExampleChartName() {
return getClass().getSimpleName() + " - Date Categories";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.knowm.xchart.demo.charts.horizontalbar;

import org.knowm.xchart.HorizontalBarChart;
import org.knowm.xchart.HorizontalBarChartBuilder;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.demo.charts.ExampleChart;
import org.knowm.xchart.style.Styler.ChartTheme;

import java.util.ArrayList;
import java.util.Arrays;

/**
* GGPlot2 Theme Horizontal Bar chart
*
* <p>Demonstrates the following:
*
* <ul>
* <li>String categories
* <li>Positive and negative values
* <li>Multiple series
*/
public class HorizontalBarChart03 implements ExampleChart<HorizontalBarChart> {

public static void main(String[] args) {

ExampleChart<HorizontalBarChart> exampleChart = new HorizontalBarChart03();
HorizontalBarChart chart = exampleChart.getChart();
new SwingWrapper<>(chart).displayChart();
}

@Override
public HorizontalBarChart getChart() {

// Create Chart
HorizontalBarChart chart =
new HorizontalBarChartBuilder()
.width(800)
.height(600)
.title(getClass().getSimpleName())
.yAxisTitle("Color")
.xAxisTitle("Temperature")
.theme(ChartTheme.GGPlot2)
.build();

// Customize Chart
chart.getStyler().setPlotGridVerticalLinesVisible(false);

// Series
chart.addSeries(
"fish",
new ArrayList<Number>(Arrays.asList(new Number[]{-40, 30, 20, 60, 60})),
new ArrayList<>(Arrays.asList(new String[]{"Blue", "Red", "Green", "Yellow", "Orange"})));
chart.addSeries(
"worms",
new ArrayList<Number>(Arrays.asList(new Number[]{50, 10, -20, 40, 60})),
new ArrayList<>(Arrays.asList(new String[]{"Blue", "Red", "Green", "Yellow", "Orange"})));
chart.addSeries(
"birds",
new ArrayList<Number>(Arrays.asList(new Number[]{13, 22, -23, -34, 37})),
new ArrayList<>(Arrays.asList(new String[]{"Blue", "Red", "Green", "Yellow", "Orange"})));
chart.addSeries(
"ants",
new ArrayList<Number>(Arrays.asList(new Number[]{50, 57, -14, -20, 31})),
new ArrayList<>(Arrays.asList(new String[]{"Blue", "Red", "Green", "Yellow", "Orange"})));
chart.addSeries(
"slugs",
new ArrayList<Number>(Arrays.asList(new Number[]{-2, 29, 49, -16, -43})),
new ArrayList<>(Arrays.asList(new String[]{"Blue", "Red", "Green", "Yellow", "Orange"})));

return chart;
}

@Override
public String getExampleChartName() {

return getClass().getSimpleName() + " - GGPlot2 Theme";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.knowm.xchart.demo.charts.horizontalbar;

import org.knowm.xchart.HorizontalBarChart;
import org.knowm.xchart.HorizontalBarChartBuilder;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.demo.charts.ExampleChart;
import org.knowm.xchart.style.Styler;

import java.util.Arrays;

/**
* Multiple series, labels and tooltips
*
* <p>Demonstrates the following:
*
* <ul>
* <li>Number categories
* <li>Positive values
* <li>Multiple series
* <li>Missing point in series
* <li>Labels
* <li>Bar Chart Annotations
* <li>Horizontal Legend OutsideS
*/
public class HorizontalBarChart04 implements ExampleChart<HorizontalBarChart> {

public static void main(String[] args) {

ExampleChart<HorizontalBarChart> exampleChart = new HorizontalBarChart04();
HorizontalBarChart chart = exampleChart.getChart();
new SwingWrapper<>(chart).displayChart();
}

@Override
public HorizontalBarChart getChart() {

// Create Chart
HorizontalBarChart chart =
new HorizontalBarChartBuilder()
.width(800)
.height(600)
.title(getClass().getSimpleName())
.yAxisTitle("Age")
.xAxisTitle("XFactor")
.build();

// Customize Chart
chart.getStyler().setLabelsVisible(true);
chart.getStyler().setToolTipsEnabled(true);
chart.getStyler().setPlotGridVerticalLinesVisible(false);
chart.getStyler().setLegendPosition(Styler.LegendPosition.OutsideS);
chart.getStyler().setLegendLayout(Styler.LegendLayout.Horizontal);

// Series
chart.addSeries("female", Arrays.asList(50, 10, 20, 40, 35), Arrays.asList(10, 20, 30, 40, 50));
chart.addSeries("male", Arrays.asList(40, 30, 20, null, 60), Arrays.asList(10, 20, 30, 40, 50));

return chart;
}

@Override
public String getExampleChartName() {

return getClass().getSimpleName() + " - Multiple series, labels and tooltips";
}
}
247 changes: 247 additions & 0 deletions xchart/src/main/java/org/knowm/xchart/HorizontalBarChart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package org.knowm.xchart;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.knowm.xchart.internal.Utils;
import org.knowm.xchart.internal.chartpart.AxisPair;
import org.knowm.xchart.internal.chartpart.Chart;
import org.knowm.xchart.internal.chartpart.Legend_HorizontalBar;
import org.knowm.xchart.internal.chartpart.Plot_HorizontalBar;
import org.knowm.xchart.internal.style.SeriesColorMarkerLineStyle;
import org.knowm.xchart.internal.style.SeriesColorMarkerLineStyleCycler;
import org.knowm.xchart.style.HorizontalBarStyler;
import org.knowm.xchart.style.Styler.ChartTheme;
import org.knowm.xchart.style.theme.Theme;

public class HorizontalBarChart extends Chart<HorizontalBarStyler, HorizontalBarSeries> {

/**
* Constructor - the default Chart Theme will be used (XChartTheme)
*
* @param width
* @param height
*/
public HorizontalBarChart(int width, int height) {

super(width, height, new HorizontalBarStyler());
axisPair = new AxisPair<HorizontalBarStyler, HorizontalBarSeries>(this);
plot = new Plot_HorizontalBar<HorizontalBarStyler, HorizontalBarSeries>(this);
legend = new Legend_HorizontalBar<HorizontalBarStyler, HorizontalBarSeries>(this);
}

/**
* Constructor
*
* @param width
* @param height
* @param theme - pass in a instance of Theme class, probably a custom Theme.
*/
public HorizontalBarChart(int width, int height, Theme theme) {

this(width, height);
styler.setTheme(theme);
}

/**
* Constructor
*
* @param width
* @param height
* @param chartTheme - pass in the desired ChartTheme enum
*/
public HorizontalBarChart(int width, int height, ChartTheme chartTheme) {

this(width, height, chartTheme.newInstance(chartTheme));
}

/**
* Constructor
*
* @param chartBuilder
*/
public HorizontalBarChart(HorizontalBarChartBuilder chartBuilder) {

this(chartBuilder.width, chartBuilder.height, chartBuilder.chartTheme);
setTitle(chartBuilder.title);
setXAxisTitle(chartBuilder.xAxisTitle);
setYAxisTitle(chartBuilder.yAxisTitle);
}

/**
* Add a series for a horizontal bar type chart using double arrays with error bars
*
* @param seriesName
* @param xData the X-Axis data
* @param yData the Y-Axis data
* @return A Series object that you can set properties on
*/
public HorizontalBarSeries addSeries(String seriesName, double[] xData, double[] yData) {

return addSeries(
seriesName,
Utils.getNumberListFromDoubleArray(xData),
Utils.getNumberListFromDoubleArray(yData));
}

/**
* Add a series for a horizontal bar type chart using int arrays with error bars
*
* @param seriesName
* @param xData the X-Axis data
* @param yData the Y-Axis data
* @return A Series object that you can set properties on
*/
public HorizontalBarSeries addSeries(String seriesName, int[] xData, int[] yData) {

return addSeries(
seriesName, Utils.getNumberListFromIntArray(xData), Utils.getNumberListFromIntArray(yData));
}

/**
* Add a series for a horizontal bar type chart using Lists
*
* @param seriesName
* @param xData the X-Axis data
* @param yData the Y-Axis data
* @return A Series object that you can set properties on
*/
public HorizontalBarSeries addSeries(
String seriesName, List<? extends Number> xData, List<?> yData) {

// Sanity checks
sanityCheck(seriesName, xData, yData);

HorizontalBarSeries series;
if (xData != null) {

// Sanity check
if (xData.size() != yData.size()) {
throw new IllegalArgumentException("X and Y-Axis sizes are not the same!!!");
}

} else { // generate xData
xData = Utils.getGeneratedDataAsList(yData.size());
}
series = new HorizontalBarSeries(seriesName, xData, yData);

seriesMap.put(seriesName, series);

return series;
}

/**
* Update a series by updating the X-Axis and Y-Axis
*
* @param seriesName
* @param newXData
* @param newYData - set null to be automatically generated as a list of increasing Integers
* starting from 1 and ending at the size of the new X-Axis data list.
* @return
*/
public HorizontalBarSeries updateCategorySeries(
String seriesName, List<? extends Number> newXData, List<?> newYData) {

Map<String, HorizontalBarSeries> seriesMap = getSeriesMap();
HorizontalBarSeries series = seriesMap.get(seriesName);
if (series == null) {
throw new IllegalArgumentException("Series name >" + seriesName + "< not found!!!");
}
if (newYData == null) {
// generate Y-Data
List<Integer> generatedYData = new ArrayList<Integer>();
for (int i = 1; i <= newXData.size(); i++) {
generatedYData.add(i);
}
series.replaceData(newXData, generatedYData);
} else {
series.replaceData(newXData, newYData);
}

return series;
}

/**
* Update a series by updating the X-Axis, Y-Axis and error bar data
*
* @param seriesName
* @param newXData
* @param newYData - set null to be automatically generated as a list of increasing Integers
* starting from 1 and ending at the size of the new X-Axis data list.
* @return
*/
public HorizontalBarSeries updateCategorySeries(
String seriesName, double[] newXData, double[] newYData) {

return updateCategorySeries(
seriesName,
Utils.getNumberListFromDoubleArray(newXData),
Utils.getNumberListFromDoubleArray(newYData));
}

///////////////////////////////////////////////////
// Internal Members and Methods ///////////////////
///////////////////////////////////////////////////

private void sanityCheck(String seriesName, List<? extends Number> xData, List<?> yData) {

if (seriesMap.containsKey(seriesName)) {
throw new IllegalArgumentException(
"Series name >"
+ seriesName
+ "< has already been used. Use unique names for each series!!!");
}
if (yData == null) {
throw new IllegalArgumentException("Y-Axis data cannot be null!!!");
}
if (yData.size() == 0) {
throw new IllegalArgumentException("Y-Axis data cannot be empty!!!");
}
if (xData != null && xData.size() == 0) {
throw new IllegalArgumentException("X-Axis data cannot be empty!!!");
}
}

@Override
public void paint(Graphics2D g, int width, int height) {

setWidth(width);
setHeight(height);

setSeriesStyles();

paintBackground(g);

axisPair.paint(g);
plot.paint(g);
chartTitle.paint(g);
legend.paint(g);
annotations.forEach(x -> x.paint(g));
}

/** set the series color, marker and line style based on theme */
private void setSeriesStyles() {

SeriesColorMarkerLineStyleCycler seriesColorMarkerLineStyleCycler =
new SeriesColorMarkerLineStyleCycler(
getStyler().getSeriesColors(),
getStyler().getSeriesMarkers(),
getStyler().getSeriesLines());
for (HorizontalBarSeries series : getSeriesMap().values()) {

SeriesColorMarkerLineStyle seriesColorMarkerLineStyle =
seriesColorMarkerLineStyleCycler.getNextSeriesColorMarkerLineStyle();

if (series.getLineStyle() == null) { // wasn't set manually
series.setLineStyle(seriesColorMarkerLineStyle.getStroke());
}
if (series.getLineColor() == null) { // wasn't set manually
series.setLineColor(seriesColorMarkerLineStyle.getColor());
}
if (series.getFillColor() == null) { // wasn't set manually
series.setFillColor(seriesColorMarkerLineStyle.getColor());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.knowm.xchart;

import org.knowm.xchart.internal.ChartBuilder;

public class HorizontalBarChartBuilder
extends ChartBuilder<HorizontalBarChartBuilder, HorizontalBarChart> {

String xAxisTitle = "";
String yAxisTitle = "";

public HorizontalBarChartBuilder() {}

public HorizontalBarChartBuilder xAxisTitle(String xAxisTitle) {

this.xAxisTitle = xAxisTitle;
return this;
}

public HorizontalBarChartBuilder yAxisTitle(String yAxisTitle) {

this.yAxisTitle = yAxisTitle;
return this;
}

/**
* return fully built Chart_Category
*
* @return a CategoryChart
*/
@Override
public HorizontalBarChart build() {

return new HorizontalBarChart(this);
}
}
121 changes: 121 additions & 0 deletions xchart/src/main/java/org/knowm/xchart/HorizontalBarSeries.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.knowm.xchart;

import java.util.*;
import org.knowm.xchart.internal.chartpart.RenderableSeries.LegendRenderType;
import org.knowm.xchart.internal.series.AxesChartSeries;

/** A Series containing horizontal bar data to be plotted on a Chart */
public class HorizontalBarSeries extends AxesChartSeries {

List<? extends Number> xData;

List<?> yData;

/**
* Constructor
*
* @param name
* @param xData
* @param yData
*/
public HorizontalBarSeries(String name, List<? extends Number> xData, List<?> yData) {

super(name, getDataType(xData), getDataType(yData));
this.xData = xData;
this.yData = yData;
calculateMinMax();
}

public void replaceData(List<? extends Number> xData, List<?> yData) {

this.xData = xData;
this.yData = yData;
calculateMinMax();
}

@Override
protected void calculateMinMax() {

// xData
double[] xMinMax = findMinMax(xData, getxAxisDataType());
xMin = xMinMax[0];
xMax = xMinMax[1];
// System.out.println(xMin);
// System.out.println(xMax);

// yData
double[] yMinMax;
yMinMax = findMinMax(yData, getyAxisDataType());
yMin = yMinMax[0];
yMax = yMinMax[1];
// System.out.println(yMin);
// System.out.println(yMax);
}

double[] findMinMax(Collection<?> data, DataType dataType) {

double min = Double.MAX_VALUE;
double max = -Double.MAX_VALUE;

for (Object dataPoint : data) {

if (dataPoint == null) {
continue;
}

double value = 0.0;

if (dataType == DataType.Number) {
value = ((Number) dataPoint).doubleValue();
} else if (dataType == DataType.Date) {
Date date = (Date) dataPoint;
value = date.getTime();
} else if (dataType == DataType.String) {
return new double[] {Double.NaN, Double.NaN};
}
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}

return new double[] {min, max};
}

@Override
public LegendRenderType getLegendRenderType() {

return null;
}

private static DataType getDataType(List<?> data) {

DataType axisType;

Iterator<?> itr = data.iterator();
Object dataPoint = itr.next();
if (dataPoint instanceof Number) {
axisType = DataType.Number;
} else if (dataPoint instanceof Date) {
axisType = DataType.Date;
} else if (dataPoint instanceof String) {
axisType = DataType.String;
} else {
throw new IllegalArgumentException(
"Series data must be either Number, Date or String type!!!");
}
return axisType;
}

public List<? extends Number> getXData() {

return xData;
}

public List<?> getYData() {

return yData;
}
}
66 changes: 48 additions & 18 deletions xchart/src/main/java/org/knowm/xchart/internal/chartpart/Axis.java
Original file line number Diff line number Diff line change
@@ -7,30 +7,18 @@
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.knowm.xchart.CategoryChart;
import org.knowm.xchart.CategorySeries;
import org.knowm.xchart.HeatMapChart;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYSeries;
import org.knowm.xchart.*;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.internal.Utils;
import org.knowm.xchart.internal.series.AxesChartSeries;
import org.knowm.xchart.internal.series.AxesChartSeriesCategory;
import org.knowm.xchart.internal.series.Series;
import org.knowm.xchart.internal.series.Series.DataType;
import org.knowm.xchart.style.AxesChartStyler;
import org.knowm.xchart.style.BoxStyler;
import org.knowm.xchart.style.CategoryStyler;
import org.knowm.xchart.style.HeatMapStyler;
import org.knowm.xchart.style.*;
import org.knowm.xchart.style.Styler.LegendPosition;
import org.knowm.xchart.style.Styler.YAxisPosition;
import org.knowm.xchart.style.XYStyler;

/** Axis */
public class Axis<ST extends AxesChartStyler, S extends AxesChartSeries> implements ChartPart {
@@ -395,7 +383,21 @@ private AxisTickCalculator getAxisTickCalculator(double workingSpace) {

private AxisTickCalculator getAxisTickCalculatorForY(double workingSpace) {
List<Double> yData = new ArrayList<>();
if (axesChartStyler instanceof HeatMapStyler) {
if (axesChartStyler instanceof HorizontalBarStyler) {
Set<Double> uniqueYData = new LinkedHashSet<>();
for (HorizontalBarSeries categorySeries :
((HorizontalBarChart) chart).getSeriesMap().values()) {
uniqueYData.addAll(
categorySeries.getYData().stream()
.filter(Objects::nonNull)
.filter(it -> it instanceof Number)
.mapToDouble(it -> ((Number) it).doubleValue())
.boxed()
.collect(Collectors.toList()));
}
yData.addAll(uniqueYData);
Collections.reverse(yData);
} else if (axesChartStyler instanceof HeatMapStyler) {
List<?> categories = ((HeatMapChart) chart).getHeatMapSeries().getYData();
yData =
categories.stream()
@@ -446,6 +448,18 @@ private AxisTickCalculator getAxisTickCalculatorForY(double workingSpace) {

return new AxisTickCalculator_Logarithmic(
getDirection(), workingSpace, min, max, axesChartStyler, getYIndex());
} else if (axesChartStyler instanceof HorizontalBarStyler) {
List<?> categories =
((HorizontalBarChart) chart)
.getSeriesMap().values().stream()
.flatMap(it -> it.getYData().stream())
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
Collections.reverse(categories);
DataType axisType = chart.getAxisPair().getYAxis().getDataType();

return new AxisTickCalculator_Category(
getDirection(), workingSpace, categories, axisType, axesChartStyler);
} else if (axesChartStyler instanceof HeatMapStyler) {

List<?> categories = ((HeatMapChart) chart).getHeatMapSeries().getYData();
@@ -465,7 +479,20 @@ private AxisTickCalculator getAxisTickCalculatorForY(double workingSpace) {

private AxisTickCalculator_ getAxisTickCalculatorForX(double workingSpace) {
List<Double> xData = new ArrayList<>();
if (axesChartStyler instanceof HeatMapStyler) {
if (axesChartStyler instanceof HorizontalBarStyler) {
Set<Double> uniqueXData = new LinkedHashSet<>();
for (HorizontalBarSeries categorySeries :
((HorizontalBarChart) chart).getSeriesMap().values()) {
List<Double> numericCategoryXData =
categorySeries.getXData().stream()
.filter(Objects::nonNull)
.mapToDouble(Number::doubleValue)
.boxed()
.collect(Collectors.toList());
uniqueXData.addAll(numericCategoryXData);
}
xData.addAll(uniqueXData);
} else if (axesChartStyler instanceof HeatMapStyler) {
List<?> categories = ((HeatMapChart) chart).getHeatMapSeries().getXData();
xData =
categories.stream()
@@ -514,6 +541,9 @@ private AxisTickCalculator_ getAxisTickCalculatorForX(double workingSpace) {
max,
axesChartStyler);

} else if (axesChartStyler instanceof HorizontalBarStyler) {
return new AxisTickCalculator_Number(
getDirection(), workingSpace, min, max, xData, axesChartStyler);
} else if (axesChartStyler instanceof CategoryStyler || axesChartStyler instanceof BoxStyler) {

// TODO Cleanup? More elegant way?
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
import org.knowm.xchart.style.AxesChartStyler;
import org.knowm.xchart.style.BoxStyler;
import org.knowm.xchart.style.CategoryStyler;
import org.knowm.xchart.style.HorizontalBarStyler;
import org.knowm.xchart.style.Styler.LegendPosition;
import org.knowm.xchart.style.Styler.YAxisPosition;

@@ -375,6 +376,16 @@ private void overrideMinMaxForXAxis() {

double overrideXAxisMinValue = xAxis.getMin();
double overrideXAxisMaxValue = xAxis.getMax();

if (chart.getStyler() instanceof HorizontalBarStyler) {
if (xAxis.getMin() > 0.0) {
overrideXAxisMinValue = 0.0;
}
if (xAxis.getMax() < 0.0) {
overrideXAxisMaxValue = 0.0;
}
}

// override min and maxValue if specified
if (chart.getStyler().getXAxisMin() != null) {

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.knowm.xchart.internal.chartpart;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.Map;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.internal.chartpart.RenderableSeries.LegendRenderType;
import org.knowm.xchart.style.Styler;

public class Legend_HorizontalBar<ST extends Styler, S extends HorizontalBarSeries>
extends Legend_<ST, S> {

private final ST axesChartStyler;

/**
* Constructor
*
* @param chart
*/
public Legend_HorizontalBar(Chart<ST, S> chart) {

super(chart);
axesChartStyler = chart.getStyler();
}

@Override
public void doPaint(Graphics2D g) {

// Draw legend content inside legend box
double startx = xOffset + chart.getStyler().getLegendPadding();
double starty = yOffset + chart.getStyler().getLegendPadding();

Object oldHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

Map<String, S> map = chart.getSeriesMap();
for (S series : map.values()) {

if (!series.isShowInLegend()) {
continue;
}
if (!series.isEnabled()) {
continue;
}

Map<String, Rectangle2D> seriesTextBounds = getSeriesTextBounds(series);
float legendEntryHeight = getLegendEntryHeight(seriesTextBounds, BOX_SIZE);

// paint line and marker

// paint inner box
Shape rectSmall = new Rectangle2D.Double(startx, starty, BOX_SIZE, BOX_SIZE);
g.setColor(series.getFillColor());
g.fill(rectSmall);

// paint series text
double x = startx + BOX_SIZE + chart.getStyler().getLegendPadding();
paintSeriesText(g, seriesTextBounds, BOX_SIZE, x, starty);

if (chart.getStyler().getLegendLayout() == Styler.LegendLayout.Vertical) {
starty += legendEntryHeight + chart.getStyler().getLegendPadding();
} else {
int markerWidth = BOX_SIZE;
if (series.getLegendRenderType() == LegendRenderType.Line) {
markerWidth = chart.getStyler().getLegendSeriesLineLength();
}
float legendEntryWidth = getLegendEntryWidth(seriesTextBounds, markerWidth);
startx += legendEntryWidth + chart.getStyler().getLegendPadding();
}
}
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHint);
}

@Override
public double getSeriesLegendRenderGraphicHeight(S series) {

return BOX_SIZE;
}
}
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
public abstract class PlotContent_<ST extends Styler, S extends Series> implements ChartPart {

final Chart<ST, S> chart;
ToolTips toolTips; // tooltips are available for Category, OHLC and XY charts
ToolTips toolTips; // tooltips are available for Category, HorizontalBar, OHLC and XY charts
ChartZoom chartZoom;
// Cursor cursor;

Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package org.knowm.xchart.internal.chartpart;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.*;
import java.util.*;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.internal.Utils;
import org.knowm.xchart.style.HorizontalBarStyler;

public class PlotContent_HorizontalBar<
ST extends HorizontalBarStyler, S extends HorizontalBarSeries>
extends PlotContent_<ST, S> {

private final ST styler;

/**
* Constructor
*
* @param chart
*/
PlotContent_HorizontalBar(Chart<ST, S> chart) {

super(chart);
this.styler = chart.getStyler();
}

@Override
public void doPaint(Graphics2D g) {

// X-Axis
double yTickSpace = styler.getPlotContentSize() * getBounds().getHeight();
// System.out.println("xTickSpace: " + xTickSpace);
double yVerticalMargin = Utils.getTickStartOffset(getBounds().getHeight(), yTickSpace);
// System.out.println("xLeftMargin: " + xLeftMargin);
Map<String, S> seriesMap = chart.getSeriesMap();
int numCategories = seriesMap.values().iterator().next().getYData().size();
double gridStep = yTickSpace / numCategories;
// System.out.println("gridStep: " + gridStep);

// Y-Axis
double xMin = chart.getXAxis().getMin();
double xMax = chart.getXAxis().getMax();

// figure out the general form of the chart
final int chartForm; // 1=positive, -1=negative, 0=span
if (xMin > 0.0 && xMax > 0.0) {
chartForm = 1; // positive chart
} else if (xMin < 0.0 && xMax < 0.0) {
chartForm = -1; // negative chart
} else {
chartForm = 0; // span chart
}
// System.out.println(xMin);
// System.out.println(xMax);
// System.out.println("chartForm: " + chartForm);

double xTickSpace = styler.getPlotContentSize() * getBounds().getWidth();

double xHorizontalMargin = Utils.getTickStartOffset(getBounds().getWidth(), xTickSpace);

// plot series
int seriesCounter = 0;

for (S series : seriesMap.values()) {

if (!series.isEnabled()) {
continue;
}

xMin = chart.getXAxis().getMin();
xMax = chart.getXAxis().getMax();
if (styler.isXAxisLogarithmic()) {
xMin = Math.log10(xMin);
xMax = Math.log10(xMax);
}

Iterator<? extends Number> xItr = series.getXData().iterator();
Iterator<?> yItr = series.getYData().iterator();

int categoryCounter = 0;
while (xItr.hasNext()) {

Number next = xItr.next();
Object nextCat = yItr.next();
// skip when a value is null
if (next == null) {

categoryCounter++;
continue;
}

double xOrig = next.doubleValue();
double x;
if (styler.isXAxisLogarithmic()) {
x = Math.log10(xOrig);
} else {
x = xOrig;
}

double xRight = 0.0;
double xLeft = 0.0;
switch (chartForm) {
case 1: // positive chart
// check for points off the chart draw area due to a custom yMin
if (x < xMin) {
categoryCounter++;
continue;
}
xRight = x;
xLeft = xMin;
break;

case -1: // negative chart
// check for points off the chart draw area due to a custom yMin
if (x > xMax) {
categoryCounter++;
continue;
}
xRight = xMax;
xLeft = x;
break;
case 0: // span chart
if (x >= 0.0) { // positive
xRight = x;
xLeft = 0.0;
} else {
xRight = 0.0;
xLeft = x;
}
break;
default:
break;
}

double xTransform = (xHorizontalMargin + (xRight - xMin) / (xMax - xMin) * xTickSpace);
double xOffset = getBounds().getX() + xTransform;

double zeroTransform = (xHorizontalMargin + (xLeft - xMin) / (xMax - xMin) * xTickSpace);
double zeroOffset = getBounds().getX() + zeroTransform;
double yOffset;
double barHeight;

{
double barHeightPercentage = styler.getAvailableSpaceFill();
barHeight = gridStep / chart.getSeriesMap().size() * barHeightPercentage;
double barMargin = gridStep * (1 - barHeightPercentage) / 2;
yOffset =
getBounds().getY()
+ yVerticalMargin
+ gridStep * categoryCounter++
+ seriesCounter * barHeight
+ barMargin;
}

// paint series
// paint bar
Path2D.Double barPath = new Path2D.Double();
barPath.moveTo(zeroOffset, yOffset);
barPath.lineTo(xOffset, yOffset);
barPath.lineTo(xOffset, yOffset + barHeight);
barPath.lineTo(zeroOffset, yOffset + barHeight);
barPath.closePath();

g.setColor(series.getFillColor());
g.fill(barPath);

if (styler.isLabelsVisible() && next != null) {
drawLabels(g, next, xOffset, yOffset, zeroOffset, barHeight, series.getFillColor());
}

// add data labels
if (chart.getStyler().isToolTipsEnabled()) {
Rectangle2D.Double rect =
new Rectangle2D.Double(
zeroOffset, yOffset, Math.abs(xOffset - zeroOffset), barHeight);
double xPoint;
if (x < 0) {
xPoint = -zeroOffset;
} else {
xPoint = xOffset;
}

toolTips.addData(
rect,
xPoint,
yOffset,
barHeight,
chart.getXAxisFormat().format(xOrig),
chart.getYAxisFormat().format(nextCat));
}
}

seriesCounter++;
}
}

private void drawLabels(
Graphics2D g,
Number next,
double xOffset,
double yOffset,
double zeroOffset,
double barHeight,
Color seriesColor) {

String numberAsString = chart.getXAxisFormat().format(next);

TextLayout textLayout =
new TextLayout(
numberAsString, styler.getLabelsFont(), new FontRenderContext(null, true, false));

AffineTransform rot =
AffineTransform.getRotateInstance(-1 * Math.toRadians(styler.getLabelsRotation()), 0, 0);
Shape shape = textLayout.getOutline(rot);
Rectangle2D labelRectangle = textLayout.getBounds();

double labelY;
if (styler.getLabelsRotation() > 0) {
double labelYDelta = labelRectangle.getWidth() / 2 - labelRectangle.getHeight() / 2;
double rotationOffset = labelYDelta * styler.getLabelsRotation() / 90;
labelY = yOffset + barHeight / 2 + labelRectangle.getHeight() / 2 + rotationOffset + 1;
} else {
labelY = yOffset + barHeight / 2 + labelRectangle.getHeight() / 2;
}
double labelX;

if (next.doubleValue() >= 0.0) {
labelX =
xOffset
+ (zeroOffset - xOffset) * (1 - styler.getLabelsPosition())
- labelRectangle.getWidth() * styler.getLabelsPosition();
} else {
labelX =
zeroOffset
- (zeroOffset - xOffset) * (1 - styler.getLabelsPosition())
- labelRectangle.getWidth() * (1 - styler.getLabelsPosition());
}

if (styler.isLabelsFontColorAutomaticEnabled()) {
g.setColor(styler.getLabelsFontColor(seriesColor));
} else {
g.setColor(styler.getLabelsFontColor());
}

g.setFont(styler.getLabelsFont());
AffineTransform orig = g.getTransform();
AffineTransform at = new AffineTransform();
at.translate(labelX, labelY);
g.transform(at);
g.fill(shape);
g.setTransform(orig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.knowm.xchart.internal.chartpart;

import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.style.HorizontalBarStyler;

public class Plot_HorizontalBar<ST extends HorizontalBarStyler, S extends HorizontalBarSeries>
extends Plot_AxesChart<ST, S> {

/**
* Constructor
*
* @param chart
*/
public Plot_HorizontalBar(Chart<ST, S> chart) {

super(chart);
this.plotContent = new PlotContent_HorizontalBar<ST, S>(chart);
}
}
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
import java.util.List;
import java.util.Objects;
import org.knowm.xchart.style.BoxStyler;
import org.knowm.xchart.style.HorizontalBarStyler;
import org.knowm.xchart.style.OHLCStyler;
import org.knowm.xchart.style.Styler;

@@ -169,8 +170,15 @@ private void paintToolTip(Graphics2D g, ToolTip tooltip) {
// System.out.println("topEdge = " + topEdge);
// System.out.println("bottomEdge = " + bottomEdge);

double x = tooltip.x + tooltip.w / 2 - annotationRectangle.getWidth() / 2 - MARGIN;
double y = tooltip.y - 3 * MARGIN - annotationRectangle.getHeight();
double x;
double y;
if (styler instanceof HorizontalBarStyler) {
x = tooltip.x < 0 ? -tooltip.x - w - MARGIN : tooltip.x + MARGIN;
y = tooltip.y + tooltip.w / 2 - h / 2;
} else {
x = tooltip.x + tooltip.w / 2 - annotationRectangle.getWidth() / 2 - MARGIN;
y = tooltip.y - 3 * MARGIN - annotationRectangle.getHeight();
}
// System.out.println("x = " + x);
// System.out.println("y = " + y);
// x = Math.min(x, -w);
188 changes: 188 additions & 0 deletions xchart/src/main/java/org/knowm/xchart/style/HorizontalBarStyler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package org.knowm.xchart.style;

import java.awt.*;
import org.knowm.xchart.style.colors.FontColorDetector;
import org.knowm.xchart.style.theme.Theme;

public class HorizontalBarStyler extends AxesChartStyler {

private double availableSpaceFill;

// labels //////////////////////
private boolean isLabelsVisible = false;
private Font labelsFont;
private Color labelsFontColor;
private int labelsRotation;
private double labelsPosition;
private boolean isLabelsFontColorAutomaticEnabled;
private Color labelsFontColorAutomaticLight;
private Color labelsFontColorAutomaticDark;

/** Constructor */
public HorizontalBarStyler() {

setAllStyles();
}

@Override
protected void setAllStyles() {

super.setAllStyles();

availableSpaceFill = theme.getAvailableSpaceFill();
isLabelsVisible = false;
labelsFont = theme.getBaseFont();
labelsFontColor = theme.getChartFontColor();
labelsRotation = 0;
labelsPosition = 0.5;
isLabelsFontColorAutomaticEnabled = theme.isLabelsFontColorAutomaticEnabled();
labelsFontColorAutomaticLight = theme.getLabelsFontColorAutomaticLight();
labelsFontColorAutomaticDark = theme.getLabelsFontColorAutomaticDark();
}

public double getAvailableSpaceFill() {

return availableSpaceFill;
}

/**
* Sets the available space for rendering each category as a percentage. For a bar chart with one
* series, it will be the width of the bar as a percentage of the maximum space alloted for the
* bar. If there are three series and three bars, the three bars will share the available space.
* This affects all category series render types, not only bar charts. Full width is 100%, i.e.
* 1.0
*
* @param availableSpaceFill
*/
public HorizontalBarStyler setAvailableSpaceFill(double availableSpaceFill) {

this.availableSpaceFill = availableSpaceFill;
return this;
}

public boolean isLabelsVisible() {

return isLabelsVisible;
}

/**
* Sets if labels should be added to charts. Each chart type has a different annotation type
*
* @param labelsVisible
*/
public HorizontalBarStyler setLabelsVisible(boolean labelsVisible) {

this.isLabelsVisible = labelsVisible;
return this;
}

public Font getLabelsFont() {

return labelsFont;
}

/**
* Sets the Font used for chart labels
*
* @param labelsFont
*/
public HorizontalBarStyler setLabelsFont(Font labelsFont) {

this.labelsFont = labelsFont;
return this;
}

public Color getLabelsFontColor() {
return labelsFontColor;
}

public Color getLabelsFontColor(Color backgroundColor) {

return FontColorDetector.getAutomaticFontColor(
backgroundColor, labelsFontColorAutomaticDark, labelsFontColorAutomaticLight);
}

/**
* Sets the color of the Font used for chart labels
*
* @param labelsFontColor
*/
public HorizontalBarStyler setLabelsFontColor(Color labelsFontColor) {
this.labelsFontColor = labelsFontColor;
return this;
}

public int getLabelsRotation() {
return labelsRotation;
}

/**
* Sets the rotation (in degrees) for chart labels.
*
* @param labelsRotation
*/
public HorizontalBarStyler setLabelsRotation(int labelsRotation) {
this.labelsRotation = labelsRotation;
return this;
}

public double getLabelsPosition() {

return labelsPosition;
}

/**
* A number between 0 and 1 setting the vertical position of the data label. Default is 0.5
* placing it in the center.
*
* @param labelsPosition
* @return
*/
public HorizontalBarStyler setLabelsPosition(double labelsPosition) {

if (labelsPosition < 0 || labelsPosition > 1) {
throw new IllegalArgumentException("Annotations position must between 0 and 1!!!");
}
this.labelsPosition = labelsPosition;
return this;
}

public boolean isLabelsFontColorAutomaticEnabled() {
return isLabelsFontColorAutomaticEnabled;
}

public HorizontalBarStyler setLabelsFontColorAutomaticEnabled(
boolean isLabelsFontColorAutomaticEnabled) {
this.isLabelsFontColorAutomaticEnabled = isLabelsFontColorAutomaticEnabled;
return this;
}

public Color getLabelsFontColorAutomaticLight() {
return labelsFontColorAutomaticLight;
}

public HorizontalBarStyler setLabelsFontColorAutomaticLight(Color labelsFontColorAutomaticLight) {
this.labelsFontColorAutomaticLight = labelsFontColorAutomaticLight;
return this;
}

public Color getLabelsFontColorAutomaticDark() {
return labelsFontColorAutomaticDark;
}

public HorizontalBarStyler setLabelsFontColorAutomaticDark(Color labelsFontColorAutomaticDark) {
this.labelsFontColorAutomaticDark = labelsFontColorAutomaticDark;
return this;
}

/**
* Set the theme the styler should use
*
* @param theme
*/
public void setTheme(Theme theme) {

this.theme = theme;
setAllStyles();
}
}