Skip to content

Commit 76bdb55

Browse files
committed
feat(examples): the Maven Central banner carries the rest of the story
It was one slide: wordmark, coordinate, capability tags and a code to layout to document diagram. Everything a reader might do next — how the engine actually works, whether it is fast, how it behaves as documents grow — lived in a separate deck they had to be told about. Three pages now follow it in the same amber-on-navy language. The authoring pipeline and what each step guarantees; the measured comparison against iText 9 and JasperReports; how all three behave from 40 to 1000 rows. Every figure on the last two pages is read from the committed benchmark file at render time rather than typed into the layout, so refreshing the numbers is a file copy and the prose cannot drift away from them. Page 1 is unchanged and stays a canvas — it is a poster, and every element is placed by hand. The new pages are not: they carry a table and two charts that want a flow to measure them, so they are ordinary sections on the same night field the page background already paints. The .pptx gains the same three slides. The PNG preview stays the banner alone, which is what it is for. MavenBannerNativeShapeTest walks all four slides and holds the whole deck to a single rasterised element, the SVG checkmark. The table and both charts have to arrive as native shapes rather than an embedded image — the claim the last page makes, and the one a chart-rendering shortcut would quietly break.
1 parent 2e1a6c7 commit 76bdb55

5 files changed

Lines changed: 345 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,17 @@ follow semantic versioning; release dates are ISO 8601.
241241

242242
### Documentation
243243

244+
- **The Maven Central banner became a deck.** It was one 16:9 slide: wordmark,
245+
coordinate, capability tags and a code → layout → document diagram. It now carries
246+
three more pages in the same amber-on-navy language — the authoring pipeline and what
247+
each step guarantees, the measured comparison against iText 9 and JasperReports, and
248+
how all three behave as the report grows from 40 to 1000 rows. Every figure on the
249+
last two pages is read from the committed benchmark file at render time rather than
250+
typed into the layout. The `.pptx` gains the same three slides; the PNG preview stays
251+
the banner alone, since that is what it is for. `MavenBannerNativeShapeTest` now walks
252+
all four slides and holds the whole deck to one rasterised element — the SVG
253+
checkmark — so the table and both charts have to arrive as native shapes rather than
254+
as an embedded image, which is the claim the last page makes.
244255
- **The PowerShell commands run as written.** Two pages handed Windows readers a
245256
command that fails. PowerShell splits a `-D` flag whose property name contains a dot,
246257
passing the native command `-Dexec` and `.mainClass=…` as separate arguments, and
5.94 KB
Binary file not shown.
9.45 KB
Binary file not shown.

examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@
33
import com.demcha.compose.GraphCompose;
44
import com.demcha.compose.document.api.DocumentPageSize;
55
import com.demcha.compose.document.api.DocumentSession;
6+
import com.demcha.compose.document.chart.AxisSpec;
7+
import com.demcha.compose.document.chart.ChartData;
8+
import com.demcha.compose.document.chart.ChartSize;
9+
import com.demcha.compose.document.chart.ChartSpec;
10+
import com.demcha.compose.document.chart.ChartStyle;
11+
import com.demcha.compose.document.chart.LegendPosition;
12+
import com.demcha.compose.document.chart.ValueLabelMode;
613
import com.demcha.compose.document.dsl.EllipseBuilder;
714
import com.demcha.compose.document.dsl.ParagraphBuilder;
815
import com.demcha.compose.document.dsl.PathBuilder;
16+
import com.demcha.compose.document.dsl.RowBuilder;
17+
import com.demcha.compose.document.dsl.SectionBuilder;
918
import com.demcha.compose.document.dsl.ShapeBuilder;
1019
import com.demcha.compose.document.dsl.ShapeContainerBuilder;
20+
import com.demcha.compose.document.table.DocumentTableColumn;
21+
import com.demcha.compose.document.table.DocumentTableStyle;
1122
import com.demcha.compose.document.node.CanvasChild;
1223
import com.demcha.compose.document.node.CanvasLayerNode;
1324
import com.demcha.compose.document.node.DocumentNode;
@@ -65,6 +76,17 @@ public final class MavenBannerPptxExample {
6576
private static final DocumentColor ORANGE = DocumentColor.rgb(233, 151, 45);
6677
private static final DocumentColor ORANGE_HI = DocumentColor.rgb(242, 168, 68);
6778

79+
/**
80+
* Chart series colours for the two rivals.
81+
*
82+
* <p>Three series on one chart need three hues, not three greys. Slate and a
83+
* near-slate violet were close enough that the iText and JasperReports lines
84+
* were unreadable where they crossed, so the third series is green — far from
85+
* both the amber GraphCompose line and the slate one, on a navy field.</p>
86+
*/
87+
private static final DocumentColor SERIES_SLATE = DocumentColor.rgb(122, 133, 168);
88+
private static final DocumentColor SERIES_GREEN = DocumentColor.rgb(93, 200, 143);
89+
6890
// Text.
6991
private static final DocumentColor WHITE_TX = DocumentColor.rgb(247, 248, 252);
7092
private static final DocumentColor SUBTLE_TX = DocumentColor.rgb(206, 212, 230);
@@ -163,10 +185,291 @@ static void compose(DocumentSession document) {
163185
.build());
164186
document.pageFlow()
165187
.name("MavenBanner")
188+
.spacing(0)
166189
.add(banner())
190+
.addPageBreak(b -> b.name("ToPipeline"))
191+
.addSection("Pipeline", MavenBannerPptxExample::pipelinePage)
192+
.addPageBreak(b -> b.name("ToMeasured"))
193+
.addSection("Measured", MavenBannerPptxExample::measuredPage)
194+
.addPageBreak(b -> b.name("ToScaling"))
195+
.addSection("Scaling", MavenBannerPptxExample::scalingPage)
167196
.build();
168197
}
169198

