Data grid
Typed sorting and filters, facets, grouping with aggregates, pinned columns, saved views, editing and export.
18 demos
Typed sorting and filters, facets, grouping with aggregates, pinned columns, saved views, editing and export.
Orders grouped by region and status with sum, average, count and distinct-count in the headings and totals; each order expands into a nested grid of its lines.
Every editor type, validation from C# and from the page, read-only rows, undo and redo, and a batch save that the service answers per row.
Row highlights, tones, a data bar on CPU, a colour scale on latency, threshold bands and a stale cue, with the legend and the rule editor.
Category, priority, team and region facets with counts, and histograms for dates and age; the facets filter each other and the grid.
Months under quarters under half years, the budget line pinned left, cell ranges with copy, paste and the fill handle.
Eight columns per row and about sixty fields per record in tabs and sections, with related records, a timeline of changes and deep links.
Rows change in place from a stream with flashing cells and an order-changed prompt, and a job queue the grid polls with revision-aware refresh.
A site picker filters a grid and a hosted grid and highlights a tree; the grid's selection marks the picker.
Matches marked in place with a count and next and previous, and the Layout menu with fit, scroll and card layouts in three densities.
The page's own writers join the Export menu after CSV, with the scope picker, progress and cancellation.
Enum pills in every tone, plain values beside them, and link rows a host can route itself.
Columns from member expressions, so renaming a member changes the grid or fails the build, and rows straight from the records.
The French label pack on a reporting grid; Polish, German, Spanish and Italian packs are included too.
Records grouped by month, with facets that count by month and by amount range, including the empty values.
Amount, tax and a per-unit price computed from other columns; a division by zero shows an error marker instead of a number.
A grouped, formatted cost report that exports to Excel and PDF in the browser, without a server.
Work items as cards built from the grid definition, with the same search, filters, sorting and export as rows.
Build one like it
The smallest complete version, in each language CanopyX supports for this view. The demo above is the same API with more data.
using CanopyX;
using CanopyX.Html;
var devices = new CanopyDataGridView("devices", "Device inventory") {
Selection = CanopySelectionMode.Multiple,
StatusBar = true,
Noun = "devices",
Reporting = new CanopyGridReporting {
Grouping = new CanopyGridGrouping("site"),
HeaderFilters = true
}
};
devices.Columns.Add(new CanopyColumn("device", "Device") {
Sortable = true,
Presenter = CanopyGridPresenter.Identifier(mono: true)
});
devices.Columns.Add(new CanopyColumn("site", "Site") { Sortable = true });
devices.Columns.Add(new CanopyColumn("os", "Operating system") { Sortable = true });
devices.Columns.Add(new CanopyColumn("patch", "Patch level") {
Kind = CanopyColumnKind.Number,
Sortable = true,
Presenter = CanopyGridPresenter.Progress()
});
devices.Columns.Add(new CanopyColumn("encrypted", "Encrypted") {
Sortable = true,
Presenter = CanopyGridPresenter.Boolean()
});
devices.Reporting.FacetColumns.Add("site");
devices.Reporting.FacetColumns.Add("os");
devices.Reporting.Aggregates.Add(new CanopyGridAggregate(
"patch-average", "patch", CanopyGridAggregateOperation.Average,
"Average patch level"));
foreach (var (name, site, os, patch, encrypted) in new[] {
("WS-WAW-0001", "Warsaw", "Windows 11 24H2", 96, true),
("WS-WAW-0002", "Warsaw", "Windows 10 22H2", 61, false),
("SRV-WAW-0003", "Warsaw", "Windows Server 2025", 88, true),
("WS-LON-0004", "London", "Windows 11 24H2", 100, true),
("WS-LON-0005", "London", "Windows 11 23H2", 74, true),
("SRV-BER-0006", "Berlin", "Windows Server 2022", 58, true)
}) {
var row = new CanopyGridRow(name.ToLowerInvariant());
row.Cells["device"] = name;
row.Cells["site"] = site;
row.Cells["os"] = os;
row.Cells["patch"] = new CanopyCell(patch) {
Text = $"{patch}%",
Tone = patch < 70 ? CanopyTone.Danger : CanopyTone.None
};
row.Cells["encrypted"] = encrypted;
devices.Rows.Add(row);
}
devices.SaveHtmlPage("inventory.html");Run this snippet (data grid, C#)Its output, generated with the site.
using CanopyX;
using CanopyX.Html;
var grid = new CanopyDataGridView<Device>(
"devices", "Device inventory", x => x.Name);
grid.Column(x => x.Name, "Device", c => {
c.Sortable = true;
c.Presenter = CanopyGridPresenter.Identifier(mono: true);
});
grid.Column(x => x.Site, configure: c => c.Sortable = true);
grid.Column(x => x.Os, "Operating system", c => c.Sortable = true);
grid.Column(x => x.Patch, "Patch level",
c => {
c.Sortable = true;
c.Presenter = CanopyGridPresenter.Progress();
},
cell: patch => new CanopyCell(patch) {
Text = $"{patch}%",
Tone = patch < 70 ? CanopyTone.Danger : CanopyTone.None
});
grid.Column(x => x.Encrypted,
configure: c => c.Presenter = CanopyGridPresenter.Boolean());
grid.Field(x => x.Owner);
grid.View.StatusBar = true;
grid.View.Reporting = new CanopyGridReporting {
Grouping = new CanopyGridGrouping(grid.ColumnId(x => x.Site))
};
grid.View.Reporting.FacetColumns.Add(grid.ColumnId(x => x.Os));
grid.AddRows(new Device[] {
new("WS-WAW-0001", "Warsaw", "Windows 11 24H2", 96, true, "anna.kowalska"),
new("WS-WAW-0002", "Warsaw", "Windows 10 22H2", 61, false, "ben.okafor"),
new("SRV-WAW-0003", "Warsaw", "Windows Server 2025", 88, true, "service"),
new("WS-LON-0004", "London", "Windows 11 24H2", 100, true, "chen.wei"),
new("SRV-BER-0005", "Berlin", "Windows Server 2022", 58, true, "service")
}).View.SaveHtmlPage("inventory.html");
internal sealed record Device(
string Name, string Site, string Os, int Patch, bool Encrypted, string Owner);Run this snippet (data grid, C# fluent)Its output, generated with the site.
import { createDataGrid } from "@evotecit/canopyx/reporting";
import "@evotecit/canopyx/reporting.css";
const devices = [
["WS-WAW-0001", "Warsaw", "Windows 11 24H2", 96, true],
["WS-WAW-0002", "Warsaw", "Windows 10 22H2", 61, false],
["SRV-WAW-0003", "Warsaw", "Windows Server 2025", 88, true],
["WS-LON-0004", "London", "Windows 11 24H2", 100, true],
["WS-LON-0005", "London", "Windows 11 23H2", 74, true],
["SRV-BER-0006", "Berlin", "Windows Server 2022", 58, true]
];
createDataGrid(document.getElementById("inventory"), {
view: {
protocol: "canopyx/2", type: "grid", id: "devices", label: "Device inventory",
pageSize: 100,
selection: "multiple",
statusBar: true,
noun: "devices",
columns: [
{
id: "device", title: "Device", kind: "text", sortable: true,
presenter: { kind: "identifier", mono: true }
},
{ id: "site", title: "Site", kind: "text", sortable: true },
{ id: "os", title: "Operating system", kind: "text", sortable: true },
{
id: "patch", title: "Patch level", kind: "number", sortable: true,
presenter: { kind: "progress", maximum: 100 }
},
{
id: "encrypted", title: "Encrypted", kind: "text", sortable: true,
presenter: { kind: "boolean" }
}
],
source: { kind: "inline" },
reporting: {
grouping: { column: "site", direction: "asc" },
facetColumns: ["site", "os"],
aggregates: [{
id: "patch-average", column: "patch", operation: "average",
label: "Average patch level"
}]
},
rows: devices.map(([device, site, os, patch, encrypted]) => ({
id: device.toLowerCase(),
cells: {
device, site, os, encrypted,
patch: {
value: patch,
text: `${patch}%`,
tone: patch < 70 ? "danger" : undefined
}
}
}))
}
});Run this snippet (data grid, JavaScript)Its output, generated with the site.
Try this in the demo
- Grouping
Collapse the Berlin group heading.
The heading keeps its count and its patch level summary; collapsed rows still count in selections and exports.
- Facets
Open the filter in the Site or Operating system header and tick a few values.
The value counts come from all 1,200 devices that match, not from the rows on screen.
- Aggregates
Read the totals row under the grid, then filter.
Average and lowest patch level follow the filtered query.
- Selection
Tick a few rows, then use Select all matching.
The status bar counts the selection. Select all matching captures the query instead of every record ID.
- CSV export
Open Export and choose Download CSV.
Pick filtered, selected or all records. The file follows the accepted query and the column order, not the visible rows.
- Saved views
Hide a column, then open Views and save the layout.
Named views keep columns, filters and sorting, and Reset view returns to the definition.