HtmlForgeX.CanopyX
2.2.0
dotnet add package HtmlForgeX.CanopyX --version 2.2.0
NuGet\Install-Package HtmlForgeX.CanopyX -Version 2.2.0
<PackageReference Include="HtmlForgeX.CanopyX" Version="2.2.0" />
<PackageVersion Include="HtmlForgeX.CanopyX" Version="2.2.0" />
<PackageReference Include="HtmlForgeX.CanopyX" />
paket add HtmlForgeX.CanopyX --version 2.2.0
#r "nuget: HtmlForgeX.CanopyX, 2.2.0"
#:package HtmlForgeX.CanopyX@2.2.0
#addin nuget:?package=HtmlForgeX.CanopyX&version=2.2.0
#tool nuget:?package=HtmlForgeX.CanopyX&version=2.2.0
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,downand 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, andSettings(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 withCanopyX.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 itscanopyx:filterevent, 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 saysRowHeader(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 replacedCanopyReportGridview setCanopyColumn.RowHeaderdirectly. - Row action.
CanopyReportGridviews 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 (CanopyXhrefColumn); 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 withview.RowActionorSettings(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; aCanopyDataGridusesSettings(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(...)andDataExplorerSettings.QuietTones(...)add the report quiet tones:Successbecomespass,upandsuccess,Infobecomesinfoandmaintenance.
What CanopyX does not offer yet, and HtmlForgeX works around:
- Enter on a row whose
rowActionislinkopens the details hook instead of following the row link. The explorer's details hook follows the link itself; a plainCanopyDataGridneeds CanopyX to make Enter run the row action (alinkrow 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
onLinkcallback), 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
startslot, its active filter chips inafterViews(after the grid's Views menu) and its row count and Export inend, 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 }, plusexport: falsewhen 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 (
actionswithrender): 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 (actionswithrun) 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, or12 / 52when 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 withoverflow: hiddendoes not cut them off. Browsers without the popover API get the same placement with fixed positioning, which escapesoverflow: hiddenbut not an ancestor with atransform,filterorcontain.
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 | Versions 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. |
-
.NETFramework 4.7.2
- CanopyX (>= 0.3.1)
- CanopyX.Html (>= 0.3.1)
- HtmlForgeX (>= 2.2.0)
-
.NETStandard 2.0
- CanopyX (>= 0.3.1)
- CanopyX.Html (>= 0.3.1)
- HtmlForgeX (>= 2.2.0)
-
net10.0
- CanopyX (>= 0.3.1)
- CanopyX.Html (>= 0.3.1)
- HtmlForgeX (>= 2.2.0)
-
net8.0
- CanopyX (>= 0.3.1)
- CanopyX.Html (>= 0.3.1)
- HtmlForgeX (>= 2.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.