Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.knowm.xchart.demo.charts.bar;

import java.util.Arrays;
import org.knowm.xchart.CategoryChart;
import org.knowm.xchart.CategoryChartBuilder;
import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.demo.charts.ExampleChart;
import org.knowm.xchart.style.Styler.LegendPosition;

/**
* Bar Chart with Gradient Fills
*
* <p>Demonstrates the following:
*
* <ul>
* <li>Opt-in bar gradient from the series fill color at the base to a brighter color at the value
* <li>Positive and negative values, with the brighter end away from zero
* <li>Legend swatches use the positive-bar gradient
* </ul>
*/
public class BarChart14 implements ExampleChart<CategoryChart> {

public static void main(String[] args) {

ExampleChart<CategoryChart> exampleChart = new BarChart14();
CategoryChart chart = exampleChart.getChart();
new SwingWrapper<>(chart).displayChart();
}

@Override
public CategoryChart getChart() {

// Create Chart
CategoryChart chart =
new CategoryChartBuilder()
.width(800)
.height(600)
.title(getClass().getSimpleName())
.xAxisTitle("Category")
.yAxisTitle("Value")
.build();

// Customize Chart
chart.getStyler().setLegendPosition(LegendPosition.InsideNW);
chart.getStyler().setBarFillGradientEnabled(true);

// Series
chart.addSeries(
"high", Arrays.asList("A", "B", "C", "D", "E"), Arrays.asList(50, -20, 70, -40, 30));
chart.addSeries(
"low", Arrays.asList("A", "B", "C", "D", "E"), Arrays.asList(20, -35, 25, -15, 45));

return chart;
}

@Override
public String getExampleChartName() {

return getClass().getSimpleName() + " - Bar Chart with Gradient Fills";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.knowm.xchart.internal.chartpart;

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;

/**
* Shared two-color bar gradient used by the category and horizontal bar plots and their legends.
* The gradient runs from the series fill at the bar base to {@link #endColor(Color)} at the value
* end.
*/
final class BarGradient {

/** Share of white blended into the fill to get the value-end color. */
static final float END_COLOR_WHITE_BLEND = 0.4f;

private static final String VECTOR_GRAPHICS_PACKAGE = "de.erichseifert.vectorgraphics2d";

private BarGradient() {}

/**
* The value-end color: {@code fill} blended toward white, alpha kept. Unlike {@link
* Color#brighter()}, this also lightens saturated and black fills such as {@link Color#RED} or
* {@link Color#BLACK}. Only a white fill has no lighter end.
*/
static Color endColor(Color fill) {

return lerp(fill, Color.WHITE, END_COLOR_WHITE_BLEND);
}

/**
* Color of the gradient at {@code fraction} of the way from the base (0) to the value end (1),
* clamped to that range.
*/
static Color colorAt(Color fill, double fraction) {

double t = Math.max(0.0, Math.min(1.0, fraction));
return lerp(fill, endColor(fill), t);
}

/** True when the bar grows toward more negative transformed values. */
static boolean extendsNegative(int chartForm, double transformed) {

if (chartForm > 0) {
return false;
}
if (chartForm < 0) {
return true;
}
return transformed < 0.0;
}

/**
* Fills {@code shape} with {@code fill}, or with a non-cyclic gradient from {@code fill} at the
* base to {@link #endColor(Color)} at the value end. Coincident endpoints keep the flat fill, as
* do vector backends, whose writers fill from the last {@code setColor} and drop a {@link
* GradientPaint}. The paint in effect before a gradient fill is restored.
*/
static void fill(
Graphics2D g,
Shape shape,
Color fill,
boolean gradient,
double baseX,
double baseY,
double valueX,
double valueY) {

if (gradient && fill != null && !isVectorGraphics(g)) {
float x1 = (float) baseX;
float y1 = (float) baseY;
float x2 = (float) valueX;
float y2 = (float) valueY;
if (x1 != x2 || y1 != y2) {
Paint previousPaint = g.getPaint();
g.setPaint(new GradientPaint(x1, y1, fill, x2, y2, endColor(fill), false));
g.fill(shape);
g.setPaint(previousPaint);
return;
}
}
g.setColor(fill);
g.fill(shape);
}

static double clamp(double value, double min, double max) {

return Math.max(min, Math.min(max, value));
}

private static boolean isVectorGraphics(Graphics2D g) {

return g.getClass().getName().startsWith(VECTOR_GRAPHICS_PACKAGE);
}

private static Color lerp(Color from, Color to, double t) {

int r = (int) Math.round(from.getRed() + (to.getRed() - from.getRed()) * t);
int g = (int) Math.round(from.getGreen() + (to.getGreen() - from.getGreen()) * t);
int b = (int) Math.round(from.getBlue() + (to.getBlue() - from.getBlue()) * t);
return new Color(r, g, b, from.getAlpha());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.awt.geom.Rectangle2D;
import java.util.Map;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.style.HorizontalBarStyler;
import org.knowm.xchart.style.Styler;

public class Legend_HorizontalBar<ST extends Styler, S extends HorizontalBarSeries>
Expand Down Expand Up @@ -34,6 +35,11 @@ public void doPaint(Graphics2D g) {
: RenderingHints.VALUE_ANTIALIAS_OFF);

Map<String, S> map = chart.getSeriesMap();
boolean barFillGradient =
chart.getStyler() instanceof HorizontalBarStyler
&& ((HorizontalBarStyler) chart.getStyler()).isBarFillGradientEnabled();
// Match the bars: lighter at the right, or at the left when every bar grows leftward.
boolean allNegative = barFillGradient && allNegativeXAxis();

// In a horizontal legend, entries flow across shared rows and wrap to a new row when a row
// fills up (issue #577); in a vertical legend each entry gets its own row.
Expand Down Expand Up @@ -62,10 +68,19 @@ public void doPaint(Graphics2D g) {
starty = cursor.y;
}

// paint inner box
// paint inner box, with the bar gradient when enabled
Shape rectSmall = new Rectangle2D.Double(startx, starty, BOX_SIZE, BOX_SIZE);
g.setColor(series.getFillColor());
g.fill(rectSmall);
double left = startx;
double right = startx + BOX_SIZE;
BarGradient.fill(
g,
rectSmall,
series.getFillColor(),
barFillGradient,
allNegative ? right : left,
starty,
allNegative ? left : right,
starty);

// paint series text
double x = startx + BOX_SIZE + chart.getStyler().getLegendPadding();
Expand All @@ -85,4 +100,13 @@ public double getSeriesLegendRenderGraphicHeight(S series) {

return BOX_SIZE;
}

private boolean allNegativeXAxis() {

if (!(chart instanceof AxesChart)) {
return false;
}
Axis_X<?, ?> xAxis = ((AxesChart<?, ?>) chart).getXAxis();
return xAxis.getMin() < 0.0 && xAxis.getMax() < 0.0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.Map;
import org.knowm.xchart.CategorySeries;
import org.knowm.xchart.CategorySeries.CategorySeriesRenderStyle;
import org.knowm.xchart.internal.chartpart.RenderableSeries.LegendRenderType;
import org.knowm.xchart.internal.series.MarkerSeries;
import org.knowm.xchart.style.CategoryStyler;
import org.knowm.xchart.style.Styler;
import org.knowm.xchart.style.lines.SeriesLines;

Expand Down Expand Up @@ -118,8 +121,7 @@ public void doPaint(Graphics2D g) {

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

// Draw outline
if (series.getLegendRenderType() != LegendRenderType.BoxNoOutline) {
Expand Down Expand Up @@ -180,6 +182,50 @@ public void doPaint(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHint);
}

/**
* Category bars with the gradient enabled use the bar gradient inside the swatch, lighter at the
* top, or at the bottom when every bar on the plot grows downward (an all-negative y-axis). Every
* other legend entry, including non-bar category styles and charts that share this renderer,
* keeps the flat fill.
*/
private void fillLegendBox(Graphics2D g, S series, Shape box, double x, double y) {

boolean gradient = isCategoryBarGradient(series);
double top = y;
double bottom = y + BOX_SIZE;
boolean negative = gradient && allNegativeYAxis();
BarGradient.fill(
g,
box,
series.getFillColor(),
gradient,
x,
negative ? top : bottom,
x,
negative ? bottom : top);
}

private boolean allNegativeYAxis() {

if (!(chart instanceof AxesChart)) {
return false;
}
Axis_Y<?, ?> yAxis = ((AxesChart<?, ?>) chart).getYAxis();
return yAxis.getMin() < 0.0 && yAxis.getMax() < 0.0;
}

private boolean isCategoryBarGradient(S series) {

if (!(series instanceof CategorySeries) || !(axesChartStyler instanceof CategoryStyler)) {
return false;
}
if (!((CategoryStyler) axesChartStyler).isBarFillGradientEnabled()) {
return false;
}
return ((CategorySeries) series).getChartCategorySeriesRenderStyle().orElse(null)
== CategorySeriesRenderStyle.Bar;
}

@Override
public double getSeriesLegendRenderGraphicHeight(S series) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,20 @@ public void doPaint(Graphics2D g) {
barPath.lineTo(xOffset, zeroOffset);
barPath.closePath();

g.setColor(series.getFillColor());
g.fill(barPath);
// Gradient runs from the segment base (fill) to the value end (lighter). Positive-only
// axes, including logarithmic axes, always use that direction. Negative bars flip it so
// the lighter end stays away from zero. Each stacked segment restarts at its own base.
// Endpoints are clamped to the plot so a bar clipped at an axis limit still spans the
// whole gradient.
boolean gradient = stylerCategory.isBarFillGradientEnabled();
boolean negativeBar = BarGradient.extendsNegative(chartForm, y);
double plotTop = getBounds().getMinY();
double plotBottom = getBounds().getMaxY();
double baseY = BarGradient.clamp(negativeBar ? yOffset : zeroOffset, plotTop, plotBottom);
double valueY =
BarGradient.clamp(negativeBar ? zeroOffset : yOffset, plotTop, plotBottom);
BarGradient.fill(
g, barPath, series.getFillColor(), gradient, xOffset, baseY, xOffset, valueY);

// TODO maybe we want outlines of the bars?
// Legend markers now also draw the outline. It has been disabled for
Expand All @@ -311,6 +323,12 @@ public void doPaint(Graphics2D g) {
// }

if (stylerCategory.isLabelsVisible()) {
// An inside label sits labelsPosition of the way from the base to the value end, so
// its automatic contrast color is computed against the gradient at that point.
Color labelBackground =
gradient
? BarGradient.colorAt(series.getFillColor(), stylerCategory.getLabelsPosition())
: series.getFillColor();
drawLabels(
g,
next,
Expand All @@ -320,7 +338,7 @@ public void doPaint(Graphics2D g) {
barWidth,
false,
false,
series.getFillColor());
labelBackground);
}
if (stylerCategory.isLabelsVisible()
&& stylerCategory.isShowStackSum()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,26 @@ public void doPaint(Graphics2D g) {
barPath.lineTo(zeroOffset, yOffset + barHeight);
barPath.closePath();

g.setColor(series.getFillColor());
g.fill(barPath);
// Same base-to-value gradient as category bars, along X. xOffset is the right edge and
// zeroOffset the left, including for negative bars. Positive-only axes (including a
// logarithmic axis) keep the lighter end at the value; negative bars flip it. Endpoints
// are clamped to the plot so a bar clipped at an axis limit still spans the gradient.
boolean gradient = styler.isBarFillGradientEnabled();
boolean negativeBar = BarGradient.extendsNegative(chartForm, x);
double plotLeft = getBounds().getMinX();
double plotRight = getBounds().getMaxX();
double baseX = BarGradient.clamp(negativeBar ? xOffset : zeroOffset, plotLeft, plotRight);
double valueX = BarGradient.clamp(negativeBar ? zeroOffset : xOffset, plotLeft, plotRight);
BarGradient.fill(
g, barPath, series.getFillColor(), gradient, baseX, yOffset, valueX, yOffset);

if (styler.isLabelsVisible() && next != null) {
drawLabels(g, next, xOffset, yOffset, zeroOffset, barHeight, series.getFillColor());
// Inside labels get their automatic contrast color from the gradient at their position.
Color labelBackground =
gradient
? BarGradient.colorAt(series.getFillColor(), styler.getLabelsPosition())
: series.getFillColor();
drawLabels(g, next, xOffset, yOffset, zeroOffset, barHeight, labelBackground);
}

// add data labels
Expand Down
Loading
Loading