HtmlForgeX.CanopyX 2.2.0

dotnet add package HtmlForgeX.CanopyX --version 2.2.0
                    
NuGet\Install-Package HtmlForgeX.CanopyX -Version 2.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HtmlForgeX.CanopyX" Version="2.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HtmlForgeX.CanopyX" Version="2.2.0" />
                    
Directory.Packages.props
<PackageReference Include="HtmlForgeX.CanopyX" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HtmlForgeX.CanopyX --version 2.2.0
                    
#r "nuget: HtmlForgeX.CanopyX, 2.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HtmlForgeX.CanopyX@2.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HtmlForgeX.CanopyX&version=2.2.0
                    
Install as a Cake Addin
#tool nuget:?package=HtmlForgeX.CanopyX&version=2.2.0
                    
Install as a Cake Tool

HtmlForgeX.CanopyX

HtmlForgeX.CanopyX hosts CanopyX tree grids, flat data grids and two-axis matrices in HtmlForgeX documents and reports. CanopyX owns the view (columns, records, nodes, lazy children, search, detail rows, keyboard and accessibility behaviour); this package places a view in the page and makes it part of the document.

using CanopyX;
using HtmlForgeX;

var view = new CanopyTreeView("gpo", "Group Policy objects");
view.Columns.Add(new CanopyColumn("name", "Name") { Sizing = CanopyColumnSizing.Preferred(320), Sortable = true });
view.Columns.Add(new CanopyColumn("links", "Links") { Kind = CanopyColumnKind.Number, Sortable = true });

var domain = new CanopyNode("corp.example", "corp.example");
var policy = new CanopyNode("corp.example/default-domain-policy", "Default Domain Policy");
policy.Cells["links"] = 1;
policy.Badges.Add(new CanopyBadge("changed", CanopyTone.Warning));
domain.Children.Add(policy);
view.Roots.Add(domain);

using var document = new Document();
document.Body.CanopyTreeGrid(view, grid => grid.Height(520));

For a flat inventory, map a typed report dataset into a grid definition:

var dataset = ReportDataset.Create("inventory", records, columns => columns
    .Text("id", "Record", record => record.Id, column => column.Key())
    .Number("size", "Size", record => record.Size));

var view = CanopyReportGrid.FromDataset(dataset, "inventory-grid");
document.Body.CanopyDataGrid(view, grid => grid.Height(480));
// Prefer grouped settings when configuring several options.
document.Body.CanopyDataGrid(view, grid => grid.Settings(s => s
    .Height(480).Density(CanopyGridDensity.Compact).StaticSnapshot(null, maxRows: 100).End()));

To retain a configured explorer's authored views, default filters/sorts and supported host settings, call CanopyReportGrid.FromExplorer(explorer, "records-grid") after its configuration callback. The returned view carries the full dataset; its default native query filters the initial result, and its named native preferences let the reader switch views after live updates. The native host retains authored height, fixed-height behaviour, viewport fitting, density, search, drawer availability and matching control labels. A drawer subtitle becomes a native header fact. View preferences live in the mounted page through CanopyX's public view-store contract. When every authored view leaves its search scope implicit, search follows the active native view's visible columns. Switching presets or changing visible columns updates the scope and reapplies a nonempty search through native state, preserving the active predicates, sorting, selection and scroll. Live row replacements use that same active scope.

Supported predicates are typed equality/membership, single-value inequality (including missing values), single-value text/category contains, numeric comparisons, and null checks. Unsupported predicates and query caps, selection, paging, differing per-view labels or explicit search scopes throw instead of being discarded. Any preset with locked Scope(...) filters is also rejected: an editable native query cannot preserve that boundary. Scoped reports use DataExplorer with document.UseCanopyXExplorers(); automatic native explorers retain scope support. Date predicates need the host's time policy. Relations, shortcuts, report-definition exports and host files remain explorer/host features.

An HTTP(S) report can replace an inline grid's rows from a same-origin server-sent event stream:

document.Body.CanopyDataGrid(CanopyReportGrid.FromDataset(dataset, "inventory-live"),
    grid => grid.LiveEvents("/api/report/events", dataset.Id).FitToViewport());

Send named dataset events with {datasetId, revision, rows}. rows is the complete native row array from CanopyDataGridView.ToJson(); an empty array clears the grid. Each grid receives only its exact, case-sensitive dataset identifier. Grids using the same resolved URL share one EventSource, including its ordinary same-origin authentication cookies. The browser rejects cross-origin URLs. The host owns access control and the endpoint; the library has no service or report policy.

