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

### Public API

- **`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
"height must be finite" failure deep in the layout pass. `null` entries
are still allowed as gaps.
- **Block-level horizontal alignment** (`@since 1.8.0`). Fixed-size flow
children (paths, images, SVG icons, barcodes, shape containers) left-align
by default — there was no built-in way to centre or right-align one without
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/com/demcha/compose/document/chart/ChartData.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,31 @@ public int categoryCount() {
/**
* One named value series. {@code null} entries are allowed and mean a
* missing point — a gap in a line, a skipped bar — distinct from {@code 0}.
* Non-finite entries ({@code NaN} / ±∞) are rejected at construction: they
* would otherwise poison axis derivation and only surface as a misleading
* "height must be finite" failure deep inside the layout pass.
*
* @param name legend label for this series
* @param values value per category, aligned by index; entries may be null
* (a gap) but must otherwise be finite
*/
public record Series(String name, List<Double> values) {
/**
* Normalizes the name and tolerates {@code null} value entries.
* Normalizes the name, tolerates {@code null} value entries (gaps),
* and rejects non-finite values with the offending index named.
*/
public Series {
name = name == null ? "" : name;
Objects.requireNonNull(values, "values");
values = java.util.Collections.unmodifiableList(new ArrayList<>(values));
for (int i = 0; i < values.size(); i++) {
Double v = values.get(i);
if (v != null && !Double.isFinite(v)) {
throw new IllegalArgumentException(
"series '" + name + "' value at index " + i
+ " must be finite or null, got " + v);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.demcha.compose.document.chart;

import com.demcha.compose.document.chart.ChartData.Series;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Value-level contract of {@link ChartData} and its {@link Series}: null
* entries are tolerated as gaps, but non-finite values are rejected at
* authoring time (rather than poisoning axis derivation and surfacing as a
* misleading "height must be finite" failure deep in the layout pass).
*/
class ChartDataTest {

@Test
void nullValuesAreAllowedAsGaps() {
Series s = new Series("revenue", Arrays.asList(1.0, null, 3.0));

assertThat(s.values()).containsExactly(1.0, null, 3.0);
}

@Test
void nanValueIsRejectedNamingTheSeriesAndIndex() {
assertThatThrownBy(() -> new Series("revenue", Arrays.asList(1.0, Double.NaN, 3.0)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("revenue")
.hasMessageContaining("index 1 must be finite");
}

@Test
void infiniteValuesAreRejected() {
assertThatThrownBy(() -> Series.of("a", 1.0, Double.POSITIVE_INFINITY))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("finite");
assertThatThrownBy(() -> Series.of("b", Double.NEGATIVE_INFINITY))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("finite");
}

@Test
void finiteSeriesBuildsCleanly() {
ChartData data = ChartData.builder()
.categories("Q1", "Q2")
.series("revenue", 10.0, 20.0)
.build();

assertThat(data.seriesCount()).isEqualTo(1);
assertThat(data.categoryCount()).isEqualTo(2);
assertThat(data.series().get(0).values()).containsExactly(10.0, 20.0);
}

@Test
void raggedSeriesIsRejected() {
assertThatThrownBy(() -> new ChartData(List.of("Q1", "Q2"),
List.of(Series.of("revenue", 10.0))))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("values but there are");
}
}