EasyBI.Core
0.3.0-beta.1
See the version list below for details.
dotnet add package EasyBI.Core --version 0.3.0-beta.1
NuGet\Install-Package EasyBI.Core -Version 0.3.0-beta.1
<PackageReference Include="EasyBI.Core" Version="0.3.0-beta.1" />
<PackageVersion Include="EasyBI.Core" Version="0.3.0-beta.1" />
<PackageReference Include="EasyBI.Core" />
paket add EasyBI.Core --version 0.3.0-beta.1
#r "nuget: EasyBI.Core, 0.3.0-beta.1"
#:package EasyBI.Core@0.3.0-beta.1
#addin nuget:?package=EasyBI.Core&version=0.3.0-beta.1&prerelease
#tool nuget:?package=EasyBI.Core&version=0.3.0-beta.1&prerelease
EasyBI
A self-service BI library for WPF. The developer declares what can be analyzed, end users build their visuals by drag-and-drop, and rendering is delegated to interchangeable connectors (LiveCharts2, OxyPlot, ...).
Source (IQueryable) ─▶ Semantic model ─▶ AnalyticQuery ─▶ ResultSet ─▶ ChartData ─▶ IVisualRenderer
(developer) (end user) (engine) (pivot) (connector)
Projects
| Project | Role | Dependencies |
|---|---|---|
EasyBI.Abstractions |
The contract: model, query, ResultSet, VisualDefinition, ReportDefinition |
none |
EasyBI.Core |
Model builder, query engine over IQueryable (expression trees), pivot, filtering, JSON |
Abstractions |
EasyBI.Wpf |
IVisualRenderer, EasyBIContext, VisualView, ReportDesigner, FilterBar, dashboard (DashboardPanel, DashboardView, behaviors), built-in KPI, Table, Text and Image renderers |
Core + Microsoft.Xaml.Behaviors.Wpf |
EasyBI.Renderers.LiveCharts2 |
LiveCharts2 connector (bars, stacked bars, lines, areas, points, combined charts, pies) | Wpf + LiveCharts2 |
EasyBI.Renderers.OxyPlot |
OxyPlot connector (bars, stacked bars, lines, areas, points, combined charts, pies) | Wpf + OxyPlot |
EasyBI.Themes.MaterialDesign |
EasyBI follows a Material Design in XAML theme | Wpf + MaterialDesignThemes |
EasyBI.Themes.WpfUi |
EasyBI follows a WPF UI (lepoco) theme | Wpf + WPF-UI |
samples/EasyBI.Sample |
Demo: dashboard + designer + renderer switch + JSON save/load | |
tests/EasyBI.Core.Tests |
Engine, filtering and serialization tests | |
tests/EasyBI.Wpf.Tests |
Designer and dashboard tests |
Installation
dotnet add package EasyBI.Wpf
dotnet add package EasyBI.Renderers.LiveCharts2 # and/or EasyBI.Renderers.OxyPlot
dotnet add package EasyBI.Themes.WpfUi # optional: if the application uses WPF UI or Material Design
EasyBI.Wpf brings EasyBI.Core and EasyBI.Abstractions. Every public WPF type is available
under a single XAML namespace:
xmlns:bi="https://github.com/TheoCvg/EasyBI"
For the WPF developer
var model = SemanticModel.For(dbContext.Sales, "sales", "Sales")
.Dimension(s => s.Date, "Date")
.Dimension(s => s.Region, "Region")
.Dimension(s => s.Product.Category, "Category")
.Measure(s => s.Amount, "Revenue", format: "#,##0 €")
.Count("Number of sales");
Context = EasyBIContext.Create()
.AddModel(model)
.UseRenderer(new LiveCharts2Renderer())
.UseRenderer(new OxyPlotRenderer());
With EF Core, prefer a context factory: each query opens its own DbContext, and the visuals of a
dashboard run in parallel. On a shared IQueryable (other than LINQ to objects), queries run one
after another, because a DbContext does not support concurrent operations.
var model = SemanticModel.For(() => dbFactory.CreateDbContext(), db => db.Sales, "sales", "Sales")
.Dimension(s => s.Region, "Region") /* ... */;
Each visual becomes one database query (filters, grouping, aggregates, sort and top in SQL): only the aggregated rows reach the application, whatever the number of rows in the table.
Visuals cache their last result and query again only when their query changes. When the data behind a model changes (new rows, another source), tell them, from any thread:
Context.RefreshData("sales"); // the visuals of this model; RefreshData() for all of them
<bi:ReportDesigner Context="{Binding Context}" />
<bi:VisualView Context="{Binding Context}" Definition="{Binding Visual}" />
Calculated measures
A measure is a row value that the end user aggregates (sum, average...). Ratios need more: the margin rate of a region is its total margin divided by its total revenue, not the average of its sales' rates. Calculated measures are computed from the rows of each group, inside the same query (in SQL with EF Core), and can be sorted and limited like the others; the end user cannot change their aggregation.
model
.Ratio(s => s.Margin, s => s.Amount, "Margin rate", "MarginRate", format: "P1") // Σ margin / Σ revenue
.Ratio(s => s.Amount, s => 1, "Average basket", "AverageBasket", format: "#,##0 €") // Σ revenue / number of sales
.Calculated(g => g.Sum(s => s.Amount) - g.Sum(s => s.Cost), "Gross profit", "GrossProfit");
Ratio is empty when the denominator sums to zero. With Calculated, guard divisions yourself: some databases fail
on a division by zero.
End users create their own from the field list (+ Calculated measure, then ✎ and ✕ on hover): a name, a format and a formula over the model's measures, typed with their captions and checked as it is typed, with its total as a preview:
[Margin] / [Revenue] ([Revenue] - [Margin]) / [Number of sales] [Margin] * 100 / [Revenue]
Measures in brackets are aggregated as in the field list; the operators are + - * / with parentheses and numbers
with a decimal point; a division by zero gives an empty value. The formula runs in the same query as the other
measures. These measures live in EasyBIContext.Measures (one context per open report): every field list, designer
and visual of the context sees them, and a DashboardView loads and saves them with its document (undo does not
remove them). An application that saves reports without a dashboard puts Context.Measures.Measures in
ReportDefinition.Measures and loads them back with Context.Measures.Load(...).
Comparisons over time
Each measure of a visual can be seen through time (MeasureRef.Time, the "Calculation" list of a measure chip in the
designer): its value, its value the previous year, the change since (in value or in %), or its total since the start of
the year along the date axis.
Values = [new("Amount"), new("Amount") { Time = TimeCalculation.PercentChangeFromPreviousYear }],
Filters = [PeriodFilters.Relative("Date", FilterOperator.Current, DateGrain.Year)], // this year vs last year
The previous year is the same query with its dates shifted back one year (period filters, clicked periods), run once
more and matched on the groups (each month with the same month a year earlier, per series). It needs a date on the axis
or a date filter, and is empty otherwise. The year-to-date total adds the values up along the date axis and starts
again each year. A KPI shows a change with ▲ / ▼ in the measures' or the error color. This runs in
VisualPipeline.PrepareAsync (ISemanticModel.ExecuteWithTimeAsync) on top of any model.
Combined charts
The cartesian chart types (Bar, StackedBar, Line, Area, Scatter) can be mixed in one chart, one per
measure: MeasureRef.Mark overrides the visual's type for that measure's series (with a legend, every series of
the measure keeps its type), and MeasureRef.SecondaryAxis draws it against a second value axis on the right. In
the designer, each measure of a chart has a type selector and a "Y2" toggle. These settings only change the drawing:
the query is the same, so changing them does not query the source again.
new VisualDefinition
{
Model = "sales",
Mark = MarkType.Bar, // default type of the measures
Axis = [new("Date", DateGrain.Quarter)],
Values =
[
new("Amount"), // bars
new("Margin") { Mark = MarkType.Line }, // line
new("Count") { Mark = MarkType.Scatter, SecondaryAxis = true }, // points, right axis
],
};
Besides charts, KPIs and tables, a visual can be a text (MarkType.Text, content in
VisualDefinition.Text) or an image (MarkType.Image, embedded as base64 in VisualDefinition.Image).
They query no model and are drawn by the built-in TextRenderer and ImageRenderer.
Dashboard
Arranging visuals is a WPF concern: the report (easybi/v1) never contains a layout. The dashboard is
built in three layers, each usable on its own.
The layout (DashboardLayoutDefinition) is a stack of bands, each with a mode. Its items are greedy:
without a constraint, they share the room their neighbours leave, so a dashboard never has a gap.
- Fit bands are a grid of 12 columns that fills the width. A band is a stack of lines; a line is an item, or a
container (
LayoutNode.Row: side by side,LayoutNode.Column: stacked) of items and containers. Along a container, constrained children keep their size (ColumnSpanin columns,RowSpanin rows) and the others share the rest; across it, every child takes the whole room. An item with aRowSpansets the height of its line and the items next to it stretch, so items stacked beside a tall item share its height. - Wrap bands start their items at
DashboardPanel.MinItemWidth(or their fixedWidthin pixels), break lines when the next one does not fit, then share what is left on each line. Scroll bands do the same on one line, and scroll horizontally when the items do not fit. - A container whose children are all constrained leaves a free space, which the dashboard offers to fill.
new DashboardLayoutDefinition
{
Bands =
[
new BandLayout
{
Items =
[
new LayoutNode { VisualId = title.Id, RowSpan = 1 }, // a whole line
LayoutNode.Row(
new LayoutNode { VisualId = map.Id, ColumnSpan = 6, RowSpan = 6 }, // left half, 2 lines high
LayoutNode.Column(LayoutNode.Item(sales.Id), LayoutNode.Item(margin.Id))), // stacked beside it
],
},
new BandLayout { Mode = BandMode.Scroll, Items = [LayoutNode.Item(a.Id), new LayoutNode { VisualId = b.Id, Width = 360 }] },
],
};
DashboardPanel shows a layout (Layout property); children are matched to its items by the attached
DashboardPanel.ItemId. The geometry itself is a pure function (BandArrangement.Arrange), and the panel exposes
hit tests for interactions (EdgeAt, FlowIndexAt, FreeSpaces, and CaptureGeometry to hit-test the layout from
before a drag).
DashboardLayout is the static class that changes a panel's layout while keeping it consistent: AddNextTo,
MoveNextTo (ItemSide.Right, Left, Below, Above), InsertAt / MoveTo (a new line), Resize,
ResizeWidth, SetAuto, Remove, InsertBand, RemoveBand, SetBandMode, plus Update for custom
operations, Capture (snapshot to restore) and GetLayout / SetLayout (to save and restore). Next to an item, a
side along its container joins it (the line's columns are shared when it has no room left), a side across it
wraps the item and the newcomer in a new container. The rules are pure functions on records in
DashboardLayoutEngine, usable without any UI.
DashboardView is the ready-made UserControl: DashboardItems showing visuals in a
DashboardPanel, with every interaction provided by a behavior (Microsoft.Xaml.Behaviors):
| Behavior | Feature |
|---|---|
AddButtonsBehavior |
"+" on the right and bottom edges of an item, "+" between lines and between bands |
FreeSpaceBehavior |
fills free spaces with a placeholder that adds an item there |
FieldDropBehavior |
makes visuals from fields dragged from a FieldList (see below) |
MoveBehavior |
grip to drag an item: a picture follows the pointer and a placeholder shows where it lands, the others making room live; near an item's edge it goes next to it (like "+"), between two lines it becomes a line, over a free space it fills it; across bands, auto-scroll near the edges; Escape cancels |
ResizeBehavior |
corner handle: constrains the item (columns and rows in a grid band, pixels and rows elsewhere) |
ItemToolbarBehavior |
automatic size (for a resized item), edit, remove; text size, bold, alignment; image fit |
BandMenuBehavior |
⋮ menu of a band: Fit / Scroll / Wrap, insert, remove (MenuOpening to customize it) |
BandScrollBehavior |
scroll bar and Shift+wheel for scrolling bands |
TextEditBehavior |
double-click a text to edit it in place |
UndoRedoBehavior |
Ctrl+Z / Ctrl+Y (ApplicationCommands.Undo / Redo) |
<bi:DashboardView x:Name="Dashboard" Context="{Binding Context}" Document="{Binding Dashboard}">
<i:Interaction.Behaviors>
<bi:UndoRedoBehavior MaxHistory="100" />
</i:Interaction.Behaviors>
</bi:DashboardView>
Hooks for applications:
- events
EditVisualRequested(replace the built-in designer dialog),ImageRequested(replace the file dialog),TextEditRequested,ItemRemoving(veto),Changed(every change, with the documents before and after),DocumentLoaded; DashboardCommands(AddVisual,AddText,AddImage,EditItem,RemoveItem,ResetSize,AddRight,AddBelow,InsertBand,RemoveBand...) for custom templates, and public methods (AddNextTo,AddIna free space,InsertLine,SetAutoSize,SetVisual,InsertBand,BeginEditto group changes into one undo step...);- an implicit style for
DashboardItem(lookless control) and theDashboardOverlaystyle keys (PlusButtonStyleKey,IconButtonStyleKey,GripStyleKey,ResizeGripStyleKey,FreeSpaceStyleKey,DropPlaceholderStyleKey,DragGhostStyleKey) to restyle; DashboardBehavioras a base class for new interactions: it gives access to the view, the panel and the overlay where behaviors draw their controls;DragGesturehandles press-drag-release gestures (with auto-scroll:AutoScroll,ScrollsBands).
IsReadOnly="True" hides the editing chrome; clicks keep cross-filtering in every mode.
Building visuals by dragging fields. FieldList shows the fields of the context's models (model picker,
dimensions, measures) and drags them as ModelFields. Placed next to a dashboard, it lets users build it directly:
<DockPanel>
<bi:FieldList DockPanel.Dock="Left" Width="210" Context="{Binding Context}" />
<bi:DashboardView Context="{Binding Context}" />
</DockPanel>
- dropped where an item can go (an edge, between two lines, a free space, a scrolling or wrapping band), a field makes a new visual: a KPI for a measure, the first measure by that dimension for a dimension (a line by month for a date, bars otherwise); the other items make room live around a placeholder;
- dropped on an empty item, it fills it; dropped on a visual, it is added to it (a measure to its values, a dimension to its axis, then its legend; a dimension breaks a KPI down into bars);
- the rules are pure functions in
FieldVisuals(Create,AddField);FieldDropBehavior.CreateVisualandAddFieldreplace them. Each drop is one undo step.
The designer's field panel is the same FieldList (its shelves accept the ModelFields of the selected model).
DashboardDropTarget.Find, the place an item dropped at a point goes, is shared by moves and field drops.
Drag and drop
The designer's drag and drop is built on reusable attached properties (namespace EasyBI.Wpf.DragAndDrop,
XAML prefix bi:), usable on any ItemsControl of an application. Being attached properties, they can be set
from a style:
<ListBox ItemsSource="{Binding Fields}" bi:DragSource.IsEnabled="True" bi:DragSource.AllowedEffects="Copy" />
<ItemsControl ItemsSource="{Binding Columns}" bi:DropTarget.Handler="{Binding}" bi:DropTarget.Key="Columns" />
public sealed class MyViewModel : IDropHandler
{
public DragDropEffects DragOver(DropInfo drop) => drop.Item is Field ? DragDropEffects.Copy : DragDropEffects.None;
public void Drop(DropInfo drop) => Columns.Insert(drop.Index, ((Field)drop.Item!).ToColumn());
}
DropInfogives the draggedItem, the target'sKey(one handler for several targets), the insertionIndexamong the target's items and the rawDatafor drags coming from elsewhere.DropTarget.IsDragOveris true while an accepted drag is over the target (style triggers);DropTarget.Orientationswitches the insertion rules to a vertical stack.- A scrollable target, or its scroll viewer, scrolls when the pointer nears its edges (
AutoScroller, also usable on its own);InsertionIndexholds the pure insertion rules. - Style keys
DragSource.PreviewStyleKey(aContentControl;DragSource.PreviewTemplatesets the content) andDropTarget.InsertionLineStyleKey(aBorder).
In the designer, fields are dropped between chips, chips are reordered within a shelf, and dimensions move between Axis and Legend keeping their granularity.
Filtering: global filters and cross-filtering
A FilterContext holds the filtering state of a dashboard. Set once on the container (it is an
inherited property), it is used by the FilterBar and by every VisualView:
<DockPanel bi:VisualView.FilterContext="{Binding Filters}">
<bi:FilterBar DockPanel.Dock="Top" Context="{Binding Context}" />
</DockPanel>
- Global filters (slicers): added from the
FilterBar, they apply to every visual of the same model and are saved with the report (ReportDefinition.Filters). - Cross-filtering: clicking a bar, a point, a slice or a table row filters the other visuals of
the same model on the element's dimensions (axis, plus legend for a bar). Clicking the same element
again clears the selection. The source visual is not filtered and shows the current selection.
Periods (quarter, month...) filter through
FilterSpec.Grain. - Period filters: a date dimension, in the
FilterBaror on the designer's Filters shelf, is filtered on a period rather than on a list of values: between two dates, the current day / month / quarter / year, the last n (current one included: "Last 12 months") or the n previous complete ones ("Last month"). Relative periods are resolved when the query runs, so a saved report keeps showing the last 12 months; the date column is compared to the period's bounds (date >= start AND date < end), so a database can use an index on it.
PeriodFilters.Relative("Date", FilterOperator.Last, DateGrain.Month, 12); // last 12 months
PeriodFilters.Between("Date", new DateOnly(2024, 1, 1), null); // since January 1st, 2024
PeriodFilters.Describe(filter); // "Last 12 months", localized
A connector only has to call request.Select?.Invoke(new CategorySelection(category, series)) on
click (or RowSelection for a table): Select is null when interactivity is disabled, for example
in the designer preview.
Theming
Every EasyBI control (designer, dashboard, filter bar, field list, KPI and table renderers, chart connectors) draws with resources looked up dynamically: an application restyles EasyBI by defining them, and switches theme at runtime by replacing the dictionary that defines them. Charts follow too, without querying their data again.
Light and dark
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<bi:EasyBIThemeDictionary Mode="Dark" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Without it, the light theme applies. To switch at runtime, change Mode (or swap the dictionary):
foreach (var theme in Application.Current.Resources.MergedDictionaries.OfType<EasyBIThemeDictionary>())
theme.Mode = EasyBIThemeMode.Light;
Overriding a few colors
Define the keys you want in your resources, after the theme dictionary; the others keep the theme's values:
<SolidColorBrush x:Key="{x:Static bi:EasyBITheme.AccentBrushKey}" Color="#7C3AED" />
<bi:ChartPalette x:Key="{x:Static bi:EasyBITheme.ChartPaletteKey}">
<Color>#7C3AED</Color>
<Color>#F59E0B</Color>
<Color>#10B981</Color>
</bi:ChartPalette>
<CornerRadius x:Key="{x:Static bi:EasyBITheme.CornerRadiusKey}">4</CornerRadius>
The complete list, with their light and dark values, is in
Themes/Light.xaml and Themes/Dark.xaml:
Key (EasyBITheme.…) |
Used for |
|---|---|
BackgroundBrushKey |
behind the dashboard, the designer and the visual editor |
SurfaceBrushKey |
cards (dashboard items, designer preview), popups, toolbars, chart tooltips |
SurfaceAltBrushKey |
field lists, designer shelves, empty items, alternate table rows |
BorderBrushKey, BorderStrongBrushKey |
borders and separators; empty items and free spaces |
ForegroundBrushKey, MutedForegroundBrushKey, DisabledForegroundBrushKey |
text, secondary text (headers, hints, icons, axis labels), unavailable options |
AccentBrushKey, AccentHoverBrushKey, AccentForegroundBrushKey |
selection, "+" buttons, drop indicators, and the text on them |
AccentSoftBrushKey, AccentSoftBorderBrushKey |
field chips, selection chip, drop placeholders |
SecondarySoftBrushKey, SecondarySoftBorderBrushKey |
filter chips |
HoverBrushKey |
small buttons under the pointer |
ErrorBrushKey |
error messages |
MeasureBrushKey |
the Σ of measures in field lists |
GridLineBrushKey |
grid lines of charts and tables |
ChartPaletteKey (ChartPalette) |
colors of chart series, in order |
CornerRadiusKey (CornerRadius) |
rounding of cards |
Control-specific parts have their own style keys (DashboardOverlay.PlusButtonStyleKey, FreeSpaceStyleKey,
DropPlaceholderStyleKey, DragSource.PreviewStyleKey... see Dashboard and
Drag and drop), and DashboardItem has an implicit style.
Standard controls
Buttons, combo boxes, text boxes and check boxes inside EasyBI have no explicit style: they take your application's implicit styles, so a control library (WPF UI, Material Design...) styles them too. EasyBI's own light and dark themes color EasyBI's surfaces, text and charts, not the standard controls: with the dark theme and no control library, they keep the system's light look.
Following a control library
With WPF UI or Material Design in XAML, install the matching package and merge its dictionary after the library's:
EasyBI's surfaces, text, accent and charts then take the library's colors, light or dark, and follow its theme switches
(ApplicationThemeManager.Apply, PaletteHelper.SetTheme). Its classes are in the bi: XAML namespace.
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
<bi:WpfUiTheme />
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.MergedDictionaries>
<md:BundledTheme BaseTheme="Dark" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
<bi:MaterialDesignTheme />
</ResourceDictionary.MergedDictionaries>
WpfUiTheme.Map and MaterialDesignTheme.Map list which library resource each EasyBI key takes. For another library
or an application's own design system, derive from EasyBIThemeMapping and map the keys you need; unmapped keys keep
EasyBI's light values, and the chart palette follows the mapped background (light or dark palette). If the library
switches theme by swapping dictionaries without notifying, call Refresh() on the mapping afterwards.
ApplicationThemeManager.Apply sets the main window's Background itself: put an EasyBI background on the window's root
panel rather than on the window (as the sample does).
public sealed class MyTheme() : EasyBIThemeMapping(new Dictionary<ComponentResourceKey, object>
{
[EasyBITheme.BackgroundBrushKey] = "MyWindowBrush",
[EasyBITheme.AccentBrushKey] = "MyPrimaryBrush",
});
Charts
VisualView passes the theme to renderers in RenderRequest.Theme (BiTheme): the palette, and the Foreground,
MutedForeground, GridLine and Background colors (hexadecimal, null when unknown). It is built by
EasyBITheme.ChartTheme(element) from the resources above. A palette or colors set on EasyBIContext.Theme take
precedence over the resources.
Localization
EasyBI's own texts (designer, filter bar, messages, granularity and aggregation names, blank and boolean values) ship in English and French. The language can change at runtime: designer, filter bar and visuals update immediately.
EasyBILocalization.Culture = CultureInfo.GetCultureInfo("fr-FR"); // null = follow CurrentUICulture
The same culture formats numbers and dates. Your semantic model captions can follow the language too:
call Localize with your own resources, and every caption becomes a resource key (a missing key falls
back to the caption itself).
var model = SemanticModel.For(db.Sales, "sales", "Sales")
.Dimension(s => s.Region, "Region")
.Measure(s => s.Amount, "Revenue")
.Localize(MyStrings.ResourceManager);
Your own XAML texts can follow it as well with LocalizedStrings and the Localize markup extension:
public static LocalizedStrings Texts { get; } = new(MyStrings.ResourceManager);
<Button Content="{bi:Localize Save, Source={x:Static local:App.Texts}}" />
Visual and report titles are user content: they are stored as typed and not translated.
Writing a connector
A connector implements a single interface. It receives already queried and pivoted data
(request.Chart: categories + series) and the raw ResultSet if needed:
public sealed class MyRenderer : IVisualRenderer
{
public string Name => "MyLib";
public RendererCapabilities Capabilities { get; } = new(new HashSet<MarkType> { MarkType.Bar });
public FrameworkElement Render(RenderRequest request) { /* request.Chart.Categories, request.Chart.Series */ }
}
Each ChartSeries comes with its resolved Mark and SecondaryAxis: a connector draws every series with its own
type, so one chart can mix bars, lines, areas and points.
Colors come from request.Theme: series take ColorAt(index), text Foreground, axis labels MutedForeground, grid
lines GridLine, tooltips Background (each one null when the theme does not set it: keep the library's default).
Draw on a transparent background, so that the chart sits on the theme's surface.
EasyBIContext.ResolveRenderer negotiates capabilities: the active renderer if it supports every type the
visual uses (the visual's and its measures'), otherwise another plugged-in renderer, otherwise a built-in one. The designer disables
the types that no renderer can draw.
Report format (easybi/v1)
{
"schema": "easybi/v1",
"title": "Dashboard",
"visuals": [
{
"title": "Revenue by region and channel",
"model": "sales",
"mark": "Bar",
"axis": [ { "field": "Region", "grain": "None" } ],
"legend": { "field": "Channel", "grain": "None" },
"values": [ { "measure": "Amount" }, { "measure": "Count", "mark": "Line", "secondaryAxis": true } ],
"filters": [
{ "field": "Product.Category", "operator": "In", "values": [ "Services" ] },
{ "field": "Date", "operator": "Last", "values": [ "12" ], "grain": "Month" }
]
}
],
"filters": [
{ "model": "sales", "filter": { "field": "Region", "operator": "In", "values": [ "North", null ] } }
]
}
null in values means a missing value. Besides In and NotIn, a date filter's operator can be Between
(values: from and to as yyyy-MM-dd, inclusive, null for an open end), Current, Last or Previous (value: the
number of periods; grain is their unit). EasyBIContext.Validate(report) reports the unknown
models and fields of a loaded report, and the formulas that no longer match their model.
Calculated measures created by the end user are saved with the report (the property is omitted when there are none), and visuals reference them by name like a field:
"measures": [
{ "name": "calc_3f9a1c0d2b7e", "caption": "Margin rate", "model": "sales", "formula": "[Margin] / [Amount]", "format": "P1" }
]
Text and image visuals carry their content and no model:
{ "mark": "Text", "text": { "text": "Sales dashboard", "fontSize": 24, "isBold": true, "alignment": "Left" } }
{ "mark": "Image", "image": { "data": "iVBORw0KGgo...", "mediaType": "image/png", "fit": "Contain" } }
Dashboard file (easybi-dashboard/v1)
DashboardSerializer saves what a DashboardView shows: the unchanged report next to the WPF layout,
which references visuals by id. Sizes that are not saved are greedy. It also opens a plain easybi/v1 report
(visuals then get a default layout).
{
"schema": "easybi-dashboard/v1",
"report": { "schema": "easybi/v1", "visuals": [ ... ], "filters": [ ... ] },
"layout": {
"columns": 12,
"rowHeight": 110,
"bands": [
{
"mode": "Fit",
"items": [
{ "visualId": "...", "rowSpan": 1 },
{ "split": "Row", "children": [ { "visualId": "...", "columnSpan": 6 }, { "visualId": "..." } ] }
]
},
{ "mode": "Scroll", "items": [ { "visualId": "...", "width": 360 }, { "visualId": "..." } ] }
]
}
}
The same file renders identically with any connector.
Run
dotnet test
dotnet run --project samples/EasyBI.Sample
The demo starts in your system language (English or French) and has a language picker in its toolbar. Its data
picker switches the same sales between memory (20,000 rows) and a SQLite database of 1 or 5 million rows, generated
once in %LOCALAPPDATA%\EasyBI.Sample; the toolbar shows how long the last refresh took.
Contributing
See AGENTS.md for the code style, language rules and Git workflow.
Known limitations / roadmap
- Cross-filtering: one selection at a time; the selected element is not yet highlighted in the source chart (a "Sélection : ..." banner shows it).
- Designer and
FilterBarfilters: "one of" a list of values, or a period for a date dimension; no ranges on numbers. - Relative periods ("last 12 months") are resolved in the local time zone when a visual queries; an open dashboard does not query again at midnight.
Topapplies to aggregated rows, not to categories when a legend is present.- Grouping, sorting and
Topare pushed down to the provider, and execution is asynchronous when the source implementsIAsyncEnumerable(EF Core). The SQL translation was checked manually with EF Core 8 on SQLite (every query shape, including date granularities, navigation properties and sort on an aggregate); there is no automated test against a database. SQLite cannot sumdecimalcolumns (EF Core refuses the query). - At most 4 grouping dimensions (axis + legend).
- The built-in light and dark themes do not restyle standard controls (buttons, combo boxes...): use a control library for those (see Following a control library).
EasyBI.Themes.WpfUiis checked against WPF UI's light and dark themes; its high contrast themes are not verified.- Dashboard images are embedded in the file (2 MB by default,
DashboardView.MaxImageBytes).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net8.0
- EasyBI.Abstractions (>= 0.3.0-beta.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on EasyBI.Core:
| Package | Downloads |
|---|---|
|
EasyBI.Wpf
Self-service BI for WPF: drag-and-drop report designer, visual view, filter bar with cross-filtering, built-in KPI and table renderers, pluggable chart connectors. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0-beta.1 | 96 | 10/28/2026 |
| 0.5.0-beta.1 | 90 | 10/26/2026 |
| 0.4.0-beta.1 | 89 | 10/21/2026 |
| 0.3.0-beta.1 | 92 | 10/19/2026 |
| 0.2.0-beta.1 | 84 | 10/19/2026 |
| 0.1.0-beta.1 | 81 | 10/16/2026 |