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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ Entries land here as they merge.

### Public API

- **Line-chart interpolation modes** (`@since 1.8.0`). New
`LineInterpolation` enum selects how a line series connects its points:
`LINEAR` (straight, exact), `SMOOTH` (the existing pretty Catmull-Rom
curve, which may overshoot local extremes on sharp swings), and the new
`MONOTONE` (Fritsch-Carlson) — a curve that looks just as smooth but is
constrained to never overshoot, staying within the value range of the
points it spans, for an accurate yet smooth reading of the data. Set it
with `ChartSpec.line().interpolation(LineInterpolation.MONOTONE)` — the
single, explicit knob for line shape. All three render through the same
native PDF curve operators with zero tessellation, so geometry stays
deterministic and the hot path is unchanged.
- **`ChartData.Series` rejects non-finite values.** A `NaN` / ±∞ entry now
fails at construction — naming the series and the offending index —
instead of poisoning axis derivation and surfacing as a misleading
Expand Down
Binary file modified assets/readme/chart-showcase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/examples/chart-showcase.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.demcha.compose.document.chart.ChartStyle;
import com.demcha.compose.document.style.DocumentPaint;
import com.demcha.compose.document.chart.LegendPosition;
import com.demcha.compose.document.chart.LineInterpolation;
import com.demcha.compose.document.chart.NumberFormatSpec;
import com.demcha.compose.document.chart.PointMarker;
import com.demcha.compose.document.chart.SliceLabelMode;
Expand Down Expand Up @@ -123,12 +124,32 @@ public static Path generate() throws Exception {
// Smooth area: Catmull-Rom curves with a translucent fill to the baseline.
ChartSpec areaSpec = ChartSpec.line()
.data(revenue)
.smooth(true)
.interpolation(LineInterpolation.SMOOTH)
.area(true)
.legend(LegendPosition.TOP)
.size(ChartSize.aspectRatio(16, 7))
.build();

// Accuracy vs. beauty on the same volatile series. SMOOTH (Catmull-Rom)
// is the prettiest curve but bulges past the peaks and troughs it
// connects; MONOTONE (Fritsch-Carlson) stays just as smooth yet never
// leaves the data's range — the curve never claims a value the data
// never had. Markers pin the true data points so the difference shows.
ChartData volatileSeries = ChartData.builder()
.categories("Jan", "Feb", "Mar", "Apr", "May", "Jun")
.series("Price", 20.0, 95.0, 92.0, 18.0, 24.0, 90.0)
.build();
ChartSpec smoothSwingSpec = ChartSpec.line()
.data(volatileSeries)
.interpolation(LineInterpolation.SMOOTH)
.size(ChartSize.aspectRatio(16, 6))
.build();
ChartSpec monotoneSwingSpec = ChartSpec.line()
.data(volatileSeries)
.interpolation(LineInterpolation.MONOTONE)
.size(ChartSize.aspectRatio(16, 6))
.build();

// Horizontal bars: categories on Y, values on X, legend as a right column.
ChartSpec horizontalSpec = ChartSpec.bar()
.data(revenue)
Expand Down Expand Up @@ -236,6 +257,24 @@ public static Path generate() throws Exception {
.textStyle(THEME.text().h3())
.margin(DocumentInsets.zero()))
.chart(areaSpec))
.addSection("SmoothSwingCard", section -> section
.keepTogether()
.softPanel(DocumentColor.WHITE, 8, 16)
.spacing(10)
.addParagraph(p -> p
.text("Volatile price — SMOOTH curve (pretty, overshoots the peaks)")
.textStyle(THEME.text().h3())
.margin(DocumentInsets.zero()))
.chart(smoothSwingSpec, lineStyle))
.addSection("MonotoneSwingCard", section -> section
.keepTogether()
.softPanel(DocumentColor.WHITE, 8, 16)
.spacing(10)
.addParagraph(p -> p
.text("Volatile price — MONOTONE curve (smooth, never leaves the data range)")
.textStyle(THEME.text().h3())
.margin(DocumentInsets.zero()))
.chart(monotoneSwingSpec, lineStyle))
.addSection("HorizontalCard", section -> section
.keepTogether()
.softPanel(DocumentColor.WHITE, 8, 16)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.demcha.compose.document.chart.ChartSpec;
import com.demcha.compose.document.chart.ChartStyle;
import com.demcha.compose.document.chart.LegendPosition;
import com.demcha.compose.document.chart.LineInterpolation;
import com.demcha.compose.document.chart.NumberFormatSpec;
import com.demcha.compose.document.chart.PointMarker;
import com.demcha.compose.document.chart.SliceLabelMode;
Expand Down Expand Up @@ -145,7 +146,7 @@ static ChartSpec scalingLineChart(EngineDeckData.BenchRun b) {
static ChartSpec memoryAreaChart(EngineDeckData.BenchRun b) {
return ChartSpec.line()
.data(bySize(b, false))
.smooth(true)
.interpolation(LineInterpolation.SMOOTH)
.area(true)
.valueAxis(AxisSpec.builder().baselineAtZero(true)
.format(NumberFormatSpec.pattern("#,##0").withSuffix(" MB")).build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.demcha.compose.document.chart.ChartSpec;
import com.demcha.compose.document.chart.ChartStyle;
import com.demcha.compose.document.chart.LegendPosition;
import com.demcha.compose.document.chart.LineInterpolation;
import com.demcha.compose.document.chart.PointMarker;
import com.demcha.compose.document.chart.SliceLabelMode;
import com.demcha.compose.document.chart.ValueLabelMode;
Expand Down Expand Up @@ -213,13 +214,13 @@ public static Path generate() throws Exception {

feature(flow, "Line chart — smooth, area fill, markers, label halos", """
section.chart(ChartSpec.line().data(revenue)
.smooth(true).area(true).valueLabels(ValueLabelMode.OUTSIDE)
.interpolation(LineInterpolation.SMOOTH).area(true).valueLabels(ValueLabelMode.OUTSIDE)
.size(ChartSize.fixedHeight(150)).build(),
ChartStyle.builder().lineWidth(1.8)
.pointMarker(PointMarker.circle(5).withStroke(DocumentStroke.of(WHITE, 1.2)))
.build())""",
demo -> demo.chart(ChartSpec.line().data(revenue)
.smooth(true).area(true).valueLabels(ValueLabelMode.OUTSIDE)
.interpolation(LineInterpolation.SMOOTH).area(true).valueLabels(ValueLabelMode.OUTSIDE)
.size(ChartSize.fixedHeight(150)).build(),
ChartStyle.builder().lineWidth(1.8)
.pointMarker(PointMarker.circle(5)
Expand Down
45 changes: 23 additions & 22 deletions src/main/java/com/demcha/compose/document/chart/ChartSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ public Bar build() {
* from {@link ChartStyle#pointMarker()} so the geometry is reused).
*
* @param data tabular data
* @param smooth true = curved (Catmull-Rom) segments; false = straight
* @param interpolation how points are connected: {@link LineInterpolation#LINEAR}
* straight, {@link LineInterpolation#SMOOTH} pretty curve,
* {@link LineInterpolation#MONOTONE} smooth without overshoot
* @param area fill the region between each series and the axis baseline
* (translucent series colour; see {@code ChartStyle.areaOpacity})
* @param valueAxis numeric-axis configuration
Expand All @@ -261,7 +263,7 @@ public Bar build() {
*/
record Line(
ChartData data,
boolean smooth,
LineInterpolation interpolation,
boolean area,
AxisSpec valueAxis,
LegendPosition legend,
Expand All @@ -274,26 +276,27 @@ record Line(
*/
public Line {
Objects.requireNonNull(data, "data");
interpolation = interpolation == null ? LineInterpolation.LINEAR : interpolation;
valueAxis = valueAxis == null ? AxisSpec.defaults() : valueAxis;
legend = legend == null ? LegendPosition.NONE : legend;
valueLabels = valueLabels == null ? ValueLabelMode.NONE : valueLabels;
size = size == null ? ChartSize.aspectRatio(16, 9) : size;
}

/**
* Backward-compatible constructor without the area and category-label
* toggles (no area fill; category labels shown).
* Convenience constructor without the area and category-label toggles
* (no area fill; category labels shown).
*
* @param data tabular data
* @param smooth curved segments
* @param valueAxis numeric-axis configuration
* @param legend legend placement
* @param valueLabels per-point value label mode
* @param size sizing policy
* @param data tabular data
* @param interpolation point-connection mode
* @param valueAxis numeric-axis configuration
* @param legend legend placement
* @param valueLabels per-point value label mode
* @param size sizing policy
*/
public Line(ChartData data, boolean smooth, AxisSpec valueAxis, LegendPosition legend,
ValueLabelMode valueLabels, ChartSize size) {
this(data, smooth, false, valueAxis, legend, valueLabels, size, true);
public Line(ChartData data, LineInterpolation interpolation, AxisSpec valueAxis,
LegendPosition legend, ValueLabelMode valueLabels, ChartSize size) {
this(data, interpolation, false, valueAxis, legend, valueLabels, size, true);
}

@Override
Expand All @@ -306,7 +309,7 @@ public NumberFormatSpec valueFormat() {
*/
public static final class Builder {
private ChartData data;
private boolean smooth = false;
private LineInterpolation interpolation = LineInterpolation.LINEAR;
private boolean area = false;
private AxisSpec valueAxis = AxisSpec.defaults();
private LegendPosition legend = LegendPosition.NONE;
Expand All @@ -326,16 +329,14 @@ public Builder data(ChartData d) {
}

/**
* Sets smoothing. Curves are Catmull-Rom splines subdivided at a
* fixed step, so geometry stays deterministic. Like any
* interpolating spline, a curve may slightly overshoot local
* extremes between data points on sharp value swings.
* Sets how the line connects its points; see
* {@link LineInterpolation} for the beauty-vs-accuracy trade-offs.
*
* @param v true for curved segments
* @param mode point-connection mode
* @return this builder
*/
public Builder smooth(boolean v) {
this.smooth = v;
public Builder interpolation(LineInterpolation mode) {
this.interpolation = mode;
return this;
}

Expand Down Expand Up @@ -412,7 +413,7 @@ public Builder size(ChartSize s) {
* @return line spec
*/
public Line build() {
return new Line(data, smooth, area, valueAxis, legend, valueLabels, size,
return new Line(data, interpolation, area, valueAxis, legend, valueLabels, size,
showCategoryLabels);
}
}
Expand Down
Loading