The native live runtime validates rows and preserves the reader's sort, search, filters, surviving selection and scroll anchor without replacing the controller. Revisions can be monotonically increasing nonnegative safe integers or nonempty opaque strings of at most 512 characters. Numeric revisions reject stale snapshots. An opaque revision is deduplicated only against the currently accepted snapshot: empty → one → empty applies the final empty snapshot even when its hash occurred earlier. While an update runs, only the latest pending snapshot is retained. Shared cached snapshots follow SSE arrival order regardless of which grid finishes applying them first. Malformed rows leave the accepted rows and revision unchanged. Removing the last subscriber or leaving the page closes the connection; restoring the page reconnects. This hook accepts inline grids; hosted sources use CanopyX's source refresh API.

For a default page that has no mounted grid, add a hidden root subscription to the document's content:

document.Body.CanopyLiveEvents("/api/report/events");

CanopyLiveEvents registers only the transport asset. It needs no CanopyX runtime, grid or controller and takes no visible space. It shares the same connection with grids added or mounted later. Removing a grid leaves the root's connection available; removing the last root/grid subscription closes it. Page exit closes subscriptions and page restoration reconnects roots still in the document. Keep the root outside deferred grid pages when the overview needs summary events immediately.

While a live root or grid owns the shared connection, every valid dataset envelope also dispatches one document CustomEvent named hfx:canopyx-live-dataset. Its detail contains an isolated copy of the full payload, including additional host fields such as content. The event announces transport acceptance before native grid application; it does not mean the rows passed native validation or finished applying. It fires even for datasets with no matching grid, so a host can update other page components using the same connection. An envelope with rows: null still raises the event and does not replace grid rows. Host listeners cannot mutate pending grid rows through detail.

A live page can keep the rest of its content current on the same connection. Mark a view the page should update in place, and send newer renderings of it with CanopyLiveContent in the content of a dataset the page names:

// The page: the strip carries its key and source on its own root element; no wrapper changes the layout.
page.CanopyLiveRegion("now:figures", "page-live", region => AddFigures(region, model));
shell.BarItem(new CanopyLiveStatus("page-live") { Clock = serverNow });

// The host, for each newer model: the same builder, rendered by HtmlForgeX, plus navigation and provenance.
string content = new CanopyLiveContent("page-live")
    .Region("now:figures", region => AddFigures(region, newer))
    .Navigation(manifest)
    .Tab("now/overview", "4")
    .Provenance(generated, dataAsOf, clock: serverNow)
    .ToJson();

A region renders exactly one view. Unchanged content renders the page's own markup, so nothing is touched. Text and attribute changes update the existing elements and highlight the changed items (a figure, a row, a list item) with a short accent wash, shown statically under prefers-reduced-motion; a changed structure replaces the view, holding its height while page runtimes settle, and highlights only the new items. A quiet region (quiet: true) updates without highlights, for wording that follows the clock. Views inside chapters or deferred content catch up when shown. Navigation counts, second lines, chapter badges and provenance times follow the content's navigation, tabs and provenance. ReportKpi.Elapsed figures keep counting on the server's clock (CanopyLiveStatus.Clock, then each content's clock), in the host's duration wording.

CanopyLiveStatus is a quiet status for the chrome bar: connecting, live, updating (briefly after a change), stale (connected, but the provenance's data is older than its stale limit; the shell's stale badge follows) or disconnected (after a reconnect lasting more than a few seconds, or a closed stream). It is hidden in print. Add live regions and the status only to pages that are served live; files and print need neither. The transport reports its state as the document event hfx:canopyx-live-status with {url, state} (connecting, open, reconnecting, closed).

Content is trusted markup the host renders with CanopyLiveContent; it is inserted into the page, and scripts in it do not run, so live views must not depend on inline script. The clock is the server's current time, not the data time. Envelopes the page has seen before (a reconnect sends every dataset again) are not applied twice.

For a bounded summary panel, grid.Settings(s => s.Summary()) draws the grid without toolbar, column chooser, result line, status bar, paging footer, row selection, column menus, record drawer or frame, and fits its height to the rows; Summary(sorting: true) keeps sortable headers. CanopyStatusStrip.Period(...) and CanopyStatusStrip.Cell(...) fill CanopyX's status-strip presenter from report states (healthy quiet, degraded and down in their tones, no data neutral).

