# TerraFluent — Full Documentation # Fluent report generation for .NET: print-ready paginated HTML (TerraFluent.Html.Reporting), # native Word .docx documents (TerraFluent.Docx.Reporting), and true PDF files # (TerraFluent.Pdf.Reporting). MIT licensed. # Site: https://terrafluent.dev/ ==================================================================== TerraFluent.Html.Reporting — Getting Started URL: https://terrafluent.dev/docs/html/getting-started/ ==================================================================== # Getting Started ## Installation TerraFluent.Html.Reporting targets both `netstandard2.0` and `net10.0` and has zero third-party dependencies. Add the package to your project: ``` dotnet add package TerraFluent.Html.Reporting ``` ## The four-step pipeline Every report follows the same shape: ```csharp using TerraFluent.Html.Reporting.Model; using TerraFluent.Html.Reporting.Model.Elements; var report = ReportDocument.Create(PageSize.A4, PageOrientation.Portrait) // 1. start .SetMargins(40, 40, 60, 60) .Header(h => h.AddText("Monthly Sales Report").AlignCenter().Bold()) // 2. configure .Footer(f => f.AddPageNumber("Page {page} of {totalPages}")) .Content(c => { c.AddHeading("Sales Summary", HeadingLevel.H1); c.AddParagraph("This report summarizes sales activity for the period."); c.AddImage("logo.png", widthPx: 120, heightPx: 60); c.AddTable(table => { table.AddColumns("Product", "Qty", "Revenue"); table.AddRow("Widget A", "120", "$2,400"); }); }) .Build(); // 3. build string html = report.RenderHtml(); // 4. render ``` 1. **`ReportDocument.Create(pageSize, orientation)`** returns a [`ReportDocumentBuilder`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Fluent/ReportDocumentBuilder.cs) - despite living on `ReportDocument`, this is the fluent entry point, not the document itself. 2. **`.SetMargins(...)`, `.Header(...)`, `.Footer(...)`, `.Content(...)`** configure the builder. `Header`/`Footer`/`Content` each take an `Action` callback - calling any of them more than once *appends* to the same section rather than replacing it. 3. **`.Build()`** produces an immutable [`ReportDocument`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Model/ReportDocument.cs). Nothing is measured or paginated yet. 4. **`.RenderHtml(...)`** (or one of its siblings - see [Rendering](/docs/html/rendering/)) paginates the document and returns a single, self-contained HTML string: inline `
... one absolutely-positioned element per placed fragment ...
...
``` Key points: - **`@page { size: ...; margin: 0; }`** sets the print page size to exactly match the document's `PageSize`. Page margins are *not* expressed as `@page margin` - they're baked into each element's absolute `left`/`top` position instead, so what you see on screen (a white page with a drop shadow) is pixel-identical to what prints. - **One `.fhr-page` `
` per page**, sized exactly to `PageSize`, with `overflow:hidden` so any force-placed, overflowing content (see [`LayoutWarning`](/docs/html/pagination-and-layout/#layoutwarning-when-content-doesnt-fit)) is clipped visually rather than spilling into the next page's div. - **`page-break-after: always`** (plus the modern `break-after: page`) on every page except the last is a redundant signal for browsers that don't fully honor `@page` sizing during print - belt-and-suspenders, since the exact-pixel page divs are normally enough on their own. - **Every element renders as its own absolutely-positioned tag** (`

`, `

`-`

`, ``, ``, `
    `/`
      `, or a styled `
      `) at the `left`/`top`/`width`/`height` the layout engine computed - the renderer has no pagination logic of its own; it only translates each `ElementPlacement` from section-relative to page-absolute coordinates (see [Pagination and Layout: The output](/docs/html/pagination-and-layout/#the-output-layoutresult)) and calls `IReportElement.RenderHtml`. - **All CSS pixel values use the invariant culture** (`123.45px`, never `123,45px`) - see [`CssFormat`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Rendering/CssFormat.cs) - so generated reports are correct regardless of the server's locale. - **All user-supplied text is HTML-encoded** before being written (again via `CssFormat.Encode`, i.e. `WebUtility.HtmlEncode`) - the one exception is `RawHtml`, which is emitted verbatim by design (see [Content Elements: Raw HTML](/docs/html/content-elements/#raw-html)). - **`` defaults to `Report`**, or the document's own title (HTML-encoded) when `ReportDocumentBuilder.Title(...)` was called - a custom `IHtmlReportRenderer` can read it via `LayoutResult.Title`. `RenderFragmentTo` emits the page styles and page `<div>`s without the `<html>`/`<head>`/`<body>` wrapper. Its stylesheet omits the document-level `html`/`body` reset and background rules, so embedding a report does not change the host page's margins, background, or font. ## Printing to PDF Because the HTML is built around exact-pixel `@page` sizing and one `<div>` per page, a browser's native "Print to PDF" (or a headless-browser print API, e.g. Playwright/Puppeteer) reproduces the same page breaks you see when viewing the HTML directly - there's no separate PDF-specific code path in this library. If you need exact pixel-perfect text wrapping in that PDF, see [Text Measurement](/docs/html/text-measurement/) for why the bundled measurer alone may not guarantee that. ## Tested against real browsers The pagination and rendering logic isn't only checked with plain unit tests - CI also runs [`PrintLayoutBrowserTests`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/tests/TerraFluent.Html.Reporting.BrowserTests/PrintLayoutBrowserTests.cs) against **real, headless Chromium, Firefox, and WebKit** via [Playwright](https://playwright.dev/dotnet/), rendering a generated report and asserting - under `@media print`, the same media browsers use for "Print to PDF" - that every page's `getBoundingClientRect()` matches the requested `PageSize` exactly and that no element's box overflows its page's bounds. See [`.github/workflows/ci.yml`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/.github/workflows/ci.yml) for the CI job that installs and runs all three engines on every push and pull request. This is in addition to, not instead of, the unit tests that exercise the pagination math itself (see [Pagination and Layout](/docs/html/pagination-and-layout/)) - the browser tests specifically guard the *rendered HTML/CSS* against real browsers' print-layout behavior, which a markup-only assertion cannot. ## Where to go next - [Text Measurement](/docs/html/text-measurement/) for the seam that determines how accurately layout matches real browser rendering. - [Extending the Library](/docs/html/extending/) for writing a custom `IHtmlReportRenderer`. - [Cookbook: Streaming a large report](/docs/html/cookbook/#streaming-a-large-report-to-disk-asynchronously) for a runnable example of the async file API. ==================================================================== TerraFluent.Html.Reporting — Text Measurement URL: https://terrafluent.dev/docs/html/text-measurement/ ==================================================================== # Text Measurement Pagination needs to know how tall a block of text will be *before* it's rendered - which means measuring how it wraps at a given width. This is the single seam, `ITextMeasurer`, that every height calculation in the layout engine ultimately depends on. ## `ITextMeasurer` [`ITextMeasurer`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Measurement/ITextMeasurer.cs) is a one-method interface: ```csharp public interface ITextMeasurer { TextMeasurement Measure(string text, FontSpecification font, double maxWidthPx); } ``` - **`text`** may contain explicit newlines, which must be treated as hard breaks (not wrapped through). - **`font`** is a [`FontSpecification`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Measurement/FontSpecification.cs) - family, size, bold, italic, and line-height multiplier - deliberately decoupled from `Model.Styling.TextStyle` so an `ITextMeasurer` implementation (including one shipped in a separate package) doesn't need to depend on the document model at all. - **`maxWidthPx`** is the width a single line may occupy. The return type, [`TextMeasurement`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Measurement/TextMeasurement.cs), holds the resulting `Lines` (each one guaranteed to fit within the measured width), the resolved `LineHeightPx`, and `WidestLineWidthPx`. `TotalHeightPx` (`Lines.Count * LineHeightPx`) is what the layout engine actually sums for pagination; elements that support splitting (`Paragraph`, table cells) slice `Lines` at a line boundary to build the head/tail fragments for `IReportElement.Split`. ## The default: `ApproximateTextMeasurer` [`ApproximateTextMeasurer`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Measurement/ApproximateTextMeasurer.cs) (exposed as the stateless singleton `ApproximateTextMeasurer.Instance`, and used automatically unless you override it) estimates wrapping using per-character average-width tables for Helvetica ([`HelveticaCharacterWidths`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Measurement/HelveticaCharacterWidths.cs)), scaled by font size, with a flat 1.08x multiplier approximating bold's extra width. It has **zero runtime dependencies** - no `System.Drawing`, no native text-shaping library - which is what keeps the whole core package usable on `netstandard2.0` and trimmable/AOT-compatible on modern .NET. What this buys you, and what it doesn't: - It **will not** match a browser's actual layout engine pixel-for-pixel - particularly for proportional fonts other than a generic sans-serif, text with heavy kerning/ligatures, or any font where Helvetica's metrics are a poor stand-in. - It **does not hyphenate**: a single word wider than the available width is placed alone on its own (overflowing) line rather than broken mid-word. - `FontFamily`/`Bold`/`Italic` on a `TextStyle` still affect the *rendered* output normally (the CSS faithfully sets `font-family`, `font-weight`, `font-style`) - they just don't change which width table `ApproximateTextMeasurer` consults, since it only ever measures against the one Helvetica table. In practice this means: page breaks chosen with the default measurer are *close* to where a real browser would wrap the same text, but not guaranteed exact - acceptable for most reports, but worth knowing about if your report's pagination needs to be pixel-exact (e.g. a legally significant multi-page contract where a line must never silently shift to the next page in print). ## Supplying a precise measurer ```csharp ReportDocument.Create(PageSize.A4) .UseTextMeasurer(myPreciseMeasurer) .Content(c => { ... }) .Build(); ``` `ReportDocumentBuilder.UseTextMeasurer(ITextMeasurer measurer)` overrides the default for the whole document; it throws `ArgumentNullException` if you pass `null`. There's no per-element override - one document, one measurer, used consistently for every element so pagination is internally coherent. Implement `ITextMeasurer` against a real rendering engine - a headless browser (e.g. Playwright/Puppeteer measuring actual DOM layout), `System.Drawing`/GDI+ on Windows, or any text-shaping library that can report glyph advances for your target font - to get pagination that matches actual rendering. **No ready-made precise measurer ships with the core package today** - this is a documented extension point, not a plug-in registry. The intended pattern is a separate companion package (e.g. a hypothetical `TerraFluent.Html.Reporting.Measurement.Playwright`) that depends on the core package and supplies one, keeping the core package itself free of native/runtime dependencies. A working reference implementation of exactly that pattern lives at [samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer) - it measures word widths with a real, headless Chromium instance's Canvas 2D `measureText` API rather than `ApproximateTextMeasurer`'s static Helvetica table. Its README spells out exactly what it improves on and what it still doesn't do (it's closer to a real browser, not a full DOM layout pass) - read that before assuming it gives you pixel-exact pagination. See [Extending the Library](/docs/html/extending/#a-custom-itextmeasurer) for the `ITextMeasurer` contract sketch this sample implements. ## Where to go next - [Pagination and Layout](/docs/html/pagination-and-layout/) for how `Measure` results feed into page-break decisions. - [Extending the Library](/docs/html/extending/) for a concrete starting point if you're writing a custom measurer. - [FAQ: Why didn't my page break where I expected?](/docs/html/faq-troubleshooting/#why-didnt-my-page-break-exactly-where-i-expected) ==================================================================== TerraFluent.Html.Reporting — Cookbook URL: https://terrafluent.dev/docs/html/cookbook/ ==================================================================== # Cookbook Working recipes you can copy and adapt. Most are trimmed versions of the scenarios in [samples/TerraFluent.Html.Reporting.Sample/Scenarios](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios) - run `dotnet run --project samples/TerraFluent.Html.Reporting.Sample` to generate the full versions as HTML and open them in a browser. ## A Multi-Page Report with Header, Footer, and a Table That Spans Pages The "kitchen sink" example: a repeating header/footer with page numbers, a heading, a paragraph, an image, a 45-row table that spans several pages (header repeated, rows split correctly), and a numbered list. ```csharp using TerraFluent.Html.Reporting.Model; using TerraFluent.Html.Reporting.Model.Elements; var products = new[] { "Widget A", "Widget B", "Gadget C", "Gizmo D" }; var random = new Random(42); var report = ReportDocument.Create(PageSize.A4, PageOrientation.Portrait) .SetMargins(40, 40, 60, 60) .Header(h => h.AddText("Monthly Sales Report").AlignCenter().Bold().FontSize(16)) .Footer(f => f.AddPageNumber("Page {page} of {totalPages}").AlignCenter()) .Content(c => { c.AddHeading("Sales Summary", HeadingLevel.H1); c.AddParagraph( "This report summarizes sales activity across all regions for the current " + "period. The table below spans multiple pages: the header row repeats on " + "every continuation page, and a row too tall to fit is split mid-row."); c.AddImage("logo.png", widthPx: 72); c.AddRule(); c.AddHeading("Detailed Line Items", HeadingLevel.H2); c.AddTable(table => { table.AddColumns("Product", "Qty", "Revenue"); for (var i = 0; i < 45; i++) { var product = products[i % products.Length]; var qty = random.Next(10, 500); var revenue = qty * (decimal)(5 + random.NextDouble() * 45); table.AddRow(product, qty.ToString(), revenue.ToString("C2")); } }); c.AddSpacer(12); c.AddHeading("Notes", HeadingLevel.H2); c.AddList(ListStyle.Numbered, new[] { "Revenue figures are pre-tax.", "Contact the finance team for a region-level breakdown.", }); }) .Build(); string html = report.RenderHtml(); ``` See [Tables](/docs/html/tables/) for the column-width and row-split rules at play, and [Pagination and Layout](/docs/html/pagination-and-layout/) for why the header row repeats automatically. Full source: [`GettingStartedScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/GettingStartedScenario.cs). ## Comparing Table Row-Split Behaviors Put the same long row through both `TableStyle.RowSplitBehavior` options side by side, on a deliberately small custom page size so the row is forced to overflow: ```csharp using TerraFluent.Html.Reporting.Model.Styling; const string longNote = "This note is deliberately long, and the column it sits in deliberately " + "narrow, so it cannot fit in the remaining space and must be handled by " + "the table's row-split behavior."; var report = ReportDocument.Create(PageSize.FromPixels(650, 480)) .SetMargins(20) .Content(c => { c.AddHeading("AllowSplitWithContinuedHeader (the default)", HeadingLevel.H2); c.AddTable( table => { table.AddColumn("Item", widthPx: 80); table.AddColumn("Notes", widthPx: 220); table.AddRow("Item 1", "Short note."); table.AddRow("Item 2", longNote); table.AddRow("Item 3", "Another short note."); }, TableStyle.Default.With(rowSplitBehavior: RowSplitBehavior.AllowSplitWithContinuedHeader)); c.AddPageBreak(); c.AddHeading("KeepRowIntact", HeadingLevel.H2); c.AddTable( table => { table.AddColumn("Item", widthPx: 80); table.AddColumn("Notes", widthPx: 220); table.AddRow("Item 1", "Short note."); table.AddRow("Item 2", longNote); table.AddRow("Item 3", "Another short note."); }, TableStyle.Default.With(rowSplitBehavior: RowSplitBehavior.KeepRowIntact)); }) .Build(); ``` With `AllowSplitWithContinuedHeader`, "Item 2"'s note is truncated mid-sentence and continues on the next page under a header marked "(continued)". With `KeepRowIntact`, the whole row moves to the next page instead, leaving trailing whitespace on the first page. See [Tables: Row splitting](/docs/html/tables/#row-splitting-rowsplitbehavior). Full source: [`TableStylingScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/TableStylingScenario.cs). ## Grouping Rows with `RowSpan` A category cell spanning every line item beneath it, plus a `ColSpan`-based label on the closing totals row: ```csharp c.AddTable(table => { table.AddColumn("Category", widthPx: 110); table.AddColumn("Item"); table.AddColumn("Qty", widthPx: 50); table.AddColumn("Price", widthPx: 90); table.AddRow(new TableCell[] { new TableCell("Electronics") { RowSpan = 3 }, // spans this row and the next two "Wireless Mouse", "2", "$39.98", }); table.AddRow(new TableCell[] { "Mechanical Keyboard", "1", "$89.00" }); // omits "Category" - covered above table.AddRow(new TableCell[] { "USB-C Dock", "1", "$64.50" }); table.AddRow(new TableCell[] { new TableCell("Grand Total") { ColSpan = 3 }, // spans "Category", "Item", and "Qty" "$193.48", }); }); ``` A row underneath a `RowSpan` cell must omit a cell for the column(s) it covers - the `Table` constructor throws if a row supplies the wrong number of cells once spans are accounted for. The rows linked by the `RowSpan` are treated as one atomic unit during pagination: they move to the next page together if they don't all fit, even under `AllowSplitWithContinuedHeader`. See [Tables: Column and row spans](/docs/html/tables/#column-and-row-spans) for the full rules. Full source: [`TableSpansScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/TableSpansScenario.cs). ## A Header and Footer with a Logo Row A realistic invoice-style header (logo + company name side by side) and footer (small logo + a line of fine print + page number), using `AddRow` in both: ```csharp var report = ReportDocument.Create(PageSize.A4) .SetMargins(40) .Header(h => { h.AddRow(row => { row.AddColumn(48, col => col.AddImage("logo.png", widthPx: 40)) .Padding(topPx: 15, 0, 0, 0); row.AddColumn(col => { col.AddText("Acme Corporation").Bold().FontSize(20); col.AddText("123 Market Street, Springfield, USA").FontSize(12); }); }); h.AddRule(); }) .Footer(f => { f.AddRule(); f.AddRow(row => { row.AddColumn(32, col => col.AddImage("logo.png", widthPx: 24)); row.AddColumn(col => col.AddText("Payment is due within 30 days.").FontSize(10)); }); f.AddPageNumber().AlignCenter().FontSize(9); }) .Content(c => { /* ... */ }) .Build(); ``` The fixed-width logo column (`48px`/`32px`) leaves the company-name column to auto-share the rest of the content width; `RowVerticalAlignment.Middle` (the default) keeps the logo centered against the two-line text block next to it. See [Rows and Columns](/docs/html/rows-and-columns/). Full source: [`SalesInvoiceScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/SalesInvoiceScenario.cs) (also see [section A Complete Sales Invoice](#a-complete-sales-invoice) below for the rest of this report). ## A Barcode in the Header (Invoice Number) A Code 128 barcode of the invoice number, pinned to the top-right corner of the header next to the company info, using a fixed-width right-aligned column: ```csharp const string invoiceNumber = "20264471"; var report = ReportDocument.Create(PageSize.A4) .SetMargins(40) .Header(h => { h.AddRow(row => { row.AddColumn(col => { col.AddText("Acme Corporation").Bold().FontSize(20); col.AddText("123 Market Street, Springfield, USA").FontSize(12); }); row.AddColumn(300, col => { col.AddBarcode(invoiceNumber, moduleWidthPx: 2, heightPx: 40).AlignRight(); col.AddText(invoiceNumber, TextStyle.Default.With(alignment: TextAlignment.Right, marginBottomPx: 0)).FontSize(10); }); }, verticalAlignment: RowVerticalAlignment.Top); h.AddRule(); }) .Content(c => { /* ... */ }) .Build(); ``` The barcode column has a fixed `300px` width so it doesn't grow or shrink with the company-info column, and `.AlignRight()` pins the barcode (and the human-readable number printed underneath it) flush against the page's right margin. **The column must be at least as wide as the barcode's rendered width** (`quietZoneModules * 2 + sum of per-character module widths`, times `moduleWidthPx` - see [Content Elements: Barcode](/docs/html/content-elements/#barcode)) or the barcode overflows past the column - and past the page's right margin - instead of stopping at it; an 8-digit Code 128 value at the defaults shown here renders to 286px wide, so 300px leaves a small safety margin. `RowVerticalAlignment.Top` keeps both columns aligned to the top of the header instead of centering the shorter one. See [Content Elements: Barcode](/docs/html/content-elements/#barcode) for the encoding rules and parameters. Full source: [`InvoiceBarcodeScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/InvoiceBarcodeScenario.cs). ## Detecting Content That Doesn't Fit (`LayoutWarning`) An image taller than the entire page's content area, and unsplittable by definition, triggers a `LayoutWarning` instead of silently clipping or throwing: ```csharp using TerraFluent.Html.Reporting.Layout; var report = ReportDocument.Create(PageSize.FromPixels(400, 150)) .SetMargins(10) .Content(c => { c.AddHeading("Warnings", HeadingLevel.H2); c.AddParagraph("The image below is taller than this page's entire content area."); c.AddImage(oversizedImageBytes, "image/png", widthPx: 300, heightPx: 300); }) .Build(); var layout = LayoutEngine.Paginate(report); foreach (var warning in layout.Warnings) { Console.WriteLine(warning); // "Page 1: A ReportImage required 300px but only ... was available ..." } ``` Check `LayoutResult.Warnings` after pagination - e.g. to log a warning or reject the report before it reaches a user - rather than only discovering clipped content by eyeballing the rendered HTML. See [Pagination and Layout: Warnings](/docs/html/pagination-and-layout/#layoutwarning-when-content-doesnt-fit). Full source: [`WarningsAndAsyncScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/WarningsAndAsyncScenario.cs). ## Streaming a Large Report to Disk Asynchronously For a report large enough that holding the full HTML string in memory is undesirable, render straight to a file with the async, streaming API instead of `RenderHtml()`: ```csharp await report.RenderHtmlDocumentAsync( Path.Combine(outputDir, "report.html"), cancellationToken: cancellationToken); ``` This writes one page's HTML at a time rather than building the entire document as a single in-memory string, and the `async` signature exists so the final flush/dispose doesn't block a thread-pool thread in an async call chain (e.g. inside an ASP.NET request handler) - pagination and HTML generation themselves are still synchronous, CPU-bound work. See [Rendering: The APIs on `ReportDocument`](/docs/html/rendering/#the-apis-on-reportdocument). The sample project's [`Program.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Program.cs) uses this API for every scenario it writes out. ## A Landscape Certificate A single-page, landscape-oriented certificate, built entirely from centered content with no header/footer: ```csharp var report = ReportDocument.Create(PageSize.Letter, PageOrientation.Landscape) .SetMargins(50) .Content(c => { c.AddImage("logo.png", widthPx: 48); c.AddSpacer(30); c.AddHeading("Certificate of Completion", HeadingLevel.H1).AlignCenter(); c.AddSpacer(20); c.AddParagraph("This certifies that").AlignCenter(); c.AddHeading("Jane Doe", HeadingLevel.H2).AlignCenter(); c.AddParagraph("has successfully completed the TerraFluent.Html.Reporting advanced training course.").AlignCenter(); c.AddSpacer(40); c.AddRule(); c.AddParagraph("Issued June 23, 2026").AlignCenter(); }) .Build(); ``` `PageOrientation.Landscape` swaps `PageSize.Letter`'s width/height (see [Core Concepts: Page geometry](/docs/html/core-concepts/#page-geometry-pagesize-margins-orientation)). Full source: [`LandscapeCertificateScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/LandscapeCertificateScenario.cs). ## Images: File, Bytes, and Base64, with Aspect-Ratio Sizing ```csharp // From a file path, explicit width and height (stretched to exactly 240x80): c.AddImage("photo.png", widthPx: 240, heightPx: 80); // From bytes, only width given - height derived from the source's aspect ratio: c.AddImage(imageBytes, "image/png", widthPx: 360); // From bytes, only height given - width derived from the source's aspect ratio: c.AddImage(tallImageBytes, "image/png", heightPx: 200); // From a data: URI or bare base64 payload (Content only): c.AddImageFromBase64($"data:image/png;base64,{base64Source}", widthPx: 150, heightPx: 150); ``` See [Content Elements: Image](/docs/html/content-elements/#image) for how the missing dimension is derived (sniffed from the image's own header bytes) and what happens if the format can't be recognized. Full source: [`ImagesScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/ImagesScenario.cs). ## A Numbered List Spanning Multiple Pages ```csharp c.AddParagraph( "This list has enough items to split across a page boundary; numbering " + "resumes correctly on the next page instead of restarting at 1."); c.AddList(ListStyle.Numbered, Enumerable.Range(1, 60).Select(i => $"Numbered list entry #{i}")); ``` The list splits at item boundaries only (an item's own wrapped lines are never separated), and the continuation fragment's `StartIndex` keeps the `<ol start="...">` numbering correct. See [Pagination and Layout: Paragraph splitting](/docs/html/pagination-and-layout/#paragraph-splitting-widoworphan-control) for the related text-splitting rules. Full source: [`ListsScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/ListsScenario.cs). ## Forcing Chapter Breaks with `AddPageBreak` ```csharp c.AddHeading("Chapter 1: Introduction", HeadingLevel.H1); c.AddParagraph("..."); c.AddPageBreak(); c.AddHeading("Chapter 2: Methodology", HeadingLevel.H1); c.AddParagraph("..."); c.AddPageBreak(); ``` Each chapter starts on a fresh page regardless of how much room was left on the previous one. A page break with nothing yet placed on the page is a no-op, so this never produces a blank page between chapters even if a chapter happens to end exactly at a page boundary already. See [Content Elements: Page break](/docs/html/content-elements/#page-break). Full source: [`PageBreaksScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/PageBreaksScenario.cs). ## Injecting Raw HTML for Custom Markup ```csharp c.AddRawHtml( "<div style=\"border:2px dashed #2f4858;border-radius:8px;padding:16px;background:#eef3f6;\">" + "<strong>Custom callout box</strong><br/>This entire block is raw HTML supplied by the " + "caller, including its own inline styles.</div>", heightPx: 110); ``` You supply the height because the layout engine cannot measure markup it doesn't understand; it treats the block as opaque and unsplittable, exactly like an oversized image (see [section Detecting Content That Doesn't Fit](#detecting-content-that-doesnt-fit-layoutwarning) above if it doesn't fit). The HTML is emitted **verbatim** with no encoding - don't pass unsanitized end-user input here. See [Content Elements: Raw HTML](/docs/html/content-elements/#raw-html). Full source: [`RawHtmlScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/RawHtmlScenario.cs). ## A Complete Sales Invoice Putting headers/footers, rows, right-aligned table columns via per-cell style overrides, and running totals together: ```csharp using TerraFluent.Html.Reporting.Model.Styling; var rightAlign = TextStyle.Default.With(alignment: TextAlignment.Right, marginBottomPx: 0); var report = ReportDocument.Create(PageSize.A4) .SetMargins(40) .Header(h => { /* logo + company name row, see above */ }) .Footer(f => { /* logo + fine print row + page number, see above */ }) .Content(c => { c.AddHeading("INVOICE", HeadingLevel.H1).AlignCenter(); c.AddParagraph("Invoice #: INV-1042\nInvoice Date: June 23, 2026\nDue Date: July 23, 2026") .AlignRight().FontSize(11); c.AddSpacer(8); c.AddHeading("Bill To", HeadingLevel.H3); c.AddParagraph("Jane Doe\n456 Oak Avenue\nSpringfield, USA"); c.AddSpacer(20); c.AddTable(table => { table.AddColumn("Item"); table.AddColumn("Qty", widthPx: 50); table.AddColumn("Unit Price", widthPx: 100); table.AddColumn("Amount", widthPx: 100); table.AddRow(new TableCell[] { "Website Redesign", new TableCell("1", rightAlign), new TableCell("$1,200.00", rightAlign), new TableCell("$1,200.00", rightAlign), }); // ... more rows ... }); c.AddSpacer(12); c.AddParagraph("Subtotal: $2,300.00").AlignRight(); c.AddParagraph("Tax (8%): $184.00").AlignRight(); c.AddRule(); c.AddParagraph("Total: $2,484.00").AlignRight().Bold().FontSize(16); }) .Build(); ``` Note the `\n` inside `AddParagraph`'s text - `ITextMeasurer.Measure` treats explicit newlines as hard breaks, so a single `Paragraph` can hold multiple visually distinct lines (an address block, here) without needing several separate elements. See [Tables: Per-cell style overrides](/docs/html/tables/#per-cell-style-overrides) for the `rightAlign` pattern used on numeric columns. Full source: [`SalesInvoiceScenario.cs`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample/Scenarios/SalesInvoiceScenario.cs). ## Where to go next - [FAQ / Troubleshooting](/docs/html/faq-troubleshooting/) if something in your own report isn't behaving like these recipes. - [Extending the Library](/docs/html/extending/) if you need a capability none of these cover. ==================================================================== TerraFluent.Html.Reporting — Extending the Library URL: https://terrafluent.dev/docs/html/extending/ ==================================================================== # Extending the Library TerraFluent.Html.Reporting has three deliberate extension seams: the text measurer used for pagination, the renderer used to turn a layout into HTML, and the element contract itself. None of them require forking the library - each is a small interface the engine talks to abstractly. ## A custom `ITextMeasurer` Implement this when the bundled `ApproximateTextMeasurer` isn't precise enough for your needs - see [Text Measurement](/docs/html/text-measurement/) for why it's only approximate in the first place. ```csharp using TerraFluent.Html.Reporting.Measurement; public sealed class MyPreciseTextMeasurer : ITextMeasurer { public TextMeasurement Measure(string text, FontSpecification font, double maxWidthPx) { // 1. Resolve a real font/glyph source matching font.FontFamily, // font.FontSizePx, font.Bold, font.Italic - e.g. a headless // browser's measureText(), System.Drawing.Graphics.MeasureString // on Windows, or a text-shaping library. // 2. Word-wrap `text` (splitting on '\n' first - each segment is a // hard break, never wrapped through) so each resulting line's // measured width is <= maxWidthPx. // 3. Compute lineHeightPx = font.FontSizePx * font.LineHeightMultiplier // (or your engine's own metric, if more accurate) and the widest // line's width. var lines = WrapPreciselyWithYourEngine(text, font, maxWidthPx); var lineHeightPx = font.FontSizePx * font.LineHeightMultiplier; var widestLineWidthPx = lines.Count == 0 ? 0 : lines.Max(l => MeasureLineWidth(l, font)); return new TextMeasurement(lines, lineHeightPx, widestLineWidthPx); } } ``` Plug it in via `UseTextMeasurer`: ```csharp ReportDocument.Create(PageSize.A4) .UseTextMeasurer(new MyPreciseTextMeasurer()) .Content(c => { ... }) .Build(); ``` A few things to get right, since the layout engine trusts this contract completely: - **Determinism.** `Measure` may be called more than once for the same `(text, font, maxWidthPx)` triple - it must return the same result every time (no randomness, no mutable shared state that changes the outcome). - **Hard breaks.** Explicit `\n`/`\r\n` in `text` must never be wrapped through - each line they delimit is wrapped independently. - **`LineHeightPx` must be > 0** - `TextMeasurement`'s constructor throws `ArgumentOutOfRangeException` otherwise. - **Match what actually renders.** The whole point of a custom measurer is that its measurements agree with however the HTML is eventually displayed or printed - if you measure against one font but the rendered HTML's `font-family` resolves to a different one in the consumer's browser, you've just moved the mismatch rather than removed it. - **Package it separately if it has native/runtime dependencies.** The intended pattern (see [Text Measurement: Supplying a precise measurer](/docs/html/text-measurement/#supplying-a-precise-measurer)) is a companion NuGet package depending on the core library, keeping the core package's own dependency footprint at zero. A full, working example of this pattern - a headless-Chromium-backed measurer using Canvas 2D `measureText` for real font metrics - lives at [samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer) rather than only as a sketch here; its README documents exactly what it improves on versus the default and what it still doesn't capture. ## A custom `IHtmlReportRenderer` Implement this when you need different output shape than the bundled `HtmlReportRenderer` produces - a different page-wrapper structure, extra metadata embedded in the markup, or PDF-engine-specific tweaks. ```csharp using TerraFluent.Html.Reporting.Layout; using TerraFluent.Html.Reporting.Rendering; public sealed class MyCustomRenderer : IHtmlReportRenderer { public string RenderDocument(LayoutResult layout) { var writer = new StringWriter(); RenderDocumentTo(writer, layout); return writer.ToString(); } public string RenderFragment(LayoutResult layout) { var writer = new StringWriter(); RenderFragmentTo(writer, layout); return writer.ToString(); } public void RenderDocumentTo(TextWriter writer, LayoutResult layout, CancellationToken cancellationToken = default) { // Emit your own <html>/<head>/<style>, then delegate per-page // rendering to RenderFragmentTo (or reimplement it) so both // entry points share one page-rendering code path. } public void RenderFragmentTo(TextWriter writer, LayoutResult layout, CancellationToken cancellationToken = default) { var totalPages = layout.Pages.Count; foreach (var page in layout.Pages) { cancellationToken.ThrowIfCancellationRequested(); var renderContext = new RenderContext(page.PageIndex + 1, totalPages); // For each of page.HeaderElements / page.ContentElements / page.FooterElements: // var absolute = placed.Placement.Translate(offsetX, offsetY); // writer.Write(placed.Element.RenderHtml(absolute, renderContext)); // matching how HtmlReportRenderer.RenderElements does it - the // offsets you choose determine where each section sits on the page. } } } ``` Pass an instance to any of the five render methods on `ReportDocument`: ```csharp string html = report.RenderHtml(new MyCustomRenderer()); ``` The key insight a custom renderer needs to internalize: by the time it runs, **all positioning decisions are already made** - `LayoutResult` is a plain description of pages and placements (see [Pagination and Layout: The output](/docs/html/pagination-and-layout/#the-output-layoutresult)). A renderer's job is purely translation - section-relative coordinates to whatever coordinate system your output format wants - not pagination logic. `RenderContext(pageNumber, totalPages)` is what lets `PageNumberText` resolve its `{page}`/`{totalPages}` tokens; construct one per page with the correct 1-based page number. ## Implementing a new `IReportElement` Implement this when none of the built-in elements (see [Content Elements](/docs/html/content-elements/)) cover what you need, and `AddRawHtml`'s caller-supplied-height escape hatch isn't precise enough (e.g. you want the engine to measure and split your content automatically). ```csharp using TerraFluent.Html.Reporting.Layout; using TerraFluent.Html.Reporting.Model; using TerraFluent.Html.Reporting.Rendering; public sealed class Watermark : IReportElement { public string Text { get; } public double HeightPx { get; } public Watermark(string text, double heightPx) { Text = text; HeightPx = heightPx; } public ElementMeasurement Measure(LayoutContext context) => new(HeightPx); // Unsplittable, like ReportImage/HorizontalRule/Spacer - a watermark // band doesn't make sense torn across two pages. public SplitResult Split(double availableHeightPx, LayoutContext context) => SplitResult.Unsplittable(this); public string RenderHtml(ElementPlacement placement, RenderContext context) => "<div style=\"position:absolute;left:" + placement.XPx + "px;top:" + placement.YPx + "px;" + "width:" + placement.WidthPx + "px;height:" + placement.HeightPx + "px;" + "opacity:0.15;font-size:48px;text-align:center;\">" + Text + "</div>"; } ``` ```csharp ReportDocument.Create(PageSize.A4) .Content(c => c.AddElement(new Watermark("DRAFT", heightPx: 80))) .Build(); ``` Re-read [Pagination and Layout: The `IReportElement` contract](/docs/html/pagination-and-layout/#the-ireportelement-contract) before writing one of these - in particular: `Measure` must be pure and side-effect-free (the engine may call it repeatedly), `Split` is only ever called *after* `Measure` reported more height than is available, and if your element can be partially placed, the head/tail fragments your `Split` returns must themselves satisfy this same contract (they're just more `IReportElement` instances, often of the very same type with a trimmed-down payload - see how `Paragraph.Split` and `Table.Split` build their head/tail as new instances of themselves for the pattern to follow). If your content truly can't be partially placed, always return `SplitResult.Unsplittable(this)` - that's what makes the "force-place on an empty page + warn" fallback in the layout engine kick in correctly instead of looping. **HTML-encode any user-supplied text yourself** inside `RenderHtml` (mirror [`CssFormat.Encode`](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/src/TerraFluent.Html.Reporting/Rendering/CssFormat.cs)'s use of `WebUtility.HtmlEncode`) unless you specifically intend to emit raw markup - nothing upstream of your element does this for you. ## Where to go next - [Pagination and Layout](/docs/html/pagination-and-layout/) for the full measure/split/place algorithm your element or renderer plugs into. - [Text Measurement](/docs/html/text-measurement/) for the measurer contract in more detail. - [Rendering](/docs/html/rendering/) for exactly what the bundled renderer emits, if you're building a variation on it rather than starting from scratch. ==================================================================== TerraFluent.Html.Reporting — FAQ / Troubleshooting URL: https://terrafluent.dev/docs/html/faq-troubleshooting/ ==================================================================== # FAQ / Troubleshooting ## Why didn't my page break exactly where I expected? The default `ApproximateTextMeasurer` estimates text wrapping using generic Helvetica character-width tables, not the actual font/engine that will eventually render the HTML - so its line-wrap decisions are *close* to a real browser's but not guaranteed identical, especially for proportional fonts other than a plain sans-serif, or text with heavy kerning/ligatures. If your pagination needs to be pixel-exact, supply a custom `ITextMeasurer` backed by a real rendering engine via `UseTextMeasurer(...)`. See [Text Measurement](/docs/html/text-measurement/) and [Extending the Library: A custom `ITextMeasurer`](/docs/html/extending/#a-custom-itextmeasurer). ## My header/footer text has different spacing than my content text `PageSectionBuilder.AddText`/`AddPageNumber` (header/footer) and `RowColumnBuilder.AddText`/`AddHeading`/`AddPageNumber` (row columns) default to `marginBottomPx: 0`, while `ContentBuilder.AddParagraph`/`AddHeading` default to the normal `TextStyle.Default`/`TextStyle.ForHeading(level)` margins (`8px` bottom for body text, more for headings). This is intentional, not a bug - see [Styling: Margin vs. padding](/docs/html/styling/#margin-vs-padding). Chain `.MarginBottom(...)` explicitly if you want spacing that differs from the context's default. ## My image/row is positioned differently than I expected - **Alignment only matters when the container is wider than the content.** `AlignCenter()`/`AlignRight()` on an image position its box within whatever width is left over after margin/padding - if the image already fills the available width, alignment has no visible effect. - **Left/right margin shrinks the available width**, it doesn't just shift the element - an image with `MarginLeftPx: 20` inside a 200px-wide container effectively centers/right-aligns within 180px, not 200px. - **Row column padding doesn't change the column's resolved width** in the row's layout math - it only insets that column's own content within whatever width the column already got. See [Rows and Columns: Column padding](/docs/html/rows-and-columns/#column-padding). ## Why does my row/image/heading move to the next page instead of splitting? Only `Paragraph`, `ReportList`, and `Table` support partial splitting. `Heading`, `ReportImage`, `Row`, `HorizontalRule`, `Spacer`, `PageBreak`, `RawHtml`, and `PageNumberText` always move whole to the next page if they don't fit - see [Pagination and Layout: The `IReportElement` contract](/docs/html/pagination-and-layout/#the-ireportelement-contract). If one of these is also taller than an entire empty page's content area, it gets force-placed (overflowing visually) and recorded in `LayoutResult.Warnings` rather than dropped - see [Pagination and Layout: Warnings](/docs/html/pagination-and-layout/#layoutwarning-when-content-doesnt-fit). ## `AddTable`/`Table` throws `ArgumentException` about cell counts Every row passed to a table must supply exactly one cell per column. `table.AddColumns("A", "B")` followed by `table.AddRow("only one value")` throws, naming the offending row index. Check that every `AddRow(...)` call matches the column count from `AddColumns`/`AddColumn`. ## `LayoutEngine.Paginate` throws `InvalidOperationException` This means the page geometry leaves no room for content at all: - *"Left/right margins leave no horizontal room for content."* - `Margins.Left + Margins.Right >= PageSize.WidthPx`. - *"Margins and header/footer leave no vertical room for content."* - `Margins.Top + Margins.Bottom` plus the header's and footer's combined *measured* height (see [Pagination and Layout: Headers and footers](/docs/html/pagination-and-layout/#headers-and-footers)) is `>= PageSize.HeightPx`. Reduce margins, shrink the header/footer content, or use a larger page size. ## Does `PageOrientation.Landscape` work with a custom `PageSize.FromPixels(...)`? Yes - orientation is a plain width/height swap applied uniformly, regardless of how the `PageSize` was constructed. It is **not** a "force width > height" coercion: a custom size built via `FromPixels` rotates exactly the same way `A4`/`Letter`/`Legal` do. See [Core Concepts: Page geometry](/docs/html/core-concepts/#page-geometry-pagesize-margins-orientation). ## Can I nest a row inside a row, or a table inside a row column? No. `RowColumnBuilder` (what configures one column's content) deliberately exposes a smaller method set than `ContentBuilder` - no `AddRow`, `AddTable`, `AddList`, `AddPageBreak`, or `AddRawHtml`. See the comparison table in [Content Elements](/docs/html/content-elements/#three-different-builders-three-different-method-sets). If you need a table or list inside what's visually a row-like layout, consider `AddColumns` instead (a `MultiColumnSection` column *can* hold a table or list, unlike a `Row` column), `AddRawHtml` with a manually computed height, or restructure the content to avoid the nesting. See [Supported Composition Patterns](/docs/html/composition-patterns/) for the full picture across `Row`, `Table`, and `AddColumns`. ## Why does `RawHtml`/`Spacer` need an explicit height? The layout engine measures every element by calling `Measure`, which for ordinary elements computes height from the element's own content and style. `RawHtml` wraps markup the engine doesn't understand (it can't run a browser layout pass on it), and `Spacer` has no content to measure at all - both simply report back whatever height you constructed them with. If the content you put in `RawHtml` is actually taller than the height you supply, the engine still treats it as that height for pagination purposes - the `overflow:hidden` wrapper will clip anything taller for that block specifically, but pagination decisions for surrounding content won't see the real height. ## Is this thread-safe? `LayoutEngine.Paginate` and `ApproximateTextMeasurer` are stateless and safe to call concurrently, including for the same `ReportDocument` (it's immutable). `Table`/`Row` use an internal measurement cache that may redundantly recompute under concurrent pagination of the very same instance - a benign race, not a correctness issue. See [Pagination and Layout: Thread safety](/docs/html/pagination-and-layout/#thread-safety). ## Known limitations As of this release: - **Text measurement is approximate by default.** Exact, pixel-perfect pagination requires supplying a custom `ITextMeasurer` (see [Text Measurement](/docs/html/text-measurement/)) - none ships in the *core* package, though a reference Playwright-backed sample measurer exists at [samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer). - **Custom font embedding exists but is a manual data-URI embed, not a font subsystem** - `ReportDocumentBuilder.EmbedFont(...)` embeds a font file as a base64 `@font-face` rule; there's no font discovery, subsetting, or fallback-stack management beyond what you configure yourself. - **Multi-column page layout (`AddColumns`) is conservative by design** - equal-width columns only, fill-then-wrap (no balanced/equal-height rebalancing), no column-spanning elements, and it can't nest inside itself or inside a `Row` - see [Rows and Columns: Multi-column sections](/docs/html/rows-and-columns/#multi-column-sections-addcolumns). - **A table `RowSpan` group can't split across a page break** - it either fits together on a page or moves to the next page as a whole; see [Tables: Column and row spans](/docs/html/tables/#column-and-row-spans). - **Right-to-left (RTL) text support covers `dir`/`direction` only** - `TextStyle.Direction` is independent of `TextAlignment`/margin/padding, which stay physical (`Left`/`Right`), not logical (`Start`/`End`) - see [Styling: Text direction](/docs/html/styling/#text-direction) for what to set explicitly for a fully mirrored layout. - **Rows don't nest, and row columns can't contain a table, list, nested row, page break, or raw HTML** - see [Supported Composition Patterns](/docs/html/composition-patterns/) for the full picture (including what `AddColumns` columns *can* hold instead). - **No shrink-to-fit for table/row columns** when fixed-width columns already exceed the available width - auto columns are pinned to `0`. This is no longer silent: it records a `LayoutWarning` (`LayoutWarningReason.ColumnWidthCollapsed`), and you can opt into throwing instead via `TableStyle`/`RowStyle`'s `ColumnWidthOverflowMode` or document-wide via `ReportDocumentBuilder.UseStrictLayoutValidation()`. Check [CHANGELOG.md](https://github.com/sahebansari/TerraFluent.Html.Reporting/blob/master/CHANGELOG.md) for what's changed most recently. ## Where to go next - [Pagination and Layout](/docs/html/pagination-and-layout/) for the full algorithm behind most of the answers above. - [Extending the Library](/docs/html/extending/) if the built-in elements/measurer/renderer genuinely don't cover your case. ==================================================================== TerraFluent.Html.Reporting — Supported Composition Patterns URL: https://terrafluent.dev/docs/html/composition-patterns/ ==================================================================== # Supported Composition Patterns The library has several container-like elements - `Row`/`RowColumn`, `Table`, and `MultiColumnSection` (via `AddColumns`) - and each one deliberately supports a *narrower* set of nested content than the top-level `ContentBuilder` does. This page is the single reference for what's supported where, so you don't have to piece it together from each container's own doc page or discover a restriction by hitting an exception. ## Quick reference | Container | Can contain | Cannot contain | |---|---|---| | `Content`/`Header`/`Footer` (`ContentBuilder`/`PageSectionBuilder`) | Everything: paragraphs, headings, images, barcodes, QR codes, tables, lists, rules, spacers, raw HTML, rows, multi-column sections, page breaks (content only) | - | | `Row` column (`RowColumnBuilder`) | Text, headings, page-number text, images, barcodes, QR codes, rules, spacers | Tables, lists, raw HTML, nested rows, page breaks, multi-column sections | | `Table` cell | Plain text only (`TableCell`) | Any element - a cell is text, not a container | | `MultiColumnSection` column (`ColumnsBuilder`, via `AddColumns`) | Paragraphs, headings, images, barcodes, QR codes, tables, lists, rules, spacers, raw HTML | Rows, nested multi-column sections, page breaks | Two patterns recur across every restricted container: **no page breaks** (a page break is a page-level concept, not a within-container one) and **no self-nesting** (`Row` can't contain another `Row`; `MultiColumnSection` can't contain another `MultiColumnSection`). ## Why the restrictions exist Each container's restrictions come from what it needs to guarantee about its own layout, not an arbitrary limitation: - **`Row` never splits across pages** (see [Rows and Columns](/docs/html/rows-and-columns/)) - so it deliberately excludes content that *needs* to split (a long table, a long list) or that only makes sense at the page level (a page break). Nesting a row inside a row would also make "never splits" apply transitively to arbitrarily complex content, which stops being a useful guarantee. - **A table cell is text, not a container** - `TableCell`'s whole measurement/splitting model (shared line budgets across a row, see [Tables: Column and row spans](/docs/html/tables/#column-and-row-spans)) assumes plain wrapped text, not arbitrary nested layout. - **`MultiColumnSection` fills columns dynamically based on remaining space** (see [Rows and Columns: Multi-column sections](/docs/html/rows-and-columns/#multi-column-sections-addcolumns)) - a nested multi-column section would need to answer "how tall is a column's worth of *columns*," which has no well-defined answer; a `Row` inside a column would need to answer "what happens when a row that never splits doesn't fit in the remaining column space," which is exactly the kind of edge case the "no column-spanning elements" v1 scope avoids. ## Combining containers: what already works well These are the patterns worth reaching for first, in order of how often real reports need them: - **A `Row` in the header/footer, ordinary content below it.** The most common pattern - a logo next to a company name in the header, plain paragraphs/tables in the body. See [Cookbook: A letterhead with a logo](/docs/html/cookbook/). - **A `Table` after a `Row`.** A row for a summary strip (e.g. three centered stat numbers), followed by an ordinary table for the detail data - two independent top-level elements, not nested. - **A barcode/QR code inside a `Row` column** (e.g. an invoice number barcode next to the company name) - both are just images once created, so every image modifier (`AlignCenter`, `Margin`, ...) works on them inside a column exactly like it does at the top level. See [Cookbook: A barcode in the header](/docs/html/cookbook/#a-barcode-in-the-header-invoice-number). - **A `Table` inside a `MultiColumnSection` column** - a genuinely new capability multi-column sections add: unlike a `Row` column, a `MultiColumnSection` column can hold a table, and that table can even split across the column boundary the same way it splits across a page boundary. Useful for a two-column glossary/reference layout where one column happens to need a small table. ## If you need something not on this list If your content doesn't fit one of the supported patterns above, two escape hatches exist rather than trying to force a nested container to do something it wasn't designed for: - **`AddRawHtml(html, heightPx)`** - inject arbitrary markup at a caller-supplied height when you need a layout the built-in containers don't support. See [FAQ: Why does RawHtml/Spacer need an explicit height?](/docs/html/faq-troubleshooting/#why-does-rawhtmlspacer-need-an-explicit-height). - **Restructure as independent top-level elements.** Most "I want to nest X inside Y" needs are actually satisfied by two separate top-level elements placed next to each other in document order (e.g. a `Row` for a header strip, then a `Table` immediately after it) rather than true nesting - see the combining patterns above. ## Where to go next - [Rows and Columns](/docs/html/rows-and-columns/) for `Row`/`RowColumn` and `MultiColumnSection` in full detail. - [Tables](/docs/html/tables/) for what a `TableCell` supports. - [FAQ: Can I nest a row inside a row?](/docs/html/faq-troubleshooting/#can-i-nest-a-row-inside-a-row-or-a-table-inside-a-row-column) for the original version of this question, answered inline. ==================================================================== TerraFluent.Docx.Reporting — Getting Started URL: https://terrafluent.dev/docs/docx/getting-started/ ==================================================================== # Getting Started This guide gets you from an empty project to a generated `.docx` report. ## Requirements - .NET Framework 4.6.1+, .NET Core 2.0+, or any modern .NET (5-10) runtime — the package targets `netstandard2.0` with a dedicated `net10.0` build. - A project that can reference NuGet packages. - Microsoft Word, LibreOffice, or another DOCX reader for visual validation. TerraFluent.Docx.Reporting does not require Microsoft Word to be installed on the machine that generates documents. ## Install ```powershell dotnet add package TerraFluent.Docx.Reporting ``` Use the namespace: ```csharp using TerraFluent.Docx.Reporting; ``` Use `TerraFluent.Docx.Reporting.Infra` when you implement reusable components: ```csharp using TerraFluent.Docx.Reporting.Infra; ``` ## Create Your First Document ```csharp using TerraFluent.Docx.Reporting; Document.Create(doc => { doc.MetadataTitle("Quarterly Report") .MetadataAuthor("Northwind Consulting") .MetadataSubject("Q4 performance summary") .MetadataKeywords("quarterly report, finance, operations") .MetadataCreator("TerraFluent.Docx.Reporting"); doc.Page(page => { page.Size(PageSize.A4); page.Margin(Unit.Centimetre(2)); page.Header().Text("Quarterly Report", text => text .Bold() .FontColor(Colors.Blue.L800) .AlignCenter()); page.Footer().Text(text => { text.Span("Page "); text.CurrentPageNumber(); text.Span(" of "); text.TotalPages(); text.AlignCenter().FontSize(9).FontColor(Colors.Grey.L600); }); page.Content().H1("Executive Summary"); page.Content().Text("Revenue improved across every practice."); page.Content().Text("The report was generated without automating Microsoft Word."); }); }).PublishDocx("quarterly-report.docx"); ``` ## Write To A File, Byte Array, Or Stream ```csharp var document = Document.Create(doc => { doc.Page(page => page.Content().Text("Portable output options.")); }); document.PublishDocx("output.docx"); byte[] bytes = document.PublishDocx(); using var stream = File.Create("stream-output.docx"); document.PublishDocx(stream); ``` ## Add A Second Section Each `doc.Page(...)` call adds a new section. Use separate sections when page size, orientation, headers, footers, columns, or page numbering should change. ```csharp Document.Create(doc => { doc.Page(page => { page.Size(PageSize.A4); page.Content().H1("Portrait Summary"); }); doc.Page(page => { page.Size(PageSize.A4).Landscape(); page.Content().H1("Landscape Appendix"); }); }).PublishDocx("multi-section.docx"); ``` ## Next Steps - Learn the mental model in [Core Concepts](/docs/docx/core-concepts/). - Copy larger examples from the [Feature Guide](/docs/docx/features/). - Browse runnable sample files in [Samples](/docs/docx/samples/). ==================================================================== TerraFluent.Docx.Reporting — Core Concepts URL: https://terrafluent.dev/docs/docx/core-concepts/ ==================================================================== # Core Concepts TerraFluent.Docx.Reporting uses a fluent builder model. You compose a document by configuring descriptors: document, page, container, table, row, cell, image, barcode, chart, and text descriptors. ## Document Flow ```csharp Document.Create(doc => { doc.Theme(theme => { }); doc.ParagraphStyle("Name", text => { }); doc.TableStyle("Name", table => { }); doc.Page(page => { page.Header().Text("Header"); page.Content().H1("Title"); page.Footer().Text("Footer"); }); }); ``` The document owns metadata, themes, reusable styles, and one or more pages. Pages own layout settings and containers. Containers own content. ## Units The layout unit is points. One inch is 72 points. ```csharp page.Margin(Unit.Centimetre(2)); page.Margin(Unit.Inch(0.75f)); page.Content().Image("logo.png", img => img.Width(Unit.Millimetre(35))); ``` Use `Unit.Point(value)` when you already have point values. ## Colors Colors are six-character hex RGB strings without `#`. ```csharp text.FontColor("1F4E79"); table.Border(0.75f, Colors.Grey.L300); page.Watermark("DRAFT", Colors.Grey.L300, 72); ``` Built-in palettes include `Black`, `White`, `Grey`, `Blue`, `Red`, `Green`, and `Orange`. ## Themes Themes configure defaults for a document. ```csharp doc.Theme(theme => theme .DefaultFont("Aptos", 10.5f) .DefaultTextColor(Colors.Grey.L900) .HeadingColor(Colors.Blue.L800) .AccentColor(Colors.Green.L700) .HyperlinkColor(Colors.Blue.L700) .TableHeaderBackground(Colors.Blue.L800) .TableAlternateRowBackground(Colors.Grey.L100) .TableBorder(0.5f, Colors.Grey.L300) .TableCellPadding(4, 6)); ``` ## Custom Styles Register named styles once and apply them later. ```csharp doc.ParagraphStyle("Callout", text => text .Shading(Colors.Blue.L100) .BorderLeft(4, Colors.Blue.L700) .LeftIndent(12) .RightIndent(12) .SpacingBefore(6) .SpacingAfter(6)); doc.TableStyle("FinancialTable", table => table .WidthPercent(100) .HeaderBackground(Colors.Blue.L800) .AlternateRowBackground(Colors.Grey.L100) .CellPadding(4, 6) .Border(0.5f, Colors.Grey.L300)); doc.Page(page => { page.Content().Text("Important board note.", t => t.Style("Callout")); page.Content().Table(table => table.Style("FinancialTable")); }); ``` ## Containers Containers are reusable content surfaces. You will see them in: - `page.Header()`, `page.Content()`, and `page.Footer()`. - Row items created with `row.RelativeItem()`, `row.AutoItem()`, and `row.ConstantItem(...)`. - Column items created with `column.Item()`. - Table cells created with `row.Cell()`. Because table cells are containers, you can nest headings, text, lists, images, barcodes, and tables inside cells. ## Sections And Page Settings Every `doc.Page(...)` call creates a section. Use sections to change orientation, margins, headers, footers, columns, watermarks, and page numbering. ```csharp doc.Page(page => { page.Size(PageSize.A4).Portrait(); page.Content().Text("Normal section."); }); doc.Page(page => { page.Size(PageSize.A4).Landscape(); page.Columns(2, spacingPoints: 24, separatorLine: true); page.Content().Text("Landscape two-column appendix."); }); ``` ## Reusable Components Implement `IComponent` for reusable blocks. ```csharp using TerraFluent.Docx.Reporting.Infra; public sealed class StatusBanner : IComponent { private readonly string _message; public StatusBanner(string message) { _message = message; } public void Compose(IContainer container) { container.Text(_message, text => text .Bold() .Shading(Colors.Green.L100) .BorderLeft(4, Colors.Green.L700) .LeftIndent(12) .SpacingAfter(8)); } } page.Content().Component(new StatusBanner("All systems operational.")); ``` ## Validation Mindset TerraFluent.Docx.Reporting writes Open XML directly. The test suite validates generated packages with the Open XML SDK, but final report layouts should still be visually checked in Word or LibreOffice before a public release. ==================================================================== TerraFluent.Docx.Reporting — Feature Guide URL: https://terrafluent.dev/docs/docx/features/ ==================================================================== # Feature Guide This guide shows practical examples for the main document features. ## Document Metadata ```csharp Document.Create(doc => { doc.MetadataTitle("Annual Report") .MetadataAuthor("Contoso Finance") .MetadataSubject("FY2026 performance") .MetadataKeywords("annual report, finance, operations") .MetadataCreator("TerraFluent.Docx.Reporting"); }); ``` ## Headers, Footers, And Page Numbers ```csharp doc.Page(page => { page.FirstPageHeader().Text("Annual Report", t => t.Bold().AlignCenter()); page.EvenPageHeader().Text("Contoso", t => t.AlignLeft().FontSize(9)); page.OddPageHeader().Text("FY2026", t => t.AlignRight().FontSize(9)); page.Footer().Text(t => { t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages(); t.AlignCenter().FontSize(9); }); }); ``` ## Page Layout ```csharp doc.Page(page => { page.Size(PageSize.A4) .Margin(Unit.Centimetre(2)) .Background("FAFAFA") .Watermark("DRAFT", Colors.Grey.L300, 72); page.Content().Text("The page background is document-wide in Word."); }); doc.Page(page => { page.Size(PageSize.A4).Landscape(); page.Columns(2, spacingPoints: 24, separatorLine: true); page.Content().H1("Two Column Appendix"); page.Content().Text("Content flows from the first column to the second."); }); ``` ## Text And Rich Runs ```csharp page.Content().Text(text => { text.Span("Revenue ").Bold(); text.Span("increased 12%").FontColor(Colors.Green.L700).Bold(); text.Span(" year over year."); text.SpacingAfter(8).KeepLinesTogether(); }); page.Content().Text("Important policy note.", text => text .Shading(Colors.Orange.L100) .BorderLeft(4, Colors.Orange.L700) .LeftIndent(14) .RightIndent(14) .SpacingBefore(6) .SpacingAfter(6)); ``` ## Links, Bookmarks, Cross References, And Notes ```csharp page.Content().Bookmark("revenue-section", "Revenue", text => text.Bold()); page.Content().Text(text => { text.Span("See "); text.CrossReference("revenue-section", "the revenue section"); text.Span(" for details."); }); ``` `H1` through `H6` don't accept a bookmark name directly. To bookmark a heading, use `Bookmark` with `Style` instead of `H1`-`H6`: ```csharp page.Content().Bookmark("operations-section", "Operations", text => text.Style("Heading1")); page.Content().Text(text => { text.Span("External reference: "); text.Hyperlink("company site", "https://example.com", link => link.FontColor(Colors.Blue.L700)); }); page.Content().Text(text => { text.Span("Net revenue excludes discontinued products."); text.Footnote("A discontinued product is excluded after the final shipment date."); }); ``` ## Lists ```csharp page.Content().BulletList(list => { list.Marker(">"); list.Marker("-", level: 1); list.Item("Prepare source data"); list.Item("Validate totals", level: 1, text => text.FontColor(Colors.Green.L700)); list.Item("Generate final report"); }); page.Content().NumberedList(list => { list.Item("Open the generated document in Word."); list.Item("Confirm there are no repair prompts."); list.Item("Export to PDF if needed."); }); ``` ## Tables ```csharp page.Content().Table(table => { table.WidthPercent(100) .CellPadding(4, 6) .HeaderBackground(Colors.Blue.L800) .AlternateRowBackground(Colors.Grey.L100) .Border(0.5f, Colors.Grey.L300); table.ColumnsDefinition(cols => { cols.RelativeColumn(3); cols.ConstantColumn(90); cols.ConstantColumn(90); }); table.HeaderRow(row => { row.KeepTogether(); row.Cell().Text("Metric", t => t.Bold().FontColor(Colors.White.Default)); row.Cell().Text("Q3", t => t.Bold().FontColor(Colors.White.Default).AlignRight()); row.Cell().Text("Q4", t => t.Bold().FontColor(Colors.White.Default).AlignRight()); }); table.Row(row => { row.Cell().Text("Revenue"); row.Cell().Text("$4.1M", t => t.AlignRight()); row.Cell().Text("$4.8M", t => t.AlignRight().Bold()); }); }); ``` ### Spans And Vertical Merges ```csharp page.Content().Table(table => { table.ColumnsDefinition(cols => { cols.RelativeColumn(); cols.RelativeColumn(); cols.RelativeColumn(); }); table.Row(row => { row.Cell(3).Background(Colors.Blue.L100).Text("Regional Summary", t => t.Bold().AlignCenter()); }); table.Row(row => { row.Cell().VerticalMergeStart().Text("North"); row.Cell().Text("Revenue"); row.Cell().Text("$2.3M"); }); table.Row(row => { row.Cell().VerticalMergeContinue(); row.Cell().Text("Margin"); row.Cell().Text("31%"); }); }); ``` ## Images ```csharp page.Content().Image("logo.png", image => image .Width(120) .AltText("Company logo") .AlignCenter() .Caption("Figure 1. Company logo")); page.Content().Image(File.ReadAllBytes("photo.png"), "photo.png", image => image .Width(160) .WrapSquare(8) .FloatRight(8) .Border(1, Colors.Grey.L400) .Rounded() .Crop(4, 4, 4, 4)); ``` ## Barcodes ```csharp page.Content().Barcode("SKU-00100011"); page.Content().Barcode("ACME-99887766", bc => bc .Width(220) .Height(50) .BarColor(Colors.Blue.L700) .AlignCenter() .Caption("Figure 1. Product tracking code")); page.Content().Barcode("Internal-Only", bc => bc.ShowText(false)); ``` Barcodes encode text as Code 128 and render as vector bars (not raster images), so they stay crisp at any size. Only ASCII 32-126 (space through `~`) can be encoded; anything else throws `ArgumentException` immediately. ## QR Codes ```csharp page.Content().QrCode("https://example.com"); page.Content().QrCode("WIFI:T:WPA;S:MyNetwork;P:secret123;;", qr => qr .Size(140) .ErrorCorrection(QrErrorCorrectionLevel.High) .ForegroundColor(Colors.Blue.L800) .AlignCenter() .Caption("Guest Wi-Fi")); ``` QR codes encode arbitrary UTF-8 text and, like barcodes, render as vector shapes - no raster image, no media part, crisp at any size - with a built-in quiet zone so they scan reliably by default. The smallest symbol version (1-40) that fits the payload at the requested error correction level is chosen automatically; a payload too long for even the largest version at that level throws `ArgumentException` immediately. Error correction levels (`QrErrorCorrectionLevel`), from least to most redundant: `Low` (~7% damage tolerance), `Medium` (~15%, the default), `Quartile` (~25%), `High` (~30%). Higher levels produce a larger symbol for the same payload in exchange for more resilience to damage or obstruction (e.g. an overlaid logo, at High or Quartile). ## Charts ```csharp page.Content().Chart(chart => chart .Title("Quarterly Revenue") .Series("Revenue", series => series .Bar("Q1", 4.1) .Bar("Q2", 4.3) .Bar("Q3", 4.8) .Bar("Q4", 5.2) .Color(Colors.Green.L700))); ``` Line chart: ```csharp page.Content().Chart(chart => chart .Title("Customer Growth") .Series("Customers", series => series .Line("Jan", 120) .Line("Feb", 132) .Line("Mar", 150) .Color(Colors.Blue.L700))); ``` Pie chart: ```csharp page.Content().Chart(chart => chart .Title("Revenue Mix") .Series(series => series .Pie("Consulting", 45) .Pie("Support", 30) .Pie("Licensing", 25) .Color(Colors.Orange.L700))); ``` Legend placement, axis titles, data labels, and stacked bars: ```csharp page.Content().Chart(chart => chart .Title("Stacked Revenue by Quarter") .Width(430) // points; default is 432 x 252 (6 x 3.5 in) .Height(250) .AlignCenter() .Legend(ChartLegendPosition.Bottom) // or .HideLegend() .CategoryAxisTitle("Quarter") .ValueAxisTitle("Revenue ($M)") .DataLabels() .Stacked() // or .PercentStacked(); bar charts only .Series("Product", s => s.Bar("Q1", 4.1).Bar("Q2", 4.3).Color(Colors.Blue.L700)) .Series("Services", s => s.Bar("Q1", 2.2).Bar("Q2", 2.6).Color(Colors.Orange.L700))); ``` ## Rows And Columns ```csharp page.Content().Row(row => { row.Spacing(12); row.RelativeItem(2).Text("Main narrative column."); row.RelativeItem(1).Text("Sidebar", text => text .Shading(Colors.Grey.L100) .Border(0.5f, Colors.Grey.L300) .LeftIndent(8) .RightIndent(8)); }); page.Content().Column(column => { column.Spacing(6); column.Item().H2("Stacked Content"); column.Item().Text("First block."); column.Item().Text("Second block."); }); ``` ## Table Of Contents ```csharp page.Content().TableOfContents("Contents", minLevel: 1, maxLevel: 3); page.Content().PageBreak(); page.Content().H1("Executive Summary"); page.Content().H2("Revenue"); page.Content().H2("Operations"); ``` Word updates TOC fields when the document is opened or when fields are refreshed. ## Auto-Numbered Captions And Table Of Figures `FigureCaption` (images) and `Caption` (tables) insert Word `SEQ` fields, so Word renumbers captions automatically and can collect them into a table of figures: ```csharp page.Content().TableOfFigures(); // lists Figure captions page.Content().TableOfFigures("List of Tables", "Table"); // lists Table captions page.Content().Image("chart.png", img => img .Width(200) .FigureCaption("Revenue growth chart")); // "Figure 1. Revenue growth chart" page.Content().Table(t => { t.Caption("Quarterly results by region"); // "Table 1. Quarterly results by region", above the table t.ColumnsDefinition(d => { d.RelativeColumn(1); d.RelativeColumn(1); }); t.Row(r => { r.Cell().Text("North"); r.Cell().Text("$4.1M"); }); }); ``` Figure captions render below the image; table captions render above the table. The static `Caption(...)` image overload remains available for unnumbered captions. ## Restrict Editing `RestrictEditing` applies Word's "Restrict Editing" protection, optionally guarded by a password: ```csharp Document.Create(doc => { doc.RestrictEditing(DocumentProtection.ReadOnly, "secret123"); doc.Page(page => page.Content().Text("Read-only content.")); }); ``` Modes: `ReadOnly`, `CommentsOnly`, `TrackedChangesOnly`, and `FormsOnly`. The password (first 15 characters) is stored as a salted, spun SHA-512 verifier per ISO/IEC 29500. This is a guard rail, not security: the file is not encrypted, and a malicious tool can strip the setting. ## Templates Use templates when a `.docx` already exists and you only need to replace placeholders or content controls. ```csharp DocxTemplate.Open("invoice-template.docx") .Replace("{{CustomerName}}", "Ada Lovelace") .Replace("{{InvoiceTotal}}", "$1,250.00") .ReplaceContentControl("PaymentTerms", "Net 30") .SaveAs("invoice-output.docx"); ``` To return bytes: ```csharp byte[] output = DocxTemplate.Open("template.docx") .Replace("{{Name}}", "Grace Hopper") .Save(); ``` ==================================================================== TerraFluent.Docx.Reporting — API Reference URL: https://terrafluent.dev/docs/docx/api/ ==================================================================== # API Reference This page lists the public fluent API exposed by `TerraFluent.Docx.Reporting`. ## Public API Contract TerraFluent.Docx.Reporting validates public inputs before writing packages. Null callbacks, empty paths or names, negative sizes or margins, unreadable streams, missing image files, empty image byte arrays, and barcode values outside ASCII 32-126 throw standard .NET exceptions such as `ArgumentException`, `ArgumentOutOfRangeException`, `ArgumentNullException`, or `FileNotFoundException`. The fluent API clamps only where the Open XML concept is naturally bounded and documented by the API. Otherwise invalid input fails fast so production callers can catch configuration mistakes before distributing a damaged document. ## Namespace ```csharp using TerraFluent.Docx.Reporting; using TerraFluent.Docx.Reporting.Infra; ``` Most users only need `TerraFluent.Docx.Reporting`. Add `TerraFluent.Docx.Reporting.Infra` when implementing reusable components or receiving descriptor interfaces in your own helper methods. ## Document | API | Purpose | | --- | --- | | `Document.Create(Action<IDocumentContainer> configure)` | Builds a document with the fluent descriptor API. | | `PublishDocx(string filePath)` | Writes a `.docx` file to disk. | | `PublishDocx()` | Returns the generated `.docx` package as a byte array. | | `PublishDocx(Stream stream)` | Writes the generated `.docx` package to a stream. | ```csharp var document = Document.Create(doc => { doc.MetadataTitle("Quarterly Report"); doc.Page(page => page.Content().Text("Hello from TerraFluent.Docx.Reporting.")); }); document.PublishDocx("report.docx"); byte[] bytes = document.PublishDocx(); ``` ## Document Container `IDocumentContainer` is the root builder received by `Document.Create`. | API | Purpose | | --- | --- | | `Theme(DocumentTheme theme)` | Applies an existing theme object. | | `Theme(Action<IDocumentThemeDescriptor> configure)` | Configures the document theme inline. | | `ParagraphStyle(string name, Action<ITextDescriptor> configure)` | Registers a reusable paragraph style. | | `TableStyle(string name, Action<ITableDescriptor> configure)` | Registers a reusable table style. | | `Page(Action<IPageDescriptor> configure)` | Adds a document section/page definition. | | `MetadataTitle`, `MetadataAuthor`, `MetadataSubject`, `MetadataKeywords`, `MetadataCreator` | Sets package metadata. | | `RestrictEditing(DocumentProtection protection, string? password = null)` | Restricts editing in Word (read-only, comments-only, tracked-changes-only, or forms-only), optionally requiring a password to stop the protection. Not encryption - the file content remains readable. | | `Compose(IDocument document)` | Composes a reusable document module. | ```csharp Document.Create(doc => { doc.MetadataTitle("Board Pack") .MetadataAuthor("Finance Team") .MetadataCreator("TerraFluent.Docx.Reporting") .Theme(theme => theme .DefaultFont("Aptos", 10.5f) .HeadingColor(Colors.Blue.L800) .AccentColor(Colors.Green.L700)); doc.ParagraphStyle("FinePrint", text => text.FontSize(8).FontColor(Colors.Grey.L600)); }); ``` ## Theme | API | Purpose | | --- | --- | | `DefaultFont(string family, float size = 11)` | Sets default family and size. | | `DefaultFontFamily(string family)` | Sets only the default family. | | `DefaultFontSize(float size)` | Sets only the default size. | | `DefaultTextColor(string hexColor)` | Sets default body text color. | | `HeadingColor(string hexColor)` | Sets heading color. | | `AccentColor(string hexColor)` | Sets accent color. | | `HyperlinkColor(string hexColor)` | Sets hyperlink color. | | `TableHeaderBackground(string hexColor)` | Sets default table header fill. | | `TableAlternateRowBackground(string hexColor)` | Sets default alternating row fill. | | `TableBorder(float width, string hexColor)` | Sets default table border style. | | `TableCellPadding(float points)` | Sets uniform table cell padding. | | `TableCellPadding(float verticalPoints, float horizontalPoints)` | Sets vertical and horizontal cell padding. | | `TableRowMinHeight(float points)` | Sets default body row minimum height. | | `TableHeaderRowMinHeight(float points)` | Sets default header row minimum height. | ## Page | API | Purpose | | --- | --- | | `Size(PageSize size)` / `Size(float widthPoints, float heightPoints)` | Sets page size in points. | | `Landscape()` / `Portrait()` | Changes orientation. | | `Margin(...)`, `MarginTop`, `MarginRight`, `MarginBottom`, `MarginLeft` | Sets margins in points. | | `DefaultTextStyle(Action<ITextDescriptor> configure)` | Sets section default text style. | | `PageNumberStart(int value)` | Starts page numbering for the section. | | `PageNumberFormat(string format)` | Writes an OOXML number format such as `decimal`, `lowerRoman`, or `upperLetter`. | | `Background(string hexColor)` | Sets document page background color. Word stores one document-wide background. | | `Watermark(string text, string hexColor = "D9D9D9", float fontSize = 54)` | Adds a section watermark. | | `Columns(int count, float spacingPoints = 36, bool separatorLine = false)` | Enables newspaper-style columns. | | `SingleColumn()` | Returns the section to one column. | | `Header`, `OddPageHeader`, `FirstPageHeader`, `EvenPageHeader` | Returns header containers. | | `Content()` | Returns the body content container. | | `Footer`, `OddPageFooter`, `FirstPageFooter`, `EvenPageFooter` | Returns footer containers. | ```csharp doc.Page(page => { page.Size(PageSize.A4) .Margin(Unit.Centimetre(2)) .PageNumberStart(1) .PageNumberFormat("decimal") .Watermark("DRAFT", Colors.Grey.L300, 72); page.Header().Text("Confidential", t => t.AlignCenter().FontSize(9)); page.Footer().Text(t => { t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages(); t.AlignCenter(); }); }); ``` ## Container `IContainer` is used for page body content, headers, footers, columns, row items, and table cells. | API | Purpose | | --- | --- | | `H1` through `H6` | Adds Word heading paragraphs. | | `Column(Action<IColumnDescriptor>)` | Adds a vertical stack of content. | | `Row(Action<IRowDescriptor>)` | Adds a horizontal layout row. | | `Text(string, Action<ITextDescriptor>? configure = null)` | Adds a paragraph. | | `Text(Action<ITextDescriptor> configure)` | Adds a rich paragraph with multiple runs. | | `Hyperlink(string text, string url, Action<ITextDescriptor>? configure = null)` | Adds a hyperlink paragraph. | | `Bookmark(string name)` | Adds an invisible bookmark anchor. | | `Bookmark(string name, string text, Action<ITextDescriptor>? configure = null)` | Adds a visible bookmarked paragraph. | | `TableOfContents(string title = "Contents", int minLevel = 1, int maxLevel = 3)` | Adds a Word TOC field. | | `TableOfFigures(string title = "Table of Figures", string captionLabel = "Figure")` | Adds a Word table-of-figures field listing auto-numbered captions (use label "Table" for table captions). | | `BulletList` / `NumberedList` | Adds list content. | | `Table` | Adds a table. | | `Chart` | Adds a chart. | | `Image` overloads | Adds an image from path, bytes, or stream. | | `Barcode(string value, ...)` | Adds a Code 128 barcode. | | `QrCode(string value, ...)` | Adds a QR code. | | `Line()` | Adds a horizontal rule. | | `PageBreak()` | Adds a page break. | | `Component(IComponent component)` | Composes reusable content. | ## Text | API | Purpose | | --- | --- | | `Bold`, `Italic`, `Underline`, `Strikethrough` | Run formatting. | | `Superscript`, `Subscript` | Vertical text positioning. | | `FontSize`, `FontColor`, `FontFamily`, `Highlight` | Font styling. | | `Style(string name)` | Applies a registered paragraph style. | | `AlignLeft`, `AlignCenter`, `AlignRight`, `Justify` | Paragraph alignment. | | `LineHeight`, `SpacingBefore`, `SpacingAfter` | Paragraph spacing. | | `LeftIndent`, `RightIndent`, `FirstLineIndent`, `HangingIndent` | Indentation. | | `KeepWithNext`, `KeepLinesTogether`, `PageBreakBefore` | Pagination controls. | | `Shading`, `Border`, `BorderTop`, `BorderRight`, `BorderBottom`, `BorderLeft` | Paragraph background and borders. | | `TabStop`, `Tab` | Tab layout. | | `Span` | Adds formatted text runs. | | `Hyperlink` | Adds inline hyperlink runs. | | `CrossReference` | Adds an internal reference to a bookmark. | | `Footnote`, `Endnote` | Adds notes. | | `CurrentPageNumber`, `TotalPages` | Adds page number fields. | ```csharp page.Content().Text(t => { t.Span("Revenue ").Bold(); t.Span("increased 12%").FontColor(Colors.Green.L700); t.Span(" year over year."); t.SpacingAfter(8).KeepLinesTogether(); }); ``` ## Lists | API | Purpose | | --- | --- | | `Marker(string marker, int level = 0, string? fontFamily = null)` | Sets a marker for a list level. | | `Item(string text, Action<ITextDescriptor>? configure = null)` | Adds a level-0 item. | | `Item(string text, int level, Action<ITextDescriptor>? configure = null)` | Adds an item at a specific level. | | `Item(Action<ITextDescriptor> configure)` | Adds a rich level-0 item. | | `Item(int level, Action<ITextDescriptor> configure)` | Adds a rich item at a specific level. | ## Tables | API | Purpose | | --- | --- | | `Style(string name)` | Applies a registered table style. | | `Width(float points)` / `WidthPercent(float percent)` | Sets table width. | | `AlignLeft`, `AlignCenter`, `AlignRight` | Sets table alignment. | | `ColumnsDefinition(Action<ITableColumnsDefinition>)` | Sets relative or fixed columns. | | `HeaderRow(Action<ITableRowDescriptor>)` | Adds a repeating-style header row. | | `Row(Action<ITableRowDescriptor>)` | Adds a body row. | | `Border`, `CellPadding`, `HeaderBackground`, `AlternateRowBackground` | Sets table formatting. | | `RowMinHeight`, `HeaderRowMinHeight` | Sets row heights. | | `Caption(string description, ...)` | Adds an auto-numbered "Table N." caption above the table (Word `SEQ Table` field). | Column APIs: | API | Purpose | | --- | --- | | `RelativeColumn(float size = 1)` | Adds proportional width column. | | `ConstantColumn(float widthPoints)` | Adds fixed width column. | Row APIs: | API | Purpose | | --- | --- | | `KeepTogether(bool value = true)` | Keeps a row together. | | `Cell(int columnSpan = 1)` | Adds a cell; table cells are containers. | Cell APIs: | API | Purpose | | --- | --- | | `ColumnSpan(int columns)` | Spans multiple columns. | | `VerticalMergeStart` / `VerticalMergeContinue` | Creates merged vertical cells. | | `Background`, `Border`, `BorderTop`, `BorderRight`, `BorderBottom`, `BorderLeft` | Cell styling. | | `Padding(...)` | Cell padding. | | `VerticalAlignTop`, `VerticalAlignMiddle`, `VerticalAlignBottom` | Vertical alignment. | | `TextDirectionLeftToRight`, `TextDirectionTopToBottom`, `TextDirectionBottomToTop` | Text direction. | ## Images Image paths are resolved when the document is published. A missing image path or empty image byte array throws instead of emitting placeholder text into the document. | API | Purpose | | --- | --- | | `Width`, `Height`, `MaxWidth` | Controls size in points. | | `AltText` | Adds accessibility text. | | `Caption` | Adds a caption paragraph. | | `FigureCaption(string description, ...)` | Adds an auto-numbered "Figure N." caption below the image (Word `SEQ Figure` field). | | `AlignLeft`, `AlignCenter`, `AlignRight` | Aligns inline image paragraphs. | | `WrapInline`, `WrapSquare`, `WrapTight`, `WrapTopBottom` | Text wrapping. | | `BehindText`, `InFrontOfText` | Floating layer. | | `FloatLeft`, `FloatRight`, `FloatCenter` | Floating alignment. | | `Position`, `PositionFromPage` | Absolute positioning. | | `Margin(...)` | Wrap margin. | | `Border`, `Rounded`, `Crop` | Visual styling. | ## Barcodes Barcodes are rendered as vector shapes (grouped rectangles), not raster images, so they stay crisp at any zoom or print size. The value is validated eagerly: a null, empty, or non-ASCII-32-126 value throws `ArgumentException` at the fluent-call site, not at publish time. | API | Purpose | | --- | --- | | `Width`, `Height`, `MaxWidth` | Controls size in points. `Width` is the total rendered width of the bars; `Height` is the bar height. | | `AltText` | Adds accessibility text. | | `ShowText(bool show = true)` | Shows or hides the human-readable value below the bars (shown by default). | | `BarColor(string hexColor)` | Sets the bar color. | | `AlignLeft`, `AlignCenter`, `AlignRight` | Aligns the barcode's paragraph. | | `Caption` | Adds a caption paragraph below the barcode. | ## QR Codes QR codes are rendered as vector shapes (grouped rectangles, run-length-encoded per row), not raster images. Any UTF-8 text is accepted; the smallest of the 40 QR versions that fits the payload at the requested error correction level is chosen automatically. A payload too long for even the largest version at that level throws `ArgumentException` at the fluent-call site, not at publish time. A 4-module quiet zone is always included, so the default output scans reliably without extra margin from the caller. | API | Purpose | | --- | --- | | `Size(float points)` | Sets the rendered width/height (QR codes are square), including the quiet zone. Defaults to 100pt. | | `MaxSize(float points)` | Caps the rendered size, scaling down if it would otherwise be larger. | | `ErrorCorrection(QrErrorCorrectionLevel level)` | Sets damage tolerance vs. symbol size: `Low` (~7%), `Medium` (~15%, default), `Quartile` (~25%), `High` (~30%). | | `AltText` | Adds accessibility text. | | `ForegroundColor(string hexColor)` | Sets the dark-module color. Defaults to black. | | `BackgroundColor(string hexColor)` | Sets a fill behind the code and its quiet zone. Transparent (page background shows through) unless set. | | `AlignLeft`, `AlignCenter`, `AlignRight` | Aligns the QR code's paragraph. | | `Caption` | Adds a caption paragraph below the QR code. | ## Charts | API | Purpose | | --- | --- | | `Title(string title)` | Sets chart title. | | `Series(Action<ISeriesDescriptor> configure)` | Adds an unnamed series. | | `Series(string name, Action<ISeriesDescriptor> configure)` | Adds a named series for legend output. | | `Width(float points)` / `Height(float points)` | Sets the chart frame size (default 432 x 252 points, i.e. 6 x 3.5 inches). Size charts to fit their container - a chart inside a row column or table cell does not shrink automatically. | | `AlignLeft`, `AlignCenter`, `AlignRight` | Aligns the chart's paragraph. | | `Legend(ChartLegendPosition position)` | Places the legend right, left, top, or bottom (default right). | | `HideLegend()` | Hides the legend. | | `CategoryAxisTitle(string title)` / `ValueAxisTitle(string title)` | Adds axis titles (ignored by pie/doughnut charts). | | `DataLabels(bool show = true)` | Shows each data point's value on the chart. | | `Stacked()` / `PercentStacked()` | Stacks multi-series bar charts (absolute or normalized to 100%). Bar charts only. | Series APIs: | API | Purpose | | --- | --- | | `Bar`, `Line`, `Pie`, `Doughnut` | Adds a data point of that chart kind. | | `Color(string hexColor)` | Sets the series color. | All series in a chart must use the same chart kind. Pie and doughnut charts support one series. ## Layout Helpers Row APIs: | API | Purpose | | --- | --- | | `Spacing(float points)` | Sets spacing between row items. | | `RelativeItem(float size = 1)` | Adds a proportional width item container. | | `AutoItem()` | Adds an auto-sized item container. | | `ConstantItem(float widthPoints)` | Adds a fixed-width item container. | Column APIs: | API | Purpose | | --- | --- | | `Spacing(float points)` | Sets spacing between column items. | | `Item()` | Adds an item container. | ## Templates Template placeholder replacement is scoped to Word text nodes and supports placeholders split across multiple runs, as Word often stores formatted text. `Replace("Name", value)` targets `{{Name}}`; bare occurrences of `Name` are not replaced. Use content controls when you need strongly scoped replacements in authored Word templates. | API | Purpose | | --- | --- | | `DocxTemplate.Open(string templatePath)` | Opens a `.docx` template. | | `Replace(string placeholder, string value)` | Replaces plain text placeholders. | | `ReplaceContentControl(string tagOrAlias, string value)` | Replaces matching content controls. | | `SaveAs(string outputPath)` | Writes the result to disk. | | `Save()` | Returns the result as a byte array. | ## Utilities ### Unit | API | Purpose | | --- | --- | | `Unit.Centimetre(float value)` | Converts centimetres to points. | | `Unit.Millimetre(float value)` | Converts millimetres to points. | | `Unit.Inch(float value)` | Converts inches to points. | | `Unit.Point(float value)` | Returns points unchanged. | ### PageSize Built-in sizes: `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `Letter`, `Legal`, `Ledger`, `Tabloid`. Use `new PageSize(widthPoints, heightPoints)` for custom sizes and `PageSize.A4.Landscape()` for a landscape value. ### Colors `Colors` provides common hex values under `Black`, `White`, `Grey`, `Blue`, `Red`, `Green`, and `Orange`. You can also pass any six-character hex color string, such as `"1F4E79"`. ### Enums | Enum | Values | | --- | --- | | `HighlightColor` | `Yellow`, `Green`, `Cyan`, `Magenta`, `Blue`, `Red`, `DarkBlue`, `DarkCyan`, `DarkGreen`, `DarkMagenta`, `DarkRed`, `DarkYellow`, `DarkGray`, `LightGray`, `Black`, `White`, `None` | | `TabStopAlignment` | `Left`, `Center`, `Right`, `Decimal` | ==================================================================== TerraFluent.Docx.Reporting — Samples URL: https://terrafluent.dev/docs/docx/samples/ ==================================================================== # Samples The sample project creates several realistic `.docx` files and writes them to `Desktop\SampleDocs`. ## Run All Samples ```powershell dotnet run --project samples\TerraFluent.Docx.Reporting.Sample\TerraFluent.Docx.Reporting.Sample.csproj ``` ## Sample Files | Sample | Source | Demonstrates | | --- | --- | --- | | Feature showcase | [FeatureShowcaseSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/FeatureShowcaseSample.cs) | Headings, text, headers, footers, images, barcodes, hyperlinks, and general document structure. | | Barcode showcase | [BarcodeShowcaseSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/BarcodeShowcaseSample.cs) | Shipping manifest with barcodes in a header, a table column, and standalone printable labels. | | Market analysis | [MarketAnalysisSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/MarketAnalysisSample.cs) | Auto-numbered figure/table captions, table of figures and list of tables, sized/stacked/labeled charts, and comments-only document protection. | | Invoice | [InvoiceSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/InvoiceSample.cs) | Invoice layout, branding, totals, and tables. | | Long invoice | [LongInvoiceSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/LongInvoiceSample.cs) | Multi-page invoice behavior and repeated tabular content. | | Annual report | [AnnualReportSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/AnnualReportSample.cs) | Realistic business report with sections, images, tables, and rich formatting. | | Layout features | [LayoutFeaturesSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/LayoutFeaturesSample.cs) | Page size, orientation, margins, columns, watermarks, and layout behavior. | | Template replacement | [TemplateReplacementSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/TemplateReplacementSample.cs) | Placeholder and content-control replacement. | | API reference document | [ApiReferenceSample.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/ApiReferenceSample.cs) | Broad tour of the public descriptor API in generated document form. | Support files: - [InvoiceBranding.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/InvoiceBranding.cs) - [SampleImage.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/SampleImage.cs) - [SampleOutput.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/SampleOutput.cs) - [Program.cs](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/samples/TerraFluent.Docx.Reporting.Sample/Program.cs) ## Copy A Sample Pattern For a new report, start with this structure: ```csharp using TerraFluent.Docx.Reporting; public static class ReportSample { public static string Generate(string outputDirectory) { var path = Path.Combine(outputDirectory, "report.docx"); Document.Create(doc => { doc.MetadataTitle("Report") .MetadataAuthor("Reporting Team") .Theme(theme => theme.DefaultFont("Aptos", 10.5f)); doc.Page(page => { page.Size(PageSize.A4); page.Margin(Unit.Centimetre(2)); page.Content().H1("Report"); page.Content().Text("Add report content here."); }); }).PublishDocx(path); return path; } } ``` ## Visual QA Checklist After running samples: - Open each generated `.docx` in Microsoft Word. - Confirm there are no repair prompts. - Refresh fields if you use a table of contents. - Open or convert the same files in LibreOffice when cross-application compatibility matters. - Check images, charts, page numbers, watermarks, and table layout. ==================================================================== TerraFluent.Docx.Reporting — Troubleshooting URL: https://terrafluent.dev/docs/docx/troubleshooting/ ==================================================================== # Troubleshooting ## Building A Document Throws An Exception TerraFluent.Docx.Reporting validates fluent API inputs eagerly, before any Open XML is written, so configuration mistakes fail fast at the call site instead of producing a damaged or unreadable `.docx`: - `ArgumentNullException` - a required callback, object, or stream argument was `null` (e.g. `Document.Create(null)`, `Component(null)`). - `ArgumentException` - a required string was `null`, empty, or whitespace (e.g. an image file path or page number format), a stream was unreadable/unwritable, or a barcode value contained a character outside ASCII 32-126. - `ArgumentOutOfRangeException` - a numeric value was outside its valid range, such as a negative margin, a non-positive width/height, or a column count outside 1-45. See [Public API Contract](/docs/docx/api/#public-api-contract) for the full validation contract. ## Word Shows A Repair Prompt Run the automated tests and inspect the generated document with Word. ```powershell dotnet test TerraFluent.Docx.Reporting.sln ``` Common causes: - Invalid color values. Use six-character hex strings such as `1F4E79`, not `#1F4E79`. - Invalid page number formats. `PageNumberFormat` writes the OOXML value as-is. Use values like `decimal`, `lowerRoman`, `upperRoman`, `lowerLetter`, or `upperLetter`. - Mixed chart types in one chart. All chart series must use the same kind. - Multiple series in a pie or doughnut chart. These chart types support one series. ## Images Do Not Appear Check the path or byte data supplied to the image API. ```csharp page.Content().Image("logo.png", image => image .Width(120) .AltText("Company logo")); ``` Tips: - Use an absolute path when the current working directory is unclear. - Ensure file names have supported extensions such as `.png`, `.jpg`, or `.jpeg`. - Missing file paths and empty image byte arrays throw during document creation or publishing. - Use `AltText` for accessibility and easier inspection. - Use `MaxWidth` when documents may receive images of unknown size. ## Template Values Are Not Replaced `DocxTemplate.Replace("Name", value)` replaces `{{Name}}`, including placeholders split across Word runs. It intentionally does not replace every bare occurrence of `Name`, which avoids accidental edits to ordinary document text. For authored Word templates, prefer tagged content controls for business fields: ```csharp DocxTemplate.Open("template.docx") .ReplaceContentControl("CustomerName", "Ada Lovelace") .SaveAs("output.docx"); ``` Content controls are matched by tag or alias. ## Page Background Does Not Change Per Section Word stores page background as a document-wide setting. If multiple sections call `Background`, the first non-empty value wins. Use section-specific watermarks or shaded containers when you need visual differences per section. ## Table Of Contents Does Not Show Page Numbers Immediately The TOC is emitted as a Word field. Word normally updates fields when you open the document or explicitly refresh fields. In Word, select the table of contents and choose update field. ## Layout Differs Between Word And LibreOffice Open XML rendering can differ between applications. Before publishing templates or report layouts: - Test in Microsoft Word. - Test in LibreOffice if your users rely on it. - Avoid overly tight fixed-width tables. - Prefer relative columns for responsive report tables. - Keep images within page margins with `MaxWidth`. ## NuGet Package Missing Docs Or License Pack the project and inspect the package: ```powershell dotnet pack src\TerraFluent.Docx.Reporting\TerraFluent.Docx.Reporting.csproj -c Release -o artifacts\nuget tar -tf artifacts\nuget\TerraFluent.Docx.Reporting.*.nupkg ``` The package should include: - `README.md` - `CHANGELOG.md` - `LICENSE.txt` - `lib/netstandard2.0/TerraFluent.Docx.Reporting.dll` - `lib/netstandard2.0/TerraFluent.Docx.Reporting.xml` - `lib/net10.0/TerraFluent.Docx.Reporting.dll` - `lib/net10.0/TerraFluent.Docx.Reporting.xml` See [Release And Publishing](https://github.com/sahebansari/TerraFluent.Docx.Reporting/blob/master/docs/RELEASE.md) for the full checklist. ==================================================================== TerraFluent.Pdf.Reporting — Getting Started URL: https://terrafluent.dev/docs/pdf/getting-started/ ==================================================================== # Getting Started with TerraFluent.Pdf.Reporting ## Installation ```sh dotnet add package TerraFluent.Pdf.Reporting ``` ## Namespaces | Namespace | Contents | |-----------|----------| | `TerraFluent.Pdf.Reporting.Core` | Fluent API entry points, descriptors, extension methods | | `TerraFluent.Pdf.Reporting.Infra` | `IContainer`, `IDocument`, `IComponent` interfaces | | `TerraFluent.Pdf.Reporting.Helpers` | `Color`, `PageSize`, `Unit`, `TextStyle` | --- TerraFluent.Pdf.Reporting 1.4.0 brings a few notable upgrades for document authorship: AES-256 encryption is now the default, images can be supplied from bytes or streams, anchor-based bookmarks track rendered content automatically, and the multi-span text API uses immutable `TextStyle` callbacks. ## Minimal Example ```csharp using TerraFluent.Pdf.Reporting.Core; using TerraFluent.Pdf.Reporting.Helpers; PdfDocument.Create(container => { container.Page(page => { page.Size(PageSize.A4); page.Margin(2, Unit.Centimetre); page.PageColor(Color.White); page.DefaultTextStyle(s => s.FontSize(11)); page.Header().Text("My First PDF").Bold().FontSize(20); page.Content().Column(col => { col.Spacing(8); col.Item().Text("Hello, TerraFluent.Pdf.Reporting!"); col.Item().Text("A second paragraph.").Italic(); }); page.Footer().AlignCenter().Text(t => { t.Span("Page "); t.CurrentPageNumber(); t.Span(" / "); t.TotalPages(); }); }); }) .PublishPdf("output.pdf"); ``` --- ## Document Entry Points ```csharp // Inline callback PdfDocument.Create(container => { ... }).PublishPdf("output.pdf"); // Reusable IDocument class PdfDocument.Create(new MyReport(data)).PublishPdf("output.pdf"); ``` --- ## Page Configuration Every page is configured through `PageDescriptor`: ```csharp container.Page(page => { // Size page.Size(PageSize.A4); // standard size page.Size(PageSize.Landscape(PageSize.A4)); // landscape page.Size(210, 297, Unit.Millimetre); // explicit dimensions // Margins page.Margin(2, Unit.Centimetre); // all sides page.MarginVertical(1, Unit.Centimetre); // top + bottom page.MarginHorizontal(1.5, Unit.Centimetre); // left + right page.Margin(top: 72, right: 54, bottom: 72, left: 54); // individual (points) // Appearance page.PageColor(Color.White); page.DefaultTextStyle(s => s.FontSize(11).FontColor(Color.Grey.Darken2)); // Layout slots page.Header() // IContainer — drawn above content on every page page.Content() // IContainer — main scrollable area page.Footer() // IContainer — drawn below content on every page }); ``` --- ## Output Methods ```csharp var composer = PdfDocument.Create(...); // Write to file composer.PublishPdf("report.pdf"); // Return as byte array (API responses, email attachments) byte[] bytes = composer.PublishPdf(); // Write to any stream using var stream = new MemoryStream(); composer.PublishPdf(stream); ``` --- ## Next Steps - [Text & Spans](/docs/pdf/text-and-spans/) — styling, underline, line-height, multi-span, page numbers - [Layout](/docs/pdf/layout/) — Column, Row, Table - [Decorators](/docs/pdf/decorators/) — Padding, Margin, Background, Border, Rounded Border, Per-Edge Borders, Alignment, Lines, PageBreak, Hyperlink, ShowIf - [Vector Graphics](/docs/pdf/vector-graphics/) — Canvas API, lines, shapes, Bézier paths, polygons, grids, charts - [Images](/docs/pdf/images/) — PNG and JPEG embedding - [Encryption](/docs/pdf/encryption/) — AES-256 by default, plus AES-128 compatibility mode and permission flags - [Table of Contents](/docs/pdf/table-of-contents/) — headings, automatic TOC generation, internal links - [Bookmarks](/docs/pdf/bookmarks/) — PDF outlines / hierarchical navigation tree - [Unicode & Character Encoding](/docs/pdf/unicode-and-encoding/) — WinAnsiEncoding coverage, Windows-1252 specials, Latin-1 Supplement, avoiding `?` characters - [Metadata](/docs/pdf/metadata/) — document properties (Title, Author, Subject, Keywords, Creator) - [Colors](/docs/pdf/colors/) — full built-in palette reference - [Page Sizes & Units](/docs/pdf/page-sizes-and-units/) — all standard sizes and unit conversions - [Components & Templates](/docs/pdf/components-and-templates/) — reusable `IComponent` and `IDocument` - [Row & Column Layout](/docs/pdf/row-and-column-layout/) — deep dive with diagrams ==================================================================== TerraFluent.Pdf.Reporting — Text & Spans URL: https://terrafluent.dev/docs/pdf/text-and-spans/ ==================================================================== # Text & Spans ## Single-String Text The simplest form — one string, block-level style: ```csharp container.Text("Hello, world!"); ``` Chain style methods on the returned `TextDescriptor` to format the whole block: ```csharp container.Text("Section Heading") .Bold() .FontSize(16) .FontColor(Color.Blue.Darken2) .AlignCenter(); ``` ### All `TextDescriptor` style methods | Method | Effect | |--------|--------| | `.Bold()` | Bold weight | | `.SemiBold()` | Semi-bold (mapped to bold in the built-in font set) | | `.Italic()` | Italic style | | `.Strikethrough()` | Horizontal strikethrough line | | `.Underline()` | Underline beneath the text | | `.FontSize(double)` | Font size in PDF points | | `.FontFamily(string)` | Font family: `"Helvetica"` (default), `"Times"`, or `"Courier"` | | `.LineHeight(double)` | Line-height multiplier (e.g. `1.0` = tight, `1.4` = default, `2.0` = double-spaced) | | `.FontColor(string)` | Hex colour, e.g. `"#1a4a8a"` or `Color.Red.Medium` | | `.AlignLeft()` | Left-align (default) | | `.AlignCenter()` | Centre-align | | `.AlignRight()` | Right-align | | `.Justify()` | Justify all lines except the last | ### Font families TerraFluent.Pdf.Reporting ships the three standard-14 font families, each in regular, **bold**, *italic*, and ***bold-italic*** — no font files or embedding required: ```csharp container.Text("Sans-serif (default)"); // Helvetica container.Text("Serif body text").FontFamily("Times"); // Times-Roman container.Text("Code sample").FontFamily("Courier"); // Courier (monospaced) container.Text("Serif heading").FontFamily("Times").Bold(); // Times-Bold ``` `Bold()` and `Italic()` always stay within the selected family — bold Helvetica renders **Helvetica-Bold**, not a serif face. Common aliases are accepted (`"Arial"` → Helvetica, `"Times New Roman"` → Times, `"Courier New"` → Courier); unknown names fall back to Helvetica. --- ## Multi-Span Text Use the `Func<TextStyle, TextStyle>` overload to compose a text block from multiple independently styled spans. Because `TextStyle` is immutable, the callback receives a fresh style object and must return the configured style. ```csharp container.Text(t => { t.Span("Normal "); t.Span("Bold ", s => s.Bold()); t.Span("Italic ", s => s.Italic()); t.Span("Struck ", s => s.Strikethrough()); t.Span("Coloured ", s => s.FontColor(Color.Red.Medium)); t.Span("Large", s => s.FontSize(16).FontColor("#1a4a8a")); }); ``` > **Important:** `t.Span(...)` returns a `SpanDescriptor`, not a `TextDescriptor`. > Style methods chained after `.Span()` apply **only to that span**. This is intentional — > it prevents accidental formatting of the whole block. ### `SpanDescriptor` methods | Method | Effect | |--------|--------| | `.Bold()` | Bold weight for this span | | `.SemiBold()` | Semi-bold for this span | | `.Italic()` | Italic for this span | | `.Strikethrough()` | Strikethrough for this span | | `.Underline()` | Underline for this span | | `.FontSize(double)` | Font size for this span | | `.FontFamily(string)` | Font family for this span (`"Helvetica"`, `"Times"`, `"Courier"`) | | `.FontColor(string)` | Text colour for this span | --- ## Page Numbers `CurrentPageNumber()` and `TotalPages()` also return `SpanDescriptor` so they can be individually styled: ```csharp page.Footer().AlignCenter().Text(t => { t.Span("Page ").FontSize(9).FontColor(Color.Grey.Medium); t.CurrentPageNumber().FontSize(9).FontColor(Color.Grey.Medium); t.Span(" of ").FontSize(9).FontColor(Color.Grey.Medium); t.TotalPages().FontSize(9).FontColor(Color.Grey.Medium); }); ``` --- ## Mixing Styles in One Block Because the block's alignment is controlled at the `TextDescriptor` level, you can combine per-span colour/size with a block-level alignment: ```csharp container.Text(t => { t.Span("Status: ").Bold(); t.Span("Approved").FontColor(Color.Green.Darken2).Bold(); t.Span(" (June 2025)").FontColor(Color.Grey.Medium).FontSize(9); }) .AlignRight(); ``` --- ## Underline `.Underline()` draws a line beneath the text. It works on both the whole block (`TextDescriptor`) and on individual spans (`SpanDescriptor`). ```csharp // Whole block underlined container.Text("Important notice").Underline().Bold(); // Only one span underlined in a mixed block container.Text(t => { t.Span("Visit "); t.Span("TerraFluent.Pdf.Reporting").Underline().FontColor(Color.Blue.Medium); t.Span(" for more info."); }); // Underline and strikethrough can be combined container.Text("Deprecated").Underline().Strikethrough().FontColor(Color.Grey.Medium); ``` --- ## Line Height `.LineHeight(double)` sets a multiplier applied to the natural line height. The default multiplier is approximately `1.4`. ```csharp container.Text("Tight paragraph.").LineHeight(1.0); container.Text("Normal paragraph.").LineHeight(1.4); container.Text("Relaxed paragraph.").LineHeight(1.6); container.Text("Double-spaced paragraph.").LineHeight(2.0); ``` Line height can also be set page-wide via `DefaultTextStyle`: ```csharp page.DefaultTextStyle(s => s.FontSize(11).LineHeight(1.5)); ``` --- ## Default Text Style A page-wide default style is set on `PageDescriptor` and inherited by all text unless explicitly overridden at the block or span level: ```csharp page.DefaultTextStyle(s => s.FontSize(11).FontColor(Color.Grey.Darken2)); ``` Style resolution order (highest wins): ``` Span style > Block style (TextDescriptor) > Page default style > Library default (12 pt, black) ``` ==================================================================== TerraFluent.Pdf.Reporting — Layout URL: https://terrafluent.dev/docs/pdf/layout/ ==================================================================== # Layout TerraFluent.Pdf.Reporting provides three layout elements: **Column**, **Row**, and **Table**. They are all accessed through extension methods on `IContainer`. For a visual explanation of how Column and Row relate to each other see [Row & Column Layout](/docs/pdf/row-and-column-layout/). --- ## Column `Column` stacks its children **vertically** (top → bottom). ```csharp container.Column(col => { col.Spacing(8); // vertical gap between items in points col.Item().Text("First paragraph"); col.Item().Text("Second paragraph"); col.Item().Background(Color.Grey.Lighten4).Padding(10).Text("Highlighted box"); }); ``` ### Column alignment Each `Item()` can be aligned independently by calling an `AlignItems*` method **before** the `Item()` call it should affect: ```csharp col.AlignItemsLeft(); // default col.Item().Text("Left"); col.AlignItemsCenter(); col.Item().Text("Centred"); col.AlignItemsRight(); col.Item().Text("Right"); ``` ### `ColumnDescriptor` API | Method | Description | |--------|-------------| | `Spacing(double)` | Vertical gap between items in points | | `AlignItemsLeft()` | Left-align subsequent items (default) | | `AlignItemsCenter()` | Centre-align subsequent items | | `AlignItemsRight()` | Right-align subsequent items | | `Item()` | Adds an item slot; returns `IContainer` | | `PageBreak()` | Inserts an explicit page break at this position | --- ## Row `Row` arranges its children **horizontally** (left → right). ```csharp container.Row(row => { row.Spacing(6); // horizontal gap between items in points row.RelativeItem(2).Text("Takes 2x the share"); row.RelativeItem(1).Text("Takes 1x the share"); row.AutoItem().Text("Natural content width"); row.ConstantItem(80).Text("Always 80 pt wide"); }); ``` ### Item sizing | Method | Width | |--------|-------| | `RelativeItem(weight = 1)` | Proportional share of remaining space after constant/auto items | | `AutoItem()` | Measured natural content width | | `ConstantItem(points)` | Fixed number of PDF points | Width calculation order: 1. Subtract spacing from available width. 2. Measure all `ConstantItem` widths and `AutoItem` widths (content measurement pass). 3. Distribute remaining space among `RelativeItem` slots proportionally by weight. ### `RowDescriptor` API | Method | Returns | Description | |--------|---------|-------------| | `Spacing(double)` | `RowDescriptor` | Horizontal gap in points | | `RelativeItem(double weight = 1)` | `IContainer` | Proportional slot | | `AutoItem()` | `IContainer` | Auto-sized slot | | `ConstantItem(double pts)` | `IContainer` | Fixed-width slot | --- ## Table `Table` renders a grid with fixed or proportional columns. Header rows are automatically repeated on every continuation page when the table spans multiple pages. ```csharp container.Table(table => { // 1. Define columns table.ColumnsDefinition(cols => { cols.RelativeColumn(4); // proportional cols.RelativeColumn(1); cols.ConstantColumn(70); // fixed width in points }); // 2. Header row (repeated on continuation pages) table.HeaderRow(row => { row.Cell().Background("#1a4a8a").Padding(6) .Text("Description").Bold().FontColor(Color.White); row.Cell().Background("#1a4a8a").Padding(6).AlignCenter() .Text("Qty").Bold().FontColor(Color.White); row.Cell().Background("#1a4a8a").Padding(6).AlignRight() .Text("Price").Bold().FontColor(Color.White); }); // 3. Data rows bool shade = false; foreach (var item in lineItems) { string bg = shade ? Color.Grey.Lighten4 : Color.White; table.Row(row => { row.Cell().Background(bg).Padding(6).Text(item.Name); row.Cell().Background(bg).Padding(6).AlignCenter().Text(item.Qty.ToString()); row.Cell().Background(bg).Padding(6).AlignRight().Text($"${item.Price:N2}"); }); shade = !shade; } }); ``` ### Column definitions | Method | Description | |--------|-------------| | `RelativeColumn(weight = 1)` | Proportional share of available width | | `ConstantColumn(points)` | Fixed width in PDF points | ### `TableDescriptor` API | Method | Description | |--------|-------------| | `ColumnsDefinition(Action<ColumnsDefinitionDescriptor>)` | Define all columns | | `HeaderRow(Action<TableRowDescriptor>)` | Add a header row (repeats on new pages) | | `Row(Action<TableRowDescriptor>)` | Add a data row | ### `TableRowDescriptor` API | Method | Returns | Description | |--------|---------|-------------| | `Cell(columnSpan = 1, rowSpan = 1)` | `IContainer` | Next cell slot (left to right, auto-skipping columns reserved by an earlier `columnSpan` in the same row or a `rowSpan` reaching down from a previous row); supports all decorators | > **Tip:** Cells support the full decorator chain: > `row.Cell().Background(bg).Padding(6).AlignRight().Text("value")` ### Column and row spans ```csharp table.Row(row => { // rowSpan: add the cell once, on the first row it covers. row.Cell(rowSpan: 2).Background("#E8F5F0").Padding(8).AlignMiddle() .Text("Laptops").Bold(); row.Cell().Padding(6).Text("UltraBook Pro 14\""); row.Cell().Padding(6).AlignRight().Text("$1,299.00"); }); table.Row(row => { // Column 1 is already occupied by the rowSpan cell above — Cell() // skips it automatically and lands in column 2. row.Cell().Padding(6).Text("UltraBook Pro 16\""); row.Cell().Padding(6).AlignRight().Text("$1,799.00"); }); // columnSpan merges cells left to right within a single row. table.Row(row => row.Cell(columnSpan: 3).Background("#0F5C4C").Padding(6) .AlignCenter().Text("NEW ARRIVALS").Bold().FontColor(Color.White)); ``` - `columnSpan` merges the cell across that many columns of its own row; the next `Cell()` call in the same row automatically starts after it. - `rowSpan` extends the cell down that many rows; call it once, on the first row of the group — every following row's `Cell()` calls automatically skip the column it occupies. - Row heights grow to fit a `rowSpan` cell's content when it's taller than the rows it spans (the extra height is split evenly across them). - A `rowSpan` group is never split across a page break — if it doesn't fit on the current page, the whole group moves to the next one together. See `17_TableSpanShowcase.cs` in the samples project for a full worked example (category-grouped rows, a banner row, and a `rowSpan` group crossing a page break). --- ## Header on First Page Only By default, the page header appears on every page. Use `HeaderOnFirstPageOnly()` to show the header only on the first page, freeing vertical space for content on continuation pages: ```csharp page.Header().Text("Confidential").Bold(); page.HeaderOnFirstPageOnly(); // header only on page 1 ``` This is useful for cover pages or documents where the header should not repeat. --- ## Nesting Layouts `Column`, `Row`, and `Table` nest freely inside each other. ### Two-column page layout ```csharp container.Row(row => { row.RelativeItem().Column(left => { left.Spacing(6); left.Item().Text("Left heading").Bold(); left.Item().Text("Left body text."); }); row.ConstantItem(1).Background(Color.Grey.Lighten2); // divider row.RelativeItem().PaddingLeft(12).Column(right => { right.Spacing(6); right.Item().Text("Right heading").Bold(); right.Item().Text("Right body text."); }); }); ``` ### Header / body / footer page structure ```csharp container.Column(page => { page.Spacing(10); // Header row page.Item().Row(header => { header.RelativeItem().Text("Logo").Bold(); header.AutoItem().Text("Page 1").FontColor(Color.Grey.Medium); }); // Body page.Item().Text("Main content goes here."); // Footer row page.Item().Row(footer => { footer.RelativeItem().Text("Company Name"); footer.AutoItem().Text("Confidential").Italic(); }); }); ``` ==================================================================== TerraFluent.Pdf.Reporting — Row & Column Layout URL: https://terrafluent.dev/docs/pdf/row-and-column-layout/ ==================================================================== # Row & Column Layout in TerraFluent.Pdf.Reporting ## Core Concept The name describes **how children are arranged**, not the shape of the container itself. | Element | Direction | Axis | |---------|-----------|------| | `Column` | Top → Bottom | Vertical | | `Row` | Left → Right | Horizontal | --- ## Column A `Column` stacks its children **vertically**, one below the other — like a column of text in a newspaper. ``` ┌──────────────────┐ │ Item 1 │ ├──────────────────┤ │ Item 2 │ ├──────────────────┤ │ Item 3 │ └──────────────────┘ ``` ### Fluent API ```csharp container.Column(col => { col.Spacing(10); // vertical gap between items in points col.Item().Text("First line"); col.Item().Text("Second line"); col.Item().Text("Third line"); }); ``` ### What happens internally - `Measure()` accumulates **height** for each item and tracks the **maximum width**. - `Draw()` advances the cursor **downward** (`curY`) after each item. --- ## Row A `Row` arranges its children **horizontally**, side by side — like cells in a table row. ``` ┌──────────┬──────────┬──────────┐ │ Item 1 │ Item 2 │ Item 3 │ └──────────┴──────────┴──────────┘ ``` ### Fluent API ```csharp container.Row(row => { row.Spacing(5); // horizontal gap between items in points row.AutoItem().Text("Auto-sized"); // takes its natural content width row.RelativeItem(2).Text("2x wide"); // takes 2x the share of remaining space row.RelativeItem(1).Text("1x wide"); // takes 1x the share of remaining space row.ConstantItem(80).Text("Fixed 80pt"); // always exactly 80 points wide }); ``` ### Item sizing options | Method | Behaviour | |--------|-----------| | `AutoItem()` | Width = natural content width (measured first) | | `RelativeItem(weight = 1)` | Width = proportional share of remaining space after auto/constant items (default weight = 1) | | `ConstantItem(pts)` | Width = fixed number of PDF points, always | ### What happens internally - `CalculateWidths()` resolves all item widths from available space. - `Measure()` accumulates **widths** and tracks the **maximum height**. - `Draw()` advances the cursor **to the right** (`curX`) after each item. --- ## Combining Row & Column `Row` and `Column` are designed to be **nested** freely to build any layout. ### Example: Two-column page layout ```csharp // Side-by-side columns, each containing stacked content container.Row(row => { row.RelativeItem().Column(left => { left.Spacing(8); left.Item().Text("Left heading"); left.Item().Text("Left body text..."); }); row.ConstantItem(20); // spacer row.RelativeItem().Column(right => { right.Spacing(8); right.Item().Text("Right heading"); right.Item().Text("Right body text..."); }); }); ``` Result: ``` ┌─────────────────────┬────┬─────────────────────┐ │ Left heading │ │ Right heading │ │ Left body text... │ │ Right body text... │ └─────────────────────┴────┴─────────────────────┘ ``` ### Example: Header + body + footer (Column wrapping Rows) ```csharp container.Column(page => { page.Spacing(12); // Header row page.Item().Row(header => { header.RelativeItem().Text("Logo"); header.RelativeItem().Text("Title"); header.AutoItem().Text("Page 1"); }); // Body content page.Item().Text("Main body paragraph text goes here..."); // Footer row page.Item().Row(footer => { footer.RelativeItem().Text("Company Name"); footer.AutoItem().Text("Confidential"); }); }); ``` Result: ``` ┌──────────────────────────────────────────┐ │ Logo Title Page 1 │ ← Row (header) ├──────────────────────────────────────────┤ │ Main body paragraph text goes here... │ ← Column item ├──────────────────────────────────────────┤ │ Company Name Confidential│ ← Row (footer) └──────────────────────────────────────────┘ ``` --- ## Quick Reference ``` Column = vertical stacking (think: stack of pancakes) Row = horizontal stacking (think: seats in a cinema row) ``` > **Tip:** This is the same convention used by CSS Flexbox (`flex-direction: column` / `row`), > Flutter (`Column` / `Row` widgets) — so the mental model transfers directly. ==================================================================== TerraFluent.Pdf.Reporting — Decorators URL: https://terrafluent.dev/docs/pdf/decorators/ ==================================================================== # Decorators Decorators wrap a container slot and modify how its content is drawn. They are chainable and compose from the **outside in**: ``` .Margin() → .Background() → .Border() → .Padding() → content ``` Each decorator method returns a new inner `IContainer` so the chain continues into the decorated area. --- ## Padding Adds space **inside** the element's box, between the background/border edge and the content. The background and border cover the padded region. ```csharp container.Padding(10) // all sides, in points container.Padding(0.5, Unit.Centimetre) // all sides, with unit container.PaddingVertical(8) // top + bottom container.PaddingVertical(4, Unit.Millimetre) container.PaddingHorizontal(12) // left + right container.PaddingTop(4) container.PaddingBottom(4) container.PaddingLeft(6) container.PaddingRight(6) ``` --- ## Margin Adds space **outside** the element's box, between the element and its surroundings. The margin region is always transparent — background and border start after it. ```csharp container.Margin(10) // all sides, in points container.Margin(0.5, Unit.Centimetre) // all sides, with unit container.MarginVertical(8) // top + bottom container.MarginVertical(4, Unit.Millimetre) container.MarginHorizontal(12) // left + right container.MarginTop(4) container.MarginBottom(4) container.MarginLeft(6) container.MarginRight(6) ``` ### Padding vs Margin illustrated ``` ┌─────────────────────────────────────────┐ ← outer slot │ margin (transparent) │ │ ┌─────────────────────────────────┐ │ │ │ background / border │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ padding │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ content │ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └─────────────────────────┘ │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` ```csharp // The red box is inset by 10 pt; text is inset 5 pt inside the red edge. container .Margin(10) .Background(Color.Red.Lighten4) .Padding(5) .Text("Inside the box"); ``` --- ## Background Fills the element's area with a solid colour. ```csharp container.Background(Color.Blue.Lighten5) container.Background("#1a4a8a") ``` --- ## Border Draws a rectangular border around the element's area. ```csharp container.Border(1.5, "#1a4a8a") // line width in points + hex colour container.Border(1) // black, 1 pt (default colour) ``` --- ## Rounded Border Draws a border with rounded corners. Optionally fills the interior to create a "rounded box" in a single call. ```csharp // Stroke-only rounded border container.RoundedBorder() // 8 pt radius, 1 pt black container.RoundedBorder(radius: 12, lineWidth: 1.5, hexColor: Color.Blue.Darken2) // Filled rounded box (background fill + rounded border in one call) container.RoundedBox() // white fill, 8 pt radius, 1 pt black border container.RoundedBox(radius: 10, fillHexColor: Color.Blue.Lighten5, borderHexColor: Color.Blue.Darken2) ``` ### Rounded card example ```csharp col.Item() .Margin(6) .RoundedBox(radius: 10, fillHexColor: Color.Grey.Lighten5, borderHexColor: Color.Grey.Lighten2) .Padding(12) .Column(card => { card.Item().Text("Card Title").Bold().FontSize(13); card.Item().PaddingTop(4).Text("Card body text.").FontColor(Color.Grey.Darken1); }); ``` --- ## Per-Edge Borders Draw a border on individual sides only, each with its own width and colour. Useful for table-cell separators or left-accent quote blocks. ```csharp container.BorderTop(1.5, Color.Grey.Darken2) // top only container.BorderBottom(1) // bottom only, black container.BorderLeft(3, Color.Blue.Darken2) // left accent container.BorderRight(0.5, "#cccccc") // right only ``` All four methods accept an optional `hexColor` (defaults to `"#000000"`). ### Table column-separator pattern ```csharp table.Row(row => { row.Cell().BorderBottom(1.5, Color.Grey.Darken2).Padding(6).Text("Column A").Bold(); row.Cell().BorderBottom(1.5, Color.Grey.Darken2).Padding(6).Text("Column B").Bold(); }); ``` ### Left-accent callout block ```csharp col.Item() .BorderLeft(4, Color.Blue.Darken2) .PaddingLeft(10).PaddingVertical(6) .Text("Note: this is important information.").Italic(); ``` --- ## Page Break Forces the document engine to start a new PDF page at the current position. A `PageBreak()` inside a `Column` is silently ignored when it falls at the very bottom of one page (i.e. the next page would start anyway). ```csharp container.Column(col => { col.Item().Text("Chapter 1").Bold().FontSize(18); col.Item().Text("Body text for chapter one..."); col.Item().PageBreak(); // ← forces new page here col.Item().Text("Chapter 2").Bold().FontSize(18); col.Item().Text("Body text for chapter two..."); }); ``` --- ## Hyperlink Wraps any child content in a clickable PDF URI annotation. Clicking the rendered area in a conforming PDF viewer opens the URL in a browser. ```csharp // Wrap a text span container.Hyperlink("https://example.com").Text("Visit Example"); // Wrap styled text container.Hyperlink("https://example.com") .Text("Click here").Underline().FontColor(Color.Blue.Medium); // Wrap an image (clickable logo) container.AlignCenter() .Hyperlink("https://example.com") .Image("logo.png", 120); ``` Hyperlink participates in the full decorator chain — chain it after alignment and margin decorators and before the content: ```csharp col.Item() .Margin(4) .Hyperlink("https://docs.example.com") .Text("Read the documentation").Underline().FontColor(Color.Blue.Darken2); ``` --- ### Internal Link Creates a clickable internal link (GoTo action) that navigates to a specific page within the same PDF document. This is the mechanism used by the automatic Table of Contents feature to make entries clickable. ```csharp // Jump to page 5, at the default vertical position (top of page) container.InternalLink(5).Text("See Chapter 5"); // Jump to page 3 at a specific Y coordinate (e.g. 150 pt from top) container.InternalLink(3, 150).Text("Back to top"); ``` Multiple internal links can be combined with other decorators. The target page must exist when the PDF is rendered; otherwise an `InvalidOperationException` is thrown during saving. --- ### Horizontal alignment Positions the child within the available width without changing available height. ```csharp container.AlignLeft() // default container.AlignCenter() // centres child horizontally container.AlignRight() // pushes child to the right edge ``` ### Vertical alignment Positions the child within the available height. ```csharp container.AlignMiddle() // centres child vertically container.AlignBottom() // pushes child to the bottom edge ``` ### Combining horizontal and vertical ```csharp container.AlignCenter().AlignMiddle().Text("Centred both axes"); ``` --- ## Lines Rule lines that span the full available width or height of their container. ```csharp // Horizontal rule container.LineHorizontal() // 1 pt, black container.LineHorizontal(2, Color.Blue.Darken2) // 2 pt, coloured // Vertical rule container.LineVertical() // 1 pt, black container.LineVertical(1.5, "#cccccc") ``` > **Tip:** Use a `ConstantItem` inside a `Row` as a thin vertical divider: > ```csharp > row.ConstantItem(1).Background(Color.Grey.Lighten2); > ``` --- ## ShowIf — Conditional Rendering Renders child content only when a condition is `true`. When `false` the slot is replaced with a zero-size no-op, so surrounding layout is unaffected. ```csharp container.ShowIf(isAdmin).Text("Admin panel"); container.ShowIf(invoice.IsPaid).Background(Color.Green.Lighten4).Padding(6).Text("PAID"); ``` --- ## Chaining Examples ### Card with coloured header ```csharp col.Item() .Margin(6) .Border(1, Color.Grey.Lighten2) .Column(card => { card.Item() .Background(Color.Blue.Darken2) .Padding(8) .Text("Card Title").Bold().FontColor(Color.White); card.Item() .Padding(10) .Text("Card body text goes here."); }); ``` ### Right-aligned badge ```csharp row.AutoItem() .Margin(4) .Background(Color.Green.Lighten4) .Border(1, Color.Green.Darken1) .Padding(4) .AlignCenter() .Text("NEW").Bold().FontSize(9).FontColor(Color.Green.Darken2); ``` ==================================================================== TerraFluent.Pdf.Reporting — Images URL: https://terrafluent.dev/docs/pdf/images/ ==================================================================== # Images TerraFluent.Pdf.Reporting supports **PNG** and **JPEG** image embedding with automatic aspect-ratio preservation. The format is detected from the data itself (magic bytes), so the file extension does not matter. In 1.4.0, images can also be supplied directly from `byte[]` or `Stream` instances, which is useful for embedded resources and generated content. --- ## Basic Usage ### Fill available width The image scales to fill the full width of its container slot while keeping the original aspect ratio. Height is calculated automatically; when the available height is the binding constraint, both axes shrink together so the image is never distorted. ```csharp container.Image("path/to/photo.jpg"); container.Image("path/to/diagram.png"); ``` ### From bytes or a stream Images can come from embedded resources, databases, HTTP responses, or generated data — no temporary file needed: ```csharp byte[] logoBytes = await httpClient.GetByteArrayAsync(logoUrl); container.Image(logoBytes, 120); using Stream s = assembly.GetManifestResourceStream("MyApp.logo.png")!; container.Image(s); // stream is read to the end; caller disposes it ``` ### Transparency RGBA PNGs keep their alpha channel — it is embedded as a PDF soft mask (`/SMask`), so transparent logos composite correctly over page backgrounds. Fully opaque images skip the mask automatically. Indexed-transparency (tRNS) PNGs are not supported and render opaque. ### Deduplication Identical image data used on multiple pages (for example a logo in a repeated header) is embedded **once** and shared document-wide — file size does not grow with the page count. ### Fixed width Constrains the image to a specific width in PDF points. Height is still computed from the aspect ratio. Useful for logos and icons that should not fill the page. ```csharp container.Image("logo.png", 120); // 120 pt wide container.Image("thumbnail.jpg", 60); ``` --- ## Positioning Fixed-Width Images Because `Image()` returns `IContainer`, wrap it with an alignment decorator to control horizontal position: ```csharp // Centred logo container.AlignCenter().Image("logo.png", 150); // Right-aligned stamp container.AlignRight().Image("stamp.png", 80); // Left-aligned (default, no wrapper needed) container.Image("icon.png", 32); ``` --- ## Combining with Other Decorators Images participate in the full decorator chain: ```csharp // Logo inside a padded, bordered box container .Border(1, Color.Grey.Lighten2) .Padding(8) .AlignCenter() .Image("logo.png", 100); // Full-width banner with a bottom accent bar page.Header().Column(col => { col.Item().Image("banner.jpg"); col.Item().Background(Color.Blue.Darken2).Padding(3); }); ``` --- ## Supported Formats | Format | Extensions | |--------|------------| | PNG | `.png` | | JPEG | `.jpg`, `.jpeg` | > Files are read from the file-system path supplied at render time. > Use `AppContext.BaseDirectory` to resolve paths relative to the executable: > ```csharp > string logo = Path.Combine(AppContext.BaseDirectory, "logo.png"); > container.Image(logo, 120); > ``` --- ## Checking File Existence When the image file may not be present (e.g. optional branding), guard with a file check and provide a text fallback: ```csharp if (File.Exists(logoPath)) container.Image(logoPath, 100); else container.Text("CompanyName").Bold().FontSize(18); ``` ==================================================================== TerraFluent.Pdf.Reporting — Page Sizes & Units URL: https://terrafluent.dev/docs/pdf/page-sizes-and-units/ ==================================================================== # Page Sizes & Units --- ## Page Sizes All constants live in `TerraFluent.Pdf.Reporting.Helpers.PageSize` and are expressed as `(double Width, double Height)` tuples in **PDF points** (1 pt = 1/72 inch). ### ISO A-Series | Constant | Width (pt) | Height (pt) | Approx. mm | |----------|-----------|------------|------------| | `PageSize.A0` | 2383.94 | 3370.39 | 841 × 1189 | | `PageSize.A1` | 1683.78 | 2383.94 | 594 × 841 | | `PageSize.A2` | 1190.55 | 1683.78 | 420 × 594 | | `PageSize.A3` | 841.89 | 1190.55 | 297 × 420 | | `PageSize.A4` | 595.28 | 841.89 | 210 × 297 | | `PageSize.A5` | 419.53 | 595.28 | 148 × 210 | | `PageSize.A6` | 297.64 | 419.53 | 105 × 148 | ### North American | Constant | Width (pt) | Height (pt) | Approx. inches | |----------|-----------|------------|----------------| | `PageSize.Letter` | 612.00 | 792.00 | 8.5 × 11 | | `PageSize.Legal` | 612.00 | 1008.00 | 8.5 × 14 | | `PageSize.Tabloid` | 792.00 | 1224.00 | 11 × 17 | | `PageSize.Executive` | 521.86 | 756.00 | 7.25 × 10.5 | ### Landscape Variant Use `PageSize.Landscape()` to swap width and height for any size: ```csharp page.Size(PageSize.Landscape(PageSize.A4)); // 841.89 × 595.28 pt page.Size(PageSize.Landscape(PageSize.Letter)); // 792.00 × 612.00 pt ``` ### Custom Size Pass explicit dimensions with an optional unit: ```csharp page.Size(148, 210, Unit.Millimetre); // A5 in millimetres page.Size(6, 4, Unit.Inch); // 6 × 4 inch card page.Size(300, 500); // raw points ``` --- ## Units All API methods that accept a measurement also accept an optional `Unit` parameter. Without a unit the value is interpreted as **PDF points**. | `Unit` value | Description | Conversion | |--------------|-------------|------------| | `Unit.Point` | PDF native unit (default) | 1 pt = 1/72 inch | | `Unit.Millimetre` | Millimetres | 1 mm ≈ 2.835 pt | | `Unit.Centimetre` | Centimetres | 1 cm ≈ 28.35 pt | | `Unit.Inch` | Inches | 1 in = 72 pt | ### Methods that accept a Unit ```csharp // Page margin page.Margin(2, Unit.Centimetre); page.MarginVertical(10, Unit.Millimetre); page.MarginHorizontal(0.75, Unit.Inch); // Container padding container.Padding(0.5, Unit.Centimetre); container.PaddingTop(5, Unit.Millimetre); // Container margin container.Margin(0.25, Unit.Inch); container.MarginLeft(8, Unit.Millimetre); ``` ### Manual conversion Use `UnitConversion.ToPoints()` when you need to convert a value yourself: ```csharp using TerraFluent.Pdf.Reporting.Helpers; double pts = UnitConversion.ToPoints(2.5, Unit.Centimetre); // ≈ 70.87 pt ``` ==================================================================== TerraFluent.Pdf.Reporting — Colors URL: https://terrafluent.dev/docs/pdf/colors/ ==================================================================== # Colors TerraFluent.Pdf.Reporting ships a full **Material Design**-inspired colour palette as static string constants in the `TerraFluent.Pdf.Reporting.Helpers.Color` class. All values are CSS hex strings compatible with every API that accepts a colour (`FontColor`, `Background`, `Border`, `LineHorizontal`, etc.). --- ## Special Constants ```csharp Color.White // "#FFFFFF" Color.Black // "#000000" Color.Transparent // "#00000000" ``` --- ## Full Palette Each colour family exposes shades from `Lighten5` (near-white) through `Medium` to `Darken4` (near-black). Not all families have every shade. ### Red | Constant | Hex | |----------|-----| | `Color.Red.Lighten5` | `#FFEBEE` | | `Color.Red.Lighten4` | `#FFCDD2` | | `Color.Red.Lighten3` | `#EF9A9A` | | `Color.Red.Lighten2` | `#E57373` | | `Color.Red.Lighten1` | `#EF5350` | | `Color.Red.Medium` | `#F44336` | | `Color.Red.Darken1` | `#E53935` | | `Color.Red.Darken2` | `#D32F2F` | | `Color.Red.Darken3` | `#C62828` | | `Color.Red.Darken4` | `#B71C1C` | ### Pink | Constant | Hex | |----------|-----| | `Color.Pink.Medium` | `#E91E63` | | `Color.Pink.Darken2` | `#C2185B` | ### Purple | Constant | Hex | |----------|-----| | `Color.Purple.Medium` | `#9C27B0` | | `Color.Purple.Darken2` | `#7B1FA2` | ### Deep Purple | Constant | Hex | |----------|-----| | `Color.DeepPurple.Medium` | `#673AB7` | | `Color.DeepPurple.Darken2` | `#512DA8` | ### Indigo | Constant | Hex | |----------|-----| | `Color.Indigo.Lighten5` | `#E8EAF6` | | `Color.Indigo.Medium` | `#3F51B5` | | `Color.Indigo.Darken2` | `#283593` | ### Blue | Constant | Hex | |----------|-----| | `Color.Blue.Lighten5` | `#E3F2FD` | | `Color.Blue.Lighten4` | `#BBDEFB` | | `Color.Blue.Lighten3` | `#90CAF9` | | `Color.Blue.Lighten2` | `#64B5F6` | | `Color.Blue.Lighten1` | `#42A5F5` | | `Color.Blue.Medium` | `#2196F3` | | `Color.Blue.Darken1` | `#1E88E5` | | `Color.Blue.Darken2` | `#1976D2` | | `Color.Blue.Darken3` | `#1565C0` | | `Color.Blue.Darken4` | `#0D47A1` | ### Teal | Constant | Hex | |----------|-----| | `Color.Teal.Medium` | `#009688` | | `Color.Teal.Darken2` | `#00796B` | ### Green | Constant | Hex | |----------|-----| | `Color.Green.Lighten5` | `#E8F5E9` | | `Color.Green.Lighten4` | `#C8E6C9` | | `Color.Green.Lighten3` | `#A5D6A7` | | `Color.Green.Lighten2` | `#81C784` | | `Color.Green.Lighten1` | `#66BB6A` | | `Color.Green.Medium` | `#4CAF50` | | `Color.Green.Darken1` | `#43A047` | | `Color.Green.Darken2` | `#388E3C` | | `Color.Green.Darken3` | `#2E7D32` | | `Color.Green.Darken4` | `#1B5E20` | ### Light Green | Constant | Hex | |----------|-----| | `Color.LightGreen.Medium` | `#8BC34A` | | `Color.LightGreen.Darken2` | `#689F38` | ### Lime | Constant | Hex | |----------|-----| | `Color.Lime.Medium` | `#CDDC39` | | `Color.Lime.Darken2` | `#AFB42B` | ### Yellow | Constant | Hex | |----------|-----| | `Color.Yellow.Medium` | `#FFEB3B` | | `Color.Yellow.Darken2` | `#F9A825` | ### Amber | Constant | Hex | |----------|-----| | `Color.Amber.Medium` | `#FFC107` | | `Color.Amber.Darken2` | `#FF8F00` | ### Orange | Constant | Hex | |----------|-----| | `Color.Orange.Medium` | `#FF9800` | | `Color.Orange.Darken2` | `#E65100` | ### Deep Orange | Constant | Hex | |----------|-----| | `Color.DeepOrange.Medium` | `#FF5722` | | `Color.DeepOrange.Darken2` | `#BF360C` | ### Brown | Constant | Hex | |----------|-----| | `Color.Brown.Lighten5` | `#EFEBE9` | | `Color.Brown.Medium` | `#795548` | | `Color.Brown.Darken2` | `#4E342E` | ### Grey | Constant | Hex | |----------|-----| | `Color.Grey.Lighten5` | `#FAFAFA` | | `Color.Grey.Lighten4` | `#F5F5F5` | | `Color.Grey.Lighten3` | `#EEEEEE` | | `Color.Grey.Lighten2` | `#E0E0E0` | | `Color.Grey.Lighten1` | `#BDBDBD` | | `Color.Grey.Medium` | `#9E9E9E` | | `Color.Grey.Darken1` | `#757575` | | `Color.Grey.Darken2` | `#616161` | | `Color.Grey.Darken3` | `#424242` | | `Color.Grey.Darken4` | `#212121` | ### Blue Grey | Constant | Hex | |----------|-----| | `Color.BlueGrey.Lighten5` | `#ECEFF1` | | `Color.BlueGrey.Lighten4` | `#CFD8DC` | | `Color.BlueGrey.Medium` | `#607D8B` | | `Color.BlueGrey.Darken2` | `#37474F` | | `Color.BlueGrey.Darken4` | `#263238` | --- ## Using Raw Hex Strings Any method that accepts a colour string also accepts a plain hex literal: ```csharp container.Background("#FFF8E1"); container.FontColor("#333333"); container.Border(1, "#DDDDDD"); ``` ==================================================================== TerraFluent.Pdf.Reporting — Templates URL: https://terrafluent.dev/docs/pdf/templates/ ==================================================================== # Templates Every PDF starts as a blank page — `PageDescriptor` gives you a solid `PageColor()`, and three empty layout slots (`Header()`, `Content()`, `Footer()`). Templates are a thin, opt-in layer on top of that: predefined, professionally-styled combinations of page background, decorative artwork, and header/footer that you apply in one call, so you get a branded-looking document instantly and only write the content that's actually unique to your document. Every predefined template treats its color/gradient/pattern as **one continuous design covering the whole page** — not a colored rectangle at the top with a plain page underneath it. A soft-tinted background, a full-page gradient, decorative accent shapes (rounded panels, corner-bleeding circles, a frame) all read as a single seamless piece of art, the same way a professionally-designed page-background template does; your content is then composed on top of it in `page.Content()`. A template never touches `Content()` — it only configures the page background/decoration, `Header()`, `Footer()`, margins, and the default text style. Your content always goes into `page.Content()` exactly as it does today. --- ## Quick start ```csharp using TerraFluent.Pdf.Reporting.Core; using TerraFluent.Pdf.Reporting.Helpers; using TerraFluent.Pdf.Reporting.Templating; PdfDocument.Create(doc => { doc.Page(page => { page.Size(PageSize.A4); page.UseTemplate(Templates.Corporate, opts => { opts.Title = "Q3 Financial Report"; opts.Subtitle = "Prepared for the Board of Directors"; opts.AccentColor = Color.Teal.Darken2; opts.LogoPath = "logo.png"; opts.FooterText = "Confidential — Acme Inc."; }); page.Content().Padding(30).Column(col => { col.Spacing(12); col.Item().Text("Executive summary...").Justify(); }); }); }) .PublishPdf("report.pdf"); ``` `UseTemplate` returns the `PageDescriptor`, so you can keep chaining (`.Margin(...)`, etc.) afterward — a template's settings are just a starting point a later call can still override. --- ## Predefined templates | Template | Look | Best for | |---|---|---| | `Templates.Corporate` | A light brand-tinted page (no plain-white gap), solid accent header band with logo/title/subtitle, a thin accent rule, and a pair of ribbon sweeps bleeding off the bottom edge | Reports, invoices, company profiles | | `Templates.Modern` | Near-white page organized by a full-bleed, two-tone accent rail down the left edge with a quiet dot-grid texture in the bottom-right corner; logo, title, subtitle, body, and footer all sit in one column to the rail's right | Product briefs, startup/tech-leaning documents | | `Templates.Minimal` | Near-plain white page, small title over a thin top rule, generous margins, and one pale arc grazing the bottom-right corner | Whitepapers, editorial documents | | `Templates.Elegant` | Soft page-wide tint of the accent color inside a classic double-rule frame, serif title framed by its own rules | Certificates, formal proposals | | `Templates.Cover` | Full-bleed diagonal gradient with ribbon sweeps framing the top and bottom edges, large centered title/subtitle in white | Cover pages, section dividers | All five are built from the same public fluent API you already use (`Background`, `BackgroundGradient`, `DecoratePage`, `Padding`, `Row`, `Column`, `Text`, `Image`, `LineHorizontal`, page numbers) — nothing about them is special or inaccessible to your own code. ### `Templates.Cover` is a cover-page template, not a body template `PageDescriptor.Content()` always returns the same underlying container — whatever your own `page.Content()...` calls draw there completely replaces anything a template could have pre-drawn *inside that container*. A template therefore can't reliably wrap `Content()` in a decoration (the very next line of your code would overwrite it) — but it *can* reliably reserve the visual *space* content will occupy, because margins are geometry, not something your `.Content()...` call ever touches. `Templates.Modern` uses exactly this trick: it draws a full-height accent rail with `DecoratePage` at a fixed position along the left edge and sets `page.Margin(...)` to clear it, so whatever you put in `Header()`/`Content()`/`Footer()` always lands to its right — see [Page decoration](#page-decoration) below. `Templates.Cover` deliberately doesn't do this: its full-bleed gradient sits directly behind whatever you put in `Content()`, styled for a short, large-type cover page (title + subtitle) — not a paragraph of body text. For a multi-page document, use a Cover template on the first page and a body template (`Corporate`, `Modern`, `Minimal`, `Elegant`) on the rest, in the same document: ```csharp PdfDocument.Create(doc => { doc.Page(page => // page 1: cover { page.Size(PageSize.A4); page.UseTemplate(Templates.Cover, o => { o.Title = "Annual Report 2026"; o.Subtitle = "Building for the next decade"; }); }); doc.Page(page => // page 2+: body { page.Size(PageSize.A4); page.UseTemplate(Templates.Corporate, o => o.Title = "Annual Report 2026"); page.Content().Text("Financial summary..."); }); }) .PublishPdf("annual-report.pdf"); ``` --- ## `TemplateOptions` reference | Property | Type | Default | Used by | |---|---|---|---| | `Title` | `string?` | `null` | All templates — main header/cover title | | `Subtitle` | `string?` | `null` | All templates — line under `Title` | | `AccentColor` | `string` | `Color.Blue.Darken2` | All templates — brand color | | `SecondaryColor` | `string?` | darker shade of `AccentColor` | `Modern` — rail's lower tone; `Cover` — gradient end color | | `LogoPath` | `string?` | `null` | `Corporate`, `Modern` — header logo (ignored if the file doesn't exist) | | `LogoBytes` | `byte[]?` | `null` | Same as `LogoPath`, from in-memory bytes | | `ShowPageNumbers` | `bool` | `true` | All templates — footer "Page X of Y" | | `FooterText` | `string?` | `null` | Templates with a footer band | --- ## Gradient and image backgrounds Templates are built on general-purpose primitives you can use directly, without going through a template at all. **Full-page background** (`PageDescriptor`, mutually exclusive — the last one you call wins): ```csharp page.PageColor("#FFFFFF"); // solid page.PageBackgroundGradient("#1976D2", "#0D47A1", GradientDirection.TopToBottom); page.PageBackgroundImage("background.jpg", ImageFit.Cover); // or Stretch ``` **In-layout gradient band** (any container — header, footer, a card): ```csharp container.BackgroundGradient("#1976D2", "#0D47A1", GradientDirection.LeftToRight) .Padding(16) .Text("Banner text").FontColor(Color.White); ``` > This library has no native PDF shading support, so gradients are > approximated as ~48 thin solid-color bands rather than a true smooth > blend. At normal viewing sizes the banding is not visually distinct — on > `Templates.Cover`'s diagonal gradient it even reads as an intentional > subtle stripe texture. ## Page decoration `page.DecoratePage(canvas => ...)` draws arbitrary vector artwork — the same `VectorCanvas` API `.Canvas(height, draw)` already gives you inside a layout (rectangles, rounded rectangles, circles/ellipses, and arbitrary Bézier `Path(...)`s) — as a layer over the page's background and under `Header()`/`Content()`/`Footer()`. Coordinates are page-relative, top-left origin, spanning the full page (`canvas.AllocatedWidth`/`AllocatedHeight`) regardless of margins. This is how the predefined templates build their corner-bleeding accent circles, `Elegant`'s frame, and `Modern`'s accent rail and dot-grid: ```csharp page.DecoratePage(canvas => { double w = canvas.AllocatedWidth, h = canvas.AllocatedHeight; // A soft accent circle bleeding off a corner — center it AT the corner // so only a quarter of it is actually visible on the page. canvas.FillCircle(w, h, Math.Min(w, h) * 0.15, "#BBDEFB"); // A refined frame inset from the page edges. canvas.StrokeRoundedRect(18, 18, w - 36, h - 36, 6, "#1976D2", 1); }); ``` ### Ribbon sweeps The sweeping bands the predefined templates use are just a **cubic Bézier stroked at a large line width** — the band's two edges are the stroke's own offsets, so there's no offset-curve geometry to compute: ```csharp page.DecoratePage(canvas => { double w = canvas.AllocatedWidth, h = canvas.AllocatedHeight; canvas.Path(p => p .MoveTo(-40, h * 0.26) // start off-page… .CurveTo(w * 0.16, h * 0.02, w * 0.42, -26, w + 40, -40) // …end off-page .Stroke("#1976D2", 30)); // thickness = band width }); ``` Two details matter: push the end points *past* the page edges (PDF's default flat line cap would otherwise show as a blunt edge mid-page), and remember a stroke can't be clipped — to make a ribbon stop at a shape, draw the ribbon first and paint an opaque shape (e.g. a content panel or card) over it. **Reserving space for a decorated content panel**: because `DecoratePage` runs before `Content()` draws and margins are geometry your later `.Content()...` call never overwrites, you can draw a shape (e.g. a rounded white panel) and set `page.Margin(...)` to match its bounds so your content always lands inside it. `Templates.Modern` uses the same underlying idea in its simpler form — a fixed-position accent rail with the left margin set to clear it — but the technique generalizes to any shape: ```csharp const double top = 50, side = 45, bottom = 50, pad = 20; page.Margin(top, side, bottom, side); page.DecoratePage(canvas => { double w = canvas.AllocatedWidth, h = canvas.AllocatedHeight; canvas.FillRoundedRect(side - pad, top - pad, w - (side - pad) * 2, h - (top - pad) - (bottom - pad), 20, Color.White); }); ``` > Call `page.Size(...)` before `page.DecoratePage(...)`/`page.Margin(...)` in > a template: the decoration callback itself always sees the page's final > size (it runs at render time), but margin math you compute inline needs > the final `page.PageWidth`/`PageHeight` already set. --- ## Custom templates A template is just a page-configuration callback — build your own with `PdfTemplate.Create` using the exact same API the predefined templates use: ```csharp var brandTemplate = PdfTemplate.Create((page, opts) => { page.PageColor(Color.White); page.Header().Background(opts.AccentColor).Padding(16) .Text(opts.Title ?? string.Empty).Bold().FontSize(20).FontColor(Color.White); page.Footer().AlignCenter().Text(t => { t.Span("Page "); t.CurrentPageNumber(); }); }); page.UseTemplate(brandTemplate, opts => opts.Title = "Custom Report"); ``` Nothing about `PdfTemplate` is special — it's a reusable `Action<PageDescriptor, TemplateOptions>`, so anything you could write directly in `doc.Page(page => ...)` you can factor into a template and reuse across documents. ==================================================================== TerraFluent.Pdf.Reporting — Custom Fonts & Full Unicode URL: https://terrafluent.dev/docs/pdf/custom-fonts/ ==================================================================== # Custom Fonts & Full Unicode TerraFluent.Pdf.Reporting ships with the three PDF standard-14 families (Helvetica, Times, Courier), rendered via `WinAnsiEncoding` — this covers Western European languages but not brand typefaces, and not scripts outside Windows-1252 (Cyrillic, Greek, and beyond). `FontFamily.Register` embeds a real TrueType font so you can use any typeface, with full Unicode text support wherever that font has glyphs. No external packages are required. Font parsing, embedding, and CID-keyed text encoding are implemented entirely in `System`-namespace code. --- ## Quick start ```csharp using TerraFluent.Pdf.Reporting.Helpers; // Register once (e.g. at application startup) — parsing is cached, so this // is safe to call from a long-lived server process and reused by every // document rendered afterwards, including concurrently. FontFamily.Register("Brand", "fonts/Brand-Regular.ttf"); FontFamily.Register("Brand", "fonts/Brand-Bold.ttf", bold: true); PdfDocument.Create(container => { container.Page(page => { page.Size(PageSize.A4); page.DefaultTextStyle(s => s.FontFamily("Brand")); page.Content().Column(col => { col.Item().Text("Héllo, Привет, Γειά σου").FontSize(18); col.Item().Text("Now in bold.").Bold(); }); }); }) .PublishPdf("branded.pdf"); ``` `TextStyle.FontFamily("Brand")` and `.Bold()`/`.Italic()` are the same API you already use for the built-in families — nothing else changes. --- ## `FontFamily.Register` overloads | Overload | Use when | |----------|----------| | `Register(string familyName, string fontFilePath, bool bold = false, bool italic = false)` | Loading directly from a `.ttf`/`.otf` file on disk. | | `Register(string familyName, byte[] fontFileBytes, bool bold = false, bool italic = false)` | The font data is already in memory (embedded resource, downloaded, etc.). | | `Register(string familyName, Stream fontFileStream, bool bold = false, bool italic = false)` | Reading from a stream (read to completion; the stream is not disposed). | A family can have up to four registered variants — regular, bold, italic, and bold-italic — registered independently under the same `familyName`. Requesting a style that wasn't registered falls back to the closest available variant instead of throwing (e.g. calling `.Bold()` on a family that only registered a regular file renders with the regular outlines). --- ## What gets embedded Each registered variant is embedded as a `Type0`/`CIDFontType2` composite font with `Identity-H` encoding: text is addressed by glyph ID, not by a fixed 256-slot encoding, so any glyph the font contains can be shown — not just Windows-1252. A `ToUnicode` CMap is included so copy/paste and text extraction recover the correct Unicode text. The font is embedded **once per document**, even when used across many pages or many times per page — a registered font used throughout a 200-page report still costs one embedded copy. Characters the font has no glyph for render as the font's `.notdef` glyph (usually a blank box) rather than throwing — the same graceful-fallback behaviour as the standard fonts substituting `?` for unmappable characters. --- ## Known limitations - **TrueType outlines only.** `.ttf` files, and `.otf` files that still carry a `glyf`/`loca` table, are supported. CFF-flavoured OpenType (`OTTO`) and TrueType Collections (`.ttc`) throw `NotSupportedException` — a different embedding path (`/FontFile3`, `CIDFontType0`) would be needed for those. Future versions may add it. - **No glyph subsetting.** The whole font file is embedded, so a large font increases the output PDF's size accordingly. Future versions may add subsetting to embed only the glyphs actually used. - **No synthetic bold/italic.** If a style wasn't registered, TerraFluent.Pdf.Reporting falls back to the closest registered variant rather than skewing or thickening glyphs to approximate it. - **Partial OpenType shaping for Devanagari — no full GSUB/GPOS engine.** Codepoints map through the font's `cmap` to a glyph ID as usual, but TerraFluent.Pdf.Reporting applies two pure-C# corrections automatically for any text drawn through a registered custom font (no dependency, no public API — this is transparent): - **Matra reordering**: the vowel sign ि (`U+093F`) is stored after its consonant in Unicode but must render before it; TerraFluent.Pdf.Reporting moves it to the correct position before the cluster it attaches to. - **Conjunct ligatures**: TerraFluent.Pdf.Reporting reads the font's own `GSUB` table for the `half`/`akhn`/`cjct` features and substitutes the ligature glyphs the font itself defines — e.g. स्व, स्थ, क्ष, ज्ञ render as proper joined forms, not separate glyphs with a visible ् mark, whenever the font provides those substitution rules (most well-designed Devanagari fonts define them for nearly every consonant). - **र-conjuncts**: reph (र् at the start of a cluster, e.g. धर्म, वर्तमान) is corrected by moving the र् pair to the end of the cluster it attaches to, then substituting it for the font's reph glyph via GSUB's `rphf` feature. Below-base/post-base 'ra' forms (प्र, क्र, त्र, ष्ट्र, …) are substituted via GSUB's `rkrf` feature — no reordering needed there, since the codepoints are already in the right order. Both use the same substitution mechanism as the other conjunct ligatures above, applied automatically whenever the font provides those GSUB rules. - **Not covered**: `blwf` — below-base forms for consonants *other* than र (used by some fonts for other subjoined forms) — uses contextual GSUB lookups TerraFluent.Pdf.Reporting's reader doesn't parse, so those specific cases may still draw as separate glyphs. - This is intentional: `src/TerraFluent.Pdf.Reporting` stays pure managed C# with no native dependency and will never bundle a full shaping engine (no GPOS mark positioning, no general Indic reordering beyond the ि matra and reph cases above) — this is a scoped, font-data-driven substitution, not a shaping engine. ==================================================================== TerraFluent.Pdf.Reporting — Encryption & Password Protection URL: https://terrafluent.dev/docs/pdf/encryption/ ==================================================================== # Encryption & Password Protection TerraFluent.Pdf.Reporting encrypts documents with **AES-256** using the PDF Standard Security Handler **Revision 6** (ISO 32000-2 / PDF 2.0) by default — SHA-2 based key derivation with no MD5 or RC4 in the chain. Encrypted documents are opened by every major PDF viewer — Adobe Acrobat 9+ (2008), Chrome, Edge, Firefox, Preview, Foxit, Okular, and others. For documents that must open in very old viewers, legacy **AES-128 (Revision 4)** remains available via `Algorithm = EncryptionAlgorithm.Aes128`. No external packages are required. The entire implementation uses `System.Security.Cryptography` only. --- ## Quick start ```csharp PdfDocument.Create(container => { container.Encrypt(new EncryptionOptions { UserPassword = "open123", // required to open the document OwnerPassword = "admin456", // grants full access regardless of permissions Permissions = PdfPermissions.Print | PdfPermissions.CopyText, }); container.Page(page => { page.Size(PageSize.A4); page.Margin(2, Unit.Centimetre); page.Content().Text("This PDF is password-protected.").Bold().FontSize(18); }); }) .PublishPdf("protected.pdf"); ``` --- ## `EncryptionOptions` properties | Property | Type | Default | Description | |----------|------|---------|-------------| | `UserPassword` | `string?` | `null` | Password required to *open* the document. `null` or empty = no open password (but content is still encrypted and permissions still enforced). | | `OwnerPassword` | `string?` | `null` | Password granting *full* access, bypassing all `Permissions` restrictions. When `null` a random value is used so the encryption dictionary is always valid. | | `Permissions` | `PdfPermissions` | `All` | Bitwise combination of permission flags that apply when the user opens with the `UserPassword`. | | `Algorithm` | `EncryptionAlgorithm` | `Aes256` | `Aes256` = Revision 6 (modern, PDF 2.0, default). `Aes128` = Revision 4 for viewers released before ~2008. | --- ## `PdfPermissions` flags Combine flags with the bitwise-OR operator: ```csharp PdfPermissions.Print | PdfPermissions.CopyText ``` | Flag | Description | |------|-------------| | `PdfPermissions.Print` | High-quality printing | | `PdfPermissions.PrintLowResolution` | Degraded (low-resolution) printing only | | `PdfPermissions.ModifyContents` | Modify document contents | | `PdfPermissions.CopyText` | Copy or extract text and graphics | | `PdfPermissions.ModifyAnnotations` | Add or modify annotations and form fields | | `PdfPermissions.FillForms` | Fill in interactive form fields | | `PdfPermissions.ExtractForAccessibility` | Text extraction for screen readers | | `PdfPermissions.AssembleDocument` | Insert, rotate, or delete pages | | `PdfPermissions.All` | All permissions granted (default) | | `PdfPermissions.None` | No permissions — view only | --- ## Common patterns ### View-only — no printing or copying ```csharp container.Encrypt(new EncryptionOptions { UserPassword = "readonly", Permissions = PdfPermissions.None, }); ``` ### Open without a password, but restrict printing Leaving `UserPassword` empty means the viewer opens the document without prompting, while still enforcing the permission flags: ```csharp container.Encrypt(new EncryptionOptions { OwnerPassword = "admin", Permissions = PdfPermissions.None, // viewer cannot print or copy }); ``` ### Owner password only — full restriction for all users ```csharp container.Encrypt(new EncryptionOptions { UserPassword = "userpass", OwnerPassword = "ownerpass", Permissions = PdfPermissions.Print | PdfPermissions.ExtractForAccessibility, }); ``` ### Encrypt everything, allow all permissions This encrypts content (making copy-paste from outside viewers harder) while allowing full interactive use inside the viewer: ```csharp container.Encrypt(new EncryptionOptions { UserPassword = "open", Permissions = PdfPermissions.All, }); ``` --- ## Technical details ### AES-256 — Revision 6 (default) | Property | Value | |----------|-------| | Security Handler | PDF Standard Security Handler | | Revision | 6 (ISO 32000-2 §7.6.4 / PDF 2.0) | | Content cipher | AES-256 CBC, file encryption key used directly (V5 has no per-object keys) | | IV | 16-byte random (per string/stream) | | Padding | PKCS#7 to 16-byte boundary | | Key derivation | SHA-256/384/512 iterated hash (algorithm 2.B) with random salts | | O / U entries | 48-byte verifiers (algorithms 8 & 9) | | OE / UE entries | File key wrapped with AES-256 under password-derived intermediate keys | | Perms entry | Permission bits encrypted with the file key (algorithm 10) | | Passwords | UTF-8, up to 127 bytes | | PDF version | 2.0 (set automatically) | ### AES-128 — Revision 4 (legacy opt-in) | Property | Value | |----------|-------| | Revision | 4 (PDF 1.6 / §7.6.5) | | Content cipher | AES-128 CBC with per-object keys (Algorithm 1 — FEK + obj/gen bytes + `sAlT`) | | Key derivation | MD5 + 50 rounds (PDF §7.6.3.3 Algorithm 2) | | O entry | Algorithm 3 — MD5 key + RC4 × 20 | | U entry | Algorithm 5 (Rev 4) — MD5 + RC4 × 20 | | PDF version | 1.6 (set automatically) | > **Note on MD5/RC4:** used exclusively in the Revision 4 key-derivation steps > mandated by the PDF specification — never for content encryption, and not at > all in the Revision 6 default. ### What gets encrypted - **Content streams** — the PDF drawing operators for every page - **Image XObjects** — PNG and JPEG pixel data (and alpha soft masks) - **Strings** — document metadata, bookmark titles, hyperlink URIs ### What is NOT encrypted (per PDF specification) - The `/Encrypt` dictionary itself (§7.6.1) - Cross-reference tables and trailer - Stream lengths - The `%PDF-` header ### Zero-dependency The implementation uses only `System.Security.Cryptography` (`Aes`, `SHA256`/`SHA384`/`SHA512`, `MD5` for Rev 4 only, and `RandomNumberGenerator`). --- ## Calling `Encrypt` in your document Call `container.Encrypt(options)` once inside the `PdfDocument.Create` callback. It may be called before or after adding pages; the encryption is applied when `PublishPdf()` is called. Calling `Encrypt` a second time replaces the previous settings. ```csharp PdfDocument.Create(container => { // Encryption must be configured before PublishPdf() is called. // Position relative to Page() calls does not matter. container.Encrypt(new EncryptionOptions { UserPassword = "secret" }); container.MetadataTitle("Confidential Report"); container.Page(page => { /* … */ }); container.Page(page => { /* … */ }); }) .PublishPdf("report.pdf"); ``` ==================================================================== TerraFluent.Pdf.Reporting — Vector Graphics URL: https://terrafluent.dev/docs/pdf/vector-graphics/ ==================================================================== # Vector Graphics TerraFluent.Pdf.Reporting provides a fluent **Canvas API** for drawing vector graphics directly inside any layout container. You can render lines, rectangles, circles, ellipses, rounded rectangles, arbitrary Bézier paths, polygons, and grids — all without any external dependencies. --- ## Adding a canvas Call `container.Canvas(height, draw)` anywhere a container slot is available. The canvas occupies the full available width and the exact height you specify. ```csharp container.Canvas(120, c => { c.FillRect(0, 0, 200, 80, Color.Blue.Lighten4); c.StrokeRect(0, 0, 200, 80, Color.Blue.Darken2, 1.5); c.Line(0, 40, 200, 40, Color.Blue.Medium, 0.5); }); ``` | Parameter | Type | Description | |-----------|------|-------------| | `height` | `double` | Canvas height in PDF points (must be > 0) | | `draw` | `Action<VectorCanvas>` | Callback that issues drawing commands | ### Coordinate system All coordinates are in **PDF points** with a **top-left origin** (0, 0) at the upper-left corner of the canvas element — consistent with TerraFluent.Pdf.Reporting's layout coordinate system. ``` (0,0) ──────────────────────► x │ │ canvas area │ ▼ y ``` --- ## VectorCanvas primitives Every method returns `this` so calls can be chained. --- ### Lines ```csharp canvas.Line(x1, y1, x2, y2, hexColor = "#000000", lineWidth = 1); ``` Draws a straight line from `(x1, y1)` to `(x2, y2)`. ```csharp c.Line(0, 20, 300, 20, "#CCCCCC", 0.5); // thin grey rule c.Line(0, 0, 150, 80, Color.Red.Medium, 2); ``` --- ### Rectangles Three variants give you fill-only, stroke-only, or both: ```csharp // Filled rectangle canvas.FillRect(x, y, width, height, hexColor = "#000000"); // Stroked (outline) rectangle canvas.StrokeRect(x, y, width, height, hexColor = "#000000", lineWidth = 1); // Filled + stroked rectangle canvas.DrawRect(x, y, width, height, fillHex = "#FFFFFF", strokeHex = "#000000", lineWidth = 1); ``` ```csharp c.FillRect ( 0, 0, 80, 50, Color.Blue.Lighten3); c.StrokeRect(100, 0, 80, 50, Color.Blue.Darken2, 1.5); c.DrawRect (200, 0, 80, 50, Color.Blue.Lighten5, Color.Blue.Darken2, 1); ``` --- ### Rounded rectangles Identical variants to the rectangle API, but with a `radius` parameter for the corner curve: ```csharp canvas.FillRoundedRect (x, y, w, h, radius, hexColor = "#000000"); canvas.StrokeRoundedRect(x, y, w, h, radius, hexColor = "#000000", lineWidth = 1); canvas.DrawRoundedRect (x, y, w, h, radius, fillHex = "#FFFFFF", strokeHex = "#000000", lineWidth = 1); ``` ```csharp c.FillRoundedRect ( 0, 0, 90, 50, 6, "#2E6DA4"); // r=6 badge c.StrokeRoundedRect(110, 0, 90, 50, 12, "#E87722", 2); // r=12 outline c.DrawRoundedRect (220, 10, 90, 30, 15, "#FFF", "#1A3C5E", 1); // pill ``` --- ### Circles ```csharp canvas.FillCircle (cx, cy, radius, hexColor = "#000000"); canvas.StrokeCircle(cx, cy, radius, hexColor = "#000000", lineWidth = 1); canvas.DrawCircle (cx, cy, radius, fillHex = "#FFFFFF", strokeHex = "#000000", lineWidth = 1); ``` `(cx, cy)` is the centre of the circle. ```csharp c.FillCircle ( 40, 40, 30, Color.Blue.Medium); c.StrokeCircle(120, 40, 30, Color.Orange.Medium, 2); c.DrawCircle (200, 40, 30, Color.Green.Lighten4, Color.Green.Darken2, 1.5); ``` --- ### Ellipses Same variants as circles, but with independent horizontal (`rx`) and vertical (`ry`) radii: ```csharp canvas.FillEllipse (cx, cy, rx, ry, hexColor = "#000000"); canvas.StrokeEllipse(cx, cy, rx, ry, hexColor = "#000000", lineWidth = 1); canvas.DrawEllipse (cx, cy, rx, ry, fillHex = "#FFFFFF", strokeHex = "#000000", lineWidth = 1); ``` ```csharp c.FillEllipse(100, 40, 80, 30, Color.Purple.Lighten3); // wide, flat ellipse ``` --- ### Grid helper Draws a full-canvas grid of evenly spaced vertical and horizontal lines: ```csharp canvas.Grid(cellWidth, cellHeight = null, hexColor = "#CCCCCC", lineWidth = 0.5); ``` When `cellHeight` is `null`, square cells are used (`cellHeight = cellWidth`). ```csharp c.Grid(20); // 20 × 20 pt square grid, light grey c.Grid(30, 20, "#E0E0E0", 0.3); // 30 × 20 pt rectangular grid ``` > **Note:** `Grid` reads the canvas's allocated width and height to fill the area, > so it must be called inside the `Canvas(height, draw)` callback (not stored and > called later). --- ## Arbitrary paths with `PathDescriptor` `canvas.Path(p => ...)` gives you full control via a fluent `PathDescriptor`. Use it for triangles, custom polygons, Bézier curves, compound shapes, and shapes with holes. ### Move and line commands ```csharp canvas.Path(p => p .MoveTo(50, 10) // lift pen, move to (50, 10) .LineTo(90, 80) // line to (90, 80) .LineTo(10, 80) // line to (10, 80) .Close() // close subpath back to (50, 10) .Fill(Color.Blue.Lighten3) .Stroke(Color.Blue.Darken2, 1.5)); ``` ### Cubic Bézier curves ```csharp p.CurveTo(cx1, cy1, cx2, cy2, x, y) ``` Draws a cubic Bézier curve from the current point to `(x, y)`, using `(cx1, cy1)` and `(cx2, cy2)` as control points. ```csharp canvas.Path(p => p .MoveTo(10, 60) .CurveTo(30, 10, 70, 10, 90, 60) // smooth arch .Stroke("#1A3C5E", 2)); ``` ### Convenience shapes on PathDescriptor These helpers append subpaths to the current descriptor: | Method | Description | |--------|-------------| | `Rect(x, y, width, height)` | Rectangular subpath | | `Ellipse(cx, cy, rx, ry)` | Ellipse subpath (cubic Bézier approximation) | | `Circle(cx, cy, radius)` | Circle subpath | | `Polyline((x,y)[] points)` | Open polyline through 2+ points | | `Polygon((x,y)[] points)` | Closed polygon through 3+ points | ```csharp // Star-of-David using two overlapping triangles canvas.Path(p => p .Polygon((50,10), (90,80), (10,80)) .Fill(Color.Blue.Lighten4) .Stroke(Color.Blue.Darken2, 1)); canvas.Path(p => p .Polygon((50,80), (10,10), (90,10)) .Fill(Color.Blue.Lighten4) .Stroke(Color.Blue.Darken2, 1)); ``` ### Paint methods | Method | Description | |--------|-------------| | `.Fill(hexColor)` | Fill the path with the given colour | | `.Stroke(hexColor, lineWidth = 1)` | Stroke the path outline | | `.UseEvenOddFill()` | Use even-odd rule (for shapes with holes, e.g. donuts) | You can call both `.Fill()` and `.Stroke()` on the same path to fill and stroke it. ### Shapes with holes (even-odd fill) ```csharp // Donut: outer circle + inner circle, even-odd fill creates the hole canvas.Path(p => p .Circle(100, 60, 50) // outer .Circle(100, 60, 25) // inner (becomes a hole) .Fill(Color.Orange.Medium) .UseEvenOddFill()); ``` --- ## All `VectorCanvas` methods at a glance | Method | Description | |--------|-------------| | `Line(x1,y1, x2,y2, color, lw)` | Straight line | | `FillRect(x,y,w,h, color)` | Filled rectangle | | `StrokeRect(x,y,w,h, color, lw)` | Stroked rectangle | | `DrawRect(x,y,w,h, fill, stroke, lw)` | Filled + stroked rectangle | | `FillRoundedRect(x,y,w,h, r, color)` | Filled rounded rectangle | | `StrokeRoundedRect(x,y,w,h, r, color, lw)` | Stroked rounded rectangle | | `DrawRoundedRect(x,y,w,h, r, fill, stroke, lw)` | Filled + stroked rounded rect | | `FillCircle(cx,cy, r, color)` | Filled circle | | `StrokeCircle(cx,cy, r, color, lw)` | Stroked circle | | `DrawCircle(cx,cy, r, fill, stroke, lw)` | Filled + stroked circle | | `FillEllipse(cx,cy, rx,ry, color)` | Filled ellipse | | `StrokeEllipse(cx,cy, rx,ry, color, lw)` | Stroked ellipse | | `DrawEllipse(cx,cy, rx,ry, fill, stroke, lw)` | Filled + stroked ellipse | | `Path(Action<PathDescriptor>)` | Arbitrary path with full Bézier support | | `Grid(cw, ch?, color, lw)` | Full-canvas rectangular grid | --- ## All `PathDescriptor` methods at a glance | Method | Description | |--------|-------------| | `MoveTo(x, y)` | Move current point without drawing | | `LineTo(x, y)` | Straight line to point | | `CurveTo(cx1,cy1, cx2,cy2, x,y)` | Cubic Bézier curve | | `Close()` | Close subpath to its start | | `Rect(x,y,w,h)` | Append rectangular subpath | | `Ellipse(cx,cy,rx,ry)` | Append ellipse subpath | | `Circle(cx,cy,r)` | Append circle subpath | | `Polyline(points[])` | Append open polyline (≥ 2 points) | | `Polygon(points[])` | Append closed polygon (≥ 3 points) | | `Fill(hexColor)` | Set fill paint | | `Stroke(hexColor, lw)` | Set stroke paint and width | | `UseEvenOddFill()` | Switch to even-odd fill rule | --- ## Practical examples ### Simple bar chart ```csharp (string Label, double Value)[] data = [("Q1", 38), ("Q2", 52), ("Q3", 71), ("Q4", 64)]; double maxValue = 80; double canvasHeight = 120; double barWidth = 40; double gap = 20; container.Canvas(canvasHeight, c => { for (int i = 0; i < data.Length; i++) { double barH = data[i].Value / maxValue * (canvasHeight - 20); double x = i * (barWidth + gap); double y = canvasHeight - 20 - barH; c.FillRoundedRect(x, y, barWidth, barH, 3, Color.Blue.Medium); } // baseline c.Line(0, canvasHeight - 20, data.Length * (barWidth + gap), canvasHeight - 20, Color.Grey.Lighten2, 0.5); }); ``` ### Donut chart segment ```csharp // Draw a filled donut wedge using Path + UseEvenOddFill container.Canvas(160, c => { // Full outer circle minus inner circle c.Path(p => p .Circle(80, 80, 60) // outer radius .Circle(80, 80, 35) // inner radius (hole) .Fill(Color.Blue.Lighten3) .UseEvenOddFill()); c.Path(p => p .Circle(80, 80, 60) .Stroke(Color.White, 2)); }); ``` ### Sparkline ```csharp double[] values = [22, 35, 29, 48, 41, 60, 55, 73]; double canvasH = 60; double stepX = 40; container.Canvas(canvasH, c => { // Shaded area under the line c.Path(p => { p.MoveTo(0, canvasH); for (int i = 0; i < values.Length; i++) p.LineTo(i * stepX, canvasH - (values[i] / 80.0 * canvasH)); p.LineTo((values.Length - 1) * stepX, canvasH); p.Close(); p.Fill(Color.Blue.Lighten5); }); // Line on top c.Path(p => { p.MoveTo(0, canvasH - (values[0] / 80.0 * canvasH)); for (int i = 1; i < values.Length; i++) p.LineTo(i * stepX, canvasH - (values[i] / 80.0 * canvasH)); p.Stroke(Color.Blue.Darken2, 1.5); }); }); ``` --- ## Sample The `10_VectorGraphicsShowcase.cs` sample in `samples/TerraFluent.Pdf.Reporting.Sample/Samples/` generates a 4-page PDF demonstrating every primitive and three chart types: ``` Page 1 Primitives reference sheet (lines, rects, rounded rects, circles, ellipses) Page 2 Arbitrary paths — triangles, polygons, Bézier curves, compound shapes Page 3 Data visualisation — bar chart, donut chart, line/sparkline chart Page 4 Design patterns — badges, progress bars, callout boxes, icon grid ``` Run it with: ```sh cd samples/TerraFluent.Pdf.Reporting.Sample dotnet run # Output: 10_vector_graphics_showcase.pdf ``` ==================================================================== TerraFluent.Pdf.Reporting — Table of Contents URL: https://terrafluent.dev/docs/pdf/table-of-contents/ ==================================================================== # Table of Contents TerraFluent.Pdf.Reporting can automatically generate a navigable Table of Contents page from the headings (`.H1()` — `.H6()`) used throughout your document. --- ## Adding a TOC page Call `container.TableOfContents()` inside the document composition callback. The TOC page is inserted at that exact point in the document sequence. ```csharp PdfDocument.Create(container => { // TOC page is placed first container.TableOfContents(p => { p.Size(PageSize.A4); p.Margin(2, Unit.Centimetre); p.PageColor(Color.White); p.DefaultTextStyle(s => s.FontSize(11)); }); // Rest of the document (chapters, sections, etc.) container.Page(page => { /* … */ }); }).PublishPdf("output.pdf"); ``` The TOC page can be configured like any other page (size, margins, default text style, header/footer). ### Optional configuration The `TableOfContents` method accepts an optional `Action<PageDescriptor>` for per-page settings. If omitted, sensible defaults are used. --- ## Headings (H1 – H6) TerraFluent.Pdf.Reporting provides six levels of headings via extension methods on `IContainer`: | Method | Typical use | Default style | |--------|------------|---------------| | `.H1(string)` | Document / chapter title | 24 pt, bold | | `.H2(string)` | Major section heading | 20 pt, bold | | `.H3(string)` | Sub-section heading | 16 pt, bold | | `.H4(string)` | Minor heading | 14 pt, italic | | `.H5(string)` | Small heading | 12 pt, bold | | `.H6(string)` | Smallest heading | 11 pt, regular | All six methods accept plain text and return a `TextDescriptor`, so you can customise the appearance further: ```csharp container.H1("Chapter 1") .FontColor(Color.Blue.Darken2) .Underline(); container.H2("2.1 Installation") .FontSize(18) .FontColor(Color.Grey.Darken2); ``` ### Where to place headings Headings can be added anywhere a normal text element is allowed — directly in a `Column`, inside a decorated container, or nested in Rows and Tables. The TOC engine walks the entire element tree, collecting every heading it finds in the order they would be rendered. --- ## How it works TerraFluent.Pdf.Reporting uses a **two-pass layout engine**: 1. **First pass** – The document is measured with a placeholder TOC. Every `HeadingElement` encountered reports its level, title, and page position to the TOC builder. 2. **Second pass** – The final TOC is constructed with real page numbers and internal hyperlinks, inserted into the TOC page slot. The document is rendered again to produce the final PDF. This approach guarantees accurate page numbers even when headings cause page breaks or overflow. --- ## Internal links The TOC entries themselves are clickable. Under the hood TerraFluent.Pdf.Reporting creates `/GoTo` link annotations that jump directly to the heading's page and vertical position. You can also add your own internal links: ```csharp container.InternalLink(targetPageNumber, topY).Text("Jump to section"); ``` Use `InternalLink` to build cross-references, a custom index, or a "return to top" link at the end of a chapter. --- ## Page numbering TOC entries display **logical page numbers** — the TOC page itself is treated as page 0 and is not counted. Therefore, the first content page after the TOC is shown as page 1 in the TOC, regardless of how many pages the TOC occupies. Internal links use the **physical page numbers** (including the TOC pages), so clicking a TOC entry correctly jumps to the heading's location. TerraFluent.Pdf.Reporting computes the offset automatically from the actual height of the generated TOC. Example: If the TOC occupies 2 pages, the first chapter heading appears on physical page 3. The TOC will display "1" for that chapter, but the link will go to page 3. --- ## Limitations & notes - Only one `TableOfContents()` call is allowed per document. Attempting to add multiple TOC pages throws `InvalidOperationException`. - The TOC page(s) themselves are **excluded** from the heading list. Headings placed inside the TOC container are ignored (they would appear as self-references). - Heading levels can be mixed arbitrarily; the generated TOC defaults to 20 pt indentation per level. - Headings inside tables or rows are collected just like block-level headings. ==================================================================== TerraFluent.Pdf.Reporting — Bookmarks (PDF Outlines) URL: https://terrafluent.dev/docs/pdf/bookmarks/ ==================================================================== # Bookmarks (PDF Outlines) TerraFluent.Pdf.Reporting supports PDF bookmarks (also called outlines) — the hierarchical tree structure displayed in PDF viewers' sidebar that allows readers to jump directly to sections of a document. --- ## Overview Bookmarks are defined at the document level via `IDocumentContainer.Bookmark()` methods. Each bookmark entry consists of: - A **title** displayed in the viewer's outline pane - A **destination** — the page number (and optional position) the bookmark links to - Optional **hierarchy** — child bookmarks nested under a parent When the PDF is saved, TerraFluent.Pdf.Reporting generates a complete `/Outlines` dictionary tree referenced from the document catalog. --- ## Anchor-Based Bookmarks (recommended) Instead of supplying a page number, anchor a bookmark directly to content with the `IContainer.Bookmark()` extension — the target page and vertical position are resolved automatically when the document is rendered, so bookmarks stay correct as content grows or reflows: ```csharp PdfDocument.Create(c => { c.Page(p => { p.Size(PageSize.A4); p.Content().Column(col => { col.Item().Bookmark("Chapter 1").H1("Chapter 1"); col.Item().Text("..."); col.PageBreak(); // Nested under Chapter 1, wherever it lands col.Item().Bookmark("Section 1.1", parentTitle: "Chapter 1").H2("Section 1.1"); }); }); }) .PublishPdf("book.pdf"); ``` Anchored bookmarks generate zoom-retaining `/XYZ` destinations at the anchored element's position — clicking one scrolls to the element without changing the reader's zoom level — and can be nested under other anchors or manual bookmarks via `parentTitle`. Anchors wrap individual items — wrapping a whole multi-page column records only the column's starting position. These destinations also keep the reader's zoom level and land at the correct Y position after layout. The page-number-based API below remains available for cases where you want an outline entry that does not correspond to a specific element. --- ## Basic Usage (manual page numbers) ### Simple bookmark ```csharp PdfDocument.Create(c => { c.Page(p => { p.Size(PageSize.A4); p.Content().Text("Chapter 1 content..."); }); // Bookmark pointing to page 1 c.Bookmark("Chapter 1", 1); }) .PublishPdf("book.pdf"); ``` The bookmark above appears in the PDF viewer's outline pane as **Chapter 1**. Clicking it jumps to page 1. ### Bookmark with view position Supply a Y-coordinate (in points from the top of the page) to control where the view is positioned when the bookmark is activated: ```csharp c.Bookmark("Introduction", 1, 72.0); // starts 1 inch from page top ``` This generates an `/XYZ` destination with a null zoom, so the view scrolls to the given position while keeping the reader's current zoom level. If the `top` parameter is omitted, the view scrolls to the top of the page (still zoom-retaining). --- ## Hierarchical Bookmarks Create nested bookmark structures by specifying a parent title: ```csharp PdfDocument.Create(c => { c.Page(p => { p.Size(PageSize.A4); p.Content().Column(col => { col.Item().Text("Chapter 1 content..."); col.Item().Text("Section 1.1 content..."); col.Item().Text("Section 1.2 content..."); }); }); // Parent bookmark — must be added first c.Bookmark("Chapter 1", 1); // Children c.Bookmark("1.1 Introduction", 1, "Chapter 1"); c.Bookmark("1.2 Overview", 1, "Chapter 1"); // Grandchild (three levels deep) c.Bookmark("1.2.1 Background", 1, "1.2 Overview"); }) .PublishPdf("book.pdf"); ``` **Rules:** - Parent bookmarks must exist before creating children. Call `c.Bookmark()` for the parent before any child calls that reference it. - Parent lookup is by **exact title match**. The title string is case-sensitive. - Children can reference any previously defined bookmark as their parent, not only top-level entries. - All bookmarks (parents and children) are linked into a single outline tree with proper `/First`, `/Last`, `/Count`, `/Parent`, `/Prev`, and `/Next` references. --- ## Multi-Page Documents Bookmarks work seamlessly with multi-page documents. The `pageNumber` argument is the 1-based logical page number of the target page. ```csharp PdfDocument.Create(c => { // Page 1 c.Page(p => { p.Size(PageSize.A4); p.Content().Text("Cover page content"); }); // Page 2 c.Page(p => { p.Size(PageSize.A4); p.Content().Text("Table of Contents"); }); // Page 3+ c.Page(p => { p.Size(PageSize.A4); p.Content().Column(col => { col.Item().Text("Chapter 1 — Basics"); col.Item().Text("Chapter 2 — Advanced"); }); }); // Bookmarks c.Bookmark("Cover", 1); c.Bookmark("Contents", 2); c.Bookmark("Chapter 1", 3); c.Bookmark("Chapter 2", 3); }) .PublishPdf("book.pdf"); ``` > **Note:** If a bookmark targets a page number greater than the total pages in the final document, an `InvalidOperationException` is thrown at save time. --- ## Complete API Reference All bookmark methods are defined on `IDocumentContainer` (the parameter passed to `PdfDocument.Create`). ### Method Signatures | Method | Parameters | Description | |--------|------------|-------------| | `Bookmark(string title, int pageNumber)` | `title`: display text<br>`pageNumber`: 1-based page | Top-level bookmark with `/Fit` destination | | `Bookmark(string title, int pageNumber, double top)` | `title`, `pageNumber`, `top`: Y position in points | Top-level bookmark with `/FitH` destination | | `Bookmark(string title, int pageNumber, string parentTitle)` | `title`, `pageNumber`, `parentTitle`: existing bookmark title | Child bookmark under `parentTitle` with `/Fit` | | `Bookmark(string title, int pageNumber, string parentTitle, double top)` | `title`, `pageNumber`, `parentTitle`, `top` | Child bookmark with `/FitH` destination | ### Exceptions - `ArgumentNullException` / `ArgumentException` — `title` or `parentTitle` is null/empty/whitespace - `ArgumentOutOfRangeException` — `pageNumber ≤ 0` or `top < 0` - `InvalidOperationException` — `parentTitle` does not match any previously defined bookmark --- ## PDF Structure Details TerraFluent.Pdf.Reporting emits standard PDF 1.7 outline objects: - **Outlines dictionary** (`/Type /Outlines`) — the root node referenced from `/Catalog` - `/First` → first top-level bookmark object - `/Last` → last top-level bookmark object - `/Count` → total number of outline entries (positive integer; negative when collapsed, but TerraFluent.Pdf.Reporting emits positive) Each **bookmark item** dictionary contains: - `/Type /Outlines` - `/Title (string)` — the display title (PDF string literal; special chars escaped) - `/Parent N 0 R` — reference to parent, or to the Outlines root for top-level - `/Prev N 0 R` — previous sibling (omitted for first sibling) - `/Next N 0 R` — next sibling (omitted for last sibling) - `/First N 0 R` — first child (if any) - `/Last N 0 R` — last child (if any) - `/Count N` — number of children (positive; negative would indicate collapsed state, unused) - `/Dest [ pageObj N 0 R /Fit ]` — fit-whole-page destination, **or** - `/Dest [ pageObj N 0 R /FitH top ]` — fit-width with top edge at `top` coordinate Page object references are resolved from the 1-based `pageNumber` after all pages are created. --- ## Best Practices ### Order of Definition Always define parent bookmarks **before** their children: ```csharp // Good — parent first c.Bookmark("Chapter 1", 1); c.Bookmark("Section 1.1", 1, "Chapter 1"); // Bad — child before parent throws InvalidOperationException c.Bookmark("Section 1.1", 1, "Chapter 1"); // ❌ parent not yet defined c.Bookmark("Chapter 1", 1); ``` ### Unique Titles Bookmark titles must be unique among siblings but can be reused in different branches: ```csharp c.Bookmark("Chapter 1", 1); c.Bookmark("Section A", 1, "Chapter 1"); // OK c.Bookmark("Chapter 2", 1); c.Bookmark("Section A", 1, "Chapter 2"); // OK — different parent ``` ### Page Number Validation Page numbers are validated at `PublishPdf()` time after the full document is composed. This means bookmarks can reference any page regardless of the order in which `Page()` calls appear: ```csharp c.Bookmark("Appendix", 5); // OK even if Page 5 is defined later c.Page(p => { /* page 5 definition */ }); ``` ### Use Positioning for Precision For long documents, consider setting `top` on section-opening bookmarks so the view lands at the section heading rather than the page top: ```csharp c.Bookmark("Chapter 1", 3, 72.0); // starts 1 inch down, heading area c.Bookmark("Chapter 2", 8, 72.0); ``` --- ## Limitations - PDF outlines do not support styling (font, colour, icons). Appearance is controlled by the PDF viewer. - No support for **named destinations** with zoom levels beyond `/Fit` and `/FitH`. Future versions may add `/XYZ` for custom zoom. - No direct API for collapsible initial state — all outline trees open by default in viewers. - Bookmarks are document-global; they cannot be scoped to a single `PageDescriptor`. --- ## Sample Code A full working example is available in the TerraFluent.Pdf.Reporting sample application: ``` samples/TerraFluent.Pdf.Reporting.Sample/Program.cs → GenerateReportWithBookmarks() ``` This generates `08_report_with_bookmarks.pdf` with 5 top-level bookmarks, nested children, and a mix of `/Fit` and `/FitH` destinations across 6 pages. --- ## Related - [Getting Started](/docs/pdf/getting-started/) — basic document structure - [Page Sizes & Units](/docs/pdf/page-sizes-and-units/) — setting page dimensions - [Layout](/docs/pdf/layout/) — Column, Row, Table for structuring content ==================================================================== TerraFluent.Pdf.Reporting — Components & Templates URL: https://terrafluent.dev/docs/pdf/components-and-templates/ ==================================================================== # Components & Templates TerraFluent.Pdf.Reporting provides two interfaces for structuring and reusing document content: | Interface | Scope | Purpose | |-----------|-------|---------| | `IComponent` | Container slot | Reusable content block injected anywhere in a layout | | `IDocument` | Whole document | Reusable, self-contained document template | Both live in the `TerraFluent.Pdf.Reporting.Infra` namespace. --- ## IComponent — Reusable Content Blocks `IComponent` encapsulates a piece of content that can be composed into any `IContainer` slot. Ideal for repeated UI elements like header cards, badges, callout boxes, or address blocks. ### Interface ```csharp namespace TerraFluent.Pdf.Reporting.Infra; public interface IComponent { void Compose(IContainer container); } ``` ### Example — Callout box ```csharp using TerraFluent.Pdf.Reporting.Core; using TerraFluent.Pdf.Reporting.Helpers; using TerraFluent.Pdf.Reporting.Infra; public class CalloutBox : IComponent { private readonly string _text; private readonly string _color; public CalloutBox(string text, string color = "#E3F2FD") { _text = text; _color = color; } public void Compose(IContainer container) => container .Margin(6) .Background(_color) .Border(1, Color.Blue.Lighten2) .Padding(10) .Text(_text).Italic().FontColor(Color.Blue.Darken2); } ``` ### Usage ```csharp col.Item().Component(new CalloutBox("Note: prices exclude VAT.")); col.Item().Component(new CalloutBox("Warning: read before proceeding.", Color.Orange.Medium)); ``` ### Example — Address block ```csharp public class AddressBlock : IComponent { private readonly string _name; private readonly string[] _lines; public AddressBlock(string name, params string[] lines) { _name = name; _lines = lines; } public void Compose(IContainer container) { container.Column(col => { col.Spacing(2); col.Item().Text(_name).Bold(); foreach (var line in _lines) col.Item().Text(line).FontColor(Color.Grey.Darken1); }); } } // Usage row.RelativeItem().Component(new AddressBlock( "Acme Corp.", "88 Commerce Blvd, Floor 12", "New York, NY 10001", "billing@acme.example" )); ``` --- ## IDocument — Reusable Document Templates `IDocument` encapsulates an entire multi-page document. Use it to separate document structure from data and to enable unit testing. ### Interface ```csharp namespace TerraFluent.Pdf.Reporting.Infra; public interface IDocument { void Compose(IDocumentContainer container); } ``` ### Example — Invoice template ```csharp using TerraFluent.Pdf.Reporting.Core; using TerraFluent.Pdf.Reporting.Helpers; using TerraFluent.Pdf.Reporting.Infra; public record InvoiceData(string Number, string ClientName, decimal Total); public class InvoiceDocument : IDocument { private readonly InvoiceData _data; public InvoiceDocument(InvoiceData data) => _data = data; public void Compose(IDocumentContainer container) { container.Page(page => { page.Size(PageSize.A4); page.Margin(2, Unit.Centimetre); page.DefaultTextStyle(s => s.FontSize(11)); page.Header().Column(col => { col.Item() .Background(Color.Blue.Darken2) .Padding(12) .Text($"INVOICE #{_data.Number}") .Bold().FontSize(18).FontColor(Color.White); }); page.Content().Column(col => { col.Spacing(10); col.Item().Text($"Bill To: {_data.ClientName}").Bold(); col.Item().Text($"Total Due: ${_data.Total:N2}").FontSize(14); }); page.Footer().AlignCenter().Text(t => { t.Span("Page ").FontSize(9).FontColor(Color.Grey.Medium); t.CurrentPageNumber().FontSize(9).FontColor(Color.Grey.Medium); }); }); } } ``` ### Generating the document ```csharp var data = new InvoiceData("2025-042", "Acme Corp.", 14_250.00m); // To file PdfDocument.Create(new InvoiceDocument(data)).PublishPdf("invoice.pdf"); // To byte array byte[] pdf = PdfDocument.Create(new InvoiceDocument(data)).PublishPdf(); // To stream PdfDocument.Create(new InvoiceDocument(data)).PublishPdf(responseStream); ``` --- ## Combining IDocument with IComponent Components can be used freely inside `IDocument.Compose`: ```csharp public class ReportDocument : IDocument { public void Compose(IDocumentContainer container) { container.Page(page => { page.Size(PageSize.A4); page.Margin(2, Unit.Centimetre); page.Content().Column(col => { col.Spacing(12); col.Item().Component(new CalloutBox("This report is confidential.")); col.Item().Text("Report body...").Justify(); }); }); } } ``` ==================================================================== TerraFluent.Pdf.Reporting — Document Metadata URL: https://terrafluent.dev/docs/pdf/metadata/ ==================================================================== # Document Metadata TerraFluent.Pdf.Reporting allows you to set standard PDF document metadata properties that appear in the document properties dialog of PDF viewers (File → Properties in Adobe Reader, Preview, etc.). These include Title, Author, Subject, Keywords, and Creator. Metadata is set via methods on `IDocumentContainer` (the object passed to `PdfDocument.Create`). --- ## Supported Fields | Method | PDF Key | Description | |--------|---------|-------------| | `MetadataTitle(string?)` | `/Title` | Document title | | `MetadataAuthor(string?)` | `/Author` | Author name(s) | | `MetadataSubject(string?)` | `/Subject` | Subject line / topic | | `MetadataKeywords(string?)` | `/Keywords` | Comma- or semicolon-separated keywords | | `MetadataCreator(string?)` | `/Creator` | Software that generated the PDF | All methods accept `null` or whitespace strings to clear/omit that field. --- ## Basic Usage Set metadata before adding pages (or after — order doesn't matter): ```csharp using TerraFluent.Pdf.Reporting.Core; using TerraFluent.Pdf.Reporting.Helpers; PdfDocument.Create(c => { // Set metadata first (optional order) c.MetadataTitle("Quarterly Report Q1 2025"); c.MetadataAuthor("Acme Finance Division"); c.MetadataSubject("Financial Performance Review"); c.MetadataKeywords("finance, quarterly, report, 2025"); c.MetadataCreator("Acme Reporting Engine v3.2"); // Add at least one page c.Page(p => { p.Size(PageSize.A4); p.Margin(2, Unit.Centimetre); p.Content().Text("Report content goes here..."); }); }) .PublishPdf("report.pdf"); ``` The resulting PDF will contain an **Info dictionary**: ```pdf << /Title (Quarterly Report Q1 2025) /Author (Acme Finance Division) /Subject (Financial Performance Review) /Keywords (finance, quarterly, report, 2025) /Creator (Acme Reporting Engine v3.2) /Producer (TerraFluent.Pdf.Reporting) % added automatically by TerraFluent.Pdf.Reporting? No /CreationDate (D:202505031...) % PDF viewers may add >> ``` > **Note:** TerraFluent.Pdf.Reporting does not currently set `/Producer` or `/CreationDate` automatically. Only the fields you explicitly set appear in the Info dictionary. --- ## Null / Whitespace Handling Passing `null`, empty, or whitespace-only strings omits that field from the Info dictionary: ```csharp c.MetadataAuthor(""); // Author key not included c.MetadataKeywords(null); // Keywords key not included c.MetadataTitle(" "); // Title key not included ``` If **no metadata fields are set**, the Info dictionary is omitted entirely and the Catalog contains no `/Info` entry. --- ## Special Character Escaping PDF string literals require escaping for: - Backslash `\` → `\\` - Opening parenthesis `(` → `\(` - Closing parenthesis `)` → `\)` TerraFluent.Pdf.Reporting handles this escaping automatically for all metadata values: ```csharp c.MetadataTitle("Report (Final) \\ 2025"); // Escapes to: /Title (Report \(Final\) \\ 2025) ``` --- ## API Reference All metadata methods have the signature: ```csharp void MetadataXxx(string? value); ``` They are chainable (though they return `void` — they are called for side effects on the composer, not for fluent chaining on the `IDocumentContainer`): ```csharp c.MetadataTitle("My Doc"); c.MetadataAuthor("Jane Smith"); // Not chainable: c.MetadataTitle("...").MetadataAuthor("...") won't compile ``` If you need conditional setting: ```csharp if (includeAuthor) c.MetadataAuthor(userName); ``` --- ## PDF Structure Details ### Info Dictionary Object When metadata is present, TerraFluent.Pdf.Reporting allocates a PDF object like: ```pdf 5 0 obj << /Title (My Document) /Author (John Smith) /CreationDate (D:20250503153000-05'00') >> endobj ``` ### Catalog Reference The Catalog object gains an `/Info` entry pointing to this dictionary: ```pdf 7 0 obj << /Type /Catalog /Pages 6 0 R /Info 5 0 R >> endobj ``` If no metadata is set, the `/Info` entry is omitted from the Catalog. --- ## Best Practices ### Set metadata early Call metadata methods at the start of your `PdfDocument.Create` block to keep configuration together: ```csharp PdfDocument.Create(c => { // Metadata first c.MetadataTitle("Invoice #12345"); c.MetadataAuthor("Acme Billing"); c.MetadataKeywords("invoice; billing; 2025"); // Then pages c.Page(...); }) ``` ### Use consistent keywords PDF viewers use the Keywords field for search and filtering. Separate with commas or semicolons: ``` keywords: "quarterly, finance, board, q1" // ← OK keywords: "annual report; 2025; audit" // ← OK ``` Avoid extremely long keyword strings. ### Include author when distributing externally For documents that leave your organization, setting the `/Author` field aids traceability and professionalism. ### Combine with `Bookmark` for complete navigation Metadata provides document identity; bookmarks provide in-document navigation: ```csharp PdfDocument.Create(c => { c.MetadataTitle("User Guide"); c.MetadataAuthor("Tech Writers Ltd"); c.Bookmark("Introduction", 1); c.Bookmark("Installation", 2); c.Bookmark("Configuration", 3); // ... }); ``` --- ## Limitations - No support for custom metadata keys beyond the five standard fields. For advanced use cases (XMP metadata, custom XMP properties), you'd need to extend `PdfWriter` directly. - No automatic population of `/CreationDate` or `/ModDate`. You can suggest a `Creator` string (e.g., app name + version), but PDF viewers show their own creation timestamp. - All values are **text strings** — no date type parsing or numeric types. If you need a date field, format it yourself and put it into `Subject` or `Keywords`. - Unicode beyond Latin-1 may not display correctly in older PDF viewers; TerraFluent.Pdf.Reporting uses ISO-8859-1 encoding for Info strings (same as content streams). --- ## Sample A full working example is in the TerraFluent.Pdf.Reporting sample application: ``` samples/TerraFluent.Pdf.Reporting.Sample/Program.cs ``` Look for `Metadata` in the code or add the calls to any sample to produce a PDF with document properties filled in. --- ## Related - [Getting Started](/docs/pdf/getting-started/) — basic document structure - [Bookmarks](/docs/pdf/bookmarks/) — hierarchical outline entries for navigation - [Page Sizes & Units](/docs/pdf/page-sizes-and-units/) — page configuration ==================================================================== TerraFluent.Pdf.Reporting — Unicode & Character Encoding URL: https://terrafluent.dev/docs/pdf/unicode-and-encoding/ ==================================================================== # Unicode & Character Encoding TerraFluent.Pdf.Reporting uses **WinAnsiEncoding** for all built-in Type 1 fonts (Helvetica, Times, Courier and their Bold/Italic variants). This page explains what that means in practice — which characters render correctly, how they are encoded in the PDF content stream, and how to avoid the common pitfall of characters appearing as `?` in the output. --- ## What is WinAnsiEncoding? WinAnsiEncoding is the character encoding vector declared in the PDF font dictionary for every built-in Type 1 font. It maps byte values in the range 0x20–0xFF to Unicode code points (and therefore to printable glyphs) using the **Windows-1252** code page. The mapping covers three distinct regions: | Byte range | Region | Characters | |------------|--------|-----------| | 0x20–0x7E | Printable ASCII | Space through tilde — all 95 printable ASCII characters | | 0x80–0x9F | Windows-1252 specials | 27 typographic characters (see table below) | | 0xA0–0xFF | Latin-1 Supplement | All 96 Latin-1 characters (U+00A0–U+00FF) | Byte values 0x7F, 0x81, 0x8D, 0x8F, 0x90, and 0x9D are **undefined** in WinAnsiEncoding and have no glyph. Any Unicode code point outside the ranges listed above — such as characters in the Latin Extended-A/B blocks (U+0100+) — likewise has no mapping and **will render as `?`** in the PDF viewer. --- ## Safe character ranges To guarantee correct rendering with WinAnsiEncoding, limit your text to: - All standard ASCII printable characters (U+0020–U+007E). - The 27 Windows-1252 typographic specials listed below. - The full Latin-1 Supplement block (U+00A0–U+00FF), which includes all common Western-European accented letters (À–ÿ), currency symbols (¢ £ ¥), mathematical symbols (± × ÷ °), and more. Characters in **U+0100 and above** (e.g. Polish Ł/ł, Czech Č/č/Ř/ř, Turkish Ş/ş/Ğ/ğ/ı, Romanian Ș/ț, Hungarian Ő/ő/Ű/ű) are **not** covered by WinAnsiEncoding and will not render correctly with the built-in Type 1 fonts. --- ## Windows-1252 Typographic Specials (0x80–0x9F) These 27 characters occupy the byte range that the C1 control codes occupy in pure ISO-8859-1. Windows-1252 repurposes them for useful typographic glyphs, and TerraFluent.Pdf.Reporting's AFM width tables cover all 27: | Unicode | WinAnsi byte | Character | Name | |---------|-------------|-----------|------| | U+2018 | 0x91 | ' | Left single quotation mark | | U+2019 | 0x92 | ' | Right single quotation mark | | U+201C | 0x93 | " | Left double quotation mark | | U+201D | 0x94 | " | Right double quotation mark | | U+2013 | 0x96 | – | En dash | | U+2014 | 0x97 | — | Em dash | | U+2026 | 0x85 | … | Horizontal ellipsis | | U+2022 | 0x95 | • | Bullet | | U+20AC | 0x80 | € | Euro sign | | U+2122 | 0x99 | ™ | Trade mark sign | | U+0152 | 0x8C | Œ | OE ligature (capital) | | U+0153 | 0x9C | œ | OE ligature (small) | | U+2020 | 0x86 | † | Dagger | | U+2021 | 0x87 | ‡ | Double dagger | | U+2030 | 0x89 | ‰ | Per mille sign | | U+0160 | 0x8A | Š | S with caron (capital) | | U+0161 | 0x9A | š | S with caron (small) | | U+2018 | 0x91 | ' | Left single quotation mark | | U+201A | 0x82 | ‚ | Single low-9 quotation mark | | U+0192 | 0x83 | ƒ | Latin small letter f with hook | | U+201E | 0x84 | „ | Double low-9 quotation mark | | U+2020 | 0x86 | † | Dagger | | U+0152 | 0x8C | Œ | OE ligature (capital) | | U+017D | 0x8E | Ž | Z with caron (capital) | | U+017E | 0x9E | ž | Z with caron (small) | | U+0178 | 0x9F | Ÿ | Y with diaeresis (capital) | | U+2039 | 0x8B | ‹ | Single left-pointing angle quotation mark | | U+203A | 0x9B | › | Single right-pointing angle quotation mark | Use these characters directly in your C# strings with their Unicode escape sequences or by typing them as literal characters: ```csharp // Typographic quotes, dashes, and ellipsis container.Text("\u201CHello,\u201D she said\u2026"); container.Text("Pages 42\u201347"); // en dash container.Text("Time\u2014and tide."); // em dash container.Text("Price: \u20AC 1,299.00"); // Euro sign container.Text("TerraFluent.Pdf.Reporting\u2122"); // trade mark ``` --- ## Latin-1 Supplement (U+00A0–U+00FF) All 96 characters in the Latin-1 Supplement block are covered by WinAnsiEncoding. This includes: - **Accented capitals**: À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö Ø Ù Ú Û Ü Ý Þ - **Accented lowercase**: à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ø ù ú û ü ý þ ÿ ß - **Punctuation & spacing**: non-breaking space, «, », ¿, ¡, ·, ¶, § - **Mathematical & technical**: ± × ÷ ° µ ² ³ ¼ ½ ¾ - **Currency & commerce**: ¢ £ ¤ ¥ ¦ © ® ¯ These render perfectly in all built-in TerraFluent.Pdf.Reporting fonts: ```csharp // All of these are safe — they are in Latin-1 Supplement container.Text("Ágnes sétált a városban."); // á, é — U+00E1, U+00E9 ✓ container.Text("Français, señor, São Paulo."); // ç, ñ, ã — all Latin-1 ✓ container.Text("Björk åkte till Göteborg."); // ö, å — U+00F6, U+00E5 ✓ container.Text("Mjölk kostar 3 kr/l ± 5 öre."); // ö, ± — all Latin-1 ✓ ``` --- ## Content-stream encoding Internally, TerraFluent.Pdf.Reporting converts every non-ASCII character to its WinAnsi byte value and writes it as an **octal escape** (`\nnn`) in the PDF string literal. This keeps the content stream pure 7-bit ASCII while the PDF reader resolves each byte value through the font's `/WinAnsiEncoding` vector. For example, the character `é` (U+00E9) maps to WinAnsi byte 0xE9 (233 decimal) and is written as `\351` in the content stream. The PDF reader sees byte 0xE9, looks it up in the `/WinAnsiEncoding` array, finds `eacute`, and renders the correct glyph. Metadata fields (document title, author, subject, keywords) and bookmark titles are encoded as **UTF-16BE hex strings** (`<FEFF…>`) so that viewer title bars and outline panels display the correct text regardless of encoding limitations. --- ## AFM glyph-width tables TerraFluent.Pdf.Reporting ships with extended AFM (Adobe Font Metrics) advance-width tables covering the **full WinAnsi byte range** 0x20–0xFF — not just printable ASCII. This ensures that word-wrapping, justification, and column-width calculations are pixel-accurate for every accented character and typographic special. The width tables are sourced from the Adobe Core-14 AFM files and are compiled directly into the library (no external files required at runtime). --- ## Avoiding `?` characters A character renders as `?` when TerraFluent.Pdf.Reporting cannot map it to a WinAnsi byte value. The most common cause is using characters from the Latin Extended-A/B blocks (U+0100+) that Windows-1252 does not cover. **Checklist:** 1. Check the Unicode code point of the problem character (e.g. `Ł` is U+0141). 2. If it is above U+00FF and not one of the 27 Windows-1252 specials, it will not render. 3. Replace it with a visually similar character that is within the safe range, or rewrite the text to avoid it. **Common substitutions:** | Problematic character | Unicode | Substitute | Notes | |-----------------------|---------|-----------|-------| | Ł / ł (Polish L) | U+0141 / U+0142 | L / l | Drop the stroke | | Ż / ż (Polish Z dot) | U+017B / U+017C | Z / z | Drop the dot | | Č / č (Czech C caron) | U+010C / U+010D | C / c | Drop the caron | | Ř / ř (Czech R caron) | U+0158 / U+0159 | R / r | Drop the caron | | Ş / ş (Turkish S cedilla) | U+015E / U+015F | Ş → use ş from Latin-1? No — use S/s | Not in WinAnsi | | Ğ / ğ (Turkish G breve) | U+011E / U+011F | G / g | Drop the breve | | İ / ı (Turkish dotted/dotless I) | U+0130 / U+0131 | I / i | Use plain I | | Ő / ő (Hungarian O double acute) | U+0150 / U+0151 | Ö / ö (U+00D6 / U+00F6) | Close visual match | | Ű / ű (Hungarian U double acute) | U+0170 / U+0171 | Ü / ü (U+00DC / U+00FC) | Close visual match | --- ## Sample: language showcase The `11_UnicodeShowcase.cs` sample in `samples/TerraFluent.Pdf.Reporting.Sample/Samples/` generates a 5-page PDF that demonstrates all aspects of WinAnsiEncoding support: ``` Page 1 Introduction and 18-language sample table Page 2 Windows-1252 specials table + Latin-1 supplement groups Page 3 Complete WinAnsiEncoding reference grid (0x20–0xFF) Page 4 Multi-font comparison (Helvetica / Times / Courier) Page 5 Font-metrics deep-dive: advance-width heat-map + justified paragraph ``` Run it with: ```sh cd samples/TerraFluent.Pdf.Reporting.Sample dotnet run # Output: 11_unicode_showcase.pdf ``` ==================================================================== TerraFluent.Chart.Reporting — Getting Started URL: https://terrafluent.dev/docs/chart/getting-started/ ==================================================================== # Getting Started ## 1. Reference the Library **Project reference (before NuGet publishing):** Add to your `.csproj`: ```xml <ItemGroup> <ProjectReference Include="..\src\TerraFluent.Chart.Reporting\TerraFluent.Chart.Reporting.csproj" /> </ItemGroup> ``` **After NuGet publishing:** ```bash dotnet add package TerraFluent.Chart.Reporting ``` --- ## 2. Your First Chart All charts start with `ChartBuilder.Create()`. Every method returns `this`, so you chain them freely: ```csharp using TerraFluent.Chart.Reporting.Builder; string svg = ChartBuilder.Create() .Title("Monthly Website Visitors") .Subtitle("Jan – Jun 2025") .Size(700, 400) .XAxis("Month", "Jan", "Feb", "Mar", "Apr", "May", "Jun") .YAxis("Visitors", min: 0) .AsAnimated() .Series(s => s .AddLine("Visitors", new double[] { 12400, 14800, 16200, 18500, 21000, 19800 })) .RenderToSvg(); ``` **Output** — self-contained SVG (excerpt): ```xml <svg xmlns="http://www.w3.org/2000/svg" width="700" height="400" viewBox="0 0 700 400"> <rect width="700" height="400" fill="#ffffff"/> <text x="350" y="28" text-anchor="middle" font-size="16" font-weight="bold" fill="#333333"> Monthly Website Visitors </text> <text x="350" y="46" text-anchor="middle" font-size="11" fill="#666666">Jan – Jun 2025</text> <!-- grid lines, axis labels, SMIL-animated line path … --> <path d="M … L … L …" fill="none" stroke="#7CB5EC" stroke-width="2"> <animate attributeName="stroke-dashoffset" from="…" to="0" dur="0.8s" fill="freeze"/> </path> </svg> ``` --- ## 3. Save to a File ```csharp // Save as SVG ChartBuilder.Create() .Title("Sales Report") .Size(800, 500) .Series(s => s.AddColumn("Q1", new double[] { 320, 410, 390, 480 })) .RenderToFile("output/sales.svg"); // Save as HTML fragment (SVG wrapped in <figure>) ChartBuilder.Create() .Title("Sales Report") .Size(800, 500) .Series(s => s.AddColumn("Q1", new double[] { 320, 410, 390, 480 })) .RenderToHtmlFile("output/sales.html", caption: "FY 2025 Sales"); ``` --- ## 4. ASP.NET Core — Return as HTTP Response ```csharp // Minimal API app.MapGet("/charts/revenue", () => { string svg = ChartBuilder.Create() .Title("Revenue") .Size(700, 400) .XAxis("Q", "Q1", "Q2", "Q3", "Q4") .YAxis("$k", min: 0) .AsStatic() // no JS — safe for any client .Series(s => s.AddColumn("Revenue", new double[] { 310, 390, 420, 510 })) .RenderToSvg(); return Results.Content(svg, "image/svg+xml"); }); // Controller [HttpGet("chart")] public IActionResult GetChart() { string svg = ChartBuilder.Create() .Title("Revenue") .Size(700, 400) .AsStatic() .Series(s => s.AddColumn("Revenue", new double[] { 310, 390, 420, 510 })) .RenderToSvg(); return Content(svg, "image/svg+xml"); } ``` --- ## 5. Blazor — Render Inline SVG ```razor @using TerraFluent.Chart.Reporting.Builder <div class="chart-container"> @((MarkupString)ChartSvg) </div> @code { private string ChartSvg = string.Empty; protected override void OnInitialized() { ChartSvg = ChartBuilder.Create() .Title("Active Users") .Size(700, 380) .XAxis("Day", "Mon", "Tue", "Wed", "Thu", "Fri") .YAxis("Users", min: 0) .AsAnimated() // SMIL animations work in Blazor WASM .Series(s => s .AddArea("DAU", new double[] { 4200, 5100, 4800, 6300, 7200 })) .RenderToSvg(); } } ``` > **Render Mode Tip:** Use `.AsAnimated()` in Blazor. Use `.AsStatic()` for PDF / email. Use `.AsInteractive()` only for plain HTML browser pages. --- ## 6. Dependency Injection Register via the `IChartBuilder` interface for testable, decoupled code: ```csharp // Program.cs — register builder.Services.AddScoped<IChartBuilder>(_ => ChartBuilder.Create()); // Service — inject and use public class ReportService { private readonly IChartBuilder _charts; public ReportService(IChartBuilder charts) => _charts = charts; public string BuildRevenueSvg(double[] data) { return _charts .Title("Revenue") .Size(700, 400) .Series(s => s.AddColumn("Revenue", data)) .RenderToSvg(); } } ``` --- ## 7. Core Pattern — How the Builder Works ``` ChartBuilder.Create() ← creates a fresh builder .Title(…) ← chart-level settings .Size(…) .Theme(…) .XAxis(…) / .YAxis(…) .AsAnimated() ← render mode .Series(s => s ← open the series scope .AddLine(…) ← add first series .AddColumn(…)) ← add second series (chain inside lambda) .RenderToSvg() ← terminate and get the SVG string ``` All methods are null-safe and throw `ArgumentException` / `ArgumentNullException` on invalid input. Every `double?[]` parameter supports `null` gaps (the line or column is skipped at that index). --- ## 8. Choosing a Render Mode | Mode | Method | CSS hover? | JS? | SMIL? | Use for | |---|---|---|---|---|---| | **Static** | `.AsStatic()` | No | No | No | PDF, email, server-side image | | **Animated** | `.AsAnimated()` | Yes | No | Yes | Blazor, browser embedding | | **Interactive** | `.AsInteractive()` | Yes | Yes | Yes | Browser-only dashboards | See [Advanced Features](/docs/chart/advanced/#render-modes) for full details. ==================================================================== TerraFluent.Chart.Reporting — Chart Showcase URL: https://terrafluent.dev/docs/chart/showcase/ ==================================================================== # Chart Showcase A live visual catalogue of every chart type and feature in TerraFluent.Chart.Reporting — each entry is a self-contained SVG rendered entirely server-side, with zero JavaScript dependency. > **[▶ Open the interactive showcase (showcase.html)](/chart/showcase/)** — a single browsable page with all charts rendered inline. Best viewed in a browser. For copy-paste code behind each chart type, see **[Chart Types](/docs/chart/chart-types/)**. _Catalogue of 102 charts · generated 2026-09-06 UTC._ | # | Chart | What it demonstrates | |---|---|---| | 01 | Line Chart | Single-series animated line chart with monthly visitors data. | | 02 | Multi-Series Line | Three lines (Revenue, Cost, Profit). | | 03 | Area Chart | Filled area chart showing daily active users over 10 days. | | 04 | Column Chart | Single-series column chart — product sales by category. | | 05 | Grouped Columns | Two column series side-by-side for Budget vs Actual spend. | | 06 | Pie Chart | Pie chart with legend showing market share by vendor. | | 07 | Spline Chart | Smooth spline curves — temperature trends for three cities. | | 08 | Mixed Line + Area | Area series (Volume) combined with a Line series (Price). | | 09 | Static Mode (PDF / Email) | SvgMode.Static — no CSS hover rules, no embedded JS. | | 10 | Interactive Mode (Browser) | SvgMode.Interactive — CSS hover tooltips + embedded JS. | | 11 | True Spline (Bézier) | Catmull-Rom cubic Bézier curves. | | 12 | Horizontal Bar Chart | ChartType.Bar — bars grow left-to-right. | | 13 | Scatter Chart | ChartType.Scatter — dots only, no connecting line. | | 14 | Animated Column (SMIL) | Bars grow from baseline on load via SMIL. | | 15 | Dark Theme | ChartTheme.Dark — navy background, light text. | | 16 | Pastel Theme | ChartTheme.Pastel — soft off-white background. | | 17 | Data Labels | ShowDataLabels — values rendered on bars and line points. | | 18 | Donut Chart | Pie chart with DonutHolePercent = 0.55 creating a donut hole. | | 19 | Stacked Columns (Normal) | Stacking.Normal — bars are stacked so totals are visible. | | 20 | Stacked Columns (100 %) | Stacking.Percent — each column normalised to 100 %. | | 21 | Stacked Area | Stacking.Normal on Area series. | | 22 | Secondary Y-Axis | WithYAxis2() — columns on left axis, line on right axis. | | 23 | Plot Bands & Reference Lines | YAxis.PlotBands and YAxis.PlotLines. | | 24 | Waterfall Chart | ChartType.Waterfall — incremental running-total chart. | | 25 | Gauge / Radial Chart | ChartType.Gauge — semi-circular dial. | | 26 | X-Axis Label Rotation | Axis.LabelRotation = -45 — diagonal labels. | | 27 | Data Label Showcase | DataLabel: FontSize · Format — three distinct styles on one chart. | | 28 | Pie — Label Placement | DataLabelRadius: inside, at edge, and outside with connector. | | 29 | Legend — Vertical Right | LegendBuilder: Vertical() · AlignRight() · SymbolRadius. | | 30 | Legend — Top Center | LegendBuilder: AtTop() · AlignCenter() · Horizontal() · Padding. | | 31 | Legend — Bottom Left | LegendBuilder: AtBottom() · AlignLeft() · SymbolSize · Offset. | | 32 | Column — Border & Corner Radius | Series.BorderWidth · BorderRadius per column series. | | 33 | Scatter — Marker Borders | Series.BorderWidth on scatter dot markers. | | 34 | Area — Fill Opacity | Series.FillOpacity: 0.15 · 0.40 · 0.70 — three overlapping area series. | | 35 | Tooltip — Custom Style | TooltipBuilder: FontSize · Padding · Format · TransitionDuration. | | 36 | Tooltip — No Arrow | TooltipBuilder: HideArrow() · custom Format template. | | 37 | Donut — Center Label | DonutCenter: auto total · title caption · font size. | | 38 | Monthly Sales — USD Donut | Donut chart with USD data labels and DonutCenter total. | | 39 | Data Ring — KPI | ChartType.DataRing — full 360° progress ring. | | 40 | Data Ring — Dashboard Trio | Three DataRing charts side-by-side as a KPI dashboard. | | 41 | Fluent API Showcase | Size · AsInteractive · Animate · StackNormal · YAxisFormat · ShowDataLabels · SeriesBuilder. | | 42 | Fixed Width Chart | Width(600) — pins the SVG to exactly 600 px wide. | | 43 | Responsive Width Chart | ResponsiveWidth() — SVG fills its container. | | 44 | X-Axis Numeric Tick Interval | XAxisTickInterval — numeric X-axis with explicit tick spacing. | | 45 | Fork — Chart Variants | Fork() — produce a Static and an Animated variant. | | 46 | Bubble Chart | AddBubble() — scatter with a third dimension (Z). | | 47 | Heatmap | AddHeatmap() — colour-coded matrix. | | 48 | Column Range | AddColumnRange() — each category spans from low to high. | | 49 | Area Range | AddAreaRange() — filled band between a lower and upper line. | | 50 | Funnel Chart | AddFunnel() — stacked trapezoid stages. | | 51 | Treemap | AddTreemap() — nested rectangles sized proportionally. | | 52 | Data Point Click — Line Chart | OnPointClick() — click any data point to see its details. | | 53 | Data Point Click — Column Chart | OnPointClick() — two column series; click any bar. | | 54 | Data Point Click — Shared Tooltip | OnPointClick() combined with shared tooltip. | | 55 | Legend Series Toggle | SvgMode.Interactive — click any legend item to hide/show that series. | | 56 | SVG Export Button | ShowExportButton() — download button appears top-right. | | 57 | Multi-Format Export Menu | ShowExportMenu() — SVG, PNG, JPEG, and PDF options. | | 58 | AutoInsight — Line Chart | AnomalyBands · TrendLine · MovingAverage · HighlightPeaks · NarrativeSummary. | | 59 | AutoInsight — Area Chart | AutoInsight on an Area series — pure SVG, no JavaScript. | | 60 | Parliament Chart | AddParliament() — semicircular hemicycle seating diagram. | | 61 | Logarithmic Y Axis | YAxisLogarithmic() — base-10 log scale. | | 62 | Date/Time X Axis | XAxisDateTime() — auto-formatted by span. | | 63 | Radar / Spider Chart | AddRadar() — closed polygon, great for multivariate comparisons. | | 64 | Box-and-Whisker Plot | AddBoxPlot() — five-number summary per category. | | 65 | Column + Error Bars | AddErrorBar() — I-beam uncertainty whiskers. | | 66 | Gradient-Filled Area | LinearGradientFill() — vertical gradient via SVG <linearGradient>. | | 67 | Pattern-Filled Columns | PatternFill() — diagonal lines, dots, grid via SVG <pattern>. | | 68 | Marker Symbol Shapes | MarkerSymbol() — circle, square, diamond, and triangle. | | 69 | Threshold Zones | Zones() — recolour a line by value band. | | 70 | Candlestick | AddCandlestick() — OHLC bodies with wicks. | | 71 | OHLC Bars | AddOhlc() — high-low bar with open/close ticks. | | 72 | Annotations | Annotations() — labels, lines, rectangles and circles. | | 73 | Vivid Theme | ChartTheme.Vivid — full-spectrum palette. | | 74 | Label Layout Builder | LabelLayout() — rotation, wrap, stagger, font scaling, collision detection. | | 75 | High-Contrast Theme | ChartTheme.HighContrast — WCAG AA palette, all colours ≥ 4.5:1 on white. | | 76 | Null-Gap Policy | NullGap() — Break (gap), Connect (bridge), Zero (baseline) for missing values. | | 77 | Per-Series Target Lines | TargetLine() — horizontal reference lines scoped to a single series. | | 78 | RenderToDataUri() | Embeds the chart as a Base64 data: URI inside an HTML img tag. | | 79 | 100% Stacked Columns | StackPercent() — Y-axis automatically labels 0% … 100%. | | 80 | Dual-Axis Combo Chart | Column (primary Y) + Line (secondary Y) on one chart. | | 81 | Mixed-Type Combo Chart | Column + Line + Area series rendered together. | | 82 | Inverted Y-Axis | YAxisInverted() — minimum at the top, ranking style. | | 83 | Linear Regression Overlay | AddLinearRegression() — least-squares trend line over raw data. | | 84 | Moving Average Overlay | AddMovingAverage() — simple 3-period moving average over a line series. | | 85 | Exponential Smoothing Overlay | AddExponentialSmoothing() — EMA overlay (α = 0.4) for noisy data. | | 86 | Data Table Toggle | ShowDataTable() — appends a per-category value grid below the chart. | | 87 | Custom ARIA Labels | AriaLabel() + AriaDescription() — custom accessible title and description. | | 88 | German Culture (de-DE) | Culture("de-DE") — number formatting uses comma decimal separator. | | 89 | Right-to-Left Chart | RightToLeft() — dir="rtl" on SVG root, direction:rtl CSS on all text. | | 90 | Template: Revenue | ChartTemplate.Revenue — column chart with N0-formatted Y-axis. | | 91 | Template: KPI Dashboard | ChartTemplate.KpiDashboard — dark theme, interactive, no grid/legend. | | 92 | Template: Time Series | ChartTemplate.TimeSeries — spline, animated, 1 s entry. | | 93 | Template: Executive Summary | ChartTemplate.ExecutiveSummary — pastel bar chart, static/PDF-safe. | | 94 | JSON Round-Trip | ChartOptions.ToJson() / ChartBuilder.FromJson() — persist and restore a chart. | | 95 | Dumbbell / Dot-Plot | AddDumbbell() — two dots per category connected by a vertical line. | | 96 | Stream Graph (ThemeRiver) | AddStream() — stacked areas with a centered wiggle baseline. | | 97 | Gantt / Timeline | AddGantt() — horizontal task bars on a numeric time axis. | | 98 | Sankey Flow Diagram | AddSankey() — node-link flow diagram with cubic-bezier links. | | 99 | Range Selector (Navigator) | RangeSelector() — interactive brush strip below the chart fires tf:rangechange. | | 100 | Synchronized Tooltips | SyncGroup() — two charts share the same hover group so tooltips mirror each other. | | 101 | Drill-Down Chart | WithDrilldown() — click a column to open a child detail chart; Back returns to overview. | | 102 | Grid Lines Toggle | GridLines(false) / HideGridLines() — show or hide the background plot grid. | ==================================================================== TerraFluent.Chart.Reporting — Chart Types URL: https://terrafluent.dev/docs/chart/chart-types/ ==================================================================== # Chart Types All 26 chart types available in TerraFluent.Chart.Reporting, each with a complete code example and description of the rendered output. --- ## Quick Reference | Chart type | Builder method | Data input | Best for | |---|---|---|---| | [Line](#1-line) | `AddLine` | `double[]` | Trends over time | | [Spline](#2-spline) | `AddSpline` | `double[]` | Smooth trend curves | | [Area](#3-area) | `AddArea` | `double[]` | Volume under a trend | | [Column](#4-column) | `AddColumn` | `double[]` | Category comparison | | [Bar (horizontal)](#5-bar-horizontal) | `AddBar` | `double[]` | Ranked categories | | [Pie / Donut](#6-pie--donut) | `Add` + `.AsPie()` | `double[]` | Part-of-whole | | [Scatter](#7-scatter) | `AddScatter` | `double[]` | Correlation / distribution | | [Waterfall](#8-waterfall) | `AddWaterfall` | `double?[]` + `bool[]` | Cumulative P&L | | [Gauge](#9-gauge) | `AddGauge` | single `double` | Single KPI dial | | [DataRing](#10-dataring) | `AddDataRing` | single `double` | Full-circle KPI ring | | [Bubble](#11-bubble) | `AddBubble` | `BubblePoint[]` | 3D scatter (X, Y, size) | | [Heatmap](#12-heatmap) | `AddHeatmap` | `HeatmapPoint[]` | Matrix / correlation | | [ColumnRange](#13-columnrange) | `AddColumnRange` | `RangePoint[]` | Low–high bars (temperature etc.) | | [AreaRange](#14-arearange) | `AddAreaRange` | `RangePoint[]` | Confidence bands | | [Funnel](#15-funnel) | `AddFunnel` | `double[]` | Pipeline / conversion | | [Treemap](#16-treemap) | `AddTreemap` | `double[]` | Hierarchical proportions | | [Radar](#17-radar) | `AddRadar` | `double[]` | Multivariate comparison | | [BoxPlot](#18-boxplot) | `AddBoxPlot` | `BoxPlotPoint[]` | Statistical distribution | | [ErrorBar](#19-errorbar) | `AddErrorBar` | `RangePoint[]` | Uncertainty / variance | | [Candlestick](#20-candlestick) | `AddCandlestick` | `OhlcPoint[]` | OHLC price candles | | [OHLC](#21-ohlc) | `AddOhlc` | `OhlcPoint[]` | OHLC price bars | | [Dumbbell](#22-dumbbell) | `AddDumbbell` | `RangePoint[]` | Before/after comparison | | [Stream](#23-stream) | `AddStream` | `double[]` | ThemeRiver flow over time | | [Gantt](#24-gantt) | `AddGantt` | `GanttTask[]` | Project timeline | | [Sankey](#25-sankey) | `AddSankey` | `SankeyNode[]` + `SankeyLink[]` | Flow between stages | | [Parliament](#26-parliament) | `AddParliament` | `ParliamentGroup[]` | Seat / composition layout | > **Computed overlays** — you can also derive series from existing data with `AddLinearRegression`, `AddMovingAverage`, and `AddExponentialSmoothing`. See the [API Reference](/docs/chart/api-reference/#computed-overlays). --- ## 1. Line Connects data points with straight segments. Multi-series lines are automatically coloured from the theme palette. ```csharp string svg = ChartBuilder.Create() .Title("Monthly Website Visitors") .Subtitle("Jan – Dec 2025") .Size(700, 420) .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec") .YAxis("Visitors", min: 0) .AsAnimated() .Series(s => s .AddLine("Visitors", new double[] { 12400, 14800, 16200, 18500, 21000, 19800, 22300, 25100, 23700, 20400, 17900, 15600 })) .RenderToSvg(); ``` **Rendered output:** 700 × 420 px SVG. A single blue line is drawn across 12 months, animating in left-to-right on load. Y-axis starts at 0 and auto-scales to ~25 000. Grid lines are drawn from the theme. **Multi-series line:** ```csharp string svg = ChartBuilder.Create() .Title("Revenue vs Cost vs Profit") .Size(700, 420) .XAxis("Quarter", "Q1", "Q2", "Q3", "Q4") .YAxis("USD (thousands)", min: 0) .AsAnimated() .Series(s => s .AddLine("Revenue", new double[] { 320, 410, 390, 480 }) .AddLine("Cost", new double[] { 210, 260, 245, 290 }) .AddLine("Profit", new double[] { 110, 150, 145, 190 })) .RenderToSvg(); ``` **Rendered output:** Three coloured lines (blue/orange/green from palette). The legend shows each series name. All three lines animate sequentially on load. --- ## 2. Spline Same as Line but uses Catmull-Rom cubic Bézier curves for smooth interpolation between points. ```csharp string svg = ChartBuilder.Create() .Title("Temperature Trends — Smooth Spline") .Size(700, 400) .XAxis("Week", "W1","W2","W3","W4","W5","W6","W7","W8") .YAxis("Temperature (°C)") .AsAnimated() .Series(s => s .AddSpline("London", new double[] { 18, 20, 22, 25, 27, 24, 21, 19 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddSpline("Madrid", new double[] { 28, 31, 34, 38, 40, 37, 33, 30 }, cfg => cfg.Color(ChartColor.ChartOrange))) .RenderToSvg(); ``` **Rendered output:** Two smooth curves with no sharp corners at each data point. London's curve (blue) sits below Madrid's (orange). The curves are rendered as cubic Bézier SVG paths. > **Tip:** Use `AddSpline` instead of `AddLine` whenever the data represents a naturally continuous signal (temperature, heart rate, sensor readings). --- ## 3. Area A line chart where the region between the line and the X-axis baseline is filled with a semi-transparent colour. ```csharp string svg = ChartBuilder.Create() .Title("Daily Active Users") .Size(700, 420) .XAxis("Day", "Mon","Tue","Wed","Thu","Fri","Sat","Sun") .YAxis("Users", min: 0) .AsAnimated() .Series(s => s .AddArea("Active Users", new double[] { 4200, 5100, 4800, 6300, 7200, 3800, 2900 }, cfg => cfg.Color(ChartColor.ChartBlue).FillOpacity(0.3))) .RenderToSvg(); ``` **Rendered output:** A blue-filled area sweeps up from the baseline. The fill is 30 % opaque, so grid lines are visible through it. **Stacked Area:** ```csharp string svg = ChartBuilder.Create() .Title("Traffic by Channel") .Size(720, 420) .StackNormal() // stack areas cumulatively .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("Sessions", min: 0) .AsAnimated() .Series(s => s .AddArea("Organic", new double[] { 1200, 1350, 1500, 1700, 1900, 2100 }) .AddArea("Direct", new double[] { 800, 850, 900, 950, 1000, 1100 }) .AddArea("Referral", new double[] { 400, 420, 440, 480, 520, 560 })) .RenderToSvg(); ``` **Rendered output:** Three colour-filled bands stack on top of each other. The top edge of the topmost band shows total sessions across all channels for each month. --- ## 4. Column Vertical bars — the most common chart for category comparisons. Supports grouped (side-by-side) and stacked layouts. ```csharp // Single series string svg = ChartBuilder.Create() .Title("Product Sales by Category") .Size(700, 420) .XAxis("Category", "Electronics","Clothing","Books","Home","Sports","Toys") .YAxis("Units Sold", min: 0) .AsAnimated() .Series(s => s .AddColumn("Units Sold", new double[] { 8400, 5200, 3100, 6700, 4300, 2800 })) .RenderToSvg(); ``` **Rendered output:** Six blue columns of varying heights. Each column grows from the baseline via SMIL animation on page load. ```csharp // Grouped columns (two series side-by-side) string svg = ChartBuilder.Create() .Title("Budget vs Actual Spend") .Size(750, 420) .XAxis("Department", "Engineering","Marketing","Sales","Operations","HR") .YAxis("USD (thousands)", min: 0) .AsAnimated() .Series(s => s .AddColumn("Budget", new double[] { 500, 200, 350, 280, 120 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddColumn("Actual", new double[] { 470, 230, 310, 295, 108 }, cfg => cfg.Color(ChartColor.ChartOrange))) .RenderToSvg(); ``` **Rendered output:** For each department, two bars appear side-by-side — blue (Budget) and orange (Actual). The legend labels both series at the bottom. ```csharp // Stacked columns (100 % normalised) string svg = ChartBuilder.Create() .Title("Revenue Mix — 100 % Stacked") .Size(700, 420) .StackPercent() // normalise to 100 % .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("%", min: 0, max: 100) .YAxisFormat("{value}%") .Series(s => s .AddColumn("Product A", new double[] { 180, 220, 240, 280 }, cfg => cfg.DataLabel.Show().Format("{value}%")) .AddColumn("Product B", new double[] { 140, 160, 175, 200 }, cfg => cfg.DataLabel.Show().Format("{value}%")) .AddColumn("Product C", new double[] { 90, 110, 130, 150 }, cfg => cfg.DataLabel.Show().Format("{value}%"))) .RenderToSvg(); ``` **Rendered output:** Each column fills the full chart height (100 %). Three colour segments per column show the proportional contribution of each product. Data labels show percentages inside each segment. --- ## 5. Bar (Horizontal) Horizontal bars — ideal for ranked or labelled categories where the labels are long. ```csharp string svg = ChartBuilder.Create() .Title("Population by City") .Size(700, 420) .XAxis(x => x.Categories.AddRange( new[] { "Tokyo","Delhi","Shanghai","São Paulo","Mexico City","Cairo" })) .YAxis(y => { y.Title = "Population (M)"; y.Min = 0; y.Max = 40; }) .AsAnimated() .Series(s => s .AddBar("Population (M)", new double[] { 37.4, 32.9, 27.1, 22.4, 21.9, 21.3 }, cfg => cfg.Color(ChartColor.ChartBlue))) .RenderToSvg(); ``` **Rendered output:** Six horizontal blue bars grow left-to-right from the Y-axis. Tokyo's bar is the longest. City names appear on the vertical axis. --- ## 6. Pie / Donut A circular chart divided into slices proportional to each value. Add a donut hole with `.DonutHole(fraction)`. ```csharp // Pie string svg = ChartBuilder.Create() .AsPie() .Title("Market Share by Vendor") .Size(600, 440) .Labels("TerraFluent","Competitor A","Competitor B","Others") .Legend(l => l.AtBottom()) .AsAnimated() .Series(s => s .Add("Market Share", new double[] { 38, 27, 21, 14 }, cfg => cfg.DataLabel.Show().Radius(1.2).Format("{value}%"))) .RenderToSvg(); ``` **Rendered output:** A circular pie with four coloured slices. Labels are placed outside each slice (radius > 1) with spline connector lines. The legend lists the four vendors at the bottom. ```csharp // Donut with center label string svg = ChartBuilder.Create() .AsPie() .Title("Revenue by Region") .Size(620, 460) .Labels("North America","Europe","Asia-Pacific","Rest of World") .Legend(l => l.AtBottom()) .AsAnimated() .Series(s => s .Add("Revenue %", new double[] { 42, 28, 22, 8 }, cfg => { cfg.DonutHole(0.55); // 55 % hole radius cfg.DataLabel.Show().Radius(1.0).Format("{value}%"); cfg.DonutCenter.Show().Title("Total"); })) .RenderToSvg(); ``` **Rendered output:** A donut ring with a hollow center. The summed total (100) is shown large in the center with "Total" as a caption above it. Labels sit at the edge of each slice. --- ## 7. Scatter Dots only — no connecting line. Use when the relationship between two variables matters more than a sequence. ```csharp string svg = ChartBuilder.Create() .Title("Sales Rep Performance") .Size(700, 420) .XAxis(x => x.Title = "Calls Made") .YAxis(y => { y.Title = "Deals Closed"; y.Min = 0; }) .AsAnimated() .Series(s => s .AddScatter("Team A", new double[] { 62, 58, 74, 55, 80, 67, 71 }, cfg => cfg.Color(ChartColor.ChartBlue).MarkerSize(8)) .AddScatter("Team B", new double[] { 45, 52, 61, 48, 57, 70, 43 }, cfg => cfg.Color(ChartColor.ChartOrange).MarkerSize(8))) .RenderToSvg(); ``` **Rendered output:** Two sets of coloured dots plotted at their respective Y-values. No lines connect the dots. A legend distinguishes Team A (blue) from Team B (orange). --- ## 8. Waterfall Shows incremental gains and losses leading to a running total. Positive changes are green, negative are red, totals use the series colour. ```csharp string svg = ChartBuilder.Create() .Title("Annual Cash Flow Analysis") .Size(720, 440) .XAxis(x => x.Categories.AddRange(new[] { "Opening","Revenue","COGS","Gross Profit","OpEx","EBITDA","Tax","Net Profit" })) .YAxis(y => y.Title = "USD ($k)") .AsAnimated() .Series(s => s .AddWaterfall( name: "P&L", data: new double?[] { 500, 800, -320, 980, -450, 530, -120, 410 }, totals: new[] { true, false, false, true, false, true, false, true })) .RenderToSvg(); ``` **Rendered output:** Eight bars. "Opening", "Gross Profit", "EBITDA", and "Net Profit" are full-height bars (totals). "Revenue" and "EBITDA" increases are green floating bars. "COGS", "OpEx", and "Tax" decreases are red bars hanging from the running total. Each bar starts where the last left off. **Data rules:** - `data` values with `totals[i] = true` reset the running total — they draw from zero. - `data` values with `totals[i] = false` are incremental (positive = up, negative = down). - Pass `null` in `data` to skip a column while keeping the running total intact. --- ## 9. Gauge A semi-circular dial showing a single value between a configurable min and max. ```csharp string svg = ChartBuilder.Create() .Title("Server CPU Utilisation") .Size(460, 360) .YAxis(y => { y.Min = 0; y.Max = 100; }) .AsAnimated() .Series(s => s .AddGauge("CPU %", 67, cfg => cfg.Color(ChartColor.ChartBlue))) .RenderToSvg(); ``` **Rendered output:** A 180° arc (top half of a circle). A coloured filled arc spans from the left baseline to the 67 % mark. The numeric value "67" is displayed below the arc center. The needle animates in on load. --- ## 10. DataRing A full 360° progress ring (like a circular progress bar) centered on a large value display. ```csharp string svg = ChartBuilder.Create() .Title("Q2 2025 — Customer Satisfaction") .Size(400, 400) .YAxis(y => { y.Min = 0; y.Max = 100; }) .AsAnimated() .Series(s => s .AddDataRing("CSAT Score", 87, cfg => { cfg.Color(ChartColor.ChartBlue); cfg.DonutCenter .Title("CSAT") .Color("#1a202c") .TitleColor("#718096") .FontSize(42) .TitleFontSize(14); })) .RenderToSvg(); ``` **Rendered output:** A thick circular ring fills 87 % of its circumference (clockwise from top). The value "87" is displayed large in the center with "CSAT" as a subtitle below. The ring sweeps in with a clockwise animation on load. **KPI Dashboard — three rings side-by-side:** ```csharp // Render three separate SVGs and combine in your layout string cpu = ChartBuilder.Create().Title("CPU") .Size(300,300).YAxis(y=>{y.Min=0;y.Max=100;}).AsAnimated() .Series(s => s.AddDataRing("CPU", 67, cfg => cfg.Color(ChartColor.ChartOrange).DonutCenter.FontSize(32))) .RenderToSvg(); string ram = ChartBuilder.Create().Title("RAM") .Size(300,300).YAxis(y=>{y.Min=0;y.Max=100;}).AsAnimated() .Series(s => s.AddDataRing("RAM", 82, cfg => cfg.Color(ChartColor.ChartRose).DonutCenter.FontSize(32))) .RenderToSvg(); string disk = ChartBuilder.Create().Title("Disk").Size(300,300).YAxis(y=>{y.Min=0;y.Max=100;}).AsAnimated() .Series(s => s.AddDataRing("Disk", 34, cfg => cfg.Color(ChartColor.ChartGreen).DonutCenter.FontSize(32))) .RenderToSvg(); // Embed in a flex container in your HTML/Blazor: // <div style="display:flex;gap:1rem"> // @((MarkupString)cpu) @((MarkupString)ram) @((MarkupString)disk) // </div> ``` --- ## 11. Bubble A scatter chart with a third dimension (Z) encoded as the bubble radius. Ideal for showing three correlated business metrics simultaneously. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Market Share vs. Growth — Bubble Size = Revenue") .Size(700, 400) .XAxis(x => { x.Title = "Market Share (%)"; x.Min = 0; x.Max = 40; }) .YAxis(y => { y.Title = "YoY Growth (%)"; y.Min = -10; y.Max = 50; }) .AsAnimated() .Legend(l => l.TopRight()) .Series(s => s .AddBubble("Product A", new[] { new BubblePoint(12, 28, 85), // (X=market share, Y=growth, Z=revenue $M) new BubblePoint(22, 15, 120), new BubblePoint( 8, 42, 60), }) .AddBubble("Product B", new[] { new BubblePoint(30, 5, 200), new BubblePoint(18, 22, 95), new BubblePoint(35, -5, 140), })) .RenderToSvg(); ``` **Rendered output:** Circles plotted at (X, Y) coordinates. The radius of each circle is proportional to its Z value — Product B's 200-unit bubble is visibly larger than the 60-unit one. Two series are coloured differently and shown in the legend. **`BubblePoint` struct:** | Property | Description | |---|---| | `X` | Horizontal axis position | | `Y` | Vertical axis position | | `Z` | Bubble size (proportional to area) | --- ## 12. Heatmap A colour-coded grid matrix. Cell colour interpolates between a cold colour and the series colour based on value intensity. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Weekly Sales by Region & Day") .Size(700, 380) .XAxis("Day", "Mon","Tue","Wed","Thu","Fri") // column labels .Series(s => s .AddHeatmap("Sales", new[] { // HeatmapPoint(col, row, value) new HeatmapPoint(0,0,42), new HeatmapPoint(1,0,58), new HeatmapPoint(2,0,73), new HeatmapPoint(3,0,61), new HeatmapPoint(4,0,88), new HeatmapPoint(0,1,31), new HeatmapPoint(1,1,45), new HeatmapPoint(2,1,52), new HeatmapPoint(3,1,78), new HeatmapPoint(4,1,65), new HeatmapPoint(0,2,67), new HeatmapPoint(1,2,83), new HeatmapPoint(2,2,91), new HeatmapPoint(3,2,55), new HeatmapPoint(4,2,48), }, cfg => { cfg.DataLabel.Show().Format("{value}"); // show value in each cell cfg.HeatmapRowLabels.AddRange(new[] { "North","South","East" }); })) .RenderToSvg(); ``` **Rendered output:** A 5 × 3 grid of coloured rectangles. Low values appear in a cold blue-grey; high values (like 91) appear in a saturated series colour. Row labels "North / South / East" are on the left; column labels "Mon – Fri" are on the bottom. Each cell shows its numeric value. **`HeatmapPoint` struct:** | Property | Description | |---|---| | `Col` | Zero-based column index (maps to `XAxis.Categories`) | | `Row` | Zero-based row index (maps to `HeatmapRowLabels`) | | `Value` | Numeric intensity controlling cell colour | --- ## 13. ColumnRange A vertical bar spanning from a low to a high value per category. Perfect for temperature ranges, confidence intervals, or scheduling spans. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Monthly Temperature Range (°C)") .Size(700, 400) .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec") .YAxis(y => { y.Title = "Temperature (°C)"; y.Min = -10; y.Max = 40; y.LabelFormat = "{value}°"; }) .AsAnimated() .Series(s => s .AddColumnRange("London", new[] { new RangePoint( 2, 8), new RangePoint( 2, 9), new RangePoint( 4, 13), new RangePoint( 6, 16), new RangePoint( 9, 20), new RangePoint(12, 23), new RangePoint(14, 26), new RangePoint(14, 25), new RangePoint(11, 21), new RangePoint( 8, 17), new RangePoint( 5, 12), new RangePoint( 3, 9), }, cfg => cfg.Color(ChartColor.ChartBlue))) .RenderToSvg(); ``` **Rendered output:** 12 blue bars — one per month — each floating between its low (bottom) and high (top) temperature. July's bar is the tallest and highest; January's is the lowest. **`RangePoint` struct:** | Property | Description | |---|---| | `Low` | Bottom of the bar / band | | `High` | Top of the bar / band | --- ## 14. AreaRange A filled band drawn between two lines (low and high). Typically used to visualise forecast confidence intervals or min/max ranges alongside a mean line. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Revenue Forecast — Confidence Band") .Size(700, 380) .XAxis("Week", "W1","W2","W3","W4","W5","W6","W7","W8") .YAxis(y => { y.Title = "Revenue ($k)"; y.Min = 80; y.Max = 280; }) .AsAnimated() .Series(s => s // Mean forecast line on top .AddLine("Forecast", new double[] { 140, 152, 161, 170, 178, 185, 192, 200 }, cfg => cfg.Color("#2ecc71").LineWidth(2)) // Confidence band underneath .AddAreaRange("Confidence Band", new[] { new RangePoint(120, 160), new RangePoint(130, 172), new RangePoint(138, 184), new RangePoint(148, 192), new RangePoint(155, 200), new RangePoint(160, 210), new RangePoint(167, 217), new RangePoint(174, 226), }, cfg => cfg.Color("#2ecc71"))) // same colour, semi-transparent fill .RenderToSvg(); ``` **Rendered output:** A green-tinted band fills the space between the lower and upper confidence bounds. A solid green line (the mean forecast) runs through the middle of the band. The band and line share the same green palette but the fill is semi-transparent. --- ## 15. Funnel Stacked trapezoid stages that narrow as a value decreases. Ideal for sales pipelines, conversion funnels, and process flow analysis. ```csharp string svg = ChartBuilder.Create() .Title("Sales Pipeline") .Size(600, 440) .XAxis("Stage", "Leads","Qualified","Proposal","Negotiation","Closed") .AsAnimated() .Series(s => s .AddFunnel("Pipeline", new double[] { 5000, 2800, 1400, 620, 310 }, cfg => cfg.Label(dl => dl.Show()))) .RenderToSvg(); ``` **Rendered output:** Five trapezoids stacked vertically, each narrower than the one above it. "Leads" (5 000) is the widest; "Closed" (310) is the narrowest. Stage labels ("Leads", "Qualified", etc.) appear on the left side of each trapezoid. **Proportionality:** Each stage's width is `value / maxValue` of the total funnel width. The library computes the widths automatically. --- ## 16. Treemap Nested rectangles sized proportionally to their value, using a balanced binary-split layout. Excellent for portfolio allocation, budget breakdown, or disk usage visualisation. ```csharp string svg = ChartBuilder.Create() .Title("Portfolio Allocation") .Size(700, 420) .XAxis("Asset", "US Equities","EU Equities","EM Equities", "Gov Bonds", "Corp Bonds", "Real Estate","Commodities","Cash") .AsAnimated() .Series(s => s .AddTreemap("Allocation", new double[] { 3200, 1800, 900, 1500, 1100, 700, 400, 300 })) .RenderToSvg(); ``` **Rendered output:** The chart area is divided into eight coloured rectangles. "US Equities" (3 200) occupies the largest rectangle (roughly half the chart). Each rectangle is labelled with its asset name and value. Colours cycle through the theme palette. --- ## 17. Radar Plots several axes radiating from a centre, with each series drawn as a closed polygon. Ideal for comparing multiple entities across the same set of metrics. ```csharp string svg = ChartBuilder.Create() .Title("Skill Assessment") .Size(560, 520) .XAxis("Skill", "Coding","Design","Testing","DevOps","Docs","Comms") .AsAnimated() .Series(s => s .AddRadar("Alice", new double[] { 90, 60, 75, 50, 65, 80 }, cfg => cfg.Color(ChartColor.ChartBlue).FillOpacity(0.25)) .AddRadar("Bob", new double[] { 65, 85, 60, 80, 55, 70 }, cfg => cfg.Color(ChartColor.ChartOrange).FillOpacity(0.25))) .RenderToSvg(); ``` **Rendered output:** Six spokes labelled with each skill. Two translucent polygons (blue, orange) overlay one another so strengths and gaps are immediately visible. A legend distinguishes the two people. --- ## 18. BoxPlot A box-and-whisker chart showing the five-number summary (min, Q1, median, Q3, max) per category. Perfect for comparing distributions. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Response Time Distribution by Endpoint (ms)") .Size(700, 420) .XAxis("Endpoint", "/login","/search","/checkout","/report") .YAxis("ms", min: 0) .AsAnimated() .Series(s => s .AddBoxPlot("Latency", new[] { // BoxPlotPoint(low, q1, median, q3, high) new BoxPlotPoint( 40, 70, 95, 130, 210), new BoxPlotPoint( 55, 90, 120, 160, 260), new BoxPlotPoint( 80, 140, 190, 250, 400), new BoxPlotPoint(120, 220, 300, 410, 620), })) .RenderToSvg(); ``` **Rendered output:** Four boxes, one per endpoint. Each box spans Q1–Q3 with a median line inside; whiskers extend to min and max. `/report` sits highest, revealing the slowest and most variable endpoint. **`BoxPlotPoint` struct:** `Low`, `Q1`, `Median`, `Q3`, `High`. --- ## 19. ErrorBar Draws a vertical whisker from a low to a high value per category — typically overlaid on a line or column series to show uncertainty. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Measured Mean ± Std. Dev.") .Size(700, 400) .XAxis("Sample", "A","B","C","D","E") .YAxis("Value", min: 0) .AsAnimated() .Series(s => s .AddLine("Mean", new double[] { 30, 42, 38, 55, 48 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddErrorBar("± SD", new[] { new RangePoint(26, 34), new RangePoint(37, 47), new RangePoint(33, 43), new RangePoint(49, 61), new RangePoint(43, 53), }, cfg => cfg.Color(ChartColor.Charcoal))) .RenderToSvg(); ``` **Rendered output:** A blue mean line with a grey error whisker at each point, each capped top and bottom, spanning the low–high uncertainty band. --- ## 20. Candlestick Financial OHLC candles. Up sessions (close ≥ open) use the theme's positive colour; down sessions use the negative colour. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("ACME — Daily OHLC") .Size(720, 420) .XAxis("Day", "Mon","Tue","Wed","Thu","Fri") .YAxis("Price ($)") .AsAnimated() .Series(s => s .AddCandlestick("ACME", new[] { // OhlcPoint(open, high, low, close) new OhlcPoint(120, 128, 118, 126), new OhlcPoint(126, 130, 122, 123), new OhlcPoint(123, 133, 121, 132), new OhlcPoint(132, 135, 128, 129), new OhlcPoint(129, 140, 127, 138), })) .RenderToSvg(); ``` **Rendered output:** Five candles. Each has a thin high–low wick and a thick open–close body. Green bodies mark up days, red bodies mark down days. **`OhlcPoint` struct:** `Open`, `High`, `Low`, `Close`. --- ## 21. OHLC The same open/high/low/close data drawn as bars instead of candles: a high–low vertical bar with a left tick (open) and a right tick (close). ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("ACME — OHLC Bars") .Size(720, 420) .XAxis("Day", "Mon","Tue","Wed","Thu","Fri") .YAxis("Price ($)") .AsAnimated() .Series(s => s .AddOhlc("ACME", new[] { new OhlcPoint(120, 128, 118, 126), new OhlcPoint(126, 130, 122, 123), new OhlcPoint(123, 133, 121, 132), new OhlcPoint(132, 135, 128, 129), new OhlcPoint(129, 140, 127, 138), })) .RenderToSvg(); ``` **Rendered output:** Five vertical bars. A left-pointing tick marks the open price and a right-pointing tick marks the close. Colour follows the same up/down convention as candlesticks. --- ## 22. Dumbbell A dot-plot connecting a low and a high value per category — great for before/after or start/end comparisons. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Salary Change After Promotion") .Size(700, 420) .XAxis("Role", "Junior","Mid","Senior","Lead","Principal") .YAxis("Salary ($k)", min: 0) .AsAnimated() .Series(s => s .AddDumbbell("Before → After", new[] { new RangePoint( 60, 72), new RangePoint( 85, 100), new RangePoint(110, 132), new RangePoint(140, 168), new RangePoint(175, 210), }, cfg => cfg.Color(ChartColor.ChartBlue))) .RenderToSvg(); ``` **Rendered output:** For each role, two dots connected by a bar — the left/lower dot is the "before" value, the right/upper dot the "after". The connector length shows the size of each raise. --- ## 23. Stream A ThemeRiver: stacked areas rendered around a centred baseline so the whole silhouette flows organically. Good for showing how a total and its composition evolve over time. ```csharp string svg = ChartBuilder.Create() .Title("Genre Popularity Over Time") .Size(720, 420) .XAxis("Year", "2019","2020","2021","2022","2023","2024") .AsAnimated() .Series(s => s .AddStream("Pop", new double[] { 40, 45, 52, 60, 58, 64 }) .AddStream("Rock", new double[] { 55, 50, 48, 44, 42, 40 }) .AddStream("Hip-Hop", new double[] { 30, 38, 47, 55, 62, 70 }) .AddStream("Jazz", new double[] { 18, 17, 16, 16, 15, 15 })) .RenderToSvg(); ``` **Rendered output:** Four coloured bands flow left-to-right around a central axis. The thickness of each band at any year is its value; the overall shape widens as total popularity grows. --- ## 24. Gantt A horizontal project timeline. Each task is a bar spanning its start-to-end position on the X (time) axis. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Release Plan (weeks)") .Size(760, 380) .XAxis(x => { x.Title = "Week"; x.Min = 0; x.Max = 12; }) .AsAnimated() .Series(s => s .AddGantt("Schedule", new[] { new GanttTask { Name = "Design", Start = 0, End = 3 }, new GanttTask { Name = "Development", Start = 2, End = 8, Color = ChartColor.ChartBlue }, new GanttTask { Name = "Testing", Start = 7, End = 10 }, new GanttTask { Name = "Launch", Start = 10, End = 12, Color = ChartColor.ChartGreen }, })) .RenderToSvg(); ``` **Rendered output:** Four stacked rows, each with a bar positioned along the week axis. Overlapping bars show parallel work (Design and Development overlap at weeks 2–3). Custom colours highlight key phases. **`GanttTask` properties:** `Name`, `Start`, `End`, optional `Color`, optional `Label`. --- ## 25. Sankey A node-link flow diagram. Nodes are stages; links carry a quantity whose thickness is proportional to its value. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Website Conversion Flow") .Size(760, 440) .AsAnimated() .Series(s => s .AddSankey("Flow", nodes: new[] { new SankeyNode { Name = "Visitors" }, // 0 new SankeyNode { Name = "Sign-ups" }, // 1 new SankeyNode { Name = "Trials" }, // 2 new SankeyNode { Name = "Paid" }, // 3 new SankeyNode { Name = "Churned" }, // 4 }, links: new[] { new SankeyLink { From = 0, To = 1, Value = 1000 }, new SankeyLink { From = 1, To = 2, Value = 620 }, new SankeyLink { From = 2, To = 3, Value = 240 }, new SankeyLink { From = 2, To = 4, Value = 380 }, })) .RenderToSvg(); ``` **Rendered output:** Five stacked nodes connected by curved ribbons. The Visitors→Sign-ups ribbon is the thickest; the split from Trials shows how many converted to Paid versus Churned. Link thickness encodes each flow's magnitude. **`SankeyLink.From` / `To`** are zero-based indices into the `nodes` array. --- ## 26. Parliament A semicircular seating chart — one dot per seat, grouped by party. Standard for election results and any whole-of-assembly composition. ```csharp using TerraFluent.Chart.Reporting.Models; string svg = ChartBuilder.Create() .Title("Parliament Composition (200 seats)") .Size(640, 400) .AsAnimated() .Series(s => s .AddParliament("Seats", new[] { new ParliamentGroup("Progressive", ChartColor.ChartBlue, 82), new ParliamentGroup("Conservative", ChartColor.ChartRed, 74), new ParliamentGroup("Green", ChartColor.ChartGreen, 26), new ParliamentGroup("Independent", ChartColor.Charcoal, 18), })) .RenderToSvg(); ``` **Rendered output:** An arc of 200 coloured dots arranged in concentric rows, grouped left-to-right by party. Each party occupies a contiguous block sized by its seat count. A legend maps colours to parties. **`ParliamentGroup` constructor:** `(string name, string color, int seats)`. --- ## Combining Chart Types (Mixed Charts) You can mix any series types within a single chart: ```csharp string svg = ChartBuilder.Create() .Title("Revenue & Profit Margin") .Size(720, 420) .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("Revenue ($k)", min: 0) .YAxis2(y => { y.Title = "Margin (%)"; y.Min = 0; y.Max = 50; }) .AsAnimated() .Series(s => s // Columns on the primary (left) Y-axis .AddColumn("Revenue", new double[] { 310, 390, 420, 510 }, cfg => cfg.Color(ChartColor.ChartBlue)) // Line on the secondary (right) Y-axis .AddLine("Margin %", new double[] { 22, 28, 25, 31 }, cfg => cfg.Color(ChartColor.ChartRose).LineWidth(3).OnSecondaryAxis())) .RenderToSvg(); ``` **Rendered output:** Blue columns grow from the left axis. A pink line floats independently, scaled against the right axis (0 – 50 %). Both axes are labelled. The legend shows both series. ==================================================================== TerraFluent.Chart.Reporting — Themes & Styling URL: https://terrafluent.dev/docs/chart/themes-and-styling/ ==================================================================== # Themes & Styling Everything you need to control the visual appearance of your charts: themes, colour palettes, series styling, data labels, and tooltips. --- ## Built-in Themes Apply a theme with `.Theme(ChartTheme.X)`: ```csharp ChartBuilder.Create() .Theme(ChartTheme.Dark) // … ``` | Theme | Background | Text | Palette style | Best for | |---|---|---|---|---| | `ChartTheme.Default` | `#ffffff` white | `#333333` dark | Blue-orange | General use | | `ChartTheme.Dark` | `#1a1a2e` navy | `#f0f0ff` light | Neon-adjacent bright palette | Browser dashboards | | `ChartTheme.Pastel` | `#fafafa` off-white | `#555555` grey | Soft muted palette | Presentations, reports | | `ChartTheme.Monochrome` | `#ffffff` white | `#000000` black | Greyscale only | Print, greyscale PDFs | | `ChartTheme.Ocean` | `#0d1b2a` deep navy | `#c8e6ff` pale blue | Blue-teal palette | Analytics dashboards | | `ChartTheme.Sunset` | `#1a0a2e` purple-navy | `#ffe8c8` warm cream | Vivid warm palette | Editorial, marketing | | `ChartTheme.Forest` | `#f6f4ee` warm cream | `#2c2a1a` earthy brown | Earthy greens | Nature / ESG reporting | | `ChartTheme.Neon` | `#0a0a0a` near-black | `#f0f0ff` light | Electric high-contrast palette | Dark dashboards, streaming overlays | | `ChartTheme.Minimal` | `#ffffff` white | `#333333` graphite | Muted professional palette | Clean editorial reports | | `ChartTheme.Warm` | `#faf3e0` parchment | `#3e2723` espresso | Amber-brown earth tones | Warm, print-style reports | | `ChartTheme.Arctic` | `#f0f8ff` ice blue | `#1c2e4a` polar navy | Cool crisp blues | Clean corporate dashboards | | `ChartTheme.Business` | `#ffffff` white | `#333333` graphite | Corporate blue-red palette | Executive summaries | | `ChartTheme.Material` | `#ffffff` white | `#444444` charcoal | Material Design 500-level palette | Modern web dashboards | | `ChartTheme.TrafficLight` | `#fafafa` off-white | `#333333` graphite | Green/amber/red status palette | KPI / status dashboards | | `ChartTheme.Accessible` | `#ffffff` white | `#333333` graphite | Colour-blind-safe (Wong 2011) | Accessibility-critical charts | | `ChartTheme.Vivid` | `#ffffff` white | `#2c3e50` slate | Full-spectrum distinct palette | High-impact presentations | | `ChartTheme.HighContrast` | `#ffffff` white | `#000000` black | WCAG AA (≥ 4.5:1) palette | Accessibility-critical charts | | `ChartTheme.Modern` | `#f8f8ff` ghost white | `#2c3e50` slate | Material palette + `ModernStyle` on | Default recommendation for new dashboards | See [Modern Styling](#modern-styling) below for what `ModernStyle` changes, and [ChartColor Catalogue](#chartcolor-catalogue) for the exact hex values behind each palette. ### Dark Theme Example ```csharp string svg = ChartBuilder.Create() .Theme(ChartTheme.Dark) .Title("Server Throughput") .Size(700, 420) .XAxis("Hour", "00","04","08","12","16","20","24") .YAxis("Req/s", min: 0) .AsAnimated() .Series(s => s .AddArea("API", new double[] { 820, 430, 1200, 1800, 1650, 1100, 640 }) .AddLine("Cache", new double[] { 600, 310, 900, 1300, 1200, 850, 480 })) .RenderToSvg(); ``` **Output:** Navy blue background (`#1a1a2e`), light axis labels, bright palette area/line. Grid lines rendered in muted `#3a3a5e`. Title text is `#f0f0ff`. ### Pastel Theme Example ```csharp string svg = ChartBuilder.Create() .Theme(ChartTheme.Pastel) .Title("Monthly Sales") .Size(700, 420) .XAxis(x => x.Categories.AddRange(new[] { "Jan","Feb","Mar","Apr","May","Jun" })) .YAxis("Units", min: 0) .AsAnimated() .Series(s => s .AddColumn("Product A", new double[] { 420, 380, 510, 490, 560, 600 }) .AddColumn("Product B", new double[] { 310, 290, 370, 400, 430, 450 })) .RenderToSvg(); ``` **Output:** Off-white `#fafafa` background, very light grid lines, soft muted palette (`#a8d8ea`, `#aa96da`, …). Suitable for presentations and printed reports. --- ## Modern Styling Every built-in theme — including `Default` — has `ChartTheme.ModernStyle` set to `true`. It applies a consistent set of refinements uniformly across **Static**, **Animated**, and **Interactive** render modes: - Softly rounded corners on column/bar rectangles. - A subtle top-lighter fill gradient with soft elevation shadow on columns, bars, and areas. - Hollow-ring line/scatter markers instead of solid dots. - Crisper separators between pie/donut slices. - Fading (gradient) area-chart fills instead of a flat opacity. - Lighter, horizontal-only grid lines. - In **Interactive** mode only: a full-height/width **hover band** that highlights the entire category column or row on mouse-over, in addition to the point-level tooltip. `ChartTheme.Modern` is a dedicated preset (Material palette on a ghost-white background) for teams that want the modern look front-and-centre, but the styling itself is not tied to that one preset — it is on by default for every theme above. To opt out and get the classic flat look, clone a theme and turn it off: ```csharp var classic = ChartTheme.Dark.Clone(); classic.ModernStyle = false; string svg = ChartBuilder.Create() .Theme(classic) .AsInteractive() .Series(s => s.AddColumn("Sales", new double[] { 420, 380, 510, 490 })) .RenderToSvg(); ``` With `ModernStyle = false`, columns/bars render with sharp corners and a flat fill, markers render as solid dots, and hover bands are omitted from Interactive output. --- ## Custom Theme Create a one-off theme with `ChartTheme.Custom(…)`: ```csharp var brandTheme = ChartTheme.Custom( backgroundColor: "#0d1117", // GitHub-dark background plotBackgroundColor: "none", gridLineColor: "#21262d", axisLineColor: "#30363d", textColor: "#e6edf3", fontFamily: "'Segoe UI', sans-serif", colors: new[] { "#58a6ff", "#3fb950", "#f78166", "#d29922", "#a371f7" } ); string svg = ChartBuilder.Create() .Theme(brandTheme) .Title("Custom Brand Theme") .Size(700, 420) .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("Revenue ($k)", min: 0) .AsAnimated() .Series(s => s .AddLine("Revenue", new double[] { 310, 420, 380, 510 }) .AddLine("Target", new double[] { 340, 400, 420, 500 })) .RenderToSvg(); ``` **Output:** Dark background styled like GitHub's dark mode. Blue and green lines are drawn against a near-black `#0d1117` background with subtle grid lines. **`ChartTheme.Custom` parameters** (all optional — unset properties fall back to `Default`): | Parameter | Description | |---|---| | `backgroundColor` | SVG/page background colour. | | `plotBackgroundColor` | Plot area fill. Use `"none"` for transparent. | | `gridLineColor` | Colour of horizontal grid lines. | | `axisLineColor` | Colour of axis border lines. | | `textColor` | Default text fill colour (title, labels, legend). | | `fontFamily` | CSS font-family for all text. | | `colors` | Ordered palette array — must have at least one entry when provided. | --- ## Overriding Palette Only Use `.Colors(…)` to swap the palette without changing the rest of the active theme: ```csharp string svg = ChartBuilder.Create() .Theme(ChartTheme.Dark) // dark background + text .Colors(ChartColor.ChartRose, ChartColor.ChartYellow, ChartColor.ChartTeal) // but use a custom 3-colour palette .Title("Sales Channels") .Size(700, 420) .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("Units", min: 0) .AsAnimated() .Series(s => s .AddColumn("Online", new double[] { 180, 210, 190, 260, 310, 280 }) .AddColumn("Store", new double[] { 140, 160, 175, 200, 195, 220 }) .AddLine("Total", new double[] { 320, 370, 365, 460, 505, 500 })) .RenderToSvg(); ``` **Output:** Dark theme background and text, but the three series use pink, yellow, and teal instead of the dark theme's default palette. Colours wrap: if you have 5 series and 3 colours, series 4 and 5 use colours 1 and 2 again. --- ## ChartColor Catalogue `ChartColor` (namespace `TerraFluent.Chart.Reporting.Models`) provides 400+ named colour constants as `public const string` values. Use them anywhere a colour string is accepted — no hex codes to remember. ```csharp using TerraFluent.Chart.Reporting.Models; // Palette override .Colors(ChartColor.ChartBlue, ChartColor.ChartOrange, ChartColor.ChartGreen) // Per-series colour .AddLine("Sales", data, cfg => cfg.Color(ChartColor.ChartRose)) // Plot annotation .PlotLine(500, color: ChartColor.Red, width: 2, label: "SLA limit") ``` ### Chart Palette Constants | Constant | Hex | Default series slot | |---|---|---| | `ChartColor.ChartBlue` | `#7CB5EC` | 1st | | `ChartColor.ChartOrange` | `#F7A35C` | 2nd | | `ChartColor.ChartGreen` | `#90ED7D` | 3rd | | `ChartColor.ChartYellow` | `#E4D354` | 4th | | `ChartColor.ChartIndigo` | `#8085E9` | 5th | | `ChartColor.ChartRose` | `#F15C80` | 6th | | `ChartColor.ChartTeal` | `#2B908F` | 7th | | `ChartColor.ChartRed` | `#F45B5B` | 8th | | `ChartColor.Charcoal` | `#434348` | Dark accent | ### Pastel Palette Constants | Constant | Hex | |---|---| | `ChartColor.PastelSkyBlue` | `#A8D8EA` | | `ChartColor.PastelPurple` | `#AA96DA` | | `ChartColor.PastelPink` | `#FCBAD3` | | `ChartColor.PastelMint` | `#B5EAD7` | | `ChartColor.PastelPeach` | `#FFDAC1` | | `ChartColor.PastelPeriwinkle` | `#C7CEEA` | | `ChartColor.PastelLime` | `#E2F0CB` | | `ChartColor.PastelLemon` | `#FFFFD2` | ### Pre-built Palettes Apply a complete curated palette in one call: ```csharp .Colors(ChartColor.Palette.Default) // Default palette (same as no override) .Colors(ChartColor.Palette.Pastel) // soft pastels — matches ChartTheme.Pastel .Colors(ChartColor.Palette.Material) // Material Design colours .Colors(ChartColor.Palette.Business) // professional blues / greys .Colors(ChartColor.Palette.Accessible) // WCAG high-contrast .Colors(ChartColor.Palette.TrafficLight) // red / amber / green .Colors(ChartColor.Palette.Monochrome) // greyscale range ``` ### Colour Utility Methods ```csharp // Semi-transparent variant → "rgba(124,181,236,0.25)" string faded = ChartColor.WithOpacity(ChartColor.ChartBlue, 0.25); // Tint / shade string lighter = ChartColor.Lighten(ChartColor.ChartBlue, 0.20); string darker = ChartColor.Darken(ChartColor.ChartBlue, 0.20); // From RGB components → "#7CB5EC" string custom = ChartColor.FromRgb(124, 181, 236); // Blend two colours at equal weight string mixed = ChartColor.Mix(ChartColor.ChartBlue, ChartColor.White, 0.5); ``` > The full catalogue (400+ constants: CSS named colours, web-safe primaries, chart palette, pastels, theme-specific tokens, and more) is defined in `Models/ChartColor.cs`. --- ## Per-Series Colour Override a single series colour inside the `cfg =>` lambda: ```csharp .Series(s => s .AddLine("Revenue", data, cfg => cfg.Color(ChartColor.ChartBlue)) .AddLine("Cost", data, cfg => cfg.Color(ChartColor.ChartOrange)) .AddLine("Profit", data, cfg => cfg.Color(ChartColor.ChartGreen))) ``` --- ## Line Styling ```csharp .AddLine("Target", data, cfg => cfg .Color(ChartColor.ChartRose) .LineWidth(3) // stroke width (px), default 2 .Dashed() // "Solid" | "Dashed" | "Dotted" | "DashDotted" | "LongDashed" .MarkerEnabled(false)) ``` **`Dashed` variants:** | Method | SVG stroke-dasharray | Looks like | |---|---|---| | `Solid()` | (none) | ——————— | | `Dashed()` | `8 4` | - - - - - - | | `Dotted()` | `2 3` | · · · · · · | | `DashDotted()` | `8 4 2 4` | -·-·-·-·- | | `LongDashed()` | `16 6` | ——— ——— | **Example — dashed reference line on a line chart:** ```csharp string svg = ChartBuilder.Create() .Title("Actuals vs Target") .Size(700, 400) .XAxis("Q", "Q1","Q2","Q3","Q4") .YAxis("Revenue ($k)", min: 0) .AsAnimated() .Series(s => s .AddColumn("Actuals", new double[] { 310, 390, 420, 510 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddLine("Target", new double[] { 300, 380, 420, 450 }, cfg => cfg.Color(ChartColor.ChartRose).LineWidth(2).Dashed())) .RenderToSvg(); ``` **Output:** Blue columns for actuals. A pink dashed horizontal reference line for the target. The dashes distinguish the target clearly without overwhelming the data. --- ## Area Fill Opacity ```csharp .AddArea("Organic", data, cfg => cfg.Color(ChartColor.ChartBlue).FillOpacity(0.15)) .AddArea("Direct", data, cfg => cfg.Color(ChartColor.ChartGreen).FillOpacity(0.40)) .AddArea("Paid", data, cfg => cfg.Color(ChartColor.ChartOrange).FillOpacity(0.70)) ``` **`FillOpacity`** accepts values from `0.0` (fully transparent) to `1.0` (fully opaque). Default is `0.25` for normal area, `0.7` for stacked area. **Output:** Three overlapping area series, each at a different opacity. The Organic area at 15 % barely tints the background; the Paid area at 70 % is visually dominant. --- ## Column / Bar Borders & Corner Radius ```csharp .AddColumn("Revenue", data, cfg => cfg .Color(ChartColor.ChartBlue) .BorderColor("#1a6090") // contrasting outline .BorderWidth(1) // border thickness (px) .BorderRadius(6)) // rounded top corners (px) ``` **Example — three styled column series:** ```csharp string svg = ChartBuilder.Create() .Title("Series Formatting — Border & Radius") .Size(760, 440) .XAxis("Q", "Q1","Q2","Q3","Q4") .YAxis("Value ($k)", min: 0) .AsAnimated() .Series(s => s .AddColumn("Revenue", new double[] { 320, 410, 390, 480 }, cfg => cfg .Color(ChartColor.ChartBlue).BorderColor("#1a6090").BorderWidth(1).BorderRadius(6)) .AddColumn("Cost", new double[] { 210, 260, 245, 290 }, cfg => cfg .Color(ChartColor.ChartOrange).BorderColor("#7a3a00").BorderWidth(2).BorderRadius(3)) .AddColumn("Profit", new double[] { 110, 150, 145, 190 }, cfg => cfg .Color(ChartColor.ChartGreen).BorderColor("#1e7a00").BorderWidth(2).BorderRadius(0))) .RenderToSvg(); ``` **Output:** Three grouped column series. Revenue has rounded tops with a blue border; Cost has a dark orange border with subtle corners; Profit has a dark green thick border and sharp square corners. --- ## Scatter Marker Styling ```csharp .AddScatter("Group A", data, cfg => cfg .Color(ChartColor.ChartBlue) // dot fill .BorderColor("#e6b800") // gold ring around dot .BorderWidth(2) // ring thickness .MarkerSize(6)) // dot radius (px) ``` **Output:** Sky-blue dots with a gold ring border. Use this to differentiate overlapping points or to match corporate brand colours. --- ## Data Labels Show a value label on or near each data point. ```csharp // Enable on all series at once: ChartBuilder.Create() .ShowDataLabels() .Series(s => s.AddColumn("Sales", data)) // Enable per-series with custom formatting: .AddColumn("Budget", data, cfg => cfg .DataLabel .Show() .Format("${value}k") .Color("#1a6090") .FontSize(11) .Background("rgba(124,181,236,0.25)")) ``` **`DataLabelOptions` quick reference:** | Method | Example | Result | |---|---|---| | `.Show()` | — | Enables labels | | `.Format("{value}%")` | `38%` | Value with suffix | | `.Format("${value}k")` | `$320k` | Currency format | | `.Color(ChartColor.White)` | — | White text on coloured background | | `.FontSize(11)` | — | Smaller text for dense charts | | `.Background(ChartColor.ChartBlue)` | — | Solid pill background | | `.Background("rgba(…,0.25)")` | — | Tinted transparent pill | | `.Radius(1.25)` | — | Pie: label outside with connector | | `.OffsetY(-10)` | — | Nudge label 10 px upward | **Three label styles on one chart:** ```csharp string svg = ChartBuilder.Create() .Title("Data Label Showcase") .Size(760, 460) .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("Value ($k)", min: 0) .AsAnimated() .Series(s => s // Style 1: solid pill — white text on series colour .AddLine("Revenue", new double[] { 310, 420, 385, 510 }, cfg => { cfg.Color(ChartColor.ChartBlue).LineWidth(3); cfg.DataLabel.Show().Color("#fff").FontSize(11) .Background(ChartColor.ChartBlue).Format("${value}k"); }) // Style 2: tinted transparent background .AddColumn("Expenses", new double[] { 180, 210, 195, 240 }, cfg => { cfg.Color(ChartColor.ChartOrange); cfg.DataLabel.Show().Color("#7a2f00").FontSize(10) .Background("rgba(247,163,92,0.3)").Format("${value}k"); }) // Style 3: no background, larger font .AddSpline("Profit", new double[] { 130, 210, 190, 270 }, cfg => { cfg.Color(ChartColor.ChartGreen).LineWidth(2); cfg.DataLabel.Show().Color("#1a5e00").FontSize(13) .Format("+{value}k").Background("#cfffcf"); })) .RenderToSvg(); ``` **Output:** Revenue labels appear as solid blue pills. Expense column labels float above each bar with a light orange tint. Profit spline labels use a green-tinted background with larger text. --- ## Pie / Donut Label Placement Use `DataLabel.Radius(fraction)` to position slice labels: | Fraction | Placement | Connector | |---|---|---| | `0.6` | Inside the slice | No | | `1.0` | At the slice edge | No | | `> 1.0` e.g. `1.25` | Outside the pie | Yes — spline connector | ```csharp string svg = ChartBuilder.Create() .AsPie() .Title("Market Share") .Size(680, 480) .Labels("North America","Europe","Asia-Pacific","Lat. America","Rest of World") .Legend(l => l.AtBottom()) .AsAnimated() .Series(s => s .Add("Share", new double[] { 38, 24, 20, 11, 7 }, cfg => cfg.DataLabel.Show() .Radius(1.25) // outside, with connector lines .Format("{value}%") .FontSize(11) .Color("#333333") .Background("rgba(255,255,255,0.85)"))) .RenderToSvg(); ``` **Output:** Each slice has a label outside the pie arc, connected to its slice by a thin curved spline line. Labels show "38%", "24%", etc. on a semi-transparent white pill. --- ## Custom Tooltip Styling ```csharp string svg = ChartBuilder.Create() .Title("Tooltip — Dark Style") .Size(760, 440) .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("USD ($k)", min: 0) .AsAnimated() .Tooltip(t => t .BackgroundColor("rgba(30,30,60,0.92)") .TextColor("#e8f4ff") .FontSize(12) .Border(ChartColor.ChartBlue, width: 1, radius: 6) .Padding(14) .Format("{label}: ${value}k") .TransitionDuration(0.20)) .Series(s => s .AddLine("Revenue", new double[] { 320, 410, 390, 480 }, cfg => cfg.Color(ChartColor.ChartBlue).LineWidth(3)) .AddColumn("Cost", new double[] { 210, 260, 245, 290 }, cfg => cfg.Color(ChartColor.ChartOrange))) .RenderToSvg(); ``` **Output (Interactive / Animated mode):** When hovering a data point, a dark navy tooltip appears with a blue border, light text, and shows e.g. "Revenue: $410k". It fades in over 200 ms. **Light tooltip without arrow:** ```csharp .Tooltip(t => t .BackgroundColor("rgba(255,255,255,0.95)") .TextColor("#1a202c") .FontSize(11) .Border("#cbd5e0", width: 1, radius: 4) .Padding(12) .HideArrow() .Format("{label} — {value}k sessions")) ``` **Output:** White box with no triangular pointer, subtle grey border, dark text. Minimal and clean. --- ## Legend Styling ```csharp // Vertical legend on the right side .Legend(l => l .Vertical() .AlignRight() .AtMiddle() .Padding(12) .ItemFontSize(13) .ItemFontColor("#2d3748") .SymbolSize(14, 14) .SymbolRadius(7) // fully circular swatch .Border("#cbd5e0", width: 1, radius: 6) .BackgroundColor("rgba(255,255,255,0.92)")) // Horizontal legend at the top .Legend(l => l .AtTop() .AlignCenter() .Horizontal() .Padding(8) .ItemFontSize(12) .Border("#4a5568", width: 1, radius: 4) .BackgroundColor("rgba(255,255,255,0.85)")) ``` **Output (vertical right):** A bordered legend box floats on the right side of the chart with circular coloured swatches and slightly larger font than the default. **Output (top center):** A horizontal legend bar sits above the plot area with a subtle border, centred. ==================================================================== TerraFluent.Chart.Reporting — Advanced Features URL: https://terrafluent.dev/docs/chart/advanced/ ==================================================================== # Advanced Features Covers render modes, all output methods, axis configuration, plot annotations, stacking, the Fork pattern, and Dependency Injection. --- ## Render Modes TerraFluent generates different SVG variants depending on the target environment. ### Static Safe for PDF, email, and server-side image generation. No JavaScript, no CSS hover rules, no SMIL animation. ```csharp string svg = ChartBuilder.Create() .Title("Annual Report — Revenue Summary") .Size(700, 400) .AsStatic() // no hover, no JS, no animation .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis("Revenue ($k)", min: 0) .Series(s => s .AddColumn("Revenue", new double[] { 320, 410, 390, 480 }, cfg => cfg.Color(ChartColor.Charcoal)) .AddLine("Target", new double[] { 300, 380, 420, 450 }, cfg => cfg.Color(ChartColor.ChartRose).Dashed())) .RenderToSvg(); ``` **Output:** A pure SVG with no `<script>` blocks, no CSS `:hover` rules, and no `<animate>` elements. Every charting tool that supports SVG 1.1 can render it — including iTextSharp, PuppeteerSharp, and most email clients. > **Note:** `AsStatic()` automatically sets `Animation.Enabled = false`. Call `.DisableAnimation()` explicitly if you use `AsAnimated()` but want to suppress animation. --- ### Animated SMIL/CSS animations only — no JavaScript. Recommended for Blazor, embedded SVG in HTML, and server-rendered pages. ```csharp string svg = ChartBuilder.Create() .Title("Monthly Sales") .Size(700, 420) .AsAnimated() .Animate(800) // optional: custom duration (ms) .Animation(a => a.EaseInOut()) // optional: custom easing .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("Units", min: 0) .Series(s => s .AddColumn("Sales", new double[] { 420, 380, 510, 490, 560, 600 })) .RenderToSvg(); ``` **Output:** Each column contains an `<animate>` element that grows the bar from zero height to its target height over 800 ms with an ease-in-out curve. Line and area charts animate with a stroke-dashoffset sweep. **Easing options:** ```csharp .Animation(a => a.EaseOut()) // default — fast then slow .Animation(a => a.EaseIn()) // slow then fast .Animation(a => a.EaseInOut()) // slow at both ends .Animation(a => a.Linear()) // constant speed .Animation(a => a.Bounce()) // bounces at the end .Animation(a => a.Elastic()) // elastic snap ``` --- ### Interactive Adds CSS hover highlight effects and embedded JavaScript for click interactions. Browser-only — do not use in PDFs or emails. ```csharp string svg = ChartBuilder.Create() .Title("Interactive Sales Dashboard") .Size(750, 440) .AsInteractive() .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("USD (thousands)", min: 0) .Series(s => s .AddLine("Online", new double[] { 180, 210, 190, 260, 310, 280 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddLine("In-Store", new double[] { 140, 160, 175, 200, 195, 220 }, cfg => cfg.Color(ChartColor.ChartOrange))) .RenderToSvg(); ``` **Output:** The SVG includes `<style>` blocks with `:hover` transitions and a `<script>` section that highlights the hovered data point and shows a tooltip pop-up. The tooltip is rendered with CSS positioning inside the SVG's foreignObject. --- ### Mode Decision Guide ``` ┌─ Does it go into a PDF or email? │ └─ Yes → AsStatic() │ └─ No │ ├─ Is it in a Blazor component / embedded SVG tag? │ │ └─ Yes → AsAnimated() │ └─ Is it a standalone HTML page / dashboard? │ └─ Yes → AsInteractive() or AsAnimated() ``` --- ## Output Methods ### String (most common) ```csharp string svg = builder.RenderToSvg(); ``` Returns the complete SVG markup as a `string`. Embed it directly in HTML, Blazor `MarkupString`, or a response body. ### HTML Fragment ```csharp // String string html = builder.RenderToHtml(caption: "FY 2025 Sales", cssClass: "chart-card"); // File builder.RenderToHtmlFile("output/chart.html", caption: "FY 2025 Sales"); ``` Wraps the SVG in `<figure>` + optional `<figcaption>`. The outer `<figure>` has `display:inline-block; width:100%` so the SVG is responsive within its container. **Output structure:** ```html <figure class="chart-card" style="margin:0;padding:0;display:inline-block;width:100%;"> <div style="width:100%;overflow:hidden;"> <svg …> … </svg> </div> <figcaption style="text-align:center;font-size:0.85em;color:#666;margin-top:4px;"> FY 2025 Sales </figcaption> </figure> ``` ### File ```csharp builder.RenderToFile("output/chart.svg"); builder.RenderToHtmlFile("output/chart.html", caption: "My Chart"); ``` The parent directory must exist before calling either method. A `DirectoryNotFoundException` is thrown otherwise. ### Stream ```csharp // Synchronous using var fs = File.OpenWrite("output/chart.svg"); builder.RenderToStream(fs); // Asynchronous (.NET 6+) await using var fs = File.OpenWrite("output/chart.svg"); await builder.RenderToStreamAsync(fs, cancellationToken); ``` ### Byte Array ```csharp byte[] bytes = builder.RenderToBytes(); // Useful for: Response.Body.WriteAsync, Azure Blob upload, etc. ``` ### Async File Methods (.NET 6+) ```csharp await builder.RenderToFileAsync("output/chart.svg", cancellationToken); await builder.RenderToHtmlFileAsync("output/chart.html", "Caption", cancellationToken: ct); ``` --- ## Axes — Advanced Configuration ### Categorical X-Axis ```csharp // Shorthand (title + categories in one call) .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") // Lambda (full control) .XAxis(x => { x.Title = "Month"; x.LabelRotation = -45; // diagonal labels for long names x.GridLineVisible = true; x.Categories.AddRange(new[] { "Jan","Feb","Mar","Apr","May","Jun" }); }) ``` **Output with rotation:** Category labels are rotated 45° counter-clockwise, preventing overlap on dense charts. ### Numeric X-Axis (no categories) Use `XAxis.Min`, `XAxis.Max`, and `XAxis.TickInterval` for charts with a numeric X dimension: ```csharp .XAxis(x => { x.Title = "Time (s)"; x.Min = 0; x.Max = 30; x.TickInterval = 5; // ticks at 0, 5, 10, 15, 20, 25, 30 x.GridLineVisible = true; }) .YAxis("Amplitude", min: -1.5, max: 1.5) .Series(s => s .AddLine("Signal", new double[] { 0.0, 0.87, 1.0, 0.5, -0.5, -1.0, -0.87, 0.0, 0.87, 1.0, 0.5, -0.5, -1.0, -0.87, 0.0, 0.87 })) ``` **Output:** A numeric axis from 0 to 30, with tick marks every 5 units. The signal values are plotted at their Y positions; the X positions are evenly spaced. ### Y-Axis Format ```csharp .YAxisFormat("${value}k") // → $320k .YAxisFormat("{value}%") // → 42% .YAxisFormat("{value:N0}") // → 12,400 ``` ### Secondary Y-Axis Bind a second, independent Y-axis on the right side: ```csharp string svg = ChartBuilder.Create() .Title("Revenue & Profit Margin") .Size(720, 420) .XAxis("Quarter", "Q1 2024","Q2 2024","Q3 2024","Q4 2024") .YAxis(y => { y.Title = "Revenue ($k)"; y.Min = 0; }) .YAxis2(y => { y.Title = "Margin (%)"; y.Min = 0; y.Max = 50; }) .AsAnimated() .Series(s => s .AddColumn("Revenue", new double[] { 310, 390, 420, 510 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddLine("Margin %", new double[] { 22, 28, 25, 31 }, cfg => cfg.Color(ChartColor.ChartRose).LineWidth(3).OnSecondaryAxis())) .RenderToSvg(); ``` **Output:** Columns scaled against the left axis (0–510+). The margin line floats independently scaled against the right axis (0–50 %). Both axis labels are visible. --- ## Plot Bands & Reference Lines ### Plot Band (shaded region) ```csharp string svg = ChartBuilder.Create() .Title("Server Response Time") .Size(720, 420) .XAxis("Hour", "00","02","04","06","08","10","12","14","16","18","20","22") .YAxis(y => { y.Title = "ms"; y.Min = 0; // Green band: response time is "Good" y.PlotBands.Add(new PlotBand { From = 0, To = 200, Color = "rgba(144,237,125,0.15)", Label = "Good" }); // Orange band: "Acceptable" y.PlotBands.Add(new PlotBand { From = 200, To = 500, Color = "rgba(247,163,92,0.15)", Label = "Acceptable" }); }) .AsAnimated() .Series(s => s .AddLine("p50", new double[] { 85, 78, 72, 90, 145, 220, 310, 280, 195, 160, 130, 105 }, cfg => cfg.Color(ChartColor.ChartBlue)) .AddLine("p99", new double[] { 210, 185, 170, 240, 420, 610, 780, 720, 490, 380, 290, 230 }, cfg => cfg.Color(ChartColor.ChartRed).Dashed())) .RenderToSvg(); ``` **Output:** Two semi-transparent coloured bands fill the plot area at `0–200 ms` (green) and `200–500 ms` (orange). Band labels "Good" and "Acceptable" appear at the right edge of each band. The p99 line visibly crosses out of the orange band into the danger zone during peak hours. ### Reference Line (PlotLine) ```csharp // Shorthand on ChartBuilder .PlotLine(value: 500, color: ChartColor.ChartRed, width: 2, label: "SLA limit", dashStyle: "Dash") // Via lambda on YAxis .YAxis(y => y.PlotLines.Add(new PlotLine { Value = 500, Color = ChartColor.ChartRed, Width = 2, DashStyle = "Dash", Label = "SLA limit" })) ``` **Output:** A red dashed horizontal line drawn at y = 500. The label "SLA limit" floats to the right of the line. --- ## Stacking ### Normal (cumulative) ```csharp string svg = ChartBuilder.Create() .Title("Monthly Traffic — Stacked") .Size(720, 420) .StackNormal() .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("Sessions", min: 0) .AsAnimated() .Series(s => s .AddArea("Organic", new double[] { 1200, 1350, 1500, 1700, 1900, 2100 }) .AddArea("Direct", new double[] { 800, 850, 900, 950, 1000, 1100 }) .AddArea("Referral", new double[] { 400, 420, 440, 480, 520, 560 })) .RenderToSvg(); ``` **Output:** Three areas stack on top of each other. The top edge of the Referral band shows the combined total for all channels at each month. Users can visually compare both individual channels and the total. ### Percent (100 % normalised) ```csharp string svg = ChartBuilder.Create() .Title("Revenue Mix — 100 % Stacked") .Size(700, 420) .StackPercent() .XAxis("Quarter", "Q1","Q2","Q3","Q4") .YAxis(y => { y.Title = "%"; y.Min = 0; y.Max = 100; y.LabelFormat = "{value}%"; }) .Series(s => s .AddColumn("Product A", new double[] { 180, 220, 240, 280 }, cfg => cfg.DataLabel.Show().Format("{value}%")) .AddColumn("Product B", new double[] { 140, 160, 175, 200 }, cfg => cfg.DataLabel.Show().Format("{value}%")) .AddColumn("Product C", new double[] { 90, 110, 130, 150 }, cfg => cfg.DataLabel.Show().Format("{value}%"))) .RenderToSvg(); ``` **Output:** Each column fills 100 % of the chart height. Segments show each product's proportional share. Data labels inside each segment show the actual percentage values. --- ## Fork — Chart Variants `Fork()` creates an independent deep copy of the builder state. Use it to produce multiple variations (e.g. Static for PDF + Animated for web) without re-writing the configuration. ```csharp // Build the shared base once ChartBuilder baseChart = ChartBuilder.Create() .Size(820, 380) .Title("Team Performance Scores") .XAxis("Month", "Jan","Feb","Mar","Apr","May","Jun") .YAxis("Score", min: 0) .Colors("#4a90d9", "#e8734a") .Series(s => s .AddLine("Team A", new double[] { 70, 78, 82, 88, 85, 92 }) .AddLine("Team B", new double[] { 60, 65, 74, 71, 80, 85 })); // Fork 1: static SVG for PDF export string pdfSvg = baseChart .Fork() .AsStatic() .Title("Team Performance Scores — Q1/Q2 Report") // different title on the fork .RenderToSvg(); // Fork 2: animated SVG for the web dashboard string webSvg = baseChart .Fork() .AsAnimated() .Animate(600) .RenderToSvg(); // The original baseChart is unchanged — you can fork again as many times as needed. ``` **Key Fork rules:** - Changes on a fork do not affect the original builder or other forks. - All series data, colours, axis configuration, titles, and theme are deep-copied. - The renderer instance is shared (it is stateless, so this is safe). --- ## Dependency Injection Register `IChartBuilder` in the DI container to keep services testable: ```csharp // Program.cs / Startup.cs builder.Services.AddScoped<IChartBuilder>(_ => ChartBuilder.Create()); // Or with a shared theme: builder.Services.AddScoped<IChartBuilder>(_ => ChartBuilder.Create().Theme(ChartTheme.Dark)); ``` **Service usage:** ```csharp public class DashboardService { private readonly IChartBuilder _charts; public DashboardService(IChartBuilder charts) => _charts = charts; public string BuildRevenueSvg(IReadOnlyList<double> values, string[] months) { return _charts .Fork() // always fork so the injected instance stays clean .Title("Monthly Revenue") .Size(700, 400) .XAxis("Month", months) .YAxis("Revenue ($k)", min: 0) .AsAnimated() .Series(s => s.AddColumn("Revenue", values)) .RenderToSvg(); } } ``` > **Best practice:** Call `.Fork()` at the beginning of each method that uses the injected `IChartBuilder`. This guarantees that previous calls haven't left stale series or axis configuration on the shared instance. **Unit test mock:** ```csharp // xUnit + NSubstitute public class DashboardServiceTests { [Fact] public void BuildRevenueSvg_ReturnsSvgContainingTitle() { var mockBuilder = Substitute.For<IChartBuilder>(); mockBuilder.Fork().Returns(ChartBuilder.Create()); // return real builder on Fork // … assert on the real ChartBuilder's output } } ``` --- ## ChartOptions — Raw Access For advanced scenarios you can read or mutate the underlying options directly: ```csharp ChartBuilder builder = ChartBuilder.Create() .Title("Revenue") .Size(700, 400); // Read current options (mutable reference): ChartOptions opts = builder.GetOptions(); opts.Title.Text = "Modified Title"; // affects the builder's state // Take an independent snapshot (deep copy): ChartOptions snapshot = builder.Build(); snapshot.Title.Text = "Snapshot Title"; // does NOT affect the builder ``` This is useful when you need to pass chart configuration to a custom `ISvgRenderer` implementation or serialise the options for caching. --- ## Multi-Target Compatibility The library targets `netstandard2.0`, `netstandard2.1`, `net6.0`, `net8.0`, and `net10.0`. | Feature | netstandard2.0 | netstandard2.1 | net6+ | |---|---|---|---| | All chart types | ✅ | ✅ | ✅ | | All builder methods | ✅ | ✅ | ✅ | | `RenderToStreamAsync` | ❌ | ❌ | ✅ | | `RenderToFileAsync` | ❌ | ❌ | ✅ | | `RenderToHtmlFileAsync` | ❌ | ❌ | ✅ | | `double?[]` and `double[]` overloads | ✅ | ✅ | ✅ | Async methods are guarded by `#if NET6_0_OR_GREATER` and are absent in lower-targeting builds. **`double?[]` nullable gap support:** A `null` in a data array is treated as a missing value — the chart skips that data point: ```csharp // The line breaks between Jan and Apr; Feb and Mar are not drawn. .AddLine("Sales", new double?[] { 120, null, null, 178, 210, 195 }) ``` **Output:** Two separate line segments — one covering Jan, then a gap, then Apr–Jun. Useful for representing missing data without introducing zero distortion. --- ## Custom SVG Renderer Implement `ISvgRenderer` to replace the rendering engine entirely (for testing, custom styling engines, etc.): ```csharp public class StubRenderer : ISvgRenderer { public string Render(ChartOptions options) => $"<svg><!-- {options.Title.Text} --></svg>"; } // Use it: var chart = ChartBuilder.Create(new StubRenderer()) .Title("My Chart") .RenderToSvg(); // → "<svg><!-- My Chart --></svg>" ``` This pattern is especially useful in unit tests where you want to verify that options are configured correctly without running the full renderer. ==================================================================== TerraFluent.Chart.Reporting — API Reference URL: https://terrafluent.dev/docs/chart/api-reference/ ==================================================================== # API Reference A complete method reference for every public builder, enum, theme, and data type in TerraFluent.Chart.Reporting. All fluent methods return the builder they were called on, so every call can be chained. Unless noted otherwise, methods validate their arguments and throw `ArgumentException` / `ArgumentOutOfRangeException` / `ArgumentNullException` on invalid input. > **Namespaces** > - Builders: `TerraFluent.Chart.Reporting.Builder` > - Models & data types: `TerraFluent.Chart.Reporting.Models` > - Enums: `TerraFluent.Chart.Reporting.Enums` --- ## Contents - [ChartBuilder](#chartbuilder) — the fluent entry point - [Series (ChartSeriesBuilder)](#series-chartseriesbuilder) — adding data series - [SeriesBuilder](#seriesbuilder) — per-series styling - [LegendBuilder](#legendbuilder) - [TooltipBuilder](#tooltipbuilder) - [Credits / branding](#credits--branding) - [AnimationBuilder](#animationbuilder) - [AnnotationBuilder](#annotationbuilder) - [LabelLayoutBuilder](#labellayoutbuilder) - [DataLabelOptions](#datalabeloptions) - [DonutCenterOptions](#donutcenteroptions) - [Enums](#enums) - [ChartTheme](#charttheme) - [ChartColor](#chartcolor) - [Data-point types](#data-point-types) - [IChartBuilder](#ichartbuilder) --- ## ChartBuilder `TerraFluent.Chart.Reporting.Builder.ChartBuilder` The fluent entry point for building a chart. Create one with `ChartBuilder.Create()`. ### Creating a builder | Method | Returns | Description | |---|---|---| | `ChartBuilder.Create()` | `ChartBuilder` | Creates a fresh builder with the default renderer. | | `ChartBuilder.Create(ISvgRenderer renderer)` | `ChartBuilder` | Creates a builder with a custom renderer implementation. | | `ChartBuilder.FromJson(string json)` | `ChartBuilder` | Rehydrates a builder from serialized `ChartOptions` JSON. *(net6.0+ only)* | ### Chart type (default for `Add`) These set the fallback type used by the generic `Series(s => s.Add(...))` method. Prefer the typed `AddLine`/`AddColumn`/… helpers instead. `AsPie()` · `AsLine()` · `AsArea()` · `AsColumn()` · `AsBar()` · `AsSpline()` · `AsScatter()` · `AsWaterfall()` · `AsGauge()` · `AsDataRing()` · `AsBubble()` · `AsHeatmap()` · `AsColumnRange()` · `AsAreaRange()` · `AsFunnel()` · `AsTreemap()` · `AsDumbbell()` · `AsStream()` · `AsGantt()` · `AsSankey()` ### Render mode | Method | Description | |---|---| | `AsStatic()` | Pure SVG — no CSS hover, no JS, no animation. Safe for PDF and email. Also disables animation. | | `AsAnimated()` | SVG + CSS hover + SMIL animation. No JavaScript. Ideal for Blazor and browser embedding. | | `AsInteractive()` | SVG + CSS + embedded JavaScript (tooltips, legend toggle, export, drill-down). Browser only. | ### Dimensions & background | Method | Description | |---|---| | `Size(int width, int height)` | Sets width and height in pixels. | | `Width(int? width)` | Sets width; pass `null` for responsive (`width="100%"`). | | `Height(int height)` | Sets height. | | `ResponsiveWidth()` | Clears the fixed width — SVG renders at `width="100%"`. | | `Responsive(bool responsive = true)` | Alias for `ResponsiveWidth()`. | | `Background(string color)` | Sets the chart background colour. | ### Theme & palette | Method | Description | |---|---| | `Theme(ChartTheme theme)` | Applies a built-in or custom theme. | | `Colors(params string[] colors)` | Overrides the series palette. | | `Colors(IEnumerable<string> colors)` | Overrides the series palette from any enumerable. | | `StackNormal()` | Stacks Column/Area/Bar series cumulatively. | | `StackPercent()` | Stacks series normalised to 100 %. | ### Title & subtitle | Method | Description | |---|---| | `Title(string text)` | Sets the title text. | | `Title(Action<ChartTitle> configure)` | Configures the title (text, alignment, style). | | `Subtitle(string text)` | Sets the subtitle text. | | `Subtitle(Action<ChartTitle> configure)` | Configures the subtitle. | ### Axes | Method | Description | |---|---| | `Labels(params string[] labels)` | Sets slice or X-axis category labels. | | `XAxis(Action<Axis> configure)` | Configures the X-axis. | | `XAxis(string title, params string[] categories)` | Sets X-axis title + category labels. | | `XAxisFormat(string format)` | X-axis tick-label format string, e.g. `"{value} kg"`. | | `XAxisTickInterval(double interval)` | X-axis tick interval in data units (> 0). | | `XAxisDateTime(IEnumerable<DateTime> values, string? format = null)` | Uses a datetime X-axis with auto tick thinning. | | `YAxis(Action<Axis> configure)` | Configures the primary Y-axis. | | `YAxis(string title, double? min = null, double? max = null)` | Sets Y-axis title + optional bounds. | | `YAxisFormat(string format)` | Y-axis tick-label format string. | | `YAxisTickInterval(double interval)` | Y-axis tick interval in data units (> 0). | | `YAxisLogarithmic()` | Switches the primary Y-axis to a logarithmic scale. | | `YAxisInverted(bool inverted = true)` | Inverts the primary Y-axis direction. | | `YAxis2(Action<Axis> configure)` | Configures the secondary (right-hand) Y-axis. | | `YAxis2Logarithmic()` | Logarithmic scale on the secondary Y-axis. | | `YAxis2Inverted(bool inverted = true)` | Inverts the secondary Y-axis. | | `GridLines(bool visible = true)` | Shows/hides plot grid lines. | | `HideGridLines()` | Hides plot grid lines. | ### Plot bands & lines | Method | Description | |---|---| | `PlotBand(double from, double to, string color = "rgba(68,170,213,0.15)", string? label = null)` | Adds a shaded reference band on the primary Y-axis. | | `PlotLine(double value, string color = ChartColor.Red, int width = 1, string? label = null, string? dashStyle = null)` | Adds a reference line on the primary Y-axis. | ### Legend & tooltip | Method | Description | |---|---| | `Legend(Action<LegendBuilder> configure)` | Configures the legend. See [LegendBuilder](#legendbuilder). | | `HideLegend()` | Hides the legend. | | `Tooltip(Action<TooltipBuilder> configure)` | Configures tooltips. See [TooltipBuilder](#tooltipbuilder). | | `DisableTooltip()` | Disables tooltips. | ### Credits / branding | Method | Description | |---|---| | `ShowCredits()` | Shows the fixed TerraFluent attribution label (`terrafluent.dev`). | | `ShowCredits(CreditsPosition position)` | Shows the label in a chosen corner. | | `HideCredits()` | Removes the attribution label. | The label is **shown by default** and links to `https://terrafluent.dev`. Its **text and link are fixed** (set by the library) so output cannot be re-branded — callers may only show/hide it and choose its corner. It always renders as text; the hyperlink is clickable only when the SVG is embedded **inline** in a page — rasterised (PNG/JPEG/PDF) or `<img>`-loaded output keeps the visible text but not the link. ### Animation | Method | Description | |---|---| | `Animate(int milliseconds = 800)` | Enables animation with the given duration. | | `Animation(Action<AnimationBuilder> configure)` | Configures animation timing and easing. | | `DisableAnimation()` | Disables load animations. | ### Series | Method | Description | |---|---| | `Series(Action<ChartSeriesBuilder> configure)` | Opens the series scope. See [Series](#series-chartseriesbuilder). | | `ClearSeries()` | Removes all series. | | `RemoveSeries(string name)` | Removes the first series matching `name`. | | `ShowDataLabels()` | Enables data labels on every series. | ### Export & interactivity (Interactive mode only) | Method | Description | |---|---| | `ShowExportButton(string label = "⬇ SVG")` | Adds an SVG download button. | | `ShowExportMenu(params string[] formats)` | Adds a multi-format export menu (`"SVG"`, `"PNG"`, `"JPEG"`, `"PDF"`). Omit for all four. | | `OnPointClick(string handler)` | Registers a JavaScript click handler for data points. | ### Labels, annotations & advanced layout | Method | Description | |---|---| | `LabelLayout(Action<LabelLayoutBuilder> configure)` | Controls X-axis label rotation, wrapping, skipping, scaling. See [LabelLayoutBuilder](#labellayoutbuilder). | | `Annotations(Action<AnnotationBuilder> configure)` | Adds free-form labels/lines/rects/circles. See [AnnotationBuilder](#annotationbuilder). | | `RangeSelector(Action<RangeSelectorOptions> configure)` | Adds the interactive navigator strip (Interactive mode). | | `SyncGroup(string groupId)` | Joins a synchronised-tooltip group shared by charts on the same page. | | `ShowDataTable(int rowHeight = 20, int fontSize = 10)` | Appends a data table of raw values beneath the chart. | | `ApplyTemplate(IChartTemplate template)` | Applies a preset template, overwriting only the settings it touches. | ### Accessibility & localization | Method | Description | |---|---| | `AriaLabel(string label)` | Overrides the accessible name (SVG `<title>` / `aria-label`). | | `AriaDescription(string description)` | Overrides the accessible description (SVG `<desc>`). | | `Culture(CultureInfo culture)` | Sets the culture for number formatting and the SVG `lang`. | | `Culture(string cultureName)` | Sets the culture by name, e.g. `"de-DE"`. | | `RightToLeft(bool rtl = true)` | Enables RTL text direction. | ### Data quality | Method | Returns | Description | |---|---|---| | `ThrowOnDataQualityErrors()` | `ChartBuilder` | Throws `DataQualityException` on render when an `Error`-severity issue is found. | | `AnalyzeDataQuality()` | `DataQualityReport` | Runs quality analysis without rendering. | | `GetLastDataQualityReport()` | `DataQualityReport?` | Returns the report from the most recent analysis or render (`null` before the first). | ### Render / output | Method | Returns | Description | |---|---|---| | `RenderToSvg()` | `string` | Renders and returns the SVG markup. | | `RenderToHtml(string? caption = null, string? cssClass = null)` | `string` | Renders a self-contained HTML `<figure>` fragment. | | `RenderToBytes()` | `byte[]` | Renders the SVG as UTF-8 bytes. | | `RenderToDataUri()` | `string` | Renders as a `data:image/svg+xml;base64,…` URI. | | `RenderToStream(Stream stream)` | `void` | Writes UTF-8 SVG bytes to a stream. | | `RenderToFile(string filePath)` | `void` | Writes the SVG to a file. | | `RenderToHtmlFile(string filePath, string? caption = null, string? cssClass = null)` | `void` | Writes the HTML fragment to a file. | | `RenderToStreamAsync(Stream stream, CancellationToken ct = default)` | `Task` | Async stream write. *(net6.0+ only)* | | `RenderToFileAsync(string filePath, CancellationToken ct = default)` | `Task` | Async file write. *(net6.0+ only)* | | `RenderToHtmlFileAsync(string filePath, string? caption = null, string? cssClass = null, CancellationToken ct = default)` | `Task` | Async HTML file write. *(net6.0+ only)* | ### Snapshot & variants | Method | Returns | Description | |---|---|---| | `Fork()` | `ChartBuilder` | Returns a new builder starting from a deep copy of the current configuration. | | `Clone()` | `ChartBuilder` | Alias for `Fork()`. | | `GetOptions()` | `ChartOptions` | Returns the live options object for advanced customisation. | | `GetSnapshot()` | `ChartOptions` | Returns an independent deep-copy snapshot. | --- ## Series (ChartSeriesBuilder) `TerraFluent.Chart.Reporting.Builder.ChartSeriesBuilder` Obtained inside `.Series(s => …)`. Each `Add*` method appends one series and returns the series builder so multiple series can be chained. Every method accepts an optional `Action<SeriesBuilder>? configure` to style that series (see [SeriesBuilder](#seriesbuilder)). ### Cartesian series (`double[]` or `double?[]`) `null` values in a `double?[]` are treated as gaps (see `NullGap`). | Method | Data | Notes | |---|---|---| | `AddLine(name, data, configure?)` | `double[]` / `double?[]` | Straight-segment line. | | `AddSpline(name, data, configure?)` | `double[]` / `double?[]` | Smooth Bézier curve. | | `AddArea(name, data, configure?)` | `double[]` / `double?[]` | Filled area. | | `AddColumn(name, data, configure?)` | `double[]` / `double?[]` | Vertical bars. | | `AddBar(name, data, configure?)` | `double[]` / `double?[]` | Horizontal bars. | | `AddScatter(name, data, configure?)` | `double[]` / `double?[]` | Dots only. | | `AddRadar(name, data, configure?)` | `double[]` / `double?[]` | Radar/polar area. | | `AddFunnel(name, data, configure?)` | `double[]` / `double?[]` | Funnel stages. | | `AddTreemap(name, data, configure?)` | `double[]` / `double?[]` | Proportional rectangles. | | `AddStream(name, data, configure?)` | `double[]` / `double?[]` | ThemeRiver stacked band. | | `AddPie(name, data, configure?)` | `double[]` / `double?[]` | Pie/donut slices. | | `Add(name, data, configure?)` | `double[]` / `double?[]` | Uses the builder's default chart type (`AsX()`). | ### Single-value series | Method | Data | Notes | |---|---|---| | `AddGauge(name, double value, configure?)` | `double` | Semi-circular dial. | | `AddDataRing(name, double value, configure?)` | `double` | Full 360° progress ring. | ### Structured-point series | Method | Data type | Notes | |---|---|---| | `AddWaterfall(name, data, totals?, configure?)` | `double?[]` + `bool[]` | `totals[i]=true` marks absolute/total bars. | | `AddBubble(name, data, configure?)` | `BubblePoint[]` | X, Y, and Z (size). | | `AddHeatmap(name, data, configure?)` | `HeatmapPoint[]` | Col/row/value grid. | | `AddColumnRange(name, data, configure?)` | `RangePoint[]` | Low–high vertical bars. | | `AddAreaRange(name, data, configure?)` | `RangePoint[]` | Low–high filled band. | | `AddDumbbell(name, data, configure?)` | `RangePoint[]` | Low–high dot pairs. | | `AddErrorBar(name, data, configure?)` | `RangePoint[]` | Error whiskers. | | `AddBoxPlot(name, data, configure?)` | `BoxPlotPoint[]` | Five-number summary boxes. | | `AddCandlestick(name, data, configure?)` | `OhlcPoint[]` | OHLC candles (up/down colours). | | `AddOhlc(name, data, configure?)` | `OhlcPoint[]` | OHLC bars. | | `AddParliament(name, groups, configure?)` | `ParliamentGroup[]` | Semicircular seat layout. | | `AddGantt(name, tasks, configure?)` | `GanttTask[]` | Horizontal task timeline. | | `AddSankey(name, nodes, links, configure?)` | `SankeyNode[]` + `SankeyLink[]` | Node-link flow diagram. | ### Computed overlays Derive a new series from existing data. | Method | Notes | |---|---| | `AddLinearRegression(name, sourceData, configure?)` | Least-squares trend line. | | `AddMovingAverage(name, sourceData, int period = 3, configure?)` | Simple moving average; first `period − 1` points are `null`. | | `AddExponentialSmoothing(name, sourceData, double alpha = 0.3, configure?)` | Exponentially-smoothed series. | --- ## SeriesBuilder `TerraFluent.Chart.Reporting.Builder.SeriesBuilder` Passed to the `configure` lambda of every `Add*` method to style that one series. ### Appearance | Method | Description | |---|---| | `Color(string color)` | Series colour (accepts a `ChartColor` constant or any CSS colour). | | `LineWidth(int px)` | Line/spline stroke width. | | `FillOpacity(double opacity)` | Area/column fill opacity `0.0`–`1.0`. | | `Solid()` · `Dashed()` · `Dotted()` · `DashDotted()` · `LongDashed()` | Line dash style shortcuts. | ### Fill (gradient & pattern) | Method | Description | |---|---| | `LinearGradientFill(int angleDegrees, params (double offset, string color)[] stops)` | Linear gradient fill. | | `RadialGradientFill(params (double offset, string color)[] stops)` | Radial gradient fill. | | `PatternFill(PatternKind pattern, string foreground, string? background = null, double size = 8)` | Hatched/dotted pattern fill. | | `Fill(SeriesFill fill)` | Applies a fully-configured `SeriesFill`. | ### Border | Method | Description | |---|---| | `Border(string color, int width = 1, int radius = 0)` | Sets border colour, width, and corner radius together. | | `BorderColor(string color)` · `BorderWidth(int px)` · `BorderRadius(int px)` | Individual border properties. | ### Markers | Method | Description | |---|---| | `MarkerSize(int radiusPx)` | Marker radius. | | `MarkerEnabled(bool enabled = true)` | Show/hide markers. | | `MarkerSymbol(MarkerSymbol symbol)` | `Circle`, `Square`, `Diamond`, `Triangle`, `TriangleDown`. | ### Thresholds & gaps | Method | Description | |---|---| | `Zone(double? upTo, string color)` | Colours the series up to a threshold value (`null` = to infinity). | | `Zones(params (double? upTo, string color)[] zones)` | Multiple threshold colour bands. | | `NullGap(GapPolicy policy)` | How `null` points render: `Break`, `Connect`, or `Zero`. | | `TargetLine(double value, string? label = null, string? color = null, string? dashStyle = null, int lineWidth = 1)` | Per-series target/reference line. | ### Visibility & legend | Method | Description | |---|---| | `Visible(bool visible = true)` · `Hide()` | Show or hide the series. | | `ShowInLegend(bool show = true)` · `HideFromLegend()` | Legend inclusion. | | `OnYAxis(int index)` · `OnSecondaryAxis()` | Bind the series to a specific Y-axis. | ### Pie / donut, labels & centres | Member | Description | |---|---| | `DonutHole(double fraction)` | Sets the donut hole size (0–1). | | `DonutCenter` | Property → [DonutCenterOptions](#donutcenteroptions). | | `DataLabel` | Property → [DataLabelOptions](#datalabeloptions). | | `Label(Action<DataLabelOptions> configure)` | Configure data labels via lambda. | | `ParliamentCenter` / `CenterLabel(Action<ParliamentCenterOptions>)` | Parliament centre caption. | | `HeatmapRowLabels` | `List<string>` of heatmap row labels. | ### Drill-down & insights | Method | Description | |---|---| | `WithDrilldown(ChartOptions childChart)` | Attach a child chart shown on click (Interactive). | | `WithDrilldown(int dataIndex, ChartOptions childChart)` | Attach a child chart to a specific point. | | `AutoInsight(Action<AutoInsightBuilder> configure)` | Auto-annotate peaks/troughs/trends. | --- ## LegendBuilder `Legend(l => …)` | Group | Methods | |---|---| | Visibility | `Disable()` | | Horizontal align | `Align(string)`, `AlignLeft()`, `AlignCenter()`, `AlignRight()` | | Vertical align | `VerticalAlign(string)`, `AtTop()`, `AtMiddle()`, `AtBottom()` | | Position shorthands | `TopLeft()`, `TopCenter()`, `TopRight()`, `BottomLeft()`, `BottomCenter()`, `BottomRight()` | | Offset | `Offset(int x, int y)`, `OffsetX(int)`, `OffsetY(int)` | | Layout | `Layout(string)`, `Horizontal()`, `Vertical()`, `Padding(int)`, `Margin(int)` | | Item text | `ItemStyle(string css)`, `ItemFontSize(int)`, `ItemFontColor(string)` | | Symbol | `SymbolSize(int w, int h)`, `SymbolRadius(int)` | | Border | `Border(string color, int width = 1, int radius = 0)`, `BorderColor(string)`, `BorderWidth(int)`, `BorderRadius(int)` | | Background | `BackgroundColor(string)` | --- ## TooltipBuilder `Tooltip(t => …)` *(active in Animated and Interactive modes)* | Group | Methods | |---|---| | Visibility | `Disable()` | | Background | `BackgroundColor(string)` | | Border | `Border(string color, int width = 1, int radius = 4)`, `BorderColor(string)`, `BorderWidth(int)`, `BorderRadius(int)` | | Text | `TextColor(string)`, `FontSize(int)`, `FontFamily(string?)` | | Layout | `Padding(int)`, `HideArrow()`, `NoShadow()` | | Content | `Format(string)`, `HeaderFormat(string)`, `PointFormat(string)` | | Value formatting | `ValuePrefix(string)`, `ValueSuffix(string)` | | Crosshair | `NoCrosshair()`, `CrosshairStyle(string color, int width = 1)` | | Behaviour | `EnableShared()`, `EnableFollowPointer()`, `TransitionDuration(double seconds)` | Format tokens: `{label}`, `{value}`, `{series}`. --- ## Credits / branding The chart carries a fixed **`terrafluent.dev`** attribution label (bottom-right by default). Its text and link are set by the library and cannot be changed, so output cannot be re-branded. Callers control only visibility and corner: ```csharp ChartBuilder.Create() .Series(s => s.AddColumn("Sales", data)) // shown by default; move it or remove it: .ShowCredits(CreditsPosition.BottomLeft) .RenderToSvg(); // Remove branding entirely: ChartBuilder.Create()....HideCredits().RenderToSvg(); ``` Renders in **all** modes (including `Static`), is script/CSS-free, and survives export as text. --- ## AnimationBuilder `Animation(a => …)` | Group | Methods | |---|---| | Duration | `Duration(TimeSpan)`, `Duration(int milliseconds)` | | Easing | `Linear()`, `EaseIn()`, `EaseOut()`, `EaseInOut()`, `Bounce()`, `Elastic()` | --- ## AnnotationBuilder `Annotations(a => …)` — coordinates are in data space (category index / axis value). | Group | Methods | |---|---| | Layout | `SmartLayout(Action<AnnotationLayoutOptions>? configure = null)` | | Shapes | `Label(x, y, text, configure?)`, `Line(x1, y1, x2, y2, configure?)`, `Rect(x1, y1, x2, y2, configure?)`, `Circle(x, y, radiusPx, configure?)` | | Placement helpers | `LabelAbove(x, y, text, offsetPx = 18, configure?)`, `LabelBelow(…)`, `LabelLeft(x, y, text, offsetPx = 8, configure?)`, `LabelRight(…)` | | Raw | `Add(Annotation annotation)` | --- ## LabelLayoutBuilder `LabelLayout(l => …)` — controls dense X-axis category labels. | Group | Methods | |---|---| | Rotation | `Rotation(int degrees)`, `AutoRotate(int maxDegrees = 90)`, `NoRotation()` | | Wrapping | `Wrap(int maxCharsPerLine = 12)`, `NoWrap()` | | Skipping | `Skip(int n)`, `AutoSkip(bool enabled = true)` | | Font scaling | `FontSizeRange(int min, int max)`, `NoAutoScale()` | | Collision | `EnableCollisionDetection()`, `DisableCollisionDetection()` | | Positioning | `Stagger(int offsetPx = 10)`, `NoStagger()`, `HorizontalPadding(int px)` | --- ## DataLabelOptions Accessed via `cfg.DataLabel` or `cfg.Label(dl => …)`. | Method | Description | |---|---| | `Show()` / `Hide()` | Enable/disable labels. | | `Format(string fmt)` | Format string, e.g. `"{value}%"`, `"${value}k"`. | | `Color(string color)` | Text colour. | | `FontSize(int px)` | Text size. | | `Background(string color)` | Pill background colour. | | `Radius(double fraction)` | Pie/donut: label radius (`> 1` places labels outside with a connector). | | `OffsetY(double pixels)` | Vertical nudge. | --- ## DonutCenterOptions Accessed via `cfg.DonutCenter` on pie/donut and data-ring series. | Method | Description | |---|---| | `Show()` / `Show(string customText)` / `Hide()` | Toggle the centre label. | | `Title(string title)` | Caption shown above the value. | | `Text(string text)` | Custom centre text (overrides the computed total). | | `FontSize(int px)` / `TitleFontSize(int px)` | Value / caption sizes. | | `Color(string color)` / `TitleColor(string color)` | Value / caption colours. | --- ## Enums `TerraFluent.Chart.Reporting.Enums` ### ChartType `Line`, `Spline`, `Area`, `Column`, `Bar`, `Pie`, `Scatter`, `Waterfall`, `Gauge`, `DataRing`, `Bubble`, `Heatmap`, `ColumnRange`, `AreaRange`, `Funnel`, `Treemap`, `Radar`, `BoxPlot`, `ErrorBar`, `Candlestick`, `Ohlc`, `Dumbbell`, `Stream`, `Gantt`, `Sankey`, `Parliament`. ### SvgMode `Static` · `Animated` · `Interactive`. ### Easing `Linear` · `EaseIn` · `EaseOut` · `EaseInOut` · `Bounce` · `Elastic`. ### MarkerSymbol `Circle` · `Square` · `Diamond` · `Triangle` · `TriangleDown`. ### CreditsPosition `BottomRight` (default) · `BottomLeft` · `TopRight` · `TopLeft`. ### AxisType `Linear` · `Logarithmic` · `DateTime`. ### Stacking `None` (side-by-side) · `Normal` (cumulative) · `Percent` (100 %). ### GapPolicy `Break` (visible gaps, default) · `Connect` (skip nulls) · `Zero` (plot nulls at zero). ### WarningSeverity `Info = 0` · `Warning = 1` · `Error = 2`. --- ## ChartTheme `TerraFluent.Chart.Reporting.Models.ChartTheme` ### Built-in presets (`static readonly ChartTheme`) | Preset | Style | |---|---| | `Default` | Blue-orange on white. | | `Dark` | Bright palette on midnight navy. | | `Pastel` | Soft palette on off-white. | | `Monochrome` | Greyscale on white. | | `Ocean` | Blue-teal on deep ocean. | | `Sunset` | Warm palette on purple-navy. | | `Forest` | Earthy greens on cream. | | `Neon` | Electric palette on near-black. | | `Minimal` | Muted Tableau-10 on white. | | `Warm` | Earth tones on parchment. | | `Arctic` | Cool blues on ice-blue. | | `Business` | Corporate blue-red on white. | | `Material` | Material Design 500 palette. | | `TrafficLight` | Green/amber/red status palette. | | `Accessible` | Colour-blind-safe (Wong 2011). | | `Vivid` | Full-spectrum distinct palette. | | `HighContrast` | WCAG AA (≥ 4.5:1) palette. | | `Modern` | Material palette on ghost-white, with `ModernStyle` enabled (see below). | ### Properties `BackgroundColor`, `PlotBackgroundColor`, `GridLineColor`, `AxisLineColor`, `TextColor`, `FontFamily`, `FontScale` (default `1.0`), `Colors` (`string[]`), `ModernStyle` (`bool`, default `true` — see [Modern Styling](/docs/chart/themes-and-styling/#modern-styling)), `TooltipBackground`, `TooltipTextColor`, `PositiveColor`, `NegativeColor`, `AccentColor`. ### Factory ```csharp ChartTheme.Custom( string? backgroundColor = null, string? plotBackgroundColor = null, string? gridLineColor = null, string? axisLineColor = null, string? textColor = null, string? fontFamily = null, string[]? colors = null, string? tooltipBackground = null, string? tooltipTextColor = null,string? positiveColor = null, string? negativeColor = null, string? accentColor = null); ChartTheme Clone(); // copy an existing theme (e.g. to tweak FontScale) ``` --- ## ChartColor `TerraFluent.Chart.Reporting.Models.ChartColor` 400+ named `const string` colour constants plus curated palettes and utilities. ### Series palette constants | Constant | Hex | Slot | |---|---|---| | `ChartBlue` | `#7CB5EC` | 1 | | `ChartOrange` | `#F7A35C` | 2 | | `ChartGreen` | `#90ED7D` | 3 | | `ChartYellow` | `#E4D354` | 4 | | `ChartIndigo` | `#8085E9` | 5 | | `ChartRose` | `#F15C80` | 6 | | `ChartTeal` | `#2B908F` | 7 | | `ChartRed` | `#F45B5B` | 8 | Also includes all standard CSS named colours (`Red`, `SteelBlue`, `ForestGreen`, …), pastel constants (`PastelSkyBlue`, …), and theme-specific tokens. ### Palette arrays (`ChartColor.Palette.*`) `Default`, `Dark`, `Pastel`, `Monochrome`, `Ocean`, `Sunset`, `Forest`, `Neon`, `Minimal`, `Warm`, `Arctic`, `Business`, `Material`, `TrafficLight`, `Accessible`, `Vivid` (each 20 colours), and `HighContrast` (16 colours). ### Utility methods | Method | Returns | Description | |---|---|---| | `FromRgb(int r, int g, int b)` | `string` | Hex from RGB components. | | `WithOpacity(string hex, double alpha)` | `string` | `rgba(…)` with the given alpha. | | `Lighten(string hex, double amount = 0.3)` | `string` | Lighter tint. | | `Darken(string hex, double amount = 0.3)` | `string` | Darker shade. | | `Mix(string hexA, string hexB, double weight = 0.5)` | `string` | Blend of two colours. | --- ## Data-point types `TerraFluent.Chart.Reporting.Models` — inputs for structured series. | Type | Constructor | Purpose | |---|---|---| | `BubblePoint` | `(double x, double y, double z)` | Bubble: position + size. | | `HeatmapPoint` | `(int col, int row, double value)` | Heatmap cell. | | `RangePoint` | `(double low, double high)` | ColumnRange, AreaRange, Dumbbell, ErrorBar. | | `BoxPlotPoint` | `(double low, double q1, double median, double q3, double high)` | Box-and-whisker summary. | | `OhlcPoint` | `(double open, double high, double low, double close)` | Candlestick / OHLC. | | `GanttTask` | `{ Name, Start, End, Color?, Label? }` | Gantt row. | | `ParliamentGroup` | `(string name, string color, int seats)` | Parliament seat block. | | `SankeyNode` | `{ Name, Color? }` | Sankey node. | | `SankeyLink` | `{ From, To, Value, Color? }` | Sankey flow (indices into the node list). | --- ## IChartBuilder `TerraFluent.Chart.Reporting.Builder.IChartBuilder` An interface mirroring the full `ChartBuilder` surface. Depend on `IChartBuilder` in your services for testable, decoupled code, and register the concrete builder in DI: ```csharp services.AddScoped<IChartBuilder>(_ => ChartBuilder.Create()); ``` All chaining, configuration, and render methods listed above are available on the interface. The async render methods (`RenderToStreamAsync`, `RenderToFileAsync`, `RenderToHtmlFileAsync`) are present only on `net6.0` and newer targets. --- See also: [Getting Started](/docs/chart/getting-started/) · [Chart Types](/docs/chart/chart-types/) · [Themes & Styling](/docs/chart/themes-and-styling/) · [Advanced Features](/docs/chart/advanced/) · [Troubleshooting & FAQ](/docs/chart/troubleshooting/) ==================================================================== TerraFluent.Chart.Reporting — Troubleshooting & FAQ URL: https://terrafluent.dev/docs/chart/troubleshooting/ ==================================================================== # Troubleshooting & FAQ Common questions, error messages, and rendering issues — with fixes. --- ## Rendering & display ### My chart shows no animation or hover effects Animation and CSS hover are only emitted in **Animated** or **Interactive** render modes. If you called `.AsStatic()` (or nothing, in a context where static was applied), you get a plain SVG. ```csharp .AsAnimated() // SMIL animation + CSS hover, no JS ``` ### The export menu / tooltips / legend toggle don't work Those features rely on embedded JavaScript, which is only produced in **Interactive** mode: ```csharp .AsInteractive() .ShowExportMenu() // now the menu is functional ``` Interactive output is browser-only. For PDF or email, use `.AsStatic()`. ### The chart renders but is blank / tiny inside a PDF or email PDF and email clients strip `<script>` and often ignore SMIL animation, so a chart that reveals itself via animation can appear empty. Use **Static** mode, which draws everything immediately: ```csharp .AsStatic() ``` ### My responsive chart exports as a 100 px thumbnail A responsive chart uses `width="100%"`. When you rasterise it (PNG/PDF), give it an explicit size instead: ```csharp .Size(800, 450) // fixed pixels for raster/PDF export ``` ### Fonts look different in an exported/shared image than on my page An SVG loaded as an image (`<img src=...>`) is isolated and cannot see your page's `@font-face` rules — it falls back to a system font. For inline SVG (injected into the DOM) the page fonts apply. Set an explicit, widely-available `FontFamily` on the theme if you need consistent typography in isolated images. ### X-axis labels overlap or get cut off Use `LabelLayout` to rotate, wrap, skip, or shrink dense labels: ```csharp .LabelLayout(l => l.AutoRotate(45).AutoSkip()) ``` Long horizontal **Bar** category labels widen the left gutter automatically; very long labels can still be wrapped with `.LabelLayout(l => l.Wrap(14))`. --- ## Data & series ### Some points are missing from my line/area `null` entries in a `double?[]` are treated as gaps. Control the behaviour with `NullGap`: ```csharp .AddLine("Sales", data, cfg => cfg.NullGap(GapPolicy.Connect)) // bridge the gap // GapPolicy.Break (default) = leave a gap; GapPolicy.Zero = plot at 0 ``` ### My waterfall's first bar is invisible or wrong Mark which bars are absolute totals with the `totals` array. The first bar is an absolute start bar (`0 → value`); intermediate bars are deltas; mark running totals with `true`: ```csharp .AddWaterfall("P&L", new double?[] { 500, 800, -320, 980 }, totals: new[] { true, false, false, true }); ``` If you omit `totals`, the first bar is treated as an absolute start and the last as a total. ### Stacking has no effect `StackNormal()` / `StackPercent()` apply to stackable types only: **Column**, **Bar**, **Area**, and **Line**. Stacking is ignored for Pie, Scatter, Gauge, etc. ### Scatter/bubble points look evenly spaced regardless of their X value Scatter is **category-indexed** — points are placed by their position in the series, not by a numeric X value. For a true numeric X position with a size dimension, use **Bubble** (`BubblePoint(x, y, z)`). ### Values render with the wrong decimal/grouping format Number formatting follows the chart culture. Set it explicitly: ```csharp .Culture("de-DE") // 1.234,56 ``` Coordinates in the SVG geometry always use invariant formatting; only displayed tick/label text is localized. --- ## Exceptions | Exception | Typical cause | Fix | |---|---|---| | `ArgumentOutOfRangeException` on `Size` | Width or height ≤ 0 | Pass positive pixel sizes, e.g. `Size(700, 400)`. | | `ArgumentException` on `YAxis` | `min` ≥ `max` | Ensure `min < max`. | | `ArgumentOutOfRangeException` on `Animate` | Duration ≤ 0 | Pass a positive millisecond value. | | `ArgumentOutOfRangeException` on `FillOpacity` | Value outside `0.0`–`1.0` | Clamp to that range. | | `ArgumentOutOfRangeException` on `MarkerSize` | Size ≤ 0 | Pass a positive radius. | | `ArgumentException` on `XAxisFormat` | Empty/whitespace format | Provide a non-empty format string. | | `ArgumentException` on `ChartTheme.Custom(colors: [])` | Empty palette array | Provide at least one colour, or pass `null`. | | `DirectoryNotFoundException` on `RenderToFile` | Target folder doesn't exist | Create the directory first. | | `DataQualityException` on render | `ThrowOnDataQualityErrors()` + an `Error` finding | Inspect `GetLastDataQualityReport()` and fix the data, or remove the guard. | ### How do I see data-quality warnings without throwing? ```csharp var report = ChartBuilder.Create() .Series(s => s.AddColumn("Sales", data)) .AnalyzeDataQuality(); // or GetLastDataQualityReport() after a render foreach (var w in report.Warnings) Console.WriteLine($"{w.Severity}: {w.Message}"); ``` --- ## Integration ### Blazor: my SVG shows as escaped text Render it as raw markup: ```razor @((MarkupString)ChartSvg) ``` ### ASP.NET Core: what content type should I return? ```csharp return Results.Content(svg, "image/svg+xml"); // Minimal API return Content(svg, "image/svg+xml"); // Controller ``` Prefer `.AsStatic()` for endpoints consumed by unknown clients. ### Dependency injection: how do I register the builder? ```csharp services.AddScoped<IChartBuilder>(_ => ChartBuilder.Create()); ``` Each `ChartBuilder.Create()` is independent and stateful, so use a transient/scoped lifetime — never a singleton shared across requests. ### Can I reuse one configured builder for several charts? Yes — build a base and `Fork()` (deep copy) per variant so mutations don't leak: ```csharp var baseChart = ChartBuilder.Create().Title("Revenue").Size(700, 400) .Series(s => s.AddLine("2025", data)); string light = baseChart.RenderToSvg(); string dark = baseChart.Fork().Theme(ChartTheme.Dark).RenderToSvg(); ``` --- ## Packaging & targets ### Which frameworks are supported? `netstandard2.0`, `netstandard2.1`, `net6.0`, `net8.0`, and `net10.0`. ### Why are the async render methods missing? `RenderToStreamAsync` / `RenderToFileAsync` / `RenderToHtmlFileAsync` are available only on **net6.0 and newer** targets. On `netstandard2.0`/`2.1`, use the synchronous `RenderToStream` / `RenderToFile`. ### Does the library pull in any third-party dependencies? No. The library uses only the .NET base class library — nothing else is added to your dependency graph. --- See also: [Getting Started](/docs/chart/getting-started/) · [Chart Types](/docs/chart/chart-types/) · [API Reference](/docs/chart/api-reference/) · [Advanced Features](/docs/chart/advanced/) <!-- BEGIN chart-type-guidance (generated by scripts/split_showcase.py) --> ==================================================================== TerraFluent.Chart.Reporting — Choosing a chart type URL: https://terrafluent.dev/chart/showcase/ ==================================================================== One page per chart type, each with a live SVG specimen and the C# that renders it. The guidance below is about fit: which type answers which question, and where each one misleads. ## Line Chart in C# URL: https://terrafluent.dev/chart/types/line/ Builder: AddLine Draw line charts in C# as self-contained SVG with TerraFluent.Chart.Reporting. One AddLine call, 18 themes, no JavaScript and no browser. MIT licensed. Use it when: - A continuous measure sampled at even intervals, where the reader's question is "which way is it going?" - Comparing the shape of two to five series that share one unit and a similar range. - Long category runs — a line stays readable at 50 points where columns turn into a picket fence. Use something else when: - Categories with no inherent order. Connecting Belgium to Denmark implies a trend that does not exist; use a column or bar chart. - A single data point per category, where the line's slope is the only thing drawn and it carries no meaning. - Series whose ranges differ by an order of magnitude — put the smaller one on a secondary axis or use a log scale, or it flatlines against the axis. Related: spline, area, column ## Spline Chart in C# URL: https://terrafluent.dev/chart/types/spline/ Builder: AddSpline Render smooth spline curves in C# with AddSpline — true Bézier interpolation, self-contained SVG, zero dependencies. Part of TerraFluent.Chart.Reporting. Use it when: - Data that genuinely varies continuously between samples — temperature, pressure, a physical signal. - Presentation charts where the eye should follow one overall shape rather than each segment. - Smoothed or modelled output, where the curve is already an interpolation and drawing it as one is honest. Use something else when: - Discrete measurements. A spline invents values between your points and can overshoot past the real maximum, which reads as data you never recorded. - Any chart where the exact value at a point matters — the curve does not pass through the sample as visibly as a straight segment does. - Sparse series. With four points a spline is mostly invention; use AddLine. Related: line, area, scatter ## Area Chart in C# URL: https://terrafluent.dev/chart/types/area/ Builder: AddArea Create filled area charts in C# with AddArea — gradient and opacity control, stacking, self-contained SVG output. Zero-dependency .NET charting library. Use it when: - A single series where the magnitude under the curve is part of the story — cumulative volume, total usage. - Stacked composition over time, when the total and its parts both matter. - A baseline of zero that is meaningful. The fill is a claim about the distance to zero. Use something else when: - Overlapping unstacked series beyond two or three — the fills occlude one another even at reduced opacity. - An axis that does not start at zero. The filled region then exaggerates a small difference into a large block of colour. - Comparing precise values between stacked bands; only the bottom band sits on a flat baseline, so the rest are hard to read. Related: line, stream, area-range ## Column Chart in C# URL: https://terrafluent.dev/chart/types/column/ Builder: AddColumn Generate column charts in C# with AddColumn — grouped, stacked, and 100% stacked, rendered as self-contained SVG with no JavaScript dependency. Use it when: - Comparing a value across a modest number of discrete categories — the default choice, and usually the right one. - {'Time buckets that are genuinely discrete': 'months, quarters, sprints.'} - Grouped or stacked composition where each category's parts sum to something meaningful. Use something else when: - More than about fifteen categories, or long category names — switch to a horizontal bar chart, which has room for the labels. - A truncated Y axis. Column length is the encoding, so a non-zero baseline misstates the comparison. - Continuous data sampled densely; use a line chart. Related: bar, line, waterfall ## Horizontal Bar Chart in C# URL: https://terrafluent.dev/chart/types/bar/ Builder: AddBar Draw horizontal bar charts in C# with AddBar — room for long category labels, natural for ranked lists. Self-contained SVG, MIT licensed, zero dependencies. Use it when: - Long category names. Horizontal bars give the label a full line of text instead of a rotated sliver. - Ranked lists — top products, slowest endpoints — sorted descending so the eye reads down the order. - Many categories. A bar chart scrolls vertically without the labels colliding. Use something else when: - Time on the category axis. Readers expect time to run left to right; use a column chart. - Two or three categories, where the horizontal layout wastes the page's width. - Negative and positive values you want compared precisely — the shared centre baseline is easier to read in a column chart. Related: column, dumbbell, funnel ## Pie Chart in C# URL: https://terrafluent.dev/chart/types/pie/ Builder: Add + .AsPie() Render pie charts in C# with AsPie and Add — outside labels with connector lines, legend placement, self-contained SVG. TerraFluent.Chart.Reporting, MIT. Use it when: - Parts of a single whole that genuinely sums to 100% — market share, budget allocation. - Three to six slices. Past that the small slices become indistinguishable wedges. - When "roughly half" or "about a quarter" is the takeaway, not a precise ranking. Use something else when: - Comparing slices of similar size. Humans compare angles badly; a bar chart makes the same comparison exact. - Values that do not sum to a whole, or that can be negative. - Change over time. Two pies side by side are far harder to read than one column or line chart. Related: donut, column, treemap ## Donut Chart in C# URL: https://terrafluent.dev/chart/types/donut/ Builder: Add + .AsPie() + .DonutHole() Build donut charts in C# with DonutHole and DonutCenter — a total in the middle, labelled slices around it, rendered as self-contained SVG. Use it when: - A part-of-whole breakdown where the total itself is worth stating — put it in the hole with DonutCenter. - Dashboard tiles, where the ring reads as a compact shape at small sizes. - The same cases as a pie chart, when the centre space is useful rather than empty. Use something else when: - The comparisons a pie chart is already bad at. Removing the centre makes angle judgement slightly harder, not easier. - Many slices — the ring is thinner than a pie, so small slices disappear sooner. - A single percentage. Use a data ring or a gauge, which are built for one value. Related: pie, data-ring, gauge ## Scatter Plot in C# URL: https://terrafluent.dev/chart/types/scatter/ Builder: AddScatter Create scatter plots in C# with AddScatter — marker shapes, borders, and regression overlays. Self-contained SVG from .NET, no JavaScript required. Use it when: - Looking for correlation, clusters, or outliers between two measures. - Distributions with many points, where individual markers show density that a line would hide. - Paired with AddLinearRegression when you want to state the trend as well as show it. Use something else when: - Few points. With six markers there is no distribution to see; a table or column chart says more. - Heavy overplotting without reduced opacity or smaller markers — a solid blob encodes nothing. - A third dimension crammed in by colour alone; use a bubble chart and encode it as size. Related: bubble, line, box-plot ## Waterfall Chart in C# URL: https://terrafluent.dev/chart/types/waterfall/ Builder: AddWaterfall Build waterfall and bridge charts in C# with AddWaterfall — running totals, absolute total bars, rendered as self-contained SVG. MIT licensed .NET library. Use it when: - Showing how a starting figure becomes an ending figure through named contributions — P&L bridges, variance analysis. - Headcount, inventory, or cash movement where additions and subtractions both matter. - Any narrative of the form "we started here, these things happened, we ended here". Use something else when: - Contributions that do not actually sum to the end value. The chart asserts they do. - More than about ten steps — the running baseline gets hard to follow. - Unordered categories. The sequence is the chart's argument, so it must be meaningful. Related: column, funnel, bar ## Gauge Chart in C# URL: https://terrafluent.dev/chart/types/gauge/ Builder: AddGauge Render semi-circular gauge charts in C# with AddGauge — one KPI against its range, as self-contained SVG. Embeds into PDF, HTML, and Word reports. Use it when: - One value against a known, meaningful range — SLA attainment, capacity used, a score out of 100. - Dashboard tiles where the reader needs "are we in the green?" at a glance. - Cases where the range boundaries carry as much meaning as the value. Use something else when: - A value with no natural maximum. Without a real ceiling the needle position is arbitrary. - Comparing several gauges. Five dials in a row is harder to read than one bar chart of the same five values. - Precise readings — a dial is a coarse instrument; print the number alongside it. Related: data-ring, donut, column ## Data Ring Chart in C# URL: https://terrafluent.dev/chart/types/data-ring/ Builder: AddDataRing Draw full-circle progress rings in C# with AddDataRing — one KPI per ring, ideal for dashboard tiles. Self-contained SVG, no JavaScript dependency. Use it when: - A single percentage or progress value, shown as a full 360° sweep. - KPI dashboard tiles — several rings side by side read as a consistent set. - Completion and attainment metrics where 100% is a real, reachable target. Use something else when: - Values that can exceed the maximum; the ring has nowhere to go past a full circle. - Multi-part breakdowns — that is a donut chart, not a progress ring. - Very small render sizes without a centre label. The ring alone is not precise enough to read. Related: gauge, donut, pie ## Bubble Chart in C# URL: https://terrafluent.dev/chart/types/bubble/ Builder: AddBubble Plot three variables at once in C# with AddBubble and BubblePoint — X, Y, and size. Self-contained SVG output, zero third-party dependencies. Use it when: - Three numeric measures per item where the third is a magnitude — revenue, population, spend. - {'Portfolio and quadrant views': 'two axes of position, one of weight.'} - Twenty to sixty items. Enough for a pattern, few enough that the bubbles do not merge. Use something else when: - A third variable that is not a magnitude. Area reads as "how much", so encoding a rate or an index misleads. - Precise comparison of the third value — area is judged poorly, so pair it with labels or a table. - Dense data. Overlapping bubbles hide each other far faster than scatter markers do. Related: scatter, heatmap, treemap ## Heatmap in C# URL: https://terrafluent.dev/chart/types/heatmap/ Builder: AddHeatmap Generate heatmaps in C# with AddHeatmap and HeatmapPoint — colour-scaled grids for matrices and correlations, rendered as self-contained SVG. Use it when: - A value across two categorical dimensions — hour by weekday, region by product. - Correlation matrices and confusion matrices, where the pattern matters more than each cell. - Spotting hot spots and gaps in a dense grid that a table of numbers would bury. Use something else when: - Precise comparison. Colour is the weakest quantitative encoding; add cell labels when the numbers matter. - Diverging data on a sequential palette — a scale that does not mark the midpoint hides the sign of the value. - Grids so large the cells fall below a few pixels, where nothing is distinguishable. Related: treemap, bubble, column ## Column Range Chart in C# URL: https://terrafluent.dev/chart/types/column-range/ Builder: AddColumnRange Draw floating low-high column ranges in C# with AddColumnRange and RangePoint — temperature and spread charts as self-contained SVG. Zero dependencies. Use it when: - A minimum and maximum per category — daily temperature range, price range, min/max latency. - Spreads where the gap itself is the message, not either endpoint alone. - Comparing the width of a range across categories. Use something else when: - Data with a meaningful zero baseline you want compared; a floating bar deliberately hides the distance to zero. - Distributions where the quartiles matter — use a box plot, which shows shape as well as extent. - Uncertainty around a central estimate; use an error bar, which draws the estimate too. Related: area-range, error-bar, box-plot ## Area Range Chart in C# URL: https://terrafluent.dev/chart/types/area-range/ Builder: AddAreaRange Render low-high confidence bands in C# with AddAreaRange and RangePoint — forecast ranges and uncertainty envelopes as self-contained SVG. Use it when: - Forecast and prediction intervals, usually with the central estimate drawn over the band as a line. - Min/max or percentile envelopes over a continuous axis. - Any "expected range" that should read as a region rather than a set of points. Use something else when: - Discrete categories — the band implies continuity between them. - More than two overlapping bands; the fills compound and nothing stays legible. - Showing the band without its central series, which leaves the reader no estimate to anchor on. Related: column-range, area, error-bar ## Funnel Chart in C# URL: https://terrafluent.dev/chart/types/funnel/ Builder: AddFunnel Build funnel charts in C# with AddFunnel — stage-by-stage conversion and sales pipelines rendered as self-contained SVG. MIT licensed, no dependencies. Use it when: - A sequential process where each stage is a subset of the one before it — signup to paid, lead to close. - Drop-off analysis, where the narrowing itself is the finding. - Four to seven stages. Fewer is a bar chart; more and the bottom stages vanish. Use something else when: - Stages that are not nested. If a step can gain items the funnel shape is a lie; use a Sankey diagram. - Precise comparison between adjacent stages — the tapering shape distorts width judgement. - Processes that branch. A funnel has one path; flows that split need a Sankey. Related: sankey, waterfall, bar ## Treemap in C# URL: https://terrafluent.dev/chart/types/treemap/ Builder: AddTreemap Render treemaps in C# with AddTreemap — proportional rectangles for part-of-whole with many categories. Self-contained SVG from .NET. Use it when: - Part-of-whole with too many categories for a pie chart — twenty or more still works. - Where relative magnitude across a wide dynamic range should be visible at once. - Space-constrained layouts; a treemap fills its rectangle completely. Use something else when: - Comparing similarly-sized items. Rectangles of different aspect ratios are hard to compare by area. - Negative or zero values, which have no area to draw. - Change over time — the layout reshuffles between periods and the reader loses track of items. Related: pie, heatmap, bubble ## Radar Chart in C# URL: https://terrafluent.dev/chart/types/radar/ Builder: AddRadar Create radar and spider charts in C# with AddRadar — multivariate profiles on a shared scale, rendered as self-contained SVG with zero dependencies. Use it when: - Comparing two or three items across five to eight attributes on one common scale. - Profile and capability comparisons, where the overall shape is the takeaway. - Normalised scores — ratings out of ten, percentages — where every axis means the same thing. Use something else when: - Axes with different units or ranges. The enclosed area becomes meaningless. - More than three overlaid series; the polygons occlude each other. - Precise reading. Values far from the centre occupy more area than values near it, which overstates them. Related: line, column, parliament ## Box Plot in C# URL: https://terrafluent.dev/chart/types/box-plot/ Builder: AddBoxPlot Draw box-and-whisker plots in C# with AddBoxPlot and BoxPlotPoint — median, quartiles, and whiskers as self-contained SVG. Zero-dependency .NET charting. Use it when: - Comparing the distribution — not just the average — of a measure across groups. - Latency and response-time analysis, where the spread and the tail are the point. - Showing skew and outliers that a mean would conceal. Use something else when: - An audience unfamiliar with the five-number summary; annotate it or use a simpler chart. - Small samples, where quartiles computed from a handful of values are noise. - Multimodal data — a box plot shows the same box for one cluster or two, so check the shape first. Related: error-bar, scatter, column-range ## Error Bar Chart in C# URL: https://terrafluent.dev/chart/types/error-bar/ Builder: AddErrorBar Add error bars in C# with AddErrorBar and RangePoint — confidence intervals and variance whiskers over columns, rendered as self-contained SVG. Use it when: - Measurements with known uncertainty — confidence intervals, standard deviation, measurement tolerance. - Overlaid on columns, so the reader sees both the estimate and how much to trust it. - Experimental results where overlapping intervals are the honest answer to "is this difference real?". Use something else when: - Charts where you have not defined what the bar represents. State whether it is SD, SE, or a CI. - Decorative use on data with no uncertainty model behind it. - Dense series, where the whiskers overlap into a hedge — use an area range band instead. Related: box-plot, column-range, column ## Candlestick Chart in C# URL: https://terrafluent.dev/chart/types/candlestick/ Builder: AddCandlestick Render financial candlestick charts in C# with AddCandlestick and OhlcPoint — open, high, low, close with up/down colouring. Self-contained SVG, MIT. Use it when: - Price series where each period's open, high, low, and close all matter. - Financial reporting and trading dashboards, where the candle body convention is already understood. - Twenty to a hundred and twenty periods — enough to see pattern, few enough that candles stay distinct. Use something else when: - Any data that is not genuinely OHLC. The body and wick have fixed meanings. - Long ranges — years of daily candles compress into an unreadable comb; aggregate to weekly first. - Audiences outside finance, for whom the convention needs explaining; a line of closing prices is clearer. Related: ohlc, line, column-range ## OHLC Chart in C# URL: https://terrafluent.dev/chart/types/ohlc/ Builder: AddOhlc Draw OHLC bar charts in C# with AddOhlc and OhlcPoint — high-low bars with open and close ticks, rendered as self-contained SVG from .NET. Use it when: - The same OHLC data as a candlestick, when you want the thinner, more traditional bar form. - Dense series — OHLC bars stay legible at narrower spacing than candle bodies do. - Print and greyscale output, where the open/close ticks survive without colour. Use something else when: - Small render sizes, where the open and close ticks fall below a pixel or two and disappear. - Readers who find the tick convention harder than candle bodies — candlesticks are more widely recognised. - Non-financial data, for the same reason as candlesticks. Related: candlestick, line, column-range ## Dumbbell Chart in C# URL: https://terrafluent.dev/chart/types/dumbbell/ Builder: AddDumbbell Build dumbbell and dot-plot charts in C# with AddDumbbell and RangePoint — before/after comparison per category as self-contained SVG. Use it when: - Two points in time per category, where the size of the change is the finding — before and after, plan and actual. - Comparing gaps across categories; the connector length is easy to compare at a glance. - Cases where two grouped columns would waste ink to say the same thing. Use something else when: - More than two values per category — a dumbbell has exactly two ends. - Series where the direction of change flips between categories without a colour cue; the reader cannot tell which dot is which. - Data where the absolute values matter more than the gap; grouped columns read those better. Related: column-range, bar, scatter ## Stream Graph in C# URL: https://terrafluent.dev/chart/types/stream/ Builder: AddStream Render stream graphs and ThemeRiver charts in C# with AddStream — stacked bands around a centred baseline, as self-contained SVG. Zero dependencies. Use it when: - Composition evolving over many time steps, where the overall silhouette tells the story. - Many series — a stream graph tolerates more bands than a stacked area chart before it fails. - Presentation and editorial contexts, where the organic shape is the point. Use something else when: - Reading exact values. No band sits on a flat baseline, so every one is judged by thickness alone. - Few time steps; with five points it is a lumpy stacked area chart. - Analytical work where the total must be read off the chart — the centred baseline hides it. Related: area, line, treemap ## Gantt Chart in C# URL: https://terrafluent.dev/chart/types/gantt/ Builder: AddGantt Generate Gantt charts in C# with AddGantt and GanttTask — task bars on a time axis, per-task colours, rendered as self-contained SVG for reports and PDFs. Use it when: - Project schedules and release plans, where overlap between tasks is what the reader is checking. - Resource and occupancy timelines — anything with a start, an end, and a name. - Embedding a schedule into a generated PDF or Word status report. Use something else when: - Dependency networks. A Gantt shows position, not "B cannot start until A finishes". - Very many tasks — past thirty rows it becomes a wall; group or filter first. - Plans that change hourly, where a rendered snapshot is stale before it is read. Related: column-range, bar, sankey ## Sankey Diagram in C# URL: https://terrafluent.dev/chart/types/sankey/ Builder: AddSankey Draw Sankey flow diagrams in C# with AddSankey, SankeyNode and SankeyLink — proportional ribbons between stages, as self-contained SVG. MIT licensed. Use it when: - Quantities flowing and splitting between stages — conversion funnels that branch, energy budgets, traffic sources. - Showing where volume is lost, and to what, in one picture. - Processes where a stage feeds more than one downstream stage; this is what a funnel cannot do. Use something else when: - Flows that do not conserve quantity. A Sankey asserts that what enters a node also leaves it. - Many nodes and links — ribbon crossings turn into a tangle past roughly a dozen links. - Precise value comparison; label the links when the numbers matter. Related: funnel, treemap, gantt ## Parliament Chart in C# URL: https://terrafluent.dev/chart/types/parliament/ Builder: AddParliament Render parliament seating charts in C# with AddParliament and ParliamentGroup — one dot per seat, grouped by party, as self-contained SVG. Use it when: - Election results and assembly composition, where the count of seats is discrete and countable. - Any whole made of individually meaningful units — committee seats, allocated slots. - Showing whether a group reaches a threshold such as a majority; the arc makes the halfway point visible. Use something else when: - Continuous proportions. If the units are not countable seats, a pie or bar chart is more honest. - Very large assemblies at small render sizes, where individual dots merge. - Comparing two compositions side by side; the arcs are harder to compare than paired bars. Related: pie, radar, treemap <!-- END chart-type-guidance -->