199+
// ---------------------------------------------------------------------
200+
// Pages 2–4.
201+
//
202+
// Page 1 is a canvas: every element is placed by hand because the banner is
203+
// a poster. These are not. They carry a table and four charts, which want a
204+
// flow that measures them — so they are ordinary sections on the same night
205+
// field the page background already paints, styled from the same tokens.
206+
// ---------------------------------------------------------------------
207+
208+
/** Page 2 — the authoring pipeline, four steps and what each one guarantees. */
209+
private static void pipelinePage(SectionBuilder page) {
210+
page.padding(DocumentInsets.symmetric(38, 52)).spacing(0);
211+
kicker(page, "HOW IT WORKS", "From one Java file to a finished document");
212+
lede(page, "You describe the document semantically — sections, rows, tables, charts, "
213+
+ "shapes, layers — and the engine resolves the rest. No manual coordinates, "
214+
+ "no XML templates, no per-format authoring.");
215+
216+
page.addRow("Steps", row -> {
217+
row.spacing(14).evenWeights().margin(DocumentInsets.top(AFTER_LEDE));
218+
stepCard(row, "1", "AUTHOR", "A fluent DSL describes intent, not geometry.");
219+
stepCard(row, "2", "MEASURE", "Every node measured twice, deterministically.");
220+
stepCard(row, "3", "PAGINATE", "The flow splits across pages row by row.");
221+
stepCard(row, "4", "RENDER", "A backend writes the bytes — PDF or PPTX.");
222+
});
223+
224+
page.addRow("Guarantees", row -> {
225+
// Row to row, not lede to row: the card above brings its own bottom
226+
// padding, so this reads as the same gap at a smaller number.
227+
row.spacing(14).evenWeights().margin(DocumentInsets.top(22));
228+
proofCard(row, "Deterministic",
229+
"The same input renders the same bytes on every machine. Layout is "
230+
+ "reproducible, not best-effort.");
231+
proofCard(row, "Regression-tested",
232+
"layoutSnapshot() captures the resolved geometry, so a layout change "
233+
+ "fails a test instead of surfacing in a release.");
234+
proofCard(row, "One model, two formats",
235+
"The same composition emits this PDF and an editable PPTX. Nothing is "
236+
+ "authored twice, so the two cannot drift.");
237+
});
238+
}
239+
240+
/** Page 3 — the measured comparison, drawn from the committed benchmark file. */
241+
private static void measuredPage(SectionBuilder page) {
242+
EngineDeckData.BenchRun bench = EngineDeckData.loadBench();
243+
page.padding(DocumentInsets.symmetric(38, 52)).spacing(0);
244+
kicker(page, "MEASURED, NOT CLAIMED", "GraphCompose against the field");
245+
lede(page, "The same documents through three engines — render time and peak heap, "
246+
+ "measured by this repository's own harness. Every figure on this page and the "
247+
+ "next is read from the result file at render time, not typed in.");
248+
249+
page.addTable(table -> {
250+
table.name("Comparison")
251+
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(),
252+
DocumentTableColumn.auto(), DocumentTableColumn.auto())
253+
.headerStyle(cellStyle(sansBold(12, WHITE_TX), CARD_2))
254+
.defaultCellStyle(cellStyle(sans(12, SUBTLE_TX), CARD))
255+
.headerRow("Report size", "GraphCompose", "iText 9", "JasperReports")
256+
.row("1 page · 3 lines",
257+
cell(bench.timeMs("GraphCompose Canonical"), bench.heapMb("GraphCompose Canonical")),
258+
cell(bench.timeMs("iText 9"), bench.heapMb("iText 9")),
259+
cell(bench.timeMs("JasperReports"), bench.heapMb("JasperReports")));
260+
for (int size : new int[]{40, 200, 1000}) {
261+
table.row(size + " rows",
262+
cell(bench.timeMs("GraphCompose", size), bench.heapMb("GraphCompose", size)),
263+
cell(bench.timeMs("iText 9", size), bench.heapMb("iText 9", size)),
264+
cell(bench.timeMs("JasperReports", size), bench.heapMb("JasperReports", size)));
265+
}
266+
table.margin(DocumentInsets.top(AFTER_LEDE));
267+
});
268+
269+
page.addRow("Headline", row -> {
270+
row.spacing(14).evenWeights().margin(DocumentInsets.top(20));
271+
ratioCard(row, times(bench.timeMs("iText 9", 1000) / bench.timeMs("GraphCompose", 1000)),
272+
"faster than iText 9 at 1000 rows");
273+
ratioCard(row, times(bench.heapMb("iText 9", 1000) / bench.heapMb("GraphCompose", 1000)),
274+
"lighter than iText 9");
275+
ratioCard(row, times(bench.heapMb("JasperReports", 1000) / bench.heapMb("GraphCompose", 1000)),
276+
"lighter than JasperReports");
277+
});
278+
279+
page.addParagraph(p -> p
280+
.text("Measured " + bench.timestamp() + " · " + bench.warmup() + " warmup / "
281+
+ bench.measure() + " measurement iterations on one machine. Read the "
282+
+ "ratios rather than the milliseconds: all three engines are measured "
283+
+ "in the same run, so their proportions survive a change of hardware "
284+
+ "while the timings do not.")
285+
.textStyle(sans(9.5, FAINT_TX))
286+
.lineSpacing(3)
287+
.margin(DocumentInsets.top(18)));
288+
}
289+
290+
/** Page 4 — how the three engines behave as the report grows. */
291+
private static void scalingPage(SectionBuilder page) {
292+
EngineDeckData.BenchRun bench = EngineDeckData.loadBench();
293+
page.padding(DocumentInsets.symmetric(38, 52)).spacing(0);
294+
kicker(page, "SCALING", "What changes as the report grows");
295+
lede(page, "From 40 to 1000 rows the time lead over iText widens; JasperReports closes "
296+
+ "to roughly the same render time at the top size, while GraphCompose stays "
297+
+ "markedly lighter on memory than both throughout. Every series reads from the "
298+
+ "same file as the table on the previous page.");
299+
300+
page.addRow("ScalingCharts", row -> {
301+
row.spacing(20).evenWeights().margin(DocumentInsets.top(AFTER_LEDE));
302+
chartCard(row, "Render time vs. report size — ms", "lower is faster",
303+
seriesOf(bench, true));
304+
chartCard(row, "Peak heap vs. report size — MB", "lower is lighter",
305+
seriesOf(bench, false));
306+
});
307+
308+
page.addParagraph(p -> p
309+
.text("Both charts are native vector output: the engine compiles them to the same "
310+
+ "shapes, lines and text frames as everything else on the page — no "
311+
+ "image is embedded, and the numbers come from the file, not the caption.")
312+
.textStyle(sans(9.5, FAINT_TX))
313+
.lineSpacing(3)
314+
.margin(DocumentInsets.top(20)));
315+
}
316+
317+
// ---------------------------------------------------------------------
318+
// Page furniture for 2–4.
319+
// ---------------------------------------------------------------------
320+
321+
/**
322+
* Inner width of a card in a four-across / three-across row.
323+
*
324+
* <p>A section measures to its longest line, so a row of cards whose text
325+
* happens to be shorter renders with ragged edges. A zero-height spacer of
326+
* this width sets the floor and the row lines up.</p>
327+
*/
328+
private static final double CONTENT_W = PAGE_W - 104;
329+
private static final double STEP_INNER = (CONTENT_W - 3 * 14) / 4 - 32;
330+
private static final double TRIPLE_INNER = (CONTENT_W - 2 * 14) / 3 - 32;
331+
332+
/**
333+
* Space between the lede and the panels under it.
334+
*
335+
* <p>Larger than the gap between two rows of cards, and deliberately so. A card
336+
* carries its own bottom padding, so between two rows the eye sees that padding
337+
* plus the margin; between the lede and the first row it sees the margin alone.
338+
* Equal margins therefore read as unequal, and the text looked stuck to the
339+
* cards. This is the row gap plus a card's padding, so both gaps land the same.</p>
340+
*/
341+
private static final double AFTER_LEDE = 37;
342+
343+
private static void kicker(SectionBuilder page, String eyebrow, String headline) {
344+
page.addParagraph(p -> p.text(eyebrow).textStyle(monoBold(11, ORANGE)));
345+
page.addParagraph(p -> p
346+
.text(headline)
347+
.textStyle(sansBold(30, WHITE_TX))
348+
.margin(DocumentInsets.top(6)));
349+
page.addShape(sh -> sh.size(96, 4).fillColor(ORANGE).margin(DocumentInsets.top(12)));
350+
}
351+
352+
private static void lede(SectionBuilder page, String text) {
353+
page.addParagraph(p -> p
354+
.text(text)
355+
.textStyle(sans(13, SUBTLE_TX))
356+
.lineSpacing(4)
357+
.margin(DocumentInsets.top(16)));
358+
}
359+
360+
private static void stepCard(RowBuilder row, String number, String title, String desc) {
361+
row.addSection("Step" + number, s -> s
362+
.softPanel(CARD, 10, 16)
363+
.stroke(DocumentStroke.of(BORDER, 1))
364+
.spacing(0)
365+
.addSpacer(sp -> sp.size(STEP_INNER, 0))
366+
.addParagraph(p -> p.text(number).textStyle(monoBold(20, ORANGE)))
367+
.addParagraph(p -> p.text(title)
368+
.textStyle(sansBold(13, WHITE_TX))
369+
.margin(DocumentInsets.top(8)))
370+
.addParagraph(p -> p.text(desc)
371+
.textStyle(sans(11, MUTED_TX))
372+
.lineSpacing(3)
373+
.margin(DocumentInsets.top(6))));
374+
}
375+
376+
private static void proofCard(RowBuilder row, String title, String desc) {
377+
row.addSection("Proof" + title, s -> s
378+
.softPanel(CARD_2, 10, 16)
379+
.accentTop(ORANGE, 3)
380+
.spacing(0)
381+
.addSpacer(sp -> sp.size(TRIPLE_INNER, 0))
382+
.addParagraph(p -> p.text(title).textStyle(sansBold(13, WHITE_TX)))
383+
.addParagraph(p -> p.text(desc)
384+
.textStyle(sans(11, MUTED_TX))
385+
.lineSpacing(3)
386+
.margin(DocumentInsets.top(7))));
387+
}
388+
389+
private static void ratioCard(RowBuilder row, String figure, String label) {
390+
row.addSection("Ratio" + label, s -> s
391+
.softPanel(CARD, 10, 16)
392+
.stroke(DocumentStroke.of(BORDER, 1))
393+
.spacing(0)
394+
.addSpacer(sp -> sp.size(TRIPLE_INNER, 0))
395+
.addParagraph(p -> p.text(figure).textStyle(sansBold(28, ORANGE)))
396+
.addParagraph(p -> p.text(label)
397+
.textStyle(sans(11, MUTED_TX))
398+
.lineSpacing(3)
399+
.margin(DocumentInsets.top(4))));
400+
}
401+
402+
private static void chartCard(RowBuilder row, String title, String note, ChartSpec spec) {
403+
row.addSection("Chart" + title, s -> s
404+
.softPanel(CARD, 10, 16)
405+
.stroke(DocumentStroke.of(BORDER, 1))
406+
.spacing(0)
407+
.addParagraph(p -> p.text(title).textStyle(sansBold(12, WHITE_TX)))
408+
.addParagraph(p -> p.text(note)
409+
.textStyle(sans(10, FAINT_TX))
410+
.margin(DocumentInsets.top(3)))
411+
.chart(spec, chartStyle()));
412+
}
413+
414+
/** Time or heap for all three engines across the row-count sweep. */
415+
private static ChartSpec seriesOf(EngineDeckData.BenchRun bench, boolean time) {
416+
ChartData.Builder data = ChartData.builder().categories("40", "200", "1000");
417+
for (String lib : new String[]{"GraphCompose", "iText 9", "JasperReports"}) {
418+
data.series(lib,
419+
time ? bench.timeMs(lib, 40) : bench.heapMb(lib, 40),
420+
time ? bench.timeMs(lib, 200) : bench.heapMb(lib, 200),
421+
time ? bench.timeMs(lib, 1000) : bench.heapMb(lib, 1000));
422+
}
423+
return ChartSpec.line()
424+
.data(data.build())
425+
.legend(LegendPosition.BOTTOM)
426+
.size(ChartSize.aspectRatio(16, 7))
427+
.build();
428+
}
429+
430+
/**
431+
* The chart palette, matched to the banner: GraphCompose in amber, the two
432+
* rivals in the muted greys the cards already use, so the reader's eye lands
433+
* on the series the document is about without the chart shouting.
434+
*/
435+
private static ChartStyle chartStyle() {
436+
return ChartStyle.builder()
437+
.seriesPaint(0, DocumentPaint.solid(ORANGE))
438+
.seriesPaint(1, DocumentPaint.solid(SERIES_SLATE))
439+
.seriesPaint(2, DocumentPaint.solid(SERIES_GREEN))
440+
.barCornerRadius(DocumentCornerRadius.top(3))
441+
.barWidthRatio(0.5)
442+
.lineWidth(2.0)
443+
.axisTextStyle(mono(8.5, MUTED_TX))
444+
.legendTextStyle(sans(9, MUTED_TX))
445+
.valueLabelTextStyle(monoBold(10, ORANGE))
446+
// The labels sit over the card, not a white page: match the halo to
447+
// the card fill so a gridline stops behind the digits instead of a
448+
// white chip appearing on the navy.
449+
.valueLabelHalo(DocumentPaint.solid(CARD))
450+
.grid(ChartStyle.GridStyle.horizontal(DocumentStroke.of(BORDER, 0.6)))
451+
.build();
452+
}
453+
454+
/** A table cell on the night field: the card fill behind the given text style. */
455+
private static DocumentTableStyle cellStyle(DocumentTextStyle text, DocumentColor fill) {
456+
return DocumentTableStyle.builder()
457+
.textStyle(text)
458+
.fillColor(fill)
459+
.stroke(DocumentStroke.of(BORDER, 0.8))
460+
.padding(DocumentInsets.symmetric(5, 10))
461+
.build();
462+
}
463+
464+
/** {@code 40.0 ms · 19.3 MB} — one cell carrying both measurements. */
465+
private static String cell(double ms, double mb) {
466+
return String.format(java.util.Locale.ROOT, "%.1f ms · %.1f MB", ms, mb);
467+
}
468+
469+
private static String times(double ratio) {
470+
return String.format(java.util.Locale.ROOT, "%.1fx", ratio);
471+
}
472+
170473
// ---------------------------------------------------------------------
171474
// Full-bleed banner scene.
172475
// ---------------------------------------------------------------------

0 commit comments

Comments
 (0)