The key must be unique and nonempty. Raw numbers and dates remain typed; display text and report status tones are retained. The flat grid provides typed filters, stable sorting, selection, saved views, copying and explicit filtered, selected or all-result export scope. Its browser host hooks govern record details and Office exports. A host-backed definition supplies its data source before mounting and can pass an explicit accepted page to StaticSnapshot for script-free output.

An existing materialized report explorer can use the shared viewport while retaining HtmlForgeX's report controls and drawer:

document.Body.CanopyDataExplorer(dataset, explorer => explorer
    .Title("Inventory")
    .AddView("All records", view => view.Default())
    .Settings(settings => settings.Height(480).Exports(DataExplorerExports.All).SelectedRowExports()));

This host retains report views, quick filters, column formatting, related records and report definitions. Its export menu distinguishes filtered results, selected rows and all results. Column order, labels and sorting come from the accepted query; selected rows include identities outside the current filter. CSV and record copying work independently of virtual rows. Copying includes headers, quotes tabs and line breaks, protects text formulas and accepts at most 10,000 rows. DataExplorerExports.All enables CSV, record copying and definition copying. Office files use Settings(settings => settings.HostExports("/api/exports/{id}", "xlsx")) on an HTTP(S) host; the host writes the file.

The explorer accepts a unique, nonempty text, category or numeric key, or an explicit ReportRowIdentity.Ordinal for snapshot-local event rows. Keyed datasets order ties by their key using numeric comparison for numeric keys and ordinal comparison for text keys. Widths and pins the reader chose survive query changes; stable keyed selections do too. Columns on automatic sizing fit the new result again. Remote report sources continue to use DataExplorer; the CanopyX explorer accepts complete materialized datasets. A host removing a mounted explorer calls its browser controller's destroy() to release the viewport and drawer ownership.

To choose the viewport for every explorer in a document, configure the bridge once:

document.UseCanopyXExplorers(options => options.OnDiagnostic(d => log.WriteLine(d.ToString())));
document.Body.DataExplorer(dataset, explorer => explorer.AddView("All records"));

Each supported explorer uses the CanopyX grid. Other explorers retain their existing renderer. The callback receives one decision per explorer instance, including its id and the reason; repeated rendering does not repeat it. Routing checks the document's actual dataset delivery, so hosted endpoints, streamed or sidecar data and unsupported identities keep their source renderer. No per-explorer CanHost branch is needed.

View tabs show their independent row counts, such as Last 7 days · 214, for both viewport renderers. Counts include the view's authored filters, sort-independent grouping/distinct rules and limit; temporary reader filters do not rewrite another view's count. Settings(s => s.ViewCounts(false)) hides them. Unknown remote counts remain unlabelled.

Use CanopyReportGrid.CanHost(dataset, out reason) for an optional preflight diagnostic. Document-level UseCanopyXExplorers() above owns automatic renderer selection:

var eligible = CanopyReportGrid.CanHost(dataset, out var reason);
log.WriteLine(eligible ? "Dataset supports native hosting." : reason);

CanHost(dataset) returns only the decision. The diagnostic overload sets reason to null on success and otherwise explains the first incompatible key or column setting. Both CanopyDataExplorer and CanopyReportGrid.FromDataset enforce this same check. It accepts every current scalar column type and the history presenters, validates native column sizing and link targets, and requires at least one visible column. Datasets use explicit snapshot-local ordinal identity, or unique, nonblank text/category keys or finite numbers within +/-9,007,199,254,740,991. Numeric uniqueness uses the browser's invariant identity text; text keys are compared case-sensitively. Empty datasets are eligible when their key declaration and column configuration are valid. Blank dataset titles use the dataset identifier as the native grid's accessible label.

The check reads the current materialized dataset without changing it or building a cell snapshot. Recheck after changing column settings. It does not inspect remote sources, explorer views or later host options. Null datasets throw ArgumentNullException; diagnostic text is for people and is not a stable error code. For an inline grid's explicit key override, pass the same keyColumn to CanHost(dataset, out reason, keyColumn: "id") and FromDataset(dataset, "records-grid", keyColumn: "id"). Ordinal snapshots reject key overrides. The explorer uses the dataset's identity policy.

Services replay an exact selection with new ReportQuery().SelectKeys("record-A", "record-a"). Keys preserve case and whitespace, and an empty selection yields no records. The query's filters, sorting and paging then apply to those records. A source advertises SupportsRowKeys only when it enforces this identity contract; materialized datasets support it, while the database bridge rejects it before executing SQL. Host query limits bound the selected-key count as well as the request size. Ordinary category filters retain their existing comparison behavior.

Configure a date-time column with c => c.ShowSeconds() when readers need precise event times. This setting includes seconds in cells and searches; typed instants, sorting, filters and export precision stay the same. In CanopyDataExplorer, .LocalTime().ShowSeconds() uses the reader's browser time zone and follows the report's local/UTC switch. CanopyReportGrid.FromDataset(...) instead supplies per-column display text: its local text uses the generating machine's zone because CanopyX has a view-wide time zone. Changing the native time-zone control does not rewrite that snapshot text. Author CanopyDataGridView without text overrides when native formatting is needed.

CanopyX owns cell presentation, row density, column menus and filter popovers. The bridge maps the HtmlForgeX Graphite tokens onto the native grid and uses native link, identifier, long-text, boolean and status presenters. A row click opens the report's record (its row target, its report entity with RowEntity(type), or its drawer); without those it follows the row header's link when that column is a link (Enter does the same), otherwise it only focuses the row. A plain click never selects: row checkboxes, Space and the header's selection menu do. Enter, a double-click or the native Details action also open the record. The Grid views menu saves native layouts. The report's Save view entry saves replayable report definitions. Export offers the export scopes, report definitions and host files. Settings(s => s.Labels.GridExport = "Export grid") (used when the explorer has no exports of its own, so the grid keeps its Export) and Labels.GridViews localize those menu names. Explorer labels for columns, filters, details and export scopes are also passed to the native controls; advanced native editor labels keep CanopyX's defaults.

Use Settings(...) on both hosts for sizing and advanced options. CanopyDataExplorer reuses DataExplorerSettings; CanopyDataGridSettings wraps height, window fit (FitToViewport(minimum, maximum)), native density, row action (RowAction(CanopyGridRowAction.Details)), auto height (AutoHeight(false) keeps a fixed-height panel), row accents (RowAccents()), summary mode and static snapshots and returns the grid through End(). Existing root methods remain compatible. Empty states use Labels.Empty for an empty view, Labels.EmptyFiltered for no matches, and Labels.ClearFilters for the scoped clear action. Clearing retains the selected report view. Entity panels and tab panels initialize when shown; removing their roots releases their grids automatically. A new grid accepts its definition and first page before the old controller is destroyed.

Native column conditions become an exact-key snapshot in report exports and saved report definitions. CSV can cover the materialized result; service replay of those snapshots has the report protocol's 10,000-key limit. All-matching selection is captured against the accepted native query, with exclusions. Explicit selections survive report query changes; an all-matching selection is retired when that query changes. An explorer never hides its last visible column.

The data-explorer example builds one ReportPackage for single-file and bundle delivery, including portable compressed datasets and related certificate observations. Flat-grid runtimes include the matching native editor assets, so column menus, filter popovers and long-text dialogs also work without a network connection.

Existing DataTables tables can switch to CanopyX grids without changing the code that builds them:

document.UseCanopyXTables(options => options.OnDiagnostic(d => log.WriteLine(d.ToString())));

Each table is bridged when the grid can carry everything it uses (highlight rules become row and cell tones, exports become CanopyX exports, responsive priorities become column layout); otherwise it keeps DataTables and document.GetCanopyXTableDiagnostics() says why. See Docs/COMPONENT_REFERENCE_DATATABLES_CANOPYX.md.

Column data selectors that remap a field or use nested JavaScript access stay on DataTables. The diagnostic reports ColumnDataSource; strict mode rejects that table, including when the table explicitly requests the provider.

What the bridge adds:

  • Assets follow the document. The CanopyX runtime and stylesheet are inlined in a single file, written next to the page when the document saves its assets as files, and kept in a report bundle, always under CanopyX's content-hashed names (canopyx.<hash>.js), so a page, its runtime and its tree data come from one CanopyX build. They load once per page however many grids it holds. Checkbox, editable, movable, and context-action views select the complete interactive entry point for every grid on the page.
  • Theme. The grid takes its colours from the Evotec design tokens, so it matches the report in light and dark and follows the reader's theme choice.
  • Without script. A static outline of the tree (up to 200 rows by default, with a note about the rest) is shown where script cannot run. It is written in the light token colours (what email, Word and print keep) and follows the page's theme on screen through the same variables as the grid. Settings(s => s.StaticOutline(CanopyStaticOutline.Only)) renders only the outline, for documents that are printed, mailed or converted.
  • Status. status.ToCanopyTone() gives a badge or a cell the tone of a report status (critical, high, pass, not-evaluated, down and so on); the grid and the static outline colour it from the design tokens.
  • Time. Date columns follow the report's choice of the reader's time zone or UTC.
  • Language. Settings(s => s.Labels(l => ...)) sets the texts CanopyX writes itself (CanopyLabels: search box, loading and error rows, announcements, the outline's notes). They are the view's labels, so the grid and the outline are translated together; Settings(s => s.FullReport(address)) links a shortened outline to the full report.
  • Entity addresses. node.ReportEntity("ou", key) marks a row as a report entity, and Settings(s => s.EntityLinks()) replaces the grid's own link scheme with the report's: selecting the row writes #ou/<key>, and that address shows the page of the grid and opens the row. The bridge registers the scheme with CanopyX.configure (formatLink, parseLink) before it mounts the view.
  • Filters in the report's style. Settings(s => s.FilterBar(b => b.Choice("Changed", "Any time", "day", "week"))) draws the view's filters (CanopyFilter) as report chips above the grid instead of the grid's own switches: one switch per filter, and one choice for filters that are alternatives. The grid keeps the state (updateActiveFilters, getActiveFilters), and the chips follow its canopyx:filter event, also when the grid turns the filters off to show a row a search or an address leads to. A choice is a radio group (one tab stop, arrow keys); switching its option replaces its alternatives in one transaction and retains unrelated pending selections on query-backed data. Failed queries leave the chips on the accepted state. Rendering the bar leaves the view's filter definitions intact. A filter the author already hid keeps the author's own control.
  • Large trees. Settings(s => s.Data(CanopyTreeGridData.Sidecar)) moves the nodes into small script files next to the page when the document is saved with its assets as files; the page then loads children as the reader opens nodes, also from a file share.
page.CanopyTreeGrid(view, grid => grid
    .HeightOfWindow(70)
    .Settings(s => s
        .Frame(false)
        .Data(CanopyTreeGridData.Sidecar)
        .StaticOutline(CanopyStaticOutline.WithoutScript, maxRows: 100)
        .Labels(l => l.OverflowNote = "Pokazano {shown} z {total} wierszy.")));

This package replaces the jQuery FancyTree wrapper (FancyTree, FancyTreeNode) that HtmlForgeX shipped before. See MIGRATION.md in the repository for the mapping.

Column layout

CanopyX sizes flat-grid columns to their content, fills the width and decides what gives way when it runs short (see CanopyX docs/column-layout.md). HtmlForgeX passes the intent through under CanopyX's own names and does not size columns itself:

Host Where the options come from
CanopyDataGrid The hosted view's CanopyColumn options: ResponsivePriority, Pinned, RowHeader and Sizing. Choose CanopyColumnSizing.Content(...), Preferred(width, ...), Flex(weight, ...) or Fixed(width, minimum, maximum) to declare width, bounds and automatic measurement. The view also declares RowAction, AutoHeight and RowAccents, available through Settings(...).
CanopyReportGrid.FromDataset Each ReportDatasetColumn's layout (c => c.Priority(...), ResponsivePriority(n), Pinned(...), RowHeader(), MinimumWidth(px), MaximumWidth(px), Fixed(), Flex(w), AutoSize(...)), mapped onto CanopyColumn.Sizing, ResponsivePriority and pins. Width(px) supplies the preferred or fixed width.
CanopyDataExplorer The same ReportDatasetColumn layout, written into the page as the column's layout and forwarded to each grid the explorer creates.
DataTables bridge DataTables responsivePriority becomes CanopyX's numeric priority (the same scale: 0 and 1 essential, 10000 and above optional); in a Responsive table a column without one takes DataTables' default of 10000, except the identity column and in summary grids, and the all class keeps a column essential. The first shown column is the row header. FixedColumns edges become pins (one left column by default, as in DataTables; leftColumns: 0 unpins the identity column). A DataTables width is the preferred width.
CanopyTreeGrid Nothing to forward: CanopyX writes these options for flat grids only; trees keep PinTreeColumn.

Report-layer defaults, each overridable:

  • Row header. The column marked RowHeader(), otherwise the dataset's key column (unless it says RowHeader(false)), otherwise the first shown column, is the identity column: essential and pinned left. A hidden column is never the row header, because CanopyX's own fallback (the first column) could be one. Pinned(ReportColumnPin.None) or a priority on it changes that; on a replaced CanopyReportGrid view set CanopyColumn.RowHeader directly.
  • Row action. CanopyReportGrid views open the record details on a row click, or follow the row header's link when it is a link column; the explorer opens its row target, its report entity (RowEntity) or its drawer, and follows the row header's link when it opens none of them. A link-text row header (c.Link(targetColumn)) shows the row's name and links to the target column's address (CanopyX hrefColumn); an address that is a report entity opens the shell drawer instead of navigating to the bare fragment. Summary grids open nothing. Bridged tables open the record details only in Responsive tables (not summaries), where CanopyX's drawer replaces DataTables' child rows. Change it with view.RowAction or Settings(s => s.RowAction(...)).
  • Row accents stay off. The bridge turns them on only for a table whose highlight rules colour whole rows, the author's explicit request; Settings(s => s.RowAccents()) turns them on for any grid.
  • Height. CanopyX's auto height is kept: a grid whose rows all fit is as tall as its rows. A fixed-height explorer (FixedHeight()) turns it off; a CanopyDataGrid uses Settings(s => s.AutoHeight(false)). FitToViewport(minimum, maximum) on the explorer settings or the grid settings makes the bound the room down to the window's bottom (20 px above it, the rule of the report layout's fitted lists) instead of a fixed number of pixels.
  • Quiet tones. A toned column's enum presenter always lists CanopyX's neutral tone in quietTones, so values without a tone are plain muted text, never neutral chips. ReportDatasetColumn.QuietTones(...) and DataExplorerSettings.QuietTones(...) add the report quiet tones: Success becomes pass, up and success, Info becomes info and maintenance.

What CanopyX does not offer yet, and HtmlForgeX works around:

  • Enter on a row whose rowAction is link opens the details hook instead of following the row link. The explorer's details hook follows the link itself; a plain CanopyDataGrid needs CanopyX to make Enter run the row action (a link row action following the first row link on Enter, with Ctrl+Enter kept for details).
  • An enum or state presenter has no way to draw a value without a tone in the normal text colour: quiet tones are muted text, so untoned values are muted too. A presenter option such as untoned: "text" would draw them as plain cell text. Quiet text also starts at the cell edge while pills are inset by their padding, so quiet and chipped values of one column do not line up; the quiet text could take the pill's inline padding.
  • Link cells carry no hook for a host's link handling (an attribute or an onLink callback), so HtmlForgeX finds entity links by their resolved address in a document-level click handler.

Embedded grids inside a report shell take the shell's scrollbar tokens (--hfx-scrollbar-* set as CanopyX's --cx-scrollbar-* on the grid host), so light, dark and a runtime theme switch match the report.

Status history and sparklines

Give a report column ordered ReportCellPeriod values to show how each record changed. StatusHistory(...) draws a status strip. Sparkline(...) draws bars by default; c.Sparkline(ReportSparklineStyle.Line) selects a line. Supply periods oldest first. Sparklines emphasise the latest sample at the right end. A null numeric sample represents a gap.

Compact histories use CanopyX's ranked semantic palette so downsampled intervals retain their worst result: passes and up states are success; high or critical failures and down states are danger; other failures, degraded states and collection failures are warning; informational passes, recovering and maintenance are info; skipped, unknown and unobservable results are neutral. The period's state text remains available in its tooltip.

var rows = new[] {
    new {
        Number = 10,
        Name = "Collection A",
        History = new[] {
            new ReportCellPeriod("Earlier", state: "Passed", status: ReportStatus.Pass),
            new ReportCellPeriod("Latest", state: "Needs review", status: ReportStatus.Fail(ReportSeverity.Low))
        },
        Samples = new[] { new ReportCellPeriod("Earlier", value: 12), new ReportCellPeriod("Latest", value: 18) }
    }
};
var dataset = ReportDataset.Create("collection-history", rows, columns => columns
    .Number("number", "Record number", row => row.Number, c => c.Key().Width(112).Fixed().Priority(ReportColumnPriority.Optional))
    .Text("name", "Collection", row => row.Name, c => c.RowHeader().MinimumWidth(160).MaximumWidth(300).Flex(2))
    .StatusHistory("history", "Recent results", row => row.History, c => c.Priority(ReportColumnPriority.Essential))
    .Sparkline("count", "Items collected", row => row.Samples, c => c.Sparkline(ReportSparklineStyle.Line).Priority(ReportColumnPriority.Optional)));

using var document = new Document { LibraryMode = LibraryMode.Offline };
document.Body.CanopyDataExplorer(dataset, explorer => explorer
    .Title("Records over time").Settings(s => s.Height(480)));

Sorting, filtering, searches and CSV exports use the latest state or latest non-null numeric sample. The visual periods stay alongside that scalar value in the dataset. CanopyReportGrid.FromDataset(...) carries them into a native grid too. Width(...).Fixed() fixes a column's automatic width; MinimumWidth(...), MaximumWidth(...) and Flex(...) bound a column and give it a share of spare room. Keep history essential and secondary columns optional when readers need the history on a phone.

Matrices and report entity drawers

Define the axes, grouping, states, symbols, values and native details with CanopyX.CanopyMatrix. Add it to the document with container.CanopyMatrix(view, configure). The HtmlForgeX component supplies document assets, theme integration, height settings, static output and links to report entities.

var view = new CanopyX.CanopyMatrix("collection-quality", "Collection quality")
    .State("review", "Needs review", CanopyX.CanopyTone.Warning, CanopyX.CanopyMatrixSymbol.Warning, attention: true)
    .State("pass", "Passed", CanopyX.CanopyTone.Success, CanopyX.CanopyMatrixSymbol.Check);
view.Rows.Title = "Collection";
view.Rows.Attribute("group", "Group", facet: true);
view.Rows.Item("a", "Collection A", item => item.Value("group", "First group"));
view.Rows.GroupBy = "group";
view.Columns.Title = "Check";
view.Columns.Item("coverage", "Coverage");
var cell = view.Set("a", "coverage", "review").Detail("Evidence", "Two items need review.");
cell.Value = 2;

using var document = new Document { LibraryMode = LibraryMode.Offline };
document.Body.ReportShell(shell => {
    shell.Title("Collection quality");
    shell.AddPage("Quality", page => page.CanopyMatrix(view, matrix => matrix
        .Settings(s => s.Height(480).AutoHeight().HideEmpty().StaticSnapshot(200).End())
        .CellEntity("a", "coverage", "observation", "a-coverage")));
    shell.AddEntityPanel("observation", "a-coverage", "Collection A · Coverage",
        panel => panel.Text("Two items need review."));
});

Expand a group to inspect its collections. Activating the mapped collection cell opens the report entity drawer; unmapped cells and group summaries keep CanopyX's details behaviour. View exposes the native definition when it needs further configuration. AutoHeight() fits short matrices within the Height(...) cap, and HideEmpty() initially hides axes whose visible cells lack data.

new HtmlForgeX.CanopyMatrix(view).StaticHtml(maxRows: 200) returns CanopyX's script-free snapshot for email, print or PDF composition. StaticSnapshot(200) limits the component's authored no-script and print snapshot. These authored snapshots use declared axis order; reader grouping and filters do not rewrite them.

Change views: diffs, change lists and propagation

CanopyX's change views show what changed, how and when: CanopyX.CanopyTextDiff (inline or side by side, word marks, folded regions, copy-ready unified diff), CanopyX.CanopyChangeList (Added, Removed, Modified and Renamed badges, folders, hashes before and after, risk flags with reasons, details with diffs and host actions) and CanopyX.CanopyPropagation (each replica's state, first and last sightings and delays). Add them with container.CanopyChangeList(view), container.CanopyTextDiff(view) and container.CanopyPropagation(view). The components supply document assets, the HtmlForgeX theme and a static snapshot for print projections and pages without scripts (StaticSnapshot(rows)); StaticHtml(rows) returns the same script-free HTML for email. The change-list snapshot lists the items without their diffs; a mounted view prints its own layout instead, diffs included. CanopyX never acts on an item: the page handles canopyx:changeAction for host actions, and copy actions copy their value themselves.

var list = new CanopyX.CanopyChangeList("file-changes", "File changes")
    .Action("copy-path", "Copy path", CanopyX.CanopyChangeActionIcon.Copy, copy: "path")
    .Action("acknowledge", "Acknowledge", CanopyX.CanopyChangeActionIcon.Check);
var item = list.Add("payload", CanopyX.CanopyChangeKind.Added, "src/app/startup.exe");
item.Flag("unsigned", "Unsigned", "The file has no embedded signature.", CanopyX.CanopyTone.Danger);
page.CanopyChangeList(list);

Runnable examples

The examples project includes two reports with synthetic data and embedded offline assets:

  • CanopyM1HistoryDemo: numeric record keys, status history, bar and line sparklines, a missing sample, identifier and date/time columns, and responsive column sizing.
  • CanopyM1MatrixDemo: grouped axes, state symbols, cell values, empty-axis hiding and evidence in report entity drawers.

From the repository root, pass the output directory explicitly:

dotnet run --project HtmlForgeX.Examples -- canopyx-m1 "./artifacts/canopyx-m1"

This writes CanopyM1History.html and CanopyM1Matrix.html to that directory. Relative paths resolve from the caller's working directory. Use the report's display settings to switch between light and dark themes.

Report explorer toolbar

A CanopyDataExplorer has one toolbar, built with CanopyX's toolbar hooks (reporting runtime) rather than by moving grid elements:

  • Slots. The explorer's quick views keep their own row above the grid. Its search sits in the grid toolbar's start slot, its active filter chips in afterViews (after the grid's Views menu) and its row count and Export in end, the start of the right-hand group. CanopyX lays the slots out with its own controls, keeps them through re-renders and the phone layout, and each replacement grid takes the explorer's controls into its own slots.
  • Switches. The grid is created with toolbar: { count: false }, plus export: false when the explorer offers its own Export, so every control appears once. The explorer's Export also offers report definitions and host-rendered files; without explorer exports the grid's Export (Labels.GridExport) stays.
  • More menu. Save view is a content entry of the grid's more (⋯) menu (actions with render): it opens a name field and a Save button styled as grid controls; Escape folds it back, saving closes the menu. Reset is an entry that runs (actions with run) and returns to the current view without the reader's search and filters.
  • Phone. On a phone (CanopyX's cx-phone, a grid 640 px wide or less) the toolbar follows CanopyX's phone pattern: the search takes its own row, and Filters, Columns, the row count and the more menu share one compact row. The count shows its short form (data-short: 52, or 12 / 52 when filters narrow the rows) while its text keeps the full sentence for assistive technology, and the explorer's Export moves into the more menu as its first entry, as CanopyX moves its own Views and Export there. Export then opens as a sheet of its own that covers the more sheet (one drag handle, a Back action, Labels.Back); Escape or Back returns to the more sheet with focus on its Export entry, and the covered entries are inert meanwhile. Widening the grid brings Export back to the toolbar as a popover.
  • Layered menus. The grid's menus open in the browser's top layer. The explorer's Export panel does the same: the CanopyX bridge sets explorer.layeredMenus, and the explorer then opens its menu panels as manual popovers placed by their button and kept inside the viewport (hfx-data-explorer.menus.js), so a one-row grid or a host with overflow: hidden does not cut them off. Browsers without the popover API get the same placement with fixed positioning, which escapes overflow: hidden but not an ancestor with a transform, filter or contain.

The Columns menu is CanopyX's (find, drag or Alt+Up/Down to reorder, Show all, Reset columns; it may show "Loading…" while its editor chunk arrives), so the explorer's headers carry no move handles. Which columns are out of view, and why, comes from CanopyX's getColumnLayout() (visible and hiddenReason: user, priority or phone); the report's own columns are the ones the reader did not hide, so a column that gives way to the width or the phone layout stays in the report's exports and saved views.

OfficeIMO document exports

A flat grid opts into Excel and PDF writers through grid.Settings(settings => settings.OfficeIMOExports()). The document export toolbar offers filtered, selected and all-result scopes with progress and cancellation. CanopyX owns query and selection capture; OfficeIMO consumes that immutable record capture, preserving scalar values, resolved display text and mapped report tones. Document exports use the displayed text by default. Set OfficeIMOExports(rawValues: true) to allow original scalar values, including values changed by a display formatter.

For the DataTables bridge, select document.UseCanopyXTables(options => options.Exports(CanopyXTableExports.OfficeIMO)). This carries configured Excel/PDF buttons and typed Excel layout to scoped exports. The bridge honors settings.ExportRawValues() for the same explicit raw-value choice. The default NativeSubstitutes policy remains unchanged. Native callbacks and DataTables row/column selectors keep a table on DataTables rather than silently widening its export. Print continues through the report's print version.

Each document button has its own CanopyX format provider, so two Excel or PDF buttons keep their own filename, title and layout. These providers stay out of CanopyX's native Export menu; its CSV and Copy actions remain available. CanopyX exportAs owns capture, cancellation, timeout and download for both toolbar and programmatic exports. A single button of a format uses the provider ID xlsx or pdf. Repeated formats use hfx-xlsx-<index> or hfx-pdf-<index>, where the index is the zero-based document-button order. The unqualified format ID is unavailable when it would be ambiguous. Programmatic calls choose their own scope and values through the CanopyX request options.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.2.0 167 10/24/2026
2.0.0 130 10/19/2026